摘要

  • 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;冻结来源也没有证明任何具名服务正在实施或采用某种保留策略。

这项成熟度边界再次说明:注册表中“支持历史”的一列可以协调实现,只有运行证据才能回答到底捕获了哪些版本、折叠了什么、剪除了什么、谁被授权,以及恢复是否成功。

来源