@agentunion/fastaun-browser 0.5.4 → 0.5.8
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 +177 -67
- 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 +177 -67
- package/_packed_docs/INDEX.md +124 -65
- package/_packed_docs/KITE_DOCS_GUIDE.md +65 -30
- package/_packed_docs/agent.md/SCHEMA.md +83 -58
- package/_packed_docs/agent.md/examples/codeagent-claudecode.md +1 -1
- package/_packed_docs/agent.md/examples/openclaw-lobster.md +1 -1
- package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +1 -1
- 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 +234 -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 +1095 -0
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +1199 -0
- package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +6 -4
- package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
- package/_packed_docs/message-online-push-alignment.md +572 -0
- 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 +63 -18
- 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 +93 -84
- 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 +73 -34
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +21 -9
- package/_packed_docs/sdk/09-group-rpc-manual.md +319 -285
- package/_packed_docs/sdk/09-message-rpc-manual.md +144 -103
- package/_packed_docs/sdk/09-payload-reference.md +1 -1
- package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
- package/_packed_docs/sdk/09-stream-rpc-manual.md +8 -8
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +34 -23
- package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
- package/_packed_docs/sdk/INDEX.md +45 -37
- package/_packed_docs/sdk/README.md +11 -9
- package/_packed_docs//345/217/221/345/270/203/346/212/245/345/221/212-0.5.6.md +260 -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/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-schema.d.ts +14 -0
- package/dist/agent-md-schema.d.ts.map +1 -0
- package/dist/agent-md-schema.js +103 -0
- package/dist/agent-md-schema.js.map +1 -0
- package/dist/agent-md.d.ts +11 -5
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +295 -53
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +7 -7
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +46 -18
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +2 -0
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +29 -7
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +31 -11
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +29920 -19719
- package/dist/cert-utils.d.ts +1 -0
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +36 -16
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +106 -11
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +1925 -374
- 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 +15 -1
- package/dist/client/lifecycle.d.ts.map +1 -1
- package/dist/client/lifecycle.js +477 -132
- 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 +34 -2
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +503 -99
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/runtime.d.ts +1 -3
- package/dist/client/runtime.d.ts.map +1 -1
- package/dist/client/runtime.js +4 -7
- package/dist/client/runtime.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +43 -2
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +931 -138
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +46 -15
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +785 -304
- 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 +26 -1
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +142 -60
- package/dist/facades.js.map +1 -1
- package/dist/group-id.d.ts.map +1 -1
- package/dist/group-id.js +2 -17
- package/dist/group-id.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 +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- 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 +224 -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 +399 -11
- 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 +226 -102
- 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/validators.d.ts.map +1 -1
- package/dist/validators.js +8 -14
- package/dist/validators.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -9
- 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,133 @@
|
|
|
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 safeNonNegativeSeq(value) {
|
|
25
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
26
|
+
}
|
|
27
|
+
function strictWindowLimit(value) {
|
|
28
|
+
const limit = strictWindowSeq(value, 'limit', true);
|
|
29
|
+
if (limit > V2_PULL_MAX_PAGE_LIMIT)
|
|
30
|
+
throw new ValidationError(`limit must be <= ${V2_PULL_MAX_PAGE_LIMIT}`);
|
|
31
|
+
return limit;
|
|
32
|
+
}
|
|
33
|
+
function deduplicateWindowMessages(value, lowerSeq, upperSeq, boundary) {
|
|
34
|
+
if (!Array.isArray(value))
|
|
35
|
+
throw new ValidationError('messages must be an array');
|
|
36
|
+
const bySeq = new Map();
|
|
37
|
+
for (const raw of value) {
|
|
38
|
+
if (!isJsonObject(raw))
|
|
39
|
+
throw new ValidationError('messages entries must be objects');
|
|
40
|
+
const row = raw;
|
|
41
|
+
const seq = strictWindowSeq(row.seq, 'messages[].seq', true);
|
|
42
|
+
if (seq < lowerSeq || seq > upperSeq)
|
|
43
|
+
throw new ValidationError(`messages[].seq is outside ${boundary}`);
|
|
44
|
+
const previous = bySeq.get(seq);
|
|
45
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(row))
|
|
46
|
+
throw new ValidationError('messages contains conflicting duplicate seq');
|
|
47
|
+
bySeq.set(seq, row);
|
|
48
|
+
}
|
|
49
|
+
return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
|
|
50
|
+
}
|
|
51
|
+
function deduplicateForwardPageMessages(value, afterSeq) {
|
|
52
|
+
if (!Array.isArray(value))
|
|
53
|
+
return [];
|
|
54
|
+
const bySeq = new Map();
|
|
55
|
+
for (const raw of value) {
|
|
56
|
+
if (!isJsonObject(raw))
|
|
57
|
+
continue;
|
|
58
|
+
const row = raw;
|
|
59
|
+
const seq = safeNonNegativeSeq(row.seq);
|
|
60
|
+
if (seq === null || seq <= 0 || seq <= afterSeq || bySeq.has(seq))
|
|
61
|
+
continue;
|
|
62
|
+
bySeq.set(seq, row);
|
|
63
|
+
}
|
|
64
|
+
return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
|
|
65
|
+
}
|
|
66
|
+
function shouldContinueForwardPage({ rawCount, nextAfterSeq, nextAfter, limit, hasMoreField, hasMore, serverHead, pendingAckSeq, }) {
|
|
67
|
+
// 无新 seq 或游标未前进时必须停止,避免错误元数据造成循环。
|
|
68
|
+
if (rawCount <= 0 || nextAfter <= nextAfterSeq)
|
|
69
|
+
return false;
|
|
70
|
+
if (hasMore || serverHead > nextAfter || pendingAckSeq > 0)
|
|
71
|
+
return true;
|
|
72
|
+
// 仅旧服务缺失 has_more 时,以满页保留兼容续拉。
|
|
73
|
+
return !hasMoreField && limit > 0 && rawCount >= limit;
|
|
74
|
+
}
|
|
75
|
+
function validateTailPage(result, afterSeq, pageLimit) {
|
|
76
|
+
if (!isJsonObject(result))
|
|
77
|
+
throw new ValidationError('tail response must be an object');
|
|
78
|
+
const row = result;
|
|
79
|
+
if (row.window_mode !== 'tail')
|
|
80
|
+
throw new ValidationError('tail response window_mode must equal tail');
|
|
81
|
+
strictWindowSeq(afterSeq, 'after_seq');
|
|
82
|
+
const windowStart = strictWindowSeq(row.window_start_seq, 'window_start_seq');
|
|
83
|
+
const head = strictWindowSeq(row.head_seq, 'head_seq');
|
|
84
|
+
const covered = strictWindowSeq(row.covered_through_seq, 'covered_through_seq');
|
|
85
|
+
if (windowStart > head || covered !== head || head - windowStart + 1 > pageLimit) {
|
|
86
|
+
throw new ValidationError('tail response proof is invalid');
|
|
87
|
+
}
|
|
88
|
+
let lowerSeq = windowStart;
|
|
89
|
+
if (head <= afterSeq) {
|
|
90
|
+
if (windowStart !== head)
|
|
91
|
+
throw new ValidationError('empty tail window must end at head_seq');
|
|
92
|
+
lowerSeq = head + 1;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
if (windowStart < afterSeq + 1)
|
|
96
|
+
throw new ValidationError('tail window_start_seq is below after_seq');
|
|
97
|
+
lowerSeq = Math.max(windowStart, afterSeq + 1);
|
|
98
|
+
}
|
|
99
|
+
const messages = deduplicateWindowMessages(row.messages, lowerSeq, head, 'the proven tail window');
|
|
100
|
+
if (head <= afterSeq && messages.length > 0)
|
|
101
|
+
throw new ValidationError('tail response must not include messages at or below after_seq');
|
|
102
|
+
return { windowStart, head, messages };
|
|
103
|
+
}
|
|
104
|
+
function validateHistoryPage(result, beforeSeq) {
|
|
105
|
+
if (!isJsonObject(result))
|
|
106
|
+
throw new ValidationError('history response must be an object');
|
|
107
|
+
const row = result;
|
|
108
|
+
const rawStart = row.window_start_seq;
|
|
109
|
+
const rawNext = row.next_before_seq;
|
|
110
|
+
if ((rawStart === undefined) !== (rawNext === undefined))
|
|
111
|
+
throw new ValidationError('history pagination boundaries must both be present or absent');
|
|
112
|
+
let lowerSeq = 1;
|
|
113
|
+
let windowStart;
|
|
114
|
+
if (rawStart !== undefined) {
|
|
115
|
+
windowStart = strictWindowSeq(rawStart, 'window_start_seq', true);
|
|
116
|
+
const nextBefore = strictWindowSeq(rawNext, 'next_before_seq', true);
|
|
117
|
+
if (windowStart !== nextBefore || windowStart >= beforeSeq)
|
|
118
|
+
throw new ValidationError('history boundary is invalid');
|
|
119
|
+
lowerSeq = windowStart;
|
|
120
|
+
}
|
|
121
|
+
const messages = deduplicateWindowMessages(row.messages, lowerSeq, beforeSeq - 1, 'the history page');
|
|
122
|
+
if (messages.length > 0 && (windowStart === undefined || Number(messages[0].seq) !== windowStart)) {
|
|
123
|
+
throw new ValidationError('history messages require exact pagination boundaries');
|
|
124
|
+
}
|
|
125
|
+
if (messages.length === 0 && windowStart !== undefined)
|
|
126
|
+
throw new ValidationError('empty history page must not include pagination boundaries');
|
|
127
|
+
if (typeof row.has_older !== 'boolean' || (messages.length === 0 && row.has_older))
|
|
128
|
+
throw new ValidationError('has_older is invalid');
|
|
129
|
+
return messages;
|
|
130
|
+
}
|
|
13
131
|
const MEMBERSHIP_ACTIONS = new Set([
|
|
14
132
|
'member_added',
|
|
15
133
|
'member_left',
|
|
@@ -205,6 +323,12 @@ function v2WrapCapabilities() {
|
|
|
205
323
|
per_device_wrap: true,
|
|
206
324
|
};
|
|
207
325
|
}
|
|
326
|
+
function withExplicitGroupAid(params, groupAid) {
|
|
327
|
+
const aid = String(groupAid ?? '').trim();
|
|
328
|
+
if (aid)
|
|
329
|
+
params.group_aid = aid;
|
|
330
|
+
return params;
|
|
331
|
+
}
|
|
208
332
|
function applyV2WrapPolicyToTargets(targets, policy) {
|
|
209
333
|
if (!policy)
|
|
210
334
|
return targets;
|
|
@@ -231,6 +355,10 @@ function applyV2WrapPolicyToTargets(targets, policy) {
|
|
|
231
355
|
export class V2E2EECoordinator {
|
|
232
356
|
runtime;
|
|
233
357
|
targetSetCache = new Map();
|
|
358
|
+
senderIKSyncFetches = new Map();
|
|
359
|
+
/** 服务端 ACK cursor 高于本地 A 时,延迟到下一次 Forward Pull 再恢复。 */
|
|
360
|
+
pendingForwardCursors = new Map();
|
|
361
|
+
pendingForwardAcks = new Map();
|
|
234
362
|
v2SessionInitRetryTimer = null;
|
|
235
363
|
v2SessionInitRetryBackgroundSync = false;
|
|
236
364
|
v2SessionInitRetryIdentityKey = null;
|
|
@@ -240,6 +368,51 @@ export class V2E2EECoordinator {
|
|
|
240
368
|
get client() {
|
|
241
369
|
return this.runtime.client;
|
|
242
370
|
}
|
|
371
|
+
resetForwardCursorState() {
|
|
372
|
+
this.pendingForwardCursors.clear();
|
|
373
|
+
this.pendingForwardAcks.clear();
|
|
374
|
+
}
|
|
375
|
+
pendingForwardCursor(ns, localAck) {
|
|
376
|
+
if (!ns)
|
|
377
|
+
return 0;
|
|
378
|
+
const pending = this.pendingForwardCursors.get(ns) ?? 0;
|
|
379
|
+
if (pending <= localAck) {
|
|
380
|
+
if (pending > 0)
|
|
381
|
+
this.pendingForwardCursors.delete(ns);
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
return pending;
|
|
385
|
+
}
|
|
386
|
+
recordForwardCursor(ns, cursor, localAck) {
|
|
387
|
+
if (!ns || !Number.isSafeInteger(cursor) || cursor <= localAck)
|
|
388
|
+
return;
|
|
389
|
+
this.pendingForwardCursors.set(ns, Math.max(this.pendingForwardCursors.get(ns) ?? 0, cursor));
|
|
390
|
+
}
|
|
391
|
+
clearForwardCursor(ns, committedAck) {
|
|
392
|
+
const pending = this.pendingForwardCursors.get(ns) ?? 0;
|
|
393
|
+
if (pending > 0 && committedAck >= pending)
|
|
394
|
+
this.pendingForwardCursors.delete(ns);
|
|
395
|
+
}
|
|
396
|
+
pendingForwardAck(ns, localAck) {
|
|
397
|
+
if (!ns)
|
|
398
|
+
return 0;
|
|
399
|
+
const pending = this.pendingForwardAcks.get(ns) ?? 0;
|
|
400
|
+
if (!Number.isSafeInteger(pending) || pending <= 0 || localAck <= 0 || pending > localAck) {
|
|
401
|
+
this.pendingForwardAcks.delete(ns);
|
|
402
|
+
return 0;
|
|
403
|
+
}
|
|
404
|
+
return pending;
|
|
405
|
+
}
|
|
406
|
+
recordForwardAck(ns, ackSeq) {
|
|
407
|
+
if (!ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0)
|
|
408
|
+
return;
|
|
409
|
+
this.pendingForwardAcks.set(ns, Math.max(this.pendingForwardAcks.get(ns) ?? 0, ackSeq));
|
|
410
|
+
}
|
|
411
|
+
clearForwardAck(ns, confirmedSeq) {
|
|
412
|
+
const pending = this.pendingForwardAcks.get(ns) ?? 0;
|
|
413
|
+
if (pending > 0 && confirmedSeq >= pending)
|
|
414
|
+
this.pendingForwardAcks.delete(ns);
|
|
415
|
+
}
|
|
243
416
|
get bootstrapCache() {
|
|
244
417
|
const client = this.client;
|
|
245
418
|
if (!(client._v2BootstrapCache instanceof Map)) {
|
|
@@ -253,6 +426,18 @@ export class V2E2EECoordinator {
|
|
|
253
426
|
setBootstrapCacheEntry(key, entry) {
|
|
254
427
|
this.bootstrapCache.set(key, entry);
|
|
255
428
|
}
|
|
429
|
+
cacheBundledSelfDevices(bootstrap, wrapPolicy) {
|
|
430
|
+
const client = this.client;
|
|
431
|
+
if (!client._aid || !Object.prototype.hasOwnProperty.call(bootstrap, 'self_devices'))
|
|
432
|
+
return;
|
|
433
|
+
const selfDevices = (Array.isArray(bootstrap.self_devices) ? bootstrap.self_devices : []);
|
|
434
|
+
this.setBootstrapCacheEntry(client._aid, {
|
|
435
|
+
devices: selfDevices,
|
|
436
|
+
auditRecipients: [],
|
|
437
|
+
cachedAt: Date.now(),
|
|
438
|
+
wrapPolicy,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
256
441
|
deleteBootstrapCacheEntry(key) {
|
|
257
442
|
this.bootstrapCache.delete(key);
|
|
258
443
|
this.targetSetCache.delete(key);
|
|
@@ -555,6 +740,84 @@ export class V2E2EECoordinator {
|
|
|
555
740
|
pendingSenderIKFetchKey(fromAid, senderDeviceId, groupId) {
|
|
556
741
|
return `${fromAid}#${senderDeviceId}#${groupId || ''}`;
|
|
557
742
|
}
|
|
743
|
+
async fetchSenderIKOnce(fromAid, senderDeviceId, groupId, certFingerprint = '') {
|
|
744
|
+
const client = this.client;
|
|
745
|
+
const peerAid = String(fromAid ?? '').trim();
|
|
746
|
+
const deviceId = String(senderDeviceId ?? '').trim();
|
|
747
|
+
const gid = String(groupId ?? '').trim();
|
|
748
|
+
const fingerprint = String(certFingerprint ?? '').trim().toLowerCase();
|
|
749
|
+
if (!peerAid)
|
|
750
|
+
return null;
|
|
751
|
+
const fetchKey = [peerAid, deviceId, gid, fingerprint].join('#');
|
|
752
|
+
const existing = this.senderIKSyncFetches.get(fetchKey);
|
|
753
|
+
if (existing)
|
|
754
|
+
return await existing;
|
|
755
|
+
const task = (async () => {
|
|
756
|
+
const started = Date.now();
|
|
757
|
+
let timer;
|
|
758
|
+
const timedOut = Symbol('sender-ik-timeout');
|
|
759
|
+
const fetch = async () => {
|
|
760
|
+
try {
|
|
761
|
+
const bootstrap = await client.call('message.v2.bootstrap', {
|
|
762
|
+
peer_aid: peerAid,
|
|
763
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
764
|
+
});
|
|
765
|
+
const peers = (Array.isArray(bootstrap?.peer_devices) ? bootstrap.peer_devices : []);
|
|
766
|
+
for (const dev of peers)
|
|
767
|
+
client._cacheV2PeerIKFromDevice(dev, peerAid);
|
|
768
|
+
}
|
|
769
|
+
catch (exc) {
|
|
770
|
+
client._clientLog.warn(`V2 sender IK 同步 bootstrap 失败: peer=${peerAid} device=${deviceId || '-'} err=${String(formatE2EEError(exc))}`);
|
|
771
|
+
}
|
|
772
|
+
let senderPubDer = await this.getV2SenderPubDer(peerAid, deviceId, fingerprint);
|
|
773
|
+
if (senderPubDer || !gid)
|
|
774
|
+
return senderPubDer;
|
|
775
|
+
try {
|
|
776
|
+
const bootstrap = await client.call('group.v2.bootstrap', {
|
|
777
|
+
group_id: gid,
|
|
778
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
779
|
+
});
|
|
780
|
+
const devices = (Array.isArray(bootstrap?.devices) ? bootstrap.devices : []);
|
|
781
|
+
const audit = (Array.isArray(bootstrap?.audit_recipients) ? bootstrap.audit_recipients : []);
|
|
782
|
+
for (const dev of devices)
|
|
783
|
+
client._cacheV2PeerIKFromDevice(dev);
|
|
784
|
+
for (const dev of audit)
|
|
785
|
+
client._cacheV2PeerIKFromDevice(dev);
|
|
786
|
+
}
|
|
787
|
+
catch (exc) {
|
|
788
|
+
client._clientLog.warn(`V2 sender IK 同步 group bootstrap 失败: group=${gid} peer=${peerAid} err=${String(formatE2EEError(exc))}`);
|
|
789
|
+
}
|
|
790
|
+
senderPubDer = await this.getV2SenderPubDer(peerAid, deviceId, fingerprint);
|
|
791
|
+
return senderPubDer;
|
|
792
|
+
};
|
|
793
|
+
try {
|
|
794
|
+
const result = await Promise.race([
|
|
795
|
+
fetch(),
|
|
796
|
+
new Promise((resolve) => {
|
|
797
|
+
timer = setTimeout(() => resolve(timedOut), V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS);
|
|
798
|
+
}),
|
|
799
|
+
]);
|
|
800
|
+
if (result === timedOut) {
|
|
801
|
+
client._clientLog.warn(`V2 sender IK 同步获取超时: peer=${peerAid} device=${deviceId || '-'} group=${gid || '<p2p>'} timeout_ms=${V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS}`);
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
return result;
|
|
805
|
+
}
|
|
806
|
+
finally {
|
|
807
|
+
if (timer !== undefined)
|
|
808
|
+
clearTimeout(timer);
|
|
809
|
+
client._clientLog.debug(`V2 sender IK 同步获取完成: peer=${peerAid} device=${deviceId || '-'} group=${gid || '<p2p>'} elapsed_ms=${Date.now() - started}`);
|
|
810
|
+
}
|
|
811
|
+
})();
|
|
812
|
+
this.senderIKSyncFetches.set(fetchKey, task);
|
|
813
|
+
try {
|
|
814
|
+
return await task;
|
|
815
|
+
}
|
|
816
|
+
finally {
|
|
817
|
+
if (this.senderIKSyncFetches.get(fetchKey) === task)
|
|
818
|
+
this.senderIKSyncFetches.delete(fetchKey);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
558
821
|
scheduleSenderIKPending(args) {
|
|
559
822
|
const client = this.client;
|
|
560
823
|
const fromAid = String(args.fromAid ?? '').trim();
|
|
@@ -571,7 +834,8 @@ export class V2E2EECoordinator {
|
|
|
571
834
|
createdAt: Date.now(),
|
|
572
835
|
});
|
|
573
836
|
client._clientLog.debug(`V2 decrypt pending sender IK: key=${messageKey} from=${fromAid} device=${senderDeviceId || '-'} group=${groupId || '<p2p>'} pending=${client._v2SenderIKPending.size}`);
|
|
574
|
-
|
|
837
|
+
if (args.startFetch !== false)
|
|
838
|
+
this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupId);
|
|
575
839
|
}
|
|
576
840
|
scheduleSenderIKFetch(fromAid, senderDeviceId, groupId) {
|
|
577
841
|
const client = this.client;
|
|
@@ -622,26 +886,33 @@ export class V2E2EECoordinator {
|
|
|
622
886
|
const pendingItems = [...client._v2SenderIKPending.entries()].filter(([, entry]) => entry.fromAid === fromAid && entry.senderDeviceId === senderDeviceId && entry.groupId === groupId);
|
|
623
887
|
for (const [key, entry] of pendingItems) {
|
|
624
888
|
let plaintext = null;
|
|
889
|
+
const retryStatus = {};
|
|
625
890
|
try {
|
|
626
|
-
plaintext = await client._decryptV2Message(entry.msg, false);
|
|
891
|
+
plaintext = await client._decryptV2Message(entry.msg, false, true, true, true, retryStatus);
|
|
627
892
|
}
|
|
628
893
|
catch (exc) {
|
|
629
894
|
client._clientLog.warn(`V2 sender IK pending retry raised: key=${key} err=${String(formatE2EEError(exc))}`);
|
|
895
|
+
client._v2SenderIKPending.delete(key);
|
|
896
|
+
continue;
|
|
630
897
|
}
|
|
631
|
-
client._v2SenderIKPending.delete(key);
|
|
632
898
|
if (plaintext === null) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
899
|
+
if (retryStatus.deferred) {
|
|
900
|
+
client._clientLog.warn(`V2 pending retry still missing key material: key=${key}`);
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
client._clientLog.warn(`V2 sender IK pending retry still undecryptable: key=${key}`);
|
|
904
|
+
const isGroup = Boolean(entry.groupId);
|
|
905
|
+
const eventName = isGroup ? 'group.message_undecryptable' : 'message.undecryptable';
|
|
906
|
+
const safeEvent = this.safeUndecryptablePushEvent(entry.msg, isGroup);
|
|
907
|
+
safeEvent._decrypt_error = 'sender IK pending retry failed';
|
|
908
|
+
safeEvent._decrypt_stage = 'sender_ik_pending_retry';
|
|
909
|
+
await client._publishAppEvent(eventName, safeEvent, 'pending_retry');
|
|
641
910
|
continue;
|
|
642
911
|
}
|
|
912
|
+
client._v2SenderIKPending.delete(key);
|
|
643
913
|
const seq = Number(entry.msg.seq ?? 0);
|
|
644
914
|
if (entry.groupId) {
|
|
915
|
+
plaintext = normalizeGroupMentionMode(plaintext, entry.msg, entry.msg.envelope_json);
|
|
645
916
|
plaintext.group_id = entry.groupId;
|
|
646
917
|
await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext);
|
|
647
918
|
}
|
|
@@ -709,19 +980,211 @@ export class V2E2EECoordinator {
|
|
|
709
980
|
throw exc;
|
|
710
981
|
}
|
|
711
982
|
}
|
|
983
|
+
historyDecryptFailure(msg, reason) {
|
|
984
|
+
return {
|
|
985
|
+
message_id: String(msg.message_id ?? ''),
|
|
986
|
+
seq: Number(msg.seq ?? 0),
|
|
987
|
+
encrypted: true,
|
|
988
|
+
history_error: { code: 'decrypt_failed', message: reason },
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
|
|
992
|
+
const client = this.client;
|
|
993
|
+
const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
|
|
994
|
+
let plaintext = null;
|
|
995
|
+
if (String(msg.version ?? '') === 'v1') {
|
|
996
|
+
const legacy = isJsonObject(msg.legacy_v1)
|
|
997
|
+
? msg.legacy_v1 : {};
|
|
998
|
+
const payload = legacy.payload;
|
|
999
|
+
const payloadType = isJsonObject(payload)
|
|
1000
|
+
? String(payload.type ?? '').trim() : '';
|
|
1001
|
+
if (payload !== undefined && payload !== null && !['e2ee.encrypted', 'e2ee.group_encrypted'].includes(payloadType)) {
|
|
1002
|
+
plaintext = {
|
|
1003
|
+
message_id: String(msg.message_id ?? ''), from: String(msg.from_aid ?? ''),
|
|
1004
|
+
to: String(legacy.to ?? client._aid ?? ''), seq, type: String(msg.type ?? ''),
|
|
1005
|
+
timestamp: msg.t_server, payload: payload, encrypted: false,
|
|
1006
|
+
};
|
|
1007
|
+
attachGatewayProximity(plaintext, msg);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
plaintext = source === 'inline_push'
|
|
1012
|
+
? await client._decryptV2Message(msg, false, false, false, false)
|
|
1013
|
+
: await client._decryptV2Message(msg, false);
|
|
1014
|
+
}
|
|
1015
|
+
if (!plaintext) {
|
|
1016
|
+
if (history)
|
|
1017
|
+
return this.historyDecryptFailure(msg, 'message could not be decrypted');
|
|
1018
|
+
client._clientLog.warn(`P2P Tail 单条消息解密失败,连续性证明仍保留: seq=${seq}`);
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
if (publish) {
|
|
1022
|
+
const event = client._delivery.p2pAppEventForMessage(plaintext);
|
|
1023
|
+
if (orderedPublish) {
|
|
1024
|
+
await client._delivery.publishOrderedMessage(event.event, client._aid ? `p2p:${client._aid}` : '', seq, event.payload);
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
await client._publishAppEvent(event.event, event.payload, source);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return plaintext;
|
|
1031
|
+
}
|
|
1032
|
+
async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source, orderedPublish = false) {
|
|
1033
|
+
const client = this.client;
|
|
1034
|
+
const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
|
|
1035
|
+
let plaintext = null;
|
|
1036
|
+
if (String(msg.version ?? '') === 'v1') {
|
|
1037
|
+
const payload = msg.payload;
|
|
1038
|
+
if (payload === undefined || payload === null) {
|
|
1039
|
+
if (history)
|
|
1040
|
+
return this.historyDecryptFailure(msg, 'legacy payload is missing');
|
|
1041
|
+
client._clientLog.warn(`Group Tail 缺少 payload 的 V1 行已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
if (isJsonObject(payload)
|
|
1045
|
+
&& ['e2ee.encrypted', 'e2ee.group_encrypted'].includes(String(payload.type ?? ''))) {
|
|
1046
|
+
if (history)
|
|
1047
|
+
return this.historyDecryptFailure(msg, 'unsupported legacy encrypted envelope');
|
|
1048
|
+
client._clientLog.warn(`Group Tail 不支持的旧加密消息已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
plaintext = {
|
|
1052
|
+
message_id: String(msg.message_id ?? ''), from: String(msg.from_aid ?? ''),
|
|
1053
|
+
group_id: groupId, group_aid: groupAid, seq, type: String(msg.type ?? ''),
|
|
1054
|
+
timestamp: msg.t_server, payload: payload, encrypted: false,
|
|
1055
|
+
};
|
|
1056
|
+
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
1057
|
+
attachGatewayProximity(plaintext, msg);
|
|
1058
|
+
}
|
|
1059
|
+
else {
|
|
1060
|
+
plaintext = source === 'inline_push'
|
|
1061
|
+
? await client._decryptV2Message(msg, false, false, false, false)
|
|
1062
|
+
: await client._decryptV2Message(msg, false);
|
|
1063
|
+
if (plaintext) {
|
|
1064
|
+
// Inline Head 已绑定群身份;解密结果若声称群身份,必须与其严格一致。
|
|
1065
|
+
if (source === 'inline_push' && Object.prototype.hasOwnProperty.call(plaintext, 'group_id')
|
|
1066
|
+
&& (typeof plaintext.group_id !== 'string' || !plaintext.group_id || plaintext.group_id !== groupId)) {
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
plaintext.group_id = groupId;
|
|
1070
|
+
plaintext.group_aid = groupAid;
|
|
1071
|
+
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (!plaintext) {
|
|
1075
|
+
if (history)
|
|
1076
|
+
return this.historyDecryptFailure(msg, 'message could not be decrypted');
|
|
1077
|
+
client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
if (publish) {
|
|
1081
|
+
if (orderedPublish) {
|
|
1082
|
+
await client._delivery.publishOrderedMessage('group.message_created', `group:${groupId}`, seq, plaintext);
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
await client._publishAppEvent('group.message_created', plaintext, source);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return plaintext;
|
|
1089
|
+
}
|
|
1090
|
+
async decodeInlineP2PMessage(msg) {
|
|
1091
|
+
return await this.decodeP2PWindowMessage(msg, false, false, 'inline_push');
|
|
1092
|
+
}
|
|
1093
|
+
async decodeInlineGroupMessage(msg, groupId, groupAid) {
|
|
1094
|
+
return await this.decodeGroupWindowMessage(msg, groupId, groupAid, false, false, 'inline_push');
|
|
1095
|
+
}
|
|
1096
|
+
async applyTailFloor(ns, floor) {
|
|
1097
|
+
const client = this.client;
|
|
1098
|
+
if (!ns || floor <= 0 || client._seqTracker.getContiguousSeq(ns) >= floor)
|
|
1099
|
+
return;
|
|
1100
|
+
client._seqTracker.forceContiguousSeq(ns, floor);
|
|
1101
|
+
client._persistSeq(ns);
|
|
1102
|
+
await client._saveSeqTrackerState?.();
|
|
1103
|
+
}
|
|
1104
|
+
async pullV2TailInternal(params, deferPublish = false) {
|
|
1105
|
+
const client = this.client;
|
|
1106
|
+
if (params.window_mode !== 'tail')
|
|
1107
|
+
throw new ValidationError('window_mode must equal tail');
|
|
1108
|
+
if ('ack_up_to_seq' in params || params.force === true)
|
|
1109
|
+
throw new ValidationError('Tail does not accept ack_up_to_seq or force');
|
|
1110
|
+
const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
|
|
1111
|
+
const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
|
|
1112
|
+
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
1113
|
+
const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true });
|
|
1114
|
+
const floor = Number(result.retention_floor_seq ?? 0);
|
|
1115
|
+
const previous = ns ? client._seqTracker.getSyncState(ns) : null;
|
|
1116
|
+
await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
|
|
1117
|
+
const page = validateTailPage(result, afterSeq, limit);
|
|
1118
|
+
if (ns) {
|
|
1119
|
+
client._seqTracker.commitTailWindow(ns, {
|
|
1120
|
+
windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
|
|
1121
|
+
});
|
|
1122
|
+
client._persistSeq(ns);
|
|
1123
|
+
await client._saveSeqTrackerState?.();
|
|
1124
|
+
}
|
|
1125
|
+
const decoded = [];
|
|
1126
|
+
for (const msg of page.messages) {
|
|
1127
|
+
const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
|
|
1128
|
+
if (plaintext)
|
|
1129
|
+
decoded.push(plaintext);
|
|
1130
|
+
}
|
|
1131
|
+
const ack = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
|
|
1132
|
+
if (previous && ack > previous.ack)
|
|
1133
|
+
await this.ackV2(ack);
|
|
1134
|
+
return { ...result, messages: decoded, raw_count: page.messages.length };
|
|
1135
|
+
}
|
|
1136
|
+
async historyV2Internal(params) {
|
|
1137
|
+
const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
|
|
1138
|
+
const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
|
|
1139
|
+
const result = await this.client._callRawV2Rpc('message.history', { before_seq: beforeSeq, limit, _rpc_foreground: true });
|
|
1140
|
+
const messages = validateHistoryPage(result, beforeSeq);
|
|
1141
|
+
const decoded = [];
|
|
1142
|
+
for (const msg of messages) {
|
|
1143
|
+
const plaintext = await this.decodeP2PWindowMessage(msg, false, true, 'history');
|
|
1144
|
+
if (plaintext)
|
|
1145
|
+
decoded.push(plaintext);
|
|
1146
|
+
}
|
|
1147
|
+
return { ...result, messages: decoded };
|
|
1148
|
+
}
|
|
712
1149
|
async pullV2(afterSeq = 0, limit = 50, opts) {
|
|
713
1150
|
const client = this.client;
|
|
714
1151
|
await client._ensureV2SessionReady('message.pull');
|
|
715
1152
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
1153
|
+
if (opts?.windowMode === 'tail') {
|
|
1154
|
+
if (!opts.gateLocked) {
|
|
1155
|
+
const key = `${ns}|tail|after=${afterSeq}|limit=${limit}`;
|
|
1156
|
+
return await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, {
|
|
1157
|
+
...(opts ?? {}),
|
|
1158
|
+
gateLocked: true,
|
|
1159
|
+
}), false);
|
|
1160
|
+
}
|
|
1161
|
+
const result = await this.pullV2TailInternal({ window_mode: 'tail', after_seq: afterSeq, limit });
|
|
1162
|
+
return Array.isArray(result.messages) ? result.messages : [];
|
|
1163
|
+
}
|
|
1164
|
+
if (ns && !opts?.gateLocked) {
|
|
1165
|
+
const key = `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
|
|
1166
|
+
return await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, {
|
|
1167
|
+
...(opts ?? {}),
|
|
1168
|
+
gateLocked: true,
|
|
1169
|
+
}), true);
|
|
1170
|
+
}
|
|
716
1171
|
const decrypted = [];
|
|
717
1172
|
let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
|
|
1173
|
+
let pullGateKey = ns
|
|
1174
|
+
? `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`
|
|
1175
|
+
: '';
|
|
1176
|
+
const deferredServerCursor = ns ? this.pendingForwardCursor(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
|
|
1177
|
+
const deferredForwardAck = ns ? this.pendingForwardAck(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
|
|
718
1178
|
let pageCount = 0;
|
|
719
1179
|
let lastAutoAckSeq = 0;
|
|
720
|
-
let pendingAckSeq =
|
|
721
|
-
const maxPages = 100;
|
|
1180
|
+
let pendingAckSeq = deferredForwardAck;
|
|
1181
|
+
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
722
1182
|
while (pageCount < maxPages) {
|
|
723
1183
|
pageCount += 1;
|
|
724
1184
|
const ackUpToSeq = pendingAckSeq;
|
|
1185
|
+
const pendingInlineGeneration = ackUpToSeq > 0
|
|
1186
|
+
? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null
|
|
1187
|
+
: null;
|
|
725
1188
|
const result = await client._callRawV2Rpc('message.v2.pull', {
|
|
726
1189
|
after_seq: nextAfterSeq,
|
|
727
1190
|
limit,
|
|
@@ -729,23 +1192,38 @@ export class V2E2EECoordinator {
|
|
|
729
1192
|
...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
|
|
730
1193
|
});
|
|
731
1194
|
if (ackUpToSeq > 0) {
|
|
732
|
-
|
|
733
|
-
|
|
1195
|
+
const actualAckSeq = client._delivery?.resolveP2PPullAckSeq?.(result, ackUpToSeq) ?? 0;
|
|
1196
|
+
if (actualAckSeq >= ackUpToSeq) {
|
|
1197
|
+
if (pendingInlineGeneration !== null) {
|
|
1198
|
+
client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
|
|
1199
|
+
}
|
|
1200
|
+
pendingAckSeq = 0;
|
|
1201
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1202
|
+
this.clearForwardAck(ns, ackUpToSeq);
|
|
1203
|
+
}
|
|
1204
|
+
else {
|
|
1205
|
+
client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
|
|
1206
|
+
await this.confirmForwardP2PAck(ackUpToSeq, pendingInlineGeneration ?? undefined);
|
|
1207
|
+
pendingAckSeq = 0;
|
|
1208
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1209
|
+
}
|
|
734
1210
|
}
|
|
735
1211
|
const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
736
|
-
const messages = pageMessages
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1212
|
+
const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
|
|
1213
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
1214
|
+
const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
|
|
1215
|
+
const hasMore = result.has_more === true;
|
|
1216
|
+
const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
|
|
1217
|
+
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_head=${serverHead}, server_ack_seq=${String(result.server_ack_seq ?? '')}`);
|
|
741
1218
|
const seqs = messages
|
|
742
1219
|
.map((msg) => Number(msg.seq ?? 0))
|
|
743
1220
|
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
744
1221
|
const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
|
|
1222
|
+
const pageTrackerSnapshot = ns && typeof client._seqTracker.snapshotNamespace === 'function'
|
|
1223
|
+
? client._seqTracker.snapshotNamespace(ns)
|
|
1224
|
+
: null;
|
|
745
1225
|
const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
|
|
746
|
-
|
|
747
|
-
client._seqTracker.forceContiguousSeq(ns, pageMaxSeq);
|
|
748
|
-
}
|
|
1226
|
+
const deferredKeyFetches = new Map();
|
|
749
1227
|
for (const msg of messages) {
|
|
750
1228
|
const seq = Number(msg.seq ?? 0);
|
|
751
1229
|
if (!Number.isFinite(seq) || seq <= 0)
|
|
@@ -772,7 +1250,7 @@ export class V2E2EECoordinator {
|
|
|
772
1250
|
attachGatewayProximity(v1Msg, msg);
|
|
773
1251
|
const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
|
|
774
1252
|
if (ns)
|
|
775
|
-
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload);
|
|
1253
|
+
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
|
|
776
1254
|
else
|
|
777
1255
|
await client._publishAppEvent(appEvent.event, appEvent.payload);
|
|
778
1256
|
decrypted.push(v1Msg);
|
|
@@ -790,11 +1268,20 @@ export class V2E2EECoordinator {
|
|
|
790
1268
|
if (msgSpkId && client._v2Session && !client._v2Session.isCurrentSPK(msgSpkId)) {
|
|
791
1269
|
client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
|
|
792
1270
|
}
|
|
793
|
-
const
|
|
1271
|
+
const deferStatus = {};
|
|
1272
|
+
const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
|
|
1273
|
+
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
1274
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
1275
|
+
deferredKeyFetches.set(key, {
|
|
1276
|
+
fromAid: deferStatus.fromAid,
|
|
1277
|
+
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
1278
|
+
groupId: deferStatus.groupId ?? '',
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
794
1281
|
if (plaintext === null)
|
|
795
1282
|
continue;
|
|
796
1283
|
if (ns) {
|
|
797
|
-
await client._publishPulledMessage('message.received', ns, seq, plaintext);
|
|
1284
|
+
await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
|
|
798
1285
|
decrypted.push(plaintext);
|
|
799
1286
|
}
|
|
800
1287
|
else {
|
|
@@ -803,26 +1290,60 @@ export class V2E2EECoordinator {
|
|
|
803
1290
|
}
|
|
804
1291
|
}
|
|
805
1292
|
const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
}
|
|
1293
|
+
const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
|
|
1294
|
+
const serverAckSeq = parsedServerAckSeq ?? 0;
|
|
1295
|
+
const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
|
|
814
1296
|
if (ns) {
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1297
|
+
const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
|
|
1298
|
+
let ackSeq = pageContigBefore;
|
|
1299
|
+
let contigAdvanced = false;
|
|
1300
|
+
try {
|
|
1301
|
+
if (commitTarget > pageContigBefore) {
|
|
1302
|
+
client._seqTracker.forceContiguousSeq(ns, commitTarget);
|
|
1303
|
+
if (retentionFloor > pageContigBefore) {
|
|
1304
|
+
client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
|
|
1305
|
+
}
|
|
1306
|
+
if (deferredServerCursor > pageContigBefore && pageCount === 1) {
|
|
1307
|
+
client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
|
|
1308
|
+
}
|
|
1309
|
+
if (seqs.length > 0) {
|
|
1310
|
+
client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1314
|
+
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1315
|
+
if (contigAdvanced) {
|
|
1316
|
+
await client._commitSeqTrackerState(ns);
|
|
1317
|
+
if (pageCount === 1 && deferredServerCursor > 0)
|
|
1318
|
+
this.clearForwardCursor(ns, ackSeq);
|
|
1319
|
+
}
|
|
820
1320
|
}
|
|
821
|
-
|
|
822
|
-
|
|
1321
|
+
catch (exc) {
|
|
1322
|
+
if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
1323
|
+
client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
|
|
1324
|
+
}
|
|
1325
|
+
client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
1326
|
+
throw exc;
|
|
1327
|
+
}
|
|
1328
|
+
if (contigAdvanced)
|
|
1329
|
+
await client._drainOrderedMessages(ns, undefined, false, false);
|
|
1330
|
+
for (const fetch of deferredKeyFetches.values()) {
|
|
1331
|
+
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
1332
|
+
}
|
|
1333
|
+
if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
|
|
1334
|
+
this.recordForwardCursor(ns, serverAckSeq, ackSeq);
|
|
1335
|
+
}
|
|
1336
|
+
// 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
|
|
1337
|
+
// 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
|
|
1338
|
+
const knownServerAckSeq = hasServerAckSeq
|
|
1339
|
+
? serverAckSeq
|
|
1340
|
+
: (pageCount === 1 ? deferredServerCursor : 0);
|
|
1341
|
+
const ackNeeded = ackSeq > 0
|
|
823
1342
|
&& ackSeq > lastAutoAckSeq
|
|
824
|
-
&& (
|
|
1343
|
+
&& ((hasServerAckSeq && ackSeq > serverAckSeq)
|
|
1344
|
+
|| (contigAdvanced && ackSeq > knownServerAckSeq));
|
|
825
1345
|
if (ackNeeded) {
|
|
1346
|
+
this.recordForwardAck(ns, ackSeq);
|
|
826
1347
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
827
1348
|
const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq;
|
|
828
1349
|
if (canContinuePage) {
|
|
@@ -830,18 +1351,30 @@ export class V2E2EECoordinator {
|
|
|
830
1351
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
831
1352
|
}
|
|
832
1353
|
else {
|
|
833
|
-
await this.
|
|
1354
|
+
await this.confirmForwardP2PAck(ackSeq);
|
|
834
1355
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
835
1356
|
}
|
|
836
1357
|
}
|
|
837
1358
|
}
|
|
838
1359
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
839
1360
|
const rawCount = messages.length;
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
1361
|
+
const shouldContinue = shouldContinueForwardPage({
|
|
1362
|
+
rawCount,
|
|
1363
|
+
nextAfterSeq,
|
|
1364
|
+
nextAfter,
|
|
1365
|
+
limit,
|
|
1366
|
+
hasMoreField,
|
|
1367
|
+
hasMore,
|
|
1368
|
+
serverHead,
|
|
1369
|
+
pendingAckSeq,
|
|
1370
|
+
});
|
|
843
1371
|
if (!shouldContinue)
|
|
844
1372
|
break;
|
|
1373
|
+
if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
1374
|
+
const nextKey = `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
|
|
1375
|
+
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
1376
|
+
pullGateKey = nextKey;
|
|
1377
|
+
}
|
|
845
1378
|
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
846
1379
|
if (tailDelayMs > 0)
|
|
847
1380
|
await v2Sleep(tailDelayMs);
|
|
@@ -850,40 +1383,69 @@ export class V2E2EECoordinator {
|
|
|
850
1383
|
if (pageCount >= maxPages) {
|
|
851
1384
|
client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
|
|
852
1385
|
if (pendingAckSeq > 0) {
|
|
853
|
-
await this.
|
|
1386
|
+
await this.confirmForwardP2PAck(pendingAckSeq);
|
|
854
1387
|
pendingAckSeq = 0;
|
|
855
1388
|
}
|
|
856
1389
|
}
|
|
857
1390
|
return decrypted;
|
|
858
1391
|
}
|
|
859
|
-
async
|
|
1392
|
+
async confirmForwardP2PAck(upToSeq, inlineGeneration) {
|
|
1393
|
+
const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
|
|
1394
|
+
try {
|
|
1395
|
+
const result = await this.ackV2ForForward(upToSeq, inlineGeneration);
|
|
1396
|
+
const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
|
|
1397
|
+
if (actualAckSeq < upToSeq) {
|
|
1398
|
+
throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
|
|
1399
|
+
}
|
|
1400
|
+
this.clearForwardAck(ns, upToSeq);
|
|
1401
|
+
}
|
|
1402
|
+
catch (exc) {
|
|
1403
|
+
this.recordForwardAck(ns, upToSeq);
|
|
1404
|
+
throw exc;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
async ackV2(upToSeq, inlineGeneration) {
|
|
1408
|
+
return this.ackV2WithConfirmation(upToSeq, inlineGeneration, false);
|
|
1409
|
+
}
|
|
1410
|
+
async ackV2ForForward(upToSeq, inlineGeneration) {
|
|
1411
|
+
return this.ackV2WithConfirmation(upToSeq, inlineGeneration, true);
|
|
1412
|
+
}
|
|
1413
|
+
async ackV2WithConfirmation(upToSeq, inlineGeneration, requireServerWatermark) {
|
|
860
1414
|
const client = this.client;
|
|
861
1415
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
862
1416
|
let seq = upToSeq ?? (ns ? client._seqTracker.getContiguousSeq(ns) : 0);
|
|
863
|
-
if (seq <= 0)
|
|
1417
|
+
if (typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq <= 0)
|
|
864
1418
|
return { acked: 0 };
|
|
865
1419
|
seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
|
|
1420
|
+
if (!Number.isSafeInteger(seq) || seq <= 0)
|
|
1421
|
+
return { acked: 0 };
|
|
1422
|
+
// ACK 发出前绑定当时的 inline pending;迟到响应不得结算重置后新建的 pending。
|
|
1423
|
+
const ackGeneration = inlineGeneration ?? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null;
|
|
1424
|
+
const v2SessionAtStart = client._v2Session;
|
|
1425
|
+
const v2RuntimeGenerationAtStart = Number(client._v2RuntimeGeneration ?? 0);
|
|
866
1426
|
const raw = await client._callRawV2Rpc('message.v2.ack', { up_to_seq: seq });
|
|
867
1427
|
const result = isJsonObject(raw)
|
|
868
1428
|
? { ...raw }
|
|
869
1429
|
: { result: raw };
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
actualAckSeq = Number(result.cursor ?? 0);
|
|
877
|
-
if (!Number.isFinite(actualAckSeq))
|
|
878
|
-
actualAckSeq = seq;
|
|
1430
|
+
const actualAckSeq = requireServerWatermark
|
|
1431
|
+
? (client._delivery?.resolveForwardAckSeq?.(result, seq) ?? 0)
|
|
1432
|
+
: (client._delivery?.resolveActualAckSeq?.(result, seq) ?? seq);
|
|
1433
|
+
if (requireServerWatermark && actualAckSeq < seq) {
|
|
1434
|
+
throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
|
|
1435
|
+
}
|
|
879
1436
|
result.ack_seq = actualAckSeq;
|
|
880
1437
|
result.success = true;
|
|
881
|
-
if (
|
|
882
|
-
|
|
883
|
-
|
|
1438
|
+
if (ackGeneration !== null) {
|
|
1439
|
+
client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, ackGeneration);
|
|
1440
|
+
}
|
|
1441
|
+
if (v2SessionAtStart
|
|
1442
|
+
&& client._v2Session === v2SessionAtStart
|
|
1443
|
+
&& Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart) {
|
|
884
1444
|
try {
|
|
885
|
-
const destroyed = await
|
|
886
|
-
if (
|
|
1445
|
+
const destroyed = await v2SessionAtStart.maybeDestroyOldSPKs(actualAckSeq);
|
|
1446
|
+
if (client._v2Session === v2SessionAtStart
|
|
1447
|
+
&& Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart
|
|
1448
|
+
&& destroyed.length > 0) {
|
|
887
1449
|
client._clientLog.info(`V2 destroyed old SPKs after ack: ${destroyed.slice(0, 3)} (PFS)`);
|
|
888
1450
|
}
|
|
889
1451
|
}
|
|
@@ -896,9 +1458,10 @@ export class V2E2EECoordinator {
|
|
|
896
1458
|
async sendGroupV2(groupId, payload, opts) {
|
|
897
1459
|
const client = this.client;
|
|
898
1460
|
await client._ensureV2SessionReady('group.send', 'V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
|
|
899
|
-
const gid =
|
|
1461
|
+
const gid = String(groupId ?? '').trim();
|
|
900
1462
|
if (!gid)
|
|
901
1463
|
throw new ValidationError("group.send requires 'group_id'");
|
|
1464
|
+
const groupAid = String(opts?.groupAid ?? '').trim();
|
|
902
1465
|
if (!isJsonObject(payload))
|
|
903
1466
|
throw new ValidationError('group.send payload must be a dict for V2 encryption');
|
|
904
1467
|
const attempt = async (useCache) => {
|
|
@@ -911,10 +1474,10 @@ export class V2E2EECoordinator {
|
|
|
911
1474
|
context: opts?.context,
|
|
912
1475
|
useCache,
|
|
913
1476
|
});
|
|
914
|
-
return client.call('group.v2.send', {
|
|
1477
|
+
return client.call('group.v2.send', withExplicitGroupAid({
|
|
915
1478
|
group_id: gid,
|
|
916
1479
|
envelope,
|
|
917
|
-
});
|
|
1480
|
+
}, groupAid));
|
|
918
1481
|
};
|
|
919
1482
|
try {
|
|
920
1483
|
const result = await attempt(true);
|
|
@@ -927,14 +1490,14 @@ export class V2E2EECoordinator {
|
|
|
927
1490
|
await client._saveSeqTrackerState();
|
|
928
1491
|
}
|
|
929
1492
|
}
|
|
930
|
-
return client._delivery.attachSendResultEnvelope('group.send', {
|
|
1493
|
+
return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
|
|
931
1494
|
group_id: gid,
|
|
932
1495
|
payload,
|
|
933
1496
|
message_id: opts?.messageId,
|
|
934
1497
|
timestamp: opts?.timestamp,
|
|
935
1498
|
protected_headers: opts?.protectedHeaders,
|
|
936
1499
|
context: opts?.context,
|
|
937
|
-
}, result, true);
|
|
1500
|
+
}, groupAid), result, true);
|
|
938
1501
|
}
|
|
939
1502
|
catch (exc) {
|
|
940
1503
|
const excCode = exc?.code;
|
|
@@ -951,65 +1514,184 @@ export class V2E2EECoordinator {
|
|
|
951
1514
|
await client._saveSeqTrackerState();
|
|
952
1515
|
}
|
|
953
1516
|
}
|
|
954
|
-
return client._delivery.attachSendResultEnvelope('group.send', {
|
|
1517
|
+
return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
|
|
955
1518
|
group_id: gid,
|
|
956
1519
|
payload,
|
|
957
1520
|
message_id: opts?.messageId,
|
|
958
1521
|
timestamp: opts?.timestamp,
|
|
959
1522
|
protected_headers: opts?.protectedHeaders,
|
|
960
1523
|
context: opts?.context,
|
|
961
|
-
}, result, true);
|
|
1524
|
+
}, groupAid), result, true);
|
|
962
1525
|
}
|
|
963
1526
|
throw exc;
|
|
964
1527
|
}
|
|
965
1528
|
}
|
|
1529
|
+
async pullGroupV2TailInternal(params, deferPublish = false) {
|
|
1530
|
+
const client = this.client;
|
|
1531
|
+
if (params.window_mode !== 'tail')
|
|
1532
|
+
throw new ValidationError('window_mode must equal tail');
|
|
1533
|
+
if ('ack_up_to_seq' in params || params.force === true)
|
|
1534
|
+
throw new ValidationError('Tail does not accept ack_up_to_seq or force');
|
|
1535
|
+
const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
|
|
1536
|
+
if (!groupId)
|
|
1537
|
+
throw new ValidationError('group_id is required');
|
|
1538
|
+
let groupAid = String(params.group_aid ?? '').trim();
|
|
1539
|
+
const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
|
|
1540
|
+
const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
|
|
1541
|
+
const ns = `group:${groupId}`;
|
|
1542
|
+
const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
|
|
1543
|
+
group_id: groupId, window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true,
|
|
1544
|
+
}, groupAid));
|
|
1545
|
+
const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
|
|
1546
|
+
if (resultAid)
|
|
1547
|
+
groupAid = resultAid;
|
|
1548
|
+
const floor = Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0);
|
|
1549
|
+
const previous = client._seqTracker.getSyncState(ns);
|
|
1550
|
+
await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
|
|
1551
|
+
const page = validateTailPage(result, afterSeq, limit);
|
|
1552
|
+
client._seqTracker.commitTailWindow(ns, {
|
|
1553
|
+
windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
|
|
1554
|
+
});
|
|
1555
|
+
client._persistSeq(ns);
|
|
1556
|
+
await client._saveSeqTrackerState?.();
|
|
1557
|
+
const decoded = [];
|
|
1558
|
+
for (const msg of page.messages) {
|
|
1559
|
+
client._logMessageDebug?.('tail-raw', 'group.v2.pull', 'group.message_created', msg);
|
|
1560
|
+
const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
|
|
1561
|
+
if (plaintext)
|
|
1562
|
+
decoded.push(plaintext);
|
|
1563
|
+
}
|
|
1564
|
+
const ack = client._seqTracker.getContiguousSeq(ns);
|
|
1565
|
+
if (ack > previous.ack)
|
|
1566
|
+
await this.ackGroupV2(groupId, ack, groupAid);
|
|
1567
|
+
return { ...result, messages: decoded, raw_count: page.messages.length };
|
|
1568
|
+
}
|
|
1569
|
+
async historyGroupV2Internal(params) {
|
|
1570
|
+
const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
|
|
1571
|
+
if (!groupId)
|
|
1572
|
+
throw new ValidationError('group_id is required');
|
|
1573
|
+
let groupAid = String(params.group_aid ?? '').trim();
|
|
1574
|
+
const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
|
|
1575
|
+
const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
|
|
1576
|
+
const result = await this.client._callRawV2Rpc('group.history', withExplicitGroupAid({
|
|
1577
|
+
group_id: groupId, before_seq: beforeSeq, limit, _rpc_foreground: true,
|
|
1578
|
+
}, groupAid));
|
|
1579
|
+
const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
|
|
1580
|
+
if (resultAid)
|
|
1581
|
+
groupAid = resultAid;
|
|
1582
|
+
const messages = validateHistoryPage(result, beforeSeq);
|
|
1583
|
+
const decoded = [];
|
|
1584
|
+
for (const msg of messages) {
|
|
1585
|
+
const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, false, true, 'history');
|
|
1586
|
+
if (plaintext)
|
|
1587
|
+
decoded.push(plaintext);
|
|
1588
|
+
}
|
|
1589
|
+
return { ...result, messages: decoded };
|
|
1590
|
+
}
|
|
966
1591
|
async pullGroupV2Internal(params) {
|
|
967
|
-
await this.pullGroupV2(params.group_id, params.after_seq, params.limit
|
|
1592
|
+
await this.pullGroupV2(params.group_id, params.after_seq, params.limit, {
|
|
1593
|
+
gateLocked: true,
|
|
1594
|
+
explicitAfterSeq: true,
|
|
1595
|
+
force: params.force === true,
|
|
1596
|
+
});
|
|
968
1597
|
}
|
|
969
1598
|
async pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {
|
|
970
1599
|
const client = this.client;
|
|
971
1600
|
await client._ensureV2SessionReady('group.pull');
|
|
972
|
-
const gid =
|
|
1601
|
+
const gid = String(groupId ?? '').trim();
|
|
973
1602
|
if (!gid)
|
|
974
1603
|
throw new ValidationError('group.pull requires group_id');
|
|
1604
|
+
let groupAid = String(opts?.groupAid ?? '').trim();
|
|
975
1605
|
const ns = `group:${gid}`;
|
|
1606
|
+
if (opts?.windowMode === 'tail') {
|
|
1607
|
+
if (!opts.gateLocked) {
|
|
1608
|
+
const key = `${ns}|tail|after=${afterSeq}|limit=${limit}`;
|
|
1609
|
+
return await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
|
|
1610
|
+
...(opts ?? {}),
|
|
1611
|
+
gateLocked: true,
|
|
1612
|
+
}), false);
|
|
1613
|
+
}
|
|
1614
|
+
const result = await this.pullGroupV2TailInternal({
|
|
1615
|
+
group_id: String(opts.wireGroupId ?? gid), group_aid: groupAid || undefined,
|
|
1616
|
+
window_mode: 'tail', after_seq: afterSeq, limit,
|
|
1617
|
+
});
|
|
1618
|
+
return Array.isArray(result.messages) ? result.messages : [];
|
|
1619
|
+
}
|
|
1620
|
+
if (!opts?.gateLocked) {
|
|
1621
|
+
const key = `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
|
|
1622
|
+
return await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
|
|
1623
|
+
...(opts ?? {}),
|
|
1624
|
+
gateLocked: true,
|
|
1625
|
+
}), true);
|
|
1626
|
+
}
|
|
976
1627
|
const decrypted = [];
|
|
977
1628
|
const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
|
|
978
1629
|
const cursorParams = opts?.cursorParams ?? {};
|
|
979
1630
|
const ownsCursor = opts?.ownsCursor !== false;
|
|
980
|
-
let
|
|
1631
|
+
let pullGateKey = ownsCursor
|
|
1632
|
+
? `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`
|
|
1633
|
+
: '';
|
|
1634
|
+
let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
|
|
1635
|
+
? afterSeq
|
|
1636
|
+
: (afterSeq || client._seqTracker.getContiguousSeq(ns));
|
|
1637
|
+
const deferredServerCursor = ownsCursor
|
|
1638
|
+
? this.pendingForwardCursor(ns, client._seqTracker.getContiguousSeq(ns))
|
|
1639
|
+
: 0;
|
|
1640
|
+
const deferredForwardAck = ownsCursor ? this.pendingForwardAck(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
|
|
981
1641
|
let pageCount = 0;
|
|
982
1642
|
let lastAutoAckSeq = 0;
|
|
983
|
-
let pendingAckSeq =
|
|
984
|
-
const maxPages = 100;
|
|
1643
|
+
let pendingAckSeq = deferredForwardAck;
|
|
1644
|
+
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
985
1645
|
while (pageCount < maxPages) {
|
|
986
1646
|
pageCount += 1;
|
|
987
1647
|
const ackUpToSeq = ownsCursor ? pendingAckSeq : 0;
|
|
988
|
-
const
|
|
1648
|
+
const pendingInlineGeneration = ackUpToSeq > 0
|
|
1649
|
+
? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null
|
|
1650
|
+
: null;
|
|
1651
|
+
const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
|
|
989
1652
|
group_id: wireGroupId,
|
|
990
|
-
group_aid: gid,
|
|
991
1653
|
after_seq: nextAfterSeq,
|
|
992
1654
|
limit,
|
|
993
1655
|
...cursorParams,
|
|
1656
|
+
...(opts?.force ? { force: true } : {}),
|
|
994
1657
|
...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
|
|
995
|
-
});
|
|
1658
|
+
}, groupAid));
|
|
996
1659
|
if (ackUpToSeq > 0) {
|
|
997
|
-
|
|
998
|
-
|
|
1660
|
+
const actualAckSeq = client._delivery?.resolveGroupPullAckSeq?.(result, ackUpToSeq) ?? 0;
|
|
1661
|
+
if (actualAckSeq >= ackUpToSeq) {
|
|
1662
|
+
if (pendingInlineGeneration !== null) {
|
|
1663
|
+
client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
|
|
1664
|
+
}
|
|
1665
|
+
pendingAckSeq = 0;
|
|
1666
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1667
|
+
this.clearForwardAck(ns, ackUpToSeq);
|
|
1668
|
+
}
|
|
1669
|
+
else {
|
|
1670
|
+
client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
|
|
1671
|
+
await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
|
|
1672
|
+
pendingAckSeq = 0;
|
|
1673
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1674
|
+
}
|
|
999
1675
|
}
|
|
1676
|
+
const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
|
|
1677
|
+
if (resultGroupAid)
|
|
1678
|
+
groupAid = resultGroupAid;
|
|
1679
|
+
const eventGroupAid = resultGroupAid || groupAid || gid;
|
|
1000
1680
|
const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
1001
|
-
const messages = pageMessages
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1681
|
+
const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
|
|
1682
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
1683
|
+
const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
|
|
1684
|
+
const hasMore = result.has_more === true;
|
|
1685
|
+
const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
|
|
1005
1686
|
const seqs = messages
|
|
1006
1687
|
.map((msg) => Number(msg.seq ?? 0))
|
|
1007
1688
|
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
1008
1689
|
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
1690
|
+
const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
|
|
1691
|
+
? client._seqTracker.snapshotNamespace(ns)
|
|
1692
|
+
: null;
|
|
1009
1693
|
const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
|
|
1010
|
-
|
|
1011
|
-
client._seqTracker.forceContiguousSeq(ns, pageMaxSeq);
|
|
1012
|
-
}
|
|
1694
|
+
const deferredKeyFetches = new Map();
|
|
1013
1695
|
for (const msg of messages) {
|
|
1014
1696
|
const seq = Number(msg.seq ?? 0);
|
|
1015
1697
|
if (!Number.isFinite(seq) || seq <= 0)
|
|
@@ -1020,7 +1702,11 @@ export class V2E2EECoordinator {
|
|
|
1020
1702
|
const payloadObj = isJsonObject(payload) ? payload : null;
|
|
1021
1703
|
// 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
|
|
1022
1704
|
if (client._delivery.recallEventFromGroupMessage(msg)) {
|
|
1023
|
-
await client._delivery.publishGroupRecallTombstone(gid, seq,
|
|
1705
|
+
await client._delivery.publishGroupRecallTombstone(gid, seq, {
|
|
1706
|
+
...msg,
|
|
1707
|
+
group_id: gid,
|
|
1708
|
+
group_aid: eventGroupAid,
|
|
1709
|
+
});
|
|
1024
1710
|
client._markPublishedSeq(ns, seq);
|
|
1025
1711
|
client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
|
|
1026
1712
|
continue;
|
|
@@ -1028,35 +1714,39 @@ export class V2E2EECoordinator {
|
|
|
1028
1714
|
if (payloadObj) {
|
|
1029
1715
|
const payloadType = String(payloadObj.type ?? '').trim();
|
|
1030
1716
|
if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted') {
|
|
1031
|
-
|
|
1717
|
+
let v1Msg = {
|
|
1032
1718
|
message_id: String(msg.message_id ?? ''),
|
|
1033
1719
|
from: String(msg.from_aid ?? ''),
|
|
1034
1720
|
group_id: gid,
|
|
1721
|
+
group_aid: eventGroupAid,
|
|
1035
1722
|
seq: msg.seq,
|
|
1036
1723
|
type: String(msg.type ?? ''),
|
|
1037
1724
|
timestamp: msg.t_server,
|
|
1038
1725
|
payload,
|
|
1039
1726
|
encrypted: false,
|
|
1040
1727
|
};
|
|
1728
|
+
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
1041
1729
|
attachGatewayProximity(v1Msg, msg);
|
|
1042
|
-
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
|
|
1730
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
1043
1731
|
decrypted.push(v1Msg);
|
|
1044
1732
|
continue;
|
|
1045
1733
|
}
|
|
1046
1734
|
}
|
|
1047
1735
|
else if (payload !== undefined && payload !== null) {
|
|
1048
|
-
|
|
1736
|
+
let v1Msg = {
|
|
1049
1737
|
message_id: String(msg.message_id ?? ''),
|
|
1050
1738
|
from: String(msg.from_aid ?? ''),
|
|
1051
1739
|
group_id: gid,
|
|
1740
|
+
group_aid: eventGroupAid,
|
|
1052
1741
|
seq: msg.seq,
|
|
1053
1742
|
type: String(msg.type ?? ''),
|
|
1054
1743
|
timestamp: msg.t_server,
|
|
1055
1744
|
payload,
|
|
1056
1745
|
encrypted: false,
|
|
1057
1746
|
};
|
|
1747
|
+
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
1058
1748
|
attachGatewayProximity(v1Msg, msg);
|
|
1059
|
-
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
|
|
1749
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
1060
1750
|
decrypted.push(v1Msg);
|
|
1061
1751
|
continue;
|
|
1062
1752
|
}
|
|
@@ -1067,35 +1757,83 @@ export class V2E2EECoordinator {
|
|
|
1067
1757
|
client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
|
|
1068
1758
|
continue;
|
|
1069
1759
|
}
|
|
1070
|
-
const
|
|
1760
|
+
const deferStatus = {};
|
|
1761
|
+
let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
|
|
1762
|
+
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
1763
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
1764
|
+
deferredKeyFetches.set(key, {
|
|
1765
|
+
fromAid: deferStatus.fromAid,
|
|
1766
|
+
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
1767
|
+
groupId: deferStatus.groupId ?? '',
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1071
1770
|
if (plaintext === null)
|
|
1072
1771
|
continue;
|
|
1772
|
+
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
1073
1773
|
plaintext.group_id = gid;
|
|
1074
|
-
|
|
1774
|
+
plaintext.group_aid = eventGroupAid;
|
|
1775
|
+
await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
|
|
1075
1776
|
decrypted.push(plaintext);
|
|
1076
1777
|
}
|
|
1077
|
-
const
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1778
|
+
const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
|
|
1779
|
+
const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
|
|
1780
|
+
const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
|
|
1781
|
+
const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
|
|
1782
|
+
const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
|
|
1783
|
+
const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
|
|
1784
|
+
const commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
|
|
1785
|
+
let ackSeq = pageContigBefore;
|
|
1786
|
+
let contigAdvanced = false;
|
|
1787
|
+
if (ownsCursor) {
|
|
1788
|
+
try {
|
|
1789
|
+
if (commitTarget > pageContigBefore) {
|
|
1790
|
+
client._seqTracker.forceContiguousSeq(ns, commitTarget);
|
|
1791
|
+
if (effectiveFloor > pageContigBefore) {
|
|
1792
|
+
client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
|
|
1793
|
+
}
|
|
1794
|
+
if (deferredServerCursor > pageContigBefore && pageCount === 1) {
|
|
1795
|
+
client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
|
|
1796
|
+
}
|
|
1797
|
+
if (seqs.length > 0) {
|
|
1798
|
+
client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1802
|
+
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1803
|
+
if (contigAdvanced) {
|
|
1804
|
+
await client._commitSeqTrackerState(ns);
|
|
1805
|
+
if (pageCount === 1 && deferredServerCursor > 0)
|
|
1806
|
+
this.clearForwardCursor(ns, ackSeq);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
catch (exc) {
|
|
1810
|
+
if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
1811
|
+
client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
|
|
1812
|
+
}
|
|
1813
|
+
client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
1814
|
+
throw exc;
|
|
1084
1815
|
}
|
|
1816
|
+
if (contigAdvanced)
|
|
1817
|
+
await client._drainOrderedMessages(ns, undefined, false, false);
|
|
1085
1818
|
}
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
if (contigAdvanced) {
|
|
1089
|
-
await client._drainOrderedMessages(ns);
|
|
1090
|
-
client._persistSeq(ns);
|
|
1819
|
+
for (const fetch of deferredKeyFetches.values()) {
|
|
1820
|
+
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
1091
1821
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1822
|
+
if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
|
|
1823
|
+
this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
|
|
1824
|
+
}
|
|
1825
|
+
// 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
|
|
1826
|
+
// 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
|
|
1827
|
+
const knownServerCursorSeq = hasServerCursor
|
|
1828
|
+
? cursorCurrentSeq
|
|
1829
|
+
: (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
|
|
1830
|
+
const ackNeeded = ackSeq > 0
|
|
1095
1831
|
&& ackSeq > lastAutoAckSeq
|
|
1096
1832
|
&& ownsCursor
|
|
1097
|
-
&& (
|
|
1833
|
+
&& ((hasServerCursor && ackSeq > cursorCurrentSeq)
|
|
1834
|
+
|| (contigAdvanced && ackSeq > knownServerCursorSeq));
|
|
1098
1835
|
if (ackNeeded) {
|
|
1836
|
+
this.recordForwardAck(ns, ackSeq);
|
|
1099
1837
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
1100
1838
|
const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
|
|
1101
1839
|
if (canContinuePage) {
|
|
@@ -1103,7 +1841,7 @@ export class V2E2EECoordinator {
|
|
|
1103
1841
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1104
1842
|
}
|
|
1105
1843
|
else {
|
|
1106
|
-
await this.
|
|
1844
|
+
await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
|
|
1107
1845
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1108
1846
|
}
|
|
1109
1847
|
}
|
|
@@ -1111,11 +1849,23 @@ export class V2E2EECoordinator {
|
|
|
1111
1849
|
if (!ownsCursor)
|
|
1112
1850
|
break;
|
|
1113
1851
|
const rawCount = messages.length;
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1852
|
+
const shouldContinue = shouldContinueForwardPage({
|
|
1853
|
+
rawCount,
|
|
1854
|
+
nextAfterSeq,
|
|
1855
|
+
nextAfter,
|
|
1856
|
+
limit,
|
|
1857
|
+
hasMoreField,
|
|
1858
|
+
hasMore,
|
|
1859
|
+
serverHead,
|
|
1860
|
+
pendingAckSeq,
|
|
1861
|
+
});
|
|
1117
1862
|
if (!shouldContinue)
|
|
1118
1863
|
break;
|
|
1864
|
+
if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
1865
|
+
const nextKey = `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
|
|
1866
|
+
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
1867
|
+
pullGateKey = nextKey;
|
|
1868
|
+
}
|
|
1119
1869
|
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
1120
1870
|
if (tailDelayMs > 0)
|
|
1121
1871
|
await v2Sleep(tailDelayMs);
|
|
@@ -1124,23 +1874,66 @@ export class V2E2EECoordinator {
|
|
|
1124
1874
|
if (pageCount >= maxPages) {
|
|
1125
1875
|
client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
|
|
1126
1876
|
if (pendingAckSeq > 0) {
|
|
1127
|
-
await this.
|
|
1877
|
+
await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
|
|
1128
1878
|
pendingAckSeq = 0;
|
|
1129
1879
|
}
|
|
1130
1880
|
}
|
|
1131
1881
|
return decrypted;
|
|
1132
1882
|
}
|
|
1133
|
-
async
|
|
1883
|
+
async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
|
|
1884
|
+
const ns = `group:${String(groupId ?? '').trim()}`;
|
|
1885
|
+
try {
|
|
1886
|
+
const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid);
|
|
1887
|
+
const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
|
|
1888
|
+
if (actualAckSeq < upToSeq) {
|
|
1889
|
+
throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
|
|
1890
|
+
}
|
|
1891
|
+
this.clearForwardAck(ns, upToSeq);
|
|
1892
|
+
}
|
|
1893
|
+
catch (exc) {
|
|
1894
|
+
this.recordForwardAck(ns, upToSeq);
|
|
1895
|
+
throw exc;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
async ackGroupV2(groupId, upToSeq, groupAid, inlineGeneration) {
|
|
1899
|
+
return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, false);
|
|
1900
|
+
}
|
|
1901
|
+
async ackGroupV2ForForward(groupId, upToSeq, groupAid) {
|
|
1902
|
+
return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, undefined, true);
|
|
1903
|
+
}
|
|
1904
|
+
async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, requireServerWatermark) {
|
|
1134
1905
|
const client = this.client;
|
|
1135
|
-
const gid =
|
|
1906
|
+
const gid = typeof groupId === 'string' ? groupId.trim() : '';
|
|
1136
1907
|
if (!gid)
|
|
1137
1908
|
throw new ValidationError('group.ack_messages requires group_id');
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1909
|
+
if (groupAid !== undefined && typeof groupAid !== 'string') {
|
|
1910
|
+
throw new ValidationError('group.ack_messages group_aid must be a string');
|
|
1911
|
+
}
|
|
1912
|
+
const explicitGroupAid = groupAid?.trim() ?? '';
|
|
1913
|
+
const wireNs = `group:${gid}`;
|
|
1914
|
+
const ns = client._delivery?.resolvePendingGroupInlineAckNamespace?.(wireNs) ?? wireNs;
|
|
1915
|
+
let seq = upToSeq ?? client._seqTracker.getContiguousSeq(ns);
|
|
1916
|
+
if (typeof upToSeq !== 'undefined' && (typeof upToSeq !== 'number' || !Number.isSafeInteger(upToSeq))) {
|
|
1917
|
+
return { acked: 0 };
|
|
1918
|
+
}
|
|
1919
|
+
if (!Number.isSafeInteger(seq) || seq <= 0)
|
|
1141
1920
|
return { acked: 0 };
|
|
1142
1921
|
seq = client._clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
|
|
1143
|
-
|
|
1922
|
+
if (!Number.isSafeInteger(seq) || seq <= 0)
|
|
1923
|
+
return { acked: 0 };
|
|
1924
|
+
// 与 P2P 一致,必须将 ACK 响应限定到请求开始时对应的 inline generation。
|
|
1925
|
+
const ackGeneration = inlineGeneration ?? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null;
|
|
1926
|
+
const raw = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq }, explicitGroupAid));
|
|
1927
|
+
const actualAckSeq = requireServerWatermark
|
|
1928
|
+
? (client._delivery?.resolveForwardAckSeq?.(raw, seq) ?? 0)
|
|
1929
|
+
: (client._delivery?.resolveActualAckSeq?.(raw, seq) ?? seq);
|
|
1930
|
+
if (requireServerWatermark && actualAckSeq < seq) {
|
|
1931
|
+
throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
|
|
1932
|
+
}
|
|
1933
|
+
if (ackGeneration !== null) {
|
|
1934
|
+
client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, ackGeneration);
|
|
1935
|
+
}
|
|
1936
|
+
return raw;
|
|
1144
1937
|
}
|
|
1145
1938
|
async putMessageThoughtEncryptedV2(params) {
|
|
1146
1939
|
const client = this.client;
|
|
@@ -1318,7 +2111,7 @@ export class V2E2EECoordinator {
|
|
|
1318
2111
|
let e2ee;
|
|
1319
2112
|
if (payload?.type === 'e2ee.group_encrypted' && String(payload.version ?? '') === 'v2') {
|
|
1320
2113
|
e2ee = v2E2eeMeta(payload);
|
|
1321
|
-
const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid });
|
|
2114
|
+
const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid, fetchSenderIK: true });
|
|
1322
2115
|
if (plain === null) {
|
|
1323
2116
|
decryptFailed = true;
|
|
1324
2117
|
client._clientLog.debug(`group.thought.get decrypt returned null: group=${groupId}, thought_id=${thoughtId}, from=${fromAid}`);
|
|
@@ -1388,7 +2181,7 @@ export class V2E2EECoordinator {
|
|
|
1388
2181
|
let e2ee;
|
|
1389
2182
|
if (payload?.type === 'e2ee.p2p_encrypted' && String(payload.version ?? '') === 'v2') {
|
|
1390
2183
|
e2ee = v2E2eeMeta(payload);
|
|
1391
|
-
const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid });
|
|
2184
|
+
const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid, fetchSenderIK: true });
|
|
1392
2185
|
if (plain === null) {
|
|
1393
2186
|
decryptFailed = true;
|
|
1394
2187
|
client._clientLog.debug(`message.thought.get decrypt returned null: thought_id=${thoughtId}, from=${fromAid}, to=${toAid}`);
|
|
@@ -1475,7 +2268,10 @@ export class V2E2EECoordinator {
|
|
|
1475
2268
|
const fromAid = String(opts.fromAid || aad.from || '').trim();
|
|
1476
2269
|
const senderDeviceId = String(aad.from_device ?? '');
|
|
1477
2270
|
const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
1478
|
-
|
|
2271
|
+
let senderPubDer = await this.getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
|
|
2272
|
+
if (!senderPubDer && opts.fetchSenderIK) {
|
|
2273
|
+
senderPubDer = await this.fetchSenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
|
|
2274
|
+
}
|
|
1479
2275
|
if (!senderPubDer) {
|
|
1480
2276
|
client._clientLog.warn(`V2 thought decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
|
|
1481
2277
|
this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupIdForKeys);
|
|
@@ -1543,7 +2339,7 @@ export class V2E2EECoordinator {
|
|
|
1543
2339
|
return String(envelope.version ?? '').trim().toLowerCase() === 'v2' && payloadType.startsWith('e2ee.');
|
|
1544
2340
|
}
|
|
1545
2341
|
safeUndecryptablePushEvent(msg, group) {
|
|
1546
|
-
|
|
2342
|
+
let event = {
|
|
1547
2343
|
message_id: msg.message_id ?? null,
|
|
1548
2344
|
from: msg.from ?? null,
|
|
1549
2345
|
seq: msg.seq ?? null,
|
|
@@ -1567,6 +2363,10 @@ export class V2E2EECoordinator {
|
|
|
1567
2363
|
attachV2EnvelopeMetadata(event, v2E2eeMeta(envelope));
|
|
1568
2364
|
}
|
|
1569
2365
|
}
|
|
2366
|
+
if (group) {
|
|
2367
|
+
const modeSource = envelope ? { ...msg, envelope } : msg;
|
|
2368
|
+
event = normalizeGroupMentionMode(event, modeSource);
|
|
2369
|
+
}
|
|
1570
2370
|
return event;
|
|
1571
2371
|
}
|
|
1572
2372
|
async decryptEncryptedPushPayload(msg, group) {
|
|
@@ -1580,7 +2380,7 @@ export class V2E2EECoordinator {
|
|
|
1580
2380
|
if (!plaintext)
|
|
1581
2381
|
return null;
|
|
1582
2382
|
const e2eeMeta = v2E2eeMeta(envelope);
|
|
1583
|
-
|
|
2383
|
+
let result = {
|
|
1584
2384
|
message_id: String(msg.message_id ?? ''),
|
|
1585
2385
|
from: fromAid,
|
|
1586
2386
|
seq: msg.seq ?? null,
|
|
@@ -1599,6 +2399,7 @@ export class V2E2EECoordinator {
|
|
|
1599
2399
|
attachGatewayProximity(result, msg);
|
|
1600
2400
|
if (group) {
|
|
1601
2401
|
result.group_id = msg.group_id ?? aad.group_id ?? envelope.group_id ?? null;
|
|
2402
|
+
result = normalizeGroupMentionMode(result, msg, envelope);
|
|
1602
2403
|
}
|
|
1603
2404
|
else {
|
|
1604
2405
|
result.to = msg.to ?? client._aid ?? '';
|
|
@@ -1659,6 +2460,7 @@ export class V2E2EECoordinator {
|
|
|
1659
2460
|
peerDevices = (Array.isArray(bs?.peer_devices) ? bs.peer_devices : []);
|
|
1660
2461
|
auditRaw = (Array.isArray(bs?.audit_recipients) ? bs.audit_recipients : []);
|
|
1661
2462
|
wrapPolicy = normalizeV2WrapPolicy(bs?.e2ee_wrap_policy);
|
|
2463
|
+
this.cacheBundledSelfDevices(bs, wrapPolicy);
|
|
1662
2464
|
if (peerDevices.length > 0) {
|
|
1663
2465
|
this.setBootstrapCacheEntry(to, {
|
|
1664
2466
|
devices: peerDevices,
|
|
@@ -1758,8 +2560,7 @@ export class V2E2EECoordinator {
|
|
|
1758
2560
|
throw new StateError('V2 session not initialized');
|
|
1759
2561
|
}
|
|
1760
2562
|
const session = client._v2Session;
|
|
1761
|
-
const
|
|
1762
|
-
const groupId = normalizeGroupId(rawGroupId) || rawGroupId;
|
|
2563
|
+
const groupId = String(opts.groupId ?? '').trim();
|
|
1763
2564
|
if (!groupId)
|
|
1764
2565
|
throw new ValidationError("group.send requires 'group_id'");
|
|
1765
2566
|
const useCache = opts.useCache !== false;
|
|
@@ -1770,15 +2571,7 @@ export class V2E2EECoordinator {
|
|
|
1770
2571
|
let auditRecipientsRaw = [];
|
|
1771
2572
|
let wrapPolicy;
|
|
1772
2573
|
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
|
-
}
|
|
2574
|
+
const cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : undefined;
|
|
1782
2575
|
if (cached && (Date.now() - Number(cached.cachedAt ?? 0)) < V2_BOOTSTRAP_TTL_MS) {
|
|
1783
2576
|
allDevices = (cached.devices ?? []);
|
|
1784
2577
|
epoch = Number(cached.epoch ?? 0) || 0;
|