@oxidezap/baileyrs 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) 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__/wrap-legacy-store-lid-mapping.test.d.ts +23 -0
  93. package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.d.ts.map +1 -0
  94. package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.js +157 -0
  95. package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.js.map +1 -0
  96. package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.d.ts +31 -0
  97. package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.d.ts.map +1 -0
  98. package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.js +253 -0
  99. package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.js.map +1 -0
  100. package/lib/Utils/__tests__/wrap-legacy-store-session.test.d.ts +32 -0
  101. package/lib/Utils/__tests__/wrap-legacy-store-session.test.d.ts.map +1 -0
  102. package/lib/Utils/__tests__/wrap-legacy-store-session.test.js +488 -0
  103. package/lib/Utils/__tests__/wrap-legacy-store-session.test.js.map +1 -0
  104. package/lib/Utils/boom.d.ts +33 -0
  105. package/lib/Utils/boom.d.ts.map +1 -0
  106. package/lib/Utils/boom.js +63 -0
  107. package/lib/Utils/boom.js.map +1 -0
  108. package/lib/Utils/browser-utils.d.ts.map +1 -1
  109. package/lib/Utils/event-buffer.d.ts.map +1 -1
  110. package/lib/Utils/generics.d.ts +37 -1
  111. package/lib/Utils/generics.d.ts.map +1 -1
  112. package/lib/Utils/generics.js +60 -2
  113. package/lib/Utils/generics.js.map +1 -1
  114. package/lib/Utils/index.d.ts +1 -0
  115. package/lib/Utils/index.d.ts.map +1 -1
  116. package/lib/Utils/index.js +10 -9
  117. package/lib/Utils/index.js.map +1 -1
  118. package/lib/Utils/lid-mapping-store.d.ts +81 -0
  119. package/lib/Utils/lid-mapping-store.d.ts.map +1 -0
  120. package/lib/Utils/lid-mapping-store.js +126 -0
  121. package/lib/Utils/lid-mapping-store.js.map +1 -0
  122. package/lib/Utils/logger.d.ts.map +1 -1
  123. package/lib/Utils/logger.js +25 -1
  124. package/lib/Utils/logger.js.map +1 -1
  125. package/lib/Utils/messages-media.d.ts.map +1 -1
  126. package/lib/Utils/messages-media.js +1 -1
  127. package/lib/Utils/messages-media.js.map +1 -1
  128. package/lib/Utils/messages.d.ts +30 -1
  129. package/lib/Utils/messages.d.ts.map +1 -1
  130. package/lib/Utils/messages.js +115 -41
  131. package/lib/Utils/messages.js.map +1 -1
  132. package/lib/Utils/native-crypto-provider.d.ts +18 -0
  133. package/lib/Utils/native-crypto-provider.d.ts.map +1 -0
  134. package/lib/Utils/native-crypto-provider.js +67 -0
  135. package/lib/Utils/native-crypto-provider.js.map +1 -0
  136. package/lib/Utils/use-bridge-store.d.ts.map +1 -1
  137. package/lib/Utils/use-memory-store.d.ts.map +1 -1
  138. package/lib/Utils/use-memory-store.js +1 -1
  139. package/lib/Utils/use-memory-store.js.map +1 -1
  140. package/lib/Utils/use-multi-file-auth-state.d.ts.map +1 -1
  141. package/lib/Utils/use-multi-file-auth-state.js +1 -1
  142. package/lib/Utils/use-multi-file-auth-state.js.map +1 -1
  143. package/lib/Utils/wrap-legacy-store.d.ts.map +1 -1
  144. package/lib/Utils/wrap-legacy-store.js +621 -13
  145. package/lib/Utils/wrap-legacy-store.js.map +1 -1
  146. package/lib/WABinary/index.d.ts.map +1 -1
  147. package/lib/WABinary/index.js +1 -1
  148. package/lib/WABinary/index.js.map +1 -1
  149. package/lib/__tests__/expect.d.ts +30 -0
  150. package/lib/__tests__/expect.d.ts.map +1 -0
  151. package/lib/__tests__/expect.js +60 -0
  152. package/lib/__tests__/expect.js.map +1 -0
  153. package/lib/index.d.ts +1 -1
  154. package/lib/index.d.ts.map +1 -1
  155. package/lib/index.js +9 -6
  156. package/lib/index.js.map +1 -1
  157. package/package.json +30 -25
  158. package/WAProto/index.d.ts +0 -13875
  159. 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,20 @@ 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
