zapo-js 1.7.1 → 1.8.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 (101) hide show
  1. package/dist/client/WaClient.js +1 -0
  2. package/dist/client/WaClientFactory.d.ts +2 -0
  3. package/dist/client/WaClientFactory.js +26 -2
  4. package/dist/client/coordinators/WaIncomingNodeCoordinator.d.ts +1 -0
  5. package/dist/client/coordinators/WaIncomingNodeCoordinator.js +2 -1
  6. package/dist/client/coordinators/WaMessageCoordinator.d.ts +50 -0
  7. package/dist/client/coordinators/WaMessageCoordinator.js +39 -9
  8. package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +8 -0
  9. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +39 -2
  10. package/dist/client/coordinators/WaMobileCoordinator.js +1 -2
  11. package/dist/client/coordinators/WaPrivacyCoordinator.d.ts +18 -2
  12. package/dist/client/coordinators/WaPrivacyCoordinator.js +36 -3
  13. package/dist/client/coordinators/WaProfileCoordinator.d.ts +36 -4
  14. package/dist/client/coordinators/WaProfileCoordinator.js +80 -2
  15. package/dist/client/events/business.js +1 -1
  16. package/dist/client/events/incoming.d.ts +2 -0
  17. package/dist/client/events/incoming.js +7 -0
  18. package/dist/client/events/privacy.d.ts +9 -0
  19. package/dist/client/events/privacy.js +30 -26
  20. package/dist/client/events/username.d.ts +8 -0
  21. package/dist/client/events/username.js +26 -0
  22. package/dist/client/persistence/WriteBehindPersistence.js +1 -0
  23. package/dist/client/persistence/group-history.js +57 -45
  24. package/dist/client/persistence/history-blob.d.ts +21 -0
  25. package/dist/client/persistence/history-blob.js +41 -0
  26. package/dist/client/persistence/history-sync.d.ts +25 -30
  27. package/dist/client/persistence/history-sync.js +297 -267
  28. package/dist/client/persistence/mailbox.js +1 -1
  29. package/dist/client/types.d.ts +45 -0
  30. package/dist/esm/client/WaClient.js +1 -0
  31. package/dist/esm/client/WaClientFactory.js +26 -2
  32. package/dist/esm/client/coordinators/WaIncomingNodeCoordinator.js +2 -1
  33. package/dist/esm/client/coordinators/WaMessageCoordinator.js +40 -10
  34. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +39 -2
  35. package/dist/esm/client/coordinators/WaMobileCoordinator.js +2 -3
  36. package/dist/esm/client/coordinators/WaPrivacyCoordinator.js +36 -3
  37. package/dist/esm/client/coordinators/WaProfileCoordinator.js +81 -3
  38. package/dist/esm/client/events/business.js +2 -2
  39. package/dist/esm/client/events/incoming.js +7 -0
  40. package/dist/esm/client/events/privacy.js +30 -26
  41. package/dist/esm/client/events/username.js +23 -0
  42. package/dist/esm/client/persistence/WriteBehindPersistence.js +1 -0
  43. package/dist/esm/client/persistence/group-history.js +59 -47
  44. package/dist/esm/client/persistence/history-blob.js +40 -0
  45. package/dist/esm/client/persistence/history-sync.js +299 -268
  46. package/dist/esm/client/persistence/mailbox.js +1 -1
  47. package/dist/esm/media/crypto/media-retry.js +40 -0
  48. package/dist/esm/message/crypto/addon-crypto.js +114 -0
  49. package/dist/esm/message/encode/content.js +3 -1
  50. package/dist/esm/message/kinds/group-history.js +32 -0
  51. package/dist/esm/message/primitives/incoming.js +48 -6
  52. package/dist/esm/message/primitives/media-retry.js +198 -0
  53. package/dist/esm/protocol/constants.js +1 -0
  54. package/dist/esm/protocol/defaults.js +8 -0
  55. package/dist/esm/protocol/notification.js +1 -0
  56. package/dist/esm/protocol/username.js +127 -0
  57. package/dist/esm/store/memory/contact.store.js +1 -0
  58. package/dist/esm/transport/index.js +1 -1
  59. package/dist/esm/transport/node/builders/privacy.js +32 -9
  60. package/dist/esm/transport/node/builders/profile.js +34 -1
  61. package/dist/esm/transport/node/builders/usync.js +1 -1
  62. package/dist/esm/transport/node/helpers.js +9 -0
  63. package/dist/esm/transport/noise/WaMobileClientPayload.js +3 -1
  64. package/dist/esm/util/proto-stream.js +325 -0
  65. package/dist/index.d.ts +4 -3
  66. package/dist/media/crypto/media-retry.d.ts +29 -0
  67. package/dist/media/crypto/media-retry.js +45 -0
  68. package/dist/message/crypto/addon-crypto.d.ts +49 -0
  69. package/dist/message/crypto/addon-crypto.js +118 -0
  70. package/dist/message/encode/content.js +3 -1
  71. package/dist/message/kinds/group-history.d.ts +24 -0
  72. package/dist/message/kinds/group-history.js +34 -0
  73. package/dist/message/primitives/incoming.d.ts +2 -1
  74. package/dist/message/primitives/incoming.js +48 -6
  75. package/dist/message/primitives/media-retry.d.ts +43 -0
  76. package/dist/message/primitives/media-retry.js +202 -0
  77. package/dist/protocol/constants.d.ts +2 -0
  78. package/dist/protocol/constants.js +12 -2
  79. package/dist/protocol/defaults.d.ts +8 -0
  80. package/dist/protocol/defaults.js +8 -0
  81. package/dist/protocol/notification.d.ts +1 -0
  82. package/dist/protocol/notification.js +1 -0
  83. package/dist/protocol/username.d.ts +45 -0
  84. package/dist/protocol/username.js +137 -0
  85. package/dist/store/contracts/contact.store.d.ts +2 -0
  86. package/dist/store/memory/contact.store.js +1 -0
  87. package/dist/transport/index.d.ts +1 -1
  88. package/dist/transport/index.js +2 -1
  89. package/dist/transport/node/builders/privacy.d.ts +12 -7
  90. package/dist/transport/node/builders/privacy.js +32 -9
  91. package/dist/transport/node/builders/profile.d.ts +16 -0
  92. package/dist/transport/node/builders/profile.js +35 -0
  93. package/dist/transport/node/builders/usync.d.ts +2 -1
  94. package/dist/transport/node/builders/usync.js +1 -1
  95. package/dist/transport/node/helpers.d.ts +7 -0
  96. package/dist/transport/node/helpers.js +10 -0
  97. package/dist/transport/noise/WaMobileClientPayload.d.ts +1 -0
  98. package/dist/transport/noise/WaMobileClientPayload.js +3 -1
  99. package/dist/util/proto-stream.d.ts +48 -0
  100. package/dist/util/proto-stream.js +329 -0
  101. package/package.json +1 -1
