zapo-js 1.7.0 → 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 (183) hide show
  1. package/README.md +26 -12
  2. package/dist/appstate/crypto/WaAppStateCrypto.js +4 -6
  3. package/dist/auth/WaAuthClient.d.ts +14 -1
  4. package/dist/auth/WaAuthClient.js +22 -1
  5. package/dist/auth/credentials-flow.d.ts +3 -1
  6. package/dist/auth/credentials-flow.js +3 -0
  7. package/dist/auth/pairing/WaPairingFlow.d.ts +5 -0
  8. package/dist/auth/pairing/WaPairingFlow.js +66 -14
  9. package/dist/auth/types.d.ts +13 -0
  10. package/dist/client/WaClient.js +1 -0
  11. package/dist/client/WaClientFactory.d.ts +2 -0
  12. package/dist/client/WaClientFactory.js +29 -3
  13. package/dist/client/coordinators/WaIncomingNodeCoordinator.d.ts +1 -0
  14. package/dist/client/coordinators/WaIncomingNodeCoordinator.js +2 -1
  15. package/dist/client/coordinators/WaMessageCoordinator.d.ts +50 -0
  16. package/dist/client/coordinators/WaMessageCoordinator.js +39 -9
  17. package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +8 -0
  18. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +74 -29
  19. package/dist/client/coordinators/WaMobileCoordinator.js +1 -2
  20. package/dist/client/coordinators/WaPrivacyCoordinator.d.ts +18 -2
  21. package/dist/client/coordinators/WaPrivacyCoordinator.js +36 -3
  22. package/dist/client/coordinators/WaProfileCoordinator.d.ts +36 -4
  23. package/dist/client/coordinators/WaProfileCoordinator.js +80 -2
  24. package/dist/client/events/business.js +1 -1
  25. package/dist/client/events/incoming.d.ts +2 -0
  26. package/dist/client/events/incoming.js +7 -0
  27. package/dist/client/events/privacy.d.ts +9 -0
  28. package/dist/client/events/privacy.js +30 -26
  29. package/dist/client/events/username.d.ts +8 -0
  30. package/dist/client/events/username.js +26 -0
  31. package/dist/client/messaging/messages.js +16 -12
  32. package/dist/client/newsletter/discovery.js +4 -2
  33. package/dist/client/persistence/WriteBehindPersistence.js +1 -0
  34. package/dist/client/persistence/group-history.js +57 -45
  35. package/dist/client/persistence/history-blob.d.ts +21 -0
  36. package/dist/client/persistence/history-blob.js +41 -0
  37. package/dist/client/persistence/history-sync.d.ts +26 -13
  38. package/dist/client/persistence/history-sync.js +307 -185
  39. package/dist/client/persistence/mailbox.js +1 -1
  40. package/dist/client/plugins/types.d.ts +10 -2
  41. package/dist/client/types.d.ts +45 -0
  42. package/dist/crypto/core/index.d.ts +1 -1
  43. package/dist/crypto/core/index.js +2 -1
  44. package/dist/crypto/core/primitives.d.ts +19 -0
  45. package/dist/crypto/core/primitives.js +48 -0
  46. package/dist/crypto/core/xeddsa.js +19 -4
  47. package/dist/crypto/curves/X25519.js +16 -2
  48. package/dist/esm/appstate/crypto/WaAppStateCrypto.js +4 -6
  49. package/dist/esm/auth/WaAuthClient.js +22 -1
  50. package/dist/esm/auth/credentials-flow.js +3 -0
  51. package/dist/esm/auth/pairing/WaPairingFlow.js +67 -15
  52. package/dist/esm/client/WaClient.js +1 -0
  53. package/dist/esm/client/WaClientFactory.js +29 -3
  54. package/dist/esm/client/coordinators/WaIncomingNodeCoordinator.js +2 -1
  55. package/dist/esm/client/coordinators/WaMessageCoordinator.js +40 -10
  56. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +76 -31
  57. package/dist/esm/client/coordinators/WaMobileCoordinator.js +2 -3
  58. package/dist/esm/client/coordinators/WaPrivacyCoordinator.js +36 -3
  59. package/dist/esm/client/coordinators/WaProfileCoordinator.js +81 -3
  60. package/dist/esm/client/events/business.js +2 -2
  61. package/dist/esm/client/events/incoming.js +7 -0
  62. package/dist/esm/client/events/privacy.js +30 -26
  63. package/dist/esm/client/events/username.js +23 -0
  64. package/dist/esm/client/messaging/messages.js +16 -12
  65. package/dist/esm/client/newsletter/discovery.js +4 -2
  66. package/dist/esm/client/persistence/WriteBehindPersistence.js +1 -0
  67. package/dist/esm/client/persistence/group-history.js +59 -47
  68. package/dist/esm/client/persistence/history-blob.js +40 -0
  69. package/dist/esm/client/persistence/history-sync.js +308 -187
  70. package/dist/esm/client/persistence/mailbox.js +1 -1
  71. package/dist/esm/crypto/core/index.js +1 -1
  72. package/dist/esm/crypto/core/primitives.js +47 -0
  73. package/dist/esm/crypto/core/xeddsa.js +19 -4
  74. package/dist/esm/crypto/curves/X25519.js +16 -2
  75. package/dist/esm/media/crypto/WaMediaCrypto.js +38 -19
  76. package/dist/esm/media/crypto/media-retry.js +40 -0
  77. package/dist/esm/message/crypto/addon-crypto.js +114 -0
  78. package/dist/esm/message/crypto/reporting-token.js +38 -216
  79. package/dist/esm/message/encode/content.js +56 -9
  80. package/dist/esm/message/kinds/group-history.js +32 -0
  81. package/dist/esm/message/primitives/incoming.js +62 -13
  82. package/dist/esm/message/primitives/media-retry.js +198 -0
  83. package/dist/esm/protocol/constants.js +1 -0
  84. package/dist/esm/protocol/defaults.js +9 -0
  85. package/dist/esm/protocol/device-identity.js +10 -5
  86. package/dist/esm/protocol/jid.js +83 -36
  87. package/dist/esm/protocol/notification.js +1 -0
  88. package/dist/esm/protocol/username.js +127 -0
  89. package/dist/esm/signal/group/SenderKeyChain.js +1 -2
  90. package/dist/esm/signal/group/SenderKeyManager.js +1 -7
  91. package/dist/esm/signal/session/SignalProtocol.js +22 -21
  92. package/dist/esm/signal/session/SignalRatchet.js +20 -17
  93. package/dist/esm/signal/session/encoding.js +25 -3
  94. package/dist/esm/signal/session/resolver.js +11 -5
  95. package/dist/esm/store/memory/contact.store.js +1 -0
  96. package/dist/esm/transport/binary/constants.js +19 -0
  97. package/dist/esm/transport/binary/decoder.js +17 -19
  98. package/dist/esm/transport/binary/encoder.js +61 -18
  99. package/dist/esm/transport/index.js +1 -1
  100. package/dist/esm/transport/node/builders/global.js +20 -0
  101. package/dist/esm/transport/node/builders/message.js +32 -15
  102. package/dist/esm/transport/node/builders/privacy.js +32 -9
  103. package/dist/esm/transport/node/builders/profile.js +34 -1
  104. package/dist/esm/transport/node/builders/usync.js +1 -1
  105. package/dist/esm/transport/node/helpers.js +9 -0
  106. package/dist/esm/transport/noise/WaClientPayload.js +43 -22
  107. package/dist/esm/transport/noise/WaFrameCodec.js +14 -4
  108. package/dist/esm/transport/noise/WaMobileClientPayload.js +3 -1
  109. package/dist/esm/util/proto-stream.js +325 -0
  110. package/dist/esm/util/protoscan.js +181 -0
  111. package/dist/esm/util/runtime.js +34 -0
  112. package/dist/index.d.ts +4 -3
  113. package/dist/media/crypto/WaMediaCrypto.js +36 -17
  114. package/dist/media/crypto/media-retry.d.ts +29 -0
  115. package/dist/media/crypto/media-retry.js +45 -0
  116. package/dist/message/crypto/addon-crypto.d.ts +49 -0
  117. package/dist/message/crypto/addon-crypto.js +118 -0
  118. package/dist/message/crypto/reporting-token.d.ts +3 -1
  119. package/dist/message/crypto/reporting-token.js +38 -216
  120. package/dist/message/encode/content.d.ts +17 -1
  121. package/dist/message/encode/content.js +57 -9
  122. package/dist/message/kinds/group-history.d.ts +24 -0
  123. package/dist/message/kinds/group-history.js +34 -0
  124. package/dist/message/primitives/incoming.d.ts +2 -1
  125. package/dist/message/primitives/incoming.js +62 -13
  126. package/dist/message/primitives/media-retry.d.ts +43 -0
  127. package/dist/message/primitives/media-retry.js +202 -0
  128. package/dist/message/types.d.ts +10 -0
  129. package/dist/protocol/constants.d.ts +2 -0
  130. package/dist/protocol/constants.js +12 -2
  131. package/dist/protocol/defaults.d.ts +9 -0
  132. package/dist/protocol/defaults.js +9 -0
  133. package/dist/protocol/device-identity.d.ts +14 -4
  134. package/dist/protocol/device-identity.js +9 -4
  135. package/dist/protocol/jid.d.ts +4 -1
  136. package/dist/protocol/jid.js +83 -36
  137. package/dist/protocol/notification.d.ts +1 -0
  138. package/dist/protocol/notification.js +1 -0
  139. package/dist/protocol/username.d.ts +45 -0
  140. package/dist/protocol/username.js +137 -0
  141. package/dist/signal/group/SenderKeyChain.d.ts +1 -1
  142. package/dist/signal/group/SenderKeyChain.js +1 -2
  143. package/dist/signal/group/SenderKeyManager.js +1 -7
  144. package/dist/signal/session/SignalProtocol.d.ts +13 -0
  145. package/dist/signal/session/SignalProtocol.js +22 -21
  146. package/dist/signal/session/SignalRatchet.d.ts +4 -4
  147. package/dist/signal/session/SignalRatchet.js +19 -16
  148. package/dist/signal/session/SignalSession.d.ts +8 -1
  149. package/dist/signal/session/encoding.d.ts +5 -0
  150. package/dist/signal/session/encoding.js +25 -3
  151. package/dist/signal/session/resolver.js +11 -5
  152. package/dist/store/contracts/contact.store.d.ts +2 -0
  153. package/dist/store/memory/contact.store.js +1 -0
  154. package/dist/transport/binary/constants.d.ts +8 -0
  155. package/dist/transport/binary/constants.js +20 -1
  156. package/dist/transport/binary/decoder.js +17 -19
  157. package/dist/transport/binary/encoder.js +60 -17
  158. package/dist/transport/index.d.ts +1 -1
  159. package/dist/transport/index.js +2 -1
  160. package/dist/transport/node/builders/global.d.ts +8 -0
  161. package/dist/transport/node/builders/global.js +21 -0
  162. package/dist/transport/node/builders/message.js +32 -15
  163. package/dist/transport/node/builders/privacy.d.ts +12 -7
  164. package/dist/transport/node/builders/privacy.js +32 -9
  165. package/dist/transport/node/builders/profile.d.ts +16 -0
  166. package/dist/transport/node/builders/profile.js +35 -0
  167. package/dist/transport/node/builders/usync.d.ts +2 -1
  168. package/dist/transport/node/builders/usync.js +1 -1
  169. package/dist/transport/node/helpers.d.ts +7 -0
  170. package/dist/transport/node/helpers.js +10 -0
  171. package/dist/transport/noise/WaClientPayload.js +43 -22
  172. package/dist/transport/noise/WaFrameCodec.d.ts +1 -0
  173. package/dist/transport/noise/WaFrameCodec.js +14 -4
  174. package/dist/transport/noise/WaMobileClientPayload.d.ts +1 -0
  175. package/dist/transport/noise/WaMobileClientPayload.js +3 -1
  176. package/dist/transport/noise/types.d.ts +17 -0
  177. package/dist/util/proto-stream.d.ts +48 -0
  178. package/dist/util/proto-stream.js +329 -0
  179. package/dist/util/protoscan.d.ts +33 -0
  180. package/dist/util/protoscan.js +186 -0
  181. package/dist/util/runtime.d.ts +12 -0
  182. package/dist/util/runtime.js +35 -0
  183. package/package.json +1 -1
