摘要
draft-ietf-jmap-object-history-00扩展Foo/get,可返回保留下来的旧版本和已删除对象,但服务器可以折叠连续变更、任意剪枝,并因存储压力静默清除历史。- 返回的快照适合恢复,不会自动记录操作者、变更请求、授权判定与外部结果;甚至
hasMoreHistory: false也不能证明不存在更多历史。
服务器可以找回联系人删除电话号码之前的样子,也可以取出一封邮件被销毁前的最终状态。这是实用能力:误改可以撤回,“最近删除”不必另外发明一套协议。
问题在于这份结果很容易被界面包装成比自身更强的证据。它带着同一个对象 ID、版本序号和被替换时间,时间线看起来干净整齐。然而其中没有字段说明谁发起变更、哪个客户端提交请求、哪条授权策略放行、为何变更,或后续动作是否真正完成。
9 月 15 日转为工作组命名的 JMAP Object History 正好画出了这条边界:它让旧状态可检索,并没有把恢复存储升级成审计账本。
协议返回状态,不返回事件
JMAP 核心已经为对象类型定义 Foo/get、Foo/changes、Foo/set、Foo/query 与 Foo/queryChanges。现有模型擅长同步当前状态;Foo/changes 可以告诉客户端哪些 ID 发生变化,却不给出旧值。
新能力 urn:ietf:params:jmap:object-history 扩展 Foo/get。设置 includeReplaced: true 后,可在当前对象之外返回保留的旧版本;设置 includeDestroyed: true 后,还可返回已经不存在的对象。两者同时使用时,服务器可给出它仍保存的全部已删除对象版本。
同一对象的各版仍使用相同 ID。objectHistory.version 负责在一次响应内排序;objectHistory.replaced 表示该表示被更新或销毁取代的时刻,null 表示当前版本。
这些是状态坐标,不是事件来源。被替换时间不是旧版创建时间,版本号也不是交易 ID。两者都没有操作者、会话、设备、请求、审批、策略、通知或业务结果。相邻快照能算出字段差异,却不能证明一个原子操作造成了全部差异。
消失的中间过程可能从未保存
草案允许服务器把快速连续更新折叠为一个历史版本,不要求每次个别变更都生成版本。某个电话号码在短时间内连续修改三次,历史面可能只保留这一段开始前和结束后的值。
服务器还可以按任意顺序剪除旧版本。版本序号存在缺口完全有效,客户端不得假设历史完整或连续。因此,缺少一个版本至少有两种解释:服务器从未为那次变化创建单独版本,或者曾经创建、后来删除。
序号在跨请求时也故意保持弱语义。服务器“应该”一致,但客户端“不得”依赖;其主要用途只是排列一次响应中同一对象的版本。把它当永久审计事件 ID,会依赖规范拒绝保证的行为。
这使存储合同更诚实,也意味着“给我发生过的一切”不是该接口能可靠回答的问题。若业务需要完整事件,必须由另一套事件系统承担。
hasMoreHistory 是检索提示,不是完整性证书
historyLimit 限制响应总条目,并要求优先返回最近、序号最高的版本。只要某个请求 ID 触及上限,hasMoreHistory: true 就表示还有较旧的保留条目。多对象响应不会指出究竟哪个 ID 还有更多,客户端需要逐个查询。
true 只是瞬时事实。草案警告,下一次请求前服务器就可能清除这些条目;标志没有替客户端预留数据。
false 更弱。通常它表示服务器已经返回所报告的全部保留历史,但即使仍存在更多历史,服务器若难以高效判断也可以返回 false。它不证明每次变更都曾被捕获,不证明没有剪枝,甚至不保证所有仍存在的旧表示都已被发现。
把 false 渲染成“审计历史完整”,正好颠倒了协议含义。可辩护的表述只能是:这次响应没有报告更多可取历史。
null 时长并不代表永久保存
账户能力会公开 maxHistoryDuration。数值表示从版本被替换后起算的最大保留秒数,超龄版本可能已经丢弃;null 只表示服务器不设置基于时间的上限。
null 不是无限期承诺。安全章节承认总存储压力,并允许达到容量限制时静默删除历史。时间只是一个控制,容量是另一个。某个数据类型即使宣称支持历史接口,也可以不保留任何旧版;服务器仍返回当前对象及 objectHistory,甚至把所有当前对象都标成 version 1。
因此,能力广告证明接口存在,不证明背后存在最低证据档案。采购、合规和事故响应需要另行规定最短保留期、删除通知、导出、完整性与 legal hold。
恢复与问责需要不同收据
作为恢复面,这项设计很合适。客户端可以先用 Email/changes 找到被销毁 ID,再把它们传给 Email/get,请求已删除对象,展示销毁前最后状态。内容可以恢复,协议也不必假装对象仍处于 live 状态。
作为问责面,同一响应远远不够。严肃的变更收据至少要包括:经过认证的操作者与会话、来源客户端和设备、完整 mutation 请求、条件状态令牌、生效的授权策略版本与判定、服务器接受的交易、前后哈希、不可变事件 ID、通知送达与观察到的结果。
对象历史可以贡献前态或后态,却无法填满其余栏位。不能从对象所有者反推操作者,不能从状态改变反推授权规则,也不能从保存的新值反推下游执行成功。需要问责时,应把恢复快照与 append-only 事件记录关联,而不是迫使一份存储同时满足互相冲突的合同。
历史访问权不等于当前访问权
历史还带来复杂的权限问题。草案要求:当前无权读取对象的人不得读取其历史;如果对象权限曾经改变,服务器只能返回请求者在对应时期本来就有权读取的版本。
这样可以防止刚刚获得权限的读者看到自己当时无权查看的旧值,但也要求服务器保留足够的历史授权语境。只看当前 ACL 或 shareWith,可能无法回答过去的权利。
JMAP Sharing 已经区分 Principal、期望权限与有效访问。Object History 再加入时间维度:读取旧版本的权利取决于历史权利,而非当前权限图。响应只能证明服务器此刻选择披露该版本;若授权判定没有独立记录,它并不是权限决策的完整收据。
有时“正确历史”本来就应当不完整
提案承认旧对象可能暴露用户或管理员原本想移除的信息。联系人已删除的电话号码仍可能留在历史中,因此草案建议提供按对象清除历史的管理能力,用于隐私与合规。
这不是应该隐藏的缺陷。恢复、问责、隐私删除和存储经济性本来就朝不同方向拉扯。产品需要说明每类数据优先满足哪个目标、谁能批准清除、清除后是否保留防篡改收据,以及在删除敏感内容之后还应保存哪些不含内容的审计事实。
在一个允许清除的接口上承诺“不可变历史”是危险的;因为它不是法证账本就关闭恢复功能也同样错误。正确做法是分开存储,并公开两者边界。
工作组采用不等于部署
9 月的文件与 3 月的个人草案几乎逐字相同。协议正文没有新增实施证据;实际变化是提案改用 JMAP 工作组名称。
Datatracker 将其列为 WG Document、状态 I-D Exists,没有负责 AD、shepherd 或 telechat。页面的 intended status 摘要为空,而草案页眉写着 Standards Track。它仍是 Internet-Draft,不是 RFC;冻结来源也没有证明任何具名服务正在实施或采用某种保留策略。
这项成熟度边界再次说明:注册表中“支持历史”的一列可以协调实现,只有运行证据才能回答到底捕获了哪些版本、折叠了什么、剪除了什么、谁被授权,以及恢复是否成功。
来源
- JMAP Object History Datatracker 页面
- JMAP Object History 修订历史
- JMAP Object History,工作组第 00 版
- JMAP Object History,个人第 00 版
- RFC 8620:JSON Meta Application Protocol
- RFC 8621:JMAP for Mail
- RFC 9610:JMAP for Contacts
- RFC 9670:JMAP Sharing
- IANA JMAP 参数注册表
- RFC 2119:需求级别关键词
- RFC 8174:RFC 2119 大小写关键词歧义
- 最小初始规范、局部未来决策与自愿采用
- 运行代码优先
会员简报
档案背景详情
使用相应会员等级登录,即可解锁完整简报与来源注释。
仅限 Strategic Circle
Strategic Circle
所有读者均可浏览。加入并登录后可解锁档案简报。
加入 Strategic Circle仅限 Leadership Alliance
Leadership Alliance
符合条件的 IP 资产所有者和管理层可登录查看 Leadership Alliance 简报。
加入 Leadership Alliance

