@oxidezap/baileyrs 0.1.3 → 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.
@@ -0,0 +1,28 @@
1
+ import type { BinaryNode } from '../Types/index.js';
2
+ /**
3
+ * The tag the engine refused, or `undefined` when the error is something else.
4
+ *
5
+ * Read off the message rather than off a field: the rejection names the tag
6
+ * inside its text (`extra stanza child <biz> conflicts with …`), and matching
7
+ * the sentence keeps this from firing on an unrelated invalid-argument — a
8
+ * malformed JID, an out-of-range enum — that a blanket retry would silently
9
+ * send twice.
10
+ */
11
+ export declare const derivedNodeConflictTag: (error: unknown) => string | undefined;
12
+ /** The caller's nodes without the one the engine derives, or `undefined` if none matched. */
13
+ export declare const withoutDerivedNode: (nodes: readonly BinaryNode[], tag: string) => BinaryNode[] | undefined;
14
+ /**
15
+ * Send, dropping whichever derived node the engine names, until it stops
16
+ * naming one.
17
+ *
18
+ * A loop rather than a single retry: a DM carrying an interactive payload
19
+ * derives both `<biz>` and `<bot>`, and the engine reports one conflicting tag
20
+ * per attempt — so a caller supplying both would have the first dropped and
21
+ * fail on the second. Each pass removes at least one node, and the caller's own
22
+ * list bounds the passes, so this cannot spin.
23
+ *
24
+ * Every attempt is refused before the engine encrypts or sends, which is what
25
+ * makes retrying safe here and nowhere else.
26
+ */
27
+ export declare const sendDroppingDerivedNodes: <T>(nodes: readonly BinaryNode[], send: (nodes: BinaryNode[]) => Promise<T>, onDrop: (tag: string) => void) => Promise<T>;
28
+ //# sourceMappingURL=derived-stanza-nodes.d.ts.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Upstream Baileys derives no stanza children of its own, so a caller that
3
+ * wants the `<biz>` on an interactive message has to attach it — which is what
4
+ * every payment recipe in that ecosystem tells them to do. The engine here
5
+ * derives it from the message content instead, and from
6
+ * `whatsapp-rust-bridge` 0.11.0 it refuses a caller node whose tag it already
7
+ * derives rather than sending both. Two of them on one `<message>` is what the
8
+ * client silently declines to render.
9
+ *
10
+ * That refusal is the correct behaviour and this is not an attempt to undo it.
11
+ * It only keeps the promise this library makes: Baileys code runs unchanged.
12
+ * A caller node the engine did not derive is left alone, because on a message
13
+ * that derives nothing the caller's node is the only one there is.
14
+ */
15
+ /** The wire text the engine uses. Matched rather than parsed: it carries the tag. */
16
+ const DERIVED_CONFLICT = 'conflicts with the one this send derives';
17
+ /**
18
+ * The tag the engine refused, or `undefined` when the error is something else.
19
+ *
20
+ * Read off the message rather than off a field: the rejection names the tag
21
+ * inside its text (`extra stanza child <biz> conflicts with …`), and matching
22
+ * the sentence keeps this from firing on an unrelated invalid-argument — a
23
+ * malformed JID, an out-of-range enum — that a blanket retry would silently
24
+ * send twice.
25
+ */
26
+ export const derivedNodeConflictTag = (error) => {
27
+ if (typeof error !== 'object' || error === null)
28
+ return undefined;
29
+ const { kind, reason, message } = error;
30
+ if (kind !== 'invalid-argument')
31
+ return undefined;
32
+ const text = typeof reason === 'string' ? reason : typeof message === 'string' ? message : '';
33
+ if (!text.includes(DERIVED_CONFLICT))
34
+ return undefined;
35
+ return /<([a-z-]+)>/u.exec(text)?.[1];
36
+ };
37
+ /** The caller's nodes without the one the engine derives, or `undefined` if none matched. */
38
+ export const withoutDerivedNode = (nodes, tag) => {
39
+ const kept = nodes.filter(node => node.tag !== tag);
40
+ return kept.length === nodes.length ? undefined : kept;
41
+ };
42
+ /**
43
+ * Send, dropping whichever derived node the engine names, until it stops
44
+ * naming one.
45
+ *
46
+ * A loop rather than a single retry: a DM carrying an interactive payload
47
+ * derives both `<biz>` and `<bot>`, and the engine reports one conflicting tag
48
+ * per attempt — so a caller supplying both would have the first dropped and
49
+ * fail on the second. Each pass removes at least one node, and the caller's own
50
+ * list bounds the passes, so this cannot spin.
51
+ *
52
+ * Every attempt is refused before the engine encrypts or sends, which is what
53
+ * makes retrying safe here and nowhere else.
54
+ */
55
+ export const sendDroppingDerivedNodes = async (nodes, send, onDrop) => {
56
+ let current = [...nodes];
57
+ for (let passes = nodes.length;; passes--) {
58
+ try {
59
+ return await send(current);
60
+ }
61
+ catch (error) {
62
+ const tag = passes > 0 ? derivedNodeConflictTag(error) : undefined;
63
+ const kept = tag === undefined ? undefined : withoutDerivedNode(current, tag);
64
+ if (tag === undefined || !kept)
65
+ throw error;
66
+ onDrop(tag);
67
+ current = kept;
68
+ }
69
+ }
70
+ };
71
+ //# sourceMappingURL=derived-stanza-nodes.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `encodeProto`, with the two inputs the bridge codec stopped accepting put back.
3
+ *
4
+ * From 0.8.0 the codec refuses an empty string where the schema declares a
5
+ * 64-bit integer, and an unpaired surrogate in a text field. Both were written
6
+ * before — as `0` and as U+FFFD — and upstream Baileys still encodes both, so a
7
+ * message that used to reach the server would now throw in the caller's face.
8
+ * This is where that is absorbed, so the strict contract stays true of the
9
+ * bridge and the tolerant one stays true of this library.
10
+ *
11
+ * Repair on failure rather than check on write: the ordinary encode is exactly
12
+ * the call it was before, with no scan of any field, and the repair runs only
13
+ * for a message that was already going to throw. `encodeProto` returns finished
14
+ * bytes rather than a lazy writer, so one try/catch covers it.
15
+ */
16
+ export declare const encodeProtoCompat: (path: string, message: unknown) => Uint8Array;
17
+ //# sourceMappingURL=encode-proto.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { encodeProto } from '@oxidezap/whatsapp-rust-bridge';
2
+ import { repairProtoMessage } from './proto-runtime.js';
3
+ /**
4
+ * `encodeProto`, with the two inputs the bridge codec stopped accepting put back.
5
+ *
6
+ * From 0.8.0 the codec refuses an empty string where the schema declares a
7
+ * 64-bit integer, and an unpaired surrogate in a text field. Both were written
8
+ * before — as `0` and as U+FFFD — and upstream Baileys still encodes both, so a
9
+ * message that used to reach the server would now throw in the caller's face.
10
+ * This is where that is absorbed, so the strict contract stays true of the
11
+ * bridge and the tolerant one stays true of this library.
12
+ *
13
+ * Repair on failure rather than check on write: the ordinary encode is exactly
14
+ * the call it was before, with no scan of any field, and the repair runs only
15
+ * for a message that was already going to throw. `encodeProto` returns finished
16
+ * bytes rather than a lazy writer, so one try/catch covers it.
17
+ */
18
+ export const encodeProtoCompat = (path, message) => {
19
+ try {
20
+ return encodeProto(path, message);
21
+ }
22
+ catch (error) {
23
+ const repaired = repairProtoMessage(path, message);
24
+ // Reference equality: nothing was coerced, so the failure is something this
25
+ // does not explain — an unmodelled type, a number no int64 can hold — and
26
+ // it has to keep propagating rather than be retried into a second throw.
27
+ if (repaired === message)
28
+ throw error;
29
+ return encodeProto(path, repaired);
30
+ }
31
+ };
32
+ //# sourceMappingURL=encode-proto.js.map
@@ -1,4 +1,14 @@
1
1
  type DynamicObject = Record<PropertyKey, unknown>;
