whalibmob 5.5.30 → 5.5.32

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
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { dbg: _whaDbg, configureLogger: _whaConfigLogger } = require('./logger');
4
+
3
5
  const EventEmitter = require('events');
4
6
  const path = require('path');
5
7
  const crypto = require('crypto');
@@ -120,6 +122,7 @@ class WhalibmobClient extends EventEmitter {
120
122
  this._connected = false;
121
123
  this._reconnecting = false;
122
124
  this._reconnectTry = 0;
125
+ this._hasConnectedOnce = false; // true after first successful open; used to detect reconnects
123
126
  this._phoneNumber = null;
124
127
  this._mediaConn = null;
125
128
  this._pendingIqs = new Map(); // id → resolve fn
@@ -136,6 +139,7 @@ class WhalibmobClient extends EventEmitter {
136
139
  this._tcTokenStore = null; // TcTokenStore — loaded in init()
137
140
  this._inFlightTcTokenIssuance = new Set(); // dedupe concurrent proactive issuePrivacyTokens per JID
138
141
  this._inFlight463Recoveries = new Set(); // dedupe concurrent 463-triggered token issuances per JID (separate from proactive)
142
+ if (opts.pino !== undefined) _whaConfigLogger(opts.pino);
139
143
  }
140
144
 
141
145
  get store() { return this._store; }
@@ -181,6 +185,7 @@ class WhalibmobClient extends EventEmitter {
181
185
 
182
186
  this._signal = SignalProtocol.fromStore(this._store, signalFile, skFile);
183
187
  this._devMgr = new DeviceManager(this);
188
+ this._devMgr.attachSession(this._sessionDir, phoneNumber);
184
189
 
185
190
  // Restore LID ↔ phone mappings persisted from previous sessions.
186
191
  // This ensures we can route to LID JIDs even on fresh connections where no
@@ -191,7 +196,7 @@ class WhalibmobClient extends EventEmitter {
191
196
  this._pnToLid.set(phone, lid);
192
197
  this._lidToPn.set(lid, phone);
193
198
  }
194
- process.stderr.write('[DBG] LID_RESTORED count=' + Object.keys(persistedLid).length + '\n');
199
+ _whaDbg('[DBG] LID_RESTORED count=' + Object.keys(persistedLid).length);
195
200
 
196
201
  await this._connectSocket();
197
202
  return this;
@@ -209,6 +214,8 @@ class WhalibmobClient extends EventEmitter {
209
214
  socket.on('close', () => this._onClose());
210
215
  socket.on('error', err => this.emit('error', err));
211
216
 
217
+ // Pass reconnect attempt count into the Noise handshake ClientPayload
218
+ this._store.connectAttemptCount = this._reconnectTry;
212
219
  await socket.connect();
213
220
  return socket;
214
221
  }
@@ -271,6 +278,32 @@ class WhalibmobClient extends EventEmitter {
271
278
 
272
279
  this._requestMediaConnection();
273
280
  this._uploadPreKeys();
281
+
282
+ // ── Reconnect: flush stale device cache + background re-usync ─────────────
283
+ // On first connect _hasConnectedOnce is false — nothing extra to do.
284
+ // On every reconnect (network drop / NAT reset / server restart) we flush the
285
+ // in-memory device cache because contacts may have linked or unlinked devices
286
+ // while we were offline. Disk files are kept for next restart warm-read;
287
+ // in-memory entries are cleared so the next send triggers a fresh usync IQ.
288
+ if (this._hasConnectedOnce && this._devMgr) {
289
+ const phonesInCache = Object.keys(this._devMgr._cacheSnapshot || {});
290
+ _whaDbg('[DBG] RECONNECT: flushing device cache (' + phonesInCache.length + ' entries) for fresh usync');
291
+ this._devMgr._dcFlush();
292
+
293
+ // Immediately re-usync own devices in the background (tablets / linked devices
294
+ // could have been added or unlinked while we were offline).
295
+ if (this._store && this._store.phoneNumber && this._signal) {
296
+ const ownPhone = this._store.phoneNumber;
297
+ setImmediate(() => {
298
+ if (!this._connected || !this._devMgr) return;
299
+ this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal, false)
300
+ .then(() => _whaDbg('[DBG] RECONNECT own-device usync done'))
301
+ .catch(e => _whaDbg('[DBG] RECONNECT own-device usync err: ' + e.message));
302
+ });
303
+ }
304
+ }
305
+ this._hasConnectedOnce = true;
306
+
274
307
  this.emit('connected');
275
308
  }
276
309
 
@@ -293,11 +326,11 @@ class WhalibmobClient extends EventEmitter {
293
326
  if (!node) return;
294
327
  const attrs = node.attrs || {};
295
328
  // Debug: log the full success node structure
296
- process.stderr.write('[DBG] SUCCESS node attrs=' + JSON.stringify(attrs) + '\n');
329
+ _whaDbg('[DBG] SUCCESS node attrs=' + JSON.stringify(attrs));
297
330
  if (Array.isArray(node.content)) {
298
331
  for (const child of node.content) {
299
332
  if (child && child.description) {
300
- process.stderr.write('[DBG] SUCCESS child tag=' + child.description + ' attrs=' + JSON.stringify(child.attrs || {}) + '\n');
333
+ _whaDbg('[DBG] SUCCESS child tag=' + child.description + ' attrs=' + JSON.stringify(child.attrs || {}));
301
334
  }
302
335
  }
303
336
  }
@@ -331,7 +364,7 @@ class WhalibmobClient extends EventEmitter {
331
364
  _sendActiveIq() {
332
365
  if (!this._socket) return;
333
366
  const id = this._genMsgId();
334
- process.stderr.write('[DBG] SEND active IQ id=' + id + '\n');
367
+ _whaDbg('[DBG] SEND active IQ id=' + id);
335
368
  this._socket.sendNode(new BinaryNode('iq', {
336
369
  id,
337
370
  to: 's.whatsapp.net',
@@ -391,7 +424,7 @@ class WhalibmobClient extends EventEmitter {
391
424
  for (const type of dirtyTypes) {
392
425
  const mapped = COLLECTION_MAP[type];
393
426
  if (!mapped) {
394
- process.stderr.write('[DBG] ignoring unknown dirty type: ' + type + '\n');
427
+ _whaDbg('[DBG] ignoring unknown dirty type: ' + type);
395
428
  continue;
396
429
  }
397
430
  for (const c of mapped) collections.add(c);
@@ -409,7 +442,7 @@ class WhalibmobClient extends EventEmitter {
409
442
  }, null);
410
443
  });
411
444
 
412
- process.stderr.write('[DBG] SEND appStateSync IQ id=' + id + ' collections=' + [...collections].join(',') + '\n');
445
+ _whaDbg('[DBG] SEND appStateSync IQ id=' + id + ' collections=' + [...collections].join(','));
413
446
  this._socket.sendNode(new BinaryNode('iq', {
414
447
  id,
415
448
  to: 's.whatsapp.net',
@@ -436,6 +469,7 @@ class WhalibmobClient extends EventEmitter {
436
469
  _onClose() {
437
470
  this._connected = false;
438
471
  this._stopTimers();
472
+ this._socket = null; // clear dead reference — prevents accidental sends before reconnect
439
473
  this.emit('disconnected');
440
474
 
441
475
  // Reject all pending IQs / acks
@@ -469,11 +503,27 @@ class WhalibmobClient extends EventEmitter {
469
503
  }, [new BinaryNode('ping', {}, null)]));
470
504
  }
471
505
  }, KEEPALIVE_INTERVAL);
506
+
507
+ // Watchdog: if no data arrived from WhatsApp in 2× keepalive window,
508
+ // the connection is dead but TCP hasn't emitted 'close' (common with NAT drops).
509
+ // Force-destroy the socket so _onTcpClose fires and reconnection kicks in.
510
+ const WATCHDOG_INTERVAL = KEEPALIVE_INTERVAL * 2 + 5000;
511
+ this._watchdogTimer = setInterval(() => {
512
+ if (!this._socket || !this._connected) return;
513
+ const noiseSocket = this._socket;
514
+ const lastRx = noiseSocket._lastRxAt || 0;
515
+ const silentMs = Date.now() - lastRx;
516
+ if (silentMs > WATCHDOG_INTERVAL) {
517
+ _whaDbg('[DBG] WATCHDOG: no data for ' + Math.round(silentMs / 1000) + 's — force-closing dead connection');
518
+ try { noiseSocket.close(); } catch (_) {}
519
+ }
520
+ }, KEEPALIVE_INTERVAL);
472
521
  }
473
522
 
474
523
  _stopTimers() {
475
- if (this._pingTimer) { clearInterval(this._pingTimer); this._pingTimer = null; }
476
- if (this._keepTimer) { clearInterval(this._keepTimer); this._keepTimer = null; }
524
+ if (this._pingTimer) { clearInterval(this._pingTimer); this._pingTimer = null; }
525
+ if (this._keepTimer) { clearInterval(this._keepTimer); this._keepTimer = null; }
526
+ if (this._watchdogTimer) { clearInterval(this._watchdogTimer); this._watchdogTimer = null; }
477
527
  }
478
528
 
479
529
  // ─── Node dispatch ────────────────────────────────────────────────────────
@@ -482,9 +532,9 @@ class WhalibmobClient extends EventEmitter {
482
532
  if (!node || !node.description) return;
483
533
  const tag = node.description;
484
534
  // Debug: log every node received
485
- process.stderr.write('[DBG] _onNode tag=' + tag + ' attrs=' + JSON.stringify(node.attrs || {}) + '\n');
535
+ _whaDbg('[DBG] _onNode tag=' + tag + ' attrs=' + JSON.stringify(node.attrs || {}));
486
536
  if (tag === 'iq' && node.attrs && node.attrs.type === 'error') {
487
- try { process.stderr.write('[DBG] IQ_ERROR content=' + JSON.stringify(node.content) + '\n'); } catch(_) {}
537
+ try { _whaDbg('[DBG] IQ_ERROR content=' + JSON.stringify(node.content)); } catch(_) {}
488
538
  }
489
539
 
490
540
  if (tag === 'iq') this._handleIq(node);
@@ -554,11 +604,11 @@ class WhalibmobClient extends EventEmitter {
554
604
  }
555
605
  }
556
606
  }
557
- process.stderr.write('[DBG] _handleMessage from=' + from + ' participant=' + participant + ' senderPn=' + senderPn + ' id=' + id + '\n');
607
+ _whaDbg('[DBG] _handleMessage from=' + from + ' participant=' + participant + ' senderPn=' + senderPn + ' id=' + id);
558
608
  if (Array.isArray(node.content)) {
559
609
  for (const c of node.content) {
560
610
  if (c && c.description) {
561
- process.stderr.write('[DBG] MSG_CHILD tag=' + c.description + ' attrs=' + JSON.stringify(c.attrs || {}) + ' contentLen=' + (Buffer.isBuffer(c.content) ? c.content.length : (typeof c.content === 'string' ? c.content.length : 0)) + '\n');
611
+ _whaDbg('[DBG] MSG_CHILD tag=' + c.description + ' attrs=' + JSON.stringify(c.attrs || {}) + ' contentLen=' + (Buffer.isBuffer(c.content) ? c.content.length : (typeof c.content === 'string' ? c.content.length : 0)));
562
612
  }
563
613
  }
564
614
  }
@@ -632,12 +682,12 @@ class WhalibmobClient extends EventEmitter {
632
682
  const sigJid = senderPn
633
683
  || ((participant && participant !== from) ? participant : from);
634
684
 
635
- process.stderr.write('[DBG] DM_DECRYPT id=' + id + ' type=' + encType + ' sigJid=' + sigJid + ' cipherLen=' + cipherBuf.length + '\n');
685
+ _whaDbg('[DBG] DM_DECRYPT id=' + id + ' type=' + encType + ' sigJid=' + sigJid + ' cipherLen=' + cipherBuf.length);
636
686
 
637
687
  this._signal.decrypt(sigJid, encType, cipherBuf)
638
688
  .then(plaintext => {
639
689
  const decoded = this._decodeMsg(plaintext);
640
- process.stderr.write('[DBG] DM_DECODED id=' + id + ' type=' + decoded.type + ' ptLen=' + (plaintext ? plaintext.length : 0) + ' hasSKDM=' + !!(decoded.skdm || decoded.type === 'senderKeyDistribution') + '\n');
690
+ _whaDbg('[DBG] DM_DECODED id=' + id + ' type=' + decoded.type + ' ptLen=' + (plaintext ? plaintext.length : 0) + ' hasSKDM=' + !!(decoded.skdm || decoded.type === 'senderKeyDistribution'));
641
691
 
642
692
  // Process embedded SKDM when bundled with a real message (WhatsApp MD always does this
643
693
  // on the first message of a new session, even for 1-on-1 DMs).
@@ -645,9 +695,9 @@ class WhalibmobClient extends EventEmitter {
645
695
  if (decoded.skdm && decoded.skdm.axolotlBytes && decoded.skdm.axolotlBytes[0] === 0x33) {
646
696
  try {
647
697
  this._signal.processSKDM(decoded.skdm.groupId, sigJid, decoded.skdm.axolotlBytes);
648
- process.stderr.write('[DBG] SKDM_PROCESSED groupId=' + decoded.skdm.groupId + ' sender=' + sigJid + '\n');
698
+ _whaDbg('[DBG] SKDM_PROCESSED groupId=' + decoded.skdm.groupId + ' sender=' + sigJid);
649
699
  } catch (e) {
650
- process.stderr.write('[DBG] SKDM_ERR ' + e.message + '\n');
700
+ _whaDbg('[DBG] SKDM_ERR ' + e.message);
651
701
  }
652
702
  }
653
703
 
@@ -656,9 +706,9 @@ class WhalibmobClient extends EventEmitter {
656
706
  if (decoded.axolotlBytes && decoded.axolotlBytes[0] === 0x33) {
657
707
  try {
658
708
  this._signal.processSKDM(decoded.groupId, sigJid, decoded.axolotlBytes);
659
- process.stderr.write('[DBG] SKDM_ONLY_PROCESSED groupId=' + decoded.groupId + ' sender=' + sigJid + '\n');
709
+ _whaDbg('[DBG] SKDM_ONLY_PROCESSED groupId=' + decoded.groupId + ' sender=' + sigJid);
660
710
  } catch (e) {
661
- process.stderr.write('[DBG] SKDM_ONLY_ERR ' + e.message + '\n');
711
+ _whaDbg('[DBG] SKDM_ONLY_ERR ' + e.message);
662
712
  }
663
713
  }
664
714
  this._sendReadReceipt(id, fromRaw, partRaw);
@@ -666,7 +716,7 @@ class WhalibmobClient extends EventEmitter {
666
716
  }
667
717
 
668
718
  // Protocol messages (revoke, ephemeral, etc.) — silent ACK
669
- if (decoded.type === 'protocol') { process.stderr.write('[DBG] DM_FILTER proto id=' + id + '\n'); return; }
719
+ if (decoded.type === 'protocol') { _whaDbg('[DBG] DM_FILTER proto id=' + id); return; }
670
720
 
671
721
  this.emit('message', { id, from, participant, ts, decoded, node });
672
722
  this._sendReadReceipt(id, fromRaw, partRaw);
@@ -679,7 +729,7 @@ class WhalibmobClient extends EventEmitter {
679
729
  }
680
730
  })
681
731
  .catch(err => {
682
- process.stderr.write('[DBG] DM_ERR id=' + id + ' err=' + (err && err.message) + '\n');
732
+ _whaDbg('[DBG] DM_ERR id=' + id + ' err=' + (err && err.message));
683
733
  this.emit('decrypt_error', { id, from, participant, err });
684
734
  this._sendRetryRequest(id, node);
685
735
  });
@@ -785,7 +835,7 @@ class WhalibmobClient extends EventEmitter {
785
835
  const msgId = String(attrs.id || '');
786
836
  const cached = this._sentMsgCache && this._sentMsgCache.get(msgId);
787
837
  if (!cached || !this._sender) {
788
- process.stderr.write('[DBG] RETRY_RECV msgId=' + msgId + ' — no cached plaintext, skipping resend\n');
838
+ _whaDbg('[DBG] RETRY_RECV msgId=' + msgId + ' — no cached plaintext, skipping resend\n');
789
839
  return;
790
840
  }
791
841
 
@@ -793,7 +843,7 @@ class WhalibmobClient extends EventEmitter {
793
843
  // Extract bare phone number: "40756469325@s.whatsapp.net" → "40756469325"
794
844
  const recipientPhone = fromStr.split('@')[0].split('.')[0];
795
845
 
796
- process.stderr.write('[DBG] RETRY_RECV msgId=' + msgId + ' from=' + fromStr + ' — clearing session + resending\n');
846
+ _whaDbg('[DBG] RETRY_RECV msgId=' + msgId + ' from=' + fromStr + ' — clearing session + resending\n');
797
847
 
798
848
  // Delete all Signal sessions for the recipient's devices so next send creates fresh pkmsg
799
849
  const sigStore = this._signal && this._signal.store;
@@ -801,7 +851,7 @@ class WhalibmobClient extends EventEmitter {
801
851
  const sessions = sigStore._sessions;
802
852
  Object.keys(sessions).forEach(addr => {
803
853
  if (addr.startsWith(recipientPhone + '.')) {
804
- process.stderr.write('[DBG] RETRY deleting session for ' + addr + '\n');
854
+ _whaDbg('[DBG] RETRY deleting session for ' + addr);
805
855
  delete sessions[addr];
806
856
  }
807
857
  });
@@ -816,8 +866,8 @@ class WhalibmobClient extends EventEmitter {
816
866
  this._sender._sendDMMessage(
817
867
  cached.toJid, cached.msgId, cached.plaintext, cached.mediaType, cached.options
818
868
  )
819
- .then(r => process.stderr.write('[DBG] RETRY_RESEND ok id=' + r.id + '\n'))
820
- .catch(e => process.stderr.write('[DBG] RETRY_RESEND_ERR: ' + e.message + '\n'));
869
+ .then(r => _whaDbg('[DBG] RETRY_RESEND ok id=' + r.id + '\n'))
870
+ .catch(e => _whaDbg('[DBG] RETRY_RESEND_ERR: ' + e.message));
821
871
  }
822
872
 
823
873
  _handleAck(node) {
@@ -886,7 +936,7 @@ class WhalibmobClient extends EventEmitter {
886
936
  // Assign a stable, unique prekey index per msgId on first retry
887
937
  let pkIdx = existing ? existing.pkIdx : -1;
888
938
  if (pkIdx < 0 && this._signal) {
889
- const allKeys = this._signal.getPreKeysForUpload(50);
939
+ const allKeys = this._signal.getPreKeysForUpload(800);
890
940
  pkIdx = this._retryPreKeyIdx++ % Math.max(1, allKeys.length);
891
941
  }
892
942
  this._retryPending.set(msgId, { node: origNode, count, pkIdx });
@@ -907,7 +957,7 @@ class WhalibmobClient extends EventEmitter {
907
957
  try {
908
958
  const spk = this._signal.getSignedPreKeyForUpload();
909
959
  const identKey = this._signal.getIdentityKey();
910
- const allPreKeys = this._signal.getPreKeysForUpload(50);
960
+ const allPreKeys = this._signal.getPreKeysForUpload(800);
911
961
  const pk = allPreKeys[pkIdx] || allPreKeys[0];
912
962
 
913
963
  if (spk && identKey && pk) {
@@ -929,15 +979,15 @@ class WhalibmobClient extends EventEmitter {
929
979
  const advBytes = buildOrGetAdvIdentity(this._store);
930
980
  if (advBytes && advBytes.length > 0) {
931
981
  keysChildren.push(new BinaryNode('device-identity', {}, advBytes));
932
- process.stderr.write('[DBG] RETRY #' + count + ' for ' + msgId + ' — preKeyId=' + pk.keyId + ' +device-identity(' + advBytes.length + 'b)\n');
982
+ _whaDbg('[DBG] RETRY #' + count + ' for ' + msgId + ' — preKeyId=' + pk.keyId + ' +device-identity(' + advBytes.length + 'b)\n');
933
983
  } else {
934
- process.stderr.write('[DBG] RETRY #' + count + ' for ' + msgId + ' — preKeyId=' + pk.keyId + ' (no advIdentity)\n');
984
+ _whaDbg('[DBG] RETRY #' + count + ' for ' + msgId + ' — preKeyId=' + pk.keyId + ' (no advIdentity)\n');
935
985
  }
936
986
 
937
987
  children.push(new BinaryNode('keys', {}, keysChildren));
938
988
  }
939
989
  } catch (e) {
940
- process.stderr.write('[DBG] RETRY prekey bundle error: ' + e.message + '\n');
990
+ _whaDbg('[DBG] RETRY prekey bundle error: ' + e.message);
941
991
  }
942
992
  }
943
993
 
@@ -952,7 +1002,7 @@ class WhalibmobClient extends EventEmitter {
952
1002
  if (origAttrs.recipient) receiptAttrs.recipient = origAttrs.recipient;
953
1003
  if (origAttrs.participant) receiptAttrs.participant = origAttrs.participant;
954
1004
 
955
- process.stderr.write('[DBG] RETRY #' + count + ' receipt to=' + JSON.stringify(fromJid) + ' participant=' + JSON.stringify(origAttrs.participant || null) + '\n');
1005
+ _whaDbg('[DBG] RETRY #' + count + ' receipt to=' + JSON.stringify(fromJid) + ' participant=' + JSON.stringify(origAttrs.participant || null));
956
1006
  this._socket.sendNode(new BinaryNode('receipt', receiptAttrs, children));
957
1007
  }
958
1008
 
@@ -986,6 +1036,50 @@ class WhalibmobClient extends EventEmitter {
986
1036
  this._handlePrivacyTokenNotification(node);
987
1037
  }
988
1038
 
1039
+ if (type === 'devices') {
1040
+ // Server push: a contact's linked device list changed (they linked or
1041
+ // unlinked a tablet, desktop, etc.). Invalidate that phone's cache entry
1042
+ // so the next send triggers a fresh usync IQ and reaches all their devices.
1043
+ const fromJid = attrs.from || '';
1044
+ const fromPhone = fromJid.split('@')[0].split(':')[0];
1045
+ if (fromPhone && this._devMgr) {
1046
+ this._devMgr._dcDel([fromPhone]);
1047
+ _whaDbg('[DBG] NOTIF_DEVICES invalidated cache for ' + fromPhone);
1048
+ // Also process any inline <devices> list the server included
1049
+ const devicesNode = findChildDeep(node, 'devices');
1050
+ if (devicesNode) this._processDeviceUpdate(devicesNode);
1051
+ // Background re-usync so the cache is warm before the next send
1052
+ if (this._signal) {
1053
+ setImmediate(() => {
1054
+ if (!this._connected || !this._devMgr) return;
1055
+ this._devMgr.bulkEnsureSessions([fromPhone], this._signal, false)
1056
+ .then(() => _whaDbg('[DBG] NOTIF_DEVICES bg-usync done for ' + fromPhone))
1057
+ .catch(e => _whaDbg('[DBG] NOTIF_DEVICES bg-usync err: ' + e.message));
1058
+ });
1059
+ }
1060
+ }
1061
+ }
1062
+
1063
+ if (type === 'identity') {
1064
+ // Server push: a contact re-registered WhatsApp (new identity key / new phone).
1065
+ // Their old Signal sessions are no longer valid — clear them and the device
1066
+ // cache so the next send builds a fresh pkmsg session from scratch.
1067
+ const fromJid = attrs.from || '';
1068
+ const fromPhone = fromJid.split('@')[0].split(':')[0];
1069
+ if (fromPhone && this._devMgr) {
1070
+ this._devMgr._dcDel([fromPhone]);
1071
+ _whaDbg('[DBG] NOTIF_IDENTITY flushed device cache for re-registered ' + fromPhone);
1072
+ }
1073
+ if (fromPhone && this._signal && this._signal.store && this._signal.store._sessions) {
1074
+ const sessions = this._signal.store._sessions;
1075
+ let cleared = 0;
1076
+ for (const addr of Object.keys(sessions)) {
1077
+ if (addr.startsWith(fromPhone + '.')) { delete sessions[addr]; cleared++; }
1078
+ }
1079
+ _whaDbg('[DBG] NOTIF_IDENTITY cleared ' + cleared + ' Signal sessions for ' + fromPhone);
1080
+ }
1081
+ }
1082
+
989
1083
  // Ack the notification
990
1084
  if (this._socket && this._connected) {
991
1085
  this._socket.sendNode(new BinaryNode('ack', {
@@ -998,16 +1092,29 @@ class WhalibmobClient extends EventEmitter {
998
1092
  }
999
1093
 
1000
1094
  _processDeviceUpdate(devicesNode) {
1001
- if (!Array.isArray(devicesNode.content)) return;
1095
+ // Called from _handleNotification(type='account_sync') and type='devices'.
1096
+ // Rebuilds the device cache for the affected phones from the server-provided list.
1097
+ // Previous bug: called Set.add() on cache entries that are number[] arrays → silently failed.
1098
+ if (!Array.isArray(devicesNode.content) || !this._devMgr) return;
1099
+
1100
+ // Group all device IDs per phone from the notification
1101
+ const phoneDevices = new Map(); // phone → number[]
1002
1102
  for (const deviceNode of devicesNode.content) {
1003
1103
  if (!deviceNode || deviceNode.description !== 'device') continue;
1004
1104
  const jid = deviceNode.attrs && deviceNode.attrs.jid;
1005
- if (jid && this._devMgr) {
1006
- const phone = jid.split('@')[0].split(':')[0];
1007
- const device = parseInt((jid.split(':')[1] || '0').split('@')[0], 10);
1008
- if (!this._devMgr._deviceCache.has(phone)) this._devMgr._deviceCache.set(phone, new Set());
1009
- this._devMgr._deviceCache.get(phone).add(device);
1010
- }
1105
+ if (!jid) continue;
1106
+ const phone = String(jid).split('@')[0].split(':')[0];
1107
+ const device = parseInt((String(jid).split(':')[1] || '0').split('@')[0], 10);
1108
+ if (!phoneDevices.has(phone)) phoneDevices.set(phone, []);
1109
+ const ids = phoneDevices.get(phone);
1110
+ if (!ids.includes(device)) ids.push(device);
1111
+ }
1112
+
1113
+ // Replace cache entries atomically per phone (server list is authoritative)
1114
+ for (const [phone, ids] of phoneDevices) {
1115
+ this._devMgr._dcDel([phone]); // evict stale entry
1116
+ for (const id of ids) this._devMgr._dcAdd(phone, id);
1117
+ _whaDbg('[DBG] DEV_UPDATE phone=' + phone + ' ids=[' + ids.join(',') + ']');
1011
1118
  }
1012
1119
  }
1013
1120
 
@@ -1114,7 +1221,7 @@ class WhalibmobClient extends EventEmitter {
1114
1221
  _requestMediaConnection() {
1115
1222
  if (!this._socket || !this._connected) return;
1116
1223
  const mcId = this._genMsgId();
1117
- process.stderr.write('[DBG] SEND media_conn IQ id=' + mcId + '\n');
1224
+ _whaDbg('[DBG] SEND media_conn IQ id=' + mcId);
1118
1225
  this._socket.sendNode(new BinaryNode('iq', {
1119
1226
  id: mcId,
1120
1227
  to: 's.whatsapp.net',
@@ -1146,7 +1253,7 @@ class WhalibmobClient extends EventEmitter {
1146
1253
  _uploadPreKeys() {
1147
1254
  if (!this._signal || !this._socket || !this._connected) return;
1148
1255
 
1149
- const preKeys = this._signal.getPreKeysForUpload(50);
1256
+ const preKeys = this._signal.getPreKeysForUpload(800);
1150
1257
  const spk = this._signal.getSignedPreKeyForUpload();
1151
1258
  const identKey = this._signal.getIdentityKey();
1152
1259
  if (!preKeys.length || !spk) return;
@@ -1163,7 +1270,7 @@ class WhalibmobClient extends EventEmitter {
1163
1270
  ]);
1164
1271
 
1165
1272
  const pkId = this._genMsgId();
1166
- process.stderr.write('[DBG] SEND uploadPreKeys IQ id=' + pkId + '\n');
1273
+ _whaDbg('[DBG] SEND uploadPreKeys IQ id=' + pkId);
1167
1274
  this._socket.sendNode(new BinaryNode('iq', {
1168
1275
  id: pkId,
1169
1276
  to: 's.whatsapp.net',
@@ -1413,7 +1520,7 @@ class WhalibmobClient extends EventEmitter {
1413
1520
  })
1414
1521
  ])
1415
1522
  ]);
1416
- process.stderr.write('[DBG] ISSUE_PRIVACY_TOKENS jid=' + normalizedJid + ' t=' + timestamp + ' iq_id=' + id + '\n');
1523
+ _whaDbg('[DBG] ISSUE_PRIVACY_TOKENS jid=' + normalizedJid + ' t=' + timestamp + ' iq_id=' + id);
1417
1524
  return this._sendIq(node);
1418
1525
  }
1419
1526
 
@@ -1437,23 +1544,23 @@ class WhalibmobClient extends EventEmitter {
1437
1544
 
1438
1545
  // ── Debug: dump result structure so we can trace server response ─────────
1439
1546
  if (!result) {
1440
- process.stderr.write('[DBG] STORE_TCTOKEN result=NULL (IQ timeout or not matched) fallback=' + fallbackJid + '\n');
1547
+ _whaDbg('[DBG] STORE_TCTOKEN result=NULL (IQ timeout or not matched) fallback=' + fallbackJid);
1441
1548
  } else {
1442
1549
  const contentLen = Array.isArray(result.content) ? result.content.length
1443
1550
  : Buffer.isBuffer(result.content) ? result.content.length
1444
1551
  : 0;
1445
- process.stderr.write('[DBG] STORE_TCTOKEN result type=' + (result.attrs && result.attrs.type) +
1552
+ _whaDbg('[DBG] STORE_TCTOKEN result type=' + (result.attrs && result.attrs.type) +
1446
1553
  ' id=' + (result.attrs && result.attrs.id) +
1447
- ' contentLen=' + contentLen + '\n');
1554
+ ' contentLen=' + contentLen);
1448
1555
  if (Array.isArray(result.content)) {
1449
1556
  result.content.forEach((c, i) => {
1450
1557
  if (c && c.description) {
1451
1558
  const childContent = Array.isArray(c.content) ? c.content.length + ' children'
1452
1559
  : Buffer.isBuffer(c.content) ? c.content.length + 'B'
1453
1560
  : String(c.content);
1454
- process.stderr.write('[DBG] STORE_TCTOKEN child[' + i + '] tag=' + c.description +
1561
+ _whaDbg('[DBG] STORE_TCTOKEN child[' + i + '] tag=' + c.description +
1455
1562
  ' attrs=' + JSON.stringify(c.attrs || {}) +
1456
- ' content=' + childContent + '\n');
1563
+ ' content=' + childContent);
1457
1564
  }
1458
1565
  });
1459
1566
  }
@@ -1486,13 +1593,13 @@ class WhalibmobClient extends EventEmitter {
1486
1593
  if (bytes && bytes.length) {
1487
1594
  this._tcTokenStore.setToken(jid, bytes, t);
1488
1595
  tokenStoredCount++;
1489
- process.stderr.write('[DBG] TCTOKEN_STORED jid=' + jid + ' t=' + t + ' len=' + bytes.length + '\n');
1596
+ _whaDbg('[DBG] TCTOKEN_STORED jid=' + jid + ' t=' + t + ' len=' + bytes.length);
1490
1597
  } else {
1491
- process.stderr.write('[DBG] STORE_TCTOKEN token node has no bytes jid=' + jid + '\n');
1598
+ _whaDbg('[DBG] STORE_TCTOKEN token node has no bytes jid=' + jid);
1492
1599
  }
1493
1600
  }
1494
1601
  } catch (e) {
1495
- process.stderr.write('[DBG] STORE_TCTOKEN parse error: ' + e.message + '\n');
1602
+ _whaDbg('[DBG] STORE_TCTOKEN parse error: ' + e.message);
1496
1603
  }
1497
1604
  }
1498
1605
 
@@ -1505,8 +1612,8 @@ class WhalibmobClient extends EventEmitter {
1505
1612
  if (saveSenderTs && issueTs != null) {
1506
1613
  try {
1507
1614
  this._tcTokenStore.setSenderTimestamp(fallbackJid, issueTs);
1508
- process.stderr.write('[DBG] SENDER_TS_SAVED jid=' + fallbackJid + ' ts=' + issueTs +
1509
- ' tokenBytes=' + tokenStoredCount + '\n');
1615
+ _whaDbg('[DBG] SENDER_TS_SAVED jid=' + fallbackJid + ' ts=' + issueTs +
1616
+ ' tokenBytes=' + tokenStoredCount);
1510
1617
  } catch (_) {}
1511
1618
  }
1512
1619
  }
@@ -1533,14 +1640,14 @@ class WhalibmobClient extends EventEmitter {
1533
1640
  const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
1534
1641
 
1535
1642
  const attrs = node.attrs || {};
1536
- process.stderr.write('[DBG] PRIVACY_TOKEN_NOTIF_ENTER from=' + String(attrs.from || '') +
1537
- ' sender_lid=' + String(attrs.sender_lid || '') + '\n');
1643
+ _whaDbg('[DBG] PRIVACY_TOKEN_NOTIF_ENTER from=' + String(attrs.from || '') +
1644
+ ' sender_lid=' + String(attrs.sender_lid || ''));
1538
1645
 
1539
1646
  // ── 1. Require <tokens> wrapper (matches Baileys' getBinaryNodeChild check) ──
1540
1647
  const content = Array.isArray(node.content) ? node.content : [];
1541
1648
  const tokensNode = content.find(n => n && n.description === 'tokens');
1542
1649
  if (!tokensNode) {
1543
- process.stderr.write('[DBG] PRIVACY_TOKEN_NOTIF no <tokens> wrapper — abort\n');
1650
+ _whaDbg('[DBG] PRIVACY_TOKEN_NOTIF no <tokens> wrapper — abort\n');
1544
1651
  return;
1545
1652
  }
1546
1653
 
@@ -1569,7 +1676,7 @@ class WhalibmobClient extends EventEmitter {
1569
1676
  storageJid = normalizeJidForTcToken(rawFrom);
1570
1677
  }
1571
1678
 
1572
- process.stderr.write('[DBG] PRIVACY_TOKEN_NOTIF storageJid=' + storageJid + '\n');
1679
+ _whaDbg('[DBG] PRIVACY_TOKEN_NOTIF storageJid=' + storageJid);
1573
1680
 
1574
1681
  // ── 3. Parse <token> children and store bytes (no senderTimestamp update) ──
1575
1682
  // Reuse _storeTcTokenFromIqResult with the full notification node so that its
@@ -1606,15 +1713,15 @@ class WhalibmobClient extends EventEmitter {
1606
1713
  return;
1607
1714
  }
1608
1715
 
1609
- process.stderr.write('[DBG] REISSUE_TC_TOKEN_AFTER_IDENTITY_CHANGE jid=' + tcJid +
1610
- ' prevSenderTs=' + senderTs + '\n');
1716
+ _whaDbg('[DBG] REISSUE_TC_TOKEN_AFTER_IDENTITY_CHANGE jid=' + tcJid +
1717
+ ' prevSenderTs=' + senderTs);
1611
1718
 
1612
1719
  const issueTs = Math.floor(Date.now() / 1000);
1613
1720
  const result = await this._issuePrivacyTokens(tcJid, issueTs);
1614
1721
  this._storeTcTokenFromIqResult(result, tcJid, issueTs);
1615
1722
  } catch (e) {
1616
- process.stderr.write('[DBG] REISSUE_TC_TOKEN_ERR from=' + from +
1617
- ' err=' + (e && e.message) + '\n');
1723
+ _whaDbg('[DBG] REISSUE_TC_TOKEN_ERR from=' + from +
1724
+ ' err=' + (e && e.message));
1618
1725
  }
1619
1726
  })();
1620
1727
  }