@agentunion/fastaun-browser 0.5.0 → 0.5.1

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 (116) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/_packed_docs/CHANGELOG-validators.md +144 -0
  3. package/_packed_docs/CHANGELOG.md +56 -0
  4. package/_packed_docs/INDEX.md +198 -181
  5. package/_packed_docs/KITE_DOCS_GUIDE.md +26 -23
  6. package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +4 -3
  7. package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +331 -0
  8. package/_packed_docs/protocol/08-AUN-E2EE-Group.md +296 -902
  9. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +64 -114
  10. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +7 -1
  11. 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
  12. package/_packed_docs/protocol/README.md +2 -1
  13. package/_packed_docs/protocol/index.md +8 -3
  14. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +4 -252
  15. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +483 -457
  16. package/_packed_docs/sdk/09-collab-rpc-manual.md +581 -550
  17. package/_packed_docs/sdk/09-group-rpc-manual.md +248 -335
  18. package/_packed_docs/sdk/09-storage-rpc-manual.md +56 -19
  19. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +13 -13
  20. package/_packed_docs/sdk/INDEX.md +14 -14
  21. package/dist/bundle.js +1560 -1159
  22. package/dist/client/delivery.d.ts +13 -2
  23. package/dist/client/delivery.d.ts.map +1 -1
  24. package/dist/client/delivery.js +251 -46
  25. package/dist/client/delivery.js.map +1 -1
  26. package/dist/client/group-state.d.ts.map +1 -1
  27. package/dist/client/group-state.js +36 -14
  28. package/dist/client/group-state.js.map +1 -1
  29. package/dist/client/lifecycle.js +2 -2
  30. package/dist/client/lifecycle.js.map +1 -1
  31. package/dist/client/rpc-pipeline.d.ts +1 -0
  32. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  33. package/dist/client/rpc-pipeline.js +166 -50
  34. package/dist/client/rpc-pipeline.js.map +1 -1
  35. package/dist/client/v2-e2ee.d.ts +14 -1
  36. package/dist/client/v2-e2ee.d.ts.map +1 -1
  37. package/dist/client/v2-e2ee.js +300 -121
  38. package/dist/client/v2-e2ee.js.map +1 -1
  39. package/dist/client.d.ts +5 -4
  40. package/dist/client.d.ts.map +1 -1
  41. package/dist/client.js +187 -46
  42. package/dist/client.js.map +1 -1
  43. package/dist/collab/client.d.ts +8 -0
  44. package/dist/collab/client.d.ts.map +1 -1
  45. package/dist/collab/client.js +12 -0
  46. package/dist/collab/client.js.map +1 -1
  47. package/dist/errors.d.ts +0 -20
  48. package/dist/errors.d.ts.map +1 -1
  49. package/dist/errors.js +8 -51
  50. package/dist/errors.js.map +1 -1
  51. package/dist/facades.d.ts +9 -4
  52. package/dist/facades.d.ts.map +1 -1
  53. package/dist/facades.js +192 -31
  54. package/dist/facades.js.map +1 -1
  55. package/dist/group-fs.d.ts +17 -0
  56. package/dist/group-fs.d.ts.map +1 -1
  57. package/dist/group-fs.js +54 -11
  58. package/dist/group-fs.js.map +1 -1
  59. package/dist/group-id.d.ts +9 -12
  60. package/dist/group-id.d.ts.map +1 -1
  61. package/dist/group-id.js +41 -63
  62. package/dist/group-id.js.map +1 -1
  63. package/dist/index.d.ts +4 -2
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +4 -1
  66. package/dist/index.js.map +1 -1
  67. package/dist/keystore/index.d.ts +2 -54
  68. package/dist/keystore/index.d.ts.map +1 -1
  69. package/dist/keystore/indexeddb-identity-store.d.ts +3 -0
  70. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  71. package/dist/keystore/indexeddb-identity-store.js +65 -0
  72. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  73. package/dist/keystore/indexeddb-shared.d.ts +3 -17
  74. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  75. package/dist/keystore/indexeddb-shared.js +4 -47
  76. package/dist/keystore/indexeddb-shared.js.map +1 -1
  77. package/dist/keystore/indexeddb-token-store.d.ts +1 -64
  78. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  79. package/dist/keystore/indexeddb-token-store.js +45 -774
  80. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  81. package/dist/logger.d.ts +2 -0
  82. package/dist/logger.d.ts.map +1 -1
  83. package/dist/logger.js +4 -0
  84. package/dist/logger.js.map +1 -1
  85. package/dist/storage/lowlevel.d.ts +9 -1
  86. package/dist/storage/lowlevel.d.ts.map +1 -1
  87. package/dist/storage/lowlevel.js +12 -1
  88. package/dist/storage/lowlevel.js.map +1 -1
  89. package/dist/storage/vfs.d.ts +22 -0
  90. package/dist/storage/vfs.d.ts.map +1 -1
  91. package/dist/storage/vfs.js +54 -0
  92. package/dist/storage/vfs.js.map +1 -1
  93. package/dist/tools/cross-sdk-agent.js +336 -49
  94. package/dist/tools/cross-sdk-agent.js.map +1 -1
  95. package/dist/transport.d.ts +2 -0
  96. package/dist/transport.d.ts.map +1 -1
  97. package/dist/transport.js +96 -3
  98. package/dist/transport.js.map +1 -1
  99. package/dist/types.d.ts +39 -56
  100. package/dist/types.d.ts.map +1 -1
  101. package/dist/v2/session/session.d.ts +2 -0
  102. package/dist/v2/session/session.d.ts.map +1 -1
  103. package/dist/v2/session/session.js +58 -22
  104. package/dist/v2/session/session.js.map +1 -1
  105. package/dist/v2/state/commitment.d.ts +1 -1
  106. package/dist/v2/state/commitment.d.ts.map +1 -1
  107. package/dist/v2/state/commitment.js +5 -3
  108. package/dist/v2/state/commitment.js.map +1 -1
  109. package/dist/validators.d.ts +35 -0
  110. package/dist/validators.d.ts.map +1 -0
  111. package/dist/validators.js +127 -0
  112. package/dist/validators.js.map +1 -0
  113. package/dist/version.d.ts +1 -1
  114. package/dist/version.js +1 -1
  115. package/package.json +1 -1
  116. package/_packed_docs/collab-gateway-boundary-test-report.md +0 -164
