whalibmob 5.5.93 → 5.6.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 CHANGED
@@ -436,7 +436,8 @@ const HELP = `
436
436
  /reg confirm <phone> <code> complete registration
437
437
 
438
438
  Connection
439
- /connect <phone> connect to WhatsApp
439
+ /connect <phone> [sms|pair] connect to WhatsApp (asks which if unset)
440
+ /pair <phone> [code] link to an existing account by 8-digit code
440
441
  /disconnect disconnect
441
442
  /reconnect force reconnection
442
443
  /session show session info
@@ -571,7 +572,19 @@ function attachEvents(client) {
571
572
 
572
573
  client.on('auth_failure', (f) => {
573
574
  _rl && _rl.pause();
574
- fail('session revoked: ' + f.reason + ' — re-register with /reg code');
575
+ const how = client._mode === 'web'
576
+ ? 'link again with /pair'
577
+ : 're-register with /reg code';
578
+ fail('session revoked: ' + f.reason + ' — ' + how);
579
+ _rl && (_rl.resume(), _rl.prompt(true));
580
+ });
581
+
582
+ // The server declined the client, not the session. Nothing to re-register.
583
+ client.on('client_rejected', (r) => {
584
+ _rl && _rl.pause();
585
+ fail('rejected by WhatsApp (' + r.reason +
586
+ (r.location ? ', edge ' + r.location : '') + ')');
587
+ out(' ' + r.message);
575
588
  _rl && (_rl.resume(), _rl.prompt(true));
576
589
  });
577
590
 
@@ -604,6 +617,108 @@ function openShell(prompt) {
604
617
 
605
618
  // ─── connect helper ───────────────────────────────────────────────────────────
606
619
 
620
+ // Which way in: register this number over SMS as the account's own device, or
621
+ // link to an account that already exists the way WhatsApp Web does.
622
+ //
623
+ // Asked only when it is genuinely ambiguous. If exactly one kind of session is
624
+ // already on disk that one wins, and a non-interactive stdin never blocks on a
625
+ // question nobody is there to answer.
626
+ function hasMobileSession(phone) {
627
+ return fs.existsSync(path.join(_sessDir, phone + '.json'));
628
+ }
629
+
630
+ function hasWebSession(phone) {
631
+ const f = path.join(_sessDir, phone + '.web.json');
632
+ if (!fs.existsSync(f)) return false;
633
+ try {
634
+ const j = JSON.parse(fs.readFileSync(f, 'utf8'));
635
+ return !!(j.registered && j.me && j.me.id);
636
+ } catch (_) { return false; }
637
+ }
638
+
639
+ function askLoginMethod(phone) {
640
+ const mob = hasMobileSession(phone);
641
+ const web = hasWebSession(phone);
642
+ if (web && !mob) return Promise.resolve('pairing');
643
+ if (mob && !web) return Promise.resolve('sms');
644
+ if (!process.stdin.isTTY) return Promise.resolve(mob ? 'sms' : 'pairing');
645
+
646
+ return new Promise(resolve => {
647
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
648
+ out('');
649
+ out(' how do you want to connect?');
650
+ out(' 1) sms register this number as its own device');
651
+ out(' 2) pairing code link to an existing WhatsApp account (8-digit code)');
652
+ rl.question(' sms or pairing code? [1/2] ', (answer) => {
653
+ rl.close();
654
+ const a = String(answer).trim().toLowerCase();
655
+ resolve((a === '2' || a.startsWith('p')) ? 'pairing' : 'sms');
656
+ });
657
+ });
658
+ }
659
+
660
+ // Link as a companion device.
661
+ //
662
+ // Connect first, then ask for the code: the request rides on the encrypted
663
+ // channel, so there has to be one before there can be a code.
664
+ async function doConnectWeb(phone, opts) {
665
+ opts = opts || {};
666
+ phone = normalizePhone(phone);
667
+ const client = new WhalibmobClient({ sessionDir: _sessDir });
668
+ attachEvents(client);
669
+
670
+ client.on('pair_device', () => { /* QR path — nothing to draw in a terminal */ });
671
+
672
+ client.once('paired', (p) => {
673
+ out('');
674
+ out(' linked as ' + p.jid + (p.lid ? ' (' + p.lid + ')' : ''));
675
+ out(' device slot ' + p.deviceIndex + (p.platform ? ' · primary is ' + p.platform : ''));
676
+ out(' finishing handshake...');
677
+ });
678
+
679
+ client.on('history_sync', (r) => {
680
+ out(' history ' + r.syncTypeName +
681
+ ' chats=' + r.chats.length +
682
+ ' contacts=' + r.contacts.length);
683
+ });
684
+
685
+ client.once('connected', () => {
686
+ _client = client;
687
+ _phone = phone;
688
+ out('connected as +' + phone + ' (web / companion)');
689
+ _rl.setPrompt('wa +' + phone + '> ');
690
+ _rl.prompt();
691
+ });
692
+
693
+ const alreadyLinked = hasWebSession(phone);
694
+
695
+ try {
696
+ await client.connectWeb(phone, { syncFullHistory: true });
697
+
698
+ if (!alreadyLinked) {
699
+ const code = await client.requestPairingCode(phone, opts.customCode);
700
+ out('');
701
+ hr();
702
+ out(' pairing code ' + code.slice(0, 4) + '-' + code.slice(4));
703
+ hr();
704
+ out(' on the phone that owns +' + phone + ':');
705
+ out(' WhatsApp → Settings → Linked Devices → Link a device');
706
+ out(' → Link with phone number instead → enter the code above');
707
+ out('');
708
+ out(' the code is valid for a few minutes; waiting...');
709
+ } else {
710
+ out('session already linked — reconnecting');
711
+ }
712
+
713
+ const keepAlive = setInterval(() => {}, 10000);
714
+ client.once('connected', () => clearInterval(keepAlive));
715
+ client.once('auth_failure', () => clearInterval(keepAlive));
716
+ } catch (e) {
717
+ fail(e.message);
718
+ _rl.prompt();
719
+ }
720
+ }
721
+
607
722
  async function doConnect(phone) {
608
723
  phone = normalizePhone(phone);
609
724
  const client = new WhalibmobClient({ sessionDir: _sessDir });
@@ -676,9 +791,16 @@ async function handleLine(line) {
676
791
  if (!_client || !_client.store) { fail('not connected'); break; }
677
792
  let _meJid = '—';
678
793
  try { _meJid = assertMeId(_client.store); } catch (_) {}
794
+ const web = _client._mode === 'web';
679
795
  hr();
680
796
  kv('phone', _client.store.phoneNumber);
681
- kv('jid', _meJid);
797
+ kv('mode', web ? 'web / companion' : 'mobile / primary');
798
+ kv('jid', web && _client.store.me ? _client.store.me.id : _meJid);
799
+ if (web) {
800
+ kv('lid', (_client.store.me && _client.store.me.lid) || '—');
801
+ kv('device', String(_client.store.deviceIndex || 0));
802
+ kv('primary', _client.store.platform || '—');
803
+ }
682
804
  kv('name', _client.store.pushName || _client.store.name || '—');
683
805
  kv('session', _sessDir);
684
806
  hr();
@@ -687,10 +809,30 @@ async function handleLine(line) {
687
809
 
688
810
  case '/connect': {
689
811
  const ph = p[1];
690
- if (!ph) { fail('usage: /connect <phone>'); break; }
812
+ if (!ph) { fail('usage: /connect <phone> [sms|pair]'); break; }
691
813
  if (_client && _client.connected) { fail('already connected'); break; }
814
+ const phn = normalizePhone(ph);
815
+ const forced = (p[2] || '').toLowerCase();
816
+ const method = forced === 'pair' || forced === 'pairing' ? 'pairing'
817
+ : forced === 'sms' ? 'sms'
818
+ : await askLoginMethod(phn);
692
819
  out('connecting...');
693
- await doConnect(ph);
820
+ if (method === 'pairing') await doConnectWeb(phn);
821
+ else await doConnect(phn);
822
+ break;
823
+ }
824
+
825
+ case '/pair': {
826
+ const ph = p[1] || _phone;
827
+ if (!ph) {
828
+ fail('usage: /pair <phone> [8-char-code]');
829
+ out(' the phone number is the account you want to link to, digits only');
830
+ out(' example: /pair 40756469325');
831
+ break;
832
+ }
833
+ if (_client && _client.connected) { fail('already connected — /disconnect first'); break; }
834
+ out('connecting...');
835
+ await doConnectWeb(ph, { customCode: p[2] });
694
836
  break;
695
837
  }
696
838
 
@@ -1709,6 +1851,7 @@ whalibmob v${VERSION}
1709
1851
  usage:
1710
1852
  wa open interactive shell
1711
1853
  wa connect <phone> connect and open interactive shell
1854
+ wa pair <phone> [code] link to an existing account (8-digit code)
1712
1855
  wa listen <phone> connect and listen (stay-alive)
1713
1856
  wa registration --request-code <phone> request SMS code
1714
1857
  wa registration --register <phone> --code <code>
@@ -1717,6 +1860,8 @@ usage:
1717
1860
 
1718
1861
  options:
1719
1862
  --session <dir> session directory (default: ~/.waSession)
1863
+ --sms connect by registering this number over SMS
1864
+ --pair connect by linking to an existing account (8-digit code)
1720
1865
  --method sms | voice | wa_old | email (default: sms)
1721
1866
  --email <address> email address (required when --method email)
1722
1867
 
@@ -1890,9 +2035,27 @@ async function main() {
1890
2035
  if (cmd === 'connect') {
1891
2036
  const phone = normalizePhone(sub || pos[0] || flags.phone || '');
1892
2037
  if (!phone) { fail('phone number required'); process.exit(1); }
2038
+ const forced = String(flags.method || '').toLowerCase();
2039
+ const method = flags.pair || forced === 'pair' || forced === 'pairing' ? 'pairing'
2040
+ : flags.sms || forced === 'sms' ? 'sms'
2041
+ : await askLoginMethod(phone);
1893
2042
  openShell('wa +' + phone + '> ');
1894
2043
  out('connecting to +' + phone + '...');
1895
- await doConnect(phone);
2044
+ if (method === 'pairing') await doConnectWeb(phone);
2045
+ else await doConnect(phone);
2046
+ return;
2047
+ }
2048
+
2049
+ if (cmd === 'pair') {
2050
+ // `wa pair <phone> [code]` — parseArgs puts the phone in sub, so the
2051
+ // optional code is the first positional that remains.
2052
+ const phone = normalizePhone(sub || pos[0] || flags.phone || '');
2053
+ if (!phone) { fail('phone number required'); process.exit(1); }
2054
+ const custom = (sub ? pos[0] : pos[1]) ||
2055
+ (typeof flags.code === 'string' ? flags.code : undefined);
2056
+ openShell('wa +' + phone + '> ');
2057
+ out('linking +' + phone + ' to an existing WhatsApp account...');
2058
+ await doConnectWeb(phone, { customCode: custom });
1896
2059
  return;
1897
2060
  }
1898
2061
 
package/index.js CHANGED
@@ -17,6 +17,22 @@ const {
17
17
  assertMeId,
18
18
  initAuthCreds
19
19
  } = require('./lib/auth-utils');
20
+ const {
21
+ createNewWebStore, saveWebStore, loadWebStore, webSessionPath
22
+ } = require('./lib/WebStore');
23
+ const {
24
+ generatePairingCode,
25
+ derivePairingCodeKey,
26
+ buildWrappedEphemeralPub,
27
+ decipherLinkPublicKey,
28
+ buildCompanionFinishBundle
29
+ } = require('./lib/PairingCode');
30
+ const { configureSuccessfulPairing } = require('./lib/CompanionPairing');
31
+ const { fetchWaWebVersion } = require('./lib/WebVersion');
32
+ const {
33
+ encodeCompanionRegisterPayload,
34
+ encodeCompanionLoginPayload
35
+ } = require('./lib/webproto');
20
36
  const {
21
37
  processHistorySyncNotification,
22
38
  decodeHistorySyncNotification,
@@ -53,6 +69,20 @@ module.exports = {
53
69
  storeFromJson,
54
70
  toSixParts,
55
71
  fromSixParts,
72
+ // WhatsApp Web (companion) mode — pairing-code linking to an existing account
73
+ createNewWebStore,
74
+ saveWebStore,
75
+ loadWebStore,
76
+ webSessionPath,
77
+ generatePairingCode,
78
+ derivePairingCodeKey,
79
+ buildWrappedEphemeralPub,
80
+ decipherLinkPublicKey,
81
+ buildCompanionFinishBundle,
82
+ configureSuccessfulPairing,
83
+ encodeCompanionRegisterPayload,
84
+ encodeCompanionLoginPayload,
85
+ fetchWaWebVersion,
56
86
  // Signal / encryption internals
57
87
  SignalProtocol,
58
88
  SignalStore,