whalibmob 5.1.1 → 5.1.3
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/cli.js +20 -11
- package/lib/Registration.js +142 -25
- package/lib/Store.js +3 -0
- package/package.json +10 -2
package/cli.js
CHANGED
|
@@ -789,11 +789,8 @@ async function handleLine(line) {
|
|
|
789
789
|
if (!store) {
|
|
790
790
|
store = createNewStore(ph);
|
|
791
791
|
saveStore(store, sessFile);
|
|
792
|
-
} else {
|
|
793
|
-
//
|
|
794
|
-
// If they are (not "incorrect"), generate fresh keys so the new
|
|
795
|
-
// code request and confirm use matching fresh keys — mirrors Cobalt's
|
|
796
|
-
// assertRegistrationKeys() behaviour.
|
|
792
|
+
} else if (!store.codePending && !store.registered) {
|
|
793
|
+
// Only check /exist when keys were never used to request a code.
|
|
797
794
|
out('checking device keys...');
|
|
798
795
|
const waVersion = await fetchIosVersion();
|
|
799
796
|
const fresh = await assertRegistrationKeys(store, waVersion);
|
|
@@ -806,7 +803,7 @@ async function handleLine(line) {
|
|
|
806
803
|
}
|
|
807
804
|
out('requesting ' + method + ' code for +' + ph + '...');
|
|
808
805
|
const r = await requestSmsCode(store, method);
|
|
809
|
-
|
|
806
|
+
store.codePending = true;
|
|
810
807
|
saveStore(store, sessFile);
|
|
811
808
|
out(' status ' + (r && r.status));
|
|
812
809
|
out(' important: enter the code within 10 minutes');
|
|
@@ -822,7 +819,10 @@ async function handleLine(line) {
|
|
|
822
819
|
const r = await verifyCode(store, code);
|
|
823
820
|
if (r && (r.status === 'ok' || r.status === 'sent' || r.status === 'verified')) {
|
|
824
821
|
if (!fs.existsSync(_sessDir)) fs.mkdirSync(_sessDir, { recursive: true });
|
|
825
|
-
|
|
822
|
+
const finalStore = r.store || store;
|
|
823
|
+
finalStore.registered = true;
|
|
824
|
+
finalStore.codePending = false;
|
|
825
|
+
saveStore(finalStore, file);
|
|
826
826
|
out('registered session saved to ' + file);
|
|
827
827
|
out('now run: /connect ' + ph);
|
|
828
828
|
} else {
|
|
@@ -944,9 +944,13 @@ async function main() {
|
|
|
944
944
|
const sessFile = path.join(_sessDir, `${ph}.json`);
|
|
945
945
|
let store = loadStore(sessFile);
|
|
946
946
|
if (!store) {
|
|
947
|
+
// Brand new — generate fresh keys, save immediately, no need to check /exist
|
|
947
948
|
store = createNewStore(ph);
|
|
948
949
|
saveStore(store, sessFile);
|
|
949
|
-
} else {
|
|
950
|
+
} else if (!store.codePending && !store.registered) {
|
|
951
|
+
// Existing store but code was never sent and not registered — check if
|
|
952
|
+
// keys are already taken (e.g. leftover from a previous failed attempt).
|
|
953
|
+
// FIX: only 1 /exist call now (was 2), and skipped entirely when codePending.
|
|
950
954
|
out('checking device keys...');
|
|
951
955
|
const waVersion = await fetchIosVersion();
|
|
952
956
|
const fresh = await assertRegistrationKeys(store, waVersion);
|
|
@@ -956,9 +960,12 @@ async function main() {
|
|
|
956
960
|
saveStore(store, sessFile);
|
|
957
961
|
}
|
|
958
962
|
}
|
|
963
|
+
// If store.codePending === true, keys were already accepted by WhatsApp in a
|
|
964
|
+
// prior /code request — reuse the exact same store without any /exist call.
|
|
959
965
|
out('requesting ' + method + ' code for +' + ph + '...');
|
|
960
966
|
try {
|
|
961
967
|
const r = await requestSmsCode(store, method);
|
|
968
|
+
store.codePending = true;
|
|
962
969
|
saveStore(store, sessFile);
|
|
963
970
|
out(' status ' + (r && r.status));
|
|
964
971
|
if (r && (r.status === 'sent' || r.status === 'ok')) {
|
|
@@ -966,8 +973,7 @@ async function main() {
|
|
|
966
973
|
out(' run: wa registration --register ' + ph + ' --code <code>');
|
|
967
974
|
}
|
|
968
975
|
} catch (e) {
|
|
969
|
-
|
|
970
|
-
else fail(e.message);
|
|
976
|
+
out(' ' + (e.message || String(e)));
|
|
971
977
|
}
|
|
972
978
|
out('\nstaying in shell — use /reg confirm ' + ph + ' <code> to complete');
|
|
973
979
|
openShell(); _rl.prompt();
|
|
@@ -986,7 +992,10 @@ async function main() {
|
|
|
986
992
|
const r = await verifyCode(store, code);
|
|
987
993
|
if (r && (r.status === 'ok' || r.status === 'sent' || r.status === 'verified')) {
|
|
988
994
|
if (!fs.existsSync(_sessDir)) fs.mkdirSync(_sessDir, { recursive: true });
|
|
989
|
-
|
|
995
|
+
const finalStore = r.store || store;
|
|
996
|
+
finalStore.registered = true;
|
|
997
|
+
finalStore.codePending = false;
|
|
998
|
+
saveStore(finalStore, file);
|
|
990
999
|
out('registered session saved to ' + file);
|
|
991
1000
|
out('run: wa connect ' + ph);
|
|
992
1001
|
} else {
|
package/lib/Registration.js
CHANGED
|
@@ -1,8 +1,87 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
-
const https
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const tls = require('tls');
|
|
5
6
|
const curveJs = require('curve25519-js');
|
|
7
|
+
|
|
8
|
+
// ---------- SOCKS5 / Tor support ----------
|
|
9
|
+
// Set TOR_PROXY=socks5://127.0.0.1:9050 (or any socks5 host) to route all
|
|
10
|
+
// WhatsApp registration traffic through Tor / a residential proxy.
|
|
11
|
+
const SOCKS_LIB = '/home/runner/workspace/.config/npm/node_global/lib/node_modules/socks/build/index.js';
|
|
12
|
+
|
|
13
|
+
async function httpPostViaSocks(path, body, waVersion, proxyUrl) {
|
|
14
|
+
const url = new URL(proxyUrl);
|
|
15
|
+
const pHost = url.hostname;
|
|
16
|
+
const pPort = parseInt(url.port) || 1080;
|
|
17
|
+
const dHost = 'v.whatsapp.net';
|
|
18
|
+
const dPort = 443;
|
|
19
|
+
|
|
20
|
+
const { SocksClient } = require(SOCKS_LIB);
|
|
21
|
+
const { socket: rawSocket } = await SocksClient.createConnection({
|
|
22
|
+
proxy: { host: pHost, port: pPort, type: 5 },
|
|
23
|
+
command: 'connect',
|
|
24
|
+
destination: { host: dHost, port: dPort }
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const tlsSocket = tls.connect({
|
|
28
|
+
socket: rawSocket,
|
|
29
|
+
host: dHost,
|
|
30
|
+
servername: dHost,
|
|
31
|
+
rejectUnauthorized: true
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await new Promise((res, rej) => {
|
|
35
|
+
tlsSocket.once('secureConnect', res);
|
|
36
|
+
tlsSocket.once('error', rej);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const userAgent = `WhatsApp/${waVersion} iOS/${require('./constants').IOS_DEVICE.osVersion} Device/${require('./constants').IOS_DEVICE.model}`;
|
|
40
|
+
const req = [
|
|
41
|
+
`POST /v2${path} HTTP/1.1`,
|
|
42
|
+
`Host: ${dHost}`,
|
|
43
|
+
`User-Agent: ${userAgent}`,
|
|
44
|
+
`Content-Type: application/x-www-form-urlencoded`,
|
|
45
|
+
`Content-Length: ${Buffer.byteLength(body)}`,
|
|
46
|
+
`Connection: close`,
|
|
47
|
+
'',
|
|
48
|
+
body
|
|
49
|
+
].join('\r\n');
|
|
50
|
+
|
|
51
|
+
tlsSocket.write(req);
|
|
52
|
+
|
|
53
|
+
const chunks = [];
|
|
54
|
+
await new Promise((res, rej) => {
|
|
55
|
+
tlsSocket.on('data', d => chunks.push(d));
|
|
56
|
+
tlsSocket.on('end', res);
|
|
57
|
+
tlsSocket.on('error', rej);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const raw = Buffer.concat(chunks);
|
|
61
|
+
const rawStr = raw.toString('utf8');
|
|
62
|
+
const headerEnd = rawStr.indexOf('\r\n\r\n');
|
|
63
|
+
const headerPart = rawStr.slice(0, headerEnd);
|
|
64
|
+
const httpStatus = parseInt(rawStr.split(' ')[1]);
|
|
65
|
+
let bodyStr = rawStr.slice(headerEnd + 4);
|
|
66
|
+
|
|
67
|
+
// Handle chunked transfer encoding
|
|
68
|
+
if (/transfer-encoding:\s*chunked/i.test(headerPart)) {
|
|
69
|
+
let decoded = '';
|
|
70
|
+
let pos = 0;
|
|
71
|
+
while (pos < bodyStr.length) {
|
|
72
|
+
const lineEnd = bodyStr.indexOf('\r\n', pos);
|
|
73
|
+
if (lineEnd === -1) break;
|
|
74
|
+
const chunkSize = parseInt(bodyStr.slice(pos, lineEnd), 16);
|
|
75
|
+
if (!chunkSize) break;
|
|
76
|
+
decoded += bodyStr.slice(lineEnd + 2, lineEnd + 2 + chunkSize);
|
|
77
|
+
pos = lineEnd + 2 + chunkSize + 2;
|
|
78
|
+
}
|
|
79
|
+
bodyStr = decoded;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (httpStatus !== 200) throw new Error(`HTTP ${httpStatus} ${path}: ${bodyStr}`);
|
|
83
|
+
try { return JSON.parse(bodyStr); } catch (_) { return { raw: bodyStr }; }
|
|
84
|
+
}
|
|
6
85
|
const {
|
|
7
86
|
REGISTRATION_ENDPOINT,
|
|
8
87
|
REGISTRATION_PUBLIC_KEY,
|
|
@@ -278,6 +357,9 @@ function encryptPayload(plaintext) {
|
|
|
278
357
|
// ---------- HTTP ----------
|
|
279
358
|
|
|
280
359
|
function httpPost(path, body, waVersion) {
|
|
360
|
+
const proxy = process.env.TOR_PROXY || process.env.SOCKS_PROXY || '';
|
|
361
|
+
if (proxy) return httpPostViaSocks(path, body, waVersion, proxy);
|
|
362
|
+
|
|
281
363
|
return new Promise((resolve, reject) => {
|
|
282
364
|
const userAgent = `WhatsApp/${waVersion} iOS/${IOS_DEVICE.osVersion} Device/${IOS_DEVICE.model}`;
|
|
283
365
|
const opts = {
|
|
@@ -426,15 +508,20 @@ async function checkNumberStatus(phoneNumber) {
|
|
|
426
508
|
note: 'Could not determine status. Possible datacenter IP restriction. Try a residential proxy.' };
|
|
427
509
|
}
|
|
428
510
|
|
|
429
|
-
// ---------- assertRegistrationKeys (mirrors Cobalt) ----------
|
|
430
|
-
//
|
|
431
|
-
// Returns true
|
|
511
|
+
// ---------- assertRegistrationKeys (mirrors Cobalt exactly) ----------
|
|
512
|
+
// Cobalt does TWO /exist attempts before giving up.
|
|
513
|
+
// Returns true → keys are fresh (safe to proceed with /code).
|
|
514
|
+
// Returns false → keys already registered (generate new store before /code).
|
|
432
515
|
async function assertRegistrationKeys(store, waVersion) {
|
|
433
516
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
434
517
|
try {
|
|
435
518
|
const result = await sendRequest('/exist', store, waVersion, false, null);
|
|
519
|
+
// Cobalt: if reason === 'incorrect' → keys not found → fresh
|
|
436
520
|
if (result && result.reason === 'incorrect') return true;
|
|
521
|
+
// Any non-ok status on 2nd attempt → still treat as fresh (network/server glitch)
|
|
522
|
+
if (attempt === 1 && result && result.status && result.status !== 'ok') return true;
|
|
437
523
|
} catch (_) {
|
|
524
|
+
// Network error = treat keys as fresh (mirrors Cobalt behaviour)
|
|
438
525
|
return true;
|
|
439
526
|
}
|
|
440
527
|
}
|
|
@@ -446,37 +533,67 @@ async function requestSmsCode(store, method) {
|
|
|
446
533
|
const waVersion = await fetchIosVersion();
|
|
447
534
|
store.version = waVersion;
|
|
448
535
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
536
|
+
const extra = [
|
|
537
|
+
'method', method,
|
|
538
|
+
'sim_mcc', '000',
|
|
539
|
+
'sim_mnc', '000',
|
|
540
|
+
'reason', '',
|
|
541
|
+
'cellular_strength', '1'
|
|
542
|
+
];
|
|
543
|
+
|
|
544
|
+
// Mirror Cobalt's while(true) loop exactly:
|
|
545
|
+
// 1. Try /code
|
|
546
|
+
// 2. If success → return
|
|
547
|
+
// 3. If too_recent/too_many → throw immediately (isTooRecent)
|
|
548
|
+
// 4. If no_routes → throw immediately with useful hint (isRegistrationBlocked)
|
|
549
|
+
// 5. If same error as last attempt → throw (prevents infinite loop)
|
|
550
|
+
// 6. Otherwise → retry ONCE (Cobalt's default behavior for unknown errors like "blocked")
|
|
551
|
+
let lastReason = null;
|
|
552
|
+
while (true) {
|
|
461
553
|
const result = await sendRequest('/code', store, waVersion, true, extra);
|
|
462
|
-
lastResult = result;
|
|
463
554
|
|
|
464
555
|
const status = result.status;
|
|
465
556
|
if (status === 'ok' || status === 'sent') return result;
|
|
466
557
|
|
|
467
|
-
const reason = result.reason ||
|
|
468
|
-
|
|
469
|
-
|
|
558
|
+
const reason = result.reason || status || '';
|
|
559
|
+
|
|
560
|
+
// isTooRecent() — throw immediately
|
|
561
|
+
if (/too_recent|too_many|too_many_guesses|too_many_all_methods/i.test(reason) ||
|
|
562
|
+
/too_recent|too_many|too_many_guesses|too_many_all_methods/i.test(status)) {
|
|
563
|
+
const waitSec = result.sms_wait || result.retry_after || null;
|
|
564
|
+
const waitMsg = waitSec ? ` (wait ${Math.ceil(waitSec / 60)} min, ${waitSec}s)` : '';
|
|
565
|
+
throw new Error('code already sent recently — check your phone or wait a few minutes (' + reason + ')' + waitMsg);
|
|
470
566
|
}
|
|
567
|
+
|
|
568
|
+
// isRegistrationBlocked() — only "no_routes" per Cobalt
|
|
471
569
|
if (reason === 'no_routes' || status === 'no_routes') {
|
|
472
|
-
|
|
570
|
+
if (method === 'wa_old') {
|
|
571
|
+
throw new Error(
|
|
572
|
+
'Registration blocked (no_routes): number may not be on WhatsApp, ' +
|
|
573
|
+
'or try a different platform. Try: --method sms'
|
|
574
|
+
);
|
|
575
|
+
} else {
|
|
576
|
+
throw new Error(
|
|
577
|
+
'Registration blocked (no_routes): try using WhatsApp OTP. ' +
|
|
578
|
+
'Try: --method wa_old'
|
|
579
|
+
);
|
|
580
|
+
}
|
|
473
581
|
}
|
|
474
|
-
|
|
475
|
-
|
|
582
|
+
|
|
583
|
+
// custom_block_screen = WhatsApp security block (IP/number flagged)
|
|
584
|
+
if (result.custom_block_screen) {
|
|
585
|
+
const body = result.custom_block_screen.body || 'blocked by WhatsApp security';
|
|
586
|
+
throw new Error(`WhatsApp security block: "${body}" — use a residential IP/proxy`);
|
|
476
587
|
}
|
|
477
|
-
}
|
|
478
588
|
|
|
479
|
-
|
|
589
|
+
// Same error twice in a row → give up (Cobalt: Objects.equals(reason, lastError))
|
|
590
|
+
if (reason && reason === lastReason) {
|
|
591
|
+
throw new Error(`Registration error (${method}): ${reason}`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// First occurrence of unknown error → retry once (Cobalt's default loop behaviour)
|
|
595
|
+
lastReason = reason;
|
|
596
|
+
}
|
|
480
597
|
}
|
|
481
598
|
|
|
482
599
|
async function verifyCode(store, code) {
|
package/lib/Store.js
CHANGED
|
@@ -73,6 +73,7 @@ function createNewStore(phoneNumber) {
|
|
|
73
73
|
deviceId,
|
|
74
74
|
identityId,
|
|
75
75
|
registered: false,
|
|
76
|
+
codePending: false, // true after /code request, cleared on successful /register
|
|
76
77
|
name: 'User',
|
|
77
78
|
version: IOS_VERSION_FALLBACK,
|
|
78
79
|
device: IOS_DEVICE,
|
|
@@ -108,6 +109,7 @@ function storeToJson(store) {
|
|
|
108
109
|
deviceId: store.deviceId.toString('base64'),
|
|
109
110
|
identityId: store.identityId.toString('base64'),
|
|
110
111
|
registered: store.registered,
|
|
112
|
+
codePending: store.codePending || false,
|
|
111
113
|
name: store.name,
|
|
112
114
|
version: store.version,
|
|
113
115
|
device: store.device,
|
|
@@ -139,6 +141,7 @@ function storeFromJson(obj) {
|
|
|
139
141
|
deviceId: Buffer.from(obj.deviceId, 'base64'),
|
|
140
142
|
identityId: Buffer.from(obj.identityId, 'base64'),
|
|
141
143
|
registered: obj.registered,
|
|
144
|
+
codePending: obj.codePending || false,
|
|
142
145
|
name: obj.name || 'User',
|
|
143
146
|
version: obj.version || IOS_VERSION_FALLBACK,
|
|
144
147
|
device: obj.device || IOS_DEVICE,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whalibmob",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.3",
|
|
4
4
|
"description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
"ios",
|
|
24
24
|
"multi-device"
|
|
25
25
|
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/Kunboruto20/whalibmob.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/Kunboruto20/whalibmob#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/Kunboruto20/whalibmob/issues"
|
|
33
|
+
},
|
|
26
34
|
"dependencies": {
|
|
27
35
|
"@noble/curves": "^1.8.1",
|
|
28
36
|
"@noble/hashes": "^1.7.2",
|
|
@@ -30,4 +38,4 @@
|
|
|
30
38
|
"protobufjs": "^6.11.4",
|
|
31
39
|
"uuid": "^11.1.0"
|
|
32
40
|
}
|
|
33
|
-
}
|
|
41
|
+
}
|