@aunucore/baileys 1.0.79 → 1.0.81

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.
@@ -884,7 +884,19 @@ export const makeChatsSocket = (config) => {
884
884
  }
885
885
  // update our pushname too
886
886
  if (msg.key.fromMe && msg.pushName && authState.creds.me?.name !== msg.pushName) {
887
- ev.emit('creds.update', { me: { ...authState.creds.me, name: msg.pushName } });
887
+ // AunuBaileys (1.0.81, HB-257): 'creds.update' is
888
+ // not a bufferable event, so this emit is
889
+ // synchronous — and it sits BEFORE the Promise.all
890
+ // below, so a throwing listener used to reject
891
+ // upsertMessage before processMessage ever ran for
892
+ // this message, costing a NACK 500 on a delivered
893
+ // stanza. Log and continue with the message.
894
+ try {
895
+ ev.emit('creds.update', { me: { ...authState.creds.me, name: msg.pushName } });
896
+ }
897
+ catch (err) {
898
+ onUnexpectedError(err, 'creds.update (pushName)');
899
+ }
888
900
  }
889
901
  }
890
902
  const historyMsg = getHistoryMsg(msg.message);
@@ -917,7 +929,17 @@ export const makeChatsSocket = (config) => {
917
929
  logger.info('App state sync complete, transitioning to Online state and flushing buffer');
918
930
  ev.flush();
919
931
  const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1;
920
- ev.emit('creds.update', { accountSyncCounter });
932
+ // AunuBaileys (1.0.81, HB-257): same guard as the
933
+ // pushName emit above — a throwing 'creds.update'
934
+ // listener must not reject the app-state sync that
935
+ // just completed (and skip the rest of this
936
+ // message's processing with it).
937
+ try {
938
+ ev.emit('creds.update', { accountSyncCounter });
939
+ }
940
+ catch (err) {
941
+ onUnexpectedError(err, 'creds.update (accountSyncCounter)');
942
+ }
921
943
  }
922
944
  };
