zapo-js 1.5.0 → 1.5.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.
@@ -761,8 +761,8 @@ class WaAppStateMutationCoordinator {
761
761
  statusPrivacy: {
762
762
  mode: modeValue,
763
763
  userJid,
764
- ...(input.shareToFB === undefined ? {} : { shareToFB: input.shareToFB }),
765
- ...(input.shareToIG === undefined ? {} : { shareToIG: input.shareToIG })
764
+ ...(input.shareToFB === undefined ? {} : { shareToFb: input.shareToFB }),
765
+ ...(input.shareToIG === undefined ? {} : { shareToIg: input.shareToIG })
766
766
  }
767
767
  };
768
768
  const timestamp = this.serverClock.nowMs();
@@ -318,7 +318,7 @@ class WaMessageDispatchCoordinator {
318
318
  }
319
319
  try {
320
320
  const results = await this.deps.signalDeviceSync.queryLidsByPhoneJids([pnUserJid]);
321
- const match = results.find((entry) => entry.phoneJid === pnUserJid);
321
+ const match = results.find((entry) => entry.queriedJid === pnUserJid);
322
322
  if (match?.lidJid)
323
323
  return match.lidJid;
324
324
  }
@@ -758,8 +758,8 @@ export class WaAppStateMutationCoordinator {
758
758
  statusPrivacy: {
759
759
  mode: modeValue,
760
760
  userJid,
761
- ...(input.shareToFB === undefined ? {} : { shareToFB: input.shareToFB }),
762
- ...(input.shareToIG === undefined ? {} : { shareToIG: input.shareToIG })
761
+ ...(input.shareToFB === undefined ? {} : { shareToFb: input.shareToFB }),
762
+ ...(input.shareToIG === undefined ? {} : { shareToIg: input.shareToIG })
763
763
  }
764
764
  };
765
765
  const timestamp = this.serverClock.nowMs();
@@ -315,7 +315,7 @@ export class WaMessageDispatchCoordinator {
315
315
  }
316
316
  try {
317
317
  const results = await this.deps.signalDeviceSync.queryLidsByPhoneJids([pnUserJid]);
318
- const match = results.find((entry) => entry.phoneJid === pnUserJid);
318
+ const match = results.find((entry) => entry.queriedJid === pnUserJid);
319
319
  if (match?.lidJid)
320
320
  return match.lidJid;
321
321
  }
@@ -1,8 +1,8 @@
1
1
  import { PromiseDedup } from '../../infra/perf/PromiseDedup.js';
2
2
  import { WA_DEFAULTS, WA_NODE_TAGS, WA_USYNC_CONTEXTS } from '../../protocol/constants.js';
3
- import { buildDeviceJid, isHostedDeviceId, splitJid, toUserJid } from '../../protocol/jid.js';
3
+ import { buildDeviceJid, isHostedDeviceId, parsePhoneJid, splitJid, toUserJid } from '../../protocol/jid.js';
4
4
  import { buildUsyncIq, iterateUsyncUsers, parseUsyncResultEnvelope } from '../../transport/node/builders/usync.js';
5
- import { findNodeChild, getNodeChildrenByTag } from '../../transport/node/helpers.js';
5
+ import { findNodeChild, getNodeChildrenByTag, getNodeTextContent } from '../../transport/node/helpers.js';
6
6
  import { assertIqResult } from '../../transport/node/query.js';
7
7
  import { createUsyncSidGenerator, logUsyncProtocolErrors } from '../../transport/node/usync.js';
