aunubaileys 1.0.16 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,6 +64,14 @@ Everything else is pure upstream. When upstream moves, we rebase the patch layer
64
64
 
65
65
 
66
66
 
67
+ ### 🆕 What's new in 1.0.18 / Baru di 1.0.18
68
+
69
+ - 🔁 **Retry receipts: the peer's own key bundle is finally used** — when a recipient device asks for a resend, its retry receipt often embeds that device's own current key bundle. Upstream ignored it and always re-fetched a bundle from the server (extra round trip, and JID→bundle mapping is fragile for LID participants). AunuBaileys now parses the bundle (new exported `parseRetryReceiptSession`, unit-verified) and injects it as the fresh session for the resend, skipping the server fetch for that resend. Receipts without a bundle behave exactly as before.
70
+
71
+ ### 🆕 What's new in 1.0.17 / Baru di 1.0.17
72
+
73
+ - 👥 **Group sender-key heal** — the 1.0.14 failure counter now covers group messages too: after 3 consecutive group decrypt failures from the same author in the same group, the stale sender-key record is deleted (new `signalRepository.deleteSenderKey`), so the next retry makes the author resend with a fresh distribution message and the group cipher rebuilds from scratch. One rotten sender-key record can no longer mute a group member forever.
74
+
67
75
  ### 🆕 What's new in 1.0.16 / Baru di 1.0.16
68
76
 
69
77
  - 🪪 **Identity changes are handled, not just logged** — when WhatsApp notifies that a contact's identity changed (new phone / reinstall / recycled number), upstream Baileys logs “identity changed” and does nothing, leaving your stored session bound to the old identity so that contact's messages keep failing. AunuBaileys now deletes the stored session for that JID on the spot, so the next exchange re-initiates cleanly against the new identity.
@@ -66,6 +66,10 @@ export function makeLibSignalRepository(auth) {
66
66
  senderKeyDistributionMessage: senderKeyDistributionMessage.serialize()
67
67
  };
68
68
  },
