zapo-js 1.1.2 → 1.2.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 (45) hide show
  1. package/dist/client/WaClient.js +1 -7
  2. package/dist/client/WaClientFactory.js +7 -4
  3. package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +16 -0
  4. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +43 -6
  5. package/dist/client/coordinators/WaPresenceCoordinator.d.ts +6 -0
  6. package/dist/client/coordinators/WaPresenceCoordinator.js +8 -2
  7. package/dist/client/coordinators/WaProfileCoordinator.d.ts +7 -0
  8. package/dist/client/coordinators/WaProfileCoordinator.js +10 -4
  9. package/dist/client/coordinators/WaRetryCoordinator.js +1 -1
  10. package/dist/client/coordinators/WaTrustedContactTokenCoordinator.d.ts +9 -0
  11. package/dist/client/coordinators/WaTrustedContactTokenCoordinator.js +23 -7
  12. package/dist/client/persistence/mailbox.d.ts +6 -0
  13. package/dist/client/persistence/mailbox.js +2 -2
  14. package/dist/client/types.d.ts +49 -0
  15. package/dist/esm/client/WaClient.js +1 -7
  16. package/dist/esm/client/WaClientFactory.js +8 -5
  17. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +44 -7
  18. package/dist/esm/client/coordinators/WaPresenceCoordinator.js +8 -2
  19. package/dist/esm/client/coordinators/WaProfileCoordinator.js +10 -4
  20. package/dist/esm/client/coordinators/WaRetryCoordinator.js +1 -1
  21. package/dist/esm/client/coordinators/WaTrustedContactTokenCoordinator.js +23 -7
  22. package/dist/esm/client/persistence/mailbox.js +2 -2
  23. package/dist/esm/message/primitives/incoming.js +99 -42
  24. package/dist/esm/protocol/jid.js +42 -0
  25. package/dist/esm/retry/reason.js +2 -2
  26. package/dist/esm/transport/node/WaNodeOrchestrator.js +6 -2
  27. package/dist/esm/transport/node/builders/message.js +3 -0
  28. package/dist/esm/transport/node/builders/presence.js +2 -1
  29. package/dist/esm/transport/node/builders/profile.js +3 -2
  30. package/dist/esm/transport/node/builders/retry.js +2 -4
  31. package/dist/index.d.ts +1 -1
  32. package/dist/message/primitives/incoming.d.ts +3 -1
  33. package/dist/message/primitives/incoming.js +98 -41
  34. package/dist/protocol/jid.d.ts +12 -0
  35. package/dist/protocol/jid.js +44 -0
  36. package/dist/retry/reason.js +2 -2
  37. package/dist/transport/node/WaNodeOrchestrator.js +6 -2
  38. package/dist/transport/node/builders/message.d.ts +1 -0
  39. package/dist/transport/node/builders/message.js +3 -0
  40. package/dist/transport/node/builders/presence.d.ts +6 -0
  41. package/dist/transport/node/builders/presence.js +2 -1
  42. package/dist/transport/node/builders/profile.d.ts +1 -1
  43. package/dist/transport/node/builders/profile.js +3 -2
  44. package/dist/transport/node/builders/retry.js +2 -4
  45. package/package.json +2 -2
@@ -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) => {
@@ -487,7 +488,8 @@ function buildWaClientDependencies(input) {
487
488
  });
488
489
  const presenceCoordinator = (0, WaPresenceCoordinator_1.createPresenceCoordinator)({
489
490
  sendNode: (node) => nodeOrchestrator.sendNode(node, false),
490
- getCurrentCredentials
491
+ getCurrentCredentials,
492
+ resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid)
491
493
  });
