whalibmob 5.5.80 → 5.5.82

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
@@ -359,7 +359,23 @@ class WhalibmobClient extends EventEmitter {
359
359
  }
360
360
  }
361
361
  if (attrs.platform) this._platform = attrs.platform;
362
- if (attrs.lid) this._myLid = String(attrs.lid);
362
+ if (attrs.lid) {
363
+ this._myLid = String(attrs.lid);
364
+ // Record our own PN ↔ LID pair alongside every contact's. Prekey bundles
365
+ // are only ever answered for phone JIDs, so opening a session with one of
366
+ // our own LID-addressed linked devices needs this mapping just as much as
367
+ // a contact's does — without it the bundle IQ goes out addressed to @lid
368
+ // and is never answered.
369
+ const ownLidUser = this._myLid.split('@')[0].split(':')[0];
370
+ const ownPnUser = String(this._store && this._store.phoneNumber || '');
371
+ if (ownLidUser && ownPnUser) {
372
+ this._lidToPn.set(ownLidUser, ownPnUser);
373
+ this._pnToLid.set(ownPnUser, ownLidUser);
374
+ if (this._signal && this._signal.store && this._signal.store.setLidMapping) {
375
+ this._signal.store.setLidMapping(ownPnUser, ownLidUser);
376
+ }
377
+ }
378
+ }
363
379
 
364
380
  const devIdNode = findChild(node, 'device-identity');
