@agentunion/fastaun-browser 0.5.4 → 0.5.6

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 (151) hide show
  1. package/CHANGELOG.md +130 -94
  2. package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/216/245/345/205/245/344/270/216/346/274/224/347/244/272/346/214/207/345/215/227.md +290 -0
  3. package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/234/215/345/212/241/346/236/266/346/236/204/344/270/216/350/257/246/347/273/206/345/256/236/347/216/260/350/256/241/345/210/222-codex.md +994 -0
  4. package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/234/215/345/212/241/350/277/220/347/273/264/344/270/216/345/217/221/345/270/203/346/214/207/345/215/227.md +144 -0
  5. package/_packed_docs/CHANGELOG.md +130 -94
  6. package/_packed_docs/INDEX.md +127 -69
  7. package/_packed_docs/KITE_DOCS_GUIDE.md +63 -29
  8. package/_packed_docs/audit/AUN/346/234/215/345/212/241Go/345/214/226/351/207/215/346/236/204/347/262/276/347/273/206/345/214/226/346/272/220/347/240/201/345/256/241/346/237/245-20260718.md +366 -0
  9. package/_packed_docs/aun/345/205/254/347/275/221/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +227 -0
  10. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +1004 -0
  11. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +999 -0
  12. package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +6 -4
  13. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +58 -20
  14. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +219 -247
  15. package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +14 -14
  16. package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +3 -3
  17. 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 +165 -421
  18. package/_packed_docs/protocol/README.md +1 -0
  19. package/_packed_docs/protocol/aun-docs-guide.md +7 -4
  20. package/_packed_docs/protocol/index.md +25 -19
  21. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +62 -17
  22. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -0
  23. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -78
  24. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +6 -2
  25. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +50 -14
  26. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +18 -5
  27. package/_packed_docs/sdk/09-group-rpc-manual.md +312 -282
  28. package/_packed_docs/sdk/09-message-rpc-manual.md +142 -103
  29. package/_packed_docs/sdk/09-payload-reference.md +1 -1
  30. package/_packed_docs/sdk/09-stream-rpc-manual.md +8 -8
  31. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +26 -17
  32. package/_packed_docs/sdk/INDEX.md +42 -34
  33. package/_packed_docs/sdk/README.md +7 -6
  34. package/_packed_docs//346/266/210/346/201/257/345/220/214/346/255/245/344/270/216/345/216/206/345/217/262/346/213/211/345/217/226/344/273/243/347/240/201/345/256/241/346/237/245/351/227/256/351/242/230/346/270/205/345/215/225.md +229 -0
  35. package/_packed_docs//346/266/210/346/201/257/345/220/214/346/255/245/344/270/216/345/216/206/345/217/262/346/213/211/345/217/226/346/224/271/351/200/240/346/226/271/346/241/210.md +750 -0
  36. package/dist/agent-md.d.ts +4 -5
  37. package/dist/agent-md.d.ts.map +1 -1
  38. package/dist/agent-md.js +66 -43
  39. package/dist/agent-md.js.map +1 -1
  40. package/dist/aid-store.d.ts +2 -6
  41. package/dist/aid-store.d.ts.map +1 -1
  42. package/dist/aid-store.js +20 -14
  43. package/dist/aid-store.js.map +1 -1
  44. package/dist/auth.d.ts.map +1 -1
  45. package/dist/auth.js +30 -11
  46. package/dist/auth.js.map +1 -1
  47. package/dist/bundle.js +3297 -1023
  48. package/dist/client/delivery.d.ts +75 -8
  49. package/dist/client/delivery.d.ts.map +1 -1
  50. package/dist/client/delivery.js +1419 -245
  51. package/dist/client/delivery.js.map +1 -1
  52. package/dist/client/group-state.d.ts.map +1 -1
  53. package/dist/client/group-state.js +27 -24
  54. package/dist/client/group-state.js.map +1 -1
  55. package/dist/client/lifecycle.d.ts.map +1 -1
  56. package/dist/client/lifecycle.js +48 -4
  57. package/dist/client/lifecycle.js.map +1 -1
  58. package/dist/client/mention-mode.d.ts +7 -0
  59. package/dist/client/mention-mode.d.ts.map +1 -0
  60. package/dist/client/mention-mode.js +184 -0
  61. package/dist/client/mention-mode.js.map +1 -0
  62. package/dist/client/peers.d.ts +1 -1
  63. package/dist/client/peers.d.ts.map +1 -1
  64. package/dist/client/peers.js +26 -3
  65. package/dist/client/peers.js.map +1 -1
  66. package/dist/client/rpc-pipeline.d.ts +22 -1
  67. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  68. package/dist/client/rpc-pipeline.js +309 -74
  69. package/dist/client/rpc-pipeline.js.map +1 -1
  70. package/dist/client/v2-e2ee.d.ts +25 -2
  71. package/dist/client/v2-e2ee.d.ts.map +1 -1
  72. package/dist/client/v2-e2ee.js +606 -97
  73. package/dist/client/v2-e2ee.js.map +1 -1
  74. package/dist/client.d.ts +24 -5
  75. package/dist/client.d.ts.map +1 -1
  76. package/dist/client.js +404 -215
  77. package/dist/client.js.map +1 -1
  78. package/dist/errors.d.ts.map +1 -1
  79. package/dist/errors.js +4 -1
  80. package/dist/errors.js.map +1 -1
  81. package/dist/facades.d.ts +3 -0
  82. package/dist/facades.d.ts.map +1 -1
  83. package/dist/facades.js +91 -53
  84. package/dist/facades.js.map +1 -1
  85. package/dist/group-index.d.ts +6 -1
  86. package/dist/group-index.d.ts.map +1 -1
  87. package/dist/group-index.js +44 -25
  88. package/dist/group-index.js.map +1 -1
  89. package/dist/index.d.ts +1 -1
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +1 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/keystore/index.d.ts +10 -5
  94. package/dist/keystore/index.d.ts.map +1 -1
  95. package/dist/keystore/indexeddb-identity-store.d.ts +0 -12
  96. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  97. package/dist/keystore/indexeddb-identity-store.js +0 -60
  98. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  99. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  100. package/dist/keystore/indexeddb-shared.js +9 -5
  101. package/dist/keystore/indexeddb-shared.js.map +1 -1
  102. package/dist/keystore/indexeddb-token-store.d.ts +3 -1
  103. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  104. package/dist/keystore/indexeddb-token-store.js +47 -2
  105. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  106. package/dist/register-flow.d.ts.map +1 -1
  107. package/dist/register-flow.js +28 -3
  108. package/dist/register-flow.js.map +1 -1
  109. package/dist/seq-tracker.d.ts +28 -8
  110. package/dist/seq-tracker.d.ts.map +1 -1
  111. package/dist/seq-tracker.js +218 -61
  112. package/dist/seq-tracker.js.map +1 -1
  113. package/dist/storage/vfs.d.ts +1 -0
  114. package/dist/storage/vfs.d.ts.map +1 -1
  115. package/dist/storage/vfs.js +26 -2
  116. package/dist/storage/vfs.js.map +1 -1
  117. package/dist/tools/cross-sdk-agent.js +376 -10
  118. package/dist/tools/cross-sdk-agent.js.map +1 -1
  119. package/dist/transport.d.ts +12 -0
  120. package/dist/transport.d.ts.map +1 -1
  121. package/dist/transport.js +218 -101
  122. package/dist/transport.js.map +1 -1
  123. package/dist/v2/session/session.d.ts.map +1 -1
  124. package/dist/v2/session/session.js +3 -4
  125. package/dist/v2/session/session.js.map +1 -1
  126. package/dist/v2/state/commitment.d.ts.map +1 -1
  127. package/dist/v2/state/commitment.js +1 -2
  128. package/dist/v2/state/commitment.js.map +1 -1
  129. package/dist/version.d.ts +1 -1
  130. package/dist/version.js +1 -1
  131. package/package.json +6 -5
  132. package/dist/group-resources.d.ts +0 -98
  133. package/dist/group-resources.d.ts.map +0 -1
  134. package/dist/group-resources.js +0 -635
  135. package/dist/group-resources.js.map +0 -1
  136. package/dist/keystore/indexeddb.d.ts +0 -179
  137. package/dist/keystore/indexeddb.d.ts.map +0 -1
  138. package/dist/keystore/indexeddb.js +0 -2031
  139. package/dist/keystore/indexeddb.js.map +0 -1
  140. package/dist/namespaces/auth.d.ts +0 -98
  141. package/dist/namespaces/auth.d.ts.map +0 -1
  142. package/dist/namespaces/auth.js +0 -992
  143. package/dist/namespaces/auth.js.map +0 -1
  144. package/dist/namespaces/custody.d.ts +0 -51
  145. package/dist/namespaces/custody.d.ts.map +0 -1
  146. package/dist/namespaces/custody.js +0 -302
  147. package/dist/namespaces/custody.js.map +0 -1
  148. package/dist/namespaces/meta.d.ts +0 -109
  149. package/dist/namespaces/meta.d.ts.map +0 -1
  150. package/dist/namespaces/meta.js +0 -549
  151. package/dist/namespaces/meta.js.map +0 -1
