Client Preferences & Account Data
0. 规范语言
本文中的规范关键字(MUST / SHOULD / MAY 等)按 conformance/normative-language.md 解释;仅大写形式具规范约束力。
1. 目标
在 Arkret 网络中,绝大部分数据是跨节点共享的协作对象(Realm、Space、Strand、Message、Morph、Relation、View)。但每个用户(Actor)也有大量的私有状态需要在其各个设备之间同步,但不应该对网络中的其他人可见。
本规范定义了这些客户端偏好与账户数据 (Account Data) 的存储、同步与标准 Schema。
2. 存储模型
Account Data 的存储、namespace key、derive_account_data_key、value encryption、HKDF/AAD transcript 与轮换规则的单一真相源是 ../models/account-data.md。本文只定义客户端偏好 data type 与服务端 policy projection 协商,不重复基础原语。
{ "kind": "ak.account_data.set", "key": "ak.client.ui_state", "body": { "mode": "dark", "accent_color": "#FF5733" }}2.1 服务端 policy projection 能力协商(normative)
account data 默认是 holder-private 加密数据,Station sync surface 只存不透明密文(../models/account-data.md §1)。presence / typing 的精确 kind、target 与 visibility policy 不交给服务端读取;发送端按 profiles-presence.md §3.4 选择可安全加密的 scope。服务端只能执行已有正式载体明确披露的 policy projection;个人 blocklist 在 v1 没有这种载体:
- 服务端 MUST 在
ak.server.read.describe.v1(ServiceDescribe)中声明它能否读取每个最小 policy projection(例如通过plaintext_visible_services.data_classes或等价policy_projection_readable[]声明)。presence_visibility不得声明为服务端可读;未声明的其它 data class 视为不能读取。 - Presence / typing 的 policy gate 固定在发送客户端:客户端只向符合本端 membership、contact 与 visibility 判断的整个加密 scope 发送;无法安全选择 scope 时 MUST 抑制发送。Station sync surface 只按外层已签名
scope_ref做成员级 fanout,不读取或推断ak.presence.visibility,也不得因无法读取该 key 而把整个 opaque Signal rail 判为不可转发。 - 个人 blocklist 只在持钥 holder 设备上解密和过滤;服务端不得从其它 plaintext data class、Realm 治理声明或服务自述推断 blocklist 明文读取权(§3.5)。
3. 标准账户数据类型
为了保证不同客户端间的互操作性,本规范定义了以下标准 Key 命名空间:
这些 key/pattern 的机器索引位于 account-data-key-registry.json;新增标准 Account Data key 时 MUST 同步更新该 registry,并通过 tools/artifact_pipeline.py check 校验 source refs 与写入 Event.kind。
3.1 空间标签与分类 (Realm Tags)
用户可以给加入的 Realm 打上私有标签(例如“收藏”、“低优先级”、“公司项目”)。
Key: ak.tags.realm.<realm_id>
{ "tags": { "ak.favorite": { "order": "m" }, "ak.low_priority": {}, "org.example.work": {} }}客户端 SHOULD 根据这些标签将 Realm 在 UI 上分组或排序。order 是用于自定义排序的稳定 rank string(跨端确定性见 §6;客户端 MAY 在 UI 内用 float 计算临时位置,但写回 account data 时 MUST 归一为规范 rank string)。
Tag 命名保留规则(normative):ak.* tag 命名空间保留给本规范;客户端扩展 tag MUST 使用 <vendor>.* 反向域名风格前缀(例如 org.example.work)。§3.6 / §3.7 的私有 tags 字段沿用同一命名规则。
标准 tag 词表(normative):
| tag | 语义 | 客户端行为 |
|---|---|---|
ak.favorite | 收藏 | 客户端 SHOULD 在分组 / 排序中置顶展示。 |
ak.low_priority | 低优先级 | 客户端 SHOULD 降权展示(折叠、置底或降低通知突出度)。 |
未识别的 ak.* tag MUST 原样保留(存储与回写),客户端 MAY 不渲染;新增标准 tag MUST 登记到本词表。
3.2 勿扰与通知设置 (Notification Settings)
控制各个 Realm 或全局的通知覆盖行为(详见 push-notifications.md)。
Key: ak.push_rules 和 ak.dnd_schedule
ak.dnd_schedule 解密后的 plaintext 使用闭合 schema ak.schema.dnd_schedule.v1;时间段边界、
canonical IANA timezone、固定 tzdb 及异常值恢复规则见
push-notifications.md §7。
Notification projection 的 dismissed / archived 跨设备状态使用
ak.notifications.inbox.<notification_id>。加密 value MUST 绑定同一 notification_id、
state ∈ {dismissed, archived}、HLC 与 device tie-break 材料,并按 account-data CAS
重试循环合并;read / unread 仍由 read cursor 派生,不得写入该 key。
<notification_id> MUST 与 account subscribe notifications.items[].id 逐字节相等:普通源 Event 通知用
ak:notification_projection:<44 字符 token>。该 key 是 inbox 处置的唯一真源,账号同步通道不携带 state,
服务端也不得代写或代删该 key;同步通道的 remove 只删除本地当前行,是否清理对应 key 由持有者客户端自行决定
(见 ../sync/client-sync.md §3.1.2)。Agent runtime approval 分支只做 local dismiss,
不写该 durable key。
Actor-private View 使用 ak.views.private.<view_id>;加密 value MUST validate 为
ak.schema.view.v1 且 visibility="private"。共享 View 仍只能使用 ak.view.* Event。
以上两个 key 的可执行覆盖见
../conformance/conformance-vectors.md §5.11 的
ak.vector.account_data.private_view_inbox_binding.v1。
3.3 自定义 Emoji 与 Sticker (Custom Emojis)
用户个人收藏的表情包或贴纸集。
Key: ak.collections.stickers
{ "images": { "party_parrot": { "blob_ref": "ak:blob:sha256:abcd...", "mime_type": "image/gif" } }}3.4 客户端 UI 偏好 (UI State)
用于保存用户的视图偏好,以便在新设备登录时恢复熟悉的界面。
Key: ak.client.ui_state
{ "sidebar_collapsed": false, "recent_realms": [ "ak:realm:AdHing2meouJofkXXyApJTyFCo7SNoZHrSjQHaitT3D8", "ak:realm:AbFCxTyW_gTLgSNJVZge-vUGLGIA_xSq6UixRuoPDk-W" ], "language": "zh-CN"}字段命名(normative):该数组承载的是
ak:realm:ID,canonical 字段名为recent_realms。客户端 MUST 写入并读取recent_realms;recent_spaces不是 v1 字段名。
3.5 个人屏蔽与过滤 (Personal Blocklist)
用户可以在私有 account data 中保存个人 blocklist。该数据只影响用户自己的客户端、本地搜索/投影、通知规则和联系请求处理,不改变 Realm 的共享事实。
blocklist 没有独立 Event kind:它是 account-data key ak.account.blocklist 的加密值,只经 ak.account_data.set
(ak.self.account_data.resource.replace.v1/.delete.v1)以 server-revision CAS 整值写入,owner、唯一键、exact retry
与零副作用拒绝见 ../models/actor-private-effects.md §3.1。
下列明文是 holder 设备解密后的值。
Key: ak.account.blocklist
{ "entries": [ { "entry_id": "ak:block:019640b3-cc00-7000-8000-000000000000", "target": { "kind": "actor", "actor_id": { "kind": "account", "account_id": { "principal_id": "ak:did_core:webvh:zGMfBAbnRTYqW4943CVr9Dcii", "station_id": "ak:did_core:webvh:z6mkfixturestationexample" } } }, "mode": "block", "applies_to": [ "messages", "mentions", "dm", "calls", "contacts", "applets", "presence", "notifications", "directory" ], "reason_code": "harassment", "created_at": "2026-04-26T10:00:00.000Z", "expires_at": null } ]}target.kind 取值:
actordevicehandledomainappletkeyword
规则:
- blocklist holder 只取自承载该值的已验
ak.account_data.setEvent envelope 的完整 accountactor_id,值不复制 holder 坐标。唯一 revision 是该 key 的 account-dataexpected_server_revisionCAS:写入必须携带当前 revision(首次为0),旧 revision 或并发写入均cas_conflict,明文不另带版本号;同 principal core 异 Station 的账号绝不共享 counter 或规则。 - 每次写入是全量替换,不是 entry patch:加入屏蔽对象是在下一 revision 中加入新
entry_id;修改同一规则时保留entry_id;移除屏蔽对象是在下一 revision 中省略对应 entry;entries=[]清空全部规则。服务端或客户端不得把“移除”解释为删除共享消息、撤销 capability 或通知被屏蔽方。expires_at到期只令该 entry 在 holder projection 中失效;同步写者 SHOULD 在下一 revision 中清除它,receiver 不得用本地计时器改写 durable payload。 target是闭合 discriminated union:actor必须且只能携带完整actor_id;applet必须且只能携带 canonicalak:applet:object_ref;handle | domain | keyword必须且只能携带value;device携带 canonicalak:device:object_ref,或在无法取得 device id 时携带 verification-method DID URLvalue。仅有裸 display name、W3C DID 或did_core_id不得成为 actor target;device 与 applet 的 typed-id 前缀必须由 schema 校验,不能把其它object_ref塞入对应分支。actortarget 只按被过滤 Event / request 已验证的完整发送者actor_id逐字匹配:Station 承载的 user、organization、team、Agent、Bot 或 integration 账号均使用ActorId.account{account_id:{principal_id,station_id}},service 以自身身份实际作为发送者时使用ActorId.service{service_id}。不得按actor_kind、Organization DID / claim、ak.realm.organization、托管 Station、转发 service 或其它 affiliation 把一条 actor rule 扩张到关联主体;service 仅作为 transport / relay 时也不得命中。- v1 不定义
organization或realmblocklist target。Organization 官方账号发出的内容按其 exact account ActorId 过滤;service 自身发出的内容按其 service ActorId 过滤。整个 Realm 的通知或默认视图偏好属于独立 per-Realm 客户端偏好;退出、拒绝新写入或撤销共享权限必须使用 membership / Contact / capability 的既有协议路径,不得由个人 blocklist 合成。 - 同一 revision 内最多 4096 个 entry;规范化后的
(target, applies_to)不得被多个 entry 重复覆盖;需要不同 mode 时必须使用互不重叠的applies_to。applies_to至少一个值并决定规则作用面,其中contacts覆盖 contact request/relationship surface,applets覆盖 applet-mediated request;不得用dm或notifications猜测替代这两个独立 surface。 mode="block":在所选 holder-facing surface 上拒绝新的 contact / DM / call / applet request 或隐藏来自 target 的内容;但共享 Realm Event 仍按下文“收取与过滤边界”处理。mode="mute":内容仍可见、可搜索和正常同步,只抑制铃声、push、mention badge 等 attention surface。mode="hide":内容仍同步、验证和保留,但从默认 holder view / search 中排除;它不拒绝新的协议请求。- 通过非可信服务同步时,account blocklist MUST 仅为 holder 自己的设备加密。
- 客户端 SHOULD 抑制来自被屏蔽对象的通知、联系人请求、通话邀请与 DM 请求。
- 客户端 MAY 在共享 Realm 视图中隐藏或折叠被屏蔽内容。
- 客户端 MUST NOT 把 blocklist 发布到公共 Realm 状态或目录服务。
- 客户端和受托服务不得返回
blocked_by_user,服务端不得因为个人 blocklist 命中改变公开错误码、响应形状、排队、延迟模式或 directory 结果。持钥客户端过滤后可不发送本来会为可见消息发送的自动 read receipt,也可不产生 typing / presence;这些信号的有无可能使对方推测过滤状态。v1 不保证从客户端行为推断不出拉黑。需要本地诊断时只能在 holder 自己的加密 account data 或本地日志中记录。 ak.account.blocklist是 account-private durable account-data 值:它可以在 holder 的设备间同步,但不进入共享 Realm RealmCommit coverage、membership state、Directory ingest 或 federation payload。- v1 没有 holder-private blocklist 明文授权或密钥交付操作。Station、通知服务、Directory 与 federation peer MUST NOT 解密、接收明文或代 holder 根据个人 blocklist 作准入、投递或查询判定;
plaintext_visible_services是 Realm 治理对象,不能授予该 account-private key 的读取权。它们按其它已有权限正常转发;持钥 holder 客户端在本地过滤。服务端的提交结果不披露 block 命中;客户端后续信号不受此保证。 handletarget 只与发送者已验证 handle claim 中的 canonicalhandle逐字比较;domaintarget 只与该已验证 handle claim 的 domain 分量,或按../identity/did-usage-and-verification.md已验证的 DID 域名绑定比较。display name、未验证 handle / DID 字符串或裸字符串后缀均不得命中这两类 target;keyword才是纯内容字符串过滤。三者命中都只在 holder-private projection 生效,不证明也不得推断任何 actor、service、Organization 或 Realm 的控制关系。
3.5.1 收取与过滤边界(normative)
- 共享 Realm 消息:必须先按正常 federation / sync 路径收取、验签、准入、存储并推进 canonical Event / RealmCommit 状态,因为同一 Event 对其他成员、引用链和 state root 仍然有效;随后才在 holder-private projection 应用
block/hide。不得在网络层丢弃该 Operation,也不得从共享 history、RealmCommit coverage 或其它成员视图删除它。被过滤内容不生成 holder notification、mention attention 或自动 read receipt;可见内容仍按有效 read-receipt policy 产生自动回执。发送方可能观察到这一差异。 - 现有 Direct Conversation:个人 blocklist 自身只是私有过滤器,不撤销 membership、Contact authority 或 participant authority。若产品的“拉黑用户”承诺阻止后续 DM 写入,客户端 MUST 把 blocklist 更新与
ak.self.contact.command.tombstone.v1{block_peer=true}作为同一持久化 saga 执行并重试至闭合;Contact tombstone使稳定 conversation 投影为suspended并禁止新 application message,Consent revoke不得作为替代或附加门槛。只写 blocklist 时,对端仍可能成功提交 shared DM Event,本端必须同步后私下过滤。 - 新的 holder-private 请求:contact request、首次 DM invite、call invite 或 applet-mediated request 按各自既有权限正常转发给 holder,由持钥客户端在本地过滤;服务端不得因个人 blocklist 在 holder surface 前 drop。服务端不得向发送方返回
blocked_by_user或因 block 命中改变提交响应、投递结果;holder 的后续应答或信号可能不同,不作为服务端不可枚举的保证。 - 解除屏蔽:下一 revision 移除 entry 后,未来 projection 立即停止过滤。此前已经正常收取并按 retention 保留的共享 Realm / DM 历史会重新出现在 holder view;若产品希望解除后仍不显示旧内容,必须另存 holder-private hide/tombstone 或执行已有 erasure 流程,不能把 blocklist removal 偷换成历史删除。Block 期间被 Contact tombstone真正拒绝、从未 accepted 的新请求或消息不会因解除屏蔽而补写。
- 离线与多设备:设备只能依据其已同步到的最高 accepted blocklist revision 过滤。尚未取得新 revision 的设备必须把 blocklist freshness 视为 unknown,禁止发送 read receipt / presence 等可能泄漏差异的信号,待 actor-private account-data catch-up 后重算 holder projection。
上述 CAS、receive-before-filter、解除后 projection rebuild、DM Contact authority 与 Consent 完全隔离及服务端提交路径不披露 block 命中,由 conformance vector ak.vector.account.blocklist_projection.v1 闭合。
服务端提交时序的 v1 验收边界(normative):对照使用同一发送者、Realm、操作与授权, 只改变 holder 私有 blocklist 是否命中;比较从提交请求发出到完整响应收到的同一 transport 区间, 并分别记录真实自动 receipt 的出站行为。合格的必要结构证据是共享 Event 在服务端与 federation 的准入、 Commit、响应及投递路径均不读取/解密 blocklist,也不按命中与否分支、排队或人为延迟;过滤只在 holder 接收并验证后发生。发送者可见的提交 status、Problem type/reason、响应形状不得由 blocklist 命中改变;任何可复现的服务端命中依赖分支或不同提交结果即失败。自动 receipt 因本地过滤而有差异 是 v1 允许的观察,测试须记录这一限制,不得将其计为服务端失败或宣称端到端不可推断。 真实 transport 对照须记录环境、样本与原始耗时,作为发现回归的辅助证据;网络抖动下两次耗时不等 不单独判失败,有限样本也不能证明任意网络中的绝对等时。v1 不规定毫秒阈值、固定 timing bucket 或 统计显著性通关线;测试不得以常量伪造耗时或把单次观测当成无侧信道证明。用户主动阅读或手动发送 receipt 是独立行为,不能拿不同用户行为的结果归因于 blocklist。
3.6 联系人备注 (Contact Remarks)
用户可以为 ak.self.contact.read.list.v1 中 peer.kind="human" 且状态为 accepted 的联系人保存只对自己可见的全局备注名(wire 字段 petname)、笔记和私有标签。逻辑记录由 (holder principal_id, peer.principal_id) 唯一确定;同一 holder 对同一联系人至多存在一条 live value,并在所有 Realm 共用。用户不得设置 per-Realm 联系人备注名,也不得创建 ak.contacts.realm_actor.*、ak.contacts.actor.<principal_key>.<realm_id> 或等价分叉载体。
该数据是 principal-private 的渲染覆盖层,不修改对方公开 profile,不写入 Realm history、mention、sender attribution 或任何协议主体字段。peer.principal_id 是 human Contact 的用户 did_core_id;peer_id 只是托管对端的 Station service DID,不是联系人身份键。Realm actor_id、Realm-scoped pairwise DID、MemberIdentity row、realm_id、handle、display name 与 ak.profile.realm_override 均不得成为联系人备注的逻辑键。
ak.contacts.* account-data key 只表达 holder-private 备注、标签、置顶、别名和本地排序。它不通知对方,不证明对方接受,也不打开 direct_message / invite / call / presence gate。联系人关系状态与 Contact-based action gate MUST 只从 ../identity/contact-and-direct-conversation.md 定义的 ak.contact.* directional fact log 投影;Consent不得参与。客户端 MAY 把本地备注与 ak.self.contact.read.list.v1 结果合并展示,但不得把 account-data note 当作 accepted contact。
Key: ak.contacts.actor.<principal_key>,其中:
principal_key = derive_account_data_key( RFC8785_JCS(["ak.contacts.actor", peer.principal_id]))storage_key = "ak.contacts.actor." || principal_keyderive_account_data_key 与 holder 的 account_data_namespace_key 定义见 ../models/account-data.md §2。principal_key 必须是 32-byte HMAC 输出的无 padding base64url(43 个 ASCII 字符);服务端只能看到不透明尾段。
{ "version": 1, "subject": { "kind": "human", "principal_id": "ak:did_core:webvh:z5Z2tUHXdembzXVX7EE5SJp5g" }, "petname": "老王(前同事)", "note": "2024 年 ArkretCon 认识", "tags": ["org.example.work", "ak.favorite"], "pinned": true, "verified_handle_at_save": "wang:example.com", "confirmed_display_name": "Wang Wei", "saved_at": "2026-05-08T10:00:00.000Z", "updated_at": "2026-05-08T10:00:00.000Z"}字段(顺序与 contact-remark.schema.json 的属性顺序一致,
由正文字段表门禁机械比对):
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
version | int | yes | 值格式版本,v1 恒为 1。取别的值是被拒绝的值,不是可协商的变体。 |
subject | object | yes | 封闭对象,展开见下表。principal 级作用域是刻意的:备注是 holder 私有展示数据,不参与授权或主体相等,因此不带 Station / actor role。 |
petname | display_text_128 | no | holder 为该稳定联系人主体保存的全局备注名,最大 128 个 Unicode code point;UI 中文称“备注名”。跨所有 Realm 生效。缺省表示没有备注名;不得用空串清除(display profile 会拒绝空串)。 |
note | string | no | 自由文本笔记,最大 4096 个 Unicode code point。可以为空,也可以跨行,因此不复用单行 display profile。 |
tags | string[] | no | 私有分组标签,命名规则同 §3.1 Realm tags(ak.* 保留给本规范,<vendor>.* 用于客户端扩展)。schema 只校验 array-of-string;命名空间由客户端按 §3.1 作为领域规则校验,schema 通过不等于命名空间合法。 |
pinned | bool | no | 是否置顶。 |
verified_handle_at_save | canonical_handle | no | 保存或最近一次更新时该 DID 的 verified handle 快照,用于反冒充比对。存 wire canonical form(<localpart>:<domain>),不含输入用的前缀标记。 |
confirmed_display_name | display_text_128 | no | holder 最近一次显式确认联系人身份时所见的 verified PCR actor_profile.display_name;与 Profile 共用同一个 display_text_128(最大 128 个 Unicode code point),两侧不可能漂移到不同上限。不得写入 Realm override、MemberIdentity display、Directory 裸结果、OIDC name 或其它 fallback。 |
saved_at | timestamp | yes | 首次保存时间。 |
updated_at | timestamp | no | 最近修改时间。 |
subject 的两个成员(封闭对象,additionalProperties:false):
kind:const("human"),与 Contact human branch 对齐;Organization、设备、service 或 Realm actor 不得复用该记录。principal_id:did_core_id,取ak.self.contact.read.list.v1human row 的peer.principal_id; 解密后 MUST 用它与 holder namespace key 重算<principal_key>并精确匹配当前 storage key。
optional 表示成员缺失,不是 null。裸 {} 不能冒充删除(version / subject / saved_at 是必需的),
物理删除的 tombstone 也不按本 schema 校验。petname 与 confirmed_display_name 同时缺失是合法值:
没有 Profile evidence 不阻断 Contact accept。
跨实现向量是 ak.vector.contacts.remark_value.v1(fixture contact-remark-fixture.json),
结构半与领域半必须都过,见 ../conformance/conformance-vectors.md §5.13。
校验点(normative):producer 在加密与签名之前校验最终待写明文(含 CAS 重试时合并出的那一版), consumer 在 AEAD 解密之后、应用之前校验。承载该 key 的 Station 没有密钥, MUST NOT 因此要求明文、明文镜像或服务端 validator。结构校验只证明形状: storage key 绑定、显式确认证据与 tag 命名空间是同一客户端另行执行的领域规则。
规则:
- 只有 accepted human Contact 可以新建或编辑
petname。Contact tombstone / suspended 后记录 MAY 保留,但非 accepted 期间不得将它作为实时身份面的主标签;同一peer.principal_id后续重新成为 accepted Contact 时继续使用原记录,不得重键或自动覆盖。 - 该 key 是 principal-private,MUST 与 §3.5 blocklist 一样以加密 account data 形式同步,Station sync surface 不得读取明文。客户端解密后 MUST 验证
subject.kind="human",并用完整subject.principal_id和 holder 的 namespace key 重算 storage key;不匹配时 MUST fail closed,且不得覆盖本地已验证记录。 petname与noteMUST NOT 通过 mention、quote、forward、profile、Realm state、directory 或 Realm export 泄露给备注对象本人或其他成员。客户端构造引用、转发或导出时 MUST 使用对方公开的 display name / handle,不得替换为备注名。- 本地备注 MUST NOT 参与 ACL、grant subject、policy condition、audit attribution、sender verification 或 MLS credential 判定,约束与
identity/identity-handles.md§2.3 中 display name 一致。 - roster、消息 sender、联系人 / DM 列表、mention autocomplete、邀请 / 请求确认等实时 holder-facing 身份面,只有在 verified evidence 能把可见主体唯一归约到 accepted Contact 的
peer.principal_id时才可 join 备注;映射缺失、不唯一或仅有 display name / handle 时 MUST 按“无备注”处理,不得按字符串猜测关联。非空petnameMUST 作为主标签并带可识别的“备注”角标,当前 verified PCR Profile display 作为次要上下文;没有petname时主标签使用当前 verified PCR Profile display,不可达时才按既有 verified handle / protocol ID 降级。confirmed_display_name是确认基准,不得充当实时名称缓存。 - 历史 replay、audit 与 export 中,当前
petnameMAY 作为明确标注的 holder-private name 并列,但 MUST NOT 取代事件的subject_id、as-of handle、display_name_at_time或 audit attribution。安全敏感 UI MUST 能直接显示完整peer.principal_id;若显示peer_id,必须标记为托管服务而不是联系人身份。 - 当对方当前 verified handle 与
verified_handle_at_save不一致时,客户端 SHOULD 在该联系人的渲染处显示 handle changed / transferred 标记,并提示用户复核备注,与identity/identity-handles.md§6.1 的缓存失效语义一致。 - 当前可验证的 PCR
actor_profile.display_name与confirmed_display_name不一致时,普通实时身份面 SHOULD、安全敏感面 MUST 显示“全局显示名已变更”并同时提供旧确认值。profile 不可达、候选不唯一或只有 Realm override 时状态是 unknown,不得伪报 changed;只有 holder 的显式身份确认动作才可刷新已有值。普通 petname / note / tags / pin 编辑与 account-data CAS merge MUST 原样保留该字段。 - 客户端 MUST 在本地构造 accepted human Contact 的 confusable comparison set(confusable 比较基准集):每个非空
petname及已保存的confirmed_display_name。渲染主体 S 的当前 surface public display 时,MUST 以conformance/encoding.md§2.2 的arkret_display_confusable_v1与其它 Contact 的比较基准值比较,并排除 S 自己的基准值。碰撞主体不是 Contact 时必须显示“非联系人”及 verified handle / DID;是另一 Contact 时使用其自己的petname(若有)并加 handle / DID 消歧,且不得继承被碰撞联系人的头像信任环、verified-contact badge 或颜色。roster、请求、mention autocomplete、邀请确认与不可逆操作面必须使用同一判据。 - Contact accept 生效后,客户端仅可在该 key 从未存在、并且接受界面当场持有并展示了经
ak.self.actor_profile.read.resolve.v1取得并自行验签的 exact signed Profile Event 时,把接受动作作为首次身份确认,初始化confirmed_display_name;此动作 MUST NOT 自动创建或覆盖petname。当时没有可验证 Profile 时保持两字段缺失,Contact accept 仍成功;稍后的静默后台读取不得冒充用户确认。不得以 Realm override、Directory 裸结果、handle、DID、MemberIdentity display 或 OIDCname代替 verified PCR Profile。已有记录的petname、note、tags、pin、saved_at与确认值必须保留;并发初始化必须按 account-data whole-value CAS 做 read/decrypt → domain merge → encrypt/write,且保持幂等。 - 备注初始化或同步失败 MUST NOT 回滚、拒绝或伪装成 Contact accept 失败;客户端 SHOULD 持久重试并可显示“备注尚未跨设备同步”。手工输入备注和备注写入成功都不得成为 request / respond / glare accepted 的协议前置,也不得成为 typing / presence 等可被对端观察的差异信号。
- 客户端 MUST NOT 在未加密的本地缓存、日志、push payload 或崩溃报告中泄露
petname与note。服务端仍可能观察不透明 key 的数量、大小与更新时间;实现不得声称消除了这些流量 metadata。 - 删除联系人备注 MUST 使用
ak.self.account_data.resource.delete.v1写入../models/account-data.md§5.3 定义的有版本 physical-delete tombstone,不依赖客户端本地清理,也不得用无法通过本节字段验证的空对象冒充删除。
不透明 key transcript、raw principal / Realm / service DID 负例与解密后 slot-binding 校验由 ak.vector.account_data.contact_petname_binding.v1 闭合;成对名称碰撞由 ak.vector.encoding.confusable_check.v1 闭合。
3.7 Realm 备注 (Realm Remarks)
用户可以为已加入或已收藏的 Realm 保存只对自己可见的本地备注名、笔记和私有标签。该数据是 actor-private 的渲染覆盖层,不修改 Realm 公开的 title / summary,不写入 Realm history、invite 文案、directory 投影或任何协议主体字段。
典型场景:用户加入多个 title 相同的 Realm(例如多个 “Engineering”、多家客户都用 “项目 A”),需要在本地侧栏稳定区分而无需向其他成员暴露区分依据。
Key: ak.contacts.realm.<realm_id>
{ "version": 1, "subject": { "kind": "realm", "id": "ak:realm:Ac1aCK8aQdnkYImvdH3DFjq4jDCP198pXYWCGzGuVyj5" }, "local_name": "Acme 内部 · 工程", "note": "和外包侧 Engineering Realm 同名,注意区分", "tags": ["work", "high_signal"], "pinned": true, "verified_title_at_save": "Engineering", "verified_owning_organization_ids_at_save": ["ak:did_core:webvh:zGUwpRSnyVCLzU7upsm9iSwEv"], "saved_at": "2026-05-08T10:00:00Z", "updated_at": "2026-05-08T10:00:00Z"}字段:
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
version | int | yes | schema 版本,当前为 1。 |
subject.kind | enum(realm) | yes | 固定 realm,与 §3.6 仅以 human Contact peer.principal_id 为主体的全局联系人备注正交。 |
subject.id | id:realm | yes | 备注对象 Realm ID;MUST 与 key 中 <realm_id> 完全一致。 |
local_name | string | no | Realm 本地备注名,最大 128 字符;字符串验证与 §3.6 petname 一致,成对 confusable 比较见 conformance/encoding.md §2.2。 |
note | string | no | 自由文本笔记,最大 4096 字符。 |
tags | string[] | no | 私有分组标签,命名空间与 §3.1 ak.tags.realm.<realm_id>.tags 互通(同名 tag 视为同一分组);ak.* 保留给本规范,<vendor>.* 用于客户端扩展。 |
pinned | bool | no | 是否置顶。 |
verified_title_at_save | string | no | 保存或最近一次更新时 Realm 公开 title 的快照,用于反”改名混淆”。 |
verified_owning_organization_ids_at_save | did_core_id[] | no | 保存时 owning_organization_ids 快照,用于在组织漂移 / takeover 时给出复核提示。 |
saved_at | timestamp | yes | 首次保存时间。 |
updated_at | timestamp | no | 最近修改时间。 |
规则:
- 该 key 是 actor-private,MUST 与 §3.5、§3.6 一样以加密 account data 形式同步,Station sync surface 不得读取明文。
local_name与noteMUST NOT 通过 invite 文案、mention、quote、forward、directory 投影、shared link preview 或任何 Realm state 字段泄露给其他 Realm 成员;客户端构造邀请、跨端 share sheet、跨 Realm 引用或导出时 MUST 使用 Realm 公开title,不得替换为本地备注。- 本地备注 MUST NOT 参与 ACL、capability subject、policy condition、audit attribution、MLS credential 或 federation routing 判定,约束与 §3.6 中本地联系人备注一致。
- UI 显示本地备注时 SHOULD 同时呈现 Realm 公开
title或ak:realm:token 短摘要(44-character suffix 的前 8 字符),使用户可识别”备注相同但 Realm 不同”的误判;安全敏感 UI(删除 / archive / tombstone Realm、跨 Realm 邀请确认、转账类 applet 调用)MUST 能直接显示完整realm_id与owning_organization_ids。 - 当 Realm 公开
title与verified_title_at_save不一致,或owning_organization_ids与verified_owning_organization_ids_at_save不一致时,客户端 SHOULD 在该 Realm 渲染处显示 title changed / organization changed 标记,并提示用户复核备注;该机制与 §3.6verified_handle_at_save对称。 - 当用户已加入的多个 Realm 的公开
title字符串经arkret_display_confusable_v1判为碰撞时,UI MUST 优先按local_name区分;缺少local_name时 MUST 退化到owning_organization_ids/ source Realm /ak:realm:短摘要等附加上下文,不得在仅显示title的情况下让用户做破坏性或不可逆操作。 - 客户端 MUST NOT 在未加密的本地缓存、日志、push payload 或崩溃报告中泄露
local_name与note。 - 删除 Realm 备注 MUST 使用
ak.self.account_data.resource.delete.v1写入../models/account-data.md§5.3 定义的有版本 physical-delete tombstone(与 §3.6 联系人备注同一机制),不依赖客户端本地清理,也不得用空对象冒充删除——空对象会被ak.self.account_data.resource.replace.v1的 closed schema 当作普通值写入,删不掉任何东西。用户离开或被踢出 Realm MAY 触发自动 tombstone(客户端策略,规范不强制)。 ak.contacts.realm.<realm_id>与 §3.1ak.tags.realm.<realm_id>并存:前者负责命名与笔记,后者负责分组与order排序;客户端 SHOULD 在本地 projection 中按realm_idjoin 二者,规范上互不替代。
3.8 已读回执偏好 (Read Receipt Preferences)
控制是否向其他成员发送 ak.receipt.read(详见 discovery/read-receipts.md)。MAY 设全局默认,并对特定 Realm 或 Strand / discussion track 单独重写。
Key: ak.read_receipt.preferences
{ "default": { "send": true, "display": true }, "realms": { "ak:realm:Ac1aCK8aQdnkYImvdH3DFjq4jDCP198pXYWCGzGuVyj5": { "send": false } }, "strands": { "ak:strand:AaalePlTK6W4ZKrbKyKzlmmcdhXVx-InxeWY4ul69tiN": { "send": true } }}字段:
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
default.send | bool | true | 全局是否发送 ak.receipt.read。 |
default.display | bool | true | 全局是否在本地 UI 显示他人的 ak.receipt.read。只影响本地渲染,不改变订阅、fanout 或 unread 计算。 |
realms.<realm_id>.send | bool | 针对单个 Realm 的覆盖,优先于 default。 | |
realms.<realm_id>.display | bool | 针对单个 Realm 的本地显示覆盖,优先于 default。 | |
strands.<strand_id>.send | bool | 针对单个 Strand / discussion track 的覆盖,优先于 realms.<realm_id>。 | |
strands.<strand_id>.display | bool | 针对单个 Strand / discussion track 的本地显示覆盖,优先于 realms.<realm_id>。 |
规则:
- 该 key 是 actor-private,加密存储于 account data;其他成员或 Station sync surface 不得读取明文。
- 客户端在生成
ak.receipt.read前 MUST 按 (strand, realm, default) 顺序解析有效send,最先命中的非空值生效。 - 客户端在渲染他人的
ak.receipt.read前 SHOULD 按相同顺序解析有效display;display=false只隐藏本地 UI,不得要求 Station sync surface 停止投递,也不得改变 read cursor、unread count 或 push suppression 的协议状态。 - 该偏好 MUST NOT 影响 §3 中 actor-private 的 Read Cursor(
ak.read_cursor.advance)发送或多端同步。 - 当目标 Realm / Strand 声明
ak.realm.read_receipt_policy.disclosure="required"(详见discovery/read-receipts.md§2.5)时,合规客户端 MUST NOT 允许该 scope 设置为send=false,并 SHOULD 在 UI 标注该开关被 Realm / Strand 策略锁定;声明为disabled时同样无视用户的send=true不发送。 - 客户端 MAY 在 UI 上将常用过滤维度(按 Realm 标签、按 Organization)做成批量编辑入口,但实际 canonical state 仍以本 key 中的逐 ID 覆盖为准。
4. 与本地投影的交互
虽然 account data 对外不公开,但用户自己的客户端或可信端侧节点会拉取并解密这些数据,并合并到本地查询结果中。
例如:当客户端以 object_kinds=["realm"] 查询加入的 Realm 列表时,本地 projection 可以按 realm_id 同时 join ak.tags.realm.*(私有标签与排序)与 ak.contacts.realm.*(本地备注名、笔记、置顶),得到带 local_name 与 tag 的 Realm 列表,并在 title 重复时优先按 local_name 区分。
5. 安全与隐私
- 涉及用户敏感信息的 Account Data(例如访问第三方服务的私钥、密码管理器的 Vault),MUST 另外进行客户端加密(Client-Side Encryption),使用类似 Matrix 4S (Secret Storage) 的机制,通过单独的 Recovery Key 保护。
- 普通的 UI 偏好和标签可以直接由用户的 Device Key 签名写入加密 account data。
6. v1 规则
- 4S / Secret Storage 与 Key Backup 的存储格式必须使用客户端加密 envelope,绑定 principal DID、device / recovery key、algorithm、KDF parameters、created_at、version 和 payload hash。服务端不得获得解锁材料。
- 跨端排序字段 MUST 使用稳定 rank string 或 HLC + tie-break 组合,不得使用非确定性 float 作为唯一排序真相。客户端可在 UI 内使用 float 计算临时位置,但写回必须归一为规范 rank。