@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
@@ -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',
@@ -17,6 +21,18 @@ const MEMBERSHIP_ACTIONS = new Set([
17
21
  'invite_code_used',
18
22
  ]);
19
23
  const JOIN_ACTIONS = new Set(['member_added', 'joined', 'join_approved', 'invite_code_used']);
24
+ function v2PullTailDelayMs(msgCount, pageLimit) {
25
+ const count = Math.trunc(Number(msgCount || 0));
26
+ const limit = Math.trunc(Number(pageLimit || 0));
27
+ if (count <= 0 || (limit > 0 && count >= limit))
28
+ return 0;
29
+ return Math.min(500, Math.max(5, 1000 / (count + 1)));
30
+ }
31
+ function v2Sleep(ms) {
32
+ return new Promise((resolve) => {
33
+ globalThis.setTimeout(resolve, ms);
34
+ });
35
+ }
20
36
  function exactArrayBuffer(bytes) {
21
37
  return bytes.slice().buffer;
22
38
  }
@@ -57,6 +73,17 @@ function v2B64uToBytes(value) {
57
73
  function formatE2EEError(error) {
58
74
  return error instanceof Error ? error : String(error);
59
75
  }
76
+ function isTransientV2SessionInitError(error) {
77
+ const text = (error instanceof Error ? error.message : String(error)).toLowerCase();
78
+ return text.includes('no_service')
79
+ || text.includes('service plane unavailable')
80
+ || text.includes('transport not connected')
81
+ || text.includes('transport closed')
82
+ || text.includes('websocket closed')
83
+ || text.includes('websocket is not open')
84
+ || text.includes('connection closed')
85
+ || text.includes('socket closed');
86
+ }
60
87
  function uuidV4() {
61
88
  if (typeof crypto.randomUUID === 'function')
62
89
  return crypto.randomUUID();
@@ -203,6 +230,10 @@ function applyV2WrapPolicyToTargets(targets, policy) {
203
230
  }
204
231
  export class V2E2EECoordinator {
205
232
  runtime;
233
+ targetSetCache = new Map();
234
+ v2SessionInitRetryTimer = null;
235
+ v2SessionInitRetryBackgroundSync = false;
236
+ v2SessionInitRetryIdentityKey = null;
206
237
  constructor(runtime) {
207
238
  this.runtime = runtime;
208
239
  }
@@ -224,29 +255,151 @@ export class V2E2EECoordinator {
224
255
  }
225
256
  deleteBootstrapCacheEntry(key) {
226
257
  this.bootstrapCache.delete(key);
258
+ this.targetSetCache.delete(key);
227
259
  }
228
260
  clearBootstrapCache() {
229
261
  this.bootstrapCache.clear();
262
+ this.targetSetCache.clear();
230
263
  }
231
264
  pruneExpiredBootstrapCache(ttlMs, now = Date.now()) {
232
265
  for (const [key, entry] of this.bootstrapCache) {
233
266
  if (now - Number(entry.cachedAt ?? 0) >= ttlMs) {
234
267
  this.bootstrapCache.delete(key);
268
+ this.targetSetCache.delete(key);
235
269
  }
236
270
  }
271
+ for (const [key, entry] of this.targetSetCache) {
272
+ if (now - Number(entry.cachedAt ?? 0) >= ttlMs) {
273
+ this.targetSetCache.delete(key);
274
+ }
275
+ }
276
+ }
277
+ getTargetSetCache(key) {
278
+ const cached = this.targetSetCache.get(key);
279
+ if (!cached || Date.now() - cached.cachedAt >= V2_BOOTSTRAP_TTL_MS) {
280
+ if (cached)
281
+ this.targetSetCache.delete(key);
282
+ return undefined;
283
+ }
284
+ return cached.value;
285
+ }
286
+ setTargetSetCache(key, value) {
287
+ this.targetSetCache.set(key, { cachedAt: Date.now(), value });
288
+ return value;
289
+ }
290
+ cloneP2PTargetSet(value) {
291
+ return {
292
+ targets: value.targets.map((target) => ({ ...target })),
293
+ auditRecipients: value.auditRecipients.map((target) => ({ ...target })),
294
+ };
295
+ }
296
+ cloneGroupTargetSet(value) {
297
+ return {
298
+ targets: value.targets.map((target) => ({ ...target })),
299
+ };
237
300
  }
238
301
  async onConnected(opts) {
239
302
  const client = this.client;
240
303
  try {
241
304
  await client._initV2Session();
305
+ this.clearV2SessionInitRetry();
242
306
  }
243
307
  catch (exc) {
244
308
  client._clientLog.warn(`V2 session init failed (non-fatal): ${formatE2EEError(exc)}`);
309
+ if (isTransientV2SessionInitError(exc)) {
310
+ this.scheduleV2SessionInitRetry(1, opts.backgroundSync, exc);
311
+ }
245
312
  }
246
313
  if (opts.backgroundSync && client._v2Session && typeof client._v2AutoConfirmPendingProposals === 'function') {
247
314
  client._safeAsync(client._v2AutoConfirmPendingProposals());
248
315
  }
249
316
  }
317
+ currentV2SessionInitRetryIdentityKey() {
318
+ const client = this.client;
319
+ if (!client._aid)
320
+ return null;
321
+ return JSON.stringify([
322
+ client._aid,
323
+ client._deviceId ?? '',
324
+ client._currentAid ?? null,
325
+ Number(client._v2RuntimeGeneration ?? 0),
326
+ ]);
327
+ }
328
+ canRetryV2SessionInit(identityKey) {
329
+ const client = this.client;
330
+ return client.state === ConnectionState.READY
331
+ && !client._closing
332
+ && this.currentV2SessionInitRetryIdentityKey() === identityKey;
333
+ }
334
+ clearV2SessionInitRetry() {
335
+ if (this.v2SessionInitRetryTimer) {
336
+ globalThis.clearTimeout(this.v2SessionInitRetryTimer);
337
+ this.v2SessionInitRetryTimer = null;
338
+ }
339
+ this.v2SessionInitRetryBackgroundSync = false;
340
+ this.v2SessionInitRetryIdentityKey = null;
341
+ }
342
+ scheduleV2SessionInitRetry(attempt, backgroundSync, reason) {
343
+ const client = this.client;
344
+ if (attempt > V2_SESSION_INIT_RETRY_LIMIT) {
345
+ this.v2SessionInitRetryBackgroundSync = false;
346
+ this.v2SessionInitRetryIdentityKey = null;
347
+ client._clientLog.warn(`V2 session init retry exhausted: attempts=${V2_SESSION_INIT_RETRY_LIMIT} last_error=${formatE2EEError(reason)}`);
348
+ return;
349
+ }
350
+ const identityKey = this.currentV2SessionInitRetryIdentityKey();
351
+ if (!identityKey || !this.canRetryV2SessionInit(identityKey))
352
+ return;
353
+ if (this.v2SessionInitRetryIdentityKey && this.v2SessionInitRetryIdentityKey !== identityKey) {
354
+ this.clearV2SessionInitRetry();
355
+ }
356
+ if (this.v2SessionInitRetryTimer) {
357
+ this.v2SessionInitRetryBackgroundSync = this.v2SessionInitRetryBackgroundSync || backgroundSync;
358
+ return;
359
+ }
360
+ this.v2SessionInitRetryIdentityKey = identityKey;
361
+ this.v2SessionInitRetryBackgroundSync = this.v2SessionInitRetryBackgroundSync || backgroundSync;
362
+ const delayMs = Math.min(V2_SESSION_INIT_RETRY_BASE_MS * attempt, V2_SESSION_INIT_RETRY_MAX_MS);
363
+ client._clientLog.debug(`V2 session init retry scheduled: attempt=${attempt} delay_ms=${delayMs} reason=${formatE2EEError(reason)}`);
364
+ this.v2SessionInitRetryTimer = globalThis.setTimeout(() => {
365
+ this.v2SessionInitRetryTimer = null;
366
+ if (!this.canRetryV2SessionInit(identityKey)) {
367
+ this.clearV2SessionInitRetry();
368
+ return;
369
+ }
370
+ client._safeAsync(this.retryV2SessionInit(attempt, identityKey));
371
+ }, delayMs);
372
+ }
373
+ async retryV2SessionInit(attempt, identityKey) {
374
+ const client = this.client;
375
+ if (!this.canRetryV2SessionInit(identityKey)) {
376
+ this.clearV2SessionInitRetry();
377
+ return;
378
+ }
379
+ try {
380
+ await client._initV2Session();
381
+ const backgroundSync = this.v2SessionInitRetryBackgroundSync;
382
+ this.clearV2SessionInitRetry();
383
+ client._clientLog.debug(`V2 session init retry succeeded: attempt=${attempt}`);
384
+ if (backgroundSync && client._v2Session) {
385
+ if (typeof client._v2AutoConfirmPendingProposals === 'function') {
386
+ client._safeAsync(client._v2AutoConfirmPendingProposals());
387
+ }
388
+ if (typeof client._fillP2pGap === 'function') {
389
+ client._safeAsync(client._fillP2pGap());
390
+ }
391
+ }
392
+ }
393
+ catch (exc) {
394
+ client._clientLog.warn(`V2 session init retry failed: attempt=${attempt} err=${formatE2EEError(exc)}`);
395
+ if (isTransientV2SessionInitError(exc)) {
396
+ this.scheduleV2SessionInitRetry(attempt + 1, this.v2SessionInitRetryBackgroundSync, exc);
397
+ }
398
+ else {
399
+ this.clearV2SessionInitRetry();
400
+ }
401
+ }
402
+ }
250
403
  async initV2Session() {
251
404
  const client = this.client;
252
405
  if (!client._aid)
@@ -563,15 +716,28 @@ export class V2E2EECoordinator {
563
716
  const decrypted = [];
564
717
  let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
565
718
  let pageCount = 0;
719
+ let lastAutoAckSeq = 0;
720
+ let pendingAckSeq = 0;
566
721
  const maxPages = 100;
567
722
  while (pageCount < maxPages) {
568
723
  pageCount += 1;
724
+ const ackUpToSeq = pendingAckSeq;
569
725
  const result = await client._callRawV2Rpc('message.v2.pull', {
570
726
  after_seq: nextAfterSeq,
571
727
  limit,
572
728
  ...(opts?.force ? { force: true } : {}),
729
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
573
730
  });
574
- const messages = (Array.isArray(result?.messages) ? result.messages : []);
731
+ if (ackUpToSeq > 0) {
732
+ pendingAckSeq = 0;
733
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
734
+ }
735
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
736
+ const messages = pageMessages.filter((msg) => {
737
+ const seq = Number(msg.seq ?? 0);
738
+ return Number.isFinite(seq) && seq > nextAfterSeq;
739
+ });
740
+ 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
741
  const seqs = messages
576
742
  .map((msg) => Number(msg.seq ?? 0))
577
743
  .filter((seq) => Number.isFinite(seq) && seq > 0);
@@ -650,22 +816,43 @@ export class V2E2EECoordinator {
650
816
  const contigAdvanced = ackSeq !== pageContigBefore;
651
817
  if (contigAdvanced) {
652
818
  await client._drainOrderedMessages(ns);
653
- client._saveSeqTrackerState();
819
+ client._persistSeq(ns);
654
820
  }
655
- const ackNeeded = messages.length > 0
821
+ const ackNeeded = pageMessages.length > 0
656
822
  && ackSeq > 0
823
+ && ackSeq > lastAutoAckSeq
657
824
  && (contigAdvanced || (hasServerAckSeq && ackSeq > serverAckSeq));
658
825
  if (ackNeeded) {
659
- await this.ackV2(ackSeq);
826
+ const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
827
+ const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq;
828
+ if (canContinuePage) {
829
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
830
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
831
+ }
832
+ else {
833
+ await this.ackV2(ackSeq);
834
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
835
+ }
660
836
  }
661
837
  }
662
838
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
663
- if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
839
+ const rawCount = messages.length;
840
+ const canContinuePage = rawCount > 0 && nextAfter > nextAfterSeq;
841
+ const fullPage = canContinuePage && limit > 0 && rawCount >= limit;
842
+ const shouldContinue = canContinuePage && (fullPage || pendingAckSeq > 0);
843
+ if (!shouldContinue)
664
844
  break;
845
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
846
+ if (tailDelayMs > 0)
847
+ await v2Sleep(tailDelayMs);
665
848
  nextAfterSeq = nextAfter;
666
849
  }
667
850
  if (pageCount >= maxPages) {
668
851
  client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
852
+ if (pendingAckSeq > 0) {
853
+ await this.ackV2(pendingAckSeq);
854
+ pendingAckSeq = 0;
855
+ }
669
856
  }
670
857
  return decrypted;
671
858
  }
@@ -737,7 +924,7 @@ export class V2E2EECoordinator {
737
924
  const ns = `group:${gid}`;
738
925
  client._seqTracker.onMessageSeq(ns, seq);
739
926
  client._markPublishedSeq(ns, seq);
740
- client._saveSeqTrackerState();
927
+ await client._saveSeqTrackerState();
741
928
  }
742
929
  }
743
930
  return client._delivery.attachSendResultEnvelope('group.send', {
@@ -761,7 +948,7 @@ export class V2E2EECoordinator {
761
948
  const ns = `group:${gid}`;
762
949
  client._seqTracker.onMessageSeq(ns, seq);
763
950
  client._markPublishedSeq(ns, seq);
764
- client._saveSeqTrackerState();
951
+ await client._saveSeqTrackerState();
765
952
  }
766
953
  }
767
954
  return client._delivery.attachSendResultEnvelope('group.send', {
@@ -787,20 +974,34 @@ export class V2E2EECoordinator {
787
974
  throw new ValidationError('group.pull requires group_id');
788
975
  const ns = `group:${gid}`;
789
976
  const decrypted = [];
977
+ const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
790
978
  const cursorParams = opts?.cursorParams ?? {};
791
979
  const ownsCursor = opts?.ownsCursor !== false;
792
980
  let nextAfterSeq = opts?.explicitAfterSeq ? afterSeq : (afterSeq || client._seqTracker.getContiguousSeq(ns));
793
981
  let pageCount = 0;
982
+ let lastAutoAckSeq = 0;
983
+ let pendingAckSeq = 0;
794
984
  const maxPages = 100;
795
985
  while (pageCount < maxPages) {
796
986
  pageCount += 1;
987
+ const ackUpToSeq = ownsCursor ? pendingAckSeq : 0;
797
988
  const result = await client._callRawV2Rpc('group.v2.pull', {
798
- group_id: gid,
989
+ group_id: wireGroupId,
990
+ group_aid: gid,
799
991
  after_seq: nextAfterSeq,
800
992
  limit,
801
993
  ...cursorParams,
994
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
995
+ });
996
+ if (ackUpToSeq > 0) {
997
+ pendingAckSeq = 0;
998
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
999
+ }
1000
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1001
+ const messages = pageMessages.filter((msg) => {
1002
+ const seq = Number(msg.seq ?? 0);
1003
+ return Number.isFinite(seq) && seq > nextAfterSeq;
802
1004
  });
803
- const messages = (Array.isArray(result?.messages) ? result.messages : []);
804
1005
  const seqs = messages
805
1006
  .map((msg) => Number(msg.seq ?? 0))
806
1007
  .filter((seq) => Number.isFinite(seq) && seq > 0);
@@ -886,25 +1087,46 @@ export class V2E2EECoordinator {
886
1087
  const contigAdvanced = ackSeq !== pageContigBefore;
887
1088
  if (contigAdvanced) {
888
1089
  await client._drainOrderedMessages(ns);
889
- client._saveSeqTrackerState();
1090
+ client._persistSeq(ns);
890
1091
  }
891
1092
  const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
892
- const ackNeeded = messages.length > 0
1093
+ const ackNeeded = pageMessages.length > 0
893
1094
  && ackSeq > 0
1095
+ && ackSeq > lastAutoAckSeq
894
1096
  && ownsCursor
895
1097
  && (contigAdvanced || (hasServerCursor && ackSeq > cursorCurrentSeq));
896
1098
  if (ackNeeded) {
897
- await this.ackGroupV2(gid, ackSeq);
1099
+ const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1100
+ const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
1101
+ if (canContinuePage) {
1102
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1103
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1104
+ }
1105
+ else {
1106
+ await this.ackGroupV2(gid, ackSeq);
1107
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1108
+ }
898
1109
  }
899
1110
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
900
1111
  if (!ownsCursor)
901
1112
  break;
902
- if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
1113
+ const rawCount = messages.length;
1114
+ const canContinuePage = rawCount > 0 && nextAfter > nextAfterSeq;
1115
+ const fullPage = canContinuePage && limit > 0 && rawCount >= limit;
1116
+ const shouldContinue = canContinuePage && (fullPage || pendingAckSeq > 0);
1117
+ if (!shouldContinue)
903
1118
  break;
1119
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1120
+ if (tailDelayMs > 0)
1121
+ await v2Sleep(tailDelayMs);
904
1122
  nextAfterSeq = nextAfter;
905
1123
  }
906
1124
  if (pageCount >= maxPages) {
907
1125
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
1126
+ if (pendingAckSeq > 0) {
1127
+ await this.ackGroupV2(gid, pendingAckSeq);
1128
+ pendingAckSeq = 0;
1129
+ }
908
1130
  }
909
1131
  return decrypted;
910
1132
  }
@@ -918,7 +1140,7 @@ export class V2E2EECoordinator {
918
1140
  if (!Number.isFinite(seq) || seq <= 0)
919
1141
  return { acked: 0 };
920
1142
  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 });
1143
+ return client._callRawV2Rpc('group.v2.ack', { group_id: gid, group_aid: gid, up_to_seq: seq });
922
1144
  }
923
1145
  async putMessageThoughtEncryptedV2(params) {
924
1146
  const client = this.client;
@@ -1429,6 +1651,7 @@ export class V2E2EECoordinator {
1429
1651
  wrapPolicy = cached.wrapPolicy;
1430
1652
  }
1431
1653
  else {
1654
+ this.targetSetCache.delete(to);
1432
1655
  const bs = await client.call('message.v2.bootstrap', {
1433
1656
  peer_aid: to,
1434
1657
  e2ee_wrap_capabilities: v2WrapCapabilities(),
@@ -1448,73 +1671,85 @@ export class V2E2EECoordinator {
1448
1671
  if (peerDevices.length === 0) {
1449
1672
  throw new E2EEError(`V2 bootstrap: no devices found for ${to}`);
1450
1673
  }
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);
1674
+ let targetSet = useCache ? this.getTargetSetCache(to) : undefined;
1675
+ if (targetSet) {
1676
+ targetSet = this.cloneP2PTargetSet(targetSet);
1474
1677
  }
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(),
1678
+ else {
1679
+ const targets = [];
1680
+ for (const dev of peerDevices) {
1681
+ const devId = getV2DeviceId(dev);
1682
+ const target = await client._v2BuildTargetFromDevice({
1683
+ dev,
1684
+ aid: to,
1685
+ deviceId: devId.value,
1686
+ role: 'peer',
1687
+ defaultKeySource: 'peer_device_prekey',
1688
+ });
1689
+ if (target)
1690
+ targets.push(target);
1691
+ }
1692
+ const auditTargets = [];
1693
+ for (const dev of auditRaw) {
1694
+ const target = await client._v2BuildTargetFromDevice({
1695
+ dev,
1696
+ aid: String(dev.aid ?? ''),
1697
+ deviceId: String(dev.device_id ?? ''),
1698
+ role: 'audit',
1699
+ defaultKeySource: 'peer_device_prekey',
1700
+ });
1701
+ if (target)
1702
+ auditTargets.push(target);
1703
+ }
1704
+ if (client._aid && client._aid !== to) {
1705
+ try {
1706
+ const selfCached = this.getBootstrapCacheEntry(client._aid);
1707
+ let selfDevices = [];
1708
+ if (selfCached && (Date.now() - Number(selfCached.cachedAt ?? 0)) < V2_BOOTSTRAP_TTL_MS) {
1709
+ selfDevices = (selfCached.devices ?? []);
1710
+ }
1711
+ else {
1712
+ this.targetSetCache.delete(client._aid);
1713
+ const selfBs = await client.call('message.v2.bootstrap', {
1714
+ peer_aid: client._aid,
1715
+ e2ee_wrap_capabilities: v2WrapCapabilities(),
1716
+ });
1717
+ selfDevices = (Array.isArray(selfBs?.peer_devices) ? selfBs.peer_devices : []);
1718
+ if (selfDevices.length > 0) {
1719
+ this.setBootstrapCacheEntry(client._aid, {
1720
+ devices: selfDevices,
1721
+ auditRecipients: [],
1722
+ cachedAt: Date.now(),
1723
+ });
1724
+ }
1725
+ }
1726
+ for (const dev of selfDevices) {
1727
+ const devId = getV2DeviceId(dev);
1728
+ if (!devId.present || devId.value === client._deviceId)
1729
+ continue;
1730
+ const target = await client._v2BuildTargetFromDevice({
1731
+ dev,
1732
+ aid: client._aid,
1733
+ deviceId: devId.value,
1734
+ role: 'self_sync',
1735
+ defaultKeySource: 'peer_device_prekey',
1493
1736
  });
1737
+ if (target)
1738
+ targets.push(target);
1494
1739
  }
1495
1740
  }
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);
1741
+ catch (exc) {
1742
+ client._clientLog.debug(`V2 thought self-sync bootstrap failed (non-fatal): ${String(exc)}`);
1509
1743
  }
1510
1744
  }
1511
- catch (exc) {
1512
- client._clientLog.debug(`V2 thought self-sync bootstrap failed (non-fatal): ${String(exc)}`);
1513
- }
1745
+ targetSet = this.setTargetSetCache(to, {
1746
+ targets: applyV2WrapPolicyToTargets(targets, wrapPolicy),
1747
+ auditRecipients: applyV2WrapPolicyToTargets(auditTargets, wrapPolicy),
1748
+ });
1749
+ targetSet = this.cloneP2PTargetSet(targetSet);
1514
1750
  }
1515
1751
  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 });
1752
+ const envelope = await encryptP2PMessage(sender, targetSet, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context });
1518
1753
  return envelope;
1519
1754
  }
1520
1755
  async buildV2GroupEnvelope(opts) {
@@ -1523,7 +1758,8 @@ export class V2E2EECoordinator {
1523
1758
  throw new StateError('V2 session not initialized');
1524
1759
  }
1525
1760
  const session = client._v2Session;
1526
- const groupId = normalizeGroupId(opts.groupId) || String(opts.groupId ?? '').trim();
1761
+ const rawGroupId = String(opts.groupId ?? '').trim();
1762
+ const groupId = normalizeGroupId(rawGroupId) || rawGroupId;
1527
1763
  if (!groupId)
1528
1764
  throw new ValidationError("group.send requires 'group_id'");
1529
1765
  const useCache = opts.useCache !== false;
@@ -1533,15 +1769,26 @@ export class V2E2EECoordinator {
1533
1769
  let stateCommitment = { state_version: 0, state_hash: '', state_chain: '' };
1534
1770
  let auditRecipientsRaw = [];
1535
1771
  let wrapPolicy;
1536
- const cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : undefined;
1772
+ let usedCachedBootstrap = false;
1773
+ let cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : undefined;
1774
+ if (!cached && useCache && rawGroupId && rawGroupId !== groupId) {
1775
+ const legacyCacheKey = `group:${rawGroupId}`;
1776
+ cached = this.getBootstrapCacheEntry(legacyCacheKey);
1777
+ if (cached) {
1778
+ this.setBootstrapCacheEntry(cacheKey, cached);
1779
+ this.deleteBootstrapCacheEntry(legacyCacheKey);
1780
+ }
1781
+ }
1537
1782
  if (cached && (Date.now() - Number(cached.cachedAt ?? 0)) < V2_BOOTSTRAP_TTL_MS) {
1538
1783
  allDevices = (cached.devices ?? []);
1539
1784
  epoch = Number(cached.epoch ?? 0) || 0;
1540
1785
  stateCommitment = cached.stateCommitment ?? { state_version: 0, state_hash: '', state_chain: '' };
1541
1786
  auditRecipientsRaw = (cached.auditRecipients ?? []);
1542
1787
  wrapPolicy = cached.wrapPolicy;
1788
+ usedCachedBootstrap = true;
1543
1789
  }
1544
1790
  else {
1791
+ this.targetSetCache.delete(cacheKey);
1545
1792
  const bs = await client.call('group.v2.bootstrap', {
1546
1793
  group_id: groupId,
1547
1794
  e2ee_wrap_capabilities: v2WrapCapabilities(),
@@ -1576,40 +1823,64 @@ export class V2E2EECoordinator {
1576
1823
  if (allDevices.length === 0) {
1577
1824
  throw new E2EEError(`V2 group bootstrap: no devices found for group ${groupId}`);
1578
1825
  }
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',
1826
+ let targetSet = useCache ? this.getTargetSetCache(cacheKey) : undefined;
1827
+ if (targetSet) {
1828
+ targetSet = this.cloneGroupTargetSet(targetSet);
1829
+ }
1830
+ else {
1831
+ const targets = [];
1832
+ for (const dev of allDevices) {
1833
+ const devAid = String(dev.aid ?? '').trim();
1834
+ const devId = getV2DeviceId(dev);
1835
+ if (devAid === client._aid && devId.present && devId.value === client._deviceId)
1836
+ continue;
1837
+ const role = devAid === client._aid ? 'self_sync' : 'member';
1838
+ const target = await client._v2BuildTargetFromDevice({
1839
+ dev,
1840
+ aid: devAid,
1841
+ deviceId: devId.value,
1842
+ role,
1843
+ defaultKeySource: 'peer_device_prekey',
1844
+ });
1845
+ if (target)
1846
+ targets.push(target);
1847
+ }
1848
+ if (targets.length === 0) {
1849
+ if (usedCachedBootstrap) {
1850
+ throw new E2EEError(`V2 group: no target devices for group ${groupId}; bootstrap may be stale`, {
1851
+ code: V2_GROUP_STALE_BOOTSTRAP_CODE,
1852
+ data: { expected_peer_aids: [] },
1853
+ });
1854
+ }
1855
+ throw new E2EEError(`V2 group: no target devices for group ${groupId}`);
1856
+ }
1857
+ for (const dev of auditRecipientsRaw) {
1858
+ const target = await client._v2BuildTargetFromDevice({
1859
+ dev,
1860
+ aid: String(dev.aid ?? ''),
1861
+ deviceId: String(dev.device_id ?? ''),
1862
+ role: 'audit',
1863
+ defaultKeySource: 'peer_device_prekey',
1864
+ });
1865
+ if (target)
1866
+ targets.push(target);
1867
+ }
1868
+ targetSet = this.setTargetSetCache(cacheKey, {
1869
+ targets: applyV2WrapPolicyToTargets(targets, wrapPolicy),
1592
1870
  });
1593
- if (target)
1594
- targets.push(target);
1871
+ targetSet = this.cloneGroupTargetSet(targetSet);
1595
1872
  }
1596
- if (targets.length === 0) {
1873
+ if (targetSet.targets.length === 0) {
1874
+ if (usedCachedBootstrap) {
1875
+ throw new E2EEError(`V2 group: no target devices for group ${groupId}; bootstrap may be stale`, {
1876
+ code: V2_GROUP_STALE_BOOTSTRAP_CODE,
1877
+ data: { expected_peer_aids: [] },
1878
+ });
1879
+ }
1597
1880
  throw new E2EEError(`V2 group: no target devices for group ${groupId}`);
1598
1881
  }
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
1882
  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);
1883
+ 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
1884
  return envelope;
1614
1885
  }
1615
1886
  handleGroupChangedSpk(data, groupId, action) {
@@ -1627,26 +1898,5 @@ export class V2E2EECoordinator {
1627
1898
  this.scheduleGroupSpkRotation(groupId, { reason: `group_changed:${action}` });
1628
1899
  }
1629
1900
  }
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
1901
  }
1652
1902
  //# sourceMappingURL=v2-e2ee.js.map