@agentunion/fastaun-browser 0.2.19 → 0.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/_packed_docs/CHANGELOG.md +26 -0
  3. package/_packed_docs/agent.md/SCHEMA.md +173 -0
  4. package/_packed_docs/agent.md/examples/codeagent-claudecode.md +61 -0
  5. package/_packed_docs/agent.md/examples/human-developer.md +60 -0
  6. package/_packed_docs/agent.md/examples/openclaw-lobster.md +52 -0
  7. package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +43 -0
  8. package/_packed_docs/protocol/00-/346/200/273/350/247/210/344/270/216/345/210/206/345/261/202.md +205 -0
  9. package/_packed_docs/protocol/00A-/350/256/276/350/256/241/345/216/237/345/210/231-/344/270/272Agent/350/200/214/347/224/237.md +197 -0
  10. package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +549 -0
  11. package/_packed_docs/protocol/02-/350/257/201/344/271/246/344/270/216/344/277/241/344/273/273/344/275/223/347/263/273.md +810 -0
  12. package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +262 -0
  13. package/_packed_docs/protocol/04-Peer-/345/255/220/345/215/217/350/256/256.md +180 -0
  14. package/_packed_docs/protocol/05-Relay-/345/255/220/345/215/217/350/256/256.md +164 -0
  15. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1135 -0
  16. package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +234 -0
  17. package/_packed_docs/protocol/08-AUN-E2EE-Group.md +900 -0
  18. package/_packed_docs/protocol/08-AUN-E2EE.md +413 -0
  19. package/_packed_docs/protocol/09-/345/256/211/345/205/250/350/200/203/350/231/221.md +316 -0
  20. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +804 -0
  21. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +271 -0
  22. package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +329 -0
  23. package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +141 -0
  24. package/_packed_docs/protocol/14-/344/272/244/344/272/222/346/234/272/345/210/266-/345/223/215/345/272/224/346/250/241/345/274/217/344/270/216/350/207/252/344/270/273/346/250/241/345/274/217.md +170 -0
  25. package/_packed_docs/protocol/README.md +71 -0
  26. package/_packed_docs/protocol/agent.md/SCHEMA.md +118 -0
  27. package/_packed_docs/protocol/agent.md/examples/codeagent-claudecode.md +61 -0
  28. package/_packed_docs/protocol/agent.md/examples/human-developer.md +60 -0
  29. package/_packed_docs/protocol/agent.md/examples/openclaw-lobster.md +52 -0
  30. package/_packed_docs/protocol/aun-docs-guide.md +49 -0
  31. package/_packed_docs/protocol/index.md +114 -0
  32. package/_packed_docs/protocol//350/215/211/346/241/210-agent.md/347/255/276/345/220/215/345/215/217/350/256/256.md +205 -0
  33. package/_packed_docs/protocol//350/215/211/346/241/210-/346/213/222/347/273/235/344/277/241/345/217/267/345/215/217/350/256/256.md +249 -0
  34. package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +337 -0
  35. package/_packed_docs/protocol//351/231/204/345/275/225B-/346/211/251/345/261/225/346/200/247/346/214/207/345/215/227.md +80 -0
  36. package/_packed_docs/protocol//351/231/204/345/275/225C-/347/247/201/351/222/245/347/256/241/347/220/206/344/270/216/350/272/253/344/273/275/346/201/242/345/244/215.md +704 -0
  37. package/_packed_docs/protocol//351/231/204/345/275/225D-Root_CA_/346/262/273/347/220/206/346/234/272/345/210/266.md +620 -0
  38. package/_packed_docs/protocol//351/231/204/345/275/225E-Root_CA_/345/207/206/345/205/245/346/265/201/347/250/213.md +605 -0
  39. package/_packed_docs/protocol//351/231/204/345/275/225F-Issuer_CA_/347/224/263/350/257/267/346/265/201/347/250/213.md +548 -0
  40. package/_packed_docs/protocol//351/231/204/345/275/225G-AID_/345/255/244/345/204/277/351/242/204/351/230/262/344/270/216/346/225/221/346/217/264/346/234/272/345/210/266.md +513 -0
  41. package/_packed_docs/protocol//351/231/204/345/275/225H-Identity/346/234/215/345/212/241/345/256/236/347/216/260/346/214/207/345/215/227.md +619 -0
  42. package/_packed_docs/protocol//351/231/204/345/275/225I-/350/267/250/345/237/237/346/266/210/346/201/257/350/267/257/347/224/261/345/256/236/347/216/260/346/214/207/345/215/227.md +492 -0
  43. package/_packed_docs/protocol//351/231/204/345/275/225J-/345/256/242/346/210/267/347/253/257/346/216/245/345/205/245/347/244/272/344/276/213.md +402 -0
  44. package/_packed_docs/protocol//351/231/204/345/275/225K-Agent_Web/345/217/221/347/216/260/345/215/217/350/256/256.md +130 -0
  45. package/_packed_docs/protocol//351/231/204/345/275/225L-E2EE/345/256/236/347/216/260/346/214/207/345/215/227.md +267 -0
  46. package/_packed_docs/protocol//351/231/204/345/275/225M-JWT/350/256/244/350/257/201/345/256/236/347/216/260/346/214/207/345/215/227.md +367 -0
  47. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +223 -0
  48. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +354 -0
  49. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +172 -0
  50. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +373 -0
  51. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +611 -0
  52. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +1152 -0
  53. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +150 -0
  54. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +89 -0
  55. package/_packed_docs/sdk/09-custody-api-manual.md +445 -0
  56. package/_packed_docs/sdk/09-group-rpc-manual.md +1895 -0
  57. package/_packed_docs/sdk/09-message-rpc-manual.md +597 -0
  58. package/_packed_docs/sdk/09-meta-rpc-manual.md +142 -0
  59. package/_packed_docs/sdk/09-payload-reference.md +702 -0
  60. package/_packed_docs/sdk/09-storage-rpc-manual.md +408 -0
  61. package/_packed_docs/sdk/09-stream-rpc-manual.md +275 -0
  62. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +72 -0
  63. package/_packed_docs/sdk/INDEX.md +131 -0
  64. package/_packed_docs/sdk/README.md +307 -0
  65. package/dist/auth.d.ts +2 -1
  66. package/dist/auth.d.ts.map +1 -1
  67. package/dist/auth.js +13 -11
  68. package/dist/auth.js.map +1 -1
  69. package/dist/client.d.ts +38 -8
  70. package/dist/client.d.ts.map +1 -1
  71. package/dist/client.js +179 -97
  72. package/dist/client.js.map +1 -1
  73. package/dist/namespaces/auth.d.ts +1 -0
  74. package/dist/namespaces/auth.d.ts.map +1 -1
  75. package/dist/namespaces/auth.js +20 -6
  76. package/dist/namespaces/auth.js.map +1 -1
  77. package/dist/transport.d.ts +9 -1
  78. package/dist/transport.d.ts.map +1 -1
  79. package/dist/transport.js +24 -0
  80. package/dist/transport.js.map +1 -1
  81. package/package.json +40 -37
