whalibmob 5.5.93 → 5.6.2

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
@@ -604,6 +605,108 @@ function openShell(prompt) {
604
605
 
605
606
  // ─── connect helper ───────────────────────────────────────────────────────────
606
607
 
608
+ // Which way in: register this number over SMS as the account's own device, or
609
+ // link to an account that already exists the way WhatsApp Web does.
610
+ //
611
+ // Asked only when it is genuinely ambiguous. If exactly one kind of session is
612
+ // already on disk that one wins, and a non-interactive stdin never blocks on a
613
+ // question nobody is there to answer.
614
+ function hasMobileSession(phone) {
615
+ return fs.existsSync(path.join(_sessDir, phone + '.json'));
616
+ }
617
+
618
+ function hasWebSession(phone) {
619
+ const f = path.join(_sessDir, phone + '.web.json');
620
+ if (!fs.existsSync(f)) return false;
621
+ try {
622
+ const j = JSON.parse(fs.readFileSync(f, 'utf8'));
623
+ return !!(j.registered && j.me && j.me.id);
624
+ } catch (_) { return false; }
625
+ }
626
+
627
+ function askLoginMethod(phone) {
628
+ const mob = hasMobileSession(phone);
629
+ const web = hasWebSession(phone);
630
+ if (web && !mob) return Promise.resolve('pairing');
631
+ if (mob && !web) return Promise.resolve('sms');
632
+ if (!process.stdin.isTTY) return Promise.resolve(mob ? 'sms' : 'pairing');
633
+
634
+ return new Promise(resolve => {
635
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
636
+ out('');
637
+ out(' how do you want to connect?');
638
+ out(' 1) sms register this number as its own device');
639
+ out(' 2) pairing code link to an existing WhatsApp account (8-digit code)');
640
+ rl.question(' sms or pairing code? [1/2] ', (answer) => {
641
+ rl.close();
642
+ const a = String(answer).trim().toLowerCase();
643
+ resolve((a === '2' || a.startsWith('p')) ? 'pairing' : 'sms');
644
+ });
645
+ });
646
+ }
647
+
648
+ // Link as a companion device.
649
+ //
650
+ // Connect first, then ask for the code: the request rides on the encrypted
651
+ // channel, so there has to be one before there can be a code.
652
+ async function doConnectWeb(phone, opts) {
653
+ opts = opts || {};
654
+ phone = normalizePhone(phone);
655
+ const client = new WhalibmobClient({ sessionDir: _sessDir });
656
+ attachEvents(client);
657
+
658
+ client.on('pair_device', () => { /* QR path — nothing to draw in a terminal */ });
659
+
660
+ client.once('paired', (p) => {
661
+ out('');
662
+ out(' linked as ' + p.jid + (p.lid ? ' (' + p.lid + ')' : ''));
663
+ out(' device slot ' + p.deviceIndex + (p.platform ? ' · primary is ' + p.platform : ''));
664
+ out(' finishing handshake...');
665
+ });
666
+
667
+ client.on('history_sync', (r) => {
668
+ out(' history ' + r.syncTypeName +
669
+ ' chats=' + r.chats.length +
670
+ ' contacts=' + r.contacts.length);
671
+ });
672
+
673
+ client.once('connected', () => {
674
+ _client = client;
675
+ _phone = phone;
676
+ out('connected as +' + phone + ' (web / companion)');
677
+ _rl.setPrompt('wa +' + phone + '> ');
678
+ _rl.prompt();
679
+ });
680
+
681
+ const alreadyLinked = hasWebSession(phone);
682
+
683
+ try {
684
+ await client.connectWeb(phone, { syncFullHistory: true });
685
+
686
+ if (!alreadyLinked) {
687
+ const code = await client.requestPairingCode(phone, opts.customCode);
688
+ out('');
689
+ hr();
690
+ out(' pairing code ' + code.slice(0, 4) + '-' + code.slice(4));
691
+ hr();
692
+ out(' on the phone that owns +' + phone + ':');
693
+ out(' WhatsApp → Settings → Linked Devices → Link a device');
694
+ out(' → Link with phone number instead → enter the code above');
695
+ out('');
696
+ out(' the code is valid for a few minutes; waiting...');
697
+ } else {
698
+ out('session already linked — reconnecting');
699
+ }
700
+
701
+ const keepAlive = setInterval(() => {}, 10000);
702
+ client.once('connected', () => clearInterval(keepAlive));
703
+ client.once('auth_failure', () => clearInterval(keepAlive));
704
+ } catch (e) {
705
+ fail(e.message);
706
+ _rl.prompt();
707
+ }
708
+ }
709
+
607
710
  async function doConnect(phone) {
608
711
  phone = normalizePhone(phone);
609
712
  const client = new WhalibmobClient({ sessionDir: _sessDir });
@@ -676,9 +779,16 @@ async function handleLine(line) {
676
779
  if (!_client || !_client.store) { fail('not connected'); break; }
677
780
  let _meJid = '—';
678
781
  try { _meJid = assertMeId(_client.store); } catch (_) {}
782
+ const web = _client._mode === 'web';
679
783
  hr();
680
784
  kv('phone', _client.store.phoneNumber);
681
- kv('jid', _meJid);
785
+ kv('mode', web ? 'web / companion' : 'mobile / primary');
786
+ kv('jid', web && _client.store.me ? _client.store.me.id : _meJid);
787
+ if (web) {
788
+ kv('lid', (_client.store.me && _client.store.me.lid) || '—');
789
+ kv('device', String(_client.store.deviceIndex || 0));
790
+ kv('primary', _client.store.platform || '—');
791
+ }
682
792
  kv('name', _client.store.pushName || _client.store.name || '—');
683
793
  kv('session', _sessDir);
684
794
  hr();
@@ -687,10 +797,25 @@ async function handleLine(line) {
687
797
 
688
798
  case '/connect': {
689
799
  const ph = p[1];
690
- if (!ph) { fail('usage: /connect <phone>'); break; }
800
+ if (!ph) { fail('usage: /connect <phone> [sms|pair]'); break; }
691
801
  if (_client && _client.connected) { fail('already connected'); break; }
802
+ const phn = normalizePhone(ph);
803
+ const forced = (p[2] || '').toLowerCase();
804
+ const method = forced === 'pair' || forced === 'pairing' ? 'pairing'
805
+ : forced === 'sms' ? 'sms'
806
+ : await askLoginMethod(phn);
692
807
  out('connecting...');
693
- await doConnect(ph);
808
+ if (method === 'pairing') await doConnectWeb(phn);
809
+ else await doConnect(phn);
810
+ break;
811
+ }
812
+
813
+ case '/pair': {
814
+ const ph = p[1] || _phone;
815
+ if (!ph) { fail('usage: /pair <phone> [8-char-code]'); break; }
816
+ if (_client && _client.connected) { fail('already connected — /disconnect first'); break; }
817
+ out('connecting...');
818
+ await doConnectWeb(ph, { customCode: p[2] });
694
819
  break;
695
820
  }
696
821
 
@@ -1709,6 +1834,7 @@ whalibmob v${VERSION}
1709
1834
  usage:
1710
1835
  wa open interactive shell
1711
1836
  wa connect <phone> connect and open interactive shell
1837
+ wa pair <phone> [code] link to an existing account (8-digit code)
1712
1838
  wa listen <phone> connect and listen (stay-alive)
1713
1839
  wa registration --request-code <phone> request SMS code
1714
1840
  wa registration --register <phone> --code <code>
@@ -1717,6 +1843,8 @@ usage:
1717
1843
 
1718
1844
  options:
1719
1845
  --session <dir> session directory (default: ~/.waSession)
1846
+ --sms connect by registering this number over SMS
1847
+ --pair connect by linking to an existing account (8-digit code)
1720
1848
  --method sms | voice | wa_old | email (default: sms)
1721
1849
  --email <address> email address (required when --method email)
1722
1850
 
@@ -1890,9 +2018,27 @@ async function main() {
1890
2018
  if (cmd === 'connect') {
1891
2019
  const phone = normalizePhone(sub || pos[0] || flags.phone || '');
1892
2020
  if (!phone) { fail('phone number required'); process.exit(1); }
2021
+ const forced = String(flags.method || '').toLowerCase();
2022
+ const method = flags.pair || forced === 'pair' || forced === 'pairing' ? 'pairing'
2023
+ : flags.sms || forced === 'sms' ? 'sms'
2024
+ : await askLoginMethod(phone);
1893
2025
  openShell('wa +' + phone + '> ');
1894
2026
  out('connecting to +' + phone + '...');
1895
- await doConnect(phone);
2027
+ if (method === 'pairing') await doConnectWeb(phone);
2028
+ else await doConnect(phone);
2029
+ return;
2030
+ }
2031
+
2032
+ if (cmd === 'pair') {
2033
+ // `wa pair <phone> [code]` — parseArgs puts the phone in sub, so the
2034
+ // optional code is the first positional that remains.
2035
+ const phone = normalizePhone(sub || pos[0] || flags.phone || '');
2036
+ if (!phone) { fail('phone number required'); process.exit(1); }
2037
+ const custom = (sub ? pos[0] : pos[1]) ||
2038
+ (typeof flags.code === 'string' ? flags.code : undefined);
2039
+ openShell('wa +' + phone + '> ');
2040
+ out('linking +' + phone + ' to an existing WhatsApp account...');
2041
+ await doConnectWeb(phone, { customCode: custom });
1896
2042
  return;
1897
2043
  }
1898
2044
 
package/index.js CHANGED
@@ -17,6 +17,21 @@ 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 {
32
+ encodeCompanionRegisterPayload,
33
+ encodeCompanionLoginPayload
34
+ } = require('./lib/webproto');
20
35
  const {
21
36
  processHistorySyncNotification,
22
37
  decodeHistorySyncNotification,
@@ -53,6 +68,19 @@ module.exports = {
53
68
  storeFromJson,
54
69
  toSixParts,
55
70
  fromSixParts,
71
+ // WhatsApp Web (companion) mode — pairing-code linking to an existing account
72
+ createNewWebStore,
73
+ saveWebStore,
74
+ loadWebStore,
75
+ webSessionPath,
76
+ generatePairingCode,
77
+ derivePairingCodeKey,
78
+ buildWrappedEphemeralPub,
79
+ decipherLinkPublicKey,
80
+ buildCompanionFinishBundle,
81
+ configureSuccessfulPairing,
82
+ encodeCompanionRegisterPayload,
83
+ encodeCompanionLoginPayload,
56
84
  // Signal / encryption internals
57
85
  SignalProtocol,
58
86
  SignalStore,