@@ -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,
@@ -398,17 +400,16 @@ class WaMessageDispatchCoordinator {
398
400
  ]);
399
401
  const messageWithIcdc = (0, icdc_1.injectDeviceListMetadata)(messageWithSecret, senderIcdc, recipientIcdc);
400
402
  const plaintext = await (0, padding_1.writeRandomPadMax16)(_proto_1.proto.Message.encode(messageWithIcdc).finish());
401
- const buttonAddonKind = (0, content_1.resolveButtonAddonKind)(messageWithIcdc);
403
+ const outboundAttrs = (0, content_1.resolveOutboundMessageAttrs)(messageWithIcdc);
404
+ const buttonAddonKind = outboundAttrs.buttonAddonKind;
402
405
  const buttonAddonNode = buttonAddonKind ? (0, message_1.buildButtonAddonNode)(buttonAddonKind) : undefined;
403
406
  // when a <biz> companion is attached the stanza must advertise type=text and
404
407
  // omit enc.mediatype; sending type=media + mediatype=list/button alongside the
405
408
  // companion is rejected by the server as SMAX_INVALID (479).
406
- const type = buttonAddonKind ? 'text' : (0, content_1.resolveMessageTypeAttr)(messageWithIcdc);
407
- const edit = (0, content_1.resolveEditAttr)(messageWithIcdc) ?? undefined;
408
- const mediatype = buttonAddonKind
409
- ? undefined
410
- : ((0, content_1.resolveEncMediaType)(messageWithIcdc) ?? undefined);
411
- const metaAttrs = (0, content_1.resolveMetaAttrs)(messageWithIcdc);
409
+ const type = buttonAddonKind ? 'text' : outboundAttrs.typeAttr;
410
+ const edit = outboundAttrs.edit ?? undefined;
411
+ const mediatype = buttonAddonKind ? undefined : (outboundAttrs.mediatype ?? undefined);
412
+ const metaAttrs = outboundAttrs.metaAttrs;
412
413
  const metaNode = metaAttrs ? (0, message_1.buildMetaNode)(metaAttrs) : undefined;
