zapo-js 1.1.3 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -717,7 +717,12 @@ class WaAppStateSyncClient {
717
717
  if (keyData !== null) {
718
718
  const expectedSnapshotMac = await this.crypto.generateSnapshotMac(keyData, ltHash, version, collection);
719
719
  if (!(0, bytes_1.uint8TimingSafeEqual)(expectedSnapshotMac, snapshot.mac)) {
720
- throw new Error(`snapshot MAC mismatch for ${collection}`);
720
+ // Poisoned server-side snapshot (MAC unverifiable by any client):
721
+ // keep partial state instead of throwing, which would loop refetch forever.
722
+ this.logger.warn('snapshot LT-hash verification failed, continuing with partial state', {
723
+ collection,
724
+ version
725
+ });
721
726
  }
722
727
  }
723
728
  this.setCollectionState(collection, version, ltHash, indexValueMap);
@@ -12,7 +12,6 @@ const _proto_1 = require("../proto");
12
12
  const constants_1 = require("../protocol/constants");
13
13
  const jid_1 = require("../protocol/jid");
14
14
  const stream_1 = require("../protocol/stream");
15
- const noop_store_1 = require("../store/noop.store");
16
15
  const device_1 = require("../transport/node/builders/device");
17
16
  const query_1 = require("../transport/node/query");
18
17
  const wa_web_version_fetcher_1 = require("../transport/wa-web-version-fetcher");
@@ -91,12 +90,6 @@ class WaClient extends node_events_1.EventEmitter {
91
90
  threadStore: this.stores.threads,
92
91
  contactStore: this.stores.contacts
93
92
  }, this.logger, this.options.writeBehind);