@@ -0,0 +1,150 @@
1
+ # AUN SDK Python - 错误处理
2
+
3
+ ---
4
+
5
+ ## 错误类层级
6
+
7
+ ```
8
+ AUNError
9
+ ├── ConnectionError # 网络连接失败
10
+ ├── TimeoutError # 操作超时
11
+ ├── AuthError # 认证失败(code: 4001/4010/-32001/-32003)
12
+ │ └── CertificateRevokedError # 证书已吊销(code: -32050)
13
+ ├── PermissionError # 权限不足(code: 4030/403/-32004)
14
+ ├── ValidationError # 参数校验失败(code: 4000/-32600/-32601/-32602)
15
+ ├── NotFoundError # 资源不存在(code: 4040/404/-32008)
16
+ ├── RateLimitError # 请求限流(code: 4290/429/-32029),retryable=True
17
+ ├── VersionConflictError # 版本冲突(code: -32009)
18
+ ├── StateError # 非法状态操作
19
+ ├── SerializationError # JSON 序列化失败
20
+ ├── SessionError # 会话错误(code: -32010/-32011/-32013)
21
+ ├── ClientSignatureError # 客户端签名验证失败(code: -32051)
22
+ ├── GroupError
23
+ │ ├── GroupNotFoundError # code: -33001
24
+ │ └── GroupStateError # code: -33002/-33003
25
+ └── E2EEError
26
+ ├── E2EEDecryptFailedError # P2P 消息解密失败
27
+ ├── E2EEDegradedError # E2EE 降级为 long_term_key(无 prekey 可用)
28
+ ├── E2EEGroupSecretMissingError # code: -32040,缺少群密钥
29
+ ├── E2EEGroupEpochMismatchError # code: -32041,epoch 不匹配
30
+ ├── E2EEGroupCommitmentInvalidError # code: -32042,成员承诺验证失败
31
+ ├── E2EEGroupNotMemberError # code: -32043,请求者非群成员
32
+ └── E2EEGroupDecryptFailedError # code: -32044,群消息解密失败
33
+ ```
34
+
35
+ ## 错误属性
36
+
37
+ ```python
38
+ from aun_core import AUNError
39
+
40
+ try:
41
+ await client.call("method.name", {...})
42
+ except AUNError as e:
43
+ e.code # int,错误码
44
+ e.data # Any,附加数据
45
+ e.retryable # bool,是否可重试
46
+ e.trace_id # str | None,追踪 ID
47
+ ```
48
+
49
+ ## 错误码速查
50
+
51
+ | 范围 | 含义 | 对应异常 |
52
+ |------|------|----------|
53
+ | 4000 | 参数校验失败 | `ValidationError` |
54
+ | 4001 / 4010 | 认证失败 | `AuthError` |
55
+ | 4030 / 403 | 权限不足 | `PermissionError` |
56
+ | 4040 / 404 | 资源不存在 | `NotFoundError` |
57
+ | 4290 / 429 | 请求限流 | `RateLimitError` |
58
+ | -32001 / -32003 | 认证失败(协议级) | `AuthError` |
59
+ | -32004 | 权限不足(协议级) | `PermissionError` |
60
+ | -32008 | 资源不存在(协议级) | `NotFoundError` |
61
+ | -32009 | 版本冲突 | `VersionConflictError` |
62
+ | -32010 / -32011 / -32013 | 会话错误 | `SessionError` |
63
+ | -32029 | 请求限流(协议级) | `RateLimitError` |
64
+ | -32600 / -32601 / -32602 | JSON-RPC 参数错误 | `ValidationError` |
65
+ | -32040 | 缺少群密钥 | `E2EEGroupSecretMissingError` |
66
+ | -32041 | 群 epoch 不匹配 | `E2EEGroupEpochMismatchError` |
67
+ | -32042 | 成员承诺验证失败 | `E2EEGroupCommitmentInvalidError` |
68
+ | -32043 | 密钥请求者非群成员 | `E2EEGroupNotMemberError` |
69
+ | -32044 | 群消息解密失败 | `E2EEGroupDecryptFailedError` |
70
+ | -32050 | 证书已吊销 | `CertificateRevokedError` |
71
+ | -32051 | 客户端签名验证失败 | `ClientSignatureError` |
72
+ | -33001 | 群组不存在 | `GroupNotFoundError` |
73
+ | -33002 / -33003 | 群组状态异常 | `GroupStateError` |
74
+ | -33004 ~ -33009 | 群组通用错误 | `GroupError` |
75
+
76
+ ## 重试策略
77
+
78
+ ```python
79
+ from aun_core import AUNError, RateLimitError, AuthError
80
+
81
+ async def send_with_retry(client, params, max_retries=3):
82
+ for i in range(max_retries):
83
+ try:
84
+ return await client.call("message.send", params)
85
+ except RateLimitError:
86
+ await asyncio.sleep(2 ** i)
87
+ except AuthError:
88
+ raise # 认证错误不重试
89
+ except AUNError as e:
90
+ if not e.retryable or i == max_retries - 1:
91
+ raise
92
+ await asyncio.sleep(0.5)
93
+ ```
94
+
95
+ ## 常见错误场景
96
+
97
+ ### 未认证就发消息
98
+
99
+ ```python
100
+ # ❌ 错误:跳过认证直接操作
101
+ client = AUNClient()
102
+ await client.call("message.send", {...}) # → AuthError
103
+
104
+ # ✅ 正确:先认证再操作
105
+ auth = await client.auth.authenticate({"aid": MY_AID})
106
+ await client.connect(auth, {})
107
+ await client.call("message.send", {...})
108
+ ```
109
+
110
+ ### E2EE 解密失败
111
+
112
+ ```python
113
+ # E2EEDecryptFailedError — prekey 不匹配、AAD 篡改、密文损坏
114
+ # SDK 自动处理:解密失败的消息返回原始密文,不中断其他消息
115
+ ```
116
+
117
+ ### 群组 E2EE 错误
118
+
119
+ ```python
120
+ # E2EEGroupSecretMissingError — 发送加密群消息时本地无群密钥
121
+ # 常见于建群后 create_epoch 尚未完成、或通过邀请码入群后尚未恢复密钥
122
+ # SDK 会抛出此异常,调用方可捕获后等待密钥恢复完成再重试
123
+ # SDK 自动编排:建群后自动 create_epoch;缺密钥时自动发起恢复请求
124
+
125
+ # E2EEGroupDecryptFailedError — 群消息解密失败(密钥不匹配或密文损坏)
126
+ # 协议层定义的错误语义,当前 Python SDK 的部分路径表现为返回 None、拒绝状态或跳过自动解密,不一定抛出对应异常
127
+
128
+ # E2EEGroupEpochMismatchError — 收到的密钥分发 epoch 低于当前 epoch
129
+ # 协议层定义的错误语义,当前 Python SDK 的部分路径表现为返回 None、拒绝状态或跳过自动解密,不一定抛出对应异常
130
+
131
+ # E2EEGroupCommitmentInvalidError — 收到的密钥分发中成员承诺校验失败
132
+ # 协议层定义的错误语义,当前 Python SDK 的部分路径表现为返回 None、拒绝状态或跳过自动解密,不一定抛出对应异常
133
+ # 可能原因:中间人篡改、成员列表不一致
134
+
135
+ # E2EEGroupNotMemberError — 密钥恢复请求被拒,请求者不在群成员列表中
136
+ # 协议层定义的错误语义,当前 Python SDK 的部分路径表现为返回 None、拒绝状态或跳过自动解密,不一定抛出对应异常
137
+ ```
138
+
139
+ ### 连接状态错误
140
+
141
+ ```python
142
+ # ❌ 错误:断连后直接调用
143
+ await client.call("message.send", {...}) # → ConnectionError("client is not connected")
144
+
145
+ # ✅ 正确:重新认证并启用自动重连
146
+ auth = await client.auth.authenticate({"aid": MY_AID})
147
+ await client.connect(auth, {
148
+ "auto_reconnect": True,
149
+ })
150
+ ```
@@ -0,0 +1,89 @@
1
+ # AUN SDK Python - 最佳实践
2
+
3
+ ---
4
+
5
+ ## 1. 幂等的连接初始化
6
+
7
+ 每次启动时安全地确保已认证并连接,无论是首次还是重复运行:
8
+
9
+ ```python
10
+ async def ensure_connected(client: AUNClient, aid: str) -> str:
11
+ try:
12
+ await client.auth.create_aid({"aid": aid})
13
+ except Exception as e:
14
+ print(f"创建 AID 失败: {e}")
15
+ raise
16
+ auth = await client.auth.authenticate({"aid": aid})
17
+ await client.connect(auth, {"auto_reconnect": True})
18
+ return aid
19
+ ```
20
+
21
+ ## 2. 安全关闭多个客户端
22
+
23
+ ```python
24
+ async def close_all(*clients: AUNClient):
25
+ for c in clients:
26
+ try:
27
+ await c.close()
28
+ except Exception:
29
+ pass
30
+ ```
31
+
32
+ ## 3. E2EE 幂等运行
33
+
34
+ 每次运行前清除旧 prekey 缓存并跳过历史消息,避免计数器冲突:
35
+
36
+ ```python
37
+ # 清除旧 prekey 缓存
38
+ sender.e2ee.invalidate_prekey_cache(peer_aid=receiver_aid)
39
+ receiver.e2ee.invalidate_prekey_cache(peer_aid=sender_aid)
40
+
41
+ # 跳过旧消息(获取当前最大 seq 作为 cursor 起点)
42
+ r = await receiver.call("message.pull", {"after_seq": 0, "limit": 1})
43
+ recv_cursor = r.get("latest_seq", 0)
44
+ ```
45
+
46
+ ## 4. 多 AID 管理
47
+
48
+ 一个 `aun_path` 可管理多个 AID,每个 AID 数据隔离在 `{aun_path}/AIDs/{aid}/` 下:
49
+
50
+ ```python
51
+ # 推荐:用应用名作为 aun_path,多个 AID 共存于同一目录
52
+ client = AUNClient({"aun_path": "~/.aun/myapp"})
53
+
54
+ # 创建不同的 AID,各自数据自动隔离
55
+ await client.auth.create_aid({"aid": "alice.agentid.pub"})
56
+ await client.auth.create_aid({"aid": "bob.agentid.pub"})
57
+ # 数据分别在 ~/.aun/myapp/AIDs/alice.agentid.pub/ 和 bob.agentid.pub/ 下
58
+ ```
59
+
60
+ > **注意**:不要用 AID 名称作为 `aun_path`(如 `~/.aun/{aid}`),否则会产生冗余嵌套。
61
+
62
+ ## 5. 环境变量驱动配置
63
+
64
+ ```python
65
+ import os
66
+ from pathlib import Path
67
+
68
+ DATA_ROOT = os.environ.get("AUN_DATA_ROOT", str(Path.home() / ".aun"))
69
+ ```
70
+
71
+ ## 6. 资源清理
72
+
73
+ ```python
74
+ async def main():
75
+ client = AUNClient()
76
+ try:
77
+ await ensure_connected(client, aid)
78
+ # ... 业务逻辑
79
+ finally:
80
+ await client.close()
81
+ ```
82
+
83
+ ## 参考
84
+
85
+ - 协议文档:`../src/aun_core/docs/protocol/`
86
+ - SDK 架构文档:`../src/aun_core/docs/skill/sdk-core/`
87
+ - RPC 方法手册:`../src/aun_core/docs/skill/rpc-manual/`
88
+ - 可运行示例:`../src/aun_core/docs/skill/examples/`
89
+ - GitHub:https://github.com/ModelUnion/aun-sdk-core
@@ -0,0 +1,445 @@
1
+ # AID 托管(Custody)HTTP API 手册
2
+
3
+ AID Custody 是 AUN AP 可选提供的 AID 备份、恢复与跨设备复制服务,不属于 AUN 核心协议强制能力。当前阶段定义两条主流程:通过手机号和验证码上传、下载 AID 证书以及客户端加密后的私钥文件;以及旧设备通过 AID token 授权,新设备凭一次性复制码领取 OTP 加密材料的跨设备复制流程。
4
+
5
+ 用户也可以部署自己的 AID 托管服务。只要服务地址发现格式和本手册定义的 HTTP 接口、请求响应语义保持一致,SDK 即可通过 `client.custody.set_url(...)` 或 well-known 自动发现接入自部署服务。
6
+
7
+ 核心原则:
8
+
9
+ - 私钥必须由客户端先加密,再上传到 custody 服务。
10
+ - custody 服务只保存证书 PEM、加密私钥密文和加密参数元数据,不知道用户密码,也不解密私钥。
11
+ - 恢复时,服务端只返回证书和加密私钥密文;客户端使用自己的密码和 `metadata` 解密。
12
+
13
+ 服务地址建议:
14
+
15
+ ```text
16
+ https://aid_custody.{issuer_domain}:{port}
17
+ ```
18
+
19
+ SDK 使用约束:
20
+
21
+ - `custody_url` 不作为 `AUNClient` 构造配置参数传入。
22
+ - 客户端通过 `client.custody.set_url(...)` 显式配置托管服务地址。
23
+ - 也可以通过 `client.custody.discover_url(aid=...)` 从 `https://{aid}/.well-known/aun-custody` 自动发现官方托管服务地址;发现结果会缓存在当前 `client.custody` 实例中。
24
+
25
+ ## 手机号验证码备份/恢复时序
26
+
27
+ 备份/绑定手机号:
28
+
29
+ ```mermaid
30
+ sequenceDiagram
31
+ autonumber
32
+ participant User as 用户
33
+ participant Client as 已登录设备 AUNClient
34
+ participant Custody as Custody 服务
35
+ participant SMS as 短信服务
36
+
37
+ User->>Client: 输入手机号
38
+ Client->>Custody: POST /custody/accounts/send-code<br/>Authorization: Bearer aun_token<br/>{ phone }
39
+ Custody->>Custody: 校验 AID token,生成绑定验证码
40
+ Custody->>SMS: 发送验证码
41
+ SMS-->>User: 手机验证码
42
+ Custody-->>Client: request_id / expires_in_seconds
43
+ User->>Client: 输入验证码和本地加密口令
44
+ Client->>Client: 本地加密私钥,生成 cert / key / metadata
45
+ Client->>Custody: POST /custody/accounts/bind-phone<br/>Authorization: Bearer aun_token<br/>{ phone, code, cert, key, metadata }
46
+ Custody->>Custody: 校验验证码、证书 AID、保存手机号绑定和密文备份
47
+ Custody-->>Client: binding / backup_result
48
+ ```
49
+
50
+ 恢复/下载:
51
+
52
+ ```mermaid
53
+ sequenceDiagram
54
+ autonumber
55
+ participant User as 用户
56
+ participant Client as 新设备 AUNClient
57
+ participant Custody as Custody 服务
58
+ participant SMS as 短信服务
59
+
60
+ User->>Client: 输入手机号和要恢复的 AID
61
+ Client->>Custody: POST /custody/accounts/send-code<br/>{ phone, aid }
62
+ Custody->>Custody: 校验手机号已绑定该 AID,生成恢复验证码
63
+ Custody->>SMS: 发送验证码
64
+ SMS-->>User: 手机验证码
65
+ Custody-->>Client: request_id / expires_in_seconds
66
+ User->>Client: 输入验证码和本地解密口令
67
+ Client->>Custody: POST /custody/accounts/restore-phone<br/>{ phone, code, aid }
68
+ Custody->>Custody: 校验验证码和绑定关系,读取证书与加密私钥密文
69
+ Custody-->>Client: aid / cert_pem / key_pem / metadata
70
+ Client->>Client: 校验证书归属,使用 metadata 和用户口令解密私钥
71
+ Client->>Client: 导入本地 AID keystore
72
+ ```
73
+
74
+ ## AID token 授权的跨设备复制方案
75
+
76
+ 除手机号验证码备份/恢复外,还可以在旧设备仍可用时,基于 AUN 的 AID 认证 token 实现一次性跨设备复制。该方案的核心目标是让 custody 只做短期密文中转:
77
+
78
+ - 旧设备用已通过 AUN 认证的 `aun_token` 证明自己控制该 AID。
79
+ - custody 生成短期复制码,并把复制码绑定到 AID、旧设备认证上下文和过期时间。
80
+ - 旧设备本地生成一次性 OTP,用 OTP 加密私钥后上传密文。
81
+ - 新设备只向 custody 提交 `aid + transfer_code` 领取密文,不提交 OTP。
82
+ - 新设备在本地用 OTP 解密私钥并导入 keystore。
83
+ - custody 在领取成功或过期后销毁复制材料。
84
+
85
+ 端点索引:
86
+
87
+ | 端点 | 方法 | 认证 | 说明 |
88
+ |------|------|------|------|
89
+ | `/custody/transfers` | POST | `aun_token` | 旧设备发起复制,返回短期复制码 |
90
+ | `/custody/transfers/{transfer_code}/materials` | POST | `aun_token` | 旧设备上传证书和 OTP 加密后的私钥密文 |
91
+ | `/custody/transfers/claim` | POST | 无 | 新设备凭 `aid + transfer_code` 领取密文材料,不传 OTP |
92
+
93
+ SDK 方法:
94
+
95
+ | 语言 | 发起复制 | 上传复制材料 | 领取复制材料 |
96
+ |------|----------|--------------|--------------|
97
+ | Python | `client.custody.create_device_copy(...)` | `client.custody.upload_device_copy_materials(...)` | `client.custody.claim_device_copy(...)` |
98
+ | JS/TS | `client.custody.createDeviceCopy(...)` | `client.custody.uploadDeviceCopyMaterials(...)` | `client.custody.claimDeviceCopy(...)` |
99
+ | Go | `client.Custody.CreateDeviceCopy(...)` | `client.Custody.UploadDeviceCopyMaterials(...)` | `client.Custody.ClaimDeviceCopy(...)` |
100
+
101
+ 复制请求:
102
+
103
+ ```http
104
+ POST /custody/transfers
105
+ Content-Type: application/json
106
+ Authorization: Bearer <aun_token>
107
+ ```
108
+
109
+ ```json
110
+ {
111
+ "aid": "alice.agentid.pub"
112
+ }
113
+ ```
114
+
115
+ 响应:
116
+
117
+ ```json
118
+ {
119
+ "transfer_code": "A7K9Q2ZB",
120
+ "expires_in_seconds": 300,
121
+ "expires_at": 1713000300000
122
+ }
123
+ ```
124
+
125
+ 上传复制材料:
126
+
127
+ ```http
128
+ POST /custody/transfers/A7K9Q2ZB/materials
129
+ Content-Type: application/json
130
+ Authorization: Bearer <aun_token>
131
+ ```
132
+
133
+ ```json
134
+ {
135
+ "aid": "alice.agentid.pub",
136
+ "cert": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
137
+ "key": "<OTP 加密后的私钥密文>",
138
+ "metadata": {
139
+ "envelope_version": 1,
140
+ "purpose": "device-transfer",
141
+ "encryption": "aes-256-gcm",
142
+ "kdf": "argon2id",
143
+ "kdf_params": {"m": 65536, "t": 3, "p": 4},
144
+ "salt": "...",
145
+ "nonce": "...",
146
+ "otp_hint": "一次性 OTP 只显示给用户,不上传服务端"
147
+ }
148
+ }
149
+ ```
150
+
151
+ 领取复制材料:
152
+
153
+ ```http
154
+ POST /custody/transfers/claim
155
+ Content-Type: application/json
156
+ ```
157
+
158
+ ```json
159
+ {
160
+ "aid": "alice.agentid.pub",
161
+ "transfer_code": "A7K9Q2ZB"
162
+ }
163
+ ```
164
+
165
+ 响应:
166
+
167
+ ```json
168
+ {
169
+ "aid": "alice.agentid.pub",
170
+ "cert_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
171
+ "key_pem": "<OTP 加密后的私钥密文>",
172
+ "key_encrypted": true,
173
+ "metadata": {
174
+ "envelope_version": 1,
175
+ "purpose": "device-transfer",
176
+ "encryption": "aes-256-gcm",
177
+ "kdf": "argon2id",
178
+ "kdf_params": {"m": 65536, "t": 3, "p": 4},
179
+ "salt": "...",
180
+ "nonce": "..."
181
+ }
182
+ }
183
+ ```
184
+
185
+ 跨设备复制时序:
186
+
187
+ ```mermaid
188
+ sequenceDiagram
189
+ autonumber
190
+ participant User as 用户
191
+ participant Old as 原有设备 AUNClient
192
+ participant Custody as Custody 服务
193
+ participant New as 新设备 AUNClient
194
+
195
+ Old->>Custody: POST /custody/transfers<br/>Authorization: Bearer aun_token<br/>{ aid }
196
+ Custody->>Custody: 校验 AID token 与 aid 绑定关系
197
+ Custody->>Custody: 生成 transfer_code,绑定 aid,设置 5 分钟 TTL
198
+ Custody-->>Old: transfer_code / expires_in_seconds
199
+ Old->>Old: 生成高熵 OTP,使用 OTP 本地加密私钥
200
+ Old->>Custody: POST /custody/transfers/{transfer_code}/materials<br/>Authorization: Bearer aun_token<br/>{ aid, cert, key, metadata }
201
+ Custody->>Custody: 保存证书和 OTP 加密后的私钥密文,不保存 OTP
202
+ Old-->>User: 显示 aid、transfer_code、OTP
203
+ User->>New: 输入 aid、transfer_code、OTP
204
+ New->>Custody: POST /custody/transfers/claim<br/>{ aid, transfer_code }
205
+ Custody->>Custody: 校验 transfer_code 未过期、未领取且绑定该 aid
206
+ Custody->>Custody: 标记已领取并销毁服务端复制材料
207
+ Custody-->>New: cert_pem / key_pem / metadata
208
+ New->>New: 用 OTP 解密私钥,校验证书 AID 和私钥匹配
209
+ New->>New: 导入本地 AID keystore
210
+ ```
211
+
212
+ 安全等级判断:
213
+
214
+ - 该方案通常强于单纯手机号验证码恢复,因为发起方必须持有已认证的旧设备 AID token。
215
+ - 该方案的端到端保密性取决于 OTP 强度和客户端加密实现;custody 不拿到 OTP 时,无法解密私钥。
216
+ - 8 位字母数字复制码约 48 bit 熵,只适合作为短期在线领取码;必须配合 5 分钟 TTL、按 AID/IP 限速、错误次数上限和一次性领取。
217
+ - OTP 不能是 6 位短信码级别的低熵密码。建议由旧设备生成至少 128 bit 随机值,再用 Base32、分组字符或助记词展示;并使用 Argon2id 等 KDF 派生加密密钥。
218
+
219
+ 主要风险与约束:
220
+
221
+ - 如果旧设备已被攻破,攻击者可发起复制并拿到 OTP;该方案无法抵抗旧设备完全失陷。
222
+ - 如果用户把 `transfer_code + OTP` 同时泄露给攻击者,攻击者可领取并解密私钥。
223
+ - 如果只泄露 `transfer_code`,攻击者可能抢先领取密文并造成拒绝服务;因此领取应一次性、限速,并写审计日志。
224
+ - custody 数据库或日志泄露时,攻击者可拿到短期密文;若 OTP 熵不足,会产生离线爆破风险。
225
+ - 所有请求必须走 HTTPS,客户端必须校验证书,避免转移码被中间人抢用。
226
+ - custody 不得把 OTP、明文私钥、解密后的私钥、完整转移密文写入日志。
227
+ - 新设备导入前必须校验证书归属、证书链、返回的 `aid`、以及解密后的私钥与证书公钥是否匹配。
228
+ - 如果产品语义要求“复制后让旧设备失效”,完成后还需要触发密钥轮换、旧证书吊销或旧设备注销流程;否则旧设备继续可用。
229
+
230
+ CT 日志记录:
231
+
232
+ - `aid.backup`:备份证书和客户端加密私钥时记录。
233
+ - `aid.restore` / `aid.restore_by_phone`:通过会话或手机号验证码恢复时记录。
234
+ - `device_copy.create`:旧设备创建复制会话时记录,不包含复制码。
235
+ - `device_copy.materials_uploaded`:旧设备上传 OTP 加密材料时记录,不包含 OTP 和密文正文。
236
+ - `device_copy.claim`:新设备领取复制材料时记录,不包含 OTP 和密文正文。
237
+
238
+ ## 端点索引
239
+
240
+ | 端点 | 方法 | 认证 | 说明 |
241
+ |------|------|------|------|
242
+ | `/custody/accounts/send-code` | POST | 绑定场景需要 `aun_token`;恢复场景不需要 | 发送手机验证码 |
243
+ | `/custody/accounts/bind-phone` | POST | `aun_token` | 绑定手机号并上传 AID 证书、加密私钥 |
244
+ | `/custody/accounts/restore-phone` | POST | 无 | 手机号 + 验证码 + AID 下载证书和加密私钥 |
245
+ | `/custody/transfers` | POST | `aun_token` | 旧设备发起一次性跨设备复制 |
246
+ | `/custody/transfers/{transfer_code}/materials` | POST | `aun_token` | 旧设备上传 OTP 加密后的证书和私钥密文 |
247
+ | `/custody/transfers/claim` | POST | 无 | 新设备领取复制材料,不传 OTP |
248
+
249
+ ## send-code
250
+
251
+ 发送手机号验证码。该端点兼容两种场景:
252
+
253
+ - 绑定/上传场景:携带 `Authorization: Bearer <aun_token>`,请求体不传 `aid`。
254
+ - 恢复/下载场景:不携带 token,请求体传 `aid`,服务端校验手机号已绑定该 AID。
255
+
256
+ 请求:
257
+
258
+ ```http
259
+ POST /custody/accounts/send-code
260
+ Content-Type: application/json
261
+ Authorization: Bearer <aun_token>
262
+ ```
263
+
264
+ 绑定/上传场景:
265
+
266
+ ```json
267
+ {
268
+ "phone": "+8613800138000"
269
+ }
270
+ ```
271
+
272
+ 恢复/下载场景:
273
+
274
+ ```json
275
+ {
276
+ "phone": "+8613800138000",
277
+ "aid": "alice.agentid.pub"
278
+ }
279
+ ```
280
+
281
+ 参数:
282
+
283
+ | 参数 | 类型 | 必填 | 说明 |
284
+ |------|------|------|------|
285
+ | `phone` | string | 是 | E.164 格式手机号 |
286
+ | `aid` | string | 否 | 恢复/下载场景填写。填写后不需要 token |
287
+
288
+ 响应:
289
+
290
+ ```json
291
+ {
292
+ "request_id": "bind-phone-a1b2c3d4e5f6",
293
+ "phone": "+8613800138000",
294
+ "provider": "mock",
295
+ "expires_in_seconds": 300,
296
+ "purpose": "bind-phone",
297
+ "debug_code": "123456"
298
+ }
299
+ ```
300
+
301
+ 说明:
302
+
303
+ - `debug_code` 只应在开发环境返回;生产环境应为空。
304
+ - 同一手机号和 IP 会受发送频率限制。
305
+
306
+ ## bind-phone
307
+
308
+ 验证手机验证码,绑定手机号到当前 AID,并上传 AID 证书和客户端加密后的私钥密文。
309
+
310
+ 请求:
311
+
312
+ ```http
313
+ POST /custody/accounts/bind-phone
314
+ Content-Type: application/json
315
+ Authorization: Bearer <aun_token>
316
+ ```
317
+
318
+ ```json
319
+ {
320
+ "phone": "+8613800138000",
321
+ "code": "123456",
322
+ "cert": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
323
+ "key": "<客户端加密后的私钥密文>",
324
+ "metadata": {
325
+ "encryption": "aes-256-gcm",
326
+ "kdf": "argon2id",
327
+ "kdf_params": {"m": 65536, "t": 3, "p": 4},
328
+ "device": "iPhone 15",
329
+ "note": "主密钥备份"
330
+ }
331
+ }
332
+ ```
333
+
334
+ 参数:
335
+
336
+ | 参数 | 类型 | 必填 | 说明 |
337
+ |------|------|------|------|
338
+ | `phone` | string | 是 | E.164 格式手机号 |
339
+ | `code` | string | 是 | 6 位数字验证码 |
340
+ | `cert` | string | 是 | AID 证书 PEM |
341
+ | `key` | string | 是 | 客户端加密后的私钥密文 |
342
+ | `metadata` | object | 否 | 加密算法、KDF 参数、设备信息等,服务端原样保存 |
343
+
344
+ 响应:
345
+
346
+ ```json
347
+ {
348
+ "binding": {
349
+ "provider": "phone",
350
+ "external_subject": "+8613800138000",
351
+ "aid": "alice.agentid.pub",
352
+ "status": "active",
353
+ "created_at": 1713000000000,
354
+ "updated_at": 1713000000000
355
+ },
356
+ "backup_result": {
357
+ "aid": "alice.agentid.pub",
358
+ "status": "active",
359
+ "source": "backup",
360
+ "cert_sn": "1a2b3c4d",
361
+ "curve": "P-256",
362
+ "key_encrypted": true,
363
+ "metadata": {"encryption": "aes-256-gcm"}
364
+ }
365
+ }
366
+ ```
367
+
368
+ 校验要求:
369
+
370
+ - `aun_token` 必须能证明当前调用者就是要绑定的 AID。
371
+ - `cert` 中的 CN 必须是该 AID。
372
+ - `key` 必须是密文;服务端不应要求、也不应保存用户密码。
373
+
374
+ ## restore-phone
375
+
376
+ 凭手机号、验证码和 AID 下载已备份的证书和加密私钥。该接口不要求 AID 登录,因为恢复场景下用户可能已经丢失私钥。
377
+
378
+ 请求:
379
+
380
+ ```http
381
+ POST /custody/accounts/restore-phone
382
+ Content-Type: application/json
383
+ ```
384
+
385
+ ```json
386
+ {
387
+ "phone": "+8613800138000",
388
+ "code": "123456",
389
+ "aid": "alice.agentid.pub"
390
+ }
391
+ ```
392
+
393
+ 参数:
394
+
395
+ | 参数 | 类型 | 必填 | 说明 |
396
+ |------|------|------|------|
397
+ | `phone` | string | 是 | E.164 格式手机号 |
398
+ | `code` | string | 是 | 6 位数字验证码 |
399
+ | `aid` | string | 是 | 要恢复的 AID |
400
+
401
+ 响应:
402
+
403
+ ```json
404
+ {
405
+ "aid": "alice.agentid.pub",
406
+ "cert_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
407
+ "key_pem": "<客户端加密后的私钥密文>",
408
+ "key_encrypted": true,
409
+ "cert_sn": "1a2b3c4d",
410
+ "curve": "P-256",
411
+ "metadata": {
412
+ "encryption": "aes-256-gcm",
413
+ "kdf": "argon2id",
414
+ "kdf_params": {"m": 65536, "t": 3, "p": 4},
415
+ "device": "iPhone 15"
416
+ }
417
+ }
418
+ ```
419
+
420
+ 客户端处理:
421
+
422
+ 1. 校验返回的 `aid` 与请求一致。
423
+ 2. 保存 `cert_pem`。
424
+ 3. 使用用户密码和 `metadata` 中的 KDF 参数解密 `key_pem`。
425
+ 4. 将解密后的私钥导入本地 AID keystore。
426
+
427
+ ## 错误语义
428
+
429
+ | HTTP 状态 | 错误码示例 | 说明 |
430
+ |-----------|------------|------|
431
+ | 400 | `invalid_phone` | 手机号格式无效 |
432
+ | 400 | `invalid_code` | 验证码无效或已过期 |
433
+ | 400 | `invalid_crt` | 证书 PEM 无效 |
434
+ | 401 | `invalid_token` | `aun_token` 无效 |
435
+ | 403 | `phone_not_bound` | 手机号未绑定到该 AID |
436
+ | 403 | `wrong_auth_type` | 需要 AID 登录场景却未提供 AID token |
437
+ | 404 | `backup_not_found` | 未找到该 AID 的备份 |
438
+ | 429 | `rate_limited` | 请求频率超限 |
439
+
440
+ ## 安全边界
441
+
442
+ - custody 服务不是 AID 身份发行方;AID 身份仍由 auth/CA 体系签发和验证。
443
+ - 手机号不是 AID 身份,只是恢复凭据。
444
+ - 服务端被攻破时,攻击者最多拿到证书和加密私钥密文;密文强度取决于客户端加密算法、用户密码和 KDF 参数。
445
+ - 用户忘记加密密码时,服务端无法解密私钥,也不应提供后门恢复。