@aunucore/baileys 1.0.81 → 1.0.83

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
  }
@@ -188,7 +188,11 @@ export const makeMessagesSocket = (config) => {
188
188
  }
189
189
  const result = await sock.executeUSyncQuery(query);
190
190
  if (result) {
191
- const extracted = extractDeviceJids(result?.list, authState.creds.me?.id, ignoreZeroDevices);
191
+ // AunuBaileys (1.0.83, HB-258): pass our LID as well —
192
+ // comparing only the PN user left our own LID devices in
193
+ // the extracted list, so group sends encrypted a copy to
194
+ // our own LID device (upstream v7 made the same fix).
195
+ const extracted = extractDeviceJids(result?.list, authState.creds.me?.id, authState.creds.me?.lid, ignoreZeroDevices);
192
196
  const deviceMap = {};
193
197
  for (const item of extracted) {
194
198
  deviceMap[item.user] = deviceMap[item.user] || [];
@@ -327,6 +331,16 @@ export const makeMessagesSocket = (config) => {
327
331
  return { nodes: filteredNodes, shouldIncludeDeviceIdentity };
328
332
  };
329
333
  const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, addBizAttributes, statusJidList, statusPrivacy }) => {
334
+ // AunuBaileys (1.0.83, HB-253): a non-object message — or an
335
+ // empty object — proto-encodes to ZERO content bytes without
336
+ // throwing, so the relay used to report success while the
337
+ // peer received random padding only: the 1.0.71 silent-empty
338
+ // class at the one public entry point that never got a guard
339
+ // (sendMessage fails loudly for the same class of input).
340
+ // Fail fast, before any wait or network work.
341
+ if (!message || typeof message !== 'object' || Array.isArray(message) || !Object.keys(message).length) {
342
+ throw new Boom('relayMessage requires a non-empty message object', { statusCode: 400 });
343
+ }
330
344
  if (!ws.isOpen) {
331
345
  // AunuBaileys: a send attempted during a reconnect window used to die
332
346
  // instantly with Boom 428 from the first internal query — the message
@@ -412,6 +426,10 @@ export const makeMessagesSocket = (config) => {
412
426
  const { user, device } = decodedParticipant;
413
427
  devices.push({ user, device });
414
428
  }
429
+ // AunuBaileys (1.0.83, HB-178): the stanza built inside the
430
+ // transaction is captured here and sent only AFTER the
431
+ // transaction commits — see the tail of this function.
432
+ let stanzaToSend;
415
433
  await authState.keys.transaction(async () => {
416
434
  const mediaType = getMediaType(message);
417
435
  if (mediaType) {
@@ -420,6 +438,15 @@ export const makeMessagesSocket = (config) => {
420
438
  if (isNewsletter) {
421
439
  // Patch message if needed, then encode as plaintext
422
440
  const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message;
441
+ // AunuBaileys (1.0.83, HB-212): a per-jid patch ARRAY
442
+ // is in-contract for patchMessageBeforeSending, but
443
+ // encodeNewsletterMessage is a bare proto encode —
444
+ // an array encodes to 0 bytes, so the channel got an
445
+ // empty post while sendMessage resolved as success.
446
+ // Mirror the group branch and refuse it loudly.
447
+ if (Array.isArray(patched)) {
448
+ throw new Boom('Per-jid patching is not supported in newsletters', { statusCode: 400 });
449
+ }
423
450
  const bytes = encodeNewsletterMessage(patched);
424
451
  binaryNodeContent.push({
425
452
  tag: 'plaintext',
@@ -447,8 +474,14 @@ export const makeMessagesSocket = (config) => {
447
474
  content: binaryNodeContent
448
475
  };
449
476
  logger.debug({ msgId }, `sending newsletter message to ${jid}`);
450
- await sendNode(stanza);
451
- sentMessageCacheFor(config).add(jid, msgId, message);
477
+ // AunuBaileys (1.0.83, HB-178): captured, not sent —
478
+ // the send happens after the commit at the tail of
479
+ // relayMessage, and the sent-message cache add lives
480
+ // there too (it used to sit here, inside the
481
+ // transaction; with send-after-commit it would
482
+ // register a message the commit failure prevented
483
+ // from ever being sent).
484
+ stanzaToSend = stanza;
452
485
  return;
453
486
  }
454
487
  if (normalizeMessageContent(message)?.pinInChatMessage) {
@@ -586,10 +619,43 @@ export const makeMessagesSocket = (config) => {
586
619
  allJids.push(jid);
587
620
  }
588
621
  await assertSessions(allJids, false);
589
- const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
622
+ // AunuBaileys (1.0.83, HB-161): the echo to our own
623
+ // devices and the send to the peer used to share one
624
+ // Promise.all — when encrypting for ALL of our own
625
+ // devices failed (a rotten self-session the one-shot
626
+ // heal inside createParticipantNodes could not save),
627
+ // the peer's already-encrypted nodes were thrown away
628
+ // with it and the DM never left, even though the
629
+ // peer's sessions were healthy; symmetrically, a
630
+ // total peer failure took the echo down too. The echo
631
+ // is best-effort: settle both sides independently and
632
+ // fail only when the side that must deliver produced
633
+ // nothing.
634
+ const [meSettled, otherSettled] = await Promise.allSettled([
590
635
  createParticipantNodes(meJids, meMsg, extraAttrs),
591
636
  createParticipantNodes(otherJids, message, extraAttrs)
592
637
  ]);
638
+ if (otherSettled.status === 'rejected') {
639
+ throw otherSettled.reason;
640
+ }
641
+ let meNodes = [];
642
+ let s1 = false;
643
+ if (meSettled.status === 'fulfilled') {
644
+ meNodes = meSettled.value.nodes;
645
+ s1 = meSettled.value.shouldIncludeDeviceIdentity;
646
+ }
647
+ else if (!otherSettled.value.nodes.length && meJids.length) {
648
+ // Our own devices were the ONLY recipients (a
649
+ // retry resend aimed at our own device, a
650
+ // self-chat): with the echo gone there is nothing
651
+ // to send — keep the original failure instead of
652
+ // emitting an empty stanza.
653
+ throw meSettled.reason;
654
+ }
655
+ else {
656
+ logger.warn({ err: meSettled.reason }, 'aunu: encrypting the echo to our own devices failed; sending to the peer without the device-sent echo');
657
+ }
658
+ const { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 } = otherSettled.value;
593
659
  participants.push(...meNodes);
594
660
  participants.push(...otherNodes);
595
661
  shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2;
@@ -658,8 +724,24 @@ export const makeMessagesSocket = (config) => {
658
724
  stanza.content.push(getBizBinaryNode(message));
659
725
  }
660
726
  logger.debug({ msgId }, `sending message to ${participants.length} devices`);
661
- await sendNode(stanza);
727
+ stanzaToSend = stanza;
662
728
  });
729
+ // AunuBaileys (1.0.83, HB-178): the stanza goes on the wire
730
+ // only now — AFTER the transaction above has committed the
731
+ // ratchet state it was encrypted under. The send used to be
732
+ // the last await INSIDE the transaction, so a commit that
733
+ // failed through every retry threw after the message was
734
+ // already delivered: the caller saw an error for a delivered
735
+ // message (and a host retry then produced a duplicate the
736
+ // peer could not decrypt, re-using the un-persisted chain
737
+ // key), while the cache add and the message-secret
738
+ // registration below were skipped for that delivered
739
+ // message. Persist first, then send: a commit failure now
740
+ // fails a message that was never sent, and a sent message
741
+ // is always registered.
742
+ if (stanzaToSend) {
743
+ await sendNode(stanzaToSend);
744
+ }
663
745
  sentMessageCacheFor(config).add(jid, msgId, message);
664
746
  // AunuBaileys (1.0.71): register the messageSecret at send time
665
747
  // so an msmsg reply can be decrypted even if it arrives before
@@ -759,7 +841,19 @@ export const makeMessagesSocket = (config) => {
759
841
  }
760
842
  const callId = newCallId();
761
843
  const offerContent = buildCallOfferContent(isVideo);
762
- const devices = (await getUSyncDevices([toJid], true, false)).map(({ user, device }) => jidEncode(user, 's.whatsapp.net', device));
844
+ // AunuBaileys (1.0.83, HB-184): the callee's devices take
845
+ // the server of the DESTINATION jid — a LID callee's devices
846
+ // live on 'lid'; encoding them on 's.whatsapp.net' addressed
847
+ // a phone number that does not exist (LID digits are not a
848
+ // number), so the offer could never ring.
849
+ const callServer = jidDecode(toJid)?.server === 'lid' ? 'lid' : 's.whatsapp.net';
850
+ const devices = (await getUSyncDevices([toJid], true, false)).map(({ user, device }) => jidEncode(user, callServer, device));
851
+ // AunuBaileys (1.0.83, HB-147): an empty device list built
852
+ // an offer with ZERO <destination> nodes — a call that could
853
+ // ring nobody, with no client-side complaint. Fail fast.
854
+ if (!devices.length) {
855
+ throw new Boom(`offerCall: no devices found for ${toJid}; the call would ring nobody`, { statusCode: 400 });
856
+ }
763
857
  await assertSessions(devices, true);
764
858
  const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(devices, {
765
859
  call: { callKey: createCallKey() }
@@ -800,7 +894,25 @@ export const makeMessagesSocket = (config) => {
800
894
  }
801
895
  const callId = newCallId();
802
896
  const offerContent = buildCallOfferContent(isVideo);
803
- const devices = (await getUSyncDevices(participants, true, false)).map(({ user, device }) => jidEncode(user, 's.whatsapp.net', device));
897
+ // AunuBaileys (1.0.83, HB-184): each participant's devices
898
+ // take the server of that participant's OWN jid — in a
899
+ // LID-addressed group the participants are LID jids and
900
+ // their devices live on 'lid', not 's.whatsapp.net'.
901
+ const serverByUser = new Map();
902
+ for (const participantJid of participants) {
903
+ const decodedParticipant = jidDecode(jidNormalizedUser(participantJid) || '');
904
+ if (decodedParticipant?.user) {
905
+ serverByUser.set(decodedParticipant.user, decodedParticipant.server === 'lid' ? 'lid' : 's.whatsapp.net');
906
+ }
907
+ }
908
+ const devices = (await getUSyncDevices(participants, true, false)).map(({ user, device }) => jidEncode(user, serverByUser.get(user) ?? 's.whatsapp.net', device));
909
+ // AunuBaileys (1.0.83, HB-147): the default participants=[]
910
+ // (or a usync answer with no devices) built an offer with
911
+ // ZERO <destination> nodes — a call that could ring nobody,
912
+ // with no client-side complaint. Fail fast.
913
+ if (!devices.length) {
914
+ throw new Boom('groupCall: no participant devices to call; the call would ring nobody', { statusCode: 400 });
915
+ }
804
916
  await assertSessions(devices, true);
805
917
  const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(devices, {
806
918
  call: { callKey: createCallKey() }
@@ -869,19 +981,44 @@ export const makeMessagesSocket = (config) => {
869
981
  throw new Boom(`exclusive target is not a valid JID: ${target}`, { statusCode: 400 });
870
982
  }
871
983
  const devices = await getUSyncDevices([targetUser], relayOpts.useUserDevicesCache !== false, false);
872
- const deviceJids = devices.map(({ user, device }) => jidEncode(user, 's.whatsapp.net', device)).filter(Boolean);
984
+ // AunuBaileys (1.0.83, HB-188): the target's devices take
985
+ // the server of the TARGET jid — for a LID member they live
986
+ // on 'lid'; encoding them on 's.whatsapp.net' addressed a
987
+ // phone number that does not exist (the LID digits are not
988
+ // a number), so the exclusive message could never arrive.
989
+ const targetServer = jidDecode(targetUser)?.server === 'lid' ? 'lid' : 's.whatsapp.net';
990
+ const deviceJids = devices.map(({ user, device }) => jidEncode(user, targetServer, device)).filter(Boolean);
873
991
  // No device list means the member is unknown to usync; addressing
874
992
  // the bare jid still reaches their primary device rather than
875
993
  // failing the send outright.
876
994
  const targets = deviceJids.length ? deviceJids : [targetUser];
877
995
  logger.debug({ jid, target: targetUser, devices: targets.length }, 'sending exclusive group message');
996
+ // AunuBaileys (1.0.83, HB-108): per-device isolation — one
997
+ // device's failure (a rotten session, a rejected stanza)
998
+ // used to abort the loop, so the remaining devices never
999
+ // received the message and the error did not say the
1000
+ // delivery was partial. Deliver to every device that works
1001
+ // and fail only when NONE did; the return value now lists
1002
+ // the devices actually delivered to.
1003
+ const delivered = [];
1004
+ let firstDeviceError;
878
1005
  for (const deviceJid of targets) {
879
- await relayMessage(jid, message, {
880
- ...relayOpts,
881
- participant: { jid: deviceJid, count: 0 }
882
- });
1006
+ try {
1007
+ await relayMessage(jid, message, {
1008
+ ...relayOpts,
1009
+ participant: { jid: deviceJid, count: 0 }
1010
+ });
1011
+ delivered.push(deviceJid);
1012
+ }
1013
+ catch (err) {
1014
+ firstDeviceError = firstDeviceError ?? err;
1015
+ logger.warn({ err, jid, deviceJid }, 'aunu: exclusive send failed for one device; continuing with the remaining devices');
1016
+ }
883
1017
  }
884
- return targets;
1018
+ if (!delivered.length) {
1019
+ throw firstDeviceError ?? new Boom('exclusive send failed for every target device', { statusCode: 500 });
1020
+ }
1021
+ return delivered;
885
1022
  };
886
1023
  // AunuBaileys (1.0.71): named so the wrappers below can call the
887
1024
  // socket's own sendMessage — the `sock` closure variable is the
@@ -1094,60 +1231,99 @@ export const makeMessagesSocket = (config) => {
1094
1231
  // AunuBaileys (1.0.71): the option images follow the parent,
1095
1232
  // each pointing back at it via messageAssociation.
1096
1233
  if (imagePollOptionMedia?.length) {
1234
+ // AunuBaileys (1.0.83, HB-173): per-child
1235
+ // isolation — one failing option image used to
1236
+ // abort the loop, leaving the already-sent poll
1237
+ // parent with only some (or none) of its images
1238
+ // while the caller saw a total failure. Deliver
1239
+ // every child that works; fail only when NONE
1240
+ // did.
1241
+ let deliveredChildren = 0;
1242
+ let firstChildError;
1097
1243
  for (const media of imagePollOptionMedia) {
1098
- const optionMsg = generateWAMessageFromContent(jid, {
1099
- pollCreationOptionImageMessage: { message: media },
1100
- messageContextInfo: {
1101
- messageAssociation: {
1102
- parentMessageKey: fullMsg.key,
1103
- associationType: proto.MessageAssociation.AssociationType.MEDIA_POLL
1244
+ try {
1245
+ const optionMsg = generateWAMessageFromContent(jid, {
1246
+ pollCreationOptionImageMessage: { message: media },
1247
+ messageContextInfo: {
1248
+ messageAssociation: {
1249
+ parentMessageKey: fullMsg.key,
1250
+ associationType: proto.MessageAssociation.AssociationType.MEDIA_POLL
1251
+ }
1104
1252
  }
1105
- }
1106
- }, {
1107
- userJid,
1108
- messageId: generateMessageIDV2(sock.user?.id)
1109
- });
1110
- await relay(jid, optionMsg.message, {
1111
- messageId: optionMsg.key.id,
1112
- useCachedGroupMetadata: options.useCachedGroupMetadata,
1113
- additionalAttributes,
1114
- additionalNodes: []
1115
- });
1253
+ }, {
1254
+ userJid,
1255
+ messageId: generateMessageIDV2(sock.user?.id)
1256
+ });
1257
+ await relay(jid, optionMsg.message, {
1258
+ messageId: optionMsg.key.id,
1259
+ useCachedGroupMetadata: options.useCachedGroupMetadata,
1260
+ additionalAttributes,
1261
+ additionalNodes: []
1262
+ });
1263
+ deliveredChildren += 1;
1264
+ }
1265
+ catch (err) {
1266
+ firstChildError = firstChildError ?? err;
1267
+ logger.warn({ err, jid }, 'aunu: one image-poll option image failed to send; continuing with the rest');
1268
+ }
1269
+ }
1270
+ if (!deliveredChildren) {
1271
+ throw firstChildError ?? new Boom('all image-poll option images failed to send', { statusCode: 500 });
1116
1272
  }
1117
1273
  }
1118
1274
  // AunuBaileys: kirim isi album sebagai pesan terasosiasi (dari @itsliaaa/baileys)
1119
1275
  if ('album' in content) {
1120
1276
  const { delayMs = 1500 } = options;
1277
+ // AunuBaileys (1.0.83, HB-173): per-item
1278
+ // isolation — one failing item (upload timeout,
1279
+ // total encryption failure) used to abort the
1280
+ // loop, leaving the already-sent album parent
1281
+ // with only some (or none) of its items while
1282
+ // the caller saw a total failure (and a retry
1283
+ // duplicated the parent). Deliver every item
1284
+ // that works; fail only when NONE did.
1285
+ let deliveredItems = 0;
1286
+ let firstItemError;
1121
1287
  for (const albumMedia of content.album) {
1122
- const albumMsg = await generateWAMessage(jid, albumMedia, {
1123
- logger,
1124
- userJid,
1125
- upload: (fp, opts) => waUploadToServer(fp, { ...opts, newsletter: isJidNewsletter(jid) }),
1126
- mediaCache: config.mediaCache,
1127
- options: config.options,
1128
- ...options,
1129
- messageId: generateMessageIDV2(sock.user?.id)
1130
- });
1131
- const normalized = normalizeMessageContent(albumMsg.message);
1132
- if (!normalized || !('imageMessage' in normalized || 'videoMessage' in normalized)) {
1133
- throw new Boom('Invalid message type for album', { statusCode: 400 });
1288
+ try {
1289
+ const albumMsg = await generateWAMessage(jid, albumMedia, {
1290
+ logger,
1291
+ userJid,
1292
+ upload: (fp, opts) => waUploadToServer(fp, { ...opts, newsletter: isJidNewsletter(jid) }),
1293
+ mediaCache: config.mediaCache,
1294
+ options: config.options,
1295
+ ...options,
1296
+ messageId: generateMessageIDV2(sock.user?.id)
1297
+ });
1298
+ const normalized = normalizeMessageContent(albumMsg.message);
1299
+ if (!normalized || !('imageMessage' in normalized || 'videoMessage' in normalized)) {
1300
+ throw new Boom('Invalid message type for album', { statusCode: 400 });
1301
+ }
1302
+ albumMsg.message.messageContextInfo = albumMsg.message.messageContextInfo || {};
1303
+ albumMsg.message.messageContextInfo.messageAssociation = {
1304
+ parentMessageKey: fullMsg.key,
1305
+ associationType: proto.MessageAssociation.AssociationType.MEDIA_ALBUM
1306
+ };
1307
+ await relay(jid, albumMsg.message, {
1308
+ messageId: albumMsg.key.id,
1309
+ useCachedGroupMetadata: options.useCachedGroupMetadata,
1310
+ statusJidList: options.statusJidList,
1311
+ statusPrivacy: options.statusPrivacy,
1312
+ statusPrivacy: options.statusPrivacy,
1313
+ additionalAttributes,
1314
+ additionalNodes
1315
+ });
1316
+ deliveredItems += 1;
1317
+ }
1318
+ catch (err) {
1319
+ firstItemError = firstItemError ?? err;
1320
+ logger.warn({ err, jid }, 'aunu: one album item failed to send; continuing with the rest of the album');
1134
1321
  }
1135
- albumMsg.message.messageContextInfo = albumMsg.message.messageContextInfo || {};
1136
- albumMsg.message.messageContextInfo.messageAssociation = {
1137
- parentMessageKey: fullMsg.key,
1138
- associationType: proto.MessageAssociation.AssociationType.MEDIA_ALBUM
1139
- };
1140
- await relay(jid, albumMsg.message, {
1141
- messageId: albumMsg.key.id,
1142
- useCachedGroupMetadata: options.useCachedGroupMetadata,
1143
- statusJidList: options.statusJidList,
1144
- statusPrivacy: options.statusPrivacy,
1145
- statusPrivacy: options.statusPrivacy,
1146
- additionalAttributes,
1147
- additionalNodes
1148
- });
1149
1322
  await delay(delayMs);
1150
1323
  }
1324
+ if (content.album.length && !deliveredItems) {
1325
+ throw firstItemError ?? new Boom('all album items failed to send', { statusCode: 500 });
1326
+ }
1151
1327
  }
1152
1328
  if (config.emitOwnEvents) {
1153
1329
  process.nextTick(() => {
@@ -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);
@@ -603,6 +603,18 @@ const withAunuButtonIcon = (button) => {
603
603
  buttonParamsJson: typeof params === 'string' ? params : JSON.stringify(params ?? {})
604
604
  };
605
605
  };
606
+ // AunuBaileys (1.0.83, HB-254): delete / pin / edit each carry a
607
+ // message KEY, and WAProto.Message.fromObject silently DROPS a key
608
+ // whose value is undefined — so { delete: undefined } used to relay
609
+ // a key-less REVOKE (pin and edit likewise) that "succeeded" but
610
+ // could never take effect, because the server had no key to act
611
+ // on. Mirror the pollUpdate branch's discipline: the argument
612
+ // must be a real key object carrying a message id.
613
+ const assertMessageKeyArg = (value, field) => {
614
+ if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.id !== 'string' || !value.id) {
615
+ throw new Boom(`"${field}" requires a valid message key (an object with a message id)`, { statusCode: 400 });
616
+ }
617
+ };
606
618
  export const generateWAMessageContent = async (message, options) => {
607
619
  var _a, _b;
608
620
  let m = {};
@@ -610,8 +622,13 @@ export const generateWAMessageContent = async (message, options) => {
610
622
  // straight through to generateWAMessage() unprocessed (donor:
611
623
  // @violetix/baileys). The caller owns the proto shape entirely.
612
624
  if (message && typeof message === 'object' && 'raw' in message && !!message.raw) {
613
- delete message.raw;
614
- return message;
625
+ // AunuBaileys (1.0.83, HB-165): work on a copy — the object
626
+ // belongs to the caller, and deleting `raw` from it made a
627
+ // second send of the same object silently lose the raw
628
+ // passthrough entirely.
629
+ const rawContent = { ...message };
630
+ delete rawContent.raw;
631
+ return rawContent;
615
632
  }
616
633
  // AunuBaileys: rich response — blok kode / tabel / tautan (dari @itsliaaa/baileys)
617
634
  if ('code' in message || 'links' in message || 'table' in message || 'richResponse' in message || 'html' in message || 'richHtml' in message) {
@@ -664,12 +681,19 @@ export const generateWAMessageContent = async (message, options) => {
664
681
  m.locationMessage = WAProto.Message.LocationMessage.fromObject(message.location);
665
682
  }
666
683
  else if ('react' in message) {
667
- if (!message.react.senderTimestampMs) {
668
- message.react.senderTimestampMs = Date.now();
684
+ // AunuBaileys (1.0.83, HB-165): stamp the timestamp on a
685
+ // copy — the caller's react object used to keep it, so a
686
+ // re-send of the same object carried the first send's
687
+ // (stale) timestamp. A non-object react keeps the original
688
+ // behaviour (the deref below throws, caller-visible).
689
+ const react = message.react && typeof message.react === 'object' ? { ...message.react } : message.react;
690
+ if (!react.senderTimestampMs) {
691
+ react.senderTimestampMs = Date.now();
669
692
  }
670
- m.reactionMessage = WAProto.Message.ReactionMessage.fromObject(message.react);
693
+ m.reactionMessage = WAProto.Message.ReactionMessage.fromObject(react);
671
694
  }
672
695
  else if ('delete' in message) {
696
+ assertMessageKeyArg(message.delete, 'delete');
673
697
  m.protocolMessage = {
674
698
  key: message.delete,
675
699
  type: WAProto.Message.ProtocolMessage.Type.REVOKE
@@ -706,6 +730,7 @@ export const generateWAMessageContent = async (message, options) => {
706
730
  }
707
731
  }
708
732
  else if ('pin' in message) {
733
+ assertMessageKeyArg(message.pin, 'pin');
709
734
  m.pinInChatMessage = {};
710
735
  m.messageContextInfo = {};
711
736
  m.pinInChatMessage.key = message.pin;
@@ -729,6 +754,13 @@ export const generateWAMessageContent = async (message, options) => {
729
754
  type: proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT
730
755
  };
731
756
  break;
757
+ default:
758
+ // AunuBaileys (1.0.83, HB-253 family): an unknown
759
+ // reply type used to fall through the switch,
760
+ // leaving m empty — a content-less message was
761
+ // relayed as if the reply had been sent. Fail
762
+ // loudly instead.
763
+ throw new Boom(`Unknown buttonReply type: ${message.type} (expected 'template' or 'plain')`, { statusCode: 400 });
732
764
  }
733
765
  }
734
766
  else if ('ptv' in message && message.ptv) {
@@ -814,6 +846,10 @@ export const generateWAMessageContent = async (message, options) => {
814
846
  throw new Boom('Event responses (RSVPs) are sent as encEventResponseMessage (Message field 76), encrypted with the event creation key — not yet implemented in AunuBaileys', { statusCode: 501 });
815
847
  }
816
848
  else if ('poll' in message) {
849
+ // AunuBaileys (1.0.83, HB-165): the default stamps below
850
+ // (selectableCount / toAnnouncementGroup) land on a copy of
851
+ // the caller's content, not on the caller's poll object.
852
+ message = { ...message, poll: { ...message.poll } };
817
853
  (_a = message.poll).selectableCount || (_a.selectableCount = 0);
818
854
  (_b = message.poll).toAnnouncementGroup || (_b.toAnnouncementGroup = false);
819
855
  if (!Array.isArray(message.poll.values)) {
@@ -1190,7 +1226,10 @@ export const generateWAMessageContent = async (message, options) => {
1190
1226
  };
1191
1227
  }
1192
1228
  m = { groupStatusMessageV2: { message: m } };
1193
- delete message.groupStatus;
1229
+ // AunuBaileys (1.0.83, HB-165): the flag used to be deleted
1230
+ // from the CALLER's object here — a re-send of the same
1231
+ // content object silently lost its group-status wrapping.
1232
+ // The flag is already consumed into m; leave it in place.
1194
1233
  }
1195
1234
  // AunuBaileys (1.0.71): spoiler — set contextInfo.isSpoiler and wrap
1196
1235
  // the message into spoilerMessage (Message field 118).
@@ -1206,7 +1245,9 @@ export const generateWAMessageContent = async (message, options) => {
1206
1245
  };
1207
1246
  }
1208
1247
  m = { spoilerMessage: { message: m } };
1209
- delete message.spoiler;
1248
+ // AunuBaileys (1.0.83, HB-165): same caller-object mutation
1249
+ // as the groupStatus block above — the flag stays on the
1250
+ // caller's object, so a re-send wraps identically.
1210
1251
  }
1211
1252
  // AunuBaileys: interactiveAsTemplate (dari @itsliaaa/baileys) — bungkus interactiveMessage
1212
1253
  // ke templateMessage; messageContextInfo (deviceListMetadata) dipertahankan di sisinya
@@ -1223,6 +1264,7 @@ export const generateWAMessageContent = async (message, options) => {
1223
1264
  };
1224
1265
  }
1225
1266
  if ('edit' in message) {
1267
+ assertMessageKeyArg(message.edit, 'edit');
1226
1268
  m = {
1227
1269
  protocolMessage: {
1228
1270
  key: message.edit,
@@ -18,7 +18,7 @@ export declare const xmppPreKey: (pair: KeyPair, id: number) => BinaryNode;
18
18
  export declare const parseAndInjectE2ESessions: (node: BinaryNode, repository: SignalRepository) => Promise<void>;
19
19
  /** AunuBaileys: session parsed from a retry receipt's embedded key bundle, or undefined when absent. */
20
20
  export declare const parseRetryReceiptSession: (node: BinaryNode) => { registrationId: number; identityKey: Buffer; signedPreKey?: { keyId: number; publicKey: Buffer; signature?: Buffer }; preKey?: { keyId: number; publicKey: Buffer } } | undefined;
21
- export declare const extractDeviceJids: (result: USyncQueryResultList[], myJid: string, excludeZeroDevices: boolean) => JidWithDevice[];
21
+ export declare const extractDeviceJids: (result: USyncQueryResultList[], myJid: string, myLid: string | undefined, excludeZeroDevices: boolean) => JidWithDevice[];
22
22
  /**
23
23
  * get the next N keys for upload or processing
24
24
  * @param count number of pre-keys to get or generate
@@ -184,8 +184,19 @@ export const parseAndInjectE2ESessions = async (node, repository, logger) => {
184
184
  }));
185
185
  }
186
186
  };
187
- export const extractDeviceJids = (result, myJid, excludeZeroDevices) => {
187
+ export const extractDeviceJids = (result, myJid, myLid, excludeZeroDevices) => {
188
188
  const { user: myUser, device: myDevice } = jidDecode(myJid) || {};
189
+ // AunuBaileys (1.0.83, HB-258): our own devices exist under TWO
190
+ // identities — the PN user and the LID user. Comparing only the
191
+ // PN user left our own LID devices in the extracted list, so a
192
+ // group send encrypted and delivered a copy to our own LID
193
+ // device (self-delivery plus wasted encryption). Upstream v7
194
+ // added the same myLid parameter; the comparison below decodes
195
+ // it to its user first, so the current device is excluded
196
+ // under BOTH identities while our other devices stay listed
197
+ // (group sender-key distribution and the DM echo still need
198
+ // them, exactly like the PN side).
199
+ const myLidUser = myLid ? jidDecode(myLid)?.user : undefined;
189
200
  const extracted = [];
190
201
  for (const userResult of result) {
191
202
  const { devices, id } = userResult;
@@ -197,7 +208,7 @@ export const extractDeviceJids = (result, myJid, excludeZeroDevices) => {
197
208
  if (Array.isArray(deviceList)) {
198
209
  for (const { id: device, keyIndex } of deviceList) {
199
210
  if ((!excludeZeroDevices || device !== 0) && // if zero devices are not-excluded, or device is non zero
200
- (myUser !== user || myDevice !== device) && // either different user or if me user, not this device
211
+ ((myUser !== user && myLidUser !== user) || myDevice !== device) && // either a different user (neither our PN nor our LID) or if me user, not this device
201
212
  device !== 99 && // AunuBaileys: hosted / Cloud-API companion devices (:99) are not E2E targets — libsignal rejects them ("Unexpected non-hosted device JID with device 99"); skip them when building the device list
202
213
  (device === 0 || !!keyIndex) // ensure that "key-index" is specified for "non-zero" devices, produces a bad req otherwise
203
214
  ) {
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.83",
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",