+ // `sender_key` and `session` are intentionally excluded — both diverge in
52
+ // on-disk format between Rust libsignal (protobuf `SenderKeyRecordStructure`
53
+ // / `RecordStructure`) and JS libsignal (`SenderKeyRecord` UTF-8 JSON /
54
+ // `SessionRecord` plain JS object). They route through converters so the
55
+ // same auth folder remains readable from either implementation, which is
56
+ // the whole point of `wrap-legacy-store`.
57
+ const BINARY_STORES = new Set(['identity']);
58
+ // Bridge-only stores — raw binary persisted through keys interface.
59
+ // `lid_mapping` is intentionally excluded — it has a converter so the
60
+ // `pn:`/`lid:` prefixed bridge keys translate to upstream's bare-userpart
61
+ // `{pnUser}` + `{lidUser}_reverse` shape, letting both libraries share
62
+ // the same on-disk LID↔PN mapping table.
52
63
  const BRIDGE_ONLY_STORES = new Set([
53
- 'lid_mapping',
54
64
  'signed_prekey',
55
65
  'sender_key_devices',
56
66
  'base_key',
@@ -203,8 +213,521 @@ const converters = {
203
213
  return [];
204
214
  }
205
215
  }
216
+ },
217
+ // ── lid_mapping ──
218
+ // Cross-impl converter for LID ↔ PN device mapping table.
219
+ // • Bridge: keyed by `lid:{lidUser}` (value=JSON LidPnMappingEntry
220
+ // with phone_number/created_at/updated_at/learning_source) AND
221
+ // `pn:{pnUser}` (value=raw bytes of the LID user, UTF-8).
222
+ // • Upstream (`Signal/lid-mapping.js`): keyed by bare `{pnUser}`
223
+ // (value=string LID user) AND `{lidUser}_reverse` (value=string
224
+ // PN user). No timestamps.
225
+ // Translation goes both ways; metadata fields default to "now" /
226
+ // "wrap-legacy-store" when reading from upstream-only state since the
227
+ // JS side doesn't track them.
228
+ lid_mapping: {
229
+ toBridge(key, value) {
230
+ // `value` here is whatever upstream stored: a string
231
+ // (`pnUser→lidUser` or `lidUser_reverse→pnUser`) — useMultiFile's
232
+ // BufferJSON.reviver leaves strings as strings.
233
+ try {
234
+ if (key.startsWith('pn:')) {
235
+ // Bridge wants the raw LID user as bytes.
236
+ const lid = typeof value === 'string' ? value : Buffer.from(value).toString('utf-8');
237
+ if (!lid)
238
+ return null;
239
+ return new TextEncoder().encode(lid);
240
+ }
241
+ if (key.startsWith('lid:')) {
242
+ // Bridge wants the JSON LidPnMappingEntry. Upstream stored
243
+ // only the phone number string; reconstruct a synthetic
244
+ // entry — bridge tolerates synthetic timestamps + source.
245
+ const lidUser = key.slice(4);
246
+ const phoneNumber = typeof value === 'string' ? value : Buffer.from(value).toString('utf-8');
247
+ if (!phoneNumber)
248
+ return null;
249
+ const now = Math.floor(Date.now() / 1000);
250
+ return toJson({
251
+ lid: lidUser,
252
+ phone_number: phoneNumber,
253
+ created_at: now,
254
+ updated_at: now,
255
+ learning_source: 'wrap-legacy-store'
256
+ });
257
+ }
258
+ warn(`lid_mapping.toBridge: unknown key prefix in "${key}"`);
259
+ return null;
260
+ }
261
+ catch (e) {
262
+ warn('lid_mapping.toBridge failed:', e);
263
+ return null;
264
+ }
265
+ },
266
+ fromBridge(key, value) {
267
+ // `value` here is bridge's Uint8Array. We return a string for
268
+ // upstream to store — useMultiFile's BufferJSON.replacer keeps
269
+ // strings as strings, which is what `lid-mapping.js` expects to
270
+ // read back via `keys.get('lid-mapping', […])`.
271
+ try {
272
+ if (key.startsWith('pn:')) {
273
+ // Bridge wrote raw LID bytes. Upstream wants the string.
274
+ return Buffer.from(value).toString('utf-8');
275
+ }
276
+ if (key.startsWith('lid:')) {
277
+ // Bridge wrote JSON entry. Upstream wants the phone_number
278
+ // string under `{lidUser}_reverse`.
279
+ const entry = fromJson(value);
280
+ return entry.phone_number ?? '';
281
+ }
282
+ warn(`lid_mapping.fromBridge: unknown key prefix in "${key}"`);
283
+ return Buffer.from(value);
284
+ }
285
+ catch (e) {
286
+ warn('lid_mapping.fromBridge failed:', e);
287
+ return Buffer.from(value);
288
+ }
289
+ }
290
+ },
291
+ // ── session ──
292
+ // Cross-impl converter for pairwise Signal sessions.
293
+ // • Rust libsignal (bridge): raw protobuf `RecordStructure` bytes
294
+ // (current_session + previous_sessions, each `SessionStructure`).
295
+ // • JS libsignal (upstream): a plain JS object
296
+ // `{ _sessions: { [base64BaseKey]: SessionEntry }, version: 'v1' }`
297
+ // persisted by upstream's `useMultiFileAuthState` via
298
+ // JSON.stringify-with-BufferJSON. The byte fields inside SessionEntry
299
+ // are stored as base64 STRINGS (not BufferJSON Buffer wrappers).
300
+ // We translate at the boundary so the same auth folder works for both.
301
+ // Note: per-message-counter caches (`messageKeys`) are dropped during
302
+ // conversion — the two implementations derive them differently (Rust
303
+ // stores cipher/mac/iv post-split; JS stores the pre-split chain output)
304
+ // and they're a CACHE, not state — protocol re-derives or asks for retry.
305
+ session: {
306
+ toBridge(_key, value) {
307
+ // Upstream stored: a plain JS object (NOT bytes). Convert → proto bytes.
308
+ try {
309
+ if (value == null)
310
+ return null; // eslint-disable-line eqeqeq -- intentional null+undefined check
311
+ return upstreamSessionRecordToProto(value);
312
+ }
313
+ catch (e) {
314
+ warn('session.toBridge encode failed:', e);
315
+ return null;
316
+ }
317
+ },
318
+ fromBridge(_key, value) {
319
+ // Bridge wrote: raw proto bytes. Convert → upstream JS object.
320
+ try {
321
+ return bridgeSessionProtoToUpstreamRecord(value);
322
+ }
323
+ catch (e) {
324
+ warn('session.fromBridge decode failed:', e);
325
+ return Buffer.from(value);
326
+ }
327
+ }
328
+ },
329
+ // ── sender_key ──
330
+ // Cross-impl converter for group sender keys. Both libraries produce
331
+ // the same logical record (`SenderKeyRecordStructure`) but persist it
332
+ // under DIFFERENT bytes:
333
+ // • Rust libsignal (bridge): raw protobuf bytes
334
+ // • JS libsignal (upstream): UTF-8 JSON of the record array, with
335
+ // each Buffer wrapped as `{ type:'Buffer', data:'<base64>' }`
336
+ // (BufferJSON.replacer in upstream's `Utils/generics`).
337
+ // We translate at the boundary so the same on-disk file is readable
338
+ // by either implementation — the entire point of `wrap-legacy-store`.
339
+ sender_key: {
340
+ toBridge(_key, value) {
341
+ // Upstream stored: Buffer of UTF-8 JSON. Convert → proto bytes.
342
+ try {
343
+ const buf = toBuf(value);
344
+ if (!buf)
345
+ return null;
346
+ return upstreamSenderKeyJsonToProto(buf);
347
+ }
348
+ catch (e) {
349
+ warn('sender_key.toBridge decode failed:', e);
350
+ return null;
351
+ }
352
+ },
353
+ fromBridge(_key, value) {
354
+ // Bridge wrote: raw proto bytes. Convert → upstream JSON Buffer.
355
+ try {
356
+ return bridgeSenderKeyProtoToJson(value);
357
+ }
358
+ catch (e) {
359
+ warn('sender_key.fromBridge encode failed:', e);
360
+ // Fall back to passthrough so we don't drop data on the floor.
361
+ return Buffer.from(value);
362
+ }
363
+ }
364
+ }
365
+ };
366
+ const bufferReplacerForSenderKey = (_k, v) => {
367
+ if (Buffer.isBuffer(v) || v instanceof Uint8Array || v?.type === 'Buffer') {
368
+ const data = v?.data ?? v;
369
+ return { type: 'Buffer', data: Buffer.from(data).toString('base64') };
370
+ }
371
+ return v;
372
+ };
373
+ const bufferReviverForSenderKey = (_k, v) => {
374
+ if (typeof v === 'object' && v !== null) {
375
+ const obj = v;
376
+ if (obj.type === 'Buffer' && typeof obj.data === 'string') {
377
+ return Buffer.from(obj.data, 'base64');
378
+ }
206
379
  }
380
+ return v;
207
381
  };
