非官方中文译本声明:本页为 IETF RFC 8620《The JSON Meta Application Protocol (JMAP)(JSON 元数据应用协议)》 的中文译本,由 ztpop.net 整理翻译,仅供学习参考。RFC 文档由 IETF 发布,受 BCP 78 与 IETF 信托法律条款约束;本译本保留原文编号与结构,权威性以英文原文为准。英文原文见 rfc-editor.org/rfc/rfc8620。
RFC 8620:JSON 元数据应用协议(JMAP)
摘要
本文档规定了一个协议,供客户端高效地查询、获取和修改基于 JSON 的数据对象,并支持变更推送通知、快速重新同步,以及带外(out-of-band)二进制数据的上传与下载。
本备忘录的状态
本文是一份互联网标准跟踪(Standards Track)文档。
本文是互联网工程任务组(IETF)的成果,代表 IETF 社区的共识,已经过公开评审,并由互联网工程指导组(IESG)批准发布。有关互联网标准的更多信息见 RFC 7841 第 2 节。
关于本文当前状态、任何勘误以及如何提供反馈的信息,可访问 https://www.rfc-editor.org/info/rfc8620。
版权声明
Copyright (c) 2019 IETF 信托及被列为文档作者的个人。保留所有权利。
本文受 BCP 78 以及 IETF 信托的《IETF 文档相关法律规定》(https://trustee.ietf.org/license-info)约束,以本文档发布之日生效的版本为准。请仔细审阅这些文档,因为它们描述了您就本文档所享有的权利与限制。从本文档中提取的代码组件必须包含《简化 BSD 许可证》文本(见信托法律条款第 4.e 节),并按该许可证的描述"不提供任何担保"。
目录
- 1. 引言
- 1.1. 符号约定
- 1.2. Id 数据类型
- 1.3. Int 与 UnsignedInt 数据类型
- 1.4. Date 与 UTCDate 数据类型
- 1.5. JSON 作为数据编码格式
- 1.6. 术语
- 1.6.1. 用户
- 1.6.2. 账户
- 1.6.3. 数据类型与记录
- 1.7. JMAP API 模型
- 1.8. 厂商特定扩展
- 2. JMAP 会话资源
- 2.1. 示例
- 2.2. 服务自动发现
- 3. 结构化数据交换
- 3.1. 发起 API 请求
- 3.2. Invocation 数据类型
- 3.3. Request 对象
- 3.3.1. 请求示例
- 3.4. Response 对象
- 3.4.1. 响应示例
- 3.5. 省略参数
- 3.6. 错误
- 3.6.1. 请求级错误
- 3.6.2. 方法级错误
- 3.7. 对先前方法结果的引用
- 3.8. 用户可见字符串的本地化
- 3.9. 安全
- 3.10. 并发
- 4. Core/echo 方法
- 4.1. 示例
- 5. 标准方法与命名约定
- 5.1. /get
- 5.2. /changes
- 5.3. /set
- 5.4. /copy
- 5.5. /query
- 5.6. /queryChanges
- 5.7. 示例
- 5.8. 代理考量
- 6. 二进制数据
- 6.1. 上传二进制数据
- 6.2. 下载二进制数据
- 6.3. Blob/copy
- 7. 推送
- 7.1. StateChange 对象
- 7.1.1. 示例
- 7.2. PushSubscription
- 7.2.1. PushSubscription/get
- 7.2.2. PushSubscription/set
- 7.2.3. 示例
- 7.3. 事件源
- 7.1. StateChange 对象
- 8. 安全考量
- 8.1. 传输机密性
- 8.2. 认证方案
- 8.3. 服务自动发现
- 8.4. JSON 解析
- 8.5. 拒绝服务
- 8.6. 连接到未知推送服务器
- 8.7. 推送加密
- 8.8. 流量分析
- 9. IANA 考量
- 9.1. 分配 jmap 服务名
- 9.2. 注册 JMAP 的 Well-Known URI 后缀
- 9.3. 注册 jmap URN 子命名空间
- 9.4. 创建"JMAP Capabilities"注册表
- 9.4.1. 初步社区评审
- 9.4.2. 向 IANA 提交请求
- 9.4.3. 指定专家评审
- 9.4.4. 变更流程
- 9.4.5. JMAP Capabilities 注册表模板
- 9.4.6. JMAP Core 的初始注册
- 9.4.7. 在 JMAP Capabilities 注册表中为 JMAP 错误占位符注册
- 9.5. 创建"JMAP Error Codes"注册表
- 9.5.1. 专家评审
- 9.5.2. JMAP Error Codes 注册表模板
- 9.5.3. JMAP Error Codes 注册表的初始内容
- 10. 参考文献
- 10.1. 规范性参考文献
- 10.2. 资料性参考文献
- 作者地址
1. 引言
JSON 元数据应用协议(JMAP)用于在客户端与服务器之间同步数据,例如邮件、日历或联系人。它针对移动与 Web 环境进行了优化,旨在为不同的数据类型提供一致的接口。
本规范描述的是数据同步的通用机制。其他规范定义了可通过 JMAP 同步的不同数据类型的模型。
JMAP 的设计旨在高效利用有限的网络资源。多个 API 调用可以在单个请求中批处理,从而减少往返次数并改善移动设备的电池续航。推送连接消除了轮询的需要,而一种高效的增量更新机制确保传输的数据量最小。
JMAP 设计为可水平扩展到非常多的用户。这得益于登录后面向用户的独立端点、二进制数据与结构化数据的分离,以及一种不允许账户之间存在数据依赖的共享数据模型。
1.1. 符号约定
本文档中的关键词"MUST(必须)"、"MUST NOT(不得)"、"REQUIRED(要求)"、"SHALL(应)"、"SHALL NOT(不应)"、"SHOULD(应该)"、"SHOULD NOT(不应该)"、"RECOMMENDED(推荐)"、"NOT RECOMMENDED(不建议)"、"MAY(可以)"和"OPTIONAL(可选)",当且仅当它们以如这里所示的全大写形式出现时,才应按照 BCP 14 [RFC2119] [RFC8174] 中的描述进行解释。
本规范使用的基础格式是 JSON。因此,"object(对象)"和"array(数组)"这两个术语,以及四种基本类型(字符串、数字、布尔值和 null),应按照 [RFC8259] 第 1 节的描述进行解释。除非另有说明,所有属性名和属性值都是大小写敏感的。
本文档中的部分示例包含用于说明目的的"局部"JSON 文档。在这些示例中,使用三个句点"..."表示为了简洁而被删除的文档部分。
为了符合出版要求,在长 JSON 字符串内部插入了换行,后续行缩进显示。要构成有效的 JSON 示例,字符串内部的任何换行都必须替换为空格,并去除换行之后的任何其他空白。
除非另有说明,API 交换的示例仅显示 Request 对象的 methodCalls 数组或 Response 对象的 methodResponses 数组。为简洁起见,Request/Response 对象的其余部分被省略。
本文档为所有 JSON 值给出了类型签名。使用以下约定:
- "*" —— 类型未定义(值可以是任何类型,不过允许的值可能受该值的上下文约束)。
- "String" —— JSON 字符串类型。
- "Number" —— JSON 数字类型。
- "Boolean" —— JSON 布尔类型。
- "A[B]" —— 一个 JSON 对象,其键全部为类型"A",值全部为类型"B"。
- "A[]" —— 一个由类型"A"的值组成的数组。
- "A|B" —— 值要么是类型"A",要么是类型"B"。
其他类型也可能被给出,其表示形式在本文档的其他位置定义。
对象属性除类型签名外,还可能带有一组属性。它们的含义如下:
- "server-set(服务器设置)" —— 只有服务器可以设置该属性的值。在创建该类型的新对象时,客户端不得发送该属性。
- "immutable(不可变)" —— 对象创建后该值不得更改。
- "default(默认)" —— (其后跟一个 JSON 值。)如果在参数中省略该属性,或在创建该类型的新对象时省略该属性,将使用的默认值。
1.2. Id 数据类型
所有记录 id 都由服务器分配且不可变。
当给出"Id"作为数据类型时,它表示一个"String(字符串)",大小至少为 1 个八位组、最多为 255 个八位组,并且只能包含 [RFC4648] 第 5 节定义的"URL 及文件名安全"base64 字母表(不包括填充字符"=")中的字符。这意味着允许的字符是 ASCII 字母数字字符("A-Za-z0-9")、连字符("-")和下划线("_")。
这些字符在几乎所有上下文中使用都是安全的(例如文件系统、URI 和 IMAP atom)。出于最大安全性考虑,服务器还应遵循防御性的分配策略,以避免在可能存在 glob 补全或数据类型检测的地方产生风险(例如在文件系统或电子表格中)。尤其应当明智地避免:
- 以短横线开头的 id;
- 以数字开头的 id;
- 仅由数字组成的 id;
- 仅通过 ASCII 大小写区分的 id(例如 A 与 a);
- 特定三字符序列"NIL"(因为该序列可能与 IMAP 协议中表示 null 值的表达式混淆)。
解决这些问题的一个好办法是为每个 id 加上单个字母前缀。
1.3. Int 与 UnsignedInt 数据类型
当给出"Int"作为数据类型时,它表示一个范围在 -2^53+1 <= value <= 2^53-1 内的整数,即存储在浮点双精度数中的整数的安全范围,表示为 JSON 的"Number(数字)"。
当给出"UnsignedInt"作为数据类型时,它表示一个"Int",其取值必须在 0 <= value <= 2^53-1 范围内。
1.4. Date 与 UTCDate 数据类型
当给出"Date"作为类型时,它表示一个采用"date-time"格式 [RFC3339] 的字符串。为确保规范化形式,"time-secfrac" 如果为零则必须省略,字符串中的任何字母(例如"T"和"Z")必须为大写。例如 "2014-10-30T14:12:00+08:00"。
当给出"UTCDate"作为类型时,它表示一个"Date",其"time-offset"部分必须为"Z"(即必须采用 UTC 时间)。例如 "2014-10-30T06:12:00Z"。
1.5. JSON 作为数据编码格式
JSON 是 [RFC8259] 中规定的基于文本的数据交换格式。[RFC7493] 中定义的互联网 JSON(I-JSON)格式是其严格子集,增加了一些限制以避免可能产生混淆的场景(例如,它要求对象不得有两个同名的成员)。
从客户端发往服务器、或从服务器发往客户端的所有数据(二进制文件的上传/下载除外)都必须是符合该 RFC 的有效 I-JSON,因此是大小写敏感的,并以 UTF-8 [RFC3629] 编码。
1.6. 术语
1.6.1. 用户
用户是通过 JMAP 访问数据的个人。用户拥有一组权限,决定了其能够看到的数据。
1.6.2. 账户
账户是一组数据的集合。单个账户可以包含任意一组数据类型,例如一组邮件、联系人和日历。大多数 JMAP 方法都带有一个必需的"accountId"参数,用于指定操作在哪个账户上进行。
账户与用户并不相同,尽管通常一个主要账户直接属于该用户。例如,你可能拥有一个包含某个群组或企业数据的账户,多个用户可以访问它。
一组凭证可以提供对多个账户的访问,例如,如果另一位用户正在与经过认证的用户共享其工作日历,或者存在一个用于支持台收件箱的群组邮箱。
在发生严重内部错误时,服务器可能不得不重新分配 id,或做出其他违反某账户标准 JMAP 数据约束的操作。在这种情况下,服务器上的数据不再与客户端此前可能缓存的数据兼容。服务器必须将此视为该账户已被删除、随后以新的账户 id 重新创建。客户端随后将被迫丢弃带有旧账户 id 的任何数据,并从头重新获取所有数据。
1.6.3. 数据类型与记录
JMAP 为创建、检索、更新和删除各类对象提供了统一的接口。一个"数据类型"是一组具名、带类型的属性的集合,就像数据库表的模式一样。数据类型的一个实例称为一条"记录"。
记录的 id 不可变,并由服务器分配。该 id 在同一账户内对同类型的所有记录必须是唯一的。id 可能在跨账户时冲突,或在同账户内对两种不同类型的记录冲突。
1.7. JMAP API 模型
JMAP 使用 HTTP [RFC7230] 来暴露 API、推送、上传和下载资源。所有 HTTP 请求都必须使用"https://"方案(基于 TLS 的 HTTP [RFC2818])。所有 HTTP 请求都必须经过认证。
经过认证的客户端可以获取用户的 Session(会话)对象,其中包含有关服务器可提供的数据与能力的详细信息,如第 2 节所示。客户端随后可以通过以下方式与服务器交换数据:
- 客户端可以向服务器发起 API 请求以获取或设置结构化数据。该请求由一组有序的方法调用组成。这些方法由服务器处理,随后服务器返回一组有序的响应。这在第 3、4、5 节中描述。
- 客户端可以从服务器下载或向服务器上传二进制文件。这在第 6 节详述。
- 客户端可以连接到服务器上的推送通道,以在数据发生变化时收到通知。这在第 7 节解释。
1.8. 厂商特定扩展
各个服务商都会有一些希望通过 JMAP 暴露的自定义特性。这可能表现为规范之外的额外数据类型和/或方法、JMAP 方法的额外参数,或现有数据类型上的额外属性(这些属性也可能出现在以属性名为参数的方法中)。
服务器可以通过将标识符包含在 capabilities(能力)对象中来通告其支持的自定义扩展。厂商扩展的标识符必须是属于该厂商拥有的域的 URL,以避免冲突。该 URL 应当能解析到描述该扩展所做变更的文档。
客户端必须通过在该 Request 对象的"using"数组中传入相应的能力标识符来选择启用(opt in)某个扩展,如第 3.3 节所述。服务器必须只遵循被选择启用的规范,并在处理请求时表现得好像它没有实现任何其他东西一样。这是为了确保与不知道某个特定自定义扩展的客户端之间的兼容性,以及与未来版本 JMAP 的兼容性。
2. JMAP 会话资源
连接到 JMAP 服务器需要两样东西:
- JMAP 会话资源的 URL。这可以直接向用户索取,也可以根据用户名域自动发现(见下文第 2.2 节)。
- 用于认证的凭证。如何获取凭证不在本文档范围内。
对 JMAP 会话资源发起的成功且经过认证的 GET 请求必须返回一个 JSON 编码的 *Session* 对象,给出在给定这些凭证的情况下,服务器可向客户端提供的数据与能力详情。它具有以下属性:
- capabilities: "String[Object]"
- 一个指定本服务器能力的对象。每个键都是服务器支持的一种能力的 URI。这些键对应的值是一个对象,带有关于该能力的服务器能力的进一步信息。
- 客户端必须忽略任何它不理解的性质。
capabilities 对象必须包含一个名为"urn:ietf:params:jmap:core"的属性。该属性的值是一个对象,必须包含以下关于服务器能力的信息(给出了建议的最小值限制,以使客户端能高效利用网络):
- maxSizeUpload: "UnsignedInt"
- 服务器愿意为单次文件上传(出于任何目的)接受的单个文件的最大大小(以八位组计)。建议最小值:50,000,000。
- maxConcurrentUpload: "UnsignedInt"
- 服务器愿意在上传端点接受的并发请求的最大数量。建议最小值:4。
- maxSizeRequest: "UnsignedInt"
- 服务器愿意在 API 端点接受的单个请求的最大大小(以八位组计)。建议最小值:10,000,000。
- maxConcurrentRequests: "UnsignedInt"
- 服务器愿意在 API 端点接受的并发请求的最大数量。建议最小值:4。
- maxCallsInRequest: "UnsignedInt"
- 服务器愿意在 API 端点的单个请求中接受的方法调用的最大数量。建议最小值:16。
- maxObjectsInGet: "UnsignedInt"
- 客户端可以在单个 /get 类型的方法调用中请求的对象的最大数量。建议最小值:500。
- maxObjectsInSet: "UnsignedInt"
- 客户端可以在单个 /set 类型的方法调用中发送以创建、更新或销毁的对象的最大数量。这是合并后的总数,例如,如果最大值为 10,你则不能创建 7 个对象并销毁 6 个对象,因为那将是 13 个动作,超过了限制。建议最小值:500。
- collationAlgorithms: "String[]"
- 由服务器支持的、在查询记录排序时使用的、注册在 collation 注册表(如 [RFC4790] 所定义)中的算法标识符列表。
未来能力的规范将在 capabilities 对象上定义它们自己的属性。
如第 1.8 节所述,服务器可以通告厂商特定的 JMAP 扩展。为避免冲突,厂商特定扩展的标识符必须是厂商拥有的域下的 URL。客户端必须选择启用它希望使用的任何能力(见第 3.3 节)。
- accounts: "Id[Account]"
- 从账户 id 到用户有权访问的每个账户(见第 1.6.2 节)的 Account 对象的映射。一个 *Account* 对象具有以下属性:
- name: "String"
- 展示来自该账户的内容时使用的、对用户友好的字符串,例如代表账户所有者的电子邮件地址。
- isPersonal: "Boolean"
- 如果该账户属于经过认证的用户,而非群组账户或已与其共享的另一用户的个人账户,则为 true。
- isReadOnly: "Boolean"
- 如果整个账户是只读的,则为 true。
- accountCapabilities: "String[Object]"
- 该账户中支持的方法所对应的能力 URI 集合。每个键都是一种能力的 URI,该能力带有可与本账户一同使用的方法。这些键对应的值是一个对象,带有关于该账户相对于该能力的权限与限制的进一步信息(如该能力的规范中所定义)。
- 客户端必须忽略任何它不理解的性质。
- name: "String"
- 从账户 id 到用户有权访问的每个账户(见第 1.6.2 节)的 Account 对象的映射。一个 *Account* 对象具有以下属性:
服务器在上述 capabilities 对象中通告其支持的完整能力列表。如果某一能力定义了新方法,那么当用户可以使用这些方法操作本账户时,服务器必须将其包含在 accountCapabilities 对象中;当用户无法使用这些方法操作本账户时,服务器不得将其包含在 accountCapabilities 对象中。
例如,你可能可以访问自己的包含邮件、日历和联系人数据的账户,以及一个仅有联系人数据(例如企业通讯录)的共享账户。在这种情况下,第一个账户的 accountCapabilities 属性将包含类似 "urn:ietf:params:jmap:mail"、"urn:ietf:params:jmap:calendars" 和 "urn:ietf:params:jmap:contacts" 的内容,而第二个账户将仅包含最后这一项。
试图在某一不支持该能力的账户上使用该方法所定义的方法,将以"accountNotSupportedByMethod"错误被拒绝(见第 3.6.2 节"方法级错误")。
- primaryAccounts: "String[Id]"
- 从能力 URI(如同在 accountCapabilities 中找到的那样)到被视为该能力相关数据所属用户的"主"账户或默认账户的账户 id 的映射。如果没有属于用户的被返回账户,或以任何其他方式无法合理确定默认账户,那么即使服务器(以及 capabilities 对象中)支持该能力,也可能没有该 URI 的条目。"urn:ietf:params:jmap:core" 不应出现。
- username: "String"
- 与给定凭证相关联的用户名,如果没有则为空字符串。
- apiUrl: "String"
- 用于发起 JMAP API 请求的 URL。
- downloadUrl: "String"
- 下载文件时使用的 URL 端点,采用 URI 模板(level 1)格式 [RFC6570]。该 URL 必须包含名为 "accountId"、"blobId"、"type" 和 "name" 的变量。这些变量的用法在第 6.2 节描述。由于内容类型中斜杠可能存在编码问题,建议将 "type" 变量放在 URL 的查询部分。
- uploadUrl: "String"
- 上传文件时使用的 URL 端点,采用 URI 模板(level 1)格式 [RFC6570]。该 URL 必须包含名为 "accountId" 的变量。该变量的用法在第 6.1 节描述。
- eventSourceUrl: "String"
- 用于连接以接收推送事件的 URL,如第 7.3 节所述,采用 URI 模板(level 1)格式 [RFC6570]。该 URL 必须包含名为 "types"、"closeafter" 和 "ping" 的变量。这些变量的用法在第 7.3 节描述。
- state: "String"
- 一个(最好是短的)字符串,代表服务器上该对象的状态。如果 Session 对象上任何其他属性的值发生更改,该字符串也会更改。当前值也会在 API Response 对象(见第 3.4 节)上返回,使客户端能够快速判断会话信息是否已更改(例如已添加或移除一个账户),从而需要重新获取该对象。
为确保未来的兼容性,Session 对象上可能包含其他属性。客户端必须忽略任何它未预期的性质。
实现者必须注意避免在 HTTP 层不当缓存 Session 对象。由于客户端只应在检测到发生更改时(通过 API 响应的 sessionState 属性)才重新获取,建议完全禁用 HTTP 缓存,例如通过在响应上设置 "Cache-Control: no-cache, no-store, must-revalidate"。
2.1. 示例
在下面的 Session 对象示例中,用户可通过 JMAP 访问自己的邮件和联系人,以及对另一用户共享邮件的只读访问。服务器正在通告自定义的 "https://example.com/apis/foobar" 能力。
{
"capabilities": {
"urn:ietf:params:jmap:core": {
"maxSizeUpload": 50000000,
"maxConcurrentUpload": 8,
"maxSizeRequest": 10000000,
"maxConcurrentRequest": 8,
"maxCallsInRequest": 32,
"maxObjectsInGet": 256,
"maxObjectsInSet": 128,
"collationAlgorithms": [
"i;ascii-numeric",
"i;ascii-casemap",
"i;unicode-casemap"
]
},
"urn:ietf:params:jmap:mail": {}
"urn:ietf:params:jmap:contacts": {},
"https://example.com/apis/foobar": {
"maxFoosFinangled": 42
}
},
"accounts": {
"A13824": {
"name": "john@example.com",
"isPersonal": true,
"isReadOnly": false,
"accountCapabilities": {
"urn:ietf:params:jmap:mail": {
"maxMailboxesPerEmail": null,
"maxMailboxDepth": 10,
...
},
"urn:ietf:params:jmap:contacts": {
...
}
}
},
"A97813": {
"name": "jane@example.com",
"isPersonal": false,
"isReadOnly": true,
"accountCapabilities": {
"urn:ietf:params:jmap:mail": {
"maxMailboxesPerEmail": 1,
"maxMailboxDepth": 10,
...
}
}
}
},
"primaryAccounts": {
"urn:ietf:params:jmap:mail": "A13824",
"urn:ietf:params:jmap:contacts": "A13824"
},
"username": "john@example.com",
"apiUrl": "https://jmap.example.com/api/",
"downloadUrl": "https://jmap.example.com
/download/{accountId}/{blobId}/{name}?accept={type}",
"uploadUrl": "https://jmap.example.com/upload/{accountId}/",
"eventSourceUrl": "https://jmap.example.com
/eventsource/?types={types}&closeafter={closeafter}&ping={ping}",
"state": "75128aab4b1b"
}
2.2. 服务自动发现
互联网协议目前有两种标准化的自动发现方法:
- DNS SRV(见 [RFC2782]、[RFC6186] 和 [RFC6764])。
- .well-known/servicename(见 [RFC8615])。
对于域 "example.com" 支持 JMAP 的主机应当发布一条 SRV 记录 "_jmap._tcp.example.com",给出主机名和端口(通常为端口 "443")。JMAP 会话资源随后即为 "https://${hostname}[:${port}]/.well-known/jmap"(遵循任何重定向)。
如果客户端拥有形式为电子邮件地址的用户名,它可以使用该用户名的域部分来尝试自动发现 JMAP 服务器。
3. 结构化数据交换
客户端可以向服务器发起 API 请求以获取或设置结构化数据。该请求由一组有序的方法调用组成。这些方法由服务器处理,随后服务器返回一组有序的响应。
3.1. 发起 API 请求
要发起 API 请求,客户端向 API 资源发起一个经过认证的 POST 请求,该资源由 Session 对象(见第 2 节)中的 "apiUrl" 属性定义。
该请求必须为 "application/json" 类型,并包含一个由第 3.3 节定义的、单个 JSON 编码的 "Request" 对象。如果成功,响应也必须为 "application/json" 类型,并包含由第 3.4 节定义的单个 "Response" 对象。
3.2. Invocation 数据类型
方法调用与响应由 *Invocation* 数据类型表示。这是一个元组,表示为一个包含三个元素的 JSON 数组:
- 一个方法名或响应名的 "String(字符串)" *name*。
- 一个包含该方法或响应的具名 *arguments(参数)* 的 "String[*]" 对象。
- 一个 *method call id(方法调用 id)*,类型为 "String":客户端提供的任意字符串,将随该方法调用发出的响应一同回显(一个方法可能返回 1 个或多个响应,因为它可能隐式调用其他方法;所有由该方法调用发起的响应在响应中都获得相同的方法调用 id)。
3.3. Request 对象
一个 *Request* 对象具有以下属性:
- using: "String[]"
- 客户端希望使用的能力集合。即使客户端发起的方法调用并未利用这些能力,它也可以包含能力标识符。服务器在 Session 对象(见第 2 节)中作为 "capabilities" 属性的键通告其支持的一组规范。
- methodCalls: "Invocation[]"
- 要在服务器上处理的一组方法调用。这些方法调用必须按顺序依次处理。
- createdIds: "Id[Id]"(可选)
- 从(客户端指定的)创建 id 到记录成功创建时服务器所分配 id 的映射。
- 如本规范后文所述,某些记录可能具有包含另一记录 id 的属性。为了实现更高效的网络利用,你可以将该属性设置为引用同一 API 请求中更早创建的记录。由于在创建请求时尚不知道真实的 id,客户端可以改为指定它分配的创建 id,并加上 "#" 前缀(详见第 5.3 节)。
- 当服务器处理 API 请求时,每当它成功创建一个新记录,就会将该创建 id 加入此映射(见第 5.3 节 /set 的 "create" 参数),值即为服务器分配的真实 id。如果在 create/update 中遇到对某个创建 id 的引用,它会在映射中查找该 id,并在找到时用真实 id 替换该引用。
- 客户端可以将该映射的初始值作为 Request 对象的 "createdIds" 属性传入。这可以是一个空对象。如果在请求中给出,响应也将包含一个 createdIds 属性。这使得代理服务器能够轻松地将一个 JMAP 请求拆分为多个 JMAP 请求,发送到不同的服务器。例如,它可以将前两个方法调用发送到服务器 A,然后将第三个发送到服务器 B,再将第四个发送回服务器 A。通过将前一个响应的 createdIds 传给下一个请求,它可以确保所有这些引用仍然能够解析。有关代理考量的进一步讨论见第 5.8 节。
未来的规范可能会向 Request 对象添加更多属性以扩展其语义。为确保前向兼容性,服务器必须忽略 JMAP Request 对象上任何它不理解的其他属性。
3.3.1. 请求示例
{
"using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
"methodCalls": [
[ "method1", {
"arg1": "arg1data",
"arg2": "arg2data"
}, "c1" ],
[ "method2", {
"arg1": "arg1data"
}, "c2" ],
[ "method3", {}, "c3" ]
]
}
3.4. Response 对象
一个 *Response* 对象具有以下属性:
- methodResponses: "Invocation[]"
- 一组响应,格式与 Request 对象上的 "methodCalls" 相同。方法的输出必须按照方法被处理的相同顺序添加到 "methodResponses" 数组中。
- createdIds: "Id[Id]"(可选;仅在请求中给出时才返回)
- 从(客户端指定的)创建 id 到记录成功创建时服务器所分配 id 的映射。它必须包含原始 Request 对象的 createdIds 参数中传入的所有创建 id,以及为 newly created records 添加的任何额外创建 id。
- sessionState: "String"
- Session 对象上 "state" 字符串的当前值,如第 2 节所述。客户端可以使用它来检测该对象是否已更改并需要重新获取。
除非另有说明,如果方法调用成功完成,其响应名与请求中的方法名相同。
3.4.1. 响应示例
{
"methodResponses": [
[ "method1", {
"arg1": 3,
"arg2": "foo"
}, "c1" ],
[ "method2", {
"isBlah": true
}, "c2" ],
[ "anotherResponseFromMethod2", {
"data": 10,
"yetmoredata": "Hello"
}, "c2"],
[ "error", {
"type":"unknownMethod"
}, "c3" ]
],
"sessionState": "75128aab4b1b"
}
3.5. 省略参数
方法的某个参数可能被指定为具有默认值。如果客户端省略了它,服务器必须像指定了默认值一样处理该方法调用。类似地,服务器可以省略响应中任何具有默认值的参数。
除非在方法描述中另有说明,在请求或响应中(且类型签名允许的情况下)null 是任何参数的默认值。其他参数只有在方法描述中定义了显式默认值时才能被省略。
3.6. 错误
在 JMAP 中,错误可以在三个不同粒度级别上返回。
当发起一个 API 请求时,整个请求可能由于速率限制、格式错误的 JSON、对未知能力的请求等原因被拒绝。在这种情况下,整个请求被拒绝,并返回适当的 HTTP 错误响应码,以及为客户端提供详情的额外 JSON 主体。
只要请求本身在语法上有效(JSON 有效,且解码后匹配 Request 对象的类型签名),其中的方法就会由服务器按顺序执行。每个方法都可能单独失败,例如,如果给出了无效参数,或调用了未知的方法名。
最后,那些改变服务器状态的方法通常在单次调用中对多个不同的记录进行操作。每个记录的变更都可能被单独以 SetError 拒绝,如第 5.3 节所述。
3.6.1. 请求级错误
当向客户端返回 HTTP 错误响应时,服务器应当返回一个 JSON "problem details(问题详情)"对象作为响应主体,如 [RFC7807] 所述。
定义了以下问题类型:
- "urn:ietf:params:jmap:error:unknownCapability":客户端在请求的 "using" 属性中包含了一个服务器不支持的能力。
- "urn:ietf:params:jmap:error:notJSON":请求的内容类型不是 "application/json",或请求未能解析为 I-JSON。
- "urn:ietf:params:jmap:error:notRequest":请求解析为 JSON,但与 Request 对象的类型签名不匹配。
- "urn:ietf:params:jmap:error:limit":请求未被处理,因为它会超出能力对象上定义的某个请求限制,例如 maxSizeRequest、maxCallsInRequest 或 maxConcurrentRequests。"problem details" 对象上还必须包含一个 "limit" 属性,包含所施加限制的名称。
3.6.1.1. 示例
{
"type": "urn:ietf:params:jmap:error:unknownCapability",
"status": 400,
"detail": "The Request object used capability
'https://example.com/apis/foobar', which is not supported
by this server."
}
另一个示例:
{
"type": "urn:ietf:params:jmap:error:limit",
"limit": "maxSizeRequest",
"status": 400,
"detail": "The request is larger than the server is willing to
process."
}
3.6.2. 方法级错误
如果一个方法遇到错误,必须在 "methodResponses" 数组中的当前位置插入相应的 "error" 响应,并且除非另有说明,该方法调用内不得进行进一步处理。
请求中的任何其他方法调用随后必须照常处理。方法级的错误不得产生 HTTP 级别的错误。
一个 "error" 响应如下所示:
[ "error", {
"type": "unknownMethod"
}, "call-id" ]
响应名为 "error",并且必须具有一个 type 属性。可能存在其他属性以提供进一步信息;这些在相应的错误类型描述中详述。
除非返回的是 "serverPartialFail" 错误,否则如果返回了方法级错误,服务器对外可见的状态不得发生更改。
定义了以下错误类型,它们可能在对任何方法调用的适当情况下返回:
- "serverUnavailable":某个内部服务器资源临时不可用。稍后(也许在带随机退避因子后)重试同一操作可能会成功。
- "serverFail":在处理该调用期间发生了意外或未知的错误。应当提供一个 "description" 属性,以更详细地说明错误。该方法调用未对服务器状态做出任何更改。再次尝试同一操作预计仍会失败。若问题持续存在,很可能需要联系服务管理员解决。
- "serverPartialFail":描述的预期变更中有一部分(而非全部)发生了。客户端必须重新同步受影响的数据以确定服务器状态。强烈不建议使用此错误。
- "unknownMethod":服务器无法识别该方法名。
- "invalidArguments":某个参数的类型错误或以其他方式无效,或者缺少必需的参数。可以提供一个 "description" 属性,以解释问题所在来帮助调试。这是一个未本地化的字符串,不打算直接展示给最终用户。
- "invalidResultReference":该方法为其某个参数使用了结果引用(见第 3.7 节),但解析失败。
- "forbidden":方法和参数有效,但执行该方法会违反访问控制列表(ACL)或其他权限策略。
- "accountNotFound":accountId 不与有效的账户相对应。
- "accountNotSupportedByMethod":给定的 accountId 对应于一个有效的账户,但该账户不支持此方法或数据类型。
- "accountReadOnly":此方法修改状态,但账户是只读的(如 JMAP 会话资源的相应 Account 对象所返回)。
特定方法的进一步可能错误在方法描述中给出。
未来的 RFC 中可能定义更多的一般性错误。如果客户端收到一个它不理解的错误类型,它必须将其与 "serverFail" 类型同等对待。
3.7. 对先前方法结果的引用
为使客户端更高效地利用网络并避免往返,一个方法的某个参数可以取自同一请求中先前方法调用的结果。
为此,客户端在该参数名前加上 "#"(octothorpe,井号)。其值为如下所述的 ResultReference 对象。在处理方法调用时,服务器必须首先检查 arguments 对象中是否有任何以 "#" 开头的名称。如果找到,应先解析该结果引用,并将该值用作"真实"参数。随后方法照常处理。如果任何结果引用解析失败,整个方法必须以 "invalidResultReference" 错误被拒绝。如果 arguments 对象同时包含同名参数的普通形式与引用形式(例如 "foo" 和 "#foo"),该方法必须返回 "invalidArguments" 错误。
一个 *ResultReference* 对象具有以下属性:
- resultOf: "String"
- 当前请求中先前某个方法调用的方法调用 id(见第 3.2 节)。
- name: "String"
- 该方法调用的某个响应的必需名称。
- path: "String"
- 指向通过 name 和 resultOf 属性选定的响应参数的指针。这是一个 JSON Pointer [RFC6901],但还允许使用 "*" 来映射遍历一个数组(见下文描述)。
解析步骤:
- 在 "methodResponses" 数组中,寻找方法调用 id 与 ResultReference 的 "resultOf" 属性相同的第一个响应(来自同一请求中此前已处理的方法调用)。如果没有,则求值失败。
- 如果响应名与 ResultReference 的 "name" 属性不一致,则求值失败。
- 对响应的参数对象(响应数组中的第二项)应用 "path",遵循 JSON Pointer 算法 [RFC6901],但有以下对"求值"(见第 4 节)的补充:
- 如果当前被引用的值是一个 JSON 数组,引用令牌可以恰好是单个字符 "*",使新的被引用值成为将该 JSON Pointer 剩余令牌应用于数组中每一项、并以相同顺序在一个新数组中返回结果的结果。如果将剩余指针令牌应用于每一项的结果是其本身也是一个数组,则该数组的内容会被加入输出,而非数组本身(即结果从一个数组的数组被展平为单个数组)。
作为一个简单示例,假设我们有以下 API 请求的 "methodCalls":
[[ "Foo/changes", {
"accountId": "A1",
"sinceState": "abcdef"
}, "t0" ],
[ "Foo/get", {
"accountId": "A1",
"#ids": {
"resultOf": "t0",
"name": "Foo/changes",
"path": "/created"
}
}, "t1" ]]
执行第一个方法调用后,"methodResponses" 数组为:
[[ "Foo/changes", {
"accountId": "A1",
"oldState": "abcdef",
"newState": "123456",
"hasMoreChanges": false,
"created": [ "f1", "f4" ],
"updated": [],
"destroyed": []
}, "t0" ]]
要执行 "Foo/get" 调用,我们遍历参数,发现有一个带 "#" 前缀的参数。要解析它,我们应用上述算法:
- 寻找方法调用 id 为 "t0" 的第一个响应。"Foo/changes" 响应满足该标准。
- 检查响应名是否与结果引用中的相同。相同,因此没问题。
- 将 "path" 作为 JSON Pointer 应用于参数对象。这只需选择 "created" 属性,因此求值结果为:[ "f1", "f4" ]。
JMAP 服务器现在继续处理 "Foo/get" 调用,就好像参数是:
{
"accountId": "A1",
"ids": [ "f1", "f4" ]
}
现在,一个使用 JMAP Mail 数据模型的更复杂示例:获取收件箱中前 10 个会话(按最新优先排序)中每封邮件的 "from"/"date"/"subject":
[[ "Email/query", {
"accountId": "A1",
"filter": { "inMailbox": "id_of_inbox" },
"sort": [{ "property": "receivedAt", "isAscending": false }],
"collapseThreads": true,
"position": 0,
"limit": 10,
"calculateTotal": true
}, "t0" ],
[ "Email/get", {
"accountId": "A1",
"#ids": {
"resultOf": "t0",
"name": "Email/query",
"path": "/ids"
},
"properties": [ "threadId" ]
}, "t1" ],
[ "Thread/get", {
"accountId": "A1",
"#ids": {
"resultOf": "t1",
"name": "Email/get",
"path": "/list/*/threadId"
}
}, "t2" ],
[ "Email/get", {
"accountId": "A1",
"#ids": {
"resultOf": "t2",
"name": "Thread/get",
"path": "/list/*/emailIds"
},
"properties": [ "from", "receivedAt", "subject" ]
}, "t3" ]]
执行前 3 个方法调用后,"methodResponses" 数组可能是:
[[ "Email/query", {
"accountId": "A1",
"queryState": "abcdefg",
"canCalculateChanges": true,
"position": 0,
"total": 101,
"ids": [ "msg1023", "msg223", "msg110", "msg93", "msg91",
"msg38", "msg36", "msg33", "msg11", "msg1" ]
}, "t0" ],
[ "Email/get", {
"accountId": "A1",
"state": "123456",
"list": [{
"id": "msg1023",
"threadId": "trd194"
}, {
"id": "msg223",
"threadId": "trd114"
},
...
],
"notFound": []
}, "t1" ],
[ "Thread/get", {
"accountId": "A1",
"state": "123456",
"list": [{
"id": "trd194",
"emailIds": [ "msg1020", "msg1021", "msg1023" ]
}, {
"id": "trd114",
"emailIds": [ "msg201", "msg223" ]
},
...
],
"notFound": []
}, "t2" ]]
要执行最后的 "Email/get" 调用,我们遍历参数,发现有一个带 "#" 前缀的参数。要解析它,我们应用该算法:
- 寻找方法调用 id 为 "t2" 的第一个响应。"Thread/get" 响应满足该标准。
- "Thread/get" 是结果引用中指定的名称,因此没问题。
- 将 "path" 作为 JSON Pointer 应用于参数对象。逐令牌:
- "list":获取会话对象数组。
- "*":对数组中的每一项:
- "emailIds":获取邮件 id 数组。
- 将这些连接成结果中所有 id 的单个数组。
JMAP 服务器现在继续处理 "Email/get" 调用,就好像参数是:
{
"accountId": "A1",
"ids": [ "msg1020", "msg1021", "msg1023", "msg201", "msg223", ... ],
"properties": [ "from", "receivedAt", "subject" ]
}
ResultReference 与创建 id 扮演类似的角色,因为它允许链式的方法调用引用在生成请求时尚不可用的信息。然而,它们是不同的东西,不可互换;唯一的共同点是都使用了用于标识它们的井号(octothorpe)。
3.8. 用户可见字符串的本地化
如果要返回展示给用户的自定义字符串(例如错误消息),服务器应当使用请求的 Accept-Language 头(如 [RFC7231] 第 5.3.5 节所定义)中的信息来选择最佳的可用本地化。响应的 Content-Language 头(见 [RFC7231] 第 3.1.3.2 节)应当指明用于用户可见字符串的语言。
例如,假设发起了带有以下头的请求:
Accept-Language: fr-CH, fr;q=0.9, de;q=0.8, en;q=0.7, *;q=0.5
某个方法生成了一个要展示给用户的错误。服务器拥有该错误消息的英文和德文译文。查看 Accept-Language 头,用户的首选语言是法语。由于我们没有法语译文,我们查看下一个最受偏好的语言,即德语。我们有德语译文,因此服务器返回该译文,并在 Content-Language 头中指明所选语言,如下所示:
Content-Language: de
3.9. 安全
一如既往,服务器必须严格对待从客户端接收的数据。需要检查参数的有效性;恶意用户可能试图通过 API 找到漏洞。在参数无效(数据未知/不足/类型错误等)的情况下,该方法必须返回 "invalidArguments" 错误并终止。
3.10. 并发
单个请求内的方法调用必须按顺序执行。然而,来自不同并发 API 请求的方法调用可能交错进行。这意味着在单个 API 请求内的两个方法调用之间,服务器上的数据可能发生变化。
4. Core/echo 方法
"Core/echo" 方法原样返回它所接收到的参数。它对于测试你是否拥有一个到 JMAP API 端点的有效、经过认证的连接有用。
4.1. 示例
请求:
[[ "Core/echo", {
"hello": true,
"high": 5
}, "b3ff" ]]
响应:
[[ "Core/echo", {
"hello": true,
"high": 5
}, "b3ff" ]]
5. 标准方法与命名约定
JMAP 为创建、检索、更新和删除特定类型的对象提供了统一的接口。对于 "Foo" 数据类型,该类型的记录将通过 "Foo/get" 调用获取,并通过 "Foo/set" 调用修改。增量更新可以通过 "Foo/changes" 调用获取。这些方法都遵循如下所述的标准格式。
某些类型可能没有所有这些方法。定义类型的规范必须指定该类型可用哪些方法。
5.1. /get
Foo 类型的对象通过调用 "Foo/get" 获取。
它接受以下参数:
- accountId: "Id"
- 要使用的账户 id。
- ids: "Id[]|null"
- 要返回的 Foo 对象的 id。如果为 null,则返回该数据类型的所有记录(前提是该数据类型支持此操作,且记录数量不超过 "maxObjectsInGet" 限制)。
- properties: "String[]|null"
- 如果提供,则仅为每个 Foo 对象返回数组中列出的属性。如果为 null,则返回该对象的所有属性。对象的 id 属性始终被返回,即使未显式请求。如果请求了无效属性,必须以 "invalidArguments" 错误拒绝该调用。
响应具有以下参数:
- accountId: "Id"
- 用于该调用的账户 id。
- state: "String"
- 一个(最好是短的)字符串,代表服务器上该账户中此类型所有数据的状态(而不仅仅是本次调用返回的对象)。如果数据发生变化,该字符串必须更改。如果 Foo 数据未变,服务器应在对该数据类型的后续请求上返回相同的 state 字符串。当客户端收到的响应带有与先前调用不同的 state 字符串时,它必须丢弃该类型的所有当前缓存对象,或调用 "Foo/changes" 来获取确切的变更。
- list: "Foo[]"
- 所请求的 Foo 对象数组。如果没有找到对象,或传入的 "ids" 参数也是空数组,则这是空数组。结果顺序可能与请求参数中 "ids" 的顺序不同。如果在请求中多次包含相同的 id,服务器必须只在 "list" 或响应的 "notFound" 参数之一中包含它一次。
- notFound: "Id[]"
- 该数组包含传给方法但对应记录不存在的 id。如果所有请求的 id 都找到了,或传入的 "ids" 参数为 null 或空数组,则该数组为空。
可能返回以下额外错误以代替 "Foo/get" 响应:
- "requestTooLarge":客户端请求的 id 数量超过了服务器愿意在单个方法调用中处理的最大数量。
5.2. /changes
当账户中一组 Foo 记录的状态在服务器上发生变化时(无论是由于创建、更新还是删除),"Foo/get" 响应的 "state" 属性会变化。"Foo/changes" 方法允许客户端高效地将其 Foo 缓存状态更新为与服务器上的新状态匹配。它接受以下参数:
- accountId: "Id"
- 要使用的账户 id。
- sinceState: "String"
- 客户端的当前状态。这是 "Foo/get" 响应中作为 "state" 参数返回的那个字符串。服务器将返回自该状态以来发生的变更。
- maxChanges: "UnsignedInt|null"
- 响应中返回的 id 的最大数量。服务器可以选择返回少于此值,但不得超过。如果客户端未给出,服务器可自行决定返回多少。如果由客户端给出,该值必须是大于 0 的正整数。如果给出此范围之外的值,服务器必须以 "invalidArguments" 错误拒绝该调用。
响应具有以下参数:
- accountId: "Id"
- 用于该调用的账户 id。
- oldState: "String"
- 被回显的 "sinceState" 参数;它是服务器返回变更所依据的状态。
- newState: "String"
- 客户端在将这组变更应用到旧状态后将处于的状态。
- hasMoreChanges: "Boolean"
- 如果为 true,客户端可以再次调用 "Foo/changes",传入返回的 "newState" 以获取进一步更新。如果为 false,"newState" 即为当前服务器状态。
- created: "Id[]"
- 自旧状态以来已创建的记录 id 数组。
- updated: "Id[]"
- 自旧状态以来已更新的记录 id 数组。
- destroyed: "Id[]"
- 自旧状态以来已被销毁的记录 id 数组。
如果一条记录自旧状态以来既被创建又被更新,服务器应当只将该 id 返回在 "created" 列表中,但也可以将其返回在 "updated" 列表中。
如果一条记录自旧状态以来既被更新又被销毁,服务器应当只将该 id 返回在 "destroyed" 列表中,但也可以将其返回在 "updated" 列表中。
如果一条记录自旧状态以来既被创建又被销毁,服务器应当从响应中完全移除该 id。不过,它可能只将其包含在 "destroyed" 列表中,或同时包含在 "destroyed" 和 "created" 列表中。
如果提供了 "maxChanges" 或由服务器自动设置,服务器必须确保跨 "created"、"updated" 和 "destroyed" 返回的 id 数量不超过此限制。如果客户端状态与当前服务器状态之间的变更数量超过此限制,服务器应当生成一个将客户端带到中间状态的更新,客户端可继续调用 "Foo/changes" 直到完全更新。如果它无法计算中间状态,则必须返回一个 "cannotCalculateChanges" 错误响应。
在生成中间状态时,服务器可以选择如何划分变更。对许多类型而言,先返回较新的变更能提供更好的用户体验,因为这更可能是用户最感兴趣的。随后客户端可以在用户查看较新数据的同时继续分页载入较旧的变更。例如,假设服务器经历了以下状态:
A -> B -> C -> D -> E
客户端请求自状态 "B" 起的变更。服务器可能首先获取状态 D 与 E 之间创建、更新或销毁的记录 id,并返回:
state: "B-D-E"
hasMoreChanges: true
客户端随后将请求自 "B-D-E" 起的变更,服务器可以返回状态 C 与 D 之间的变更,返回:
state: "B-C-E"
hasMoreChanges: true
最后,客户端将请求自 "B-C-E" 起的变更,服务器可以返回状态 B 与 C 之间的变更,返回:
state: "E"
hasMoreChanges: false
如果在此过程中服务器状态被修改(变为 "F"),服务器仍执行相同操作,但现在当返回到状态 "E" 的更新时,它会指示仍有更多变更需要客户端获取。
当对一条记录的多次变更被拆分到不同的中间状态时,服务器不得在某个将其视为已更新或已销毁的响应之后,又将该记录作为已创建返回;并且不得在某个将其视为已创建或已更新的响应之前,将该记录作为已销毁返回。服务器可能需要合并对某条记录的多次变更以满足此要求。
可能返回以下额外错误以代替 "Foo/changes" 响应:
- "cannotCalculateChanges":服务器无法根据客户端给出的状态字符串计算变更。通常这是因为客户端的状态太旧,或服务器在更新过多时无法生成到中间状态的更新。客户端必须使其 Foo 缓存失效。
维持状态以便计算 "Foo/changes" 对服务器而言可能代价高昂,但总是返回 "cannotCalculateChanges" 会严重增加客户端的网络流量和资源使用。为了实现高效同步,服务器应当能够从过去 30 天内提供给客户端的任何状态字符串计算变更(当然也可以支持从更旧的状态计算更新)。
5.3. /set
修改服务器上 Foo 对象的状态通过 "Foo/set" 方法完成。它涵盖创建、更新和销毁 Foo 记录。这使得服务器能够理清一次性执行多个操作(例如,确保某种记录类型始终具有最小数量)时可能存在的顺序与依赖关系。
"Foo/set" 方法接受以下参数:
- accountId: "Id"
- 要使用的账户 id。
- ifInState: "String|null"
- 这是由 "Foo/get" 方法返回的状态字符串(代表该账户中此类型所有对象的状态)。如果提供,该字符串必须与当前状态匹配;否则,该方法将被中止并返回 "stateMismatch" 错误。如果为 null,任何变更都将应用到当前状态。
- create: "Id[Foo]|null"
- 从创建 id(客户端设置的临时 id)到 Foo 对象的映射,如果没有要创建的对象则为 null。
- Foo 对象类型定义可能为属性定义默认值。客户端可以省略任何此类属性。
- 客户端必须省略任何只能由服务器设置的属性(例如大多数对象类型上的 "id" 属性)。
- update: "Id[PatchObject]|null"
- 从 id 到要应用于具有该 id 的当前 Foo 对象的 Patch 对象的映射,如果没有要更新的对象则为 null。
- 一个 *PatchObject* 的类型为 "String[*]",表示一组无序的 patch。键是 JSON Pointer 格式 [RFC6901] 的路径,带有隐式的前导 "/"(即在应用 JSON Pointer 求值算法前,为每个键加上 "/")。
- 所有路径还必须符合以下限制;如果有任何违反,必须以 "invalidPatch" 错误拒绝该更新:
- 指针不得引用数组内部(即,你不得插入/删除数组元素;数组必须整体替换)。
- 除最后一个之外的所有部分(即最后一个斜杠之后的部分)必须已存在于被 patch 的对象上。
- 不得存在 PatchObject 中两个 patch 的指针一个是另一个前缀的情况,例如 "alerts/1/offset" 和 "alerts"。
- 与每个指针相关联的值决定了如何应用该 patch:
- 如果为 null,则设置为该属性指定的默认值(如果已指定);否则,从该被 patch 的对象中移除该属性。如果该键在父对象中不存在,则这是一个空操作(no-op)。
- 其他任何值:为该属性设置的值(这可以是替换或添加到被 patch 的对象)。
- 任何服务器设置的属性如果其值与被 patch 对象应用 patch 之前的当前服务器值相同,则可以包含在 patch 中。否则,必须以 "invalidProperties" SetError 拒绝该更新。
- 该 patch 定义的目的是使整个 Foo 对象也是一个有效的 PatchObject。客户端可以选择仅发送差异来优化网络利用,也可以发送整个对象;服务器以相同方式处理两者。
- destroy: "Id[]|null"
- 要永久删除的 Foo 对象 id 列表,如果没有要销毁的对象则为 null。
每个对象的创建、修改或销毁都被视为一个原子单元。允许服务器将某些对象的变更提交而其他对象不提交;但是,它不得将单个记录更新的一部分(例如,如果更新对象中同时提供了 "name" 和 "count" 属性,却只更新了 "name" 而未更新 "count")仅提交一部分。
"Foo/set" 完成后的最终状态必须是有效的;不过,服务器在处理单个 create/update/destroy 请求时,可能必须经历无效的(不对客户端暴露的)中间状态。例如,假设有一个必须唯一的 "name" 属性。单个方法调用可以将对象 A 重命名为 B,同时将两个对象 B 重命名为 A。如果最终状态有效,则允许这样做。否则,每个对象的创建、修改或销毁应当按顺序处理,并根据当前服务器状态接受/拒绝。
如果某个创建、更新或销毁被拒绝,必须将相应的错误添加到响应的 notCreated/notUpdated/notDestroyed 属性中,并且服务器必须继续下一个 create/update/destroy。它不会终止该方法。
如果找不到给定的 id,必须以 "notFound" set 错误拒绝该更新或销毁。
如果某个对象在同一 /set 请求中被销毁,服务器可以跳过该更新(以 "willDestroy" SetError 拒绝它)。
某些记录可能持有对其他记录的引用(外键)。该引用可以在同一请求中与被引用记录的创建同时设置(通过 create 或 update)。为此,客户端使用其创建 id(加上 "#" 前缀)来引用新记录。客户端在请求中方法调用的顺序必须使得被引用的记录在相同或更早的调用中被创建。因此,服务器永远无需向前查看。相反,在处理请求时,服务器必须在请求的持续期间为每种新创建的记录维护一个从创建 id 到记录 id 的简单映射,以便在后续方法调用中必要时能够替换进正确的值。对于引用同类型的记录,服务器必须在单个方法调用内对 create 和 update 排序,使得 create 在其创建 id 被同一调用中的另一个 create/update/destroy 引用之前发生。
创建 id 不是按类型划分作用域的,而是所有类型的单一映射。客户端不应在同一 API 请求中的任何地方复用创建 id。如果复用了创建 id,服务器必须将该创建 id 映射到最近用该 id 创建的项。为便于代理 API 请求,可以将一组初始的创建 id 到真实 id 的值随请求传入(见第 3.3 节"Request 对象"),并将映射的最终状态随响应传出(见第 3.4 节"Response 对象")。
响应具有以下参数:
- accountId: "Id"
- 用于该调用的账户 id。
- oldState: "String|null"
- 在做出请求的变更之前,"Foo/get" 本应返回的状态字符串;如果服务器不知道先前的状态字符串是什么,则为 null。
- newState: "String"
- "Foo/get" 现在将返回的状态字符串。
- created: "Id[Foo]|null"
- 从创建 id 到对象的映射,该对象包含客户端未发送的、已创建的 Foo 对象的任何属性。这包括所有服务器设置的属性(例如大多数对象类型中的 "id"),以及被客户端省略并因此由服务器设置为默认值的任何属性。
- 如果没有成功创建 Foo 对象,该参数为 null。
- updated: "Id[Foo|null]|null"
- 该映射中的键是成功更新的所有 Foo 的 id。
- 每个 id 的值是一个 Foo 对象,包含以*非*由发给服务器的 PatchObject 显式请求的方式更改的任何属性,如果没有则为 null。这使客户端得知对服务器设置或计算属性的任何更改。
- 如果没有成功更新 Foo 对象,该参数为 null。
- destroyed: "Id[]|null"
- 成功销毁的 Foo id 列表,如果没有则为 null。
- notCreated: "Id[SetError]|null"
- 从创建 id 到 SetError 对象的映射,对应每个创建失败的对象,如果全部成功则为 null。
- notUpdated: "Id[SetError]|null"
- 从 Foo id 到 SetError 对象的映射,对应每个更新失败的对象,如果全部成功则为 null。
- notDestroyed: "Id[SetError]|null"
- 从 Foo id 到 SetError 对象的映射,对应每个销毁失败的对象,如果全部成功则为 null。
一个 *SetError* 对象具有以下属性:
- type: "String"
- 错误的类型。
- description: "String|null"
- 有助于调试的错误描述,包含对问题所在的解释。这是一个未本地化的字符串,不打算直接展示给最终用户。
定义了以下 SetError 类型,并可能在适当情况下针对任何记录类型的 set 操作返回:
- "forbidden":(create;update;destroy)操作会违反 ACL 或其他权限策略。
- "overQuota":(create;update)创建会超出服务器定义的、该类型对象数量或总大小的限制。
- "tooLarge":(create;update)创建/更新会导致一个超出服务器定义的、该类型单个对象最大大小限制的对象。
- "rateLimit":(create)最近创建了过多该类型的对象,达到了服务器定义的速率限制。稍后重试可能成功。
- "notFound":(update;destroy)用于更新/销毁的给定 id 找不到。
- "invalidPatch":(update)用于更新记录的 PatchObject 不是有效的 patch(见 patch 描述)。
- "willDestroy":(update)客户端请求在同一 /set 请求中既更新又销毁某个对象,服务器因此决定忽略该更新。
- "invalidProperties":(create;update)给定的记录在某种方式上无效。例如:
- 它包含根据该记录类型的类型规范而言无效的属性。
- 它包含一个只能由服务器设置的属性(例如 "id"),且与当前值不同。注意,为允许客户端回传整个对象,只要该值与服务器上的当前值相同,在更新中包含服务器设置的属性并不算错误。
- 存在对另一记录(外键)的引用,且给定的 id 不与有效的记录相对应。
SetError 对象还应当具有一个名为 "properties" 的 "String[]" 类型属性,列出所有无效的属性。
各个方法可能为否则会导致 invalidProperties 错误的某些条件指定更具体的错误。如果满足了其中某一条件的情形,则必须返回该错误而非 invalidProperties 错误。
- "singleton":(create;destroy)这是一个单例类型,因此你不能创建另一个,也不能销毁现有的这一个。
其他可能的 SetError 类型可能在特定的方法描述中给出。SetError 对象上也可能存在其他属性,如相关方法中所述。
可能返回以下额外错误以代替 "Foo/set" 响应:
- "requestTooLarge":要创建、更新或销毁的对象总数超过了服务器愿意在单个方法调用中处理的最大数量。
- "stateMismatch":提供了 "ifInState" 参数,但它与当前状态不匹配。
5.4. /copy
在两个不同账户之间移动 Foo 记录的唯一方式,是使用 "Foo/copy" 方法复制它们;复制成功后,再删除原始记录。"onSuccessDestroyOriginal" 参数允许你尝试在单个方法调用中完成此操作;但请注意,这两个不同的动作不是原子的,因此有可能复制成功但由于某种原因原始记录未被销毁。
复制在概念上分为三个阶段:
- 从 "from" 账户读取当前值。
- 将新副本写入另一个账户。
- 如果请求,销毁 "from" 账户中的原始记录。
由于并发请求,阶段之间数据可能发生变化。
"Foo/copy" 方法接受以下参数:
- fromAccountId: "Id"
- 要从中复制记录的账户 id。
- ifFromInState: "String|null"
- 这是由 "Foo/get" 方法返回的状态字符串。如果提供,在读取要复制的数据时,该字符串必须与 fromAccountId 所引用账户的当前状态匹配;否则,该方法将被中止并返回 "stateMismatch" 错误。如果为 null,数据将从当前状态读取。
- accountId: "Id"
- 要复制记录到的账户 id。它必须与 "fromAccountId" 不同。
- ifInState: "String|null"
- 这是由 "Foo/get" 方法返回的状态字符串。如果提供,该字符串必须与 accountId 所引用账户的当前状态匹配;否则,该方法将被中止并返回 "stateMismatch" 错误。如果为 null,任何变更都将应用到当前状态。
- create: "Id[Foo]"
- 从创建 id 到 Foo 对象的映射。Foo 对象必须包含一个 "id" 属性,即要复制的记录(在 fromAccount 中)的 id。创建副本时,包含的任何其他属性将用于替代原始记录上该属性的当前值。
- onSuccessDestroyOriginal: "Boolean"(默认:false)
- 如果为 true,将尝试销毁成功复制的原始记录:在发出 "Foo/copy" 响应之后、但在处理下一个方法之前,服务器必须发起单次 "Foo/set" 调用,销毁每个成功复制的记录的原记录;该调用的输出像平常一样被加入响应,返回给客户端。
- destroyFromIfInState: "String|null"
- 如果在本请求末尾发起隐式 "Foo/set" 调用以销毁成功复制的原记录,该参数作为该调用的 "ifInState" 参数传入。
每个记录副本被视为一个原子单元,可单独成功或失败。
响应具有以下参数:
- fromAccountId: "Id"
- 记录所复制自的账户 id。
- accountId: "Id"
- 记录所复制到的账户 id。
- oldState: "String|null"
- 在做出请求的变更之前,"Foo/get" 在本账户(记录复制到的账户)上本应返回的状态字符串;如果服务器不知道先前的状态字符串是什么,则为 null。
- newState: "String"
- "Foo/get" 现在在记录复制到的账户上本应返回的状态字符串。
- created: "Id[Foo]|null"
- 从创建 id 到对象的映射,该对象包含被复制的 Foo 对象中由服务器设置的任何属性(例如大多数对象类型中的 "id";注意,该 id 可能与被复制对象所在账户中的 id 不同)。
- 如果没有成功复制 Foo 对象,该参数为 null。
- notCreated: "Id[SetError]|null"
- 从创建 id 到 SetError 对象的映射,对应每个复制失败的对象,如果没有则为 null。
SetError 可以是为 create 或 update 返回的任何标准 set 错误。此外,定义了以下 SetError:
- "alreadyExists":服务器禁止重复,且记录已存在于目标账户中。SetError 对象上必须包含一个类型为 "Id" 的 "existingId" 属性,带有现有记录的 id。
可能返回以下额外错误以代替 "Foo/copy" 响应:
- "fromAccountNotFound":"fromAccountId" 不与有效的账户相对应。
- "fromAccountNotSupportedByMethod":给定的 "fromAccountId" 对应于一个有效的账户,但该账户不支持该数据类型。
- "stateMismatch":提供了 "ifInState" 参数且其不与当前状态匹配,或提供了 "ifFromInState" 参数且其不与 from 账户中的当前状态匹配。
5.5. /query
对于预期数据总量非常小的数据集,客户端可以只获取完整数据集,然后在本地进行任何排序/过滤。然而,对于大型数据集(例如数 GB 的邮箱),客户端需要能够在服务器上对数据类型进行搜索/排序/窗口分页。
对账户中一组 Foo 的查询通过调用 "Foo/query" 完成。它接受若干参数以确定包含哪些记录、如何排序,以及返回结果的哪一部分(完整列表可能*非常*长)。结果以 Foo id 列表的形式返回。
对 "Foo/query" 的调用接受以下参数:
- accountId: "Id"
- 要使用的账户 id。
- filter: "FilterOperator|FilterCondition|null"
- 确定结果中包含的 Foo 集合。如果为 null,账户中该类型的所有对象都包含在结果中。
- 一个 *FilterOperator* 对象具有以下属性:
- operator: "String"
- 必须是以下字符串之一:
- "AND":所有条件都必须匹配,过滤器才算匹配。
- "OR":至少其中一个条件必须匹配,过滤器才算匹配。
- "NOT":所有条件都不能匹配,过滤器才算匹配。
- 必须是以下字符串之一:
- conditions: "(FilterOperator|FilterCondition)[]"
- 要对每条记录求值的各条件。
- operator: "String"
- 一个 *FilterCondition* 是一个"object(对象)",其允许的属性与语义取决于数据类型,并在该类型的 /query 方法规范中定义。它不得具有 "operator" 属性。
- sort: "Comparator[]|null"
- 列出要在两条 Foo 记录之间比较的属性名,以及如何比较,以确定排序中谁在前。如果两条 Foo 记录对第一个比较器的值相同,则考虑下一个比较器,依此类推。如果所有比较器都相同(这包括给出了空数组或 null 作为 "sort" 参数的情况),排序顺序是服务器相关的,但在对 "Foo/query" 的调用之间必须稳定。一个 *Comparator* 具有以下属性:
- property: "String"
- 要比较的 Foo 对象上的属性名。
- isAscending: "Boolean"(可选;默认:true)
- 如果为 true,按升序排序。如果为 false,反转比较器结果以按降序排序。
- collation: "String"(可选;默认是服务器相关的)
- 在 [RFC4790] 定义的 collation 注册表中注册的算法标识符,用于比较字符串顺序时。服务器支持的算法在随 Session 对象返回的 capabilities 对象中通告(见第 2 节)。
- 如果省略,默认算法是服务器相关的,但:
- 它必须能识别 Unicode。
- 它可以基于请求中的 Accept-Language 头(如 [RFC7231] 第 5.3.5 节所定义)或关于用户语言/区域设置的带外信息来选择。
- 在概念上对语言/区域设置适用的地方,它应当是大小写不敏感的。在用户语言未知时,建议遵循 [RFC8264] 第 5.2.3 节的指导。
- "i;unicode-casemap" collation [RFC5051] 和 Unicode Collation Algorithm(<http://www.unicode.org/reports/tr10/>)是两个满足这些标准、并为大量语言提供合理行为的例子。
- 当被比较的属性不是字符串时,"collation" 属性被忽略,并基于类型应用以下比较规则(升序):
- "Boolean":false 在 true 之前。
- "Number":较小的数在较大的数之前。
- "Date"/"UTCDate":较早的日期在前。
- property: "String"
- Comparator 对象还可能具有类型 /query 方法中为特定排序操作所需的额外属性。
- 列出要在两条 Foo 记录之间比较的属性名,以及如何比较,以确定排序中谁在前。如果两条 Foo 记录对第一个比较器的值相同,则考虑下一个比较器,依此类推。如果所有比较器都相同(这包括给出了空数组或 null 作为 "sort" 参数的情况),排序顺序是服务器相关的,但在对 "Foo/query" 的调用之间必须稳定。一个 *Comparator* 具有以下属性:
- position: "Int"(默认:0)
- 要返回的完整结果列表中、第一个 id 的从零开始的索引。
- 如果给定负值,它是相对于列表末尾的偏移量。具体来说,必须将负值加到给定过滤器的结果总数上,如果仍然为负,则钳制为 "0"。这就是要返回的第一个 id 的从零开始的索引。
- 如果索引大于或等于结果列表中的对象总数,那么响应中的 "ids" 数组将为空,但这不是一个错误。
- anchor: "Id|null"
- 一个 Foo id。如果提供,"position" 参数被忽略。该 id 在结果中的索引将与 "anchorOffset" 参数结合使用,以确定要返回的第一个结果的索引(详见下文)。
- anchorOffset: "Int"(默认:0)
- 相对于锚点索引、要返回的第一个结果的索引,如果给定了锚点。它可以为负。例如,"-1" 表示锚点紧前面的 Foo 是返回列表中的第一个结果(详见下文)。
- limit: "UnsignedInt|null"
- 要返回的最大结果数。如果为 null,假定无限制。服务器可以选择强制实施最大 "limit" 参数。在这种情况下,如果给定更大的值(或为 null),该限制被钳制为最大值;新的限制随响应返回,以便客户端知晓。如果给定负值,必须以 "invalidArguments" 错误拒绝该调用。
- calculateTotal: "Boolean"(默认:false)
- 客户端是否希望知道查询中的结果总数。对于服务器而言,计算这可能很慢且代价高昂,特别是在复杂过滤器下,因此客户端应谨慎,只在需要时才请求总数。
如果给定了 "anchor" 参数,在过滤和排序后会在结果中查找该锚点。如果找到,则将 "anchorOffset" 加到其索引上。如果得到的索引现在为负,则钳制为 0。该索引现在被完全当作如同作为 "position" 参数提供一样使用。如果找不到锚点,则以 "anchorNotFound" 错误拒绝该调用。
如果指定了 "anchor",客户端提供的任何 position 参数必须被忽略。如果未提供 "anchor",任何 "anchorOffset" 参数必须被忽略。
客户端可以使用 "anchor" 而非 "position" 来在大型结果集中查找某个 id 的索引。
响应具有以下参数:
- accountId: "Id"
- 用于该调用的账户 id。
- queryState: "String"
- 一个编码服务器上查询当前状态的字符串。如果查询结果(即匹配的 id 及其排序顺序)发生变化,该字符串必须改变。如果服务器上的某些内容发生变化,queryState 字符串可能改变,这意味着结果可能已变但服务器无法确定。
- queryState 字符串只代表匹配特定查询(包括其排序/过滤)的有序 id 列表。如果匹配查询的对象上的某个属性发生变化但查询结果未受影响,它无需改变(实际上,在这种情况下 queryState 字符串不改变会更高效)。queryState 字符串只有在与未来对同一类型/排序/过滤的查询的响应比较时,或与 /queryChanges 一起使用以获取变更时才有意义。
- 如果客户端收到与先前调用不同的 queryState 字符串的响应,它必须丢弃当前缓存的查询并重新获取(注意,这不需要重新获取记录,只需重新获取 id 列表),或调用 "Foo/queryChanges" 获取差异。
- canCalculateChanges: "Boolean"
- 如果服务器支持用这些 "filter"/"sort" 参数调用 "Foo/queryChanges",则为 true。注意,这并不保证 "Foo/queryChanges" 调用会成功,因为由于服务器内部实现细节,它可能只在有限时间内可行。
- position: "UnsignedInt"
- "ids" 数组中第一个结果在完整查询结果列表中的从零开始的索引。
- ids: "Id[]"
- 查询结果的每个 Foo 的 id 列表,从本响应的 "position" 参数给定的索引开始,一直持续到结果末尾或达到 "limit" 个 id。如果 "position" >= "total",则这必须是空列表。
- total: "UnsignedInt"(仅在请求时)
- 结果中(给定 "filter" 的)Foo 总数。如果 "calculateTotal" 请求参数不为 true,必须省略该参数。
- limit: "UnsignedInt"(如果由服务器设置)
- 服务器对要返回的最大结果数施加的限制。仅当服务器设置了限制,或使用了与请求中不同的限制时才返回。
可能返回以下额外错误以代替 "Foo/query" 响应:
- "anchorNotFound":提供了 anchor 参数,但在查询结果中找不到它。
- "unsupportedSort":"sort" 在语法上有效,但它包含服务器不支持排序的属性,或它不认识的 collation 方法。
- "unsupportedFilter":"filter" 在语法上有效,但服务器无法处理。如果过滤器是用户搜索输入的产物,客户端应建议用户简化其搜索。
5.6. /queryChanges
"Foo/queryChanges" 方法允许客户端高效地将其缓存的查询状态更新为与服务器上的新状态匹配。它接受以下参数:
- accountId: "Id"
- 要使用的账户 id。
- filter: "FilterOperator|FilterCondition|null"
- 与 "Foo/query" 一起使用的 filter 参数。
- sort: "Comparator[]|null"
- 与 "Foo/query" 一起使用的 sort 参数。
- sinceQueryState: "String"
- 客户端中的查询当前状态。这是具有相同排序/过滤的 "Foo/query" 响应中作为 "queryState" 参数返回的字符串。服务器将返回自该状态以来对查询做出的变更。
- maxChanges: "UnsignedInt|null"
- 响应中返回的最大变更数。详见下文错误描述。
- upToId: "Id|null"
- 客户端当前从查询结果中缓存的最后一个(索引最高的)id。当存在大量结果时,在常见情况下,客户端可能只下载并缓存了结果开头的一小部分。如果排序和过滤都仅基于不可变属性,这允许服务器忽略结果中该点之后的变更,从而显著提高效率。如果它们不是不可变的,该参数被忽略。
- calculateTotal: "Boolean"(默认:false)
- 客户端是否希望知道查询中现在的结果总数。对于服务器而言,计算这可能很慢且代价高昂,特别是在复杂过滤器下,因此客户端应谨慎,只在需要时才请求总数。
响应具有以下参数:
- accountId: "Id"
- 用于该调用的账户 id。
- oldQueryState: "String"
- 被回显的 "sinceQueryState" 参数;即服务器返回变更所依据的状态。
- newQueryState: "String"
- 在将这组变更应用到旧状态后,查询将处于的状态。
- total: "UnsignedInt"(仅在请求时)
- 结果中(给定 "filter" 的)Foo 总数。如果 "calculateTotal" 请求参数不为 true,必须省略该参数。
- removed: "Id[]"
- 在旧状态中位于查询结果中、但在新状态中不在结果中的每个 Foo 的 "id"。
- 如果服务器无法精确计算,它可以额外返回可能曾在旧结果中但不在新结果中的 Foo 的 id。
- 如果排序和过滤都仅基于不可变属性,且提供了 "upToId" 并且它存在于结果中,任何被移除但索引高于 "upToId" 的 id 应当被省略。
- 如果 "filter" 或 "sort" 包含可变属性,服务器必须包含当前结果中所有该属性可能已变化的 Foo。它们在结果中的位置可能已移动,因此客户端必须重新插入它们,以确保其查询缓存正确。
- added: "AddedItem[]"
- 自旧状态以来已添加到结果中的每个 Foo 的 id 及其在查询结果(新状态)中的索引,以及当前结果中包含在 "removed" 数组中(由于基于可变属性的过滤器或排序)的每个 Foo。
- 如果排序和过滤都仅基于不可变属性,且提供了 "upToId" 并且它存在于结果中,任何被添加但索引高于 "upToId" 的 id 应当被省略。
- 该数组必须按索引排序,最低索引在前。
- 一个 *AddedItem* 对象具有以下属性:
- id: "Id"
- index: "UnsignedInt"
其结果是:如果客户端有一个对应于旧状态结果的、稀疏的 Foo id 缓存数组,那么:
fooIds = [ "id1", "id2", null, null, "id3", "id4", null, null, null ]
如果它剪除掉其缓存结果中所有在 removed 数组中的 id,那么:
removed = [ "id2", "id31", ... ];
fooIds => [ "id1", null, null, "id3", "id4", null, null, null ]
并插入(按索引顺序逐个,从最低索引开始)added 数组中的所有 id:
added = [{ id: "id5", index: 0, ... }];
fooIds => [ "id5", "id1", null, null, "id3", "id4", null, null, null ]
并截断或扩展到新的总长度,结果现在就处于新状态。
注意:插入(splicing in)是在给定索引处添加该项,使该索引或更高索引处所有项的索引加一。剪除(splicing out)是其逆操作,移除该项并使数组中其后每一项的索引减一。
可能返回以下额外错误以代替 "Foo/queryChanges" 响应:
- "tooManyChanges":变更数量超过了客户端的 "maxChanges" 参数。removed 或 added 数组中的每一项都算作一个变更。客户端可以用更大的 maxChanges 重试,或使其查询结果的缓存失效。
- "cannotCalculateChanges":服务器无法根据客户端给出的 queryState 字符串计算变更,通常是因为客户端的状态太旧。客户端必须使其查询结果的缓存失效。
5.7. 示例
假设我们有一个 *Todo* 类型,具有以下属性:
- id: "Id"(immutable;server-set)
- 对象的 id。
- title: "String"
- 关于要做什么的简要摘要。
- keywords: "String[Boolean]"(默认:{})
- 适用于该 Todo 的一组关键字。该集合表示为一个对象,键为"关键字"。对象中每个键的值必须为 true。(这种格式允许你使用 patch 语法更新单个键,而无需像 "String[]" 表示那样必须更新整个关键字集合。)
- neuralNetworkTimeEstimation: "Number"(server-set)
- title 和 keywords 被输入服务器的先进神经网络,以估算该 Todo 将花费多少秒。
- subTodoIds: "Id[]|null"
- 作为该 Todo 一部分要完成的其他 Todo 的 id 列表。
还假设该类型的所有标准方法都已定义,且 FilterCondition 对象支持一个 "hasKeyword" 属性来匹配具有给定关键字的 Todo。
客户端可能希望显示一个带有 "music" 关键字或 "video" 关键字的 Todo 列表,因此它发起以下方法调用:
[[ "Todo/query", {
"accountId": "x",
"filter": {
"operator": "OR",
"conditions": [
{ "hasKeyword": "music" },
{ "hasKeyword": "video" }
]
},
"sort": [{ "property": "title" }],
"position": 0,
"limit": 10
}, "0" ],
[ "Todo/get", {
"accountId": "x",
"#ids": {
"resultOf": "0",
"name": "Todo/query",
"path": "/ids"
}
}, "1" ]]
这会向服务器查询带有 "music" 或 "video" 关键字的 Todo 集合,按 title 排序,并限制为前 10 个结果。它使用反向引用(back-references)来引用查询结果,获取每个 Todo 的完整对象。响应可能类似如下:
[[ "Todo/query", {
"accountId": "x",
"queryState": "y13213",
"canCalculateChanges": true,
"position": 0,
"ids": [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j" ]
}, "0" ],
[ "Todo/get", {
"accountId": "x",
"state": "10324",
"list": [{
"id": "a",
"title": "Practise Piano",
"keywords": {
"music": true,
"beethoven": true,
"mozart": true,
"liszt": true,
"rachmaninov": true
},
"neuralNetworkTimeEstimation": 3600
}, {
"id": "b",
"title": "Watch Daft Punk music video",
"keywords": {
"music": true,
"video": true,
"trance": true
},
"neuralNetworkTimeEstimation": 18000
},
...
]
}, "1" ]]
现在,假设用户为 "Practise Piano" 任务添加关键字 "chopin" 并移除关键字 "mozart"。客户端可以将整个对象发送给服务器,因为这是一个有效的 PatchObject:
[[ "Todo/set", {
"accountId": "x",
"ifInState": "10324",
"update": {
"a": {
"id": "a",
"title": "Practise Piano",
"keywords": {
"music": true,
"beethoven": true,
"chopin": true,
"liszt": true,
"rachmaninov": true
},
"neuralNetworkTimeEstimation": 360
}
}
}, "0" ]]
或者它可以发送一个最小的 patch:
[[ "Todo/set", {
"accountId": "x",
"ifInState": "10324",
"update": {
"a": {
"keywords/chopin": true,
"keywords/mozart": null
}
}
}, "0" ]]
在任一种情况下,服务器上的效果完全相同,并且假设服务器仍处于状态 "10324",它很可能返回成功:
[[ "Todo/set", {
"accountId": "x",
"oldState": "10324",
"newState": "10329",
"updated": {
"a": {
"neuralNetworkTimeEstimation": 5400
}
}
}, "0" ]]
服务器在此变更过程中更改了对象上的 "neuralNetworkTimeEstimation" 属性;由于该更改的方式*不是*由发给服务器的 PatchObject 显式请求的,因此它随 "updated" 确认一同返回。
现在让我们向新的 "Practise Piano" Todo 添加一个子 Todo。在此示例中,我们可以看到对创建 id 的引用的使用,使我们能够设置对同一请求中创建的记录的外键引用:
[[ "Todo/set", {
"accountId": "x",
"create": {
"k15": {
"title": "Warm up with scales"
}
},
"update": {
"a": {
"subTodoIds": [ "#k15" ]
}
}
}, "0" ]]
现在,假设另一位用户删除了 "Listen to Daft Punk" Todo。第一位用户将收到一个推送通知(见第 7 节),带有 Todo 类型已更改的状态字符串。由于新字符串与其当前状态不匹配,它知道自己需要检查更新。它可能发起类似如下的请求:
[[ "Todo/changes", {
"accountId": "x",
"sinceState": "10324",
"maxChanges": 50
}, "0" ],
[ "Todo/queryChanges", {
"accountId": "x",
"filter": {
"operator": "OR",
"conditions": [
{ "hasKeyword": "music" },
{ "hasKeyword": "video" }
]
},
"sort": [{ "property": "title" }],
"sinceQueryState": "y13213",
"maxChanges": 50
}, "1" ]]
并收到如下响应:
[[ "Todo/changes", {
"accountId": "x",
"oldState": "10324",
"newState": "871903",
"hasMoreChanges": false,
"created": [],
"updated": [],
"destroyed": ["b"]
}, "0" ],
[ "Todo/queryChanges", {
"accountId": "x",
"oldQueryState": "y13213",
"newQueryState": "y13218",
"removed": ["b"],
"added": null
}, "1" ]]
假设用户可以访问另一个账户 "y",例如多个用户之间共享的团队账户。要将现有 Todo 从账户 "x" 移出,客户端将调用:
[[ "Todo/copy", {
"fromAccountId": "x",
"accountId": "y",
"create": {
"k5122": {
"id": "a"
}
},
"onSuccessDestroyOriginal": true
}, "0" ]]
服务器成功将 Todo 复制到新账户(在那里它获得一个新 id)并删除原记录。由于隐式调用了 "Todo/set",对单个方法调用有两个响应,两者具有相同的方法调用 id:
[[ "Todo/copy", {
"fromAccountId": "x",
"accountId": "y",
"created": {
"k5122": {
"id": "DAf97"
}
},
"oldState": "c1d64ecb038c",
"newState": "33844835152b"
}, "0" ],
[ "Todo/set", {
"accountId": "x",
"oldState": "871903",
"newState": "871909",
"destroyed": [ "a" ],
...
}, "0" ]]
5.8. 代理考量
JMAP 的设计使 API 端点能够轻松地代理到一个或多个 JMAP 服务器。这对于负载均衡、扩充能力,或向托管在不同 JMAP 服务器上的账户呈现单一端点(基于每个方法的 "accountId" 参数拆分请求)可能很有用。代理只需理解 JMAP Request 对象的一般结构;它无需知道它将传递给其他服务器的任何方法或参数的具体细节。
如果将请求中的方法拆分以在不同的后端服务器上调用,代理必须做两件事,以确保反向引用和创建 id 引用以与在整个请求在单一服务器上处理时相同的方式解析:
- 它必须随每个子请求传入一个 "createdIds" 属性。如果客户端未提供,则第一个子请求应使用空对象。每个子响应的 "createdIds" 属性应传入下一个子请求。
- 它必须解析在不同服务器上处理的、对先前方法结果的反向引用。这是一个相对简单的语法替换,在第 3.7 节中描述。
当基于 accountId 拆分请求时,代理实现者确实需要意识到在账户之间复制的 "/copy" 方法。如果这些账户位于不同服务器上,代理将不得不直接实现此功能。
6. 二进制数据
在 JMAP 中,二进制数据由 *blobId* 引用,并独立于核心 API 上传/下载。blobId 仅代表数据的原始字节,而非任何关联的元数据,例如文件名或内容类型。此类元数据与引用它的对象中的 blobId 一同存储。blobId 所代表的数据是不可变的。
账户内存在的任何 blobId 都可以在该账户中创建/更新另一个对象时使用。例如,Email 类型可能有一个代表 Internet Message Format [RFC5322] 中对象的 blobId。客户端可以创建一个带有附件的新 Email 对象并使用该 blobId,实际上是将旧消息附加到新消息上。类似地,它可以在不重新下载和上传的情况下,附加旧消息的任何现有附件。
当客户端在 create/update 中使用 blobId 时,服务器可以为该新/更新对象中的相同二进制数据分配一个新的 blobId。如果这样做,它必须在 created/updated 响应中返回任何包含已更改 blobId 的属性,以便客户端获得新的 id。
未被 JMAP 对象(例如作为消息附件)引用的 blob 可能被服务器删除以释放资源。上传(见下文)最初是未引用的 blob。为确保互操作性:
- 服务器应为未引用的 blob 使用与账户通常配额分开的配额。在共享账户的情况下,该配额应每个用户分开。
- 该配额至少应为该服务器上单个对象可引用的总大小的最大值。例如,如果支持 JMAP Mail,这至少应是一条消息的最大总附件大小。
- 当上传会使用户超出配额时,服务器必须按日期顺序(最旧的先)删除未引用的 blob,直到有新 blob 的空间。
- 除非配额限制强制提前删除,否则未引用的 blob 自上传之时起至少 1 小时内不得删除;如果重新上传,可能返回相同的 blobId,但这应重置过期时间。
- 在移除最后一个引用的那个方法调用期间,不得删除 blob,以便客户端可以在同一方法调用中发起一个同时引用该 blob 的 create 和 destroy。
6.1. 上传二进制数据
存在一个处理账户所有文件上传的单一端点,无论它们将用于什么。Session 对象(见第 2 节)具有一个 "uploadUrl" 属性,采用 URI 模板(level 1)格式 [RFC6570],其中必须包含一个名为 "accountId" 的变量。客户端可以将此模板与 "accountId" 结合使用,以获取文件上传资源的 URL。
要上传文件,客户端向文件上传资源发起一个经过认证的 POST 请求。
成功的请求必须返回单个 JSON 对象,作为响应,具有以下属性:
- accountId: "Id"
- 用于该调用的账户 id。
- blobId: "Id"
- 代表所上传二进制数据的 id。该 id 的数据是不可变的。该 id仅引用二进制数据,而非任何元数据。
- type: "String"
- 文件的媒体类型(如 [RFC6838] 第 4.2 节所规定),由上传 HTTP 请求的 Content-Type 头设置。
- size: "UnsignedInt"
- 文件的大小(以八位组计)。
如果上传的内容与账户中现有 blob 的二进制内容相同,可能返回现有的 blobId。
客户端应及时使用返回的 blobId。在极少数情况下,在客户端使用之前,服务器可能已删除了该 blob;客户端应保留对本地文件的引用,以便在这种情况下可以再次上传。
当向客户端返回 HTTP 错误响应时,服务器应当返回一个 JSON "problem details" 对象作为响应主体,如 [RFC7807] 所述。
由于访问控制通常由持有对 blob 引用的对象决定,即使在共享账户中,未引用的 blob 也只能由上传者访问。
6.2. 下载二进制数据
Session 对象(见第 2 节)具有一个 "downloadUrl" 属性,采用 URI 模板(level 1)格式 [RFC6570]。该 URL 必须包含名为 "accountId"、"blobId"、"type" 和 "name" 的变量。
要下载文件,客户端向下载 URL 发起一个经过认证的 GET 请求,代入相应的变量:
- "accountId":带有该 blobId 的记录所属的账户 id。
- "blobId":代表要下载文件数据的 blobId。
- "type":服务器要在响应的 "Content-Type" 头中设置的类型;blobId 仅代表二进制数据,本身不关联内容类型。
- "name":文件的名称;如果服务器设置了 "Content-Disposition" 头,它必须将此作为文件名返回。
由于特定 blobId 的数据是不可变的,因此在生成的下载 URL 中的响应也是不可变的,建议实现者为成功的响应设置较长的缓存时间,并使用 "immutable" Cache-Control 扩展 [RFC8246],例如 "Cache-Control: private, immutable, max-age=31536000"。
当向客户端返回 HTTP 错误响应时,服务器应当返回一个 JSON "problem details" 对象作为响应主体,如 [RFC7807] 所述。
6.3. Blob/copy
二进制数据可以使用 "Blob/copy" 方法在两个不同账户之间复制,而无需在客户端下载后重新上传。
"Blob/copy" 方法接受以下参数:
- fromAccountId: "Id"
- 要从中复制 blob 的账户 id。
- accountId: "Id"
- 要复制 blob 到的账户 id。
- blobIds: "Id[]"
- 要复制到另一个账户的 blob 的 id 列表。
响应具有以下参数:
- fromAccountId: "Id"
- blob 所复制自的账户 id。
- accountId: "Id"
- blob 所复制到的账户 id。
- copied: "Id[Id]|null"
- 从 fromAccount 中的 blobId 到其复制到的账户中 blob 的 id 的映射,如果没有成功复制则为 null。
- notCopied: "Id[SetError]|null"
- 从 blobId 到 SetError 对象的映射,对应每个复制失败的 blob,如果没有则为 null。
SetError 可以是第 5.3 节中定义的、可能为 create 返回的任何标准 set 错误。此外,如果找不到要复制的 blobId,可能返回 "notFound" SetError 错误。
可能返回以下额外方法级错误以代替 "Blob/copy" 响应:
- "fromAccountNotFound":请求中包含的 "fromAccountId" 不与有效的账户相对应。
7. 推送
推送通知允许客户端高效地更新(几乎)即时地与服务器上的数据变更保持同步。推送的一般模型很简单,并在推送通道上发送最小的数据:刚好足够让客户端知道是否需要重新同步。该格式允许多个变更合并为单个推送更新,并允许服务器对推送频率进行速率限制。即使某些推送事件在到达客户端之前被丢弃也没关系;下次它获取/设置任何已变更类型的记录时,它会发现数据已变,并仍然会同步所有变更。
客户端可以通过两种不同的机制接收推送通知,以适应客户端可能存在其中的不同环境。事件源资源(见第 7.3 节)允许能够保持传输连接打开的客户端直接从 JMAP 服务器接收推送通知。这很简单且避免了第三方,但在受限平台(例如移动设备)上往往不可行。或者,客户端可以利用其环境支持的任何推送服务。将推送服务的 URL 注册到 JMAP 服务器(见第 7.2 节);随后服务器将每个通知 POST 到该 URL。该推送服务随后负责将这些通知路由到客户端。
7.1. StateChange 对象
当服务器上发生某些变更时,服务器向客户端推送一个 StateChange 对象。一个 *StateChange* 对象具有以下属性:
- @type: "String"
- 必须是字符串 "StateChange"。
- changed: "Id[TypeState]"
- 从账户 id 到对象的映射,该对象编码自上次推送 StateChange 对象以来、该账户中已变更数据类型(针对用户有权访问且发生了某些变更的每个账户)的状态。
- 一个 *TypeState* 对象是一个映射。键是类型名 "Foo"(例如 "Mailbox" 或 "Email"),值是调用 "Foo/get" 当前会返回的 "state" 属性。
- 客户端可以将新状态字符串与其当前值比较,以查看它是否具有这些类型的当前数据。如果没有,则可以在单个标准 API 请求(使用 /changes 类型方法)中高效地获取变更。
7.1.1. 示例
在此示例中,服务器在推送到客户端的以下 StateChange 对象之前,已跨用户有权访问的两个不同账户合并了若干变更:
{
"@type": "StateChange",
"changed": {
"a3123": {
"Email": "d35ecb040aab",
"EmailDelivery": "428d565f2440",
"CalendarEvent": "87accfac587a"
},
"a43461d": {
"Mailbox": "0af7a512ce70",
"CalendarEvent": "7a4297cecd76"
}
}
}
客户端可以将状态字符串与相应账户中 Email、CalendarEvent 等对象类型的当前状态进行比较,以查看它是否需要获取变更。
如果客户端自身正在做出变更,它可能在 /set API 调用还在进行中时收到一个 StateChange 对象。它可以等到调用完成,然后比较 /set 之后的新状态字符串是否与 StateChange 对象中推送的相同;如果相同,且 /set 响应的旧状态与客户端的先前状态匹配,它就不必浪费一个请求去询问它已经知道的变更。
7.2. PushSubscription
客户端可以创建一个 PushSubscription,向 JMAP 服务器注册一个 URL。JMAP 服务器随后会针对它希望发送给客户端的每个推送通知,向该 URL 发起一个 HTTP POST 请求。
由于推送订阅会导致 JMAP 服务器向先前未知的端点发起若干请求,它可被用作发起拒绝服务攻击的载体。为防止这种情况,当创建订阅时,JMAP 服务器立即向该 URL 发送一个 PushVerification 对象(见第 7.2.2 节)。在客户端收到推送并用正确的验证码更新订阅之前,JMAP 服务器不得向该 URL 发起任何进一步请求。
一个 *PushSubscription* 对象具有以下属性:
- id: "Id"(immutable;server-set)
- 推送订阅的 id。
- deviceClientId: "String"(immutable)
- 唯一标识运行它的客户端 + 设备的 id。
- 其目的是允许客户端识别它们创建的 PushSubscription 对象,即使它们丢失了本地状态,以便能够撤销或更新它们。该字符串在不同设备上必须不同,并且必须不同于其他厂商的应用。
- 它应易于重新生成且不依赖于持久化状态。建议使用包含以下内容的字符串的安全哈希:
- 与运行 JMAP 客户端的设备相关联的唯一标识符,通常由设备的操作系统提供。
- 自定义的厂商/应用 id,包括由 JMAP 客户端厂商控制的域。
- 为保护用户隐私,deviceClientId id 不得包含未混淆的设备 id。
- url: "String"(immutable)
- JMAP 服务器将 POST 推送消息数据的绝对 URL。它必须以 "https://" 开头。
- keys: "Object|null"(immutable)
- 客户端生成的加密密钥。如果提供,服务器必须按 [RFC8291] 的规定使用它们来加密发送到该推送订阅的所有数据。该对象必须具有以下属性:
- p256dh: "String"
- P-256 椭圆曲线 Diffie-Hellman(ECDH)公钥,如 [RFC8291] 所述,采用 [RFC4648] 定义的 URL 安全 base64 表示。
- auth: "String"
- 认证密钥,如 [RFC8291] 所述,采用 [RFC4648] 定义的 URL 安全 base64 表示。
- p256dh: "String"
- 客户端生成的加密密钥。如果提供,服务器必须按 [RFC8291] 的规定使用它们来加密发送到该推送订阅的所有数据。该对象必须具有以下属性:
- verificationCode: "String|null"
- 创建订阅时,这必须为 null(或省略)。JMAP 服务器随后生成验证码并在推送消息中发送,客户端用该验证码更新 PushSubscription 对象;详见第 7.2.2 节。
- expires: "UTCDate|null"
- 该推送订阅过期的时刻。如果指定,JMAP 服务器在此时刻之后不得再向该资源发起请求。它可以在此时刻或之后自动销毁该推送订阅。
- 如果客户端未给出,服务器可以选择设置一个过期时间,或将客户端给出的过期时间修改为更短的时长。
- types: "String[]|null"
- 客户端感兴趣的类型的列表(使用与上一节定义的 TypeState 对象中的键相同的名称)。只有当这些类型之一的数据发生变化时,才会发送 StateChange 通知。其他类型从 TypeState 对象中省略。如果为 null,将推送所有类型的变更。
POST 请求必须具有 "application/json" 的内容类型,并将 UTF-8 JSON 编码的对象作为主体。请求必须具有 "TTL" 头,并可以具有 "Urgency" 和/或 "Topic" 头,如 [RFC8030] 第 5 节所规定。JMAP 服务器应以合理的方式理解并处理 HTTP 状态响应。必须让 "429"(Too Many Requests)响应导致 JMAP 服务器降低推送频率;JMAP 推送结构允许多个变更合并为单个最小的 StateChange 对象。有关连接到未知服务器的风险的讨论,见第 8.6 节的安全考量。
JMAP 服务器充当 [RFC8030] 中定义的应用服务器。客户端可以结合使用 [RFC8030] 的其余部分与其自身的推送服务,以形成完整的端到端解决方案,或者也可以依赖替代机制来确保推送数据在离开 JMAP 服务器后送达。
推送订阅与用于认证创建它的 API 请求的凭证绑定。如果这些凭证过期或撤销,JMAP 服务器必须销毁该推送订阅。客户端获取现有订阅时,只返回由这些凭证创建的订阅。
当这些凭证自身有过期时间(即带有超时的会话)时,服务器不应设置或限定客户端给出的推送订阅过期时间,而必须在会话过期时使其过期。
当这些凭证没有时间限制时(例如 Basic 认证 [RFC7617]),如果客户端未给出,服务器应为推送订阅设置一个过期时间,并在设置得过于久远时限其过期时间。该最大过期时间必须至少在未来 48 小时,并应至少在未来 7 天。在移动设备上运行的应用可能只能在位于前台时刷新推送订阅生存期,因此这提供了一个合理的时间窗口来允许这种情况发生。
在单独的访问凭证与刷新凭证的情况下(如 OAuth 2.0 [RFC6749]),服务器应将推送订阅绑定到刷新 token(而非访问 token)的有效性,并根据其是否时间受限来表现。
当推送订阅被销毁时,服务器必须尽快从内存和存储中安全擦除 URL 和加密密钥。
7.2.1. PushSubscription/get
如第 5.1 节所述的标准 /get 方法,区别是它不接受或返回 "accountId" 参数,因为推送订阅不绑定到特定账户。它也不返回 "state" 参数。"ids" 参数可以为 null 以一次性获取所有。
服务器必须只返回使用与本次 "PushSubscription/get" 请求相同的认证凭证创建的推送订阅。
由于 "url" 和 "keys" 属性可能包含特定设备私有数据,这些属性的值不得返回。如果 "properties" 参数为 null 或被省略,服务器必须默认为除这两个之外的所有属性。如果显式请求了其中之一,必须以 "forbidden" 错误拒绝该方法调用。
7.2.2. PushSubscription/set
如第 5.3 节所述的标准 /set 方法,区别是它不接受或返回 "accountId" 参数,因为推送订阅不绑定到特定账户。它也不接受 "ifInState" 参数,也不返回 "oldState" 或 "newState" 参数。
"url" 和 "keys" 属性是不可变的;如果客户端希望更改这些,它必须销毁当前的推送订阅并创建一个新的。
当创建 PushSubscription 时,服务器必须立即向该 URL 推送一个 *PushVerification* 对象。它具有以下属性:
- @type: "String"
- 必须是字符串 "PushVerification"。
- pushSubscriptionId: "String"
- 被创建的推送订阅的 id。
- verificationCode: "String"
- 要添加到推送订阅的验证码。它必须包含足够的熵,以避免客户端能够通过暴力猜测该验证码。
在服务器向该订阅的 URL 发起任何进一步请求之前,客户端必须用正确的验证码更新该推送订阅。尝试用无效验证码更新订阅必须被服务器以 "invalidProperties" SetError 拒绝。
客户端可以更新 "expires" 属性以延长(或较少见地缩短)推送订阅的生存期。服务器可以修改建议的新过期时间以实施服务器定义的限制。延长生存期不需要再次验证订阅。
客户端不应更新或销毁不是它创建的推送订阅(即具有它不认识的 "deviceClientId" 的订阅)。
7.2.3. 示例
在 "2018-07-06T02:14:29Z",一个 deviceClientId 为 "a889-ffea-910" 的客户端获取服务器上当前的推送订阅集合,发起 API 请求:
[[ "PushSubscription/get", {
"ids": null
}, "0" ]]
返回:
[[ "PushSubscription/get", {
"list": [{
"id": "e50b2c1d-9553-41a3-b0a7-a7d26b599ee1",
"deviceClientId": "b37ff8001ca0",
"verificationCode": "b210ef734fe5f439c1ca386421359f7b",
"expires": "2018-07-31T00:13:21Z",
"types": [ "Todo" ]
}, {
"id": "f2d0aab5-e976-4e8b-ad4b-b380a5b987e4",
"deviceClientId": "X8980fc",
"verificationCode": "f3d4618a9ae15c8b7f5582533786d531",
"expires": "2018-07-12T05:55:00Z",
"types": [ "Mailbox", "Email", "EmailDelivery" ]
}],
"notFound": []
}, "0" ]]
由于返回的两个推送订阅对象都没有该客户端的 deviceClientId,它知道自己服务器上当前没有活跃的推送订阅。因此它创建一个,发送此请求:
[[ "PushSubscription/set", {
"create": {
"4f29": {
"deviceClientId": "a889-ffea-910",
"url": "https://example.com/push/?device=X8980fc&client=12c6d086",
"types": null
}
}
}, "0" ]]
服务器创建推送订阅,但将过期时间限制为未来 7 天,返回此响应:
[[ "PushSubscription/set", {
"created": {
"4f29": {
"id": "P43dcfa4-1dd4-41ef-9156-2c89b3b19c60",
"keys": null,
"expires": "2018-07-13T02:14:29Z"
}
}
}, "0" ]]
服务器还立即向 "https://example.com/push/?device=X8980fc&client=12c6d086" 发起一个 POST 请求,数据为:
{
"@type": "PushVerification",
"pushSubscriptionId": "P43dcfa4-1dd4-41ef-9156-2c89b3b19c60",
"verificationCode": "da1f097b11ca17f06424e30bf02bfa67"
}
客户端收到此数据,并用验证码更新订阅(注意这里存在潜在的竞态条件;客户端必须能够处理在创建订阅的请求仍在进行中时就收到推送的情况):
[[ "PushSubscription/set", {
"update": {
"P43dcfa4-1dd4-41ef-9156-2c89b3b19c60": {
"verificationCode": "da1f097b11ca17f06424e30bf02bfa67"
}
}
}, "0" ]]
服务器确认更新成功,现在将在状态变化时向注册的 URL 发起请求。
两天后,客户端更新订阅以延长其生存期,发送此请求:
[[ "PushSubscription/set", {
"update": {
"P43dcfa4-1dd4-41ef-9156-2c89b3b19c60": {
"expires": "2018-08-13T00:00:00Z"
}
}
}, "0" ]]
服务器延长过期时间,但再次只到其未来 7 天的最大限制,返回此响应:
[[ "PushSubscription/set", {
"updated": {
"P43dcfa4-1dd4-41ef-9156-2c89b3b19c60": {
"expires": "2018-07-15T02:22:50Z"
}
}
}, "0" ]]
7.3. 事件源
能够保持传输连接打开的客户端可以直接连接到 JMAP 服务器,通过 "text/event-stream" 资源接收推送通知,如 [EventSource] 所述。这是一个长时间运行的 HTTP 请求,服务器可以通过追加数据而不结束响应,从而将数据推送给客户端。
当服务器上的数据发生变化时,它会向任何已连接的客户端推送一个名为 "state" 的事件,数据为一个 StateChange 对象。
服务器还应发送一个新的事件 id,编码在发送 "state" 事件后立即对用户可见的整个服务器状态。当与事件源端点建立新连接时,遵循服务器发送事件(server-sent events)规范的客户端会发送一个 Last-Event-ID HTTP 头字段,带其看到的最后一个 id,服务器可用它来判断客户端是否错过了某些变更。如果是,它应在连接时立即发送这些变更。
Session 对象(见第 2 节)具有一个 "eventSourceUrl" 属性,采用 URI 模板(level 1)格式 [RFC6570]。该 URL 必须包含名为 "types"、"closeafter" 和 "ping" 的变量。
要连接到该资源,客户端向事件源 URL 发起一个经过认证的 GET 请求,代入相应的变量:
- "types":必须是以下之一:
- 以逗号分隔的类型名列表,例如 "Email,CalendarEvent"。服务器必须只推送该列表中类型的变更。
- 单个字符:"*"。推送所有类型的变更。
- "closeafter":必须是以下值之一:
- "state":服务器必须在推送一个 state 事件后结束 HTTP 响应。这可被在通常模式下缓冲代理阻止推送数据立即(甚至根本)到达的环境中的客户端使用。
- "no":连接由服务器作为标准事件源资源持续保持。
- "ping":一个正整数值,表示以秒为单位的时间长度,例如 "300"。如果非零,服务器必须在此时间自上一次发送事件以来经过时,发送一个名为 "ping" 的事件。它不得设置新的事件 id。如果值为 "0",服务器不得发送 ping 事件。
- 服务器可以将请求的 ping 间隔修改为受最小和/或最大值约束。为互操作性,服务器不得有高于 30 的最小允许值或低于 300 的最大允许值。
- ping 事件的数据必须是包含 "interval" 属性的 JSON 对象,其值("UnsignedInt" 类型)为服务器用于发送 ping 的间隔秒数(如果服务器将其钳制为最小/最大值,这可能与请求的值不同)。
- 客户端可以监视 ping 事件以帮助确定何时可能需要 closeafter 模式。
客户端可以保持与事件源资源的多个连接打开,尽管出于效率考虑它应尝试使用单一连接。
8. 安全考量
8.1. 传输机密性
为确保通过 JMAP 收发数据的机密性与完整性,所有请求必须使用 TLS 1.2 [RFC5246] [RFC8446] 或更高版本,遵循 [RFC7525] 中的建议。服务器应支持 TLS 1.3 [RFC8446] 或更高版本。
客户端必须校验 TLS 证书链,以防范中间人攻击 [RFC5280]。
8.2. 认证方案
已经标准化了若干 HTTP 认证方案(见 <https://www.iana.org/assignments/http-authschemes/>)。服务器在决定实现哪些方案时,应仔细评估不同方案相对于其需求的安全特性。
不建议使用 Basic 认证方案。选择使用它的服务强烈建议为每个希望连接的客户端,通过某种外部机制生成唯一的"应用密码(app password)"。这使得服务器能够区分来自不同设备的连接,并单独撤销访问。
8.3. 服务自动发现
除非有类似 DNSSEC 的保护,否则使用 SRV DNS 记录的服务器详情自动发现容易受到 DNS 投毒攻击,这可能导致客户端与攻击者的服务器而非真实的 JMAP 服务器通信。攻击者随后可能拦截请求以实施中间人攻击,并根据认证方案窃取凭证来生成其自己的请求。
不支持 SRV 查找的客户端可能会尝试直接对用户名域通过 HTTPS 使用 "/.well-known/jmap" 路径。服务器应确保该路径能解析或重定向到正确的 JMAP 会话资源,以允许这种情况工作。如果这不可行,服务器必须确保该路径不能被攻击者控制,因为同样它可能用于窃取凭证。
8.4. JSON 解析
[RFC8259] 的安全考量适用于将 JSON 用作数据交换格式的情形。
与任何序列化格式一样,解析器需要彻底检查所提供数据的语法。JSON 对若干类型与结构使用开始和结束标记,在扫描匹配结束标记时可能到达所提供数据的末尾;这是一个错误条件,实现需要在所提供数据末尾停止扫描。
JSON 还使用带某些转义序列的字符串编码来表示特殊字符。处理这些转义序列时需要小心,以确保它们在触发特殊处理之前是完整的,当转义序列出现在其他(非转义)特殊字符附近或紧邻数据末尾时(如前一段所述)要特别小心。
如果将 JSON 解析为非文本的结构化数据格式,实现可能需要分配存储来保存 JSON 字符串元素。由于 JSON 不使用显式的字符串长度,因资源耗尽而导致拒绝服务的风险较小,但实现仍可能希望对它们在任何给定上下文中愿意进行的分配大小设定限制,以避免不受信任的数据导致过度的内存分配。
8.5. 拒绝服务
如果不实施资源限制,一个小的请求可能导致非常大的响应,并在服务器上需要大量工作。JMAP 提供了通告和实施多种限制以缓解此威胁的机制,包括单次方法调用中获取的对象数量、单个请求中的方法数量、并发请求数量等的限制。
JMAP 服务器必须实施合理的限制,以缓解资源耗尽攻击。
8.6. 连接到未知推送服务器
当注册推送订阅时,应用服务器将向给定 URL 发起 POST 请求。实现这一点时必须考虑若干安全考量。
服务器必须确保该 URL 在外部可解析,以避免服务器端请求伪造(SSRF),即服务器向其内部网络上的资源发起请求。
恶意客户端可能利用推送订阅尝试向第三方服务器洪水般发送请求,制造拒绝服务攻击并掩盖攻击者的真实身份。无法保证提供给 JMAP 服务器的 URL 确实是有效的推送服务器。在创建推送订阅时,JMAP 服务器向该 URL 发送一个 PushVerification 对象,并且在客户端验证其已收到初始推送之前,不得发送任何进一步请求。验证码必须包含足够的熵,以防止客户端能够通过暴力验证该订阅。
验证码并不能保证该 URL 是有效的推送服务器,只能保证客户端能够访问提交给它的数据。虽然验证步骤显著减少了潜在目标集合,但仍然存在该服务器与客户无关、并被作为拒绝服务攻击目标的风险。
服务器必须限制任何单一用户可拥有的推送订阅数量,以确保该用户无法使服务器一次性发送大量推送通知,而这同样可能被用作拒绝服务攻击的一部分。创建速率也必须受限,以尽量降低将验证请求作为攻击载体滥用的能力。
8.7. 推送加密
当数据发生变化时,会推送一个包含已变更类型新状态字符串的小对象。虽然这里的数据最少,但被动的中间人攻击者可能获得有用的信息。为确保机密性与完整性,如果推送是通过客户端和 JMAP 服务器控制范围之外的第三方发送的,客户端必须在建立 PushSubscription 时指定加密密钥,并忽略任何未用这些密钥加密的推送通知。
[RFC8030] 和 [RFC8291] 的隐私与安全考量也适用于 PushSubscription 机制的使用。
由于 Web Push Encryption [RFC8291] 中没有加密算法敏捷性,如果未来需要新算法,将需要一个新的规范来提供。
8.8. 流量分析
虽然数据是加密的,但具有监控网络流量能力的被动观察者可能能够从 API 请求和推送通知的时序中获得信息。例如,假设一封电子邮件或日历邀请从用户 A(托管在服务器 X 上)发送给用户 B(托管在服务器 Y 上)。如果服务器 X 为许多用户托管数据,被动观察者可以看到两台服务器建立了连接,但不知道数据是为谁准备的。然而,如果立即向用户 B 发送了推送通知,且攻击者也能观察到这一点,他们可以合理地推断服务器 X 上有人正在连接到用户 B。
9. IANA 考量
9.1. 分配 jmap 服务名
IANA 已在"服务名称与传输协议端口号注册表"[RFC6335] 中分配了 'jmap' 服务名。
- Service Name:jmap
- Transport Protocol(s):tcp
- Assignee:IESG
- Contact:IETF Chair
- Description:JSON Meta Application Protocol
- Reference:RFC 8620
- Assignment Notes:此服务名先前以 "JSON Mail Access Protocol" 之名分配。在先前分配者批准后已取消分配并重新分配。
9.2. 注册 JMAP 的 Well-Known URI 后缀
如 [RFC8615] 所述,IANA 已在"Well-Known URIs"注册表中为 JMAP 注册了以下后缀:
- URI Suffix:jmap
- Change Controller:IETF
- Specification Document:RFC 8620,第 2.2 节。
9.3. 注册 jmap URN 子命名空间
如 [RFC3553] 所述,IANA 已在"Uniform Resource Name (URN) Namespace for IETF Use"注册表中的"IETF URN Sub-namespace for Registered Protocol Parameter Identifiers"注册表里,注册了以下 URN 子命名空间:
- Registered Parameter Identifier:jmap
- Reference:RFC 8620,第 9.4 节。
- IANA Registry Reference:http://www.iana.org/assignments/jmap
9.4. 创建"JMAP Capabilities"注册表
IANA 已按第 2 节所述创建了"JMAP Capabilities"注册表。JMAP 能力在 JMAP 会话资源的 "capabilities" 属性中通告。它们用于扩展 JMAP 服务器的功能。能力通过 URI 引用。JMAP 能力 URI 可以是一个以 "urn:ietf:params:jmap:" 开头、加上作为 jmap URN 子命名空间中索引值的唯一后缀的 URN。以其他形式 URI 注册 JMAP 能力对 jmap URN 子命名空间没有影响。
除非"intended use(预期用途)"字段为 "common" 或 "placeholder",否则该注册表遵循专家评审流程;在这两种情况下,注册遵循规范要求的流程。
JMAP 能力注册的预期用途可以是 "common"、"placeholder"、"limited" 或 "obsolete"。IANA 将把 common-use 注册与其他预期用途值的注册分开、突出列出。
JMAP 能力注册流程不是正式的 standards 流程,而是一种行政流程,旨在允许社区评论和健全性检查,而不会有过多的时间延迟。
"placeholder"注册为 jmap URN 命名空间的另一用途保留了一部分,但通常不包含在 JMAP 会话资源的 "capabilities" 属性中。
9.4.1. 初步社区评审
潜在 JMAP common-use 注册的通知应发送到 JMAP 邮件列表 <jmap@ietf.org> 进行评审。该邮件列表适合就提议的 JMAP 能力征集社区反馈。不打算用于 common-use 的注册也可以发送到该列表进行评审;这样做完全可选,但受到鼓励。
公开发布到该列表的目的是就能力名称的选择、规范文档的明确性,以及任何互操作性或安全考量的审查征集评论和反馈。提交者可以在任何时候提交修订的注册提案,或完全放弃注册。
9.4.2. 向 IANA 提交请求
注册请求可发送到 <iana@iana.org>。
9.4.3. 指定专家评审
对于 limited-use 注册,指定专家(DE)主要关注防止名称冲突,并鼓励提交者记录安全与隐私考量;不要求已发布的规范。对于 common-use 注册,DE 应确认存在 [RFC8126] 第 4.6 节所述的合适文档。DE 还应核实该能力不与 IETF 内活跃或已发布的工作冲突。
在 30 天期限过去之前,DE 将批准或拒绝注册请求,并向 JMAP WG 邮件列表或其继任者发布决定通知,同时通知 IANA。拒绝通知必须有解释作为理由,并且在可能的情况下,应提供关于如何修改请求以使其可接受的切实建议。
如果 DE 在 30 天内未响应,注册者可以请求 IESG 采取行动以及时处理该请求。
9.4.4. 变更流程
一旦 JMAP 能力已由 IANA 发布,变更控制者可以请求对其定义进行变更。处理变更请求时使用与原始注册请求适当的相同流程。
JMAP 能力注册不得删除;不再被认为适合使用的能力可以通过将其 "intended use" 字段变更为 "obsolete" 来声明废弃;此类能力将在 IANA 发布的列表中明确标记。
只有当已发布规范存在严重遗漏或错误时,才应请求对能力定义进行重大变更。当需要评审时,如果变更请求会使在先前定义下有效的实体在新定义下无效,则可能被拒绝。
JMAP 能力的所有者可以通过通知 IANA,将责任移交给另一个人或机构;这可以在没有讨论或评审的情况下完成。
IESG 可以重新分配 JMAP 能力的责任。最常见的情况是将责任分配给那些注册作者已去世、失去联系,或以其他方式无法做出对社区重要的变更的能力。
9.4.5. JMAP Capabilities 注册表模板
- Capability name:(见第 2 节中的 capability 属性)
- Specification document:
- Intended use:(common、limited、placeholder 或 obsolete 之一)
- Change controller:(Standards Track / BCP RFC 为 "IETF")
- Security and privacy considerations:
9.4.6. JMAP Core 的初始注册
- Capability Name:"urn:ietf:params:jmap:core"
- Specification document:RFC 8620,第 2 节
- Intended use:common
- Change Controller:IETF
- Security and privacy considerations:RFC 8620,第 8 节。
9.4.7. 在 JMAP Capabilities 注册表中为 JMAP 错误占位符注册
- Capability Name:"urn:ietf:params:jmap:error:"
- Specification document:RFC 8620,第 9.5 节
- Intended use:placeholder
- Change Controller:IETF
- Security and privacy considerations:RFC 8620,第 8 节。
9.5. 创建"JMAP Error Codes"注册表
IANA 已创建"JMAP Error Codes"注册表。JMAP 错误码出现在 JSON problem details 对象(如第 3.6.1 节所述)的 "type" 成员中、JMAP 错误对象(如第 3.6.2 节所述)的 "type" 成员中,或 JMAP 方法特定错误对象(例如第 5.3 节的 SetError)的 "type" 成员中。当用于 problem details 对象时,始终包含前缀 "urn:ietf:params:jmap:error:";当用于 JMAP 对象时,始终省略该前缀。
该注册表遵循专家评审流程。该注册表的初步社区评审遵循与"JMAP Capabilities"注册表相同的流程,但是可选的。该注册表的变更流程与"JMAP Capabilities"注册表的变更流程相同。
9.5.1. 专家评审
指定专家应评审注册的以下方面:
- 核实错误码不与现有名称冲突。
- 核实错误码遵循语法限制(不需要 URI 编码)。
- 鼓励提交者遵循先前注册错误的命名约定。
- 鼓励提交者描述建议针对该错误码做出的客户端行为。这些可能将该错误码与其他错误码区分开来。
- 鼓励提交者描述服务器应在何时发出该错误码,而非某些其他错误码。
- 鼓励提交者注明与该错误相关的任何安全考量(如果有)(例如,可能泄露经过认证用户无权知晓的数据是否存在的错误码)。
第 3-6 步旨在促进更高质量的注册表。然而,鼓励专家批准任何不会主动损害 JMAP 互操作性的注册,以使这成为一个相对轻量的流程。
9.5.2. JMAP Error Codes 注册表模板
- JMAP Error Code:
- Intended use:("common"、"limited"、"obsolete" 之一)
- Change Controller:(Standards Track / BCP RFC 为 "IETF")
- Reference:(可选。仅当在 RFC 中定义时才需要。)
- Description:
9.5.3. JMAP Error Codes 注册表的初始内容
| 错误码 | 预期用途 | 变更控制者 | 参考 | 描述 |
|---|---|---|---|---|
| accountNotFound | Common | IETF | RFC 8620,第 3.6.2 节 | accountId 不与有效的账户相对应。 |
| accountNotSupportedByMethod | Common | IETF | RFC 8620,第 3.6.2 节 | 给定的 accountId 对应于一个有效的账户,但该账户不支持此方法或数据类型。 |
| accountReadOnly | Common | IETF | RFC 8620,第 3.6.2 节 | 此方法修改状态,但账户是只读的(如 JMAP 会话资源的相应 Account 对象所返回)。 |
| anchorNotFound | Common | IETF | RFC 8620,第 5.5 节 | 提供了 anchor 参数,但在查询结果中找不到它。 |
| alreadyExists | Common | IETF | RFC 8620,第 5.4 节 | 服务器禁止重复,且记录已存在于目标账户中。SetError 对象上必须包含一个类型为 Id 的 existingId 属性,带有现有记录的 id。 |
| cannotCalculateChanges | Common | IETF | RFC 8620,第 5.2 与 5.6 节 | 服务器无法根据客户端给出的状态字符串计算变更。 |
| forbidden | Common | IETF | RFC 8620,第 3.6.2、5.3 与 7.2.1 节 | 该操作会违反 ACL 或其他权限策略。 |
| fromAccountNotFound | Common | IETF | RFC 8620,第 5.4 与 6.3 节 | fromAccountId 不与有效的账户相对应。 |
| fromAccountNotSupportedByMethod | Common | IETF | RFC 8620,第 5.4 节 | 给定的 fromAccountId 对应于一个有效的账户,但该账户不支持此数据类型。 |
| invalidArguments | Common | IETF | RFC 8620,第 3.6.2 节 | 某个参数的类型错误或以其他方式无效,或者缺少必需的参数。 |
| invalidPatch | Common | IETF | RFC 8620,第 5.3 节 | 用于更新记录的 PatchObject 不是有效的 patch。 |
| invalidProperties | Common | IETF | RFC 8620,第 5.3 节 | 给定的记录无效。 |
| notFound | Common | IETF | RFC 8620,第 5.3 节 | 给定的 id 找不到。 |
| notJSON | Common | IETF | RFC 8620,第 3.6.1 节 | 请求的内容类型不是 application/json,或请求未能解析为 I-JSON。 |
| notRequest | Common | IETF | RFC 8620,第 3.6.1 节 | 请求解析为 JSON,但与 Request 对象的类型签名不匹配。 |
| overQuota | Common | IETF | RFC 8620,第 5.3 节 | 创建会超出服务器定义的、该类型对象数量或总大小的限制。 |
| rateLimit | Common | IETF | RFC 8620,第 5.3 节 | 最近创建了过多该类型的对象,达到了服务器定义的速率限制。稍后重试可能成功。 |
| requestTooLarge | Common | IETF | RFC 8620,第 5.1 与 5.3 节 | 动作总数超过了服务器愿意在单个方法调用中处理的最大数量。 |
| invalidResultReference | Common | IETF | RFC 8620,第 3.6.2 节 | 该方法为其某个参数使用了结果引用,但解析失败。 |
| serverFail | Common | IETF | RFC 8620,第 3.6.2 节 | 在处理调用期间发生了意外或未知的错误。该方法调用未对服务器状态做出任何更改。 |
| serverPartialFail | Limited | IETF | RFC 8620,第 3.6.2 节 | 描述的预期变更中有一部分(而非全部)发生了。客户端必须重新同步受影响的数据以确定服务器状态。强烈不建议使用此错误。 |
| serverUnavailable | Common | IETF | RFC 8620,第 3.6.2 节 | 某个内部服务器资源临时不可用。稍后(也许在带随机退避因子后)重试同一操作可能会成功。 |
| singleton | Common | IETF | RFC 8620,第 5.3 节 | 这是一个单例类型,因此你不能创建另一个,也不能销毁现有的这一个。 |
| stateMismatch | Common | IETF | RFC 8620,第 5.3 节 | 提供了 ifInState 参数,但它与当前状态不匹配。 |
| tooLarge | Common | IETF | RFC 8620,第 5.3 节 | 该操作会导致一个超出服务器定义的、该类型单个对象最大大小限制的对象。 |
| tooManyChanges | Common | IETF | RFC 8620,第 5.6 节 | 变更数量超过了客户端的 maxChanges 参数。 |
| unknownCapability | Common | IETF | RFC 8620,第 3.6.1 节 | 客户端在请求的 "using" 属性中包含了一个服务器不支持的能力。 |
| unknownMethod | Common | IETF | RFC 8620,第 3.6.2 节 | 服务器无法识别该方法名。 |
| unsupportedFilter | Common | IETF | RFC 8620,第 5.5 节 | 过滤器在语法上有效,但服务器无法处理。 |
| unsupportedSort | Common | IETF | RFC 8620,第 5.5 节 | 排序在语法上有效,但包含服务器不支持排序的属性,或它不认识的 collation 方法。 |
| willDestroy | Common | IETF | RFC 8620,第 5.3 节 | 客户端请求在同一 /set 请求中既更新又销毁某个对象,服务器因此决定忽略该更新。 |
10. 参考文献
10.1. 规范性参考文献
- [EventSource] Hickson, I., "Server-Sent Events", World Wide Web Consortium Recommendation REC-eventsource-20150203, February 2015, <https://www.w3.org/TR/eventsource/>.
- [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, <https://www.rfc-editor.org/info/rfc2119>.
- [RFC2782] Gulbrandsen, A., Vixie, P., and L. Esibov, "A DNS RR for specifying the location of services (DNS SRV)", RFC 2782, DOI 10.17487/RFC2782, February 2000, <https://www.rfc-editor.org/info/rfc2782>.
- [RFC2818] Rescorla, E., "HTTP Over TLS", RFC 2818, DOI 10.17487/RFC2818, May 2000, <https://www.rfc-editor.org/info/rfc2818>.
- [RFC3339] Klyne, G. and C. Newman, "Date and Time on the Internet: Timestamps", RFC 3339, DOI 10.17487/RFC3339, July 2002, <https://www.rfc-editor.org/info/rfc3339>.
- [RFC3553] Mealling, M., Masinter, L., Hardie, T., and G. Klyne, "An IETF URN Sub-namespace for Registered Protocol Parameters", BCP 73, RFC 3553, DOI 10.17487/RFC3553, June 2003, <https://www.rfc-editor.org/info/rfc3553>.
- [RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO 10646", STD 63, RFC 3629, DOI 10.17487/RFC3629, November 2003, <https://www.rfc-editor.org/info/rfc3629>.
- [RFC4648] Josefsson, S., "The Base16, Base32, and Base64 Data Encodings", RFC 4648, DOI 10.17487/RFC4648, October 2006, <https://www.rfc-editor.org/info/rfc4648>.
- [RFC4790] Newman, C., Duerst, M., and A. Gulbrandsen, "Internet Application Protocol Collation Registry", RFC 4790, DOI 10.17487/RFC4790, March 2007, <https://www.rfc-editor.org/info/rfc4790>.
- [RFC5051] Crispin, M., "i;unicode-casemap - Simple Unicode Collation Algorithm", RFC 5051, DOI 10.17487/RFC5051, October 2007, <https://www.rfc-editor.org/info/rfc5051>.
- [RFC5246] Dierks, T. and E. Rescorla, "The Transport Layer Security (TLS) Protocol Version 1.2", RFC 5246, DOI 10.17487/RFC5246, August 2008, <https://www.rfc-editor.org/info/rfc5246>.
- [RFC5280] Cooper, D., Santesson, S., Farrell, S., Boeyen, S., Housley, R., and W. Polk, "Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile", RFC 5280, DOI 10.17487/RFC5280, May 2008, <https://www.rfc-editor.org/info/rfc5280>.
- [RFC5322] Resnick, P., Ed., "Internet Message Format", RFC 5322, DOI 10.17487/RFC5322, October 2008, <https://www.rfc-editor.org/info/rfc5322>.
- [RFC6186] Daboo, C., "Use of SRV Records for Locating Email Submission/Access Services", RFC 6186, DOI 10.17487/RFC6186, March 2011, <https://www.rfc-editor.org/info/rfc6186>.
- [RFC6335] Cotton, M., Eggert, L., Touch, J., Westerlund, M., and S. Cheshire, "Internet Assigned Numbers Authority (IANA) Procedures for the Management of the Service Name and Transport Protocol Port Number Registry", BCP 165, RFC 6335, DOI 10.17487/RFC6335, August 2011, <https://www.rfc-editor.org/info/rfc6335>.
- [RFC6570] Gregorio, J., Fielding, R., Hadley, M., Nottingham, M., and D. Orchard, "URI Template", RFC 6570, DOI 10.17487/RFC6570, March 2012, <https://www.rfc-editor.org/info/rfc6570>.
- [RFC6749] Hardt, D., Ed., "The OAuth 2.0 Authorization Framework", RFC 6749, DOI 10.17487/RFC6749, October 2012, <https://www.rfc-editor.org/info/rfc6749>.
- [RFC6764] Daboo, C., "Locating Services for Calendaring Extensions to WebDAV (CalDAV) and vCard Extensions to WebDAV (CardDAV)", RFC 6764, DOI 10.17487/RFC6764, February 2013, <https://www.rfc-editor.org/info/rfc6764>.
- [RFC6838] Freed, N., Klensin, J., and T. Hansen, "Media Type Specifications and Registration Procedures", BCP 13, RFC 6838, DOI 10.17487/RFC6838, January 2013, <https://www.rfc-editor.org/info/rfc6838>.
- [RFC6901] Bryan, P., Ed., Zyp, K., and M. Nottingham, Ed., "JavaScript Object Notation (JSON) Pointer", RFC 6901, DOI 10.17487/RFC6901, April 2013, <https://www.rfc-editor.org/info/rfc6901>.
- [RFC7230] Fielding, R., Ed. and J. Reschke, Ed., "Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing", RFC 7230, DOI 10.17487/RFC7230, June 2014, <https://www.rfc-editor.org/info/rfc7230>.
- [RFC7231] Fielding, R., Ed. and J. Reschke, Ed., "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content", RFC 7231, DOI 10.17487/RFC7231, June 2014, <https://www.rfc-editor.org/info/rfc7231>.
- [RFC7493] Bray, T., Ed., "The I-JSON Message Format", RFC 7493, DOI 10.17487/RFC7493, March 2015, <https://www.rfc-editor.org/info/rfc7493>.
- [RFC7525] Sheffer, Y., Holz, R., and P. Saint-Andre, "Recommendations for Secure Use of Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS)", BCP 195, RFC 7525, DOI 10.17487/RFC7525, May 2015, <https://www.rfc-editor.org/info/rfc7525>.
- [RFC7617] Reschke, J., "The 'Basic' HTTP Authentication Scheme", RFC 7617, DOI 10.17487/RFC7617, September 2015, <https://www.rfc-editor.org/info/rfc7617>.
- [RFC7807] Nottingham, M. and E. Wilde, "Problem Details for HTTP APIs", RFC 7807, DOI 10.17487/RFC7807, March 2016, <https://www.rfc-editor.org/info/rfc7807>.
- [RFC8030] Thomson, M., Damaggio, E., and B. Raymor, Ed., "Generic Event Delivery Using HTTP Push", RFC 8030, DOI 10.17487/RFC8030, December 2016, <https://www.rfc-editor.org/info/rfc8030>.
- [RFC8126] Cotton, M., Leiba, B., and T. Narten, "Guidelines for Writing an IANA Considerations Section in RFCs", BCP 26, RFC 8126, DOI 10.17487/RFC8126, June 2017, <https://www.rfc-editor.org/info/rfc8126>.
- [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, <https://www.rfc-editor.org/info/rfc8174>.
- [RFC8259] Bray, T., Ed., "The JavaScript Object Notation (JSON) Data Interchange Format", STD 90, RFC 8259, DOI 10.17487/RFC8259, December 2017, <https://www.rfc-editor.org/info/rfc8259>.
- [RFC8264] Saint-Andre, P. and M. Blanchet, "PRECIS Framework: Preparation, Enforcement, and Comparison of Internationalized Strings in Application Protocols", RFC 8264, DOI 10.17487/RFC8264, October 2017, <https://www.rfc-editor.org/info/rfc8264>.
- [RFC8291] Thomson, M., "Message Encryption for Web Push", RFC 8291, DOI 10.17487/RFC8291, November 2017, <https://www.rfc-editor.org/info/rfc8291>.
- [RFC8446] Rescorla, E., "The Transport Layer Security (TLS) Protocol Version 1.3", RFC 8446, DOI 10.17487/RFC8446, August 2018, <https://www.rfc-editor.org/info/rfc8446>.
- [RFC8615] Nottingham, M., "Well-Known Uniform Resource Identifiers (URIs)", RFC 8615, DOI 10.17487/RFC8615, May 2019, <https://www.rfc-editor.org/info/rfc8615>.
10.2. 资料性参考文献
- [RFC8246] McManus, P., "HTTP Immutable Responses", RFC 8246, DOI 10.17487/RFC8246, September 2017, <https://www.rfc-editor.org/info/rfc8246>.
作者地址
Neil Jenkins
Fastmail
PO Box 234, Collins St. West
Melbourne, VIC 8007
Australia
Email: neilj@fastmailteam.com
Chris Newman
Oracle
Email: chris.newman@oracle.com
