@oxidezap/baileyrs 0.0.7 → 0.0.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.
Files changed (167) hide show
  1. package/README.md +119 -5
  2. package/lib/Bridge/__tests__/adapt.test.d.ts +2 -0
  3. package/lib/Bridge/__tests__/adapt.test.d.ts.map +1 -0
  4. package/lib/Bridge/__tests__/adapt.test.js +355 -0
  5. package/lib/Bridge/__tests__/adapt.test.js.map +1 -0
  6. package/lib/Bridge/__tests__/dts-drift.test.d.ts +13 -0
  7. package/lib/Bridge/__tests__/dts-drift.test.d.ts.map +1 -0
  8. package/lib/Bridge/__tests__/dts-drift.test.js +92 -0
  9. package/lib/Bridge/__tests__/dts-drift.test.js.map +1 -0
  10. package/lib/Bridge/__tests__/fixtures.d.ts +192 -0
  11. package/lib/Bridge/__tests__/fixtures.d.ts.map +1 -0
  12. package/lib/Bridge/__tests__/fixtures.js +99 -0
  13. package/lib/Bridge/__tests__/fixtures.js.map +1 -0
  14. package/lib/Bridge/adapt.d.ts +28 -0
  15. package/lib/Bridge/adapt.d.ts.map +1 -0
  16. package/lib/Bridge/adapt.js +504 -0
  17. package/lib/Bridge/adapt.js.map +1 -0
  18. package/lib/Bridge/index.d.ts +12 -0
  19. package/lib/Bridge/index.d.ts.map +1 -0
  20. package/lib/Bridge/index.js +12 -0
  21. package/lib/Bridge/index.js.map +1 -0
  22. package/lib/Bridge/primitives.d.ts +56 -0
  23. package/lib/Bridge/primitives.d.ts.map +1 -0
  24. package/lib/Bridge/primitives.js +58 -0
  25. package/lib/Bridge/primitives.js.map +1 -0
  26. package/lib/Bridge/types.d.ts +273 -0
  27. package/lib/Bridge/types.d.ts.map +1 -0
  28. package/lib/Bridge/types.js +17 -0
  29. package/lib/Bridge/types.js.map +1 -0
  30. package/lib/Defaults/index.d.ts.map +1 -1
  31. package/lib/Defaults/index.js +2 -2
  32. package/lib/Defaults/index.js.map +1 -1
  33. package/lib/Socket/blocking.d.ts.map +1 -1
  34. package/lib/Socket/chat-actions.d.ts.map +1 -1
  35. package/lib/Socket/contacts.d.ts +9 -4
  36. package/lib/Socket/contacts.d.ts.map +1 -1
  37. package/lib/Socket/contacts.js +11 -8
  38. package/lib/Socket/contacts.js.map +1 -1
  39. package/lib/Socket/events.d.ts +7 -1
  40. package/lib/Socket/events.d.ts.map +1 -1
  41. package/lib/Socket/events.js +212 -243
  42. package/lib/Socket/events.js.map +1 -1
  43. package/lib/Socket/group-notifications.d.ts +25 -0
  44. package/lib/Socket/group-notifications.d.ts.map +1 -0
  45. package/lib/Socket/group-notifications.js +162 -0
  46. package/lib/Socket/group-notifications.js.map +1 -0
  47. package/lib/Socket/groups.d.ts +12 -1
  48. package/lib/Socket/groups.d.ts.map +1 -1
  49. package/lib/Socket/groups.js +44 -1
  50. package/lib/Socket/groups.js.map +1 -1
  51. package/lib/Socket/index.d.ts +47 -13
  52. package/lib/Socket/index.d.ts.map +1 -1
  53. package/lib/Socket/index.js +158 -24
  54. package/lib/Socket/index.js.map +1 -1
  55. package/lib/Socket/messages.d.ts +5 -2
  56. package/lib/Socket/messages.d.ts.map +1 -1
  57. package/lib/Socket/messages.js +42 -21
  58. package/lib/Socket/messages.js.map +1 -1
  59. package/lib/Socket/newsletter.d.ts.map +1 -1
  60. package/lib/Socket/presence.d.ts.map +1 -1
  61. package/lib/Socket/profile.d.ts +7 -1
  62. package/lib/Socket/profile.d.ts.map +1 -1
  63. package/lib/Socket/profile.js +20 -5
  64. package/lib/Socket/profile.js.map +1 -1
  65. package/lib/Socket/transport.d.ts.map +1 -1
  66. package/lib/Socket/transport.js +14 -13
  67. package/lib/Socket/transport.js.map +1 -1
  68. package/lib/Socket/types.d.ts.map +1 -1
  69. package/lib/Types/Auth.d.ts +1 -1
  70. package/lib/Types/Auth.d.ts.map +1 -1
  71. package/lib/Types/Bussines.d.ts +1 -1
  72. package/lib/Types/Bussines.d.ts.map +1 -1
  73. package/lib/Types/Chat.d.ts +1 -1
  74. package/lib/Types/Chat.d.ts.map +1 -1
  75. package/lib/Types/Events.d.ts +12 -3
  76. package/lib/Types/Events.d.ts.map +1 -1
  77. package/lib/Types/Events.js +1 -1
  78. package/lib/Types/Events.js.map +1 -1
  79. package/lib/Types/GroupMetadata.d.ts.map +1 -1
  80. package/lib/Types/Message.d.ts +16 -1
  81. package/lib/Types/Message.d.ts.map +1 -1
  82. package/lib/Types/Message.js +1 -1
  83. package/lib/Types/Message.js.map +1 -1
  84. package/lib/Types/Socket.d.ts +42 -1
  85. package/lib/Types/Socket.d.ts.map +1 -1
  86. package/lib/Types/State.d.ts +1 -1
  87. package/lib/Types/State.d.ts.map +1 -1
  88. package/lib/Types/index.d.ts +1 -1
  89. package/lib/Types/index.d.ts.map +1 -1
  90. package/lib/Types/index.js +17 -12
  91. package/lib/Types/index.js.map +1 -1
  92. package/lib/Utils/__tests__/_legacy-store-fixtures.d.ts +65 -0
  93. package/lib/Utils/__tests__/_legacy-store-fixtures.d.ts.map +1 -0
  94. package/lib/Utils/__tests__/_legacy-store-fixtures.js +107 -0
  95. package/lib/Utils/__tests__/_legacy-store-fixtures.js.map +1 -0
  96. package/lib/Utils/__tests__/wrap-legacy-store-coverage.test.d.ts +10 -0
  97. package/lib/Utils/__tests__/wrap-legacy-store-coverage.test.d.ts.map +1 -0
  98. package/lib/Utils/__tests__/wrap-legacy-store-coverage.test.js +298 -0
  99. package/lib/Utils/__tests__/wrap-legacy-store-coverage.test.js.map +1 -0
  100. package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.d.ts +8 -0
  101. package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.d.ts.map +1 -0
  102. package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.js +91 -0
  103. package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.js.map +1 -0
  104. package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.d.ts +10 -0
  105. package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.d.ts.map +1 -0
  106. package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.js +126 -0
  107. package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.js.map +1 -0
  108. package/lib/Utils/__tests__/wrap-legacy-store-session.test.d.ts +14 -0
  109. package/lib/Utils/__tests__/wrap-legacy-store-session.test.d.ts.map +1 -0
  110. package/lib/Utils/__tests__/wrap-legacy-store-session.test.js +309 -0
  111. package/lib/Utils/__tests__/wrap-legacy-store-session.test.js.map +1 -0
  112. package/lib/Utils/boom.d.ts +33 -0
  113. package/lib/Utils/boom.d.ts.map +1 -0
  114. package/lib/Utils/boom.js +63 -0
  115. package/lib/Utils/boom.js.map +1 -0
  116. package/lib/Utils/browser-utils.d.ts.map +1 -1
  117. package/lib/Utils/event-buffer.d.ts.map +1 -1
  118. package/lib/Utils/generics.d.ts +37 -1
  119. package/lib/Utils/generics.d.ts.map +1 -1
  120. package/lib/Utils/generics.js +60 -2
  121. package/lib/Utils/generics.js.map +1 -1
  122. package/lib/Utils/index.d.ts +1 -0
  123. package/lib/Utils/index.d.ts.map +1 -1
  124. package/lib/Utils/index.js +10 -9
  125. package/lib/Utils/index.js.map +1 -1
  126. package/lib/Utils/lid-mapping-store.d.ts +81 -0
  127. package/lib/Utils/lid-mapping-store.d.ts.map +1 -0
  128. package/lib/Utils/lid-mapping-store.js +126 -0
  129. package/lib/Utils/lid-mapping-store.js.map +1 -0
  130. package/lib/Utils/logger.d.ts.map +1 -1
  131. package/lib/Utils/logger.js +25 -1
  132. package/lib/Utils/logger.js.map +1 -1
  133. package/lib/Utils/messages-media.d.ts.map +1 -1
  134. package/lib/Utils/messages-media.js +1 -1
  135. package/lib/Utils/messages-media.js.map +1 -1
  136. package/lib/Utils/messages.d.ts +30 -1
  137. package/lib/Utils/messages.d.ts.map +1 -1
  138. package/lib/Utils/messages.js +115 -41
  139. package/lib/Utils/messages.js.map +1 -1
  140. package/lib/Utils/native-crypto-provider.d.ts +18 -0
  141. package/lib/Utils/native-crypto-provider.d.ts.map +1 -0
  142. package/lib/Utils/native-crypto-provider.js +67 -0
  143. package/lib/Utils/native-crypto-provider.js.map +1 -0
  144. package/lib/Utils/use-bridge-store.d.ts.map +1 -1
  145. package/lib/Utils/use-memory-store.d.ts.map +1 -1
  146. package/lib/Utils/use-memory-store.js +1 -1
  147. package/lib/Utils/use-memory-store.js.map +1 -1
  148. package/lib/Utils/use-multi-file-auth-state.d.ts.map +1 -1
  149. package/lib/Utils/use-multi-file-auth-state.js +1 -1
  150. package/lib/Utils/use-multi-file-auth-state.js.map +1 -1
  151. package/lib/Utils/wrap-legacy-store.d.ts.map +1 -1
  152. package/lib/Utils/wrap-legacy-store.js +505 -42
  153. package/lib/Utils/wrap-legacy-store.js.map +1 -1
  154. package/lib/WABinary/index.d.ts.map +1 -1
  155. package/lib/WABinary/index.js +1 -1
  156. package/lib/WABinary/index.js.map +1 -1
  157. package/lib/__tests__/expect.d.ts +30 -0
  158. package/lib/__tests__/expect.d.ts.map +1 -0
  159. package/lib/__tests__/expect.js +60 -0
  160. package/lib/__tests__/expect.js.map +1 -0
  161. package/lib/index.d.ts +1 -1
  162. package/lib/index.d.ts.map +1 -1
  163. package/lib/index.js +9 -6
  164. package/lib/index.js.map +1 -1
  165. package/package.json +33 -25
  166. package/WAProto/index.d.ts +0 -13875
  167. package/WAProto/index.js +0 -175