2
+ /**
3
+ * Repairs a message for a codec addressed by type name rather than schema index.
4
+ *
5
+ * The send path calls the neutral `encodeProto` directly instead of going
6
+ * through this facade's constructors, so it cannot reach the repair the way
7
+ * `proto.Message.encode` does. Same coercion, same copy-on-write contract:
8
+ * reference equality still means "nothing to fix", so a caller can tell a
9
+ * repair from a failure it does not understand.
10
+ */
11
+ export declare const repairProtoMessage: (path: string, message: unknown) => unknown;
2
12
  export interface ProtoCompatibilityFacade {
3
13
  proto: DynamicObject;
4
14
  unsupportedCodecs: readonly string[];
@@ -46,33 +46,71 @@ const appendBytes = (writer, bytes) => {
46
46
  }
47
47
  return appendable;
48
48
  };
49
+ /**
50
+ * A UTF-16 code unit with no partner. There is no UTF-8 form for one, so the
51
+ * codec refuses it rather than letting `TextEncoder` substitute silently.
52
+ */
53
+ const UNPAIRED_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/gu;
54
+ /**
55
+ * Puts back what the codec used to do with an input it now refuses.
56
+ *
57
+ * Two cases, both measured against upstream Baileys, which still encodes both:
58
+ *
59
+ * - An empty string where the schema declares a 64-bit integer. Every one of the
60
+ * 134 send-path failures this addresses carried exactly `''`; a string that is
61
+ * merely not a number — `'abc'`, `'1.5'` — is left to throw, because that was
62
+ * never accepted and silently writing a value nobody sent is the worse answer.
63
+ * - An unpaired surrogate in a text field, replaced with U+FFFD. That is the
64
+ * substitution `TextEncoder` used to make, so the bytes are unchanged from what
65
+ * this library sent before.
66
+ *
67
+ * Returns `item` itself when it has nothing to do, so the caller can tell a
68
+ * repair from a failure it does not understand.
69
+ */
70
+ const repairScalar = (kind, item) => {
71
+ if (typeof item !== 'string')
72
+ return item;
73
+ if (kind === PROTO_FIELD_KIND.signed64 || kind === PROTO_FIELD_KIND.unsigned64) {
74
+ return item === '' ? 0 : item;
75
+ }
76
+ if (kind !== PROTO_FIELD_KIND.string)
77
+ return item;
78
+ const replaced = item.replace(UNPAIRED_SURROGATE, '\uFFFD');
79
+ return replaced === item ? item : replaced;
80
+ };
49
81
  const longFromWords = (low, high, unsigned) => LongRuntime.fromBits(low, high, unsigned);