69
+ async deleteSenderKey(group, authorJid) {
70
+ const senderNameStr = jidToSignalSenderKeyName(group, authorJid).toString();
71
+ await auth.keys.set({ 'sender-key': { [senderNameStr]: null } });
72
+ },
69
73
  async deleteSession(jid) {
70
74
  const addr = jidToSignalProtocolAddress(jid).toString();
71
75
  await auth.keys.set({ session: { [addr]: null } });
@@ -5,7 +5,7 @@ import Long from 'long';
5
5
  import { proto } from '../../WAProto/index.js';
6
6
  import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults/index.js';
7
7
  import { WAMessageStatus, WAMessageStubType } from '../Types/index.js';
8
- import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
8
+ import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, parseRetryReceiptSession, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
9
9
  import { makeMutex } from '../Utils/make-mutex.js';
10
10
  import { sentMessageCacheFor } from '../Utils/sent-message-cache.js';
11
11
  import { areJidsSameUser, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidStatusBroadcast, isJidUser, isLidUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
@@ -481,8 +481,7 @@ export const makeMessagesRecvSocket = (config) => {
481
481
  const newValue = (msgRetryCache.get(key) || 0) + 1;
482
482
  msgRetryCache.set(key, newValue);
483
483
  };
484
- const sendMessagesAgain = async (key, ids, retryNode) => {
485
- // todo: implement a cache to store the last 256 sent messages (copy whatsmeow)
484
+ const sendMessagesAgain = async (key, ids, retryNode, sessionPreInjected = false) => {
486
485
  const msgs = await Promise.all(ids.map(async (id) => (await getMessage({ ...key, id })) ?? sentMessageCacheFor(config).get(key.remoteJid, id)));
487
486
  const remoteJid = key.remoteJid;
488
487
  const participant = key.participant || remoteJid;
@@ -490,7 +489,9 @@ export const makeMessagesRecvSocket = (config) => {
490
489
  // just re-send the message to everyone
491
490
  // prevents the first message decryption failure
492
491
  const sendToAll = !jidDecode(participant)?.device;
493
- await assertSessions([participant], true);
492
+ if (!sessionPreInjected) {
493
+ await assertSessions([participant], true);
494
+ }
494
495
  if (isJidGroup(remoteJid)) {
495
496
  await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } });
496
497
  }
@@ -572,7 +573,24 @@ export const makeMessagesRecvSocket = (config) => {
572
573
  if (key.fromMe) {
573
574
  try {
574
575
  logger.debug({ attrs, key }, 'recv retry request');
575
- await sendMessagesAgain(key, ids, retryNode);
576
+ // AunuBaileys: when the retry receipt carries the
577
+ // requester's own key bundle, build the fresh
578
+ // session from THAT (authoritative for the exact
579
+ // device, no server round trip) and skip the
580
+ // force-fetch inside sendMessagesAgain.
581
+ let sessionPreInjected = false;
582
+ const bundleSession = parseRetryReceiptSession(node);
583
+ if (bundleSession && key.participant) {
584
+ try {
585
+ await signalRepository.injectE2ESession({ jid: key.participant, session: bundleSession });
586
+ sessionPreInjected = true;
587
+ logger.debug({ participant: key.participant }, 'aunu: injected E2E session from retry receipt bundle');
588
+ }
589
+ catch (injErr) {
590
+ logger.debug({ err: injErr }, 'aunu: retry receipt bundle injection failed; falling back to server fetch');
591
+ }
592
+ }
593
+ await sendMessagesAgain(key, ids, retryNode, sessionPreInjected);
576
594
  }
577
595
  catch (error) {
578
596
  logger.error({ key, ids, trace: error.stack }, 'error in sending message again');
@@ -677,18 +695,26 @@ export const makeMessagesRecvSocket = (config) => {
677
695
  }
678
696
  const encNode = getBinaryNodeChild(node, 'enc');
679
697
  let forceIncludeKeys = !encNode;
680
- if (!isJidGroup(msg.key.remoteJid) && author) {
698
+ if (author) {
681
699
  try {
700
+ const isGroupMsg = isJidGroup(msg.key.remoteJid);
682
701
  const addr = signalRepository.jidToSignalProtocolAddress(author);
683
- const fails = (decryptFailCounts.get(addr) || 0) + 1;
702
+ const failKey = isGroupMsg ? `sk:${msg.key.remoteJid}:${addr}` : addr;
703
+ const fails = (decryptFailCounts.get(failKey) || 0) + 1;
684
704
  if (fails >= DECRYPT_FAIL_SESSION_RESET_LIMIT) {
685
- await signalRepository.deleteSession(author);
686
- decryptFailCounts.delete(addr);
705
+ if (isGroupMsg) {
706
+ await signalRepository.deleteSenderKey(msg.key.remoteJid, author);
707
+ logger.warn({ author, group: msg.key.remoteJid }, 'aunu: deleted stale sender-key record after repeated group decrypt failures; retry request asks the author to resend with a fresh distribution message');
708
+ }
709
+ else {
710
+ await signalRepository.deleteSession(author);
711
+ logger.warn({ author, addr }, 'aunu: deleted stale E2E session after repeated decrypt failures (Bad MAC self-heal); retry receipt carries fresh keys');
712
+ }
713
+ decryptFailCounts.delete(failKey);
687
714
  forceIncludeKeys = true;
688
- logger.warn({ author, addr }, 'aunu: deleted stale E2E session after repeated decrypt failures (Bad MAC self-heal); retry receipt carries fresh keys');
689
715
  }
690
716
  else {
691
- decryptFailCounts.set(addr, fails);
717
+ decryptFailCounts.set(failKey, fails);
692
718
  }
693
719
  }
694
720
  catch (healErr) {
@@ -708,7 +734,9 @@ export const makeMessagesRecvSocket = (config) => {
708
734
  else {
709
735
  if (author) {
710
736
  try {
711
- decryptFailCounts.delete(signalRepository.jidToSignalProtocolAddress(author));
737
+ const addr = signalRepository.jidToSignalProtocolAddress(author);
738
+ decryptFailCounts.delete(addr);
739
+ decryptFailCounts.delete(`sk:${msg.key.remoteJid}:${addr}`);
712
740
  }
713
741
  catch { }
714
742
  }
@@ -54,6 +54,8 @@ export type SignalRepository = {
54
54
  injectE2ESession(opts: E2ESessionOpts): Promise<void>;
55
55
  /** AunuBaileys: delete the stored E2E session for a JID (used by the Bad MAC self-heal). */
56
56
  deleteSession(jid: string): Promise<void>;
57
+ /** AunuBaileys: delete the stored sender-key record for a group author (stale group session heal). */
58
+ deleteSenderKey(group: string, authorJid: string): Promise<void>;
57
59
  jidToSignalProtocolAddress(jid: string): string;
58
60
  };
59
61
  export {};
@@ -16,6 +16,8 @@ export declare const generateOrGetPreKeys: (creds: AuthenticationCreds, range: n
16
16
  export declare const xmppSignedPreKey: (key: SignedKeyPair) => BinaryNode;
17
17
  export declare const xmppPreKey: (pair: KeyPair, id: number) => BinaryNode;
18
18
  export declare const parseAndInjectE2ESessions: (node: BinaryNode, repository: SignalRepository) => Promise<void>;
19
+ /** AunuBaileys: session parsed from a retry receipt's embedded key bundle, or undefined when absent. */
20
+ export declare const parseRetryReceiptSession: (node: BinaryNode) => { registrationId: number; identityKey: Buffer; signedPreKey?: { keyId: number; publicKey: Buffer; signature?: Buffer }; preKey?: { keyId: number; publicKey: Buffer } } | undefined;
19
21
  export declare const extractDeviceJids: (result: USyncQueryResultList[], myJid: string, excludeZeroDevices: boolean) => JidWithDevice[];
20
22
  /**
21
23
  * get the next N keys for upload or processing
@@ -55,6 +55,37 @@ export const xmppPreKey = (pair, id) => ({
55
55
  { tag: 'value', attrs: {}, content: pair.public }
56
56
  ]
57
57
  });
58
+ /** AunuBaileys: parse the E2E key bundle embedded in a retry receipt.
59
+ * When a recipient device asks us to resend, its receipt often carries the
60
+ * device's OWN current key bundle (<keys> + <registration>) — the device
61
+ * itself stating "build your session to me from THIS". Upstream ignores it
62
+ * and re-fetches a bundle from the server instead; consuming the receipt's
63
+ * bundle skips that round trip and, more importantly, is authoritative for
64
+ * the exact requesting device (server lookups by JID are fragile for LID
65
+ * participants). Returns undefined when the receipt carries no bundle. */
66
+ export const parseRetryReceiptSession = (node) => {
67
+ const keysNode = getBinaryNodeChild(node, 'keys');
68
+ if (!keysNode) {
69
+ return undefined;
70
+ }
71
+ const identity = getBinaryNodeChildBuffer(keysNode, 'identity');
72
+ if (!identity) {
73
+ return undefined;
74
+ }
75
+ const extractKey = (key) => key
76
+ ? {
77
+ keyId: getBinaryNodeChildUInt(key, 'id', 3),
78
+ publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')),
79
+ signature: getBinaryNodeChildBuffer(key, 'signature')
80
+ }
81
+ : undefined;
82
+ return {
83
+ registrationId: getBinaryNodeChildUInt(node, 'registration', 4),
84
+ identityKey: generateSignalPubKey(identity),
85
+ signedPreKey: extractKey(getBinaryNodeChild(keysNode, 'skey')),
86
+ preKey: extractKey(getBinaryNodeChild(keysNode, 'key'))
87
+ };
88
+ };
58
89
  export const parseAndInjectE2ESessions = async (node, repository) => {
59
90
  const extractKey = (key) => key
60
91
  ? {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aunubaileys",
3
3
  "type": "module",
4
- "version": "1.0.16",
4
+ "version": "1.0.18",
5
5
  "description": "AunuBaileys \u2014 a WhatsApp Web API for bots that never sleep. Full upstream power with the beloved in-memory store restored and pairing that just works. Forged by AunuCore.",
6
6
  "keywords": [
7
7
  "whatsapp",