@agentunion/fastaun-browser 0.3.3 → 0.3.5

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 (67) hide show
  1. package/CHANGELOG.md +113 -85
  2. package/_packed_docs/CHANGELOG.md +113 -85
  3. package/_packed_docs/INDEX.md +81 -0
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +55 -0
  5. 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 +328 -0
  6. package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +686 -0
  7. package/_packed_docs/design/E2EE_V2/347/256/200/345/214/226/344/270/2721DH/345/212/240Per-AID_Wrap/346/226/271/346/241/210.md +124 -0
  8. package/_packed_docs/design//350/267/250/350/257/255/350/250/200/345/256/271/345/231/250E2E/346/265/213/350/257/225/346/226/271/346/241/210.md +665 -0
  9. package/_packed_docs/protocol//351/231/204/345/275/225N-/345/210/206/345/270/203/345/274/217Trace/345/215/217/350/256/256.md +257 -0
  10. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +5 -5
  11. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +1 -1
  12. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
  13. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +46 -6
  14. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +89 -12
  15. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +19 -1
  16. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +20 -5
  17. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +8 -8
  18. 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 +171 -0
  19. package/_packed_docs/sdk/INDEX.md +22 -22
  20. package/_packed_docs/sdk/README.md +3 -3
  21. package/dist/auth.d.ts +10 -11
  22. package/dist/auth.d.ts.map +1 -1
  23. package/dist/auth.js +127 -91
  24. package/dist/auth.js.map +1 -1
  25. package/dist/bundle.js +649 -274
  26. package/dist/client.d.ts +19 -10
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +238 -111
  29. package/dist/client.js.map +1 -1
  30. package/dist/errors.d.ts +4 -0
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/errors.js +7 -0
  33. package/dist/errors.js.map +1 -1
  34. package/dist/index.d.ts +3 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +3 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/keystore/index.d.ts +5 -0
  39. package/dist/keystore/index.d.ts.map +1 -1
  40. package/dist/keystore/indexeddb.d.ts +12 -0
  41. package/dist/keystore/indexeddb.d.ts.map +1 -1
  42. package/dist/keystore/indexeddb.js +64 -6
  43. package/dist/keystore/indexeddb.js.map +1 -1
  44. package/dist/namespaces/auth.d.ts +9 -3
  45. package/dist/namespaces/auth.d.ts.map +1 -1
  46. package/dist/namespaces/auth.js +64 -20
  47. package/dist/namespaces/auth.js.map +1 -1
  48. package/dist/secret-store/indexeddb-store.js +1 -1
  49. package/dist/secret-store/indexeddb-store.js.map +1 -1
  50. package/dist/transport.d.ts +9 -1
  51. package/dist/transport.d.ts.map +1 -1
  52. package/dist/transport.js +158 -64
  53. package/dist/transport.js.map +1 -1
  54. package/dist/v2/e2ee/decrypt.js +1 -1
  55. package/dist/v2/e2ee/decrypt.js.map +1 -1
  56. package/dist/v2/e2ee/encrypt-p2p.d.ts.map +1 -1
  57. package/dist/v2/e2ee/encrypt-p2p.js +3 -2
  58. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  59. package/dist/v2/session/session.d.ts +1 -0
  60. package/dist/v2/session/session.d.ts.map +1 -1
  61. package/dist/v2/session/session.js +7 -1
  62. package/dist/v2/session/session.js.map +1 -1
  63. package/package.json +43 -43
  64. package/dist/e2ee-group.d.ts +0 -276
  65. package/dist/e2ee-group.d.ts.map +0 -1
  66. package/dist/e2ee-group.js +0 -1653
  67. package/dist/e2ee-group.js.map +0 -1
package/dist/client.js CHANGED
@@ -270,6 +270,63 @@ function isGroupServiceAid(value) {
270
270
  const [name, ...issuerParts] = text.split('.');
271
271
  return name === 'group' && issuerParts.join('.').length > 0;
272
272
  }
