whalibmob 5.19.4 → 5.21.1

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
@@ -28,7 +28,7 @@ If you want News about whalibmob enter this whalibmob channel: https://t.me/+sHN
28
28
 
29
29
  - whalibmob does not require a browser, Selenium, or any other external runtime — it communicates directly with WhatsApp using a **TCP socket** and the **Noise Protocol** handshake.
30
30
  - The library operates as a real **iOS mobile device**, using the Mobile API endpoint, which behaves differently from the Web API.
31
- - It **also speaks WhatsApp Web over a WebSocket**. When a number cannot receive an SMS, or is already in use on a phone, whalibmob can link itself to that existing account with an **8-character pairing code** and run as one of its linked devices — with full message history and the account's address book. See [Linking to an Existing Account](#linking-to-an-existing-account-pairing-code). The API is identical in both modes.
31
+ - It **also speaks WhatsApp Web over a WebSocket**. When a number cannot receive an SMS, or is already in use on a phone, whalibmob can link itself to that existing account with an **8-character pairing code** and run as one of its linked devices — with full message history and the account's address book. See [Linking to an Existing Account](#linking-to-an-existing-account-pairing-code-or-qr). The API is identical in both modes.
32
32
  - Signal Protocol encryption is **fully inlined** in pure JavaScript — no native binaries, no node-gyp, runs anywhere Node.js runs.
33
33
 
34
34
  ## Install
