@agentunion/fastaun 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.
- package/CHANGELOG.md +126 -90
- 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 +126 -90
- 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 +50 -14
- 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 +312 -282
- 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.js +8 -2
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +2 -0
- package/dist/aid-store.js +43 -7
- package/dist/aid-store.js.map +1 -1
- package/dist/auth.d.ts +0 -2
- package/dist/auth.js +97 -50
- package/dist/auth.js.map +1 -1
- package/dist/client/delivery.d.ts +70 -7
- package/dist/client/delivery.js +1332 -236
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.js +25 -22
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/lifecycle.js +46 -3
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/mention-mode.d.ts +6 -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.js +26 -3
- package/dist/client/peers.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +20 -1
- package/dist/client/rpc-pipeline.js +332 -73
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +20 -1
- package/dist/client/v2-e2ee.js +625 -105
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +17 -5
- package/dist/client.js +332 -167
- package/dist/client.js.map +1 -1
- package/dist/errors.js +4 -1
- package/dist/errors.js.map +1 -1
- package/dist/facades.d.ts +3 -0
- package/dist/facades.js +109 -53
- package/dist/facades.js.map +1 -1
- package/dist/group-index.d.ts +5 -0
- package/dist/group-index.js +65 -30
- package/dist/group-index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +12 -0
- package/dist/keystore/aid-db.js +68 -4
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +12 -0
- package/dist/keystore/local-identity-store.d.ts +10 -0
- package/dist/keystore/local-identity-store.js +8 -2
- package/dist/keystore/local-identity-store.js.map +1 -1
- package/dist/keystore/local-token-store.d.ts +10 -0
- package/dist/keystore/local-token-store.js +8 -2
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/register-flow.d.ts +0 -1
- package/dist/register-flow.js +96 -38
- package/dist/register-flow.js.map +1 -1
- package/dist/secret-store/file-store.d.ts +3 -14
- package/dist/secret-store/file-store.js +7 -98
- package/dist/secret-store/file-store.js.map +1 -1
- package/dist/secret-store/index.d.ts +1 -4
- package/dist/secret-store/index.js +2 -2
- package/dist/secret-store/index.js.map +1 -1
- package/dist/seq-tracker.d.ts +29 -9
- package/dist/seq-tracker.js +233 -64
- package/dist/seq-tracker.js.map +1 -1
- package/dist/storage/vfs.d.ts +1 -0
- package/dist/storage/vfs.js +28 -4
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +360 -8
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +19 -0
- package/dist/transport.js +197 -14
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/decrypt.js +26 -23
- package/dist/v2/e2ee/decrypt.js.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.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 +5 -4
- package/dist/e2ee-group.d.ts +0 -249
- package/dist/e2ee-group.js +0 -1567
- package/dist/e2ee-group.js.map +0 -1
- package/dist/group-resources.d.ts +0 -31
- package/dist/group-resources.js +0 -84
- package/dist/group-resources.js.map +0 -1
- package/dist/keystore/file.d.ts +0 -109
- package/dist/keystore/file.js +0 -772
- package/dist/keystore/file.js.map +0 -1
- package/dist/keystore/sqlite-backup.d.ts +0 -44
- package/dist/keystore/sqlite-backup.js +0 -382
- package/dist/keystore/sqlite-backup.js.map +0 -1
- package/dist/namespaces/auth.d.ts +0 -118
- package/dist/namespaces/auth.js +0 -968
- package/dist/namespaces/auth.js.map +0 -1
- package/dist/namespaces/custody.d.ts +0 -48
- package/dist/namespaces/custody.js +0 -313
- package/dist/namespaces/custody.js.map +0 -1
- package/dist/namespaces/meta.d.ts +0 -76
- package/dist/namespaces/meta.js +0 -490
- package/dist/namespaces/meta.js.map +0 -1
package/dist/client/v2-e2ee.js
CHANGED
|
@@ -1,12 +1,114 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
|
-
import { normalizeGroupId } from '../group-id.js';
|
|
3
2
|
import { E2EEError, StateError, ValidationError } from '../errors.js';
|
|
4
3
|
import { decryptMessage, encryptGroupMessage, encryptP2PMessage, } from '../v2/e2ee/index.js';
|
|
5
4
|
import { V2Session } from '../v2/session/index.js';
|
|
6
5
|
import { 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;
|
|
13
|
+
function strictWindowSeq(value, field, positive = false) {
|
|
14
|
+
if (!Number.isSafeInteger(value))
|
|
15
|
+
throw new ValidationError(`${field} must be an integer`);
|
|
16
|
+
const minimum = positive ? 1 : 0;
|
|
17
|
+
if (value < minimum)
|
|
18
|
+
throw new ValidationError(`${field} must be >= ${minimum}`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function strictWindowLimit(value) {
|
|
22
|
+
const limit = strictWindowSeq(value, 'limit', true);
|
|
23
|
+
if (limit > V2_PULL_MAX_PAGE_LIMIT)
|
|
24
|
+
throw new ValidationError(`limit must be <= ${V2_PULL_MAX_PAGE_LIMIT}`);
|
|
25
|
+
return limit;
|
|
26
|
+
}
|
|
27
|
+
function deduplicateWindowMessages(value, lowerSeq, upperSeq, boundary) {
|
|
28
|
+
if (!Array.isArray(value))
|
|
29
|
+
throw new ValidationError('messages must be an array');
|
|
30
|
+
const bySeq = new Map();
|
|
31
|
+
for (const raw of value) {
|
|
32
|
+
if (!isJsonObject(raw))
|
|
33
|
+
throw new ValidationError('messages entries must be objects');
|
|
34
|
+
const row = raw;
|
|
35
|
+
const seq = strictWindowSeq(row.seq, 'messages[].seq', true);
|
|
36
|
+
if (seq < lowerSeq || seq > upperSeq)
|
|
37
|
+
throw new ValidationError(`messages[].seq is outside ${boundary}`);
|
|
38
|
+
const previous = bySeq.get(seq);
|
|
39
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(row))
|
|
40
|
+
throw new ValidationError('messages contains conflicting duplicate seq');
|
|
41
|
+
bySeq.set(seq, row);
|
|
42
|
+
}
|
|
43
|
+
return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
|
|
44
|
+
}
|
|
45
|
+
function deduplicateForwardPageMessages(value, afterSeq) {
|
|
46
|
+
if (!Array.isArray(value))
|
|
47
|
+
return [];
|
|
48
|
+
const bySeq = new Map();
|
|
49
|
+
for (const raw of value) {
|
|
50
|
+
if (!isJsonObject(raw))
|
|
51
|
+
continue;
|
|
52
|
+
const row = raw;
|
|
53
|
+
const seq = Number(row.seq ?? 0);
|
|
54
|
+
if (!Number.isSafeInteger(seq) || seq <= afterSeq || bySeq.has(seq))
|
|
55
|
+
continue;
|
|
56
|
+
bySeq.set(seq, row);
|
|
57
|
+
}
|
|
58
|
+
return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
|
|
59
|
+
}
|
|
60
|
+
function validateTailPage(result, pageLimit) {
|
|
61
|
+
if (!isJsonObject(result))
|
|
62
|
+
throw new ValidationError('tail response must be an object');
|
|
63
|
+
const row = result;
|
|
64
|
+
if (row.window_mode !== 'tail')
|
|
65
|
+
throw new ValidationError('tail response window_mode must equal tail');
|
|
66
|
+
const windowStart = strictWindowSeq(row.window_start_seq, 'window_start_seq');
|
|
67
|
+
const head = strictWindowSeq(row.head_seq, 'head_seq');
|
|
68
|
+
const covered = strictWindowSeq(row.covered_through_seq, 'covered_through_seq');
|
|
69
|
+
if (windowStart > head)
|
|
70
|
+
throw new ValidationError('window_start_seq must not exceed head_seq');
|
|
71
|
+
if (covered !== head)
|
|
72
|
+
throw new ValidationError('covered_through_seq must equal head_seq');
|
|
73
|
+
if (head - windowStart + 1 > pageLimit)
|
|
74
|
+
throw new ValidationError('tail proven window exceeds limit');
|
|
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)
|
|
91
|
+
throw new ValidationError('next_before_seq must equal window_start_seq');
|
|
92
|
+
if (windowStart >= beforeSeq)
|
|
93
|
+
throw new ValidationError('history boundary must be less than before_seq');
|
|
94
|
+
lowerSeq = windowStart;
|
|
95
|
+
}
|
|
96
|
+
const messages = deduplicateWindowMessages(row.messages, lowerSeq, beforeSeq - 1, 'the history page');
|
|
97
|
+
if (messages.length > 0) {
|
|
98
|
+
if (windowStart === undefined)
|
|
99
|
+
throw new ValidationError('history messages require pagination boundaries');
|
|
100
|
+
if (Number(messages[0].seq) !== windowStart)
|
|
101
|
+
throw new ValidationError('window_start_seq must equal the first history message seq');
|
|
102
|
+
}
|
|
103
|
+
else if (windowStart !== undefined) {
|
|
104
|
+
throw new ValidationError('empty history page must not include pagination boundaries');
|
|
105
|
+
}
|
|
106
|
+
if (typeof row.has_older !== 'boolean')
|
|
107
|
+
throw new ValidationError('has_older must be a boolean');
|
|
108
|
+
if (messages.length === 0 && row.has_older)
|
|
109
|
+
throw new ValidationError('empty history page cannot report has_older');
|
|
110
|
+
return messages;
|
|
111
|
+
}
|
|
10
112
|
const MEMBERSHIP_ACTIONS = new Set([
|
|
11
113
|
'member_added',
|
|
12
114
|
'member_left',
|
|
@@ -121,6 +223,12 @@ function v2WrapCapabilities() {
|
|
|
121
223
|
per_device_wrap: true,
|
|
122
224
|
};
|
|
123
225
|
}
|
|
226
|
+
function withExplicitGroupAid(params, groupAid) {
|
|
227
|
+
const aid = String(groupAid ?? '').trim();
|
|
228
|
+
if (aid)
|
|
229
|
+
params.group_aid = aid;
|
|
230
|
+
return params;
|
|
231
|
+
}
|
|
124
232
|
function applyV2WrapPolicyToTargets(targets, policy) {
|
|
125
233
|
if (!policy.explicit)
|
|
126
234
|
return targets;
|
|
@@ -148,6 +256,7 @@ export class V2E2EECoordinator {
|
|
|
148
256
|
runtime;
|
|
149
257
|
_initSessionPromise = null;
|
|
150
258
|
targetSetCache = new Map();
|
|
259
|
+
senderIKSyncFetches = new Map();
|
|
151
260
|
constructor(runtime) {
|
|
152
261
|
this.runtime = runtime;
|
|
153
262
|
}
|
|
@@ -167,6 +276,18 @@ export class V2E2EECoordinator {
|
|
|
167
276
|
setBootstrapCacheEntry(key, entry) {
|
|
168
277
|
this.bootstrapCache.set(key, entry);
|
|
169
278
|
}
|
|
279
|
+
cacheBundledSelfDevices(bootstrap, wrapPolicy) {
|
|
280
|
+
const client = this.client;
|
|
281
|
+
if (!client._aid || !Object.prototype.hasOwnProperty.call(bootstrap, 'self_devices'))
|
|
282
|
+
return;
|
|
283
|
+
const selfDevices = (Array.isArray(bootstrap.self_devices) ? bootstrap.self_devices : []);
|
|
284
|
+
this.setBootstrapCacheEntry(client._aid, {
|
|
285
|
+
devices: selfDevices,
|
|
286
|
+
auditRecipients: [],
|
|
287
|
+
cachedAt: Date.now(),
|
|
288
|
+
wrapPolicy,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
170
291
|
deleteBootstrapCacheEntry(key) {
|
|
171
292
|
this.bootstrapCache.delete(key);
|
|
172
293
|
this.targetSetCache.delete(key);
|
|
@@ -375,6 +496,84 @@ export class V2E2EECoordinator {
|
|
|
375
496
|
pendingSenderIKFetchKey(fromAid, senderDeviceId, groupId) {
|
|
376
497
|
return `${fromAid}#${senderDeviceId}#${groupId || ''}`;
|
|
377
498
|
}
|
|
499
|
+
async fetchSenderIKOnce(fromAid, senderDeviceId, groupId, certFingerprint = '') {
|
|
500
|
+
const client = this.client;
|
|
501
|
+
const peerAid = String(fromAid ?? '').trim();
|
|
502
|
+
const deviceId = String(senderDeviceId ?? '').trim();
|
|
503
|
+
const gid = String(groupId ?? '').trim();
|
|
504
|
+
const fingerprint = String(certFingerprint ?? '').trim().toLowerCase();
|
|
505
|
+
if (!peerAid)
|
|
506
|
+
return null;
|
|
507
|
+
const fetchKey = [peerAid, deviceId, gid, fingerprint].join('#');
|
|
508
|
+
const existing = this.senderIKSyncFetches.get(fetchKey);
|
|
509
|
+
if (existing)
|
|
510
|
+
return await existing;
|
|
511
|
+
const task = (async () => {
|
|
512
|
+
const started = Date.now();
|
|
513
|
+
let timer;
|
|
514
|
+
const timedOut = Symbol('sender-ik-timeout');
|
|
515
|
+
const fetch = async () => {
|
|
516
|
+
try {
|
|
517
|
+
const bootstrap = await client.call('message.v2.bootstrap', {
|
|
518
|
+
peer_aid: peerAid,
|
|
519
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
520
|
+
});
|
|
521
|
+
const peers = (Array.isArray(bootstrap?.peer_devices) ? bootstrap.peer_devices : []);
|
|
522
|
+
for (const dev of peers)
|
|
523
|
+
client._cacheV2PeerIKFromDevice(dev, peerAid);
|
|
524
|
+
}
|
|
525
|
+
catch (exc) {
|
|
526
|
+
client._clientLog.warn(`V2 sender IK 同步 bootstrap 失败: peer=${peerAid} device=${deviceId || '-'} err=${formatE2EEError(exc)}`);
|
|
527
|
+
}
|
|
528
|
+
let senderPubDer = await this.getV2SenderPubDer(peerAid, deviceId, fingerprint);
|
|
529
|
+
if (senderPubDer || !gid)
|
|
530
|
+
return senderPubDer;
|
|
531
|
+
try {
|
|
532
|
+
const bootstrap = await client.call('group.v2.bootstrap', {
|
|
533
|
+
group_id: gid,
|
|
534
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
535
|
+
});
|
|
536
|
+
const devices = (Array.isArray(bootstrap?.devices) ? bootstrap.devices : []);
|
|
537
|
+
const audit = (Array.isArray(bootstrap?.audit_recipients) ? bootstrap.audit_recipients : []);
|
|
538
|
+
for (const dev of devices)
|
|
539
|
+
client._cacheV2PeerIKFromDevice(dev);
|
|
540
|
+
for (const dev of audit)
|
|
541
|
+
client._cacheV2PeerIKFromDevice(dev);
|
|
542
|
+
}
|
|
543
|
+
catch (exc) {
|
|
544
|
+
client._clientLog.warn(`V2 sender IK 同步 group bootstrap 失败: group=${gid} peer=${peerAid} err=${formatE2EEError(exc)}`);
|
|
545
|
+
}
|
|
546
|
+
senderPubDer = await this.getV2SenderPubDer(peerAid, deviceId, fingerprint);
|
|
547
|
+
return senderPubDer;
|
|
548
|
+
};
|
|
549
|
+
try {
|
|
550
|
+
const result = await Promise.race([
|
|
551
|
+
fetch(),
|
|
552
|
+
new Promise((resolve) => {
|
|
553
|
+
timer = setTimeout(() => resolve(timedOut), V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS);
|
|
554
|
+
}),
|
|
555
|
+
]);
|
|
556
|
+
if (result === timedOut) {
|
|
557
|
+
client._clientLog.warn(`V2 sender IK 同步获取超时: peer=${peerAid} device=${deviceId || '-'} group=${gid || '<p2p>'} timeout_ms=${V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS}`);
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
return result;
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
if (timer !== undefined)
|
|
564
|
+
clearTimeout(timer);
|
|
565
|
+
client._clientLog.debug(`V2 sender IK 同步获取完成: peer=${peerAid} device=${deviceId || '-'} group=${gid || '<p2p>'} elapsed_ms=${Date.now() - started}`);
|
|
566
|
+
}
|
|
567
|
+
})();
|
|
568
|
+
this.senderIKSyncFetches.set(fetchKey, task);
|
|
569
|
+
try {
|
|
570
|
+
return await task;
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
573
|
+
if (this.senderIKSyncFetches.get(fetchKey) === task)
|
|
574
|
+
this.senderIKSyncFetches.delete(fetchKey);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
378
577
|
scheduleSenderIKPending(args) {
|
|
379
578
|
const client = this.client;
|
|
380
579
|
const fromAid = String(args.fromAid ?? '').trim();
|
|
@@ -450,21 +649,23 @@ export class V2E2EECoordinator {
|
|
|
450
649
|
}
|
|
451
650
|
catch (exc) {
|
|
452
651
|
client._clientLog.warn(`V2 sender IK pending retry raised: key=${key} err=${formatE2EEError(exc)}`);
|
|
652
|
+
client._v2SenderIKPending.delete(key);
|
|
653
|
+
continue;
|
|
453
654
|
}
|
|
454
|
-
client._v2SenderIKPending.delete(key);
|
|
455
655
|
if (plaintext === null) {
|
|
456
|
-
client._clientLog.
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
});
|
|
656
|
+
client._clientLog.warn(`V2 sender IK pending retry still undecryptable: key=${key}`);
|
|
657
|
+
const isGroup = Boolean(entry.groupId);
|
|
658
|
+
const eventName = isGroup ? 'group.message_undecryptable' : 'message.undecryptable';
|
|
659
|
+
const safeEvent = this.safeUndecryptablePushEvent(entry.msg, isGroup);
|
|
660
|
+
safeEvent._decrypt_error = 'sender IK pending retry failed';
|
|
661
|
+
safeEvent._decrypt_stage = 'sender_ik_pending_retry';
|
|
662
|
+
await client._publishAppEvent(eventName, safeEvent, 'pending_retry');
|
|
464
663
|
continue;
|
|
465
664
|
}
|
|
665
|
+
client._v2SenderIKPending.delete(key);
|
|
466
666
|
const seq = Number(entry.msg.seq ?? 0);
|
|
467
667
|
if (entry.groupId) {
|
|
668
|
+
plaintext = normalizeGroupMentionMode(plaintext, entry.msg, entry.msg.envelope_json);
|
|
468
669
|
plaintext.group_id = entry.groupId;
|
|
469
670
|
await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext);
|
|
470
671
|
}
|
|
@@ -510,6 +711,7 @@ export class V2E2EECoordinator {
|
|
|
510
711
|
wrapPolicy = normalizeV2WrapPolicy(bs.e2ee_wrap_policy);
|
|
511
712
|
peerDevices = (Array.isArray(bs?.peer_devices) ? bs.peer_devices : []);
|
|
512
713
|
auditRaw = (Array.isArray(bs?.audit_recipients) ? bs.audit_recipients : []);
|
|
714
|
+
this.cacheBundledSelfDevices(bs, wrapPolicy);
|
|
513
715
|
client._clientLog.debug(`message.v2.bootstrap fetched: to=${to}, devices=${peerDevices.length}, audit=${auditRaw.length}`);
|
|
514
716
|
if (peerDevices.length > 0) {
|
|
515
717
|
this.setBootstrapCacheEntry(to, {
|
|
@@ -694,26 +896,187 @@ export class V2E2EECoordinator {
|
|
|
694
896
|
throw exc;
|
|
695
897
|
}
|
|
696
898
|
}
|
|
899
|
+
historyDecryptFailure(msg, reason) {
|
|
900
|
+
return {
|
|
901
|
+
message_id: String(msg.message_id ?? ''),
|
|
902
|
+
seq: Number(msg.seq ?? 0),
|
|
903
|
+
encrypted: true,
|
|
904
|
+
history_error: { code: 'decrypt_failed', message: reason },
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
async decodeP2PWindowMessage(msg, publish, history, source) {
|
|
908
|
+
const client = this.client;
|
|
909
|
+
const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
|
|
910
|
+
let plaintext = null;
|
|
911
|
+
if (String(msg.version ?? '') === 'v1') {
|
|
912
|
+
const legacy = isJsonObject(msg.legacy_v1)
|
|
913
|
+
? msg.legacy_v1 : {};
|
|
914
|
+
const payload = legacy.payload;
|
|
915
|
+
const payloadType = isJsonObject(payload)
|
|
916
|
+
? String(payload.type ?? '').trim() : '';
|
|
917
|
+
if (payload !== undefined && payload !== null && !['e2ee.encrypted', 'e2ee.group_encrypted'].includes(payloadType)) {
|
|
918
|
+
plaintext = {
|
|
919
|
+
message_id: String(msg.message_id ?? ''),
|
|
920
|
+
from: String(msg.from_aid ?? ''),
|
|
921
|
+
to: String(legacy.to ?? client._aid ?? ''),
|
|
922
|
+
seq,
|
|
923
|
+
type: String(msg.type ?? ''),
|
|
924
|
+
timestamp: msg.t_server,
|
|
925
|
+
payload: payload,
|
|
926
|
+
encrypted: false,
|
|
927
|
+
};
|
|
928
|
+
attachGatewayProximity(plaintext, msg);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
plaintext = await client._decryptV2Message(msg, false, !history);
|
|
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 ?? ''),
|
|
961
|
+
from: String(msg.from_aid ?? ''),
|
|
962
|
+
group_id: groupId,
|
|
963
|
+
group_aid: groupAid,
|
|
964
|
+
seq,
|
|
965
|
+
type: String(msg.type ?? ''),
|
|
966
|
+
timestamp: msg.t_server,
|
|
967
|
+
payload: payload,
|
|
968
|
+
encrypted: false,
|
|
969
|
+
};
|
|
970
|
+
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
971
|
+
attachGatewayProximity(plaintext, msg);
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
plaintext = await client._decryptV2Message(msg, false, !history);
|
|
975
|
+
if (plaintext) {
|
|
976
|
+
plaintext.group_id = groupId;
|
|
977
|
+
plaintext.group_aid = groupAid;
|
|
978
|
+
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (!plaintext) {
|
|
982
|
+
if (history)
|
|
983
|
+
return this.historyDecryptFailure(msg, 'message could not be decrypted');
|
|
984
|
+
client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
if (publish)
|
|
988
|
+
await client._publishAppEvent('group.message_created', plaintext, source);
|
|
989
|
+
return plaintext;
|
|
990
|
+
}
|
|
991
|
+
async applyTailFloor(ns, floor) {
|
|
992
|
+
const client = this.client;
|
|
993
|
+
if (!ns || floor <= 0 || client._seqTracker.getContiguousSeq(ns) >= floor)
|
|
994
|
+
return;
|
|
995
|
+
client._seqTracker.forceContiguousSeq(ns, floor);
|
|
996
|
+
client._persistSeq(ns);
|
|
997
|
+
client._saveSeqTrackerState?.();
|
|
998
|
+
}
|
|
999
|
+
async pullV2TailInternal(params) {
|
|
1000
|
+
const client = this.client;
|
|
1001
|
+
if (params.window_mode !== 'tail')
|
|
1002
|
+
throw new ValidationError('window_mode must equal tail');
|
|
1003
|
+
if ('ack_up_to_seq' in params || params.force === true)
|
|
1004
|
+
throw new ValidationError('Tail does not accept ack_up_to_seq or force');
|
|
1005
|
+
const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
|
|
1006
|
+
const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
|
|
1007
|
+
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
1008
|
+
const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true });
|
|
1009
|
+
const floor = Number(result.retention_floor_seq ?? 0);
|
|
1010
|
+
const previous = ns ? client._seqTracker.getSyncState(ns) : null;
|
|
1011
|
+
await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
|
|
1012
|
+
const page = validateTailPage(result, limit);
|
|
1013
|
+
if (ns) {
|
|
1014
|
+
client._seqTracker.commitTailWindow(ns, {
|
|
1015
|
+
windowStartSeq: page.windowStart,
|
|
1016
|
+
headSeq: page.head,
|
|
1017
|
+
coveredThroughSeq: page.head,
|
|
1018
|
+
pageLimit: limit,
|
|
1019
|
+
});
|
|
1020
|
+
client._persistSeq(ns);
|
|
1021
|
+
client._saveSeqTrackerState?.();
|
|
1022
|
+
}
|
|
1023
|
+
const decoded = [];
|
|
1024
|
+
for (const msg of page.messages) {
|
|
1025
|
+
const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail');
|
|
1026
|
+
if (plaintext)
|
|
1027
|
+
decoded.push(plaintext);
|
|
1028
|
+
}
|
|
1029
|
+
const ack = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
|
|
1030
|
+
if (previous && ack > previous.ack)
|
|
1031
|
+
await this.ackV2(ack);
|
|
1032
|
+
return { ...result, messages: decoded, raw_count: page.messages.length };
|
|
1033
|
+
}
|
|
1034
|
+
async historyV2Internal(params) {
|
|
1035
|
+
const client = this.client;
|
|
1036
|
+
const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
|
|
1037
|
+
const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
|
|
1038
|
+
const result = await client._callRawV2Rpc('message.history', { before_seq: beforeSeq, limit, _rpc_foreground: true });
|
|
1039
|
+
const messages = validateHistoryPage(result, beforeSeq);
|
|
1040
|
+
const decoded = [];
|
|
1041
|
+
for (const msg of messages) {
|
|
1042
|
+
const plaintext = await this.decodeP2PWindowMessage(msg, false, true, 'history');
|
|
1043
|
+
if (plaintext)
|
|
1044
|
+
decoded.push(plaintext);
|
|
1045
|
+
}
|
|
1046
|
+
return { ...result, messages: decoded };
|
|
1047
|
+
}
|
|
697
1048
|
async pullV2(afterSeq = 0, limit = 50, opts) {
|
|
698
1049
|
const client = this.client;
|
|
699
1050
|
await client._ensureV2SessionReady('message.pull');
|
|
700
1051
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
1052
|
+
if (opts?.windowMode === 'tail') {
|
|
1053
|
+
if (!opts.gateLocked) {
|
|
1054
|
+
const key = `${ns}|tail|after=${afterSeq}|limit=${limit}`;
|
|
1055
|
+
return (await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, { ...opts, gateLocked: true }), false));
|
|
1056
|
+
}
|
|
1057
|
+
const result = await this.pullV2TailInternal({ window_mode: 'tail', after_seq: afterSeq, limit });
|
|
1058
|
+
return (Array.isArray(result.messages) ? result.messages : []);
|
|
1059
|
+
}
|
|
701
1060
|
if (ns && !opts?.gateLocked) {
|
|
702
|
-
|
|
1061
|
+
const key = `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
|
|
1062
|
+
return await client._runPullSerialized(key, async () => this.pullV2(afterSeq, limit, {
|
|
703
1063
|
...(opts ?? {}),
|
|
704
1064
|
gateLocked: true,
|
|
705
1065
|
scheduleFollowup: true,
|
|
706
|
-
}));
|
|
1066
|
+
}), true);
|
|
707
1067
|
}
|
|
708
1068
|
const decrypted = [];
|
|
709
1069
|
let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
|
|
710
1070
|
let pageCount = 0;
|
|
711
1071
|
let lastAutoAckSeq = 0;
|
|
712
1072
|
let pendingAckSeq = 0;
|
|
713
|
-
const maxPages = 100;
|
|
1073
|
+
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
714
1074
|
while (pageCount < maxPages) {
|
|
715
1075
|
pageCount += 1;
|
|
716
1076
|
const ackUpToSeq = opts?.skipAutoAck ? 0 : pendingAckSeq;
|
|
1077
|
+
const inlineAckGeneration = ackUpToSeq > 0
|
|
1078
|
+
? client._delivery.captureP2PInlineAckGeneration()
|
|
1079
|
+
: null;
|
|
717
1080
|
client._clientLog.debug(`message.v2.pull page request: page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns || '<none>'}`);
|
|
718
1081
|
const result = await client._callRawV2Rpc('message.v2.pull', {
|
|
719
1082
|
after_seq: nextAfterSeq,
|
|
@@ -722,14 +1085,14 @@ export class V2E2EECoordinator {
|
|
|
722
1085
|
...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
|
|
723
1086
|
});
|
|
724
1087
|
if (ackUpToSeq > 0) {
|
|
1088
|
+
if (inlineAckGeneration !== null) {
|
|
1089
|
+
client._delivery.onP2PV2PullAckResult(result, ackUpToSeq, inlineAckGeneration);
|
|
1090
|
+
}
|
|
725
1091
|
pendingAckSeq = 0;
|
|
726
1092
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
727
1093
|
}
|
|
728
1094
|
const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
729
|
-
const messages = pageMessages
|
|
730
|
-
const seq = Number(msg.seq ?? 0);
|
|
731
|
-
return Number.isFinite(seq) && seq > nextAfterSeq;
|
|
732
|
-
}).sort((a, b) => Number(a.seq ?? 0) - Number(b.seq ?? 0));
|
|
1095
|
+
const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
|
|
733
1096
|
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 ?? '')}`);
|
|
734
1097
|
for (const msg of messages) {
|
|
735
1098
|
client._logMessageDebug('pull-raw', 'message.v2.pull', 'message.received', msg);
|
|
@@ -738,13 +1101,12 @@ export class V2E2EECoordinator {
|
|
|
738
1101
|
.map((msg) => Number(msg.seq ?? 0))
|
|
739
1102
|
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
740
1103
|
const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
|
|
1104
|
+
const pageTrackerSnapshot = ns && typeof client._seqTracker.snapshotNamespace === 'function'
|
|
1105
|
+
? client._seqTracker.snapshotNamespace(ns)
|
|
1106
|
+
: null;
|
|
741
1107
|
let pageMaxSeq = nextAfterSeq;
|
|
742
1108
|
if (seqs.length > 0) {
|
|
743
1109
|
pageMaxSeq = Math.max(...seqs);
|
|
744
|
-
if (ns) {
|
|
745
|
-
client._seqTracker.forceContiguousSeq(ns, pageMaxSeq);
|
|
746
|
-
client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
747
|
-
}
|
|
748
1110
|
}
|
|
749
1111
|
for (const msg of messages) {
|
|
750
1112
|
const seq = Number(msg.seq ?? 0);
|
|
@@ -808,19 +1170,36 @@ export class V2E2EECoordinator {
|
|
|
808
1170
|
}
|
|
809
1171
|
const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
|
|
810
1172
|
const serverAckSeq = Number(result.server_ack_seq ?? 0);
|
|
811
|
-
|
|
812
|
-
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
813
|
-
if (contig < serverAckSeq) {
|
|
814
|
-
client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${contig} -> server_ack_seq=${serverAckSeq}`);
|
|
815
|
-
client._seqTracker.forceContiguousSeq(ns, serverAckSeq);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
1173
|
+
const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
|
|
818
1174
|
if (ns) {
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1175
|
+
const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
|
|
1176
|
+
let ackSeq = pageContigBefore;
|
|
1177
|
+
let contigAdvanced = false;
|
|
1178
|
+
try {
|
|
1179
|
+
if (commitTarget > pageContigBefore) {
|
|
1180
|
+
client._seqTracker.forceContiguousSeq(ns, commitTarget);
|
|
1181
|
+
if (retentionFloor > pageContigBefore) {
|
|
1182
|
+
client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
|
|
1183
|
+
}
|
|
1184
|
+
if (seqs.length > 0) {
|
|
1185
|
+
client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1189
|
+
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1190
|
+
if (contigAdvanced) {
|
|
1191
|
+
await client._drainOrderedMessages(ns, undefined, true);
|
|
1192
|
+
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1193
|
+
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1194
|
+
client._commitSeqTrackerState(ns);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
catch (exc) {
|
|
1198
|
+
if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
1199
|
+
client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
|
|
1200
|
+
}
|
|
1201
|
+
client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
1202
|
+
throw exc;
|
|
824
1203
|
}
|
|
825
1204
|
const ackNeeded = pageMessages.length > 0
|
|
826
1205
|
&& ackSeq > 0
|
|
@@ -874,28 +1253,30 @@ export class V2E2EECoordinator {
|
|
|
874
1253
|
return { acked: 0 };
|
|
875
1254
|
}
|
|
876
1255
|
seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
|
|
1256
|
+
if (seq <= 0)
|
|
1257
|
+
return { acked: 0 };
|
|
1258
|
+
const inlineAckGeneration = client._delivery.captureP2PInlineAckGeneration();
|
|
1259
|
+
const v2SessionAtStart = client._v2Session;
|
|
1260
|
+
const v2RuntimeGenerationAtStart = Number(client._v2RuntimeGeneration ?? 0);
|
|
877
1261
|
client._clientLog.debug(`message.v2.ack send: ns=${ns || '<none>'}, up_to_seq=${seq}`);
|
|
878
1262
|
const raw = await client._callRawV2Rpc('message.v2.ack', { up_to_seq: seq });
|
|
879
1263
|
const result = isJsonObject(raw)
|
|
880
1264
|
? { ...raw }
|
|
881
1265
|
: { result: raw };
|
|
882
|
-
|
|
883
|
-
if ('effective_ack_seq' in result)
|
|
884
|
-
actualAckSeq = Number(result.effective_ack_seq ?? 0);
|
|
885
|
-
else if ('ack_seq' in result)
|
|
886
|
-
actualAckSeq = Number(result.ack_seq ?? 0);
|
|
887
|
-
else if ('cursor' in result)
|
|
888
|
-
actualAckSeq = Number(result.cursor ?? 0);
|
|
889
|
-
if (!Number.isFinite(actualAckSeq))
|
|
890
|
-
actualAckSeq = seq;
|
|
1266
|
+
const actualAckSeq = client._delivery.resolveActualAckSeq(result, seq);
|
|
891
1267
|
result.ack_seq = actualAckSeq;
|
|
892
1268
|
result.success = true;
|
|
893
|
-
if (
|
|
894
|
-
|
|
895
|
-
|
|
1269
|
+
if (inlineAckGeneration !== null) {
|
|
1270
|
+
client._delivery.onP2PV2AckSucceeded(actualAckSeq, inlineAckGeneration);
|
|
1271
|
+
}
|
|
1272
|
+
if (v2SessionAtStart
|
|
1273
|
+
&& client._v2Session === v2SessionAtStart
|
|
1274
|
+
&& Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart) {
|
|
896
1275
|
try {
|
|
897
|
-
const destroyed = await Promise.resolve(
|
|
898
|
-
if (
|
|
1276
|
+
const destroyed = await Promise.resolve(v2SessionAtStart.maybeDestroyOldSPKs(actualAckSeq));
|
|
1277
|
+
if (client._v2Session === v2SessionAtStart
|
|
1278
|
+
&& Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart
|
|
1279
|
+
&& destroyed.length > 0) {
|
|
899
1280
|
client._clientLog.info(`V2 destroyed old SPKs after ack: ${destroyed.slice(0, 3).join(',')} (PFS)`);
|
|
900
1281
|
}
|
|
901
1282
|
}
|
|
@@ -909,16 +1290,17 @@ export class V2E2EECoordinator {
|
|
|
909
1290
|
async sendGroupV2(groupId, payload, opts) {
|
|
910
1291
|
const client = this.client;
|
|
911
1292
|
await client._ensureV2SessionReady('group.send', 'V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
|
|
912
|
-
const gid =
|
|
1293
|
+
const gid = String(groupId ?? '').trim();
|
|
913
1294
|
if (!gid)
|
|
914
1295
|
throw new ValidationError("group.send requires 'group_id'");
|
|
1296
|
+
const groupAid = String(opts?.groupAid ?? '').trim();
|
|
915
1297
|
if (!isJsonObject(payload))
|
|
916
1298
|
throw new ValidationError('group.send payload must be a dict for V2 encryption');
|
|
917
|
-
client._logMessageDebug('send-plaintext', 'group.send.v2', 'group.send', {
|
|
1299
|
+
client._logMessageDebug('send-plaintext', 'group.send.v2', 'group.send', withExplicitGroupAid({
|
|
918
1300
|
group_id: gid,
|
|
919
1301
|
message_id: opts?.messageId ?? '',
|
|
920
1302
|
payload,
|
|
921
|
-
}, { payloadOverride: payload });
|
|
1303
|
+
}, groupAid), { payloadOverride: payload });
|
|
922
1304
|
const attempt = async (useCache) => {
|
|
923
1305
|
client._clientLog.debug(`group.v2.send attempt: group=${gid}, use_cache=${useCache}`);
|
|
924
1306
|
const envelope = await client._buildV2GroupEnvelope({
|
|
@@ -930,10 +1312,10 @@ export class V2E2EECoordinator {
|
|
|
930
1312
|
context: opts?.context,
|
|
931
1313
|
useCache,
|
|
932
1314
|
});
|
|
933
|
-
const result = await client.call('group.v2.send', {
|
|
1315
|
+
const result = await client.call('group.v2.send', withExplicitGroupAid({
|
|
934
1316
|
group_id: gid,
|
|
935
1317
|
envelope,
|
|
936
|
-
});
|
|
1318
|
+
}, groupAid));
|
|
937
1319
|
client._clientLog.debug(`group.v2.send ok: group=${gid}, use_cache=${useCache}, seq=${String((isJsonObject(result) ? result.seq : '') ?? '')}`);
|
|
938
1320
|
return result;
|
|
939
1321
|
};
|
|
@@ -953,14 +1335,14 @@ export class V2E2EECoordinator {
|
|
|
953
1335
|
try {
|
|
954
1336
|
const result = await attempt(true);
|
|
955
1337
|
markSentSeq(result);
|
|
956
|
-
return client._delivery.attachSendResultEnvelope('group.send', {
|
|
1338
|
+
return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
|
|
957
1339
|
group_id: gid,
|
|
958
1340
|
payload,
|
|
959
1341
|
message_id: opts?.messageId,
|
|
960
1342
|
timestamp: opts?.timestamp,
|
|
961
1343
|
protected_headers: opts?.protectedHeaders,
|
|
962
1344
|
context: opts?.context,
|
|
963
|
-
}, result, true);
|
|
1345
|
+
}, groupAid), result, true);
|
|
964
1346
|
}
|
|
965
1347
|
catch (exc) {
|
|
966
1348
|
const excCode = Number(exc?.code);
|
|
@@ -969,14 +1351,14 @@ export class V2E2EECoordinator {
|
|
|
969
1351
|
this.deleteBootstrapCacheEntry(`group:${gid}`);
|
|
970
1352
|
const result = await attempt(false);
|
|
971
1353
|
markSentSeq(result);
|
|
972
|
-
return client._delivery.attachSendResultEnvelope('group.send', {
|
|
1354
|
+
return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
|
|
973
1355
|
group_id: gid,
|
|
974
1356
|
payload,
|
|
975
1357
|
message_id: opts?.messageId,
|
|
976
1358
|
timestamp: opts?.timestamp,
|
|
977
1359
|
protected_headers: opts?.protectedHeaders,
|
|
978
1360
|
context: opts?.context,
|
|
979
|
-
}, result, true);
|
|
1361
|
+
}, groupAid), result, true);
|
|
980
1362
|
}
|
|
981
1363
|
throw exc;
|
|
982
1364
|
}
|
|
@@ -986,8 +1368,7 @@ export class V2E2EECoordinator {
|
|
|
986
1368
|
if (!client._v2Session)
|
|
987
1369
|
throw new StateError('V2 session not initialized');
|
|
988
1370
|
const session = client._v2Session;
|
|
989
|
-
const
|
|
990
|
-
const groupId = normalizeGroupId(rawGroupId) || rawGroupId;
|
|
1371
|
+
const groupId = String(opts.groupId ?? '').trim();
|
|
991
1372
|
if (!groupId)
|
|
992
1373
|
throw new ValidationError("group.send requires 'group_id'");
|
|
993
1374
|
const cacheKey = `group:${groupId}`;
|
|
@@ -998,15 +1379,7 @@ export class V2E2EECoordinator {
|
|
|
998
1379
|
let stateCommitment = { state_version: 0, state_hash: '', state_chain: '' };
|
|
999
1380
|
let wrapPolicy = normalizeV2WrapPolicy(undefined);
|
|
1000
1381
|
let usedCachedBootstrap = false;
|
|
1001
|
-
|
|
1002
|
-
if (!cached && useCache && rawGroupId && rawGroupId !== groupId) {
|
|
1003
|
-
const legacyCacheKey = `group:${rawGroupId}`;
|
|
1004
|
-
cached = this.getBootstrapCacheEntry(legacyCacheKey);
|
|
1005
|
-
if (cached) {
|
|
1006
|
-
this.setBootstrapCacheEntry(cacheKey, cached);
|
|
1007
|
-
this.deleteBootstrapCacheEntry(legacyCacheKey);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1382
|
+
const cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : undefined;
|
|
1010
1383
|
if (cached && Date.now() - Number(cached.cachedAt ?? 0) < V2_BOOTSTRAP_TTL_MS) {
|
|
1011
1384
|
allDevices = (cached.devices ?? []);
|
|
1012
1385
|
auditRecipientsRaw = (cached.auditRecipients ?? []);
|
|
@@ -1136,53 +1509,157 @@ export class V2E2EECoordinator {
|
|
|
1136
1509
|
});
|
|
1137
1510
|
return envelope;
|
|
1138
1511
|
}
|
|
1512
|
+
async pullGroupV2TailInternal(params) {
|
|
1513
|
+
const client = this.client;
|
|
1514
|
+
if (params.window_mode !== 'tail')
|
|
1515
|
+
throw new ValidationError('window_mode must equal tail');
|
|
1516
|
+
if ('ack_up_to_seq' in params || params.force === true)
|
|
1517
|
+
throw new ValidationError('Tail does not accept ack_up_to_seq or force');
|
|
1518
|
+
const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
|
|
1519
|
+
if (!groupId)
|
|
1520
|
+
throw new ValidationError('group_id is required');
|
|
1521
|
+
let groupAid = String(params.group_aid ?? '').trim();
|
|
1522
|
+
const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
|
|
1523
|
+
const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
|
|
1524
|
+
const ns = `group:${groupId}`;
|
|
1525
|
+
const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
|
|
1526
|
+
group_id: groupId,
|
|
1527
|
+
window_mode: 'tail',
|
|
1528
|
+
after_seq: afterSeq,
|
|
1529
|
+
limit,
|
|
1530
|
+
_rpc_foreground: true,
|
|
1531
|
+
}, groupAid));
|
|
1532
|
+
const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
|
|
1533
|
+
if (resultAid)
|
|
1534
|
+
groupAid = resultAid;
|
|
1535
|
+
const floor = Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0);
|
|
1536
|
+
const previous = client._seqTracker.getSyncState(ns);
|
|
1537
|
+
await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
|
|
1538
|
+
const page = validateTailPage(result, limit);
|
|
1539
|
+
client._seqTracker.commitTailWindow(ns, {
|
|
1540
|
+
windowStartSeq: page.windowStart,
|
|
1541
|
+
headSeq: page.head,
|
|
1542
|
+
coveredThroughSeq: page.head,
|
|
1543
|
+
pageLimit: limit,
|
|
1544
|
+
});
|
|
1545
|
+
client._persistSeq(ns);
|
|
1546
|
+
client._saveSeqTrackerState?.();
|
|
1547
|
+
const decoded = [];
|
|
1548
|
+
for (const msg of page.messages) {
|
|
1549
|
+
const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail');
|
|
1550
|
+
if (plaintext)
|
|
1551
|
+
decoded.push(plaintext);
|
|
1552
|
+
}
|
|
1553
|
+
const ack = client._seqTracker.getContiguousSeq(ns);
|
|
1554
|
+
if (ack > previous.ack)
|
|
1555
|
+
await this.ackGroupV2(groupId, ack, groupAid);
|
|
1556
|
+
return { ...result, messages: decoded, raw_count: page.messages.length };
|
|
1557
|
+
}
|
|
1558
|
+
async historyGroupV2Internal(params) {
|
|
1559
|
+
const client = this.client;
|
|
1560
|
+
const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
|
|
1561
|
+
if (!groupId)
|
|
1562
|
+
throw new ValidationError('group_id is required');
|
|
1563
|
+
let groupAid = String(params.group_aid ?? '').trim();
|
|
1564
|
+
const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
|
|
1565
|
+
const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
|
|
1566
|
+
const result = await client._callRawV2Rpc('group.history', withExplicitGroupAid({
|
|
1567
|
+
group_id: groupId,
|
|
1568
|
+
before_seq: beforeSeq,
|
|
1569
|
+
limit,
|
|
1570
|
+
_rpc_foreground: true,
|
|
1571
|
+
}, groupAid));
|
|
1572
|
+
const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
|
|
1573
|
+
if (resultAid)
|
|
1574
|
+
groupAid = resultAid;
|
|
1575
|
+
const messages = validateHistoryPage(result, beforeSeq);
|
|
1576
|
+
const decoded = [];
|
|
1577
|
+
for (const msg of messages) {
|
|
1578
|
+
const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, false, true, 'history');
|
|
1579
|
+
if (plaintext)
|
|
1580
|
+
decoded.push(plaintext);
|
|
1581
|
+
}
|
|
1582
|
+
return { ...result, messages: decoded };
|
|
1583
|
+
}
|
|
1139
1584
|
async pullGroupV2Internal(params) {
|
|
1140
|
-
await this.pullGroupV2(params.group_id, params.after_seq, params.limit, {
|
|
1585
|
+
await this.pullGroupV2(params.group_id, params.after_seq, params.limit, {
|
|
1586
|
+
gateLocked: true,
|
|
1587
|
+
explicitAfterSeq: true,
|
|
1588
|
+
force: params.force === true,
|
|
1589
|
+
});
|
|
1141
1590
|
}
|
|
1142
1591
|
async pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {
|
|
1143
1592
|
const client = this.client;
|
|
1144
1593
|
await client._ensureV2SessionReady('group.pull');
|
|
1145
|
-
const gid =
|
|
1594
|
+
const gid = String(groupId ?? '').trim();
|
|
1146
1595
|
if (!gid)
|
|
1147
1596
|
throw new ValidationError('group.pull requires group_id');
|
|
1597
|
+
let groupAid = String(opts?.groupAid ?? '').trim();
|
|
1148
1598
|
const ns = `group:${gid}`;
|
|
1599
|
+
if (opts?.windowMode === 'tail') {
|
|
1600
|
+
if (!opts.gateLocked) {
|
|
1601
|
+
const key = `${ns}|tail|after=${afterSeq}|limit=${limit}`;
|
|
1602
|
+
return (await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
|
|
1603
|
+
...opts,
|
|
1604
|
+
gateLocked: true,
|
|
1605
|
+
}), false));
|
|
1606
|
+
}
|
|
1607
|
+
const result = await this.pullGroupV2TailInternal({
|
|
1608
|
+
group_id: String(opts.wireGroupId ?? gid),
|
|
1609
|
+
group_aid: groupAid || undefined,
|
|
1610
|
+
window_mode: 'tail',
|
|
1611
|
+
after_seq: afterSeq,
|
|
1612
|
+
limit,
|
|
1613
|
+
});
|
|
1614
|
+
return (Array.isArray(result.messages) ? result.messages : []);
|
|
1615
|
+
}
|
|
1149
1616
|
if (!opts?.gateLocked) {
|
|
1150
|
-
|
|
1617
|
+
const key = `${ns}|forward|after=${afterSeq}|force=${opts?.force === true}|limit=${limit}`;
|
|
1618
|
+
return await client._runPullSerialized(key, async () => this.pullGroupV2(gid, afterSeq, limit, {
|
|
1151
1619
|
...(opts ?? {}),
|
|
1152
1620
|
gateLocked: true,
|
|
1153
1621
|
scheduleFollowup: true,
|
|
1154
|
-
}));
|
|
1622
|
+
}), true);
|
|
1155
1623
|
}
|
|
1156
1624
|
const decrypted = [];
|
|
1157
1625
|
const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
|
|
1158
1626
|
const cursorParams = opts?.cursorParams ?? {};
|
|
1159
1627
|
const ownsCursor = opts?.ownsCursor !== false;
|
|
1160
|
-
let nextAfterSeq = opts?.explicitAfterSeq
|
|
1628
|
+
let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
|
|
1629
|
+
? afterSeq
|
|
1630
|
+
: (afterSeq || client._seqTracker.getContiguousSeq(ns));
|
|
1161
1631
|
let pageCount = 0;
|
|
1162
1632
|
let lastAutoAckSeq = 0;
|
|
1163
1633
|
let pendingAckSeq = 0;
|
|
1164
|
-
const maxPages = 100;
|
|
1634
|
+
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
1165
1635
|
while (pageCount < maxPages) {
|
|
1166
1636
|
pageCount += 1;
|
|
1167
1637
|
const ackUpToSeq = ownsCursor ? pendingAckSeq : 0;
|
|
1638
|
+
const inlineAckGeneration = ackUpToSeq > 0
|
|
1639
|
+
? client._delivery.captureGroupInlineAckGeneration()
|
|
1640
|
+
: null;
|
|
1168
1641
|
client._clientLog.debug(`group.v2.pull page request: group=${gid}, page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns}`);
|
|
1169
|
-
const result = await client._callRawV2Rpc('group.v2.pull', {
|
|
1642
|
+
const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
|
|
1170
1643
|
group_id: wireGroupId,
|
|
1171
|
-
group_aid: gid,
|
|
1172
1644
|
after_seq: nextAfterSeq,
|
|
1173
1645
|
limit,
|
|
1174
1646
|
...cursorParams,
|
|
1647
|
+
...(opts?.force ? { force: true } : {}),
|
|
1175
1648
|
...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
|
|
1176
|
-
});
|
|
1649
|
+
}, groupAid));
|
|
1177
1650
|
if (ackUpToSeq > 0) {
|
|
1651
|
+
if (inlineAckGeneration !== null) {
|
|
1652
|
+
client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
|
|
1653
|
+
}
|
|
1178
1654
|
pendingAckSeq = 0;
|
|
1179
1655
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1180
1656
|
}
|
|
1657
|
+
const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
|
|
1658
|
+
if (resultGroupAid)
|
|
1659
|
+
groupAid = resultGroupAid;
|
|
1660
|
+
const eventGroupAid = resultGroupAid || groupAid || gid;
|
|
1181
1661
|
const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
|
|
1182
|
-
const messages = pageMessages
|
|
1183
|
-
const seq = Number(msg.seq ?? 0);
|
|
1184
|
-
return Number.isFinite(seq) && seq > nextAfterSeq;
|
|
1185
|
-
}).sort((a, b) => Number(a.seq ?? 0) - Number(b.seq ?? 0));
|
|
1662
|
+
const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
|
|
1186
1663
|
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
1187
1664
|
client._clientLog.debug(`group.v2.pull page response: group=${gid}, page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, cursor_current=${String(cursor?.current_seq ?? '')}`);
|
|
1188
1665
|
for (const msg of messages) {
|
|
@@ -1192,11 +1669,12 @@ export class V2E2EECoordinator {
|
|
|
1192
1669
|
.map((msg) => Number(msg.seq ?? 0))
|
|
1193
1670
|
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
1194
1671
|
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
1672
|
+
const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
|
|
1673
|
+
? client._seqTracker.snapshotNamespace(ns)
|
|
1674
|
+
: null;
|
|
1195
1675
|
let pageMaxSeq = nextAfterSeq;
|
|
1196
1676
|
if (seqs.length > 0) {
|
|
1197
1677
|
pageMaxSeq = Math.max(...seqs);
|
|
1198
|
-
client._seqTracker.forceContiguousSeq(ns, pageMaxSeq);
|
|
1199
|
-
client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
1200
1678
|
}
|
|
1201
1679
|
for (const msg of messages) {
|
|
1202
1680
|
const seq = Number(msg.seq ?? 0);
|
|
@@ -1208,7 +1686,11 @@ export class V2E2EECoordinator {
|
|
|
1208
1686
|
const payloadObj = isJsonObject(payload) ? payload : null;
|
|
1209
1687
|
// 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
|
|
1210
1688
|
if (client._delivery.recallEventFromGroupMessage(msg)) {
|
|
1211
|
-
await client._delivery.publishGroupRecallTombstone(gid, seq,
|
|
1689
|
+
await client._delivery.publishGroupRecallTombstone(gid, seq, {
|
|
1690
|
+
...msg,
|
|
1691
|
+
group_id: gid,
|
|
1692
|
+
group_aid: eventGroupAid,
|
|
1693
|
+
});
|
|
1212
1694
|
client._markPublishedSeq(ns, seq);
|
|
1213
1695
|
client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
|
|
1214
1696
|
continue;
|
|
@@ -1216,16 +1698,18 @@ export class V2E2EECoordinator {
|
|
|
1216
1698
|
if (payloadObj) {
|
|
1217
1699
|
const payloadType = String(payloadObj.type ?? '').trim();
|
|
1218
1700
|
if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted') {
|
|
1219
|
-
|
|
1701
|
+
let v1Msg = {
|
|
1220
1702
|
message_id: String(msg.message_id ?? ''),
|
|
1221
1703
|
from: String(msg.from_aid ?? ''),
|
|
1222
1704
|
group_id: gid,
|
|
1705
|
+
group_aid: eventGroupAid,
|
|
1223
1706
|
seq: msg.seq,
|
|
1224
1707
|
type: String(msg.type ?? ''),
|
|
1225
1708
|
timestamp: msg.t_server,
|
|
1226
1709
|
payload,
|
|
1227
1710
|
encrypted: false,
|
|
1228
1711
|
};
|
|
1712
|
+
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
1229
1713
|
attachGatewayProximity(v1Msg, msg);
|
|
1230
1714
|
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
|
|
1231
1715
|
decrypted.push(v1Msg);
|
|
@@ -1234,16 +1718,18 @@ export class V2E2EECoordinator {
|
|
|
1234
1718
|
}
|
|
1235
1719
|
}
|
|
1236
1720
|
else if (payload !== undefined && payload !== null) {
|
|
1237
|
-
|
|
1721
|
+
let v1Msg = {
|
|
1238
1722
|
message_id: String(msg.message_id ?? ''),
|
|
1239
1723
|
from: String(msg.from_aid ?? ''),
|
|
1240
1724
|
group_id: gid,
|
|
1725
|
+
group_aid: eventGroupAid,
|
|
1241
1726
|
seq: msg.seq,
|
|
1242
1727
|
type: String(msg.type ?? ''),
|
|
1243
1728
|
timestamp: msg.t_server,
|
|
1244
1729
|
payload,
|
|
1245
1730
|
encrypted: false,
|
|
1246
1731
|
};
|
|
1732
|
+
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
1247
1733
|
attachGatewayProximity(v1Msg, msg);
|
|
1248
1734
|
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
|
|
1249
1735
|
decrypted.push(v1Msg);
|
|
@@ -1257,31 +1743,51 @@ export class V2E2EECoordinator {
|
|
|
1257
1743
|
client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
|
|
1258
1744
|
continue;
|
|
1259
1745
|
}
|
|
1260
|
-
|
|
1746
|
+
let plaintext = await client._decryptV2Message(msg);
|
|
1261
1747
|
if (plaintext === null) {
|
|
1262
1748
|
client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
|
|
1263
1749
|
continue;
|
|
1264
1750
|
}
|
|
1751
|
+
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
1265
1752
|
plaintext.group_id = gid;
|
|
1753
|
+
plaintext.group_aid = eventGroupAid;
|
|
1266
1754
|
await client._publishPulledMessage('group.message_created', ns, seq, plaintext);
|
|
1267
1755
|
decrypted.push(plaintext);
|
|
1268
1756
|
client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
|
|
1269
1757
|
}
|
|
1270
1758
|
const cursorCurrentSeq = Number(cursor?.current_seq ?? 0);
|
|
1271
1759
|
const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
|
|
1272
|
-
const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq')
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1760
|
+
const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
|
|
1761
|
+
const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
|
|
1762
|
+
const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
|
|
1763
|
+
const commitTarget = Math.max(pageContigBefore, effectiveFloor, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
|
|
1764
|
+
let ackSeq = pageContigBefore;
|
|
1765
|
+
let contigAdvanced = false;
|
|
1766
|
+
try {
|
|
1767
|
+
if (commitTarget > pageContigBefore) {
|
|
1768
|
+
client._seqTracker.forceContiguousSeq(ns, commitTarget);
|
|
1769
|
+
if (effectiveFloor > pageContigBefore) {
|
|
1770
|
+
client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
|
|
1771
|
+
}
|
|
1772
|
+
if (seqs.length > 0) {
|
|
1773
|
+
client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1777
|
+
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1778
|
+
if (contigAdvanced) {
|
|
1779
|
+
await client._drainOrderedMessages(ns, undefined, true);
|
|
1780
|
+
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1781
|
+
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1782
|
+
client._commitSeqTrackerState(ns);
|
|
1278
1783
|
}
|
|
1279
1784
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
client.
|
|
1785
|
+
catch (exc) {
|
|
1786
|
+
if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
1787
|
+
client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
|
|
1788
|
+
}
|
|
1789
|
+
client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
1790
|
+
throw exc;
|
|
1285
1791
|
}
|
|
1286
1792
|
const ackNeeded = pageMessages.length > 0
|
|
1287
1793
|
&& ackSeq > 0
|
|
@@ -1327,21 +1833,30 @@ export class V2E2EECoordinator {
|
|
|
1327
1833
|
client._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns}`);
|
|
1328
1834
|
return decrypted;
|
|
1329
1835
|
}
|
|
1330
|
-
async ackGroupV2(groupId, upToSeq) {
|
|
1836
|
+
async ackGroupV2(groupId, upToSeq, groupAid) {
|
|
1331
1837
|
const client = this.client;
|
|
1332
|
-
const gid =
|
|
1838
|
+
const gid = String(groupId ?? '').trim();
|
|
1333
1839
|
if (!gid)
|
|
1334
1840
|
throw new ValidationError('group.ack_messages requires group_id');
|
|
1335
|
-
const
|
|
1841
|
+
const explicitGroupAid = String(groupAid ?? '').trim();
|
|
1842
|
+
const wireNs = `group:${gid}`;
|
|
1843
|
+
const ns = client._delivery.resolvePendingGroupInlineAckNamespace?.(wireNs) ?? wireNs;
|
|
1336
1844
|
let seq = Number(upToSeq ?? client._seqTracker.getContiguousSeq(ns));
|
|
1337
1845
|
if (!Number.isFinite(seq) || seq <= 0) {
|
|
1338
1846
|
client._clientLog.debug(`group.v2.ack skipped: group=${gid}, ns=${ns}, up_to_seq=${String(upToSeq ?? '')}`);
|
|
1339
1847
|
return { acked: 0 };
|
|
1340
1848
|
}
|
|
1341
1849
|
seq = client._clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
|
|
1850
|
+
if (seq <= 0)
|
|
1851
|
+
return { acked: 0 };
|
|
1852
|
+
const inlineAckGeneration = client._delivery.captureGroupInlineAckGeneration();
|
|
1342
1853
|
client._clientLog.debug(`group.v2.ack send: group=${gid}, ns=${ns}, up_to_seq=${seq}`);
|
|
1343
|
-
const result = await client._callRawV2Rpc('group.v2.ack', { group_id: gid,
|
|
1344
|
-
client.
|
|
1854
|
+
const result = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq }, explicitGroupAid));
|
|
1855
|
+
const actualAckSeq = client._delivery.resolveActualAckSeq(result, seq);
|
|
1856
|
+
if (inlineAckGeneration !== null) {
|
|
1857
|
+
client._delivery.onGroupV2AckSucceeded(gid, actualAckSeq, inlineAckGeneration);
|
|
1858
|
+
}
|
|
1859
|
+
client._clientLog.debug(`group.v2.ack ok: group=${gid}, ns=${ns}, requested=${seq}, effective=${actualAckSeq}, result=${client._debugJson(result)}`);
|
|
1345
1860
|
return result;
|
|
1346
1861
|
}
|
|
1347
1862
|
async putMessageThoughtEncryptedV2(params) {
|
|
@@ -1754,7 +2269,7 @@ export class V2E2EECoordinator {
|
|
|
1754
2269
|
}
|
|
1755
2270
|
safeUndecryptablePushEvent(msg, group) {
|
|
1756
2271
|
const client = this.client;
|
|
1757
|
-
|
|
2272
|
+
let event = {
|
|
1758
2273
|
message_id: msg.message_id,
|
|
1759
2274
|
from: msg.from,
|
|
1760
2275
|
seq: msg.seq,
|
|
@@ -1778,6 +2293,10 @@ export class V2E2EECoordinator {
|
|
|
1778
2293
|
client._attachV2EnvelopeMetadata(event, client._v2E2eeMeta(envelope));
|
|
1779
2294
|
}
|
|
1780
2295
|
}
|
|
2296
|
+
if (group) {
|
|
2297
|
+
const modeSource = envelope ? { ...msg, envelope } : msg;
|
|
2298
|
+
event = normalizeGroupMentionMode(event, modeSource);
|
|
2299
|
+
}
|
|
1781
2300
|
return event;
|
|
1782
2301
|
}
|
|
1783
2302
|
async decryptEncryptedPushPayload(msg, group) {
|
|
@@ -1791,7 +2310,7 @@ export class V2E2EECoordinator {
|
|
|
1791
2310
|
if (!plaintext)
|
|
1792
2311
|
return null;
|
|
1793
2312
|
const e2ee = client._v2E2eeMeta(envelope);
|
|
1794
|
-
|
|
2313
|
+
let result = {
|
|
1795
2314
|
message_id: String(msg.message_id ?? ''),
|
|
1796
2315
|
from: fromAid,
|
|
1797
2316
|
seq: msg.seq,
|
|
@@ -1810,6 +2329,7 @@ export class V2E2EECoordinator {
|
|
|
1810
2329
|
attachGatewayProximity(result, msg);
|
|
1811
2330
|
if (group) {
|
|
1812
2331
|
result.group_id = (msg.group_id ?? aad.group_id ?? envelope.group_id);
|
|
2332
|
+
result = normalizeGroupMentionMode(result, msg, envelope);
|
|
1813
2333
|
}
|
|
1814
2334
|
else {
|
|
1815
2335
|
result.to = (msg.to ?? client._aid ?? '');
|
|
@@ -1846,7 +2366,7 @@ export class V2E2EECoordinator {
|
|
|
1846
2366
|
async decryptV2PushMessage(data) {
|
|
1847
2367
|
if (!isJsonObject(data))
|
|
1848
2368
|
return null;
|
|
1849
|
-
return await this.client._decryptV2Message(data);
|
|
2369
|
+
return await this.client._decryptV2Message(data, false, false, false, false);
|
|
1850
2370
|
}
|
|
1851
2371
|
handleGroupChangedSpk(data, groupId, action) {
|
|
1852
2372
|
if (!this.client._v2Session || !groupId || !MEMBERSHIP_ACTIONS.has(action))
|