@oxidezap/baileyrs 0.1.2 → 0.2.0

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 (50) hide show
  1. package/README.md +60 -0
  2. package/lib/Bridge/primitives.d.ts +49 -3
  3. package/lib/Bridge/primitives.js +126 -8
  4. package/lib/Bridge/schema.js +208 -87
  5. package/lib/Bridge/types.d.ts +61 -2
  6. package/lib/Compatibility/derived-stanza-nodes.d.ts +28 -0
  7. package/lib/Compatibility/derived-stanza-nodes.js +71 -0
  8. package/lib/Compatibility/encode-proto.d.ts +17 -0
  9. package/lib/Compatibility/encode-proto.js +32 -0
  10. package/lib/Compatibility/proto-runtime.d.ts +10 -0
  11. package/lib/Compatibility/proto-runtime.js +176 -11
  12. package/lib/Defaults/index.d.ts +6 -0
  13. package/lib/Defaults/index.js +6 -0
  14. package/lib/Socket/blocking.d.ts +3 -1
  15. package/lib/Socket/blocking.js +3 -0
  16. package/lib/Socket/communities.d.ts +14 -9
  17. package/lib/Socket/communities.js +24 -5
  18. package/lib/Socket/contacts.d.ts +3 -1
  19. package/lib/Socket/contacts.js +3 -0
  20. package/lib/Socket/events.js +35 -6
  21. package/lib/Socket/groups.d.ts +30 -4
  22. package/lib/Socket/groups.js +20 -4
  23. package/lib/Socket/index.d.ts +18 -17
  24. package/lib/Socket/index.js +18 -6
  25. package/lib/Socket/messages.js +18 -7
  26. package/lib/Socket/newsletter.d.ts +3 -1
  27. package/lib/Socket/newsletter.js +3 -3
  28. package/lib/Socket/presence.d.ts +3 -2
  29. package/lib/Socket/presence.js +4 -0
  30. package/lib/Socket/privacy.d.ts +7 -1
  31. package/lib/Socket/privacy.js +16 -0
  32. package/lib/Socket/server-queries.d.ts +3 -1
  33. package/lib/Socket/server-queries.js +3 -0
  34. package/lib/Types/Auth.d.ts +4 -1
  35. package/lib/Types/Chat.d.ts +26 -8
  36. package/lib/Types/Chat.js +18 -0
  37. package/lib/Types/Events.d.ts +17 -0
  38. package/lib/Types/GroupMetadata.d.ts +3 -1
  39. package/lib/Types/GroupMetadata.js +1 -1
  40. package/lib/Types/Message.d.ts +20 -2
  41. package/lib/Types/Message.js +11 -0
  42. package/lib/Utils/argument-domain.d.ts +15 -0
  43. package/lib/Utils/argument-domain.js +36 -0
  44. package/lib/Utils/event-buffer.js +48 -11
  45. package/lib/Utils/messages.d.ts +3 -1
  46. package/lib/Utils/messages.js +35 -7
  47. package/lib/Utils/process-history-message.d.ts +11 -2
  48. package/lib/Utils/process-history-message.js +11 -7
  49. package/lib/Utils/use-multi-file-auth-state.js +45 -0
  50. package/package.json +9 -3
