@agentunion/fastaun-browser 0.5.0 → 0.5.2

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 (140) hide show
  1. package/CHANGELOG.md +550 -464
  2. package/_packed_docs/CHANGELOG-validators.md +147 -0
  3. package/_packed_docs/CHANGELOG.md +550 -464
  4. package/_packed_docs/INDEX.md +201 -177
  5. package/_packed_docs/KITE_DOCS_GUIDE.md +38 -32
  6. package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +169 -116
  7. package/_packed_docs/aun-perf-audit-critical-bugs.md +315 -0
  8. package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +263 -260
  9. package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +331 -0
  10. package/_packed_docs/protocol/00-/346/200/273/350/247/210/344/270/216/345/210/206/345/261/202.md +2 -2
  11. 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 +1 -1
  12. 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 +39 -16
  13. package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +8 -5
  14. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +18 -19
  15. 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 +1 -1
  16. package/_packed_docs/protocol/08-AUN-E2EE-Group.md +139 -746
  17. package/_packed_docs/protocol/08-AUN-E2EE.md +12 -10
  18. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +117 -171
  19. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +6 -0
  20. package/_packed_docs/protocol/15-/347/246/273/347/272/277/346/216/250/351/200/201/351/200/232/347/237/245/345/215/217/350/256/256.md +1 -1
  21. package/_packed_docs/protocol/16-/347/263/273/347/273/237/347/233/256/345/275/225/344/277/235/346/212/244/346/226/271/346/241/210.md +177 -0
  22. package/_packed_docs/protocol/README.md +5 -4
  23. package/_packed_docs/protocol/aun-docs-guide.md +2 -2
  24. package/_packed_docs/protocol/index.md +12 -7
  25. 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 +1 -1
  26. package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +13 -13
  27. package/_packed_docs/protocol//351/231/204/345/275/225L-E2EE/345/256/236/347/216/260/346/214/207/345/215/227.md +9 -9
  28. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +15 -13
  29. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +26 -18
  30. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +35 -284
  31. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +473 -442
  32. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +11 -7
  33. package/_packed_docs/sdk/09-collab-rpc-manual.md +581 -550
  34. package/_packed_docs/sdk/09-group-rpc-manual.md +367 -433
  35. package/_packed_docs/sdk/09-message-rpc-manual.md +50 -28
  36. package/_packed_docs/sdk/09-payload-reference.md +3 -3
  37. package/_packed_docs/sdk/09-storage-rpc-manual.md +57 -20
  38. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +18 -17
  39. package/_packed_docs/sdk/E2EE_V2/346/266/210/346/201/257/351/200/232/344/277/241/346/227/266/345/272/217/345/233/276.md +3 -2
  40. package/_packed_docs/sdk/INDEX.md +25 -24
  41. package/_packed_docs/sdk/Notify/351/200/232/347/237/245/346/226/271/346/241/210.md +6 -2
  42. package/dist/agent-md.d.ts.map +1 -1
  43. package/dist/agent-md.js +18 -9
  44. package/dist/agent-md.js.map +1 -1
  45. package/dist/bundle.js +1661 -1180
  46. package/dist/client/delivery.d.ts +13 -2
  47. package/dist/client/delivery.d.ts.map +1 -1
  48. package/dist/client/delivery.js +251 -46
  49. package/dist/client/delivery.js.map +1 -1
  50. package/dist/client/group-state.d.ts.map +1 -1
  51. package/dist/client/group-state.js +36 -14
  52. package/dist/client/group-state.js.map +1 -1
  53. package/dist/client/lifecycle.js +2 -2
  54. package/dist/client/lifecycle.js.map +1 -1
  55. package/dist/client/rpc-pipeline.d.ts +1 -0
  56. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  57. package/dist/client/rpc-pipeline.js +166 -50
  58. package/dist/client/rpc-pipeline.js.map +1 -1
  59. package/dist/client/v2-e2ee.d.ts +14 -1
  60. package/dist/client/v2-e2ee.d.ts.map +1 -1
  61. package/dist/client/v2-e2ee.js +376 -126
  62. package/dist/client/v2-e2ee.js.map +1 -1
  63. package/dist/client.d.ts +5 -4
  64. package/dist/client.d.ts.map +1 -1
  65. package/dist/client.js +187 -46
  66. package/dist/client.js.map +1 -1
  67. package/dist/collab/client.d.ts +8 -0
  68. package/dist/collab/client.d.ts.map +1 -1
  69. package/dist/collab/client.js +12 -0
  70. package/dist/collab/client.js.map +1 -1
  71. package/dist/errors.d.ts +0 -20
  72. package/dist/errors.d.ts.map +1 -1
  73. package/dist/errors.js +8 -51
  74. package/dist/errors.js.map +1 -1
  75. package/dist/facades.d.ts +9 -4
  76. package/dist/facades.d.ts.map +1 -1
  77. package/dist/facades.js +192 -31
  78. package/dist/facades.js.map +1 -1
  79. package/dist/group-fs.d.ts +17 -0
  80. package/dist/group-fs.d.ts.map +1 -1
  81. package/dist/group-fs.js +54 -11
  82. package/dist/group-fs.js.map +1 -1
  83. package/dist/group-id.d.ts +9 -12
  84. package/dist/group-id.d.ts.map +1 -1
  85. package/dist/group-id.js +41 -63
  86. package/dist/group-id.js.map +1 -1
  87. package/dist/index.d.ts +4 -2
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +4 -1
  90. package/dist/index.js.map +1 -1
  91. package/dist/keystore/index.d.ts +2 -54
  92. package/dist/keystore/index.d.ts.map +1 -1
  93. package/dist/keystore/indexeddb-identity-store.d.ts +3 -0
  94. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  95. package/dist/keystore/indexeddb-identity-store.js +65 -0
  96. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  97. package/dist/keystore/indexeddb-shared.d.ts +3 -17
  98. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  99. package/dist/keystore/indexeddb-shared.js +4 -47
  100. package/dist/keystore/indexeddb-shared.js.map +1 -1
  101. package/dist/keystore/indexeddb-token-store.d.ts +1 -64
  102. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  103. package/dist/keystore/indexeddb-token-store.js +45 -774
  104. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  105. package/dist/logger.d.ts +2 -0
  106. package/dist/logger.d.ts.map +1 -1
  107. package/dist/logger.js +4 -0
  108. package/dist/logger.js.map +1 -1
  109. package/dist/storage/lowlevel.d.ts +9 -1
  110. package/dist/storage/lowlevel.d.ts.map +1 -1
  111. package/dist/storage/lowlevel.js +12 -1
  112. package/dist/storage/lowlevel.js.map +1 -1
  113. package/dist/storage/vfs.d.ts +22 -0
  114. package/dist/storage/vfs.d.ts.map +1 -1
  115. package/dist/storage/vfs.js +54 -0
  116. package/dist/storage/vfs.js.map +1 -1
  117. package/dist/tools/cross-sdk-agent.js +336 -49
  118. package/dist/tools/cross-sdk-agent.js.map +1 -1
  119. package/dist/transport.d.ts +2 -0
  120. package/dist/transport.d.ts.map +1 -1
  121. package/dist/transport.js +96 -3
  122. package/dist/transport.js.map +1 -1
  123. package/dist/types.d.ts +39 -56
  124. package/dist/types.d.ts.map +1 -1
  125. package/dist/v2/session/session.d.ts +2 -0
  126. package/dist/v2/session/session.d.ts.map +1 -1
  127. package/dist/v2/session/session.js +58 -22
  128. package/dist/v2/session/session.js.map +1 -1
  129. package/dist/v2/state/commitment.d.ts +1 -1
  130. package/dist/v2/state/commitment.d.ts.map +1 -1
  131. package/dist/v2/state/commitment.js +5 -3
  132. package/dist/v2/state/commitment.js.map +1 -1
  133. package/dist/validators.d.ts +35 -0
  134. package/dist/validators.d.ts.map +1 -0
  135. package/dist/validators.js +127 -0
  136. package/dist/validators.js.map +1 -0
  137. package/dist/version.d.ts +1 -1
  138. package/dist/version.js +1 -1
  139. package/package.json +1 -1
  140. package/_packed_docs/collab-gateway-boundary-test-report.md +0 -164