@@ -1,15 +1,106 @@
1
1
  import { importCertPublicKeyEcdsa } from '../crypto.js';
2
2
  import { E2EEError, StateError, ValidationError } from '../errors.js';
3
- import { normalizeGroupId } from '../group-id.js';
4
3
  import { decryptMessage, encryptGroupMessage, encryptP2PMessage, } from '../v2/e2ee/index.js';
5
4
  import { V2Session, V2KeyStore } from '../v2/session/index.js';
6
5
  import { ConnectionState, isJsonObject } from '../types.js';
6
+ import { normalizeGroupMentionMode } from './mention-mode.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
9
  const V2_GROUP_STALE_BOOTSTRAP_CODE = -33054;
10
+ const V2_PULL_DEFAULT_PAGE_LIMIT = 50;
11
+ const V2_PULL_MAX_PAGE_LIMIT = 50;
12
+ const V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS = 3_000;
10
13
  const V2_SESSION_INIT_RETRY_LIMIT = 12;
11
14
  const V2_SESSION_INIT_RETRY_BASE_MS = 1000;
12
15
  const V2_SESSION_INIT_RETRY_MAX_MS = 5000;
16
+ function strictWindowSeq(value, field, positive = false) {
17
+ if (!Number.isSafeInteger(value))
18
+ throw new ValidationError(`${field} must be an integer`);
19
+ const minimum = positive ? 1 : 0;
20
+ if (value < minimum)
21
+ throw new ValidationError(`${field} must be >= ${minimum}`);
22
+ return value;
23
+ }
24
+ function strictWindowLimit(value) {
25
+ const limit = strictWindowSeq(value, 'limit', true);
26
+ if (limit > V2_PULL_MAX_PAGE_LIMIT)
27
+ throw new ValidationError(`limit must be <= ${V2_PULL_MAX_PAGE_LIMIT}`);
28
+ return limit;
29
+ }
30
+ function deduplicateWindowMessages(value, lowerSeq, upperSeq, boundary) {
31
+ if (!Array.isArray(value))
32
+ throw new ValidationError('messages must be an array');
33
+ const bySeq = new Map();
34
+ for (const raw of value) {
35
+ if (!isJsonObject(raw))
36
+ throw new ValidationError('messages entries must be objects');
37
+ const row = raw;
38
+ const seq = strictWindowSeq(row.seq, 'messages[].seq', true);
39
+ if (seq < lowerSeq || seq > upperSeq)
40
+ throw new ValidationError(`messages[].seq is outside ${boundary}`);
41
+ const previous = bySeq.get(seq);
42
+ if (previous && JSON.stringify(previous) !== JSON.stringify(row))
43
+ throw new ValidationError('messages contains conflicting duplicate seq');
44
+ bySeq.set(seq, row);
45
+ }
46
+ return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
47
+ }
48
+ function deduplicateForwardPageMessages(value, afterSeq) {
49
+ if (!Array.isArray(value))
50
+ return [];
51
+ const bySeq = new Map();
52
+ for (const raw of value) {
53
+ if (!isJsonObject(raw))
54
+ continue;
55
+ const row = raw;
56
+ const seq = Number(row.seq ?? 0);
57
+ if (!Number.isSafeInteger(seq) || seq <= afterSeq || bySeq.has(seq))
58
+ continue;
59
+ bySeq.set(seq, row);
60
+ }
61
+ return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
62
+ }
63
+ function validateTailPage(result, pageLimit) {
64
+ if (!isJsonObject(result))
65
+ throw new ValidationError('tail response must be an object');
66
+ const row = result;
67
+ if (row.window_mode !== 'tail')
68
+ throw new ValidationError('tail response window_mode must equal tail');
69
+ const windowStart = strictWindowSeq(row.window_start_seq, 'window_start_seq');
70
+ const head = strictWindowSeq(row.head_seq, 'head_seq');
71
+ const covered = strictWindowSeq(row.covered_through_seq, 'covered_through_seq');
72
+ if (windowStart > head || covered !== head || head - windowStart + 1 > pageLimit) {
73
+ throw new ValidationError('tail response proof is invalid');
74
+ }
75
+ return { windowStart, head, messages: deduplicateWindowMessages(row.messages, windowStart, head, 'the proven tail window') };
76
+ }
77
+ function validateHistoryPage(result, beforeSeq) {
78
+ if (!isJsonObject(result))
79
+ throw new ValidationError('history response must be an object');
80
+ const row = result;
81
+ const rawStart = row.window_start_seq;
82
+ const rawNext = row.next_before_seq;
83
+ if ((rawStart === undefined) !== (rawNext === undefined))
84
+ throw new ValidationError('history pagination boundaries must both be present or absent');
85
+ let lowerSeq = 1;
86
+ let windowStart;
87
+ if (rawStart !== undefined) {
88
+ windowStart = strictWindowSeq(rawStart, 'window_start_seq', true);
89
+ const nextBefore = strictWindowSeq(rawNext, 'next_before_seq', true);
90
+ if (windowStart !== nextBefore || windowStart >= beforeSeq)
91
+ throw new ValidationError('history boundary is invalid');
92
+ lowerSeq = windowStart;
93
+ }
94
+ const messages = deduplicateWindowMessages(row.messages, lowerSeq, beforeSeq - 1, 'the history page');
95
+ if (messages.length > 0 && (windowStart === undefined || Number(messages[0].seq) !== windowStart)) {
96
+ throw new ValidationError('history messages require exact pagination boundaries');
97
+ }
98
+ if (messages.length === 0 && windowStart !== undefined)
99
+ throw new ValidationError('empty history page must not include pagination boundaries');
100
+ if (typeof row.has_older !== 'boolean' || (messages.length === 0 && row.has_older))
101
+ throw new ValidationError('has_older is invalid');
102
+ return messages;
103
+ }
13
104
  const MEMBERSHIP_ACTIONS = new Set([
14
105
  'member_added',
15
106
  'member_left',
@@ -205,6 +296,12 @@ function v2WrapCapabilities() {
205
296
  per_device_wrap: true,
206
297
  };
207
298
  }
299
+ function withExplicitGroupAid(params, groupAid) {
300
+ const aid = String(groupAid ?? '').trim();
301
+ if (aid)
302
+ params.group_aid = aid;
303
+ return params;
304
+ }
208
305
  function applyV2WrapPolicyToTargets(targets, policy) {
209
306
  if (!policy)
210
307
  return targets;
@@ -231,6 +328,7 @@ function applyV2WrapPolicyToTargets(targets, policy) {
231
328
  export class V2E2EECoordinator {
232
329
  runtime;
233
330
  targetSetCache = new Map();
331
+ senderIKSyncFetches = new Map();
234
332
  v2SessionInitRetryTimer = null;
235
333
  v2SessionInitRetryBackgroundSync = false;
236
334
  v2SessionInitRetryIdentityKey = null;
@@ -253,6 +351,18 @@ export class V2E2EECoordinator {
253
351
  setBootstrapCacheEntry(key, entry) {
254
352
  this.bootstrapCache.set(key, entry);
255
353
  }
354
+ cacheBundledSelfDevices(bootstrap, wrapPolicy) {
355
+ const client = this.client;
356
+ if (!client._aid || !Object.prototype.hasOwnProperty.call(bootstrap, 'self_devices'))
357
+ return;
358
+ const selfDevices = (Array.isArray(bootstrap.self_devices) ? bootstrap.self_devices : []);
359
+ this.setBootstrapCacheEntry(client._aid, {
360
+ devices: selfDevices,
361
+ auditRecipients: [],
362
+ cachedAt: Date.now(),
363
+ wrapPolicy,
364
+ });
365
+ }
256
366
  deleteBootstrapCacheEntry(key) {
257
367
  this.bootstrapCache.delete(key);
258
368
  this.targetSetCache.delete(key);
@@ -555,6 +665,84 @@ export class V2E2EECoordinator {
555
665
  pendingSenderIKFetchKey(fromAid, senderDeviceId, groupId) {
556
666
  return `${fromAid}#${senderDeviceId}#${groupId || ''}`;
557
667
  }
668
+ async fetchSenderIKOnce(fromAid, senderDeviceId, groupId, certFingerprint = '') {
669
+ const client = this.client;
670
+ const peerAid = String(fromAid ?? '').trim();
671
+ const deviceId = String(senderDeviceId ?? '').trim();
672
+ const gid = String(groupId ?? '').trim();
673
+ const fingerprint = String(certFingerprint ?? '').trim().toLowerCase();
674
+ if (!peerAid)
675
+ return null;
676
+ const fetchKey = [peerAid, deviceId, gid, fingerprint].join('#');
677
+ const existing = this.senderIKSyncFetches.get(fetchKey);
678
+ if (existing)
679
+ return await existing;
680
+ const task = (async () => {
681
+ const started = Date.now();
682
+ let timer;
683
+ const timedOut = Symbol('sender-ik-timeout');
684
+ const fetch = async () => {
685
+ try {
686
+ const bootstrap = await client.call('message.v2.bootstrap', {
687
+ peer_aid: peerAid,
688
+ e2ee_wrap_capabilities: v2WrapCapabilities(),
689
+ });
690
+ const peers = (Array.isArray(bootstrap?.peer_devices) ? bootstrap.peer_devices : []);
691
+ for (const dev of peers)
692
+ client._cacheV2PeerIKFromDevice(dev, peerAid);
693
+ }
694
+ catch (exc) {
695
+ client._clientLog.warn(`V2 sender IK 同步 bootstrap 失败: peer=${peerAid} device=${deviceId || '-'} err=${String(formatE2EEError(exc))}`);
696
+ }
697
+ let senderPubDer = await this.getV2SenderPubDer(peerAid, deviceId, fingerprint);
698
+ if (senderPubDer || !gid)
699
+ return senderPubDer;
700
+ try {
701
+ const bootstrap = await client.call('group.v2.bootstrap', {
702
+ group_id: gid,
703
+ e2ee_wrap_capabilities: v2WrapCapabilities(),
704
+ });
705
+ const devices = (Array.isArray(bootstrap?.devices) ? bootstrap.devices : []);
706
+ const audit = (Array.isArray(bootstrap?.audit_recipients) ? bootstrap.audit_recipients : []);
707
+ for (const dev of devices)
708
+ client._cacheV2PeerIKFromDevice(dev);
709
+ for (const dev of audit)
710
+ client._cacheV2PeerIKFromDevice(dev);
711
+ }
712
+ catch (exc) {
713
+ client._clientLog.warn(`V2 sender IK 同步 group bootstrap 失败: group=${gid} peer=${peerAid} err=${String(formatE2EEError(exc))}`);
714
+ }
715
+ senderPubDer = await this.getV2SenderPubDer(peerAid, deviceId, fingerprint);
716
+ return senderPubDer;
717
+ };
718
+ try {
719
+ const result = await Promise.race([
720
+ fetch(),
721
+ new Promise((resolve) => {
722
+ timer = setTimeout(() => resolve(timedOut), V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS);
723
+ }),
724
+ ]);
725
+ if (result === timedOut) {
726
+ client._clientLog.warn(`V2 sender IK 同步获取超时: peer=${peerAid} device=${deviceId || '-'} group=${gid || '<p2p>'} timeout_ms=${V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS}`);
727
+ return null;
728
+ }
729
+ return result;
730
+ }
731
+ finally {
732
+ if (timer !== undefined)
733
+ clearTimeout(timer);
734
+ client._clientLog.debug(`V2 sender IK 同步获取完成: peer=${peerAid} device=${deviceId || '-'} group=${gid || '<p2p>'} elapsed_ms=${Date.now() - started}`);
735
+ }
736
+ })();
737
+ this.senderIKSyncFetches.set(fetchKey, task);
738
+ try {
739
+ return await task;
740
+ }
741
+ finally {
742
+ if (this.senderIKSyncFetches.get(fetchKey) === task)
743
+ this.senderIKSyncFetches.delete(fetchKey);
744
+ }
745
+ }
558
746
  scheduleSenderIKPending(args) {
559
747
  const client = this.client;
560
748
  const fromAid = String(args.fromAid ?? '').trim();
@@ -627,21 +815,23 @@ export class V2E2EECoordinator {
627
815
  }
628
816
  catch (exc) {
629
817
  client._clientLog.warn(`V2 sender IK pending retry raised: key=${key} err=${String(formatE2EEError(exc))}`);
818
+ client._v2SenderIKPending.delete(key);
819
+ continue;
630
820
  }
631
- client._v2SenderIKPending.delete(key);
632
821
  if (plaintext === null) {
633
- client._clientLog.debug(`V2 sender IK pending retry failed: key=${key}`);
634
- client.emit('message.undecryptable', {
635
- from: entry.fromAid,
636
- sender_device_id: entry.senderDeviceId,
637
- group_id: entry.groupId || undefined,
638
- seq: Number(entry.msg.seq ?? 0),
639
- reason: 'sender_ik_retry_failed',
640
- });
822
+ client._clientLog.warn(`V2 sender IK pending retry still undecryptable: key=${key}`);
823
+ const isGroup = Boolean(entry.groupId);
824
+ const eventName = isGroup ? 'group.message_undecryptable' : 'message.undecryptable';
825
+ const safeEvent = this.safeUndecryptablePushEvent(entry.msg, isGroup);
826
+ safeEvent._decrypt_error = 'sender IK pending retry failed';
827
+ safeEvent._decrypt_stage = 'sender_ik_pending_retry';
828
+ await client._publishAppEvent(eventName, safeEvent, 'pending_retry');
641
829
  continue;
642
830
  }
831
+ client._v2SenderIKPending.delete(key);
643
832
  const seq = Number(entry.msg.seq ?? 0);
644
833
  if (entry.groupId) {
834
+ plaintext = normalizeGroupMentionMode(plaintext, entry.msg, entry.msg.envelope_json);
645
835
  plaintext.group_id = entry.groupId;
646
836
  await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext);
647
837
  }
@@ -709,19 +899,175 @@ export class V2E2EECoordinator {
709
899
  throw exc;
710
900
  }
711
901
  }
902
+ historyDecryptFailure(msg, reason) {
903
+ return {
904
+ message_id: String(msg.message_id ?? ''),
905
+ seq: Number(msg.seq ?? 0),
906
+ encrypted: true,
907
+ history_error: { code: 'decrypt_failed', message: reason },
908
+ };
909
+ }
910
+ async decodeP2PWindowMessage(msg, publish, history, source) {
911
+ const client = this.client;
912
+ const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
913
+ let plaintext = null;
914
+ if (String(msg.version ?? '') === 'v1') {
915
+ const legacy = isJsonObject(msg.legacy_v1)
916
+ ? msg.legacy_v1 : {};
917
+ const payload = legacy.payload;
918
+ const payloadType = isJsonObject(payload)
919
+ ? String(payload.type ?? '').trim() : '';
920
+ if (payload !== undefined && payload !== null && !['e2ee.encrypted', 'e2ee.group_encrypted'].includes(payloadType)) {
921
+ plaintext = {
922
+ message_id: String(msg.message_id ?? ''), from: String(msg.from_aid ?? ''),
923
+ to: String(legacy.to ?? client._aid ?? ''), seq, type: String(msg.type ?? ''),
924
+ timestamp: msg.t_server, payload: payload, encrypted: false,
925
+ };
926
+ attachGatewayProximity(plaintext, msg);
927
+ }
928
+ }
929
+ else {
930
+ plaintext = source === 'inline_push'
931
+ ? await client._decryptV2Message(msg, false, false, false, false)
932
+ : await client._decryptV2Message(msg, false);
933
+ }
934
+ if (!plaintext) {
935
+ if (history)
936
+ return this.historyDecryptFailure(msg, 'message could not be decrypted');
937
+ client._clientLog.warn(`P2P Tail 单条消息解密失败,连续性证明仍保留: seq=${seq}`);
938
+ return null;
939
+ }
940
+ if (publish) {
941
+ const event = client._delivery.p2pAppEventForMessage(plaintext);
942
+ await client._publishAppEvent(event.event, event.payload, source);
943
+ }
944
+ return plaintext;
945
+ }
946
+ async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source) {
947
+ const client = this.client;
948
+ const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
949
+ let plaintext = null;
950
+ if (String(msg.version ?? '') === 'v1') {
951
+ const payload = msg.payload;
952
+ if (isJsonObject(payload)
953
+ && ['e2ee.encrypted', 'e2ee.group_encrypted'].includes(String(payload.type ?? ''))) {
954
+ if (history)
955
+ return this.historyDecryptFailure(msg, 'unsupported legacy encrypted envelope');
956
+ client._clientLog.warn(`Group Tail 不支持的旧加密消息已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
957
+ return null;
958
+ }
959
+ plaintext = {
960
+ message_id: String(msg.message_id ?? ''), from: String(msg.from_aid ?? ''),
961
+ group_id: groupId, group_aid: groupAid, seq, type: String(msg.type ?? ''),
962
+ timestamp: msg.t_server, payload: payload, encrypted: false,
963
+ };
964
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
965
+ attachGatewayProximity(plaintext, msg);
966
+ }
967
+ else {
968
+ plaintext = source === 'inline_push'
969
+ ? await client._decryptV2Message(msg, false, false, false, false)
970
+ : await client._decryptV2Message(msg, false);
971
+ if (plaintext) {
972
+ // Inline Head 已绑定群身份;解密结果若声称群身份,必须与其严格一致。
973
+ if (source === 'inline_push' && Object.prototype.hasOwnProperty.call(plaintext, 'group_id')
974
+ && (typeof plaintext.group_id !== 'string' || !plaintext.group_id || plaintext.group_id !== groupId)) {
975
+ return null;
976
+ }
977
+ plaintext.group_id = groupId;
978
+ plaintext.group_aid = groupAid;
979
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
980
+ }
981
+ }
982
+ if (!plaintext) {
983
+ if (history)
984
+ return this.historyDecryptFailure(msg, 'message could not be decrypted');
985
+ client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
986
+ return null;
987
+ }
988
+ if (publish)
989
+ await client._publishAppEvent('group.message_created', plaintext, source);
990
+ return plaintext;
991
+ }
992
+ async decodeInlineP2PMessage(msg) {
993
+ return await this.decodeP2PWindowMessage(msg, false, false, 'inline_push');
994
+ }
995
+ async decodeInlineGroupMessage(msg, groupId, groupAid) {
996
+ return await this.decodeGroupWindowMessage(msg, groupId, groupAid, false, false, 'inline_push');
997
+ }
998
+ async applyTailFloor(ns, floor) {
999
+ const client = this.client;
1000
+ if (!ns || floor <= 0 || client._seqTracker.getContiguousSeq(ns) >= floor)
1001
+ return;
1002
+ client._seqTracker.forceContiguousSeq(ns, floor);
1003
+ client._persistSeq(ns);
1004
+ await client._saveSeqTrackerState?.();
1005
+ }
1006
+ async pullV2TailInternal(params) {
1007
+ const client = this.client;
1008
+ if (params.window_mode !== 'tail')
1009
+ throw new ValidationError('window_mode must equal tail');
1010
+ if ('ack_up_to_seq' in params || params.force === true)
1011
+ throw new ValidationError('Tail does not accept ack_up_to_seq or force');
1012
+ const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
1013
+ const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1014
+ const ns = client._aid ? `p2p:${client._aid}` : '';
1015
+ const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true });
1016
+ const floor = Number(result.retention_floor_seq ?? 0);
1017
+ const previous = ns ? client._seqTracker.getSyncState(ns) : null;
1018
+ await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
1019
+ const page = validateTailPage(result, limit);
1020
+ if (ns) {
1021
+ client._seqTracker.commitTailWindow(ns, {
1022
+ windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
1023
+ });
1024
+ client._persistSeq(ns);
1025
+ await client._saveSeqTrackerState?.();
1026
+ }
1027
+ const decoded = [];
1028
+ for (const msg of page.messages) {
1029
+ const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail');
1030
+ if (plaintext)
1031
+ decoded.push(plaintext);
1032
+ }
1033
+ const ack = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1034
+ if (previous && ack > previous.ack)
1035
+ await this.ackV2(ack);
1036
+ return { ...result, messages: decoded, raw_count: page.messages.length };
1037
+ }
1038
+ async historyV2Internal(params) {
1039
+ const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
1040
+ const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1041
+ const result = await this.client._callRawV2Rpc('message.history', { before_seq: beforeSeq, limit, _rpc_foreground: true });
1042
+ const messages = validateHistoryPage(result, beforeSeq);
1043
+ const decoded = [];
1044
+ for (const msg of messages) {
1045
+ const plaintext = await this.decodeP2PWindowMessage(msg, false, true, 'history');
1046
+ if (plaintext)
1047
+ decoded.push(plaintext);
1048
+ }
1049
+ return { ...result, messages: decoded };
1050
+ }
712
1051
  async pullV2(afterSeq = 0, limit = 50, opts) {
713
1052
  const client = this.client;
714
1053
  await client._ensureV2SessionReady('message.pull');
715
1054
  const ns = client._aid ? `p2p:${client._aid}` : '';
1055
+ if (opts?.windowMode === 'tail') {
1056
+ const result = await this.pullV2TailInternal({ window_mode: 'tail', after_seq: afterSeq, limit });
1057
+ return Array.isArray(result.messages) ? result.messages : [];
1058
+ }
716
1059
  const decrypted = [];
717
1060
  let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
718
1061
  let pageCount = 0;
719
1062
  let lastAutoAckSeq = 0;
720
1063
  let pendingAckSeq = 0;
721
- const maxPages = 100;
1064
+ const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
722
1065
  while (pageCount < maxPages) {
723
1066
  pageCount += 1;
724
1067
  const ackUpToSeq = pendingAckSeq;
1068
+ const pendingInlineGeneration = ackUpToSeq > 0
1069
+ ? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null
1070
+ : null;
725
1071
  const result = await client._callRawV2Rpc('message.v2.pull', {
726
1072
  after_seq: nextAfterSeq,
727
1073
  limit,
@@ -729,23 +1075,32 @@ export class V2E2EECoordinator {
729
1075
  ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
730
1076
  });
731
1077
  if (ackUpToSeq > 0) {
1078
+ if (pendingInlineGeneration !== null) {
1079
+ let actualAckSeq = ackUpToSeq;
1080
+ if (Object.prototype.hasOwnProperty.call(result, 'server_ack_seq')) {
1081
+ actualAckSeq = typeof result.server_ack_seq === 'number'
1082
+ && Number.isSafeInteger(result.server_ack_seq) && result.server_ack_seq >= 0
1083
+ ? result.server_ack_seq
1084
+ : 0;
1085
+ }
1086
+ if (actualAckSeq > 0) {
1087
+ client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
1088
+ }
1089
+ }
732
1090
  pendingAckSeq = 0;
733
1091
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
734
1092
  }
735
1093
  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
- }).sort((a, b) => Number(a.seq ?? 0) - Number(b.seq ?? 0));
1094
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
740
1095
  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 ?? '')}`);
741
1096
  const seqs = messages
742
1097
  .map((msg) => Number(msg.seq ?? 0))
743
1098
  .filter((seq) => Number.isFinite(seq) && seq > 0);
744
1099
  const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1100
+ const pageTrackerSnapshot = ns && typeof client._seqTracker.snapshotNamespace === 'function'
1101
+ ? client._seqTracker.snapshotNamespace(ns)
1102
+ : null;
745
1103
  const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
746
- if (ns && seqs.length > 0) {
747
- client._seqTracker.forceContiguousSeq(ns, pageMaxSeq);
748
- }
749
1104
  for (const msg of messages) {
750
1105
  const seq = Number(msg.seq ?? 0);
751
1106
  if (!Number.isFinite(seq) || seq <= 0)
@@ -804,19 +1159,36 @@ export class V2E2EECoordinator {
804
1159
  }
805
1160
  const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
806
1161
  const serverAckSeq = Number(result.server_ack_seq ?? 0);
807
- if (ns && Number.isFinite(serverAckSeq) && serverAckSeq > 0) {
808
- const contig = client._seqTracker.getContiguousSeq(ns);
809
- if (contig < serverAckSeq) {
810
- client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${contig} -> server_ack_seq=${serverAckSeq}`);
811
- client._seqTracker.forceContiguousSeq(ns, serverAckSeq);
812
- }
813
- }
1162
+ const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
814
1163
  if (ns) {
815
- const ackSeq = client._seqTracker.getContiguousSeq(ns);
816
- const contigAdvanced = ackSeq !== pageContigBefore;
817
- if (contigAdvanced) {
818
- await client._drainOrderedMessages(ns);
819
- client._persistSeq(ns);
1164
+ const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1165
+ let ackSeq = pageContigBefore;
1166
+ let contigAdvanced = false;
1167
+ try {
1168
+ if (commitTarget > pageContigBefore) {
1169
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
1170
+ if (retentionFloor > pageContigBefore) {
1171
+ client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
1172
+ }
1173
+ if (seqs.length > 0) {
1174
+ client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1175
+ }
1176
+ }
1177
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
1178
+ contigAdvanced = ackSeq !== pageContigBefore;
1179
+ if (contigAdvanced) {
1180
+ await client._drainOrderedMessages(ns);
1181
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
1182
+ contigAdvanced = ackSeq !== pageContigBefore;
1183
+ await client._commitSeqTrackerState(ns);
1184
+ }
1185
+ }
1186
+ catch (exc) {
1187
+ if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1188
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1189
+ }
1190
+ client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1191
+ throw exc;
820
1192
  }
821
1193
  const ackNeeded = pageMessages.length > 0
822
1194
  && ackSeq > 0
@@ -856,30 +1228,28 @@ export class V2E2EECoordinator {
856
1228
  }
857
1229
  return decrypted;
858
1230
  }
859
- async ackV2(upToSeq) {
1231
+ async ackV2(upToSeq, inlineGeneration) {
860
1232
  const client = this.client;
861
1233
  const ns = client._aid ? `p2p:${client._aid}` : '';
862
1234
  let seq = upToSeq ?? (ns ? client._seqTracker.getContiguousSeq(ns) : 0);
863
- if (seq <= 0)
1235
+ if (typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq <= 0)
864
1236
  return { acked: 0 };
865
1237
  seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
1238
+ if (!Number.isSafeInteger(seq) || seq <= 0)
1239
+ return { acked: 0 };
866
1240
  const raw = await client._callRawV2Rpc('message.v2.ack', { up_to_seq: seq });
867
1241
  const result = isJsonObject(raw)
868
1242
  ? { ...raw }
869
1243
  : { result: raw };
870
- let actualAckSeq = seq;
871
- if ('effective_ack_seq' in result)
872
- actualAckSeq = Number(result.effective_ack_seq ?? 0);
873
- else if ('ack_seq' in result)
874
- actualAckSeq = Number(result.ack_seq ?? 0);
875
- else if ('cursor' in result)
876
- actualAckSeq = Number(result.cursor ?? 0);
877
- if (!Number.isFinite(actualAckSeq))
878
- actualAckSeq = seq;
1244
+ const actualAckSeq = client._delivery?.resolveActualAckSeq?.(result, seq) ?? seq;
879
1245
  result.ack_seq = actualAckSeq;
880
1246
  result.success = true;
881
- if (Number(result.acked ?? 0) === 0)
882
- result.acked = actualAckSeq;
1247
+ if (inlineGeneration === undefined) {
1248
+ client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq);
1249
+ }
1250
+ else {
1251
+ client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, inlineGeneration);
1252
+ }
883
1253
  if (client._v2Session) {
884
1254
  try {
885
1255
  const destroyed = await client._v2Session.maybeDestroyOldSPKs(actualAckSeq);
@@ -896,9 +1266,10 @@ export class V2E2EECoordinator {
896
1266
  async sendGroupV2(groupId, payload, opts) {
897
1267
  const client = this.client;
898
1268
  await client._ensureV2SessionReady('group.send', 'V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
899
- const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1269
+ const gid = String(groupId ?? '').trim();
900
1270
  if (!gid)
901
1271
  throw new ValidationError("group.send requires 'group_id'");
1272
+ const groupAid = String(opts?.groupAid ?? '').trim();
902
1273
  if (!isJsonObject(payload))
903
1274
  throw new ValidationError('group.send payload must be a dict for V2 encryption');
904
1275
  const attempt = async (useCache) => {
@@ -911,10 +1282,10 @@ export class V2E2EECoordinator {
911
1282
  context: opts?.context,
912
1283
  useCache,
913
1284
  });
914
- return client.call('group.v2.send', {
1285
+ return client.call('group.v2.send', withExplicitGroupAid({
915
1286
  group_id: gid,
916
1287
  envelope,
917
- });
1288
+ }, groupAid));
918
1289
  };
919
1290
  try {
920
1291
  const result = await attempt(true);
@@ -927,14 +1298,14 @@ export class V2E2EECoordinator {
927
1298
  await client._saveSeqTrackerState();
928
1299
  }
929
1300
  }
930
- return client._delivery.attachSendResultEnvelope('group.send', {
1301
+ return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
931
1302
  group_id: gid,
932
1303
  payload,
933
1304
  message_id: opts?.messageId,
934
1305
  timestamp: opts?.timestamp,
935
1306
  protected_headers: opts?.protectedHeaders,
936
1307
  context: opts?.context,
937
- }, result, true);
1308
+ }, groupAid), result, true);
938
1309
  }
939
1310
  catch (exc) {
940
1311
  const excCode = exc?.code;
@@ -951,65 +1322,159 @@ export class V2E2EECoordinator {
951
1322
  await client._saveSeqTrackerState();
952
1323
  }
953
1324
  }
954
- return client._delivery.attachSendResultEnvelope('group.send', {
1325
+ return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
955
1326
  group_id: gid,
956
1327
  payload,
957
1328
  message_id: opts?.messageId,
958
1329
  timestamp: opts?.timestamp,
959
1330
  protected_headers: opts?.protectedHeaders,
960
1331
  context: opts?.context,
961
- }, result, true);
1332
+ }, groupAid), result, true);
962
1333
  }
963
1334
  throw exc;
964
1335
  }
965
1336
  }
1337
+ async pullGroupV2TailInternal(params) {
1338
+ const client = this.client;
1339
+ if (params.window_mode !== 'tail')
1340
+ throw new ValidationError('window_mode must equal tail');
1341
+ if ('ack_up_to_seq' in params || params.force === true)
1342
+ throw new ValidationError('Tail does not accept ack_up_to_seq or force');
1343
+ const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
1344
+ if (!groupId)
1345
+ throw new ValidationError('group_id is required');
1346
+ let groupAid = String(params.group_aid ?? '').trim();
1347
+ const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
1348
+ const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1349
+ const ns = `group:${groupId}`;
1350
+ const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
1351
+ group_id: groupId, window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true,
1352
+ }, groupAid));
1353
+ const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
1354
+ if (resultAid)
1355
+ groupAid = resultAid;
1356
+ const floor = Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0);
1357
+ const previous = client._seqTracker.getSyncState(ns);
1358
+ await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
1359
+ const page = validateTailPage(result, limit);
1360
+ client._seqTracker.commitTailWindow(ns, {
1361
+ windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
1362
+ });
1363
+ client._persistSeq(ns);
1364
+ await client._saveSeqTrackerState?.();
1365
+ const decoded = [];
1366
+ for (const msg of page.messages) {
1367
+ const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail');
1368
+ if (plaintext)
1369
+ decoded.push(plaintext);
1370
+ }
1371
+ const ack = client._seqTracker.getContiguousSeq(ns);
1372
+ if (ack > previous.ack)
1373
+ await this.ackGroupV2(groupId, ack, groupAid);
1374
+ return { ...result, messages: decoded, raw_count: page.messages.length };
1375
+ }
1376
+ async historyGroupV2Internal(params) {
1377
+ const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
1378
+ if (!groupId)
1379
+ throw new ValidationError('group_id is required');
1380
+ let groupAid = String(params.group_aid ?? '').trim();
1381
+ const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
1382
+ const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1383
+ const result = await this.client._callRawV2Rpc('group.history', withExplicitGroupAid({
1384
+ group_id: groupId, before_seq: beforeSeq, limit, _rpc_foreground: true,
1385
+ }, groupAid));
1386
+ const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
1387
+ if (resultAid)
1388
+ groupAid = resultAid;
1389
+ const messages = validateHistoryPage(result, beforeSeq);
1390
+ const decoded = [];
1391
+ for (const msg of messages) {
1392
+ const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, false, true, 'history');
1393
+ if (plaintext)
1394
+ decoded.push(plaintext);
1395
+ }
1396
+ return { ...result, messages: decoded };
1397
+ }
966
1398
  async pullGroupV2Internal(params) {
967
- await this.pullGroupV2(params.group_id, params.after_seq, params.limit);
1399
+ await this.pullGroupV2(params.group_id, params.after_seq, params.limit, {
1400
+ gateLocked: true,
1401
+ explicitAfterSeq: true,
1402
+ force: params.force === true,
1403
+ });
968
1404
  }
969
1405
  async pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {
970
1406
  const client = this.client;
971
1407
  await client._ensureV2SessionReady('group.pull');
972
- const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1408
+ const gid = String(groupId ?? '').trim();
973
1409
  if (!gid)
974
1410
  throw new ValidationError('group.pull requires group_id');
1411
+ let groupAid = String(opts?.groupAid ?? '').trim();
975
1412
  const ns = `group:${gid}`;
1413
+ if (opts?.windowMode === 'tail') {
1414
+ const result = await this.pullGroupV2TailInternal({
1415
+ group_id: String(opts.wireGroupId ?? gid), group_aid: groupAid || undefined,
1416
+ window_mode: 'tail', after_seq: afterSeq, limit,
1417
+ });
1418
+ return Array.isArray(result.messages) ? result.messages : [];
1419
+ }
976
1420
  const decrypted = [];
977
1421
  const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
978
1422
  const cursorParams = opts?.cursorParams ?? {};
979
1423
  const ownsCursor = opts?.ownsCursor !== false;
980
- let nextAfterSeq = opts?.explicitAfterSeq ? afterSeq : (afterSeq || client._seqTracker.getContiguousSeq(ns));
1424
+ let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
1425
+ ? afterSeq
1426
+ : (afterSeq || client._seqTracker.getContiguousSeq(ns));
981
1427
  let pageCount = 0;
982
1428
  let lastAutoAckSeq = 0;
983
1429
  let pendingAckSeq = 0;
984
- const maxPages = 100;
1430
+ const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
985
1431
  while (pageCount < maxPages) {
986
1432
  pageCount += 1;
987
1433
  const ackUpToSeq = ownsCursor ? pendingAckSeq : 0;
988
- const result = await client._callRawV2Rpc('group.v2.pull', {
1434
+ const pendingInlineGeneration = ackUpToSeq > 0
1435
+ ? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null
1436
+ : null;
1437
+ const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
989
1438
  group_id: wireGroupId,
990
- group_aid: gid,
991
1439
  after_seq: nextAfterSeq,
992
1440
  limit,
993
1441
  ...cursorParams,
1442
+ ...(opts?.force ? { force: true } : {}),
994
1443
  ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
995
- });
1444
+ }, groupAid));
996
1445
  if (ackUpToSeq > 0) {
1446
+ if (pendingInlineGeneration !== null) {
1447
+ let actualAckSeq = ackUpToSeq;
1448
+ const ackCursor = isJsonObject(result.cursor)
1449
+ ? result.cursor
1450
+ : null;
1451
+ if (ackCursor && Object.prototype.hasOwnProperty.call(ackCursor, 'current_seq')) {
1452
+ actualAckSeq = typeof ackCursor.current_seq === 'number'
1453
+ && Number.isSafeInteger(ackCursor.current_seq) && ackCursor.current_seq >= 0
1454
+ ? ackCursor.current_seq
1455
+ : 0;
1456
+ }
1457
+ if (actualAckSeq > 0) {
1458
+ client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
1459
+ }
1460
+ }
997
1461
  pendingAckSeq = 0;
998
1462
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
999
1463
  }
1464
+ const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
1465
+ if (resultGroupAid)
1466
+ groupAid = resultGroupAid;
1467
+ const eventGroupAid = resultGroupAid || groupAid || gid;
1000
1468
  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;
1004
- }).sort((a, b) => Number(a.seq ?? 0) - Number(b.seq ?? 0));
1469
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1005
1470
  const seqs = messages
1006
1471
  .map((msg) => Number(msg.seq ?? 0))
1007
1472
  .filter((seq) => Number.isFinite(seq) && seq > 0);
1008
1473
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
1474
+ const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
1475
+ ? client._seqTracker.snapshotNamespace(ns)
1476
+ : null;
1009
1477
  const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1010
- if (seqs.length > 0) {
1011
- client._seqTracker.forceContiguousSeq(ns, pageMaxSeq);
1012
- }
1013
1478
  for (const msg of messages) {
1014
1479
  const seq = Number(msg.seq ?? 0);
1015
1480
  if (!Number.isFinite(seq) || seq <= 0)
@@ -1020,7 +1485,11 @@ export class V2E2EECoordinator {
1020
1485
  const payloadObj = isJsonObject(payload) ? payload : null;
1021
1486
  // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
1022
1487
  if (client._delivery.recallEventFromGroupMessage(msg)) {
1023
- await client._delivery.publishGroupRecallTombstone(gid, seq, msg);
1488
+ await client._delivery.publishGroupRecallTombstone(gid, seq, {
1489
+ ...msg,
1490
+ group_id: gid,
1491
+ group_aid: eventGroupAid,
1492
+ });
1024
1493
  client._markPublishedSeq(ns, seq);
1025
1494
  client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
1026
1495
  continue;
@@ -1028,16 +1497,18 @@ export class V2E2EECoordinator {
1028
1497
  if (payloadObj) {
1029
1498
  const payloadType = String(payloadObj.type ?? '').trim();
1030
1499
  if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted') {
1031
- const v1Msg = {
1500
+ let v1Msg = {
1032
1501
  message_id: String(msg.message_id ?? ''),
1033
1502
  from: String(msg.from_aid ?? ''),
1034
1503
  group_id: gid,
1504
+ group_aid: eventGroupAid,
1035
1505
  seq: msg.seq,
1036
1506
  type: String(msg.type ?? ''),
1037
1507
  timestamp: msg.t_server,
1038
1508
  payload,
1039
1509
  encrypted: false,
1040
1510
  };
1511
+ v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
1041
1512
  attachGatewayProximity(v1Msg, msg);
1042
1513
  await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
1043
1514
  decrypted.push(v1Msg);
@@ -1045,16 +1516,18 @@ export class V2E2EECoordinator {
1045
1516
  }
1046
1517
  }
1047
1518
  else if (payload !== undefined && payload !== null) {
1048
- const v1Msg = {
1519
+ let v1Msg = {
1049
1520
  message_id: String(msg.message_id ?? ''),
1050
1521
  from: String(msg.from_aid ?? ''),
1051
1522
  group_id: gid,
1523
+ group_aid: eventGroupAid,
1052
1524
  seq: msg.seq,
1053
1525
  type: String(msg.type ?? ''),
1054
1526
  timestamp: msg.t_server,
1055
1527
  payload,
1056
1528
  encrypted: false,
1057
1529
  };
1530
+ v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
1058
1531
  attachGatewayProximity(v1Msg, msg);
1059
1532
  await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
1060
1533
  decrypted.push(v1Msg);
@@ -1067,27 +1540,48 @@ export class V2E2EECoordinator {
1067
1540
  client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
1068
1541
  continue;
1069
1542
  }
1070
- const plaintext = await client._decryptV2Message(msg);
1543
+ let plaintext = await client._decryptV2Message(msg);
1071
1544
  if (plaintext === null)
1072
1545
  continue;
1546
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1073
1547
  plaintext.group_id = gid;
1548
+ plaintext.group_aid = eventGroupAid;
1074
1549
  await client._publishPulledMessage('group.message_created', ns, seq, plaintext);
1075
1550
  decrypted.push(plaintext);
1076
1551
  }
1077
1552
  const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1078
1553
  const cursorCurrentSeq = Number(cursor?.current_seq ?? 0);
1079
- if (Number.isFinite(cursorCurrentSeq) && cursorCurrentSeq > 0) {
1080
- const contig = client._seqTracker.getContiguousSeq(ns);
1081
- if (contig < cursorCurrentSeq) {
1082
- client._clientLog.info(`group.v2.pull retention-floor advance: ns=${ns} contiguous=${contig} -> cursor.current_seq=${cursorCurrentSeq}`);
1083
- client._seqTracker.forceContiguousSeq(ns, cursorCurrentSeq);
1554
+ const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
1555
+ const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
1556
+ const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
1557
+ const commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1558
+ let ackSeq = pageContigBefore;
1559
+ let contigAdvanced = false;
1560
+ try {
1561
+ if (commitTarget > pageContigBefore) {
1562
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
1563
+ if (effectiveFloor > pageContigBefore) {
1564
+ client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
1565
+ }
1566
+ if (seqs.length > 0) {
1567
+ client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1568
+ }
1569
+ }
1570
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
1571
+ contigAdvanced = ackSeq !== pageContigBefore;
1572
+ if (contigAdvanced) {
1573
+ await client._drainOrderedMessages(ns);
1574
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
1575
+ contigAdvanced = ackSeq !== pageContigBefore;
1576
+ await client._commitSeqTrackerState(ns);
1084
1577
  }
1085
1578
  }
1086
- const ackSeq = client._seqTracker.getContiguousSeq(ns);
1087
- const contigAdvanced = ackSeq !== pageContigBefore;
1088
- if (contigAdvanced) {
1089
- await client._drainOrderedMessages(ns);
1090
- client._persistSeq(ns);
1579
+ catch (exc) {
1580
+ if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1581
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1582
+ }
1583
+ client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1584
+ throw exc;
1091
1585
  }
1092
1586
  const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
1093
1587
  const ackNeeded = pageMessages.length > 0
@@ -1130,17 +1624,35 @@ export class V2E2EECoordinator {
1130
1624
  }
1131
1625
  return decrypted;
1132
1626
  }
1133
- async ackGroupV2(groupId, upToSeq) {
1627
+ async ackGroupV2(groupId, upToSeq, groupAid, inlineGeneration) {
1134
1628
  const client = this.client;
1135
- const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1629
+ const gid = typeof groupId === 'string' ? groupId.trim() : '';
1136
1630
  if (!gid)
1137
1631
  throw new ValidationError('group.ack_messages requires group_id');
1138
- const ns = `group:${gid}`;
1139
- let seq = Number(upToSeq ?? client._seqTracker.getContiguousSeq(ns));
1140
- if (!Number.isFinite(seq) || seq <= 0)
1632
+ if (groupAid !== undefined && typeof groupAid !== 'string') {
1633
+ throw new ValidationError('group.ack_messages group_aid must be a string');
1634
+ }
1635
+ const explicitGroupAid = groupAid?.trim() ?? '';
1636
+ const wireNs = `group:${gid}`;
1637
+ const ns = client._delivery?.resolvePendingGroupInlineAckNamespace?.(wireNs) ?? wireNs;
1638
+ let seq = upToSeq ?? client._seqTracker.getContiguousSeq(ns);
1639
+ if (typeof upToSeq !== 'undefined' && (typeof upToSeq !== 'number' || !Number.isSafeInteger(upToSeq))) {
1640
+ return { acked: 0 };
1641
+ }
1642
+ if (!Number.isSafeInteger(seq) || seq <= 0)
1141
1643
  return { acked: 0 };
1142
1644
  seq = client._clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
1143
- return client._callRawV2Rpc('group.v2.ack', { group_id: gid, group_aid: gid, up_to_seq: seq });
1645
+ if (!Number.isSafeInteger(seq) || seq <= 0)
1646
+ return { acked: 0 };
1647
+ const raw = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq }, explicitGroupAid));
1648
+ const actualAckSeq = client._delivery?.resolveActualAckSeq?.(raw, seq) ?? seq;
1649
+ if (inlineGeneration === undefined) {
1650
+ client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq);
1651
+ }
1652
+ else {
1653
+ client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, inlineGeneration);
1654
+ }
1655
+ return raw;
1144
1656
  }
1145
1657
  async putMessageThoughtEncryptedV2(params) {
1146
1658
  const client = this.client;
@@ -1543,7 +2055,7 @@ export class V2E2EECoordinator {
1543
2055
  return String(envelope.version ?? '').trim().toLowerCase() === 'v2' && payloadType.startsWith('e2ee.');
1544
2056
  }
1545
2057
  safeUndecryptablePushEvent(msg, group) {
1546
- const event = {
2058
+ let event = {
1547
2059
  message_id: msg.message_id ?? null,
1548
2060
  from: msg.from ?? null,
1549
2061
  seq: msg.seq ?? null,
@@ -1567,6 +2079,10 @@ export class V2E2EECoordinator {
1567
2079
  attachV2EnvelopeMetadata(event, v2E2eeMeta(envelope));
1568
2080
  }
1569
2081
  }
2082
+ if (group) {
2083
+ const modeSource = envelope ? { ...msg, envelope } : msg;
2084
+ event = normalizeGroupMentionMode(event, modeSource);
2085
+ }
1570
2086
  return event;
1571
2087
  }
1572
2088
  async decryptEncryptedPushPayload(msg, group) {
@@ -1580,7 +2096,7 @@ export class V2E2EECoordinator {
1580
2096
  if (!plaintext)
1581
2097
  return null;
1582
2098
  const e2eeMeta = v2E2eeMeta(envelope);
1583
- const result = {
2099
+ let result = {
1584
2100
  message_id: String(msg.message_id ?? ''),
1585
2101
  from: fromAid,
1586
2102
  seq: msg.seq ?? null,
@@ -1599,6 +2115,7 @@ export class V2E2EECoordinator {
1599
2115
  attachGatewayProximity(result, msg);
1600
2116
  if (group) {
1601
2117
  result.group_id = msg.group_id ?? aad.group_id ?? envelope.group_id ?? null;
2118
+ result = normalizeGroupMentionMode(result, msg, envelope);
1602
2119
  }
1603
2120
  else {
1604
2121
  result.to = msg.to ?? client._aid ?? '';
@@ -1659,6 +2176,7 @@ export class V2E2EECoordinator {
1659
2176
  peerDevices = (Array.isArray(bs?.peer_devices) ? bs.peer_devices : []);
1660
2177
  auditRaw = (Array.isArray(bs?.audit_recipients) ? bs.audit_recipients : []);
1661
2178
  wrapPolicy = normalizeV2WrapPolicy(bs?.e2ee_wrap_policy);
2179
+ this.cacheBundledSelfDevices(bs, wrapPolicy);
1662
2180
  if (peerDevices.length > 0) {
1663
2181
  this.setBootstrapCacheEntry(to, {
1664
2182
  devices: peerDevices,
@@ -1758,8 +2276,7 @@ export class V2E2EECoordinator {
1758
2276
  throw new StateError('V2 session not initialized');
1759
2277
  }
1760
2278
  const session = client._v2Session;
1761
- const rawGroupId = String(opts.groupId ?? '').trim();
1762
- const groupId = normalizeGroupId(rawGroupId) || rawGroupId;
2279
+ const groupId = String(opts.groupId ?? '').trim();
1763
2280
  if (!groupId)
1764
2281
  throw new ValidationError("group.send requires 'group_id'");
1765
2282
  const useCache = opts.useCache !== false;
@@ -1770,15 +2287,7 @@ export class V2E2EECoordinator {
1770
2287
  let auditRecipientsRaw = [];
1771
2288
  let wrapPolicy;
1772
2289
  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
- }
2290
+ const cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : undefined;
1782
2291
  if (cached && (Date.now() - Number(cached.cachedAt ?? 0)) < V2_BOOTSTRAP_TTL_MS) {
1783
2292
  allDevices = (cached.devices ?? []);
1784
2293
  epoch = Number(cached.epoch ?? 0) || 0;