@aunucore/baileys 1.0.81 → 1.0.82

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.
@@ -44,6 +44,14 @@ export const makeMessagesRecvSocket = (config) => {
44
44
  * upstream Baileys retries forever against the same stale session. */
45
45
  const decryptFailCounts = new Map();
46
46
  const DECRYPT_FAIL_SESSION_RESET_LIMIT = 3;
47
+ /** AunuBaileys (1.0.82, HB-153): upstream v7 skips the retry for
48
+ * status broadcasts older than this — the peer has already
49
+ * deleted the expired status, so a resend can never be honoured
50
+ * and the failure says nothing about the session. */
51
+ const STATUS_EXPIRY_SECONDS = 86400;
52
+ /** AunuBaileys (1.0.82, HB-152): upstream v7 only asks the phone
53
+ * to resend a placeholder for messages younger than this. */
54
+ const PLACEHOLDER_MAX_AGE_SECONDS = 14 * 86400;
47
55
  /** this mutex ensures that each retryRequest will wait for the previous one to finish */
48
56
  const retryMutex = makeMutex();
49
57
  // AunuBaileys: bounded by default (see bounded-cache.js) — a TTL
@@ -238,8 +246,28 @@ export const makeMessagesRecvSocket = (config) => {
238
246
  }
239
247
  if (retryCount > 1 || forceIncludeKeys) {
240
248
  const { update, preKeys } = await getNextPreKeys(authState, 1);
241
- const [keyId] = Object.keys(preKeys);
242
- const key = preKeys[+keyId];
249
+ let [keyId] = Object.keys(preKeys);
250
+ let key = preKeys[+keyId];
251
+ if (!key) {
252
+ // AunuBaileys (1.0.82, HB-154 + addendum): the
253
+ // stored head pre-key is missing entirely (store
254
+ // cleaned / partial restore) or was filtered out
255
+ // as corrupt by getNextPreKeys ({} / wrong-typed
256
+ // material, HB-83/98 filter) — the map came back
257
+ // empty and the bare deref below used to throw a
258
+ // TypeError inside this transaction: the receipt
259
+ // was never built, yet the retry counter above
260
+ // was already spent. Generate a fresh pair into
261
+ // the dead head slot instead: `update` advances
262
+ // firstUnuploadedPreKeyId past exactly this id,
263
+ // and the write joins this transaction, so the
264
+ // private half is persisted before the receipt
265
+ // carrying its public half is sent.
266
+ keyId = `${authState.creds.firstUnuploadedPreKeyId}`;
267
+ key = Curve.generateKeyPair();
268
+ await authState.keys.set({ 'pre-key': { [keyId]: key } });
269
+ logger.warn({ keyId }, 'aunu: no usable stored pre-key for the retry receipt; generated a fresh one into the head slot');
270
+ }
243
271
  const content = receipt.content;
244
272
  content.push({
245
273
  tag: 'keys',
@@ -670,7 +698,21 @@ export const makeMessagesRecvSocket = (config) => {
670
698
  msgRetryCache.set(key, newValue);
671
699
  };
672
700
  const sendMessagesAgain = async (key, ids, retryNode, sessionPreInjected = false) => {
673
- const msgs = await Promise.all(ids.map(async (id) => (await getMessage({ ...key, id })) ?? sentMessageCacheFor(config, (jid) => aunuLid?.getAltJid?.(jid)).get(key.remoteJid, id)));
701
+ // AunuBaileys (1.0.82, HB-107): per-id isolation at the FETCH
702
+ // stage, mirroring HB-61 at the relay stage below. getMessage
703
+ // is host code (a DB/file store can throw); one throwing id
704
+ // used to reject the whole Promise.all, so NO id in the batch
705
+ // was resent this round and no retry counter advanced — the
706
+ // next peer receipt walked into the same wall.
707
+ const msgs = await Promise.all(ids.map(async (id) => {
708
+ try {
709
+ return (await getMessage({ ...key, id })) ?? sentMessageCacheFor(config, (jid) => aunuLid?.getAltJid?.(jid)).get(key.remoteJid, id);
710
+ }
711
+ catch (err) {
712
+ logger.warn({ err, jid: key.remoteJid, id }, 'aunu: getMessage failed for one id in a retry batch; treating it as unavailable and continuing with the rest');
713
+ return undefined;
714
+ }
715
+ }));
674
716
  const remoteJid = key.remoteJid;
675
717
  const participant = key.participant || remoteJid;
676
718
  // if it's the primary jid sending the request
@@ -775,7 +817,18 @@ export const makeMessagesRecvSocket = (config) => {
775
817
  // correctly set who is asking for the retry
776
818
  key.participant = key.participant || attrs.from;
777
819
  const retryNode = getBinaryNodeChild(node, 'retry');
778
- if (willSendMessageAgain(ids[0], key.participant)) {
820
+ // AunuBaileys (1.0.82, HB-160): the gate used
821
+ // to check ids[0] ALONE — a capped first id
822
+ // starved the fresh ids behind it, and a
823
+ // fresh first id let capped ids behind it be
824
+ // resent past the cap without limit. Filter
825
+ // per id; the loop below only ever sees ids
826
+ // that still have budget.
827
+ const eligibleIds = ids.filter(id => willSendMessageAgain(id, key.participant));
828
+ if (eligibleIds.length < ids.length) {
829
+ logger.debug({ skipped: ids.filter(id => !eligibleIds.includes(id)), participant: key.participant }, 'aunu: some ids in the retry receipt are at the resend cap; resending only the rest');
830
+ }
831
+ if (eligibleIds.length) {
779
832
  if (key.fromMe) {
780
833
  try {
781
834
  logger.debug({ attrs, key }, 'recv retry request');
@@ -796,7 +849,7 @@ export const makeMessagesRecvSocket = (config) => {
796
849
  logger.debug({ err: injErr }, 'aunu: retry receipt bundle injection failed; falling back to server fetch');
797
850
  }
798
851
  }
799
- await sendMessagesAgain(key, ids, retryNode, sessionPreInjected);
852
+ await sendMessagesAgain(key, eligibleIds, retryNode, sessionPreInjected);
800
853
  }
801
854
  catch (error) {
802
855
  logger.error({ key, ids, trace: error.stack }, 'error in sending message again');
@@ -880,13 +933,37 @@ export const makeMessagesRecvSocket = (config) => {
880
933
  let response;
881
934
  if (getBinaryNodeChild(node, 'unavailable') && !encNode) {
882
935
  await sendMessageAck(node);
883
- const { key } = decodeMessageNode(node, authState.creds.me?.id, authState.creds.me?.lid || '').fullMessage;
936
+ // AunuBaileys (1.0.82, HB-146): this decode sat OUTSIDE
937
+ // any try (the HB-1 fix covered only the decrypt path
938
+ // below). A stanza whose `from` is not a recognised JID
939
+ // type — malformed today, a future JID domain tomorrow —
940
+ // threw 'Unknown message type' after the ack had already
941
+ // gone out: only logged by the caller, and the
942
+ // placeholder was never resent nor upserted. Classify
943
+ // and drop it here, like the main decode path.
944
+ let key;
945
+ try {
946
+ key = decodeMessageNode(node, authState.creds.me?.id, authState.creds.me?.lid || '').fullMessage.key;
947
+ }
948
+ catch (decodeErr) {
949
+ logger.error({ error: decodeErr, node }, 'aunu: error decoding unavailable message node; acked, skipping placeholder resend');
950
+ return;
951
+ }
884
952
  // AunuBaileys: bot / hosted / view_once placeholders can never
885
953
  // be resent by the phone — don't burn a resend request on
886
954
  // them (behaviour learned from @violetix/baileys).
887
955
  if (isUnrecoverableUnavailable(node)) {
888
956
  logger.debug({ msgId: key.id, unavailableType: unavailablePlaceholderType(node) }, 'unavailable placeholder cannot be resent; skipping resend request');
889
957
  }
958
+ else if (!authState.creds.me?.id) {
959
+ // AunuBaileys (1.0.82, HB-106): requestPlaceholderResend
960
+ // throws Boom 'Not authenticated' in the pre-success
961
+ // window — and because the ack above has ALREADY gone
962
+ // out, the throw (caught only by the caller's logger)
963
+ // lost the placeholder silently: no resend, no upsert.
964
+ // Skip cleanly, like sendRetryRequest does (HB-6).
965
+ logger.warn({ msgId: key.id }, 'aunu: cannot request placeholder resend before pairing completes; skipping');
966
+ }
890
967
  else {
891
968
  response = await requestPlaceholderResend(key);
892
969
  }
@@ -941,14 +1018,85 @@ export const makeMessagesRecvSocket = (config) => {
941
1018
  }
942
1019
  // message failed to decrypt
943
1020
  if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT) {
944
- if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
945
- return sendMessageAck(node, NACK_REASONS.ParsingError);
1021
+ const stubParam = msg?.messageStubParameters?.[0];
1022
+ const isMissingKeysFailure = stubParam === MISSING_KEYS_ERROR_TEXT;
1023
+ const isNoMessageFailure = stubParam === NO_MESSAGE_FOUND_ERROR_TEXT;
1024
+ // AunuBaileys (1.0.82, HB-170): an msmsg (Meta
1025
+ // AI) failure is a message-SECRET problem, not
1026
+ // an E2E session problem — @bot has no resend
1027
+ // path, so a retry receipt only burns budget
1028
+ // and the fail counter marched toward a
1029
+ // pointless @bot session reset.
1030
+ const isMsmsgFailure = encNode?.attrs?.type === 'msmsg' ||
1031
+ (typeof stubParam === 'string' && stubParam.startsWith('msmsg'));
1032
+ // AunuBaileys (1.0.82, HB-153): a status that
1033
+ // expired on the peer can never be resent.
1034
+ const isExpiredStatusFailure = !isNoMessageFailure &&
1035
+ isJidStatusBroadcast(msg.key.remoteJid) &&
1036
+ Number.isFinite(+node.attrs.t) &&
1037
+ (unixTimestampSeconds() - +node.attrs.t) > STATUS_EXPIRY_SECONDS;
1038
+ if (isMissingKeysFailure) {
1039
+ // AunuBaileys (1.0.82, HB-206): the 487
1040
+ // NACK stays, but the early return is
1041
+ // gone. 'Key used already or never filled'
1042
+ // is a SESSION-layer failure, so it now
1043
+ // flows through the counting / self-heal
1044
+ // machinery below and the stub upsert at
1045
+ // the end of this branch, like every other
1046
+ // decrypt failure — only the retry receipt
1047
+ // is skipped (a resend of the same
1048
+ // ciphertext cannot restore a message key
1049
+ // our ratchet no longer holds).
1050
+ await sendMessageAck(node, NACK_REASONS.ParsingError);
1051
+ }
1052
+ if (isNoMessageFailure) {
1053
+ // AunuBaileys (1.0.82, HB-152): the stanza
1054
+ // carried no encrypted content at all —
1055
+ // there is nothing to re-decrypt and
1056
+ // nothing wrong with the session, so no
1057
+ // retry receipt and no fail count (three
1058
+ // of these used to reset a healthy
1059
+ // session). Ask the phone for a placeholder
1060
+ // resend when eligible (v7 policy:
1061
+ // recoverable type, fresh enough) and let
1062
+ // the stub upsert below stand as the
1063
+ // trace. Fire-and-forget: the resend waits
1064
+ // 5 s and must not hold the processing
1065
+ // mutex (see the 1.0.70 note in
1066
+ // sendRetryRequest).
1067
+ if (!response && !msg.key.fromMe && !isUnrecoverableUnavailable(node) &&
1068
+ Number.isFinite(+node.attrs.t) &&
1069
+ (unixTimestampSeconds() - +node.attrs.t) <= PLACEHOLDER_MAX_AGE_SECONDS) {
1070
+ requestPlaceholderResend(msg.key).then(
1071
+ (resendId) => logger.debug({ resendId }, 'aunu: requested placeholder resend for a message with no encrypted content'),
1072
+ (err) => logger.debug({ err }, 'aunu: placeholder resend for a message with no encrypted content failed')
1073
+ );
1074
+ }
1075
+ logger.debug({ msgId: msg.key.id, author }, 'aunu: message stanza carried no encrypted content; no retry receipt, no session failure counted');
1076
+ }
1077
+ else if (isMsmsgFailure) {
1078
+ logger.debug({ msgId: msg.key.id, author, stubParam }, 'aunu: msmsg decrypt failure; no retry receipt and no session failure counted (a secret problem, not a session problem)');
946
1079
  }
947
- retryMutex.mutex(async () => {
1080
+ else if (isExpiredStatusFailure) {
1081
+ logger.debug({ msgId: msg.key.id, author, ageSeconds: unixTimestampSeconds() - +node.attrs.t }, 'aunu: decrypt failure for an expired status; no retry receipt and no session failure counted');
1082
+ }
1083
+ else retryMutex.mutex(async () => {
948
1084
  if (ws.isOpen) {
949
- if (getBinaryNodeChild(node, 'unavailable')) {
950
- return;
951
- }
1085
+ // AunuBaileys (1.0.82, HB-183): the
1086
+ // blanket early return for stanzas
1087
+ // carrying an <unavailable> child is
1088
+ // gone — it suppressed the retry
1089
+ // receipt, the fail counting, the
1090
+ // self-heal AND the decrypt-failure
1091
+ // event for every enc-bearing failure
1092
+ // that rode along with one (view-once
1093
+ // above all), leaving the content
1094
+ // unrecoverable and the rotten session
1095
+ // uncounted. Upstream v7 has no such
1096
+ // guard on this path; the placeholder
1097
+ // choice for unavailable types lives in
1098
+ // the no-enc branch and in HB-152's
1099
+ // classification above.
952
1100
  const encNode = getBinaryNodeChild(node, 'enc');
953
1101
  let forceIncludeKeys = !encNode;
954
1102
  if (author) {
@@ -1058,7 +1206,7 @@ export const makeMessagesRecvSocket = (config) => {
1058
1206
  // under that device's session; a retry
1059
1207
  // receipt would ask ourselves to resend to
1060
1208
  // ourselves.
1061
- if (!msg.key.fromMe) {
1209
+ if (!msg.key.fromMe && !isMissingKeysFailure) {
1062
1210
  await sendRetryRequest(node, forceIncludeKeys);
1063
1211
  if (retryRequestDelayMs) {
1064
1212
  await delay(retryRequestDelayMs);
@@ -1474,12 +1622,16 @@ export const makeMessagesRecvSocket = (config) => {
1474
1622
  const offlineNodeProcessor = makeOfflineNodeProcessor();
1475
1623
  const processNode = (type, node, identifier, exec) => {
1476
1624
  // AunuBaileys: ignoreKey filter — a matching stanza is dropped
1477
- // before any handling (no decryption, no persistence); messages
1625
+ // before any handling (no decryption, no persistence); stanzas
1478
1626
  // are still acked so the server stops redelivering them.
1627
+ // AunuBaileys (1.0.82, HB-210): the ack used to cover only
1628
+ // type 'message' — a dropped receipt/notification/call was
1629
+ // never acked, so the server redelivered it forever while the
1630
+ // rule stayed active (all four real handlers ack in a
1631
+ // finally; the drop bypassed every one of them, against the
1632
+ // filter's own documented intent).
1479
1633
  if (sock.aunuIgnoreFilter?.shouldDrop(type, node, [authState.creds.me?.id, authState.creds.me?.lid].filter(Boolean))) {
1480
- if (type === 'message') {
1481
- sendMessageAck(node).catch(() => { });
1482
- }
1634
+ sendMessageAck(node).catch(() => { });
1483
1635
  logger.debug({ type, from: node.attrs.from, id: node.attrs.id }, 'aunu: stanza dropped by ignoreKey');
1484
1636
  return;
1485
1637
  }
@@ -287,6 +287,17 @@ export const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
287
287
  // 'last' = complete response, 'first' =
288
288
  // streaming partial, intentionally skipped.
289
289
  if (botType !== null && !['full', 'last'].includes(botType)) {
290
+ // AunuBaileys (1.0.82, HB-171
291
+ // addendum): the skip used to leave
292
+ // no trace at all — the caller took
293
+ // the SUCCESS branch, sent a
294
+ // 'delivered' receipt and upserted
295
+ // an empty message for content that
296
+ // was never read. Leave a CIPHERTEXT
297
+ // stub, like any other undecrypted
298
+ // content.
299
+ fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT;
300
+ fullMessage.messageStubParameters = [`msmsg streaming partial skipped (edit=${botType})`];
290
301
  break;
291
302
  }
292
303
  const secretIdCandidates = [botEditTargetId, metaTargetId, fullMessage.key?.id].filter(Boolean);
@@ -315,6 +326,16 @@ export const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
315
326
  }
316
327
  if (!secretCandidates.length) {
317
328
  logger.warn({ metaTargetId, botType, secretIdCandidates }, 'msmsg: no candidate messageSecret found, skipping');
329
+ // AunuBaileys (1.0.82, HB-171): same
330
+ // false-success as the partial skip
331
+ // above — without a stub the caller
332
+ // receipts this as delivered and
333
+ // stores an empty message. The stub
334
+ // is the trace; messages-recv
335
+ // excludes the msmsg class from
336
+ // retry/fail counting (HB-170).
337
+ fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT;
338
+ fullMessage.messageStubParameters = ['msmsg: no candidate messageSecret found'];
318
339
  break;
319
340
  }
320
341
  const msMsg = proto.MessageSecretMessage.decode(content);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aunucore/baileys",
3
3
  "type": "module",
4
- "version": "1.0.81",
4
+ "version": "1.0.82",
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",