whalibmob 5.24.1 → 5.25.0

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
@@ -2884,7 +2884,7 @@ client.on('version_update', ({ from, to, source }) => {
2884
2884
  console.log('announcing', to, 'instead of', from, '—', source)
2885
2885
  })
2886
2886
 
2887
- await client.init('40756469325')
2887
+ await client.init('919634847671')
2888
2888
  ```
2889
2889
 
2890
2890
  **Reconnects are covered too.** The check runs from the socket-connect step
@@ -2923,8 +2923,8 @@ Set `{ refreshVersion: false }` on the client to turn it off.
2923
2923
  The manual tool still works on both platforms:
2924
2924
 
2925
2925
  ```bash
2926
- wa refresh-version 40756469325 # current build for the platform
2927
- wa refresh-version 40756469325 --version 2.25.1.2 # or one you name
2926
+ wa refresh-version 919634847671 # current build for the platform
2927
+ wa refresh-version 919634847671 --version 2.25.1.2 # or one you name
2928
2928
  ```
2929
2929
 
2930
2930
  Companion sessions have never had this problem: `connectWeb()` reads the live
@@ -3575,6 +3575,14 @@ whalibmob implements the full lifecycle to prevent this:
3575
3575
 
3576
3576
  Token storage uses the **LID JID** of the contact (e.g. `112345678901234@lid`) as the key — never the phone-number JID — matching WhatsApp's internal convention.
3577
3577
 
3578
+ **cstoken — the companion fallback.** When there is no `<tctoken>` for a recipient, the genuine client attaches a `<cstoken>` instead. Unlike a tctoken it is not granted per contact; it is computed locally:
3579
+
3580
+ ```
3581
+ cstoken = HMAC-SHA256(nctSalt, recipient-LID)
3582
+ ```
3583
+
3584
+ The `nctSalt` is an account-wide secret the server distributes over app state, so only a **companion (QR / pairing-code) session** ever holds one — a primary (SMS) session has none and sends no cstoken, exactly as the real primary does until it has minted its own. When a companion session receives the salt it fires a `nct_salt` event and persists it; from then on, any first DM without a tctoken carries a `<cstoken>` derived for that recipient's LID. Like the rest of this section it is fully automatic — there is nothing to call.
3585
+
3578
3586
  <a id="account-restriction"></a>
3579
3587
 
3580
3588
  ### When 463 Means the Account Is Restricted
package/cli.js CHANGED
@@ -1385,7 +1385,7 @@ async function handleLine(line) {
1385
1385
  if (!ph) {
1386
1386
  fail('usage: /pair <phone> [8-char-code]');
1387
1387
  out(' the phone number is the account you want to link to, digits only');
1388
- out(' example: /pair 40756469325');
1388
+ out(' example: /pair 919634847671');
1389
1389
  break;
1390
1390
  }
1391
1391
  if (_client && _client.connected) { fail('already connected — /disconnect first'); break; }
@@ -1402,7 +1402,7 @@ async function handleLine(line) {
1402
1402
  out(' links the number as a companion by QR instead of a pairing code');
1403
1403
  out(' a QR is drawn in the terminal — scan it from the phone that owns');
1404
1404
  out(' the number: WhatsApp → Linked Devices → Link a device');
1405
- out(' example: /qrcode 40756469325');
1405
+ out(' example: /qrcode 919634847671');
1406
1406
  break;
1407
1407
  }
1408
1408
  if (_client && _client.connected) { fail('already connected — /disconnect first'); break; }
package/index.d.ts CHANGED
@@ -688,6 +688,8 @@ export interface WhalibmobEvents {
688
688
  /** The server refused the client itself — `405` means the announced version is not accepted. */
689
689
  client_rejected: (r: { reason: string; location?: string; message?: string }) => void;
690
690
  version_update: (v: { from: string; to: string; source: string }) => void;
691
+ /** A companion session received the NCT salt (over app state) that cstoken is derived from. */
692
+ nct_salt: (s: { bytes: number }) => void;
691
693
  apk_material_stale: (m: { materialVersion: string; liveVersion: string; hint?: string }) => void;
692
694
  number_corrected: (n: { from: string; to: string }) => void;
693
695
 
package/lib/BinaryNode.js CHANGED
@@ -49,7 +49,7 @@ const _JID_SERVERS = new Set([
49
49
  's.whatsapp.net', 'g.us', 'lid', 'broadcast', 'call', 'newsletter', 'c.us', 's.us'
50
50
  ]);
51
51
 
52
- // "120363427770886227@g.us" / "40756469325:2@s.whatsapp.net" → JID object.
52
+ // "120363427770886227@g.us" / "919634847671:2@s.whatsapp.net" → JID object.
53
53
  // Returns null for anything that is not a full JID, so plain strings (and
54
54
  // bare server tokens like "g.us", which the dictionary already encodes as a
55
55
  // valid address) keep their existing encoding untouched.
package/lib/Client.js CHANGED
@@ -2229,6 +2229,27 @@ class WhalibmobClient extends EventEmitter {
2229
2229
  case 'delete':
2230
2230
  this.emit('chat_removed', { jid, kind: what.kind, remote: true });
2231
2231
  break;
2232
+ case 'nct_salt_sync': {
2233
+ // The salt the cstoken fallback is derived from. The server
2234
+ // distributes it into app state; a SET carries the bytes, a REMOVE
2235
+ // withdraws it. Kept on the store so it survives a reconnect. Only a
2236
+ // companion ever gets one, which is why cstoken is a companion-only
2237
+ // thing — a primary has no salt on file and sends none, exactly as
2238
+ // the real primary does until it has minted its own.
2239
+ if (removed) {
2240
+ if (this._store) { this._store.nctSalt = null; this._persistStore(); }
2241
+ _whaDbg('[DBG] NCT_SALT cleared');
2242
+ } else {
2243
+ const salt = a.nctSaltSyncAction && a.nctSaltSyncAction.salt;
2244
+ if (salt && salt.length && this._store) {
2245
+ this._store.nctSalt = Buffer.from(salt).toString('base64');
2246
+ this._persistStore();
2247
+ _whaDbg('[DBG] NCT_SALT stored (' + salt.length + ' bytes)');
2248
+ this.emit('nct_salt', { bytes: salt.length });
2249
+ }
2250
+ }
2251
+ break;
2252
+ }
2232
2253
  default:
2233
2254
  // Something the server tracks that this library does not model yet.
2234
2255
  // Reported rather than dropped, so it is visible that it happened.
@@ -2707,6 +2728,67 @@ class WhalibmobClient extends EventEmitter {
2707
2728
  return lid ? lid + device + '@lid' : null;
2708
2729
  }
2709
2730
 
2731
+ // ─── cstoken ────────────────────────────────────────────────────────────────
2732
+ //
2733
+ // The anti-abuse token the genuine client attaches to a message when it has no
2734
+ // trusted-contact token for the recipient. Unlike a tctoken it is not granted
2735
+ // by the server per contact — it is computed locally:
2736
+ //
2737
+ // cstoken = HMAC-SHA256(nctSalt, recipient LID)
2738
+ //
2739
+ // The salt is account-wide and arrives over app state, so only a companion
2740
+ // ever holds one. A primary (an SMS session) has no salt on file and sends no
2741
+ // cstoken, which is what the real primary does until it has minted its own.
2742
+ //
2743
+ // This mirrors whatsmeow's generateCsToken line for line: a regular user, not
2744
+ // the PSA account and not a bot; the recipient resolved to a LID; the HMAC
2745
+ // taken over that LID's bare `user@lid` string.
2746
+
2747
+ /** The stored NCT salt as bytes, or null when this session has none. */
2748
+ _nctSalt() {
2749
+ const b64 = this._store && this._store.nctSalt;
2750
+ if (!b64) return null;
2751
+ try { const b = Buffer.from(b64, 'base64'); return b.length ? b : null; }
2752
+ catch (_) { return null; }
2753
+ }
2754
+
2755
+ /**
2756
+ * The recipient's bare LID (`user@lid`), or null when there is no mapping.
2757
+ *
2758
+ * A JID already on the LID server is used as-is; a phone JID is resolved
2759
+ * through the same PN→LID map every send uses. Device and agent parts are
2760
+ * dropped — the token is taken over the non-AD address, as whatsmeow does.
2761
+ */
2762
+ _bareLidFor(jid) {
2763
+ if (!jid) return null;
2764
+ const str = String(jid);
2765
+ const user = str.split('@')[0].split(':')[0];
2766
+ if (!user) return null;
2767
+ if (str.endsWith('@lid')) return user + '@lid';
2768
+ const lid = this._pnToLid && this._pnToLid.get(user);
2769
+ return lid ? lid + '@lid' : null;
2770
+ }
2771
+
2772
+ /**
2773
+ * The cstoken bytes for a recipient, or null when none should be sent.
2774
+ *
2775
+ * Returns null — so no `<cstoken>` is attached — unless a salt is on file,
2776
+ * the recipient is a regular user, and a LID resolves for them. Every one of
2777
+ * those is a reason the genuine client also sends nothing.
2778
+ */
2779
+ _csTokenFor(jid) {
2780
+ const salt = this._nctSalt();
2781
+ if (!salt) return null;
2782
+
2783
+ const { isRegularTcTokenUser } = require('./messages/TcTokenStore');
2784
+ if (!isRegularTcTokenUser(jid)) return null;
2785
+
2786
+ const lidJid = this._bareLidFor(jid);
2787
+ if (!lidJid) return null;
2788
+
2789
+ return crypto.createHmac('sha256', salt).update(lidJid, 'utf8').digest();
2790
+ }
2791
+
2710
2792
  // Every address this message's session might be filed under, best first.
2711
2793
  _signalJidCandidates(from, participant, senderPn) {
2712
2794
  const out = [];
@@ -6,17 +6,17 @@
6
6
  // the app state, the device cache, the LID maps, the history — used to sit
7
7
  // side by side in one directory, named after the number:
8
8
  //
9
- // ~/.waSession/40756469325.json
10
- // ~/.waSession/40756469325.signal.json
11
- // ~/.waSession/40756469325.sk.json
9
+ // ~/.waSession/919634847671.json
10
+ // ~/.waSession/919634847671.signal.json
11
+ // ~/.waSession/919634847671.sk.json
12
12
  // ... × 11 files × every number
13
13
  //
14
14
  // Fifty numbers made that five hundred files in one place, with no way to move,
15
15
  // back up or delete one account without picking its files out of the pile by
16
16
  // prefix. A number now gets a directory of its own:
17
17
  //
18
- // <base>/40756469325/40756469325.json
19
- // <base>/40756469325/40756469325.signal.json
18
+ // <base>/919634847671/919634847671.json
19
+ // <base>/919634847671/919634847671.signal.json
20
20
  // ...
21
21
  //
22
22
  // The file names inside are unchanged, so every path built as
package/lib/WebStore.js CHANGED
@@ -63,7 +63,9 @@ function webStoreToJson(store) {
63
63
  platform: store.platform || null,
64
64
  webVersion: store.webVersion || WEB_VERSION_FALLBACK.slice(),
65
65
  browser: store.browser || WEB_BROWSER.slice(),
66
- syncFullHistory: store.syncFullHistory !== false
66
+ syncFullHistory: store.syncFullHistory !== false,
67
+ // The NCT salt (base64) a cstoken is derived from, received over app state.
68
+ nctSalt: store.nctSalt || null
67
69
  });
68
70
  }
69
71
 
@@ -87,6 +89,7 @@ function webStoreFromJson(obj) {
87
89
  store.webVersion = obj.webVersion || WEB_VERSION_FALLBACK.slice();
88
90
  store.browser = obj.browser || WEB_BROWSER.slice();
89
91
  store.syncFullHistory = obj.syncFullHistory !== false;
92
+ store.nctSalt = obj.nctSalt || null;
90
93
 
91
94
  return store;
92
95
  }
@@ -96,6 +96,8 @@ function describeIndex(index) {
96
96
  case 'setting_pushName':return { kind: 'pushName' };
97
97
  case 'clearChat': return { kind: 'clear', jid };
98
98
  case 'deleteChat': return { kind: 'delete', jid };
99
+ // Account-wide, no jid: the salt the cstoken fallback is derived from.
100
+ case 'nct_salt_sync': return { kind: 'nct_salt_sync' };
99
101
  default: return { kind, jid };
100
102
  }
101
103
  }
@@ -125,7 +125,10 @@ const ACTION_FIELDS = {
125
125
  17: 'archiveChatAction',
126
126
  20: 'markChatAsReadAction',
127
127
  21: 'clearChatAction',
128
- 22: 'deleteChatAction'
128
+ 22: 'deleteChatAction',
129
+ // NctSaltSyncAction { salt = 1 }. The salt a cstoken is derived from; the
130
+ // server distributes it into app state and a companion reads it from here.
131
+ 80: 'nctSaltSyncAction'
129
132
  };
130
133
 
131
134
  function decodeSyncActionValue(buf) {
@@ -156,6 +159,8 @@ function decodeSyncActionValue(buf) {
156
159
  out.clearChatAction = {}; break;
157
160
  case 'deleteChatAction':
158
161
  out.deleteChatAction = {}; break;
162
+ case 'nctSaltSyncAction':
163
+ out.nctSaltSyncAction = { salt: Buffer.isBuffer(a[1]) ? a[1] : null }; break;
159
164
  case 'pushNameSetting':
160
165
  out.pushNameSetting = { name: _str(a[1]) }; break;
161
166
  case 'contactAction':
@@ -853,7 +853,7 @@ class MessageSender {
853
853
  // ─── Clear all Signal sessions for a phone-number recipient ─────────────────
854
854
  //
855
855
  // Called on error 479 (stale session). Deletes:
856
- // • All Signal sessions keyed by phone-number device JIDs (e.g. 40756469325:0)
856
+ // • All Signal sessions keyed by phone-number device JIDs (e.g. 919634847671:0)
857
857
  // • The corresponding LID device session if a LID mapping is known
858
858
  //
859
859
  // Then flushes the device-ID cache for that phone so the next send re-runs
@@ -1119,11 +1119,13 @@ class MessageSender {
1119
1119
  await this._client.ensureTcTokenBeforeSend(tcJid, routingToJid);
1120
1120
  }
1121
1121
 
1122
+ let tcTokenAttached = false;
1122
1123
  if (tcStore) {
1123
1124
  const tcEntry = tcStore.get(tcJid);
1124
1125
  if (tcEntry && tcEntry.token && tcEntry.token.length) {
1125
1126
  if (!tcTokenExpired(tcEntry.timestamp)) {
1126
1127
  msgContent.push(new BinaryNode('tctoken', {}, tcEntry.token));
1128
+ tcTokenAttached = true;
1127
1129
  _whaDbg('[DBG] TCTOKEN_ATTACH jid=' + tcJid);
1128
1130
  } else {
1129
1131
  // Expired token — clear it, keep senderTimestamp for dedupe
@@ -1132,6 +1134,19 @@ class MessageSender {
1132
1134
  }
1133
1135
  }
1134
1136
  }
1137
+
1138
+ // cstoken — the fallback the genuine client attaches when it has no
1139
+ // trusted-contact token. Locally computed HMAC over the recipient's LID; a
1140
+ // companion holds the salt, a primary does not, so this is a no-op on an SMS
1141
+ // session. Never on a protocol message, and never alongside a tctoken —
1142
+ // exactly the `if tctoken … else if cstoken …` the real client does.
1143
+ if (!tcTokenAttached && !isProtocolMsg) {
1144
+ const cs = this._client._csTokenFor(routingToJid);
1145
+ if (cs && cs.length) {
1146
+ msgContent.push(new BinaryNode('cstoken', {}, cs));
1147
+ _whaDbg('[DBG] CSTOKEN_ATTACH jid=' + tcJid);
1148
+ }
1149
+ }
1135
1150
  // ─────────────────────────────────────────────────────────────────────────
1136
1151
 
1137
1152
  // Extra children a caller needs on the stanza — the <meta appdata> a
@@ -43,9 +43,9 @@ const DEBOUNCE_MS = 300;
43
43
  // One-time pre-keys do not live in the snapshot above. They get a file each,
44
44
  // named one per id:
45
45
  //
46
- // <base>/40756469325/40756469325.pre-key-1.json
46
+ // <base>/919634847671/919634847671.pre-key-1.json
47
47
  // ...
48
- // <base>/40756469325/40756469325.pre-key-812.json
48
+ // <base>/919634847671/919634847671.pre-key-812.json
49
49
  //
50
50
  // Everything else about a pre-key is small, but there are 812 of them
51
51
  // and every one that is consumed rewrites the whole store — so keeping them in
@@ -55,8 +55,8 @@ const DEBOUNCE_MS = 300;
55
55
  // authentication folders expects to see, and a write now touches one key.
56
56
  //
57
57
  // The stem in front of `.pre-key-` is the one the .signal.json file already
58
- // carries, so a number's mobile half (40756469325.pre-key-N.json) and its
59
- // companion half (40756469325.web.pre-key-N.json) keep separate pools, as they
58
+ // carries, so a number's mobile half (919634847671.pre-key-N.json) and its
59
+ // companion half (919634847671.web.pre-key-N.json) keep separate pools, as they
60
60
  // must — and a session still sitting in the old flat layout, where every
61
61
  // number shares one directory, does not collide with its neighbours either.
62
62
  //
@@ -224,8 +224,8 @@ class SignalStore {
224
224
  attachFile(filePath) {
225
225
  this._filePath = filePath;
226
226
  // The pre-key files sit beside the snapshot and share its stem, so
227
- // 40756469325.signal.json gives 40756469325.pre-key-<id>.json and
228
- // 40756469325.web.signal.json gives 40756469325.web.pre-key-<id>.json.
227
+ // 919634847671.signal.json gives 919634847671.pre-key-<id>.json and
228
+ // 919634847671.web.signal.json gives 919634847671.web.pre-key-<id>.json.
229
229
  const base = path.basename(filePath);
230
230
  this._pkDir = path.dirname(filePath);
231
231
  this._pkPrefix = base.endsWith('.signal.json')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.24.1",
3
+ "version": "5.25.0",
4
4
  "description": "Node.js library for WhatsApp — register a number over SMS, or link as a companion by QR. Signal E2E encryption, media, groups, channels.",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",