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/lib/Client.js CHANGED
@@ -193,6 +193,11 @@ class WhalibmobClient extends EventEmitter {
193
193
  this._signal = null;
194
194
  this._devMgr = null;
195
195
  this._sessionDir = opts.sessionDir || process.env.HOME + '/.waSession';
196
+ // 'mobile' — registered over SMS as the account's primary device (default,
197
+ // and what every existing caller gets). 'web' — linked to an existing
198
+ // account as a companion, via pairing code.
199
+ this._mode = 'mobile';
200
+ this._webSessionFile = null;
196
201
  this._pingTimer = null;
197
202
  this._keepTimer = null;
198
203
  this._connected = false;
@@ -296,6 +301,8 @@ class WhalibmobClient extends EventEmitter {
296
301
  }
297
302
 
298
303
  async _connectSocket() {
304
+ if (this._mode === 'web') return this._connectWebSocket();
305
+
299
306
  const socket = new NoiseSocket(this._store);
300
307
  this._socket = socket;
301
308
 
@@ -313,6 +320,406 @@ class WhalibmobClient extends EventEmitter {
313
320
  return socket;
314
321
  }
315
322
 
323
+ // ──────────────────────────────────────────────────────────────────────────
324
+ // WHATSAPP WEB (companion) MODE
325
+ //
326
+ // Same library, same public API, different way in. Instead of registering a
327
+ // number over SMS as the account's primary device, we link to an account that
328
+ // already exists — the way the web client and the desktop apps do. Everything
329
+ // above the connection is unchanged: the Signal layer, the sender, the group
330
+ // and media code all read the same store fields.
331
+ //
332
+ // Two things do differ once connected, and both are handled below:
333
+ // · our own JID carries a device index, so we are no longer device 0 and
334
+ // the account's phone becomes just another device we encrypt to;
335
+ // · the primary ships history over, which a primary device never receives.
336
+ // ──────────────────────────────────────────────────────────────────────────
337
+
338
+ /**
339
+ * Connect as a companion device.
340
+ *
341
+ * If the session has never been paired this resolves as soon as the encrypted
342
+ * channel is up, leaving the caller to call requestPairingCode(). If it has,
343
+ * it behaves like connect(): resolves once the server sends <success>.
344
+ *
345
+ * @param {string} phoneNumber account phone number, digits only
346
+ * @param {object} [opts] { syncFullHistory, browser }
347
+ */
348
+ async connectWeb(phoneNumber, opts) {
349
+ return this.initWeb(phoneNumber, opts);
350
+ }
351
+
352
+ async initWeb(phoneNumber, opts) {
353
+ opts = opts || {};
354
+ phoneNumber = String(phoneNumber).replace(/\D/g, '');
355
+ this._phoneNumber = phoneNumber;
356
+ this._mode = 'web';
357
+
358
+ const { createNewWebStore, loadWebStore, saveWebStore, webSessionPath } =
359
+ require('./WebStore');
360
+
361
+ this._webSessionFile = webSessionPath(this._sessionDir, phoneNumber);
362
+ this._store = loadWebStore(this._webSessionFile) || createNewWebStore(phoneNumber);
363
+
364
+ if (opts.syncFullHistory !== undefined) this._store.syncFullHistory = !!opts.syncFullHistory;
365
+ if (opts.browser) this._store.browser = opts.browser;
366
+ saveWebStore(this._store, this._webSessionFile);
367
+
368
+ // Signal state lives in its own files so a web link and an SMS
369
+ // registration for the same number never share sessions or pre-keys.
370
+ const signalFile = path.join(this._sessionDir, `${phoneNumber}.web.signal.json`);
371
+ const skFile = path.join(this._sessionDir, `${phoneNumber}.web.sk.json`);
372
+ const tcTokenFile = path.join(this._sessionDir, `${phoneNumber}.web.tctoken.json`);
373
+
374
+ const { TcTokenStore } = require('./messages/TcTokenStore');
375
+ this._tcTokenStore = new TcTokenStore(tcTokenFile);
376
+
377
+ this._signal = SignalProtocol.fromStore(this._store, signalFile, skFile);
378
+ this._devMgr = new DeviceManager(this);
379
+ this._devMgr.attachSession(this._sessionDir, phoneNumber + '.web');
380
+
381
+ const persistedLid = this._signal.store.getLidMappings
382
+ ? this._signal.store.getLidMappings() : {};
383
+ for (const [phone, lid] of Object.entries(persistedLid)) {
384
+ this._pnToLid.set(phone, lid);
385
+ this._lidToPn.set(lid, phone);
386
+ }
387
+
388
+ if (this._store.me && this._store.me.lid) this._myLid = this._store.me.lid;
389
+
390
+ await this._connectWebSocket();
391
+ return this;
392
+ }
393
+
394
+ _saveWebStore() {
395
+ if (this._mode !== 'web' || !this._webSessionFile) return;
396
+ const { saveWebStore } = require('./WebStore');
397
+ saveWebStore(this._store, this._webSessionFile);
398
+ }
399
+
400
+ async _connectWebSocket() {
401
+ const { encodeCompanionRegisterPayload, encodeCompanionLoginPayload } =
402
+ require('./webproto');
403
+
404
+ const store = this._store;
405
+ const registered = !!store.registered && !!(store.me && store.me.id);
406
+
407
+ const payloadOpts = {
408
+ version: store.webVersion,
409
+ browser: store.browser,
410
+ countryCode: 'US',
411
+ pushName: store.name && store.name !== 'User' ? store.name : undefined,
412
+ syncFullHistory: store.syncFullHistory !== false,
413
+ registrationId: store.registrationId,
414
+ identityKeyPair: store.identityKeyPair,
415
+ signedPreKey: store.signedPreKey
416
+ };
417
+
418
+ const socket = new NoiseSocket(store, {
419
+ transport: 'web',
420
+ // An unpaired companion never gets <success> on this connection — the
421
+ // server answers with pair-device and waits for a human to act.
422
+ expectSuccess: registered,
423
+ buildPayload: () => registered
424
+ ? encodeCompanionLoginPayload(Object.assign({}, payloadOpts, {
425
+ username: store.phoneNumber,
426
+ device: store.deviceIndex || 0
427
+ }))
428
+ : encodeCompanionRegisterPayload(payloadOpts)
429
+ });
430
+
431
+ this._socket = socket;
432
+ this._sender = new MessageSender(this);
433
+
434
+ socket.on('open', (sn) => this._onOpen(sn));
435
+ socket.on('secured', () => {
436
+ _whaDbg('[DBG] WEB_CHANNEL_SECURED — awaiting pairing');
437
+ this.emit('web_ready_for_pairing');
438
+ });
439
+ socket.on('node', node => this._onNode(node));
440
+ socket.on('close', () => this._onClose());
441
+ socket.on('error', err => this.emit('error', err));
442
+
443
+ await socket.connect();
444
+ return socket;
445
+ }
446
+
447
+ /**
448
+ * Ask WhatsApp for an 8-character pairing code.
449
+ *
450
+ * The account owner enters it on their phone under
451
+ * Settings → Linked Devices → Link a device → Link with phone number instead.
452
+ * The code is a password, not an identifier: it never travels to the server
453
+ * in the clear, and both sides run it through PBKDF2 to wrap the ephemeral
454
+ * keys they exchange.
455
+ *
456
+ * Resolves with the code as soon as the request is on the wire. The link
457
+ * itself completes later — wait for the 'paired' or 'open' event.
458
+ *
459
+ * @param {string} [phoneNumber] defaults to the number initWeb was given
460
+ * @param {string} [customCode] exactly 8 characters, if you want to choose
461
+ * @returns {Promise<string>}
462
+ */
463
+ async requestPairingCode(phoneNumber, customCode) {
464
+ if (this._mode !== 'web') {
465
+ throw new Error('requestPairingCode is only available in web mode — call connectWeb() first');
466
+ }
467
+ if (!this._socket || !this._socket.secured) {
468
+ throw new Error('Not connected — call connectWeb() before requesting a pairing code');
469
+ }
470
+ if (this._store.registered) {
471
+ throw new Error('This session is already linked — no pairing code needed');
472
+ }
473
+
474
+ const { generatePairingCode, buildWrappedEphemeralPub, generateEphemeralKeyPair } =
475
+ require('./PairingCode');
476
+ const { platformTypeFor } = require('./webproto');
477
+
478
+ if (customCode != null && String(customCode).length !== 8) {
479
+ throw new Error('A custom pairing code must be exactly 8 characters');
480
+ }
481
+
482
+ const phone = String(phoneNumber || this._phoneNumber || this._store.phoneNumber)
483
+ .replace(/\D/g, '');
484
+ if (!phone) throw new Error('requestPairingCode: a phone number is required');
485
+
486
+ const code = customCode ? String(customCode).toUpperCase() : generatePairingCode();
487
+
488
+ // A fresh ephemeral pair per attempt: a code that was requested and never
489
+ // used must not be able to complete a later exchange.
490
+ this._store.pairingEphemeralKeyPair = generateEphemeralKeyPair();
491
+ this._store.pairingCode = code;
492
+ this._store.phoneNumber = phone;
493
+ this._store.me = { id: phone + '@s.whatsapp.net', name: '~', lid: null };
494
+ this._saveWebStore();
495
+
496
+ const browser = this._store.browser || ['Ubuntu', 'Chrome', '120.0.0.0'];
497
+ const wrapped = await buildWrappedEphemeralPub(code, this._store.pairingEphemeralKeyPair.public);
498
+
499
+ // The bare 32-byte curve point, not the 33-byte Signal form.
500
+ //
501
+ // whalibmob stores every public key with the 0x05 type prefix, and the
502
+ // Noise handshake strips it on the way out. This node had no such strip, so
503
+ // the primary would have hashed 33 bytes where it expected 32 and the link
504
+ // would fail with nothing to point at.
505
+ const { stripKeyPrefix } = require('./PairingCode');
506
+ const noisePub = stripKeyPrefix(this._store.noiseKeyPair.public);
507
+
508
+ const node = new BinaryNode('iq', {
509
+ id: this._genMsgId(),
510
+ to: 's.whatsapp.net',
511
+ type: 'set',
512
+ xmlns: 'md'
513
+ }, [
514
+ new BinaryNode('link_code_companion_reg', {
515
+ jid: this._store.me.id,
516
+ stage: 'companion_hello',
517
+ should_show_push_notification: 'true'
518
+ }, [
519
+ new BinaryNode('link_code_pairing_wrapped_companion_ephemeral_pub', {}, wrapped),
520
+ new BinaryNode('companion_server_auth_key_pub', {}, noisePub),
521
+ new BinaryNode('companion_platform_id', {}, Buffer.from(
522
+ String(platformTypeFor(browser[1])), 'utf8')),
523
+ new BinaryNode('companion_platform_display', {}, Buffer.from(
524
+ `${browser[1]} (${browser[0]})`, 'utf8')),
525
+ new BinaryNode('link_code_pairing_nonce', {}, Buffer.from('0', 'utf8'))
526
+ ])
527
+ ]);
528
+
529
+ _whaDbg('[DBG] REQUEST_PAIRING_CODE phone=' + phone + ' code=' + code);
530
+ this._socket.sendNode(node);
531
+ this.emit('pairing_code', { code, phoneNumber: phone });
532
+ return code;
533
+ }
534
+
535
+ /**
536
+ * Stage 3 of the pairing exchange, driven by the server.
537
+ *
538
+ * Arrives once the account owner has typed the code into their phone. We
539
+ * unwrap the primary's ephemeral key with the key derived from that code, do
540
+ * the two Diffie-Hellmans, and store adv_secret — the key pair-success will
541
+ * be verified under.
542
+ */
543
+ async _handleCompanionFinish(node) {
544
+ const { buildCompanionFinishBundle } = require('./PairingCode');
545
+
546
+ const reg = findChild(node, 'link_code_companion_reg');
547
+ if (!reg) return;
548
+
549
+ const childBuf = (desc) => {
550
+ const c = findChild(reg, desc);
551
+ if (!c) return null;
552
+ return Buffer.isBuffer(c.content) ? c.content
553
+ : ArrayBuffer.isView(c.content) ? Buffer.from(c.content)
554
+ : typeof c.content === 'string' ? Buffer.from(c.content, 'utf8')
555
+ : null;
556
+ };
557
+
558
+ const ref = childBuf('link_code_pairing_ref');
559
+ const primaryIdentityPub = childBuf('primary_identity_pub');
560
+ const wrappedEphemeral = childBuf('link_code_pairing_wrapped_primary_ephemeral_pub');
561
+
562
+ if (!ref || !primaryIdentityPub || !wrappedEphemeral) {
563
+ _whaDbg('[DBG] COMPANION_FINISH missing fields — aborting');
564
+ this.emit('error', new Error('Pairing failed: incomplete companion_finish from server'));
565
+ return;
566
+ }
567
+
568
+ try {
569
+ const { advSecretKey, wrappedKeyBundle } = await buildCompanionFinishBundle({
570
+ pairingCode: this._store.pairingCode,
571
+ pairingEphemeralPrivate: this._store.pairingEphemeralKeyPair.private,
572
+ primaryEphemeralPubWrapped: wrappedEphemeral,
573
+ primaryIdentityPub,
574
+ identityKeyPair: this._store.identityKeyPair
575
+ });
576
+
577
+ this._store.advSecretKey = advSecretKey;
578
+ this._saveWebStore();
579
+ _whaDbg('[DBG] COMPANION_FINISH adv_secret derived');
580
+
581
+ // Baileys awaits this one rather than firing and forgetting, so a server
582
+ // rejection surfaces as an error here instead of as a silent stall
583
+ // waiting for a pair-success that is never coming.
584
+ const finishResult = await this._sendIq(new BinaryNode('iq', {
585
+ to: 's.whatsapp.net',
586
+ type: 'set',
587
+ id: this._genMsgId(),
588
+ xmlns: 'md'
589
+ }, [
590
+ new BinaryNode('link_code_companion_reg', {
591
+ jid: this._store.me.id,
592
+ stage: 'companion_finish'
593
+ }, [
594
+ new BinaryNode('link_code_pairing_wrapped_key_bundle', {}, wrappedKeyBundle),
595
+ new BinaryNode('companion_identity_public', {},
596
+ require('./PairingCode').stripKeyPrefix(this._store.identityKeyPair.public)),
597
+ new BinaryNode('link_code_pairing_ref', {}, ref)
598
+ ])
599
+ ]));
600
+
601
+ if (finishResult && finishResult.attrs && finishResult.attrs.type === 'error') {
602
+ const errNode = findChild(finishResult, 'error');
603
+ const code = errNode && errNode.attrs && errNode.attrs.code;
604
+ throw new Error('server rejected companion_finish' + (code ? ' (' + code + ')' : ''));
605
+ }
606
+ _whaDbg('[DBG] COMPANION_FINISH accepted — awaiting pair-success');
607
+ } catch (err) {
608
+ _whaDbg('[DBG] COMPANION_FINISH_ERR ' + (err && err.message));
609
+ this.emit('error', new Error('Pairing failed: ' + (err && err.message)));
610
+ }
611
+ }
612
+
613
+ /**
614
+ * <iq><pair-success> — the primary has authorised us.
615
+ *
616
+ * Verify what it sent, counter-sign it, reply, and persist. The server closes
617
+ * the connection right after with a 515; the reconnect logs in normally and
618
+ * that is when history starts arriving.
619
+ */
620
+ _handlePairSuccess(node) {
621
+ const { configureSuccessfulPairing } = require('./CompanionPairing');
622
+ try {
623
+ const result = configureSuccessfulPairing(node, {
624
+ advSecretKey: this._store.advSecretKey,
625
+ identityKeyPair: this._store.identityKeyPair
626
+ }, BinaryNode);
627
+
628
+ this._store.me = result.me;
629
+ this._store.deviceIndex = result.deviceIndex;
630
+ this._store.platform = result.platform;
631
+ // The same field the mobile path fills from <success>: the signed device
632
+ // identity that rides on outgoing pkmsg stanzas.
633
+ this._store.advIdentity = result.accountEnc;
634
+ this._store.registered = true;
635
+ this._store.pairingCode = null;
636
+ if (result.me && result.me.name) this._store.name = result.me.name;
637
+ this._saveWebStore();
638
+
639
+ if (result.me && result.me.lid) {
640
+ this._myLid = result.me.lid;
641
+ const lidUser = String(result.me.lid).split('@')[0].split(':')[0];
642
+ const pnUser = String(result.me.id).split('@')[0].split(':')[0];
643
+ this._lidToPn.set(lidUser, pnUser);
644
+ this._pnToLid.set(pnUser, lidUser);
645
+ }
646
+
647
+ this._socket.sendNode(result.reply);
648
+ this._sendUnifiedSession();
649
+ _whaDbg('[DBG] PAIR_SUCCESS jid=' + result.me.id + ' lid=' + result.me.lid +
650
+ ' device=' + result.deviceIndex);
651
+ this.emit('paired', {
652
+ jid: result.me.id,
653
+ lid: result.me.lid,
654
+ deviceIndex: result.deviceIndex,
655
+ platform: result.platform
656
+ });
657
+ } catch (err) {
658
+ _whaDbg('[DBG] PAIR_SUCCESS_ERR ' + (err && err.message));
659
+ this.emit('error', err);
660
+ }
661
+ }
662
+
663
+ /**
664
+ * <iq><pair-device> — the QR path.
665
+ *
666
+ * We do not display QR codes, but the node still has to be acknowledged or
667
+ * the server keeps re-sending refs. The refs are surfaced as an event so a
668
+ * caller that wants to render a QR can.
669
+ */
670
+ _handlePairDevice(node) {
671
+ const id = node.attrs && node.attrs.id;
672
+ if (id) {
673
+ this._socket.sendNode(new BinaryNode('iq', {
674
+ to: 's.whatsapp.net', type: 'result', id
675
+ }, null));
676
+ }
677
+ const pairDevice = findChild(node, 'pair-device');
678
+ const refs = pairDevice && Array.isArray(pairDevice.content)
679
+ ? pairDevice.content
680
+ .filter(c => c && c.description === 'ref')
681
+ .map(c => (Buffer.isBuffer(c.content) ? c.content.toString('utf8') : String(c.content)))
682
+ : [];
683
+ this.emit('pair_device', { refs });
684
+ }
685
+
686
+ /**
687
+ * The JID this device sends as.
688
+ *
689
+ * A primary is device 0 and its JID is the bare number. A companion occupies
690
+ * a numbered slot, and the account's phone is then a peer we encrypt to like
691
+ * any other linked device — which is exactly the difference the fan-out code
692
+ * needs to know about.
693
+ */
694
+ _ownDeviceJid() {
695
+ const phone = String(this._store.phoneNumber);
696
+ const idx = (this._mode === 'web' && this._store.deviceIndex) || 0;
697
+ return idx > 0 ? `${phone}:${idx}@s.whatsapp.net` : `${phone}@s.whatsapp.net`;
698
+ }
699
+
700
+ // <ib><unified_session/> — the telemetry ping a web client emits after
701
+ // pairing and after each login. It carries no state we need; sending it keeps
702
+ // our traffic shaped like the client we are claiming to be. Failures are
703
+ // ignored, as they are in the reference.
704
+ _sendUnifiedSession() {
705
+ if (this._mode !== 'web' || !this._socket) return;
706
+ try {
707
+ const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
708
+ const id = ((Date.now() + 3 * 24 * 60 * 60 * 1000) % WEEK_MS).toString();
709
+ this._socket.sendNode(new BinaryNode('ib', {}, [
710
+ new BinaryNode('unified_session', { id }, null)
711
+ ]));
712
+ } catch (_) {}
713
+ }
714
+
715
+ /** Same, in LID space. */
716
+ _ownDeviceLidJid() {
717
+ if (!this._myLid) return null;
718
+ const lidUser = String(this._myLid).split('@')[0].split(':')[0];
719
+ const idx = (this._mode === 'web' && this._store.deviceIndex) || 0;
720
+ return idx > 0 ? `${lidUser}:${idx}@lid` : `${lidUser}@lid`;
721
+ }
722
+
316
723
  disconnect() {
317
724
  this._reconnecting = false;
318
725
  this._reconnectTry = 0;
@@ -372,6 +779,9 @@ class WhalibmobClient extends EventEmitter {
372
779
  this._requestMediaConnection();
373
780
  this._uploadPreKeys();
374
781
 
782
+ // Same telemetry ping the web client sends on every completed login.
783
+ this._sendUnifiedSession();
784
+
375
785
  // Warm the privacy settings so read receipts respect them from the first
376
786
  // message rather than from whenever something first asks for them.
377
787
  this.queryPrivacySettings({ force: true }).catch(err =>
@@ -457,18 +867,34 @@ class WhalibmobClient extends EventEmitter {
457
867
  if (bytes && bytes.length > 0) {
458
868
  this._store.advIdentity = bytes;
459
869
  // Persist to session file so it survives restart
460
- const sessionFile = path.join(
461
- this._sessionDir,
462
- `${this._store.phoneNumber}.json`
463
- );
464
- try {
465
- const { saveStore } = require('./Store');
466
- saveStore(this._store, sessionFile);
467
- } catch (_) {}
870
+ if (this._mode === 'web') {
871
+ this._saveWebStore();
872
+ } else {
873
+ const sessionFile = path.join(
874
+ this._sessionDir,
875
+ `${this._store.phoneNumber}.json`
876
+ );
877
+ try {
878
+ const { saveStore } = require('./Store');
879
+ saveStore(this._store, sessionFile);
880
+ } catch (_) {}
881
+ }
468
882
  }
469
883
  } else if (this._store.advIdentity) {
470
884
  // Already have it persisted from a previous session — keep using it
471
885
  }
886
+
887
+ // A companion learns its own LID from pair-success, not from <success>, so
888
+ // keep what pairing established rather than leaving it unset on reconnect.
889
+ if (this._mode === 'web') {
890
+ if (!this._myLid && this._store.me && this._store.me.lid) {
891
+ this._myLid = this._store.me.lid;
892
+ }
893
+ if (attrs.lid && this._store.me) {
894
+ this._store.me.lid = String(attrs.lid);
895
+ this._saveWebStore();
896
+ }
897
+ }
472
898
  }
473
899
 
474
900
  // ── Feature 3 (part A): Activate the connection ────────────────────────────
@@ -645,10 +1071,31 @@ class WhalibmobClient extends EventEmitter {
645
1071
  else if (tag === 'ib') this._handleIb(node);
646
1072
  else if (tag === 'success') this.emit('session_refresh', { node }); // late success (re-auth)
647
1073
  else if (tag === 'failure') this._handleFailure(node);
648
- else if (tag === 'stream:error') this.emit('stream_error', { reason: node.attrs && node.attrs.reason });
1074
+ else if (tag === 'stream:error') this._handleStreamError(node);
649
1075
  else this.emit('node', node);
650
1076
  }
651
1077
 
1078
+ // A stream error ends the connection; the reconnect in _onClose picks it up.
1079
+ //
1080
+ // 515 is not a failure. The server sends it immediately after a companion
1081
+ // finishes pairing to say "you are linked now, start over as a normal
1082
+ // client" — the next connection carries the login payload instead of the
1083
+ // pairing bundle, and that is the one that receives history.
1084
+ _handleStreamError(node) {
1085
+ const attrs = (node && node.attrs) || {};
1086
+ const code = attrs.code ? String(attrs.code) : null;
1087
+ const reason = attrs.reason || code || 'unknown';
1088
+
1089
+ if (code === '515') {
1090
+ _whaDbg('[DBG] STREAM_ERROR 515 — restart required after pairing, reconnecting');
1091
+ this.emit('restart_required', { reason });
1092
+ return;
1093
+ }
1094
+
1095
+ _whaDbg('[DBG] STREAM_ERROR code=' + code + ' reason=' + reason);
1096
+ this.emit('stream_error', { reason, code });
1097
+ }
1098
+
652
1099
  // ─── IQ handling ──────────────────────────────────────────────────────────
653
1100
 
654
1101
  _handleIq(node) {
@@ -656,6 +1103,18 @@ class WhalibmobClient extends EventEmitter {
656
1103
  const type = node.attrs && node.attrs.type;
657
1104
  const xmlns = node.attrs && node.attrs.xmlns;
658
1105
 
1106
+ // Companion pairing — these arrive unsolicited, so they are checked before
1107
+ // the pending-IQ table rather than after it.
1108
+ //
1109
+ // pair-device is matched on type=set; pair-success is matched on any type,
1110
+ // the way Baileys distinguishes them ('CB:iq,type:set,pair-device' against
1111
+ // a bare 'CB:iq,,pair-success'). Requiring type=set on pair-success would
1112
+ // drop the stanza that completes the link.
1113
+ if (this._mode === 'web') {
1114
+ if (type === 'set' && findChild(node, 'pair-device')) { this._handlePairDevice(node); return; }
1115
+ if (findChild(node, 'pair-success')) { this._handlePairSuccess(node); return; }
1116
+ }
1117
+
659
1118
  // Resolve pending IQ
660
1119
  if (id) {
661
1120
  const handler = this._pendingIqs.get(id);
@@ -1407,6 +1866,11 @@ class WhalibmobClient extends EventEmitter {
1407
1866
  this._handlePrivacyTokenNotification(node);
1408
1867
  }
1409
1868
 
1869
+ if (type === 'link_code_companion_reg') {
1870
+ // The account owner just typed our pairing code into their phone.
1871
+ this._handleCompanionFinish(node);
1872
+ }
1873
+
1410
1874
  if (type === 'md-msg-hist') {
1411
1875
  // Server signals that history sync data is available.
1412
1876
  // The actual encrypted notification may arrive as:
@@ -2229,7 +2693,12 @@ class WhalibmobClient extends EventEmitter {
2229
2693
  */
2230
2694
  _handleHistorySync(notification, msgId) {
2231
2695
  if (!notification) return;
2232
- const phone = this._store && this._store.phoneNumber;
2696
+ // Web-mode history lands in its own files. A number can be registered over
2697
+ // SMS and separately linked as a companion, and only the companion ever
2698
+ // receives history — merging the two would be merging different accounts'
2699
+ // views of the same number.
2700
+ const phone = (this._store && this._store.phoneNumber) +
2701
+ (this._mode === 'web' ? '.web' : '');
2233
2702
  const sessionDir = this._sessionDir;
2234
2703
 
2235
2704
  _whaDbg('[DBG] HIST_SYNC start syncType=' + notification.syncType +
@@ -2806,7 +3275,7 @@ class WhalibmobClient extends EventEmitter {
2806
3275
  const info = await this.queryPictureInfo(jid, opts);
2807
3276
  if (!info || !info.url) return null;
2808
3277
  const { httpGetBuffer } = require('./MediaService');
2809
- return httpGetBuffer(info.url);
3278
+ return httpGetBuffer(info.url, { web: this._mode === 'web' });
2810
3279
  }
2811
3280
 
2812
3281
  // Parse the <list> a blocklist query or update comes back with.