zapo-js 1.5.0 → 1.6.0

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.
Files changed (34) hide show
  1. package/dist/client/WaClient.d.ts +1 -1
  2. package/dist/client/WaClient.js +1 -1
  3. package/dist/client/WaClientFactory.js +1 -0
  4. package/dist/client/coordinators/WaAppStateMutationCoordinator.js +2 -2
  5. package/dist/client/coordinators/WaMessageCoordinator.d.ts +69 -1
  6. package/dist/client/coordinators/WaMessageCoordinator.js +76 -1
  7. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +1 -1
  8. package/dist/client/media.d.ts +1 -0
  9. package/dist/client/media.js +1 -0
  10. package/dist/client/messaging/messages.d.ts +45 -1
  11. package/dist/client/messaging/messages.js +57 -45
  12. package/dist/esm/client/WaClient.js +1 -1
  13. package/dist/esm/client/WaClientFactory.js +1 -0
  14. package/dist/esm/client/coordinators/WaAppStateMutationCoordinator.js +2 -2
  15. package/dist/esm/client/coordinators/WaMessageCoordinator.js +78 -3
  16. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +1 -1
  17. package/dist/esm/client/media.js +1 -1
  18. package/dist/esm/client/messaging/messages.js +56 -45
  19. package/dist/esm/index.js +2 -0
  20. package/dist/esm/signal/api/SignalDeviceSyncApi.js +133 -63
  21. package/dist/index.d.ts +4 -1
  22. package/dist/index.js +6 -2
  23. package/dist/signal/api/SignalDeviceSyncApi.d.ts +29 -2
  24. package/dist/signal/api/SignalDeviceSyncApi.js +131 -61
  25. package/package.json +1 -1
  26. package/spec/appstate/index.d.ts +5 -4
  27. package/spec/appstate/index.js +21 -8
  28. package/spec/mex/index.d.ts +335 -73
  29. package/spec/mex/index.js +29 -21
  30. package/spec/proto/index.d.ts +833 -69
  31. package/spec/proto/index.js +1 -1
  32. package/spec/version/index.d.ts +1 -1
  33. package/spec/version/index.js +1 -1
  34. package/spec/version/version.json +1 -1
@@ -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
  }
@@ -198,7 +198,7 @@ export async function performPlaintextMediaUpload(deps, input) {
198
198
  await prepared.cleanup?.();
199
199
  }
200
200
  }