@@ -41,14 +41,14 @@ await client.call("message.send", {
41
41
  })
42
42
  ```
43
43
 
44
- SDK 优先使用 prekey_ecdh_v2,并默认要求前向保密:
45
-
46
- 1. **prekey_ecdh_v2** 对方有预上传的 prekey,四路 ECDH(ephemeral×prekey + ephemeral×identity + sender×prekey + sender×identity),前向安全,附带发送方签名
47
- 2. **long_term_key** 对方无 prekey,双路 ECDH(ephemeral×recipient_identity + sender×recipient_identity)+ HKDF 派生密钥(降级模式),附带发送方签名
48
-
49
- > Python SDK 默认 `require_forward_secrecy=true`,当加密结果不满足前向保密(无论是因为无 prekey 还是 prekey 加密失败降级到 long_term_key)时拒绝发送并抛出错误。需显式配置 `require_forward_secrecy=false` 才允许降级。
50
-
51
- 每条消息独立生成临时 ECDH 密钥对,实现一消息一密钥。
44
+ 当前 SDK 使用 E2EE V2 多设备 wrap 主路径:
45
+
46
+ 1. 发送前通过 `message.v2.bootstrap` `group.v2.bootstrap` 获取接收方当前活跃设备、设备 prekey、self-sync 设备和 audit recipients。
47
+ 2. SDK 为每条消息生成独立 `master_key`、消息 nonce 和发送方临时 session key,只加密一次正文。
48
+ 3. SDK 为每个接收设备生成一条 recipient wrap。设备有可用 SPK 时使用 `3DH` wrap;缺少 SPK 的兼容场景才使用 `1DH` wrap。
49
+ 4. E2EE 信封包含 `sender_signature`、AAD、`recipients_digest` / Merkle proof,接收端必须验签、验 AAD、验 recipient proof 后再解密。
50
+
51
+ 每条消息独立密钥;V2 当前主路径不再使用旧 `prekey_ecdh_v2` / `long_term_key` 信封作为默认发送格式。旧术语只用于历史兼容文档或迁移排查。
52
52
 
53
53
  ## ProtectedHeaders 与可验证上下文
54
54
 
@@ -56,18 +56,20 @@ SDK 优先使用 prekey_ecdh_v2,并默认要求前向保密:
56
56
 
57
57
  `protected_headers` 会随 E2EE 信封发送,接收端可以读取,因此它提供完整性保护,不提供机密性保护。不要把访问令牌、私钥、隐私正文或其他只允许端到端可见的内容放入 `protected_headers`;这类内容应放进加密的 `payload`。
58
58
 
59
- 推荐通过 SDK 实例级 setter 设置稳定元数据,例如 `client.set_protected_headers(...)` / `client.setProtectedHeaders(...)` / `client.SetProtectedHeaders(...)`。发送方也可以在以下 SDK 调用中传入顶层 `protected_headers` 作为单次发送的高级覆盖;`headers` 仅作为兼容旧调用的别名,不推荐新代码使用:
59
+ 推荐通过 SDK 实例级 setter 设置稳定元数据,例如 `client.set_protected_headers(...)` / `client.setProtectedHeaders(...)` / `client.SetProtectedHeaders(...)`。发送方也可以在以下 SDK 调用中传入顶层 `protected_headers` 作为单次发送的高级覆盖;`headers` 仅作为兼容旧调用的别名,不推荐新代码使用:
60
60
 
61
61
  - `message.send`
62
62
  - `message.thought.put`
63
63
  - `group.send`
64
64
  - `group.thought.put`
65
65
 
66
- `payload_type` 不需要应用层传入。SDK 会读取加密前 `payload.type`,自动写入 `protected_headers.payload_type`,接收端解密后会校验它与明文 `payload.type` 一致。
67
-
68
- `protected_headers` / `headers` 是 send/thought 参数的顶层字段,不放入单独的 `envelope` 入参对象,也不属于业务 `payload`。裸 WebSocket 客户端若自行发送已加密信封,需要把 protected headers 放在自构造的 E2EE 信封内并自行完成 `_auth`,服务端不会替裸 RPC 调用生成或校验明文侧的 protected headers。
66
+ `payload_type` 不需要应用层传入。SDK 会读取加密前 `payload.type`,自动写入 `protected_headers.payload_type`,接收端解密后会校验它与明文 `payload.type` 一致。
69
67
 
70
- 示例:
68
+ `protected_headers` / `headers` 是 send/thought 参数的顶层字段,不放入单独的 `envelope` 入参对象,也不属于业务 `payload`。裸 WebSocket 客户端若自行发送已加密信封,需要把 protected headers 放在自构造的 E2EE 信封内并自行完成 `_auth`,服务端不会替裸 RPC 调用生成或校验明文侧的 protected headers。
69
+
70
+ `agent_md` 是独立于 E2EE 的版本提示元数据,不属于 `protected_headers`,也不参与 AAD 或业务鉴权。当前 Message Service V2 P2P 信封可携带 `agent_md.sender`;SDK 也识别 `agent_md.group`。Gateway 在 RPC response / event push 的 `_meta.agent_md_etags` 中注入 `requester`、`peer`、`group`,四端 SDK 会自动写入对应 AID 的 `remote_etag` / `last_modified`,后续仍以下载后的 agent.md 签名验证作为可信依据。
71
+
72
+ 示例:
71
73
 
72
74
  ```python
