whalibmob 5.32.2 → 5.33.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/.env.example +9 -7
- package/README.md +32 -7
- package/cli.js +12 -10
- package/index.d.ts +6 -0
- package/index.js +9 -2
- package/lib/PushClient.js +36 -12
- package/lib/Registration.js +4 -4
- package/lib/Store.js +9 -0
- package/lib/apns-courier.js +675 -0
- package/lib/apns.js +642 -0
- package/lib/plist.js +451 -0
- package/llms.txt +1 -1
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -145,16 +145,18 @@
|
|
|
145
145
|
# WA_SOCKS_LIB=/absolute/path/to/socks/build/index.js
|
|
146
146
|
|
|
147
147
|
# ─── Push token ──────────────────────────────────────────────────────────────
|
|
148
|
-
# Every WhatsApp on a real phone holds a
|
|
149
|
-
#
|
|
150
|
-
#
|
|
148
|
+
# Every WhatsApp on a real phone holds a push token — it is how the server wakes
|
|
149
|
+
# the app. Registration fetches one and ships it as push_token, so the request
|
|
150
|
+
# looks like the install it claims to be. Which network it comes from follows
|
|
151
|
+
# from WA_OS: Firebase on android, APNs on ios. The two are not interchangeable.
|
|
151
152
|
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
153
|
+
# Both on by default, once per number, cached with the session afterwards. Any
|
|
154
|
+
# failure is silent: the field is dropped and registration proceeds exactly as
|
|
155
|
+
# it would without it.
|
|
155
156
|
#
|
|
156
|
-
# Set to 0 to skip
|
|
157
|
+
# Set either to 0 to skip that one entirely.
|
|
157
158
|
# WA_FCM_PUSH=0
|
|
159
|
+
# WA_APNS_PUSH=0
|
|
158
160
|
|
|
159
161
|
|
|
160
162
|
# ─── Registration funnel telemetry ───────────────────────────────────────────
|
package/README.md
CHANGED
|
@@ -254,6 +254,8 @@ npm install -g whalibmob
|
|
|
254
254
|
- [The Number WhatsApp Files Your Account Under](#the-number-whatsapp-files-your-account-under)
|
|
255
255
|
- [When Registration Is Refused for Consent](#when-registration-is-refused-for-consent)
|
|
256
256
|
- [The Push Token](#the-push-token)
|
|
257
|
+
- [On Android: Firebase](#on-android-firebase)
|
|
258
|
+
- [On iOS: APNs](#on-ios-apns)
|
|
257
259
|
- [Receiving the Code over Push](#receiving-the-code-over-push-without-typing-it)
|
|
258
260
|
- [Routing Traffic Through a Proxy](#routing-traffic-through-a-proxy)
|
|
259
261
|
- [What Goes Through It](#what-goes-through-it)
|
|
@@ -1871,7 +1873,7 @@ if (result.status === 'ok') {
|
|
|
1871
1873
|
}
|
|
1872
1874
|
```
|
|
1873
1875
|
|
|
1874
|
-
**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
|
|
1876
|
+
**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 a registration sends a 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 — on an Android profile over Firebase, on an iOS one over APNs:
|
|
1875
1877
|
|
|
1876
1878
|
```js
|
|
1877
1879
|
const { receivePushCode } = require('whalibmob')
|
|
@@ -2777,7 +2779,7 @@ If that is refused too, the number has to go through the real app once, on a pho
|
|
|
2777
2779
|
|
|
2778
2780
|
Every WhatsApp on a real phone holds a push token. It is the address the push network 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.
|
|
2779
2781
|
|
|
2780
|
-
**
|
|
2782
|
+
**Which token you get follows from the profile.** Which push network an install uses follows from its platform: Android holds a *Firebase* token and keeps a stream open to Google, iOS holds an *APNs* token and keeps one open to Apple. They are not interchangeable, and the registration server sees both the token and the User-Agent naming the platform that sent it — an iPhone presenting a Firebase token describes a device nobody ships. Both sides are implemented, and the right one is chosen from `WA_OS` without you asking for it. A profile that is neither sends no `push_token` at all, which is the correct thing for a client with no push line.
|
|
2781
2783
|
|
|
2782
2784
|
The token does two distinct jobs, and it is easy to conflate them:
|
|
2783
2785
|
|
|
@@ -2791,6 +2793,8 @@ request a code ─┬─ method you chose → reaches a human (SMS, a call,
|
|
|
2791
2793
|
└─ push_token line → reaches the app (silent, auto-filled — if WhatsApp sends it)
|
|
2792
2794
|
```
|
|
2793
2795
|
|
|
2796
|
+
### On Android: Firebase
|
|
2797
|
+
|
|
2794
2798
|
Registration fetches a real token and sends it, in three plain HTTPS calls to Google:
|
|
2795
2799
|
|
|
2796
2800
|
| Step | Endpoint | Yields |
|
|
@@ -2803,15 +2807,36 @@ No root, no Frida, no phone. This is unrelated to Play Integrity attestation —
|
|
|
2803
2807
|
|
|
2804
2808
|
It runs once per number. The Firebase identity is cached on the session, because Google issues an android id once and expects it back; fetching a new one per registration step would mint a fresh phantom device each time.
|
|
2805
2809
|
|
|
2806
|
-
**Failure is silent by design.** A blocked network, a refusal from Google, a malformed answer — all end with the field omitted and registration proceeding exactly as it did before push tokens were wired up. A push token helps; it is never a prerequisite.
|
|
2807
|
-
|
|
2808
2810
|
Turn it off with `WA_FCM_PUSH=0`.
|
|
2809
2811
|
|
|
2812
|
+
### On iOS: APNs
|
|
2813
|
+
|
|
2814
|
+
Apple's flow is not Google's, but it lands in the same place:
|
|
2815
|
+
|
|
2816
|
+
| Step | Endpoint | Yields |
|
|
2817
|
+
|---|---|---|
|
|
2818
|
+
| 1 | `albert.apple.com` | a device certificate Apple signs for a keypair generated here — the activation every Apple device runs on first boot |
|
|
2819
|
+
| 2 | `init-p01st.push.apple.com/bag` | which courier hosts to dial |
|
|
2820
|
+
| 3 | `<n>-courier.push.apple.com:443` | the device token, then `GET_TOKEN` for `net.whatsapp.WhatsApp` — the per-topic token that *is* `push_token` |
|
|
2821
|
+
|
|
2822
|
+
Step 1 runs once per number and its result is cached on the session: the certificate Apple issues is good for a year, and the identity behind it is what makes the same push token come back on the next run. A token WhatsApp has already recorded stops being deliverable if that identity is thrown away, which is the same reason the Firebase android id is kept.
|
|
2823
|
+
|
|
2824
|
+
The courier connection is a TLS stream that offers ALPN `apns-security-v3` — the device proves itself with a signature over a fresh nonce in the first frame rather than with a TLS client certificate. Apple accepts the offer without echoing it back, so a missing echo is normal and is not treated as a failure. A network that terminates TLS in the middle cannot carry the stream; what gives it away is the certificate, since a middlebox has to present one its own CA signed rather than Apple's. The failure names that issuer, instead of looking like a dropped connection.
|
|
2825
|
+
|
|
2826
|
+
Turn it off with `WA_APNS_PUSH=0`.
|
|
2827
|
+
|
|
2828
|
+
**Failure is silent by design, on both platforms.** A blocked network, a refusal from Google or Apple, a malformed answer — all end with the field omitted and registration proceeding exactly as it did before push tokens were wired up. A push token helps; it is never a prerequisite.
|
|
2829
|
+
|
|
2810
2830
|
### Receiving the code over push, without typing it
|
|
2811
2831
|
|
|
2812
|
-
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
|
|
2832
|
+
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 push line to catch it — the same long-lived connection every phone keeps open to its push network. `receivePushCode(store, device)` opens the one that matches the profile:
|
|
2833
|
+
|
|
2834
|
+
| Profile | Connection | Where the code is |
|
|
2835
|
+
|---|---|---|
|
|
2836
|
+
| `WA_OS=android` | `mtalk.google.com:5228`, the MCS protocol, logged in with the Firebase identity | an `appData` entry keyed `registration_code` |
|
|
2837
|
+
| `WA_OS=ios` | `<n>-courier.push.apple.com:443`, the APNs courier, authenticated with the activation certificate | the `regcode` field of the notification's JSON payload |
|
|
2813
2838
|
|
|
2814
|
-
|
|
2839
|
+
Either way the call resolves with the code the moment a push carrying it arrives. A profile that is neither resolves `null` immediately rather than holding a listener open on a line no push can reach, and `/reg push` says so and stops instead of waiting out its timeout. Check with `supportsPush(store.device)` if you want to branch on it.
|
|
2815
2840
|
|
|
2816
2841
|
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.
|
|
2817
2842
|
|
|
@@ -2838,7 +2863,7 @@ From the CLI the whole sequence is one command:
|
|
|
2838
2863
|
|
|
2839
2864
|
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.
|
|
2840
2865
|
|
|
2841
|
-
|
|
2866
|
+
Either connection carries a heartbeat and acknowledges what it has read, 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.
|
|
2842
2867
|
|
|
2843
2868
|
> [!IMPORTANT]
|
|
2844
2869
|
> 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.
|
package/cli.js
CHANGED
|
@@ -663,7 +663,7 @@ 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
|
|
666
|
+
/reg push <phone> [sms|voice] request code and receive it over push
|
|
667
667
|
/reg confirm <phone> <code> complete registration
|
|
668
668
|
|
|
669
669
|
Connection
|
|
@@ -2424,7 +2424,8 @@ async function handleLine(line) {
|
|
|
2424
2424
|
const ph = normalizePhone(p[2]);
|
|
2425
2425
|
if (!ph) {
|
|
2426
2426
|
fail('usage: /reg push <phone> [sms|voice] [--name "Your Name"]');
|
|
2427
|
-
out(' opens the
|
|
2427
|
+
out(' opens the push listener this device profile keeps open — Firebase');
|
|
2428
|
+
out(' on Android, APNs on iOS — requests a code, and waits for');
|
|
2428
2429
|
out(' it to arrive over push. If it does, registration is confirmed');
|
|
2429
2430
|
out(' automatically. If no push comes, request the code normally with');
|
|
2430
2431
|
out(' /reg code and confirm it with /reg confirm.');
|
|
@@ -2439,24 +2440,25 @@ async function handleLine(line) {
|
|
|
2439
2440
|
if (!store) { store = initAuthCreds(ph, { name: regName }); saveStore(store, sessFile); }
|
|
2440
2441
|
if (!store.device) store.device = getDeviceConfig();
|
|
2441
2442
|
|
|
2442
|
-
// Push verification needs a push line, and
|
|
2443
|
-
//
|
|
2444
|
-
//
|
|
2445
|
-
//
|
|
2443
|
+
// Push verification needs a push line, and it has to be the one the
|
|
2444
|
+
// announced platform actually keeps open. Android and iOS both have
|
|
2445
|
+
// one; a profile that is neither would sit for three minutes on a push
|
|
2446
|
+
// that can never be routed to it, so say so now instead.
|
|
2446
2447
|
const pushClient = pushClientFor(store.device);
|
|
2447
2448
|
if (!pushClient.supportsPush) {
|
|
2448
2449
|
fail('push verification is not available for this device profile (' +
|
|
2449
2450
|
(store.device.os || 'unknown') + ')');
|
|
2450
2451
|
out(' the code arrives over the push line of the platform being announced,');
|
|
2451
|
-
out(' and only
|
|
2452
|
-
out(' either register this number on
|
|
2453
|
-
out(' WA_OS=android (see /device) and re-run /reg push ' + ph);
|
|
2452
|
+
out(' and only android and ios have one (Firebase and APNs).');
|
|
2453
|
+
out(' either register this number on one of those profiles:');
|
|
2454
|
+
out(' WA_OS=android or WA_OS=ios (see /device) and re-run /reg push ' + ph);
|
|
2454
2455
|
out(' or use the ordinary path: /reg code ' + ph + ' then /reg confirm ' + ph + ' <code>');
|
|
2455
2456
|
break;
|
|
2456
2457
|
}
|
|
2457
2458
|
const receivePushCode = (s, d, o) => pushClient.receivePushCode(s, d, o);
|
|
2458
2459
|
|
|
2459
|
-
out('opening
|
|
2460
|
+
out('opening the ' + (pushClient.platform === 'ios' ? 'APNs' : 'Firebase') +
|
|
2461
|
+
' push listener (this can take a moment)...');
|
|
2460
2462
|
// Open the listener first so the push has somewhere to land. onReady
|
|
2461
2463
|
// fires once MCS is logged in — only then is it safe to ask for the
|
|
2462
2464
|
// code.
|
package/index.d.ts
CHANGED
|
@@ -1779,6 +1779,12 @@ export declare const Tokens: LibModule;
|
|
|
1779
1779
|
export declare const PushClient: LibModule;
|
|
1780
1780
|
export declare const Fcm: LibModule;
|
|
1781
1781
|
export declare const FcmMcs: LibModule;
|
|
1782
|
+
/** `whalibmob/lib/apns` — the iOS push line: activation, token, push code. */
|
|
1783
|
+
export declare const Apns: LibModule;
|
|
1784
|
+
/** `whalibmob/lib/apns-courier` — the APNs stream the push code arrives on. */
|
|
1785
|
+
export declare const ApnsCourier: LibModule;
|
|
1786
|
+
/** `whalibmob/lib/plist` — Apple property lists, as APNs speaks them. */
|
|
1787
|
+
export declare const Plist: LibModule;
|
|
1782
1788
|
|
|
1783
1789
|
/**
|
|
1784
1790
|
* X25519 through Node's own OpenSSL — a drop-in for `curve25519-js`, roughly
|
package/index.js
CHANGED
|
@@ -81,8 +81,9 @@ const {
|
|
|
81
81
|
// code over push" in the README.
|
|
82
82
|
//
|
|
83
83
|
// Routed through the push client for the device's platform: Android opens the
|
|
84
|
-
// Firebase MCS stream, iOS
|
|
85
|
-
//
|
|
84
|
+
// Firebase MCS stream, iOS the APNs courier. Either way the code arrives over
|
|
85
|
+
// the line the announced platform actually keeps open, and a profile with no
|
|
86
|
+
// push transport resolves null rather than listening on somebody else's.
|
|
86
87
|
const receivePushCode = (store, device, opts) => {
|
|
87
88
|
const dev = device || (store && store.device);
|
|
88
89
|
return require('./lib/PushClient')
|
|
@@ -131,6 +132,9 @@ const Tokens = require('./lib/tokens');
|
|
|
131
132
|
const PushClient = require('./lib/PushClient');
|
|
132
133
|
const Fcm = require('./lib/fcm');
|
|
133
134
|
const FcmMcs = require('./lib/fcm-mcs');
|
|
135
|
+
const Apns = require('./lib/apns');
|
|
136
|
+
const ApnsCourier = require('./lib/apns-courier');
|
|
137
|
+
const Plist = require('./lib/plist');
|
|
134
138
|
|
|
135
139
|
const PairingCode = require('./lib/PairingCode');
|
|
136
140
|
const CompanionPairing = require('./lib/CompanionPairing');
|
|
@@ -372,6 +376,9 @@ module.exports = {
|
|
|
372
376
|
PushClient,
|
|
373
377
|
Fcm,
|
|
374
378
|
FcmMcs,
|
|
379
|
+
Apns,
|
|
380
|
+
ApnsCourier,
|
|
381
|
+
Plist,
|
|
375
382
|
|
|
376
383
|
// Linking to an account that already exists
|
|
377
384
|
// X25519, through Node's own OpenSSL. Drop-in for curve25519-js.
|
package/lib/PushClient.js
CHANGED
|
@@ -12,15 +12,16 @@
|
|
|
12
12
|
//
|
|
13
13
|
// This module is the seam. `pushClientFor(device)` returns the client that
|
|
14
14
|
// matches the device profile, so no caller has to reach for a transport by
|
|
15
|
-
// name.
|
|
15
|
+
// name. Both platforms are implemented:
|
|
16
16
|
//
|
|
17
|
-
// android → ./fcm
|
|
18
|
-
// ios →
|
|
17
|
+
// android → ./fcm (checkin → FIS install → register3, then MCS for the code)
|
|
18
|
+
// ios → ./apns (albert activation → courier GET_TOKEN, then the courier
|
|
19
|
+
// stream for the code)
|
|
19
20
|
//
|
|
20
|
-
//
|
|
21
|
-
// platform we cannot speak push for. It supplies nothing,
|
|
22
|
-
// null, and the registration body goes out without a push
|
|
23
|
-
// with somebody else's.
|
|
21
|
+
// Anything else gets NONE, which is not a failure path — it is the correct
|
|
22
|
+
// answer for a platform we cannot speak push for. It supplies nothing,
|
|
23
|
+
// `buildForm` drops the null, and the registration body goes out without a push
|
|
24
|
+
// token rather than with somebody else's.
|
|
24
25
|
|
|
25
26
|
const { dbg: _whaDbg } = require('./logger');
|
|
26
27
|
|
|
@@ -63,13 +64,34 @@ const FCM_PUSH_CLIENT = {
|
|
|
63
64
|
}
|
|
64
65
|
};
|
|
65
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The push client backed by Apple Push Notification service, for iOS profiles.
|
|
69
|
+
*
|
|
70
|
+
* Delegates to ./apns, which owns the albert activation and the courier
|
|
71
|
+
* stream. Lazy for the same reason as the Firebase client above: a session that
|
|
72
|
+
* never registers should not pay for either stack.
|
|
73
|
+
*/
|
|
74
|
+
const APNS_PUSH_CLIENT = {
|
|
75
|
+
platform: 'ios',
|
|
76
|
+
supportsPush: true,
|
|
77
|
+
|
|
78
|
+
async getPushToken(store, device) {
|
|
79
|
+
return require('./apns').getPushToken(store, device);
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
async receivePushCode(store, device, opts) {
|
|
83
|
+
return require('./apns').receivePushCode(store, device, opts);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
66
87
|
/**
|
|
67
88
|
* The push client matching a device profile.
|
|
68
89
|
*
|
|
69
|
-
* Android profiles get the Firebase client
|
|
70
|
-
* choice is made on the device rather than on a
|
|
71
|
-
* process can register an Android session and an iOS
|
|
72
|
-
* has to present its own platform's push line — or
|
|
90
|
+
* Android profiles get the Firebase client, iOS profiles the APNs one, and
|
|
91
|
+
* everything else NONE. The choice is made on the device rather than on a
|
|
92
|
+
* global setting, because a process can register an Android session and an iOS
|
|
93
|
+
* session in turn and each has to present its own platform's push line — or
|
|
94
|
+
* none at all.
|
|
73
95
|
*
|
|
74
96
|
* @param {object} device a device config; only `device.os` is read
|
|
75
97
|
* @returns {{platform: string, supportsPush: boolean,
|
|
@@ -77,6 +99,7 @@ const FCM_PUSH_CLIENT = {
|
|
|
77
99
|
*/
|
|
78
100
|
function pushClientFor(device) {
|
|
79
101
|
if (device && device.os === 'android') return FCM_PUSH_CLIENT;
|
|
102
|
+
if (device && device.os === 'ios') return APNS_PUSH_CLIENT;
|
|
80
103
|
|
|
81
104
|
if (device && device.os) {
|
|
82
105
|
_whaDbg('[DBG] no push transport for ' + device.os + ' — push fields omitted');
|
|
@@ -102,5 +125,6 @@ module.exports = {
|
|
|
102
125
|
pushClientFor,
|
|
103
126
|
supportsPush,
|
|
104
127
|
NONE_PUSH_CLIENT,
|
|
105
|
-
FCM_PUSH_CLIENT
|
|
128
|
+
FCM_PUSH_CLIENT,
|
|
129
|
+
APNS_PUSH_CLIENT
|
|
106
130
|
};
|
package/lib/Registration.js
CHANGED
|
@@ -1517,10 +1517,10 @@ async function buildPayload(store, waVersion, useToken, extraPairs) {
|
|
|
1517
1517
|
.digest()
|
|
1518
1518
|
);
|
|
1519
1519
|
// The push token a real install always has, taken from the transport that
|
|
1520
|
-
// matches this device's platform: Firebase for Android,
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1520
|
+
// matches this device's platform: Firebase for Android, APNs for iOS. Asking
|
|
1521
|
+
// ./fcm directly would put a Google token in an iPhone's registration body —
|
|
1522
|
+
// a device that keeps an MCS stream to Google and announces itself as iOS is
|
|
1523
|
+
// not a device anyone ships.
|
|
1524
1524
|
//
|
|
1525
1525
|
// Acquired once and cached on the store, so the two registration steps share
|
|
1526
1526
|
// one. Returns null on any failure or on a platform with no transport, which
|
package/lib/Store.js
CHANGED
|
@@ -213,6 +213,12 @@ function storeToJson(store) {
|
|
|
213
213
|
// and expects it back; discarding it would mint a new phantom device on
|
|
214
214
|
// every registration step, which is worse than carrying one.
|
|
215
215
|
fcm: store.fcm || null,
|
|
216
|
+
// The same thing on the iOS side: the Apple-signed device certificate, the
|
|
217
|
+
// keypair it was issued against, and the tokens derived from them. Dropping
|
|
218
|
+
// it costs an activation round-trip and, worse, changes the push_token
|
|
219
|
+
// WhatsApp has already recorded — so the verification push would be
|
|
220
|
+
// addressed to a device nobody is listening as.
|
|
221
|
+
apns: store.apns || null,
|
|
216
222
|
name,
|
|
217
223
|
version,
|
|
218
224
|
device,
|
|
@@ -263,6 +269,9 @@ function storeFromJson(obj) {
|
|
|
263
269
|
// Absent on stores written before push tokens existed; null simply means
|
|
264
270
|
// the first registration step will fetch one.
|
|
265
271
|
fcm: obj.fcm || null,
|
|
272
|
+
// Same, for the APNs side. Absent means the first registration step runs
|
|
273
|
+
// the activation handshake once and writes the result back here.
|
|
274
|
+
apns: obj.apns || null,
|
|
266
275
|
name,
|
|
267
276
|
version,
|
|
268
277
|
device,
|