382
+ function bridgeSenderKeyProtoToJson(protoBytes) {
383
+ const struct = proto.SenderKeyRecordStructure.decode(Buffer.from(protoBytes));
384
+ const states = (struct.senderKeyStates ?? []).map(s => ({
385
+ senderKeyId: s.senderKeyId ?? 0,
386
+ senderChainKey: {
387
+ iteration: s.senderChainKey?.iteration ?? 0,
388
+ seed: Buffer.from(s.senderChainKey?.seed ?? new Uint8Array())
389
+ },
390
+ senderSigningKey: {
391
+ public: Buffer.from(s.senderSigningKey?.public ?? new Uint8Array()),
392
+ private: Buffer.from(s.senderSigningKey?.private ?? new Uint8Array())
393
+ },
394
+ senderMessageKeys: (s.senderMessageKeys ?? []).map(mk => ({
395
+ iteration: mk.iteration ?? 0,
396
+ seed: Buffer.from(mk.seed ?? new Uint8Array())
397
+ }))
398
+ }));
399
+ return Buffer.from(JSON.stringify(states, bufferReplacerForSenderKey), 'utf-8');
400
+ }
401
+ function upstreamSenderKeyJsonToProto(jsonBuf) {
402
+ const text = Buffer.from(jsonBuf).toString('utf-8');
403
+ const states = JSON.parse(text, bufferReviverForSenderKey);
404
+ const senderKeyStates = states.map(s => ({
405
+ senderKeyId: s.senderKeyId ?? 0,
406
+ senderChainKey: {
407
+ iteration: s.senderChainKey?.iteration ?? 0,
408
+ seed: s.senderChainKey?.seed ? new Uint8Array(Buffer.from(s.senderChainKey.seed)) : new Uint8Array()
409
+ },
410
+ senderSigningKey: {
411
+ public: s.senderSigningKey?.public ? new Uint8Array(Buffer.from(s.senderSigningKey.public)) : new Uint8Array(),
412
+ private: s.senderSigningKey?.private ? new Uint8Array(Buffer.from(s.senderSigningKey.private)) : new Uint8Array()
413
+ },
414
+ senderMessageKeys: (s.senderMessageKeys ?? []).map(mk => ({
415
+ iteration: mk.iteration ?? 0,
416
+ seed: mk.seed ? new Uint8Array(Buffer.from(mk.seed)) : new Uint8Array()
417
+ }))
418
+ }));
419
+ const struct = proto.SenderKeyRecordStructure.create({ senderKeyStates });
420
+ return proto.SenderKeyRecordStructure.encode(struct).finish();
421
+ }
422
+ // ── sender_key key translator ──
423
+ // Bridge `cache_key`: `{groupJid}:{user}[:jidDev]@{server}.{signalDev}`
424
+ // e.g. "120363@g.us:100000037037034@lid.0"
425
+ // e.g. "120363@g.us:559980000003:5@s.whatsapp.net.0"
426
+ // Upstream `SenderKeyName.serialize()`: `{groupJid}::{signalUser}::{deviceId}`
427
+ // where signalUser = `${user}` for s.whatsapp.net (domainType 0)
428
+ // = `${user}_${domainType}` for LID (1) / hosted (2) / hosted.lid (3)
429
+ function bridgeSenderKeyToUpstream(bridgeKey) {
430
+ // Group JIDs are `{digits}@g.us` — they never contain `:` — so the
431
+ // FIRST `:` in cache_key cleanly separates group from sender address.
432
+ const sep = bridgeKey.indexOf(':');
433
+ if (sep < 0)
434
+ return null;
435
+ const groupJid = bridgeKey.slice(0, sep);
436
+ const addrStr = bridgeKey.slice(sep + 1);
437
+ const dotIdx = addrStr.lastIndexOf('.');
438
+ if (dotIdx < 0)
439
+ return null;
440
+ const jidPart = addrStr.slice(0, dotIdx); // strip the always-zero signalDev suffix
441
+ const atIdx = jidPart.indexOf('@');
442
+ if (atIdx < 0)
443
+ return null;
444
+ const userPart = jidPart.slice(0, atIdx);
445
+ const server = jidPart.slice(atIdx + 1);
446
+ let user;
447
+ let jidDev;
448
+ if (userPart.includes(':')) {
449
+ const [u, d] = userPart.split(':');
450
+ user = u;
451
+ jidDev = parseInt(d, 10) || 0;
452
+ }
453
+ else {
454
+ user = userPart;
455
+ jidDev = 0;
456
+ }
457
+ const domainType = DOMAIN_TYPE_MAP[server];
458
+ if (domainType === undefined)
459
+ return null;
460
+ const signalUser = domainType !== 0 ? `${user}_${domainType}` : user;
461
+ return `${groupJid}::${signalUser}::${jidDev}`;
462
+ }
463
+ const CHAIN_TYPE_SENDING = 1;
464
+ const CHAIN_TYPE_RECEIVING = 2;
465
+ const BASE_KEY_TYPE_OURS = 1;
466
+ const BASE_KEY_TYPE_THEIRS = 2;
467
+ const b64 = (b) => b ? Buffer.from(b).toString('base64') : Buffer.alloc(0).toString('base64');
468
+ const fromB64 = (s) => (s ? Buffer.from(s, 'base64') : Buffer.alloc(0));
469
+ /**
470
+ * Derive Rust libsignal's per-message split (cipher_key + mac_key + iv) from
471
+ * the JS libsignal `messageKey` seed. The two impls store the cache at
472
+ * different points in the derivation:
473
+ *
474
+ * • JS: stores the 32-byte HMAC(chainKey, [1]) seed; splits on decrypt
475
+ * via `deriveSecrets(seed, ZEROS_32, "WhisperMessageKeys")`.
476
+ * • Rust: stores the post-split `MessageKeys { cipher_key:[u8;32],
477
+ * mac_key:[u8;32], iv:[u8;16] }` directly.
478
+ *
479
+ * Both call HKDF-SHA256 with salt=[0u8;32] and info="WhisperMessageKeys"
480
+ * (Rust passes `None` salt which HKDF defines as a zero-byte string of
481
+ * HashLen=32), so the derivation is byte-identical. We can recover Rust's
482
+ * format from JS's seed losslessly. The reverse (Rust → JS) is impossible
483
+ * because HKDF is one-way — those keys are dropped on Rust→JS conversion.
484
+ */
485
+ function deriveProtoMessageKey(seed, counter) {
486
+ // HKDF-Extract: PRK = HMAC-SHA256(salt=ZEROS_32, ikm=seed)
487
+ const salt = Buffer.alloc(32);
488
+ const prk = createHmac('sha256', salt).update(seed).digest();
489
+ const info = Buffer.from('WhisperMessageKeys');
490
+ // HKDF-Expand: T(1)=HMAC(PRK, info||0x01); T(N)=HMAC(PRK, T(N-1)||info||N).
491
+ // We need 80 bytes total (cipher 32 + mac 32 + iv 16) → 3 chunks.
492
+ const t1 = createHmac('sha256', prk)
493
+ .update(Buffer.concat([info, Buffer.from([0x01])]))
494
+ .digest();
495
+ const t2 = createHmac('sha256', prk)
496
+ .update(Buffer.concat([t1, info, Buffer.from([0x02])]))
497
+ .digest();
498
+ const t3 = createHmac('sha256', prk)
499
+ .update(Buffer.concat([t2, info, Buffer.from([0x03])]))
500
+ .digest();
501
+ return {
502
+ index: counter,
503
+ cipherKey: new Uint8Array(t1), // 32
504
+ macKey: new Uint8Array(t2), // 32
505
+ iv: new Uint8Array(t3.subarray(0, 16)) // first 16 of T(3)
506
+ };
507
+ }
508
+ /** Convert all entries in a JS chain's messageKeys cache into Rust's
509
+ * post-split `MessageKey` proto form. Empty/missing → empty array. */
510
+ function jsChainMessageKeysToProto(messageKeys) {
511
+ if (!messageKeys)
512
+ return [];
513
+ const out = [];
514
+ for (const [counterStr, seedB64] of Object.entries(messageKeys)) {
515
+ const counter = parseInt(counterStr, 10);
516
+ if (!Number.isFinite(counter))
517
+ continue;
518
+ const seed = fromB64(seedB64);
519
+ if (seed.length === 0)
520
+ continue;
521
+ out.push(deriveProtoMessageKey(seed, counter));
522
+ }
523
+ return out;
524
+ }
525
+ function sessionStructureToEntry(session, closedTs) {
526
+ if (!session.senderChain || !session.rootKey)
527
+ return null;
528
+ // `aliceBaseKey` is populated by Rust libsignal in BOTH directions of
529
+ // a session: when we're Alice it stores our own ephemeral pubkey, when
530
+ // we're Bob it stores Alice's ephemeral pubkey from the incoming
531
+ // PreKeySignalMessage. So presence alone doesn't tell us which side
532
+ // we are — we use `pendingPreKey` instead (set on Alice until the first
533
+ // reply from Bob clears it; never set on Bob).
534
+ const baseKeyBytes = session.aliceBaseKey ?? session.senderChain.senderRatchetKey;
535
+ if (!baseKeyBytes)
536
+ return null;
537
+ const weAreAlice = !!session.pendingPreKey?.baseKey;
538
+ const baseKeyType = weAreAlice ? BASE_KEY_TYPE_OURS : BASE_KEY_TYPE_THEIRS;
539
+ // lastRemoteEphemeralKey: the most recent peer ratchet step. Order of
540
+ // preference:
541
+ // 1. tail of `receiverChains` — established session, peer has replied
542
+ // 2. `aliceBaseKey` when we're Bob — peer's original ephemeral was
543
+ // our `lastRemoteEphemeralKey` at session creation (matches JS
544
+ // `session_builder.initSession` line 128)
545
+ // 3. empty buffer — degenerate fallback. CANNOT use senderRatchetKey
546
+ // as a placeholder: it collides with the SENDER chain in `_chains`,
547
+ // and upstream's `maybeStepRatchet` would call
548
+ // `delete previousRatchet.chainKey.key` on the sender chain when
549
+ // the peer ratchets, corrupting outbound encryption.
550
+ const receiverChains = session.receiverChains ?? [];
551
+ const lastReceiver = receiverChains[receiverChains.length - 1];
552
+ let lastRemoteEph;
553
+ if (lastReceiver?.senderRatchetKey && lastReceiver.senderRatchetKey.length > 0) {
554
+ lastRemoteEph = lastReceiver.senderRatchetKey;
555
+ }
556
+ else if (!weAreAlice && session.aliceBaseKey && session.aliceBaseKey.length > 0) {
557
+ lastRemoteEph = session.aliceBaseKey;
558
+ }
559
+ else {
560
+ lastRemoteEph = new Uint8Array();
561
+ }
562
+ const senderRatchetPub = session.senderChain.senderRatchetKey ?? new Uint8Array();
563
+ const senderRatchetPriv = session.senderChain.senderRatchetKeyPrivate ?? new Uint8Array();
564
+ const _chains = {};
565
+ // Sender chain: keyed by our own ratchet pubkey base64.
566
+ if (session.senderChain.chainKey?.key && senderRatchetPub.length > 0) {
567
+ _chains[b64(senderRatchetPub)] = {
568
+ chainKey: {
569
+ counter: session.senderChain.chainKey.index ?? 0,
570
+ key: b64(session.senderChain.chainKey.key)
571
+ },
572
+ chainType: CHAIN_TYPE_SENDING,
573
+ messageKeys: {}
574
+ };
575
+ }
576
+ // Receiver chains: keyed by peer ratchet pubkey base64.
577
+ for (const rc of receiverChains) {
578
+ if (!rc.chainKey?.key || !rc.senderRatchetKey || rc.senderRatchetKey.length === 0)
579
+ continue;
580
+ _chains[b64(rc.senderRatchetKey)] = {
581
+ chainKey: {
582
+ counter: rc.chainKey.index ?? 0,
583
+ key: b64(rc.chainKey.key)
584
+ },
585
+ chainType: CHAIN_TYPE_RECEIVING,
586
+ messageKeys: {}
587
+ };
588
+ }
589
+ const now = Date.now();
590
+ const entry = {
591
+ registrationId: session.remoteRegistrationId ?? 0,
592
+ currentRatchet: {
593
+ ephemeralKeyPair: { pubKey: b64(senderRatchetPub), privKey: b64(senderRatchetPriv) },
594
+ lastRemoteEphemeralKey: b64(lastRemoteEph),
595
+ previousCounter: session.previousCounter ?? 0,
596
+ rootKey: b64(session.rootKey)
597
+ },
598
+ indexInfo: {
599
+ baseKey: b64(baseKeyBytes),
600
+ baseKeyType,
601
+ closed: closedTs,
602
+ used: now,
603
+ created: now,
604
+ remoteIdentityKey: b64(session.remoteIdentityPublic ?? new Uint8Array())
605
+ },
606
+ _chains
607
+ };
608
+ if (session.pendingPreKey?.baseKey) {
609
+ entry.pendingPreKey = {
610
+ preKeyId: session.pendingPreKey.preKeyId ?? undefined,
611
+ signedKeyId: session.pendingPreKey.signedPreKeyId ?? undefined,
612
+ baseKey: b64(session.pendingPreKey.baseKey)
613
+ };
614
+ }
615
+ return entry;
616
+ }
617
+ function bridgeSessionProtoToUpstreamRecord(protoBytes) {
618
+ const record = proto.RecordStructure.decode(Buffer.from(protoBytes));
619
+ const _sessions = {};
620
+ const current = record.currentSession ? sessionStructureToEntry(record.currentSession, -1) : null;
621
+ if (current)
622
+ _sessions[current.indexInfo.baseKey] = current;
623
+ // Closed sessions: assign descending timestamps so removeOldSessions
624
+ // has something to sort by. Use `1` as the floor (anything > 0 is
625
+ // "closed"; -1 is "open").
626
+ const previous = record.previousSessions ?? [];
627
+ for (let i = 0; i < previous.length; i++) {
628
+ const entry = sessionStructureToEntry(previous[i], Math.max(1, Date.now() - i));
629
+ if (!entry)
630
+ continue;
631
+ // Don't overwrite an existing baseKey (current takes precedence).
632
+ if (!_sessions[entry.indexInfo.baseKey])
633
+ _sessions[entry.indexInfo.baseKey] = entry;
634
+ }
635
+ return { _sessions, version: 'v1' };
636
+ }
637
+ function entryToSessionStructure(entry) {
638
+ const ratchet = entry.currentRatchet;
639
+ const senderRatchetPub = fromB64(ratchet.ephemeralKeyPair.pubKey);
640
+ const senderRatchetPriv = fromB64(ratchet.ephemeralKeyPair.privKey);
641
+ const senderKeyB64 = senderRatchetPub.toString('base64');
642
+ const senderChainEntry = entry._chains[senderKeyB64];
643
+ const senderChain = {
644
+ senderRatchetKey: new Uint8Array(senderRatchetPub),
645
+ senderRatchetKeyPrivate: new Uint8Array(senderRatchetPriv),
646
+ chainKey: senderChainEntry
647
+ ? { index: senderChainEntry.chainKey.counter, key: new Uint8Array(fromB64(senderChainEntry.chainKey.key)) }
648
+ : { index: 0, key: new Uint8Array() },
649
+ // Sender chain has no skipped-key cache in either impl — sending
650
+ // is sequential — so this stays empty regardless.
651
+ messageKeys: []
652
+ };
653
+ const receiverChains = [];
654
+ for (const [k, ch] of Object.entries(entry._chains)) {
655
+ if (ch.chainType !== CHAIN_TYPE_RECEIVING)
656
+ continue;
657
+ const ratchetPub = fromB64(k);
658
+ receiverChains.push({
659
+ senderRatchetKey: new Uint8Array(ratchetPub),
660
+ chainKey: { index: ch.chainKey.counter, key: new Uint8Array(fromB64(ch.chainKey.key)) },
661
+ // Recover skipped-key cache: JS holds 32-byte HMAC seeds, Rust
662
+ // holds the post-HKDF cipher/mac/iv split. HKDF is deterministic,
663
+ // so JS→Rust is a lossless one-shot derivation. See
664
+ // `deriveProtoMessageKey` for the algo + cross-impl proof.
665
+ messageKeys: jsChainMessageKeysToProto(ch.messageKeys)
666
+ });
667
+ }
668
+ const session = {
669
+ rootKey: new Uint8Array(fromB64(ratchet.rootKey)),
670
+ previousCounter: ratchet.previousCounter,
671
+ senderChain,
672
+ receiverChains,
673
+ remoteIdentityPublic: new Uint8Array(fromB64(entry.indexInfo.remoteIdentityKey)),
674
+ remoteRegistrationId: entry.registrationId ?? 0,
675
+ // `indexInfo.baseKey` IS the alice base key from the X3DH handshake
676
+ // regardless of which side we are: when we're Alice it's our own
677
+ // ephemeral pubkey, when we're Bob it's the peer's ephemeral pubkey
678
+ // from the incoming PreKeySignalMessage (`session_builder.js:135`).
679
+ // Rust libsignal populates `aliceBaseKey` symmetrically, so we
680
+ // always restore it — losing it would break Rust's
681
+ // `find_matching_previous_session_index` lookup on subsequent
682
+ // PreKeySignal arrivals.
683
+ aliceBaseKey: new Uint8Array(fromB64(entry.indexInfo.baseKey))
684
+ };
685
+ if (entry.pendingPreKey?.baseKey) {
686
+ session.pendingPreKey = {
687
+ preKeyId: entry.pendingPreKey.preKeyId ?? null,
688
+ signedPreKeyId: entry.pendingPreKey.signedKeyId ?? null,
689
+ baseKey: new Uint8Array(fromB64(entry.pendingPreKey.baseKey))
690
+ };
691
+ }
692
+ return session;
693
+ }
694
+ function upstreamSessionRecordToProto(record) {
695
+ const r = record;
696
+ const sessions = r?._sessions ?? {};
697
+ let currentSession = null;
698
+ const previousSessions = [];
699
+ // Pick the first OPEN session as `current_session`; everything else
700
+ // goes to `previous_sessions`. Both libraries should agree there is
701
+ // at most one open session per peer.
702
+ for (const entry of Object.values(sessions)) {
703
+ const ss = entryToSessionStructure(entry);
704
+ if (!currentSession && entry.indexInfo.closed === -1) {
705
+ currentSession = ss;
706
+ }
707
+ else {
708
+ previousSessions.push(ss);
709
+ }
710
+ }
711
+ // `BridgeSessionProto` is a structural mirror of `ISessionStructure`
712
+ // from `whatsapp-rust-bridge/proto-types`; the differences are nullable
713
+ // vs optional fields. We cast at the boundary to avoid pulling the full
714
+ // proto class types into our internal shape.
715
+ const recordOut = proto.RecordStructure.create({
716
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
717
+ currentSession: (currentSession ?? undefined),
718
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
719
+ previousSessions: previousSessions
720
+ });
721
+ return proto.RecordStructure.encode(recordOut).finish();
722
+ }
723
+ // ── session key translator ──
724
+ // Bridge protocol address: `{user}[:jidDev]@{server}.{signalDev}`
725
+ // Upstream session key: `{signalUser}.{deviceId}` (where deviceId is
726
+ // the JID device for `:jidDev` cases, else the signal device suffix).
727
+ // `bridgeAddrToBaileysAddr` already does this exact transformation —
728
+ // it's just been unused for the session path because sessions were
729
+ // passthrough binary. Now that sessions go through a converter, we
730
+ // route through `translateKey` → `bridgeAddrToBaileysAddr`.
208
731
  // ---- Device/Creds ----