413
414
  const customNodes = [];
414
415
  if (metaNode)
@@ -420,7 +421,7 @@ class WaMessageDispatchCoordinator {
420
421
  customNodes.push(node);
421
422
  }
422
423
  }
423
- const decryptFail = (0, content_1.resolveDecryptFailAttr)(messageWithIcdc);
424
+ const decryptFail = outboundAttrs.decryptFail;
424
425
  const envelope = {
425
426
  message: messageWithIcdc,
426
427
  plaintext,
@@ -540,7 +541,7 @@ class WaMessageDispatchCoordinator {
540
541
  replayStatusSetting: statusSetting,
541
542
  // Bare `<to jid=user>` ack hints route the skmsg through primary
542
543
  // devices that already hold the sender key.
543
- customize: async ({ fanoutDeviceJids, distributionParticipants, messageWithSecret, sendOptions }) => {
544
+ customize: ({ fanoutDeviceJids, distributionParticipants, messageWithSecret, sendOptions }) => {
544
545
  const distributedAddressKeys = new Set();
545
546
  for (let i = 0; i < distributionParticipants.length; i += 1) {
546
547
  distributedAddressKeys.add((0, jid_1.signalAddressKey)(distributionParticipants[i].address));
@@ -560,7 +561,7 @@ class WaMessageDispatchCoordinator {
560
561
  seenAck.add(userJid);
561
562
  ackHints.push({ jid: userJid });
562
563
  }
563
- const reportingArtifacts = await this.tryBuildReportingTokenArtifacts({
564
+ const reportingArtifacts = this.tryBuildReportingTokenArtifacts({
564
565
  message: messageWithSecret,
565
566
  stanzaId: sendOptions.id,
566
567
  senderUserJid: (0, jid_1.toUserJid)(senderJid),
@@ -638,14 +639,15 @@ class WaMessageDispatchCoordinator {
638
639
  participants.push(entry);
639
640
  }
640
641
  }
642
+ const outboundAttrs = (0, content_1.resolveOutboundMessageAttrs)(messageWithSecret);
641
643
  const messageNode = (0, message_1.buildGroupSenderKeyMessageNode)({
642
644
  to: input.groupJid,
643
- type: (0, content_1.resolveMessageTypeAttr)(messageWithSecret),
645
+ type: outboundAttrs.typeAttr,
644
646
  id: sendOptions.id,
645
647
  phash: extras.phash,
646
- edit: (0, content_1.resolveEditAttr)(messageWithSecret) ?? undefined,
647
- mediatype: (0, content_1.resolveEncMediaType)(messageWithSecret) ?? undefined,
648
- decryptFail: (0, content_1.resolveDecryptFailAttr)(messageWithSecret),
648
+ edit: outboundAttrs.edit ?? undefined,
649
+ mediatype: outboundAttrs.mediatype ?? undefined,
650
+ decryptFail: outboundAttrs.decryptFail,
649
651
  groupCiphertext: groupCiphertext.ciphertext,
650
652
  participants,
651
653
  deviceIdentity: shouldAttachDeviceIdentity
@@ -819,8 +821,9 @@ class WaMessageDispatchCoordinator {
819
821
  break;
820
822
  }
821
823
  }
822
- const reportingArtifacts = await this.tryBuildReportingTokenArtifacts({
824
+ const reportingArtifacts = this.tryBuildReportingTokenArtifacts({
823
825
  message,
826
+ messageBytes: (0, padding_1.unpadPkcs7)(plaintext),
824
827
  stanzaId: sendOptions.id,
825
828
  senderUserJid: (0, jid_1.toUserJid)(senderJid),
826
829
  remoteJid: groupJid,
@@ -975,8 +978,9 @@ class WaMessageDispatchCoordinator {
975
978
  phashTargets[phashTargetCount] = senderJid;
976
979
  phashTargets.length = phashTargetCount + 1;
977
980
  const localPhash = (0, phash_1.computePhashV2)(phashTargets);
978
- const reportingArtifacts = await this.tryBuildReportingTokenArtifacts({
981
+ const reportingArtifacts = this.tryBuildReportingTokenArtifacts({
979
982
  message,
983
+ messageBytes: (0, padding_1.unpadPkcs7)(plaintext),
980
984
  stanzaId: sendOptions.id,
981
985
  senderUserJid: (0, jid_1.toUserJid)(senderJid),
982
986
  remoteJid: groupJid,
@@ -1070,6 +1074,41 @@ class WaMessageDispatchCoordinator {
1070
1074
  });
1071
1075
  return constants_1.WA_ADDRESSING_MODES.PN;
1072
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
+ }
1073
1112
  resolveSenderForAddressingMode(addressingMode, meJid) {
1074
1113
  if (addressingMode === constants_1.WA_ADDRESSING_MODES.LID) {
1075
1114
  const meLid = this.deps.getCurrentCredentials()?.meLid;
@@ -1088,9 +1127,6 @@ class WaMessageDispatchCoordinator {
1088
1127
  return (0, jid_1.normalizeDeviceJid)(meJid);
1089
1128
  }
1090
1129
  async encryptGroupDistributionParticipants(groupJid, senderKeyId, senderKeyDistributionMessage, participantUserJids) {
1091
- const distributionPayload = await (0, padding_1.writeRandomPadMax16)(_proto_1.proto.Message.encode({
1092
- senderKeyDistributionMessage
1093
- }).finish());
1094
1130
  const fanoutDeviceJids = await this.deps.fanoutResolver.resolveGroupParticipantDeviceJids(participantUserJids);
1095
1131
  if (fanoutDeviceJids.length === 0) {
1096
1132
  return {
@@ -1098,13 +1134,9 @@ class WaMessageDispatchCoordinator {
1098
1134
  distributionParticipants: []
1099
1135
  };
1100
1136
  }
1101
- const fanoutTargetsByAddressKey = new Map();
1102
1137
  const fanoutAddresses = new Array(fanoutDeviceJids.length);
1103
1138
  for (let index = 0; index < fanoutDeviceJids.length; index += 1) {
1104
- const jid = fanoutDeviceJids[index];
1105
- const address = (0, jid_1.parseSignalAddressFromJid)(jid);
1106
- fanoutAddresses[index] = address;
1107
- fanoutTargetsByAddressKey.set((0, jid_1.signalAddressKey)(address), { jid, address });
1139
+ fanoutAddresses[index] = (0, jid_1.parseSignalAddressFromJid)(fanoutDeviceJids[index]);
1108
1140
  }
1109
1141
  const pendingAddresses = await this.deps.senderKeyManager.filterParticipantsNeedingDistribution(groupJid, senderKeyId, fanoutAddresses);
1110
1142
  if (pendingAddresses.length === 0) {
@@ -1113,6 +1145,14 @@ class WaMessageDispatchCoordinator {
1113
1145
  distributionParticipants: []
1114
1146
  };
1115
1147
  }
1148
+ const fanoutTargetsByAddressKey = new Map();
1149
+ for (let index = 0; index < fanoutAddresses.length; index += 1) {
1150
+ const address = fanoutAddresses[index];
1151
+ fanoutTargetsByAddressKey.set((0, jid_1.signalAddressKey)(address), {
1152
+ jid: fanoutDeviceJids[index],
1153
+ address
1154
+ });
1155
+ }
1116
1156
  const pendingAddressKeys = new Set();
1117
1157
  const pendingTargets = [];
1118
1158
  for (let index = 0; index < pendingAddresses.length; index += 1) {
@@ -1172,6 +1212,9 @@ class WaMessageDispatchCoordinator {
1172
1212
  distributionParticipants: []
1173
1213
  };
1174
1214
  }
1215
+ const distributionPayload = await (0, padding_1.writeRandomPadMax16)(_proto_1.proto.Message.encode({
1216
+ senderKeyDistributionMessage
1217
+ }).finish());
1175
1218
  const distributionEncryptRequests = new Array(availableTargets.length);
1176
1219
  for (let index = 0; index < availableTargets.length; index += 1) {
1177
1220
  const target = availableTargets[index];
@@ -1324,8 +1367,9 @@ class WaMessageDispatchCoordinator {
1324
1367
  const deviceIdentity = shouldAttachDeviceIdentity
1325
1368
  ? this.getEncodedSignedDeviceIdentity()
1326
1369
  : undefined;
1327
- const reportingArtifacts = await this.tryBuildReportingTokenArtifacts({
1370
+ const reportingArtifacts = this.tryBuildReportingTokenArtifacts({
1328
1371
  message,
1372
+ messageBytes: (0, padding_1.unpadPkcs7)(plaintext),
1329
1373
  stanzaId: sendOptions.id,
1330
1374
  senderUserJid: meUserJid,
1331
1375
  remoteJid: recipientUserJid,
@@ -1428,13 +1472,14 @@ class WaMessageDispatchCoordinator {
1428
1472
  return `3EB0${(0, bytes_1.bytesToHex)(await (0, _crypto_1.randomBytesAsync)(8)).toUpperCase()}`;
1429
1473
  }
1430
1474
  }
1431
- async tryBuildReportingTokenArtifacts(input) {
1475
+ tryBuildReportingTokenArtifacts(input) {
1432
1476
  if (!input.stanzaId) {
1433
1477
  return null;
1434
1478
  }
1435
1479
  try {
1436
- return await (0, reporting_token_1.buildReportingTokenArtifacts)({
1480
+ return (0, reporting_token_1.buildReportingTokenArtifacts)({
1437
1481
  message: input.message,
1482
+ messageBytes: input.messageBytes,
1438
1483
  stanzaId: input.stanzaId,
1439
1484
  senderUserJid: input.senderUserJid,
1440
1485
  remoteJid: input.remoteJid
@@ -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[]>;