@agentunion/fastaun-browser 0.5.3 → 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.
- package/CHANGELOG.md +130 -56
- 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
- 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
- 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
- package/_packed_docs/CHANGELOG.md +130 -56
- package/_packed_docs/INDEX.md +127 -69
- package/_packed_docs/KITE_DOCS_GUIDE.md +63 -29
- 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
- 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
- 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
- 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
- package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +6 -4
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +58 -20
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +219 -247
- package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +14 -14
- package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +3 -3
- 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
- package/_packed_docs/protocol/README.md +1 -0
- package/_packed_docs/protocol/aun-docs-guide.md +7 -4
- package/_packed_docs/protocol/index.md +25 -19
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +62 -17
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -78
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +6 -2
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +87 -41
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +18 -5
- package/_packed_docs/sdk/09-group-rpc-manual.md +307 -267
- package/_packed_docs/sdk/09-message-rpc-manual.md +142 -103
- package/_packed_docs/sdk/09-payload-reference.md +1 -1
- package/_packed_docs/sdk/09-stream-rpc-manual.md +8 -8
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +26 -17
- package/_packed_docs/sdk/INDEX.md +42 -34
- package/_packed_docs/sdk/README.md +7 -6
- 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
- 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
- package/dist/agent-md.d.ts +4 -5
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +66 -43
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +2 -6
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +20 -14
- package/dist/aid-store.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +30 -11
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +3399 -1069
- package/dist/client/delivery.d.ts +75 -8
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +1419 -245
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.d.ts.map +1 -1
- package/dist/client/group-state.js +27 -24
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/lifecycle.d.ts.map +1 -1
- package/dist/client/lifecycle.js +48 -4
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/mention-mode.d.ts +7 -0
- package/dist/client/mention-mode.d.ts.map +1 -0
- package/dist/client/mention-mode.js +184 -0
- package/dist/client/mention-mode.js.map +1 -0
- package/dist/client/peers.d.ts +1 -1
- package/dist/client/peers.d.ts.map +1 -1
- package/dist/client/peers.js +26 -3
- package/dist/client/peers.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +22 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +309 -74
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +25 -2
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +606 -97
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +24 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +404 -215
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -1
- package/dist/errors.js.map +1 -1
- package/dist/facades.d.ts +6 -0
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +199 -104
- package/dist/facades.js.map +1 -1
- package/dist/group-index.d.ts +6 -1
- package/dist/group-index.d.ts.map +1 -1
- package/dist/group-index.js +44 -25
- package/dist/group-index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +10 -5
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.d.ts +0 -12
- package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.js +0 -60
- package/dist/keystore/indexeddb-identity-store.js.map +1 -1
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.js +9 -5
- package/dist/keystore/indexeddb-shared.js.map +1 -1
- package/dist/keystore/indexeddb-token-store.d.ts +3 -1
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-token-store.js +47 -2
- package/dist/keystore/indexeddb-token-store.js.map +1 -1
- package/dist/register-flow.d.ts.map +1 -1
- package/dist/register-flow.js +28 -3
- package/dist/register-flow.js.map +1 -1
- package/dist/seq-tracker.d.ts +28 -8
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +218 -61
- package/dist/seq-tracker.js.map +1 -1
- package/dist/storage/vfs.d.ts +1 -0
- package/dist/storage/vfs.d.ts.map +1 -1
- package/dist/storage/vfs.js +26 -2
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +376 -10
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +12 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +218 -101
- package/dist/transport.js.map +1 -1
- package/dist/v2/session/session.d.ts.map +1 -1
- package/dist/v2/session/session.js +3 -4
- package/dist/v2/session/session.js.map +1 -1
- package/dist/v2/state/commitment.d.ts.map +1 -1
- package/dist/v2/state/commitment.js +1 -2
- package/dist/v2/state/commitment.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -5
- package/dist/group-resources.d.ts +0 -98
- package/dist/group-resources.d.ts.map +0 -1
- package/dist/group-resources.js +0 -635
- package/dist/group-resources.js.map +0 -1
- package/dist/keystore/indexeddb.d.ts +0 -179
- package/dist/keystore/indexeddb.d.ts.map +0 -1
- package/dist/keystore/indexeddb.js +0 -2031
- package/dist/keystore/indexeddb.js.map +0 -1
- package/dist/namespaces/auth.d.ts +0 -98
- package/dist/namespaces/auth.d.ts.map +0 -1
- package/dist/namespaces/auth.js +0 -992
- package/dist/namespaces/auth.js.map +0 -1
- package/dist/namespaces/custody.d.ts +0 -51
- package/dist/namespaces/custody.d.ts.map +0 -1
- package/dist/namespaces/custody.js +0 -302
- package/dist/namespaces/custody.js.map +0 -1
- package/dist/namespaces/meta.d.ts +0 -109
- package/dist/namespaces/meta.d.ts.map +0 -1
- package/dist/namespaces/meta.js +0 -549
- package/dist/namespaces/meta.js.map +0 -1
package/dist/client/v2-e2ee.js
CHANGED
|
@@ -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.
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
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 (
|
|
882
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
client.
|
|
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 =
|
|
1629
|
+
const gid = typeof groupId === 'string' ? groupId.trim() : '';
|
|
1136
1630
|
if (!gid)
|
|
1137
1631
|
throw new ValidationError('group.ack_messages requires group_id');
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|