209
732
  /** Parse a JID string into { user, device, server } */
210
733
  function parseJid(jid) {
@@ -259,6 +782,21 @@ function credsToDeviceJson(creds) {
259
782
  next_pre_key_id: creds.nextPreKeyId ?? 1
260
783
  });
261
784
  }
785
+ /**
786
+ * Decode a `[priv(32)…pub(32)]` byte array (the bridge's `kp` shape) back into
787
+ * the upstream Baileys `{public, private}` keypair. Returns `undefined` for
788
+ * absent / all-zero arrays so callers can use `??` to keep prior values.
789
+ */
790
+ function numArrayToKeyPair(arr) {
791
+ if (!Array.isArray(arr) || arr.length < 64)
792
+ return undefined;
793
+ if (arr.every(b => b === 0))
794
+ return undefined;
795
+ return {
796
+ private: new Uint8Array(arr.slice(0, 32)),
797
+ public: new Uint8Array(arr.slice(32, 64))
798
+ };
799
+ }
262
800
  function updateCredsFromDevice(bytes, creds) {
263
801
  try {
264
802
  const d = fromJson(bytes);
@@ -293,6 +831,29 @@ function updateCredsFromDevice(bytes, creds) {
293
831
  creds.advSecretKey = buf.toString('base64');
294
832
  }
295
833
  }
834
+ // Hydrate the noise / identity / signed pre-key material so the next
835
+ // `credsToDeviceJson(creds)` round-trip emits real bytes instead of
836
+ // zero-filled placeholders. Without this, a paired session looks
837
+ // half-valid on the next process start (`me.id` set, but key slots
838
+ // all zero) and the bridge's noise handshake gets rejected by the
839
+ // server with stream-error precondition-required.
840
+ const noise = numArrayToKeyPair(d.noise_key);
841
+ if (noise)
842
+ creds.noiseKey = noise;
843
+ const identity = numArrayToKeyPair(d.identity_key);
844
+ if (identity)
845
+ creds.signedIdentityKey = identity;
846
+ const signedPreKeyPair = numArrayToKeyPair(d.signed_pre_key);
847
+ if (signedPreKeyPair &&
848
+ Array.isArray(d.signed_pre_key_signature) &&
849
+ d.signed_pre_key_signature.length === 64 &&
850
+ d.signed_pre_key_signature.some(b => b !== 0)) {
851
+ creds.signedPreKey = {
852
+ keyPair: signedPreKeyPair,
853
+ keyId: d.signed_pre_key_id ?? 1,
854
+ signature: new Uint8Array(d.signed_pre_key_signature)
855
+ };
856
+ }
296
857
  }