8
8
  /**
@@ -112,22 +112,21 @@ export class SignalDeviceSyncApi {
112
112
  const parsedByPhoneJid = new Map();
113
113
  for (let index = 0; index < parsed.length; index += 1) {
114
114
  const entry = parsed[index];
115
- const phoneJid = entry.phoneJid ?? entry.jid;
116
- parsedByPhoneJid.set(phoneJid, {
117
- phoneJid,
115
+ parsedByPhoneJid.set(entry.jid, {
116
+ phoneJid: entry.phoneJid ?? entry.jid,
118
117
  lidJid: entry.lidJid,
119
- exists: entry.exists
118
+ exists: entry.exists,
119
+ invalid: entry.invalid
120
120
  });
121
121
  }
122
122
  const result = new Array(normalizedPhoneJids.length);
123
123
  let found = 0;
124
124
  for (let index = 0; index < normalizedPhoneJids.length; index += 1) {
125
- const phoneJid = normalizedPhoneJids[index];
126
- const resolved = parsedByPhoneJid.get(phoneJid) ?? {
127
- phoneJid,
128
- lidJid: null,
129
- exists: false
130
- };
125
+ const queriedJid = normalizedPhoneJids[index];
126
+ const hit = parsedByPhoneJid.get(queriedJid);
127
+ const resolved = hit
128
+ ? { queriedJid, ...hit }
129
+ : { queriedJid, phoneJid: queriedJid, lidJid: null, exists: false, invalid: false };
131
130
  if (resolved.exists) {
132
131
  found += 1;
133
132
  }
@@ -151,24 +150,23 @@ export class SignalDeviceSyncApi {
151
150
  if (!this.deviceListStore || results.length === 0)
152
151
  return;
153
152
  const nowMs = Date.now();
154
- const lidByPhoneJid = new Map();
155
- const phoneJids = [];
153
+ const lidByQueriedJid = new Map();
154
+ const queriedJids = [];
156
155
  for (const entry of results) {
157
- if (entry.lidJid && entry.phoneJid) {
158
- lidByPhoneJid.set(entry.phoneJid, entry.lidJid);
159
- phoneJids.push(entry.phoneJid);
156
+ if (entry.lidJid) {
157
+ lidByQueriedJid.set(entry.queriedJid, entry.lidJid);
158
+ queriedJids.push(entry.queriedJid);
160
159
  }
161
160
  }
162
- if (phoneJids.length === 0)
161
+ if (queriedJids.length === 0)
163
162
  return;
164
- const existing = await this.deviceListStore.getUserDevicesBatch(phoneJids, nowMs);
163
+ const existing = await this.deviceListStore.getUserDevicesBatch(queriedJids, nowMs);
165
164
  const updates = [];
166
- for (let index = 0; index < phoneJids.length; index += 1) {
165
+ for (let index = 0; index < queriedJids.length; index += 1) {
167
166
  const snapshot = existing[index];
168
167
  if (!snapshot)
169
168
  continue;
170
- const phoneJid = phoneJids[index];
171
- const lidJid = lidByPhoneJid.get(phoneJid);
169
+ const lidJid = lidByQueriedJid.get(queriedJids[index]);
172
170
  if (!lidJid || snapshot.altUserJid === lidJid)
173
171
  continue;
174
172
  updates.push({
@@ -271,7 +269,13 @@ export class SignalDeviceSyncApi {
271
269
  if (!userJid) {
272
270
  continue;
273
271
  }
274
- const normalizedUserJid = this.normalizeUserJid(userJid);
272
+ const normalizedUserJid = this.tryNormalizeUserJid(userJid);
273
+ if (normalizedUserJid === null) {
274
+ this.logger.debug('signal device sync skipping user node with invalid jid', {
275
+ jid: userJid
276
+ });
277
+ continue;
278
+ }
275
279
  if (!requestedSet.has(normalizedUserJid)) {
276
280
  continue;
277
281
  }
@@ -292,55 +296,107 @@ export class SignalDeviceSyncApi {
292
296
  throw new Error('signal lid sync response missing usync envelope');
293
297
  }
294
298
  const requestedSet = new Set(requestedUsers);
295
- const parsed = new Array(userNodes.length);
296
- let parsedCount = 0;
299
+ const parsed = [];
300
+ const lidUserErrors = [];
297
301
  for (let index = 0; index < userNodes.length; index += 1) {
298
302
  const userNode = userNodes[index];
299
- const userJid = userNode.attrs.jid;
300
- if (!userJid) {
303
+ const rawUserJid = userNode.attrs.jid;
304
+ if (!rawUserJid) {
301
305
  continue;
302
306
  }
303
- const normalizedUserJid = this.normalizeUserJid(userJid);
304
- const normalizedPhoneJid = userNode.attrs.pn_jid
305
- ? this.normalizeUserJid(userNode.attrs.pn_jid)
307
+ const resolvedJid = this.tryNormalizeUserJid(rawUserJid);
308
+ const pnJid = userNode.attrs.pn_jid
309
+ ? this.tryNormalizeUserJid(userNode.attrs.pn_jid)
310
+ : null;
311
+ const phoneJid = pnJid ?? resolvedJid;
312
+ const lidNode = findNodeChild(userNode, WA_NODE_TAGS.LID);
313
+ const lidErrorNode = lidNode ? findNodeChild(lidNode, WA_NODE_TAGS.ERROR) : null;
314
+ if (lidErrorNode) {
315
+ lidUserErrors.push({
316
+ jid: resolvedJid ?? rawUserJid,
317
+ code: lidErrorNode.attrs.code,
318
+ text: lidErrorNode.attrs.text
319
+ });
320
+ }
321
+ const lidJid = !lidErrorNode && lidNode?.attrs.val
322
+ ? this.tryNormalizeUserJid(lidNode.attrs.val)
306
323
  : null;
307
- const wasRequested = requestedSet.has(normalizedUserJid) ||
308
- (normalizedPhoneJid !== null && requestedSet.has(normalizedPhoneJid));
309
- if (!wasRequested) {
324
+ const contactNodes = getNodeChildrenByTag(userNode, WA_NODE_TAGS.CONTACT);
325
+ let matched = false;
326
+ for (let c = 0; c < contactNodes.length; c += 1) {
327
+ const contactNode = contactNodes[c];
328
+ const inputJid = this.recoverContactJid(contactNode);
329
+ if (inputJid === null || !requestedSet.has(inputJid)) {
330
+ continue;
331
+ }
332
+ matched = true;
333
+ const invalid = resolvedJid === null || contactNode.attrs.type === 'invalid';
334
+ parsed.push({
335
+ jid: inputJid,
336
+ phoneJid: phoneJid ?? inputJid,
337
+ lidJid,
338
+ exists: !invalid &&
339
+ this.parseLidSyncContactExists(contactNode, inputJid, lidJid !== null),
340
+ invalid
341
+ });
342
+ }
343
+ if (matched) {
310
344
  continue;
311
345
  }
312
- const lidNode = findNodeChild(userNode, WA_NODE_TAGS.LID);
313
- const contactNode = findNodeChild(userNode, WA_NODE_TAGS.CONTACT);
314
- if (!lidNode) {
315
- parsed[parsedCount] = this.buildLidSyncResult(normalizedUserJid, normalizedPhoneJid, contactNode, null);
316
- parsedCount += 1;
346
+ if (resolvedJid === null) {
347
+ this.logger.debug('signal lid sync skipping user node with invalid jid', {
348
+ jid: rawUserJid
349
+ });
317
350
  continue;
318
351
  }
319
- const errorNode = findNodeChild(lidNode, WA_NODE_TAGS.ERROR);
320
- if (errorNode) {
321
- this.logger.warn('signal lid sync user error', {
322
- jid: normalizedUserJid,
323
- code: errorNode.attrs.code,
324
- text: errorNode.attrs.text
352
+ const requestedKey = requestedSet.has(resolvedJid)
353
+ ? resolvedJid
354
+ : pnJid !== null && requestedSet.has(pnJid)
355
+ ? pnJid
356
+ : null;
357
+ if (requestedKey === null) {
358
+ this.logger.debug('signal lid sync unmatched user (no contact echo)', {
359
+ jid: resolvedJid,
360
+ pnJid
325
361
  });
326
- parsed[parsedCount] = this.buildLidSyncResult(normalizedUserJid, normalizedPhoneJid, contactNode, null);
327
- parsedCount += 1;
328
362
  continue;
329
363
  }
330
- const lidJid = lidNode.attrs.val ? this.normalizeUserJid(lidNode.attrs.val) : null;
331
- parsed[parsedCount] = this.buildLidSyncResult(normalizedUserJid, normalizedPhoneJid, contactNode, lidJid);
332
- parsedCount += 1;
364
+ const contactNode = findNodeChild(userNode, WA_NODE_TAGS.CONTACT);
365
+ parsed.push({
366
+ jid: requestedKey,
367
+ phoneJid: phoneJid ?? requestedKey,
368
+ lidJid,
369
+ exists: this.parseLidSyncContactExists(contactNode, requestedKey, lidJid !== null),
370
+ invalid: false
371
+ });
372
+ }
373
+ if (lidUserErrors.length > 0) {
374
+ this.logger.warn('signal lid sync user errors', {
375
+ droppedCount: lidUserErrors.length,
376
+ totalExpected: requestedUsers.length,
377
+ sample: lidUserErrors.slice(0, 3)
378
+ });
333
379
  }
334
- parsed.length = parsedCount;
335
380
  return parsed;
336
381
  }
337
- buildLidSyncResult(jid, phoneJid, contactNode, lidJid) {
338
- return {
339
- jid,
340
- lidJid,
341
- phoneJid,
342
- exists: this.parseLidSyncContactExists(contactNode, jid, lidJid !== null)
343
- };
382
+ /**
383
+ * Recovers the queried phone JID from a `<contact>` echo. Each `<contact>` in a
384
+ * usync response echoes the `+<number>` we sent (base64 text content), so it maps
385
+ * a response node back to the exact input - even when the server corrected the
386
+ * `<user jid>` or rejected it as `jid='undefined'`. Returns `null` when there is
387
+ * no decodable phone echo.
388
+ */
389
+ recoverContactJid(contactNode) {
390
+ const echoed = getNodeTextContent(contactNode);
391
+ if (!echoed) {
392
+ return null;
393
+ }
394
+ try {
395
+ return parsePhoneJid(echoed);
396
+ }
397
+ catch {
398
+ return null;
399
+ }
344
400
  }