273
+ function normalizeV2WrapPolicy(raw) {
274
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
275
+ return undefined;
276
+ const obj = raw;
277
+ let protocol = String(obj.protocol ?? '').trim().toUpperCase();
278
+ let scope = String(obj.scope ?? '').trim().toLowerCase();
279
+ if (scope !== 'aid' && scope !== 'device') {
280
+ if (obj.per_aid_wrap === true)
281
+ scope = 'aid';
282
+ else if (obj.per_device_wrap === true)
283
+ scope = 'device';
284
+ else
285
+ scope = '';
286
+ }
287
+ if (protocol !== '1DH' && protocol !== '3DH')
288
+ protocol = '';
289
+ if (scope === 'aid')
290
+ protocol = '1DH';
291
+ if (!protocol && !scope)
292
+ return undefined;
293
+ return {
294
+ protocol: protocol ? protocol : undefined,
295
+ scope: scope ? scope : undefined,
296
+ };
297
+ }
298
+ function v2WrapCapabilities() {
299
+ return {
300
+ version: 'v2.1',
301
+ protocols: ['1DH', '3DH'],
302
+ scopes: ['aid', 'device'],
303
+ per_aid_wrap: true,
304
+ per_device_wrap: true,
305
+ };
306
+ }
307
+ function applyV2WrapPolicyToTargets(targets, policy) {
308
+ if (!policy)
309
+ return targets;
310
+ const normalized = targets.map((target) => {
311
+ const row = { ...target };
312
+ if (policy.protocol === '1DH') {
313
+ row.keySource = 'aid_master';
314
+ row.spkPkDer = undefined;
315
+ row.spkId = '';
316
+ }
317
+ return row;
318
+ });
319
+ if (policy.scope !== 'aid')
320
+ return normalized;
321
+ const collapsed = new Map();
322
+ for (const target of normalized) {
323
+ const key = `${target.aid}\u0000${target.role}`;
324
+ if (!collapsed.has(key)) {
325
+ collapsed.set(key, { ...target, deviceId: '' });
326
+ }
327
+ }
328
+ return Array.from(collapsed.values());
329
+ }
273
330
  /** 32 字节左侧零填充(用于 P-256 私钥 scalar 规范化) */