50
82
  /**
51
- * The neutral codec normally returns safe JS numbers. The compatibility
52
- * facade supplies this reader so the same generated decoder materializes
53
- * protobuf 64-bit words directly as Long values, without a second decode or
54
- * an intermediate string/BigInt allocation.
83
+ * The neutral codec returns a JS number while a 64-bit value is exact as a
84
+ * double and a plain `{ low, high, unsigned }` past that. The compatibility
85
+ * facade supplies this reader so the same generated decoder materializes every
86
+ * 64-bit word as a long.js Long instead — uniformly, whatever the magnitude —
87
+ * without a second decode or an intermediate string/BigInt allocation.
88
+ *
89
+ * Uniformity is the point: upstream's types declare `Long` for these fields,
90
+ * so a consumer calling `.toNumber()` must not have that work only for values
91
+ * under 2^53. The neutral shape is structurally a Long minus its methods, and
92
+ * the methods are exactly what upstream code calls.
55
93
  */
56
94
  class LongBinaryReader extends BinaryReader {
57
- uint64Number() {
95
+ uint64Value() {
58
96
  const [low, high] = this.varint64();
59
97
  return longFromWords(low, high, true);
60
98
  }
61
- int64Number() {
99
+ int64Value() {
62
100
  const [low, high] = this.varint64();
63
101
  return longFromWords(low, high, false);
64
102
  }
65
- sint64Number() {
103
+ sint64Value() {
66
104
  let [low, high] = this.varint64();
67
105
  const sign = -(low & 1);
68
106
  low = ((low >>> 1) | ((high & 1) << (WORD_BITS - 1))) ^ sign;
69
107
  high = (high >>> 1) ^ sign;
70
108
  return longFromWords(low, high, false);
71
109
  }
72
- fixed64Number() {
110
+ fixed64Value() {
73
111
  return longFromWords(this.sfixed32(), this.sfixed32(), true);
74
112
  }
75
- sfixed64Number() {
113
+ sfixed64Value() {
76
114
  return longFromWords(this.sfixed32(), this.sfixed32(), false);
77
115
  }
78
116
  }
@@ -194,6 +232,99 @@ const defineLazyValue = (target, key, build) => {
194
232
  }
195
233
  });
196
234
  };
235
+ /**
236
+ * Type path to schema index, built on first use.
237
+ *
238
+ * Only the repair path needs it, and that path is only reached after an encode
239
+ * has already failed — so an importer that never sends a refused value never
240
+ * pays for the 498 entries.
241
+ */
242
+ let schemaIdsByPath;
243
+ const schemaIdFor = (path) => {
244
+ schemaIdsByPath ?? (schemaIdsByPath = new Map(PROTO_MESSAGE_SCHEMAS.map(([name], index) => [name, index])));
245
+ return schemaIdsByPath.get(path);
246
+ };
247
+ /**
248
+ * Coerces the two inputs the bridge codec stopped accepting back to what it
249
+ * used to write, and returns `value` itself when there was nothing to coerce.
250
+ *
251
+ * Reference equality is the signal: the caller only reaches here after an
252
+ * encode threw, and an unchanged result means the failure was something else
253
+ * — a genuinely invalid number, a missing codec — which must keep propagating.
254
+ *
255
+ * Copy-on-write throughout, like `projectForEncode`: a branch with nothing to
256
+ * fix is shared, not rebuilt.
257
+ */
258
+ const repairMessage = (schemaId, value, ancestors) => {
259
+ if (!isObject(value))
260
+ return value;
261
+ const fields = PROTO_MESSAGE_SCHEMAS[schemaId]?.[1];
262
+ if (!fields)
263
+ return value;
264
+ // A message that contains itself, not one that is merely deep. A fixed depth
265
+ // cap was the earlier guard and it silently stopped repairing below it: 12
266
+ // nested `ephemeralMessage.message` wrappers is 24 levels, and an empty-string
267
+ // int64 under that many threw instead of being coerced. Recursive protobuf
268
+ // messages have no depth limit, so only an actual cycle can be refused.
269
+ const seen = ancestors ?? new Set();
270
+ if (seen.has(value))
271
+ return value;
272
+ seen.add(value);
273
+ let output;
274
+ for (const field of fields) {
275
+ if (!hasOwn(value, field[0]))
276
+ continue;
277
+ const current = value[field[0]];
278
+ if (current === null || current === undefined)
279
+ continue;
280
+ const repair = (item) => field[1] === PROTO_FIELD_KIND.message ? repairMessage(field[2], item, seen) : repairScalar(field[1], item);
281
+ let converted = current;
282
+ if (field[3] & PROTO_FIELD_FLAG.repeated) {
283
+ if (Array.isArray(current)) {
284
+ let items;
285
+ for (let index = 0; index < current.length; index++) {
286
+ const item = repair(current[index]);
287
+ if (item !== current[index])
288
+ (items ?? (items = current.slice()))[index] = item;
289
+ }
290
+ converted = items ?? current;
291
+ }
292
+ }
293
+ else if (field[3] & PROTO_FIELD_FLAG.map) {
294
+ if (isObject(current)) {
295
+ let entries;
296
+ for (const key in current) {
297
+ const item = repair(current[key]);
298
+ if (item !== current[key])
299
+ (entries ?? (entries = { ...current }))[key] = item;
300
+ }
301
+ converted = entries ?? current;
302
+ }
303
+ }
304
+ else {
305
+ converted = repair(current);
306
+ }
307
+ if (converted !== current)
308
+ (output ?? (output = { ...value }))[field[0]] = converted;
309
+ }
310
+ // The ancestor path, not everything ever visited: the same object reached
311
+ // twice in different branches is legitimate and must still be repaired.
312
+ seen.delete(value);
313
+ return output ?? value;
314
+ };
315
+ /**
316
+ * Repairs a message for a codec addressed by type name rather than schema index.
317
+ *
318
+ * The send path calls the neutral `encodeProto` directly instead of going
319
+ * through this facade's constructors, so it cannot reach the repair the way
320
+ * `proto.Message.encode` does. Same coercion, same copy-on-write contract:
321
+ * reference equality still means "nothing to fix", so a caller can tell a
322
+ * repair from a failure it does not understand.
323
+ */
324
+ export const repairProtoMessage = (path, message) => {
325
+ const schemaId = schemaIdFor(path);
326
+ return schemaId === undefined ? message : repairMessage(schemaId, message);
327
+ };
197
328
  class ProtoCompatibilityRuntime {
198
329
  constructor(sourceNamespace) {
199
330
  this.namespace = { ...sourceNamespace };
@@ -315,8 +446,42 @@ class ProtoCompatibilityRuntime {
315
446
  constructor.encode = (message, writer) => {
316
447
  if (!sourceCodec)
317
448
  throw new Error(`protobuf codec unavailable for ${path}`);
318
- const encoded = sourceCodec.encode(this.projectForEncode(schemaId, message));
319
- return writer === undefined ? encoded : appendBytes(writer, encoded.finish());
449
+ const projected = this.projectForEncode(schemaId, message);
450
+ const encoded = sourceCodec.encode(projected);
451
+ // The bridge refuses two inputs it used to accept silently, and upstream
452
+ // Baileys still encodes both. Repairing on failure rather than checking
453
+ // every field on the way in is what keeps the ordinary encode free: a
454
+ // message the codec accepts never reaches the repair, and one that does
455
+ // not was already going to throw.
456
+ //
457
+ // The retry hangs off `finish` because the bridge's writer is lazy —
458
+ // `encode` queues the fields and `finish` is what writes them, so a
459
+ // refused value surfaces there. Re-encoding from the repaired message is
460
+ // safe for the same reason it would not be inside an overridden
461
+ // `string()`: that would have to resume after a tag and a length were
462
+ // already emitted, where this starts from a fresh writer.
463
+ const write = encoded.finish.bind(encoded);
464
+ const finish = () => {
465
+ try {
466
+ return write();
467
+ }
468
+ catch (error) {
469
+ const repaired = repairMessage(schemaId, projected);
470
+ if (repaired === projected)
471
+ throw error;
472
+ return sourceCodec.encode(repaired).finish();
473
+ }
474
+ };
475
+ if (writer !== undefined)
476
+ return appendBytes(writer, finish());
477
+ // The codec's own writer is what comes back, with `finish` shadowed on
478
+ // the instance rather than replaced by a bare `{ finish }`. The published
479
+ // declaration types this return as a protobufjs `Writer`, and a caller
480
+ // that chains anything on it — `fork`, `join`, another field — has to
481
+ // find the rest of the surface still there. The writer is freshly made
482
+ // by this call, so shadowing one method on it touches nothing else.
483
+ encoded.finish = finish;
484
+ return encoded;
320
485
  };
321
486
  constructor.decode = (input, length) => {
322
487
  if (!sourceCodec)
@@ -263,7 +263,7 @@ const DISPATCHERS = {
263
263
  // belongs on the bus: the QR the user was shown is spent, and `connecting`
264
264
  // clears it while telling the consumer a fresh one is coming.
265
265
  pairError: (evt, { ctx }) => {
266
- ctx.logger.error({ err: evt.error }, 'pairing failed; the engine will retry');
266
+ ctx.logger.error({ err: evt.error, rejection: evt.rejection, backoff: evt.backoff }, 'pairing failed; the engine will retry');
267
267
  emitRetrying(ctx);
268
268
  },
269
269
  loggedOut: (evt, dispatchCtx) => emitClose(dispatchCtx, evt.reason ? `Logged out: ${evt.reason}` : 'Logged out', DisconnectReason.loggedOut),
@@ -592,13 +592,42 @@ const DISPATCHERS = {
592
592
  deleted: evt.deleted,
593
593
  predefinedId: evt.predefinedId
594
594
  }),
595
- labelAssociation: (evt, { ctx }) =>
596
- // Inbound sync only ever carries chat associations (the bridge event
597
- // has a `chat_jid`); message-label associations are a separate path.
598
- ctx.ev.emit('labels.association', {
599
- association: { type: LabelAssociationType.Chat, chatId: evt.chatJid, labelId: evt.labelId },
595
+ labelAssociation: (evt, { ctx }) => ctx.ev.emit('labels.association', {
596
+ // The two halves arrive on separate bridge events and upstream tells
597
+ // them apart by the association's own type, so a message id is what
598
+ // decides which of the two shapes this is.
599
+ association: evt.messageId
600
+ ? {
601
+ type: LabelAssociationType.Message,
602
+ chatId: evt.chatJid,
603
+ messageId: evt.messageId,
604
+ labelId: evt.labelId
605
+ }
606
+ : { type: LabelAssociationType.Chat, chatId: evt.chatJid, labelId: evt.labelId },
600
607
  type: evt.labeled ? 'add' : 'remove'
601
608
  }),
609
+ appStateSyncFailed: (evt, { ctx }) => {
610
+ // Logged as well as published: a fatal collection is an operator's
611
+ // problem before it is a handler's, and `critical_block` carries the
612
+ // push name, so presence stays unavailable until it syncs.
613
+ if (evt.fatal.length) {
614
+ ctx.logger.warn({ fatal: evt.fatal, connected: evt.connected }, 'app state collections refused by the server');
615
+ }
616
+ ctx.ev.emit('app-state-sync.failed', {
617
+ fatal: evt.fatal,
618
+ retryable: evt.retryable,
619
+ skipped: evt.skipped,
620
+ connected: evt.connected
621
+ });
622
+ },
623
+ // Upstream ends the socket with `timedOut` when its own QR timer gives up
624
+ // (`Socket/socket.ts`), so the canonical reconnect handler already knows
625
+ // this state. Reporting anything else would make it learn a second one.
626
+ qrCodesExhausted: (_, dispatchCtx) => emitClose(dispatchCtx, 'QR refs attempts ended', DisconnectReason.timedOut),
627
+ // Straight onto upstream's own channel for this: `settings.update` already
628
+ // declares the `disableLinkPreviews` arm, and a consumer that reads it is
629
+ // reading the account-wide setting whichever device changed it.
630
+ settingUpdate: (evt, { ctx }) => ctx.ev.emit('settings.update', { setting: evt.setting, value: evt.value }),
602
631
  // ── Calls ──
603
632
  incomingCall: (evt, { ctx, callbacks }) => {
604
633
  callbacks?.onIncomingCall?.(evt);
@@ -2,7 +2,7 @@ import { bridgeInviteLinkToCode, bridgeMembershipRequestsToBaileys, bridgeMember
2
2
  import { emitMessageUpsert } from '../Compatibility/message-upsert.js';
3
3
  import { PARTICIPANT_ACTIONS, WAMessageStubType } from '../Types/index.js';
4
4
  import { assertArgumentDomain } from '../Utils/argument-domain.js';
5
- import { generateMessageIDV2, unixTimestampSeconds } from '../Utils/generics.js';
5
+ import { generateMessageIDV2, toNumber, unixTimestampSeconds } from '../Utils/generics.js';
6
6
  import { proto } from '../WAProto/runtime.js';
7
7
  import { bridgeGroupMetadataToBaileys } from '../Compatibility/group-metadata.js';
8
8
  const GROUP_SETTING_ALIASES = {
@@ -15,7 +15,11 @@ export const GROUP_SETTINGS = Object.keys(GROUP_SETTING_ALIASES);
15
15
  export const GROUP_REQUEST_ACTIONS = ['approve', 'reject'];
16
16
  export const MEMBER_ADD_MODES = ['admin_add', 'all_member_add'];
17
17
  export const JOIN_APPROVAL_MODES = ['on', 'off'];
18
- const inviteExpirationNumber = (value) => typeof value === 'number' ? value : (value?.toNumber() ?? 0);
18
+ // `toNumber` rather than calling `.toNumber()`: a 64-bit field now crosses the
19
+ // bridge as a plain `{ low, high, unsigned }` once the value is too wide to be
20
+ // exact as a double, and that shape carries no methods. The helper reads both
21
+ // forms, and reconstructs the high word instead of dropping it.
22
+ const inviteExpirationNumber = (value) => toNumber(value);
19
23
  export const makeGroupMethods = (ctx) => {
20
24
  const groupMetadata = async (jid) => {
21
25
  const metadata = await (await ctx.getClient()).getGroupMetadata(jid);
@@ -1,6 +1,7 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { randomBytes } from 'node:crypto';
3
- import { createWhatsAppClient, encodeProto, initWasmEngine } from '@oxidezap/whatsapp-rust-bridge';
3
+ import { createWhatsAppClient, initWasmEngine } from '@oxidezap/whatsapp-rust-bridge';
4
+ import { encodeProtoCompat } from '../Compatibility/encode-proto.js';
4
5
  import { normalizeSocketAuthenticationState } from '../Compatibility/internal/auth-state.js';
5
6
  import { makeMutex } from '../Compatibility/internal/make-mutex.js';
6
7
  import { isNativeMemoryStore } from '../Compatibility/internal/native-memory-store.js';
@@ -806,7 +807,7 @@ const makeWASocket = (config) => {
806
807
  if (dsmMessage) {
807
808
  throw new Boom('createParticipantNodes: dsmMessage is not supported, the engine encrypts one payload for every recipient and cannot substitute a different one for your own devices', { statusCode: 501 });
808
809
  }
809
- const bytes = encodeProto('Message', message);
810
+ const bytes = encodeProtoCompat('Message', message);
810
811
  return (await ctx.getClient()).createParticipantNodesBytes(jids, bytes, extraAttrs ?? {});
811
812
  },
812
813
  signalRepository,
@@ -886,7 +887,7 @@ const makeWASocket = (config) => {
886
887
  return (await ctx.getClient()).fetchMessageHistory(count, oldestMsgKey.remoteJid || '', oldestMsgKey.id || '', oldestMsgKey.fromMe || false, typeof oldestMsgTimestamp === 'number' ? oldestMsgTimestamp : oldestMsgTimestamp.toNumber());
887
888
  },
888
889
  sendStatusMessage: async (message, recipients) => {
889
- const bytes = encodeProto('Message', message);
890
+ const bytes = encodeProtoCompat('Message', message);
890
891
  return (await ctx.getClient()).sendStatusMessageBytes(bytes, recipients);
891
892
  },
892
893
  ...makeMessageMethods(ctx),
@@ -1,4 +1,5 @@
1
- import { encodeProto } from '@oxidezap/whatsapp-rust-bridge';
1
+ import { sendDroppingDerivedNodes } from '../Compatibility/derived-stanza-nodes.js';
2
+ import { encodeProtoCompat } from '../Compatibility/encode-proto.js';
2
3
  import { planMessageRelay } from '../Compatibility/message-relay.js';
3
4
  import { receiptMessageKeys } from '../Compatibility/message-keys.js';
4
5
  import { MESSAGE_RECEIPT_TYPES, WAProto } from '../Types/index.js';
@@ -69,7 +70,7 @@ export const makeMessageMethods = (ctx) => ({
69
70
  }
70
71
  }
71
72
  let msgId;
72
- const msgBytes = encodeProto('Message', msg);
73
+ const msgBytes = encodeProtoCompat('Message', msg);
73
74
  if (jid === 'status@broadcast' && options?.statusJidList?.length) {
74
75
  msgId = await client.sendStatusMessageBytes(msgBytes, options.statusJidList);
75
76
  }
@@ -138,15 +139,20 @@ export const makeMessageMethods = (ctx) => ({
138
139
  // The message goes to the bridge as the caller built it: the core settles
139
140
  // messageSecret / reportingTokenVersion itself, reusing a caller-set secret
140
141
  // rather than replacing it, so nothing here has to be dropped.
141
- const bytes = encodeProto('Message', message);
142
+ const bytes = encodeProtoCompat('Message', message);
142
143
  if (plan.kind === 'retransmission') {
143
144
  await client.retransmitMessageBytes(jid, bytes, plan.input);
144
145
  return plan.messageId;
145
146
  }
147
+ // Both sends go through the same retry: a caller node the engine derives
148
+ // is refused the same way whether the message is bound for a chat or for
149
+ // status, and the escape hatch — a node it does not derive — survives
150
+ // either way.
151
+ const drop = (tag) => ctx.logger.debug({ jid, messageId: plan.messageId, tag }, 'dropped an additionalNodes entry the engine derives from the message');
146
152
  if (plan.kind === 'status') {
147
- return client.sendStatusMessageBytesWithOptions(bytes, plan.recipients, plan.messageId, plan.nodes, plan.refreshDevices);
153
+ return sendDroppingDerivedNodes(plan.nodes, nodes => client.sendStatusMessageBytesWithOptions(bytes, plan.recipients, plan.messageId, nodes, plan.refreshDevices), drop);
148
154
  }
149
- return client.relayMessageBytesWithOptions(jid, bytes, plan.messageId, plan.nodes, plan.refreshGroupMetadata, plan.refreshDevices);
155
+ return sendDroppingDerivedNodes(plan.nodes, nodes => client.relayMessageBytesWithOptions(jid, bytes, plan.messageId, nodes, plan.refreshGroupMetadata, plan.refreshDevices), drop);
150
156
  },
151
157
  readMessages: async (keys) => {
152
158
  const receiptKeys = receiptMessageKeys(keys);
@@ -229,7 +235,7 @@ export const makeMessageMethods = (ctx) => ({
229
235
  type: WAProto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_MESSAGE
230
236
  }
231
237
  };
232
- const bytes = encodeProto('Message', message);
238
+ const bytes = encodeProtoCompat('Message', message);
233
239
  return (await ctx.getClient()).relayMessageBytes(messageKey.remoteJid, bytes, null);
234
240
  }
235
241
  });
@@ -1,3 +1,4 @@
1
+ import type Long from 'long';
1
2
  import type { Buffer } from 'node:buffer';
2
3
  import type { JsStoreCallbacks } from '@oxidezap/whatsapp-rust-bridge';
3
4
  import type { proto } from '../WAProto/runtime.js';
@@ -43,7 +44,9 @@ export type AccountSettings = {
43
44
  /** unarchive chats when a new message is received */
44
45
  unarchiveChats: boolean;
45
46
  /** the default mode to start new conversations with */
46
- defaultDisappearingMode?: Pick<proto.IConversation, 'ephemeralExpiration' | 'ephemeralSettingTimestamp'>;
47
+ defaultDisappearingMode?: Pick<proto.IConversation, 'ephemeralExpiration'> & {
48
+ ephemeralSettingTimestamp?: number | Long | null;
49
+ };
47
50
  };
48
51
  /**
49
52
  * Authentication credentials.
@@ -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;
@@ -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;