whalibmob 5.14.1 → 5.14.3

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
@@ -127,6 +127,7 @@ npm install -g whalibmob
127
127
  - [Reading it out of an APK you already have](#reading-it-out-of-an-apk-you-already-have)
128
128
  - [Device Attestation with Frida (optional)](#device-attestation-with-frida-optional)
129
129
  - [Connect](#connect)
130
+ - [Client Options](#client-options)
130
131
  - [Linking to an Existing Account (Pairing Code)](#linking-to-an-existing-account-pairing-code)
131
132
  - [Requesting a Pairing Code](#requesting-a-pairing-code)
132
133
  - [Reconnecting a Linked Session](#reconnecting-a-linked-session)
@@ -1928,6 +1929,37 @@ client.on('connected', () => {
1928
1929
  await client.init('919634847671')
1929
1930
  ```
1930
1931
 
1932
+ #### Client Options
1933
+
1934
+ Every option is optional; `sessionDir` is the only one most senders ever set.
1935
+
1936
+ | Option | Default | What it does |
1937
+ |---|---|---|
1938
+ | `sessionDir` | `~/.waSession` | The authentication folder. Each number gets its own subfolder inside it — see [Saving & Restoring Sessions](#saving--restoring-sessions). |
1939
+ | `autoFixNumber` | `true` | Re-file the session automatically when the server reports the account under a different number. Set `false` to be told instead of fixed — see [The Number WhatsApp Files Your Account Under](#the-number-whatsapp-files-your-account-under). |
1940
+ | `autoRead` | `true` | Send read receipts for incoming messages. `false` leaves them unread. |
1941
+ | `pino` | off | Debug logging. `true` turns it on at `debug` level; an object is passed to `pino` as-is. |
1942
+ | `sentCacheSize` | `2000` | How many sent messages keep their plaintext so a retry receipt naming them can be answered. |
1943
+ | `maxRetryResends` | `5` | How many times one message may be re-sent in answer to retry receipts before the client gives up. |
1944
+
1945
+ ```js
1946
+ const client = new WhalibmobClient({
1947
+ sessionDir: path.join(process.env.HOME, '.waSession'),
1948
+ sentCacheSize: 10000,
1949
+ maxRetryResends: 8
1950
+ })
1951
+ ```
1952
+
1953
+ **When to raise the last two.** A recipient's device that cannot decrypt a message asks for it again, and whalibmob answers by rebuilding the Signal session and re-sending. Answering requires the original text, which is why sent messages are held: a receipt naming a message no longer in the cache cannot be answered at all, and that message — already acked by the server — silently never arrives.
1954
+
1955
+ The defaults cover an ordinary sender. Raise `sentCacheSize` if you push messages faster than replies come back, which is easy to do when several recipients are being worked through at once: your own replies age out of the cache while the recipient is still asking for them. The symptom is this line in the debug log:
1956
+
1957
+ ```
1958
+ [DBG] RETRY_RECV msgId=... — no cached plaintext, skipping resend
1959
+ ```
1960
+
1961
+ If you see it, the cache is smaller than your in-flight window. An entry holds the encoded message, not the media it points at, so entries are small and raising the bound costs little memory.
1962
+
1931
1963
  ## Linking to an Existing Account (Pairing Code)
1932
1964
 
1933
1965
  Registering a number over SMS makes whalibmob that number's **own device**. Sometimes that is not what you want — the number is already in use on a phone, or the verification SMS never arrives. For those cases whalibmob can instead connect over a **WebSocket** and link itself to an account that already exists, exactly the way the WhatsApp Web and desktop clients do.
package/lib/Client.js CHANGED
@@ -29,7 +29,19 @@ const {
29
29
  // How many times a message may be re-sent in answer to retry receipts before
30
30
  // the client stops. Each resend can itself draw retries, so this is what keeps
31
31
  // an undecryptable message from turning into a flood.
32
- const MAX_RETRY_RESENDS = 2;
32
+ //
33
+ // Matched to the cap on retries we ask for ourselves (5, in _sendRetryRequest):
34
+ // a device that keeps failing is worth the same number of rounds in either
35
+ // direction. Two rounds gave up while the recipient was still asking.
36
+ const MAX_RETRY_RESENDS = 5;
37
+
38
+ // How many sent messages keep their plaintext for answering a retry receipt.
39
+ // A receipt naming a message no longer held here cannot be answered at all —
40
+ // the message is acked by the server and then silently never arrives. The
41
+ // bound exists for memory, and an entry is small (the encoded Message, not the
42
+ // media it points at), so it is set well above a busy sender's in-flight window
43
+ // rather than at the smallest number that usually works.
44
+ const SENT_CACHE_SIZE = 2000;
33
45
  const { NodeCache } = require('@cacheable/node-cache');
34
46
 
35
47
  // Group metadata cache lifetime — matches DeviceManager's device cache.
@@ -369,6 +381,11 @@ class WhalibmobClient extends EventEmitter {
369
381
  // the second sender's pkmsg arrives after the key was consumed and deleted.
370
382
  this._retryAdvertisedPreKeys = new Set();
371
383
  this._sentMsgCache = new Map(); // msgId → {plaintext, toJid, msgId, mediaType, options, recipientPhone}
384
+ // A sender that keeps more messages in flight than the cache holds outruns
385
+ // its own ability to answer retries, so both bounds are open to being
386
+ // raised for that kind of load.
387
+ this._sentCacheSize = Number(opts.sentCacheSize) > 0 ? Number(opts.sentCacheSize) : SENT_CACHE_SIZE;
388
+ this._maxRetryResends = Number(opts.maxRetryResends) > 0 ? Number(opts.maxRetryResends) : MAX_RETRY_RESENDS;
372
389
  this._tcTokenStore = null; // TcTokenStore — loaded in init()
373
390
  this._inFlightTcTokenIssuance = new Set(); // dedupe concurrent proactive issuePrivacyTokens per JID
374
391
  this._inFlight463Recoveries = new Set(); // dedupe concurrent 463-triggered token issuances per JID (separate from proactive)
@@ -2415,7 +2432,7 @@ class WhalibmobClient extends EventEmitter {
2415
2432
  // another send and floods the chat. Give up after a few rounds and leave
2416
2433
  // the message undelivered rather than keep spamming.
2417
2434
  const depth = (cached.retryDepth || 0) + 1;
2418
- if (depth > MAX_RETRY_RESENDS) {
2435
+ if (depth > this._maxRetryResends) {
2419
2436
  _whaDbg('[DBG] RETRY_RECV msgId=' + msgId + ' — giving up after ' +
2420
2437
  cached.retryDepth + ' resends\n');
2421
2438
  return;
@@ -2432,50 +2449,9 @@ class WhalibmobClient extends EventEmitter {
2432
2449
  : fromStr;
2433
2450
  const recipientPhone = targetJid.split('@')[0].split(':')[0].split('.')[0];
2434
2451
 
2435
- // The retry names the device under one address family, but a contact holds
2436
- // a session under each: a Signal address is (user, device) with the server
2437
- // stripped, and the LID user and the phone user are different strings, so
2438
- // one session is not reachable under the other's name. Purging only the
2439
- // family the receipt happened to arrive in left the other one in place —
2440
- // the resend picked it straight back up and produced the same ciphertext
2441
- // the device had just said it could not read, so the message stayed
2442
- // undecryptable until something else rebuilt the session.
2443
- //
2444
- // This is what made a reply sent after a companion-device exchange stick on
2445
- // "Waiting for this message" on the primary: the receipt came in one family,
2446
- // the stale session sat in the other, and every resend re-used it.
2447
- const recipientUsers = new Set([recipientPhone]);
2448
- const mappedPn = this._lidToPn && this._lidToPn.get(recipientPhone);
2449
- const mappedLid = this._pnToLid && this._pnToLid.get(recipientPhone);
2450
- if (mappedPn) recipientUsers.add(mappedPn);
2451
- if (mappedLid) recipientUsers.add(mappedLid);
2452
-
2453
2452
  _whaDbg('[DBG] RETRY_RECV msgId=' + msgId + ' from=' + fromStr +
2454
- ' users=[' + [...recipientUsers].join(',') + '] — clearing session + resending\n');
2455
-
2456
- // Delete all Signal sessions for the recipient's devices so next send creates fresh pkmsg
2457
- const sigStore = this._signal && this._signal.store;
2458
- if (sigStore && sigStore._sessions) {
2459
- const sessions = sigStore._sessions;
2460
- Object.keys(sessions).forEach(addr => {
2461
- // Match on the user segment rather than a bare prefix: "4071.0" must
2462
- // not be cleared by a retry for "407".
2463
- const user = addr.slice(0, addr.lastIndexOf('.'));
2464
- if (recipientUsers.has(user)) {
2465
- _whaDbg('[DBG] RETRY deleting session for ' + addr);
2466
- delete sessions[addr];
2467
- }
2468
- });
2469
- }
2470
-
2471
- // Clear device manager cache so we re-fetch devices on next send. LID device
2472
- // lists are filed under a "lid:" key, so clearing the bare user only ever
2473
- // dropped the phone-side list.
2474
- if (this._devMgr) {
2475
- const cacheKeys = [];
2476
- for (const u of recipientUsers) cacheKeys.push(u, 'lid:' + u);
2477
- this._devMgr.clearCache(cacheKeys);
2478
- }
2453
+ ' — clearing session + resending\n');
2454
+ this._purgeContactSessions(recipientPhone, 'RETRY');
2479
2455
 
2480
2456
  // Re-send the message — will create fresh pre-key (pkmsg) sessions.
2481
2457
  // A group resend must go back through the group path: it needs a fresh
@@ -2723,6 +2699,71 @@ class WhalibmobClient extends EventEmitter {
2723
2699
  this._retryPending.delete(msgId);
2724
2700
  }
2725
2701
 
2702
+ // Drop every Signal session and cached device list a contact owns, so the
2703
+ // next send to them has to fetch a prekey bundle and open a fresh session.
2704
+ //
2705
+ // A contact holds a session under each address family: a Signal address is
2706
+ // (user, device) with the server stripped, and the LID user and the phone
2707
+ // user are different strings, so one session is not reachable under the
2708
+ // other's name. Clearing only the family whose name happened to be at hand
2709
+ // leaves the other in place, and the next send picks it straight back up —
2710
+ // which is how a message stayed undecryptable on a device that had already
2711
+ // said it could not read it.
2712
+ //
2713
+ // Device lists are filed the same way, LID ones under a "lid:" key, so both
2714
+ // spellings of both users have to go.
2715
+ //
2716
+ // Returns the set of users it resolved, for callers that want to log it.
2717
+ _purgeContactSessions(user, tag) {
2718
+ const label = tag || 'PURGE';
2719
+ const users = new Set([String(user)]);
2720
+ const mappedPn = this._lidToPn && this._lidToPn.get(user);
2721
+ const mappedLid = this._pnToLid && this._pnToLid.get(user);
2722
+ if (mappedPn) users.add(mappedPn);
2723
+ if (mappedLid) users.add(mappedLid);
2724
+
2725
+ const sigStore = this._signal && this._signal.store;
2726
+ if (sigStore && sigStore._sessions) {
2727
+ const sessions = sigStore._sessions;
2728
+ for (const addr of Object.keys(sessions)) {
2729
+ // Match on the address's user segment rather than a bare string
2730
+ // prefix: a purge for "407" must not take "4071.0" with it.
2731
+ const addrUser = addr.slice(0, addr.lastIndexOf('.'));
2732
+ if (users.has(addrUser)) {
2733
+ _whaDbg('[DBG] ' + label + ' deleting session for ' + addr);
2734
+ // Through the store rather than off the object it hands back: a bare
2735
+ // `delete` leaves the file alone and the store unmarked, so nothing
2736
+ // is written on the way out and a restart loads every purged session
2737
+ // straight back — the one case the purge exists to prevent. Not
2738
+ // awaited on purpose; the method's body is synchronous, so the
2739
+ // delete and the save have both happened by the time it returns.
2740
+ sigStore.deleteSession(addr);
2741
+ }
2742
+ }
2743
+ }
2744
+
2745
+ if (this._devMgr) {
2746
+ const cacheKeys = [];
2747
+ for (const u of users) cacheKeys.push(u, 'lid:' + u);
2748
+ this._devMgr.clearCache(cacheKeys);
2749
+ }
2750
+
2751
+ _whaDbg('[DBG] ' + label + ' purged users=[' + [...users].join(',') + ']');
2752
+ return users;
2753
+ }
2754
+
2755
+ // Hold a sent message's plaintext so a retry receipt naming it can be
2756
+ // answered. Both send paths file their entry here so the bound lives in one
2757
+ // place instead of being repeated as a literal at each call site.
2758
+ _cacheSentMessage(entry) {
2759
+ if (!this._sentMsgCache || !entry || !entry.msgId) return;
2760
+ this._sentMsgCache.set(entry.msgId, entry);
2761
+ // Map preserves insertion order, so the first key is the oldest entry.
2762
+ while (this._sentMsgCache.size > this._sentCacheSize) {
2763
+ this._sentMsgCache.delete(this._sentMsgCache.keys().next().value);
2764
+ }
2765
+ }
2766
+
2726
2767
  _sendRetryRequest(msgId, origNode) {
2727
2768
  const existing = this._retryPending.get(msgId);
2728
2769
  const count = existing ? existing.count + 1 : 1;
@@ -2943,23 +2984,16 @@ class WhalibmobClient extends EventEmitter {
2943
2984
  }
2944
2985
 
2945
2986
  if (type === 'identity') {
2946
- // Server push: a contact re-registered WhatsApp (new identity key / new phone).
2947
- // Their old Signal sessions are no longer valid — clear them and the device
2948
- // cache so the next send builds a fresh pkmsg session from scratch.
2987
+ // Server push: a contact re-registered WhatsApp (new identity key / new
2988
+ // phone). Every session we hold with them is anchored in an identity key
2989
+ // that no longer exists, so all of them have to go — under both the
2990
+ // number and the LID, or the next send reaches for the survivor and the
2991
+ // contact receives ciphertext nothing on their side can open. A retry
2992
+ // receipt cannot rescue that: the session is not stale, it is dead.
2949
2993
  const fromJid = String(attrs.from || '');
2950
2994
  const fromPhone = fromJid.split('@')[0].split(':')[0];
2951
- if (fromPhone && this._devMgr) {
2952
- this._devMgr._dcDel([fromPhone]);
2953
- _whaDbg('[DBG] NOTIF_IDENTITY flushed device cache for re-registered ' + fromPhone);
2954
- }
2955
- if (fromPhone && this._signal && this._signal.store && this._signal.store._sessions) {
2956
- const sessions = this._signal.store._sessions;
2957
- let cleared = 0;
2958
- for (const addr of Object.keys(sessions)) {
2959
- if (addr.startsWith(fromPhone + '.')) { delete sessions[addr]; cleared++; }
2960
- }
2961
- _whaDbg('[DBG] NOTIF_IDENTITY cleared ' + cleared + ' Signal sessions for ' + fromPhone);
2962
- }
2995
+ if (fromPhone) this._purgeContactSessions(fromPhone, 'NOTIF_IDENTITY');
2996
+ this.emit('identity_change', { jid: fromJid, user: fromPhone });
2963
2997
  }
2964
2998
 
2965
2999
  // Ack the notification
@@ -1091,17 +1091,10 @@ class MessageSender {
1091
1091
  }
1092
1092
 
1093
1093
  // Cache plaintext so Client can re-send with fresh session on recipient retry
1094
- if (this._client._sentMsgCache) {
1095
- this._client._sentMsgCache.set(msgId, {
1096
- plaintext, toJid, msgId, mediaType, options, recipientPhone,
1097
- retryDepth: options._retryDepth || 0
1098
- });
1099
- // Keep cache bounded — evict oldest entries beyond 200
1100
- if (this._client._sentMsgCache.size > 200) {
1101
- const firstKey = this._client._sentMsgCache.keys().next().value;
1102
- this._client._sentMsgCache.delete(firstKey);
1103
- }
1104
- }
1094
+ this._client._cacheSentMessage({
1095
+ plaintext, toJid, msgId, mediaType, options, recipientPhone,
1096
+ retryDepth: options._retryDepth || 0
1097
+ });
1105
1098
 
1106
1099
  const dispatchResult = await this._dispatchAndAck(msgNode, msgId);
1107
1100
 
@@ -1436,16 +1429,10 @@ class MessageSender {
1436
1429
  // does, and without this the client had nothing to re-send and dropped it
1437
1430
  // with "no cached plaintext" — so the message was acked by the server and
1438
1431
  // then silently never arrived.
1439
- if (this._client._sentMsgCache) {
1440
- this._client._sentMsgCache.set(msgId, {
1441
- plaintext, toJid: groupJid, msgId, mediaType, options, isGroup: true,
1442
- retryDepth: options._retryDepth || 0
1443
- });
1444
- if (this._client._sentMsgCache.size > 200) {
1445
- const firstKey = this._client._sentMsgCache.keys().next().value;
1446
- this._client._sentMsgCache.delete(firstKey);
1447
- }
1448
- }
1432
+ this._client._cacheSentMessage({
1433
+ plaintext, toJid: groupJid, msgId, mediaType, options, isGroup: true,
1434
+ retryDepth: options._retryDepth || 0
1435
+ });
1449
1436
 
1450
1437
  const msgNode = new BinaryNode('message', stanzaAttrs, msgContent);
1451
1438
  const dispatchResult = await this._dispatchAndAck(msgNode, msgId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.14.1",
3
+ "version": "5.14.3",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API and web ",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",