73
75
  from aun_core import ProtectedHeaders
@@ -103,7 +105,8 @@ await client.call("group.send", {
103
105
 
104
106
  ```json
105
107
  {
106
- "type": "e2ee.encrypted",
108
+ "type": "e2ee.p2p_encrypted",
109
+ "version": "v2",
107
110
  "ciphertext": "...",
108
111
  "protected_headers": {
109
112
  "device_id": "dev-123",
@@ -127,8 +130,8 @@ await client.call("group.send", {
127
130
 
128
131
  计算规则:
129
132
 
130
- 1. 解密流程会派生出本条消息的 `message_key`。
131
- 2. `metadata_key = HMAC-SHA256(message_key, "aun-envelope-metadata-key-v1")`。
133
+ 1. 解密流程会得到本条消息的 `master_key`。
134
+ 2. `metadata_key = HMAC-SHA256(master_key, "aun-envelope-metadata-key-v1")`。
132
135
  3. 对字典去掉 `_auth` 后做 canonical JSON:UTF-8、key 排序、紧凑分隔符。
133
136
  4. `tag = HMAC-SHA256(metadata_key, domain + "\0" + canonical_json(body))`。
134
137
  5. `domain` 对 `protected_headers` 为 `aun-protected-headers-v1`,对 `context` 为 `aun-protected-context-v1`。
@@ -268,20 +271,6 @@ result = await client.call("group.thought.get", {
268
271
 
269
272
  SDK 返回的 `result["thoughts"]` 是已解密数组。`group.thought.get` 是查询操作,重复读取同一条 thought 不按消息 replay 消费处理。
270
273
 
271
- ### 手动操作(通常不需要)
272
-
273
- `rotate_epoch()` 是纯本地密码学 API。多客户端并发环境下,应通过服务端 CAS(`group.e2ee.get_epoch` + `group.e2ee.rotate_epoch`)确保原子性,SDK 自动编排已使用 CAS 路径。
274
-
275
- ```python
276
- # 手动轮换 epoch(纯本地,不经过服务端 CAS 校验)
277
- info = client.group_e2ee.rotate_epoch("g-abc123.agentid.pub", member_aids)
278
-
279
- # 查询状态
280
- client.group_e2ee.has_secret("g-abc123.agentid.pub") # 是否持有密钥
281
- client.group_e2ee.current_epoch("g-abc123.agentid.pub") # 当前 epoch
282
- client.group_e2ee.get_member_aids("g-abc123.agentid.pub") # 已知成员列表
283
- ```
284
-
285
274
  ## 接收加密消息
286
275
 
287
276
  ### 推送接收
@@ -308,258 +297,20 @@ for msg in result["messages"]:
308
297
 
309
298
  ---
310
299
 
311
- ## Prekey 管理
312
-
313
- 连接时 SDK 自动上传 prekey,并定时轮换(默认每小时)。一般无需手动管理。
314
-
315
- 手动上传 prekey:
316
-
317
- ```python
318
- # 通过 AUNClient 上传(生成 + RPC)
319
- await client._upload_prekey()
320
- ```
321
-
322
- 底层 API(E2EEManager 只生成材料,不做 RPC):
323
-
324
- ```python
325
- prekey_material = client.e2ee.generate_prekey()
326
- # 返回 {"prekey_id": "...", "public_key": "...", "signature": "...", "created_at": ...}
327
- # 需要自行上传:await transport.call("message.e2ee.put_prekey", prekey_material)
328
-
329
- ---
330
-
331
- ## 裸 WebSocket 开发者指南
332
-
333
- > 本节面向不使用 AUNClient 连接管理、自行管理 WebSocket 连接的开发者。
334
-
335
- ### 架构说明
336
-
337
- `E2EEManager` 是纯密码学工具类,无 I/O 依赖。构造函数只需:
338
-
339
- - `identity_fn` — 返回当前身份信息的函数
340
- - `keystore` — 密钥存储实现
341
-
342
- 所有 I/O(获取证书、获取 prekey、RPC 调用)由调用方自行处理。
343
-
344
- ### 实例化 E2EEManager
345
-
346
- ```python
347
- from aun_core.e2ee import E2EEManager
348
- from aun_core.keystore.local_token_store import LocalTokenStore
349
-
350
- e2ee = E2EEManager(
351
- identity_fn=lambda: my_identity,
352
- keystore=LocalTokenStore("~/.aun/myapp"),
353
- )
354
- ```
355
-
356
- ### 加密消息
357
-
358
- ```python
359
- # 1. 获取对方证书(HTTP)和 prekey(RPC,可选)
360
- peer_cert_pem = await fetch_cert(peer_aid) # 调用方实现
361
- prekey = await fetch_prekey(peer_aid) # 调用方实现,可能为 None
362
-
363
- # 2. 加密(传入 prekey 会自动缓存,后续可传 None 复用缓存)
364
- envelope, ok = e2ee.encrypt_message(
365
- to_aid="bob.agentid.pub",
366
- payload={"type": "text", "text": "秘密消息"},
367
- peer_cert_pem=peer_cert_pem,
368
- prekey=prekey,
369
- )
370
-
371
- # 3. 通过自己的 WebSocket 发送
372
- aad = envelope["aad"]
373
- await my_rpc_call("message.send", {
374
- "to": "bob.agentid.pub",
375
- "payload": envelope,
376
- "type": "e2ee.encrypted",
377
- "encrypted": True,
378
- "message_id": aad["message_id"],
379
- "timestamp": aad["timestamp"],
380
- })
381
- ```
382
-
383
- P2P 消息的投递语义来自连接阶段声明的 `delivery_mode`:
384
-
385
- - `fanout`:广播到在线实例,并保留离线历史
386
- - `queue`:只做单实例实时消费,不进入历史
387
-
388
- ### 解密消息
389
-
390
- ```python
391
- # 解密单条消息(内置本地防重放)
392
- decrypted = e2ee.decrypt_message(raw_message)
393
- if decrypted is not None:
394
- print(decrypted["payload"]) # 明文
395
- ```
396
-
397
- ### Prekey 缓存
398
-
399
- E2EEManager 内置 prekey 缓存(TTL 默认 1 小时):
400
-
401
- ```python
402
- # 首次传入 prekey → 自动缓存
403
- envelope, ok = e2ee.encrypt_message(..., prekey=fetched_prekey)
404
-
405
- # 后续传 None → 自动复用缓存
406
- envelope, ok = e2ee.encrypt_message(..., prekey=None)
407
-
408
- # 手动管理
409
- e2ee.cache_prekey("bob.agentid.pub", prekey_dict)
410
- cached = e2ee.get_cached_prekey("bob.agentid.pub")
411
- e2ee.invalidate_prekey_cache("bob.agentid.pub")
412
- ```
413
-
414
- ### 生成 Prekey
415
-
416
- ```python
417
- prekey_material = e2ee.generate_prekey()
418
- # 返回 {"prekey_id": "...", "public_key": "...", "signature": "...", "created_at": ...}
419
- # 自行上传到服务端
420
- await my_rpc_call("message.e2ee.put_prekey", prekey_material)
421
- ```
422
-
423
- ### 群组 E2EE(GroupE2EEManager)
424
-
425
- `GroupE2EEManager` 是群组 E2EE 的纯工具类,与 `E2EEManager` 平行。密码学和状态管理全自动,网络发送由调用方负责。
426
-
427
- #### 实例化
428
-
429
- ```python
430
- from aun_core.e2ee import GroupE2EEManager
431
- from aun_core.keystore.local_token_store import LocalTokenStore
432
-
433
- group_e2ee = GroupE2EEManager(
434
- identity_fn=lambda: my_identity,
435
- keystore=LocalTokenStore("~/.aun/myapp"),
436
- )
437
- ```
438
-
439
- #### 建群后创建 epoch 并分发
440
-
441
- ```python
442
- info = group_e2ee.create_epoch(group_id, member_aids)
443
- # info = {epoch, commitment, distributions: [{to, payload}, ...]}
444
-
445
- for dist in info["distributions"]:
446
- # 通过 P2P E2EE 发送密钥分发消息
447
- envelope, _ = e2ee.encrypt_message(
448
- to_aid=dist["to"], payload=dist["payload"],
449
- peer_cert_pem=fetch_cert(dist["to"]),
450
- )
451
- await my_rpc_call("message.send", {
452
- "to": dist["to"], "payload": envelope,
453
- "type": "e2ee.encrypted", "encrypted": True,
454
- ...
455
- })
456
- ```
457
-
458
- #### 加密群消息
459
-
460
- ```python
461
- envelope = group_e2ee.encrypt(group_id, {"type": "text", "text": "hello"})
462
- await my_rpc_call("group.send", {
463
- "group_id": group_id,
464
- "payload": envelope,
465
- "type": "e2ee.group_encrypted",
466
- "encrypted": True,
467
- })
468
- ```
469
-
470
- #### 解密群消息
471
-
472
- ```python
473
- decrypted = group_e2ee.decrypt(raw_group_message)
474
- # 内置防重放 + 外层 group_id/from/sender_aid 校验
475
- # 非加密消息原样返回,解密失败返回 None
476
-
477
- # 批量解密(用于 group.pull)
478
- results = group_e2ee.decrypt_batch(messages)
479
- ```
480
-
481
- #### 处理 P2P 密钥消息
482
-
483
- 所有密钥协议消息(分发/请求/响应)通过 P2P E2EE 传输。收到后先 P2P 解密,再交给 `handle_incoming`:
484
-
485
- ```python
486
- inner = e2ee.decrypt_message(p2p_message) # P2P 层解密
487
- result = group_e2ee.handle_incoming(inner["payload"])
488
-
489
- if result == "distribution":
490
- pass # 密钥已自动存储
491
- elif result == "distribution_rejected":
492
- pass # epoch 降级被拒
493
- elif result == "response":
494
- pass # 密钥恢复响应已存储
495
- elif result == "request":
496
- # 需要回复:查成员列表 → 构建响应 → P2P 发送
497
- members = get_group_members(group_id)
498
- response = group_e2ee.handle_key_request_msg(inner["payload"], members)
499
- if response:
500
- p2p_e2ee_send(inner["payload"]["requester_aid"], response)
501
- ```
502
-
503
- #### 踢人后轮换
504
-
505
- ```python
506
- info = group_e2ee.rotate_epoch(group_id, remaining_member_aids)
507
- for dist in info["distributions"]:
508
- p2p_e2ee_send(dist["to"], dist["payload"])
509
- ```
510
-
511
- 配合服务端 CAS 防脑裂(推荐):
512
-
513
- ```python
514
- # 1. 读当前 epoch
515
- status = await my_rpc_call("group.e2ee.get_epoch", {"group_id": group_id})
516
- # 2. CAS 递增(服务端校验 admin/owner 角色 + 原子递增 + rotation_signature 验签)
517
- cas = await my_rpc_call("group.e2ee.rotate_epoch", {
518
- "group_id": group_id, "current_epoch": status["epoch"],
519
- # Python SDK 自动附加 rotation_signature 和 rotation_timestamp
520
- # 裸客户端必须自行签名:sign("{group_id}|{current_epoch}|{new_epoch}|{aid}|{timestamp}")
521
- })
522
- if cas["success"]:
523
- info = group_e2ee.rotate_epoch_to(group_id, cas["epoch"], member_aids)
524
- for dist in info["distributions"]:
525
- p2p_e2ee_send(dist["to"], dist["payload"])
526
- # CAS 失败说明别的 admin 先轮换了,放弃即可
527
- ```
528
-
529
- #### 加人后轮换并分发密钥
530
-
531
- ```python
532
- # 成员加入改变成员集,推荐先通过服务端 CAS 推进 epoch。
533
- status = await my_rpc_call("group.e2ee.get_epoch", {"group_id": group_id})
534
- cas = await my_rpc_call("group.e2ee.rotate_epoch", {"group_id": group_id, "current_epoch": status["epoch"], ...})
535
- if cas["success"]:
536
- info = group_e2ee.rotate_epoch_to(group_id, cas["epoch"], updated_member_aids)
537
- for dist in info["distributions"]:
538
- p2p_e2ee_send(dist["to"], dist["payload"])
539
- ```
540
-
541
- #### 解密失败时请求恢复
542
-
543
- 当前 Python SDK 优先请求本地成员列表中的第一个候选者;零状态时退化为请求当前消息发送者。
544
-
545
- ```python
546
- recovery = group_e2ee.build_recovery_request(
547
- group_id, epoch, sender_aid=msg.get("sender_aid"),
548
- )
549
- if recovery:
550
- p2p_e2ee_send(recovery["to"], recovery["payload"])
551
- # 频率限制:同群同 epoch 30 秒内不重复请求
552
- ```
553
-
554
- #### 状态查询
555
-
556
- ```python
557
- group_e2ee.has_secret(group_id) # 是否持有密钥
558
- group_e2ee.current_epoch(group_id) # 当前 epoch,无密钥返回 None
559
- group_e2ee.get_member_aids(group_id) # 已知成员列表
560
- group_e2ee.load_all_secrets(group_id) # {epoch: secret_bytes} 映射
561
- group_e2ee.cleanup(group_id) # 清理过期旧 epoch(默认保留 7 天)
562
- ```
300
+ ## V2 设备公钥与 SPK 管理
301
+
302
+ 连接成功后,SDK 会初始化本设备 V2 session,生成或加载 IK / SPK,并通过 `message.v2.put_peer_pk` 幂等注册当前 P2P 设备 SPK。群组路径会按群生成独立 group SPK,并通过 `group.v2.put_group_pk` 注册。应用层一般无需手动管理这些密钥。
303
+
304
+ 当前主路径的要点:
305
+
306
+ - P2P 设备 SPK 的 `key_source` 为 `peer_device_prekey`,由 AID 私钥签名背书。
307
+ - 群内独立 group SPK `key_source` 为 `group_device_prekey`,按规范化后的 `group_aid` 隔离。
308
+ - SDK 发送前通过 `message.v2.bootstrap` / `group.v2.bootstrap` 获取目标设备集合和当前 SPK。
309
+ - 旧 SPK 会在本地保留一段安全窗口,用于解密引用旧 SPK 的历史消息;满足已消费和保留窗口条件后才销毁。
310
+
311
+ WebSocket 客户端如果绕过 SDK,需要自行完成同等的 SPK 生成、AID 私钥签名、注册和 bootstrap 逻辑。旧 `message.e2ee.put_prekey/get_prekey` 只用于 legacy `prekey_ecdh_v2` 信封兼容,不是当前 SDK 的默认路径。
312
+
313
+ SPK 签名里的 `spk_timestamp` 使用 Unix 秒;消息、群事件和服务端 `timestamp` / `created_at` 等主路径时间字段仍使用 Unix 毫秒。
563
314
 
564
315
  ---
565
316
 
@@ -609,7 +360,7 @@ client = AUNClient(aid)
609
360
  | P2P 消息默认要求发送方签名 | 无 `sender_signature` 的消息被拒绝 |
610
361
  | 群组消息默认要求发送方签名 | `require_signature=True`,无签名或无发送方证书的消息被拒绝 |
611
362
  | 群组 E2EE 为固定启用能力 | `group_e2ee=true`,不可关闭 |
612
- | 默认要求前向保密 | `require_forward_secrecy=true`,无 prekey 时拒绝 long_term_key 降级 |
613
- | 客户端操作签名 | `group.send`/`group.kick`/`group.add_member`/`group.leave` 等操作自动附加 `client_signature`,服务端强制验签 |
363
+ | 默认要求前向保密 | V2 优先使用设备 SPK 的 `3DH` wrap;缺少 SPK `1DH` 路径仅作为兼容降级 |
364
+ | 客户端操作签名 | SDK 会为关键操作附加 `client_signature`;Gateway 对 `send/pull/ack` 等常规 RPC 优先使用连接级身份认证,只有身份声明与连接不一致、敏感操作、能力身份或主动携签场景才执行 ECDSA 验签 |
614
365
 
615
366