@agentunion/fastaun 0.4.6 → 0.4.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 +38 -0
- package/_packed_docs/CHANGELOG.md +38 -0
- package/_packed_docs/INDEX.md +46 -22
- package/_packed_docs/KITE_DOCS_GUIDE.md +16 -12
- package/_packed_docs/design/AUNClient/346/213/206/345/210/206/351/207/215/346/236/204/346/211/247/350/241/214/346/226/271/346/241/210.md +859 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +5 -3
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +5 -5
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +4 -2
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +1 -1
- package/dist/agent-md.d.ts +1 -1
- package/dist/agent-md.js +12 -5
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +6 -1
- package/dist/aid-store.js +93 -14
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +1 -0
- package/dist/aid.js +8 -3
- package/dist/aid.js.map +1 -1
- package/dist/cert-utils.d.ts +5 -1
- package/dist/cert-utils.js +47 -9
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +50 -0
- package/dist/client/delivery.js +1147 -0
- package/dist/client/delivery.js.map +1 -0
- package/dist/client/group-state.d.ts +31 -0
- package/dist/client/group-state.js +853 -0
- package/dist/client/group-state.js.map +1 -0
- package/dist/client/identity.d.ts +7 -0
- package/dist/client/identity.js +35 -0
- package/dist/client/identity.js.map +1 -0
- package/dist/client/lifecycle.d.ts +9 -0
- package/dist/client/lifecycle.js +226 -0
- package/dist/client/lifecycle.js.map +1 -0
- package/dist/client/peers.d.ts +10 -0
- package/dist/client/peers.js +42 -0
- package/dist/client/peers.js.map +1 -0
- package/dist/client/rpc-pipeline.d.ts +36 -0
- package/dist/client/rpc-pipeline.js +461 -0
- package/dist/client/rpc-pipeline.js.map +1 -0
- package/dist/client/runtime.d.ts +5 -0
- package/dist/client/runtime.js +7 -0
- package/dist/client/runtime.js.map +1 -0
- package/dist/client/v2-e2ee.d.ts +109 -0
- package/dist/client/v2-e2ee.js +1640 -0
- package/dist/client/v2-e2ee.js.map +1 -0
- package/dist/client.d.ts +21 -57
- package/dist/client.js +303 -3859
- package/dist/client.js.map +1 -1
- package/dist/config.js +9 -8
- package/dist/config.js.map +1 -1
- package/dist/discovery.js +56 -6
- package/dist/discovery.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +2 -0
- package/dist/keystore/aid-db.js +12 -2
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +2 -2
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,1640 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import { normalizeGroupId } from '../group-id.js';
|
|
3
|
+
import { E2EEError, StateError, ValidationError } from '../errors.js';
|
|
4
|
+
import { decryptMessage, encryptGroupMessage, encryptP2PMessage, } from '../v2/e2ee/index.js';
|
|
5
|
+
import { V2Session } from '../v2/session/index.js';
|
|
6
|
+
import { isJsonObject } from '../types.js';
|
|
7
|
+
const V2_BOOTSTRAP_TTL_MS = 60 * 60 * 1000;
|
|
8
|
+
const V2_RETRYABLE_CODES = new Set([-33011, -33012, -33050, -33052, -33054]);
|
|
9
|
+
const MEMBERSHIP_ACTIONS = new Set([
|
|
10
|
+
'member_added',
|
|
11
|
+
'member_left',
|
|
12
|
+
'member_removed',
|
|
13
|
+
'role_changed',
|
|
14
|
+
'owner_transferred',
|
|
15
|
+
'joined',
|
|
16
|
+
'join_approved',
|
|
17
|
+
'invite_code_used',
|
|
18
|
+
]);
|
|
19
|
+
const JOIN_ACTIONS = new Set(['member_added', 'joined', 'join_approved', 'invite_code_used']);
|
|
20
|
+
function pubDerMatchesFingerprint(pubDer, certFingerprint) {
|
|
21
|
+
const expected = String(certFingerprint ?? '').trim().toLowerCase();
|
|
22
|
+
if (!expected)
|
|
23
|
+
return true;
|
|
24
|
+
if (!expected.startsWith('sha256:'))
|
|
25
|
+
return false;
|
|
26
|
+
const expectedHex = expected.slice('sha256:'.length);
|
|
27
|
+
if (![16, 64].includes(expectedHex.length) || !/^[0-9a-f]+$/.test(expectedHex))
|
|
28
|
+
return false;
|
|
29
|
+
const spkiHex = crypto.createHash('sha256').update(Buffer.from(pubDer)).digest('hex');
|
|
30
|
+
return expectedHex.length === 16 ? spkiHex.slice(0, 16) === expectedHex : spkiHex === expectedHex;
|
|
31
|
+
}
|
|
32
|
+
function v2LeftPad32(bytes) {
|
|
33
|
+
if (bytes.length === 32)
|
|
34
|
+
return bytes;
|
|
35
|
+
if (bytes.length > 32)
|
|
36
|
+
return bytes.subarray(bytes.length - 32);
|
|
37
|
+
const out = new Uint8Array(32);
|
|
38
|
+
out.set(bytes, 32 - bytes.length);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
function v2B64ToBytes(value) {
|
|
42
|
+
const buf = Buffer.from(String(value ?? '').trim(), 'base64');
|
|
43
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
44
|
+
}
|
|
45
|
+
function v2B64uToBytes(value) {
|
|
46
|
+
const std = String(value ?? '').replace(/-/g, '+').replace(/_/g, '/');
|
|
47
|
+
const pad = std.length % 4 === 0 ? '' : '='.repeat(4 - (std.length % 4));
|
|
48
|
+
return v2B64ToBytes(std + pad);
|
|
49
|
+
}
|
|
50
|
+
function formatE2EEError(error) {
|
|
51
|
+
return error instanceof Error ? error : String(error);
|
|
52
|
+
}
|
|
53
|
+
function truthyBool(value) {
|
|
54
|
+
if (value === true || value === 1)
|
|
55
|
+
return true;
|
|
56
|
+
if (typeof value === 'string') {
|
|
57
|
+
const normalized = value.trim().toLowerCase();
|
|
58
|
+
return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
function attachGatewayProximity(message, source) {
|
|
63
|
+
if (isJsonObject(source.proximity)) {
|
|
64
|
+
message.proximity = { ...source.proximity };
|
|
65
|
+
}
|
|
66
|
+
for (const key of ['same_device', 'same_network', 'same_egress_ip']) {
|
|
67
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
68
|
+
message[key] = source[key];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function getV2DeviceId(dev) {
|
|
73
|
+
if (Object.prototype.hasOwnProperty.call(dev, 'device_id')) {
|
|
74
|
+
return { present: true, value: String(dev.device_id ?? '').trim() };
|
|
75
|
+
}
|
|
76
|
+
if (Object.prototype.hasOwnProperty.call(dev, 'owner_device_id')) {
|
|
77
|
+
return { present: true, value: String(dev.owner_device_id ?? '').trim() };
|
|
78
|
+
}
|
|
79
|
+
return { present: false, value: '' };
|
|
80
|
+
}
|
|
81
|
+
function normalizeV2WrapPolicy(raw) {
|
|
82
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
83
|
+
return { explicit: false, version: '', protocol: '', scope: 'device' };
|
|
84
|
+
}
|
|
85
|
+
const obj = raw;
|
|
86
|
+
let protocol = String(obj.protocol ?? '').trim().toUpperCase();
|
|
87
|
+
if (protocol !== '1DH' && protocol !== '3DH')
|
|
88
|
+
protocol = '';
|
|
89
|
+
let scope = String(obj.scope ?? '').trim().toLowerCase();
|
|
90
|
+
if (scope !== 'aid' && scope !== 'device') {
|
|
91
|
+
scope = obj.per_aid_wrap === true ? 'aid' : 'device';
|
|
92
|
+
}
|
|
93
|
+
if (scope === 'aid')
|
|
94
|
+
protocol = '1DH';
|
|
95
|
+
return {
|
|
96
|
+
explicit: true,
|
|
97
|
+
version: String(obj.version ?? ''),
|
|
98
|
+
protocol,
|
|
99
|
+
scope: scope,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function v2WrapCapabilities() {
|
|
103
|
+
return {
|
|
104
|
+
version: 'v2.1',
|
|
105
|
+
protocols: ['1DH', '3DH'],
|
|
106
|
+
scopes: ['aid', 'device'],
|
|
107
|
+
per_aid_wrap: true,
|
|
108
|
+
per_device_wrap: true,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function applyV2WrapPolicyToTargets(targets, policy) {
|
|
112
|
+
if (!policy.explicit)
|
|
113
|
+
return targets;
|
|
114
|
+
const out = [];
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
for (const target of targets) {
|
|
117
|
+
const row = { ...target };
|
|
118
|
+
if (policy.protocol === '1DH') {
|
|
119
|
+
row.keySource = 'aid_master';
|
|
120
|
+
row.spkPkDer = undefined;
|
|
121
|
+
row.spkId = '';
|
|
122
|
+
}
|
|
123
|
+
if (policy.scope === 'aid') {
|
|
124
|
+
const key = `${row.aid}\x1f${row.role}`;
|
|
125
|
+
if (seen.has(key))
|
|
126
|
+
continue;
|
|
127
|
+
seen.add(key);
|
|
128
|
+
row.deviceId = '';
|
|
129
|
+
}
|
|
130
|
+
out.push(row);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
export class V2E2EECoordinator {
|
|
135
|
+
runtime;
|
|
136
|
+
constructor(runtime) {
|
|
137
|
+
this.runtime = runtime;
|
|
138
|
+
}
|
|
139
|
+
get client() {
|
|
140
|
+
return this.runtime.client;
|
|
141
|
+
}
|
|
142
|
+
get bootstrapCache() {
|
|
143
|
+
const client = this.client;
|
|
144
|
+
if (!(client._v2BootstrapCache instanceof Map)) {
|
|
145
|
+
client._v2BootstrapCache = new Map();
|
|
146
|
+
}
|
|
147
|
+
return client._v2BootstrapCache;
|
|
148
|
+
}
|
|
149
|
+
getBootstrapCacheEntry(key) {
|
|
150
|
+
return this.bootstrapCache.get(key);
|
|
151
|
+
}
|
|
152
|
+
setBootstrapCacheEntry(key, entry) {
|
|
153
|
+
this.bootstrapCache.set(key, entry);
|
|
154
|
+
}
|
|
155
|
+
deleteBootstrapCacheEntry(key) {
|
|
156
|
+
this.bootstrapCache.delete(key);
|
|
157
|
+
}
|
|
158
|
+
clearBootstrapCache() {
|
|
159
|
+
this.bootstrapCache.clear();
|
|
160
|
+
}
|
|
161
|
+
pruneExpiredBootstrapCache(ttlMs, now = Date.now()) {
|
|
162
|
+
for (const [key, entry] of this.bootstrapCache) {
|
|
163
|
+
if (now - Number(entry.cachedAt ?? 0) >= ttlMs) {
|
|
164
|
+
this.bootstrapCache.delete(key);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async onConnected(opts) {
|
|
169
|
+
const client = this.client;
|
|
170
|
+
try {
|
|
171
|
+
await client._initV2Session();
|
|
172
|
+
}
|
|
173
|
+
catch (exc) {
|
|
174
|
+
client._clientLog.warn(`V2 session init failed (non-fatal): ${formatE2EEError(exc)}`);
|
|
175
|
+
}
|
|
176
|
+
if (opts.backgroundSync && client._v2Session && typeof client._v2AutoConfirmPendingProposals === 'function') {
|
|
177
|
+
client._safeAsync(client._v2AutoConfirmPendingProposals());
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async initV2Session() {
|
|
181
|
+
const client = this.client;
|
|
182
|
+
if (!client._aid)
|
|
183
|
+
return;
|
|
184
|
+
const aidAtStart = client._aid;
|
|
185
|
+
const deviceIdAtStart = client._deviceId;
|
|
186
|
+
const currentAid = client._currentAid;
|
|
187
|
+
const generationAtStart = Number(client._v2RuntimeGeneration ?? 0);
|
|
188
|
+
const existing = client._v2Session;
|
|
189
|
+
if (existing && existing.aid === aidAtStart && existing.deviceId === deviceIdAtStart) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (existing) {
|
|
193
|
+
this.clearBootstrapCache();
|
|
194
|
+
}
|
|
195
|
+
const identityChanged = () => client._aid !== aidAtStart
|
|
196
|
+
|| client._deviceId !== deviceIdAtStart
|
|
197
|
+
|| client._currentAid !== currentAid
|
|
198
|
+
|| Number(client._v2RuntimeGeneration ?? 0) !== generationAtStart;
|
|
199
|
+
if (!currentAid?.privateKeyPem) {
|
|
200
|
+
client._clientLog.warn('V2 session init skipped: no AID private key');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const privateKey = crypto.createPrivateKey(currentAid.privateKeyPem);
|
|
204
|
+
const jwk = privateKey.export({ format: 'jwk' });
|
|
205
|
+
if (jwk.kty !== 'EC' || jwk.crv !== 'P-256' || !jwk.d) {
|
|
206
|
+
throw new StateError('AID private key must be EC P-256');
|
|
207
|
+
}
|
|
208
|
+
const aidPriv = v2LeftPad32(v2B64uToBytes(jwk.d));
|
|
209
|
+
const pubDer = crypto.createPublicKey(privateKey).export({ format: 'der', type: 'spki' });
|
|
210
|
+
const aidPubDer = new Uint8Array(pubDer);
|
|
211
|
+
const storeProvider = client._tokenStore;
|
|
212
|
+
const v2Store = storeProvider.getV2KeyStore?.call(client._tokenStore, client._aid);
|
|
213
|
+
if (!v2Store) {
|
|
214
|
+
throw new StateError('V2 key store is unavailable for current keystore');
|
|
215
|
+
}
|
|
216
|
+
if (identityChanged())
|
|
217
|
+
return;
|
|
218
|
+
const session = new V2Session(v2Store, deviceIdAtStart, aidAtStart, aidPriv, aidPubDer);
|
|
219
|
+
await session.ensureRegistered(client._v2CallFn());
|
|
220
|
+
if (identityChanged())
|
|
221
|
+
return;
|
|
222
|
+
client._v2KeyStore = v2Store;
|
|
223
|
+
client._v2Session = session;
|
|
224
|
+
client._clientLog.debug(`V2 session initialized aid=${aidAtStart} device=${deviceIdAtStart}`);
|
|
225
|
+
}
|
|
226
|
+
scheduleGroupSpkRegistration(groupId, opts) {
|
|
227
|
+
const client = this.client;
|
|
228
|
+
const gid = String(groupId ?? '').trim();
|
|
229
|
+
if (!gid || !client._v2Session)
|
|
230
|
+
return;
|
|
231
|
+
if (!(client._groupSpkRegistrationInflight instanceof Set)) {
|
|
232
|
+
client._groupSpkRegistrationInflight = new Set();
|
|
233
|
+
}
|
|
234
|
+
const inflight = client._groupSpkRegistrationInflight;
|
|
235
|
+
if (inflight.has(gid))
|
|
236
|
+
return;
|
|
237
|
+
inflight.add(gid);
|
|
238
|
+
client._safeAsync((async () => {
|
|
239
|
+
try {
|
|
240
|
+
await client._v2Session.ensureGroupRegistered?.(gid, client._v2CallFn());
|
|
241
|
+
client._clientLog.debug(`group SPK registered: group=${gid} reason=${opts.reason}`);
|
|
242
|
+
}
|
|
243
|
+
catch (exc) {
|
|
244
|
+
client._clientLog.debug(`group SPK registration failed (non-fatal): group=${gid} reason=${opts.reason} err=${formatE2EEError(exc)}`);
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
inflight.delete(gid);
|
|
248
|
+
}
|
|
249
|
+
})());
|
|
250
|
+
}
|
|
251
|
+
scheduleGroupSpkRotation(groupId, opts) {
|
|
252
|
+
const client = this.client;
|
|
253
|
+
const gid = String(groupId ?? '').trim();
|
|
254
|
+
if (!gid || !client._v2Session)
|
|
255
|
+
return;
|
|
256
|
+
if (!(client._groupSpkRotationInflight instanceof Set)) {
|
|
257
|
+
client._groupSpkRotationInflight = new Set();
|
|
258
|
+
}
|
|
259
|
+
const inflight = client._groupSpkRotationInflight;
|
|
260
|
+
if (inflight.has(gid))
|
|
261
|
+
return;
|
|
262
|
+
inflight.add(gid);
|
|
263
|
+
client._safeAsync((async () => {
|
|
264
|
+
try {
|
|
265
|
+
await client._v2Session.rotateGroupSPK?.(gid, client._v2CallFn());
|
|
266
|
+
client._clientLog.debug(`group SPK rotated: group=${gid} reason=${opts.reason}`);
|
|
267
|
+
}
|
|
268
|
+
catch (exc) {
|
|
269
|
+
client._clientLog.debug(`group SPK rotation failed (non-fatal): group=${gid} reason=${opts.reason} err=${formatE2EEError(exc)}`);
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
inflight.delete(gid);
|
|
273
|
+
}
|
|
274
|
+
})());
|
|
275
|
+
}
|
|
276
|
+
scheduleGroupSpkRegistrationAfterPeerFallback(groupId) {
|
|
277
|
+
const client = this.client;
|
|
278
|
+
const gid = String(groupId ?? '').trim();
|
|
279
|
+
if (!gid)
|
|
280
|
+
return;
|
|
281
|
+
if (!(client._groupSpkPeerFallbackRegistered instanceof Set)) {
|
|
282
|
+
client._groupSpkPeerFallbackRegistered = new Set();
|
|
283
|
+
}
|
|
284
|
+
const registered = client._groupSpkPeerFallbackRegistered;
|
|
285
|
+
if (registered.has(gid))
|
|
286
|
+
return;
|
|
287
|
+
registered.add(gid);
|
|
288
|
+
this.scheduleGroupSpkRegistration(gid, { reason: 'peer_device_prekey_fallback' });
|
|
289
|
+
}
|
|
290
|
+
async getV2SenderPubDer(fromAid, senderDeviceId, certFingerprint) {
|
|
291
|
+
const client = this.client;
|
|
292
|
+
const session = client._v2Session;
|
|
293
|
+
if (!session || !fromAid)
|
|
294
|
+
return null;
|
|
295
|
+
const senderPubDer = session.getPeerIK(fromAid, senderDeviceId);
|
|
296
|
+
if (senderPubDer && pubDerMatchesFingerprint(senderPubDer, certFingerprint))
|
|
297
|
+
return senderPubDer;
|
|
298
|
+
try {
|
|
299
|
+
const normalizedFp = String(certFingerprint ?? '').trim() || undefined;
|
|
300
|
+
const certPem = await client._fetchPeerCert(fromAid, normalizedFp, 3000);
|
|
301
|
+
const cert = new crypto.X509Certificate(certPem);
|
|
302
|
+
const certPubDer = cert.publicKey.export({ type: 'spki', format: 'der' });
|
|
303
|
+
const certPub = new Uint8Array(certPubDer);
|
|
304
|
+
if (!pubDerMatchesFingerprint(certPub, certFingerprint))
|
|
305
|
+
return null;
|
|
306
|
+
session.cachePeerIK(fromAid, senderDeviceId, certPub);
|
|
307
|
+
client._clientLog.debug(`V2 decrypt: sender IK fallback from PKI cert for ${fromAid}`);
|
|
308
|
+
return certPub;
|
|
309
|
+
}
|
|
310
|
+
catch (exc) {
|
|
311
|
+
client._clientLog.warn(`V2 decrypt: PKI cert sender IK fallback failed for ${fromAid}: ${formatE2EEError(exc)}`);
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
pendingSenderIKMessageKey(msg, groupId) {
|
|
316
|
+
const client = this.client;
|
|
317
|
+
const messageId = String(msg.message_id ?? '').trim();
|
|
318
|
+
const seq = String(msg.seq ?? '').trim();
|
|
319
|
+
const prefix = groupId ? `group:${groupId}` : `p2p:${client._aid ?? ''}`;
|
|
320
|
+
return `${prefix}:${messageId || seq || Math.random().toString(36).slice(2)}`;
|
|
321
|
+
}
|
|
322
|
+
pendingSenderIKFetchKey(fromAid, senderDeviceId, groupId) {
|
|
323
|
+
return `${fromAid}#${senderDeviceId}#${groupId || ''}`;
|
|
324
|
+
}
|
|
325
|
+
scheduleSenderIKPending(args) {
|
|
326
|
+
const client = this.client;
|
|
327
|
+
const fromAid = String(args.fromAid ?? '').trim();
|
|
328
|
+
if (!fromAid)
|
|
329
|
+
return;
|
|
330
|
+
const senderDeviceId = String(args.senderDeviceId ?? '');
|
|
331
|
+
const groupId = String(args.groupId ?? '').trim();
|
|
332
|
+
const messageKey = this.pendingSenderIKMessageKey(args.msg, groupId);
|
|
333
|
+
client._v2SenderIKPending.set(messageKey, {
|
|
334
|
+
msg: { ...args.msg },
|
|
335
|
+
fromAid,
|
|
336
|
+
senderDeviceId,
|
|
337
|
+
groupId,
|
|
338
|
+
createdAt: Date.now(),
|
|
339
|
+
});
|
|
340
|
+
client._clientLog.debug(`V2 decrypt pending sender IK: key=${messageKey} from=${fromAid} device=${senderDeviceId || '-'} group=${groupId || '<p2p>'} pending=${client._v2SenderIKPending.size}`);
|
|
341
|
+
this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupId);
|
|
342
|
+
}
|
|
343
|
+
scheduleSenderIKFetch(fromAid, senderDeviceId, groupId) {
|
|
344
|
+
const client = this.client;
|
|
345
|
+
const fetchKey = this.pendingSenderIKFetchKey(fromAid, senderDeviceId, groupId);
|
|
346
|
+
if (!fromAid || client._v2SenderIKFetching.has(fetchKey))
|
|
347
|
+
return;
|
|
348
|
+
client._v2SenderIKFetching.add(fetchKey);
|
|
349
|
+
client._safeAsync(this.resolveSenderIKPending(fromAid, senderDeviceId, groupId, fetchKey));
|
|
350
|
+
}
|
|
351
|
+
async resolveSenderIKPending(fromAid, senderDeviceId, groupId, fetchKey) {
|
|
352
|
+
const client = this.client;
|
|
353
|
+
try {
|
|
354
|
+
const session = client._v2Session;
|
|
355
|
+
if (session && fromAid) {
|
|
356
|
+
try {
|
|
357
|
+
const bs = await client.call('message.v2.bootstrap', {
|
|
358
|
+
peer_aid: fromAid,
|
|
359
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
360
|
+
});
|
|
361
|
+
if (typeof client._primeBootstrapPeerCerts === 'function') {
|
|
362
|
+
await client._primeBootstrapPeerCerts(bs, fromAid);
|
|
363
|
+
}
|
|
364
|
+
const peers = (Array.isArray(bs?.peer_devices) ? bs.peer_devices : []);
|
|
365
|
+
for (const dev of peers)
|
|
366
|
+
client._cacheV2PeerIKFromDevice(dev, fromAid);
|
|
367
|
+
}
|
|
368
|
+
catch (exc) {
|
|
369
|
+
client._clientLog.warn(`V2 sender IK pending bootstrap failed peer=${fromAid}: ${formatE2EEError(exc)}`);
|
|
370
|
+
}
|
|
371
|
+
if (groupId) {
|
|
372
|
+
try {
|
|
373
|
+
const gbs = await client.call('group.v2.bootstrap', {
|
|
374
|
+
group_id: groupId,
|
|
375
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
376
|
+
});
|
|
377
|
+
const devices = (Array.isArray(gbs?.devices) ? gbs.devices : []);
|
|
378
|
+
const audit = (Array.isArray(gbs?.audit_recipients) ? gbs.audit_recipients : []);
|
|
379
|
+
for (const dev of devices)
|
|
380
|
+
client._cacheV2PeerIKFromDevice(dev);
|
|
381
|
+
for (const dev of audit)
|
|
382
|
+
client._cacheV2PeerIKFromDevice(dev);
|
|
383
|
+
}
|
|
384
|
+
catch (exc) {
|
|
385
|
+
client._clientLog.warn(`V2 sender IK pending group bootstrap failed group=${groupId}: ${formatE2EEError(exc)}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (!session.getPeerIK(fromAid, senderDeviceId)) {
|
|
389
|
+
await this.getV2SenderPubDer(fromAid, senderDeviceId);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const pendingItems = [...client._v2SenderIKPending.entries()].filter(([, entry]) => entry.fromAid === fromAid && entry.senderDeviceId === senderDeviceId && entry.groupId === groupId);
|
|
393
|
+
for (const [key, entry] of pendingItems) {
|
|
394
|
+
let plaintext = null;
|
|
395
|
+
try {
|
|
396
|
+
plaintext = await client._decryptV2Message(entry.msg, false);
|
|
397
|
+
}
|
|
398
|
+
catch (exc) {
|
|
399
|
+
client._clientLog.warn(`V2 sender IK pending retry raised: key=${key} err=${formatE2EEError(exc)}`);
|
|
400
|
+
}
|
|
401
|
+
client._v2SenderIKPending.delete(key);
|
|
402
|
+
if (plaintext === null) {
|
|
403
|
+
client._clientLog.debug(`V2 sender IK pending retry failed: key=${key}`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const seq = Number(entry.msg.seq ?? 0);
|
|
407
|
+
if (entry.groupId) {
|
|
408
|
+
plaintext.group_id = entry.groupId;
|
|
409
|
+
await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
await client._publishPulledMessage('message.received', `p2p:${client._aid ?? ''}`, seq, plaintext);
|
|
413
|
+
}
|
|
414
|
+
client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
client._v2SenderIKFetching.delete(fetchKey);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async buildV2P2PEnvelope(opts) {
|
|
422
|
+
const client = this.client;
|
|
423
|
+
if (!client._v2Session) {
|
|
424
|
+
throw new StateError('V2 session not initialized');
|
|
425
|
+
}
|
|
426
|
+
const session = client._v2Session;
|
|
427
|
+
const to = String(opts.to ?? '').trim();
|
|
428
|
+
if (!to)
|
|
429
|
+
throw new ValidationError("message.send requires 'to'");
|
|
430
|
+
const useCache = opts.useCache !== false;
|
|
431
|
+
let peerDevices = [];
|
|
432
|
+
let auditRaw = [];
|
|
433
|
+
let wrapPolicy = normalizeV2WrapPolicy(undefined);
|
|
434
|
+
const cached = useCache ? this.getBootstrapCacheEntry(to) : undefined;
|
|
435
|
+
if (cached && Date.now() - Number(cached.cachedAt ?? 0) < V2_BOOTSTRAP_TTL_MS) {
|
|
436
|
+
peerDevices = (cached.devices ?? []);
|
|
437
|
+
auditRaw = (cached.auditRecipients ?? []);
|
|
438
|
+
wrapPolicy = cached.wrapPolicy ?? wrapPolicy;
|
|
439
|
+
client._clientLog.debug(`message.v2.bootstrap cache hit: to=${to}, devices=${peerDevices.length}, audit=${auditRaw.length}`);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
const bs = await client.call('message.v2.bootstrap', {
|
|
443
|
+
peer_aid: to,
|
|
444
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
445
|
+
});
|
|
446
|
+
if (typeof client._primeBootstrapPeerCerts === 'function') {
|
|
447
|
+
await client._primeBootstrapPeerCerts(bs, to);
|
|
448
|
+
}
|
|
449
|
+
wrapPolicy = normalizeV2WrapPolicy(bs.e2ee_wrap_policy);
|
|
450
|
+
peerDevices = (Array.isArray(bs?.peer_devices) ? bs.peer_devices : []);
|
|
451
|
+
auditRaw = (Array.isArray(bs?.audit_recipients) ? bs.audit_recipients : []);
|
|
452
|
+
client._clientLog.debug(`message.v2.bootstrap fetched: to=${to}, devices=${peerDevices.length}, audit=${auditRaw.length}`);
|
|
453
|
+
if (peerDevices.length > 0) {
|
|
454
|
+
this.setBootstrapCacheEntry(to, {
|
|
455
|
+
devices: peerDevices,
|
|
456
|
+
auditRecipients: auditRaw,
|
|
457
|
+
cachedAt: Date.now(),
|
|
458
|
+
wrapPolicy,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (peerDevices.length === 0) {
|
|
463
|
+
throw new E2EEError(`V2 bootstrap: no devices found for ${to}`);
|
|
464
|
+
}
|
|
465
|
+
const targets = [];
|
|
466
|
+
for (const dev of peerDevices) {
|
|
467
|
+
const devId = getV2DeviceId(dev);
|
|
468
|
+
const target = await client._v2BuildTargetFromDevice({
|
|
469
|
+
dev,
|
|
470
|
+
aid: to,
|
|
471
|
+
deviceId: devId.value,
|
|
472
|
+
role: 'peer',
|
|
473
|
+
defaultKeySource: 'peer_device_prekey',
|
|
474
|
+
});
|
|
475
|
+
if (target)
|
|
476
|
+
targets.push(target);
|
|
477
|
+
}
|
|
478
|
+
const auditTargets = [];
|
|
479
|
+
for (const dev of auditRaw) {
|
|
480
|
+
const target = await client._v2BuildTargetFromDevice({
|
|
481
|
+
dev,
|
|
482
|
+
aid: String(dev.aid ?? ''),
|
|
483
|
+
deviceId: String(dev.device_id ?? ''),
|
|
484
|
+
role: 'audit',
|
|
485
|
+
defaultKeySource: 'peer_device_prekey',
|
|
486
|
+
});
|
|
487
|
+
if (target)
|
|
488
|
+
auditTargets.push(target);
|
|
489
|
+
}
|
|
490
|
+
if (client._aid && client._aid !== to) {
|
|
491
|
+
try {
|
|
492
|
+
const selfCached = this.getBootstrapCacheEntry(client._aid);
|
|
493
|
+
let selfDevices = [];
|
|
494
|
+
if (selfCached && Date.now() - Number(selfCached.cachedAt ?? 0) < V2_BOOTSTRAP_TTL_MS) {
|
|
495
|
+
selfDevices = (selfCached.devices ?? []);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
const selfBs = await client.call('message.v2.bootstrap', {
|
|
499
|
+
peer_aid: client._aid,
|
|
500
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
501
|
+
});
|
|
502
|
+
if (typeof client._primeBootstrapPeerCerts === 'function') {
|
|
503
|
+
await client._primeBootstrapPeerCerts(selfBs, client._aid);
|
|
504
|
+
}
|
|
505
|
+
selfDevices = (Array.isArray(selfBs?.peer_devices) ? selfBs.peer_devices : []);
|
|
506
|
+
const selfWrapPolicy = normalizeV2WrapPolicy(selfBs.e2ee_wrap_policy);
|
|
507
|
+
if (selfDevices.length > 0) {
|
|
508
|
+
this.setBootstrapCacheEntry(client._aid, {
|
|
509
|
+
devices: selfDevices,
|
|
510
|
+
auditRecipients: [],
|
|
511
|
+
cachedAt: Date.now(),
|
|
512
|
+
wrapPolicy: selfWrapPolicy,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
for (const dev of selfDevices) {
|
|
517
|
+
const devId = getV2DeviceId(dev);
|
|
518
|
+
if (!devId.present || devId.value === client._deviceId)
|
|
519
|
+
continue;
|
|
520
|
+
const target = await client._v2BuildTargetFromDevice({
|
|
521
|
+
dev,
|
|
522
|
+
aid: client._aid,
|
|
523
|
+
deviceId: devId.value,
|
|
524
|
+
role: 'self_sync',
|
|
525
|
+
defaultKeySource: 'peer_device_prekey',
|
|
526
|
+
});
|
|
527
|
+
if (target)
|
|
528
|
+
targets.push(target);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
catch (exc) {
|
|
532
|
+
client._clientLog.debug(`V2 self-sync bootstrap failed (non-fatal): ${formatE2EEError(exc)}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (targets.length === 0) {
|
|
536
|
+
throw new E2EEError(`V2 bootstrap: no usable devices found for ${to}`);
|
|
537
|
+
}
|
|
538
|
+
const envelope = encryptP2PMessage(session.getSenderIdentity(), {
|
|
539
|
+
targets: applyV2WrapPolicyToTargets(targets, wrapPolicy),
|
|
540
|
+
auditRecipients: applyV2WrapPolicyToTargets(auditTargets, wrapPolicy),
|
|
541
|
+
}, opts.payload, {
|
|
542
|
+
messageId: opts.messageId,
|
|
543
|
+
timestamp: opts.timestamp,
|
|
544
|
+
protectedHeaders: opts.protectedHeaders,
|
|
545
|
+
context: opts.context,
|
|
546
|
+
});
|
|
547
|
+
client._logMessageDebug('send-envelope', 'message.send.v2', 'message.send', {
|
|
548
|
+
message_id: envelope.message_id,
|
|
549
|
+
to,
|
|
550
|
+
type: envelope.type,
|
|
551
|
+
version: envelope.version,
|
|
552
|
+
protected_headers: envelope.protected_headers,
|
|
553
|
+
context: envelope.context,
|
|
554
|
+
}, {
|
|
555
|
+
payloadOverride: envelope,
|
|
556
|
+
extra: {
|
|
557
|
+
plaintext_payload: opts.payload,
|
|
558
|
+
target_count: targets.length,
|
|
559
|
+
audit_count: auditTargets.length,
|
|
560
|
+
use_cache: useCache,
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
return envelope;
|
|
564
|
+
}
|
|
565
|
+
async sendV2(to, payload, opts) {
|
|
566
|
+
const client = this.client;
|
|
567
|
+
await client._ensureV2SessionReady('message.send', 'V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
|
|
568
|
+
const toAid = String(to ?? '').trim();
|
|
569
|
+
if (!toAid)
|
|
570
|
+
throw new ValidationError("message.send requires 'to'");
|
|
571
|
+
if (!isJsonObject(payload))
|
|
572
|
+
throw new ValidationError('message.send payload must be a dict for V2 encryption');
|
|
573
|
+
client._logMessageDebug('send-plaintext', 'message.send.v2', 'message.send', {
|
|
574
|
+
to: toAid,
|
|
575
|
+
message_id: opts?.messageId ?? '',
|
|
576
|
+
payload,
|
|
577
|
+
}, { payloadOverride: payload });
|
|
578
|
+
const attempt = async (useCache) => {
|
|
579
|
+
client._clientLog.debug(`message.v2.send attempt: to=${toAid}, use_cache=${useCache}`);
|
|
580
|
+
const envelope = await client._buildV2P2PEnvelope({
|
|
581
|
+
to: toAid,
|
|
582
|
+
payload,
|
|
583
|
+
messageId: opts?.messageId,
|
|
584
|
+
timestamp: opts?.timestamp,
|
|
585
|
+
protectedHeaders: opts?.protectedHeaders,
|
|
586
|
+
context: opts?.context,
|
|
587
|
+
useCache,
|
|
588
|
+
});
|
|
589
|
+
const result = await client.call('message.send', {
|
|
590
|
+
to: toAid,
|
|
591
|
+
payload: envelope,
|
|
592
|
+
encrypt: false,
|
|
593
|
+
});
|
|
594
|
+
client._clientLog.debug(`message.v2.send ok: to=${toAid}, use_cache=${useCache}, seq=${String((isJsonObject(result) ? result.seq : '') ?? '')}`);
|
|
595
|
+
return result;
|
|
596
|
+
};
|
|
597
|
+
try {
|
|
598
|
+
return await attempt(true);
|
|
599
|
+
}
|
|
600
|
+
catch (exc) {
|
|
601
|
+
const excCode = exc?.code;
|
|
602
|
+
if (V2_RETRYABLE_CODES.has(Number(excCode))) {
|
|
603
|
+
client._clientLog.debug(`V2 P2P speculative send rejected (code=${String(excCode)}), refreshing bootstrap`);
|
|
604
|
+
this.deleteBootstrapCacheEntry(toAid);
|
|
605
|
+
return await attempt(false);
|
|
606
|
+
}
|
|
607
|
+
throw exc;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async pullV2(afterSeq = 0, limit = 50, opts) {
|
|
611
|
+
const client = this.client;
|
|
612
|
+
await client._ensureV2SessionReady('message.pull');
|
|
613
|
+
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
614
|
+
if (ns && !opts?.gateLocked) {
|
|
615
|
+
return await client._runPullSerialized(ns, async () => this.pullV2(afterSeq, limit, {
|
|
616
|
+
...(opts ?? {}),
|
|
617
|
+
gateLocked: true,
|
|
618
|
+
scheduleFollowup: true,
|
|
619
|
+
}));
|
|
620
|
+
}
|
|
621
|
+
const decrypted = [];
|
|
622
|
+
let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
|
|
623
|
+
let pageCount = 0;
|
|
624
|
+
const maxPages = 100;
|
|
625
|
+
while (pageCount < maxPages) {
|
|
626
|
+
pageCount += 1;
|
|
627
|
+
client._clientLog.debug(`message.v2.pull page request: page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ns=${ns || '<none>'}`);
|
|
628
|
+
const result = await client._callRawV2Rpc('message.v2.pull', {
|
|
629
|
+
after_seq: nextAfterSeq,
|
|
630
|
+
limit,
|
|
631
|
+
...(opts?.force ? { force: true } : {}),
|
|
632
|
+
});
|
|
633
|
+
const messages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
634
|
+
client._clientLog.debug(`message.v2.pull page response: page=${pageCount}, raw_count=${messages.length}, has_more=${String(result.has_more ?? '')}, server_ack_seq=${String(result.server_ack_seq ?? '')}`);
|
|
635
|
+
for (const msg of messages) {
|
|
636
|
+
client._logMessageDebug('pull-raw', 'message.v2.pull', 'message.received', msg);
|
|
637
|
+
}
|
|
638
|
+
const seqs = messages
|
|
639
|
+
.map((msg) => Number(msg.seq ?? 0))
|
|
640
|
+
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
641
|
+
const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
|
|
642
|
+
let pageMaxSeq = nextAfterSeq;
|
|
643
|
+
if (seqs.length > 0) {
|
|
644
|
+
pageMaxSeq = Math.max(...seqs);
|
|
645
|
+
if (ns) {
|
|
646
|
+
client._seqTracker.forceContiguousSeq(ns, pageMaxSeq);
|
|
647
|
+
client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
for (const msg of messages) {
|
|
651
|
+
const seq = Number(msg.seq ?? 0);
|
|
652
|
+
if (!Number.isFinite(seq) || seq <= 0)
|
|
653
|
+
continue;
|
|
654
|
+
const version = String(msg.version ?? 'v2');
|
|
655
|
+
if (version === 'v1') {
|
|
656
|
+
const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
|
|
657
|
+
const legacyPayload = legacy.payload;
|
|
658
|
+
const payloadType = isJsonObject(legacyPayload)
|
|
659
|
+
? String(legacyPayload.type ?? '').trim()
|
|
660
|
+
: '';
|
|
661
|
+
if (legacyPayload !== undefined && legacyPayload !== null && payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted') {
|
|
662
|
+
const v1Msg = {
|
|
663
|
+
message_id: String(msg.message_id ?? ''),
|
|
664
|
+
from: String(msg.from_aid ?? ''),
|
|
665
|
+
to: String(legacy.to ?? client._aid ?? ''),
|
|
666
|
+
seq: msg.seq,
|
|
667
|
+
type: String(msg.type ?? ''),
|
|
668
|
+
timestamp: msg.t_server,
|
|
669
|
+
payload: legacyPayload,
|
|
670
|
+
encrypted: false,
|
|
671
|
+
};
|
|
672
|
+
const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
|
|
673
|
+
if (ns) {
|
|
674
|
+
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload);
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
|
|
678
|
+
}
|
|
679
|
+
decrypted.push(v1Msg);
|
|
680
|
+
client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
|
|
684
|
+
}
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (version !== 'v2') {
|
|
688
|
+
client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
const spkId = String(msg.spk_id ?? '');
|
|
692
|
+
if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
|
|
693
|
+
client._v2Session.trackOldSPKMaxSeq(spkId, seq);
|
|
694
|
+
}
|
|
695
|
+
const plaintext = await client._decryptV2Message(msg);
|
|
696
|
+
if (plaintext === null) {
|
|
697
|
+
client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
if (ns) {
|
|
701
|
+
await client._publishPulledMessage('message.received', ns, seq, plaintext);
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
await client._publishAppEvent('message.received', plaintext, 'pull');
|
|
705
|
+
}
|
|
706
|
+
decrypted.push(plaintext);
|
|
707
|
+
client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
|
|
708
|
+
}
|
|
709
|
+
const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
|
|
710
|
+
const serverAckSeq = Number(result.server_ack_seq ?? 0);
|
|
711
|
+
if (ns && Number.isFinite(serverAckSeq) && serverAckSeq > 0) {
|
|
712
|
+
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
713
|
+
if (contig < serverAckSeq) {
|
|
714
|
+
client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${contig} -> server_ack_seq=${serverAckSeq}`);
|
|
715
|
+
client._seqTracker.forceContiguousSeq(ns, serverAckSeq);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (ns) {
|
|
719
|
+
const ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
720
|
+
const contigAdvanced = ackSeq !== pageContigBefore;
|
|
721
|
+
if (contigAdvanced) {
|
|
722
|
+
await client._drainOrderedMessages(ns, undefined, true);
|
|
723
|
+
client._saveSeqTrackerState();
|
|
724
|
+
}
|
|
725
|
+
const ackNeeded = messages.length > 0
|
|
726
|
+
&& ackSeq > 0
|
|
727
|
+
&& !opts?.skipAutoAck
|
|
728
|
+
&& (contigAdvanced || (hasServerAckSeq && ackSeq > serverAckSeq));
|
|
729
|
+
if (ackNeeded) {
|
|
730
|
+
client._clientLog.debug(`message.v2.pull sending auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
|
|
731
|
+
await this.ackV2(ackSeq);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
735
|
+
if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
|
|
736
|
+
break;
|
|
737
|
+
nextAfterSeq = nextAfter;
|
|
738
|
+
}
|
|
739
|
+
if (pageCount >= maxPages) {
|
|
740
|
+
client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
|
|
741
|
+
}
|
|
742
|
+
client._clientLog.debug(`message.v2.pull done: requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns || '<none>'}`);
|
|
743
|
+
return decrypted;
|
|
744
|
+
}
|
|
745
|
+
async ackV2(upToSeq) {
|
|
746
|
+
const client = this.client;
|
|
747
|
+
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
748
|
+
let seq = Number(upToSeq ?? (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
|
|
749
|
+
if (!Number.isFinite(seq) || seq <= 0) {
|
|
750
|
+
client._clientLog.debug(`message.v2.ack skipped: ns=${ns || '<none>'}, up_to_seq=${String(upToSeq ?? '')}`);
|
|
751
|
+
return { acked: 0 };
|
|
752
|
+
}
|
|
753
|
+
seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
|
|
754
|
+
client._clientLog.debug(`message.v2.ack send: ns=${ns || '<none>'}, up_to_seq=${seq}`);
|
|
755
|
+
const raw = await client._callRawV2Rpc('message.v2.ack', { up_to_seq: seq });
|
|
756
|
+
const result = isJsonObject(raw)
|
|
757
|
+
? { ...raw }
|
|
758
|
+
: { result: raw };
|
|
759
|
+
let actualAckSeq = seq;
|
|
760
|
+
if ('effective_ack_seq' in result)
|
|
761
|
+
actualAckSeq = Number(result.effective_ack_seq ?? 0);
|
|
762
|
+
else if ('ack_seq' in result)
|
|
763
|
+
actualAckSeq = Number(result.ack_seq ?? 0);
|
|
764
|
+
else if ('cursor' in result)
|
|
765
|
+
actualAckSeq = Number(result.cursor ?? 0);
|
|
766
|
+
if (!Number.isFinite(actualAckSeq))
|
|
767
|
+
actualAckSeq = seq;
|
|
768
|
+
result.ack_seq = actualAckSeq;
|
|
769
|
+
result.success = true;
|
|
770
|
+
if (Number(result.acked ?? 0) === 0)
|
|
771
|
+
result.acked = actualAckSeq;
|
|
772
|
+
if (client._v2Session) {
|
|
773
|
+
try {
|
|
774
|
+
const destroyed = await Promise.resolve(client._v2Session.maybeDestroyOldSPKs(actualAckSeq));
|
|
775
|
+
if (destroyed.length > 0) {
|
|
776
|
+
client._clientLog.info(`V2 destroyed old SPKs after ack: ${destroyed.slice(0, 3).join(',')} (PFS)`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
catch (exc) {
|
|
780
|
+
client._clientLog.debug(`V2 SPK destroy failed (non-fatal): ${formatE2EEError(exc)}`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
client._clientLog.debug(`message.v2.ack ok: ns=${ns || '<none>'}, requested=${seq}, effective=${actualAckSeq}, acked=${String(result.acked ?? '')}`);
|
|
784
|
+
return result;
|
|
785
|
+
}
|
|
786
|
+
async sendGroupV2(groupId, payload, opts) {
|
|
787
|
+
const client = this.client;
|
|
788
|
+
await client._ensureV2SessionReady('group.send', 'V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
|
|
789
|
+
const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
|
|
790
|
+
if (!gid)
|
|
791
|
+
throw new ValidationError("group.send requires 'group_id'");
|
|
792
|
+
if (!isJsonObject(payload))
|
|
793
|
+
throw new ValidationError('group.send payload must be a dict for V2 encryption');
|
|
794
|
+
client._logMessageDebug('send-plaintext', 'group.send.v2', 'group.send', {
|
|
795
|
+
group_id: gid,
|
|
796
|
+
message_id: opts?.messageId ?? '',
|
|
797
|
+
payload,
|
|
798
|
+
}, { payloadOverride: payload });
|
|
799
|
+
const attempt = async (useCache) => {
|
|
800
|
+
client._clientLog.debug(`group.v2.send attempt: group=${gid}, use_cache=${useCache}`);
|
|
801
|
+
const envelope = await client._buildV2GroupEnvelope({
|
|
802
|
+
groupId: gid,
|
|
803
|
+
payload,
|
|
804
|
+
messageId: opts?.messageId,
|
|
805
|
+
timestamp: opts?.timestamp,
|
|
806
|
+
protectedHeaders: opts?.protectedHeaders,
|
|
807
|
+
context: opts?.context,
|
|
808
|
+
useCache,
|
|
809
|
+
});
|
|
810
|
+
const result = await client.call('group.v2.send', {
|
|
811
|
+
group_id: gid,
|
|
812
|
+
envelope,
|
|
813
|
+
});
|
|
814
|
+
client._clientLog.debug(`group.v2.send ok: group=${gid}, use_cache=${useCache}, seq=${String((isJsonObject(result) ? result.seq : '') ?? '')}`);
|
|
815
|
+
return result;
|
|
816
|
+
};
|
|
817
|
+
const markSentSeq = (result) => {
|
|
818
|
+
if (!isJsonObject(result))
|
|
819
|
+
return;
|
|
820
|
+
const obj = result;
|
|
821
|
+
const seq = Number(obj.seq ?? 0);
|
|
822
|
+
if (!Number.isFinite(seq) || seq <= 0)
|
|
823
|
+
return;
|
|
824
|
+
const ns = `group:${gid}`;
|
|
825
|
+
client._seqTracker.onMessageSeq(ns, seq);
|
|
826
|
+
client._markPublishedSeq(ns, seq);
|
|
827
|
+
client._saveSeqTrackerState();
|
|
828
|
+
client._clientLog.debug(`group.v2.send marked own seq: group=${gid}, ns=${ns}, seq=${seq}`);
|
|
829
|
+
};
|
|
830
|
+
try {
|
|
831
|
+
const result = await attempt(true);
|
|
832
|
+
markSentSeq(result);
|
|
833
|
+
return result;
|
|
834
|
+
}
|
|
835
|
+
catch (exc) {
|
|
836
|
+
const excCode = Number(exc?.code);
|
|
837
|
+
if (V2_RETRYABLE_CODES.has(excCode)) {
|
|
838
|
+
client._clientLog.debug(`V2 group speculative send rejected (code=${String(excCode)}), refreshing bootstrap`);
|
|
839
|
+
this.deleteBootstrapCacheEntry(`group:${gid}`);
|
|
840
|
+
const result = await attempt(false);
|
|
841
|
+
markSentSeq(result);
|
|
842
|
+
return result;
|
|
843
|
+
}
|
|
844
|
+
throw exc;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async buildV2GroupEnvelope(opts) {
|
|
848
|
+
const client = this.client;
|
|
849
|
+
if (!client._v2Session)
|
|
850
|
+
throw new StateError('V2 session not initialized');
|
|
851
|
+
const session = client._v2Session;
|
|
852
|
+
const groupId = normalizeGroupId(opts.groupId) || String(opts.groupId ?? '').trim();
|
|
853
|
+
if (!groupId)
|
|
854
|
+
throw new ValidationError("group.send requires 'group_id'");
|
|
855
|
+
const cacheKey = `group:${groupId}`;
|
|
856
|
+
const useCache = opts.useCache !== false;
|
|
857
|
+
let allDevices = [];
|
|
858
|
+
let auditRecipientsRaw = [];
|
|
859
|
+
let epoch = 0;
|
|
860
|
+
let stateCommitment = { state_version: 0, state_hash: '', state_chain: '' };
|
|
861
|
+
let wrapPolicy = normalizeV2WrapPolicy(undefined);
|
|
862
|
+
const cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : undefined;
|
|
863
|
+
if (cached && Date.now() - Number(cached.cachedAt ?? 0) < V2_BOOTSTRAP_TTL_MS) {
|
|
864
|
+
allDevices = (cached.devices ?? []);
|
|
865
|
+
auditRecipientsRaw = (cached.auditRecipients ?? []);
|
|
866
|
+
epoch = Number(cached.epoch ?? 0) || 0;
|
|
867
|
+
stateCommitment = cached.stateCommitment ?? stateCommitment;
|
|
868
|
+
wrapPolicy = cached.wrapPolicy ?? wrapPolicy;
|
|
869
|
+
client._clientLog.debug(`group.v2.bootstrap cache hit: group=${groupId}, devices=${allDevices.length}, audit=${auditRecipientsRaw.length}, epoch=${epoch}, state_version=${stateCommitment.state_version}`);
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
const bs = await client.call('group.v2.bootstrap', {
|
|
873
|
+
group_id: groupId,
|
|
874
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
875
|
+
});
|
|
876
|
+
allDevices = (Array.isArray(bs.devices) ? bs.devices : []);
|
|
877
|
+
auditRecipientsRaw = (Array.isArray(bs.audit_recipients) ? bs.audit_recipients : []);
|
|
878
|
+
epoch = Number(bs.epoch ?? 0) || 0;
|
|
879
|
+
wrapPolicy = normalizeV2WrapPolicy(bs.e2ee_wrap_policy);
|
|
880
|
+
client._clientLog.debug(`group.v2.bootstrap fetched: group=${groupId}, devices=${allDevices.length}, audit=${auditRecipientsRaw.length}, epoch=${epoch}, members=${Array.isArray(bs.member_aids) ? bs.member_aids.length : 0}`);
|
|
881
|
+
const stateChain = String(bs.state_chain ?? '');
|
|
882
|
+
await client._v2CheckFork(groupId, stateChain);
|
|
883
|
+
await client._v2VerifyStateSignature(groupId, bs);
|
|
884
|
+
await client._publishV2GroupSecurityLevel(groupId, bs);
|
|
885
|
+
stateCommitment = {
|
|
886
|
+
state_version: Number(bs.state_version ?? 0) || 0,
|
|
887
|
+
state_hash: String(bs.state_hash_signed ?? bs.state_hash ?? ''),
|
|
888
|
+
state_chain: stateChain,
|
|
889
|
+
};
|
|
890
|
+
if (allDevices.length > 0) {
|
|
891
|
+
this.setBootstrapCacheEntry(cacheKey, {
|
|
892
|
+
devices: allDevices,
|
|
893
|
+
auditRecipients: auditRecipientsRaw,
|
|
894
|
+
cachedAt: Date.now(),
|
|
895
|
+
epoch,
|
|
896
|
+
stateCommitment,
|
|
897
|
+
wrapPolicy,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
const pendingAdds = Array.isArray(bs.pending_adds) ? bs.pending_adds : [];
|
|
901
|
+
if (pendingAdds.length > 0 && client._v2Session) {
|
|
902
|
+
client._v2MaybeTriggerAutoPropose(groupId);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (allDevices.length === 0) {
|
|
906
|
+
throw new E2EEError(`V2 group bootstrap: no devices found for group ${groupId}`);
|
|
907
|
+
}
|
|
908
|
+
const targets = [];
|
|
909
|
+
for (const dev of allDevices) {
|
|
910
|
+
const devAid = String(dev.aid ?? '').trim();
|
|
911
|
+
const devId = getV2DeviceId(dev);
|
|
912
|
+
if (devAid === client._aid && devId.present && devId.value === client._deviceId)
|
|
913
|
+
continue;
|
|
914
|
+
const role = devAid === client._aid ? 'self_sync' : 'member';
|
|
915
|
+
const target = await client._v2BuildTargetFromDevice({
|
|
916
|
+
dev,
|
|
917
|
+
aid: devAid,
|
|
918
|
+
deviceId: devId.value,
|
|
919
|
+
role,
|
|
920
|
+
defaultKeySource: 'peer_device_prekey',
|
|
921
|
+
});
|
|
922
|
+
if (target)
|
|
923
|
+
targets.push(target);
|
|
924
|
+
}
|
|
925
|
+
if (targets.length === 0) {
|
|
926
|
+
throw new E2EEError(`V2 group: no target devices for group ${groupId}`);
|
|
927
|
+
}
|
|
928
|
+
for (const dev of auditRecipientsRaw) {
|
|
929
|
+
const target = await client._v2BuildTargetFromDevice({
|
|
930
|
+
dev,
|
|
931
|
+
aid: String(dev.aid ?? ''),
|
|
932
|
+
deviceId: String(dev.device_id ?? ''),
|
|
933
|
+
role: 'audit',
|
|
934
|
+
defaultKeySource: 'peer_device_prekey',
|
|
935
|
+
});
|
|
936
|
+
if (target)
|
|
937
|
+
targets.push(target);
|
|
938
|
+
}
|
|
939
|
+
const envelope = encryptGroupMessage(session.getSenderIdentity(), groupId, epoch, applyV2WrapPolicyToTargets(targets, wrapPolicy), opts.payload, {
|
|
940
|
+
messageId: opts.messageId,
|
|
941
|
+
timestamp: opts.timestamp,
|
|
942
|
+
protectedHeaders: opts.protectedHeaders,
|
|
943
|
+
context: opts.context,
|
|
944
|
+
}, stateCommitment);
|
|
945
|
+
client._logMessageDebug('send-envelope', 'group.send.v2', 'group.send', {
|
|
946
|
+
group_id: groupId,
|
|
947
|
+
message_id: envelope.message_id,
|
|
948
|
+
type: envelope.type,
|
|
949
|
+
version: envelope.version,
|
|
950
|
+
protected_headers: envelope.protected_headers,
|
|
951
|
+
context: envelope.context,
|
|
952
|
+
}, {
|
|
953
|
+
payloadOverride: envelope,
|
|
954
|
+
extra: {
|
|
955
|
+
plaintext_payload: opts.payload,
|
|
956
|
+
epoch,
|
|
957
|
+
target_count: targets.length,
|
|
958
|
+
audit_count: auditRecipientsRaw.length,
|
|
959
|
+
state_version: stateCommitment.state_version,
|
|
960
|
+
use_cache: useCache,
|
|
961
|
+
},
|
|
962
|
+
});
|
|
963
|
+
return envelope;
|
|
964
|
+
}
|
|
965
|
+
async pullGroupV2Internal(params) {
|
|
966
|
+
await this.pullGroupV2(params.group_id, params.after_seq, params.limit, { gateLocked: true });
|
|
967
|
+
}
|
|
968
|
+
async pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {
|
|
969
|
+
const client = this.client;
|
|
970
|
+
await client._ensureV2SessionReady('group.pull');
|
|
971
|
+
const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
|
|
972
|
+
if (!gid)
|
|
973
|
+
throw new ValidationError('group.pull requires group_id');
|
|
974
|
+
const ns = `group:${gid}`;
|
|
975
|
+
if (!opts?.gateLocked) {
|
|
976
|
+
return await client._runPullSerialized(ns, async () => this.pullGroupV2(gid, afterSeq, limit, {
|
|
977
|
+
...(opts ?? {}),
|
|
978
|
+
gateLocked: true,
|
|
979
|
+
scheduleFollowup: true,
|
|
980
|
+
}));
|
|
981
|
+
}
|
|
982
|
+
const decrypted = [];
|
|
983
|
+
const cursorParams = opts?.cursorParams ?? {};
|
|
984
|
+
const ownsCursor = opts?.ownsCursor !== false;
|
|
985
|
+
let nextAfterSeq = opts?.explicitAfterSeq ? afterSeq : (afterSeq || client._seqTracker.getContiguousSeq(ns));
|
|
986
|
+
let pageCount = 0;
|
|
987
|
+
const maxPages = 100;
|
|
988
|
+
while (pageCount < maxPages) {
|
|
989
|
+
pageCount += 1;
|
|
990
|
+
client._clientLog.debug(`group.v2.pull page request: group=${gid}, page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ns=${ns}`);
|
|
991
|
+
const result = await client._callRawV2Rpc('group.v2.pull', {
|
|
992
|
+
group_id: gid,
|
|
993
|
+
after_seq: nextAfterSeq,
|
|
994
|
+
limit,
|
|
995
|
+
...cursorParams,
|
|
996
|
+
});
|
|
997
|
+
const messages = (Array.isArray(result.messages) ? result.messages : []);
|
|
998
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
999
|
+
client._clientLog.debug(`group.v2.pull page response: group=${gid}, page=${pageCount}, raw_count=${messages.length}, has_more=${String(result.has_more ?? '')}, cursor_current=${String(cursor?.current_seq ?? '')}`);
|
|
1000
|
+
for (const msg of messages) {
|
|
1001
|
+
client._logMessageDebug('pull-raw', 'group.v2.pull', 'group.message_created', msg);
|
|
1002
|
+
}
|
|
1003
|
+
const seqs = messages
|
|
1004
|
+
.map((msg) => Number(msg.seq ?? 0))
|
|
1005
|
+
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
1006
|
+
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
1007
|
+
let pageMaxSeq = nextAfterSeq;
|
|
1008
|
+
if (seqs.length > 0) {
|
|
1009
|
+
pageMaxSeq = Math.max(...seqs);
|
|
1010
|
+
client._seqTracker.forceContiguousSeq(ns, pageMaxSeq);
|
|
1011
|
+
client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
1012
|
+
}
|
|
1013
|
+
for (const msg of messages) {
|
|
1014
|
+
const seq = Number(msg.seq ?? 0);
|
|
1015
|
+
if (!Number.isFinite(seq) || seq <= 0)
|
|
1016
|
+
continue;
|
|
1017
|
+
const version = String(msg.version ?? 'v2');
|
|
1018
|
+
if (version === 'v1') {
|
|
1019
|
+
const payload = msg.payload;
|
|
1020
|
+
const payloadObj = isJsonObject(payload) ? payload : null;
|
|
1021
|
+
if (payloadObj) {
|
|
1022
|
+
const payloadType = String(payloadObj.type ?? '').trim();
|
|
1023
|
+
if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted') {
|
|
1024
|
+
const v1Msg = {
|
|
1025
|
+
message_id: String(msg.message_id ?? ''),
|
|
1026
|
+
from: String(msg.from_aid ?? ''),
|
|
1027
|
+
group_id: gid,
|
|
1028
|
+
seq: msg.seq,
|
|
1029
|
+
type: String(msg.type ?? ''),
|
|
1030
|
+
timestamp: msg.t_server,
|
|
1031
|
+
payload,
|
|
1032
|
+
encrypted: false,
|
|
1033
|
+
};
|
|
1034
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
|
|
1035
|
+
decrypted.push(v1Msg);
|
|
1036
|
+
client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
else if (payload !== undefined && payload !== null) {
|
|
1041
|
+
const v1Msg = {
|
|
1042
|
+
message_id: String(msg.message_id ?? ''),
|
|
1043
|
+
from: String(msg.from_aid ?? ''),
|
|
1044
|
+
group_id: gid,
|
|
1045
|
+
seq: msg.seq,
|
|
1046
|
+
type: String(msg.type ?? ''),
|
|
1047
|
+
timestamp: msg.t_server,
|
|
1048
|
+
payload,
|
|
1049
|
+
encrypted: false,
|
|
1050
|
+
};
|
|
1051
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
|
|
1052
|
+
decrypted.push(v1Msg);
|
|
1053
|
+
client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
if (version !== 'v2') {
|
|
1060
|
+
client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
const plaintext = await client._decryptV2Message(msg);
|
|
1064
|
+
if (plaintext === null) {
|
|
1065
|
+
client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
plaintext.group_id = gid;
|
|
1069
|
+
await client._publishPulledMessage('group.message_created', ns, seq, plaintext);
|
|
1070
|
+
decrypted.push(plaintext);
|
|
1071
|
+
client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
|
|
1072
|
+
}
|
|
1073
|
+
const cursorCurrentSeq = Number(cursor?.current_seq ?? 0);
|
|
1074
|
+
const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
|
|
1075
|
+
const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'), Number.isFinite(cursorCurrentSeq) ? cursorCurrentSeq : 0);
|
|
1076
|
+
if (retentionFloor > 0) {
|
|
1077
|
+
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
1078
|
+
if (contig < retentionFloor) {
|
|
1079
|
+
client._clientLog.info(`group.v2.pull retention-floor advance: ns=${ns} contiguous=${contig} -> retention_floor=${retentionFloor}`);
|
|
1080
|
+
client._seqTracker.forceContiguousSeq(ns, retentionFloor);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1084
|
+
const contigAdvanced = ackSeq !== pageContigBefore;
|
|
1085
|
+
if (contigAdvanced) {
|
|
1086
|
+
await client._drainOrderedMessages(ns, undefined, true);
|
|
1087
|
+
client._saveSeqTrackerState();
|
|
1088
|
+
}
|
|
1089
|
+
const ackNeeded = messages.length > 0
|
|
1090
|
+
&& ackSeq > 0
|
|
1091
|
+
&& ownsCursor
|
|
1092
|
+
&& (contigAdvanced || (hasServerCursor && ackSeq > cursorCurrentSeq));
|
|
1093
|
+
if (ackNeeded) {
|
|
1094
|
+
client._clientLog.debug(`group.v2.pull sending auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
|
|
1095
|
+
await this.ackGroupV2(gid, ackSeq);
|
|
1096
|
+
}
|
|
1097
|
+
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
1098
|
+
if (!ownsCursor)
|
|
1099
|
+
break;
|
|
1100
|
+
if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
|
|
1101
|
+
break;
|
|
1102
|
+
nextAfterSeq = nextAfter;
|
|
1103
|
+
}
|
|
1104
|
+
if (pageCount >= maxPages) {
|
|
1105
|
+
client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
|
|
1106
|
+
}
|
|
1107
|
+
client._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns}`);
|
|
1108
|
+
return decrypted;
|
|
1109
|
+
}
|
|
1110
|
+
async ackGroupV2(groupId, upToSeq) {
|
|
1111
|
+
const client = this.client;
|
|
1112
|
+
const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
|
|
1113
|
+
if (!gid)
|
|
1114
|
+
throw new ValidationError('group.ack_messages requires group_id');
|
|
1115
|
+
const ns = `group:${gid}`;
|
|
1116
|
+
let seq = Number(upToSeq ?? client._seqTracker.getContiguousSeq(ns));
|
|
1117
|
+
if (!Number.isFinite(seq) || seq <= 0) {
|
|
1118
|
+
client._clientLog.debug(`group.v2.ack skipped: group=${gid}, ns=${ns}, up_to_seq=${String(upToSeq ?? '')}`);
|
|
1119
|
+
return { acked: 0 };
|
|
1120
|
+
}
|
|
1121
|
+
seq = client._clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
|
|
1122
|
+
client._clientLog.debug(`group.v2.ack send: group=${gid}, ns=${ns}, up_to_seq=${seq}`);
|
|
1123
|
+
const result = await client._callRawV2Rpc('group.v2.ack', { group_id: gid, up_to_seq: seq });
|
|
1124
|
+
client._clientLog.debug(`group.v2.ack ok: group=${gid}, ns=${ns}, requested=${seq}, result=${client._debugJson(result)}`);
|
|
1125
|
+
return result;
|
|
1126
|
+
}
|
|
1127
|
+
async putMessageThoughtEncryptedV2(params) {
|
|
1128
|
+
const client = this.client;
|
|
1129
|
+
const toAid = String(params.to ?? '').trim();
|
|
1130
|
+
client._validateMessageRecipient(toAid);
|
|
1131
|
+
const payload = isJsonObject(params.payload) ? params.payload : null;
|
|
1132
|
+
if (!toAid)
|
|
1133
|
+
throw new ValidationError('message.thought.put requires to');
|
|
1134
|
+
if (payload === null)
|
|
1135
|
+
throw new ValidationError('message.thought.put payload must be an object when encrypt=true');
|
|
1136
|
+
const thoughtId = String(params.thought_id ?? '').trim() || `mt-${crypto.randomUUID()}`;
|
|
1137
|
+
const timestamp = Number(params.timestamp ?? Date.now());
|
|
1138
|
+
const protectedHeaders = client._protectedHeadersFromParams(params);
|
|
1139
|
+
client._logMessageDebug('thought-send-plaintext', 'message.thought.put.v2', 'message.thought.put', {
|
|
1140
|
+
to: toAid,
|
|
1141
|
+
thought_id: thoughtId,
|
|
1142
|
+
timestamp,
|
|
1143
|
+
payload,
|
|
1144
|
+
}, { payloadOverride: payload });
|
|
1145
|
+
const attempt = async (useCache) => {
|
|
1146
|
+
client._clientLog.debug(`message.thought.put attempt: to=${toAid}, thought_id=${thoughtId}, use_cache=${useCache}`);
|
|
1147
|
+
const context = isJsonObject(params.context) ? params.context : undefined;
|
|
1148
|
+
const envelope = await client._buildV2P2PEnvelope({
|
|
1149
|
+
to: toAid,
|
|
1150
|
+
payload,
|
|
1151
|
+
messageId: thoughtId,
|
|
1152
|
+
timestamp,
|
|
1153
|
+
useCache,
|
|
1154
|
+
protectedHeaders,
|
|
1155
|
+
context,
|
|
1156
|
+
});
|
|
1157
|
+
const sendParams = {
|
|
1158
|
+
to: toAid,
|
|
1159
|
+
payload: envelope,
|
|
1160
|
+
encrypted: true,
|
|
1161
|
+
thought_id: thoughtId,
|
|
1162
|
+
timestamp,
|
|
1163
|
+
};
|
|
1164
|
+
if ('context' in params)
|
|
1165
|
+
sendParams.context = params.context;
|
|
1166
|
+
client._signClientOperation('message.thought.put', sendParams);
|
|
1167
|
+
client._logMessageDebug('thought-send-envelope', 'message.thought.put.v2', 'message.thought.put', sendParams, {
|
|
1168
|
+
payloadOverride: envelope,
|
|
1169
|
+
extra: { to: toAid, thought_id: thoughtId, use_cache: useCache },
|
|
1170
|
+
});
|
|
1171
|
+
const result = await client._transport.call('message.thought.put', sendParams);
|
|
1172
|
+
client._clientLog.debug(`message.thought.put ok: to=${toAid}, thought_id=${thoughtId}, use_cache=${useCache}`);
|
|
1173
|
+
return result;
|
|
1174
|
+
};
|
|
1175
|
+
try {
|
|
1176
|
+
return await attempt(true);
|
|
1177
|
+
}
|
|
1178
|
+
catch (exc) {
|
|
1179
|
+
const excCode = Number(exc?.code);
|
|
1180
|
+
if (V2_RETRYABLE_CODES.has(excCode)) {
|
|
1181
|
+
client._clientLog.debug(`V2 P2P thought put speculative rejected (code=${String(excCode)}), refreshing bootstrap`);
|
|
1182
|
+
this.deleteBootstrapCacheEntry(toAid);
|
|
1183
|
+
return await attempt(false);
|
|
1184
|
+
}
|
|
1185
|
+
throw exc;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
async putGroupThoughtEncryptedV2(params) {
|
|
1189
|
+
const client = this.client;
|
|
1190
|
+
const groupId = String(params.group_id ?? '').trim();
|
|
1191
|
+
const payload = isJsonObject(params.payload) ? params.payload : null;
|
|
1192
|
+
if (!groupId)
|
|
1193
|
+
throw new ValidationError("group.thought.put requires 'group_id'");
|
|
1194
|
+
if (payload === null)
|
|
1195
|
+
throw new ValidationError('group.thought.put payload must be an object when encrypt=true');
|
|
1196
|
+
const thoughtId = String(params.thought_id ?? '').trim() || `gt-${crypto.randomUUID()}`;
|
|
1197
|
+
const timestamp = Number(params.timestamp ?? Date.now());
|
|
1198
|
+
const protectedHeaders = client._protectedHeadersFromParams(params);
|
|
1199
|
+
client._logMessageDebug('thought-send-plaintext', 'group.thought.put.v2', 'group.thought.put', {
|
|
1200
|
+
group_id: groupId,
|
|
1201
|
+
thought_id: thoughtId,
|
|
1202
|
+
timestamp,
|
|
1203
|
+
payload,
|
|
1204
|
+
}, { payloadOverride: payload });
|
|
1205
|
+
const attempt = async (useCache) => {
|
|
1206
|
+
client._clientLog.debug(`group.thought.put attempt: group=${groupId}, thought_id=${thoughtId}, use_cache=${useCache}`);
|
|
1207
|
+
const context = isJsonObject(params.context) ? params.context : undefined;
|
|
1208
|
+
const envelope = await client._buildV2GroupEnvelope({
|
|
1209
|
+
groupId,
|
|
1210
|
+
payload,
|
|
1211
|
+
messageId: thoughtId,
|
|
1212
|
+
timestamp,
|
|
1213
|
+
useCache,
|
|
1214
|
+
protectedHeaders,
|
|
1215
|
+
context,
|
|
1216
|
+
});
|
|
1217
|
+
const sendParams = {
|
|
1218
|
+
group_id: groupId,
|
|
1219
|
+
payload: envelope,
|
|
1220
|
+
encrypted: true,
|
|
1221
|
+
thought_id: thoughtId,
|
|
1222
|
+
timestamp,
|
|
1223
|
+
};
|
|
1224
|
+
if ('context' in params)
|
|
1225
|
+
sendParams.context = params.context;
|
|
1226
|
+
client._signClientOperation('group.thought.put', sendParams);
|
|
1227
|
+
client._logMessageDebug('thought-send-envelope', 'group.thought.put.v2', 'group.thought.put', sendParams, {
|
|
1228
|
+
payloadOverride: envelope,
|
|
1229
|
+
extra: { group_id: groupId, thought_id: thoughtId, use_cache: useCache },
|
|
1230
|
+
});
|
|
1231
|
+
const result = await client._transport.call('group.thought.put', sendParams);
|
|
1232
|
+
client._clientLog.debug(`group.thought.put ok: group=${groupId}, thought_id=${thoughtId}, use_cache=${useCache}`);
|
|
1233
|
+
return result;
|
|
1234
|
+
};
|
|
1235
|
+
try {
|
|
1236
|
+
return await attempt(true);
|
|
1237
|
+
}
|
|
1238
|
+
catch (exc) {
|
|
1239
|
+
const excCode = Number(exc?.code);
|
|
1240
|
+
if (V2_RETRYABLE_CODES.has(excCode)) {
|
|
1241
|
+
client._clientLog.debug(`V2 group thought put speculative rejected (code=${String(excCode)}), refreshing bootstrap`);
|
|
1242
|
+
this.deleteBootstrapCacheEntry(`group:${groupId}`);
|
|
1243
|
+
return await attempt(false);
|
|
1244
|
+
}
|
|
1245
|
+
throw exc;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
async decryptGroupThoughts(result) {
|
|
1249
|
+
const client = this.client;
|
|
1250
|
+
client._clientLog.debug(`group.thought.get decrypt enter: found=${String(result.found ?? '')}, group=${String(result.group_id ?? '')}, sender=${String(result.sender_aid ?? '')}`);
|
|
1251
|
+
if (!result.found) {
|
|
1252
|
+
client._clientLog.debug('group.thought.get decrypt exit: not found');
|
|
1253
|
+
return { ...result, thoughts: [] };
|
|
1254
|
+
}
|
|
1255
|
+
const items = Array.isArray(result.thoughts) ? result.thoughts.filter(isJsonObject) : [];
|
|
1256
|
+
if (items.length === 0) {
|
|
1257
|
+
client._clientLog.debug('group.thought.get decrypt exit: empty thoughts');
|
|
1258
|
+
return { ...result, thoughts: [] };
|
|
1259
|
+
}
|
|
1260
|
+
const groupId = String(result.group_id ?? '');
|
|
1261
|
+
const senderAid = String(result.sender_aid ?? '');
|
|
1262
|
+
const thoughts = [];
|
|
1263
|
+
for (const item of items) {
|
|
1264
|
+
const payload = isJsonObject(item.payload) ? item.payload : null;
|
|
1265
|
+
const thoughtId = String(item.thought_id ?? item.message_id ?? '');
|
|
1266
|
+
const fromAid = String(item.from ?? item.sender_aid ?? senderAid);
|
|
1267
|
+
client._logMessageDebug('thought-get-raw', 'group.thought.get', 'group.thought.get', item, {
|
|
1268
|
+
extra: { group_id: groupId, thought_id: thoughtId, from: fromAid },
|
|
1269
|
+
});
|
|
1270
|
+
let decryptFailed = false;
|
|
1271
|
+
let decryptedPayload = payload ?? {};
|
|
1272
|
+
let e2ee;
|
|
1273
|
+
if (payload?.type === 'e2ee.group_encrypted' && String(payload.version ?? '') === 'v2') {
|
|
1274
|
+
e2ee = client._v2E2eeMeta(payload);
|
|
1275
|
+
const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid });
|
|
1276
|
+
if (plain === null) {
|
|
1277
|
+
decryptFailed = true;
|
|
1278
|
+
client._clientLog.debug(`group.thought.get decrypt returned null: group=${groupId}, thought_id=${thoughtId}, from=${fromAid}`);
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
decryptedPayload = plain;
|
|
1282
|
+
client._logMessageDebug('thought-decrypt-ok', 'group.thought.get', 'group.thought.get', {
|
|
1283
|
+
group_id: groupId,
|
|
1284
|
+
thought_id: thoughtId,
|
|
1285
|
+
from: fromAid,
|
|
1286
|
+
payload: plain,
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
else if (payload?.type === 'e2ee.group_encrypted') {
|
|
1291
|
+
decryptFailed = true;
|
|
1292
|
+
client._clientLog.debug(`group.thought.get unsupported encrypted payload: group=${groupId}, thought_id=${thoughtId}, type=${String(payload.type ?? '')}, version=${String(payload.version ?? '')}`);
|
|
1293
|
+
}
|
|
1294
|
+
const thought = {
|
|
1295
|
+
thought_id: thoughtId,
|
|
1296
|
+
message_id: thoughtId,
|
|
1297
|
+
payload: decryptFailed ? (payload ?? {}) : decryptedPayload,
|
|
1298
|
+
created_at: item.created_at,
|
|
1299
|
+
};
|
|
1300
|
+
if (e2ee !== undefined) {
|
|
1301
|
+
thought.e2ee = e2ee;
|
|
1302
|
+
if (isJsonObject(e2ee))
|
|
1303
|
+
client._attachV2EnvelopeMetadata(thought, e2ee);
|
|
1304
|
+
}
|
|
1305
|
+
if (decryptFailed)
|
|
1306
|
+
thought.decrypt_failed = true;
|
|
1307
|
+
if ('context' in item)
|
|
1308
|
+
thought.context = item.context;
|
|
1309
|
+
client._logMessageDebug(decryptFailed ? 'thought-decrypt-fail' : 'thought-result', 'group.thought.get', 'group.thought.get', thought, {
|
|
1310
|
+
extra: { group_id: groupId, thought_id: thoughtId },
|
|
1311
|
+
});
|
|
1312
|
+
thoughts.push(thought);
|
|
1313
|
+
}
|
|
1314
|
+
client._clientLog.debug(`group.thought.get decrypt exit: group=${groupId}, total=${items.length}, returned=${thoughts.length}`);
|
|
1315
|
+
return { ...result, thoughts };
|
|
1316
|
+
}
|
|
1317
|
+
async decryptMessageThoughts(result) {
|
|
1318
|
+
const client = this.client;
|
|
1319
|
+
client._clientLog.debug(`message.thought.get decrypt enter: found=${String(result.found ?? '')}, peer=${String(result.peer_aid ?? '')}, sender=${String(result.sender_aid ?? '')}`);
|
|
1320
|
+
if (!result.found) {
|
|
1321
|
+
client._clientLog.debug('message.thought.get decrypt exit: not found');
|
|
1322
|
+
return { ...result, thoughts: [] };
|
|
1323
|
+
}
|
|
1324
|
+
const items = Array.isArray(result.thoughts) ? result.thoughts.filter(isJsonObject) : [];
|
|
1325
|
+
if (items.length === 0) {
|
|
1326
|
+
client._clientLog.debug('message.thought.get decrypt exit: empty thoughts');
|
|
1327
|
+
return { ...result, thoughts: [] };
|
|
1328
|
+
}
|
|
1329
|
+
const senderAid = String(result.sender_aid ?? '');
|
|
1330
|
+
const peerAid = String(result.peer_aid ?? '');
|
|
1331
|
+
const thoughts = [];
|
|
1332
|
+
for (const item of items) {
|
|
1333
|
+
const payload = isJsonObject(item.payload) ? item.payload : null;
|
|
1334
|
+
const thoughtId = String(item.thought_id ?? item.message_id ?? '');
|
|
1335
|
+
const fromAid = String(item.from ?? senderAid);
|
|
1336
|
+
const toAid = String(item.to ?? peerAid);
|
|
1337
|
+
client._logMessageDebug('thought-get-raw', 'message.thought.get', 'message.thought.get', item, {
|
|
1338
|
+
extra: { thought_id: thoughtId, from: fromAid, to: toAid },
|
|
1339
|
+
});
|
|
1340
|
+
let decryptFailed = false;
|
|
1341
|
+
let decryptedPayload = payload ?? {};
|
|
1342
|
+
let e2ee;
|
|
1343
|
+
if (payload?.type === 'e2ee.p2p_encrypted' && String(payload.version ?? '') === 'v2') {
|
|
1344
|
+
e2ee = client._v2E2eeMeta(payload);
|
|
1345
|
+
const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid });
|
|
1346
|
+
if (plain === null) {
|
|
1347
|
+
decryptFailed = true;
|
|
1348
|
+
client._clientLog.debug(`message.thought.get decrypt returned null: thought_id=${thoughtId}, from=${fromAid}, to=${toAid}`);
|
|
1349
|
+
}
|
|
1350
|
+
else {
|
|
1351
|
+
decryptedPayload = plain;
|
|
1352
|
+
client._logMessageDebug('thought-decrypt-ok', 'message.thought.get', 'message.thought.get', {
|
|
1353
|
+
thought_id: thoughtId,
|
|
1354
|
+
from: fromAid,
|
|
1355
|
+
to: toAid,
|
|
1356
|
+
payload: plain,
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
else if (payload?.type === 'e2ee.encrypted' || payload?.type === 'e2ee.p2p_encrypted') {
|
|
1361
|
+
decryptFailed = true;
|
|
1362
|
+
client._clientLog.debug(`message.thought.get unsupported encrypted payload: thought_id=${thoughtId}, type=${String(payload.type ?? '')}, version=${String(payload.version ?? '')}`);
|
|
1363
|
+
}
|
|
1364
|
+
const thought = {
|
|
1365
|
+
thought_id: thoughtId,
|
|
1366
|
+
message_id: thoughtId,
|
|
1367
|
+
from: fromAid,
|
|
1368
|
+
to: toAid,
|
|
1369
|
+
payload: decryptFailed ? (payload ?? {}) : decryptedPayload,
|
|
1370
|
+
created_at: item.created_at,
|
|
1371
|
+
};
|
|
1372
|
+
if (e2ee !== undefined) {
|
|
1373
|
+
thought.e2ee = e2ee;
|
|
1374
|
+
if (isJsonObject(e2ee))
|
|
1375
|
+
client._attachV2EnvelopeMetadata(thought, e2ee);
|
|
1376
|
+
}
|
|
1377
|
+
if (decryptFailed)
|
|
1378
|
+
thought.decrypt_failed = true;
|
|
1379
|
+
if ('context' in item)
|
|
1380
|
+
thought.context = item.context;
|
|
1381
|
+
client._logMessageDebug(decryptFailed ? 'thought-decrypt-fail' : 'thought-result', 'message.thought.get', 'message.thought.get', thought, {
|
|
1382
|
+
extra: { thought_id: thoughtId },
|
|
1383
|
+
});
|
|
1384
|
+
thoughts.push(thought);
|
|
1385
|
+
}
|
|
1386
|
+
client._clientLog.debug(`message.thought.get decrypt exit: total=${items.length}, returned=${thoughts.length}`);
|
|
1387
|
+
return { ...result, thoughts };
|
|
1388
|
+
}
|
|
1389
|
+
async decryptV2EnvelopeForThought(opts) {
|
|
1390
|
+
const client = this.client;
|
|
1391
|
+
const session = client._v2Session;
|
|
1392
|
+
if (!session || !opts.envelope)
|
|
1393
|
+
return null;
|
|
1394
|
+
const envelope = opts.envelope;
|
|
1395
|
+
let spkId = '';
|
|
1396
|
+
let recipientKeySource = '';
|
|
1397
|
+
if (Array.isArray(envelope.recipients)) {
|
|
1398
|
+
for (const row of envelope.recipients) {
|
|
1399
|
+
if (!Array.isArray(row) || row.length < 6)
|
|
1400
|
+
continue;
|
|
1401
|
+
if (String(row[0] ?? '') === client._aid
|
|
1402
|
+
&& (String(row[1] ?? '') === client._deviceId || String(row[1] ?? '') === '')) {
|
|
1403
|
+
spkId = String(row[5] ?? '');
|
|
1404
|
+
recipientKeySource = String(row[3] ?? '');
|
|
1405
|
+
break;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
else if (isJsonObject(envelope.recipient)) {
|
|
1410
|
+
const recipient = envelope.recipient;
|
|
1411
|
+
spkId = String(recipient.spk_id ?? '');
|
|
1412
|
+
recipientKeySource = String(recipient.key_source ?? '');
|
|
1413
|
+
}
|
|
1414
|
+
const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
|
|
1415
|
+
const groupIdForKeys = String(aad.group_id ?? envelope.group_id ?? '').trim();
|
|
1416
|
+
const fromAid = String(opts.fromAid || aad.from || '').trim();
|
|
1417
|
+
const senderDeviceId = String(aad.from_device ?? '');
|
|
1418
|
+
client._clientLog.debug(`V2 thought decrypt start: from=${fromAid}, sender_device=${senderDeviceId}, group=${groupIdForKeys || '<p2p>'}, spk_id=${spkId || '<empty>'}, key_source=${recipientKeySource || '<empty>'}, type=${String(envelope.type ?? '')}`);
|
|
1419
|
+
let ikPriv;
|
|
1420
|
+
let spkPriv;
|
|
1421
|
+
try {
|
|
1422
|
+
if (groupIdForKeys) {
|
|
1423
|
+
const keys = session.getGroupDecryptKeys(groupIdForKeys, spkId);
|
|
1424
|
+
ikPriv = keys.ikPriv;
|
|
1425
|
+
spkPriv = keys.spkPriv ?? undefined;
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
const keys = session.getDecryptKeys(spkId);
|
|
1429
|
+
ikPriv = keys.ikPriv;
|
|
1430
|
+
spkPriv = keys.spkPriv;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
catch (exc) {
|
|
1434
|
+
client._clientLog.warn(`V2 thought decrypt: SPK lookup failed from=${fromAid}, group=${groupIdForKeys || '<p2p>'}, spk_id=${spkId || '<empty>'}: ${formatE2EEError(exc)}`);
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1437
|
+
const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
1438
|
+
const senderPubDer = await this.getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
|
|
1439
|
+
if (!senderPubDer) {
|
|
1440
|
+
client._clientLog.warn(`V2 thought decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
|
|
1441
|
+
this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupIdForKeys);
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
try {
|
|
1445
|
+
const plain = decryptMessage(envelope, client._aid ?? '', client._deviceId, ikPriv, spkPriv, senderPubDer);
|
|
1446
|
+
client._clientLog.debug(`V2 thought decrypt ok: from=${fromAid}, sender_device=${senderDeviceId}, group=${groupIdForKeys || '<p2p>'}`);
|
|
1447
|
+
if (plain !== null) {
|
|
1448
|
+
if (groupIdForKeys && recipientKeySource === 'group_device_prekey' && session.isLastUploadedGroupSPK(groupIdForKeys, spkId)) {
|
|
1449
|
+
this.scheduleGroupSpkRotation(groupIdForKeys, { reason: 'thought_group_spk_consumed' });
|
|
1450
|
+
}
|
|
1451
|
+
else if (groupIdForKeys && recipientKeySource === 'peer_device_prekey') {
|
|
1452
|
+
this.scheduleGroupSpkRegistrationAfterPeerFallback(groupIdForKeys);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return plain;
|
|
1456
|
+
}
|
|
1457
|
+
catch (exc) {
|
|
1458
|
+
client._clientLog.warn(`V2 thought decrypt failed from=${fromAid}: ${formatE2EEError(exc)}`);
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
encryptedPushEnvelope(msg) {
|
|
1463
|
+
const payload = msg.payload;
|
|
1464
|
+
if (this.isEncryptedEnvelopePayload(payload))
|
|
1465
|
+
return payload;
|
|
1466
|
+
if (typeof msg.envelope_json === 'string' && msg.envelope_json.trim()) {
|
|
1467
|
+
try {
|
|
1468
|
+
const parsed = JSON.parse(msg.envelope_json);
|
|
1469
|
+
if (this.isEncryptedEnvelopePayload(parsed))
|
|
1470
|
+
return parsed;
|
|
1471
|
+
}
|
|
1472
|
+
catch {
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
isEncryptedPushMessage(msg) {
|
|
1479
|
+
if (truthyBool(msg.encrypted))
|
|
1480
|
+
return true;
|
|
1481
|
+
return this.encryptedPushEnvelope(msg) !== null;
|
|
1482
|
+
}
|
|
1483
|
+
isEncryptedEnvelopePayload(payload) {
|
|
1484
|
+
if (!isJsonObject(payload))
|
|
1485
|
+
return false;
|
|
1486
|
+
const envelope = payload;
|
|
1487
|
+
const payloadType = String(envelope.type ?? '').trim();
|
|
1488
|
+
if (payloadType.startsWith('e2ee.'))
|
|
1489
|
+
return true;
|
|
1490
|
+
if (!String(envelope.ciphertext ?? '').trim())
|
|
1491
|
+
return false;
|
|
1492
|
+
return envelope.nonce !== undefined
|
|
1493
|
+
|| envelope.tag !== undefined
|
|
1494
|
+
|| envelope.recipient !== undefined
|
|
1495
|
+
|| envelope.recipients !== undefined
|
|
1496
|
+
|| envelope.wrapped_key !== undefined
|
|
1497
|
+
|| envelope.recipients_digest !== undefined;
|
|
1498
|
+
}
|
|
1499
|
+
isV2EncryptedEnvelopePayload(envelope) {
|
|
1500
|
+
if (!envelope)
|
|
1501
|
+
return false;
|
|
1502
|
+
const payloadType = String(envelope.type ?? '').trim();
|
|
1503
|
+
if (payloadType === 'e2ee.p2p_encrypted' || payloadType === 'e2ee.group_encrypted')
|
|
1504
|
+
return true;
|
|
1505
|
+
return String(envelope.version ?? '').trim().toLowerCase() === 'v2' && payloadType.startsWith('e2ee.');
|
|
1506
|
+
}
|
|
1507
|
+
safeUndecryptablePushEvent(msg, group) {
|
|
1508
|
+
const client = this.client;
|
|
1509
|
+
const event = {
|
|
1510
|
+
message_id: msg.message_id,
|
|
1511
|
+
from: msg.from,
|
|
1512
|
+
seq: msg.seq,
|
|
1513
|
+
timestamp: (msg.timestamp ?? msg.t_server),
|
|
1514
|
+
device_id: msg.device_id,
|
|
1515
|
+
slot_id: msg.slot_id,
|
|
1516
|
+
_decrypt_error: 'encrypted push payload is not decryptable on raw push path',
|
|
1517
|
+
_decrypt_stage: 'push_envelope',
|
|
1518
|
+
};
|
|
1519
|
+
if (group) {
|
|
1520
|
+
event.group_id = msg.group_id;
|
|
1521
|
+
}
|
|
1522
|
+
else {
|
|
1523
|
+
event.to = msg.to;
|
|
1524
|
+
}
|
|
1525
|
+
const envelope = this.encryptedPushEnvelope(msg);
|
|
1526
|
+
if (envelope) {
|
|
1527
|
+
event._envelope_type = String(envelope.type ?? '');
|
|
1528
|
+
event._suite = String(envelope.suite ?? '');
|
|
1529
|
+
if (this.isV2EncryptedEnvelopePayload(envelope)) {
|
|
1530
|
+
client._attachV2EnvelopeMetadata(event, client._v2E2eeMeta(envelope));
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return event;
|
|
1534
|
+
}
|
|
1535
|
+
async decryptEncryptedPushPayload(msg, group) {
|
|
1536
|
+
const client = this.client;
|
|
1537
|
+
const envelope = this.encryptedPushEnvelope(msg);
|
|
1538
|
+
if (!this.isV2EncryptedEnvelopePayload(envelope))
|
|
1539
|
+
return null;
|
|
1540
|
+
const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
|
|
1541
|
+
const fromAid = String(msg.from_aid ?? msg.from ?? msg.sender_aid ?? aad.from ?? '').trim();
|
|
1542
|
+
const plaintext = await client._decryptV2EnvelopeForThought({ envelope, fromAid });
|
|
1543
|
+
if (!plaintext)
|
|
1544
|
+
return null;
|
|
1545
|
+
const e2ee = client._v2E2eeMeta(envelope);
|
|
1546
|
+
const result = {
|
|
1547
|
+
message_id: String(msg.message_id ?? ''),
|
|
1548
|
+
from: fromAid,
|
|
1549
|
+
seq: msg.seq,
|
|
1550
|
+
timestamp: (msg.t_server ?? msg.timestamp),
|
|
1551
|
+
payload: plaintext,
|
|
1552
|
+
encrypted: true,
|
|
1553
|
+
e2ee,
|
|
1554
|
+
};
|
|
1555
|
+
result.direction = fromAid && fromAid === client._aid ? 'outbound_sync' : 'inbound';
|
|
1556
|
+
if (msg.t_server !== undefined)
|
|
1557
|
+
result.t_server = msg.t_server;
|
|
1558
|
+
if (msg.device_id !== undefined)
|
|
1559
|
+
result.device_id = msg.device_id;
|
|
1560
|
+
if (msg.slot_id !== undefined)
|
|
1561
|
+
result.slot_id = msg.slot_id;
|
|
1562
|
+
attachGatewayProximity(result, msg);
|
|
1563
|
+
if (group) {
|
|
1564
|
+
result.group_id = (msg.group_id ?? aad.group_id ?? envelope.group_id);
|
|
1565
|
+
}
|
|
1566
|
+
else {
|
|
1567
|
+
result.to = (msg.to ?? client._aid ?? '');
|
|
1568
|
+
}
|
|
1569
|
+
client._attachV2EnvelopeMetadata(result, e2ee);
|
|
1570
|
+
client._logMessageDebug('decrypt-ok', 'push.encrypted', group ? 'group.message_created' : 'message.received', result);
|
|
1571
|
+
return result;
|
|
1572
|
+
}
|
|
1573
|
+
async publishEncryptedPushAsUndecryptable(event, ns, seq, msg, group) {
|
|
1574
|
+
const client = this.client;
|
|
1575
|
+
const safeEvent = this.safeUndecryptablePushEvent(msg, group);
|
|
1576
|
+
client._logMessageDebug('decrypt-fail', 'push.encrypted', event, safeEvent);
|
|
1577
|
+
if (ns) {
|
|
1578
|
+
return await client._publishOrderedMessage(event, ns, seq, safeEvent);
|
|
1579
|
+
}
|
|
1580
|
+
const published = client._publishAppEvent(event, safeEvent, 'push');
|
|
1581
|
+
if (published && typeof published.then === 'function')
|
|
1582
|
+
await published;
|
|
1583
|
+
return true;
|
|
1584
|
+
}
|
|
1585
|
+
async publishEncryptedPushMessage(normalEvent, undecryptableEvent, ns, seq, msg, group) {
|
|
1586
|
+
const client = this.client;
|
|
1587
|
+
const decrypted = await this.decryptEncryptedPushPayload(msg, group);
|
|
1588
|
+
if (decrypted) {
|
|
1589
|
+
if (ns)
|
|
1590
|
+
return await client._publishOrderedMessage(normalEvent, ns, seq, decrypted);
|
|
1591
|
+
const published = client._publishAppEvent(normalEvent, decrypted, 'push');
|
|
1592
|
+
if (published && typeof published.then === 'function')
|
|
1593
|
+
await published;
|
|
1594
|
+
return true;
|
|
1595
|
+
}
|
|
1596
|
+
return await this.publishEncryptedPushAsUndecryptable(undecryptableEvent, ns, seq, msg, group);
|
|
1597
|
+
}
|
|
1598
|
+
async decryptV2PushMessage(data) {
|
|
1599
|
+
if (!isJsonObject(data))
|
|
1600
|
+
return null;
|
|
1601
|
+
return await this.client._decryptV2Message(data);
|
|
1602
|
+
}
|
|
1603
|
+
handleGroupChangedSpk(data, groupId, action) {
|
|
1604
|
+
if (!this.client._v2Session || !groupId || !MEMBERSHIP_ACTIONS.has(action))
|
|
1605
|
+
return;
|
|
1606
|
+
const joinedAid = String(data.joined_aid ?? data.member_aid ?? data.aid ?? '').trim();
|
|
1607
|
+
const actorAid = String(data.actor_aid ?? '').trim();
|
|
1608
|
+
const selfAid = String(this.client._aid ?? '').trim();
|
|
1609
|
+
const isSelfJoin = JOIN_ACTIONS.has(action) && !!selfAid && (joinedAid === selfAid ||
|
|
1610
|
+
(!joinedAid && (action === 'joined' || action === 'invite_code_used') && actorAid === selfAid));
|
|
1611
|
+
if (isSelfJoin) {
|
|
1612
|
+
this.scheduleGroupSpkRegistration(groupId, { reason: `group_changed:${action}` });
|
|
1613
|
+
}
|
|
1614
|
+
else {
|
|
1615
|
+
this.scheduleGroupSpkRotation(groupId, { reason: `group_changed:${action}` });
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
async handleV2EpochRotated(data) {
|
|
1619
|
+
const client = this.client;
|
|
1620
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
1621
|
+
return;
|
|
1622
|
+
const d = data;
|
|
1623
|
+
const groupId = String(d.group_id ?? '').trim();
|
|
1624
|
+
if (!groupId)
|
|
1625
|
+
return;
|
|
1626
|
+
const epoch = d.epoch ?? 0;
|
|
1627
|
+
client._clientLog.debug(`_onV2EpochRotated: group=${groupId} epoch=${String(epoch)}`);
|
|
1628
|
+
this.deleteBootstrapCacheEntry(`group:${groupId}`);
|
|
1629
|
+
if (!client._v2Session)
|
|
1630
|
+
return;
|
|
1631
|
+
try {
|
|
1632
|
+
await client._v2Session.rotateSPK(client._v2CallFn());
|
|
1633
|
+
client._clientLog.info(`SPK rotated after V2 epoch change: group=${groupId} epoch=${String(epoch)}`);
|
|
1634
|
+
}
|
|
1635
|
+
catch (exc) {
|
|
1636
|
+
client._clientLog.debug(`SPK rotation after V2 epoch change failed (non-fatal): ${formatE2EEError(exc)}`);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
//# sourceMappingURL=v2-e2ee.js.map
|