@@ -130,8 +130,10 @@ npm install -g whalibmob
130
130
  - [Device Attestation with Frida (optional)](#device-attestation-with-frida-optional)
131
131
  - [Connect](#connect)
132
132
  - [Client Options](#client-options)
133
- - [Linking to an Existing Account (Pairing Code)](#linking-to-an-existing-account-pairing-code)
133
+ - [Linking to an Existing Account (Pairing Code or QR)](#linking-to-an-existing-account-pairing-code-or-qr)
134
134
  - [Requesting a Pairing Code](#requesting-a-pairing-code)
135
+ - [Linking by QR Code](#linking-by-qr-code)
136
+ - [After the Link — Same for Both Paths](#after-the-link--same-for-both-paths)
135
137
  - [Reconnecting a Linked Session](#reconnecting-a-linked-session)
136
138
  - [Choosing Your Own Code](#choosing-your-own-code)
137
139
  - [Options](#options)
@@ -153,13 +155,20 @@ npm install -g whalibmob
153
155
  - [The Number WhatsApp Files Your Account Under](#the-number-whatsapp-files-your-account-under)
154
156
  - [When Registration Is Refused for Consent](#when-registration-is-refused-for-consent)
155
157
  - [The Push Token](#the-push-token)
158
+ - [Receiving the Code over Push](#receiving-the-code-over-push-without-typing-it)
156
159
  - [Routing Traffic Through a Proxy](#routing-traffic-through-a-proxy)
160
+ - [What Goes Through It](#what-goes-through-it)
157
161
  - [Saving & Restoring Sessions](#saving--restoring-sessions)
162
+ - [Keeping the Announced Version Current](#keeping-the-announced-version-current)
163
+ - [One-time Pre-keys](#one-time-pre-keys)
164
+ - [Where the Folder Comes From](#where-the-folder-comes-from)
165
+ - [Working Out the Paths Yourself](#working-out-the-paths-yourself)
158
166
  - [Signal Store Utilities](#signal-store-utilities)
159
167
  - [makeCacheableSignalKeyStore](#makecacheablesignalkeystore)
160
168
  - [addTransactionCapability](#addtransactioncapability)
161
169
  - [assertMeId](#assertmeid)
162
170
  - [initAuthCreds](#initauthcreds)
171
+ - [Recommended Stacking Pattern](#recommended-stacking-pattern)
163
172
  - [Handling Events](#handling-events)
164
173
  - [Example to Start](#example-to-start)
165
174
  - [All Events](#all-events)
@@ -248,11 +257,13 @@ npm install -g whalibmob
248
257
  - [Query Metadata](#query-metadata)
249
258
  - [Get Request Join List](#get-request-join-list)
250
259
  - [Approve / Reject Request Join](#approve--reject-request-join)
251
- - [Personal Invitations](#personal-invitations)
260
+ - [Personal Invitations](#personal-invitations-1)
252
261
  - [Toggle Ephemeral in Group](#toggle-ephemeral-in-group)
253
262
  - [WhatsApp IDs](#whatsapp-ids)
254
263
  - [Transport](#transport)
255
264
  - [Media Encryption](#media-encryption)
265
+ - [Sending (Upload Flow)](#sending-upload-flow)
266
+ - [Receiving (Download + Decrypt Flow)](#receiving-download--decrypt-flow)
256
267
  - [Device Emulation](#device-emulation)
257
268
  - [Quick Start](#device-quick-start)
258
269
  - [iOS Profiles](#ios-profiles)
@@ -261,6 +272,7 @@ npm install -g whalibmob
261
272
  - [Version & Token Overrides](#version--token-overrides)
262
273
  - [When the server answers 405 on connect](#when-the-server-answers-405-on-connect)
263
274
  - [Finding out what a 405 objects to](#finding-out-what-a-405-objects-to)
275
+ - [License](#license)
264
276
 
265
277
  ---
266
278
 
@@ -1477,6 +1489,9 @@ wa> /connect 919634847671 pair
1477
1489
  # link to an existing account by 8-digit pairing code
1478
1490
  wa> /pair 919634847671
1479
1491
 
1492
+ # or link by scanning a QR drawn in the terminal
1493
+ wa> /qrcode 919634847671
1494
+
1480
1495
  # with a code you chose yourself (exactly 8 characters)
1481
1496
  wa> /pair 919634847671 MYCODE12
1482
1497
 
@@ -1602,6 +1617,7 @@ wa> /quit
1602
1617
  | **Connection** | |
1603
1618
  | `/connect <phone> [sms\|pair]` | Connect to WhatsApp — picks the method from the session files when unset |
1604
1619
  | `/pair <phone> [code]` | Link to an existing account by 8-digit pairing code |
1620
+ | `/qrcode <phone>` | Link to an existing account by scanning a QR drawn in the terminal |
1605
1621
  | `/disconnect` | Disconnect current session |
1606
1622
  | `/reconnect` | Force reconnection |
1607
1623
  | `/session` | Show session info |
@@ -1612,6 +1628,10 @@ wa> /quit
1612
1628
 
1613
1629
  ## Library API
1614
1630
 
1631
+ Everything the CLI does is available as a Node.js library. The sections below
1632
+ cover connecting an account, sending and receiving every message type, groups,
1633
+ communities, channels, presence, privacy, history sync, and device emulation.
1634
+
1615
1635
  ## Connecting Account
1616
1636
 
1617
1637
  ### Register a New Number
@@ -1668,6 +1688,27 @@ if (result.status === 'ok') {
1668
1688
  }
1669
1689
  ```
1670
1690
 
1691
+ **Optional — let the code arrive by itself.** The two steps above are the whole flow, and nothing about them changes if you do nothing else. But because registration now sends a Firebase push token (see [The Push Token](#the-push-token)), WhatsApp *may* also deliver the six-digit code as a silent push. Open a listener for it before requesting the code, and the code can come back with nothing typed:
1692
+
1693
+ ```js
1694
+ const { receivePushCode } = require('whalibmob')
1695
+
1696
+ // open the listener FIRST, so the push has somewhere to land
1697
+ const codePromise = receivePushCode(store, store.device, { timeoutMs: 180000 })
1698
+
1699
+ await requestSmsCode(store, 'sms') // any method; the push is a copy of the code
1700
+ const code = await codePromise // the six digits, or null if no push came
1701
+
1702
+ if (code) {
1703
+ const result = await verifyCode(store, code)
1704
+ if (result.status === 'ok') saveStore(result.store, sessFile)
1705
+ } else {
1706
+ // no push this time — read the code the ordinary way and call verifyCode(store, code)
1707
+ }
1708
+ ```
1709
+
1710
+ This is purely additive: `receivePushCode` resolves `null` on a timeout or any failure, so the plain `requestSmsCode` / `verifyCode` path always stands behind it. Whether WhatsApp sends the silent push is the server's decision — see [Receiving the code over push](#receiving-the-code-over-push-without-typing-it) for what governs it. From the CLI the same thing is one command, `/reg push <phone>`.
1711
+
1671
1712
  **When the code is not the end of it.** The server can answer a submitted code with a CAPTCHA, or with a demand for the account's two-step verification PIN. Neither is something the library can work out on its own, so both come back to you through optional handlers:
1672
1713
 
1673
1714
  ```js
@@ -2032,11 +2073,16 @@ The defaults cover an ordinary sender. Raise `sentCacheSize` if you push message
2032
2073
 
2033
2074
  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.
2034
2075
 
2035
- ## Linking to an Existing Account (Pairing Code)
2076
+ ## Linking to an Existing Account (Pairing Code or QR)
2036
2077
 
2037
2078
  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.
2038
2079
 
2039
- You get an 8-character pairing code, the account owner types it into their phone, and from then on whalibmob is one of the account's linked devices. The whole library works the same afterwards — same client, same methods, same events.
2080
+ There are two ways to link, and they reach the same place:
2081
+
2082
+ - **Pairing code** — whalibmob gives you an 8-character code, the owner types it into their phone.
2083
+ - **QR code** — whalibmob draws a QR, the owner scans it with the phone's camera.
2084
+
2085
+ Both link the number as a companion device, both leave the whole library working the same afterwards — same client, same methods, same events. The only difference is what the owner does: type a code, or scan a square.
2040
2086
 
2041
2087
  > [!IMPORTANT]
2042
2088
  > The two modes are independent. SMS registration is unchanged and still the default; nothing about it is affected by linking. A single number can even have both a registered session and a linked session — they are stored in separate files and never share state.
@@ -2075,7 +2121,45 @@ On the phone that owns the number:
2075
2121
 
2076
2122
  **WhatsApp → Settings → Linked Devices → Link a device → Link with phone number instead**, then type the code.
2077
2123
 
2078
- A few seconds after the code is accepted you will see `paired`, the server restarts the stream, and `connected` fires on the new connection. From that point on everything else in this document applies unchanged:
2124
+ ### Linking by QR Code
2125
+
2126
+ The QR path is the scan-to-connect alternative. You **do not** request a pairing code — you just connect and listen for the `qr` event. Because no code is asked for, the server volunteers a QR instead, and whalibmob turns it into a ready-to-render string:
2127
+
2128
+ ```js
2129
+ const { WhalibmobClient } = require('whalibmob')
2130
+ const qrcode = require('qrcode-terminal') // npm install qrcode-terminal
2131
+ const path = require('path')
2132
+
2133
+ const client = new WhalibmobClient({
2134
+ sessionDir: path.join(process.env.HOME, '.waSession')
2135
+ })
2136
+
2137
+ // a fresh QR string arrives here, and again each time the previous one expires
2138
+ client.on('qr', ({ qr, remaining }) => {
2139
+ qrcode.generate(qr, { small: true }) // draw it in the terminal
2140
+ console.log('scan it — refreshes on its own,', remaining, 'left before it expires')
2141
+ })
2142
+
2143
+ client.on('qr_timeout', () => console.log('QR set expired — reconnect for a fresh one'))
2144
+
2145
+ client.on('paired', (p) => console.log('scanned — linked as', p.jid))
2146
+ client.on('connected', () => console.log('ready'))
2147
+
2148
+ // connect as a companion — and DO NOT request a pairing code
2149
+ await client.connectWeb('919634847671', { syncFullHistory: true })
2150
+ ```
2151
+
2152
+ On the phone that owns the number:
2153
+
2154
+ **WhatsApp → Settings → Linked Devices → Link a device**, then point the camera at the QR.
2155
+
2156
+ The `qr` string is what a WhatsApp camera reads: `ref,noise,identity,advSecret,platformId` — a one-time ref plus this client's public keys, comma-joined, the same fields WhatsApp Web itself renders. It rotates on its own (about a minute for the first, then shorter) until the account is scanned or the refs run out. `qrcode-terminal` is optional — without it you get the raw string on `qr` and can render it however you like (a PNG, a web page, an image in a chat). To emit the QR as a `wa.me/settings/linked_devices#…` link instead of the bare fields, pass `{ qrWrapUrl: true }` to `connectWeb`.
2157
+
2158
+ Everything after the scan is identical to the pairing-code path: `paired`, a stream restart, then `connected`.
2159
+
2160
+ ### After the link — same for both paths
2161
+
2162
+ A few seconds after the code is accepted (or the QR is scanned) you will see `paired`, the server restarts the stream, and `connected` fires on the new connection. From that point on everything else in this document applies unchanged:
2079
2163
 
2080
2164
  ```js
2081
2165
  await client.sendText('919876543210@s.whatsapp.net', 'sent from a linked device')
@@ -2119,9 +2203,11 @@ await client.connectWeb(phone, {
2119
2203
  | Event | Fires when |
2120
2204
  |---|---|
2121
2205
  | `pairing_code` | a code has been requested — `{ code, phoneNumber }` |
2122
- | `paired` | the owner accepted the code — `{ jid, lid, deviceIndex, platform }` |
2206
+ | `qr` | a QR is ready to render — `{ qr, ref, ttlMs, remaining }`; fires again on each rotation |
2207
+ | `qr_timeout` | the QR refs ran out — reconnect for a fresh set |
2208
+ | `paired` | the owner accepted the code or scanned the QR — `{ jid, lid, deviceIndex, platform }` |
2123
2209
  | `restart_required` | the server is restarting the stream after pairing (normal; the reconnect is automatic) |
2124
- | `pair_device` | the QR path produced reference strings — `{ refs }` |
2210
+ | `pair_device` | the raw QR reference strings, before they are turned into `qr` — `{ refs }` |
2125
2211
  | `history_sync` | a chunk of history arrived from the phone |
2126
2212
 
2127
2213
  ### Getting the History and the Address Book
@@ -2491,9 +2577,21 @@ If that is refused too, the number has to go through the real app once, on a pho
2491
2577
 
2492
2578
  ## The Push Token
2493
2579
 
2494
- Every WhatsApp on a real phone holds a Firebase push token. It is the address Google uses to wake the app when a message arrives, and no install exists without one — so a registration that ships no `push_token` describes a WhatsApp that cannot be notified.
2580
+ Every WhatsApp on a real phone holds a Firebase push token. It is the address Google uses to wake the app, and no install exists without one — so a registration that ships no `push_token` describes a WhatsApp that cannot be notified, which is a device that does not exist.
2581
+
2582
+ The token does two distinct jobs, and it is easy to conflate them:
2495
2583
 
2496
- Registration fetches a real one and sends it, in three plain HTTPS calls to Google:
2584
+ 1. **It makes the registration look real — always.** Every `/code` request now carries the token, whichever delivery method you ask for. The server sees an install that can be reached, not a headless client. This is the reason the token matters, and it applies to `sms`, `voice`, `wa_old` — all of them.
2585
+ 2. **It can carry the code itself — sometimes.** Because you handed WhatsApp a direct line, WhatsApp *may* also push the six-digit code silently down it, so the app fills the code in on its own. This is why the code auto-completes on a real phone before you have read the SMS. It happens *alongside* the method you chose, not instead of it.
2586
+
2587
+ The delivery method and the push are not alternatives. You still choose how a human receives the code (`sms`, `voice`, `wa_old`); the push, when it comes, is a second silent copy of that same code sent straight to the app.
2588
+
2589
+ ```
2590
+ request a code ─┬─ method you chose → reaches a human (SMS, a call, the existing WhatsApp)
2591
+ └─ push_token line → reaches the app (silent, auto-filled — if WhatsApp sends it)
2592
+ ```
2593
+
2594
+ Registration fetches a real token and sends it, in three plain HTTPS calls to Google:
2497
2595
 
2498
2596
  | Step | Endpoint | Yields |
2499
2597
  |---|---|---|
@@ -2509,6 +2607,40 @@ It runs once per number. The Firebase identity is cached on the session, because
2509
2607
 
2510
2608
  Turn it off with `WA_FCM_PUSH=0`.
2511
2609
 
2610
+ ### Receiving the code over push, without typing it
2611
+
2612
+ This is the receiving end of job 2 above. When WhatsApp sends the code as a silent push, something has to be listening on the Firebase line to catch it — the same long-lived connection every Android phone keeps open to Google. `receivePushCode(store, device)` opens it: a TLS stream to `mtalk.google.com:5228` speaking the MCS protocol, logged in with the Firebase identity, resolving with the code the moment a push carrying it arrives.
2613
+
2614
+ The order matters. Open the listener **first**, so the line is live before the code is requested; then request the code by whatever method; then await it.
2615
+
2616
+ ```js
2617
+ const { receivePushCode, requestSmsCode, verifyCode } = require('whalibmob')
2618
+
2619
+ // 1. open the listener first, so the push has somewhere to land
2620
+ const codePromise = receivePushCode(store, store.device, { timeoutMs: 180000 })
2621
+
2622
+ // 2. request the code — any method. The push, if it comes, is a copy of it.
2623
+ await requestSmsCode(store, 'sms') // or 'voice', 'wa_old', …
2624
+
2625
+ // 3. if the push arrives, the code is here with nothing typed
2626
+ const code = await codePromise
2627
+ if (code) await verifyCode(store, code)
2628
+ else { /* no push — read the code from SMS / the existing WhatsApp, then verifyCode */ }
2629
+ ```
2630
+
2631
+ From the CLI the whole sequence is one command:
2632
+
2633
+ ```
2634
+ /reg push <phone> [sms|voice]
2635
+ ```
2636
+
2637
+ It opens the listener, waits until it is logged in, requests the code, and confirms automatically if the push arrives — falling back to `/reg confirm <phone> <code>` when it does not.
2638
+
2639
+ The connection carries a heartbeat and remembers the message ids it has seen, so a reconnect does not re-read a delivered code, the way the native client does. It resolves `null` on timeout, a refused login, or any failure — at which point you simply read the code the ordinary way and verify it. Like the token, it routes through the configured SOCKS proxy.
2640
+
2641
+ > [!IMPORTANT]
2642
+ > Receiving the push is not the same as making WhatsApp send it. Whether WhatsApp pushes the code for a given request is the server's decision, and on a client shipping empty attestation it will often send the code only by the method you asked for (SMS, `wa_old`, a call) and no silent push. This listener catches the push correctly **when one is sent**; it cannot force that channel, and it never replaces the chosen method — it runs beside it. With a valid Play Integrity attestation in the request (`WA_FRIDA_HOST`), the server is more likely to include the silent push.
2643
+
2512
2644
  ## Routing Traffic Through a Proxy
2513
2645
 
2514
2646
  Two reasons to want this. Registration is the part of the protocol most likely to be refused from a datacenter IP, so a residential exit helps with security blocks and undeterminable number statuses. And on a network that cannot reach WhatsApp at all, nothing works without one.
package/cli.js CHANGED
@@ -663,11 +663,13 @@ const HELP = `
663
663
  /reg check <phone> check if number has WhatsApp
664
664
  /reg code <phone> [sms|voice|wa_old] request verification code
665
665
  /reg code <phone> email <address> request code via email
666
+ /reg push <phone> [sms|voice] request code and receive it over Firebase push
666
667
  /reg confirm <phone> <code> complete registration
667
668
 
668
669
  Connection
669
670
  /connect <phone> [sms|pair] connect to WhatsApp (asks which if unset)
670
671
  /pair <phone> [code] link to an existing account by 8-digit code
672
+ /qrcode <phone> link to an existing account by scanning a QR
671
673
  /fixnumber re-file a session under the number WhatsApp uses
672
674
  /disconnect disconnect
673
675
  /reconnect force reconnection
@@ -1091,6 +1093,75 @@ async function doConnectWeb(phone, opts) {
1091
1093
  }
1092
1094
  }
1093
1095
 
1096
+ // Link by QR instead of a pairing code. Same companion connection, but nothing
1097
+ // asks for a code — the server volunteers a pair-device, the client turns each
1098
+ // ref into a QR, and this draws it in the terminal for the phone to scan.
1099
+ async function doConnectWebQr(phone) {
1100
+ phone = normalizePhone(phone);
1101
+ const client = new WhalibmobClient({ sessionDir: _sessDir });
1102
+ attachEvents(client);
1103
+
1104
+ let renderQr = null;
1105
+ try {
1106
+ const qrcode = require('qrcode-terminal');
1107
+ renderQr = (text) => qrcode.generate(text, { small: true }, (art) => out('\n' + art));
1108
+ } catch (_) {
1109
+ out(' (qrcode-terminal is not installed — printing the raw QR string instead)');
1110
+ out(' install it for a scannable image: npm install qrcode-terminal');
1111
+ renderQr = (text) => { out(''); out(text); out(''); };
1112
+ }
1113
+
1114
+ client.on('qr', ({ qr, remaining }) => {
1115
+ out('');
1116
+ hr();
1117
+ out(' scan this from the phone that owns +' + phone + ':');
1118
+ out(' WhatsApp → Settings → Linked Devices → Link a device');
1119
+ hr();
1120
+ renderQr(qr);
1121
+ out(' the code refreshes on its own' +
1122
+ (remaining ? ' (' + remaining + ' more before it expires)' : '') + '; waiting...');
1123
+ });
1124
+
1125
+ client.on('qr_timeout', () => {
1126
+ out(' the QR set expired — run /qrcode ' + phone + ' again for a fresh one');
1127
+ });
1128
+
1129
+ client.once('paired', (pp) => {
1130
+ out('');
1131
+ out(' scanned — linked as ' + pp.jid + (pp.lid ? ' (' + pp.lid + ')' : ''));
1132
+ out(' device slot ' + pp.deviceIndex + (pp.platform ? ' · primary is ' + pp.platform : ''));
1133
+ out(' finishing handshake...');
1134
+ });
1135
+
1136
+ client.on('history_sync', (r) => {
1137
+ out(' history ' + r.syncTypeName +
1138
+ ' chats=' + r.chats.length + ' contacts=' + r.contacts.length);
1139
+ });
1140
+
1141
+ client.once('connected', () => {
1142
+ _client = client;
1143
+ _phone = phone;
1144
+ out('connected as +' + phone + ' (web / companion)');
1145
+ _rl.setPrompt('wa +' + phone + '> ');
1146
+ _rl.prompt();
1147
+ });
1148
+
1149
+ const alreadyLinked = hasWebSession(phone);
1150
+ try {
1151
+ // connectWeb WITHOUT requestPairingCode — that is what makes the server
1152
+ // offer the QR pair-device instead of waiting on a code.
1153
+ await client.connectWeb(phone, { syncFullHistory: true });
1154
+ if (alreadyLinked) out('session already linked — reconnecting');
1155
+
1156
+ const keepAlive = setInterval(() => {}, 10000);
1157
+ client.once('connected', () => clearInterval(keepAlive));
1158
+ client.once('auth_failure', () => clearInterval(keepAlive));
1159
+ } catch (e) {
1160
+ fail(e.message);
1161
+ notConnected();
1162
+ }
1163
+ }
1164
+
1094
1165
  async function doConnect(phone) {
1095
1166
  phone = normalizePhone(phone);
1096
1167
 
@@ -1323,6 +1394,23 @@ async function handleLine(line) {
1323
1394
  break;
1324
1395
  }
1325
1396
 
1397
+ case '/qrcode':
1398
+ case '/qr': {
1399
+ const ph = p[1] || _phone;
1400
+ if (!ph) {
1401
+ fail('usage: /qrcode <phone>');
1402
+ out(' links the number as a companion by QR instead of a pairing code');
1403
+ out(' a QR is drawn in the terminal — scan it from the phone that owns');
1404
+ out(' the number: WhatsApp → Linked Devices → Link a device');
1405
+ out(' example: /qrcode 40756469325');
1406
+ break;
1407
+ }
1408
+ if (_client && _client.connected) { fail('already connected — /disconnect first'); break; }
1409
+ out('connecting...');
1410
+ await doConnectWebQr(ph);
1411
+ break;
1412
+ }
1413
+
1326
1414
  case '/disconnect':
1327
1415
  if (_client) { _client.disconnect(); _client = null; _phone = null; }
1328
1416
  _rl.setPrompt('wa> ');
@@ -2329,8 +2417,76 @@ async function handleLine(line) {
2329
2417
  fail('verification failed ' + JSON.stringify(r));
2330
2418
  }
2331
2419
  }
2420
+ else if (sub === 'push') {
2421
+ // Full push flow: open the MCS listener, request the code, and wait
2422
+ // for it to arrive over Firebase instead of by SMS. Falls back to the
2423
+ // ordinary code path automatically when no push comes.
2424
+ const ph = normalizePhone(p[2]);
2425
+ if (!ph) {
2426
+ fail('usage: /reg push <phone> [sms|voice] [--name "Your Name"]');
2427
+ out(' opens the Firebase push listener, requests a code, and waits for');
2428
+ out(' it to arrive over push. If it does, registration is confirmed');
2429
+ out(' automatically. If no push comes, request the code normally with');
2430
+ out(' /reg code and confirm it with /reg confirm.');
2431
+ break;
2432
+ }
2433
+ const method = (p[3] && !p[3].startsWith('--')) ? p[3] : 'sms';
2434
+ const { receivePushCode } = require('./lib/fcm');
2435
+
2436
+ sessionDirFor(_sessDir, ph, { create: true });
2437
+ const sessFile = storeFileFor(_sessDir, ph);
2438
+ let store = loadStore(sessFile);
2439
+ if (!store) { store = initAuthCreds(ph, { name: regName }); saveStore(store, sessFile); }
2440
+ if (!store.device) store.device = getDeviceConfig();
2441
+
2442
+ out('opening Firebase push listener (this can take a moment)...');
2443
+ // Open the listener first so the push has somewhere to land. onReady
2444
+ // fires once MCS is logged in — only then is it safe to ask for the
2445
+ // code.
2446
+ let ready = false;
2447
+ const codePromise = receivePushCode(store, store.device, {
2448
+ timeoutMs: 180000,
2449
+ onReady: () => { ready = true; out(' push listener ready — requesting code'); }
2450
+ });
2451
+
2452
+ // Give the listener a few seconds to log in before requesting. If it
2453
+ // has not, request anyway — SMS still works, and the push may yet come.
2454
+ const waitReady = async () => {
2455
+ for (let i = 0; i < 40 && !ready; i++) await new Promise(r => setTimeout(r, 250));
2456
+ };
2457
+ await waitReady();
2458
+ if (!ready) out(' listener not ready yet — requesting code anyway (SMS fallback stands)');
2459
+
2460
+ out('requesting ' + method + ' code for +' + ph + '...');
2461
+ const r = await requestSmsCode(store, method, { onProgress: out, name: regName });
2462
+ store.codePending = true;
2463
+ saveStore(store, sessFile);
2464
+ out(' status ' + (r && r.status));
2465
+
2466
+ out('waiting for the code over push (up to 3 min; Ctrl-C to stop and use /reg confirm)...');
2467
+ const code = await codePromise;
2468
+ if (!code) {
2469
+ out('no code arrived over push — WhatsApp likely sent it by SMS.');
2470
+ out(' read the SMS and run: /reg confirm ' + ph + ' <code>');
2471
+ saveStore(store, sessFile);
2472
+ break;
2473
+ }
2474
+ out('code received over push: ' + code + ' — confirming...');
2475
+ const v = await verifyCode(store, code,
2476
+ Object.assign(registrationPrompts(), { onProgress: out, name: regName }));
2477
+ if (v && (v.status === 'ok' || v.status === 'sent' || v.status === 'verified')) {
2478
+ const finalStore = v.store || store;
2479
+ finalStore.registered = true; finalStore.codePending = false;
2480
+ const savedPhone = String(finalStore.phoneNumber || ph);
2481
+ saveStore(finalStore, storeFileFor(_sessDir, savedPhone));
2482
+ out('registered via push session saved');
2483
+ out('now run: /connect ' + savedPhone);
2484
+ } else {
2485
+ fail('verification failed ' + JSON.stringify(v));
2486
+ }
2487
+ }
2332
2488
  else {
2333
- fail('usage: /reg check|code|confirm ...');
2489
+ fail('usage: /reg check|code|push|confirm ...');
2334
2490
  }
2335
2491
  break;
2336
2492
  }
package/index.js CHANGED
@@ -61,6 +61,10 @@ module.exports = {
61
61
  checkIfRegistered,
62
62
  requestSmsCode,
63
63
  verifyCode,
64
+ // Receive the verification code as a silent Firebase push, without an SMS —
65
+ // opens the MCS listener the native client keeps to Google. See "Receiving
66
+ // the code over push" in the README.
67
+ receivePushCode: (store, device, opts) => require('./lib/fcm').receivePushCode(store, device, opts),
64
68
  assertRegistrationKeys,
65
69
  // Version fetch — use fetchWaVersion for device-aware (iOS or Android) fetching.
66
70
  // fetchIosVersion is kept for backward compatibility.
package/lib/Client.js CHANGED
@@ -576,8 +576,18 @@ class WhalibmobClient extends EventEmitter {
576
576
  try {
577
577
  const { compareVersions } = require('./Registration');
578
578
  const before = this._store.version;
579
+ // On Android, ask the way registration does — fetchWaVersion(), which
580
+ // reads the version straight off the APK material when there is any. That
581
+ // material is the exact build the token was computed from and the only
582
+ // Android source that is actually reliable: the Play Store listing no
583
+ // longer exposes a version to scrape, so fetchAndroidVersion() on its own
584
+ // usually just hands back the pinned fallback and a session can never
585
+ // climb past it. With the material preferred, refreshing it (e.g.
586
+ // `wa apk-material --download`) is picked up on the next connect and
587
+ // written through to <number>.json. fetchWaVersion falls through to the
588
+ // Play Store lookup, and then the fallback, only when no material exists.
579
589
  const live = android
580
- ? await fetchAndroidVersion(business)
590
+ ? await fetchWaVersion(device)
581
591
  : await fetchIosVersion(business);
582
592
  if (!live) return;
583
593
 
@@ -840,6 +850,10 @@ class WhalibmobClient extends EventEmitter {
840
850
 
841
851
  if (opts.syncFullHistory !== undefined) this._store.syncFullHistory = !!opts.syncFullHistory;
842
852
  if (opts.browser) this._store.browser = opts.browser;
853
+ // Emit the QR as a wa.me/settings/linked_devices# link rather than the bare
854
+ // comma-joined fields. Off by default — the bare form is what WhatsApp Web
855
+ // itself renders and the most widely scannable.
856
+ if (opts.qrWrapUrl) this._qrWrapUrl = true;
843
857
 
844
858
  // Announce the revision the web client is actually on.
845
859
  //
@@ -1177,6 +1191,8 @@ class WhalibmobClient extends EventEmitter {
1177
1191
  * that is when history starts arriving.
1178
1192
  */
1179
1193
  _handlePairSuccess(node) {
1194
+ // The phone accepted a scan (or a code) — no more QRs to show.
1195
+ this._stopQrRotation();
1180
1196
  const { configureSuccessfulPairing } = require('./CompanionPairing');
1181
1197
  try {
1182
1198
  const result = configureSuccessfulPairing(node, {
@@ -1222,9 +1238,15 @@ class WhalibmobClient extends EventEmitter {
1222
1238
  /**
1223
1239
  * <iq><pair-device> — the QR path.
1224
1240
  *
1225
- * We do not display QR codes, but the node still has to be acknowledged or
1226
- * the server keeps re-sending refs. The refs are surfaced as an event so a
1227
- * caller that wants to render a QR can.
1241
+ * The server volunteers this to an unpaired companion that has not asked for
1242
+ * a pairing code. It carries a list of one-time refs; each pairs with our
1243
+ * keys to form one QR string. The node has to be acknowledged or the server
1244
+ * keeps resending it.
1245
+ *
1246
+ * The refs are surfaced raw on `pair_device` for anything that wants them,
1247
+ * and turned into ready-to-render QR strings on `qr` — one now, then a fresh
1248
+ * one each time the current ref expires, until the list runs out. Scanning
1249
+ * any of them links the device.
1228
1250
  */
1229
1251
  _handlePairDevice(node) {
1230
1252
  const id = node.attrs && node.attrs.id;
@@ -1240,6 +1262,65 @@ class WhalibmobClient extends EventEmitter {
1240
1262
  .map(c => (Buffer.isBuffer(c.content) ? c.content.toString('utf8') : String(c.content)))
1241
1263
  : [];
1242
1264
  this.emit('pair_device', { refs });
1265
+
1266
+ this._startQrRotation(refs);
1267
+ }
1268
+
1269
+ // Turn the server's refs into QR strings and emit them one at a time.
1270
+ //
1271
+ // A ref is single-use and short-lived, so the first is shown at once and each
1272
+ // later one only when the current has aged out — mirroring the phone-facing
1273
+ // client, which shows a QR for ~60s and then rotates. When the refs run out
1274
+ // the account has to reconnect for a fresh set, which is surfaced as
1275
+ // `qr_timeout` rather than left hanging.
1276
+ _startQrRotation(refs) {
1277
+ this._stopQrRotation();
1278
+ if (!Array.isArray(refs) || refs.length === 0) return;
1279
+
1280
+ const { buildQrData } = require('./QrPairing');
1281
+ const crypto = require('crypto');
1282
+
1283
+ // The adv secret rides inside the QR and is what the phone signs the
1284
+ // account proof with; pair-success verifies against it. The pairing-code
1285
+ // path derives its own, but the QR path has none until here, so mint a
1286
+ // random 32 bytes once and keep it for the whole attempt.
1287
+ if (!this._store.advSecretKey) {
1288
+ this._store.advSecretKey = crypto.randomBytes(32).toString('base64');
1289
+ this._saveWebStore();
1290
+ }
1291
+
1292
+ const { WEB_BROWSER } = require('./constants');
1293
+ const noiseB64 = stripKeyPrefix(this._store.noiseKeyPair.public).toString('base64');
1294
+ const identityB64 = stripKeyPrefix(this._store.identityKeyPair.public).toString('base64');
1295
+ const advB64 = this._store.advSecretKey;
1296
+ const browser = this._store.browser || WEB_BROWSER;
1297
+ const wrap = !!this._qrWrapUrl;
1298
+
1299
+ const queue = refs.slice();
1300
+ let first = true;
1301
+
1302
+ const showNext = () => {
1303
+ const ref = queue.shift();
1304
+ if (!ref) {
1305
+ _whaDbg('[DBG] QR refs exhausted — reconnect for a fresh set');
1306
+ this._stopQrRotation();
1307
+ this.emit('qr_timeout');
1308
+ return;
1309
+ }
1310
+ const qr = buildQrData(ref, noiseB64, identityB64, advB64, browser, { wrap });
1311
+ // 60s for the first, 20s for each after — the reference client's cadence.
1312
+ const ttlMs = first ? 60000 : 20000;
1313
+ first = false;
1314
+ _whaDbg('[DBG] QR emitted (ref …' + ref.slice(-6) + ', ' + queue.length + ' left)');
1315
+ this.emit('qr', { qr, ref, ttlMs, remaining: queue.length });
1316
+ this._qrTimer = setTimeout(showNext, ttlMs);
1317
+ };
1318
+
1319
+ showNext();
1320
+ }
1321
+
1322
+ _stopQrRotation() {
1323
+ if (this._qrTimer) { clearTimeout(this._qrTimer); this._qrTimer = null; }
1243
1324
  }
1244
1325
 
1245
1326
  /**
@@ -1325,6 +1406,7 @@ class WhalibmobClient extends EventEmitter {
1325
1406
  if (!(opts && opts.reconnect)) this._closing = true;
1326
1407
  this._reconnecting = false;
1327
1408
  this._reconnectTry = 0;
1409
+ this._stopQrRotation();
1328
1410
  this._stopTimers();
1329
1411
  if (this._socket) {
1330
1412
  try { this._socket.close(); } catch (_) {}
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ // QR-code companion login, the scan-to-connect alternative to the pairing code.
4
+ //
5
+ // Both paths link the same way — as a companion device on the web transport —
6
+ // and diverge only in how the primary phone is told to trust this client:
7
+ //
8
+ // pairing code the client asks for an 8-character code, the user types it
9
+ // into WhatsApp → Linked devices → Link with phone number
10
+ // QR code the server volunteers a set of refs, the client renders one
11
+ // as a QR, the user points the phone's camera at it
12
+ //
13
+ // The QR is a short string the WhatsApp camera reads. It carries everything the
14
+ // phone needs to authorise this device: a one-time ref, this client's public
15
+ // noise and identity keys, and the adv secret it will sign the account proof
16
+ // with. The phone does the linking; the client only has to present the string.
17
+ //
18
+ // The wire format is the one the native web client and every maintained web
19
+ // library produce:
20
+ //
21
+ // ref,<noise pub b64>,<identity pub b64>,<adv secret b64>,<platform id>
22
+ //
23
+ // The platform id is the companion "web client type": Chrome is 1, Edge 2,
24
+ // Firefox 3, and so on. WhatsApp Web itself renders exactly these comma-joined
25
+ // fields, so that is the default; some clients wrap them in a
26
+ // wa.me/settings/linked_devices# link, which is offered as an option.
27
+
28
+ // Companion web-client type, keyed by browser name. Matches the ids the phone
29
+ // expects; anything unknown is "other web client".
30
+ const COMPANION_WEB_CLIENT = {
31
+ CHROME: 1, EDGE: 2, FIREFOX: 3, IE: 4, OPERA: 5, SAFARI: 6,
32
+ ELECTRON: 7, UWP: 8, OTHER: 9
33
+ };
34
+
35
+ // A [os, browserName] description → the numeric platform id in the QR.
36
+ function companionPlatformId(browser) {
37
+ const os = browser && browser[0];
38
+ const name = String((browser && browser[1]) || 'Chrome');
39
+ if (name === 'Desktop') return os === 'Windows' ? COMPANION_WEB_CLIENT.UWP : COMPANION_WEB_CLIENT.ELECTRON;
40
+ const key = name.toUpperCase();
41
+ return COMPANION_WEB_CLIENT[key] !== undefined ? COMPANION_WEB_CLIENT[key] : COMPANION_WEB_CLIENT.OTHER;
42
+ }
43
+
44
+ /**
45
+ * Build the string a WhatsApp camera scans to link this device.
46
+ *
47
+ * @param {string} ref one ref from the server's pair-device node
48
+ * @param {string} noisePubB64 base64 of the 32-byte noise public key
49
+ * @param {string} identityPubB64 base64 of the 32-byte signed-identity public key
50
+ * @param {string} advB64 base64 of the 32-byte adv secret
51
+ * @param {Array} browser [os, browserName, version]
52
+ * @param {object} [opts] { wrap } — prefix the wa.me linked-devices URL
53
+ * @returns {string}
54
+ */
55
+ function buildQrData(ref, noisePubB64, identityPubB64, advB64, browser, opts) {
56
+ const fields = [ref, noisePubB64, identityPubB64, advB64, String(companionPlatformId(browser))].join(',');
57
+ return (opts && opts.wrap) ? 'https://wa.me/settings/linked_devices#' + fields : fields;
58
+ }
59
+
60
+ module.exports = { buildQrData, companionPlatformId, COMPANION_WEB_CLIENT };
package/lib/constants.js CHANGED
@@ -48,7 +48,7 @@ const IOS_BUSINESS_STATIC_TOKEN = 'USUDuDYDeQhY4RF2fCSp5m3F6kJ1M2J8wS7bbNA2';
48
48
  // reason nothing in the error names. Keep them within a few releases of what
49
49
  // the stores are actually serving; the live lookup is what normally decides.
50
50
  const IOS_VERSION_FALLBACK = '2.26.9.75';
51
- const ANDROID_VERSION_FALLBACK = '2.26.29.73';
51
+ const ANDROID_VERSION_FALLBACK = '2.26.30.84';
52
52
 
53
53
  // Safari UA for the iTunes lookup only.
54
54
  const IOS_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1';
package/lib/fcm-mcs.js ADDED
@@ -0,0 +1,360 @@
1
+ 'use strict';
2
+
3
+ // MCS — the long-lived Firebase connection that carries WhatsApp's verification
4
+ // code as a silent push, so a number can be registered without an SMS.
5
+ //
6
+ // The push token from lib/fcm.js is the address; this is the mailbox. WhatsApp
7
+ // sends the six-digit code as a data message over Firebase Cloud Messaging, and
8
+ // the only way to receive it is to hold a connection open to Google's MCS
9
+ // endpoint and speak its binary protocol — the same one every Android phone
10
+ // keeps running in the background.
11
+ //
12
+ // The wire format, tag for tag, is WhatsApp's own client's:
13
+ //
14
+ // connect TLS to mtalk.google.com:5228
15
+ // → write one version byte (41), then a LoginRequest frame
16
+ // ← read one version byte, then a LoginResponse frame
17
+ // then frames both ways: [tag:1][length:varint][protobuf payload]
18
+ //
19
+ // tags: 0 heartbeat-ping 1 heartbeat-ack 2 login-request
20
+ // 3 login-response 4 close 7 iq 8 data-message
21
+ //
22
+ // The code rides on a data-message whose appData carries a
23
+ // key="registration_code". Everything else — heartbeats, iqs, acks — is
24
+ // bookkeeping that keeps the stream alive long enough to receive it.
25
+ //
26
+ // The codec is hand-written for the handful of messages involved and driven off
27
+ // the event loop; the bytes on the wire are the ones the native Android client
28
+ // produces.
29
+ //
30
+ // Nothing here is reachable without a push token, and the whole path is
31
+ // optional: registration falls back to SMS whenever any of it fails.
32
+
33
+ const tls = require('tls');
34
+ const { dbg: _whaDbg } = require('./logger');
35
+
36
+ const MCS_HOST = 'mtalk.google.com';
37
+ const MCS_PORT = 5228;
38
+ const MCS_VERSION = 41;
39
+
40
+ const TAG_HEARTBEAT_PING = 0;
41
+ const TAG_HEARTBEAT_ACK = 1;
42
+ const TAG_LOGIN_REQUEST = 2;
43
+ const TAG_LOGIN_RESPONSE = 3;
44
+ const TAG_CLOSE = 4;
45
+ const TAG_IQ_STANZA = 7;
46
+ const TAG_DATA_MESSAGE = 8;
47
+
48
+ // Ten minutes, matching the native client. Google closes an idle stream, and a
49
+ // ping well inside its window keeps it open without chattering.
50
+ const HEARTBEAT_INTERVAL_MS = 10 * 60 * 1000;
51
+
52
+ // The last N persistent ids are echoed on the next login so the server does not
53
+ // redeliver messages already seen. A short buffer is enough for a code fetch.
54
+ const PERSISTENT_ID_BUFFER = 50;
55
+
56
+ // The appData key WhatsApp files the verification code under.
57
+ const PUSH_CODE_APP_DATA_KEY = 'registration_code';
58
+
59
+ // ─── minimal protobuf codec ──────────────────────────────────────────────────
60
+
61
+ function encodeVarint(value) {
62
+ let v = typeof value === 'bigint' ? value : BigInt(Math.trunc(value));
63
+ if (v < 0n) v += 1n << 64n;
64
+ const out = [];
65
+ do {
66
+ let b = Number(v & 0x7fn);
67
+ v >>= 7n;
68
+ if (v > 0n) b |= 0x80;
69
+ out.push(b);
70
+ } while (v > 0n);
71
+ return Buffer.from(out);
72
+ }
73
+
74
+ const pbTag = (field, wire) => encodeVarint((field << 3) | wire);
75
+ const pbInt = (field, n) => Buffer.concat([pbTag(field, 0), encodeVarint(n)]);
76
+ function pbStr(field, s) {
77
+ const b = Buffer.from(String(s), 'utf8');
78
+ return Buffer.concat([pbTag(field, 2), encodeVarint(b.length), b]);
79
+ }
80
+ function pbMsg(field, buf) {
81
+ return Buffer.concat([pbTag(field, 2), encodeVarint(buf.length), buf]);
82
+ }
83
+
84
+ // Read a protobuf message into { field: value | [values] }. Length-delimited
85
+ // fields come back as Buffers, varints as BigInt, so a 64-bit id never loses
86
+ // precision. Repeated fields collect into arrays.
87
+ function pbDecode(buf) {
88
+ const out = {};
89
+ let i = 0;
90
+ const put = (f, v) => {
91
+ if (out[f] === undefined) out[f] = v;
92
+ else if (Array.isArray(out[f])) out[f].push(v);
93
+ else out[f] = [out[f], v];
94
+ };
95
+ const varint = () => {
96
+ let r = 0n, s = 0n;
97
+ while (i < buf.length) { const b = buf[i++]; r |= BigInt(b & 0x7f) << s; s += 7n; if (!(b & 0x80)) break; }
98
+ return r;
99
+ };
100
+ while (i < buf.length) {
101
+ const key = Number(varint()), field = key >> 3, wire = key & 7;
102
+ if (wire === 0) put(field, varint());
103
+ else if (wire === 2) { const n = Number(varint()); put(field, buf.slice(i, i + n)); i += n; }
104
+ else if (wire === 1) { i += 8; }
105
+ else if (wire === 5) { i += 4; }
106
+ else break;
107
+ }
108
+ return out;
109
+ }
110
+
111
+ // ─── message builders ────────────────────────────────────────────────────────
112
+
113
+ function buildLoginRequest(session) {
114
+ const androidId = String(session.androidId);
115
+ const setting = Buffer.concat([pbStr(1, 'new_vc'), pbStr(2, '1')]);
116
+
117
+ const parts = [
118
+ pbStr(1, 'android-30'), // id
119
+ pbStr(2, 'mcs.android.com'), // domain
120
+ pbStr(3, androidId), // user
121
+ pbStr(4, androidId), // resource
122
+ pbStr(5, String(session.securityToken)), // authToken
123
+ pbStr(6, 'android-' + BigInt(androidId).toString(16)), // deviceId
124
+ pbMsg(8, setting) // settings[0]
125
+ ];
126
+ for (const pid of (session.persistentIds || [])) parts.push(pbStr(10, pid));
127
+ // adaptiveHeartbeat (12) is false → omitted. Then:
128
+ parts.push(pbInt(14, 1)); // useRmq2 = true
129
+ parts.push(pbInt(16, 2)); // authService = 2
130
+ parts.push(pbInt(17, 1)); // networkType = 1
131
+ return Buffer.concat(parts);
132
+ }
133
+
134
+ const buildHeartbeatAck = streamId => pbInt(2, streamId); // lastStreamIdReceived
135
+ const buildHeartbeatPing = streamId => pbInt(2, streamId);
136
+
137
+ function frame(tag, payload) {
138
+ const len = encodeVarint(payload.length);
139
+ return Buffer.concat([Buffer.from([tag]), len, payload]);
140
+ }
141
+
142
+ // ─── connection ──────────────────────────────────────────────────────────────
143
+
144
+ /**
145
+ * Open MCS, log in, and resolve with the verification code the moment it
146
+ * arrives — or null on timeout or any failure. Never throws.
147
+ *
148
+ * The connection must be established before the /code request is sent, or the
149
+ * push can arrive with nowhere to land; callers open this first and request the
150
+ * code once `onReady` has fired.
151
+ *
152
+ * @param {object} session { androidId, securityToken, persistentIds? } from lib/fcm
153
+ * @param {object} [opts] { timeoutMs=180000, onReady, signal }
154
+ * @returns {Promise<string|null>}
155
+ */
156
+ function receivePushCode(session, opts) {
157
+ opts = opts || {};
158
+ const timeoutMs = opts.timeoutMs > 0 ? opts.timeoutMs : 180000;
159
+
160
+ return new Promise((resolve) => {
161
+ if (!session || !session.androidId || !session.securityToken) {
162
+ _whaDbg('[DBG] MCS no android id — cannot receive push code');
163
+ return resolve(null);
164
+ }
165
+
166
+ let settled = false;
167
+ let socket = null;
168
+ let heartbeat = null;
169
+ let deadline = null;
170
+ let rx = Buffer.alloc(0);
171
+ let sawVersion = false;
172
+ let loggedIn = false;
173
+ let streamId = 0;
174
+ const persistentIds = Array.isArray(session.persistentIds) ? session.persistentIds : [];
175
+
176
+ const finish = (code) => {
177
+ if (settled) return;
178
+ settled = true;
179
+ if (heartbeat) clearInterval(heartbeat);
180
+ if (deadline) clearTimeout(deadline);
181
+ try { if (socket) socket.destroy(); } catch (_) {}
182
+ resolve(code);
183
+ };
184
+
185
+ deadline = setTimeout(() => {
186
+ if (!settled) { _whaDbg('[DBG] MCS timed out after ' + timeoutMs + 'ms'); finish(null); }
187
+ }, timeoutMs);
188
+
189
+ if (opts.signal) {
190
+ if (opts.signal.aborted) return finish(null);
191
+ opts.signal.addEventListener('abort', () => finish(null), { once: true });
192
+ }
193
+
194
+ const write = (tag, payload) => {
195
+ try { socket.write(frame(tag, payload)); } catch (e) {
196
+ _whaDbg('[DBG] MCS write failed: ' + (e && e.message)); finish(null);
197
+ }
198
+ };
199
+
200
+ // Route MCS through the SOCKS agent's dialer when one is set, so a proxied
201
+ // process reaches Google here too. Loaded lazily to avoid a cycle.
202
+ const { socksProxyUrl, socksConnect } = require('./socks');
203
+ const proxyUrl = socksProxyUrl();
204
+
205
+ const onRaw = (rawSocket) => {
206
+ socket = tls.connect({
207
+ socket: rawSocket || undefined,
208
+ host: MCS_HOST,
209
+ port: MCS_PORT,
210
+ servername: MCS_HOST
211
+ }, () => {
212
+ // version byte first, then the login frame
213
+ try {
214
+ socket.write(Buffer.from([MCS_VERSION]));
215
+ socket.write(frame(TAG_LOGIN_REQUEST, buildLoginRequest({
216
+ androidId: session.androidId, securityToken: session.securityToken, persistentIds
217
+ })));
218
+ _whaDbg('[DBG] MCS connected, login sent');
219
+ } catch (e) { _whaDbg('[DBG] MCS login write failed: ' + (e && e.message)); finish(null); }
220
+ });
221
+
222
+ socket.setNoDelay(true);
223
+ socket.on('data', onData);
224
+ socket.on('error', (e) => { _whaDbg('[DBG] MCS socket error: ' + (e && e.message)); finish(null); });
225
+ socket.on('close', () => { if (!settled) { _whaDbg('[DBG] MCS closed'); finish(null); } });
226
+ };
227
+
228
+ if (proxyUrl) {
229
+ _whaDbg('[DBG] MCS dialing via SOCKS proxy');
230
+ socksConnect(proxyUrl, MCS_HOST, MCS_PORT)
231
+ .then(onRaw)
232
+ .catch(e => { _whaDbg('[DBG] MCS proxy dial failed: ' + (e && e.message)); finish(null); });
233
+ } else {
234
+ onRaw(null);
235
+ }
236
+
237
+ function onData(chunk) {
238
+ rx = Buffer.concat([rx, chunk]);
239
+
240
+ // The server's first byte is its protocol version, once, before any frame.
241
+ if (!sawVersion) {
242
+ if (rx.length < 1) return;
243
+ _whaDbg('[DBG] MCS server version=' + rx[0]);
244
+ rx = rx.slice(1);
245
+ sawVersion = true;
246
+ }
247
+
248
+ // Drain whole frames. A varint length that runs past the buffer means the
249
+ // rest is still in flight — wait for more.
250
+ for (;;) {
251
+ if (rx.length < 1) return;
252
+ const tag = rx[0];
253
+
254
+ let len = 0, shift = 0n, p = 1, complete = false;
255
+ for (; p < rx.length && p <= 10; p++) {
256
+ const b = rx[p];
257
+ len |= (b & 0x7f) << Number(shift);
258
+ shift += 7n;
259
+ if (!(b & 0x80)) { p++; complete = true; break; }
260
+ }
261
+ if (!complete) return; // length varint not fully here yet
262
+ if (rx.length < p + len) return; // payload not fully here yet
263
+
264
+ const payload = rx.slice(p, p + len);
265
+ rx = rx.slice(p + len);
266
+ handleFrame(tag, payload);
267
+ if (settled) return;
268
+ }
269
+ }
270
+
271
+ function handleFrame(tag, payload) {
272
+ if (!loggedIn) {
273
+ if (tag !== TAG_LOGIN_RESPONSE) {
274
+ _whaDbg('[DBG] MCS expected login response, got tag=' + tag); return finish(null);
275
+ }
276
+ const resp = pbDecode(payload);
277
+ if (resp[3]) { // error info present
278
+ const err = pbDecode(resp[3]);
279
+ _whaDbg('[DBG] MCS login refused code=' + (err[1] && err[1].toString()));
280
+ return finish(null);
281
+ }
282
+ loggedIn = true;
283
+ streamId = 1;
284
+ _whaDbg('[DBG] MCS logged in — waiting for push code');
285
+ heartbeat = setInterval(() => write(TAG_HEARTBEAT_PING, buildHeartbeatPing(streamId)),
286
+ HEARTBEAT_INTERVAL_MS);
287
+ if (typeof opts.onReady === 'function') { try { opts.onReady(); } catch (_) {} }
288
+ return;
289
+ }
290
+
291
+ streamId++;
292
+
293
+ switch (tag) {
294
+ case TAG_DATA_MESSAGE: {
295
+ const code = extractCode(payload, persistentIds);
296
+ if (code) { _whaDbg('[DBG] MCS push code received'); return finish(code); }
297
+ break;
298
+ }
299
+ case TAG_HEARTBEAT_PING:
300
+ write(TAG_HEARTBEAT_ACK, buildHeartbeatAck(streamId));
301
+ break;
302
+ case TAG_HEARTBEAT_ACK:
303
+ case TAG_IQ_STANZA:
304
+ break;
305
+ case TAG_CLOSE:
306
+ _whaDbg('[DBG] MCS server requested close'); return finish(null);
307
+ default:
308
+ _whaDbg('[DBG] MCS unknown tag ' + tag + ' (' + payload.length + 'b)');
309
+ }
310
+ }
311
+ });
312
+ }
313
+
314
+ // Pull the verification code out of a data-message stanza, and remember its
315
+ // persistent id so a reconnect does not ask for it again.
316
+ //
317
+ // field 9 persistentId (string)
318
+ // field 7 appData, repeated { 1: key, 2: value }
319
+ function extractCode(payload, persistentIds) {
320
+ const stanza = pbDecode(payload);
321
+
322
+ const pid = stanza[9];
323
+ if (pid && pid.length) {
324
+ persistentIds.push(pid.toString('utf8'));
325
+ if (persistentIds.length > PERSISTENT_ID_BUFFER) {
326
+ persistentIds.splice(0, persistentIds.length - PERSISTENT_ID_BUFFER);
327
+ }
328
+ }
329
+
330
+ let appData = stanza[7];
331
+ if (!appData) return null;
332
+ if (!Array.isArray(appData)) appData = [appData];
333
+
334
+ for (const entry of appData) {
335
+ const kv = pbDecode(entry);
336
+ const key = kv[1] ? kv[1].toString('utf8') : '';
337
+ if (key === PUSH_CODE_APP_DATA_KEY) {
338
+ return kv[2] ? kv[2].toString('utf8') : null;
339
+ }
340
+ }
341
+ return null;
342
+ }
343
+
344
+ module.exports = {
345
+ receivePushCode,
346
+ MCS_HOST,
347
+ MCS_PORT,
348
+ MCS_VERSION,
349
+ PUSH_CODE_APP_DATA_KEY,
350
+ // exported for tests
351
+ buildLoginRequest,
352
+ frame,
353
+ pbDecode,
354
+ extractCode,
355
+ encodeVarint,
356
+ TAG_LOGIN_RESPONSE,
357
+ TAG_DATA_MESSAGE,
358
+ TAG_HEARTBEAT_PING,
359
+ TAG_HEARTBEAT_ACK
360
+ };
package/lib/fcm.js CHANGED
@@ -512,8 +512,48 @@ async function getPushToken(store, device) {
512
512
  }
513
513
  }
514
514
 
515
+ /**
516
+ * Open the MCS listener that receives the verification code as a push, using
517
+ * the Firebase identity already established for this account.
518
+ *
519
+ * The token has to exist first — getPushToken establishes the android id and
520
+ * security token MCS logs in with — so this calls it when the store has none.
521
+ * The returned promise resolves with the code when it arrives, or null on any
522
+ * failure, so the caller can fall back to SMS.
523
+ *
524
+ * @param {object} store the account store; the Firebase session is on store.fcm
525
+ * @param {object} device device config
526
+ * @param {object} [opts] { timeoutMs, onReady, signal }
527
+ * @returns {Promise<string|null>}
528
+ */
529
+ async function receivePushCode(store, device, opts) {
530
+ if (!enabled() || !store) return null;
531
+
532
+ // Make sure we hold an android id / security token before opening MCS.
533
+ if (!store.fcm || !store.fcm.androidId || !store.fcm.securityToken) {
534
+ await getPushToken(store, device);
535
+ }
536
+ const s = store.fcm;
537
+ if (!s || !s.androidId || !s.securityToken) {
538
+ _whaDbg('[DBG] FCM no identity for MCS — cannot receive push code');
539
+ return null;
540
+ }
541
+
542
+ const { receivePushCode: mcsReceive } = require('./fcm-mcs');
543
+ const session = {
544
+ androidId: s.androidId,
545
+ securityToken: s.securityToken,
546
+ persistentIds: Array.isArray(s.persistentIds) ? s.persistentIds : []
547
+ };
548
+ const code = await mcsReceive(session, opts);
549
+ // Persist the persistent-id buffer so a later listen does not re-see messages.
550
+ store.fcm = Object.assign({}, store.fcm, { persistentIds: session.persistentIds });
551
+ return code;
552
+ }
553
+
515
554
  module.exports = {
516
555
  getPushToken,
556
+ receivePushCode,
517
557
  enabled,
518
558
  FCM_CONFIG,
519
559
  // exported for tests
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.19.4",
3
+ "version": "5.21.1",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API and web ",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",
@@ -53,6 +53,7 @@
53
53
  },
54
54
  "optionalDependencies": {
55
55
  "jimp": "^1.6.0",
56
- "socks": "^2.8.9"
56
+ "socks": "^2.8.9",
57
+ "qrcode-terminal": "^0.12.0"
57
58
  }
58
59
  }