201
- async function assertReadableFile(filePath) {
201
+ export async function assertReadableFile(filePath) {
202
202
  try {
203
203
  const stats = await stat(filePath);
204
204
  if (!stats.isFile()) {
@@ -341,9 +341,15 @@ async function buildMediaMessage(options, content) {
341
341
  const firstFrameLength = content.type === 'sticker'
342
342
  ? (content.firstFrameLength ?? detectedFirstFrameLength)
343
343
  : undefined;
344
- const uploadPromise = isReadableStream(resolved.uploadMedia)
345
- ? uploadMediaStream(options, content, resolved.uploadMedia, firstFrameLength, mimetype)
346
- : uploadMediaBytes(options, content, resolved.uploadMedia, firstFrameLength, mimetype);
344
+ const uploadType = resolveUploadType(content);
345
+ const uploadPromise = uploadMedia(options, {
346
+ source: resolved.uploadMedia,
347
+ cryptoType: uploadType,
348
+ uploadPath: resolveUploadPath(uploadType),
349
+ contentType: mimetype,
350
+ sidecar: needsSidecar(content),
351
+ firstFrameLength
352
+ });
347
353
  const processPromise = runMediaProcessor(options.media, resolved.processorInput, content, options.logger);
348
354
  const [uploadResult, processResult] = await Promise.allSettled([
349
355
  uploadPromise,
@@ -504,29 +510,42 @@ function parseUploadResponse(body, status) {
504
510
  ...(parsed.metadata_url ? { metadataUrl: parsed.metadata_url } : {})
505
511
  };
506
512
  }
507
- async function uploadMediaBytes(options, content, mediaBytes, firstFrameLength, mimetype) {
508
- const uploadType = resolveUploadType(content);
509
- const mediaKey = await WaMediaCrypto.generateMediaKey();
513
+ /**
514
+ * Shared media upload primitive: derives (or reuses) a media key, encrypts,
515
+ * fetches a media connection, and POSTs the ciphertext. Bytes take a
516
+ * zero-temp-file fast path; streams stage to a temp file first. Backs both the
517
+ * send path and `WaMessageCoordinator.upload`.
518
+ */
519
+ export async function uploadMedia(options, input) {
520
+ const mediaKey = input.mediaKey ?? (await WaMediaCrypto.generateMediaKey());
521
+ if (input.source instanceof Uint8Array) {
522
+ return uploadMediaBytes(options, input, mediaKey, input.source);
523
+ }
524
+ return uploadMediaStream(options, input, mediaKey, input.source);
525
+ }
526
+ async function uploadMediaBytes(options, input, mediaKey, mediaBytes) {
510
527
  const [encrypted, mediaConn] = await Promise.all([
511
- WaMediaCrypto.encryptBytes(uploadType, mediaKey, mediaBytes, {
512
- sidecar: needsSidecar(content),
513
- firstFrameLength
528
+ WaMediaCrypto.encryptBytes(input.cryptoType, mediaKey, mediaBytes, {
529
+ sidecar: input.sidecar,
530
+ firstFrameLength: input.firstFrameLength
514
531
  }),
515
532
  getMediaConn(options)
516
533
  ]);
517
534
  const selectedHost = selectMediaUploadHost(mediaConn);
518
- const uploadUrl = buildMediaUploadUrl(selectedHost, resolveUploadPath(uploadType), mediaConn.auth, encrypted.fileEncSha256);
519
- options.logger.debug('sending media upload request', {
520
- mediaType: content.type,
521
- uploadType,
522
- host: selectedHost
535
+ const uploadUrl = buildMediaUploadUrl(selectedHost, input.uploadPath, mediaConn.auth, encrypted.fileEncSha256);
536
+ options.logger.debug(input.logLabel ?? 'sending media upload request', {
537
+ cryptoType: input.cryptoType,
538
+ host: selectedHost,
539
+ size: mediaBytes.byteLength
523
540
  });
524
541
  const uploadResponse = await options.mediaTransfer.uploadStream({
525
542
  url: uploadUrl,
526
543
  method: 'POST',
527
544
  body: encrypted.ciphertextHmac,
528
545
  contentLength: encrypted.ciphertextHmac.byteLength,
529
- contentType: mimetype
546
+ contentType: input.contentType,
547
+ timeoutMs: input.timeoutMs,
548
+ signal: input.signal
530
549
  });
531
550
  const responseBody = await options.mediaTransfer.readResponseBytes(uploadResponse);
532
551
  const parsed = parseUploadResponse(responseBody, uploadResponse.status);
@@ -538,19 +557,22 @@ async function uploadMediaBytes(options, content, mediaBytes, firstFrameLength,
538
557
  fileLength: mediaBytes.byteLength,
539
558
  streamingSidecar: encrypted.streamingSidecar,
540
559
  firstFrameSidecar: encrypted.firstFrameSidecar,
541
- firstFrameLength
560
+ firstFrameLength: input.firstFrameLength
542
561
  };
543
562
  }
544
- async function uploadEncryptedStream(options, input) {
545
- const encResult = await WaMediaCrypto.encryptToFile(input.cryptoType, input.mediaKey, input.plaintext, { sidecar: input.sidecar, firstFrameLength: input.firstFrameLength });
563
+ async function uploadMediaStream(options, input, mediaKey, plaintext) {
564
+ const encResult = await WaMediaCrypto.encryptToFile(input.cryptoType, mediaKey, plaintext, {
565
+ sidecar: input.sidecar,
566
+ firstFrameLength: input.firstFrameLength
567
+ });
546
568
  let readStream;
547
569
  try {
548
570
  const mediaConn = await getMediaConn(options);
549
571
  const selectedHost = selectMediaUploadHost(mediaConn);
550
572
  const uploadUrl = buildMediaUploadUrl(selectedHost, input.uploadPath, mediaConn.auth, encResult.fileEncSha256);
551
- options.logger.debug(input.logLabel, {
573
+ options.logger.debug(input.logLabel ?? 'sending media stream upload request', {
574
+ cryptoType: input.cryptoType,
552
575
  host: selectedHost,
553
- uploadPath: input.uploadPath,
554
576
  plaintextLength: encResult.plaintextLength,
555
577
  encryptedSize: encResult.fileSize
556
578
  });
@@ -560,13 +582,15 @@ async function uploadEncryptedStream(options, input) {
560
582
  method: 'POST',
561
583
  body: readStream,
562
584
  contentLength: encResult.fileSize,
563
- contentType: input.contentType
585
+ contentType: input.contentType,
586
+ timeoutMs: input.timeoutMs,
587
+ signal: input.signal
564
588
  });
565
589
  const responseBody = await options.mediaTransfer.readResponseBytes(uploadResponse);
566
590
  const parsed = parseUploadResponse(responseBody, uploadResponse.status);
567
591
  return {
568
592
  ...parsed,
569
- mediaKey: input.mediaKey,
593
+ mediaKey,
570
594
  fileSha256: encResult.fileSha256,
571
595
  fileEncSha256: encResult.fileEncSha256,
572
596
  fileLength: encResult.plaintextLength,
@@ -585,19 +609,6 @@ async function uploadEncryptedStream(options, input) {
585
609
  await WaMediaCrypto.cleanupEncryptedFile(encResult.filePath);
586
610
  }
587
611
  }
588
- async function uploadMediaStream(options, content, stream, firstFrameLength, mimetype) {
589
- const cryptoType = resolveUploadType(content);
590
- return uploadEncryptedStream(options, {
591
- plaintext: stream,
592
- mediaKey: await WaMediaCrypto.generateMediaKey(),
593
- cryptoType,
594
- uploadPath: resolveUploadPath(cryptoType),
595
- contentType: mimetype,
596
- logLabel: 'sending media stream upload request',
597
- sidecar: needsSidecar(content),
598
- firstFrameLength
599
- });
600
- }
601
612
  function openStickerPackInputStream(media) {
602
613
  return typeof media === 'string' ? createReadStream(media) : Readable.from([media]);
603
614
  }
@@ -609,23 +620,23 @@ async function buildStickerPackMediaMessage(options, content) {
609
620
  const coverThumbnail = content.coverThumbnail;
610
621
  const mediaKey = await WaMediaCrypto.generateMediaKey();
611
622
  const [bundle, cover] = await Promise.all([
612
- uploadEncryptedStream(options, {
613
- plaintext: createStickerPackZipStream(toStickerPackZipEntries(content)),
614
- mediaKey,
623
+ uploadMedia(options, {
624
+ source: createStickerPackZipStream(toStickerPackZipEntries(content)),
615
625
  cryptoType: 'sticker-pack',
616
626
  uploadPath: MEDIA_UPLOAD_PATHS['sticker-pack'],
617
627
  contentType: 'application/zip',
618
- logLabel: 'sending sticker pack bundle upload',
619
- sidecar: false
620
- }),
621
- uploadEncryptedStream(options, {
622
- plaintext: openStickerPackInputStream(coverThumbnail),
623
628
  mediaKey,
629
+ sidecar: false,
630
+ logLabel: 'sending sticker pack bundle upload'
631
+ }),
632
+ uploadMedia(options, {
633
+ source: openStickerPackInputStream(coverThumbnail),
624
634
  cryptoType: 'thumbnail-sticker-pack',
625
635
  uploadPath: MEDIA_UPLOAD_PATHS['thumbnail-sticker-pack'],
626
636
  contentType: 'image/jpeg',
627
- logLabel: 'sending sticker pack thumbnail upload',
628
- sidecar: false
637
+ mediaKey,
638
+ sidecar: false,
639
+ logLabel: 'sending sticker pack thumbnail upload'
629
640
  })
630
641
  ]);
631
642
  if (cover.fileLength === 0) {
package/dist/esm/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export { WaClient } from './client/index.js';
2
2
  export { defineWaClientPlugin } from './client/plugins/index.js';
3
3
  export { downloadMediaMessage } from './client/media.js';
4
+ export { WaMediaCrypto } from './media/crypto/WaMediaCrypto.js';
5
+ export { WaMediaTransferClient } from './media/transfer/WaMediaTransferClient.js';
4
6
  export { createFileCompanionHostPersistence } from './client/persistence/companion-host.js';
5
7
  export { parseUsyncResultEnvelope } from './transport/node/builders/usync.js';
6
8
  export { getContentType, resolveEncMediaType, resolveMessageTarget, unwrapMessage } from './message/encode/content.js';
@@ -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
  }
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { defineWaClientPlugin } from './client/plugins';
3
3
  export type { WaClientDependencies } from './client/WaClientFactory';
4
4
  export type { WaClientEventMap, WaClientOptions, WaClientProxyOptions, WaDownloadMediaOptions, WaHistorySyncChunkEvent, WaHistorySyncOptions, WaWriteBehindOptions } from './client/types';
5
5
  export type { WaClientPluginContext, WaClientPluginDefinition } from './client/plugins';
6
- export type { WaMessageCoordinator } from './client/coordinators/WaMessageCoordinator';
6
+ export type { WaMediaUploadResult, WaMessageCoordinator, WaUploadMediaOptions, WaUploadMediaType } from './client/coordinators/WaMessageCoordinator';
7
7
  export type { WaAccountTakeoverNoticeEvent, WaAppStateMutationEvent, WaAppStateMutationSource, WaBusinessEvent, WaBusinessEventAction, WaBusinessProfileResult, WaConnectionEvent, WaGroupEvent, WaGroupEventAction, WaGroupEventLinkedGroup, WaGroupEventMembershipRequest, WaGroupEventParticipant, WaGroupEventSubgroupSuggestion, WaIgnoreKey, WaIgnoreKeyContext, WaIgnoreKeyPredicate, WaIgnoreStanzaKind, WaIncomingAddonEvent, WaIncomingBaseEvent, WaIncomingBotChunkEvent, WaIncomingCallEvent, WaIncomingChatstateEvent, WaIncomingErrorStanzaEvent, WaIncomingFailureEvent, WaIncomingMessageEvent, WaIncomingMessageKey, WaIncomingNewsletterEvent, WaIncomingNewsletterMessageUpdateEvent, WaIncomingNodeHandler, WaIncomingNodeHandlerRegistration, WaIncomingNotificationEvent, WaIncomingPresenceEvent, WaIncomingProtocolMessageEvent, WaIncomingReceiptEvent, WaIncomingStanzaFilter, WaIncomingUnavailableMessageEvent, WaIncomingUnhandledStanzaEvent, WaMexLidChangeEvent, WaMexMessageCappingEvent, WaMexMessageCappingStatus, WaMexNotificationEvent, WaMexNotificationGraphQlError, WaMexNotificationOperationName, WaMexNotificationUnknownEvent, WaMexOwnUsernameSyncEvent, WaMexTextStatusUpdateEvent, WaMexTextStatusUpdateHintEvent, WaMexUsernameDeleteEvent, WaMexUsernameSetEvent, WaMexUsernameUpdateHintEvent, WaOfflineResumeEvent, WaOutgoingMessageEvent, WaPictureEvent, WaPictureEventAction, WaPrivacyTokenUpdateEvent, WaReceiptStatus, WaRegistrationCodeEvent, WaSendMessageOptions, WaUnavailableMessageKind, WaVerifiedNameResult, WaAddonKind, WaNewsletterEventAction, WaNewsletterMessageUpdate, WaNewsletterPollVoteEntry, WaNewsletterReactionEntry } from './client/types';
8
8
  export type { WaAppStateMutationCoordinator, WaBroadcastListParticipant, WaSetBroadcastListInput, WaSetStatusPrivacyInput } from './client/coordinators/WaAppStateMutationCoordinator';
9
9
  export type { WaBotCoordinator, WaBotInfo, WaBotPosingAsProfessional, WaBotProfileCommand, WaBotProfilePrompt, WaBotProfileResult, WaBotPromptOptions, WaGetBotProfileOptions } from './client/coordinators/WaBotCoordinator';
@@ -11,6 +11,9 @@ export type { WaBroadcastListCoordinator, WaSendBroadcastListMessageInput } from
11
11
  export type { WaBusinessCoordinator, WaVerifiedNameBatchEntry } from './client/coordinators/WaBusinessCoordinator';
12
12
  export { downloadMediaMessage } from './client/media';
13
13
  export type { WaDownloadMediaMessageOptions, WaUploadMediaSource } from './client/media';
14
+ export { WaMediaCrypto } from './media/crypto/WaMediaCrypto';
15
+ export { WaMediaTransferClient } from './media/transfer/WaMediaTransferClient';
16
+ export type { MediaCryptoType, MediaKind, WaMediaConn, WaMediaDecryptReadableOptions, WaMediaDecryptionResult, WaMediaDerivedKeys, WaMediaEncryptionResult, WaMediaFileEncryptionResult, WaMediaReadableDecryptionResult, WaMediaReadableEncryptionResult, WaMediaTransferClientOptions } from './media/types';
14
17
  export type { WaEditBusinessProfileInput } from './transport/node/builders/business';
15
18
  export type { WaEmailCoordinator, WaEmailStatus, WaEmailVerifyCodeResult } from './client/coordinators/WaEmailCoordinator';
16
19
  export type { WaCommunityCreateOptions, WaCommunitySubGroup, WaCommunitySubGroupResult, WaCommunitySubGroupsResult, WaGroupCoordinator, WaGroupCreateOptions, WaGroupMetadata, WaGroupParticipant, WaLinkSubGroupsResult, WaMembershipRequest, WaUnlinkSubGroupsResult } from './client/coordinators/WaGroupCoordinator';
package/dist/index.js CHANGED
@@ -1,13 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WA_APP_STATE_COLLECTIONS = exports.WA_ACCOUNT_SYNC_PROTOCOLS = exports.toUserJid = exports.splitJid = exports.signalAddressKey = exports.parseSignalAddressFromJid = exports.parsePhoneJid = exports.parseJidFull = exports.normalizeRecipientJid = exports.normalizeDeviceJid = exports.isUserJid = exports.isStatusBroadcastJid = exports.isNewsletterJid = exports.isLidJid = exports.isHostedServer = exports.isHostedDeviceJid = exports.isHostedDeviceId = exports.isGroupOrBroadcastJid = exports.isGroupJid = exports.isBroadcastJid = exports.isBotJid = exports.getWaMediaHkdfInfo = exports.getWaCompanionPlatformId = exports.getLoginIdentity = exports.canonicalizeSignalServer = exports.canonicalizeSignalJid = exports.buildDeviceJid = exports.delay = exports.WaAuthMemoryStore = exports.createStore = exports.WA_VERSION = exports.createNoopLogger = exports.createPinoLogger = exports.PinoLogger = exports.ConsoleLogger = exports.fetchLatestWaMobileVersion = exports.fetchLatestWaWebVersion = exports.writeRandomPadMax16 = exports.unpadPkcs7 = exports.resolveMediaPayload = exports.getContextInfo = exports.unwrapMessage = exports.resolveMessageTarget = exports.resolveEncMediaType = exports.getContentType = exports.parseUsyncResultEnvelope = exports.createFileCompanionHostPersistence = exports.downloadMediaMessage = exports.defineWaClientPlugin = exports.WaClient = void 0;
4
- exports.proto = exports.WA_XMLNS = exports.WA_PRIVACY_VALUES = exports.WA_PRIVACY_TAGS = exports.WA_PRIVACY_SETTING_TO_CATEGORY = exports.WA_PRIVACY_DISALLOWED_LIST_CATEGORIES = exports.WA_PRIVACY_CATEGORY_TO_SETTING = exports.WA_PRIVACY_CATEGORIES = exports.WA_SUPPORTED_DIRTY_TYPES = exports.WA_STREAM_SIGNALING = exports.WA_SIGNALING = exports.WA_RETRYABLE_ACK_CODES = exports.WA_READY_STATES = exports.WA_PREVIEW_MEDIA_HKDF_INFO = exports.WA_PAIRING_KDF_INFO = exports.WA_NODE_TAGS = exports.WA_MEDIA_HKDF_INFO = exports.WA_MESSAGE_TYPES = exports.WA_MESSAGE_TAGS = exports.WA_LOGOUT_REASONS = exports.WA_IQ_TYPES = exports.WA_DISCONNECT_REASONS = exports.WA_DIRTY_TYPES = exports.WA_DIRTY_PROTOCOLS = exports.WA_DEFAULTS = exports.WA_COMPANION_PLATFORM_IDS = exports.WA_BUSINESS_HOURS_MODES = exports.WA_BUSINESS_HOURS_DAYS = exports.WA_BROWSERS = exports.WA_APP_STATE_SYNC_DATA_TYPE = exports.WA_APP_STATE_KEY_TYPES = exports.WA_APP_STATE_KDF_INFO = exports.WA_APP_STATE_ERROR_CODES = exports.WA_APP_STATE_COLLECTION_STATES = void 0;
3
+ exports.toUserJid = exports.splitJid = exports.signalAddressKey = exports.parseSignalAddressFromJid = exports.parsePhoneJid = exports.parseJidFull = exports.normalizeRecipientJid = exports.normalizeDeviceJid = exports.isUserJid = exports.isStatusBroadcastJid = exports.isNewsletterJid = exports.isLidJid = exports.isHostedServer = exports.isHostedDeviceJid = exports.isHostedDeviceId = exports.isGroupOrBroadcastJid = exports.isGroupJid = exports.isBroadcastJid = exports.isBotJid = exports.getWaMediaHkdfInfo = exports.getWaCompanionPlatformId = exports.getLoginIdentity = exports.canonicalizeSignalServer = exports.canonicalizeSignalJid = exports.buildDeviceJid = exports.delay = exports.WaAuthMemoryStore = exports.createStore = exports.WA_VERSION = exports.createNoopLogger = exports.createPinoLogger = exports.PinoLogger = exports.ConsoleLogger = exports.fetchLatestWaMobileVersion = exports.fetchLatestWaWebVersion = exports.writeRandomPadMax16 = exports.unpadPkcs7 = exports.resolveMediaPayload = exports.getContextInfo = exports.unwrapMessage = exports.resolveMessageTarget = exports.resolveEncMediaType = exports.getContentType = exports.parseUsyncResultEnvelope = exports.createFileCompanionHostPersistence = exports.WaMediaTransferClient = exports.WaMediaCrypto = exports.downloadMediaMessage = exports.defineWaClientPlugin = exports.WaClient = void 0;
4
+ exports.proto = exports.WA_XMLNS = exports.WA_PRIVACY_VALUES = exports.WA_PRIVACY_TAGS = exports.WA_PRIVACY_SETTING_TO_CATEGORY = exports.WA_PRIVACY_DISALLOWED_LIST_CATEGORIES = exports.WA_PRIVACY_CATEGORY_TO_SETTING = exports.WA_PRIVACY_CATEGORIES = exports.WA_SUPPORTED_DIRTY_TYPES = exports.WA_STREAM_SIGNALING = exports.WA_SIGNALING = exports.WA_RETRYABLE_ACK_CODES = exports.WA_READY_STATES = exports.WA_PREVIEW_MEDIA_HKDF_INFO = exports.WA_PAIRING_KDF_INFO = exports.WA_NODE_TAGS = exports.WA_MEDIA_HKDF_INFO = exports.WA_MESSAGE_TYPES = exports.WA_MESSAGE_TAGS = exports.WA_LOGOUT_REASONS = exports.WA_IQ_TYPES = exports.WA_DISCONNECT_REASONS = exports.WA_DIRTY_TYPES = exports.WA_DIRTY_PROTOCOLS = exports.WA_DEFAULTS = exports.WA_COMPANION_PLATFORM_IDS = exports.WA_BUSINESS_HOURS_MODES = exports.WA_BUSINESS_HOURS_DAYS = exports.WA_BROWSERS = exports.WA_APP_STATE_SYNC_DATA_TYPE = exports.WA_APP_STATE_KEY_TYPES = exports.WA_APP_STATE_KDF_INFO = exports.WA_APP_STATE_ERROR_CODES = exports.WA_APP_STATE_COLLECTION_STATES = exports.WA_APP_STATE_COLLECTIONS = exports.WA_ACCOUNT_SYNC_PROTOCOLS = void 0;
5
5
  var _client_1 = require("./client");
6
6
  Object.defineProperty(exports, "WaClient", { enumerable: true, get: function () { return _client_1.WaClient; } });
7
7
  var plugins_1 = require("./client/plugins");
8
8
  Object.defineProperty(exports, "defineWaClientPlugin", { enumerable: true, get: function () { return plugins_1.defineWaClientPlugin; } });
9
9
  var media_1 = require("./client/media");
10
10
  Object.defineProperty(exports, "downloadMediaMessage", { enumerable: true, get: function () { return media_1.downloadMediaMessage; } });
11
+ var WaMediaCrypto_1 = require("./media/crypto/WaMediaCrypto");
12
+ Object.defineProperty(exports, "WaMediaCrypto", { enumerable: true, get: function () { return WaMediaCrypto_1.WaMediaCrypto; } });
13
+ var WaMediaTransferClient_1 = require("./media/transfer/WaMediaTransferClient");
14
+ Object.defineProperty(exports, "WaMediaTransferClient", { enumerable: true, get: function () { return WaMediaTransferClient_1.WaMediaTransferClient; } });
11
15
  var companion_host_1 = require("./client/persistence/companion-host");
12
16
  Object.defineProperty(exports, "createFileCompanionHostPersistence", { enumerable: true, get: function () { return companion_host_1.createFileCompanionHostPersistence; } });
13
17
  var usync_1 = require("./transport/node/builders/usync");
@@ -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 {};