@@ -11,9 +11,10 @@
11
11
  * ```
12
12
  */
13
13
  import { Buffer } from 'node:buffer';
14
+ import { createHmac } from 'node:crypto';
14
15
  import { mkdir, readFile, stat, unlink, writeFile } from 'node:fs/promises';
15
16
  import { join } from 'node:path';
16
- import { proto } from '../../WAProto/index.js';
17
+ import { proto } from 'whatsapp-rust-bridge/proto-types';
17
18
  const protoLookup = proto;
18
19
  const ADVSignedDeviceIdentity = protoLookup.ADVSignedDeviceIdentity ?? protoLookup.AdvSignedDeviceIdentity;
19
20
  function warn(msg, err) {
@@ -46,11 +47,22 @@ const STORE_MAP = {
46
47
  mutation_mac: 'bridge-mutation-mac',
47
48
  meta: 'bridge-meta'
48
49
  };
49
- // Stores where bridge data is passthrough binary (no conversion)
50
- const BINARY_STORES = new Set(['identity', 'session', 'sender_key']);
51
- // Bridge-only stores raw binary persisted through keys interface
50
+ // Stores where bridge data is passthrough binary (no conversion).
51
+ // All Signal-protocol records (`identity`, `session`, `sender_key`) now
52
+ // route through converters because every one of them either:
53
+ // • has a different key shape between bridge and upstream, OR
54
+ // • has a different value-byte encoding (proto / JSON / 32-vs-33-byte
55
+ // prefix), OR
56
+ // • both.
57
+ // Keeping the empty set as an extension point — future bridge stores that
58
+ // are byte-compatible with upstream can land here without a converter.
59
+ const BINARY_STORES = new Set();
60
+ // Bridge-only stores — raw binary persisted through keys interface.
61
+ // `lid_mapping` is intentionally excluded — it has a converter so the
62
+ // `pn:`/`lid:` prefixed bridge keys translate to upstream's bare-userpart
63
+ // `{pnUser}` + `{lidUser}_reverse` shape, letting both libraries share
64
+ // the same on-disk LID↔PN mapping table.
52
65
  const BRIDGE_ONLY_STORES = new Set([
53
- 'lid_mapping',
54
66
  'signed_prekey',
55
67
  'sender_key_devices',
56
68
  'base_key',
@@ -203,8 +215,397 @@ const converters = {
203
215
  return [];
204
216
  }
205
217
  }
218
+ },
219
+ // identity: bridge holds 32-byte raw DJB pubkey, upstream holds 33 bytes
220
+ // (0x05 || 32-byte) — the curve25519-XEdDSA wire form.
221
+ identity: {
222
+ toBridge(_key, value) {
223
+ const buf = toBuf(value);
224
+ if (!buf)
225
+ return null;
226
+ return buf.length === 33 && buf[0] === 0x05 ? new Uint8Array(buf.slice(1)) : buf;
227
+ },
228
+ fromBridge(_key, value) {
229
+ const v = Buffer.isBuffer(value) ? value : Buffer.from(value);
230
+ return v.length === 32 ? Buffer.concat([Buffer.from([0x05]), v]) : Buffer.from(v);
231
+ }
232
+ },
233
+ // lid_mapping: bridge uses prefixed keys (`lid:{X}` JSON entry,
234
+ // `pn:{X}` raw LID bytes); upstream uses bare keys (`{pnUser}` string LID,
235
+ // `{lidUser}_reverse` string PN). No upstream timestamps; we synthesise.
236
+ lid_mapping: {
237
+ toBridge(key, value) {
238
+ try {
239
+ const asString = typeof value === 'string' ? value : Buffer.from(value).toString('utf-8');
240
+ if (!asString)
241
+ return null;
242
+ if (key.startsWith('pn:'))
243
+ return new TextEncoder().encode(asString);
244
+ if (key.startsWith('lid:')) {
245
+ const now = Math.floor(Date.now() / 1000);
246
+ return toJson({
247
+ lid: key.slice(4),
248
+ phone_number: asString,
249
+ created_at: now,
250
+ updated_at: now,
251
+ learning_source: 'wrap-legacy-store'
252
+ });
253
+ }
254
+ warn(`lid_mapping.toBridge: unknown key prefix in "${key}"`);
255
+ return null;
256
+ }
257
+ catch (e) {
258
+ warn('lid_mapping.toBridge failed:', e);
259
+ return null;
260
+ }
261
+ },
262
+ fromBridge(key, value) {
263
+ try {
264
+ if (key.startsWith('pn:'))
265
+ return Buffer.from(value).toString('utf-8');
266
+ if (key.startsWith('lid:')) {
267
+ const entry = fromJson(value);
268
+ return entry.phone_number ?? '';
269
+ }
270
+ warn(`lid_mapping.fromBridge: unknown key prefix in "${key}"`);
271
+ return Buffer.from(value);
272
+ }
273
+ catch (e) {
274
+ warn('lid_mapping.fromBridge failed:', e);
275
+ return Buffer.from(value);
276
+ }
277
+ }
278
+ },
279
+ // session: bridge=protobuf RecordStructure, upstream=JS object
280
+ // `{_sessions: {[b64BaseKey]: SessionEntry}, version: 'v1'}` with byte
281
+ // fields stored as base64 strings inside the entry. messageKeys cache
282
+ // is lossy in the Rust→JS direction (HKDF is one-way) — see
283
+ // `bridgeSessionProtoToUpstreamRecord`.
284
+ session: {
285
+ toBridge(_key, value) {
286
+ try {
287
+ if (value == null)
288
+ return null; // eslint-disable-line eqeqeq
289
+ return upstreamSessionRecordToProto(value);
290
+ }
291
+ catch (e) {
292
+ warn('session.toBridge encode failed:', e);
293
+ return null;
294
+ }
295
+ },
296
+ fromBridge(_key, value) {
297
+ try {
298
+ return bridgeSessionProtoToUpstreamRecord(value);
299
+ }
300
+ catch (e) {
301
+ warn('session.fromBridge decode failed:', e);
302
+ return Buffer.from(value);
303
+ }
304
+ }
305
+ },
306
+ // sender_key: bridge=protobuf SenderKeyRecordStructure, upstream=
307
+ // `Buffer.from(JSON.stringify(states, BufferJSON.replacer), 'utf-8')`
308
+ // — Buffer fields wrapped as `{type:'Buffer', data:'<base64>'}`.
309
+ sender_key: {
310
+ toBridge(_key, value) {
311
+ try {
312
+ const buf = toBuf(value);
313
+ if (!buf)
314
+ return null;
315
+ return upstreamSenderKeyJsonToProto(buf);
316
+ }
317
+ catch (e) {
318
+ warn('sender_key.toBridge decode failed:', e);
319
+ return null;
320
+ }
321
+ },
322
+ fromBridge(_key, value) {
323
+ try {
324
+ return bridgeSenderKeyProtoToJson(value);
325
+ }
326
+ catch (e) {
327
+ warn('sender_key.fromBridge encode failed:', e);
328
+ return Buffer.from(value);
329
+ }
330
+ }
331
+ }
332
+ };
333
+ // BufferJSON-compatible replacer/reviver matching upstream `Utils/generics`.
334
+ const bufferJsonReplacer = (_k, v) => {
335
+ if (Buffer.isBuffer(v) || v instanceof Uint8Array || v?.type === 'Buffer') {
336
+ const data = v?.data ?? v;
337
+ return { type: 'Buffer', data: Buffer.from(data).toString('base64') };
206
338
  }
339
+ return v;
207
340
  };
341
+ const bufferJsonReviver = (_k, v) => {
342
+ const obj = v;
343
+ if (obj && typeof obj === 'object' && obj.type === 'Buffer' && typeof obj.data === 'string') {
344
+ return Buffer.from(obj.data, 'base64');
345
+ }
346
+ return v;
347
+ };
348
+ const sk_toBytes = (v) => v ? new Uint8Array(Buffer.from(v)) : new Uint8Array();
349
+ const sk_toBuffer = (v) => Buffer.from(v ?? new Uint8Array());
350
+ function bridgeSenderKeyProtoToJson(protoBytes) {
351
+ const struct = proto.SenderKeyRecordStructure.decode(Buffer.from(protoBytes));
352
+ const states = (struct.senderKeyStates ?? []).map(s => ({
353
+ senderKeyId: s.senderKeyId ?? 0,
354
+ senderChainKey: { iteration: s.senderChainKey?.iteration ?? 0, seed: sk_toBuffer(s.senderChainKey?.seed) },
355
+ senderSigningKey: {
356
+ public: sk_toBuffer(s.senderSigningKey?.public),
357
+ private: sk_toBuffer(s.senderSigningKey?.private)
358
+ },
359
+ senderMessageKeys: (s.senderMessageKeys ?? []).map(mk => ({
360
+ iteration: mk.iteration ?? 0,
361
+ seed: sk_toBuffer(mk.seed)
362
+ }))
363
+ }));
364
+ return Buffer.from(JSON.stringify(states, bufferJsonReplacer), 'utf-8');
365
+ }
366
+ function upstreamSenderKeyJsonToProto(jsonBuf) {
367
+ const states = JSON.parse(Buffer.from(jsonBuf).toString('utf-8'), bufferJsonReviver);
368
+ const senderKeyStates = states.map(s => ({
369
+ senderKeyId: s.senderKeyId ?? 0,
370
+ senderChainKey: { iteration: s.senderChainKey?.iteration ?? 0, seed: sk_toBytes(s.senderChainKey?.seed) },
371
+ senderSigningKey: {
372
+ public: sk_toBytes(s.senderSigningKey?.public),
373
+ private: sk_toBytes(s.senderSigningKey?.private)
374
+ },
375
+ senderMessageKeys: (s.senderMessageKeys ?? []).map(mk => ({
376
+ iteration: mk.iteration ?? 0,
377
+ seed: sk_toBytes(mk.seed)
378
+ }))
379
+ }));
380
+ return proto.SenderKeyRecordStructure.encode(proto.SenderKeyRecordStructure.create({ senderKeyStates })).finish();
381
+ }
382
+ // sender_key key translation: bridge `{group}:{user}[:dev]@{server}.{sig}`
383
+ // → upstream `{group}::{signalUser}::{deviceId}`. Group JIDs never contain
384
+ // `:`, so the first `:` separates group from sender address.
385
+ function bridgeSenderKeyToUpstream(bridgeKey) {
386
+ const sep = bridgeKey.indexOf(':');
387
+ if (sep < 0)
388
+ return null;
389
+ const groupJid = bridgeKey.slice(0, sep);
390
+ const addrStr = bridgeKey.slice(sep + 1);
391
+ const dotIdx = addrStr.lastIndexOf('.');
392
+ if (dotIdx < 0)
393
+ return null;
394
+ const jidPart = addrStr.slice(0, dotIdx);
395
+ const atIdx = jidPart.indexOf('@');
396
+ if (atIdx < 0)
397
+ return null;
398
+ const userPart = jidPart.slice(0, atIdx);
399
+ const server = jidPart.slice(atIdx + 1);
400
+ const [user, jidDev] = userPart.includes(':')
401
+ ? [userPart.slice(0, userPart.indexOf(':')), parseInt(userPart.slice(userPart.indexOf(':') + 1), 10) || 0]
402
+ : [userPart, 0];
403
+ const domainType = DOMAIN_TYPE_MAP[server];
404
+ if (domainType === undefined)
405
+ return null;
406
+ return `${groupJid}::${domainType !== 0 ? `${user}_${domainType}` : user}::${jidDev}`;
407
+ }
408
+ const CHAIN_TYPE_SENDING = 1;
409
+ const CHAIN_TYPE_RECEIVING = 2;
410
+ const BASE_KEY_TYPE_OURS = 1;
411
+ const BASE_KEY_TYPE_THEIRS = 2;
412
+ const b64 = (b) => b ? Buffer.from(b).toString('base64') : Buffer.alloc(0).toString('base64');
413
+ const fromB64 = (s) => (s ? Buffer.from(s, 'base64') : Buffer.alloc(0));
414
+ /**
415
+ * Derive Rust's per-message split (cipher 32 / mac 32 / iv 16) from JS's
416
+ * 32-byte `messageKey` seed. Both impls compute HKDF-SHA256 with
417
+ * salt=[0u8;32] and info="WhisperMessageKeys" — Rust's `None` salt
418
+ * defaults to a zero-byte HashLen string, so the output is byte-identical.
419
+ * Reverse direction (Rust→JS) is impossible: HKDF is one-way.
420
+ */
421
+ function deriveProtoMessageKey(seed, counter) {
422
+ const prk = createHmac('sha256', Buffer.alloc(32)).update(seed).digest();
423
+ const info = Buffer.from('WhisperMessageKeys');
424
+ const expand = (prev, n) => createHmac('sha256', prk)
425
+ .update(Buffer.concat([prev, info, Buffer.from([n])]))
426
+ .digest();
427
+ const t1 = expand(Buffer.alloc(0), 0x01);
428
+ const t2 = expand(t1, 0x02);
429
+ const t3 = expand(t2, 0x03);
430
+ return {
431
+ index: counter,
432
+ cipherKey: new Uint8Array(t1),
433
+ macKey: new Uint8Array(t2),
434
+ iv: new Uint8Array(t3.subarray(0, 16))
435
+ };
436
+ }
437
+ function jsChainMessageKeysToProto(messageKeys) {
438
+ if (!messageKeys)
439
+ return [];
440
+ const out = [];
441
+ for (const [counterStr, seedB64] of Object.entries(messageKeys)) {
442
+ const counter = parseInt(counterStr, 10);
443
+ if (!Number.isFinite(counter))
444
+ continue;
445
+ const seed = fromB64(seedB64);
446
+ if (seed.length > 0)
447
+ out.push(deriveProtoMessageKey(seed, counter));
448
+ }
449
+ return out;
450
+ }
451
+ function sessionStructureToEntry(session, closedTs) {
452
+ if (!session.senderChain || !session.rootKey)
453
+ return null;
454
+ // aliceBaseKey is set by Rust on BOTH sides (alice=our ephemeral, bob=
455
+ // peer's ephemeral). pendingPreKey is the side discriminator (alice
456
+ // only, cleared on first reply).
457
+ const baseKeyBytes = session.aliceBaseKey ?? session.senderChain.senderRatchetKey;
458
+ if (!baseKeyBytes)
459
+ return null;
460
+ const weAreAlice = !!session.pendingPreKey?.baseKey;
461
+ const baseKeyType = weAreAlice ? BASE_KEY_TYPE_OURS : BASE_KEY_TYPE_THEIRS;
462
+ // lastRemoteEphemeralKey precedence: tail receiverChain → aliceBaseKey
463
+ // (when bob, matches JS init) → empty. NEVER senderRatchetKey: that
464
+ // collides with the SENDER chain in `_chains`, and upstream's
465
+ // `maybeStepRatchet` would `delete previousRatchet.chainKey.key` on
466
+ // the sender chain when the peer ratchets, corrupting outbound encryption.
467
+ const receiverChains = session.receiverChains ?? [];
468
+ const lastReceiver = receiverChains[receiverChains.length - 1];
469
+ const lastRemoteEph = lastReceiver?.senderRatchetKey && lastReceiver.senderRatchetKey.length > 0
470
+ ? lastReceiver.senderRatchetKey
471
+ : !weAreAlice && session.aliceBaseKey && session.aliceBaseKey.length > 0
472
+ ? session.aliceBaseKey
473
+ : new Uint8Array();
474
+ const senderRatchetPub = session.senderChain.senderRatchetKey ?? new Uint8Array();
475
+ const senderRatchetPriv = session.senderChain.senderRatchetKeyPrivate ?? new Uint8Array();
476
+ const _chains = {};
477
+ if (session.senderChain.chainKey?.key && senderRatchetPub.length > 0) {
478
+ _chains[b64(senderRatchetPub)] = {
479
+ chainKey: { counter: session.senderChain.chainKey.index ?? 0, key: b64(session.senderChain.chainKey.key) },
480
+ chainType: CHAIN_TYPE_SENDING,
481
+ messageKeys: {}
482
+ };
483
+ }
484
+ for (const rc of receiverChains) {
485
+ if (!rc.chainKey?.key || !rc.senderRatchetKey || rc.senderRatchetKey.length === 0)
486
+ continue;
487
+ _chains[b64(rc.senderRatchetKey)] = {
488
+ chainKey: { counter: rc.chainKey.index ?? 0, key: b64(rc.chainKey.key) },
489
+ chainType: CHAIN_TYPE_RECEIVING,
490
+ messageKeys: {}
491
+ };
492
+ }
493
+ const now = Date.now();
494
+ const entry = {
495
+ registrationId: session.remoteRegistrationId ?? 0,
496
+ currentRatchet: {
497
+ ephemeralKeyPair: { pubKey: b64(senderRatchetPub), privKey: b64(senderRatchetPriv) },
498
+ lastRemoteEphemeralKey: b64(lastRemoteEph),
499
+ previousCounter: session.previousCounter ?? 0,
500
+ rootKey: b64(session.rootKey)
501
+ },
502
+ indexInfo: {
503
+ baseKey: b64(baseKeyBytes),
504
+ baseKeyType,
505
+ closed: closedTs,
506
+ used: now,
507
+ created: now,
508
+ remoteIdentityKey: b64(session.remoteIdentityPublic ?? new Uint8Array())
509
+ },
510
+ _chains
511
+ };
512
+ if (session.pendingPreKey?.baseKey) {
513
+ entry.pendingPreKey = {
514
+ preKeyId: session.pendingPreKey.preKeyId ?? undefined,
515
+ signedKeyId: session.pendingPreKey.signedPreKeyId ?? undefined,
516
+ baseKey: b64(session.pendingPreKey.baseKey)
517
+ };
518
+ }
519
+ return entry;
520
+ }
521
+ function bridgeSessionProtoToUpstreamRecord(protoBytes) {
522
+ const record = proto.RecordStructure.decode(Buffer.from(protoBytes));
523
+ const _sessions = {};
524
+ const current = record.currentSession ? sessionStructureToEntry(record.currentSession, -1) : null;
525
+ if (current)
526
+ _sessions[current.indexInfo.baseKey] = current;
527
+ // Synthesize descending close timestamps so removeOldSessions can sort:
528
+ // front of Rust `previous_sessions` is the most recently archived.
529
+ const previous = record.previousSessions ?? [];
530
+ for (let i = 0; i < previous.length; i++) {
531
+ const entry = sessionStructureToEntry(previous[i], Math.max(1, Date.now() - i));
532
+ if (entry && !_sessions[entry.indexInfo.baseKey])
533
+ _sessions[entry.indexInfo.baseKey] = entry;
534
+ }
535
+ return { _sessions, version: 'v1' };
536
+ }
537
+ function entryToSessionStructure(entry) {
538
+ const ratchet = entry.currentRatchet;
539
+ const senderRatchetPub = fromB64(ratchet.ephemeralKeyPair.pubKey);
540
+ const senderRatchetPriv = fromB64(ratchet.ephemeralKeyPair.privKey);
541
+ const senderKeyB64 = senderRatchetPub.toString('base64');
542
+ const senderChainEntry = entry._chains[senderKeyB64];
543
+ const senderChain = {
544
+ senderRatchetKey: new Uint8Array(senderRatchetPub),
545
+ senderRatchetKeyPrivate: new Uint8Array(senderRatchetPriv),
546
+ chainKey: senderChainEntry
547
+ ? { index: senderChainEntry.chainKey.counter, key: new Uint8Array(fromB64(senderChainEntry.chainKey.key)) }
548
+ : { index: 0, key: new Uint8Array() },
549
+ messageKeys: [] // sender chain is sequential — no skipped cache either side
550
+ };
551
+ const receiverChains = [];
552
+ for (const [k, ch] of Object.entries(entry._chains)) {
553
+ if (ch.chainType !== CHAIN_TYPE_RECEIVING)
554
+ continue;
555
+ receiverChains.push({
556
+ senderRatchetKey: new Uint8Array(fromB64(k)),
557
+ chainKey: { index: ch.chainKey.counter, key: new Uint8Array(fromB64(ch.chainKey.key)) },
558
+ // JS holds 32-byte HMAC seeds; Rust holds the post-HKDF split.
559
+ // Lossless JS→Rust derivation via deriveProtoMessageKey.
560
+ messageKeys: jsChainMessageKeysToProto(ch.messageKeys)
561
+ });
562
+ }
563
+ // `indexInfo.baseKey` IS the alice_base_key in either direction
564
+ // (alice=our ephemeral, bob=peer's ephemeral). Rust populates it
565
+ // symmetrically; dropping it would break find_matching_previous_session_index.
566
+ const session = {
567
+ rootKey: new Uint8Array(fromB64(ratchet.rootKey)),
568
+ previousCounter: ratchet.previousCounter,
569
+ senderChain,
570
+ receiverChains,
571
+ remoteIdentityPublic: new Uint8Array(fromB64(entry.indexInfo.remoteIdentityKey)),
572
+ remoteRegistrationId: entry.registrationId ?? 0,
573
+ aliceBaseKey: new Uint8Array(fromB64(entry.indexInfo.baseKey))
574
+ };
575
+ if (entry.pendingPreKey?.baseKey) {
576
+ session.pendingPreKey = {
577
+ preKeyId: entry.pendingPreKey.preKeyId ?? null,
578
+ signedPreKeyId: entry.pendingPreKey.signedKeyId ?? null,
579
+ baseKey: new Uint8Array(fromB64(entry.pendingPreKey.baseKey))
580
+ };
581
+ }
582
+ return session;
583
+ }
584
+ function upstreamSessionRecordToProto(record) {
585
+ const sessions = record?._sessions ?? {};
586
+ let currentSession = null;
587
+ const previousSessions = [];
588
+ for (const entry of Object.values(sessions)) {
589
+ const ss = entryToSessionStructure(entry);
590
+ if (!currentSession && entry.indexInfo.closed === -1)
591
+ currentSession = ss;
592
+ else
593
+ previousSessions.push(ss);
594
+ }
595
+ // `BridgeSessionProto` is a structural mirror of `ISessionStructure`
596
+ // from `whatsapp-rust-bridge/proto-types`; the differences are nullable
597
+ // vs optional fields. We cast at the boundary to avoid pulling the full
598
+ // proto class types into our internal shape.
599
+ const recordOut = proto.RecordStructure.create({
600
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
601
+ currentSession: (currentSession ?? undefined),
602
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
603
+ previousSessions: previousSessions
604
+ });
605
+ return proto.RecordStructure.encode(recordOut).finish();
606
+ }
607
+ // session/identity key translation: bridge `{user}[:dev]@{server}.{sig}`
608
+ // → upstream `{signalUser}.{deviceId}` via `bridgeAddrToBaileysAddr`.
208
609
  // ---- Device/Creds ----
209
610
  /** Parse a JID string into { user, device, server } */
210
611
  function parseJid(jid) {
@@ -259,6 +660,21 @@ function credsToDeviceJson(creds) {
259
660
  next_pre_key_id: creds.nextPreKeyId ?? 1
260
661
  });
261
662
  }
663
+ /**
664
+ * Decode a `[priv(32)…pub(32)]` byte array (the bridge's `kp` shape) back into
665
+ * the upstream Baileys `{public, private}` keypair. Returns `undefined` for
666
+ * absent / all-zero arrays so callers can use `??` to keep prior values.
667
+ */
668
+ function numArrayToKeyPair(arr) {
669
+ if (!Array.isArray(arr) || arr.length < 64)
670
+ return undefined;
671
+ if (arr.every(b => b === 0))
672
+ return undefined;
673
+ return {
674
+ private: new Uint8Array(arr.slice(0, 32)),
675
+ public: new Uint8Array(arr.slice(32, 64))
676
+ };
677
+ }
262
678
  function updateCredsFromDevice(bytes, creds) {
263
679
  try {
264
680
  const d = fromJson(bytes);
@@ -293,6 +709,29 @@ function updateCredsFromDevice(bytes, creds) {
293
709
  creds.advSecretKey = buf.toString('base64');
294
710
  }
295
711
  }
712
+ // Hydrate the noise / identity / signed pre-key material so the next
713
+ // `credsToDeviceJson(creds)` round-trip emits real bytes instead of
714
+ // zero-filled placeholders. Without this, a paired session looks
715
+ // half-valid on the next process start (`me.id` set, but key slots
716
+ // all zero) and the bridge's noise handshake gets rejected by the
717
+ // server with stream-error precondition-required.
718
+ const noise = numArrayToKeyPair(d.noise_key);
719
+ if (noise)
720
+ creds.noiseKey = noise;
721
+ const identity = numArrayToKeyPair(d.identity_key);
722
+ if (identity)
723
+ creds.signedIdentityKey = identity;
724
+ const signedPreKeyPair = numArrayToKeyPair(d.signed_pre_key);
725
+ if (signedPreKeyPair &&
726
+ Array.isArray(d.signed_pre_key_signature) &&
727
+ d.signed_pre_key_signature.length === 64 &&
728
+ d.signed_pre_key_signature.some(b => b !== 0)) {
729
+ creds.signedPreKey = {
730
+ keyPair: signedPreKeyPair,
731
+ keyId: d.signed_pre_key_id ?? 1,
732
+ signature: new Uint8Array(d.signed_pre_key_signature)
733
+ };
734
+ }
296
735
  }
297
736
  catch (e) {
298
737
  warn('updateCredsFromDevice failed:', e);
@@ -444,22 +883,58 @@ export async function wrapLegacyStore(state, saveCreds, logger) {
444
883
  }, 100);
445
884
  };
446
885
  /** Route a store to its handler */
447
- const resolve = (store) => {
448
- if (store === 'device')
886
+ const resolve = (storeName) => {
887
+ if (storeName === 'device')
449
888
  return 'device';
450
- if (BINARY_STORES.has(store))
889
+ if (BINARY_STORES.has(storeName))
451
890
  return 'signal';
452
- if (BRIDGE_ONLY_STORES.has(store))
891
+ if (BRIDGE_ONLY_STORES.has(storeName))
453
892
  return 'bridge-only';
454
- if (STORE_MAP[store])
893
+ if (STORE_MAP[storeName])
455
894
  return 'converter';
456
895
  return 'unknown';
457
896
  };
458
897
  /** Translate bridge key format to upstream Baileys key format */
459
- const translateKey = (store, key) => {
898
+ const translateKey = (storeName, key) => {
460
899
  // Bridge encodes sync_key IDs as hex; upstream stores use base64
461
- if (store === 'sync_key')
900
+ if (storeName === 'sync_key')
462
901
  return hexToBase64(key);
902
+ // Bridge sender_key cache_key uses single-colon separator + signal-
903
+ // device suffix; upstream's `SenderKeyName.serialize()` uses double
904
+ // colons and drops the signal device — see bridgeSenderKeyToUpstream.
905
+ if (storeName === 'sender_key') {
906
+ const translated = bridgeSenderKeyToUpstream(key);
907
+ if (translated == null) {
908
+ // eslint-disable-line eqeqeq -- intentional null+undefined check
909
+ warn(`sender_key key translation failed for "${key}", falling back to passthrough`);
910
+ return key;
911
+ }
912
+ return translated;
913
+ }
914
+ // Bridge session/identity keys are the protocol address
915
+ // (`user[:dev]@server.sig`); upstream's libsignal looks under
916
+ // `signalUser.deviceId`. `bridgeAddrToBaileysAddr` does the LID/PN
917
+ // → signalUser rewrite + JID-device extraction.
918
+ if (storeName === 'session' || storeName === 'identity') {
919
+ const translated = bridgeAddrToBaileysAddr(key);
920
+ if (translated == null) {
921
+ // eslint-disable-line eqeqeq -- intentional null+undefined check
922
+ warn(`${storeName} key translation failed for "${key}", falling back to passthrough`);
923
+ return key;
924
+ }
925
+ return translated;
926
+ }
927
+ // Bridge lid_mapping uses prefixed keys (`lid:{X}`, `pn:{X}`) that
928
+ // upstream's `Signal/lid-mapping.js` doesn't recognize. Map them to
929
+ // upstream's bare-userpart shape: `pn:{X}` → `{X}`, `lid:{X}` →
930
+ // `{X}_reverse`.
931
+ if (storeName === 'lid_mapping') {
932
+ if (key.startsWith('pn:'))
933
+ return key.slice(3);
934
+ if (key.startsWith('lid:'))
935
+ return `${key.slice(4)}_reverse`;
936
+ return key;
937
+ }
463
938
  return key;
464
939
  };
465
940
  /** Read a binary value from the keys store */
@@ -496,8 +971,19 @@ export async function wrapLegacyStore(state, saveCreds, logger) {
496
971
  const route = resolve(bridgeStore);
497
972
  const type = STORE_MAP[bridgeStore];
498
973
  if (route === 'device') {
499
- if (key === 'device')
974
+ if (key === 'device') {
975
+ // Fresh/stub creds (baileyrs' `initAuthCreds()` shim, or an
976
+ // upstream flow that hadn't finished its first pair) carry
977
+ // neither a noise key nor a paired `me.id`. Returning a
978
+ // zeroed `credsToDeviceJson` in that state poisons the
979
+ // bridge's noise handshake (server responds 428 Precondition
980
+ // Required). Signaling `null` instead lets the bridge
981
+ // bootstrap its own device identity and write it back
982
+ // through `set('device', 'device', …)` on pair-success.
983
+ if (!creds.noiseKey && !creds.me?.id)
984
+ return null;
500
985
  return credsToDeviceJson(creds);
986
+ }
501
987
  if (key === 'account') {
502
988
  if (!creds.account)
503
989
  return null;
@@ -506,13 +992,12 @@ export async function wrapLegacyStore(state, saveCreds, logger) {
506
992
  return readBinary(type, key);
507
993
  }
508
994
  if (route === 'signal') {
509
- const val = await readBinary(type, key);
510
- if (val)
511
- return val;
512
- // Identity fallback: upstream Baileys uses a different address format
513
- if (bridgeStore === 'identity')
514
- return identityFallback(key, type);
515
- return null;
995
+ // `BINARY_STORES` is empty as of the move-to-converter
996
+ // migration — keeping the branch for future passthrough
997
+ // stores. No identity fallback needed: identity now goes
998
+ // through the converter, which handles both the address
999
+ // rewrite and the 32↔33-byte prefix.
1000
+ return readBinary(type, key);
516
1001
  }
517
1002
  if (route === 'bridge-only')
518
1003
  return readBinary(type, key);
@@ -594,28 +1079,6 @@ export async function wrapLegacyStore(state, saveCreds, logger) {
594
1079
  }
595
1080
  }
596
1081
  };
597
- // Identity key fallback: upstream Baileys uses different address format
598
- // (user_domainType.device) and stores 33 bytes (0x05 + 32-byte DJB key)
599
- async function identityFallback(key, type) {
600
- try {
601
- const baileysAddr = bridgeAddrToBaileysAddr(key);
602
- if (!baileysAddr)
603
- return null;
604
- const raw = await storeGetOne('identity-key', baileysAddr);
605
- if (!raw || (!Buffer.isBuffer(raw) && !(raw instanceof Uint8Array)))
606
- return null;
607
- let buf = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
608
- if (buf.length === 33 && buf[0] === 0x05)
609
- buf = buf.slice(1);
610
- const arr = new Uint8Array(buf);
611
- await writeBinary(type, key, arr); // persist under bridge key for future reads
612
- return arr;
613
- }
614
- catch (e) {
615
- warn(`identity fallback failed for ${key}:`, e);
616
- return null;
617
- }
618
- }
619
1082
  }
620
1083
  const bufferReviver = (_, value) => {
621
1084
  if (value !== null &&