whalibmob 5.24.2 → 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
@@ -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/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/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 = [];
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':
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.24.2",
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",