365
381
  if (devIdNode) {
@@ -674,7 +674,7 @@ class DeviceManager {
674
674
  // 3. fetchBundles(fetchList) — only for truly new sessions
675
675
  // 4. buildSessionFromBundle for each new session
676
676
  //
677
- async bulkEnsureSessions(phones, signalProto, allowPkmsg = true) {
677
+ async bulkEnsureSessions(phones, signalProto, allowPkmsg = true, _isRetry = false) {
678
678
  const uniquePhones = [...new Set(phones)];
679
679
  if (uniquePhones.length === 0) return [];
680
680
 
@@ -692,15 +692,34 @@ class DeviceManager {
692
692
  }
693
693
 
694
694
  if (allowPkmsg && fetchJids.length > 0) {
695
+ // Keep the requested string for each device: the server echoes JIDs back in
696
+ // its own normalised form and the participants node has to name the device
697
+ // the same way we enumerated it.
698
+ const wanted = new Map(fetchJids.map(j => [this._jidToAddr(j), j]));
695
699
  const bundles = await this.fetchBundles(fetchJids);
696
700
  for (const [jid, bundle] of bundles) {
701
+ const target = wanted.get(this._jidToAddr(jid)) || jid;
697
702
  try {
698
- await signalProto.buildSessionFromBundle(jid, bundle);
699
- readyJids.push(jid);
703
+ await signalProto.buildSessionFromBundle(target, bundle);
704
+ readyJids.push(target);
700
705
  } catch (_) {}
701
706
  }
702
707
  }
703
708
 
709
+ // The same guarantee the LID path makes: every enumerated device has to end
710
+ // up with a session, because every one of them becomes a <to><enc> entry and
711
+ // a participant list that does not cover the account's current devices comes
712
+ // back as ack 479. Coming up short means the cached list is out of date — a
713
+ // device has been unlinked since, so the server has no bundle for it. Drop
714
+ // the list and enumerate once more against what the account has now.
715
+ if (allowPkmsg && readyJids.length < deviceJids.length && !_isRetry) {
716
+ _whaDbg('[DBG] PN_DEVICE_LIST_STALE phones=[' + uniquePhones.join(',') + ']' +
717
+ ' enumerated=' + deviceJids.length + ' sessions=' + readyJids.length +
718
+ ' — re-running usync\n');
719
+ this.clearCache(uniquePhones);
720
+ return this.bulkEnsureSessions(uniquePhones, signalProto, allowPkmsg, /* _isRetry */ true);
721
+ }
722
+
704
723
  // Fallback: if still nothing, use main device JIDs directly
705
724
  if (readyJids.length === 0) {
706
725
  for (const phone of uniquePhones) readyJids.push(makeDeviceJid(phone, 0));
@@ -718,7 +737,7 @@ class DeviceManager {
718
737
  // 2. Establishes Signal sessions for LID device JIDs (e.g. 139471160877194:0@lid)
719
738
  // 3. Returns the list of ready LID device JIDs for participant fanout
720
739
  //
721
- async bulkEnsureSessionsForLid(lidUser, signalProto, allowPkmsg = true, skipUsync = false) {
740
+ async bulkEnsureSessionsForLid(lidUser, signalProto, allowPkmsg = true, skipUsync = false, _isRetry = false) {
722
741
  const lidJid = `${lidUser}@lid`;
723
742
  const cacheKey = `lid:${lidUser}`;
724
743
 
@@ -743,7 +762,47 @@ class DeviceManager {
743
762
  _whaDbg('[DBG] LID_DEVICES lidUser=' + lidUser +
744
763
  ' deviceJids=[' + deviceJids.join(',') + ']\n');
745
764
 
746
- // Split: existing sessions (ready) vs new (need bundle fetch)
765
+ const readyJids = await this._ensureLidSessions(lidUser, deviceJids, signalProto, allowPkmsg);
766
+
767
+ // Every enumerated device must end up with a session, because every one of
768
+ // them becomes a <to><enc> entry. Coming up short means the cached device
769
+ // list no longer matches the account: a device that has since been unlinked
770
+ // is still listed, the server has no bundle for it, and the stanza goes out
771
+ // missing an entry the server still expects — which it rejects with ack 479.
772
+ //
773
+ // Seen exactly that way in a live trace: five devices enumerated from cache
774
+ // (0, 49, 51, 55, 57), four bundles returned, device 51 left without a
775
+ // session, four participants sent, 479 back.
776
+ //
777
+ // Drop the stale list and re-enumerate once, so the participants match what
778
+ // the server currently believes the account has.
779
+ if (readyJids.length < deviceJids.length && !_isRetry) {
780
+ _whaDbg('[DBG] LID_DEVICE_LIST_STALE lidUser=' + lidUser +
781
+ ' enumerated=' + deviceJids.length + ' sessions=' + readyJids.length +
782
+ ' — re-running usync\n');
783
+ this._dcDel([cacheKey]);
784
+ return this.bulkEnsureSessionsForLid(
785
+ lidUser, signalProto, allowPkmsg, /* skipUsync */ false, /* _isRetry */ true);
786
+ }
787
+
788
+ // Fallback: use primary LID device 0 directly (no session yet, will use pkmsg)
789
+ if (readyJids.length === 0) {
790
+ readyJids.push(makeDeviceJid(lidUser, 0, 'lid'));
791
+ }
792
+
793
+ return readyJids;
794
+ }
795
+
796
+ // ─── Open Signal sessions for a set of @lid device JIDs ────────────────────
797
+ //
798
+ // Shared by the recipient path (bulkEnsureSessionsForLid), the own-device path
799
+ // (ensureOwnLidDeviceSessions) and the repair path (rebuildSessionsForJids).
800
+ // Returns the device JIDs — always in @lid form — that ended up with a usable
801
+ // session.
802
+ //
803
+ // pnOverride lets a caller supply the phone number to fetch bundles with when
804
+ // the LID→PN map does not have it yet (our own LID, most notably).
805
+ async _ensureLidSessions(lidUser, deviceJids, signalProto, allowPkmsg, pnOverride) {
747
806
  const readyJids = [];
748
807
  const fetchJids = [];
749
808
  for (const jid of deviceJids) {
@@ -754,70 +813,155 @@ class DeviceManager {
754
813
  }
755
814
  }
756
815
 
757
- if (allowPkmsg && fetchJids.length > 0) {
758
- // Ask for the bundles by phone JID. The encrypt IQ is not answered at all
759
- // when its <user> entries are @lid — it just times out — while the same
760
- // request in phone form comes back in full. Translate through the known
761
- // LID→PN mapping, keeping a link back so each bundle is installed under
762
- // the LID address the participants node will use.
763
- const pnForLid = this._client._lidToPn && this._client._lidToPn.get(lidUser);
764
- const wireToLid = new Map();
765
- let wireJids = fetchJids;
766
- if (pnForLid) {
767
- wireJids = fetchJids.map(j => {
768
- const at = j.indexOf('@');
769
- const base = at >= 0 ? j.slice(0, at) : j;
770
- const colon = base.indexOf(':');
771
- const dev = colon >= 0 ? (parseInt(base.slice(colon + 1), 10) || 0) : 0;
772
- const wire = makeDeviceJid(pnForLid, dev, 's.whatsapp.net');
773
- wireToLid.set(wire, j);
774
- return wire;
775
- });
776
- _whaDbg('[DBG] LID_BUNDLE_VIA_PN lid=' + lidUser + ' pn=' + pnForLid +
777
- ' jids=[' + wireJids.join(',') + ']\n');
816
+ if (!allowPkmsg || fetchJids.length === 0) return readyJids;
817
+
818
+ // Ask for the bundles by phone JID. The encrypt IQ is not answered at all
819
+ // when its <user> entries are @lid — it just times out — while the same
820
+ // request in phone form comes back in full. Translate through the known
821
+ // LID→PN mapping, keeping a link back so each bundle is installed under
822
+ // the LID address the participants node will use.
823
+ const pnForLid = pnOverride || (this._client._lidToPn && this._client._lidToPn.get(lidUser));
824
+ const wireToLid = new Map();
825
+ let wireJids = fetchJids;
826
+ if (pnForLid) {
827
+ wireJids = fetchJids.map(j => {
828
+ const at = j.indexOf('@');
829
+ const base = at >= 0 ? j.slice(0, at) : j;
830
+ const colon = base.indexOf(':');
831
+ const dev = colon >= 0 ? (parseInt(base.slice(colon + 1), 10) || 0) : 0;
832
+ const wire = makeDeviceJid(pnForLid, dev, 's.whatsapp.net');
833
+ wireToLid.set(wire, j);
834
+ return wire;
835
+ });
836
+ _whaDbg('[DBG] LID_BUNDLE_VIA_PN lid=' + lidUser + ' pn=' + pnForLid +
837
+ ' jids=[' + wireJids.join(',') + ']\n');
838
+ } else {
839
+ _whaDbg('[DBG] LID_BUNDLE_NO_PN lid=' + lidUser + ' — requesting by LID\n');
840
+ }
841
+
842
+ const bundles = await this.fetchBundles(wireJids);
843
+ for (const [wireJid, bundle] of bundles) {
844
+ // Install under the @lid address: that is what the outgoing
845
+ // participants node — and therefore the encrypt step — looks the
846
+ // session up by. Stored under the phone user it is a different Signal
847
+ // address and would never be found.
848
+ const jid = wireToLid.get(wireJid) || wireJid;
849
+ try {
850
+ await signalProto.buildSessionFromBundle(jid, bundle);
851
+ // ── CRITICAL: remap bundle JID back to @lid format ────────────────────
852
+ // fetchBundles always returns JIDs in @s.whatsapp.net format (because the
853
+ // WA server uses its own AD_JID / JID_PAIR encoding in responses).
854
+ // But the outer <message to="lidUser@lid"> requires ALL participant <to>
855
+ // nodes to also use @lid format — mixing @s.whatsapp.net participants with
856
+ // a @lid routing address causes the server to close the connection.
857
+ //
858
+ // A Signal address is (user, device) with the @server stripped — but
859
+ // the user of a LID and the user of a phone JID are different strings,
860
+ // so a session is NOT reachable under both. That assumption is what
861
+ // left sends failing: bundles fetched in phone form were stored under
862
+ // the phone user while the participants node looked them up by LID.
863
+ // The session is installed under the @lid address above; this only
864
+ // normalises the JID pushed into readyJids.
865
+ const jidBase = jid.replace(/@(?:s\.whatsapp\.net|lid)$/, '');
866
+ const lidJidForDevice = jidBase + '@lid';
867
+ readyJids.push(lidJidForDevice);
868
+ _whaDbg('[DBG] LID_SESSION_BUILT jid=' + jid + ' → participant=' + lidJidForDevice);
869
+ } catch (e) {
870
+ _whaDbg('[DBG] LID_SESSION_ERR jid=' + jid + ' err=' + e.message);
871
+ }
872
+ }
873
+
874
+ return readyJids;
875
+ }
876
+
877
+ // ─── Own linked devices, addressed in LID space ────────────────────────────
878
+ //
879
+ // ensureOwnDeviceSessions returns @s.whatsapp.net JIDs. Dropping those into a
880
+ // stanza that is routed to an @lid — a LID-addressed group, or a DM to an @lid
881
+ // — mixes two address families in one participant list, which the server
882
+ // rejects with ack 479. This is the LID-space equivalent: same device list,
883
+ // expressed as <ourLid>:<device>@lid.
884
+ //
885
+ // Device 0 is excluded exactly as it is on the phone path: it is the device
886
+ // doing the sending, and both Baileys and whatsmeow skip the exact sender
887
+ // device when building participants.
888
+ async ensureOwnLidDeviceSessions(lidUser, ownPhone, signalProto, allowPkmsg = true) {
889
+ if (!lidUser) return [];
890
+ const cacheKey = 'lid:' + lidUser;
891
+
892
+ if (!this._dcHas(cacheKey)) {
893
+ // Our LID and our phone number address the same account, so the device
894
+ // list is the same one. Reuse it when it is already known rather than
895
+ // spending a second usync IQ on ourselves.
896
+ const ownIds = ownPhone ? this._dcGet(ownPhone) : null;
897
+ if (ownIds && ownIds.length) {
898
+ this._dcSet(cacheKey, ownIds.slice());
778
899
  } else {
779
- _whaDbg('[DBG] LID_BUNDLE_NO_PN lid=' + lidUser + ' — requesting by LID\n');
900
+ await this._doUsyncIqByJid(`${lidUser}@lid`, cacheKey, lidUser);
780
901
  }
902
+ }
903
+
904
+ const deviceIds = (this._dcGet(cacheKey) || []).filter(d => d !== 0);
905
+ const deviceJids = deviceIds.map(d => makeDeviceJid(lidUser, d, 'lid'));
906
+ if (deviceJids.length === 0) return [];
907
+
908
+ _whaDbg('[DBG] OWN_LID_DEVICES lid=' + lidUser + ' jids=[' + deviceJids.join(',') + ']');
909
+ return this._ensureLidSessions(lidUser, deviceJids, signalProto, allowPkmsg, ownPhone);
910
+ }
781
911
 
782
- const bundles = await this.fetchBundles(wireJids);
783
- for (const [wireJid, bundle] of bundles) {
784
- // Install under the @lid address: that is what the outgoing
785
- // participants node — and therefore the encrypt step — looks the
786
- // session up by. Stored under the phone user it is a different Signal
787
- // address and would never be found.
788
- const jid = wireToLid.get(wireJid) || wireJid;
912
+ // ─── Rebuild sessions for devices that failed to encrypt ───────────────────
913
+ //
914
+ // Drops whatever Signal state is there and opens a brand-new session from a
915
+ // fresh prekey bundle, in whichever address family the JID came in. Used to
916
+ // close a gap in the participant list before the stanza is dispatched — a
917
+ // missing <to> entry is what the server answers with ack 479.
918
+ //
919
+ // Returns the JIDs that now have a session, in the same string form they were
920
+ // requested with, so the caller can use them as participant targets directly.
921
+ async rebuildSessionsForJids(jids, signalProto) {
922
+ const unique = [...new Set((jids || []).map(String))].filter(Boolean);
923
+ if (unique.length === 0) return [];
924
+
925
+ for (const jid of unique) {
926
+ try { await signalProto.deleteSession(jid); } catch (_) {}
927
+ }
928
+
929
+ const ready = [];
930
+ const lidJids = unique.filter(j => j.endsWith('@lid'));
931
+ const pnJids = unique.filter(j => !j.endsWith('@lid'));
932
+
933
+ if (pnJids.length > 0) {
934
+ // The server echoes JIDs in its own normalised form, so map each bundle
935
+ // back onto the exact string the caller asked for via (user, device).
936
+ const wanted = new Map(pnJids.map(j => [this._jidToAddr(j), j]));
937
+ const bundles = await this.fetchBundles(pnJids).catch(() => new Map());
938
+ for (const [jid, bundle] of bundles) {
939
+ const target = wanted.get(this._jidToAddr(jid)) || jid;
789
940
  try {
790
- await signalProto.buildSessionFromBundle(jid, bundle);
791
- // ── CRITICAL: remap bundle JID back to @lid format ────────────────────
792
- // fetchBundles always returns JIDs in @s.whatsapp.net format (because the
793
- // WA server uses its own AD_JID / JID_PAIR encoding in responses).
794
- // But the outer <message to="lidUser@lid"> requires ALL participant <to>
795
- // nodes to also use @lid format — mixing @s.whatsapp.net participants with
796
- // a @lid routing address causes the server to close the connection.
797
- //
798
- // A Signal address is (user, device) with the @server stripped — but
799
- // the user of a LID and the user of a phone JID are different strings,
800
- // so a session is NOT reachable under both. That assumption is what
801
- // left sends failing: bundles fetched in phone form were stored under
802
- // the phone user while the participants node looked them up by LID.
803
- // The session is installed under the @lid address above; this only
804
- // normalises the JID pushed into readyJids.
805
- const jidBase = jid.replace(/@(?:s\.whatsapp\.net|lid)$/, '');
806
- const lidJidForDevice = jidBase + '@lid';
807
- readyJids.push(lidJidForDevice);
808
- _whaDbg('[DBG] LID_SESSION_BUILT jid=' + jid + ' → participant=' + lidJidForDevice);
941
+ await signalProto.buildSessionFromBundle(target, bundle);
942
+ ready.push(target);
809
943
  } catch (e) {
810
- _whaDbg('[DBG] LID_SESSION_ERR jid=' + jid + ' err=' + e.message);
944
+ _whaDbg('[DBG] REBUILD_SESSION_ERR jid=' + target + ' err=' + e.message);
811
945
  }
812
946
  }
813
947
  }
814
948
 
815
- // Fallback: use primary LID device 0 directly (no session yet, will use pkmsg)
816
- if (readyJids.length === 0) {
817
- readyJids.push(makeDeviceJid(lidUser, 0, 'lid'));
949
+ // LID devices are grouped per LID user so each group can be translated
950
+ // through that user's phone number when the bundles are requested.
951
+ const byLidUser = new Map();
952
+ for (const j of lidJids) {
953
+ const u = phoneFromJid(j);
954
+ if (!byLidUser.has(u)) byLidUser.set(u, []);
955
+ byLidUser.get(u).push(j);
956
+ }
957
+ for (const [lidUser, group] of byLidUser) {
958
+ const got = await this._ensureLidSessions(lidUser, group, signalProto, true)
959
+ .catch(() => []);
960
+ ready.push(...got);
818
961
  }
819
962
 
820
- return readyJids;
963
+ _whaDbg('[DBG] REBUILD_SESSIONS requested=' + unique.length + ' ready=' + ready.length);
964
+ return ready;
821
965
  }
822
966
 
823
967
  // ─── Contact usync fallback ────────────────────────────────────────────────
@@ -165,10 +165,37 @@ function getContent(node) {
165
165
  return null;
166
166
  }
167
167
 
168
+ // The "AD string" form of a JID — user.<agent>:<device>@server — which is what
169
+ // the participant hash is computed over. The agent byte is 0 for both
170
+ // s.whatsapp.net and lid users, so "40712345678.0:2@s.whatsapp.net" and
171
+ // "112713111982325.0:0@lid".
172
+ function adString(jid) {
173
+ const str = String(jid);
174
+ const at = str.indexOf('@');
175
+ const raw = at >= 0 ? str.slice(0, at) : str;
176
+ const server = at >= 0 ? str.slice(at + 1) : 's.whatsapp.net';
177
+ const colon = raw.indexOf(':');
178
+ const user = colon >= 0 ? raw.slice(0, colon) : raw;
179
+ const device = colon >= 0 ? (parseInt(raw.slice(colon + 1), 10) || 0) : 0;
180
+ return `${user}.0:${device}@${server}`;
181
+ }
182
+
183
+ // Participant-list hash v2, as whatsmeow's participantListHashV2 computes it:
184
+ // sort the AD strings, sha256 the concatenation, then base64 the FIRST SIX
185
+ // BYTES of the digest — eight characters, prefixed "2:".
186
+ //
187
+ // This previously mirrored Baileys' generateParticipantHashV2, which hashes
188
+ // plain JID strings and then takes the first six CHARACTERS of the base64 of
189
+ // the whole digest. That produces a different, six-character value from
190
+ // different input. Baileys never puts the result on the wire — it assigns it to
191
+ // extraAttrs after the participant nodes are built, so nothing ever reads it —
192
+ // which is why the discrepancy has gone unnoticed there. whalibmob did put it
193
+ // on the wire, so every multi-device send carried a hash the server could not
194
+ // match against its own view of the participant list.
168
195
  function computePhash(jids) {
169
- const sorted = [...jids].sort();
170
- const hash = crypto.createHash('sha256').update(sorted.join('')).digest();
171
- return '2:' + hash.toString('base64').slice(0, 6);
196
+ const sorted = [...new Set(jids.map(adString))].sort();
197
+ const hash = crypto.createHash('sha256').update(sorted.join('')).digest();
198
+ return '2:' + hash.subarray(0, 6).toString('base64').replace(/=+$/, '');
172
199
  }
173
200
 
174
201
  // ─── MessageSender ────────────────────────────────────────────────────────────
@@ -542,9 +569,20 @@ class MessageSender {
542
569
  const members = this._client._getGroupMembers
543
570
  ? this._client._getGroupMembers(toJid) : [];
544
571
  for (const memberJid of members) {
545
- await this._clearSignalSessionsForRecipient(phoneFromJid(memberJid)).catch(() => {});
572
+ await this._clearSignalSessionsForJid(memberJid).catch(() => {});
546
573
  }
547
- return this._sendGroupMessage(toJid, msgId, plaintext, mediaType,
574
+ // The sessions the SenderKey distribution rode in on are gone, so the
575
+ // record of who already holds our key is meaningless — every member
576
+ // needs a fresh distribution on the retry. Without this the retry sent
577
+ // a bare skmsg to devices that had just lost their session and drew the
578
+ // same 479 straight back.
579
+ if (this._signal && this._signal.senderKeyStore) {
580
+ this._signal.senderKeyStore.invalidateSKDM(toJid);
581
+ }
582
+ // A new id for the same reason the DM path uses one: the server has
583
+ // already acked the original, and re-using it makes it drop the
584
+ // connection.
585
+ return this._sendGroupMessage(toJid, generateMessageId(), plaintext, mediaType,
548
586
  Object.assign({}, options, { _479retry: true }));
549
587
  }
550
588
  // No session could be built for a single member — usually a prekey IQ
@@ -568,9 +606,12 @@ class MessageSender {
568
606
  try {
569
607
  return await this._sendDMMessage(toJid, msgId, plaintext, mediaType, options);
570
608
  } catch (err) {
571
- // Same phash-mismatch retry for 1-to-1 messages.
609
+ // Same phash-mismatch retry for 1-to-1 messages. A LID recipient's device
610
+ // list is cached under the "lid:" key, not the bare user.
572
611
  if (err && /\b421\b/.test(err.message)) {
573
- this._devMgr.clearCache([phoneFromJid(toJid)]);
612
+ this._devMgr.clearCache([
613
+ toJid.endsWith('@lid') ? 'lid:' + phoneFromJid(toJid) : phoneFromJid(toJid)
614
+ ]);
574
615
  return this._sendDMMessage(toJid, msgId, plaintext, mediaType, options);
575
616
  }
576
617
  // ── Error 479: stale / dead Signal session ────────────────────────────────
@@ -587,7 +628,7 @@ class MessageSender {
587
628
  // _479retry flag prevents infinite loops: if the fresh session also gets
588
629
  // 479 (extremely rare — server bug or banned account) we fail cleanly.
589
630
  if (err && /\b479\b/.test(err.message) && !options._479retry) {
590
- await this._clearSignalSessionsForRecipient(phoneFromJid(toJid));
631
+ await this._clearSignalSessionsForJid(toJid);
591
632
  // CRITICAL: generate a NEW msgId — the server already ACK'd (with error=479)
592
633
  // the original ID and will close the connection if we re-send the same ID.
593
634
  const retryMsgId = generateMessageId();
@@ -612,6 +653,44 @@ class MessageSender {
612
653
  }
613
654
  }
614
655
 
656
+ // ─── Encrypt for a device list, closing any gap before it reaches the wire ──
657
+ //
658
+ // Every JID handed in becomes a <to><enc> entry, and a stanza whose
659
+ // participants do not cover the device list the server holds for those users
660
+ // is nacked with ack 479. bulkEncryptForDevices drops whatever it cannot
661
+ // encrypt, so a single unusable session silently shrank the participant list
662
+ // and the whole send failed.
663
+ //
664
+ // Anything that fails gets its session torn down and rebuilt from a fresh
665
+ // prekey bundle, then re-encrypted once. If it still fails there is nothing
666
+ // left to try — the device is genuinely unreachable — and it is logged rather
667
+ // than passed over in silence.
668
+ async _encryptForDevices(jids, plaintext) {
669
+ if (!jids || jids.length === 0) return [];
670
+
671
+ const first = await this._signal.bulkEncryptForDevicesDetailed(jids, plaintext);
672
+ if (first.failed.length === 0) return first.encrypted;
673
+
674
+ _whaDbg('[DBG] ENCRYPT_GAP ' +
675
+ first.failed.map(f => f.jid + ' (' + f.reason + ')').join(', ') +
676
+ ' — rebuilding sessions');
677
+
678
+ const repaired = await this._devMgr
679
+ .rebuildSessionsForJids(first.failed.map(f => f.jid), this._signal)
680
+ .catch(err => {
681
+ _whaDbg('[DBG] ENCRYPT_GAP_REBUILD_ERR ' + (err && err.message));
682
+ return [];
683
+ });
684
+ if (repaired.length === 0) return first.encrypted;
685
+
686
+ const second = await this._signal.bulkEncryptForDevicesDetailed(repaired, plaintext);
687
+ if (second.failed.length > 0) {
688
+ _whaDbg('[DBG] ENCRYPT_GAP_UNRESOLVED ' +
689
+ second.failed.map(f => f.jid).join(', '));
690
+ }
691
+ return [...first.encrypted, ...second.encrypted];
692
+ }
693
+
615
694
  // Drop every cached device list and Signal session for these JIDs so the next
616
695
  // send is forced to re-query devices, re-fetch prekey bundles and open new
617
696
  // pkmsg sessions from scratch.
@@ -678,6 +757,38 @@ class MessageSender {
678
757
  _whaDbg('[DBG] 479_CACHE_FLUSHED phone=' + phone);
679
758
  }
680
759
 
760
+ // ─── Clear Signal sessions for a JID in whichever family it is written in ───
761
+ //
762
+ // _clearSignalSessionsForRecipient takes a phone number. Group recovery used
763
+ // to feed it phoneFromJid(memberJid), which for a LID member is the LID user —
764
+ // it was then looked up as a phone, built @s.whatsapp.net JIDs out of it and
765
+ // deleted nothing at all, so 479 recovery on a LID-addressed group was a
766
+ // no-op that resent the identical stanza. This dispatches on the server part
767
+ // instead.
768
+ async _clearSignalSessionsForJid(jid) {
769
+ const str = String(jid);
770
+ if (!str.endsWith('@lid')) {
771
+ return this._clearSignalSessionsForRecipient(phoneFromJid(str));
772
+ }
773
+
774
+ const lidUser = phoneFromJid(str);
775
+ const devIds = (this._devMgr._dcGet && this._devMgr._dcGet('lid:' + lidUser)) || [0];
776
+ for (const devId of devIds) {
777
+ const devJid = (devId === 0) ? `${lidUser}@lid` : `${lidUser}:${devId}@lid`;
778
+ try {
779
+ await this._signal.deleteSession(devJid);
780
+ _whaDbg('[DBG] 479_DEL_LID_SESSION jid=' + devJid);
781
+ } catch (_) {}
782
+ }
783
+ try { await this._signal.deleteSession(`${lidUser}@lid`); } catch (_) {}
784
+ this._devMgr.clearCache(['lid:' + lidUser]);
785
+
786
+ // A LID member we also know by phone has phone-addressed sessions cached
787
+ // under that number too; clear both so the retry cannot pick up a stale one.
788
+ const pn = this._client._lidToPn && this._client._lidToPn.get(lidUser);
789
+ if (pn) await this._clearSignalSessionsForRecipient(pn);
790
+ }
791
+
681
792
  // ─── 1-to-1 multi-device fanout ───────────────────────────────────────────
682
793
 
683
794
  async _sendDMMessage(toJid, msgId, plaintext, mediaType, options) {
@@ -693,13 +804,6 @@ class MessageSender {
693
804
  // they simply omit the device-identity node (server accepts this for primaries).
694
805
  const allowPkmsg = true;
695
806
 
696
- // Enumerate the recipient's devices and our own in parallel — Baileys does
697
- // the same in one getUSyncDevices([senderIdentity, jid]) call.
698
- const [_recipientDevicesByPhone, ownDevices] = await Promise.all([
699
- this._devMgr.bulkEnsureSessions([recipientPhone], this._signal, allowPkmsg),
700
- this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal, allowPkmsg)
701
- ]);
702
-
703
807
  // ── Addressing family ─────────────────────────────────────────────────────
704
808
  // Baileys 7.0.0-rc13 — the release that fixed ack 479 on LID addressing —
705
809
  // derives the whole address family from the JID the caller passed, and
@@ -714,35 +818,75 @@ class MessageSender {
714
818
  // participant <to> entries — stays on phone JIDs. Send to an @lid and it
715
819
  // all stays LID.
716
820
  //
717
- // This used to look the recipient up in _pnToLid and rewrite a
718
- // phone-addressed DM into a LID-addressed one, on the reasoning that a
719
- // phone-addressed message is accepted but never delivered to a
720
- // LID-registered device. That rewrite is what produced the 479s: every
721
- // rejected stanza in the traces carried to="...@lid", and both
722
- // whatsmeow #859 and the Baileys LID reports describe the ack as appearing
723
- // specifically on @lid-addressed sends. Following the caller's JID removes
724
- // the mismatch instead of trying to compensate for it.
821
+ // "Everything" includes OUR OWN devices, which is what was missing here.
822
+ // The recipient's devices followed the caller's JID but ours were always
823
+ // enumerated by phone, so a DM to an @lid went out with @lid recipient
824
+ // entries and @s.whatsapp.net entries for our own linked devices in the same
825
+ // <participants> list. whatsmeow builds the pair as [to, ownID] with ownID
826
+ // switched to our LID for @lid destinations precisely to avoid that, and a
827
+ // participant list the server cannot reconcile is what comes back as 479.
725
828
  const isLidTarget = toJid.endsWith('@lid');
726
829
  const routingToJid = toJid;
830
+ const ownLidUser = this._client._myLid ? phoneFromJid(this._client._myLid) : null;
727
831
 
728
- let otherJids;
832
+ if (isLidTarget && !ownLidUser) {
833
+ throw new Error(
834
+ 'Cannot send to ' + toJid + ': the recipient is LID-addressed but our own LID ' +
835
+ 'is unknown (the server did not supply lid= on <success>). Reconnect so the ' +
836
+ 'LID is re-issued.'
837
+ );
838
+ }
839
+
840
+ // Enumerate the recipient's devices and our own in parallel, both in the
841
+ // address family the destination JID dictates — Baileys does the same in one
842
+ // getUSyncDevices([senderIdentity, jid]) call.
843
+ let otherJids, ownLinkedJids, ownPrimaryJid;
729
844
  if (isLidTarget) {
730
845
  const lidUser = phoneFromJid(toJid);
731
- const lidDevices = await this._devMgr.bulkEnsureSessionsForLid(
732
- lidUser, this._signal, allowPkmsg, /* skipUsync */ true);
733
- otherJids = lidDevices.length > 0 ? lidDevices : [toJid];
846
+ const [lidDevices, ownLidDevices] = await Promise.all([
847
+ this._devMgr.bulkEnsureSessionsForLid(lidUser, this._signal, allowPkmsg, /* skipUsync */ true),
848
+ this._devMgr.ensureOwnLidDeviceSessions(ownLidUser, ownPhone, this._signal, allowPkmsg)
849
+ ]);
850
+ otherJids = lidDevices.length > 0 ? lidDevices : [toJid];
851
+ ownLinkedJids = ownLidDevices;
852
+ ownPrimaryJid = `${ownLidUser}@lid`;
734
853
  } else {
735
- otherJids = _recipientDevicesByPhone.length > 0 ? _recipientDevicesByPhone : [toJid];
854
+ const [pnDevices, ownDevices] = await Promise.all([
855
+ this._devMgr.bulkEnsureSessions([recipientPhone], this._signal, allowPkmsg),
856
+ this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal, allowPkmsg)
857
+ ]);
858
+ otherJids = pnDevices.length > 0 ? pnDevices : [toJid];
859
+ ownLinkedJids = ownDevices.filter(j => j !== ownMainJid);
860
+ ownPrimaryJid = ownMainJid;
736
861
  }
737
862
 
863
+ // The device doing the sending is never one of its own recipients. Both
864
+ // Baileys ("Skipping exact sender device") and whatsmeow drop it explicitly.
865
+ const ownPrimaryAddr = adString(ownPrimaryJid);
866
+ otherJids = otherJids.filter(j => adString(j) !== ownPrimaryAddr);
867
+ ownLinkedJids = ownLinkedJids.filter(j => adString(j) !== ownPrimaryAddr);
868
+
869
+ // A device appears in <participants> exactly once. Writing to our own chat
870
+ // makes both lists resolve to the same devices; they belong to the own-device
871
+ // list, which sends them the DeviceSentMessage rather than the bare message.
872
+ const ownLinkedAddrs = new Set(ownLinkedJids.map(adString));
873
+ otherJids = otherJids.filter(j => !ownLinkedAddrs.has(adString(j)));
874
+
738
875
  _whaDbg('[DBG] DM_ROUTE to=' + toJid + ' family=' + (isLidTarget ? 'LID' : 'PN') +
739
- ' devices=[' + otherJids.join(',') + ']');
876
+ ' devices=[' + otherJids.join(',') + ']' +
877
+ ' ownLinked=[' + ownLinkedJids.join(',') + ']');
740
878
  // ─────────────────────────────────────────────────────────────────────────
741
879
 
742
- const ownLinkedJids = ownDevices.filter(j => j !== ownMainJid);
743
-
744
- const allParticipants = [...otherJids, ...ownLinkedJids];
745
- const phash = allParticipants.length > 1 ? computePhash(allParticipants) : null;
880
+ // The hash covers every device the server enumerates for this pair of users,
881
+ // our own primary included — whatsmeow hashes GetUserDevices([to, ownID]) in
882
+ // full and only skips the sending device when it encrypts. Excluding it here
883
+ // produced a hash over a different set than the server's.
884
+ //
885
+ // It is not put on the stanza: neither whatsmeow nor Baileys sends phash on a
886
+ // 1:1 message, only on a group one. It is carried inside the (encrypted)
887
+ // DeviceSentMessage for our own devices, and compared against the phash the
888
+ // server echoes back on the ack so a drifted device cache gets flushed.
889
+ const phash = computePhash([...otherJids, ownPrimaryJid, ...ownLinkedJids]);
746
890
 
747
891
  const dsmBuf = ownLinkedJids.length > 0
748
892
  ? encodeDeviceSentMessage(toJid, plaintext, phash)
@@ -751,9 +895,9 @@ class MessageSender {
751
895
  // Acquire mutex before touching Signal sessions — concurrent sends on the
752
896
  // same session corrupt the ratchet chain and produce undecryptable messages.
753
897
  const [otherEncrypted, ownEncrypted] = await this._encryptMutex.runExclusive(async () => {
754
- const other = await this._signal.bulkEncryptForDevices(otherJids, plaintext);
898
+ const other = await this._encryptForDevices(otherJids, plaintext);
755
899
  const own = ownLinkedJids.length > 0
756
- ? await this._signal.bulkEncryptForDevices(ownLinkedJids, dsmBuf)
900
+ ? await this._encryptForDevices(ownLinkedJids, dsmBuf)
757
901
  : [];
758
902
  return [other, own];
759
903
  });
@@ -764,7 +908,6 @@ class MessageSender {
764
908
  // Raw UTF-8 strings are not recognised by the server for LID recipients, causing
765
909
  // the message to be accepted (server ACK) but never delivered to the device.
766
910
  const stanzaAttrs = { to: jidStrToObj(routingToJid), id: msgId, type: mediaType, t: String(msNow()) };
767
- if (phash) stanzaAttrs.phash = phash;
768
911
  if (options.edit) stanzaAttrs.edit = String(options.edit);
769
912
 
770
913
  const hasPkmsg = encryptedList.some(e => e.type === 'pkmsg');
@@ -833,6 +976,20 @@ class MessageSender {
833
976
 
834
977
  const dispatchResult = await this._dispatchAndAck(msgNode, msgId);
835
978
 
979
+ // The server echoes its own participant hash on the ack. A difference means
980
+ // its device list for this chat is not the one we just fanned out to, so the
981
+ // cached list is stale — drop it and let the next send re-run usync, exactly
982
+ // as whatsmeow does when the hashes disagree.
983
+ if (dispatchResult && dispatchResult.phash && dispatchResult.phash !== phash) {
984
+ _whaDbg('[DBG] PHASH_MISMATCH ours=' + phash + ' server=' + dispatchResult.phash +
985
+ ' to=' + toJid + ' — flushing device cache');
986
+ if (isLidTarget) {
987
+ this._devMgr.clearCache(['lid:' + phoneFromJid(toJid)]);
988
+ } else {
989
+ this._devMgr.clearCache([recipientPhone]);
990
+ }
991
+ }
992
+
836
993
  // ─── Fire-and-forget: issue privacy token to contact ─────────────────────
837
994
  // After each DM send we ask WhatsApp to issue us a trusted-contact token
838
995
  // for this JID (one issuance per 7-day bucket is enough). The token is
@@ -900,7 +1057,10 @@ class MessageSender {
900
1057
  'Reconnect so the LID is re-issued.'
901
1058
  );
902
1059
  }
903
- const myLidUser = useLid ? phoneFromJid(this._client._myLid) : null;
1060
+ // Derived whenever the server has given us a LID, not only in LID mode: it
1061
+ // is also what recognises our own entry in a member list that names us by
1062
+ // LID while the group itself is still pn-addressed.
1063
+ const myLidUser = this._client._myLid ? phoneFromJid(this._client._myLid) : null;
904
1064
  // For LID-mode groups, sender identity is own LID JID; otherwise own PN JID.
905
1065
  // Normalised to the bare user so it matches the participant targets below.
906
1066
  const selfJid = useLid ? `${myLidUser}@lid` : ownJid;
@@ -909,12 +1069,28 @@ class MessageSender {
909
1069
  const rawSKDM = await this._signal.buildSKDM(groupJid, senderIdentity);
910
1070
  const skdmMsg = encodeSenderKeyDistributionMessage(groupJid, rawSKDM);
911
1071
 
1072
+ // We are a participant of our own group, so our own entry has to be taken
1073
+ // out of the member walk and put back through the own-device path below.
1074
+ // Left in, the walk enumerates our devices starting at device 0 — the very
1075
+ // device doing the sending — and it lands in <participants> as a recipient
1076
+ // of our own SenderKey. Baileys skips the exact sender device explicitly
1077
+ // ("Skipping exact sender device"), whatsmeow does the same with
1078
+ // `jid == ownJID || jid == ownLID`.
1079
+ //
1080
+ // The phone branch already dropped us via `p !== ownPhone`; the LID branch
1081
+ // never did, so this only ever misfired on LID-addressed groups.
1082
+ const isSelfMember = (jid) => {
1083
+ const raw = phoneFromJid(jid);
1084
+ return raw === ownPhone || (myLidUser && raw === myLidUser);
1085
+ };
1086
+
912
1087
  // Resolve member phones for session/device queries.
913
1088
  // For LID members without a PN mapping, keep the raw LID user part so
914
1089
  // bulkEnsureSessionsForLid can still acquire sessions for them.
915
1090
  const lidMembersWithoutPn = [];
916
1091
  const memberPhones = [...new Set(
917
1092
  members.map(jid => {
1093
+ if (isSelfMember(jid)) return null;
918
1094
  const isLid = jid.endsWith('@lid');
919
1095
  const raw = phoneFromJid(jid);
920
1096
  if (isLid) {
@@ -925,9 +1101,6 @@ class MessageSender {
925
1101
  const pn = this._client._lidToPn && this._client._lidToPn.get(raw);
926
1102
  if (pn) return pn;
927
1103
  }
928
- // We are a member of the group ourselves, so our own devices come out
929
- // of this same walk — Baileys likewise derives every group target
930
- // from the participant list and never appends own devices separately.
931
1104
  lidMembersWithoutPn.push(jid);
932
1105
  return null;
933
1106
  }
@@ -935,7 +1108,7 @@ class MessageSender {
935
1108
  }).filter(p => p !== null && p !== ownPhone)
936
1109
  )];
937
1110
 
938
- const [memberDevices, lidDevices, ownDevices] = await Promise.all([
1111
+ const [memberDevices, lidDevices, ownTargets] = await Promise.all([
939
1112
  memberPhones.length > 0
940
1113
  ? this._devMgr.bulkEnsureSessions(memberPhones, this._signal)
941
1114
  : Promise.resolve([]),
@@ -945,18 +1118,23 @@ class MessageSender {
945
1118
  .catch(() => [])
946
1119
  )).then(results => results.flat())
947
1120
  : Promise.resolve([]),
948
- this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal)
1121
+ // Our own linked devices, in the address family the stanza is routed with.
1122
+ // ensureOwnDeviceSessions hands back @s.whatsapp.net JIDs, which cannot go
1123
+ // into a stanza carrying addressing_mode='lid'; both helpers already leave
1124
+ // out device 0.
1125
+ useLid
1126
+ ? this._devMgr.ensureOwnLidDeviceSessions(myLidUser, ownPhone, this._signal)
1127
+ : this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal)
949
1128
  ]);
950
1129
 
951
- // Own devices come back as @s.whatsapp.net — re-express them in LID space
952
- // so every target in a LID stanza shares one address family. In LID mode
953
- // the participant walk above already covered us, so this only fills the gap
954
- // for legacy pn-addressed groups.
955
- const ownTargets = useLid ? [] : ownDevices;
956
-
957
1130
  // Dedupe: a device reached through more than one path must still appear
958
- // exactly once in <participants>.
959
- const allTargets = [...new Set([...memberDevices, ...lidDevices, ...ownTargets])];
1131
+ // exactly once in <participants>. The sending device is filtered out again
1132
+ // here in case a member list named us under an address the walk above did
1133
+ // not recognise.
1134
+ const selfAddrs = new Set([adString(`${ownPhone}@s.whatsapp.net`)]);
1135
+ if (myLidUser) selfAddrs.add(adString(`${myLidUser}@lid`));
1136
+ const allTargets = [...new Set([...memberDevices, ...lidDevices, ...ownTargets])]
1137
+ .filter(jid => !selfAddrs.has(adString(jid)));
960
1138
 
961
1139
  const skStore = this._signal.senderKeyStore;
962
1140
  const existingSkdmMap = skStore.getSKDMMap(groupJid);
@@ -980,8 +1158,13 @@ class MessageSender {
980
1158
  let skmsgCiphertext;
981
1159
  await this._encryptMutex.runExclusive(async () => {
982
1160
  if (skdmRecipients.length > 0) {
983
- skdmEncrypted = await this._signal.bulkEncryptForDevices(skdmRecipients, skdmMsg);
984
- skStore.markSKDMSent(groupJid, skdmRecipients);
1161
+ skdmEncrypted = await this._encryptForDevices(skdmRecipients, skdmMsg);
1162
+ // Record only the devices whose SenderKey distribution actually made it
1163
+ // into the stanza. Marking the whole intended list meant a device we
1164
+ // failed to encrypt for was remembered as already holding our key, so it
1165
+ // never got another distribution and could not decrypt anything we sent
1166
+ // to the group from then on.
1167
+ skStore.markSKDMSent(groupJid, skdmEncrypted.map(e => e.jid));
985
1168
  }
986
1169
  skmsgCiphertext = await this._signal.senderKeyEncrypt(groupJid, senderIdentity, plaintext);
987
1170
  });
@@ -1154,7 +1337,15 @@ class MessageSender {
1154
1337
  if (error) {
1155
1338
  reject(new Error('Send error ' + error + ' for ' + id));
1156
1339
  } else {
1157
- resolve({ id, t: ackAttrs && ackAttrs.t, status: 'sent' });
1340
+ resolve({
1341
+ id,
1342
+ t: ackAttrs && ackAttrs.t,
1343
+ // The server's own participant hash, when it sends one back. The
1344
+ // caller compares it against the list it fanned out to and drops
1345
+ // its device cache when they disagree.
1346
+ phash: ackAttrs && ackAttrs.phash ? String(ackAttrs.phash) : undefined,
1347
+ status: 'sent'
1348
+ });
1158
1349
  }
1159
1350
  });
1160
1351
 
@@ -1334,4 +1525,8 @@ function guessMime(data, fallback) {
1334
1525
  return map[ext] || fallback;
1335
1526
  }
1336
1527
 
1337
- module.exports = { MessageSender, makeJid, isGroupJid, generateMessageId, buildOrGetAdvIdentity: _buildOrGetAdvIdentity };
1528
+ module.exports = {
1529
+ MessageSender, makeJid, isGroupJid, generateMessageId,
1530
+ buildOrGetAdvIdentity: _buildOrGetAdvIdentity,
1531
+ computePhash, adString
1532
+ };
@@ -249,12 +249,31 @@ class SignalProtocol {
249
249
  // same JID are still serialized — no Signal session corruption possible.
250
250
  // Returns [{jid, type, ciphertext}]
251
251
  async bulkEncryptForDevices(jids, plaintext) {
252
+ const { encrypted } = await this.bulkEncryptForDevicesDetailed(jids, plaintext);
253
+ return encrypted;
254
+ }
255
+
256
+ // Same fanout, but also reports the devices that could NOT be encrypted.
257
+ //
258
+ // A dropped device is not a cosmetic loss: every device handed in here becomes
259
+ // a <to><enc> entry in the outgoing stanza, and a stanza whose participants do
260
+ // not cover the recipient's current device list is rejected by the server with
261
+ // ack 479. The caller needs to know which addresses failed so it can rebuild
262
+ // their sessions and fill the gap before the stanza goes out.
263
+ async bulkEncryptForDevicesDetailed(jids, plaintext) {
252
264
  const settled = await Promise.allSettled(
253
265
  jids.map(jid => this.encrypt(jid, plaintext).then(enc => ({ jid, type: enc.type, ciphertext: enc.ciphertext })))
254
266
  );
255
- return settled
256
- .filter(r => r.status === 'fulfilled')
257
- .map(r => r.value);
267
+ const encrypted = [];
268
+ const failed = [];
269
+ settled.forEach((r, i) => {
270
+ if (r.status === 'fulfilled') {
271
+ encrypted.push(r.value);
272
+ } else {
273
+ failed.push({ jid: jids[i], reason: (r.reason && r.reason.message) || String(r.reason) });
274
+ }
275
+ });
276
+ return { encrypted, failed };
258
277
  }
259
278
 
260
279
  // ─── SenderKey group encrypt / decrypt ────────────────────────────────────
@@ -27,6 +27,10 @@ class SignalStore {
27
27
  this._identities = {};
28
28
  this._filePath = null;
29
29
  this._lidMappings = {}; // phone → lid — persisted alongside sessions
30
+ // address → { value, open } — memoises the haveOpenSession() check below.
31
+ // Keyed on the stored record value, so storeSession() (which always assigns
32
+ // a freshly serialised object) invalidates the entry by identity.
33
+ this._openSessionMemo = new Map();
30
34
 
31
35
  // Debounce state
32
36
  this._dirty = false;
@@ -173,8 +177,40 @@ class SignalStore {
173
177
  this._save();
174
178
  }
175
179
 
180
+ // True only when the stored record still has an OPEN session — i.e. one we can
181
+ // actually encrypt with.
182
+ //
183
+ // A SessionRecord can hold sessions that libsignal has closed: session_builder
184
+ // closes the current one whenever it processes an incoming prekey bundle or
185
+ // installs a new outgoing one, and it stays in the record as archived state.
186
+ // The record therefore keeps existing long after it stops being usable.
187
+ //
188
+ // This used to answer `!!this._sessions[addr]`, so DeviceManager treated a
189
+ // closed session as ready, skipped the prekey fetch, and handed the address to
190
+ // SessionCipher.encrypt — which throws "No open session". bulkEncryptForDevices
191
+ // drops whatever it cannot encrypt, so the stanza went out with a <to> entry
192
+ // missing for that device and the server nacked the whole send with ack 479.
193
+ // Baileys hit the same class of bug with its peerSessionsCache and fixed it by
194
+ // validating through haveOpenSession() on every check.
176
195
  hasSession(encodedAddress) {
177
- return !!this._sessions[encodedAddress];
196
+ if (!this._openSessionMemo) this._openSessionMemo = new Map();
197
+ const data = this._sessions[encodedAddress];
198
+ if (!data) {
199
+ this._openSessionMemo.delete(encodedAddress);
200
+ return false;
201
+ }
202
+ const memo = this._openSessionMemo.get(encodedAddress);
203
+ if (memo && memo.value === data) return memo.open;
204
+
205
+ let open = false;
206
+ try {
207
+ const obj = typeof data === 'string' ? JSON.parse(data) : data;
208
+ open = SessionRecord.deserialize(obj).haveOpenSession();
209
+ } catch (_) {
210
+ open = false;
211
+ }
212
+ this._openSessionMemo.set(encodedAddress, { value: data, open });
213
+ return open;
178
214
  }
179
215
 
180
216
  // ─── PreKeys ──────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.5.80",
3
+ "version": "5.5.82",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto50",
6
6
  "main": "index.js",