keithbaileys 1.1.0 → 1.1.1

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.
@@ -40,8 +40,43 @@ const WABinary_1 = require("../WABinary");
40
40
  const sender_key_name_1 = require("./Group/sender-key-name");
41
41
  const sender_key_record_1 = require("./Group/sender-key-record");
42
42
  const Group_1 = require("./Group");
43
- function makeLibSignalRepository(auth) {
44
- const storage = signalStorage(auth);
43
+ const lid_mapping_1 = require("./lid-mapping");
44
+ /**
45
+ * Fix: previously every call built the signal protocol address straight from
46
+ * whatever JID form (PN or LID) happened to be on the message/stanza. WhatsApp
47
+ * sends the same contact as a PN JID sometimes and a LID JID other times, so
48
+ * the two forms silently ended up as two *different* Signal sessions for the
49
+ * same device -- once they drifted out of sync, decryption kept failing and
50
+ * WhatsApp shows the message as permanently "Waiting for this message".
51
+ *
52
+ * `resolveConsistentAddress` makes sure that whenever we already know a
53
+ * PN<->LID mapping for a contact, we always operate on ONE canonical address
54
+ * (preferring LID, matching current WhatsApp behavior) instead of flip-flopping.
55
+ */
56
+ function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
57
+ const log = logger || console;
58
+ const lidMapping = new lid_mapping_1.LIDMappingStore(auth.keys, log, pnToLIDFunc);
59
+ const storage = signalStorage(auth, lidMapping, log);
60
+ const resolveConsistentAddress = async (jid) => {
61
+ try {
62
+ if ((0, WABinary_1.isLidUser)(jid)) {
63
+ return jidToSignalProtocolAddress(jid);
64
+ }
65
+ if ((0, WABinary_1.isPnUser)(jid) || (0, WABinary_1.isHostedPnUser)(jid)) {
66
+ const lid = await lidMapping.getLIDForPN(jid);
67
+ if (lid) {
68
+ // if we already have session state under the old PN address,
69
+ // carry it over to the LID address so we don't lose the session
70
+ await migrateSessionIfNeeded(auth.keys, jid, lid, log);
71
+ return jidToSignalProtocolAddress(lid);
72
+ }
73
+ }
74
+ }
75
+ catch (err) {
76
+ log.warn?.({ err, jid }, 'LID resolution failed, falling back to original jid');
77
+ }
78
+ return jidToSignalProtocolAddress(jid);
79
+ };
45
80
  return {
46
81
  decryptGroupMessage({ group, authorJid, msg }) {
47
82
  const senderName = jidToSignalSenderKeyName(group, authorJid);
@@ -63,7 +98,7 @@ function makeLibSignalRepository(auth) {
63
98
  await builder.process(senderName, senderMsg);
64
99
  },
65
100
  async decryptMessage({ jid, type, ciphertext }) {
66
- const addr = jidToSignalProtocolAddress(jid);
101
+ const addr = await resolveConsistentAddress(jid);
67
102
  const session = new libsignal.SessionCipher(storage, addr);
68
103
  let result;
69
104
  switch (type) {
@@ -79,7 +114,7 @@ function makeLibSignalRepository(auth) {
79
114
  return result;
80
115
  },
81
116
  async encryptMessage({ jid, data }) {
82
- const addr = jidToSignalProtocolAddress(jid);
117
+ const addr = await resolveConsistentAddress(jid);
83
118
  const cipher = new libsignal.SessionCipher(storage, addr);
84
119
  const { type: sigType, body } = await cipher.encrypt(data);
85
120
  const type = sigType === 3 ? 'pkmsg' : 'msg';
@@ -102,14 +137,41 @@ function makeLibSignalRepository(auth) {
102
137
  };
103
138
  },
104
139
  async injectE2ESession({ jid, session }) {
105
- const cipher = new libsignal.SessionBuilder(storage, jidToSignalProtocolAddress(jid));
140
+ const addr = await resolveConsistentAddress(jid);
141
+ const cipher = new libsignal.SessionBuilder(storage, addr);
106
142
  await cipher.initOutgoing(session);
107
143
  },
108
144
  jidToSignalProtocolAddress(jid) {
109
145
  return jidToSignalProtocolAddress(jid).toString();
110
- }
146
+ },
147
+ lidMapping
111
148
  };
112
149
  }
150
+ /**
151
+ * If a session already exists under the PN address but not yet under the
152
+ * newly-resolved LID address, copy it over so we don't force a brand new
153
+ * (and momentarily out-of-sync) session just because we learned the LID
154
+ * mapping after the fact.
155
+ */
156
+ async function migrateSessionIfNeeded(keys, pnJid, lidJid, logger) {
157
+ try {
158
+ const pnAddr = jidToSignalProtocolAddress(pnJid).toString();
159
+ const lidAddr = jidToSignalProtocolAddress(lidJid).toString();
160
+ if (pnAddr === lidAddr)
161
+ return;
162
+ const { [lidAddr]: existingLidSession } = await keys.get('session', [lidAddr]);
163
+ if (existingLidSession)
164
+ return; // already migrated / already has its own session
165
+ const { [pnAddr]: pnSession } = await keys.get('session', [pnAddr]);
166
+ if (pnSession) {
167
+ await keys.set({ session: { [lidAddr]: pnSession } });
168
+ logger.debug?.({ pnAddr, lidAddr }, 'migrated signal session from PN address to LID address');
169
+ }
170
+ }
171
+ catch (err) {
172
+ logger.warn?.({ err }, 'session migration to LID address failed, continuing without it');
173
+ }
174
+ }
113
175
  const jidToSignalProtocolAddress = (jid) => {
114
176
  const { user, device } = (0, WABinary_1.jidDecode)(jid);
115
177
  return new libsignal.ProtocolAddress(user, device || 0);
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LIDMappingStore = void 0;
4
+ const WABinary_1 = require("../WABinary");
5
+ const MAPPING_TTL_MS = 3 * 24 * 60 * 60 * 1000; // 3 days
6
+ /**
7
+ * A minimal TTL cache used instead of the `lru-cache` package so this file
8
+ * doesn't require adding a new dependency to keithbaileys.
9
+ */
10
+ class SimpleTTLCache {
11
+ constructor(ttlMs) {
12
+ this.ttlMs = ttlMs;
13
+ this.map = new Map();
14
+ }
15
+ get(key) {
16
+ const entry = this.map.get(key);
17
+ if (!entry)
18
+ return undefined;
19
+ if (Date.now() - entry.t > this.ttlMs) {
20
+ this.map.delete(key);
21
+ return undefined;
22
+ }
23
+ entry.t = Date.now(); // updateAgeOnGet
24
+ return entry.v;
25
+ }
26
+ set(key, value) {
27
+ this.map.set(key, { v: value, t: Date.now() });
28
+ }
29
+ delete(key) {
30
+ this.map.delete(key);
31
+ }
32
+ }
33
+ class LIDMappingStore {
34
+ constructor(keys, logger, pnToLIDFunc) {
35
+ this.mappingCache = new SimpleTTLCache(MAPPING_TTL_MS);
36
+ this.inflightLIDLookups = new Map();
37
+ this.inflightPNLookups = new Map();
38
+ this.keys = keys;
39
+ this.pnToLIDFunc = pnToLIDFunc;
40
+ this.logger = logger || console;
41
+ }
42
+ async storeLIDPNMappings(pairs) {
43
+ if (!pairs || pairs.length === 0)
44
+ return;
45
+ const validatedPairs = [];
46
+ for (const { lid, pn } of pairs) {
47
+ if (!((0, WABinary_1.isLidUser)(lid) && (0, WABinary_1.isPnUser)(pn)) &&
48
+ !((0, WABinary_1.isPnUser)(lid) && (0, WABinary_1.isLidUser)(pn))) {
49
+ this.logger.warn?.(`Invalid LID-PN mapping: ${lid}, ${pn}`);
50
+ continue;
51
+ }
52
+ const lidDecoded = (0, WABinary_1.jidDecode)(lid);
53
+ const pnDecoded = (0, WABinary_1.jidDecode)(pn);
54
+ if (!lidDecoded || !pnDecoded)
55
+ continue;
56
+ validatedPairs.push({ pnUser: pnDecoded.user, lidUser: lidDecoded.user });
57
+ }
58
+ if (validatedPairs.length === 0)
59
+ return;
60
+ const cacheMissSet = new Set();
61
+ const existingMappings = new Map();
62
+ for (const { pnUser } of validatedPairs) {
63
+ const cached = this.mappingCache.get(`pn:${pnUser}`);
64
+ if (cached) {
65
+ existingMappings.set(pnUser, cached);
66
+ }
67
+ else {
68
+ cacheMissSet.add(pnUser);
69
+ }
70
+ }
71
+ if (cacheMissSet.size > 0) {
72
+ const cacheMisses = [...cacheMissSet];
73
+ const stored = await this.keys.get('lid-mapping', cacheMisses);
74
+ for (const pnUser of cacheMisses) {
75
+ const existingLidUser = stored[pnUser];
76
+ if (existingLidUser) {
77
+ existingMappings.set(pnUser, existingLidUser);
78
+ this.mappingCache.set(`pn:${pnUser}`, existingLidUser);
79
+ this.mappingCache.set(`lid:${existingLidUser}`, pnUser);
80
+ }
81
+ }
82
+ }
83
+ const pairMap = {};
84
+ for (const { pnUser, lidUser } of validatedPairs) {
85
+ const existingLidUser = existingMappings.get(pnUser);
86
+ if (existingLidUser === lidUser) {
87
+ continue;
88
+ }
89
+ pairMap[pnUser] = lidUser;
90
+ }
91
+ if (Object.keys(pairMap).length === 0)
92
+ return;
93
+ const batchData = {};
94
+ for (const [pnUser, lidUser] of Object.entries(pairMap)) {
95
+ batchData[pnUser] = lidUser;
96
+ batchData[`${lidUser}_reverse`] = pnUser;
97
+ const oldLidUser = existingMappings.get(pnUser);
98
+ if (oldLidUser && oldLidUser !== lidUser) {
99
+ batchData[`${oldLidUser}_reverse`] = null;
100
+ }
101
+ }
102
+ if (this.keys.transaction) {
103
+ await this.keys.transaction(async () => {
104
+ await this.keys.set({ 'lid-mapping': batchData });
105
+ }, 'lid-mapping');
106
+ }
107
+ else {
108
+ await this.keys.set({ 'lid-mapping': batchData });
109
+ }
110
+ for (const [pnUser, lidUser] of Object.entries(pairMap)) {
111
+ this.mappingCache.set(`pn:${pnUser}`, lidUser);
112
+ this.mappingCache.set(`lid:${lidUser}`, pnUser);
113
+ const oldLidUser = existingMappings.get(pnUser);
114
+ if (oldLidUser && oldLidUser !== lidUser) {
115
+ this.mappingCache.delete(`lid:${oldLidUser}`);
116
+ }
117
+ }
118
+ }
119
+ async getLIDForPN(pn) {
120
+ var _a, _b;
121
+ return ((_b = (_a = (await this.getLIDsForPNs([pn]))) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.lid) || null;
122
+ }
123
+ async getLIDsForPNs(pns) {
124
+ if (!pns || pns.length === 0)
125
+ return null;
126
+ const sortedPns = [...new Set(pns)].sort();
127
+ const cacheKey = sortedPns.join(',');
128
+ const inflight = this.inflightLIDLookups.get(cacheKey);
129
+ if (inflight) {
130
+ return inflight;
131
+ }
132
+ const promise = this._getLIDsForPNsImpl(pns);
133
+ this.inflightLIDLookups.set(cacheKey, promise);
134
+ try {
135
+ return await promise;
136
+ }
137
+ finally {
138
+ this.inflightLIDLookups.delete(cacheKey);
139
+ }
140
+ }
141
+ async _getLIDsForPNsImpl(pns) {
142
+ const usyncFetch = {};
143
+ const successfulPairs = {};
144
+ for (const pn of pns) {
145
+ if (!(0, WABinary_1.isPnUser)(pn) && !(0, WABinary_1.isHostedPnUser)(pn))
146
+ continue;
147
+ const decoded = (0, WABinary_1.jidDecode)(pn);
148
+ if (!decoded)
149
+ continue;
150
+ const pnUser = decoded.user;
151
+ let lidUser = this.mappingCache.get(`pn:${pnUser}`);
152
+ if (!lidUser) {
153
+ const stored = await this.keys.get('lid-mapping', [pnUser]);
154
+ lidUser = stored[pnUser];
155
+ if (lidUser) {
156
+ this.mappingCache.set(`pn:${pnUser}`, lidUser);
157
+ this.mappingCache.set(`lid:${lidUser}`, pnUser);
158
+ }
159
+ else {
160
+ const device = decoded.device || 0;
161
+ let normalizedPn = (0, WABinary_1.jidNormalizedUser)(pn);
162
+ if ((0, WABinary_1.isHostedPnUser)(normalizedPn)) {
163
+ normalizedPn = `${pnUser}@s.whatsapp.net`;
164
+ }
165
+ if (!usyncFetch[normalizedPn]) {
166
+ usyncFetch[normalizedPn] = [device];
167
+ }
168
+ else {
169
+ usyncFetch[normalizedPn].push(device);
170
+ }
171
+ continue;
172
+ }
173
+ }
174
+ lidUser = lidUser.toString();
175
+ if (!lidUser) {
176
+ this.logger.warn?.(`Invalid or empty LID user for PN ${pn}`);
177
+ return null;
178
+ }
179
+ const pnDevice = decoded.device !== undefined ? decoded.device : 0;
180
+ const deviceSpecificLid = `${lidUser}${pnDevice ? `:${pnDevice}` : ``}@${decoded.server === 'hosted' ? 'hosted.lid' : 'lid'}`;
181
+ successfulPairs[pn] = { lid: deviceSpecificLid, pn };
182
+ }
183
+ if (Object.keys(usyncFetch).length > 0) {
184
+ const result = this.pnToLIDFunc ? await this.pnToLIDFunc(Object.keys(usyncFetch)) : null;
185
+ if (result && result.length > 0) {
186
+ await this.storeLIDPNMappings(result);
187
+ for (const pair of result) {
188
+ const pnDecoded = (0, WABinary_1.jidDecode)(pair.pn);
189
+ const pnUser = pnDecoded === null || pnDecoded === void 0 ? void 0 : pnDecoded.user;
190
+ if (!pnUser)
191
+ continue;
192
+ const lidDecoded = (0, WABinary_1.jidDecode)(pair.lid);
193
+ const lidUser = lidDecoded === null || lidDecoded === void 0 ? void 0 : lidDecoded.user;
194
+ if (!lidUser)
195
+ continue;
196
+ for (const device of (usyncFetch[pair.pn] || [])) {
197
+ const deviceSpecificLid = `${lidUser}${device ? `:${device}` : ``}@${device === 99 ? 'hosted.lid' : 'lid'}`;
198
+ const deviceSpecificPn = `${pnUser}${device ? `:${device}` : ``}@${device === 99 ? 'hosted' : 's.whatsapp.net'}`;
199
+ successfulPairs[deviceSpecificPn] = { lid: deviceSpecificLid, pn: deviceSpecificPn };
200
+ }
201
+ }
202
+ }
203
+ else {
204
+ return null;
205
+ }
206
+ }
207
+ return Object.values(successfulPairs);
208
+ }
209
+ /** Get PNs for LIDs - user level, with device numbers reattached */
210
+ async getPNsForLIDs(lids) {
211
+ const result = [];
212
+ const missingLids = [];
213
+ for (const lid of lids) {
214
+ if (!(0, WABinary_1.isLidUser)(lid)) {
215
+ continue;
216
+ }
217
+ const decoded = (0, WABinary_1.jidDecode)(lid);
218
+ if (!decoded) {
219
+ continue;
220
+ }
221
+ const lidUser = decoded.user;
222
+ const pnUser = this.mappingCache.get(`lid:${lidUser}`);
223
+ if (!pnUser || typeof pnUser !== 'string') {
224
+ missingLids.push(lidUser);
225
+ }
226
+ else {
227
+ const lidDevice = decoded.device !== undefined ? decoded.device : 0;
228
+ const domain = decoded.domainType === WABinary_1.WAJIDDomains.HOSTED_LID ? 'hosted' : 's.whatsapp.net';
229
+ const pnJid = `${pnUser}${lidDevice ? `:${lidDevice}` : ``}@${domain}`;
230
+ result.push({ lid, pn: pnJid });
231
+ }
232
+ }
233
+ if (missingLids.length > 0) {
234
+ const lookupKeys = missingLids.map(l => `${l}_reverse`);
235
+ const stored = await this.keys.get('lid-mapping', lookupKeys);
236
+ for (const lidUser of missingLids) {
237
+ const pnUser = stored[`${lidUser}_reverse`];
238
+ if (pnUser && typeof pnUser === 'string') {
239
+ this.mappingCache.set(`lid:${lidUser}`, pnUser);
240
+ for (const lid of lids) {
241
+ const decoded = (0, WABinary_1.jidDecode)(lid);
242
+ if (decoded && decoded.user === lidUser) {
243
+ const lidDevice = decoded.device !== undefined ? decoded.device : 0;
244
+ const domain = decoded.domainType === WABinary_1.WAJIDDomains.HOSTED_LID ? 'hosted' : 's.whatsapp.net';
245
+ const pnJid = `${pnUser}${lidDevice ? `:${lidDevice}` : ``}@${domain}`;
246
+ result.push({ lid, pn: pnJid });
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+ return result;
253
+ }
254
+ async getPNForLID(lid) {
255
+ var _a, _b;
256
+ const results = await this.getPNsForLIDs([lid]);
257
+ return ((_b = (_a = results) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.pn) || null;
258
+ }
259
+ }
260
+ exports.LIDMappingStore = LIDMappingStore;
@@ -12,6 +12,8 @@ const Utils_1 = require("../Utils");
12
12
  const WABinary_1 = require("../WABinary");
13
13
  const Client_1 = require("./Client");
14
14
  const mex_1 = require("./mex");
15
+ const USyncQuery_1 = require("../WAUSync/USyncQuery");
16
+ const USyncUser_1 = require("../WAUSync/USyncUser");
15
17
  /**
16
18
  * Connects to WA servers and performs:
17
19
  * - simple queries (no retry mechanism, wait for connection establishment)
@@ -46,7 +48,37 @@ const makeSocket = (config) => {
46
48
  const { creds } = authState;
47
49
  // add transaction capability
48
50
  const keys = (0, Utils_1.addTransactionCapability)(authState.keys, logger, transactionOpts);
49
- const signalRepository = makeSignalRepository({ creds, keys });
51
+ /**
52
+ * Resolves PN -> LID mappings via a USync query. This is a closure so it
53
+ * can safely reference `query`/`generateMessageTag` even though they are
54
+ * declared further down in this function: by the time this is actually
55
+ * *called* (during message send/receive), those are already initialized.
56
+ * Passed into the LID mapping store so signal sessions stop flip-flopping
57
+ * between PN and LID addresses for the same contact.
58
+ */
59
+ const pnFromLIDUSync = async (jids) => {
60
+ const usyncQuery = new USyncQuery_1.USyncQuery().withLIDProtocol().withContext('background');
61
+ for (const jid of jids) {
62
+ if ((0, WABinary_1.isLidUser)(jid)) {
63
+ continue;
64
+ }
65
+ usyncQuery.withUser(new USyncUser_1.USyncUser().withId(jid));
66
+ }
67
+ if (usyncQuery.users.length === 0) {
68
+ return [];
69
+ }
70
+ try {
71
+ const result = await executeUSyncQuery(usyncQuery);
72
+ if (result) {
73
+ return result.list.filter(a => !!a.lid).map(({ lid, id }) => ({ pn: id, lid }));
74
+ }
75
+ }
76
+ catch (err) {
77
+ logger?.debug?.({ err }, 'pnFromLIDUSync query failed');
78
+ }
79
+ return [];
80
+ };
81
+ const signalRepository = makeSignalRepository({ creds, keys }, logger, pnFromLIDUSync);
50
82
  let lastDateRecv;
51
83
  let epoch = 1;
52
84
  let keepAliveReq;
@@ -146,6 +178,53 @@ const makeSocket = (config) => {
146
178
  }
147
179
  return result;
148
180
  };
181
+ /** run a USync query (used internally for PN<->LID resolution, also exposed on the socket) */
182
+ const executeUSyncQuery = async (usyncQuery) => {
183
+ if (usyncQuery.protocols.length === 0) {
184
+ throw new boom_1.Boom('USyncQuery must have at least one protocol');
185
+ }
186
+ const validUsers = usyncQuery.users;
187
+ const userNodes = validUsers.map(user => ({
188
+ tag: 'user',
189
+ attrs: {
190
+ jid: !user.phone ? user.id : undefined
191
+ },
192
+ content: usyncQuery.protocols.map(a => a.getUserElement(user)).filter(a => a !== null)
193
+ }));
194
+ const listNode = {
195
+ tag: 'list',
196
+ attrs: {},
197
+ content: userNodes
198
+ };
199
+ const queryNode = {
200
+ tag: 'query',
201
+ attrs: {},
202
+ content: usyncQuery.protocols.map(a => a.getQueryElement())
203
+ };
204
+ const iq = {
205
+ tag: 'iq',
206
+ attrs: {
207
+ to: WABinary_1.S_WHATSAPP_NET,
208
+ type: 'get',
209
+ xmlns: 'usync'
210
+ },
211
+ content: [
212
+ {
213
+ tag: 'usync',
214
+ attrs: {
215
+ context: usyncQuery.context,
216
+ mode: usyncQuery.mode,
217
+ sid: generateMessageTag(),
218
+ last: 'true',
219
+ index: '0'
220
+ },
221
+ content: [queryNode, listNode]
222
+ }
223
+ ]
224
+ };
225
+ const result = await query(iq);
226
+ return usyncQuery.parseUSyncQueryResult(result);
227
+ };
149
228
  /** connection handshake */
150
229
  const validateConnection = async () => {
151
230
  let helloMsg = {
@@ -615,6 +694,7 @@ const makeSocket = (config) => {
615
694
  },
616
695
  generateMessageTag,
617
696
  query,
697
+ executeUSyncQuery,
618
698
  waitForMessage,
619
699
  waitForSocketOpen,
620
700
  sendRawMessage,
@@ -64,3 +64,16 @@ const jidNormalizedUser = (jid) => {
64
64
  return (0, exports.jidEncode)(user, server === 'c.us' ? 's.whatsapp.net' : server);
65
65
  };
66
66
  exports.jidNormalizedUser = jidNormalizedUser;
67
+ /** is the jid a normal phone-number user (used by LID mapping) */
68
+ const isPnUser = (jid) => jid === null || jid === void 0 ? void 0 : jid.endsWith('@s.whatsapp.net');
69
+ exports.isPnUser = isPnUser;
70
+ /** is the jid a "hosted" phone-number user (business hosted accounts) */
71
+ const isHostedPnUser = (jid) => jid === null || jid === void 0 ? void 0 : jid.endsWith('@hosted');
72
+ exports.isHostedPnUser = isHostedPnUser;
73
+ /** domain type constants used by the LID mapping store */
74
+ exports.WAJIDDomains = {
75
+ PN: 0,
76
+ LID: 1,
77
+ HOSTED: 2,
78
+ HOSTED_LID: 3
79
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keithbaileys",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "A lightweight, full-featured WhatsApp Web API library for Node.js",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "async-mutex": "^0.5.0",
23
23
  "axios": "^1.6.0",
24
24
  "fflate": "^0.8.2",
25
- "libsignal": "latest",
25
+ "libsignal": "^6.0.0",
26
26
  "lodash": "^4.17.21",
27
27
  "music-metadata": "^7.12.3",
28
28
  "node-fetch": "^2.6.1",