@@ -61,6 +61,7 @@ class WaClientImpl extends node_events_1.EventEmitter {
61
61
  syncAppState: () => this.deps.chatCoordinator.sync().then(() => { }),
62
62
  syncAppStateWithOptions: (syncOptions) => this.deps.chatCoordinator.sync(syncOptions),
63
63
  emitEvent: this.emit.bind(this),
64
+ hasEventListener: (event) => this.listenerCount(event) > 0,
64
65
  handleIncomingMessageEvent: this.handleIncomingMessageEvent.bind(this),
65
66
  handleError: this.handleError.bind(this),
66
67
  handleIncomingFrame: this.handleIncomingFrame.bind(this),
@@ -60,6 +60,8 @@ interface WaClientBuildRuntime {
60
60
  readonly syncAppState: () => Promise<void>;
61
61
  readonly syncAppStateWithOptions: (options?: WaAppStateSyncOptions) => Promise<WaAppStateSyncResult>;
62
62
  readonly emitEvent: <K extends keyof WaClientEventMap>(event: K, ...args: Parameters<WaClientEventMap[K]>) => void;
63
+ /** Whether anything is subscribed, for events whose payload costs something to build. */
64
+ readonly hasEventListener: (event: keyof WaClientEventMap) => boolean;
63
65
  readonly handleIncomingMessageEvent: (event: WaIncomingMessageEvent) => Promise<void>;
64
66
  readonly handleError: (error: Error) => void;
65
67
  readonly handleIncomingFrame: (frame: Uint8Array) => Promise<void>;
@@ -32,6 +32,7 @@ const devices_1 = require("./events/devices");
32
32
  const dirty_1 = require("./events/dirty");
33
33
  const identity_1 = require("./events/identity");
34
34
  const privacy_token_1 = require("./events/privacy-token");
35
+ const username_1 = require("./events/username");
35
36
  const fanout_1 = require("./messaging/fanout");
36
37
  const group_metadata_1 = require("./messaging/group-metadata");
37
38
  const key_protocol_1 = require("./messaging/key-protocol");
@@ -41,6 +42,7 @@ const WaMediaTransferClient_1 = require("../media/transfer/WaMediaTransferClient
41
42
  const fetcher_1 = require("../message/addons/link-preview/fetcher");
42
43
  const newsletter_1 = require("../message/kinds/newsletter");
43
44
  const incoming_1 = require("../message/primitives/incoming");
45
+ const media_retry_1 = require("../message/primitives/media-retry");
44
46
  const peer_data_operation_1 = require("../message/primitives/peer-data-operation");
45
47
  const WaMessageClient_1 = require("../message/WaMessageClient");
46
48
  const constants_1 = require("../protocol/constants");
@@ -122,7 +124,7 @@ function resolveWaClientBase(options, logger) {
122
124
  };
123
125
  }
124
126
  function createIncomingNodeRuntime(input) {
125
- const { logger, emitEvent, authClient, connectionManager, nodeOrchestrator, streamControl, mediaMessageBuildOptions, retryCoordinator, messageDispatch, sendNode, syncAppState, disconnect, clearStoredCredentials, getCurrentCredentials, handleClientDirtyBits, incomingMessageAckOptions } = input;
127
+ const { logger, emitEvent, authClient, connectionManager, nodeOrchestrator, streamControl, mediaMessageBuildOptions, retryCoordinator, mediaRetryRequester, messageDispatch, sendNode, syncAppState, disconnect, clearStoredCredentials, getCurrentCredentials, handleClientDirtyBits, incomingMessageAckOptions } = input;
126
128
  return {
127
129
  handleStreamControlResult: streamControl.handleStreamControlResult,
128
130
  persistSuccessAttributes: (attributes) => authClient.persistSuccessAttributes(attributes, { countsAsLogin: true }),
@@ -141,6 +143,7 @@ function createIncomingNodeRuntime(input) {
141
143
  handleIncomingMessageNode: (node) => (0, incoming_1.handleIncomingMessageAck)(node, incomingMessageAckOptions),
142
144
  sendNode,
143
145
  handleIncomingRetryReceipt: (node) => retryCoordinator.handleIncomingRetryReceipt(node),
146
+ handleMediaRetryNotification: (node) => mediaRetryRequester.handleNotification(node),
144
147
  trackOutboundReceipt: (node) => retryCoordinator.trackOutboundReceipt(node),
145
148
  emitIncomingReceipt: (event) => emitEvent('receipt', event),
146
149
  emitIncomingPresence: (event) => emitEvent('presence', event),
@@ -382,6 +385,8 @@ function buildWaClientDependencies(input) {
382
385
  });
383
386
  const privacyCoordinator = (0, WaPrivacyCoordinator_1.createPrivacyCoordinator)({
384
387
  logger,
388
+ contactStore: sessionStore.contacts,
389
+ isUsernamePrivacyListIdentifierEnabled: () => abPropsCoordinator.getConfigValue('username_contact_privacy_setting_allow_uncontact_set_enable'),
385
390
  queryWithContext: runtime.queryWithContext,
386
391
  resolveUserJidPair: (userJid) => signalDeviceSync.resolveUserJidPair(userJid),
387
392
  getSelfLid: () => getCurrentCredentials()?.meLid ?? null,
@@ -514,9 +519,15 @@ function buildWaClientDependencies(input) {
514
519
  generateOutgoingMessageId: () => messageDispatch.generateOutgoingMessageId(),
515
520
  subscribeToProtocolMessage: runtime.subscribeProtocolMessage
516
521
  });
522
+ const mediaRetryRequester = (0, media_retry_1.createMediaRetryRequester)({
523
+ logger,
524
+ sendNode: (node) => nodeOrchestrator.sendNode(node, false),
525
+ getCurrentCredentials
526
+ });
517
527
  const messageCoordinator = new WaMessageCoordinator_1.WaMessageCoordinator({
518
528
  messageDispatch,
519
529
  mediaTransfer,
530
+ mediaRetry: mediaRetryRequester,
520
531
  mediaUploadOptions: mediaMessageBuildOptions,
521
532
  logger,
522
533
  messageStore: sessionStore.messages,
@@ -719,7 +730,15 @@ function buildWaClientDependencies(input) {
719
730
  },
720
731
  emitNewsletterMessageUpdate: (event) => runtime.emitEvent('newsletter_message_update', event),
721
732
  emitUnavailableMessage: (event) => runtime.emitEvent('message_unavailable', event),
722
- emitUnhandledStanza: (event) => runtime.emitEvent('debug_unhandled_stanza', event)
733
+ emitUnhandledStanza: (event) => runtime.emitEvent('debug_unhandled_stanza', event),
734
+ emitDecryptedPayload: (build) => {
735
+ // Built only if someone is subscribed: the payload carries a copy of
736
+ // the plaintext and a redacted node, and this runs per `<enc>`.
737
+ if (!runtime.hasEventListener('debug_decrypted_payload')) {
738
+ return;
739
+ }
740
+ runtime.emitEvent('debug_decrypted_payload', build());
741
+ }
723
742
  };
724
743
  const handleClientDirtyBits = (dirtyBits) => (0, dirty_1.handleDirtyBits)({
725
744
  logger,
@@ -752,6 +771,7 @@ function buildWaClientDependencies(input) {
752
771
  streamControl,
753
772
  mediaMessageBuildOptions,
754
773
  retryCoordinator,
774
+ mediaRetryRequester,
755
775
  messageDispatch,
756
776
  sendNode: runtime.sendNode,
757
777
  syncAppState: runtime.syncAppState,
@@ -928,6 +948,10 @@ function buildWaClientDependencies(input) {
928
948
  if ((0, helpers_1.findNodeChild)(node, constants_1.WA_NODE_TAGS.PRIVACY)) {
929
949
  privacyCoordinator.scheduleAccountSyncRefresh();
930
950
  }
951
+ const usernameEvent = (0, username_1.parseOwnUsernameNotification)(node);
952
+ if (usernameEvent) {
953
+ runtime.emitEvent('own_username', usernameEvent);
954
+ }
931
955
  return false;
932
956
  }
933
957
  });
@@ -22,6 +22,7 @@ interface WaIncomingNodeRuntime {
22
22
  readonly handleIncomingMessageNode: (node: BinaryNode) => Promise<boolean>;
23
23
  readonly sendNode: (node: BinaryNode) => Promise<void>;
24
24
  readonly handleIncomingRetryReceipt: (node: BinaryNode) => Promise<void>;
25
+ readonly handleMediaRetryNotification: (node: BinaryNode) => void;
25
26
  readonly trackOutboundReceipt: (node: BinaryNode) => Promise<void>;
26
27
  readonly emitIncomingReceipt: (event: WaIncomingReceiptEvent) => void;
27
28
  readonly emitIncomingPresence: (event: WaIncomingPresenceEvent) => void;
@@ -238,7 +238,8 @@ class WaIncomingNodeCoordinator {
238
238
  emitIncomingNotification: runtime.emitIncomingNotification,
239
239
  emitMexNotification: runtime.emitMexNotification,
240
240
  emitUnhandledStanza: runtime.emitUnhandledIncomingNode,
241
- syncAppState: runtime.syncAppState
241
+ syncAppState: runtime.syncAppState,
242
+ handleMediaRetryNotification: runtime.handleMediaRetryNotification
242
243
  })
243
244
  });
244
245
  this.registerIncomingHandler({
@@ -8,6 +8,7 @@ import type { WaDownloadMediaOptions, WaIncomingAddonEvent, WaIncomingMessageEve
8
8
  import type { Logger } from '../../infra/log/types';
9
9
  import type { WaMediaTransferClient } from '../../media/transfer/WaMediaTransferClient';
10
10
  import type { MediaKind } from '../../media/types';
11
+ import type { WaMediaRetryRequest, WaMediaRetryRequester, WaMediaRetryResult } from '../../message/primitives/media-retry';
11
12
  import type { PeerDataOperationRequester } from '../../message/primitives/peer-data-operation';
12
13
  import type { WaMessagePublishResult, WaSendMessageContent, WaSendReceiptEventOptions, WaSendReceiptOptions } from '../../message/types';
13
14
  import { type Proto } from '../../proto';
@@ -17,6 +18,8 @@ import { type WaMexQuerySocket } from '../../transport/node/mex/client';
17
18
  export interface WaMessageCoordinatorDeps {
18
19
  readonly messageDispatch: WaMessageDispatchCoordinator;
19
20
  readonly mediaTransfer: WaMediaTransferClient;
21
+ /** Backs {@link WaMessageCoordinator.requestMediaReupload}. */
22
+ readonly mediaRetry: WaMediaRetryRequester;
20
23
  /** Media upload wiring shared with the send path; backs {@link WaMessageCoordinator.upload}. */
21
24
  readonly mediaUploadOptions: WaMediaMessageOptions;
22
25
  readonly logger: Logger;
@@ -163,6 +166,7 @@ export interface WaMediaUploadResult extends UploadResult {
163
166
  export declare class WaMessageCoordinator {
164
167
  private readonly messageDispatch;
165
168
  private readonly mediaTransfer;
169
+ private readonly mediaRetry;
166
170
  private readonly mediaUploadOptions;
167
171
  private readonly logger;
168
172
  private readonly messageStore;
@@ -373,6 +377,52 @@ export declare class WaMessageCoordinator {
373
377
  * `options.maxBytes`.
374
378
  */
375
379
  downloadBytes(source: WaIncomingMessageEvent | Proto.IMessage, options?: WaDownloadMediaOptions): Promise<Uint8Array>;
380
+ /**
381
+ * Asks the sender's primary device to re-upload a message's media, for when
382
+ * the CDN answers `404`/`410` because the blob expired - typically old
383
+ * media surfaced by a history sync. Sends a `server-error` receipt carrying
384
+ * the sealed request and resolves once the server answers with a
385
+ * `mediaretry` notification, usually within a couple of seconds.
386
+ *
387
+ * On `result: 'success'` only the `directPath` changes - the media key,
388
+ * hashes, and length of the original message stay valid, so patch the path
389
+ * into the message and download again. A second call for a message already
390
+ * in flight joins the first request instead of sending another receipt.
391
+ *
392
+ * The other three `result` values are answers too, not thrown errors:
393
+ * `not_found` means the sender no longer holds the file and nothing can
394
+ * recover it, `decryption_error` that it could not open its own copy, and
395
+ * `general_error` anything else including a result code this version does
396
+ * not recognize.
397
+ *
398
+ * Requires the message's media key, so it only works for media you received
399
+ * or sent, never for a bare message id. Newsletter messages are rejected -
400
+ * channel media has no per-message key to seal the request with. Pass an
401
+ * explicit `WaMediaRetryRequest` instead of an event when you hold the
402
+ * message id, chat jid, and media key but not the decoded message.
403
+ *
404
+ * @throws when the message carries no downloadable media, is a newsletter
405
+ * message, the session is not paired, the stanza cannot be sent, or no
406
+ * notification arrives before `options.timeoutMs`. It does **not** throw on
407
+ * a `not_found` / `general_error` answer - check `result`.
408
+ * @example
409
+ * ```ts
410
+ * const image = event.message?.imageMessage
411
+ * try {
412
+ * return await client.message.downloadBytes(event)
413
+ * } catch {
414
+ * const retry = await client.message.requestMediaReupload(event)
415
+ * if (retry.result !== 'success') throw new Error(`reupload ${retry.result}`)
416
+ * return await client.message.downloadBytes({
417
+ * imageMessage: { ...image, directPath: retry.directPath }
418
+ * })
419
+ * }
420
+ * ```
421
+ */
422
+ requestMediaReupload(event: WaIncomingMessageEvent, options?: {
423
+ readonly timeoutMs?: number;
424
+ }): Promise<WaMediaRetryResult>;
425
+ requestMediaReupload(request: WaMediaRetryRequest): Promise<WaMediaRetryResult>;
376
426
  /**
377
427
  * Attempts to decrypt an addon payload (poll vote, reaction, edit, ...)
378
428
  * attached to `event` and, on success, emits a typed
@@ -9,6 +9,7 @@ const messages_1 = require("../messaging/messages");
9
9
  const constants_1 = require("../../media/constants");
10
10
  const addon_crypto_1 = require("../../message/crypto/addon-crypto");
11
11
  const content_1 = require("../../message/encode/content");
12
+ const media_payload_1 = require("../../message/encode/media-payload");
12
13
  const group_history_1 = require("../../message/kinds/group-history");
13
14
  const _proto_1 = require("../../proto");
14
15
  const jid_1 = require("../../protocol/jid");
@@ -79,6 +80,7 @@ class WaMessageCoordinator {
79
80
  constructor(deps) {
80
81
  this.messageDispatch = deps.messageDispatch;
81
82
  this.mediaTransfer = deps.mediaTransfer;
83
+ this.mediaRetry = deps.mediaRetry;
82
84
  this.mediaUploadOptions = deps.mediaUploadOptions;
83
85
  this.logger = deps.logger;
84
86
  this.messageStore = deps.messageStore;
@@ -486,6 +488,30 @@ class WaMessageCoordinator {
486
488
  const stream = await this.download(source, options);
487
489
  return (0, bytes_1.readAllBytes)(stream, { maxBytes: options.maxBytes });
488
490
  }
491
+ requestMediaReupload(source, options) {
492
+ if (!('key' in source)) {
493
+ return this.mediaRetry.request(source);
494
+ }
495
+ const key = source.key;
496
+ if (!key.id || !key.remoteJid) {
497
+ throw new Error('requestMediaReupload event is missing key.remoteJid or key.id');
498
+ }
499
+ if (key.isNewsletter) {
500
+ throw new Error('requestMediaReupload is not supported on newsletter messages');
501
+ }
502
+ const payload = (0, media_payload_1.resolveMediaPayload)(source.message);
503
+ if (!payload) {
504
+ throw new Error('message has no downloadable media');
505
+ }
506
+ return this.mediaRetry.request({
507
+ messageId: key.id,
508
+ chatJid: key.remoteJid,
509
+ mediaKey: payload.mediaKey,
510
+ fromMe: key.fromMe,
511
+ participant: key.participant,
512
+ timeoutMs: options?.timeoutMs
513
+ });
514
+ }
489
515
  /**
490
516
  * Attempts to decrypt an addon payload (poll vote, reaction, edit, ...)
491
517
  * attached to `event` and, on success, emits a typed
@@ -529,19 +555,23 @@ class WaMessageCoordinator {
529
555
  }
530
556
  return;
531
557
  }
532
- const parentMsgOriginalSender = parentEntry.senderJid;
533
- const modificationSender = event.key.participant ?? event.key.remoteJid;
534
- const plaintext = await (0, addon_crypto_1.decryptAddonPayload)({
558
+ const modificationSenderRaw = event.key.participant ?? event.key.remoteJid;
559
+ if (!modificationSenderRaw)
560
+ return;
561
+ const modificationSenderCandidates = (0, addon_crypto_1.collectUniqueUserJids)(event.key.fromMe ? event.rawNode.attrs.from : undefined, modificationSenderRaw, event.key.participantAlt, event.key.remoteJidAlt, event.rawNode.attrs.participant_pn, event.rawNode.attrs.sender_pn, event.rawNode.attrs.participant, event.key.isGroup ? undefined : event.rawNode.attrs.from);
562
+ const keyParentSender = (0, addon_crypto_1.resolveAddonParentSenderFromKey)(addon.targetMessageKey, event.key.isGroup);
563
+ const parentMsgOriginalSenderCandidates = (0, addon_crypto_1.collectUniqueUserJids)(keyParentSender, parentEntry.senderJid);
564
+ const senderPairs = (0, addon_crypto_1.buildAddonSenderPairs)({
565
+ parentCandidates: parentMsgOriginalSenderCandidates,
566
+ modificationCandidates: modificationSenderCandidates
567
+ });
568
+ const plaintext = await (0, addon_crypto_1.decryptAddonPayloadWithSenderFallback)({
535
569
  messageSecret: parentEntry.secret,
536
570
  stanzaId: targetMessageId,
537
- parentMsgOriginalSender,
538
- modificationSender,
571
+ senderPairs,
539
572
  modificationType: addon.modificationType,
540
573
  ciphertext: addon.encPayload,
541
- iv: addon.encIv,
542
- additionalData: (0, addon_crypto_1.shouldUseAddonAdditionalData)(addon.modificationType)
543
- ? (0, addon_crypto_1.buildAddonAdditionalData)(targetMessageId, modificationSender)
544
- : undefined
574
+ iv: addon.encIv
545
575
  });
546
576
  let decrypted = (0, addon_crypto_1.decodeAddonPlaintext)(addon.kind, plaintext);
547
577
  if (decrypted.kind === 'poll_vote' && decrypted.pollVote.selectedOptions) {
@@ -188,6 +188,14 @@ export declare class WaMessageDispatchCoordinator {
188
188
  private buildBotSidecarParticipant;
189
189
  private publishGroupSenderKeyMessage;
190
190
  private resolveGroupAddressingMode;
191
+ /**
192
+ * Parent-author JID stored with the outgoing message secret. Peers address
193
+ * us by LID in `pollCreationMessageKey` during the PN→LID migration, so a
194
+ * valid `meLid` wins. A malformed LID must not abort the send: fall back to
195
+ * a normalized `meJid`, then to the empty sender (same recoverable path as
196
+ * {@link resolveSenderForAddressingMode}).
197
+ */
198
+ private resolveOutgoingSecretSenderJid;
191
199
  private resolveSenderForAddressingMode;
192
200
  private encryptGroupDistributionParticipants;
193
201
  private publishDirectSignalMessageWithFanout;
@@ -374,9 +374,11 @@ class WaMessageDispatchCoordinator {
374
374
  rawSecret.length > 0 &&
375
375
  sendOptions.id &&
376
376
  (this.deps.persistAllMessageSecrets || (0, content_1.needsSecretPersistence)(messageWithSecret))) {
377
- const meJid = this.deps.getCurrentCredentials()?.meJid ?? '';
378
377
  void this.deps.messageSecretStore
379
- .set(sendOptions.id, { secret: rawSecret, senderJid: meJid })
378
+ .set(sendOptions.id, {
379
+ secret: rawSecret,
380
+ senderJid: this.resolveOutgoingSecretSenderJid()
381
+ })
380
382
  .catch((error) => {
381
383
  this.deps.logger.warn('failed to persist outgoing message secret', {
382
384
  id: sendOptions.id,
@@ -1072,6 +1074,41 @@ class WaMessageDispatchCoordinator {
1072
1074
  });
1073
1075
  return constants_1.WA_ADDRESSING_MODES.PN;
1074
1076
  }
1077
+ /**
1078
+ * Parent-author JID stored with the outgoing message secret. Peers address
1079
+ * us by LID in `pollCreationMessageKey` during the PN→LID migration, so a
1080
+ * valid `meLid` wins. A malformed LID must not abort the send: fall back to
1081
+ * a normalized `meJid`, then to the empty sender (same recoverable path as
1082
+ * {@link resolveSenderForAddressingMode}).
1083
+ */
1084
+ resolveOutgoingSecretSenderJid() {
1085
+ const credentials = this.deps.getCurrentCredentials();
1086
+ const meLid = credentials?.meLid;
1087
+ if (meLid && meLid.includes('@')) {
1088
+ try {
1089
+ return (0, jid_1.toUserJid)(meLid);
1090
+ }
1091
+ catch (error) {
1092
+ this.deps.logger.trace('ignoring malformed me lid jid', {
1093
+ meLid,
1094
+ message: (0, primitives_2.toError)(error).message
1095
+ });
1096
+ }
1097
+ }
1098
+ const meJid = credentials?.meJid;
1099
+ if (meJid && meJid.includes('@')) {
1100
+ try {
1101
+ return (0, jid_1.toUserJid)(meJid);
1102
+ }
1103
+ catch (error) {
1104
+ this.deps.logger.trace('ignoring malformed me jid', {
1105
+ meJid,
1106
+ message: (0, primitives_2.toError)(error).message
1107
+ });
1108
+ }
1109
+ }
1110
+ return '';
1111
+ }
1075
1112
  resolveSenderForAddressingMode(addressingMode, meJid) {
1076
1113
  if (addressingMode === constants_1.WA_ADDRESSING_MODES.LID) {
1077
1114
  const meLid = this.deps.getCurrentCredentials()?.meLid;
@@ -607,8 +607,7 @@ class WaMobileCoordinator {
607
607
  }
608
608
  }
609
609
  nodeBytes(parent, tag) {
610
- const content = (0, _transport_1.findNodeChild)(parent, tag)?.content;
611
- return content instanceof Uint8Array ? content : null;
610
+ return (0, _transport_1.getNodeBytesContent)((0, _transport_1.findNodeChild)(parent, tag)) ?? null;
612
611
  }
613
612
  requirePrimaryIdentityKeyPair() {
614
613
  if (!this.isMobilePrimary()) {
@@ -2,6 +2,7 @@ import { type WaBlocklistResult, type WaPrivacyDisallowedListResult, type WaPriv
2
2
  import type { Logger } from '../../infra/log/types';
3
3
  import { type WaPrivacyDisallowedListSettingName, type WaPrivacySettingName, type WaPrivacySettingValueMap } from '../../protocol/privacy';
4
4
  import type { SignalUserJidPair } from '../../signal/api/SignalDeviceSyncApi';
5
+ import type { WaContactStore } from '../../store/contracts/contact.store';
5
6
  import type { BinaryNode } from '../../transport/types';
6
7
  /** JIDs to add to / remove from a category's disallowed list. */
7
8
  export interface WaPrivacyDisallowedListInput {
@@ -47,7 +48,9 @@ export interface WaPrivacyCoordinator {
47
48
  /**
48
49
  * Fetches the per-category disallowed list (the JIDs explicitly excluded
49
50
  * while the category sits on `'contact_blacklist'`). Returns an empty
50
- * list when the category is on any other value.
51
+ * list when the category is on any other value. `entries` carries the same
52
+ * membership as `jids` plus each entry's handle, when the server
53
+ * identified it that way.
51
54
  */
52
55
  readonly getDisallowedList: (category: WaPrivacyDisallowedListSettingName) => Promise<WaPrivacyDisallowedListResult>;
53
56
  /**
@@ -83,7 +86,9 @@ export interface WaPrivacyCoordinator {
83
86
  /**
84
87
  * Returns the current account-wide blocklist. Blocks/unblocks performed
85
88
  * on another device are refetched through the same dirty-bit path as
86
- * {@link getPrivacySettings} and re-emitted as `blocklist`.
89
+ * {@link getPrivacySettings} and re-emitted as `blocklist`. `entries`
90
+ * carries the same membership as `jids` plus each entry's handle, when the
91
+ * server identified it that way.
87
92
  */
88
93
  readonly getBlocklist: () => Promise<WaBlocklistResult>;
89
94
  /**
@@ -98,6 +103,10 @@ export interface WaPrivacyCoordinator {
98
103
  * phone-number input is resolved to its LID first (device-list cache,
99
104
  * then a usync query). Non-migrated accounts fall back to the plain
100
105
  * phone-jid form.
106
+ *
107
+ * A migrated entry also carries one identifier attribute, read from the
108
+ * stored contact: the phone jid, the username handle, or the display name.
109
+ * Entries with none of them are sent as `unknown_identifier`.
101
110
  */
102
111
  readonly blockUser: (jid: string) => Promise<void>;
103
112
  /**
@@ -138,6 +147,13 @@ interface WaPrivacyCoordinatorOptions {
138
147
  */
139
148
  readonly getSelfLid: () => string | null;
140
149
  readonly emitPrivacy: (event: WaPrivacyAccountSyncResult) => void;
150
+ /** Source of the username / display-name identifiers a migrated write carries. */
151
+ readonly contactStore: WaContactStore;
152
+ /**
153
+ * Server-synced `username_contact_privacy_setting_allow_uncontact_set_enable`
154
+ * AB-prop, off by default - entries then fall back to `pn_jid`.
155
+ */
156
+ readonly isUsernamePrivacyListIdentifierEnabled: () => boolean;
141
157
  }
142
158
  /** Builds a {@link WaPrivacyCoordinator} backed by the given IQ query function. */
143
159
  export declare function createPrivacyCoordinator(options: WaPrivacyCoordinatorOptions): WaPrivacyCoordinatorRuntime;
@@ -10,6 +10,30 @@ const privacy_2 = require("../../protocol/privacy");
10
10
  const privacy_3 = require("../../transport/node/builders/privacy");
11
11
  const query_1 = require("../../transport/node/query");
12
12
  const primitives_1 = require("../../util/primitives");
13
+ /**
14
+ * Adds the identifier fields a migrated write may carry. Only the writes that
15
+ * consume them call it - an unblock addresses by jid alone, and a
16
+ * disallowed-list write with the AB gate closed falls back to `pn_jid`.
17
+ */
18
+ async function withContactIdentifiers(options, target) {
19
+ if (target.lidJid === null)
20
+ return target;
21
+ try {
22
+ const contact = await options.contactStore.getByJid(target.lidJid);
23
+ return {
24
+ ...target,
25
+ username: contact?.username ?? null,
26
+ displayName: contact?.displayName ?? null
27
+ };
28
+ }
29
+ catch (error) {
30
+ options.logger.debug('contact identifier lookup failed', {
31
+ jid: target.lidJid,
32
+ message: (0, primitives_1.toError)(error).message
33
+ });
34
+ return target;
35
+ }
36
+ }
13
37
  /**
14
38
  * Resolves a blocklist input into both addressing forms via
15
39
  * `resolveUserJidPair` (device-list cache first, usync fallback for phone
@@ -50,10 +74,19 @@ async function resolveDisallowedListEntries(options, input) {
50
74
  { action: privacy_2.WA_PRIVACY_LIST_ACTIONS.ADD, jids: input.add ?? [] },
51
75
  { action: privacy_2.WA_PRIVACY_LIST_ACTIONS.REMOVE, jids: input.remove ?? [] }
52
76
  ];
77
+ const usernameIdentifierAllowed = options.isUsernamePrivacyListIdentifierEnabled();
53
78
  for (const { action, jids } of actions) {
54
79
  for (const jid of jids) {
55
- const target = await resolveBlocklistTarget(options, jid);
56
- entries.push({ action, lidJid: target.lidJid, pnJid: target.pnJid });
80
+ const resolved = await resolveBlocklistTarget(options, jid);
81
+ const target = usernameIdentifierAllowed
82
+ ? await withContactIdentifiers(options, resolved)
83
+ : resolved;
84
+ entries.push({
85
+ action,
86
+ lidJid: target.lidJid,
87
+ pnJid: target.pnJid,
88
+ username: target.username ?? null
89
+ });
57
90
  }
58
91
  }
59
92
  if (entries.length === 0) {
@@ -192,7 +225,7 @@ function createPrivacyCoordinator(options) {
192
225
  return (0, privacy_1.parseBlocklist)(result);
193
226
  },
194
227
  blockUser: async (jid) => {
195
- const target = await resolveBlocklistTarget(options, jid);
228
+ const target = await withContactIdentifiers(options, await resolveBlocklistTarget(options, jid));
196
229
  const node = (0, privacy_3.buildBlocklistBlockIq)(target);
197
230
  const result = await queryWithContext('privacy.blockUser', node, undefined, {
198
231
  jid: target.lidJid ?? target.pnJid
@@ -54,6 +54,24 @@ export interface WaUsernameAvailabilityResult {
54
54
  readonly available: boolean;
55
55
  readonly suggestions: readonly string[];
56
56
  }
57
+ export type WaUsernameLookupResult = {
58
+ readonly status: 'found';
59
+ readonly jid: string;
60
+ readonly username: string | null;
61
+ readonly isBusiness: boolean;
62
+ readonly pnJid: string | null;
63
+ } | {
64
+ readonly status: 'key-required';
65
+ readonly username: string | null;
66
+ } | {
67
+ readonly status: 'not-found';
68
+ };
69
+ export interface WaResolveUsernameInput {
70
+ /** A leading `@` and a `:1234` key suffix are accepted. */
71
+ readonly username: string;
72
+ /** Overrides a key parsed out of {@link username}. */
73
+ readonly usernameKey?: string;
74
+ }
57
75
  /**
58
76
  * Coordinates own/peer profile queries and mutations: picture, status, text
59
77
  * status, username, disappearing mode, and LID lookup. Accessed via
@@ -109,13 +127,23 @@ export interface WaProfileCoordinator {
109
127
  readonly setTextStatus: (input: WaSetTextStatusInput) => Promise<void>;
110
128
  /** Batched fetch of username per JID. */
111
129
  readonly getUsernames: (jids: readonly string[]) => Promise<readonly WaUsernameResult[]>;
130
+ /**
131
+ * Resolves a username handle to the account's LID, so a message can be
132
+ * addressed to `@handle`. `'key-required'` means the server withheld the
133
+ * JID - repeat the call with `usernameKey`.
134
+ *
135
+ * @throws when the handle fails local validation, before any round-trip.
136
+ */
137
+ readonly resolveUsername: (input: WaResolveUsernameInput) => Promise<WaUsernameLookupResult>;
112
138
  /** Fetches the current account's username record (value, state, recovery pin). */
113
139
  readonly getOwnUsername: () => Promise<WaOwnUsernameResult>;
114
140
  /**
115
141
  * Reserves/sets a username on the current account. Returns `true` only
116
- * when the server reports `'SUCCESS'`; on any other outcome (taken,
117
- * invalid, rate-limited) it returns `false` without throwing - check the
118
- * value and consult {@link checkUsernameAvailability} for suggestions.
142
+ * when the server reports `'SUCCESS'`; on any other server outcome (taken,
143
+ * rate-limited) it returns `false` without throwing - check the value and
144
+ * consult {@link checkUsernameAvailability} for suggestions.
145
+ *
146
+ * @throws when the handle fails local validation, before any round-trip.
119
147
  */
120
148
  readonly setUsername: (input: WaSetUsernameInput) => Promise<boolean>;
121
149
  /** Deletes the current account's username. Returns `true` on success. */
@@ -124,7 +152,11 @@ export interface WaProfileCoordinator {
124
152
  readonly getAboutStatus: (jid: string) => Promise<string | null>;
125
153
  /** Checks whether a username is available and returns server suggestions. */
126
154
  readonly checkUsernameAvailability: (username: string) => Promise<WaUsernameAvailabilityResult>;
127
- /** Sets the username recovery PIN. Returns `true` on success. */
155
+ /**
156
+ * Sets the username recovery PIN. Returns `true` on success.
157
+ *
158
+ * @throws when `pin` is not exactly 4 digits, before any round-trip.
159
+ */
128
160
  readonly setUsernameKey: (pin: string) => Promise<boolean>;
129
161
  /** Resolves LIDs for a list of phone numbers (handles normalization). */
130
162
  readonly getLidsByPhoneNumbers: (phoneNumbers: readonly string[]) => Promise<readonly SignalLidSyncResult[]>;