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 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 Firebase push token — it is how the
149
- # server wakes the app. Registration fetches one from Google and ships it as
150
- # push_token, so the request looks like the install it claims to be.
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
- # On by default. Three plain HTTPS calls to Google, once per number, cached with
153
- # the session afterwards. Any failure is silent: the field is dropped and
154
- # registration proceeds exactly as it would without it.
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 it entirely.
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 an Android registration 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 — on an `WA_OS=android` profile; on iOS it resolves `null` straight away, since only Firebase is implemented:
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
- **This is an Android-profile feature.** 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. Only the Firebase side is implemented here, so everything in this section applies when `WA_OS=android`. An iOS registration sends no `push_token` at all, which is the correct thing for a client with no push line, and every other part of the flow is unaffected.
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 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.
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
- Like the token itself, this is Android-only. On an iOS profile `receivePushCode` 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.
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
- 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.
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 Firebase push
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 Firebase push listener, requests a code, and waits for');
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 only Android has one here.
2443
- // An iOS session holds no Firebase identity, so the listener would sit
2444
- // for three minutes on a push that can never be routed to it. Say so
2445
- // now instead, and point at the two things that do work.
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 Android has one implemented (Firebase). iOS needs APNs.');
2452
- out(' either register this number on an Android profile:');
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 Firebase push listener (this can take a moment)...');
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 resolves null because APNs is not implemented and
85
- // an iOS session holds no Firebase identity to listen with.
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. Two implementations exist today:
15
+ // name. Both platforms are implemented:
16
16
  //
17
- // android → ./fcm (checkin → FIS install → register3, then MCS for the code)
18
- // ios → NONE (APNs is not implemented; every push field is omitted)
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
- // The NONE client is not a failure path — it is the correct answer for a
21
- // platform we cannot speak push for. It supplies nothing, `buildForm` drops the
22
- // null, and the registration body goes out without a push token rather than
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; everything else gets NONE. The
70
- * choice is made on the device rather than on a global setting, because a
71
- * process can register an Android session and an iOS session in turn and each
72
- * has to present its own platform's push line — or none at all.
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
  };
@@ -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, nothing for iOS until
1521
- // APNs exists. Asking ./fcm directly would have put a Google token in an
1522
- // iPhone's registration body — a device that keeps an MCS stream to Google
1523
- // and announces itself as iOS is not a device anyone ships.
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,