923
945
  await Promise.all([
@@ -25,6 +25,32 @@ const makeWASocket = (config) => {
25
25
  ...DEFAULT_CONNECTION_CONFIG.transactionOpts,
26
26
  ...(config?.transactionOpts ?? {})
27
27
  };
28
+ // AunuBaileys (1.0.80, HB-139 + HB-174 + addendum HB-139): the
29
+ // numeric connection config was spread raw above — an explicit
30
+ // undefined/NaN from the host (classically `Number(env.X)` of an
31
+ // unset variable) silently replaced the defaults, and every
32
+ // failure mode is silent: promiseTimeout(NaN) never settles (a
33
+ // half-open socket hangs forever), a NaN/0/negative
34
+ // keepAliveIntervalMs collapses setInterval to 1 ms (~850 ping
35
+ // IQs per second, and the dead-connection watchdog compares
36
+ // against NaN so it never fires), and a NaN/Infinity
37
+ // maxMsgRetryCount breaks both retry gates in opposite
38
+ // directions at once (retry receipts never capped, resends
39
+ // never sent). Sanitise once, here at the boundary: timeouts
40
+ // and intervals must be positive finite (else the documented
41
+ // default), the retry count must be finite (else the default),
42
+ // and qrTimeout — which has no Defaults entry (genPairQR falls
43
+ // back with `|| 60000` / `|| 20000`) — must be positive finite
44
+ // or absent (a negative value is truthy and would expire every
45
+ // QR instantly).
46
+ const positiveFiniteOr = (value, fallback) => (Number.isFinite(value) && value > 0 ? value : fallback);
47
+ newConfig.connectTimeoutMs = positiveFiniteOr(newConfig.connectTimeoutMs, DEFAULT_CONNECTION_CONFIG.connectTimeoutMs);
48
+ newConfig.defaultQueryTimeoutMs = positiveFiniteOr(newConfig.defaultQueryTimeoutMs, DEFAULT_CONNECTION_CONFIG.defaultQueryTimeoutMs);
49
+ newConfig.keepAliveIntervalMs = positiveFiniteOr(newConfig.keepAliveIntervalMs, DEFAULT_CONNECTION_CONFIG.keepAliveIntervalMs);
50
+ newConfig.maxMsgRetryCount = Number.isFinite(newConfig.maxMsgRetryCount) ? newConfig.maxMsgRetryCount : DEFAULT_CONNECTION_CONFIG.maxMsgRetryCount;
51
+ if (newConfig.qrTimeout !== undefined && !(Number.isFinite(newConfig.qrTimeout) && newConfig.qrTimeout > 0)) {
52
+ newConfig.qrTimeout = undefined;
53
+ }
28
54
  // If the user hasn't provided their own history sync function,
29
55
  // let's create a default one that respects the syncFullHistory flag.
30
56
  if (config.shouldSyncHistoryMessage === undefined) {
@@ -425,7 +425,18 @@ export const makeInteropSocket = (sock) => {
425
425
  throw new Error(`sendInteropMessage: not an interop JID: ${jid}`);
426
426
  }
427
427
  const sent = await sock.sendMessage(jid, content, options);
428
- ev.emit('interop', { type: 'message.sent', jid, id: sent?.key?.id });
428
+ // AunuBaileys (1.0.81, HB-196): this emit reports a send
429
+ // that has ALREADY succeeded — a throwing 'interop'
430
+ // listener used to reject the promise anyway, so the
431
+ // caller saw a failure and retried, duplicating a
432
+ // delivered message. Log the listener error and return
433
+ // the send result.
434
+ try {
435
+ ev.emit('interop', { type: 'message.sent', jid, id: sent?.key?.id });
436
+ }
437
+ catch (err) {
438
+ logger?.error?.({ err }, 'interop: error in message.sent listener');
439
+ }
429
440
  return sent;
430
441
  };
431
442
  return {
@@ -1727,25 +1727,36 @@ export const makeMessagesRecvSocket = (config) => {
1727
1727
  // first without a catch, so a socket that closed in that window
1728
1728
  // (sendNode throws Boom 'Connection Closed') swallowed the
1729
1729
  // status event entirely.
1730
- ev.emit('newsletter.status', {
1731
- id: from,
1732
- messageId: id,
1733
- serverId,
1734
- timestamp,
1735
- isSender,
1736
- ...(offlineIndex !== undefined ? { offlineIndex } : {}),
1737
- ...(meta ? { meta } : {}),
1738
- ...(viewsCount !== undefined ? { viewsCount } : {}),
1739
- ...(responsesCount !== undefined ? { responsesCount } : {}),
1740
- ...(reactionCounts.length ? { reactionCounts } : {}),
1741
- content
1742
- });
1743
- const ackType = type === 'reaction' ? 'reaction' : edit ? 'revoke' : type;
1744
- await sendNode({
1745
- tag: 'ack',
1746
- attrs: { id, to: from, class: 'status', type: ackType || 'text' },
1747
- content: undefined
1748
- }).catch((err) => logger.warn({ err }, 'failed to ack newsletter status'));
1730
+ // AunuBaileys (1.0.81, HB-192): the ack now sits in a
1731
+ // finally. The emit-first order is unchanged (HB-59), but
1732
+ // a throwing 'newsletter.status' listener can no longer
1733
+ // take the ack down with it — an un-acked <status> stanza
1734
+ // is redelivered (or the status lost) for the sake of a
1735
+ // host bug. The throw itself still propagates to the
1736
+ // CB:status registration's catch, as before.
1737
+ try {
1738
+ ev.emit('newsletter.status', {
1739
+ id: from,
1740
+ messageId: id,
1741
+ serverId,
1742
+ timestamp,
1743
+ isSender,
1744
+ ...(offlineIndex !== undefined ? { offlineIndex } : {}),
1745
+ ...(meta ? { meta } : {}),
1746
+ ...(viewsCount !== undefined ? { viewsCount } : {}),
1747
+ ...(responsesCount !== undefined ? { responsesCount } : {}),
1748
+ ...(reactionCounts.length ? { reactionCounts } : {}),
1749
+ content
1750
+ });
1751
+ }
1752
+ finally {
1753
+ const ackType = type === 'reaction' ? 'reaction' : edit ? 'revoke' : type;
1754
+ await sendNode({
1755
+ tag: 'ack',
1756
+ attrs: { id, to: from, class: 'status', type: ackType || 'text' },
1757
+ content: undefined
1758
+ }).catch((err) => logger.warn({ err }, 'failed to ack newsletter status'));
1759
+ }
1749
1760
  };
1750
1761
  // recv a message
1751
1762
  ws.on('CB:message', (node) => {
@@ -367,13 +367,30 @@ export const makeSocket = (config) => {
367
367
  }
368
368
  catch { }
369
369
  }
370
- ev.emit('connection.update', {
371
- connection: 'close',
372
- lastDisconnect: {
373
- error,
374
- date: new Date()
375
- }
376
- });
370
+ // AunuBaileys (1.0.81, HB-138): this emit sits BEFORE the
371
+ // socket-end handlers and ev.destroy() below — cleanup the
372
+ // lib owns (watchdog stop, offline-drain stop, scheduled-
373
+ // task cleanup). A throwing host 'connection.update'
374
+ // listener used to unwind straight out of end() — and,
375
+ // via the ws 'error'/'close' handlers that call end(),
376
+ // out of the ws emit chain into an uncaught exception —
377
+ // skipping all of it, with no recovery path: `closed` is
378
+ // already true, so a second end() is a no-op. The
379
+ // event-buffer dispatcher already logs listener errors
380
+ // (1.0.81); this guard keeps end()'s own cleanup
381
+ // unconditional regardless of the emit path.
382
+ try {
383
+ ev.emit('connection.update', {
384
+ connection: 'close',
385
+ lastDisconnect: {
386
+ error,
387
+ date: new Date()
388
+ }
389
+ });
390
+ }
391
+ catch (err) {
392
+ logger.error({ err }, 'error emitting connection close update');
393
+ }
377
394
  for (const handler of socketEndHandlers) {
378
395
  try {
379
396
  handler();
@@ -13,7 +13,15 @@
13
13
  * the receive path uses.
14
14
  */
15
15
  export const makeBoundedTTLCache = ({ ttlSeconds = 3600, maxEntries = 1000 } = {}) => {
16
- const ttlMs = Math.max(1, ttlSeconds * 1000);
16
+ // AunuBaileys (1.0.80, HB-145): NaN/Infinity config defeated
17
+ // both bounds — `size > NaN` is always false (no eviction,
18
+ // ever) and a NaN ttlMs made expiresAt NaN, which never
19
+ // compares <= now, so entries lived forever. Sanitise at the
20
+ // boundary: finite values keep their exact meaning (counts
21
+ // are floored and clamped at 0), non-finite values fall back
22
+ // to the documented defaults (3600 s / 1000 entries).
23
+ const ttlMs = Number.isFinite(ttlSeconds) ? Math.max(1, ttlSeconds * 1000) : 3600 * 1000;
24
+ const limit = Number.isFinite(maxEntries) ? Math.max(0, Math.floor(maxEntries)) : 1000;
17
25
  const map = new Map();
18
26
  return {
19
27
  get(key) {
@@ -28,11 +36,20 @@ export const makeBoundedTTLCache = ({ ttlSeconds = 3600, maxEntries = 1000 } = {
28
36
  },
29
37
  set(key, value) {
30
38
  map.set(key, { value, expiresAt: Date.now() + ttlMs });
31
- while (map.size > maxEntries) {
32
- const oldest = map.keys().next().value;
33
- if (oldest === undefined)
39
+ // AunuBaileys (1.0.80, HB-226): the old guard broke out
40
+ // when the oldest KEY was `undefined` — but a Map can
41
+ // legitimately HOLD undefined as a key (two in-tree
42
+ // callers can produce one: a call offer without a
43
+ // call-id, a placeholder resend without a message id),
44
+ // and one such entry silently disabled the ceiling
45
+ // until it expired. The iterator's `done` flag is the
46
+ // real "map is empty" signal; deleting an undefined
47
+ // key is perfectly safe.
48
+ while (map.size > limit) {
49
+ const oldest = map.keys().next();
50
+ if (oldest.done)
34
51
  break;
35
- map.delete(oldest);
52
+ map.delete(oldest.value);
36
53
  }
37
54
  },
38
55
  del(key) {
@@ -32,7 +32,25 @@ export const makeEventBuffer = (logger) => {
32
32
  // take the generic event and fire it as a baileys event
33
33
  ev.on('event', (map) => {
34
34
  for (const event in map) {
35
- ev.emit(event, map[event]);
35
+ // AunuBaileys (1.0.81, HB-227/HB-228/HB-229): a host
36
+ // listener that throws must never escape this
37
+ // dispatcher. In the host's own call stack a throw is
38
+ // the host's to catch — but most emits originate inside
39
+ // lib timers and async callbacks (the nextTick
40
+ // 'connecting' update, QR cycles, the session
41
+ // watchdog's interval, PDO resend timeouts, ws
42
+ // handlers), where no host frame exists to catch it:
43
+ // the throw became an uncaught exception and killed
44
+ // the process, or unwound past lib cleanup that still
45
+ // had to run. Log the error and keep dispatching the
46
+ // remaining events of the batch; the event itself
47
+ // still fired, with its payload unchanged.
48
+ try {
49
+ ev.emit(event, map[event]);
50
+ }
51
+ catch (err) {
52
+ logger.error({ err, event }, 'error in event listener — dispatch continues');
53
+ }
36
54
  }
37
55
  });
38
56
  function buffer() {
@@ -58,10 +76,29 @@ export const makeEventBuffer = (logger) => {
58
76
  }
59
77
  }
60
78
  const consolidatedData = consolidateEvents(data);
79
+ // AunuBaileys (1.0.81, HB-166 + HB-224): swap the fresh
80
+ // buffer in BEFORE emitting, and guard the emit. The old
81
+ // order assigned `data = newData` after the emit, so
82
+ // (a) an event a listener re-buffered during the flush
83
+ // emit landed in the OLD buffer object and was silently
84
+ // discarded with it, and (b) a throw mid-emit left the
85
+ // whole batch in `data`, to be delivered a second time by
86
+ // the next flush. With the swap first, re-buffered events
87
+ // survive to the next flush and the emitted batch is
88
+ // detached no matter what a listener does. The guard
89
+ // matters because flush() is called from timer callbacks
90
+ // (chats.js history-sync transitions) and fire-and-forget
91
+ // paths (processNodeWithBuffer), where a throw became an
92
+ // uncaught exception / unhandled rejection.
93
+ data = newData;
61
94
  if (Object.keys(consolidatedData).length) {
62
- ev.emit('event', consolidatedData);
95
+ try {
96
+ ev.emit('event', consolidatedData);
97
+ }
98
+ catch (err) {
99
+ logger.error({ err }, 'error emitting flushed events');
100
+ }
63
101
  }
64
- data = newData;
65
102
  logger.trace({ conditionalChatUpdatesLeft }, 'released buffered events');
66
103
  return true;
67
104
  }
@@ -15,7 +15,16 @@ import { jidNormalizedUser, isJidUser, isLidUser } from '../WABinary/jid-utils.j
15
15
  * fed manually via `learnFromMessage` / `addPair`.
16
16
  */
17
17
  export const makeLIDStore = (options = {}) => {
18
- const maxEntries = options.maxEntries ?? 5000;
18
+ // AunuBaileys (1.0.80, HB-190 + addendum HB-145): `?? 5000`
19
+ // let NaN through (the eviction comparison `size > NaN` is
20
+ // always false — the store grew without bound) and let
21
+ // negatives through (evictIfNeeded's while loop then spun
22
+ // forever on an empty map: `size > -1` stays true while
23
+ // delete(undefined) is a no-op — a wedged process, no error).
24
+ // Sanitise at the boundary: finite values are floored and
25
+ // clamped at 0 (0 = store nothing, which terminates),
26
+ // non-finite values fall back to the default 5000.
27
+ const maxEntries = Number.isFinite(options.maxEntries) ? Math.max(0, Math.floor(options.maxEntries)) : 5000;
19
28
  // AunuBaileys (1.0.71): optional listener fired when a genuinely
20
29
  // NEW or changed PN<->LID pair is learned (re-learning an already
21
30
  // known pair does not fire). The socket wires it to 'lid-mapping.update'.
@@ -29,7 +38,17 @@ export const makeLIDStore = (options = {}) => {
29
38
  };
30
39
  const evictIfNeeded = () => {
31
40
  while (pnToLid.size > maxEntries) {
32
- const oldestPn = pnToLid.keys().next().value;
41
+ // AunuBaileys (1.0.80, HB-190 defence in depth): with a
42
+ // sanitised maxEntries (>= 0) this loop always
43
+ // terminates, but never trust a loop whose only exit is
44
+ // a size comparison — bail out if the map is somehow
45
+ // already empty (the iterator's `done`, not the key's
46
+ // value, is the empty signal).
47
+ const oldest = pnToLid.keys().next();
48
+ if (oldest.done) {
49
+ break;
50
+ }
51
+ const oldestPn = oldest.value;
33
52
  const lid = pnToLid.get(oldestPn);
34
53
  pnToLid.delete(oldestPn);
35
54
  if (lid) {
@@ -15,6 +15,14 @@
15
15
  * `sentMessagesCacheSize: 0` in the socket config to disable.
16
16
  */
17
17
  export const makeSentMessageCache = (maxEntries = 256, resolveAlternateJid) => {
18
+ // AunuBaileys (1.0.80, HB-145): a NaN cap (host config from an
19
+ // unset env var — sentMessageCacheFor's typeof check passes NaN
20
+ // straight through) made `cache.size > NaN` false forever, so
21
+ // the cache grew without bound; Infinity did the same.
22
+ // Sanitise at the boundary: finite values are floored and
23
+ // clamped at 0 (0 keeps its documented meaning — the cache is
24
+ // disabled), non-finite values fall back to the default 256.
25
+ const limit = Number.isFinite(maxEntries) ? Math.max(0, Math.floor(maxEntries)) : 256;
18
26
  const cache = new Map();
19
27
  const keyOf = (remoteJid, id) => `${remoteJid}:${id}`;
20
28
  let altResolver = typeof resolveAlternateJid === 'function' ? resolveAlternateJid : undefined;
@@ -30,13 +38,13 @@ export const makeSentMessageCache = (maxEntries = 256, resolveAlternateJid) => {
30
38
  }
31
39
  },
32
40
  add: (remoteJid, id, message) => {
33
- if (!remoteJid || !id || !message || maxEntries <= 0) {
41
+ if (!remoteJid || !id || !message || limit <= 0) {
34
42
  return;
35
43
  }
36
44
  const key = keyOf(remoteJid, id);
37
45
  cache.delete(key);
38
46
  cache.set(key, message);
39
- while (cache.size > maxEntries) {
47
+ while (cache.size > limit) {
40
48
  cache.delete(cache.keys().next().value);
41
49
  }
42
50
  },
@@ -35,6 +35,19 @@ export const generateOrGetPreKeys = (creds, range) => {
35
35
  throw new Error('AunuBaileys: creds.nextPreKeyId / creds.firstUnuploadedPreKeyId are missing — the auth state is incomplete or corrupt; restore a full backup or pair again');
36
36
  }
37
37
  const avaliable = creds.nextPreKeyId - creds.firstUnuploadedPreKeyId;
38
+ // AunuBaileys (1.0.80, HB-140 + HB-225): finite-but-INVERTED
39
+ // counters (firstUnuploadedPreKeyId > nextPreKeyId — hand-edited
40
+ // or partially restored creds) made `remaining` exceed `range`
41
+ // by the size of the gap, so the loop below generated keys for
42
+ // the GAP, not the request: gap 50k ≈ 5 s and +48 MB, gap 200k
43
+ // ≈ 21 s and +238 MB, and a ~2^31 gap would freeze the process
44
+ // outright — synchronously, inside the keys transaction, at
45
+ // every login. Same class as HB-84 above (corrupt counters),
46
+ // same answer: refuse descriptively. With consistent counters
47
+ // `remaining` can never exceed `range` by construction.
48
+ if (avaliable < 0) {
49
+ throw new Error('AunuBaileys: creds pre-key counters are inverted (firstUnuploadedPreKeyId > nextPreKeyId) — the auth state is corrupt; restore a full backup or pair again');
50
+ }
38
51
  const remaining = range - avaliable;
39
52
  const lastPreKeyId = creds.nextPreKeyId + remaining - 1;
40
53
  const newPreKeys = {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aunucore/baileys",
3
3
  "type": "module",
4
- "version": "1.0.79",
4
+ "version": "1.0.81",
5
5
  "description": "AunuBaileys — the self-healing WhatsApp Web API for Node.js. Bad MAC and \"2000 messages into the future\" errors repair themselves, LID and phone-number identities stay in sync, anti-ban is built in, pairing codes are custom — plus 29 experimental features no other Baileys fork ships: invisible watermarks, per-person tag-all cards, side-profile text, ghost mentions and more. ESM, CJS and full TypeScript types.",
6
6
  "keywords": [
7
7
  "whatsapp",