package/lib/Types/Chat.js CHANGED
@@ -1,3 +1,21 @@
1
+ /**
2
+ * privacy settings in WhatsApp Web
3
+ *
4
+ * Each of these is a set first and a type second: the wrappers that take one
5
+ * check the value against the same array the type is derived from, so a value
6
+ * cannot be accepted by the compiler and refused at runtime, or the reverse.
7
+ */
8
+ export const WA_PRIVACY_VALUES = ['all', 'contacts', 'contact_blacklist', 'none'];
9
+ export const WA_PRIVACY_ONLINE_VALUES = ['all', 'match_last_seen'];
10
+ export const WA_PRIVACY_GROUP_ADD_VALUES = ['all', 'contacts', 'contact_blacklist'];
11
+ export const WA_READ_RECEIPTS_VALUES = ['all', 'none'];
12
+ export const WA_PRIVACY_CALL_VALUES = ['all', 'known'];
13
+ export const WA_PRIVACY_MESSAGES_VALUES = ['all', 'contacts'];
14
+ /** the two the account itself broadcasts, as opposed to the per-chat states */
15
+ export const WA_PRESENCE_STATUSES = ['unavailable', 'available'];
16
+ export const WA_CHAT_STATES = ['composing', 'recording', 'paused'];
17
+ /** set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send */
18
+ export const WA_PRESENCES = [...WA_PRESENCE_STATUSES, ...WA_CHAT_STATES];
1
19
  export const ALL_WA_PATCH_NAMES = [
2
20
  'critical_block',
3
21
  'critical_unblock_low',
@@ -132,6 +132,23 @@ export type BaileysEventMap = {
132
132
  association: LabelAssociation;
133
133
  type: 'add' | 'remove';
134
134
  };
135
+ /**
136
+ * A batched app-state sync left collections unsynced. Not an upstream event:
137
+ * upstream never withholds a session on app state, so it has nothing to
138
+ * report here.
139
+ *
140
+ * The engine does the opposite of withholding — it announces a connection
141
+ * whose critical sync came back degraded, precisely so a session that works
142
+ * is usable — and this is what says which collections are missing from it.
143
+ * `connected` tells "degraded but usable" from a sync that ran before the
144
+ * connection was ready; `fatal` is the half a retry cannot fix.
145
+ */
146
+ 'app-state-sync.failed': {
147
+ fatal: string[];
148
+ retryable: string[];
149
+ skipped: string[];
150
+ connected: boolean;
151
+ };
135
152
  /** Newsletter-related events */
136
153
  'newsletter.reaction': {
137
154
  id: string;
@@ -5,7 +5,9 @@ export type GroupParticipant = Contact & {
5
5
  isSuperAdmin?: boolean;
6
6
  admin?: 'admin' | 'superadmin' | null;
7
7
  };
8
- export type ParticipantAction = 'add' | 'remove' | 'promote' | 'demote' | 'modify';
8
+ export declare const PARTICIPANT_ACTIONS: readonly ['add', 'remove', 'promote', 'demote', 'modify'];
9
+ /** Derived from the values, so the runtime check and the type cannot drift. */
10
+ export type ParticipantAction = (typeof PARTICIPANT_ACTIONS)[number];
9
11
  export type RequestJoinAction = 'created' | 'revoked' | 'rejected';
10
12
  export type RequestJoinMethod = 'invite_link' | 'linked_group_join' | 'non_admin_add' | undefined;
11
13
  export interface GroupMetadata {
@@ -1,2 +1,2 @@
1
- export {};
1
+ export const PARTICIPANT_ACTIONS = ['add', 'remove', 'promote', 'demote', 'modify'];
2
2
  //# sourceMappingURL=GroupMetadata.js.map
@@ -1,3 +1,4 @@
1
+ import type Long from 'long';
1
2
  import type { Readable } from 'stream';
2
3
  import type { URL } from 'url';
3
4
  import type { MediaType as BridgeMediaType, UploadMediaResult, WasmWhatsAppClient } from '@oxidezap/whatsapp-rust-bridge';
@@ -8,8 +9,23 @@ import type { BinaryNode } from './BinaryNode.js';
8
9
  import type { GroupMetadata } from './GroupMetadata.js';
9
10
  import type { CacheStore } from './Socket.js';
10
11
  export { proto as WAProto };
11
- export type WAMessage = Omit<proto.IWebMessageInfo, 'messageStubParameters'> & {
12
+ export type WAMessage = Omit<proto.IWebMessageInfo, 'messageStubParameters' | 'messageTimestamp'> & {
12
13
  key: WAMessageKey;
14
+ /**
15
+ * `number | Long`, as upstream declares it — not the neutral codec's `Int64`.
16
+ *
17
+ * From bridge 0.8.0 a 64-bit field is typed `number | { low, high, unsigned }`,
18
+ * a plain data shape carrying none of Long's methods. That is what the *codec*
19
+ * produces; it is not what this library hands out. The compatibility facade
20
+ * supplies a reader that materialises every 64-bit word as a long.js Long
21
+ * whatever its magnitude, and the published declaration has always said so, so
22
+ * a consumer calling `.toNumber()` is right to expect one.
23
+ *
24
+ * Declared here rather than left to flow through, because the neutral shape
25
+ * otherwise reaches every type derived from `WAMessage` and stops them being
26
+ * assignable to upstream's.
27
+ */
28
+ messageTimestamp?: number | Long | null;
13
29
  category?: string;
14
30
  retryCount?: number;
15
31
  messageStubParameters?: any;
@@ -51,7 +67,9 @@ export type MessageType = keyof proto.Message;
51
67
  export declare const WAMessageAddressingMode: typeof WAMessageAddressingModeType;
52
68
  export type WAMessageAddressingMode = WAMessageAddressingModeType;
53
69
  export type MessageWithContextInfo = 'imageMessage' | 'contactMessage' | 'locationMessage' | 'extendedTextMessage' | 'documentMessage' | 'audioMessage' | 'videoMessage' | 'call' | 'contactsArrayMessage' | 'liveLocationMessage' | 'templateMessage' | 'stickerMessage' | 'groupInviteMessage' | 'templateButtonReplyMessage' | 'productMessage' | 'listMessage' | 'orderMessage' | 'listResponseMessage' | 'buttonsMessage' | 'buttonsResponseMessage' | 'interactiveMessage' | 'interactiveResponseMessage' | 'pollCreationMessage' | 'requestPhoneNumberMessage' | 'messageHistoryBundle' | 'eventMessage' | 'newsletterAdminInviteMessage' | 'albumMessage' | 'stickerPackMessage' | 'pollResultSnapshotMessage' | 'messageHistoryNotice';
54
- export type MessageReceiptType = 'read' | 'read-self' | 'hist_sync' | 'peer_msg' | 'sender' | 'inactive' | 'played' | undefined;
70
+ /** `undefined` is a member: it is how upstream spells a delivery receipt. */
71
+ export declare const MESSAGE_RECEIPT_TYPES: readonly ['read', 'read-self', 'hist_sync', 'peer_msg', 'sender', 'inactive', 'played', undefined];
72
+ export type MessageReceiptType = (typeof MESSAGE_RECEIPT_TYPES)[number];
55
73
  export type MediaConnInfo = {
56
74
  auth: string;
57
75
  ttl: number;
@@ -7,4 +7,15 @@ export const WAMessageAddressingMode = Object.freeze({
7
7
  PN: 'pn',
8
8
  LID: 'lid'
9
9
  });
10
+ /** `undefined` is a member: it is how upstream spells a delivery receipt. */
11
+ export const MESSAGE_RECEIPT_TYPES = [
12
+ 'read',
13
+ 'read-self',
14
+ 'hist_sync',
15
+ 'peer_msg',
16
+ 'sender',
17
+ 'inactive',
18
+ 'played',
19
+ undefined
20
+ ];
10
21
  //# sourceMappingURL=Message.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Refuse a value outside a closed set, naming the method, the parameter, what
3
+ * arrived and everything that is accepted.
4
+ *
5
+ * Call it in the synchronous prefix of the public method, ahead of the first
6
+ * `await`. That is what puts the caller's own frame in the stack: a
7
+ * fire-and-forget call leaves no awaiting frame for V8 to stitch an async
8
+ * stack onto, and past the bridge boundary the rejection is built inside wasm
9
+ * and carries nothing but wasm frames.
10
+ *
11
+ * The domain is always the values that define the parameter's type, never a
12
+ * second list written beside it.
13
+ */
14
+ export declare const assertArgumentDomain: <Value extends string | undefined>(method: string, parameter: string, value: unknown, domain: readonly Value[]) => Value;
15
+ //# sourceMappingURL=argument-domain.d.ts.map
@@ -0,0 +1,36 @@
1
+ import { Boom } from './boom.js';
2
+ /** Quoted when it is a string, bare otherwise, so `""` and `undefined` read apart. */
3
+ const shown = (value) => {
4
+ if (typeof value === 'string')
5
+ return JSON.stringify(value);
6
+ try {
7
+ return String(value);
8
+ }
9
+ catch {
10
+ // A null-prototype object, a revoked proxy, a trap that throws. `typeof`
11
+ // reads nothing off the value, so reporting it cannot fail on it.
12
+ return `[${typeof value}]`;
13
+ }
14
+ };
15
+ /**
16
+ * Refuse a value outside a closed set, naming the method, the parameter, what
17
+ * arrived and everything that is accepted.
18
+ *
19
+ * Call it in the synchronous prefix of the public method, ahead of the first
20
+ * `await`. That is what puts the caller's own frame in the stack: a
21
+ * fire-and-forget call leaves no awaiting frame for V8 to stitch an async
22
+ * stack onto, and past the bridge boundary the rejection is built inside wasm
23
+ * and carries nothing but wasm frames.
24
+ *
25
+ * The domain is always the values that define the parameter's type, never a
26
+ * second list written beside it.
27
+ */
28
+ export const assertArgumentDomain = (method, parameter, value, domain) => {
29
+ if (domain.includes(value))
30
+ return value;
31
+ const error = new Boom(`${method}: ${JSON.stringify(parameter)} must be one of ${domain.map(shown).join(', ')}, received ${shown(value)}`, { statusCode: 400, data: { parameter, value, accepted: [...domain] } });
32
+ // Drop this frame: the method the consumer called is the useful top.
33
+ Error.captureStackTrace(error, assertArgumentDomain);
34
+ throw error;
35
+ };
36
+ //# sourceMappingURL=argument-domain.js.map
@@ -143,7 +143,14 @@ const append = (data, historyCache, event, eventData, logger) => {
143
143
  case 'chats.upsert': {
144
144
  for (const chat of eventData) {
145
145
  const id = chat.id || '';
146
- let existing = data.chatUpserts[id] || data.historySets.chats[id];
146
+ // The history set is only consulted for a chat that *has* an id.
147
+ // Upstream guards the lookup with `id &&`, and this port had dropped
148
+ // it: an id-less chat then folded into whatever id-less entry a
149
+ // buffered history set happened to carry, summing their unread counts,
150
+ // where upstream releases it as its own `chats.upsert`. Found by the
151
+ // buffer differential once history rows started drawing from the same
152
+ // identity pool as live traffic.
153
+ let existing = data.chatUpserts[id] || (id ? data.historySets.chats[id] : undefined);
147
154
  if (existing)
148
155
  concatChats(existing, chat);
149
156
  else {
@@ -185,14 +192,37 @@ const append = (data, historyCache, event, eventData, logger) => {
185
192
  case 'contacts.upsert': {
186
193
  for (const contact of eventData) {
187
194
  const existing = data.contactUpserts[contact.id] || data.historySets.contacts[contact.id];
188
- if (existing)
189
- Object.assign(existing, trimUndefined(contact));
190
- else
191
- data.contactUpserts[contact.id] = contact;
195
+ // A `contacts.update` already buffered for this id arrived *before*
196
+ // this upsert, so it is folded in first and the upsert's own values
197
+ // win where the two carry the same field. Folding it last — which is
198
+ // what this did — let a stale name overwrite the one that came after
199
+ // it, and the buffer released the older of the two. Fields only the
200
+ // update carried still survive, which upstream drops.
192
201
  const pending = data.contactUpdates[contact.id];
193
- if (pending) {
194
- Object.assign(existing || contact, pending);
202
+ if (pending)
195
203
  delete data.contactUpdates[contact.id];
204
+ if (existing) {
205
+ if (pending)
206
+ Object.assign(existing, pending);
207
+ Object.assign(existing, trimUndefined(contact));
208
+ }
209
+ else {
210
+ // Filling the gaps in `contact` rather than merging onto `pending`
211
+ // and storing that: `pending` was grown from `{}` by the update
212
+ // branch, and accumulating into it — or into a fresh literal —
213
+ // measured about twice the cost of writing into the contact the
214
+ // caller handed us, which arrives with a settled shape. An
215
+ // explicitly-`undefined` field counts as absent, which is what
216
+ // `trimUndefined` would decide and costs no second pass.
217
+ if (pending) {
218
+ const target = contact;
219
+ const source = pending;
220
+ for (const field in source) {
221
+ if (target[field] === undefined)
222
+ target[field] = source[field];
223
+ }
224
+ }
225
+ data.contactUpserts[contact.id] = contact;
196
226
  }
197
227
  }
198
228
  break;
@@ -326,17 +356,21 @@ const consolidateEvents = (data) => {
326
356
  if (Array.isArray(values) && values.length > 0)
327
357
  events[event] = values;
328
358
  };
359
+ // The order of these writes is the contract, not an implementation detail.
360
+ // A flush walks `Object.keys()` of this map, so insertion order decides both
361
+ // the key order a `process()` handler iterates and the order the individual
362
+ // events are re-dispatched to `.on()` listeners. Handlers that assume
363
+ // upstream's sequence — messages before the contacts they reference — see a
364
+ // different interleaving if these move. Keep them aligned with upstream's
365
+ // `consolidateEvents`.
329
366
  assignArray('chats.upsert', Object.values(data.chatUpserts));
330
367
  assignArray('chats.update', Object.values(data.chatUpdates));
331
368
  assignArray('chats.delete', [...data.chatDeletes]);
332
- assignArray('contacts.upsert', Object.values(data.contactUpserts));
333
- assignArray('contacts.update', Object.values(data.contactUpdates));
334
- assignArray('messages.update', Object.values(data.messageUpdates));
335
- assignArray('groups.update', Object.values(data.groupUpdates));
336
369
  const upserts = Object.values(data.messageUpserts);
337
370
  if (upserts.length) {
338
371
  events['messages.upsert'] = { messages: upserts.map(item => item.message), type: upserts[0].type };
339
372
  }
373
+ assignArray('messages.update', Object.values(data.messageUpdates));
340
374
  const deleted = Object.values(data.messageDeletes);
341
375
  if (deleted.length)
342
376
  events['messages.delete'] = { keys: deleted };
@@ -346,6 +380,9 @@ const consolidateEvents = (data) => {
346
380
  const receipts = Object.values(data.messageReceipts).flatMap(({ key, userReceipt }) => userReceipt.map(receipt => ({ key, receipt })));
347
381
  if (receipts.length)
348
382
  events['message-receipt.update'] = receipts;
383
+ assignArray('contacts.upsert', Object.values(data.contactUpserts));
384
+ assignArray('contacts.update', Object.values(data.contactUpdates));
385
+ assignArray('groups.update', Object.values(data.groupUpdates));
349
386
  return events;
350
387
  };
351
388
  /**
@@ -55,6 +55,8 @@ export type DownloadMediaMessageContext = {
55
55
  /** Bridge client for media download. Falls back to the registered one. */
56
56
  waClient?: Pick<WasmWhatsAppClient, 'downloadMedia' | 'downloadMediaStream'>;
57
57
  };
58
+ export declare const MEDIA_DOWNLOAD_TYPES: readonly ['buffer', 'stream'];
59
+ export type MediaDownloadType = (typeof MEDIA_DOWNLOAD_TYPES)[number];
58
60
  /**
59
61
  * Downloads the given message. Throws an error if it's not a media message.
60
62
  *
@@ -68,7 +70,7 @@ export type DownloadMediaMessageContext = {
68
70
  * several sockets should pass `ctx` explicitly, because the registration points
69
71
  * at whichever client was created last.
70
72
  */
71
- export declare const downloadMediaMessage: <Type extends 'buffer' | 'stream'>(message: WAMessage, type: Type, options: MediaDownloadOptions, ctx?: DownloadMediaMessageContext) => Promise<Type extends "buffer" ? Buffer<ArrayBufferLike> : Readable>;
73
+ export declare const downloadMediaMessage: <Type extends MediaDownloadType>(message: WAMessage, type: Type, options: MediaDownloadOptions, ctx?: DownloadMediaMessageContext) => Promise<Type extends "buffer" ? Buffer<ArrayBufferLike> : Readable>;
72
74
  export declare const _registerActiveBridgeClient: (client: WasmWhatsAppClient, logger?: ILogger) => void;
73
75
  /**
74
76
  * Drop the module-level pointer when `sock.end()` frees the client it points
@@ -6,6 +6,7 @@ import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT
6
6
  import { WAMessageStatus, WAProto } from '../Types/index.js';
7
7
  import { proto } from '../WAProto/runtime.js';
8
8
  import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
9
+ import { assertArgumentDomain } from './argument-domain.js';
9
10
  import { Boom } from './boom.js';
10
11
  import { sha256 } from './crypto.js';
11
12
  import { getKeyAuthor, toNumber, unixTimestampSeconds } from './generics.js';
@@ -227,22 +228,45 @@ export const generateForwardMessageContent = (message, forceForward) => {
227
228
  throw new Boom('no content in message', { statusCode: 400 });
228
229
  }
229
230
  content = normalizeMessageContent(content);
230
- // Shallow clone — only the inner message object gets modified (contextInfo)
231
+ // Shallow clone of the outer map one entry on it is rewritten below.
231
232
  content = { ...content };
232
233
  let key = Object.keys(content)[0];
233
234
  let score = content?.[key]?.contextInfo?.forwardingScore || 0;
234
235
  score += message.key.fromMe && !forceForward ? 0 : 1;
236
+ const contextInfo = score > 0 ? { forwardingScore: score, isForwarded: true } : {};
237
+ // The nested message object is the *caller's*, reached through the shallow
238
+ // clone above, so writing `contextInfo` onto it wrote into their argument.
239
+ // And it replaces rather than merges: a caller who forwarded a quoted message
240
+ // found `stanzaId` and `participant` gone from their own object afterwards.
241
+ // Upstream leaves the argument untouched.
242
+ //
243
+ // Rebuilt rather than deep-copied, and only the one object being written.
244
+ // Upstream's copy is `proto.Message.decode(proto.Message.encode(content))` —
245
+ // a full serialise/parse round trip on a hot send path — which is not worth
246
+ // paying to fix an aliasing bug.
235
247
  if (key === 'conversation') {
236
- content.extendedTextMessage = { text: content[key] };
248
+ // This object is created here, so nothing of the caller's is aliased and
249
+ // the `contextInfo` goes straight in. Same allocation count as before.
250
+ content.extendedTextMessage = { text: content[key], contextInfo };
237
251
  delete content.conversation;
238
252
  key = 'extendedTextMessage';
239
- }
240
- const key_ = content?.[key];
241
- if (score > 0) {
242
- key_.contextInfo = { forwardingScore: score, isForwarded: true };
253
+ return content;
254
+ }
255
+ const nested = content[key];
256
+ // A plain object is the only thing worth copying, and the only thing that can
257
+ // be the caller's to damage. Anything else — an absent slot on an empty
258
+ // message, a primitive, an array — keeps the original assignment, which
259
+ // throws for exactly the inputs it threw for before and that upstream throws
260
+ // for too. Spreading those instead invented a property named `"undefined"`
261
+ // where upstream raised a TypeError.
262
+ if (typeof nested === 'object' && nested !== null && !Array.isArray(nested)) {
263
+ // One shallow spread, of exactly the object being modified. This is the
264
+ // whole cost of the fix, and it is unavoidable: not writing into the
265
+ // caller's object means writing into a different one.
266
+ content[key] = { ...nested, contextInfo };
243
267
  }
244
268
  else {
245
- key_.contextInfo = {};
269
+ nested.contextInfo = contextInfo;
246
270
  }
247
271
  return content;
248
272
  };
@@ -664,6 +688,7 @@ export const extractMessageContent = (content) => {
664
688
  }
665
689
  return content;
666
690
  };
691
+ export const MEDIA_DOWNLOAD_TYPES = ['buffer', 'stream'];
667
692
  /**
668
693
  * Downloads the given message. Throws an error if it's not a media message.
669
694
  *
@@ -678,6 +703,9 @@ export const extractMessageContent = (content) => {
678
703
  * at whichever client was created last.
679
704
  */
680
705
  export const downloadMediaMessage = async (message, type, options, ctx) => {
706
+ // Anything but 'buffer' used to take the stream branch, so a typo returned
707
+ // a Readable to a caller holding it as a Buffer.
708
+ assertArgumentDomain('downloadMediaMessage', 'type', type, MEDIA_DOWNLOAD_TYPES);
681
709
  const waClient = ctx?.waClient ?? activeBridgeClient;
682
710
  if (!waClient) {
683
711
  throw new Boom('downloadMediaMessage: no bridge client available, and the download, its CDN failover and its decryption all happen in the engine. Pass `{ waClient: sock.waClient }`, use `sock.downloadMedia(message, type, options)`, or call after `makeWASocket()` has initialized.', { statusCode: 500 });
@@ -36,6 +36,15 @@ export declare const processHistoryMessage: (item: proto.IHistorySync, logger?:
36
36
  export declare const downloadHistory: (msg: proto.Message.IHistorySyncNotification, options: RequestInit) => Promise<proto.HistorySync>;
37
37
  /** Resolve inline or external history-sync content and normalize its public payload. */
38
38
  export declare const downloadAndProcessHistorySyncNotification: (msg: proto.Message.IHistorySyncNotification, options: RequestInit, logger?: ILogger) => Promise<ProcessedHistorySync>;
39
- /** Extract a history-sync notification through the same wrapper normalization as upstream. */
40
- export declare const getHistoryMsg: (message: proto.IMessage) => proto.Message.IHistorySyncNotification;
39
+ /**
40
+ * Extract a history-sync notification through the same wrapper normalization as
41
+ * upstream.
42
+ *
43
+ * Returns `undefined` when the message carries none. It used to throw a Boom
44
+ * 400, which breaks the shape every caller writes against a drop-in API —
45
+ * `const h = getHistoryMsg(msg); if (!h) return` crashed instead of returning.
46
+ * "Absent" is the ordinary case here, not an error: any message that is not a
47
+ * history sync takes this path.
48
+ */
49
+ export declare const getHistoryMsg: (message: proto.IMessage) => proto.Message.IHistorySyncNotification | undefined;
41
50
  //# sourceMappingURL=process-history-message.d.ts.map
@@ -14,7 +14,6 @@ import { inflateZlib } from '@oxidezap/whatsapp-rust-bridge';
14
14
  import { proto } from '../WAProto/runtime.js';
15
15
  import { WAProto } from '../Types/index.js';
16
16
  import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser } from '../WABinary/jid-utils.js';
17
- import { Boom } from './boom.js';
18
17
  import { toNumber } from './generics.js';
19
18
  import { downloadContentFromMessage, normalizeMessageContent } from './messages.js';
20
19
  import { createSparseArray } from './sparse-array.js';
@@ -199,13 +198,18 @@ export const downloadAndProcessHistorySyncNotification = async (msg, options, lo
199
198
  : await downloadHistory(msg, options);
200
199
  return processHistoryMessage(historyMsg, logger);
201
200
  };
202
- /** Extract a history-sync notification through the same wrapper normalization as upstream. */
201
+ /**
202
+ * Extract a history-sync notification through the same wrapper normalization as
203
+ * upstream.
204
+ *
205
+ * Returns `undefined` when the message carries none. It used to throw a Boom
206
+ * 400, which breaks the shape every caller writes against a drop-in API —
207
+ * `const h = getHistoryMsg(msg); if (!h) return` crashed instead of returning.
208
+ * "Absent" is the ordinary case here, not an error: any message that is not a
209
+ * history sync takes this path.
210
+ */
203
211
  export const getHistoryMsg = (message) => {
204
212
  const normalizedContent = message ? normalizeMessageContent(message) : undefined;
205
- const historySyncNotification = normalizedContent?.protocolMessage?.historySyncNotification;
206
- if (!historySyncNotification) {
207
- throw new Boom('Message does not contain a history sync notification', { statusCode: 400 });
208
- }
209
- return historySyncNotification;
213
+ return normalizedContent?.protocolMessage?.historySyncNotification ?? undefined;
210
214
  };
211
215
  //# sourceMappingURL=process-history-message.js.map
@@ -1,7 +1,44 @@
1
1
  import { mkdir, stat } from 'node:fs/promises';
2
+ import { createDeviceProjection } from '../Compatibility/legacy-store/device.js';
2
3
  import { projectNativeStore } from '../Compatibility/legacy-store/native-projection.js';
3
4
  import { initAuthCreds } from './generics.js';
4
5
  import { useBridgeStore } from './use-bridge-store.js';
6
+ /**
7
+ * The store namespace and keys the engine writes its own device under.
8
+ *
9
+ * `<folder>/device-device.bin` and `<folder>/device-account.bin` on disk.
10
+ */
11
+ const DEVICE_STORE = 'device';
12
+ const DEVICE_RECORDS = ['device', 'account'];
13
+ /**
14
+ * Rebuild the credential mirror from the device the engine persisted.
15
+ *
16
+ * Without this the mirror is whatever `initAuthCreds()` just made up: every
17
+ * restart handed back `registered: false` and `me: undefined` for a session
18
+ * that was paired and working, because nothing on this path ever read the
19
+ * device back. The hydration itself already existed for `wrapLegacyStore`
20
+ * (`Compatibility/legacy-store/adapter.ts`), which only runs for callers that
21
+ * bring their own `{ creds, keys }`; a caller using this function goes straight
22
+ * to the bridge store and used to skip it entirely.
23
+ *
24
+ * A record that fails to decode is skipped rather than fatal: a mirror missing
25
+ * a field is worth less than a socket that will not start, and the engine reads
26
+ * its own device from the same bytes regardless of what this makes of them.
27
+ */
28
+ const hydrateFromStore = async (store, creds) => {
29
+ const projection = createDeviceProjection(creds);
30
+ for (const record of DEVICE_RECORDS) {
31
+ const payload = await store.get(DEVICE_STORE, record);
32
+ if (!payload)
33
+ continue;
34
+ try {
35
+ projection.prepare(record, payload)();
36
+ }
37
+ catch {
38
+ /* a record we cannot read leaves that part of the mirror at its default */
39
+ }
40
+ }
41
+ };
5
42
  /**
6
43
  * Creates a file-based authentication state for the Rust bridge.
7
44
  *
@@ -30,6 +67,14 @@ export const useMultiFileAuthState = async (folder) => {
30
67
  }
31
68
  const store = await useBridgeStore(folder);
32
69
  const creds = initAuthCreds();
70
+ // Before `projectNativeStore`, and that ordering is the whole point: the
71
+ // projection captures `signedIdentityKey.public` and `registrationId` off
72
+ // `creds` when it is called, and builds the Signal codecs around them.
73
+ // Hydrating afterwards would leave those codecs holding the identity of the
74
+ // throwaway `initAuthCreds()` rather than the device's, so a legacy session
75
+ // written through this store would be imported under the wrong local
76
+ // identity.
77
+ await hydrateFromStore(store, creds);
33
78
  const keys = projectNativeStore(store, creds);
34
79
  return {
35
80
  state: { creds, keys, store },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oxidezap/baileyrs",
3
3
  "type": "module",
4
- "version": "0.1.2",
4
+ "version": "0.2.0",
5
5
  "description": "A Rust-powered WhatsApp Web library for JavaScript, with a Baileys-compatible API",
6
6
  "keywords": [
7
7
  "whatsapp",
@@ -47,6 +47,7 @@
47
47
  "lib/**/*",
48
48
  "!lib/**/*.map",
49
49
  "!lib/**/__tests__/**",
50
+ "!lib/__fuzz__/**",
50
51
  "!lib/**/*.test.*",
51
52
  "!lib/**/*.test-e2e.*"
52
53
  ],
@@ -58,6 +59,7 @@
58
59
  "compat:audit:missing": "npm run build --silent && node scripts/compatibility/audit.ts --only-missing",
59
60
  "compat:audit:proto": "node scripts/compatibility/proto-runtime-audit.ts --details --strict",
60
61
  "compat:audit:wire": "node scripts/compatibility/wire-fidelity-audit.ts --details --strict",
62
+ "compat:audit:lifecycle": "node scripts/compatibility/lifecycle-contract-audit.ts --strict",
61
63
  "compat:audit:strict": "npm run build --silent && node scripts/compatibility/audit.ts --strict --only-missing",
62
64
  "compat:sync-waproto": "node scripts/compatibility/waproto-facade.ts --sync",
63
65
  "compat:check-waproto": "node scripts/compatibility/waproto-facade.ts --check",
@@ -70,13 +72,17 @@
70
72
  "prepack": "npm run build && node scripts/check-pack.ts",
71
73
  "prepare": "npm run build",
72
74
  "test": "node --test",
75
+ "fuzz": "node --test --test-timeout=600000 ./src/__fuzz__/**/*.test.ts",
76
+ "fuzz:deep": "FUZZ_MODE=deep node --expose-gc --test --test-timeout=1800000 ./src/__fuzz__/**/*.test.ts",
77
+ "fuzz:record": "FUZZ_RECORD=1 node --test --test-timeout=600000 ./src/__fuzz__/**/*.test.ts",
78
+ "fuzz:report": "node scripts/fuzz/report.ts",
73
79
  "test:compat-auditor": "node --test scripts/compatibility/__tests__/audit.test.ts",
74
80
  "typecheck:compat-auditor": "npm run build --silent && npm run compat:check-waproto --silent && npm run compat:layers --silent && tsc -p scripts/compatibility/tsconfig.json",
75
- "test:e2e": "NODE_TLS_REJECT_UNAUTHORIZED=0 ADV_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= node --expose-gc --test --test-concurrency=1 ./src/__tests__/e2e/*.test-e2e.ts"
81
+ "test:e2e": "NODE_TLS_REJECT_UNAUTHORIZED=0 ADV_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= node --expose-gc --test --test-concurrency=1 ./src/__tests__/e2e/*.test-e2e.ts ./scripts/compatibility/__tests__/*.test-e2e.ts"
76
82
  },
77
83
  "dependencies": {
78
84
  "@hapi/boom": "^9.1.4",
79
- "@oxidezap/whatsapp-rust-bridge": "0.7.0",
85
+ "@oxidezap/whatsapp-rust-bridge": "0.11.0",
80
86
  "long": "^5.3.2",
81
87
  "pino": "^10.3.1",
82
88
  "protobufjs": "^7.6.5"