whalibmob 5.5.64 → 5.5.66

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/BinaryNode.js CHANGED
@@ -127,9 +127,25 @@ function _writeBytes(buf, parts) {
127
127
  parts.push(buf);
128
128
  }
129
129
 
130
+ // AD_JID agent byte ↔ domain. 0 = phone JIDs, 1 = LIDs.
131
+ const _AGENT_SERVER = { 0: 's.whatsapp.net', 1: 'lid' };
132
+ function _serverForAgent(agent) {
133
+ return _AGENT_SERVER[agent] || 's.whatsapp.net';
134
+ }
135
+ function _agentForServer(server) {
136
+ return server === 'lid' ? 1 : 0;
137
+ }
138
+
130
139
  function _writeJid(jid, parts) {
131
- if (jid.agent || jid.device) {
132
- parts.push(Buffer.from([TAG.AD_JID, jid.agent || 0, jid.device || 0]));
140
+ // Derive the agent from the domain only when this JID is going out as AD_JID
141
+ // anyway (explicit agent, or a device suffix). A plain LID keeps its
142
+ // JID_PAIR + 'lid' server-token encoding, which is what the server expects
143
+ // and what the rest of the codebase already produces.
144
+ const agent = (jid.agent !== undefined && jid.agent !== null)
145
+ ? jid.agent
146
+ : (jid.device ? _agentForServer(jid.server) : 0);
147
+ if (agent || jid.device) {
148
+ parts.push(Buffer.from([TAG.AD_JID, agent || 0, jid.device || 0]));
133
149
  _writeString(jid.user, parts);
134
150
  } else {
135
151
  parts.push(Buffer.from([TAG.JID_PAIR]));
@@ -289,7 +305,15 @@ class NodeDecoder {
289
305
  const device = this._readByte();
290
306
  const userToken = this._readByte();
291
307
  const user = this._readString(userToken);
292
- return { user, agent, device, server: 's.whatsapp.net', toString() { return `${user}@s.whatsapp.net`; } };
308
+ // The agent byte selects the domain it is not decoration. Hardcoding
309
+ // s.whatsapp.net turned every LID participant (agent 1) into a phone JID
310
+ // that does not exist, so device queries for it came back empty, no Signal
311
+ // session was ever built, and group sends were rejected with ack 479.
312
+ const server = _serverForAgent(agent);
313
+ return {
314
+ user, agent, device, server,
315
+ toString() { return `${user}@${server}`; }
316
+ };
293
317
  }
294
318
  }
295
319
 
package/lib/Client.js CHANGED
@@ -148,8 +148,11 @@ class WhalibmobClient extends EventEmitter {
148
148
  this._pnToLid = new Map(); // phone number → LID user
149
149
  this._myLid = null; // own LID JID received from <success> node
150
150
  this._groupAddressingMode = new Map(); // groupJid → 'lid' | 'pn'
151
- this._retryPending = new Map(); // msgId → {node, retryCount}
152
- this._retryPreKeyIdx = 0; // rotating index for assigning unique prekeys to retries
151
+ this._retryPending = new Map(); // msgId → {node, count, pkId}
152
+ this._retryPreKeyIdx = 0; // rotating index, fallback when every prekey is reserved
153
+ // keyIds handed out by unresolved retries — never advertise one twice, or
154
+ // the second sender's pkmsg arrives after the key was consumed and deleted.
155
+ this._retryAdvertisedPreKeys = new Set();
153
156
  this._appStateVersions = {}; // collectionName → version (int)
154
157
  this._sentMsgCache = new Map(); // msgId → {plaintext, toJid, msgId, mediaType, options, recipientPhone}
155
158
  this._tcTokenStore = null; // TcTokenStore — loaded in init()
@@ -686,6 +689,7 @@ class WhalibmobClient extends EventEmitter {
686
689
 
687
690
  this._signal.decrypt(sigJid, encType, cipherBuf)
688
691
  .then(async plaintext => {
692
+ this._resolveRetry(id);
689
693
  const decoded = this._decodeMsg(plaintext);
690
694
  _whaDbg('[DBG] DM_DECODED id=' + id + ' type=' + decoded.type + ' ptLen=' + (plaintext ? plaintext.length : 0) + ' hasSKDM=' + !!(decoded.skdm || decoded.type === 'senderKeyDistribution'));
691
695
 
@@ -756,6 +760,7 @@ class WhalibmobClient extends EventEmitter {
756
760
 
757
761
  this._signal.senderKeyDecrypt(from, participant, cipherBuf)
758
762
  .then(plaintext => {
763
+ this._resolveRetry(id);
759
764
  const decoded = this._decodeMsg(plaintext);
760
765
  if (decoded.type === 'senderKeyDistribution') return;
761
766
  if (decoded.type === 'protocol') {
@@ -959,6 +964,17 @@ class WhalibmobClient extends EventEmitter {
959
964
  // - <registration> 4-byte reg-id
960
965
  // - <keys> block (from retry #1): type + identity + key + skey + device-identity
961
966
 
967
+ // A message we asked to be re-sent finally decrypted (or we gave up on it).
968
+ // Release its reserved prekey so the pool does not drain over a long session.
969
+ _resolveRetry(msgId) {
970
+ const pending = this._retryPending.get(msgId);
971
+ if (!pending) return;
972
+ if (pending.pkId !== null && pending.pkId !== undefined) {
973
+ this._retryAdvertisedPreKeys.delete(pending.pkId);
974
+ }
975
+ this._retryPending.delete(msgId);
976
+ }
977
+
962
978
  _sendRetryRequest(msgId, origNode) {
963
979
  const existing = this._retryPending.get(msgId);
964
980
  const count = existing ? existing.count + 1 : 1;
@@ -966,13 +982,25 @@ class WhalibmobClient extends EventEmitter {
966
982
 
967
983
  if (!this._socket || !this._connected) return;
968
984
 
969
- // Assign a stable, unique prekey index per msgId on first retry
970
- let pkIdx = existing ? existing.pkIdx : -1;
971
- if (pkIdx < 0 && this._signal) {
985
+ // Pick a prekey that is not already advertised by another unresolved retry.
986
+ // A prekey is deleted the moment a pkmsg using it is decrypted, so handing
987
+ // the same one to two senders means the second arrives after the key is
988
+ // gone and dies with "Invalid PreKey ID". Indexing modulo the key count
989
+ // could not prevent that: the list shrinks as keys are consumed, so the
990
+ // rotating index wraps back onto ids that are still in flight.
991
+ let pkId = existing ? existing.pkId : null;
992
+ if (pkId === null && this._signal) {
972
993
  const allKeys = this._signal.getPreKeysForUpload(800);
973
- pkIdx = this._retryPreKeyIdx++ % Math.max(1, allKeys.length);
994
+ const free = allKeys.find(k => !this._retryAdvertisedPreKeys.has(k.keyId));
995
+ // If every key is already spoken for, fall back to rotating rather than
996
+ // sending no bundle at all — a stale id beats no retry.
997
+ const chosen = free || allKeys[this._retryPreKeyIdx++ % Math.max(1, allKeys.length)];
998
+ if (chosen) {
999
+ pkId = chosen.keyId;
1000
+ this._retryAdvertisedPreKeys.add(pkId);
1001
+ }
974
1002
  }
975
- this._retryPending.set(msgId, { node: origNode, count, pkIdx });
1003
+ this._retryPending.set(msgId, { node: origNode, count, pkId });
976
1004
 
977
1005
  // Use original message timestamp (not current time)
978
1006
  const origAttrs = (origNode && origNode.attrs) || {};
@@ -991,7 +1019,7 @@ class WhalibmobClient extends EventEmitter {
991
1019
  const spk = this._signal.getSignedPreKeyForUpload();
992
1020
  const identKey = this._signal.getIdentityKey();
993
1021
  const allPreKeys = this._signal.getPreKeysForUpload(800);
994
- const pk = allPreKeys[pkIdx] || allPreKeys[0];
1022
+ const pk = allPreKeys.find(k => k.keyId === pkId) || allPreKeys[0];
995
1023
 
996
1024
  if (spk && identKey && pk) {
997
1025
  const keysChildren = [
@@ -839,10 +839,22 @@ class MessageSender {
839
839
 
840
840
  // Determine addressing mode for this group (lid = modern groups, pn = legacy)
841
841
  const groupAddressingMode = this._client._groupAddressingMode.get(groupJid) || 'lid';
842
- // For LID-mode groups, sender identity is own LID JID; otherwise own PN JID
843
- const senderIdentity = (groupAddressingMode === 'lid' && this._client._myLid)
844
- ? this._client._myLid
845
- : ownJid;
842
+ // A stanza addressed with addressing_mode='lid' must carry participants in
843
+ // LID space only. Mixing @s.whatsapp.net targets into it is exactly what
844
+ // DeviceManager warns about, and the server answers it with ack 479.
845
+ const useLid = groupAddressingMode === 'lid';
846
+ if (useLid && !this._client._myLid) {
847
+ throw new Error(
848
+ 'Cannot send to ' + groupJid + ': group is LID-addressed but our own LID ' +
849
+ 'is unknown (the server did not supply lid= on <success>). ' +
850
+ 'Reconnect so the LID is re-issued.'
851
+ );
852
+ }
853
+ const myLidUser = useLid ? phoneFromJid(this._client._myLid) : null;
854
+ // For LID-mode groups, sender identity is own LID JID; otherwise own PN JID.
855
+ // Normalised to the bare user so it matches the participant targets below.
856
+ const selfJid = useLid ? `${myLidUser}@lid` : ownJid;
857
+ const senderIdentity = selfJid;
846
858
 
847
859
  const rawSKDM = await this._signal.buildSKDM(groupJid, senderIdentity);
848
860
  const skdmMsg = encodeSenderKeyDistributionMessage(groupJid, rawSKDM);
@@ -850,16 +862,31 @@ class MessageSender {
850
862
  // Resolve member phones for session/device queries.
851
863
  // For LID members without a PN mapping, keep the raw LID user part so
852
864
  // bulkEnsureSessionsForLid can still acquire sessions for them.
865
+ // "40778226909:2@s.whatsapp.net" → "<ourLid>:2@lid" — keep the device
866
+ // suffix, swap identity and domain.
867
+ const ownJidToLid = (jid) => {
868
+ const base = String(jid).replace(/@(?:s\.whatsapp\.net|lid)$/, '');
869
+ const colon = base.indexOf(':');
870
+ return myLidUser + (colon >= 0 ? base.slice(colon) : '') + '@lid';
871
+ };
872
+
853
873
  const lidMembersWithoutPn = [];
854
874
  const memberPhones = [...new Set(
855
875
  members.map(jid => {
856
876
  const isLid = jid.endsWith('@lid');
857
877
  const raw = phoneFromJid(jid);
858
878
  if (isLid) {
859
- const pn = this._client._lidToPn && this._client._lidToPn.get(raw);
860
- if (pn) return pn;
861
- // No PN mapping yet track for LID-direct session ensure
862
- lidMembersWithoutPn.push(jid);
879
+ // In a LID-addressed group, resolve LID members through the LID path
880
+ // even when we know their phone number — swapping in the PN would put
881
+ // an @s.whatsapp.net target into a @lid stanza.
882
+ if (!useLid) {
883
+ const pn = this._client._lidToPn && this._client._lidToPn.get(raw);
884
+ if (pn) return pn;
885
+ }
886
+ // Our own LID is a member too — ensureOwnDeviceSessions already
887
+ // covers those devices, so listing it here would encrypt the SKDM
888
+ // for ourselves twice and emit duplicate <to> entries.
889
+ if (raw !== myLidUser) lidMembersWithoutPn.push(jid);
863
890
  return null;
864
891
  }
865
892
  return raw;
@@ -879,11 +906,17 @@ class MessageSender {
879
906
  this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal)
880
907
  ]);
881
908
 
882
- const allTargets = [...memberDevices, ...lidDevices, ...ownDevices];
909
+ // Own devices come back as @s.whatsapp.net — re-express them in LID space
910
+ // so every target in a LID stanza shares one address family.
911
+ const ownTargets = useLid ? ownDevices.map(ownJidToLid) : ownDevices;
912
+
913
+ // Dedupe: a device reached through more than one path must still appear
914
+ // exactly once in <participants>.
915
+ const allTargets = [...new Set([...memberDevices, ...lidDevices, ...ownTargets])];
883
916
 
884
- const phashTargets = allTargets.includes(ownJid)
917
+ const phashTargets = allTargets.includes(selfJid)
885
918
  ? allTargets
886
- : [ownJid, ...allTargets];
919
+ : [selfJid, ...allTargets];
887
920
 
888
921
  const skStore = this._signal.senderKeyStore;
889
922
  const existingSkdmMap = skStore.getSKDMMap(groupJid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.5.64",
3
+ "version": "5.5.66",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto50",
6
6
  "main": "index.js",