@@ -3,9 +3,13 @@ import { E2EEError, StateError, ValidationError } from '../errors.js';
3
3
  import { normalizeGroupId } from '../group-id.js';
4
4
  import { decryptMessage, encryptGroupMessage, encryptP2PMessage, } from '../v2/e2ee/index.js';
5
5
  import { V2Session, V2KeyStore } from '../v2/session/index.js';
6
- import { isJsonObject } from '../types.js';
6
+ import { ConnectionState, isJsonObject } from '../types.js';
7
7
  const V2_BOOTSTRAP_TTL_MS = 60 * 60 * 1000;
8
8
  const V2_RETRYABLE_CODES = new Set([-33011, -33012, -33050, -33052, -33054]);
9
+ const V2_GROUP_STALE_BOOTSTRAP_CODE = -33054;
10
+ const V2_SESSION_INIT_RETRY_LIMIT = 12;
11
+ const V2_SESSION_INIT_RETRY_BASE_MS = 1000;
12
+ const V2_SESSION_INIT_RETRY_MAX_MS = 5000;
9
13
  const MEMBERSHIP_ACTIONS = new Set([
10
14
  'member_added',
11
15
  'member_left',
@@ -57,6 +61,17 @@ function v2B64uToBytes(value) {
57
61
  function formatE2EEError(error) {
58
62
  return error instanceof Error ? error : String(error);
59
63
  }
64
+ function isTransientV2SessionInitError(error) {
65
+ const text = (error instanceof Error ? error.message : String(error)).toLowerCase();
66
+ return text.includes('no_service')
67
+ || text.includes('service plane unavailable')
68
+ || text.includes('transport not connected')
69
+ || text.includes('transport closed')
70
+ || text.includes('websocket closed')
71
+ || text.includes('websocket is not open')
72
+ || text.includes('connection closed')
73
+ || text.includes('socket closed');
74
+ }
60
75
  function uuidV4() {
61
76
  if (typeof crypto.randomUUID === 'function')
62
77
  return crypto.randomUUID();
@@ -203,6 +218,10 @@ function applyV2WrapPolicyToTargets(targets, policy) {
203
218
  }
204
219
  export class V2E2EECoordinator {
205
220
  runtime;
221
+ targetSetCache = new Map();
222
+ v2SessionInitRetryTimer = null;
223
+ v2SessionInitRetryBackgroundSync = false;
224
+ v2SessionInitRetryIdentityKey = null;
206
225
  constructor(runtime) {
207
226
  this.runtime = runtime;
208
227
  }
@@ -224,29 +243,151 @@ export class V2E2EECoordinator {
224
243
  }
225
244
  deleteBootstrapCacheEntry(key) {
226
245
  this.bootstrapCache.delete(key);
246
+ this.targetSetCache.delete(key);
227
247
  }
228
248
  clearBootstrapCache() {
229
249
  this.bootstrapCache.clear();
250
+ this.targetSetCache.clear();
230
251
  }
231
252
  pruneExpiredBootstrapCache(ttlMs, now = Date.now()) {
232
253
  for (const [key, entry] of this.bootstrapCache) {
233
254
  if (now - Number(entry.cachedAt ?? 0) >= ttlMs) {
234
255
  this.bootstrapCache.delete(key);
256
+ this.targetSetCache.delete(key);
257
+ }
258
+ }
259
+ for (const [key, entry] of this.targetSetCache) {
260
+ if (now - Number(entry.cachedAt ?? 0) >= ttlMs) {
261
+ this.targetSetCache.delete(key);
235
262
  }
236
263
  }
237
264
  }
265
+ getTargetSetCache(key) {
266
+ const cached = this.targetSetCache.get(key);
267
+ if (!cached || Date.now() - cached.cachedAt >= V2_BOOTSTRAP_TTL_MS) {
268
+ if (cached)
269
+ this.targetSetCache.delete(key);
270
+ return undefined;
271
+ }
272
+ return cached.value;
273
+ }
274
+ setTargetSetCache(key, value) {
275
+ this.targetSetCache.set(key, { cachedAt: Date.now(), value });
276
+ return value;
277
+ }
278
+ cloneP2PTargetSet(value) {
279
+ return {
280
+ targets: value.targets.map((target) => ({ ...target })),
281
+ auditRecipients: value.auditRecipients.map((target) => ({ ...target })),
282
+ };
283
+ }
284
+ cloneGroupTargetSet(value) {
285
+ return {
286
+ targets: value.targets.map((target) => ({ ...target })),
287
+ };
288
+ }
238
289
  async onConnected(opts) {
239
290
  const client = this.client;
240
291
  try {
241
292
  await client._initV2Session();
293
+ this.clearV2SessionInitRetry();
242
294
  }
243
295
  catch (exc) {
244
296
  client._clientLog.warn(`V2 session init failed (non-fatal): ${formatE2EEError(exc)}`);
297
+ if (isTransientV2SessionInitError(exc)) {
298
+ this.scheduleV2SessionInitRetry(1, opts.backgroundSync, exc);
299
+ }
245
300
  }
246
301
  if (opts.backgroundSync && client._v2Session && typeof client._v2AutoConfirmPendingProposals === 'function') {
247
302
  client._safeAsync(client._v2AutoConfirmPendingProposals());
248
303
  }
249
304
  }
305
+ currentV2SessionInitRetryIdentityKey() {
306
+ const client = this.client;
307
+ if (!client._aid)
308
+ return null;
309
+ return JSON.stringify([
310
+ client._aid,
311
+ client._deviceId ?? '',
312
+ client._currentAid ?? null,
313
+ Number(client._v2RuntimeGeneration ?? 0),
314
+ ]);
315
+ }
316
+ canRetryV2SessionInit(identityKey) {
317
+ const client = this.client;
318
+ return client.state === ConnectionState.READY
319
+ && !client._closing
320
+ && this.currentV2SessionInitRetryIdentityKey() === identityKey;
321
+ }
322
+ clearV2SessionInitRetry() {
323
+ if (this.v2SessionInitRetryTimer) {
324
+ globalThis.clearTimeout(this.v2SessionInitRetryTimer);
325
+ this.v2SessionInitRetryTimer = null;
326
+ }
327
+ this.v2SessionInitRetryBackgroundSync = false;
328
+ this.v2SessionInitRetryIdentityKey = null;
329
+ }
330
+ scheduleV2SessionInitRetry(attempt, backgroundSync, reason) {
331
+ const client = this.client;
332
+ if (attempt > V2_SESSION_INIT_RETRY_LIMIT) {
333
+ this.v2SessionInitRetryBackgroundSync = false;
334
+ this.v2SessionInitRetryIdentityKey = null;
335
+ client._clientLog.warn(`V2 session init retry exhausted: attempts=${V2_SESSION_INIT_RETRY_LIMIT} last_error=${formatE2EEError(reason)}`);
336
+ return;
337
+ }
338
+ const identityKey = this.currentV2SessionInitRetryIdentityKey();
339
+ if (!identityKey || !this.canRetryV2SessionInit(identityKey))
340
+ return;
341
+ if (this.v2SessionInitRetryIdentityKey && this.v2SessionInitRetryIdentityKey !== identityKey) {
342
+ this.clearV2SessionInitRetry();
343
+ }
344
+ if (this.v2SessionInitRetryTimer) {
345
+ this.v2SessionInitRetryBackgroundSync = this.v2SessionInitRetryBackgroundSync || backgroundSync;
346
+ return;
347
+ }
348
+ this.v2SessionInitRetryIdentityKey = identityKey;
349
+ this.v2SessionInitRetryBackgroundSync = this.v2SessionInitRetryBackgroundSync || backgroundSync;
350
+ const delayMs = Math.min(V2_SESSION_INIT_RETRY_BASE_MS * attempt, V2_SESSION_INIT_RETRY_MAX_MS);
351
+ client._clientLog.debug(`V2 session init retry scheduled: attempt=${attempt} delay_ms=${delayMs} reason=${formatE2EEError(reason)}`);
352
+ this.v2SessionInitRetryTimer = globalThis.setTimeout(() => {
353
+ this.v2SessionInitRetryTimer = null;
354
+ if (!this.canRetryV2SessionInit(identityKey)) {
355
+ this.clearV2SessionInitRetry();
356
+ return;
357
+ }
358
+ client._safeAsync(this.retryV2SessionInit(attempt, identityKey));
359
+ }, delayMs);
360
+ }
361
+ async retryV2SessionInit(attempt, identityKey) {
362
+ const client = this.client;
363
+ if (!this.canRetryV2SessionInit(identityKey)) {
364
+ this.clearV2SessionInitRetry();
365
+ return;
366
+ }
367
+ try {
368
+ await client._initV2Session();
369
+ const backgroundSync = this.v2SessionInitRetryBackgroundSync;
370
+ this.clearV2SessionInitRetry();
371
+ client._clientLog.debug(`V2 session init retry succeeded: attempt=${attempt}`);
372
+ if (backgroundSync && client._v2Session) {
373
+ if (typeof client._v2AutoConfirmPendingProposals === 'function') {
374
+ client._safeAsync(client._v2AutoConfirmPendingProposals());
375
+ }
376
+ if (typeof client._fillP2pGap === 'function') {
377
+ client._safeAsync(client._fillP2pGap());
378
+ }
379
+ }
380
+ }
381
+ catch (exc) {
382
+ client._clientLog.warn(`V2 session init retry failed: attempt=${attempt} err=${formatE2EEError(exc)}`);
383
+ if (isTransientV2SessionInitError(exc)) {
384
+ this.scheduleV2SessionInitRetry(attempt + 1, this.v2SessionInitRetryBackgroundSync, exc);
385
+ }
386
+ else {
387
+ this.clearV2SessionInitRetry();
388
+ }
389
+ }
390
+ }
250
391
  async initV2Session() {
251
392
  const client = this.client;
252
393
  if (!client._aid)
@@ -563,6 +704,7 @@ export class V2E2EECoordinator {
563
704
  const decrypted = [];
564
705
  let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
565
706
  let pageCount = 0;
707
+ let lastAutoAckSeq = 0;
566
708
  const maxPages = 100;
567
709
  while (pageCount < maxPages) {
568
710
  pageCount += 1;
@@ -571,7 +713,12 @@ export class V2E2EECoordinator {
571
713
  limit,
572
714
  ...(opts?.force ? { force: true } : {}),
573
715
  });
574
- const messages = (Array.isArray(result?.messages) ? result.messages : []);
716
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
717
+ const messages = pageMessages.filter((msg) => {
718
+ const seq = Number(msg.seq ?? 0);
719
+ return Number.isFinite(seq) && seq > nextAfterSeq;
720
+ });
721
+ client._clientLog.debug(`message.v2.pull page response: page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, server_ack_seq=${String(result.server_ack_seq ?? '')}`);
575
722
  const seqs = messages
576
723
  .map((msg) => Number(msg.seq ?? 0))
577
724
  .filter((seq) => Number.isFinite(seq) && seq > 0);
@@ -650,17 +797,19 @@ export class V2E2EECoordinator {
650
797
  const contigAdvanced = ackSeq !== pageContigBefore;
651
798
  if (contigAdvanced) {
652
799
  await client._drainOrderedMessages(ns);
653
- client._saveSeqTrackerState();
800
+ client._persistSeq(ns);
654
801
  }
655
- const ackNeeded = messages.length > 0
802
+ const ackNeeded = pageMessages.length > 0
656
803
  && ackSeq > 0
804
+ && ackSeq > lastAutoAckSeq
657
805
  && (contigAdvanced || (hasServerAckSeq && ackSeq > serverAckSeq));
658
806
  if (ackNeeded) {
659
807
  await this.ackV2(ackSeq);
808
+ lastAutoAckSeq = ackSeq;
660
809
  }
661
810
  }
662
811
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
663
- if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
812
+ if (messages.length === 0 || nextAfter <= nextAfterSeq)
664
813
  break;
665
814
  nextAfterSeq = nextAfter;
666
815
  }
@@ -737,7 +886,7 @@ export class V2E2EECoordinator {
737
886
  const ns = `group:${gid}`;
738
887
  client._seqTracker.onMessageSeq(ns, seq);
739
888
  client._markPublishedSeq(ns, seq);
740
- client._saveSeqTrackerState();
889
+ await client._saveSeqTrackerState();
741
890
  }
742
891
  }
743
892
  return client._delivery.attachSendResultEnvelope('group.send', {
@@ -761,7 +910,7 @@ export class V2E2EECoordinator {
761
910
  const ns = `group:${gid}`;
762
911
  client._seqTracker.onMessageSeq(ns, seq);
763
912
  client._markPublishedSeq(ns, seq);
764
- client._saveSeqTrackerState();
913
+ await client._saveSeqTrackerState();
765
914
  }
766
915
  }
767
916
  return client._delivery.attachSendResultEnvelope('group.send', {
@@ -787,6 +936,7 @@ export class V2E2EECoordinator {
787
936
  throw new ValidationError('group.pull requires group_id');
788
937
  const ns = `group:${gid}`;
789
938
  const decrypted = [];
939
+ const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
790
940
  const cursorParams = opts?.cursorParams ?? {};
791
941
  const ownsCursor = opts?.ownsCursor !== false;
792
942
  let nextAfterSeq = opts?.explicitAfterSeq ? afterSeq : (afterSeq || client._seqTracker.getContiguousSeq(ns));
@@ -795,7 +945,8 @@ export class V2E2EECoordinator {
795
945
  while (pageCount < maxPages) {
796
946
  pageCount += 1;
797
947
  const result = await client._callRawV2Rpc('group.v2.pull', {
798
- group_id: gid,
948
+ group_id: wireGroupId,
949
+ group_aid: gid,
799
950
  after_seq: nextAfterSeq,
800
951
  limit,
801
952
  ...cursorParams,
@@ -886,7 +1037,7 @@ export class V2E2EECoordinator {
886
1037
  const contigAdvanced = ackSeq !== pageContigBefore;
887
1038
  if (contigAdvanced) {
888
1039
  await client._drainOrderedMessages(ns);
889
- client._saveSeqTrackerState();
1040
+ client._persistSeq(ns);
890
1041
  }
891
1042
  const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
892
1043
  const ackNeeded = messages.length > 0
@@ -918,7 +1069,7 @@ export class V2E2EECoordinator {
918
1069
  if (!Number.isFinite(seq) || seq <= 0)
919
1070
  return { acked: 0 };
920
1071
  seq = client._clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
921
- return client._callRawV2Rpc('group.v2.ack', { group_id: gid, up_to_seq: seq });
1072
+ return client._callRawV2Rpc('group.v2.ack', { group_id: gid, group_aid: gid, up_to_seq: seq });
922
1073
  }
923
1074
  async putMessageThoughtEncryptedV2(params) {
924
1075
  const client = this.client;
@@ -1429,6 +1580,7 @@ export class V2E2EECoordinator {
1429
1580
  wrapPolicy = cached.wrapPolicy;
1430
1581
  }
1431
1582
  else {
1583
+ this.targetSetCache.delete(to);
1432
1584
  const bs = await client.call('message.v2.bootstrap', {
1433
1585
  peer_aid: to,
1434
1586
  e2ee_wrap_capabilities: v2WrapCapabilities(),
@@ -1448,73 +1600,85 @@ export class V2E2EECoordinator {
1448
1600
  if (peerDevices.length === 0) {
1449
1601
  throw new E2EEError(`V2 bootstrap: no devices found for ${to}`);
1450
1602
  }
1451
- const targets = [];
1452
- for (const dev of peerDevices) {
1453
- const devId = getV2DeviceId(dev);
1454
- const target = await client._v2BuildTargetFromDevice({
1455
- dev,
1456
- aid: to,
1457
- deviceId: devId.value,
1458
- role: 'peer',
1459
- defaultKeySource: 'peer_device_prekey',
1460
- });
1461
- if (target)
1462
- targets.push(target);
1463
- }
1464
- for (const dev of auditRaw) {
1465
- const target = await client._v2BuildTargetFromDevice({
1466
- dev,
1467
- aid: String(dev.aid ?? ''),
1468
- deviceId: String(dev.device_id ?? ''),
1469
- role: 'audit',
1470
- defaultKeySource: 'peer_device_prekey',
1471
- });
1472
- if (target)
1473
- targets.push(target);
1603
+ let targetSet = useCache ? this.getTargetSetCache(to) : undefined;
1604
+ if (targetSet) {
1605
+ targetSet = this.cloneP2PTargetSet(targetSet);
1474
1606
  }
1475
- if (client._aid && client._aid !== to) {
1476
- try {
1477
- const selfCached = this.getBootstrapCacheEntry(client._aid);
1478
- let selfDevices = [];
1479
- if (selfCached && (Date.now() - Number(selfCached.cachedAt ?? 0)) < V2_BOOTSTRAP_TTL_MS) {
1480
- selfDevices = (selfCached.devices ?? []);
1481
- }
1482
- else {
1483
- const selfBs = await client.call('message.v2.bootstrap', {
1484
- peer_aid: client._aid,
1485
- e2ee_wrap_capabilities: v2WrapCapabilities(),
1486
- });
1487
- selfDevices = (Array.isArray(selfBs?.peer_devices) ? selfBs.peer_devices : []);
1488
- if (selfDevices.length > 0) {
1489
- this.setBootstrapCacheEntry(client._aid, {
1490
- devices: selfDevices,
1491
- auditRecipients: [],
1492
- cachedAt: Date.now(),
1607
+ else {
1608
+ const targets = [];
1609
+ for (const dev of peerDevices) {
1610
+ const devId = getV2DeviceId(dev);
1611
+ const target = await client._v2BuildTargetFromDevice({
1612
+ dev,
1613
+ aid: to,
1614
+ deviceId: devId.value,
1615
+ role: 'peer',
1616
+ defaultKeySource: 'peer_device_prekey',
1617
+ });
1618
+ if (target)
1619
+ targets.push(target);
1620
+ }
1621
+ const auditTargets = [];
1622
+ for (const dev of auditRaw) {
1623
+ const target = await client._v2BuildTargetFromDevice({
1624
+ dev,
1625
+ aid: String(dev.aid ?? ''),
1626
+ deviceId: String(dev.device_id ?? ''),
1627
+ role: 'audit',
1628
+ defaultKeySource: 'peer_device_prekey',
1629
+ });
1630
+ if (target)
1631
+ auditTargets.push(target);
1632
+ }
1633
+ if (client._aid && client._aid !== to) {
1634
+ try {
1635
+ const selfCached = this.getBootstrapCacheEntry(client._aid);
1636
+ let selfDevices = [];
1637
+ if (selfCached && (Date.now() - Number(selfCached.cachedAt ?? 0)) < V2_BOOTSTRAP_TTL_MS) {
1638
+ selfDevices = (selfCached.devices ?? []);
1639
+ }
1640
+ else {
1641
+ this.targetSetCache.delete(client._aid);
1642
+ const selfBs = await client.call('message.v2.bootstrap', {
1643
+ peer_aid: client._aid,
1644
+ e2ee_wrap_capabilities: v2WrapCapabilities(),
1645
+ });
1646
+ selfDevices = (Array.isArray(selfBs?.peer_devices) ? selfBs.peer_devices : []);
1647
+ if (selfDevices.length > 0) {
1648
+ this.setBootstrapCacheEntry(client._aid, {
1649
+ devices: selfDevices,
1650
+ auditRecipients: [],
1651
+ cachedAt: Date.now(),
1652
+ });
1653
+ }
1654
+ }
1655
+ for (const dev of selfDevices) {
1656
+ const devId = getV2DeviceId(dev);
1657
+ if (!devId.present || devId.value === client._deviceId)
1658
+ continue;
1659
+ const target = await client._v2BuildTargetFromDevice({
1660
+ dev,
1661
+ aid: client._aid,
1662
+ deviceId: devId.value,
1663
+ role: 'self_sync',
1664
+ defaultKeySource: 'peer_device_prekey',
1493
1665
  });
1666
+ if (target)
1667
+ targets.push(target);
1494
1668
  }
1495
1669
  }
1496
- for (const dev of selfDevices) {
1497
- const devId = getV2DeviceId(dev);
1498
- if (!devId.present || devId.value === client._deviceId)
1499
- continue;
1500
- const target = await client._v2BuildTargetFromDevice({
1501
- dev,
1502
- aid: client._aid,
1503
- deviceId: devId.value,
1504
- role: 'self_sync',
1505
- defaultKeySource: 'peer_device_prekey',
1506
- });
1507
- if (target)
1508
- targets.push(target);
1670
+ catch (exc) {
1671
+ client._clientLog.debug(`V2 thought self-sync bootstrap failed (non-fatal): ${String(exc)}`);
1509
1672
  }
1510
1673
  }
1511
- catch (exc) {
1512
- client._clientLog.debug(`V2 thought self-sync bootstrap failed (non-fatal): ${String(exc)}`);
1513
- }
1674
+ targetSet = this.setTargetSetCache(to, {
1675
+ targets: applyV2WrapPolicyToTargets(targets, wrapPolicy),
1676
+ auditRecipients: applyV2WrapPolicyToTargets(auditTargets, wrapPolicy),
1677
+ });
1678
+ targetSet = this.cloneP2PTargetSet(targetSet);
1514
1679
  }
1515
1680
  const sender = await session.getSenderIdentity();
1516
- const sendTargets = applyV2WrapPolicyToTargets(targets, wrapPolicy);
1517
- const envelope = await encryptP2PMessage(sender, { targets: sendTargets, auditRecipients: [] }, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context });
1681
+ const envelope = await encryptP2PMessage(sender, targetSet, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context });
1518
1682
  return envelope;
1519
1683
  }
1520
1684
  async buildV2GroupEnvelope(opts) {
@@ -1523,7 +1687,8 @@ export class V2E2EECoordinator {
1523
1687
  throw new StateError('V2 session not initialized');
1524
1688
  }
1525
1689
  const session = client._v2Session;
1526
- const groupId = normalizeGroupId(opts.groupId) || String(opts.groupId ?? '').trim();
1690
+ const rawGroupId = String(opts.groupId ?? '').trim();
1691
+ const groupId = normalizeGroupId(rawGroupId) || rawGroupId;
1527
1692
  if (!groupId)
1528
1693
  throw new ValidationError("group.send requires 'group_id'");
1529
1694
  const useCache = opts.useCache !== false;
@@ -1533,15 +1698,26 @@ export class V2E2EECoordinator {
1533
1698
  let stateCommitment = { state_version: 0, state_hash: '', state_chain: '' };
1534
1699
  let auditRecipientsRaw = [];
1535
1700
  let wrapPolicy;
1536
- const cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : undefined;
1701
+ let usedCachedBootstrap = false;
1702
+ let cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : undefined;
1703
+ if (!cached && useCache && rawGroupId && rawGroupId !== groupId) {
1704
+ const legacyCacheKey = `group:${rawGroupId}`;
1705
+ cached = this.getBootstrapCacheEntry(legacyCacheKey);
1706
+ if (cached) {
1707
+ this.setBootstrapCacheEntry(cacheKey, cached);
1708
+ this.deleteBootstrapCacheEntry(legacyCacheKey);
1709
+ }
1710
+ }
1537
1711
  if (cached && (Date.now() - Number(cached.cachedAt ?? 0)) < V2_BOOTSTRAP_TTL_MS) {
1538
1712
  allDevices = (cached.devices ?? []);
1539
1713
  epoch = Number(cached.epoch ?? 0) || 0;
1540
1714
  stateCommitment = cached.stateCommitment ?? { state_version: 0, state_hash: '', state_chain: '' };
1541
1715
  auditRecipientsRaw = (cached.auditRecipients ?? []);
1542
1716
  wrapPolicy = cached.wrapPolicy;
1717
+ usedCachedBootstrap = true;
1543
1718
  }
1544
1719
  else {
1720
+ this.targetSetCache.delete(cacheKey);
1545
1721
  const bs = await client.call('group.v2.bootstrap', {
1546
1722
  group_id: groupId,
1547
1723
  e2ee_wrap_capabilities: v2WrapCapabilities(),
@@ -1576,40 +1752,64 @@ export class V2E2EECoordinator {
1576
1752
  if (allDevices.length === 0) {
1577
1753
  throw new E2EEError(`V2 group bootstrap: no devices found for group ${groupId}`);
1578
1754
  }
1579
- const targets = [];
1580
- for (const dev of allDevices) {
1581
- const devAid = String(dev.aid ?? '').trim();
1582
- const devId = getV2DeviceId(dev);
1583
- if (devAid === client._aid && devId.present && devId.value === client._deviceId)
1584
- continue;
1585
- const role = devAid === client._aid ? 'self_sync' : 'member';
1586
- const target = await client._v2BuildTargetFromDevice({
1587
- dev,
1588
- aid: devAid,
1589
- deviceId: devId.value,
1590
- role,
1591
- defaultKeySource: 'peer_device_prekey',
1755
+ let targetSet = useCache ? this.getTargetSetCache(cacheKey) : undefined;
1756
+ if (targetSet) {
1757
+ targetSet = this.cloneGroupTargetSet(targetSet);
1758
+ }
1759
+ else {
1760
+ const targets = [];
1761
+ for (const dev of allDevices) {
1762
+ const devAid = String(dev.aid ?? '').trim();
1763
+ const devId = getV2DeviceId(dev);
1764
+ if (devAid === client._aid && devId.present && devId.value === client._deviceId)
1765
+ continue;
1766
+ const role = devAid === client._aid ? 'self_sync' : 'member';
1767
+ const target = await client._v2BuildTargetFromDevice({
1768
+ dev,
1769
+ aid: devAid,
1770
+ deviceId: devId.value,
1771
+ role,
1772
+ defaultKeySource: 'peer_device_prekey',
1773
+ });
1774
+ if (target)
1775
+ targets.push(target);
1776
+ }
1777
+ if (targets.length === 0) {
1778
+ if (usedCachedBootstrap) {
1779
+ throw new E2EEError(`V2 group: no target devices for group ${groupId}; bootstrap may be stale`, {
1780
+ code: V2_GROUP_STALE_BOOTSTRAP_CODE,
1781
+ data: { expected_peer_aids: [] },
1782
+ });
1783
+ }
1784
+ throw new E2EEError(`V2 group: no target devices for group ${groupId}`);
1785
+ }
1786
+ for (const dev of auditRecipientsRaw) {
1787
+ const target = await client._v2BuildTargetFromDevice({
1788
+ dev,
1789
+ aid: String(dev.aid ?? ''),
1790
+ deviceId: String(dev.device_id ?? ''),
1791
+ role: 'audit',
1792
+ defaultKeySource: 'peer_device_prekey',
1793
+ });
1794
+ if (target)
1795
+ targets.push(target);
1796
+ }
1797
+ targetSet = this.setTargetSetCache(cacheKey, {
1798
+ targets: applyV2WrapPolicyToTargets(targets, wrapPolicy),
1592
1799
  });
1593
- if (target)
1594
- targets.push(target);
1800
+ targetSet = this.cloneGroupTargetSet(targetSet);
1595
1801
  }
1596
- if (targets.length === 0) {
1802
+ if (targetSet.targets.length === 0) {
1803
+ if (usedCachedBootstrap) {
1804
+ throw new E2EEError(`V2 group: no target devices for group ${groupId}; bootstrap may be stale`, {
1805
+ code: V2_GROUP_STALE_BOOTSTRAP_CODE,
1806
+ data: { expected_peer_aids: [] },
1807
+ });
1808
+ }
1597
1809
  throw new E2EEError(`V2 group: no target devices for group ${groupId}`);
1598
1810
  }
1599
- for (const dev of auditRecipientsRaw) {
1600
- const target = await client._v2BuildTargetFromDevice({
1601
- dev,
1602
- aid: String(dev.aid ?? ''),
1603
- deviceId: String(dev.device_id ?? ''),
1604
- role: 'audit',
1605
- defaultKeySource: 'peer_device_prekey',
1606
- });
1607
- if (target)
1608
- targets.push(target);
1609
- }
1610
1811
  const sender = await session.getSenderIdentity();
1611
- const sendTargets = applyV2WrapPolicyToTargets(targets, wrapPolicy);
1612
- const envelope = await encryptGroupMessage(sender, groupId, epoch, sendTargets, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context }, stateCommitment);
1812
+ const envelope = await encryptGroupMessage(sender, groupId, epoch, targetSet.targets, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context }, stateCommitment);
1613
1813
  return envelope;
1614
1814
  }
1615
1815
  handleGroupChangedSpk(data, groupId, action) {
@@ -1627,26 +1827,5 @@ export class V2E2EECoordinator {
1627
1827
  this.scheduleGroupSpkRotation(groupId, { reason: `group_changed:${action}` });
1628
1828
  }
1629
1829
  }
1630
- async handleV2EpochRotated(data) {
1631
- const client = this.client;
1632
- if (!data || typeof data !== 'object' || Array.isArray(data))
1633
- return;
1634
- const d = data;
1635
- const groupId = String(d.group_id ?? '').trim();
1636
- if (!groupId)
1637
- return;
1638
- const epoch = d.epoch ?? 0;
1639
- client._clientLog.debug(`_onV2EpochRotated: group=${groupId} epoch=${String(epoch)}`);
1640
- this.deleteBootstrapCacheEntry(`group:${groupId}`);
1641
- if (!client._v2Session)
1642
- return;
1643
- try {
1644
- await client._v2Session.rotateSPK(client._v2CallFn());
1645
- client._clientLog.info(`SPK rotated after epoch change: group=${groupId} epoch=${String(epoch)}`);
1646
- }
1647
- catch (exc) {
1648
- client._clientLog.debug(`SPK rotation after epoch change failed (non-fatal): ${formatE2EEError(exc)}`);
1649
- }
1650
- }
1651
1830
  }
1652
1831
  //# sourceMappingURL=v2-e2ee.js.map