94
- if (this.options.addons?.autoDecrypt !== false &&
95
- this.stores.messageSecret === noop_store_1.NOOP_MESSAGE_SECRET_STORE) {
96
- this.logger.warn('addons.autoDecrypt is on (default) but messageSecret cache is noop – ' +
97
- 'addon decryption will only work if secrets are in the message store. ' +
98
- 'Set addons.autoDecrypt: false to silence this warning.');
99
- }
100
93
  const dependencies = (0, WaClientFactory_1.buildWaClientDependencies)({
101
94
  base,
102
95
  runtime: {
@@ -212,6 +205,7 @@ class WaClient extends node_events_1.EventEmitter {
212
205
  logger: this.logger,
213
206
  writeBehind: this.writeBehind,
214
207
  messageSecretStore: this.stores.messageSecret,
208
+ persistAllSecrets: this.options.addons?.persistAllSecrets === true,
215
209
  event
216
210
  });
217
211
  if (this.options.addons?.autoDecrypt !== false && event.message) {
@@ -468,6 +468,7 @@ function buildWaClientDependencies(input) {
468
468
  deviceListStore: sessionStore.deviceList,
469
469
  signalDeviceSync,
470
470
  messageSecretStore: sessionStore.messageSecret,
471
+ persistAllMessageSecrets: options.addons?.persistAllSecrets === true,
471
472
  getCurrentCredentials,
472
473
  resolvePrivacyTokenNode: (recipientJid) => trustedContactToken.resolveTokenForMessage(recipientJid),
473
474
  onDirectMessageSent: (recipientJid) => {
@@ -694,6 +695,7 @@ function buildWaClientDependencies(input) {
694
695
  .catch((err) => runtime.handleError((0, primitives_1.toError)(err)));
695
696
  },
696
697
  emitNewsletterMessageUpdate: (event) => runtime.emitEvent('newsletter_message_update', event),
698
+ emitUnavailableMessage: (event) => runtime.emitEvent('message_unavailable', event),
697
699
  emitUnhandledStanza: (event) => runtime.emitEvent('debug_unhandled_stanza', event)
698
700
  };
699
701
  const handleClientDirtyBits = (dirtyBits) => (0, dirty_1.handleDirtyBits)({
@@ -45,6 +45,12 @@ interface WaMessageDispatchCoordinatorOptions {
45
45
  readonly deviceListStore: WaDeviceListStore;
46
46
  readonly signalDeviceSync: SignalDeviceSyncApi;
47
47
  readonly messageSecretStore: WaMessageSecretStore;
48
+ /**
49
+ * When `true`, persist the message secret for every outgoing message, not
50
+ * only the poll / event / bot prompts that `needsSecretPersistence` flags.
51
+ * Wired from the client-level `addons.persistAllSecrets` option.
52
+ */
53
+ readonly persistAllMessageSecrets?: boolean;
48
54
  readonly getCurrentCredentials: () => WaAuthCredentials | null;
49
55
  readonly resolvePrivacyTokenNode: (recipientJid: string) => Promise<BinaryNode | null>;
50
56
  readonly onDirectMessageSent: (recipientJid: string) => void;
@@ -79,6 +85,16 @@ export declare class WaMessageDispatchCoordinator {
79
85
  * no LID is known/resolvable. Inputs already in LID form pass through.
80
86
  */
81
87
  private resolveDirectRecipientLid;
88
+ /**
89
+ * Resolves the `peer_recipient_pn` cross-reference for a 1:1 send, or
90
+ * `undefined` to drop the attribute. Only set when the envelope is
91
+ * LID-addressed (`directRecipientJid` is a LID): the PN is the caller's own
92
+ * JID when they passed a PN (zapo switched it to LID for sending), else the
93
+ * device-list snapshot counterpart for a recipient passed in LID form.
94
+ * Mirrors wa-web, which sets `peer_recipient_pn = getPhoneNumber($)` for a
95
+ * LID destination.
96
+ */
97
+ private resolvePeerRecipientPn;
82
98
  syncSignalSession(jid: string, reasonIdentity?: boolean): Promise<void>;
83
99
  sendReceipt(input: WaSendReceiptInput): Promise<void>;
84
100
  publishProtocolMessageToDevice(deviceJid: string, protocolMessage: Proto.Message.IProtocolMessage, options?: {
@@ -217,7 +217,7 @@ class WaMessageDispatchCoordinator {
217
217
  if (rawSecret &&
218
218
  rawSecret.length > 0 &&
219
219
  sendOptions.id &&
220
- (0, content_1.needsSecretPersistence)(messageWithSecret)) {
220
+ (this.deps.persistAllMessageSecrets || (0, content_1.needsSecretPersistence)(messageWithSecret))) {
221
221
  const meJid = this.deps.getCurrentCredentials()?.meJid ?? '';
222
222
  void this.deps.messageSecretStore
223
223
  .set(sendOptions.id, { secret: rawSecret, senderJid: meJid })
@@ -278,11 +278,14 @@ class WaMessageDispatchCoordinator {
278
278
  const directRecipientJid = isGroup
279
279
  ? recipientJid
280
280
  : await this.resolveDirectRecipientLid((0, jid_1.toUserJid)(recipientJid));
281
+ const peerRecipientPn = isGroup
282
+ ? undefined
283
+ : await this.resolvePeerRecipientPn((0, jid_1.toUserJid)(recipientJid), directRecipientJid);
281
284
  const publishResult = isGroup
282
285
  ? this.shouldUseGroupDirectPath(messageWithIcdc)
283
286
  ? await this.publishGroupDirectMessage(recipientJid, envelope)
284
287
  : await this.publishGroupSenderKeyMessage(recipientJid, envelope)
285
- : await this.publishDirectSignalMessageWithFanout(directRecipientJid, envelope);
288
+ : await this.publishDirectSignalMessageWithFanout(directRecipientJid, envelope, peerRecipientPn);
286
289
  return upload ? { ...publishResult, upload } : publishResult;
287
290
  }
288
291
  /**
@@ -317,6 +320,35 @@ class WaMessageDispatchCoordinator {
317
320
  }
318
321
  return pnUserJid;
319
322
  }
323
+ /**
324
+ * Resolves the `peer_recipient_pn` cross-reference for a 1:1 send, or
325
+ * `undefined` to drop the attribute. Only set when the envelope is
326
+ * LID-addressed (`directRecipientJid` is a LID): the PN is the caller's own
327
+ * JID when they passed a PN (zapo switched it to LID for sending), else the
328
+ * device-list snapshot counterpart for a recipient passed in LID form.
329
+ * Mirrors wa-web, which sets `peer_recipient_pn = getPhoneNumber($)` for a
330
+ * LID destination.
331
+ */
332
+ async resolvePeerRecipientPn(recipientUserJid, directRecipientJid) {
333
+ if (!(0, jid_1.isLidJid)(directRecipientJid))
334
+ return undefined;
335
+ if ((0, jid_1.isUserJid)(recipientUserJid))
336
+ return recipientUserJid;
337
+ try {
338
+ const snapshot = await this.deps.deviceListStore.findByAnyUserJid(directRecipientJid);
339
+ if (snapshot?.userJid && (0, jid_1.isUserJid)(snapshot.userJid))
340
+ return snapshot.userJid;
341
+ if (snapshot?.altUserJid && (0, jid_1.isUserJid)(snapshot.altUserJid))
342
+ return snapshot.altUserJid;
343
+ }
344
+ catch (error) {
345
+ this.deps.logger.trace('peer_recipient_pn store lookup failed', {
346
+ lid: directRecipientJid,
347
+ message: (0, primitives_2.toError)(error).message
348
+ });
349
+ }
350
+ return undefined;
351
+ }
320
352
  async syncSignalSession(jid, reasonIdentity = false) {
321
353
  const address = (0, jid_1.parseSignalAddressFromJid)(jid);
322
354
  if (address.server === constants_1.WA_DEFAULTS.GROUP_SERVER) {
@@ -997,7 +1029,7 @@ class WaMessageDispatchCoordinator {
997
1029
  distributionParticipants
998
1030
  };
999
1031
  }
1000
- async publishDirectSignalMessageWithFanout(recipientJid, envelope) {
1032
+ async publishDirectSignalMessageWithFanout(recipientJid, envelope, peerRecipientPn) {
1001
1033
  const { message, plaintext, type, edit, mediatype, sendOptions } = envelope;
1002
1034
  const meJid = this.requireCurrentMeJid('sendMessage');
1003
1035
  const meLid = this.deps.getCurrentCredentials()?.meLid;
@@ -1160,6 +1192,7 @@ class WaMessageDispatchCoordinator {
1160
1192
  customNodes: customNodes.length > 0 ? customNodes : undefined,
1161
1193
  mediatype,
1162
1194
  decryptFail: envelope.decryptFail,
1195
+ peerRecipientPn,
1163
1196
  additionalAttributes: sendOptions.additionalAttributes
1164
1197
  });
1165
1198
  const replayPayload = {
@@ -6,6 +6,12 @@ interface WaPersistIncomingMailboxOptions {
6
6
  readonly logger: Logger;
7
7
  readonly writeBehind: WriteBehindPersistence;
8
8
  readonly messageSecretStore: WaMessageSecretStore;
9
+ /**
10
+ * When `true`, persist the secret of every incoming message, not only the
11
+ * poll / event / bot prompts that `needsSecretPersistence` flags. Wired
12
+ * from the client-level `addons.persistAllSecrets` option.
13
+ */
14
+ readonly persistAllSecrets?: boolean;
9
15
  readonly event: WaIncomingMessageEvent;
10
16
  }
11
17
  export declare function persistIncomingMailboxEntities(options: WaPersistIncomingMailboxOptions): void;
@@ -56,7 +56,7 @@ function persistContacts(writeBehind, event, nowMs) {
56
56
  }
57
57
  }
58
58
  function persistIncomingMailboxEntities(options) {
59
- const { logger, writeBehind, messageSecretStore, event } = options;
59
+ const { logger, writeBehind, messageSecretStore, persistAllSecrets, event } = options;
60
60
  const stanzaId = event.key.id;
61
61
  const chatJid = event.key.remoteJid;
62
62
  if (!stanzaId || !chatJid) {
@@ -81,7 +81,7 @@ function persistIncomingMailboxEntities(options) {
81
81
  if (rawSecret &&
82
82
  rawSecret.length > 0 &&
83
83
  event.message &&
84
- (0, content_1.needsSecretPersistence)(event.message)) {
84
+ (persistAllSecrets || (0, content_1.needsSecretPersistence)(event.message))) {
85
85
  const rawSender = event.key.participant ?? event.rawNode.attrs.participant ?? event.key.remoteJid;
86
86
  const senderJid = rawSender ? (0, jid_1.toUserJid)(rawSender) : '';
87
87
  void messageSecretStore
@@ -241,6 +241,26 @@ export interface WaAddonOptions {
241
241
  * poll votes, event responses, comments).
242
242
  */
243
243
  readonly autoDecrypt?: boolean;
244
+ /**
245
+ * Persist the 32-byte message secret of every sent and received message,
246
+ * not just the poll / event / bot-prompt messages the library knows will
247
+ * get a follow-up. Off by default.
248
+ *
249
+ * Encrypted addons whose parent can be any message type - reactions,
250
+ * comments, and `secretEncryptedMessage` edits - need the parent's secret
251
+ * to decrypt. Without this, those parents stay decryptable after a restart
252
+ * only when the full `messages` archive is persistent. Enable this to keep
253
+ * them decryptable while storing only the secret, not the message body
254
+ * (e.g. with `messages: 'none'`).
255
+ *
256
+ * Has no effect when the `messageSecret` cache is `'none'`: every write
257
+ * lands in the noop store and is silently discarded. With the default
258
+ * `'memory'` provider it works for the lifetime of the process but is lost
259
+ * on restart and bounded by the cache's LRU and `messageSecretMs` TTL;
260
+ * point `messageSecret` at a persistent backend to keep the secrets across
261
+ * restarts.
262
+ */
263
+ readonly persistAllSecrets?: boolean;
244
264
  }
245
265
  export interface WaPrivacyTokenOptions {
246
266
  readonly tcTokenDurationS?: number;
@@ -721,6 +741,27 @@ export interface WaIncomingUnhandledStanzaEvent extends WaIncomingBaseEvent {
721
741
  /** Short reason describing why the dispatcher did not match a typed handler. */
722
742
  readonly reason: string;
723
743
  }
744
+ /**
745
+ * Why an incoming message arrived as a content-less placeholder. `view_once`:
746
+ * a view-once already consumed elsewhere. `hosted`: a hosted/bot message that
747
+ * could not be fanned out. `other`: an `unavailable` marker the lib does not
748
+ * categorize yet.
749
+ */
750
+ export type WaUnavailableMessageKind = 'view_once' | 'hosted' | 'other';
751
+ export interface WaIncomingUnavailableMessageEvent extends Omit<WaIncomingBaseEvent, 'chatJid' | 'stanzaId'> {
752
+ /** Which flavour of content the server signalled as unavailable. */
753
+ readonly kind: WaUnavailableMessageKind;
754
+ /**
755
+ * The message key (chat, stanza id, author, addressing metadata) – same
756
+ * shape the `message` event carries, so it can be stored or correlated. There
757
+ * is no decrypted `message`: the payload is unavailable and cannot be fetched.
758
+ */
759
+ readonly key: WaIncomingMessageKey;
760
+ /** Stanza `t` attr (seconds since epoch). */
761
+ readonly timestampSeconds?: number;
762
+ /** Sender's display name from the stanza's `notify` attr. */
763
+ readonly pushName?: string;
764
+ }
724
765
  export interface WaIncomingErrorStanzaEvent extends WaIncomingBaseEvent {
725
766
  readonly code?: number;
726
767
  readonly text?: string;
@@ -1057,6 +1098,14 @@ export interface WaClientEventMap {
1057
1098
  * typed protocol payload directly.
1058
1099
  */
1059
1100
  readonly message_protocol: (event: WaIncomingProtocolMessageEvent) => void;
1101
+ /**
1102
+ * A message the server delivered as a content-less placeholder: the payload
1103
+ * is unavailable and cannot be recovered (a view-once already consumed, or a
1104
+ * hosted/bot message that could not be fanned out). The lib acks it and emits
1105
+ * this instead of a `message` event for the same stanza. Branch on
1106
+ * `event.kind`.
1107
+ */
1108
+ readonly message_unavailable: (event: WaIncomingUnavailableMessageEvent) => void;
1060
1109
  /**
1061
1110
  * Inbound `<receipt>` for an outgoing message – delivery, read, played,
1062
1111
  * server, etc. Use this to track message ACK progression.
@@ -714,7 +714,12 @@ export class WaAppStateSyncClient {
714
714
  if (keyData !== null) {
715
715
  const expectedSnapshotMac = await this.crypto.generateSnapshotMac(keyData, ltHash, version, collection);
716
716
  if (!uint8TimingSafeEqual(expectedSnapshotMac, snapshot.mac)) {
717
- throw new Error(`snapshot MAC mismatch for ${collection}`);
717
+ // Poisoned server-side snapshot (MAC unverifiable by any client):
718
+ // keep partial state instead of throwing, which would loop refetch forever.
719
+ this.logger.warn('snapshot LT-hash verification failed, continuing with partial state', {
720
+ collection,
721
+ version
722
+ });
718
723
  }
719
724
  }
720
725
  this.setCollectionState(collection, version, ltHash, indexValueMap);
@@ -9,7 +9,6 @@ import { proto } from '../proto.js';
9
9
  import { WA_DEFAULTS, WA_MESSAGE_TYPES } from '../protocol/constants.js';
10
10
  import { normalizeDeviceJid } from '../protocol/jid.js';
11
11
  import { WA_DISCONNECT_REASONS, WA_LOGOUT_REASONS } from '../protocol/stream.js';
12
- import { NOOP_MESSAGE_SECRET_STORE } from '../store/noop.store.js';
13
12
  import { buildRemoveCompanionDeviceIq } from '../transport/node/builders/device.js';
14
13
  import { assertIqResult, queryWithContext as queryNodeWithContext } from '../transport/node/query.js';
15
14
  import { fetchLatestWaWebVersion } from '../transport/wa-web-version-fetcher.js';
@@ -88,12 +87,6 @@ export class WaClient extends EventEmitter {
88
87
  threadStore: this.stores.threads,
89
88
  contactStore: this.stores.contacts
90
89
  }, this.logger, this.options.writeBehind);
91
- if (this.options.addons?.autoDecrypt !== false &&
92
- this.stores.messageSecret === NOOP_MESSAGE_SECRET_STORE) {
93
- this.logger.warn('addons.autoDecrypt is on (default) but messageSecret cache is noop – ' +
94
- 'addon decryption will only work if secrets are in the message store. ' +
95
- 'Set addons.autoDecrypt: false to silence this warning.');
96
- }
97
90
  const dependencies = buildWaClientDependencies({
98
91
  base,
99
92
  runtime: {
@@ -209,6 +202,7 @@ export class WaClient extends EventEmitter {
209
202
  logger: this.logger,
210
203
  writeBehind: this.writeBehind,
211
204
  messageSecretStore: this.stores.messageSecret,
205
+ persistAllSecrets: this.options.addons?.persistAllSecrets === true,
212
206
  event
213
207
  });
214
208
  if (this.options.addons?.autoDecrypt !== false && event.message) {
@@ -464,6 +464,7 @@ export function buildWaClientDependencies(input) {
464
464
  deviceListStore: sessionStore.deviceList,
465
465
  signalDeviceSync,
466
466
  messageSecretStore: sessionStore.messageSecret,
467
+ persistAllMessageSecrets: options.addons?.persistAllSecrets === true,
467
468
  getCurrentCredentials,
468
469
  resolvePrivacyTokenNode: (recipientJid) => trustedContactToken.resolveTokenForMessage(recipientJid),
469
470
  onDirectMessageSent: (recipientJid) => {
@@ -690,6 +691,7 @@ export function buildWaClientDependencies(input) {
690
691
  .catch((err) => runtime.handleError(toError(err)));
691
692
  },
692
693
  emitNewsletterMessageUpdate: (event) => runtime.emitEvent('newsletter_message_update', event),
694
+ emitUnavailableMessage: (event) => runtime.emitEvent('message_unavailable', event),
693
695
  emitUnhandledStanza: (event) => runtime.emitEvent('debug_unhandled_stanza', event)
694
696
  };
695
697
  const handleClientDirtyBits = (dirtyBits) => handleDirtyBits({
@@ -12,7 +12,7 @@ import { writeRandomPadMax16 } from '../../message/encode/padding.js';
12
12
  import { buildBotInvokeProtoCopy, extractInvokedBotJid, genBotMsgSecret } from '../../message/kinds/bot.js';
13
13
  import { proto } from '../../proto.js';
14
14
  import { WA_DEFAULTS, WA_NACK_REASONS } from '../../protocol/constants.js';
15
- import { canonicalizeOwnAccountJid, isBotJid, isGroupJid, isHostedDeviceJid, isLidJid, isNewsletterJid, normalizeDeviceJid, normalizeRecipientJid, parseJidFull, parseSignalAddressFromJid, signalAddressKey, toUserJid } from '../../protocol/jid.js';
15
+ import { canonicalizeOwnAccountJid, isBotJid, isGroupJid, isHostedDeviceJid, isLidJid, isNewsletterJid, isUserJid, normalizeDeviceJid, normalizeRecipientJid, parseJidFull, parseSignalAddressFromJid, signalAddressKey, toUserJid } from '../../protocol/jid.js';
16
16
  import { encodeBinaryNode } from '../../transport/binary/index.js';
17
17
  import { buildButtonAddonNode, buildDirectMessageFanoutNode, buildGroupSenderKeyMessageNode, buildMetaNode } from '../../transport/node/builders/message.js';
18
18
  import { bytesToHex, TEXT_ENCODER } from '../../util/bytes.js';
@@ -214,7 +214,7 @@ export class WaMessageDispatchCoordinator {
214
214
  if (rawSecret &&
215
215
  rawSecret.length > 0 &&
216
216
  sendOptions.id &&
217
- needsSecretPersistence(messageWithSecret)) {
217
+ (this.deps.persistAllMessageSecrets || needsSecretPersistence(messageWithSecret))) {
218
218
  const meJid = this.deps.getCurrentCredentials()?.meJid ?? '';
219
219
  void this.deps.messageSecretStore
220
220
  .set(sendOptions.id, { secret: rawSecret, senderJid: meJid })
@@ -275,11 +275,14 @@ export class WaMessageDispatchCoordinator {
275
275
  const directRecipientJid = isGroup
276
276
  ? recipientJid
277
277
  : await this.resolveDirectRecipientLid(toUserJid(recipientJid));
278
+ const peerRecipientPn = isGroup
279
+ ? undefined
280
+ : await this.resolvePeerRecipientPn(toUserJid(recipientJid), directRecipientJid);
278
281
  const publishResult = isGroup
279
282
  ? this.shouldUseGroupDirectPath(messageWithIcdc)
280
283
  ? await this.publishGroupDirectMessage(recipientJid, envelope)
281
284
  : await this.publishGroupSenderKeyMessage(recipientJid, envelope)
282
- : await this.publishDirectSignalMessageWithFanout(directRecipientJid, envelope);
285
+ : await this.publishDirectSignalMessageWithFanout(directRecipientJid, envelope, peerRecipientPn);
283
286
  return upload ? { ...publishResult, upload } : publishResult;
284
287
  }
285
288
  /**
@@ -314,6 +317,35 @@ export class WaMessageDispatchCoordinator {
314
317
  }
315
318
  return pnUserJid;
316
319
  }
320
+ /**
321
+ * Resolves the `peer_recipient_pn` cross-reference for a 1:1 send, or
322
+ * `undefined` to drop the attribute. Only set when the envelope is
323
+ * LID-addressed (`directRecipientJid` is a LID): the PN is the caller's own
324
+ * JID when they passed a PN (zapo switched it to LID for sending), else the
325
+ * device-list snapshot counterpart for a recipient passed in LID form.
326
+ * Mirrors wa-web, which sets `peer_recipient_pn = getPhoneNumber($)` for a
327
+ * LID destination.
328
+ */
329
+ async resolvePeerRecipientPn(recipientUserJid, directRecipientJid) {
330
+ if (!isLidJid(directRecipientJid))
331
+ return undefined;
332
+ if (isUserJid(recipientUserJid))
333
+ return recipientUserJid;
334
+ try {
335
+ const snapshot = await this.deps.deviceListStore.findByAnyUserJid(directRecipientJid);
336
+ if (snapshot?.userJid && isUserJid(snapshot.userJid))
337
+ return snapshot.userJid;
338
+ if (snapshot?.altUserJid && isUserJid(snapshot.altUserJid))
339
+ return snapshot.altUserJid;
340
+ }
341
+ catch (error) {
342
+ this.deps.logger.trace('peer_recipient_pn store lookup failed', {
343
+ lid: directRecipientJid,
344
+ message: toError(error).message
345
+ });
346
+ }
347
+ return undefined;
348
+ }
317
349
  async syncSignalSession(jid, reasonIdentity = false) {
318
350
  const address = parseSignalAddressFromJid(jid);
319
351
  if (address.server === WA_DEFAULTS.GROUP_SERVER) {
@@ -994,7 +1026,7 @@ export class WaMessageDispatchCoordinator {
994
1026
  distributionParticipants
995
1027
  };
996
1028
  }
997
- async publishDirectSignalMessageWithFanout(recipientJid, envelope) {
1029
+ async publishDirectSignalMessageWithFanout(recipientJid, envelope, peerRecipientPn) {
998
1030
  const { message, plaintext, type, edit, mediatype, sendOptions } = envelope;
999
1031
  const meJid = this.requireCurrentMeJid('sendMessage');
1000
1032
  const meLid = this.deps.getCurrentCredentials()?.meLid;
@@ -1157,6 +1189,7 @@ export class WaMessageDispatchCoordinator {
1157
1189
  customNodes: customNodes.length > 0 ? customNodes : undefined,
1158
1190
  mediatype,
1159
1191
  decryptFail: envelope.decryptFail,
1192
+ peerRecipientPn,
1160
1193
  additionalAttributes: sendOptions.additionalAttributes
1161
1194
  });
1162
1195
  const replayPayload = {
@@ -53,7 +53,7 @@ function persistContacts(writeBehind, event, nowMs) {
53
53
  }
54
54
  }
55
55
  export function persistIncomingMailboxEntities(options) {
56
- const { logger, writeBehind, messageSecretStore, event } = options;
56
+ const { logger, writeBehind, messageSecretStore, persistAllSecrets, event } = options;
57
57
  const stanzaId = event.key.id;
58
58
  const chatJid = event.key.remoteJid;
59
59
  if (!stanzaId || !chatJid) {
@@ -78,7 +78,7 @@ export function persistIncomingMailboxEntities(options) {
78
78
  if (rawSecret &&
79
79
  rawSecret.length > 0 &&
80
80
  event.message &&
81
- needsSecretPersistence(event.message)) {
81
+ (persistAllSecrets || needsSecretPersistence(event.message))) {
82
82
  const rawSender = event.key.participant ?? event.rawNode.attrs.participant ?? event.key.remoteJid;
83
83
  const senderJid = rawSender ? toUserJid(rawSender) : '';
84
84
  void messageSecretStore
@@ -496,6 +496,42 @@ export async function handleIncomingMessageAck(node, options) {
496
496
  await options.sendNode(ackNode);
497
497
  return true;
498
498
  }
499
+ const unavailableNode = findNodeChild(node, 'unavailable');
500
+ if (unavailableNode) {
501
+ const kind = unavailableNode.attrs.hosted === 'true'
502
+ ? 'hosted'
503
+ : unavailableNode.attrs.type === 'view_once'
504
+ ? 'view_once'
505
+ : 'other';
506
+ const senderJid = node.attrs.participant ?? node.attrs.from;
507
+ const sender = senderJid ? parseJidFull(senderJid) : null;
508
+ const { key, pushName } = buildIncomingMessageKey(node, sender ? { userJid: sender.userJid, device: sender.address.device } : null, options);
509
+ options.emitUnavailableMessage?.({
510
+ rawNode: buildIncomingEventRawNode(node),
511
+ key,
512
+ kind,
513
+ stanzaType: node.attrs.type,
514
+ offline: node.attrs.offline !== undefined,
515
+ timestampSeconds: parseOptionalInt(node.attrs.t),
516
+ pushName
517
+ });
518
+ const ackNode = buildAckNode({
519
+ kind: 'message',
520
+ node,
521
+ id,
522
+ to: from,
523
+ from: options.getMeJid?.()
524
+ });
525
+ options.logger.trace('acking unavailable incoming message', {
526
+ id,
527
+ to: from,
528
+ type: ackNode.attrs.type,
529
+ participant: ackNode.attrs.participant,
530
+ unavailableKind: kind
531
+ });
532
+ await options.sendNode(ackNode);
533
+ return true;
534
+ }
499
535
  if (!shouldSendStandardReceipt) {
500
536
  return true;
501
537
  }
@@ -32,6 +32,9 @@ function buildMessageAttrs(input) {
32
32
  if (input.addressingMode) {
33
33
  attrs.addressing_mode = input.addressingMode;
34
34
  }
35
+ if (input.peerRecipientPn) {
36
+ attrs.peer_recipient_pn = input.peerRecipientPn;
37
+ }
35
38
  if (input.additionalAttributes) {
36
39
  Object.assign(attrs, input.additionalAttributes);
37
40
  }
@@ -1,4 +1,4 @@
1
- export { base64ToBytes, bytesToBase64, bytesToBase64UrlSafe, bytesToHex, decodeBase64Url, hexToBytes, TEXT_DECODER, toBytesView, uint8Equal } from './bytes.js';
1
+ export { base64ToBytes, bytesToBase64, bytesToBase64UrlSafe, bytesToHex, decodeBase64Url, hexToBytes, TEXT_DECODER, toBytesView, uint8Equal, uint8TimingSafeEqual } from './bytes.js';
2
2
  export { asBytes, asNumber, asOptionalBytes, asOptionalNumber, asOptionalString, asString, resolvePositive, toBoolOrUndef } from './coercion.js';
3
3
  export { normalizeQueryLimit } from './collections.js';
4
4
  export { toError, toSafeNumber } from './primitives.js';
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { WaClient } from './client';
2
2
  export type { WaClientEventMap, WaClientOptions, WaClientProxyOptions, WaDownloadMediaOptions, WaHistorySyncChunkEvent, WaHistorySyncOptions, WaWriteBehindOptions } from './client/types';
3
3
  export type { WaMessageCoordinator } from './client/coordinators/WaMessageCoordinator';
4
- 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, WaIncomingUnhandledStanzaEvent, WaMexLidChangeEvent, WaMexMessageCappingEvent, WaMexMessageCappingStatus, WaMexNotificationEvent, WaMexNotificationGraphQlError, WaMexNotificationOperationName, WaMexNotificationUnknownEvent, WaMexOwnUsernameSyncEvent, WaMexTextStatusUpdateEvent, WaMexTextStatusUpdateHintEvent, WaMexUsernameDeleteEvent, WaMexUsernameSetEvent, WaMexUsernameUpdateHintEvent, WaOfflineResumeEvent, WaPictureEvent, WaPictureEventAction, WaPrivacyTokenUpdateEvent, WaReceiptStatus, WaRegistrationCodeEvent, WaSendMessageOptions, WaVerifiedNameResult, WaAddonKind, WaNewsletterEventAction, WaNewsletterMessageUpdate, WaNewsletterPollVoteEntry, WaNewsletterReactionEntry } from './client/types';
4
+ 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, WaPictureEvent, WaPictureEventAction, WaPrivacyTokenUpdateEvent, WaReceiptStatus, WaRegistrationCodeEvent, WaSendMessageOptions, WaUnavailableMessageKind, WaVerifiedNameResult, WaAddonKind, WaNewsletterEventAction, WaNewsletterMessageUpdate, WaNewsletterPollVoteEntry, WaNewsletterReactionEntry } from './client/types';
5
5
  export type { WaAppStateMutationCoordinator, WaBroadcastListParticipant, WaSetBroadcastListInput, WaSetStatusPrivacyInput } from './client/coordinators/WaAppStateMutationCoordinator';
6
6
  export type { WaBotCoordinator, WaBotInfo, WaBotPosingAsProfessional, WaBotProfileCommand, WaBotProfilePrompt, WaBotProfileResult, WaBotPromptOptions, WaGetBotProfileOptions } from './client/coordinators/WaBotCoordinator';
7
7
  export type { WaBroadcastListCoordinator, WaSendBroadcastListMessageInput } from './client/coordinators/WaBroadcastListCoordinator';
@@ -1,4 +1,4 @@
1
- import type { WaIncomingMessageEvent, WaIncomingNewsletterMessageUpdateEvent, WaIncomingUnhandledStanzaEvent } from '../../client/types';
1
+ import type { WaIncomingMessageEvent, WaIncomingNewsletterMessageUpdateEvent, WaIncomingUnavailableMessageEvent, WaIncomingUnhandledStanzaEvent } from '../../client/types';
2
2
  import type { Logger } from '../../infra/log/types';
3
3
  import { proto } from '../../proto';
4
4
  import type { WaRetryDecryptFailureContext } from '../../retry/types';
@@ -15,6 +15,7 @@ interface WaIncomingMessageAckHandlerOptions {
15
15
  readonly onDecryptFailure?: (context: WaRetryDecryptFailureContext, error: unknown) => Promise<boolean>;
16
16
  readonly emitIncomingMessage?: (event: WaIncomingMessageEvent) => void;
17
17
  readonly emitNewsletterMessageUpdate?: (event: WaIncomingNewsletterMessageUpdateEvent) => void;
18
+ readonly emitUnavailableMessage?: (event: WaIncomingUnavailableMessageEvent) => void;
18
19
  readonly emitUnhandledStanza?: (event: WaIncomingUnhandledStanzaEvent) => void;
19
20
  }
20
21
  /**
@@ -500,6 +500,42 @@ async function handleIncomingMessageAck(node, options) {
500
500
  await options.sendNode(ackNode);
501
501
  return true;
502
502
  }
503
+ const unavailableNode = (0, helpers_1.findNodeChild)(node, 'unavailable');
504
+ if (unavailableNode) {
505
+ const kind = unavailableNode.attrs.hosted === 'true'
506
+ ? 'hosted'
507
+ : unavailableNode.attrs.type === 'view_once'
508
+ ? 'view_once'
509
+ : 'other';
510
+ const senderJid = node.attrs.participant ?? node.attrs.from;
511
+ const sender = senderJid ? (0, jid_1.parseJidFull)(senderJid) : null;
512
+ const { key, pushName } = buildIncomingMessageKey(node, sender ? { userJid: sender.userJid, device: sender.address.device } : null, options);
513
+ options.emitUnavailableMessage?.({
514
+ rawNode: buildIncomingEventRawNode(node),
515
+ key,
516
+ kind,
517
+ stanzaType: node.attrs.type,
518
+ offline: node.attrs.offline !== undefined,
519
+ timestampSeconds: (0, primitives_1.parseOptionalInt)(node.attrs.t),
520
+ pushName
521
+ });
522
+ const ackNode = (0, global_1.buildAckNode)({
523
+ kind: 'message',
524
+ node,
525
+ id,
526
+ to: from,
527
+ from: options.getMeJid?.()
528
+ });
529
+ options.logger.trace('acking unavailable incoming message', {
530
+ id,
531
+ to: from,
532
+ type: ackNode.attrs.type,
533
+ participant: ackNode.attrs.participant,
534
+ unavailableKind: kind
535
+ });
536
+ await options.sendNode(ackNode);
537
+ return true;
538
+ }
503
539
  if (!shouldSendStandardReceipt) {
504
540
  return true;
505
541
  }
@@ -15,6 +15,7 @@ type DirectMessageFanoutInput = {
15
15
  readonly customNodes?: readonly BinaryNode[];
16
16
  readonly mediatype?: string;
17
17
  readonly decryptFail?: string;
18
+ readonly peerRecipientPn?: string;
18
19
  readonly additionalAttributes?: Readonly<Record<string, string>>;
19
20
  };
20
21
  type GroupMessageFanoutInput = DirectMessageFanoutInput & {
@@ -39,6 +39,9 @@ function buildMessageAttrs(input) {
39
39
  if (input.addressingMode) {
40
40
  attrs.addressing_mode = input.addressingMode;
41
41
  }
42
+ if (input.peerRecipientPn) {
43
+ attrs.peer_recipient_pn = input.peerRecipientPn;
44
+ }
42
45
  if (input.additionalAttributes) {
43
46
  Object.assign(attrs, input.additionalAttributes);
44
47
  }
@@ -1,4 +1,4 @@
1
- export { base64ToBytes, bytesToBase64, bytesToBase64UrlSafe, bytesToHex, decodeBase64Url, hexToBytes, TEXT_DECODER, toBytesView, uint8Equal } from './bytes';
1
+ export { base64ToBytes, bytesToBase64, bytesToBase64UrlSafe, bytesToHex, decodeBase64Url, hexToBytes, TEXT_DECODER, toBytesView, uint8Equal, uint8TimingSafeEqual } from './bytes';
2
2
  export { asBytes, asNumber, asOptionalBytes, asOptionalNumber, asOptionalString, asString, resolvePositive, toBoolOrUndef } from './coercion';
3
3
  export { normalizeQueryLimit } from './collections';
4
4
  export { toError, toSafeNumber } from './primitives';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isBunRuntime = exports.toSafeNumber = exports.toError = exports.normalizeQueryLimit = exports.toBoolOrUndef = exports.resolvePositive = exports.asString = exports.asOptionalString = exports.asOptionalNumber = exports.asOptionalBytes = exports.asNumber = exports.asBytes = exports.uint8Equal = exports.toBytesView = exports.TEXT_DECODER = exports.hexToBytes = exports.decodeBase64Url = exports.bytesToHex = exports.bytesToBase64UrlSafe = exports.bytesToBase64 = exports.base64ToBytes = void 0;
3
+ exports.isBunRuntime = exports.toSafeNumber = exports.toError = exports.normalizeQueryLimit = exports.toBoolOrUndef = exports.resolvePositive = exports.asString = exports.asOptionalString = exports.asOptionalNumber = exports.asOptionalBytes = exports.asNumber = exports.asBytes = exports.uint8TimingSafeEqual = exports.uint8Equal = exports.toBytesView = exports.TEXT_DECODER = exports.hexToBytes = exports.decodeBase64Url = exports.bytesToHex = exports.bytesToBase64UrlSafe = exports.bytesToBase64 = exports.base64ToBytes = void 0;
4
4
  var bytes_1 = require("./bytes");
5
5
  Object.defineProperty(exports, "base64ToBytes", { enumerable: true, get: function () { return bytes_1.base64ToBytes; } });
6
6
  Object.defineProperty(exports, "bytesToBase64", { enumerable: true, get: function () { return bytes_1.bytesToBase64; } });
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "hexToBytes", { enumerable: true, get: function (
11
11
  Object.defineProperty(exports, "TEXT_DECODER", { enumerable: true, get: function () { return bytes_1.TEXT_DECODER; } });
12
12
  Object.defineProperty(exports, "toBytesView", { enumerable: true, get: function () { return bytes_1.toBytesView; } });
13
13
  Object.defineProperty(exports, "uint8Equal", { enumerable: true, get: function () { return bytes_1.uint8Equal; } });
14
+ Object.defineProperty(exports, "uint8TimingSafeEqual", { enumerable: true, get: function () { return bytes_1.uint8TimingSafeEqual; } });
14
15
  var coercion_1 = require("./coercion");
15
16
  Object.defineProperty(exports, "asBytes", { enumerable: true, get: function () { return coercion_1.asBytes; } });
16
17
  Object.defineProperty(exports, "asNumber", { enumerable: true, get: function () { return coercion_1.asNumber; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapo-js",
3
- "version": "1.1.3",
3
+ "version": "1.2.1",
4
4
  "description": "High-performance WhatsApp Web TypeScript library",
5
5
  "license": "MIT",
6
6
  "author": "vinikjkkj <contact@vinicius.email> (https://github.com/vinikjkkj)",