345
401
  parseLidSyncContactExists(contactNode, userJid, defaultExists) {
346
402
  if (!contactNode) {
@@ -401,7 +457,10 @@ export class SignalDeviceSyncApi {
401
457
  let normalizedCount = 0;
402
458
  const dedup = new Set();
403
459
  for (let index = 0; index < userJids.length; index += 1) {
404
- const normalizedJid = this.normalizeUserJid(userJids[index]);
460
+ const normalizedJid = toUserJid(userJids[index], {
461
+ canonicalizeSignalServer: true,
462
+ hostDomain: this.hostDomain
463
+ });
405
464
  if (dedup.has(normalizedJid)) {
406
465
  continue;
407
466
  }
@@ -412,10 +471,21 @@ export class SignalDeviceSyncApi {
412
471
  normalized.length = normalizedCount;
413
472
  return normalized;
414
473
  }
415
- normalizeUserJid(jid) {
416
- return toUserJid(jid, {
417
- canonicalizeSignalServer: true,
418
- hostDomain: this.hostDomain
419
- });
474
+ /**
475
+ * Canonicalizes a user jid, returning `null` instead of throwing when the input
476
+ * is not a valid jid (for example the literal `'undefined'` the server returns
477
+ * for an invalid contact). Lets a single malformed response node be skipped
478
+ * without discarding the rest of the batch.
479
+ */
480
+ tryNormalizeUserJid(jid) {
481
+ try {
482
+ return toUserJid(jid, {
483
+ canonicalizeSignalServer: true,
484
+ hostDomain: this.hostDomain
485
+ });
486
+ }
487
+ catch {
488
+ return null;
489
+ }
420
490
  }
421
491
  }
@@ -11,9 +11,23 @@ interface SignalDeviceSyncApiOptions {
11
11
  readonly generateSid?: WaUsyncSidGenerator;
12
12
  }
13
13
  export interface SignalLidSyncResult {
14
+ /**
15
+ * The phone jid the caller queried (normalized), so results can be correlated
16
+ * back to the input by value rather than by array position. Equals `phoneJid`
17
+ * unless the server corrected the number.
18
+ */
19
+ readonly queriedJid: string;
20
+ /** The server's canonical/corrected phone jid (e.g. BR 9th digit added). */
14
21
  readonly phoneJid: string;
15
22
  readonly lidJid: string | null;
16
23
  readonly exists: boolean;
24
+ /**
25
+ * `true` when the server rejected the number as malformed (`<user
26
+ * jid='undefined'>` with `<contact type='invalid'>`) - distinct from a
27
+ * well-formed number that simply has no WhatsApp account (`exists: false`,
28
+ * `invalid: false`).
29
+ */
30
+ readonly invalid: boolean;
17
31
  }
18
32
  /**
19
33
  * Resolves the device list and LID mapping for a set of users via the `usync`
@@ -55,10 +69,23 @@ export declare class SignalDeviceSyncApi {
55
69
  private makeLidSyncRequest;
56
70
  private parseDeviceSyncResponse;
57
71
  private parseLidSyncResponse;
58
- private buildLidSyncResult;
72
+ /**
73
+ * Recovers the queried phone JID from a `<contact>` echo. Each `<contact>` in a
74
+ * usync response echoes the `+<number>` we sent (base64 text content), so it maps
75
+ * a response node back to the exact input - even when the server corrected the
76
+ * `<user jid>` or rejected it as `jid='undefined'`. Returns `null` when there is
77
+ * no decodable phone echo.
78
+ */
79
+ private recoverContactJid;
59
80
  private parseLidSyncContactExists;
60
81
  private parseUserDeviceJids;
61
82
  private normalizeUsers;
62
- private normalizeUserJid;
83
+ /**
84
+ * Canonicalizes a user jid, returning `null` instead of throwing when the input
85
+ * is not a valid jid (for example the literal `'undefined'` the server returns
86
+ * for an invalid contact). Lets a single malformed response node be skipped
87
+ * without discarding the rest of the batch.
88
+ */
89
+ private tryNormalizeUserJid;
63
90
  }
64
91
  export {};
@@ -115,22 +115,21 @@ class SignalDeviceSyncApi {
115
115
  const parsedByPhoneJid = new Map();
116
116
  for (let index = 0; index < parsed.length; index += 1) {
117
117
  const entry = parsed[index];
118
- const phoneJid = entry.phoneJid ?? entry.jid;
119
- parsedByPhoneJid.set(phoneJid, {
120
- phoneJid,
118
+ parsedByPhoneJid.set(entry.jid, {
119
+ phoneJid: entry.phoneJid ?? entry.jid,
121
120
  lidJid: entry.lidJid,
122
- exists: entry.exists
121
+ exists: entry.exists,
122
+ invalid: entry.invalid
123
123
  });
124
124
  }
125
125
  const result = new Array(normalizedPhoneJids.length);
126
126
  let found = 0;
127
127
  for (let index = 0; index < normalizedPhoneJids.length; index += 1) {
128
- const phoneJid = normalizedPhoneJids[index];
129
- const resolved = parsedByPhoneJid.get(phoneJid) ?? {
130
- phoneJid,
131
- lidJid: null,
132
- exists: false
133
- };
128
+ const queriedJid = normalizedPhoneJids[index];
129
+ const hit = parsedByPhoneJid.get(queriedJid);
130
+ const resolved = hit
131
+ ? { queriedJid, ...hit }
132
+ : { queriedJid, phoneJid: queriedJid, lidJid: null, exists: false, invalid: false };
134
133
  if (resolved.exists) {
135
134
  found += 1;
136
135
  }
@@ -154,24 +153,23 @@ class SignalDeviceSyncApi {
154
153
  if (!this.deviceListStore || results.length === 0)
155
154
  return;
156
155
  const nowMs = Date.now();
157
- const lidByPhoneJid = new Map();
158
- const phoneJids = [];
156
+ const lidByQueriedJid = new Map();
157
+ const queriedJids = [];
159
158
  for (const entry of results) {
160
- if (entry.lidJid && entry.phoneJid) {
161
- lidByPhoneJid.set(entry.phoneJid, entry.lidJid);
162
- phoneJids.push(entry.phoneJid);
159
+ if (entry.lidJid) {
160
+ lidByQueriedJid.set(entry.queriedJid, entry.lidJid);
161
+ queriedJids.push(entry.queriedJid);
163
162
  }
164
163
  }
165
- if (phoneJids.length === 0)
164
+ if (queriedJids.length === 0)
166
165
  return;
167
- const existing = await this.deviceListStore.getUserDevicesBatch(phoneJids, nowMs);
166
+ const existing = await this.deviceListStore.getUserDevicesBatch(queriedJids, nowMs);
168
167
  const updates = [];
169
- for (let index = 0; index < phoneJids.length; index += 1) {
168
+ for (let index = 0; index < queriedJids.length; index += 1) {
170
169
  const snapshot = existing[index];
171
170
  if (!snapshot)
172
171
  continue;
173
- const phoneJid = phoneJids[index];
174
- const lidJid = lidByPhoneJid.get(phoneJid);
172
+ const lidJid = lidByQueriedJid.get(queriedJids[index]);
175
173
  if (!lidJid || snapshot.altUserJid === lidJid)
176
174
  continue;
177
175
  updates.push({
@@ -274,7 +272,13 @@ class SignalDeviceSyncApi {
274
272
  if (!userJid) {
275
273
  continue;
276
274
  }
277
- const normalizedUserJid = this.normalizeUserJid(userJid);
275
+ const normalizedUserJid = this.tryNormalizeUserJid(userJid);
276
+ if (normalizedUserJid === null) {
277
+ this.logger.debug('signal device sync skipping user node with invalid jid', {
278
+ jid: userJid
279
+ });
280
+ continue;
281
+ }
278
282
  if (!requestedSet.has(normalizedUserJid)) {
279
283
  continue;
280
284
  }
@@ -295,55 +299,107 @@ class SignalDeviceSyncApi {
295
299
  throw new Error('signal lid sync response missing usync envelope');
296
300
  }
297
301
  const requestedSet = new Set(requestedUsers);
298
- const parsed = new Array(userNodes.length);
299
- let parsedCount = 0;
302
+ const parsed = [];
303
+ const lidUserErrors = [];
300
304
  for (let index = 0; index < userNodes.length; index += 1) {
301
305
  const userNode = userNodes[index];
302
- const userJid = userNode.attrs.jid;
303
- if (!userJid) {
306
+ const rawUserJid = userNode.attrs.jid;
307
+ if (!rawUserJid) {
304
308
  continue;
305
309
  }
306
- const normalizedUserJid = this.normalizeUserJid(userJid);
307
- const normalizedPhoneJid = userNode.attrs.pn_jid
308
- ? this.normalizeUserJid(userNode.attrs.pn_jid)
310
+ const resolvedJid = this.tryNormalizeUserJid(rawUserJid);
311
+ const pnJid = userNode.attrs.pn_jid
312
+ ? this.tryNormalizeUserJid(userNode.attrs.pn_jid)
313
+ : null;
314
+ const phoneJid = pnJid ?? resolvedJid;
315
+ const lidNode = (0, helpers_1.findNodeChild)(userNode, constants_1.WA_NODE_TAGS.LID);
316
+ const lidErrorNode = lidNode ? (0, helpers_1.findNodeChild)(lidNode, constants_1.WA_NODE_TAGS.ERROR) : null;
317
+ if (lidErrorNode) {
318
+ lidUserErrors.push({
319
+ jid: resolvedJid ?? rawUserJid,
320
+ code: lidErrorNode.attrs.code,
321
+ text: lidErrorNode.attrs.text
322
+ });
323
+ }
324
+ const lidJid = !lidErrorNode && lidNode?.attrs.val
325
+ ? this.tryNormalizeUserJid(lidNode.attrs.val)
309
326
  : null;
310
- const wasRequested = requestedSet.has(normalizedUserJid) ||
311
- (normalizedPhoneJid !== null && requestedSet.has(normalizedPhoneJid));
312
- if (!wasRequested) {
327
+ const contactNodes = (0, helpers_1.getNodeChildrenByTag)(userNode, constants_1.WA_NODE_TAGS.CONTACT);
328
+ let matched = false;
329
+ for (let c = 0; c < contactNodes.length; c += 1) {
330
+ const contactNode = contactNodes[c];
331
+ const inputJid = this.recoverContactJid(contactNode);
332
+ if (inputJid === null || !requestedSet.has(inputJid)) {
333
+ continue;
334
+ }
335
+ matched = true;
336
+ const invalid = resolvedJid === null || contactNode.attrs.type === 'invalid';
337
+ parsed.push({
338
+ jid: inputJid,
339
+ phoneJid: phoneJid ?? inputJid,
340
+ lidJid,
341
+ exists: !invalid &&
342
+ this.parseLidSyncContactExists(contactNode, inputJid, lidJid !== null),
343
+ invalid
344
+ });
345
+ }
346
+ if (matched) {
313
347
  continue;
314
348
  }
315
- const lidNode = (0, helpers_1.findNodeChild)(userNode, constants_1.WA_NODE_TAGS.LID);
316
- const contactNode = (0, helpers_1.findNodeChild)(userNode, constants_1.WA_NODE_TAGS.CONTACT);
317
- if (!lidNode) {
318
- parsed[parsedCount] = this.buildLidSyncResult(normalizedUserJid, normalizedPhoneJid, contactNode, null);
319
- parsedCount += 1;
349
+ if (resolvedJid === null) {
350
+ this.logger.debug('signal lid sync skipping user node with invalid jid', {
351
+ jid: rawUserJid
352
+ });
320
353
  continue;
321
354
  }
322
- const errorNode = (0, helpers_1.findNodeChild)(lidNode, constants_1.WA_NODE_TAGS.ERROR);
323
- if (errorNode) {
324
- this.logger.warn('signal lid sync user error', {
325
- jid: normalizedUserJid,
326
- code: errorNode.attrs.code,
327
- text: errorNode.attrs.text
355
+ const requestedKey = requestedSet.has(resolvedJid)
356
+ ? resolvedJid
357
+ : pnJid !== null && requestedSet.has(pnJid)
358
+ ? pnJid
359
+ : null;
360
+ if (requestedKey === null) {
361
+ this.logger.debug('signal lid sync unmatched user (no contact echo)', {
362
+ jid: resolvedJid,
363
+ pnJid
328
364
  });
329
- parsed[parsedCount] = this.buildLidSyncResult(normalizedUserJid, normalizedPhoneJid, contactNode, null);
330
- parsedCount += 1;
331
365
  continue;
332
366
  }
333
- const lidJid = lidNode.attrs.val ? this.normalizeUserJid(lidNode.attrs.val) : null;
334
- parsed[parsedCount] = this.buildLidSyncResult(normalizedUserJid, normalizedPhoneJid, contactNode, lidJid);
335
- parsedCount += 1;
367
+ const contactNode = (0, helpers_1.findNodeChild)(userNode, constants_1.WA_NODE_TAGS.CONTACT);
368
+ parsed.push({
369
+ jid: requestedKey,
370
+ phoneJid: phoneJid ?? requestedKey,
371
+ lidJid,
372
+ exists: this.parseLidSyncContactExists(contactNode, requestedKey, lidJid !== null),
373
+ invalid: false
374
+ });
375
+ }
376
+ if (lidUserErrors.length > 0) {
377
+ this.logger.warn('signal lid sync user errors', {
378
+ droppedCount: lidUserErrors.length,
379
+ totalExpected: requestedUsers.length,
380
+ sample: lidUserErrors.slice(0, 3)
381
+ });
336
382
  }
337
- parsed.length = parsedCount;
338
383
  return parsed;
339
384
  }
340
- buildLidSyncResult(jid, phoneJid, contactNode, lidJid) {
341
- return {
342
- jid,
343
- lidJid,
344
- phoneJid,
345
- exists: this.parseLidSyncContactExists(contactNode, jid, lidJid !== null)
346
- };
385
+ /**
386
+ * Recovers the queried phone JID from a `<contact>` echo. Each `<contact>` in a
387
+ * usync response echoes the `+<number>` we sent (base64 text content), so it maps
388
+ * a response node back to the exact input - even when the server corrected the
389
+ * `<user jid>` or rejected it as `jid='undefined'`. Returns `null` when there is
390
+ * no decodable phone echo.
391
+ */
392
+ recoverContactJid(contactNode) {
393
+ const echoed = (0, helpers_1.getNodeTextContent)(contactNode);
394
+ if (!echoed) {
395
+ return null;
396
+ }
397
+ try {
398
+ return (0, jid_1.parsePhoneJid)(echoed);
399
+ }
400
+ catch {
401
+ return null;
402
+ }
347
403
  }
348
404
  parseLidSyncContactExists(contactNode, userJid, defaultExists) {
349
405
  if (!contactNode) {
@@ -404,7 +460,10 @@ class SignalDeviceSyncApi {
404
460
  let normalizedCount = 0;
405
461
  const dedup = new Set();
406
462
  for (let index = 0; index < userJids.length; index += 1) {
407
- const normalizedJid = this.normalizeUserJid(userJids[index]);
463
+ const normalizedJid = (0, jid_1.toUserJid)(userJids[index], {
464
+ canonicalizeSignalServer: true,
465
+ hostDomain: this.hostDomain
466
+ });
408
467
  if (dedup.has(normalizedJid)) {
409
468
  continue;
410
469
  }
@@ -415,11 +474,22 @@ class SignalDeviceSyncApi {
415
474
  normalized.length = normalizedCount;
416
475
  return normalized;
417
476
  }
418
- normalizeUserJid(jid) {
419
- return (0, jid_1.toUserJid)(jid, {
420
- canonicalizeSignalServer: true,
421
- hostDomain: this.hostDomain
422
- });
477
+ /**
478
+ * Canonicalizes a user jid, returning `null` instead of throwing when the input
479
+ * is not a valid jid (for example the literal `'undefined'` the server returns
480
+ * for an invalid contact). Lets a single malformed response node be skipped
481
+ * without discarding the rest of the batch.
482
+ */
483
+ tryNormalizeUserJid(jid) {
484
+ try {
485
+ return (0, jid_1.toUserJid)(jid, {
486
+ canonicalizeSignalServer: true,
487
+ hostDomain: this.hostDomain
488
+ });
489
+ }
490
+ catch {
491
+ return null;
492
+ }
423
493
  }
424
494
  }
425
495
  exports.SignalDeviceSyncApi = SignalDeviceSyncApi;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapo-js",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "High-performance WhatsApp Web TypeScript library",
5
5
  "license": "MIT",
6
6
  "author": "vinikjkkj <contact@vinicius.email> (https://github.com/vinikjkkj)",