@oxidezap/baileyrs 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@oxidezap/baileyrs?color=cb3837&logo=npm)](https://www.npmjs.com/package/@oxidezap/baileyrs)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@oxidezap/baileyrs?color=cb3837&logo=npm)](https://www.npmjs.com/package/@oxidezap/baileyrs)
5
5
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/oxidezap/baileyrs)
6
+ [![pkg.pr.new](https://pkg.pr.new/badge/oxidezap/baileyrs)](https://pkg.pr.new/~/oxidezap/baileyrs)
6
7
 
7
8
  A Rust-powered WhatsApp Web library for JavaScript, with a Baileys-compatible API.
8
9
 
@@ -87,6 +88,41 @@ That writes the alias to your `package.json` (with the latest version at install
87
88
  Every `import { makeWASocket } from '@whiskeysockets/baileys'` in your codebase
88
89
  now resolves to baileyrs.
89
90
 
91
+ ### Preview builds
92
+
93
+ Every commit on `main` and every pull request publishes an installable build
94
+ through [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new). Nothing is
95
+ published to npm — the tarball is served from a URL, so a fix can be tried
96
+ before it reaches a release:
97
+
98
+ ```sh
99
+ # the head of a pull request, following it as new commits land
100
+ npm install https://pkg.pr.new/@oxidezap/baileyrs@94
101
+
102
+ # one specific commit
103
+ npm install https://pkg.pr.new/@oxidezap/baileyrs@f59ae6f
104
+ ```
105
+
106
+ Projects using the Baileys alias point it at the same URL:
107
+
108
+ ```jsonc
109
+ {
110
+ "dependencies": {
111
+ "@whiskeysockets/baileys": "https://pkg.pr.new/@oxidezap/baileyrs@94"
112
+ }
113
+ }
114
+ ```
115
+
116
+ Every open pull request comments the URL for its own head, and
117
+ [pkg.pr.new/~/oxidezap/baileyrs](https://pkg.pr.new/~/oxidezap/baileyrs) lists
118
+ what is available.
119
+
120
+ Preview builds carry the version `0.0.0-preview-<sha>`, which no range written
121
+ for a real release can match — an install is a deliberate pin, and it stays on
122
+ that commit until you change it. They are for trying a change, not for running
123
+ one: they are unreleased code, and the URL is not a substitute for a published
124
+ version in anything that has to keep resolving.
125
+
90
126
  ## Quick Start
91
127
 
92
128
  ```ts
@@ -8,5 +8,5 @@
8
8
  */
9
9
  export { adaptBridgeEvent, adaptBridgeMessageWire, KNOWN_BRIDGE_EVENT_TYPES } from './adapt.js';
10
10
  export * from './types.js';
11
- export { asBool, asBoolOr, asJidString, asNumber, asString, bridgeJidToString, isBridgeJid, isObject, normalizeDiscriminator, toUnixSeconds } from './primitives.js';
11
+ export { asBool, asBoolOr, asJidString, asNumber, asString, asUnixSeconds, bridgeJidToString, isBridgeJid, isObject, normalizeDiscriminator, toUnixSeconds } from './primitives.js';
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -8,5 +8,5 @@
8
8
  */
9
9
  export { adaptBridgeEvent, adaptBridgeMessageWire, KNOWN_BRIDGE_EVENT_TYPES } from './adapt.js';
10
10
  export * from './types.js';
11
- export { asBool, asBoolOr, asJidString, asNumber, asString, bridgeJidToString, isBridgeJid, isObject, normalizeDiscriminator, toUnixSeconds } from './primitives.js';
11
+ export { asBool, asBoolOr, asJidString, asNumber, asString, asUnixSeconds, bridgeJidToString, isBridgeJid, isObject, normalizeDiscriminator, toUnixSeconds } from './primitives.js';
12
12
  //# sourceMappingURL=index.js.map
@@ -50,16 +50,28 @@ export declare const asJidAddressString: (x: unknown) => string | undefined;
50
50
  * strings — the bridge serializes `DateTime<Utc>` as a string unless the field
51
51
  * names one of chrono's `ts_*` modules, so being lenient about which of the two
52
52
  * arrives insulates us from drift.
53
+ *
54
+ * Absence and unparseable input stay absent (`undefined`): a missing,
55
+ * malformed, or non-finite timestamp must never become the epoch, which
56
+ * would place the event at 1970 and corrupt timeline ordering. Callers
57
+ * apply the per-field policy explicitly — required timestamps (message,
58
+ * receipt, call, group action) drop the event, optional ones (presence
59
+ * `last_seen`, server-ack time, pin/mute time) omit the field.
53
60
  */
54
- export declare const toUnixSeconds: (raw: unknown) => number;
61
+ export declare const asUnixSeconds: (raw: unknown) => number | undefined;
55
62
  /**
56
- * Same as `toUnixSeconds`, but absence stays absent.
63
+ * Validated coercion to unix seconds for external consumers that need a
64
+ * plain number (no `undefined` branch).
57
65
  *
58
- * The optional timestamps `last_seen`, a server ack's `t`, an app-state
59
- * mutation's own time mean "the server did not say" when missing, which is
60
- * not the same claim as the epoch.
66
+ * Accepts the same inputs as `asUnixSeconds` a finite number or an RFC
67
+ * 3339 stringand rejects everything else by throwing `RangeError`
68
+ * instead of fabricating the epoch: a missing, malformed, or non-finite
69
+ * timestamp must never become 1970-01-01 and corrupt timeline ordering.
70
+ * Internal adapters do not use this; they take `asUnixSeconds` with an
71
+ * explicit per-field policy (required timestamps drop the event, optional
72
+ * ones omit the field).
61
73
  */
62
- export declare const asUnixSeconds: (raw: unknown) => number | undefined;
74
+ export declare const toUnixSeconds: (raw: unknown) => number;
63
75
  /**
64
76
  * A 64-bit proto field, however the bridge chose to carry it.
65
77
  *
@@ -82,20 +82,13 @@ const parseRfc3339Seconds = (raw) => {
82
82
  * strings — the bridge serializes `DateTime<Utc>` as a string unless the field
83
83
  * names one of chrono's `ts_*` modules, so being lenient about which of the two
84
84
  * arrives insulates us from drift.
85
- */
86
- export const toUnixSeconds = (raw) => {
87
- if (typeof raw === 'number' && Number.isFinite(raw))
88
- return raw;
89
- if (typeof raw === 'string')
90
- return parseRfc3339Seconds(raw) ?? 0;
91
- return 0;
92
- };
93
- /**
94
- * Same as `toUnixSeconds`, but absence stays absent.
95
85
  *
96
- * The optional timestamps `last_seen`, a server ack's `t`, an app-state
97
- * mutation's own time mean "the server did not say" when missing, which is
98
- * not the same claim as the epoch.
86
+ * Absence and unparseable input stay absent (`undefined`): a missing,
87
+ * malformed, or non-finite timestamp must never become the epoch, which
88
+ * would place the event at 1970 and corrupt timeline ordering. Callers
89
+ * apply the per-field policy explicitly — required timestamps (message,
90
+ * receipt, call, group action) drop the event, optional ones (presence
91
+ * `last_seen`, server-ack time, pin/mute time) omit the field.
99
92
  */
100
93
  export const asUnixSeconds = (raw) => {
101
94
  if (typeof raw === 'number' && Number.isFinite(raw))
@@ -104,6 +97,26 @@ export const asUnixSeconds = (raw) => {
104
97
  return parseRfc3339Seconds(raw);
105
98
  return undefined;
106
99
  };
100
+ /**
101
+ * Validated coercion to unix seconds for external consumers that need a
102
+ * plain number (no `undefined` branch).
103
+ *
104
+ * Accepts the same inputs as `asUnixSeconds` — a finite number or an RFC
105
+ * 3339 string — and rejects everything else by throwing `RangeError`
106
+ * instead of fabricating the epoch: a missing, malformed, or non-finite
107
+ * timestamp must never become 1970-01-01 and corrupt timeline ordering.
108
+ * Internal adapters do not use this; they take `asUnixSeconds` with an
109
+ * explicit per-field policy (required timestamps drop the event, optional
110
+ * ones omit the field).
111
+ */
112
+ export const toUnixSeconds = (raw) => {
113
+ const parsed = asUnixSeconds(raw);
114
+ if (parsed === undefined) {
115
+ const preview = typeof raw === 'string' ? `: ${raw.slice(0, 80)}` : '';
116
+ throw new RangeError(`toUnixSeconds: cannot coerce ${typeof raw}${preview} to unix seconds`);
117
+ }
118
+ return parsed;
119
+ };
107
120
  /**
108
121
  * A 64-bit proto field, however the bridge chose to carry it.
109
122
  *
@@ -24,7 +24,7 @@
24
24
  */
25
25
  import { processHistoryMessage } from '../Utils/process-history-message.js';
26
26
  import { isJidGroup } from '../WABinary/jid-utils.js';
27
- import { absoluteFromDuration, asBool, asBoolOr, asStringArray, asDurationSeconds, asInt64, asJidAddressString, asJidString, asNumber, asString, asUnixSeconds, isObject, normalizeDiscriminator, toUnixSeconds } from './primitives.js';
27
+ import { absoluteFromDuration, asBool, asBoolOr, asStringArray, asDurationSeconds, asInt64, asJidAddressString, asJidString, asNumber, asString, asUnixSeconds, isObject, normalizeDiscriminator } from './primitives.js';
28
28
  /**
29
29
  * Bridge sync-action events (`pin_update`, `mute_update`, etc.) carry the
30
30
  * proto action under `data.action`, but the bridge `.d.ts` types it as
@@ -39,6 +39,17 @@ const extractAction = (data) => isObject(data?.action) ? data.action : undefined
39
39
  const resolveIsGroup = (wireValue, chatJid) => asBoolOr(wireValue, false) || isJidGroup(chatJid) === true;
40
40
  const resolveParticipantAlt = (senderAlt, isGroup) => isGroup ? senderAlt : undefined;
41
41
  const resolveRemoteJidAlt = (senderAlt, recipientAlt, isGroup, isFromMe) => (isGroup ? undefined : isFromMe ? recipientAlt : senderAlt);
42
+ /**
43
+ * Rejection diagnostic for a required timestamp that is missing or
44
+ * unparseable. Metadata only — never the payload: these objects can carry
45
+ * message bodies and JIDs.
46
+ */
47
+ const invalidTimestampDetail = (event, raw) => ({
48
+ event,
49
+ field: 'timestamp',
50
+ reason: raw === undefined || raw === null ? 'missing' : 'invalid',
51
+ receivedType: typeof raw
52
+ });
42
53
  const parseEditAttribute = (value) => {
43
54
  switch (value) {
44
55
  case '1':
@@ -141,7 +152,7 @@ const ADAPTERS = {
141
152
  error: asString(data?.error)
142
153
  };
143
154
  },
144
- undecryptable_message: data => {
155
+ undecryptable_message: (data, logger) => {
145
156
  // Bridge ships `{ info: MessageInfo, is_unavailable, unavailable_type,
146
157
  // decrypt_fail_mode }` — same MessageInfo shape as the regular
147
158
  // `message` event. Extract the chat / sender / id so the dispatcher
@@ -161,12 +172,17 @@ const ADAPTERS = {
161
172
  const isFromMe = asBoolOr(src.is_from_me, false);
162
173
  const senderAlt = asJidString(src.sender_alt);
163
174
  const recipientAlt = asJidString(src.recipient_alt);
175
+ const timestamp = asUnixSeconds(info.timestamp);
176
+ if (timestamp === undefined) {
177
+ logger?.debug(invalidTimestampDetail('undecryptable_message', info.timestamp), 'undecryptable_message adapter: missing or invalid timestamp');
178
+ return null;
179
+ }
164
180
  return {
165
181
  type: 'undecryptableMessage',
166
182
  chatJid: chat,
167
183
  senderJid: isGroup ? asJidString(src.sender) : undefined,
168
184
  id,
169
- timestamp: toUnixSeconds(info.timestamp),
185
+ timestamp,
170
186
  isFromMe,
171
187
  isGroup,
172
188
  pushName: asString(info.push_name),
@@ -386,28 +402,38 @@ const ADAPTERS = {
386
402
  call_log_sync: () => ({ type: 'noop', bridgeType: 'call_log_sync' }),
387
403
  // ── Calls ──
388
404
  incoming_call: (data, logger) => adaptIncomingCall(data, logger),
389
- missed_call: data => {
405
+ missed_call: (data, logger) => {
390
406
  const from = asJidString(data?.from);
391
407
  const callId = asString(data?.call_id);
392
408
  if (!from || !callId)
393
409
  return null;
410
+ const timestamp = asUnixSeconds(data?.timestamp);
411
+ if (timestamp === undefined) {
412
+ logger?.debug(invalidTimestampDetail('missed_call', data?.timestamp), 'missed_call adapter: missing or invalid timestamp');
413
+ return null;
414
+ }
394
415
  return {
395
416
  type: 'incomingCall',
396
417
  from,
397
- timestamp: toUnixSeconds(data?.timestamp),
418
+ timestamp,
398
419
  offline: data?.reason === 'offline',
399
420
  action: { type: 'timeout', callId }
400
421
  };
401
422
  },
402
- call_ended_elsewhere: data => {
423
+ call_ended_elsewhere: (data, logger) => {
403
424
  const from = asJidString(data?.from);
404
425
  const callId = asString(data?.call_id);
405
426
  if (!from || !callId)
406
427
  return null;
428
+ const timestamp = asUnixSeconds(data?.timestamp);
429
+ if (timestamp === undefined) {
430
+ logger?.debug(invalidTimestampDetail('call_ended_elsewhere', data?.timestamp), 'call_ended_elsewhere adapter: missing or invalid timestamp');
431
+ return null;
432
+ }
407
433
  return {
408
434
  type: 'incomingCall',
409
435
  from,
410
- timestamp: toUnixSeconds(data?.timestamp),
436
+ timestamp,
411
437
  offline: false,
412
438
  action: { type: data?.outcome === 'accepted' ? 'accept' : 'reject', callId }
413
439
  };
@@ -458,6 +484,11 @@ const ADAPTERS = {
458
484
  count: asNumber(data?.count) ?? 0
459
485
  }),
460
486
  offline_sync_preview: () => ({ type: 'noop', bridgeType: 'offline_sync_preview' }),
487
+ // Counterpart of `offline_sync_completed`, not a variant: the drain ended
488
+ // without its end marker, so the client is *not* caught up and the backlog
489
+ // is redelivered on the next connection. Emitting `receivedPendingNotifications`
490
+ // here would be a lie, so this stays a noop until the next preview/completion.
491
+ offline_sync_interrupted: () => ({ type: 'noop', bridgeType: 'offline_sync_interrupted' }),
461
492
  dirty_state: data => {
462
493
  const dirtyType = asString(data?.dirty_type);
463
494
  if (!dirtyType)
@@ -675,6 +706,15 @@ export const adaptBridgeMessageWire = (messageProto, info, logger) => {
675
706
  const isFromMe = asBoolOr(info.isFromMe, false);
676
707
  const senderAlt = asString(info.senderAlt);
677
708
  const recipientAlt = asString(info.recipientAlt);
709
+ // The packed envelope carries the timestamp as a numeric record, so the
710
+ // only invalid shapes here are a missing or non-finite value. Like the
711
+ // object route below, those drop the message rather than dating it at
712
+ // the epoch.
713
+ const timestamp = asNumber(info.timestamp);
714
+ if (timestamp === undefined) {
715
+ logger?.debug(invalidTimestampDetail('message_wire', info.timestamp), 'message wire adapter: missing or invalid timestamp');
716
+ return null;
717
+ }
678
718
  return {
679
719
  type: 'message',
680
720
  chatJid: chat,
@@ -682,7 +722,7 @@ export const adaptBridgeMessageWire = (messageProto, info, logger) => {
682
722
  isGroup,
683
723
  isFromMe,
684
724
  id,
685
- timestamp: asNumber(info.timestamp) ?? 0,
725
+ timestamp,
686
726
  pushName: asString(info.pushName),
687
727
  participantAlt: resolveParticipantAlt(senderAlt, isGroup),
688
728
  remoteJidAlt: resolveRemoteJidAlt(senderAlt, recipientAlt, isGroup, isFromMe),
@@ -707,6 +747,11 @@ const adaptMessageParts = (info, messageProto, logger) => {
707
747
  const senderJid = isGroup ? asJidString(senderRaw) : undefined;
708
748
  const senderAlt = asJidString(src.sender_alt);
709
749
  const recipientAlt = asJidString(src.recipient_alt);
750
+ const timestamp = asUnixSeconds(info.timestamp);
751
+ if (timestamp === undefined) {
752
+ logger?.debug(invalidTimestampDetail('message', info.timestamp), 'message adapter: missing or invalid timestamp');
753
+ return null;
754
+ }
710
755
  return {
711
756
  type: 'message',
712
757
  chatJid: chat,
@@ -714,7 +759,7 @@ const adaptMessageParts = (info, messageProto, logger) => {
714
759
  isGroup,
715
760
  isFromMe,
716
761
  id,
717
- timestamp: toUnixSeconds(info.timestamp),
762
+ timestamp,
718
763
  pushName: asString(info.push_name),
719
764
  participantAlt: resolveParticipantAlt(senderAlt, isGroup),
720
765
  remoteJidAlt: resolveRemoteJidAlt(senderAlt, recipientAlt, isGroup, isFromMe),
@@ -738,6 +783,16 @@ const adaptReceipt = (data, logger) => {
738
783
  return null;
739
784
  }
740
785
  const isGroup = resolveIsGroup(src.is_group, chat);
786
+ // A receipt without a trustworthy timestamp cannot be placed on any
787
+ // timeline slot (`readTimestamp` / `playedTimestamp` /
788
+ // `receiptTimestamp`), so the event is dropped rather than dated at the
789
+ // epoch. Optional timestamps elsewhere stay absent via `asUnixSeconds`.
790
+ const timestamp = asUnixSeconds(data.timestamp);
791
+ if (timestamp === undefined) {
792
+ logger?.debug(invalidTimestampDetail('receipt', data.timestamp), 'receipt adapter: missing or invalid timestamp');
793
+ return null;
794
+ }
795
+ const { receiptType, raw: receiptTypeRaw } = parseReceiptType(data.type);
741
796
  return {
742
797
  type: 'receipt',
743
798
  chatJid: chat,
@@ -745,8 +800,9 @@ const adaptReceipt = (data, logger) => {
745
800
  isGroup,
746
801
  isFromMe: asBoolOr(src.is_from_me, false),
747
802
  messageIds: ids,
748
- timestamp: toUnixSeconds(data.timestamp),
749
- receiptType: parseReceiptType(data.type)
803
+ timestamp,
804
+ receiptType,
805
+ ...(receiptTypeRaw !== undefined ? { receiptTypeRaw } : {})
750
806
  };
751
807
  };
752
808
  const adaptContactUpdate = (data) => {
@@ -767,7 +823,8 @@ const adaptContactUpdate = (data) => {
767
823
  };
768
824
  };
769
825
  /**
770
- * Bridge wire `ReceiptType` → canonical kebab-cased variant.
826
+ * Bridge wire `ReceiptType` → canonical kebab-cased variant, preserving the
827
+ * original value when it matches nothing known.
771
828
  *
772
829
  * The bridge's generated `.d.ts` advertises `ReceiptType` as
773
830
  * `{type: "delivered"} | …`, but `#[serde(from = "String")]` on the rust
@@ -775,6 +832,12 @@ const adaptContactUpdate = (data) => {
775
832
  * bare PascalCase variant name (`"Delivered"`, `"Read"`, `"PeerMsg"`).
776
833
  * Keep both spellings here so a future bridge bump that re-introduces
777
834
  * the snake_case wire form keeps working.
835
+ *
836
+ * Unrecognized wire values arrive wrapped as `{ Other: value }` (the packed
837
+ * `ReceiptWireData.type` contract) or, defensively, as a future bare
838
+ * string. Those map to `'other'` with the original value in `raw`. The
839
+ * wrapper is trusted over spelling: `{ Other: 'Read' }` is `other`, not
840
+ * `read` (see `parseReceiptType`).
778
841
  */
779
842
  const RECEIPT_TYPE_MAP = {
780
843
  Delivered: 'delivered',
@@ -805,10 +868,22 @@ const RECEIPT_TYPE_MAP = {
805
868
  server_error: 'server-error'
806
869
  };
807
870
  const parseReceiptType = (raw) => {
871
+ // The `Other` wrapper is authoritative: the packed codec round-trips
872
+ // `{ Other: value }` verbatim and never synthesizes it for a known
873
+ // variant, so even a payload whose spelling collides with a known
874
+ // variant name (e.g. `{ Other: 'Read' }`) stays category `other` with
875
+ // its exact payload preserved. Only bare strings and `{ type }` go
876
+ // through the known-variant lookup.
877
+ if (isObject(raw) && typeof raw.Other === 'string')
878
+ return { receiptType: 'other', raw: raw.Other };
808
879
  const norm = typeof raw === 'string' ? raw : isObject(raw) && typeof raw.type === 'string' ? raw.type : undefined;
809
880
  if (norm == null)
810
- return undefined;
811
- return RECEIPT_TYPE_MAP[norm] ?? 'other';
881
+ return { receiptType: undefined };
882
+ // Own-property lookup: the table is an ordinary object, so indexing an
883
+ // unknown bare name like 'constructor' or '__proto__' would otherwise
884
+ // return an inherited value instead of falling through to 'other'.
885
+ const mapped = Object.hasOwn(RECEIPT_TYPE_MAP, norm) ? RECEIPT_TYPE_MAP[norm] : undefined;
886
+ return mapped ? { receiptType: mapped } : { receiptType: 'other', raw: norm };
812
887
  };
813
888
  const adaptStarUpdate = (data) => {
814
889
  if (!isObject(data))
@@ -849,6 +924,13 @@ const adaptIncomingCall = (data, logger) => {
849
924
  logger?.debug({ data }, 'incoming_call adapter: missing action.type/call_id');
850
925
  return null;
851
926
  }
927
+ // The call offer is timeline-ordered by its timestamp downstream; an
928
+ // unparseable one drops the event rather than dating it at the epoch.
929
+ const timestamp = asUnixSeconds(data.timestamp);
930
+ if (timestamp === undefined) {
931
+ logger?.debug(invalidTimestampDetail('incoming_call', data.timestamp), 'incoming_call adapter: missing or invalid timestamp');
932
+ return null;
933
+ }
852
934
  const canonicalAction = {
853
935
  type: actionType,
854
936
  callId,
@@ -871,7 +953,7 @@ const adaptIncomingCall = (data, logger) => {
871
953
  return {
872
954
  type: 'incomingCall',
873
955
  from: asJidAddressString(data.from) ?? from,
874
- timestamp: toUnixSeconds(data.timestamp),
956
+ timestamp,
875
957
  offline: asBoolOr(data.offline, false),
876
958
  stanzaId: asString(data.stanza_id),
877
959
  notify: asString(data.notify),
@@ -1103,6 +1185,11 @@ const adaptGroupUpdate = (data, logger) => {
1103
1185
  logger?.warn({ data }, 'group_update adapter: action shape rejected');
1104
1186
  return null;
1105
1187
  }
1188
+ const timestamp = asUnixSeconds(data.timestamp);
1189
+ if (timestamp === undefined) {
1190
+ logger?.debug(invalidTimestampDetail('group_update', data.timestamp), 'group_update adapter: missing or invalid timestamp');
1191
+ return null;
1192
+ }
1106
1193
  return {
1107
1194
  type: 'groupUpdate',
1108
1195
  groupJid,
@@ -1112,7 +1199,7 @@ const adaptGroupUpdate = (data, logger) => {
1112
1199
  authorPn: asJidString(data.participant_pn),
1113
1200
  authorUsername: asString(data.participant_username),
1114
1201
  authorCountryCode: asString(data.participant_country_code),
1115
- timestamp: toUnixSeconds(data.timestamp),
1202
+ timestamp,
1116
1203
  isLidAddressingMode: asBoolOr(data.is_lid_addressing_mode, false),
1117
1204
  action
1118
1205
  };
@@ -172,6 +172,17 @@ export interface CanonicalReceipt {
172
172
  * `readTimestamp` / `playedTimestamp` slot in the emitted update.
173
173
  */
174
174
  receiptType?: 'delivered' | 'sent' | 'sender' | 'retry' | 'enc-rekey-retry' | 'read' | 'read-self' | 'played' | 'played-self' | 'inactive' | 'peer-msg' | 'history-sync' | 'server-error' | 'other';
175
+ /**
176
+ * Original wire value when `receiptType` is `'other'` — the bridge sends
177
+ * unrecognized variants as `{ Other: value }` (or a future bare string),
178
+ * and the category alone would lose which one it was. Always set for an
179
+ * `Other`-wrapped payload, even when its spelling collides with a known
180
+ * variant name: the wrapper means the producer did not recognize it.
181
+ * Absent whenever the value matched a known variant through a bare
182
+ * string or `{ type }` shape, and whenever `receiptType` itself is
183
+ * absent.
184
+ */
185
+ receiptTypeRaw?: string;
175
186
  }
176
187
  export interface CanonicalContactUpdate {
177
188
  type: 'contactUpdate';
@@ -6,8 +6,7 @@ const INSTANCE_SCHEMA = Symbol('proto compatibility schema');
6
6
  const EMPTY_ARRAY = Object.freeze([]);
7
7
  const EMPTY_OBJECT = Object.freeze({});
8
8
  const JSON_OPTIONS = Object.freeze({ longs: String, enums: String, bytes: String, json: true });
9
- const WORD_BITS = 32;
10
- const WORD_BASE = 1n << BigInt(WORD_BITS);
9
+ const WORD_BASE = 1n << 32n;
11
10
  // protobufjs resolves the CommonJS Long constructor internally. Loading that
12
11
  // same export keeps `instanceof` and prototype identity aligned without
13
12
  // loading protobufjs itself or adding a second wire runtime.
@@ -79,12 +78,25 @@ const repairScalar = (kind, item) => {
79
78
  return replaced === item ? item : replaced;
80
79
  };
81
80
  const longFromWords = (low, high, unsigned) => LongRuntime.fromBits(low, high, unsigned);
81
+ /**
82
+ * Split a decoded 64-bit value into the low/high words `longFromWords` takes.
83
+ * Shifts and masks are exact on BigInt at any magnitude, and `BigInt(string)`
84
+ * parses exactly, so the `string` leg the reader's types allow costs precision
85
+ * nothing — only the one intermediate allocation.
86
+ */
87
+ const wordsFromInt64 = (value) => {
88
+ const v = typeof value === 'bigint' ? value : BigInt(value);
89
+ return [Number(v & 0xffffffffn), Number((v >> 32n) & 0xffffffffn)];
90
+ };
82
91
  /**
83
92
  * The neutral codec returns a JS number while a 64-bit value is exact as a
84
93
  * double and a plain `{ low, high, unsigned }` past that. The compatibility
85
94
  * facade supplies this reader so the same generated decoder materializes every
86
95
  * 64-bit word as a long.js Long instead — uniformly, whatever the magnitude —
87
- * without a second decode or an intermediate string/BigInt allocation.
96
+ * through one decode. `@bufbuild/protobuf` 2.14 privatized the word-level
97
+ * varint reader the overrides used to share, so each 64-bit varint now passes
98
+ * through one BigInt intermediate; uniformity is what remains, and precision
99
+ * is unchanged.
88
100
  *
89
101
  * Uniformity is the point: upstream's types declare `Long` for these fields,
90
102
  * so a consumer calling `.toNumber()` must not have that work only for values
@@ -93,18 +105,15 @@ const longFromWords = (low, high, unsigned) => LongRuntime.fromBits(low, high, u
93
105
  */
94
106
  class LongBinaryReader extends BinaryReader {
95
107
  uint64Value() {
96
- const [low, high] = this.varint64();
108
+ const [low, high] = wordsFromInt64(this.uint64());
97
109
  return longFromWords(low, high, true);
98
110
  }
99
111
  int64Value() {
100
- const [low, high] = this.varint64();
112
+ const [low, high] = wordsFromInt64(this.int64());
101
113
  return longFromWords(low, high, false);
102
114
  }
103
115
  sint64Value() {
104
- let [low, high] = this.varint64();
105
- const sign = -(low & 1);
106
- low = ((low >>> 1) | ((high & 1) << (WORD_BITS - 1))) ^ sign;
107
- high = (high >>> 1) ^ sign;
116
+ const [low, high] = wordsFromInt64(this.sint64());
108
117
  return longFromWords(low, high, false);
109
118
  }
110
119
  fixed64Value() {
@@ -454,7 +454,13 @@ const makeWASocket = (config) => {
454
454
  }
455
455
  if (useNativeMemory)
456
456
  logger.debug('auth: using socket-local native memory backend');
457
- const created = await createWhatsAppClient(makeTransport(fullConfig), makeHttpClient(fullConfig), eventHandlers, bridgeStore, fullConfig.cache ?? null, fullConfig.version, fullConfig.wantedPreKeyCount ?? null);
457
+ const created = await createWhatsAppClient(makeTransport(fullConfig), makeHttpClient(fullConfig), eventHandlers, bridgeStore, fullConfig.cache ?? null, fullConfig.version, fullConfig.wantedPreKeyCount ?? null,
458
+ // Passed through exactly as configured, never normalized by truthiness:
459
+ // the bridge only honours a literal `true` here and rejects any
460
+ // other truthy value at construction, so a `!!`/ternary-style
461
+ // coercion could promote a malformed opt-out into an opt-in.
462
+ // Absent stays strict.
463
+ fullConfig.dangerSkipCertChainVerify);
458
464
  // `end()` can land while the client is still being built — a `sock.end()`
459
465
  // or `await using` right after `makeWASocket()` does exactly that. When
460
466
  // it has, `adopt` frees this client and tells us to stop: nothing else
@@ -1,6 +1,13 @@
1
1
  /**
2
- * Reports the terminal `connection.update { close }` — exactly once, after
3
- * teardown, and never silently not at all.
2
+ * Reports the terminal `connection.update { close }` — exactly once, and
3
+ * never silently not at all.
4
+ *
5
+ * The accepted claim publishes after its teardown settles, so a consumer that
6
+ * answers the close with a replacement socket does not overlap the old one's
7
+ * release. The watchdog is the deliberate exception: past the timeout the
8
+ * close goes out with teardown still running (and logged), because losing the
9
+ * event entirely is the worse failure. Nothing here promises that every close
10
+ * lands after every resource is released — only that at most one close lands.
4
11
  *
5
12
  * That sentence is the whole contract this branch sells, and getting it wrong
6
13
  * has two opposite failure modes, both bad:
@@ -8,9 +15,8 @@
8
15
  * - **Not reported.** The consumer's handler never runs, so it never builds a
9
16
  * replacement socket. A bot offline with nothing in its logs — the original
10
17
  * bug this branch exists to fix.
11
- * - **Reported early, or twice.** The replacement socket overlaps the old
12
- * one's auth-store flush and `free()`, or every listener sees two terminal
13
- * notifications for one socket and a handler that cleans up on close loops.
18
+ * - **Reported twice.** Every listener sees two terminal notifications for
19
+ * one socket and a handler that cleans up on close loops.
14
20
  *
15
21
  * Keeping both away used to be inline logic split across the dispatcher hook
16
22
  * and `logout()`, sharing a counter and a promise. Nine separate bugs came out
@@ -41,13 +47,26 @@ export interface TerminalCloseReporter {
41
47
  *
42
48
  * Never rejects and never leaves `publish` uncalled: teardown failures are
43
49
  * logged, a throwing listener is contained, and a teardown that hangs is
44
- * cut short by the watchdog.
50
+ * reported past by the watchdog.
51
+ *
52
+ * Idempotent per socket: the first claim wins and every later call is
53
+ * ignored entirely — neither its teardown nor its publish is invoked. A
54
+ * socket never gets a second generation — a terminal close means "build a
55
+ * new socket" — so a second terminal event (a logout racing a dispatcher
56
+ * close, a late duplicate dispatch) must not publish again.
57
+ *
58
+ * The watchdog bounds how long `published()` waits, not the teardown
59
+ * itself: it neither cancels nor settles the underlying teardown, and a
60
+ * teardown that finishes late publishes nothing further.
45
61
  */
46
62
  reportAfter: (teardown: () => Promise<void>, publish: () => void) => void;
47
63
  /**
48
64
  * Publish immediately, for a close nothing else will announce — a `logout()`
49
65
  * with no live client, or one whose `logout()` threw before the bridge
50
66
  * dispatched anything.
67
+ *
68
+ * Part of the same single claim as `reportAfter`: a no-op when a close
69
+ * was already claimed.
51
70
  */
52
71
  reportNow: (publish: () => void) => void;
53
72
  /**
@@ -63,11 +82,13 @@ export interface TerminalCloseReporter {
63
82
  */
64
83
  hasReported: () => boolean;
65
84
  /**
66
- * Settles once the most recent report has been published — the moment the
85
+ * Settles once the single report has been published — the moment the
67
86
  * consumer sees it, not the moment teardown finishes, so a waiter is not
68
87
  * left hanging when the watchdog is what released the event.
69
88
  *
70
- * Resolves immediately when nothing has been reported.
89
+ * Resolves immediately when nothing has been reported. Every waiter
90
+ * observes the same promise, so concurrent `logout()` and dispatcher
91
+ * paths cannot split across generations.
71
92
  */
72
93
  published: () => Promise<void>;
73
94
  }