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