297
858
  catch (e) {
298
859
  warn('updateCredsFromDevice failed:', e);
@@ -444,22 +1005,58 @@ export async function wrapLegacyStore(state, saveCreds, logger) {
444
1005
  }, 100);
445
1006
  };
446
1007
  /** Route a store to its handler */
447
- const resolve = (store) => {
448
- if (store === 'device')
1008
+ const resolve = (storeName) => {
1009
+ if (storeName === 'device')
449
1010
  return 'device';
450
- if (BINARY_STORES.has(store))
1011
+ if (BINARY_STORES.has(storeName))
451
1012
  return 'signal';
452
- if (BRIDGE_ONLY_STORES.has(store))
1013
+ if (BRIDGE_ONLY_STORES.has(storeName))
453
1014
  return 'bridge-only';
454
- if (STORE_MAP[store])
1015
+ if (STORE_MAP[storeName])
455
1016
  return 'converter';
456
1017
  return 'unknown';
457
1018
  };
458
1019
  /** Translate bridge key format to upstream Baileys key format */
459
- const translateKey = (store, key) => {
1020
+ const translateKey = (storeName, key) => {
460
1021
  // Bridge encodes sync_key IDs as hex; upstream stores use base64
461
- if (store === 'sync_key')
1022
+ if (storeName === 'sync_key')
462
1023
  return hexToBase64(key);
1024
+ // Bridge sender_key cache_key uses single-colon separator + signal-
1025
+ // device suffix; upstream's `SenderKeyName.serialize()` uses double
1026
+ // colons and drops the signal device — see bridgeSenderKeyToUpstream.
1027
+ if (storeName === 'sender_key') {
1028
+ const translated = bridgeSenderKeyToUpstream(key);
1029
+ if (translated == null) {
1030
+ // eslint-disable-line eqeqeq -- intentional null+undefined check
1031
+ warn(`sender_key key translation failed for "${key}", falling back to passthrough`);
1032
+ return key;
1033
+ }
1034
+ return translated;
1035
+ }
1036
+ // Bridge session key is the protocol address (`user[:dev]@server.sig`);
1037
+ // upstream's libsignal looks under `signalUser.deviceId`. Reuses
1038
+ // `bridgeAddrToBaileysAddr` which already does the LID/PN→signalUser
1039
+ // rewrite + JID-device extraction.
1040
+ if (storeName === 'session') {
1041
+ const translated = bridgeAddrToBaileysAddr(key);
1042
+ if (translated == null) {
1043
+ // eslint-disable-line eqeqeq -- intentional null+undefined check
1044
+ warn(`session key translation failed for "${key}", falling back to passthrough`);
1045
+ return key;
1046
+ }
1047
+ return translated;
1048
+ }
1049
+ // Bridge lid_mapping uses prefixed keys (`lid:{X}`, `pn:{X}`) that
1050
+ // upstream's `Signal/lid-mapping.js` doesn't recognize. Map them to
1051
+ // upstream's bare-userpart shape: `pn:{X}` → `{X}`, `lid:{X}` →
1052
+ // `{X}_reverse`.
1053
+ if (storeName === 'lid_mapping') {
1054
+ if (key.startsWith('pn:'))
1055
+ return key.slice(3);
1056
+ if (key.startsWith('lid:'))
1057
+ return `${key.slice(4)}_reverse`;
1058
+ return key;
1059
+ }
463
1060
  return key;
464
1061
  };
465
1062
  /** Read a binary value from the keys store */
@@ -496,8 +1093,19 @@ export async function wrapLegacyStore(state, saveCreds, logger) {
496
1093
  const route = resolve(bridgeStore);
497
1094
  const type = STORE_MAP[bridgeStore];
498
1095
  if (route === 'device') {
499
- if (key === 'device')
1096
+ if (key === 'device') {
1097
+ // Fresh/stub creds (baileyrs' `initAuthCreds()` shim, or an
1098
+ // upstream flow that hadn't finished its first pair) carry
1099
+ // neither a noise key nor a paired `me.id`. Returning a
1100
+ // zeroed `credsToDeviceJson` in that state poisons the
1101
+ // bridge's noise handshake (server responds 428 Precondition
1102
+ // Required). Signaling `null` instead lets the bridge
1103
+ // bootstrap its own device identity and write it back
1104
+ // through `set('device', 'device', …)` on pair-success.
1105
+ if (!creds.noiseKey && !creds.me?.id)
1106
+ return null;
500
1107
  return credsToDeviceJson(creds);
1108
+ }
501
1109
  if (key === 'account') {
502
1110
  if (!creds.account)
503
1111
  return null;