whalibmob 5.5.39 → 5.5.41

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/README.md CHANGED
@@ -8,6 +8,11 @@
8
8
 
9
9
 
10
10
 
11
+ TELEGRAM NEW WHALIBMOB CHANNEL JOIN HERE https://t.me/+jWzq-I9o0Xc1Mzc8
12
+
13
+
14
+
15
+
11
16
 
12
17
  > [!CAUTION]
13
18
  > Use a dedicated phone number with this library. Connecting with a number that is already active on a real device will cause WhatsApp to log that device out.
@@ -130,6 +135,13 @@ npm install -g whalibmob
130
135
  - [Handling Events](#handling-events)
131
136
  - [Example to Start](#example-to-start)
132
137
  - [All Events](#all-events)
138
+ - [History Sync](#history-sync)
139
+ - [How It Works](#how-history-sync-works)
140
+ - [Listening to History Sync Events](#listening-to-history-sync-events)
141
+ - [Persistent Files Written to Disk](#persistent-files-written-to-disk)
142
+ - [Reading the History Store](#reading-the-history-store)
143
+ - [tcToken — Error 463 Defense](#tctoken--error-463-defense)
144
+ - [What Is Automatic vs What You Need to Do](#what-is-automatic-vs-what-you-need-to-do)
133
145
  - [Receiving Media](#receiving-media)
134
146
  - [Sending Messages](#sending-messages)
135
147
  - [Text Message](#text-message)
@@ -550,6 +562,261 @@ The `decoded` object shape per message type:
550
562
  { type: 'protocol', subtype: string }
551
563
  ```
552
564
 
565
+ ---
566
+
567
+ ## History Sync
568
+
569
+ ### How History Sync Works
570
+
571
+ When whalibmob connects, WhatsApp automatically sends the account's chat history to the client.
572
+ The library handles the entire pipeline **without any code from you** — you only need to listen
573
+ to the events if you want to use the data.
574
+
575
+ The full internal flow:
576
+
577
+ 1. WhatsApp server sends an encrypted `ProtocolMessage` (type 6) containing a `HistorySyncNotification`.
578
+ 2. The library decrypts it via Signal Protocol.
579
+ 3. `HistorySyncHandler` downloads the encrypted blob from WhatsApp's CDN (`mmg.whatsapp.net`), or reads the inline payload if the server embedded it directly.
580
+ 4. The blob is decrypted with **AES-256-CBC** using an HKDF key derived from `"WhatsApp History Keys"`.
581
+ 5. The result is decompressed with **zlib** and decoded from **protobuf** (WAProto v2.3000.x — field numbers verified against the official proto definition).
582
+ 6. Chats, contacts, push names, LID↔PN mappings, and **tcTokens** are merged into the disk store.
583
+ 7. tcTokens from history are seeded into `TcTokenStore` in memory so the first outbound DM after reconnect already carries a valid `<tctoken>` node (prevents error 463 on cold start).
584
+ 8. The `history_sync` event fires with a summary of what was received.
585
+
586
+ History arrives in **multiple chunks**. Each chunk fires one `history_sync` event. The first chunk (sync type `INITIAL_BOOTSTRAP`) is usually the largest and carries the most recent conversations.
587
+
588
+ ### Listening to History Sync Events
589
+
590
+ ```js
591
+ const { WhalibmobClient } = require('whalibmob')
592
+ const path = require('path')
593
+
594
+ const client = new WhalibmobClient({
595
+ sessionDir: path.join(process.env.HOME, '.waSession')
596
+ })
597
+
598
+ // history_sync fires once per history chunk — may fire multiple times on first connect
599
+ client.on('history_sync', result => {
600
+ console.log('History sync chunk received:')
601
+ console.log(' type :', result.syncTypeName) // e.g. 'INITIAL_BOOTSTRAP', 'RECENT', 'FULL'
602
+ console.log(' progress:', result.progress) // 0-100 server-reported
603
+ console.log(' chunk :', result.chunkOrder)
604
+ console.log(' chats :', result.chats.length)
605
+ console.log(' contacts:', result.contacts.length)
606
+ console.log(' pushNames:', (result.pushNames || []).length)
607
+ })
608
+
609
+ // history_sync_error fires if a chunk fails to download or decrypt
610
+ client.on('history_sync_error', ({ err, notification }) => {
611
+ console.error('History sync failed:', err.message)
612
+ console.error(' syncType:', notification.syncType)
613
+ })
614
+
615
+ await client.init('919634847671')
616
+ ```
617
+
618
+ The `result` object shape emitted by `history_sync`:
619
+
620
+ ```js
621
+ {
622
+ syncType: number, // HistorySyncType enum value
623
+ syncTypeName: string, // 'INITIAL_BOOTSTRAP' | 'RECENT' | 'FULL' | 'PUSH_NAME' | 'NON_BLOCKING_DATA' | 'ON_DEMAND'
624
+ progress: number, // 0-100, server-reported progress
625
+ chunkOrder: number, // chunk sequence number
626
+ chats: [ // one entry per conversation in this chunk
627
+ {
628
+ id: string, // chat JID e.g. '919634847671@s.whatsapp.net' or '120363...@g.us'
629
+ name: string, // display name (may be undefined for unknown contacts)
630
+ unreadCount: number,
631
+ lastMsgTimestamp: number, // Unix seconds
632
+ messageCount: number // number of messages in this chunk for this chat
633
+ }
634
+ ],
635
+ contacts: [ // one entry per contact discovered in this chunk
636
+ {
637
+ id: string,
638
+ name: string,
639
+ username: string, // WhatsApp username if set
640
+ pnJid: string, // phone-number JID e.g. '919634847671@s.whatsapp.net'
641
+ lidJid: string // LID JID e.g. '112345678901234@lid'
642
+ }
643
+ ],
644
+ pushNames: Array, // push-name entries { id, pushname }
645
+ lidPnMappings: Array, // LID<->PN mapping entries { lidJid, pnJid }
646
+ merged: object // raw merged history store (see Reading the History Store below)
647
+ }
648
+ ```
649
+
650
+ Sync type values:
651
+
652
+ | `syncTypeName` | When it fires |
653
+ |---|---|
654
+ | `INITIAL_BOOTSTRAP` | First connect — most recent conversations |
655
+ | `RECENT` | Reconnect after a short offline period |
656
+ | `FULL` | Full historical sync (older messages) |
657
+ | `PUSH_NAME` | Contact name updates only |
658
+ | `NON_BLOCKING_DATA` | Background low-priority data |
659
+ | `ON_DEMAND` | Explicitly requested by the client |
660
+
661
+ ### Persistent Files Written to Disk
662
+
663
+ The library automatically writes these files to `sessionDir` per account. You do not need to create or manage them.
664
+
665
+ | File | Contents |
666
+ |---|---|
667
+ | `<phone>.history.json` | Chats, contacts, push names, LID↔PN mappings, tcTokens |
668
+ | `<phone>.messages.json` | Flat map of `msgId → message metadata` |
669
+ | `<phone>.appStateKeys.json` | App-state sync keys (used for app-state patch decryption) |
670
+ | `<phone>.tctoken.json` | Trusted-contact token store (tcToken per contact JID) |
671
+
672
+ ### Reading the History Store
673
+
674
+ After history sync completes you can read the on-disk files directly:
675
+
676
+ ```js
677
+ const fs = require('fs')
678
+ const path = require('path')
679
+
680
+ const sessDir = path.join(process.env.HOME, '.waSession')
681
+ const phone = '919634847671'
682
+
683
+ // ── Read chats ────────────────────────────────────────────────────────────────
684
+ const histPath = path.join(sessDir, phone + '.history.json')
685
+ const hist = JSON.parse(fs.readFileSync(histPath, 'utf8'))
686
+
687
+ // List all chats sorted by last message time
688
+ const chats = Object.values(hist.chats)
689
+ .sort((a, b) => (b.lastMsgTimestamp || 0) - (a.lastMsgTimestamp || 0))
690
+
691
+ for (const chat of chats.slice(0, 10)) {
692
+ console.log(chat.id, '|', chat.name || '(unknown)', '|', chat.unreadCount, 'unread')
693
+ }
694
+
695
+ // ── LID ↔ PN lookup ───────────────────────────────────────────────────────────
696
+ // Look up LID JID from phone number JID
697
+ const myLid = hist.pnLidMap['919634847671@s.whatsapp.net']
698
+ console.log('LID:', myLid) // e.g. '112345678901234@lid'
699
+
700
+ // Reverse: phone number JID from LID
701
+ const myPn = hist.lidPnMap[myLid]
702
+ console.log('PN:', myPn)
703
+
704
+ // ── Read message metadata ─────────────────────────────────────────────────────
705
+ const msgPath = path.join(sessDir, phone + '.messages.json')
706
+ const msgs = JSON.parse(fs.readFileSync(msgPath, 'utf8'))
707
+ const msgList = Object.values(msgs).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
708
+ console.log('Total messages indexed:', msgList.length)
709
+ console.log('Latest:', msgList[0])
710
+ ```
711
+
712
+ `hist.chats` schema per chat entry:
713
+
714
+ ```js
715
+ {
716
+ id: string, // JID
717
+ name: string,
718
+ displayName: string,
719
+ unreadCount: number,
720
+ lastMsgTimestamp: number, // Unix seconds
721
+ messageCount: number, // total messages indexed from history
722
+ ephemeralExpiry: number, // disappearing messages timer in seconds, if set
723
+ archived: boolean,
724
+ pinned: number, // pin sort order (0 = not pinned)
725
+ tcToken: string, // base64 — trusted-contact token (used internally by the library)
726
+ tcTokenTimestamp: number, // Unix seconds — when the token was issued by the server
727
+ tcTokenSenderTimestamp: number // Unix seconds — sender-side issuance timestamp for 7-day bucket dedup
728
+ }
729
+ ```
730
+
731
+ `msgs` schema per message entry:
732
+
733
+ ```js
734
+ {
735
+ id: string, // WhatsApp message ID
736
+ chatId: string, // JID of the conversation
737
+ fromMe: boolean,
738
+ fromJid: string, // sender JID
739
+ timestamp: number, // Unix seconds
740
+ pushName: string, // display name of sender at send time
741
+ status: number // 0=error 1=pending 2=server 3=delivered 4=read 5=played
742
+ }
743
+ ```
744
+
745
+ ### tcToken — Error 463 Defense
746
+
747
+ > [!IMPORTANT]
748
+ > This section is informational. The entire tcToken lifecycle is **fully automatic**.
749
+ > You do not need to write any code for it.
750
+
751
+ WhatsApp counts every outbound DM sent **without** a `<tctoken>` node as an anonymous "reach-out" event. Once enough such events accumulate the server enforces a time-based **Reach-out Time-lock** and returns error `463` (`NackCallerReachoutTimelocked`), blocking all outbound messages and calls for a period.
752
+
753
+ whalibmob implements the full lifecycle to prevent this:
754
+
755
+ | Step | What the library does automatically |
756
+ |---|---|
757
+ | **History seed** | On every history sync chunk, `tcToken` bytes are extracted from each conversation in the protobuf and loaded into `TcTokenStore` in memory. The first send after reconnect already has a valid token ready — no 463 risk on cold start. |
758
+ | **Attach on send** | Before dispatching any DM, `MessageSender` looks up the token for the recipient JID, checks it has not expired (28-day rolling window), and pushes a `<tctoken>` child node into the message stanza. |
759
+ | **Proactive issuance** | After each successful DM send, the library fires a `<iq type='set' xmlns='privacy'>` requesting a fresh token for that JID from the server — once per 7-day bucket, deduplicated in-flight. |
760
+ | **Incoming notification** | When a contact starts a new conversation, WhatsApp pushes a `<notification type='privacy_token'>`. The library catches it in `_handlePrivacyTokenNotification` and stores the token immediately. |
761
+ | **Identity change re-issue** | When decrypting a `pkmsg` (new Signal session from peer), the library calls `_reissueTcTokenAfterIdentityChange` to re-issue the token for the new session. |
762
+ | **Error 463 recovery** | If a send fails with error 463, the library issues a fresh token, waits for the server response, and automatically retries the same message with the new token attached. |
763
+ | **Expiry** | Tokens use a 4-bucket rolling window (4 × 7 days = 28-day TTL). Expired tokens are cleared before the send and a fresh one is requested proactively. |
764
+
765
+ Token storage uses the **LID JID** of the contact (e.g. `112345678901234@lid`) as the key — never the phone-number JID — matching WhatsApp's internal convention.
766
+
767
+ ### What Is Automatic vs What You Need to Do
768
+
769
+ **Everything in the "Automatic" column requires zero code from you.**
770
+
771
+ | Feature | Automatic | Notes |
772
+ |---|---|---|
773
+ | Download history blob from CDN | ✅ | |
774
+ | Decrypt history blob (AES-256-CBC + HKDF) | ✅ | |
775
+ | Decompress zlib | ✅ | |
776
+ | Decode protobuf (WAProto v2.3000.x) | ✅ | Field numbers verified against official proto |
777
+ | Persist chats / contacts / push names | ✅ | Written to `<phone>.history.json` |
778
+ | Persist message metadata | ✅ | Written to `<phone>.messages.json` |
779
+ | Persist app-state sync keys | ✅ | Written to `<phone>.appStateKeys.json` |
780
+ | Seed tcTokens into memory on connect | ✅ | Prevents error 463 on first send after reconnect |
781
+ | Attach tcToken to every outbound DM | ✅ | |
782
+ | Issue fresh tcTokens after each send | ✅ | Once per 7-day bucket per contact |
783
+ | Handle incoming `privacy_token` notifications | ✅ | |
784
+ | Re-issue tcToken after peer identity change | ✅ | |
785
+ | Recover from error 463 with automatic retry | ✅ | |
786
+ | Populate in-memory LID↔PN maps | ✅ | |
787
+ | Listen to `history_sync` event | 🔵 Optional | Only if your app needs to react to history data |
788
+ | Read `<phone>.history.json` | 🔵 Optional | Only if your app needs chat/contact data at rest |
789
+ | Read `<phone>.messages.json` | 🔵 Optional | Only if your app indexes messages |
790
+
791
+ **Minimum working integration — history sync, tcTokens, and error-463 defense all active with zero extra code:**
792
+
793
+ ```js
794
+ const { WhalibmobClient } = require('whalibmob')
795
+ const path = require('path')
796
+
797
+ const client = new WhalibmobClient({
798
+ sessionDir: path.join(process.env.HOME, '.waSession')
799
+ })
800
+
801
+ // History sync, tcToken seeding, error-463 defense, and all disk persistence
802
+ // happen automatically. Add the listeners below only if your app needs the data.
803
+
804
+ client.on('history_sync', result => {
805
+ // Optional — fires once per chunk (multiple times on first connect)
806
+ console.log('[', result.syncTypeName, ']',
807
+ result.chats.length, 'chats,',
808
+ result.contacts.length, 'contacts')
809
+ })
810
+
811
+ client.on('history_sync_error', ({ err }) => {
812
+ // Optional — log failures (library continues working even if a chunk fails)
813
+ console.error('History chunk failed:', err.message)
814
+ })
815
+
816
+ await client.init('919634847671')
817
+ ```
818
+
819
+
553
820
  ## Receiving Media
554
821
 
555
822
  When a media message arrives, `msg.decoded` contains a CDN `url` and a `mediaKey`.
package/index.js CHANGED
@@ -17,6 +17,19 @@ const {
17
17
  assertMeId,
18
18
  initAuthCreds
19
19
  } = require('./lib/auth-utils');
20
+ const {
21
+ processHistorySyncNotification,
22
+ decodeHistorySyncNotification,
23
+ decodeHistorySync,
24
+ decodeAppStateSyncKeyShare,
25
+ saveAppStateSyncKeys,
26
+ loadAppStateSyncKeys,
27
+ mergeHistoryToStore,
28
+ decryptHistoryBlob,
29
+ deriveHistoryKeys,
30
+ HistorySyncType,
31
+ HistorySyncTypeName
32
+ } = require('./lib/HistorySyncHandler');
20
33
 
21
34
  module.exports = {
22
35
  WhalibmobClient,
@@ -59,5 +72,17 @@ module.exports = {
59
72
  makeCacheableSignalKeyStore,
60
73
  addTransactionCapability,
61
74
  assertMeId,
62
- initAuthCreds
75
+ initAuthCreds,
76
+ // History sync
77
+ processHistorySyncNotification,
78
+ decodeHistorySyncNotification,
79
+ decodeHistorySync,
80
+ decodeAppStateSyncKeyShare,
81
+ saveAppStateSyncKeys,
82
+ loadAppStateSyncKeys,
83
+ mergeHistoryToStore,
84
+ decryptHistoryBlob,
85
+ deriveHistoryKeys,
86
+ HistorySyncType,
87
+ HistorySyncTypeName
63
88
  };
package/lib/Client.js CHANGED
@@ -13,6 +13,13 @@ const { createNewStore, saveStore, loadStore, toSixParts, fromSixParts } = requi
13
13
  const { BinaryNode } = require('./BinaryNode');
14
14
  const { SignalProtocol } = require('./signal/SignalProtocol');
15
15
  const { DeviceManager } = require('./DeviceManager');
16
+ const {
17
+ processHistorySyncNotification,
18
+ decodeHistorySyncNotification,
19
+ decodeAppStateSyncKeyShare,
20
+ saveAppStateSyncKeys,
21
+ HistorySyncType
22
+ } = require('./HistorySyncHandler');
16
23
 
17
24
  const PING_INTERVAL_MS = 25000;
18
25
  const KEEPALIVE_INTERVAL = 20000;
@@ -699,8 +706,22 @@ class WhalibmobClient extends EventEmitter {
699
706
  return;
700
707
  }
701
708
 
702
- // Protocol messages (revoke, ephemeral, etc.) silent ACK
703
- if (decoded.type === 'protocol') { _whaDbg('[DBG] DM_FILTER proto id=' + id); return; }
709
+ // Protocol messages handle history sync and app-state key share before dropping
710
+ if (decoded.type === 'protocol') {
711
+ _whaDbg('[DBG] PROTO subtype=' + decoded.subtype + ' id=' + id);
712
+
713
+ // ── HISTORY_SYNC_NOTIFICATION ─────────────────────────────────────
714
+ if (decoded.subtype === 'history_sync' && decoded.historySyncNotification) {
715
+ this._handleHistorySync(decoded.historySyncNotification, id);
716
+ }
717
+
718
+ // ── APP_STATE_SYNC_KEY_SHARE ──────────────────────────────────────
719
+ if (decoded.subtype === 'app_state_sync_key_share' && decoded.appStateSyncKeyShare) {
720
+ this._handleAppStateSyncKeyShare(decoded.appStateSyncKeyShare);
721
+ }
722
+
723
+ return;
724
+ }
704
725
 
705
726
  this.emit('message', { id, from, participant, ts, decoded, node });
706
727
  this._sendReadReceipt(id, fromRaw, partRaw);
@@ -728,7 +749,15 @@ class WhalibmobClient extends EventEmitter {
728
749
  const plaintext = this._signal.senderKeyDecrypt(from, participant, cipherBuf);
729
750
  const decoded = this._decodeMsg(plaintext);
730
751
  if (decoded.type === 'senderKeyDistribution') return;
731
- if (decoded.type === 'protocol') return;
752
+ if (decoded.type === 'protocol') {
753
+ if (decoded.subtype === 'history_sync' && decoded.historySyncNotification) {
754
+ this._handleHistorySync(decoded.historySyncNotification, id);
755
+ }
756
+ if (decoded.subtype === 'app_state_sync_key_share' && decoded.appStateSyncKeyShare) {
757
+ this._handleAppStateSyncKeyShare(decoded.appStateSyncKeyShare);
758
+ }
759
+ return;
760
+ }
732
761
  this.emit('message', { id, from, participant, ts, decoded, isGroup: true, node });
733
762
  this._sendReadReceipt(id, fromRaw, partRaw);
734
763
  } catch (err) {
@@ -1020,6 +1049,14 @@ class WhalibmobClient extends EventEmitter {
1020
1049
  this._handlePrivacyTokenNotification(node);
1021
1050
  }
1022
1051
 
1052
+ if (type === 'md-msg-hist') {
1053
+ // Server signals that history sync data is available.
1054
+ // The actual encrypted notification may arrive as:
1055
+ // (a) an encrypted <message> containing HistorySyncNotification (primary path, handled in _decryptDMMessage)
1056
+ // (b) inline in this notification node via an <enc> child (handle here)
1057
+ this._handleMdMsgHistNotification(node);
1058
+ }
1059
+
1023
1060
  if (type === 'devices') {
1024
1061
  // Server push: a contact's linked device list changed (they linked or
1025
1062
  // unlinked a tablet, desktop, etc.). Invalidate that phone's cache entry
@@ -1710,6 +1747,246 @@ class WhalibmobClient extends EventEmitter {
1710
1747
  })();
1711
1748
  }
1712
1749
 
1750
+ // ─── History sync ─────────────────────────────────────────────────────────
1751
+
1752
+ /**
1753
+ * _handleHistorySync — called when a decrypted DM protocol message contains
1754
+ * subtype='history_sync' with a HistorySyncNotification.
1755
+ *
1756
+ * Fires asynchronously (fire-and-forget with error emission).
1757
+ * Emits 'history_sync' on success, 'history_sync_error' on failure.
1758
+ *
1759
+ * @param {object} notification Decoded HistorySyncNotification
1760
+ * @param {string} msgId Source message ID (for logging)
1761
+ */
1762
+ _handleHistorySync(notification, msgId) {
1763
+ if (!notification) return;
1764
+ const phone = this._store && this._store.phoneNumber;
1765
+ const sessionDir = this._sessionDir;
1766
+
1767
+ _whaDbg('[DBG] HIST_SYNC start syncType=' + notification.syncType +
1768
+ ' progress=' + notification.progress +
1769
+ ' chunk=' + notification.chunkOrder +
1770
+ ' inline=' + !!(notification.initialHistBootstrapInlinePayload &&
1771
+ notification.initialHistBootstrapInlinePayload.length) +
1772
+ ' msgId=' + msgId);
1773
+
1774
+ processHistorySyncNotification(notification, sessionDir, phone)
1775
+ .then(result => {
1776
+ _whaDbg('[DBG] HIST_SYNC done syncType=' + result.syncTypeName +
1777
+ ' chats=' + result.chats.length +
1778
+ ' contacts=' + result.contacts.length +
1779
+ ' pushNames=' + (result.pushNames || []).length);
1780
+
1781
+ // Populate in-memory LID ↔ PN mappings from history sync data
1782
+ if (result.lidPnMappings && result.lidPnMappings.length) {
1783
+ for (const m of result.lidPnMappings) {
1784
+ if (m.lidJid && m.pnJid) {
1785
+ const lidUser = m.lidJid.split('@')[0].split(':')[0];
1786
+ const pnUser = m.pnJid.split('@')[0].split(':')[0];
1787
+ if (lidUser && pnUser) {
1788
+ this._lidToPn.set(lidUser, pnUser);
1789
+ this._pnToLid.set(pnUser, lidUser);
1790
+ if (this._signal && this._signal.store && this._signal.store.setLidMapping) {
1791
+ this._signal.store.setLidMapping(pnUser, lidUser);
1792
+ }
1793
+ }
1794
+ }
1795
+ }
1796
+ }
1797
+
1798
+ // Seed TcTokenStore from history — populates trusted-contact tokens
1799
+ // for all existing conversations so the first send after reconnect
1800
+ // already carries a valid <tctoken> node (avoids error 463 on cold start).
1801
+ this._seedTcTokensFromHistory(result.merged);
1802
+
1803
+ this.emit('history_sync', result);
1804
+ })
1805
+ .catch(err => {
1806
+ _whaDbg('[DBG] HIST_SYNC_ERR ' + (err && err.message));
1807
+ this.emit('history_sync_error', { err, notification });
1808
+ });
1809
+ }
1810
+
1811
+ /**
1812
+ * _seedTcTokensFromHistory — called after each history sync chunk.
1813
+ *
1814
+ * Reads tcToken/tcTokenTimestamp/tcTokenSenderTimestamp from the merged
1815
+ * history store and loads them into TcTokenStore so that the first outbound
1816
+ * DM after reconnect already has a valid <tctoken> node attached.
1817
+ *
1818
+ * Without this, TcTokenStore starts empty on every new connection and the
1819
+ * first send to every existing contact is treated as an anonymous reach-out
1820
+ * by the WhatsApp server, accumulating toward the error-463 timelock.
1821
+ *
1822
+ * Rules:
1823
+ * - Only seeds tokens that are not already in TcTokenStore (existing entry wins).
1824
+ * - Skips expired tokens (no point loading a token we'd immediately discard).
1825
+ * - Storage key follows TcTokenStore convention: normalized bare JID.
1826
+ * For LID chats (jid ends in @lid) we use the lid JID directly.
1827
+ * For PN chats we try to look up the LID first (Baileys always stores under LID).
1828
+ *
1829
+ * @param {object|null} merged - Return value of mergeHistoryToStore()
1830
+ */
1831
+ _seedTcTokensFromHistory(merged) {
1832
+ if (!merged || !merged.chats) return;
1833
+ if (!this._tcTokenStore) return;
1834
+
1835
+ const { normalizeJidForTcToken, tcTokenExpired } = require('./messages/TcTokenStore');
1836
+
1837
+ let seeded = 0;
1838
+ for (const [jid, chat] of Object.entries(merged.chats)) {
1839
+ // Only chats that actually have a token stored from history
1840
+ if (!chat.tcToken) continue;
1841
+
1842
+ // Convert base64 string back to Buffer (mergeHistoryToStore stores as base64)
1843
+ const tokenBuf = Buffer.from(chat.tcToken, 'base64');
1844
+ if (!tokenBuf.length) continue;
1845
+
1846
+ const ts = chat.tcTokenTimestamp || 0;
1847
+ const senderTs = chat.tcTokenSenderTimestamp || 0;
1848
+
1849
+ // Skip already-expired tokens — no use loading them
1850
+ if (tcTokenExpired(ts)) {
1851
+ _whaDbg('[DBG] SEED_TCTOKEN skip expired jid=' + jid + ' ts=' + ts);
1852
+ continue;
1853
+ }
1854
+
1855
+ // Resolve storage JID: Baileys always stores under LID.
1856
+ // If this chat is a @lid jid, use it directly.
1857
+ // If it's a PN jid, try to find its LID counterpart.
1858
+ let storageJid;
1859
+ if (jid.endsWith('@lid')) {
1860
+ storageJid = normalizeJidForTcToken(jid);
1861
+ } else {
1862
+ const pnUser = jid.split('@')[0].split(':')[0];
1863
+ const lidUser = this._pnToLid && this._pnToLid.get(pnUser);
1864
+ storageJid = lidUser
1865
+ ? normalizeJidForTcToken(lidUser + '@lid')
1866
+ : normalizeJidForTcToken(jid);
1867
+ }
1868
+
1869
+ // Don't overwrite a token that was already fetched live this session
1870
+ const existing = this._tcTokenStore.get(storageJid);
1871
+ if (existing && existing.token && existing.token.length &&
1872
+ !tcTokenExpired(existing.timestamp)) {
1873
+ _whaDbg('[DBG] SEED_TCTOKEN skip existing jid=' + storageJid);
1874
+ continue;
1875
+ }
1876
+
1877
+ // Set token + senderTimestamp atomically via the two TcTokenStore setters
1878
+ this._tcTokenStore.setToken(storageJid, tokenBuf, ts);
1879
+ if (senderTs) {
1880
+ this._tcTokenStore.setSenderTimestamp(storageJid, senderTs);
1881
+ }
1882
+ seeded++;
1883
+ _whaDbg('[DBG] SEED_TCTOKEN seeded jid=' + storageJid +
1884
+ ' ts=' + ts + ' senderTs=' + senderTs + ' len=' + tokenBuf.length);
1885
+ }
1886
+
1887
+ if (seeded > 0) {
1888
+ _whaDbg('[DBG] SEED_TCTOKEN total seeded=' + seeded + ' from history');
1889
+ }
1890
+ }
1891
+
1892
+ /**
1893
+ * _handleAppStateSyncKeyShare — saves app-state sync keys received in a
1894
+ * ProtocolMessage (type=5 APP_STATE_SYNC_KEY_SHARE).
1895
+ * These keys are needed to decrypt app-state patches in _syncAppState.
1896
+ *
1897
+ * @param {object} keyShare Decoded AppStateSyncKeyShare { keys: [...] }
1898
+ */
1899
+ _handleAppStateSyncKeyShare(keyShare) {
1900
+ if (!keyShare || !keyShare.keys || !keyShare.keys.length) return;
1901
+ const phone = this._store && this._store.phoneNumber;
1902
+ const sessionDir = this._sessionDir;
1903
+
1904
+ _whaDbg('[DBG] APP_STATE_KEY_SHARE keys=' + keyShare.keys.length);
1905
+
1906
+ try {
1907
+ saveAppStateSyncKeys(sessionDir, phone, keyShare);
1908
+ } catch (e) {
1909
+ _whaDbg('[DBG] APP_STATE_KEY_SHARE_SAVE_ERR ' + e.message);
1910
+ }
1911
+
1912
+ this.emit('app_state_keys', { keys: keyShare.keys });
1913
+ }
1914
+
1915
+ /**
1916
+ * _handleMdMsgHistNotification — called when server sends a
1917
+ * <notification type="md-msg-hist"> node.
1918
+ *
1919
+ * On mobile WhatsApp the primary history sync path is via encrypted DM
1920
+ * protocol messages (HistorySyncNotification inside a ProtocolMessage type=6).
1921
+ * This handler covers the secondary path where the server delivers the
1922
+ * notification inline inside the notification node itself with an <enc> child,
1923
+ * or simply signals availability so we emit the event.
1924
+ *
1925
+ * @param {object} node Full BinaryNode of the notification
1926
+ */
1927
+ _handleMdMsgHistNotification(node) {
1928
+ if (!node) return;
1929
+ const attrs = node.attrs || {};
1930
+
1931
+ _whaDbg('[DBG] MD_MSG_HIST from=' + (attrs.from || '') + ' id=' + (attrs.id || ''));
1932
+
1933
+ // Check for inline HistorySyncNotification bytes in an <enc> child
1934
+ // (some server versions deliver it this way)
1935
+ const encNode = findChild(node, 'enc');
1936
+ if (encNode) {
1937
+ const encBuf = Buffer.isBuffer(encNode.content)
1938
+ ? encNode.content
1939
+ : (encNode.content ? Buffer.from(encNode.content) : null);
1940
+
1941
+ if (encBuf && encBuf.length > 0) {
1942
+ // Try to decode directly as HistorySyncNotification (unencrypted path)
1943
+ try {
1944
+ const notification = decodeHistorySyncNotification(encBuf);
1945
+ if (notification && (notification.directPath || notification.initialHistBootstrapInlinePayload)) {
1946
+ _whaDbg('[DBG] MD_MSG_HIST inline notification directPath=' + notification.directPath);
1947
+ this._handleHistorySync(notification, attrs.id || 'md-msg-hist');
1948
+ return;
1949
+ }
1950
+ } catch (_) {}
1951
+ }
1952
+ }
1953
+
1954
+ // Emit raw event for the caller to inspect if needed
1955
+ this.emit('md_msg_hist', { node, attrs });
1956
+ }
1957
+
1958
+ /**
1959
+ * getHistoryStore — load the persisted history store for this phone.
1960
+ * Returns the parsed {phone}.history.json content or null if not yet synced.
1961
+ *
1962
+ * @returns {object|null}
1963
+ */
1964
+ getHistoryStore() {
1965
+ if (!this._store || !this._store.phoneNumber) return null;
1966
+ const fs = require('fs');
1967
+ const file = require('path').join(this._sessionDir, this._store.phoneNumber + '.history.json');
1968
+ try {
1969
+ if (!fs.existsSync(file)) return null;
1970
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
1971
+ } catch (_) { return null; }
1972
+ }
1973
+
1974
+ /**
1975
+ * getMessagesStore — load the persisted messages for this phone.
1976
+ * Returns the parsed {phone}.messages.json content or null if not yet synced.
1977
+ *
1978
+ * @returns {object|null}
1979
+ */
1980
+ getMessagesStore() {
1981
+ if (!this._store || !this._store.phoneNumber) return null;
1982
+ const fs = require('fs');
1983
+ const file = require('path').join(this._sessionDir, this._store.phoneNumber + '.messages.json');
1984
+ try {
1985
+ if (!fs.existsSync(file)) return null;
1986
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
1987
+ } catch (_) { return null; }
1988
+ }
1989
+
1713
1990
  // ─── Public API ───────────────────────────────────────────────────────────
1714
1991
 
1715
1992
  setPresence(available) {