274
331
  function _v2LeftPad32(b) {
275
332
  if (b.length === 32)
@@ -529,7 +586,7 @@ export class AUNClient {
529
586
  _agentMdPath = '';
530
587
  _agentMdCache = new Map();
531
588
  _agentMdFetchInflight = new Set();
532
- _agentMdListLock = Promise.resolve();
589
+ _agentMdLock = Promise.resolve();
533
590
  /** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
534
591
  _seqTracker = new SeqTracker();
535
592
  _seqTrackerContext = null;
@@ -543,6 +600,9 @@ export class AUNClient {
543
600
  _groupSynced = new Set();
544
601
  /** gap fill 来源标记:true 表示当前正在补洞(pull 触发),false 表示非补洞 */
545
602
  _gapFillActive = false;
603
+ // Pull Gate:序列化同一 key 的并发 pull 操作,防止重复拉取
604
+ _pullGates = new Map();
605
+ static _PULL_GATE_STALE_MS = 30000;
546
606
  // 重连相关
547
607
  _reconnectActive = false;
548
608
  _reconnectAbort = null;
@@ -583,7 +643,7 @@ export class AUNClient {
583
643
  this._clientLog.info(`AUNClient initialized: debug=${_debug} aunPath=${this.configModel.aunPath} aid=${initAid ?? '-'}`);
584
644
  this._dispatcher = new EventDispatcher();
585
645
  this._discovery = new GatewayDiscovery();
586
- this._keystore = new IndexedDBKeyStore();
646
+ this._keystore = new IndexedDBKeyStore({ encryptionSeed: this.configModel.seedPassword ?? undefined });
587
647
  this._slotId = '';
588
648
  this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
589
649
  this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
@@ -693,7 +753,7 @@ export class AUNClient {
693
753
  }
694
754
  /**
695
755
  * 浏览器版本 publishAgentMd。默认从 {agentMdPath}/{self_aid}/agent.md 的等价 IndexedDB 正文读取,
696
- * 然后签名、上传,并刷新 list.json 元数据。
756
+ * 然后签名、上传,并刷新 agentmd.json 元数据。
697
757
  *
698
758
  * 兼容旧浏览器调用:传入 content 时会先写入等价正文,再从该正文发布。
699
759
  */
@@ -736,7 +796,7 @@ export class AUNClient {
736
796
  }
737
797
  /**
738
798
  * 浏览器版本 fetchAgentMd。aid 缺省时取自身;下载后的正文固定写入
739
- * {agentMdPath}/{aid}/agent.md 的等价 IndexedDB 正文,list.json 只保存元数据。
799
+ * {agentMdPath}/{aid}/agent.md 的等价 IndexedDB 正文,agentmd.json 只保存元数据。
740
800
  */
741
801
  async fetchAgentMd(aid) {
742
802
  const target = String(aid ?? this._aid ?? '').trim();
@@ -809,8 +869,8 @@ export class AUNClient {
809
869
  }
810
870
  return target;
811
871
  }
812
- _agentMdListKey() {
813
- return 'list.json';
872
+ _agentMdMetaKey(aid) {
873
+ return `${this._agentMdSafeAid(aid)}/agentmd.json`;
814
874
  }
815
875
  _agentMdContentKey(aid) {
816
876
  return `${this._agentMdSafeAid(aid)}/agent.md`;
@@ -844,18 +904,11 @@ export class AUNClient {
844
904
  fetched_at: Date.now(),
845
905
  });
846
906
  }
847
- async _listAgentMdContentAids() {
848
- const list = this._keystore.listAgentMdContentAids;
849
- if (typeof list !== 'function') {
850
- throw new Error('IndexedDB agent.md storage unavailable');
851
- }
852
- return await list.call(this._keystore, this._agentMdRoot());
853
- }
854
- async _withAgentMdListLock(fn) {
855
- const previous = this._agentMdListLock.catch(() => undefined);
907
+ async _withAgentMdLock(fn) {
908
+ const previous = this._agentMdLock.catch(() => undefined);
856
909
  let release;
857
910
  const current = new Promise((resolve) => { release = resolve; });
858
- this._agentMdListLock = previous.then(() => current);
911
+ this._agentMdLock = previous.then(() => current);
859
912
  await previous;
860
913
  try {
861
914
  return await fn();
@@ -864,79 +917,39 @@ export class AUNClient {
864
917
  release();
865
918
  }
866
919
  }
867
- _normalizeAgentMdList(data) {
868
- const records = {};
869
- let iterable = [];
870
- if (isJsonObject(data)) {
871
- const payload = data;
872
- if (Array.isArray(payload.records))
873
- iterable = payload.records;
874
- else if (isJsonObject(payload.records))
875
- iterable = Object.values(payload.records);
876
- }
877
- else if (Array.isArray(data)) {
878
- iterable = data;
920
+ _normalizeAgentMdRecord(aid, data) {
921
+ if (!isJsonObject(data))
922
+ return {};
923
+ const record = {};
924
+ for (const [key, value] of Object.entries(data)) {
925
+ if (key !== 'content')
926
+ record[key] = value;
879
927
  }
880
- for (const item of iterable) {
881
- if (!isJsonObject(item))
882
- continue;
883
- const raw = item;
884
- const aid = String(raw.aid ?? '').trim();
885
- if (!aid)
886
- continue;
887
- const record = {};
888
- for (const [key, value] of Object.entries(raw)) {
889
- if (key !== 'content')
890
- record[key] = value;
891
- }
892
- record.aid = aid;
893
- for (const key of ['fetched_at', 'observed_at', 'checked_at', 'updated_at']) {
894
- record[key] = Number(record[key] ?? 0) || 0;
895
- }
896
- records[aid] = record;
928
+ record.aid = this._agentMdSafeAid(String(record.aid ?? aid));
929
+ for (const key of ['fetched_at', 'observed_at', 'checked_at', 'updated_at']) {
930
+ record[key] = Number(record[key] ?? 0) || 0;
897
931
  }
898
- return records;
899
- }
900
- async _writeAgentMdListUnlocked(records) {
901
- const sorted = {};
902
- for (const aid of Object.keys(records).sort())
903
- sorted[aid] = records[aid];
904
- await this._writeAgentMdStorage(this._agentMdListKey(), `${JSON.stringify({ version: 1, updated_at: Date.now(), records: sorted }, null, 2)}\n`);
932
+ return record;
905
933
  }
906
- async _rebuildAgentMdListUnlocked() {
907
- const records = {};
908
- const now = Date.now();
909
- for (const aid of await this._listAgentMdContentAids()) {
910
- try {
911
- const content = await this._readAgentMdStorage(this._agentMdContentKey(aid));
912
- if (content === null)
913
- continue;
914
- records[aid] = {
915
- aid,
916
- local_etag: await this._agentMdContentEtag(content),
917
- fetched_at: now,
918
- updated_at: now,
919
- };
920
- }
921
- catch (err) {
922
- this._clientLog.warn(`agent.md rebuild skipped unreadable file aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
923
- }
934
+ async _writeAgentMdRecordUnlocked(aid, record) {
935
+ const payload = {};
936
+ for (const [key, value] of Object.entries(record)) {
937
+ if (key !== 'content' && value !== undefined && value !== null)
938
+ payload[key] = value;
924
939
  }
925
- await this._writeAgentMdListUnlocked(records);
926
- this._agentMdCache.clear();
927
- return records;
940
+ payload.aid = this._agentMdSafeAid(aid);
941
+ await this._writeAgentMdStorage(this._agentMdMetaKey(aid), `${JSON.stringify(payload, null, 2)}\n`);
928
942
  }
929
- async _readAgentMdListUnlocked() {
930
- const raw = await this._readAgentMdStorage(this._agentMdListKey());
931
- if (raw === null) {
932
- return await this._rebuildAgentMdListUnlocked();
933
- }
943
+ async _readAgentMdRecordUnlocked(aid) {
944
+ const raw = await this._readAgentMdStorage(this._agentMdMetaKey(aid));
945
+ if (raw === null)
946
+ return {};
934
947
  try {
935
- return this._normalizeAgentMdList(JSON.parse(raw));
948
+ return this._normalizeAgentMdRecord(aid, JSON.parse(raw));
936
949
  }
937
950
  catch (err) {
938
- this._clientLog.warn(`agent.md list.json damaged, rebuilding: ${err instanceof Error ? err.message : String(err)}`);
939
- return await this._rebuildAgentMdListUnlocked();
951
+ this._clientLog.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
952
+ return {};
940
953
  }
941
954
  }
942
955
  async _readAgentMdContent(aid) {
@@ -960,21 +973,32 @@ export class AUNClient {
960
973
  if (!target)
961
974
  return null;
962
975
  try {
963
- const records = await this._withAgentMdListLock(async () => await this._readAgentMdListUnlocked());
964
- const record = records[target];
965
- if (record && typeof record === 'object') {
966
- const loaded = { ...record, aid: target };
967
- const content = await this._readAgentMdContent(target);
968
- if (content !== null) {
969
- loaded.content = content;
970
- loaded.local_etag = await this._agentMdContentEtag(content);
976
+ const loaded = await this._withAgentMdLock(async () => {
977
+ const record = await this._readAgentMdRecordUnlocked(target);
978
+ const next = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
979
+ try {
980
+ const content = await this._readAgentMdContent(target);
981
+ if (content !== null) {
982
+ next.content = content;
983
+ next.local_etag = await this._agentMdContentEtag(content);
984
+ }
985
+ else {
986
+ // 元数据存在但正文缺失
987
+ const metaRaw = await this._readAgentMdStorage(this._agentMdMetaKey(target));
988
+ if (metaRaw !== null) {
989
+ this._clientLog.warn(`agent.md content read failed: aid=${target}`);
990
+ }
991
+ }
971
992
  }
972
- else {
973
- this._clientLog.warn(`agent.md content read failed: aid=${target}`);
993
+ catch (err) {
994
+ this._clientLog.warn(`agent.md content read failed: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
974
995
  }
975
- this._agentMdCache.set(target, { ...loaded });
976
- return { ...loaded };
977
- }
996
+ return next;
997
+ });
998
+ if (Object.keys(loaded).length <= 1)
999
+ return null;
1000
+ this._agentMdCache.set(target, { ...loaded });
1001
+ return { ...loaded };
978
1002
  }
979
1003
  catch (err) {
980
1004
  this._clientLog.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
@@ -997,16 +1021,14 @@ export class AUNClient {
997
1021
  inputFields.fetched_at = Date.now();
998
1022
  }
999
1023
  delete inputFields.content;
1000
- const record = await this._withAgentMdListLock(async () => {
1001
- const records = await this._readAgentMdListUnlocked();
1002
- const next = { ...(records[target] ?? {}), aid: target };
1024
+ const record = await this._withAgentMdLock(async () => {
1025
+ const next = { ...(await this._readAgentMdRecordUnlocked(target)), aid: target };
1003
1026
  for (const [key, value] of Object.entries(inputFields)) {
1004
1027
  if (value !== undefined && value !== null)
1005
1028
  next[key] = value;
1006
1029
  }
1007
1030
  next.updated_at = Date.now();
1008
- records[target] = { ...next };
1009
- await this._writeAgentMdListUnlocked(records);
1031
+ await this._writeAgentMdRecordUnlocked(target, next);
1010
1032
  return next;
1011
1033
  });
1012
1034
  const loaded = { ...record };
@@ -1494,6 +1516,20 @@ export class AUNClient {
1494
1516
  return this._putMessageThoughtEncryptedV2(p);
1495
1517
  }
1496
1518
  }
1519
+ // Pull Gate:序列化同一 key 的 pull 操作,防止并发重复拉取
1520
+ const pullGateKey = this._pullGateKeyForCall(method, p);
1521
+ if (pullGateKey) {
1522
+ return await this._runPullSerialized(pullGateKey, async () => {
1523
+ return await this._callImplInner(method, p);
1524
+ });
1525
+ }
1526
+ return await this._callImplInner(method, p);
1527
+ }
1528
+ /**
1529
+ * _callImpl 的内层:pull gate 之后的实际 RPC 分发逻辑。
1530
+ * 拆分出来以便 pull gate 包裹整个操作。
1531
+ */
1532
+ async _callImplInner(method, p) {
1497
1533
  // message.pull:V2 就绪时走 V2 pull
1498
1534
  if (method === 'message.pull' && this._v2Session) {
1499
1535
  this._clientLog.debug('call route: message.pull → V2 pull');
@@ -4265,7 +4301,10 @@ export class AUNClient {
4265
4301
  const session = this._v2Session;
4266
4302
  if (session && fromAid) {
4267
4303
  try {
4268
- const bs = await this.call('message.v2.bootstrap', { peer_aid: fromAid });
4304
+ const bs = await this.call('message.v2.bootstrap', {
4305
+ peer_aid: fromAid,
4306
+ e2ee_wrap_capabilities: v2WrapCapabilities(),
4307
+ });
4269
4308
  const peers = (Array.isArray(bs?.peer_devices) ? bs.peer_devices : []);
4270
4309
  for (const dev of peers)
4271
4310
  this._cacheV2PeerIKFromDevice(dev, fromAid);
@@ -4275,7 +4314,10 @@ export class AUNClient {
4275
4314
  }
4276
4315
  if (groupId) {
4277
4316
  try {
4278
- const gbs = await this.call('group.v2.bootstrap', { group_id: groupId });
4317
+ const gbs = await this.call('group.v2.bootstrap', {
4318
+ group_id: groupId,
4319
+ e2ee_wrap_capabilities: v2WrapCapabilities(),
4320
+ });
4279
4321
  const devices = (Array.isArray(gbs?.devices) ? gbs.devices : []);
4280
4322
  const audit = (Array.isArray(gbs?.audit_recipients) ? gbs.audit_recipients : []);
4281
4323
  for (const dev of devices)
@@ -4559,7 +4601,7 @@ export class AUNClient {
4559
4601
  // 从 recipients 数组中查找本设备的 row 以获取 key_source
4560
4602
  if (!spkId) {
4561
4603
  for (const row of envelope.recipients) {
4562
- if (Array.isArray(row) && row.length >= 6 && row[0] === this._aid && row[1] === this._deviceId) {
4604
+ if (Array.isArray(row) && row.length >= 6 && row[0] === this._aid && (row[1] === this._deviceId || row[1] === '')) {
4563
4605
  spkId = String(row[5] ?? '');
4564
4606
  recipientKeySource = row.length > 3 ? String(row[3] ?? '') : '';
4565
4607
  break;
@@ -4568,7 +4610,7 @@ export class AUNClient {
4568
4610
  }
4569
4611
  else {
4570
4612
  for (const row of envelope.recipients) {
4571
- if (Array.isArray(row) && row.length >= 6 && row[0] === this._aid && row[1] === this._deviceId) {
4613
+ if (Array.isArray(row) && row.length >= 6 && row[0] === this._aid && (row[1] === this._deviceId || row[1] === '')) {
4572
4614
  recipientKeySource = row.length > 3 ? String(row[3] ?? '') : '';
4573
4615
  break;
4574
4616
  }
@@ -4932,20 +4974,27 @@ export class AUNClient {
4932
4974
  const useCache = opts.useCache !== false;
4933
4975
  let peerDevices = [];
4934
4976
  let auditRaw = [];
4977
+ let wrapPolicy;
4935
4978
  const cached = useCache ? this._v2BootstrapCache.get(to) : undefined;
4936
4979
  if (cached && (Date.now() - cached.cachedAt) < AUNClient.V2_BOOTSTRAP_TTL_MS) {
4937
4980
  peerDevices = cached.devices;
4938
4981
  auditRaw = cached.auditRecipients;
4982
+ wrapPolicy = cached.wrapPolicy;
4939
4983
  }
4940
4984
  else {
4941
- const bs = await this.call('message.v2.bootstrap', { peer_aid: to });
4985
+ const bs = await this.call('message.v2.bootstrap', {
4986
+ peer_aid: to,
4987
+ e2ee_wrap_capabilities: v2WrapCapabilities(),
4988
+ });
4942
4989
  peerDevices = (Array.isArray(bs?.peer_devices) ? bs.peer_devices : []);
4943
4990
  auditRaw = (Array.isArray(bs?.audit_recipients) ? bs.audit_recipients : []);
4991
+ wrapPolicy = normalizeV2WrapPolicy(bs?.e2ee_wrap_policy);
4944
4992
  if (peerDevices.length > 0) {
4945
4993
  this._v2BootstrapCache.set(to, {
4946
4994
  devices: peerDevices,
4947
4995
  auditRecipients: auditRaw,
4948
4996
  cachedAt: Date.now(),
4997
+ wrapPolicy,
4949
4998
  });
4950
4999
  }
4951
5000
  }
@@ -4985,7 +5034,10 @@ export class AUNClient {
4985
5034
  selfDevices = selfCached.devices;
4986
5035
  }
4987
5036
  else {
4988
- const selfBs = await this.call('message.v2.bootstrap', { peer_aid: this._aid });
5037
+ const selfBs = await this.call('message.v2.bootstrap', {
5038
+ peer_aid: this._aid,
5039
+ e2ee_wrap_capabilities: v2WrapCapabilities(),
5040
+ });
4989
5041
  selfDevices = (Array.isArray(selfBs?.peer_devices) ? selfBs.peer_devices : []);
4990
5042
  if (selfDevices.length > 0) {
4991
5043
  this._v2BootstrapCache.set(this._aid, {
@@ -5015,7 +5067,8 @@ export class AUNClient {
5015
5067
  }
5016
5068
  }
5017
5069
  const sender = await session.getSenderIdentity();
5018
- const envelope = await encryptP2PMessage(sender, { targets, auditRecipients: [] }, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context });
5070
+ const sendTargets = applyV2WrapPolicyToTargets(targets, wrapPolicy);
5071
+ const envelope = await encryptP2PMessage(sender, { targets: sendTargets, auditRecipients: [] }, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context });
5019
5072
  return envelope;
5020
5073
  }
5021
5074
  /**
@@ -5089,18 +5142,24 @@ export class AUNClient {
5089
5142
  let epoch = 0;
5090
5143
  let stateCommitment = { state_version: 0, state_hash: '', state_chain: '' };
5091
5144
  let auditRecipientsRaw = [];
5145
+ let wrapPolicy;
5092
5146
  const cached = useCache ? this._v2BootstrapCache.get(cacheKey) : undefined;
5093
5147
  if (cached && (Date.now() - cached.cachedAt) < AUNClient.V2_BOOTSTRAP_TTL_MS) {
5094
5148
  allDevices = cached.devices;
5095
5149
  epoch = cached.epoch ?? 0;
5096
5150
  stateCommitment = cached.stateCommitment ?? { state_version: 0, state_hash: '', state_chain: '' };
5097
5151
  auditRecipientsRaw = cached.auditRecipients;
5152
+ wrapPolicy = cached.wrapPolicy;
5098
5153
  }
5099
5154
  else {
5100
- const bs = await this.call('group.v2.bootstrap', { group_id: groupId });
5155
+ const bs = await this.call('group.v2.bootstrap', {
5156
+ group_id: groupId,
5157
+ e2ee_wrap_capabilities: v2WrapCapabilities(),
5158
+ });
5101
5159
  allDevices = (Array.isArray(bs?.devices) ? bs.devices : []);
5102
5160
  epoch = Number(bs?.epoch ?? 0);
5103
5161
  auditRecipientsRaw = (Array.isArray(bs?.audit_recipients) ? bs.audit_recipients : []);
5162
+ wrapPolicy = normalizeV2WrapPolicy(bs?.e2ee_wrap_policy);
5104
5163
  await this._v2CheckFork(groupId, String(bs?.state_chain ?? ''));
5105
5164
  await this._v2VerifyStateSignature(groupId, bs);
5106
5165
  await this._publishV2GroupSecurityLevel(groupId, bs);
@@ -5116,6 +5175,7 @@ export class AUNClient {
5116
5175
  cachedAt: Date.now(),
5117
5176
  epoch,
5118
5177
  stateCommitment: stateCommitment,
5178
+ wrapPolicy,
5119
5179
  });
5120
5180
  }
5121
5181
  // lazy sync 触发:发现 pending members 时异步发起提案
@@ -5159,7 +5219,8 @@ export class AUNClient {
5159
5219
  throw new E2EEError(`V2 group: no target devices for ${groupId}`);
5160
5220
  }
5161
5221
  const sender = await session.getSenderIdentity();
5162
- const envelope = await encryptGroupMessage(sender, groupId, epoch, targets, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context }, stateCommitment);
5222
+ const sendTargets = applyV2WrapPolicyToTargets(targets, wrapPolicy);
5223
+ const envelope = await encryptGroupMessage(sender, groupId, epoch, sendTargets, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context }, stateCommitment);
5163
5224
  return envelope;
5164
5225
  }
5165
5226
  async _publishV2GroupSecurityLevel(groupId, bootstrap) {
@@ -5242,7 +5303,7 @@ export class AUNClient {
5242
5303
  if (Array.isArray(recipients)) {
5243
5304
  for (const row of recipients) {
5244
5305
  if (Array.isArray(row) && row.length >= 6) {
5245
- if (row[0] === this._aid && row[1] === this._deviceId) {
5306
+ if (row[0] === this._aid && (row[1] === this._deviceId || row[1] === '')) {
5246
5307
  spkId = String(row[5] ?? '');
5247
5308
  recipientKeySource = row.length > 3 ? String(row[3] ?? '') : '';
5248
5309
  break;
@@ -5554,7 +5615,10 @@ export class AUNClient {
5554
5615
  }
5555
5616
  if (myRole !== 'owner' && myRole !== 'admin')
5556
5617
  return false;
5557
- const bootstrapResp = await this.call('group.v2.bootstrap', { group_id: groupId });
5618
+ const bootstrapResp = await this.call('group.v2.bootstrap', {
5619
+ group_id: groupId,
5620
+ e2ee_wrap_capabilities: v2WrapCapabilities(),
5621
+ });
5558
5622
  const devices = (Array.isArray(bootstrapResp?.devices) ? bootstrapResp.devices : []);
5559
5623
  const candidates = [];
5560
5624
  for (const dev of devices) {
@@ -5656,7 +5720,10 @@ export class AUNClient {
5656
5720
  }
5657
5721
  }
5658
5722
  // 获取群所有成员的设备列表(V2 bootstrap)
5659
- const bootstrapResp = await this.call('group.v2.bootstrap', { group_id: groupId });
5723
+ const bootstrapResp = await this.call('group.v2.bootstrap', {
5724
+ group_id: groupId,
5725
+ e2ee_wrap_capabilities: v2WrapCapabilities(),
5726
+ });
5660
5727
  const allDevices = (Array.isArray(bootstrapResp?.devices) ? bootstrapResp.devices : []);
5661
5728
  const auditRecipients = (Array.isArray(bootstrapResp?.audit_recipients) ? bootstrapResp.audit_recipients : []);
5662
5729
  const auditAidsList = [...new Set(auditRecipients.map(r => String(r.aid ?? '').trim()).filter(Boolean))].sort();
@@ -6000,6 +6067,66 @@ export class AUNClient {
6000
6067
  this._clientLog.warn(`background task exception:${String(exc)}`);
6001
6068
  });
6002
6069
  }
6070
+ // ── Pull Gate(序列化同一 key 的并发 pull)──────────────────
6071
+ _pullGateKeyForCall(method, params) {
6072
+ if (method === 'message.pull' || method === 'message.v2.pull') {
6073
+ return this._aid ? `p2p:${this._aid}` : '';
6074
+ }
6075
+ if (method === 'group.pull' || method === 'group.v2.pull') {
6076
+ const gid = String(params.group_id ?? '').trim();
6077
+ return gid ? `group:${gid}` : '';
6078
+ }
6079
+ if (method === 'group.pull_events') {
6080
+ const gid = String(params.group_id ?? '').trim();
6081
+ return gid ? `group_event:${gid}` : '';
6082
+ }
6083
+ return '';
6084
+ }
6085
+ _tryAcquirePullGate(key) {
6086
+ if (!key)
6087
+ return 0;
6088
+ const now = Date.now();
6089
+ const gate = this._pullGates.get(key) ?? { inflight: false, startedAt: 0, token: 0 };
6090
+ if (gate.inflight && now - gate.startedAt <= AUNClient._PULL_GATE_STALE_MS) {
6091
+ return null;
6092
+ }
6093
+ if (gate.inflight) {
6094
+ this._clientLog.warn(`pull in-flight stale reset: key=${key} age=${now - gate.startedAt}ms`);
6095
+ }
6096
+ gate.token += 1;
6097
+ gate.inflight = true;
6098
+ gate.startedAt = now;
6099
+ this._pullGates.set(key, gate);
6100
+ return gate.token;
6101
+ }
6102
+ _releasePullGate(key, token) {
6103
+ if (!key || token == null)
6104
+ return;
6105
+ const gate = this._pullGates.get(key);
6106
+ if (!gate || gate.token !== token)
6107
+ return;
6108
+ gate.inflight = false;
6109
+ gate.startedAt = 0;
6110
+ }
6111
+ async _runPullSerialized(key, operation) {
6112
+ let token = this._tryAcquirePullGate(key);
6113
+ if (token === null) {
6114
+ const deadline = Date.now() + AUNClient._PULL_GATE_STALE_MS + 100;
6115
+ while (token === null && Date.now() <= deadline) {
6116
+ await this._sleep(25);
6117
+ token = this._tryAcquirePullGate(key);
6118
+ }
6119
+ if (token === null) {
6120
+ throw new StateError(`pull already in-flight for ${key}`);
6121
+ }
6122
+ }
6123
+ try {
6124
+ return await operation();
6125
+ }
6126
+ finally {
6127
+ this._releasePullGate(key, token);
6128
+ }
6129
+ }
6003
6130
  /** 可取消的 sleep */
6004
6131
  _sleep(ms) {
6005
6132
  return new Promise((resolve) => {