whalibmob 5.29.4 → 5.29.6
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 +13 -3
- package/cli.js +10 -0
- package/lib/PlayStore.js +64 -9
- package/lib/Registration.js +71 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1866,9 +1866,19 @@ requesting sms code for +919634847671...
|
|
|
1866
1866
|
status sent
|
|
1867
1867
|
```
|
|
1868
1868
|
|
|
1869
|
-
It happens once. Every later registration reads the file
|
|
1870
|
-
|
|
1871
|
-
|
|
1869
|
+
It happens once. Every later registration reads the file — from the session
|
|
1870
|
+
directory you are using, wherever that is. `WA_NO_APK_DOWNLOAD=1` turns the
|
|
1871
|
+
fetch off if you would rather it never pulled a hundred megabytes unasked, and
|
|
1872
|
+
`wa apk-material` below does the same job by hand.
|
|
1873
|
+
|
|
1874
|
+
> [!NOTE]
|
|
1875
|
+
> The download goes through the Aurora OSS token dispenser, a free third-party
|
|
1876
|
+
> service that mints an anonymous Play account. It refuses with **HTTP 403**
|
|
1877
|
+
> when it is rate limiting and **5xx** when it is having a bad minute, so the
|
|
1878
|
+
> request is retried a few times with a growing wait before giving up.
|
|
1879
|
+
> `WA_PLAY_RETRY_DELAY_MS` sets that wait (default `2000`, doubling each time).
|
|
1880
|
+
> If it still refuses, nothing is wrong with your setup — pass an APK path to
|
|
1881
|
+
> `wa apk-material` instead, which is the same thing without the middleman.
|
|
1872
1882
|
|
|
1873
1883
|
The rest of this section is what happens behind that one command, and how to
|
|
1874
1884
|
drive each part yourself.
|
package/cli.js
CHANGED
|
@@ -2833,6 +2833,16 @@ async function main() {
|
|
|
2833
2833
|
// first question land in the wrong prompt.
|
|
2834
2834
|
_sessDir = await askSessionDir(cmd, flags.session);
|
|
2835
2835
|
|
|
2836
|
+
// One source of truth for the rest of the run. The library resolves the files
|
|
2837
|
+
// that belong to the installation rather than to a number — the Android token
|
|
2838
|
+
// material above all — through SessionPaths.defaultBaseDir(), which reads
|
|
2839
|
+
// WA_SESSION_DIR. The CLI, though, can arrive at its directory three other
|
|
2840
|
+
// ways: --session, the folder remembered from the setup prompt, or the home
|
|
2841
|
+
// default. Without publishing the answer here, `wa apk-material` wrote into
|
|
2842
|
+
// the resolved directory while registration went looking in ~/.waSession and
|
|
2843
|
+
// reported no material at all.
|
|
2844
|
+
process.env.WA_SESSION_DIR = _sessDir;
|
|
2845
|
+
|
|
2836
2846
|
await askDebugMode(cmd);
|
|
2837
2847
|
await askDonation(cmd);
|
|
2838
2848
|
|
package/lib/PlayStore.js
CHANGED
|
@@ -158,19 +158,68 @@ function _request(url, opts) {
|
|
|
158
158
|
|
|
159
159
|
// ─── Auth ─────────────────────────────────────────────────────────────────────
|
|
160
160
|
|
|
161
|
+
// How often the dispenser is asked before giving up, and how long between tries.
|
|
162
|
+
//
|
|
163
|
+
// The dispenser is a free service shared by everyone who uses it, and the way it
|
|
164
|
+
// sheds load is to refuse — 403 when it is rate limiting, 5xx when it is having
|
|
165
|
+
// a bad minute. Both are usually over in seconds, so one refusal is not an
|
|
166
|
+
// answer worth reporting to the caller. The waits grow so a dispenser that is
|
|
167
|
+
// genuinely busy is not hammered: roughly 2s, then 4s, then 8s.
|
|
168
|
+
const DISPENSER_ATTEMPTS = 4;
|
|
169
|
+
const DISPENSER_BASE_DELAY = 2000;
|
|
170
|
+
|
|
171
|
+
// The wait, overridable. Someone behind a dispenser that is having a long day
|
|
172
|
+
// may want to wait longer between tries, and the test suite wants not to wait
|
|
173
|
+
// at all. 0 is valid and means retry immediately.
|
|
174
|
+
function _dispenserDelay() {
|
|
175
|
+
const n = Number(process.env.WA_PLAY_RETRY_DELAY_MS);
|
|
176
|
+
return Number.isFinite(n) && n >= 0 ? n : DISPENSER_BASE_DELAY;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Worth asking again, or not. A refusal that says "too many, come back" is; a
|
|
180
|
+
// 400 saying the request itself is malformed never will be, and retrying it
|
|
181
|
+
// only makes the caller wait three times as long for the same error.
|
|
182
|
+
function _dispenserRetryable(status) {
|
|
183
|
+
return status === 403 || status === 408 || status === 429 || status >= 500;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const _sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
187
|
+
|
|
161
188
|
async function fetchAnonymousAuth() {
|
|
162
189
|
const body = JSON.stringify(DEVICE_PROFILE);
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
190
|
+
const headers = {
|
|
191
|
+
'Content-Type': 'application/json',
|
|
192
|
+
'User-Agent': AURORA_USER_AGENT,
|
|
193
|
+
'Content-Length': Buffer.byteLength(body)
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
let res = null;
|
|
197
|
+
let lastErr = null;
|
|
198
|
+
for (let attempt = 1; attempt <= DISPENSER_ATTEMPTS; attempt++) {
|
|
199
|
+
try {
|
|
200
|
+
res = await _request(AURORA_DISPENSER_URL, { method: 'POST', headers, body });
|
|
201
|
+
lastErr = null;
|
|
202
|
+
if (res.status === 200) break;
|
|
203
|
+
if (!_dispenserRetryable(res.status) || attempt === DISPENSER_ATTEMPTS) break;
|
|
204
|
+
_whaDbg('[DBG] PLAY dispenser HTTP ' + res.status + ' — attempt ' +
|
|
205
|
+
attempt + '/' + DISPENSER_ATTEMPTS + ', retrying');
|
|
206
|
+
} catch (e) {
|
|
207
|
+
// A dropped connection or a timeout is the same kind of transient as a
|
|
208
|
+
// 5xx, so it is retried on the same schedule rather than thrown at once.
|
|
209
|
+
lastErr = e;
|
|
210
|
+
res = null;
|
|
211
|
+
if (attempt === DISPENSER_ATTEMPTS) break;
|
|
212
|
+
_whaDbg('[DBG] PLAY dispenser ' + (e && e.message) + ' — attempt ' +
|
|
213
|
+
attempt + '/' + DISPENSER_ATTEMPTS + ', retrying');
|
|
214
|
+
}
|
|
215
|
+
await _sleep(_dispenserDelay() * Math.pow(2, attempt - 1));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (lastErr) throw lastErr;
|
|
219
|
+
|
|
172
220
|
if (res.status !== 200) {
|
|
173
221
|
throw new Error('the Aurora token dispenser answered HTTP ' + res.status +
|
|
222
|
+
' after ' + DISPENSER_ATTEMPTS + ' attempts' +
|
|
174
223
|
' — it is a free third-party service and this is what it does when it is ' +
|
|
175
224
|
'down or rate limiting. Nothing is wrong with your setup; either try again ' +
|
|
176
225
|
'later or pass an APK path instead.');
|
|
@@ -384,3 +433,9 @@ module.exports = {
|
|
|
384
433
|
AURORA_DISPENSER_URL,
|
|
385
434
|
PERSONAL_PACKAGE
|
|
386
435
|
};
|
|
436
|
+
|
|
437
|
+
// Dispenser retry internals, exposed for tests. Not part of the public API.
|
|
438
|
+
module.exports._retry = {
|
|
439
|
+
dispenserRetryable: _dispenserRetryable,
|
|
440
|
+
DISPENSER_ATTEMPTS
|
|
441
|
+
};
|
package/lib/Registration.js
CHANGED
|
@@ -957,7 +957,27 @@ function androidMaterialPath(device) {
|
|
|
957
957
|
const name = (device && device.business)
|
|
958
958
|
? 'android-apk-material-business.json'
|
|
959
959
|
: 'android-apk-material.json';
|
|
960
|
-
|
|
960
|
+
|
|
961
|
+
// The session directory, wherever it actually is — not a hardcoded
|
|
962
|
+
// ~/.waSession. `wa apk-material` writes the file into the directory the CLI
|
|
963
|
+
// resolved (--session, WA_SESSION_DIR, or the one remembered from the setup
|
|
964
|
+
// prompt), while this read it from the home default and reported "no
|
|
965
|
+
// material" for anything else. Anyone who moved their sessions had to set
|
|
966
|
+
// WA_ANDROID_APK_MATERIAL by hand to make registration find a file that was
|
|
967
|
+
// already there. SessionPaths.defaultBaseDir() is the same resolver the
|
|
968
|
+
// writer goes through, and SHARED_FILES there already names both of these as
|
|
969
|
+
// belonging to the base directory.
|
|
970
|
+
const { defaultBaseDir } = require('./SessionPaths');
|
|
971
|
+
const primary = path.join(defaultBaseDir(), name);
|
|
972
|
+
|
|
973
|
+
// Material extracted before the directory could be moved still sits in
|
|
974
|
+
// ~/.waSession. If the configured directory has none but the old default
|
|
975
|
+
// does, read that rather than claiming there is no material at all.
|
|
976
|
+
if (!fs.existsSync(primary)) {
|
|
977
|
+
const legacy = path.join(os.homedir(), '.waSession', name);
|
|
978
|
+
if (legacy !== primary && fs.existsSync(legacy)) return legacy;
|
|
979
|
+
}
|
|
980
|
+
return primary;
|
|
961
981
|
}
|
|
962
982
|
|
|
963
983
|
const _androidMaterial = {};
|
|
@@ -986,18 +1006,38 @@ function tryLoadAndroidMaterial(device) {
|
|
|
986
1006
|
* Silently ignoring WA_OS is how you end up watching an Android registration go
|
|
987
1007
|
* out under an iOS User-Agent and not knowing why.
|
|
988
1008
|
*/
|
|
1009
|
+
// A session's device identity, frozen for the whole registration.
|
|
1010
|
+
//
|
|
1011
|
+
// createNewStore captures the device when the session is created and writes it
|
|
1012
|
+
// to disk with everything else, so a session always arrives here already
|
|
1013
|
+
// carrying one. From that point it is frozen: /exist, /code, /register and
|
|
1014
|
+
// every retry read this same identity, and no request can describe one handset
|
|
1015
|
+
// while a sibling request in the same flow describes another.
|
|
1016
|
+
//
|
|
1017
|
+
// The bug this closes: the gate here used to be `registered || codePending`, so
|
|
1018
|
+
// the very first /code of a session ignored the stored device and re-read it
|
|
1019
|
+
// from the environment. /exist had already gone out as the stored device — an
|
|
1020
|
+
// Android/Samsung, say, loaded from the session file — and /code then went out
|
|
1021
|
+
// as the environment default (iOS/iPhone). One number, two phones, in one flow.
|
|
1022
|
+
// That is precisely the shape a "download the official app" block screen looks
|
|
1023
|
+
// for, and it was reproducible from the logs. See
|
|
1024
|
+
// test/registration-device-consistency.test.js.
|
|
1025
|
+
//
|
|
1026
|
+
// WA_OS / WA_BUSINESS therefore choose the device only for a session that does
|
|
1027
|
+
// not have one yet — which in practice means never, since createNewStore always
|
|
1028
|
+
// sets it. To change the platform of an existing session, start it over: delete
|
|
1029
|
+
// the session file, and the next createNewStore picks up the current WA_OS.
|
|
989
1030
|
function deviceForRegistration(store, opts) {
|
|
990
1031
|
const env = getDeviceConfig();
|
|
991
1032
|
const stored = store && store.device;
|
|
992
|
-
|
|
993
|
-
if (!settled || !stored) return env;
|
|
1033
|
+
if (!stored || !stored.os) return env;
|
|
994
1034
|
|
|
995
1035
|
const say = (opts && opts.onProgress) || (m => _whaDbg('[DBG] REG ' + m));
|
|
996
1036
|
|
|
997
1037
|
if (stored.os !== env.os) {
|
|
998
1038
|
say('note: this session was started as ' + stored.os + ', so it stays ' + stored.os +
|
|
999
|
-
' — WA_OS=' + env.os + ' does not apply to a number that
|
|
1000
|
-
'
|
|
1039
|
+
' — WA_OS=' + env.os + ' does not apply to a number that already has a ' +
|
|
1040
|
+
'device. Delete the session file to start it over as ' + env.os + '.');
|
|
1001
1041
|
}
|
|
1002
1042
|
|
|
1003
1043
|
// The same rule for the variant: an account is created as Business or as
|
|
@@ -1007,13 +1047,36 @@ function deviceForRegistration(store, opts) {
|
|
|
1007
1047
|
if (!!stored.business !== !!env.business) {
|
|
1008
1048
|
say('note: this session was started as ' + (stored.business ? 'Business' : 'consumer') +
|
|
1009
1049
|
', so it stays that — WA_BUSINESS=' + (env.business ? '1' : '0') +
|
|
1010
|
-
' does not apply to a number that
|
|
1050
|
+
' does not apply to a number that already has a device. ' +
|
|
1011
1051
|
'Delete the session file to start it over.');
|
|
1012
1052
|
}
|
|
1013
1053
|
|
|
1014
1054
|
return stored;
|
|
1015
1055
|
}
|
|
1016
1056
|
|
|
1057
|
+
// The device identity a request is about to go out with, logged so /exist and
|
|
1058
|
+
// /code can be compared side by side. Gated behind the debug channel — it costs
|
|
1059
|
+
// nothing when debug is off, and when a registration misbehaves it is the first
|
|
1060
|
+
// thing worth seeing. The User-Agent is built the same way the real request
|
|
1061
|
+
// builds it, so what this prints is what actually goes on the wire.
|
|
1062
|
+
function logDeviceIdentity(tag, store, waVersion) {
|
|
1063
|
+
const stored = store && store.device;
|
|
1064
|
+
const d = stored || getDeviceConfig();
|
|
1065
|
+
const ver = waVersion || (store && store.version) || '';
|
|
1066
|
+
let ua = '';
|
|
1067
|
+
try { ua = registrationHeaders(d, ver)['User-Agent']; } catch (_) {}
|
|
1068
|
+
_whaDbg('[DBG] DEVICE_IDENTITY ' + tag +
|
|
1069
|
+
' os=' + d.os +
|
|
1070
|
+
' osVersion=' + d.osVersion +
|
|
1071
|
+
' manufacturer=' + (d.manufacturer || '-') +
|
|
1072
|
+
' model=' + (d.model || d.modelId || '-') +
|
|
1073
|
+
' modelId=' + (d.modelId || '-') +
|
|
1074
|
+
' business=' + !!d.business +
|
|
1075
|
+
' waVersion=' + (ver || '-') +
|
|
1076
|
+
' source=' + (stored ? 'store.device (frozen)' : 'getDeviceConfig() (env/default)') +
|
|
1077
|
+
' ua="' + ua + '"');
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1017
1080
|
/**
|
|
1018
1081
|
* Have the Android token material ready, fetching the APK if it is not.
|
|
1019
1082
|
*
|
|
@@ -1983,6 +2046,7 @@ async function assertRegistrationKeys(store, waVersion) {
|
|
|
1983
2046
|
|
|
1984
2047
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1985
2048
|
try {
|
|
2049
|
+
logDeviceIdentity('before /exist', store, waVersion);
|
|
1986
2050
|
const result = await sendRequest('/exist', store, waVersion, false, null);
|
|
1987
2051
|
// reason === 'incorrect' → keys not found → fresh
|
|
1988
2052
|
if (result && result.reason === 'incorrect') {
|
|
@@ -2087,6 +2151,7 @@ async function requestSmsCode(store, method, opts) {
|
|
|
2087
2151
|
const verifyScreen = currentVerifyScreen(store);
|
|
2088
2152
|
await sendFunnelLog(store, waVersion, verifyScreen, 'request_code', 'request_code_attempt');
|
|
2089
2153
|
|
|
2154
|
+
logDeviceIdentity('before /code', store, waVersion);
|
|
2090
2155
|
const result = await sendRequest('/code', store, waVersion, true, extra);
|
|
2091
2156
|
|
|
2092
2157
|
const status = result.status;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whalibmob",
|
|
3
|
-
"version": "5.29.
|
|
3
|
+
"version": "5.29.6",
|
|
4
4
|
"description": "Node.js library for WhatsApp — register a number over SMS, or link as a companion by QR. Signal E2E encryption, media, groups, channels.",
|
|
5
5
|
"author": "Kunboruto20",
|
|
6
6
|
"main": "index.js",
|