492
494
  const peerDataOperation = (0, peer_data_operation_1.createPeerDataOperationRequester)({
493
495
  logger,
@@ -559,9 +561,7 @@ function buildWaClientDependencies(input) {
559
561
  const credentials = getCurrentCredentials();
560
562
  if (!credentials)
561
563
  return false;
562
- const candidateUser = (0, jid_1.toUserJid)(deviceJid);
563
- return ((!!credentials.meJid && (0, jid_1.toUserJid)(credentials.meJid) === candidateUser) ||
564
- (!!credentials.meLid && (0, jid_1.toUserJid)(credentials.meLid) === candidateUser));
564
+ return (0, jid_1.isOwnAccountJid)(deviceJid, credentials.meJid, credentials.meLid);
565
565
  },
566
566
  sendKeyShare: (toDeviceJid, keys, missingKeyIds) => messageDispatch.sendAppStateSyncKeyShare(toDeviceJid, keys, missingKeyIds),
567
567
  triggerSync: async () => {
@@ -604,6 +604,7 @@ function buildWaClientDependencies(input) {
604
604
  queryLidsByPhoneJids: (phoneJids) => signalDeviceSync.queryLidsByPhoneJids(phoneJids),
605
605
  mutations: appStateMutations,
606
606
  applyOwnPushName,
607
+ resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid),
607
608
  logger
608
609
  });
609
610
  const statusCoordinator = (0, WaStatusCoordinator_1.createStatusCoordinator)({
@@ -684,6 +685,7 @@ function buildWaClientDependencies(input) {
684
685
  logger,
685
686
  sendNode: runtime.sendNode,
686
687
  getMeJid: () => getCurrentCredentials()?.meJid,
688
+ getMeLid: () => getCurrentCredentials()?.meLid,
687
689
  signalProtocol,
688
690
  senderKeyManager,
689
691
  onDecryptFailure: (context, error) => retryCoordinator.onDecryptFailure(context, error),
@@ -693,6 +695,7 @@ function buildWaClientDependencies(input) {
693
695
  .catch((err) => runtime.handleError((0, primitives_1.toError)(err)));
694
696
  },
695
697
  emitNewsletterMessageUpdate: (event) => runtime.emitEvent('newsletter_message_update', event),
698
+ emitUnavailableMessage: (event) => runtime.emitEvent('message_unavailable', event),
696
699
  emitUnhandledStanza: (event) => runtime.emitEvent('debug_unhandled_stanza', event)
697
700
  };
698
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?: {
@@ -68,17 +68,20 @@ class WaMessageDispatchCoordinator {
68
68
  }
69
69
  async publishSignalMessage(input, options = {}) {
70
70
  this.requireCurrentMeJid('publishSignalMessage');
71
- const address = (0, jid_1.parseSignalAddressFromJid)(input.to);
71
+ const credentials = this.deps.getCurrentCredentials();
72
+ const sessionJid = (0, jid_1.canonicalizeOwnAccountJid)(input.to, credentials?.meJid, credentials?.meLid);
73
+ const address = (0, jid_1.parseSignalAddressFromJid)(sessionJid);
72
74
  if (address.server === constants_1.WA_DEFAULTS.GROUP_SERVER) {
73
75
  throw new Error('publishSignalMessage currently supports only direct chats; use sender-key flow for groups');
74
76
  }
75
77
  this.deps.logger.trace('wa client publish signal message', {
76
78
  to: input.to,
79
+ sessionJid,
77
80
  type: input.type
78
81
  });
79
82
  const [paddedPlaintext] = await Promise.all([
80
83
  (0, padding_1.writeRandomPadMax16)(input.plaintext),
81
- this.deps.sessionResolver.ensureSession(address, input.to, input.expectedIdentity)
84
+ this.deps.sessionResolver.ensureSession(address, sessionJid, input.expectedIdentity)
82
85
  ]);
83
86
  const encrypted = await this.deps.signalProtocol.encryptMessage(address, paddedPlaintext, input.expectedIdentity);
84
87
  const messageType = input.type ?? 'text';
@@ -214,7 +217,7 @@ class WaMessageDispatchCoordinator {
214
217
  if (rawSecret &&
215
218
  rawSecret.length > 0 &&
216
219
  sendOptions.id &&
217
- (0, content_1.needsSecretPersistence)(messageWithSecret)) {
220
+ (this.deps.persistAllMessageSecrets || (0, content_1.needsSecretPersistence)(messageWithSecret))) {
218
221
  const meJid = this.deps.getCurrentCredentials()?.meJid ?? '';
219
222
  void this.deps.messageSecretStore
220
223
  .set(sendOptions.id, { secret: rawSecret, senderJid: meJid })
@@ -275,11 +278,14 @@ class WaMessageDispatchCoordinator {
275
278
  const directRecipientJid = isGroup
276
279
  ? recipientJid
277
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);
278
284
  const publishResult = isGroup
279
285
  ? this.shouldUseGroupDirectPath(messageWithIcdc)
280
286
  ? await this.publishGroupDirectMessage(recipientJid, envelope)
281
287
  : await this.publishGroupSenderKeyMessage(recipientJid, envelope)
282
- : await this.publishDirectSignalMessageWithFanout(directRecipientJid, envelope);
288
+ : await this.publishDirectSignalMessageWithFanout(directRecipientJid, envelope, peerRecipientPn);
283
289
  return upload ? { ...publishResult, upload } : publishResult;
284
290
  }
285
291
  /**
@@ -314,6 +320,35 @@ class WaMessageDispatchCoordinator {
314
320
  }
315
321
  return pnUserJid;
316
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
+ }
317
352
  async syncSignalSession(jid, reasonIdentity = false) {
318
353
  const address = (0, jid_1.parseSignalAddressFromJid)(jid);
319
354
  if (address.server === constants_1.WA_DEFAULTS.GROUP_SERVER) {
@@ -325,7 +360,8 @@ class WaMessageDispatchCoordinator {
325
360
  await this.deps.messageClient.sendReceipt(input);
326
361
  }
327
362
  async publishProtocolMessageToDevice(deviceJid, protocolMessage, options) {
328
- const meJid = this.deps.getCurrentCredentials()?.meJid;
363
+ const credentials = this.deps.getCurrentCredentials();
364
+ const meJid = credentials?.meJid;
329
365
  const meParsed = meJid ? (0, jid_1.parseJidFull)(meJid) : undefined;
330
366
  const meUserJid = meParsed?.userJid;
331
367
  let senderIcdc = null;
@@ -993,7 +1029,7 @@ class WaMessageDispatchCoordinator {
993
1029
  distributionParticipants
994
1030
  };
995
1031
  }
996
- async publishDirectSignalMessageWithFanout(recipientJid, envelope) {
1032
+ async publishDirectSignalMessageWithFanout(recipientJid, envelope, peerRecipientPn) {
997
1033
  const { message, plaintext, type, edit, mediatype, sendOptions } = envelope;
998
1034
  const meJid = this.requireCurrentMeJid('sendMessage');
999
1035
  const meLid = this.deps.getCurrentCredentials()?.meLid;
@@ -1156,6 +1192,7 @@ class WaMessageDispatchCoordinator {
1156
1192
  customNodes: customNodes.length > 0 ? customNodes : undefined,
1157
1193
  mediatype,
1158
1194
  decryptFail: envelope.decryptFail,
1195
+ peerRecipientPn,
1159
1196
  additionalAttributes: sendOptions.additionalAttributes
1160
1197
  });
1161
1198
  const replayPayload = {
@@ -21,6 +21,12 @@ export interface WaPresenceCoordinator {
21
21
  interface WaPresenceCoordinatorOptions {
22
22
  readonly sendNode: (node: BinaryNode) => Promise<void>;
23
23
  readonly getCurrentCredentials: () => WaAuthCredentials | null;
24
+ /**
25
+ * Resolves the receiver-mode `<tctoken>` node for a contact, echoed back
26
+ * on a user presence subscription to unlock the target's presence
27
+ * visibility. Returns `null` when no valid token is held.
28
+ */
29
+ readonly resolvePrivacyTokenNode: (jid: string) => Promise<BinaryNode | null>;
24
30
  }
25
31
  /** Builds a {@link WaPresenceCoordinator} from its node-send dependency. */
26
32
  export declare function createPresenceCoordinator(options: WaPresenceCoordinatorOptions): WaPresenceCoordinator;
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createPresenceCoordinator = createPresenceCoordinator;
4
+ const jid_1 = require("../../protocol/jid");
4
5
  const chatstate_1 = require("../../transport/node/builders/chatstate");
5
6
  const presence_1 = require("../../transport/node/builders/presence");
6
7
  /** Builds a {@link WaPresenceCoordinator} from its node-send dependency. */
7
8
  function createPresenceCoordinator(options) {
8
- const { sendNode, getCurrentCredentials } = options;
9
+ const { sendNode, getCurrentCredentials, resolvePrivacyTokenNode } = options;
9
10
  return {
10
11
  send: async (type) => {
11
12
  const credentials = getCurrentCredentials();
@@ -15,7 +16,12 @@ function createPresenceCoordinator(options) {
15
16
  await sendNode((0, chatstate_1.buildChatstateNode)({ jid, ...opts }));
16
17
  },
17
18
  subscribe: async (jid, opts) => {
18
- await sendNode((0, presence_1.buildPresenceSubscribeNode)({ jid, ...opts }));
19
+ const privacyTokenNode = (0, jid_1.isGroupJid)(jid) ? null : await resolvePrivacyTokenNode(jid);
20
+ await sendNode((0, presence_1.buildPresenceSubscribeNode)({
21
+ jid,
22
+ ...opts,
23
+ ...(privacyTokenNode ? { privacyTokenNode } : {})
24
+ }));
19
25
  }
20
26
  };
21
27
  }
@@ -149,6 +149,13 @@ interface WaProfileCoordinatorOptions {
149
149
  * idempotent (a no-op when the name already matches).
150
150
  */
151
151
  readonly applyOwnPushName: (name: string) => Promise<void>;
152
+ /**
153
+ * Resolves the receiver-mode `<tctoken>` node for a contact, echoed back on
154
+ * privacy-gated profile queries (picture get, about/status usync) to prove
155
+ * this account is a trusted contact. Returns `null` when no valid token is
156
+ * held for the JID.
157
+ */
158
+ readonly resolvePrivacyTokenNode: (jid: string) => Promise<BinaryNode | null>;
152
159
  readonly logger: Logger;
153
160
  }
154
161
  /** Builds a {@link WaProfileCoordinator} from its IQ/MEX/SID dependencies. */
@@ -195,10 +195,11 @@ function buildTextStatusMutationInput(input) {
195
195
  }
196
196
  /** Builds a {@link WaProfileCoordinator} from its IQ/MEX/SID dependencies. */
197
197
  function createProfileCoordinator(options) {
198
- const { queryWithContext, generateSid, mexSocket, queryLidsByPhoneJids, mutations, applyOwnPushName, logger } = options;
198
+ const { queryWithContext, generateSid, mexSocket, queryLidsByPhoneJids, mutations, applyOwnPushName, resolvePrivacyTokenNode, logger } = options;
199
199
  return {
200
200
  getProfilePicture: async (jid, type, existingId) => {
201
- const node = (0, profile_1.buildGetProfilePictureIq)(jid, type, existingId);
201
+ const privacyTokenNode = (await resolvePrivacyTokenNode(jid)) ?? undefined;
202
+ const node = (0, profile_1.buildGetProfilePictureIq)(jid, type, existingId, privacyTokenNode);
202
203
  const result = await queryWithContext('profile.getPicture', node, undefined, {
203
204
  jid,
204
205
  type: type ?? 'preview'
@@ -225,10 +226,11 @@ function createProfileCoordinator(options) {
225
226
  getStatus: async (jid) => {
226
227
  const sid = await generateSid();
227
228
  const queryNodes = (0, profile_1.buildGetStatusUsyncQueryNodes)();
229
+ const privacyTokenNode = await resolvePrivacyTokenNode(jid);
228
230
  const usyncNode = (0, usync_1.buildUsyncIq)({
229
231
  sid,
230
232
  queryProtocolNodes: [queryNodes[1]],
231
- users: [{ jid }]
233
+ users: [{ jid, ...(privacyTokenNode ? { content: [privacyTokenNode] } : {}) }]
232
234
  });
233
235
  const result = await queryWithContext('profile.getStatus', usyncNode, undefined, {
234
236
  jid
@@ -256,10 +258,14 @@ function createProfileCoordinator(options) {
256
258
  }
257
259
  const sid = await generateSid();
258
260
  const queryProtocolNodes = (0, profile_1.buildGetStatusUsyncQueryNodes)();
261
+ const users = await Promise.all(jids.map(async (jid) => {
262
+ const privacyTokenNode = await resolvePrivacyTokenNode(jid);
263
+ return { jid, ...(privacyTokenNode ? { content: [privacyTokenNode] } : {}) };
264
+ }));
259
265
  const usyncNode = (0, usync_1.buildUsyncIq)({
260
266
  sid,
261
267
  queryProtocolNodes,
262
- users: jids.map((jid) => ({ jid }))
268
+ users
263
269
  });
264
270
  const result = await queryWithContext('profile.getProfiles', usyncNode, undefined, {
265
271
  count: jids.length
@@ -502,7 +502,7 @@ class WaRetryCoordinator {
502
502
  id: request.keyBundle.key.id,
503
503
  publicKey: request.keyBundle.key.publicKey
504
504
  }
505
- });
505
+ }, { reuseExisting: true });
506
506
  return this.applySessionBaseKeyPolicy(request, requesterJid, requesterAddress, requesterNormalizedDeviceJid);
507
507
  }
508
508
  const sessionStillExists = currentSession !== null && !regIdMismatch;
@@ -41,6 +41,15 @@ export declare class WaTrustedContactTokenCoordinator {
41
41
  readonly getConfigOverrides?: () => Partial<WaTrustedContactTokenConfig>;
42
42
  });
43
43
  private resolveConfig;
44
+ /**
45
+ * Resolves the receiver-mode `<tctoken>` node to echo back on outbound
46
+ * queries against a contact's privacy-gated data (presence subscribe,
47
+ * profile-picture get, about/status usync). Returns the token only when a
48
+ * non-expired receiver token exists for `jid`; unlike
49
+ * {@link resolveTokenForMessage} it does **not** fall back to a `<cstoken>`
50
+ * (the gated query flows attach the trusted-contact token only).
51
+ */
52
+ resolveReceiverTokenNode(jid: string): Promise<BinaryNode | null>;
44
53
  resolveTokenForMessage(recipientJid: string): Promise<BinaryNode | null>;
45
54
  handleIncomingToken(fromJid: string, tokens: readonly ParsedPrivacyToken[]): Promise<void>;
46
55
  maybeIssueSenderToken(recipientJid: string): Promise<void>;
@@ -42,14 +42,30 @@ class WaTrustedContactTokenCoordinator {
42
42
  maxDurationS
43
43
  };
44
44
  }
45
- async resolveTokenForMessage(recipientJid) {
46
- const record = await this.store.getByJid(recipientJid);
47
- const nowS = this.serverClock.nowSeconds();
45
+ /**
46
+ * Resolves the receiver-mode `<tctoken>` node to echo back on outbound
47
+ * queries against a contact's privacy-gated data (presence subscribe,
48
+ * profile-picture get, about/status usync). Returns the token only when a
49
+ * non-expired receiver token exists for `jid`; unlike
50
+ * {@link resolveTokenForMessage} it does **not** fall back to a `<cstoken>`
51
+ * (the gated query flows attach the trusted-contact token only).
52
+ */
53
+ async resolveReceiverTokenNode(jid) {
54
+ const record = await this.store.getByJid(jid);
55
+ if (!record?.tcToken || record.tcTokenTimestamp === undefined) {
56
+ return null;
57
+ }
48
58
  const config = this.resolveConfig();
49
- if (record?.tcToken &&
50
- record.tcTokenTimestamp !== undefined &&
51
- !(0, tc_token_1.isTokenExpired)(record.tcTokenTimestamp, nowS, config.durationS, config.numBuckets)) {
52
- return (0, privacy_token_2.buildTcTokenMessageNode)(record.tcToken);
59
+ const nowS = this.serverClock.nowSeconds();
60
+ if ((0, tc_token_1.isTokenExpired)(record.tcTokenTimestamp, nowS, config.durationS, config.numBuckets)) {
61
+ return null;
62
+ }
63
+ return (0, privacy_token_2.buildTcTokenMessageNode)(record.tcToken);
64
+ }
65
+ async resolveTokenForMessage(recipientJid) {
66
+ const tcTokenNode = await this.resolveReceiverTokenNode(recipientJid);
67
+ if (tcTokenNode) {
68
+ return tcTokenNode;
53
69
  }
54
70
  const nctSalt = await this.getNctSalt();
55
71
  if (!nctSalt) {
@@ -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.
@@ -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) {
@@ -39,7 +39,7 @@ import { handleIncomingMessageAck } from '../message/primitives/incoming.js';
39
39
  import { createPeerDataOperationRequester } from '../message/primitives/peer-data-operation.js';
40
40
  import { WaMessageClient } from '../message/WaMessageClient.js';
41
41
  import { getWaCompanionPlatformId, WA_DEFAULTS, WA_DISCONNECT_REASONS, WA_NEWSLETTER_NOTIFICATION_TAGS, WA_NODE_TAGS, WA_NOTIFICATION_TYPES, WA_PRIVACY_TOKEN_NOTIFICATION_TYPE } from '../protocol/constants.js';
42
- import { isNewsletterJid, parseSignalAddressFromJid, toUserJid } from '../protocol/jid.js';
42
+ import { isNewsletterJid, isOwnAccountJid, parseSignalAddressFromJid, toUserJid } from '../protocol/jid.js';
43
43
  import { WA_PRESENCE_TYPES } from '../protocol/presence.js';
44
44
  import { createOutboundRetryTracker } from '../retry/tracker.js';
45
45
  import { SignalDeviceSyncApi } from '../signal/api/SignalDeviceSyncApi.js';
@@ -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) => {
@@ -483,7 +484,8 @@ export function buildWaClientDependencies(input) {
483
484
  });
484
485
  const presenceCoordinator = createPresenceCoordinator({
485
486
  sendNode: (node) => nodeOrchestrator.sendNode(node, false),
486
- getCurrentCredentials
487
+ getCurrentCredentials,
488
+ resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid)
487
489
  });
488
490
  const peerDataOperation = createPeerDataOperationRequester({
489
491
  logger,
@@ -555,9 +557,7 @@ export function buildWaClientDependencies(input) {
555
557
  const credentials = getCurrentCredentials();
556
558
  if (!credentials)
557
559
  return false;
558
- const candidateUser = toUserJid(deviceJid);
559
- return ((!!credentials.meJid && toUserJid(credentials.meJid) === candidateUser) ||
560
- (!!credentials.meLid && toUserJid(credentials.meLid) === candidateUser));
560
+ return isOwnAccountJid(deviceJid, credentials.meJid, credentials.meLid);
561
561
  },
562
562
  sendKeyShare: (toDeviceJid, keys, missingKeyIds) => messageDispatch.sendAppStateSyncKeyShare(toDeviceJid, keys, missingKeyIds),
563
563
  triggerSync: async () => {
@@ -600,6 +600,7 @@ export function buildWaClientDependencies(input) {
600
600
  queryLidsByPhoneJids: (phoneJids) => signalDeviceSync.queryLidsByPhoneJids(phoneJids),
601
601
  mutations: appStateMutations,
602
602
  applyOwnPushName,
603
+ resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid),
603
604
  logger
604
605
  });
605
606
  const statusCoordinator = createStatusCoordinator({
@@ -680,6 +681,7 @@ export function buildWaClientDependencies(input) {
680
681
  logger,
681
682
  sendNode: runtime.sendNode,
682
683
  getMeJid: () => getCurrentCredentials()?.meJid,
684
+ getMeLid: () => getCurrentCredentials()?.meLid,
683
685
  signalProtocol,
684
686
  senderKeyManager,
685
687
  onDecryptFailure: (context, error) => retryCoordinator.onDecryptFailure(context, error),
@@ -689,6 +691,7 @@ export function buildWaClientDependencies(input) {
689
691
  .catch((err) => runtime.handleError(toError(err)));
690
692
  },
691
693
  emitNewsletterMessageUpdate: (event) => runtime.emitEvent('newsletter_message_update', event),
694
+ emitUnavailableMessage: (event) => runtime.emitEvent('message_unavailable', event),
692
695
  emitUnhandledStanza: (event) => runtime.emitEvent('debug_unhandled_stanza', event)
693
696
  };
694
697
  const handleClientDirtyBits = (dirtyBits) => handleDirtyBits({