zapo-js 1.6.2 → 1.7.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 (141) hide show
  1. package/dist/abprops-spec.d.ts +10 -0
  2. package/dist/abprops-spec.js +25 -0
  3. package/dist/client/WaClient.d.ts +11 -0
  4. package/dist/client/WaClient.js +60 -0
  5. package/dist/client/WaClientFactory.d.ts +2 -2
  6. package/dist/client/WaClientFactory.js +50 -12
  7. package/dist/client/coordinators/WaAbPropsCoordinator.d.ts +2 -2
  8. package/dist/client/coordinators/WaAbPropsCoordinator.js +16 -4
  9. package/dist/client/coordinators/WaIncomingNodeCoordinator.d.ts +2 -1
  10. package/dist/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
  11. package/dist/client/coordinators/WaMessageCoordinator.d.ts +96 -0
  12. package/dist/client/coordinators/WaMessageCoordinator.js +163 -8
  13. package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +64 -0
  14. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +217 -30
  15. package/dist/client/coordinators/WaPrivacyCoordinator.d.ts +97 -19
  16. package/dist/client/coordinators/WaPrivacyCoordinator.js +152 -101
  17. package/dist/client/coordinators/WaRetryCoordinator.d.ts +22 -0
  18. package/dist/client/coordinators/WaRetryCoordinator.js +74 -11
  19. package/dist/client/events/dirty.d.ts +3 -0
  20. package/dist/client/events/dirty.js +12 -7
  21. package/dist/client/events/offline.d.ts +9 -0
  22. package/dist/client/events/offline.js +95 -0
  23. package/dist/client/events/privacy.d.ts +45 -0
  24. package/dist/client/events/privacy.js +130 -0
  25. package/dist/client/messaging/group-metadata.d.ts +12 -0
  26. package/dist/client/messaging/group-metadata.js +24 -8
  27. package/dist/client/messaging/link-preview.d.ts +7 -0
  28. package/dist/client/messaging/link-preview.js +27 -0
  29. package/dist/client/newsletter/content.d.ts +4 -1
  30. package/dist/client/newsletter/content.js +43 -18
  31. package/dist/client/newsletter/messaging.d.ts +2 -0
  32. package/dist/client/newsletter/messaging.js +2 -1
  33. package/dist/client/newsletter/mex.d.ts +2 -2
  34. package/dist/client/persistence/WriteBehindPersistence.js +2 -1
  35. package/dist/client/persistence/ephemeral-setting.d.ts +42 -0
  36. package/dist/client/persistence/ephemeral-setting.js +120 -0
  37. package/dist/client/persistence/group-history.d.ts +29 -0
  38. package/dist/client/persistence/group-history.js +149 -0
  39. package/dist/client/persistence/history-blob.d.ts +25 -0
  40. package/dist/client/persistence/history-blob.js +38 -0
  41. package/dist/client/persistence/history-sync.d.ts +2 -0
  42. package/dist/client/persistence/history-sync.js +31 -29
  43. package/dist/client/types.d.ts +150 -16
  44. package/dist/crypto/core/xeddsa.js +16 -0
  45. package/dist/crypto/curves/X25519.js +13 -0
  46. package/dist/crypto/nativeBackend.d.ts +20 -0
  47. package/dist/crypto/nativeBackend.js +95 -0
  48. package/dist/esm/abprops-spec.js +15 -0
  49. package/dist/esm/client/WaClient.js +60 -0
  50. package/dist/esm/client/WaClientFactory.js +51 -13
  51. package/dist/esm/client/coordinators/WaAbPropsCoordinator.js +17 -5
  52. package/dist/esm/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
  53. package/dist/esm/client/coordinators/WaMessageCoordinator.js +166 -11
  54. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +218 -32
  55. package/dist/esm/client/coordinators/WaPrivacyCoordinator.js +145 -94
  56. package/dist/esm/client/coordinators/WaRetryCoordinator.js +74 -11
  57. package/dist/esm/client/events/dirty.js +12 -7
  58. package/dist/esm/client/events/offline.js +92 -0
  59. package/dist/esm/client/events/privacy.js +123 -0
  60. package/dist/esm/client/messaging/group-metadata.js +25 -9
  61. package/dist/esm/client/messaging/link-preview.js +29 -2
  62. package/dist/esm/client/newsletter/content.js +44 -19
  63. package/dist/esm/client/newsletter/messaging.js +2 -1
  64. package/dist/esm/client/persistence/WriteBehindPersistence.js +2 -1
  65. package/dist/esm/client/persistence/ephemeral-setting.js +116 -0
  66. package/dist/esm/client/persistence/group-history.js +145 -0
  67. package/dist/esm/client/persistence/history-blob.js +34 -0
  68. package/dist/esm/client/persistence/history-sync.js +24 -22
  69. package/dist/esm/crypto/core/xeddsa.js +17 -1
  70. package/dist/esm/crypto/curves/X25519.js +13 -0
  71. package/dist/esm/crypto/nativeBackend.js +93 -0
  72. package/dist/esm/index.js +3 -1
  73. package/dist/esm/message/context-info.js +39 -0
  74. package/dist/esm/message/encode/content.js +20 -4
  75. package/dist/esm/message/encode/media-payload.js +3 -1
  76. package/dist/esm/message/kinds/group-history.js +36 -0
  77. package/dist/esm/message/primitives/incoming.js +18 -6
  78. package/dist/esm/protocol/abprops.js +18 -162
  79. package/dist/esm/protocol/constants.js +4 -3
  80. package/dist/esm/protocol/defaults.js +6 -0
  81. package/dist/esm/protocol/media.js +1 -0
  82. package/dist/esm/protocol/message.js +8 -0
  83. package/dist/esm/protocol/notification.js +1 -0
  84. package/dist/esm/protocol/privacy.js +70 -4
  85. package/dist/esm/store/contracts/chat-metadata.store.js +1 -0
  86. package/dist/esm/store/createStore.js +19 -2
  87. package/dist/esm/store/index.js +1 -0
  88. package/dist/esm/store/memory/chat-metadata.store.js +66 -0
  89. package/dist/esm/store/noop.store.js +8 -0
  90. package/dist/esm/transport/node/builders/privacy.js +63 -4
  91. package/dist/esm/util/coercion.js +12 -4
  92. package/dist/esm/util/index.js +1 -1
  93. package/dist/index.d.ts +11 -6
  94. package/dist/index.js +15 -2
  95. package/dist/media/types.d.ts +1 -1
  96. package/dist/message/context-info.d.ts +10 -0
  97. package/dist/message/context-info.js +40 -0
  98. package/dist/message/encode/content.d.ts +7 -3
  99. package/dist/message/encode/content.js +20 -4
  100. package/dist/message/encode/media-payload.d.ts +5 -2
  101. package/dist/message/encode/media-payload.js +3 -1
  102. package/dist/message/kinds/group-history.d.ts +30 -0
  103. package/dist/message/kinds/group-history.js +40 -0
  104. package/dist/message/primitives/incoming.d.ts +5 -0
  105. package/dist/message/primitives/incoming.js +18 -6
  106. package/dist/protocol/abprops.d.ts +27 -140
  107. package/dist/protocol/abprops.js +18 -163
  108. package/dist/protocol/constants.d.ts +4 -3
  109. package/dist/protocol/constants.js +10 -3
  110. package/dist/protocol/defaults.d.ts +6 -0
  111. package/dist/protocol/defaults.js +6 -0
  112. package/dist/protocol/media.d.ts +1 -0
  113. package/dist/protocol/media.js +1 -0
  114. package/dist/protocol/message.d.ts +6 -0
  115. package/dist/protocol/message.js +9 -0
  116. package/dist/protocol/notification.d.ts +1 -0
  117. package/dist/protocol/notification.js +1 -0
  118. package/dist/protocol/privacy.d.ts +52 -0
  119. package/dist/protocol/privacy.js +71 -5
  120. package/dist/store/contracts/chat-metadata.store.d.ts +34 -0
  121. package/dist/store/contracts/chat-metadata.store.js +2 -0
  122. package/dist/store/contracts/group-metadata.store.d.ts +7 -0
  123. package/dist/store/contracts/thread.store.d.ts +6 -0
  124. package/dist/store/createStore.d.ts +10 -2
  125. package/dist/store/createStore.js +18 -1
  126. package/dist/store/index.d.ts +3 -1
  127. package/dist/store/index.js +3 -1
  128. package/dist/store/memory/chat-metadata.store.d.ts +26 -0
  129. package/dist/store/memory/chat-metadata.store.js +70 -0
  130. package/dist/store/noop.store.d.ts +2 -0
  131. package/dist/store/noop.store.js +9 -1
  132. package/dist/store/types.d.ts +190 -39
  133. package/dist/transport/node/builders/privacy.d.ts +26 -2
  134. package/dist/transport/node/builders/privacy.js +63 -3
  135. package/dist/util/coercion.d.ts +6 -0
  136. package/dist/util/coercion.js +13 -4
  137. package/dist/util/index.d.ts +1 -1
  138. package/dist/util/index.js +2 -1
  139. package/package.json +3 -1
  140. package/spec/abprops/index.d.ts +4402 -0
  141. package/spec/abprops/index.js +4372 -0
@@ -9,12 +9,26 @@ 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 group_history_1 = require("../../message/kinds/group-history");
12
13
  const _proto_1 = require("../../proto");
13
14
  const jid_1 = require("../../protocol/jid");
14
15
  const client_1 = require("../../transport/node/mex/client");
15
16
  const bytes_1 = require("../../util/bytes");
16
17
  const coercion_1 = require("../../util/coercion");
17
18
  const primitives_1 = require("../../util/primitives");
19
+ /** MIME type the group-history bundle is uploaded and advertised under. */
20
+ const GROUP_HISTORY_BUNDLE_MIMETYPE = 'application/protobuf';
21
+ /** Oldest `messageTimestamp` across `messages`, in seconds; `undefined` when none carries one. */
22
+ function oldestTimestampSeconds(messages) {
23
+ let oldest;
24
+ for (let index = 0; index < messages.length; index += 1) {
25
+ const timestamp = (0, primitives_1.longToNumber)(messages[index].messageTimestamp);
26
+ if (timestamp > 0 && (oldest === undefined || timestamp < oldest)) {
27
+ oldest = timestamp;
28
+ }
29
+ }
30
+ return oldest;
31
+ }
18
32
  function parseReachoutTimelockMexResponse(data) {
19
33
  const root = data?.xwa2_fetch_account_reachout_timelock;
20
34
  return {
@@ -73,6 +87,8 @@ class WaMessageCoordinator {
73
87
  this.emitAddon = deps.emitAddon;
74
88
  this.mexSocket = deps.mexSocket;
75
89
  this.peerDataOperation = deps.peerDataOperation;
90
+ this.isGroupHistorySendEnabled = deps.isGroupHistorySendEnabled;
91
+ this.getAbPropNumber = deps.getAbPropNumber;
76
92
  }
77
93
  /**
78
94
  * Asks the server to backfill older messages for `chatJid` beyond what
@@ -89,13 +105,7 @@ class WaMessageCoordinator {
89
105
  */
90
106
  async requestHistorySync(input) {
91
107
  const chatJid = (0, jid_1.normalizeRecipientJid)(input.chatJid);
92
- if (input.count !== undefined) {
93
- if (!Number.isFinite(input.count) ||
94
- !Number.isSafeInteger(input.count) ||
95
- input.count <= 0) {
96
- throw new Error(`invalid count: ${input.count}`);
97
- }
98
- }
108
+ const onDemandMsgCount = (0, coercion_1.resolveOptionalPositive)(input.count, 'count');
99
109
  if (input.oldestMsgTimestampMs !== undefined) {
100
110
  if (!Number.isFinite(input.oldestMsgTimestampMs) ||
101
111
  !Number.isSafeInteger(input.oldestMsgTimestampMs) ||
@@ -113,10 +123,155 @@ class WaMessageCoordinator {
113
123
  ...(input.oldestMsgTimestampMs === undefined
114
124
  ? {}
115
125
  : { oldestMsgTimestampMs: input.oldestMsgTimestampMs }),
116
- ...(input.count === undefined ? {} : { onDemandMsgCount: input.count })
126
+ ...(onDemandMsgCount === undefined ? {} : { onDemandMsgCount })
117
127
  };
118
128
  return this.peerDataOperation.send(_proto_1.proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND, { historySyncOnDemandRequest });
119
129
  }
130
+ /**
131
+ * Shares recent group history with members who joined after the fact - the
132
+ * counterpart of the `group_history_bundle` event on the receiving side.
133
+ *
134
+ * Sends two messages: the bundle itself, encrypted per device and fanned
135
+ * out **only** to `toJids` (never to the whole group), followed by a hidden
136
+ * notice addressed to everyone so other clients can render the "history was
137
+ * shared" marker.
138
+ *
139
+ * WhatsApp gates the sender side per account through the `group_history_send`
140
+ * AB-prop and rejects the stanza with SMAX_INVALID when it is off, so this
141
+ * throws up front rather than spending an upload. Admin-only groups
142
+ * (`memberShareGroupHistoryMode: 'admin_share'`) reject a share from a
143
+ * regular member server-side - read `client.group.queryGroupMetadata()`
144
+ * first if you want to check that too.
145
+ *
146
+ * @throws when the account is not allowed to share group history, when
147
+ * `groupJid` is not a group, when any of `toJids` is not a current member
148
+ * (or is this account), or when there is nothing to bundle.
149
+ * @example
150
+ * ```ts
151
+ * await client.message.shareGroupHistory('12036@g.us', {
152
+ * toJids: ['5511999999999@s.whatsapp.net'],
153
+ * count: 50
154
+ * })
155
+ * ```
156
+ */
157
+ async shareGroupHistory(groupJid, input) {
158
+ const normalizedGroupJid = (0, jid_1.normalizeRecipientJid)(groupJid);
159
+ if (!(0, jid_1.isGroupJid)(normalizedGroupJid)) {
160
+ throw new Error(`shareGroupHistory requires a group jid: ${normalizedGroupJid}`);
161
+ }
162
+ if (input.toJids.length === 0) {
163
+ throw new Error('shareGroupHistory requires at least one recipient');
164
+ }
165
+ const messageLimit = (0, coercion_1.resolvePositive)(input.count, this.getAbPropNumber('group_history_message_count_limit'), 'count');
166
+ if (!this.isGroupHistorySendEnabled()) {
167
+ throw new Error('shareGroupHistory is disabled for this account (group_history_send is off)');
168
+ }
169
+ const audience = await this.messageDispatch.resolveGroupHistoryAudience(normalizedGroupJid, input.toJids);
170
+ if (audience.requestedSelf) {
171
+ throw new Error('shareGroupHistory cannot share history with this account itself');
172
+ }
173
+ if (audience.unknownJids.length > 0) {
174
+ throw new Error(`shareGroupHistory recipients are not members of the group (addressing mode: ${audience.addressingMode}): ${audience.unknownJids.join(', ')}`);
175
+ }
176
+ if (audience.historyReceivers.length === 0) {
177
+ throw new Error('shareGroupHistory resolved no recipients');
178
+ }
179
+ const messages = input.messages ??
180
+ (await this.loadGroupHistoryMessages(normalizedGroupJid, messageLimit, input.sinceMs));
181
+ if (messages.length === 0) {
182
+ throw new Error('shareGroupHistory found no messages to share');
183
+ }
184
+ const { compressed } = await (0, group_history_1.encodeGroupHistoryBundle)(messages, input.outOfWindowPinnedMessages);
185
+ const upload = await (0, messages_1.uploadMedia)(this.mediaUploadOptions, {
186
+ source: compressed,
187
+ cryptoType: 'group-history',
188
+ uploadPath: constants_1.MEDIA_UPLOAD_PATHS['group-history'],
189
+ contentType: GROUP_HISTORY_BUNDLE_MIMETYPE,
190
+ sidecar: false,
191
+ logLabel: 'group history bundle upload'
192
+ });
193
+ const oldestInWindow = oldestTimestampSeconds(messages);
194
+ const oldestPin = oldestTimestampSeconds(input.outOfWindowPinnedMessages ?? []);
195
+ const oldestInBundle = oldestPin === undefined
196
+ ? oldestInWindow
197
+ : oldestInWindow === undefined
198
+ ? oldestPin
199
+ : Math.min(oldestInWindow, oldestPin);
200
+ const metadata = {
201
+ historyReceivers: audience.historyReceivers,
202
+ nonHistoryReceivers: audience.nonHistoryReceivers,
203
+ messageCount: messages.length,
204
+ oldestMessageTimestampInWindow: oldestInWindow,
205
+ oldestMessageTimestampInBundle: oldestInBundle
206
+ };
207
+ const bundleResult = await this.send(normalizedGroupJid, {
208
+ messageHistoryBundle: {
209
+ mimetype: GROUP_HISTORY_BUNDLE_MIMETYPE,
210
+ fileSha256: upload.fileSha256,
211
+ fileEncSha256: upload.fileEncSha256,
212
+ mediaKey: upload.mediaKey,
213
+ directPath: upload.directPath,
214
+ mediaKeyTimestamp: this.mediaUploadOptions.serverClock.nowSeconds(),
215
+ messageHistoryMetadata: metadata
216
+ }
217
+ });
218
+ let noticeMessageId;
219
+ try {
220
+ noticeMessageId = (await this.send(normalizedGroupJid, {
221
+ messageHistoryNotice: { messageHistoryMetadata: metadata }
222
+ })).id;
223
+ }
224
+ catch (error) {
225
+ this.logger.warn('group history notice failed after the bundle was delivered', {
226
+ groupJid: normalizedGroupJid,
227
+ id: bundleResult.id,
228
+ receiverCount: audience.historyReceivers.length,
229
+ message: (0, primitives_1.toError)(error).message
230
+ });
231
+ }
232
+ this.logger.debug('shared group history', {
233
+ groupJid: normalizedGroupJid,
234
+ id: bundleResult.id,
235
+ messagesCount: messages.length,
236
+ receiverCount: audience.historyReceivers.length
237
+ });
238
+ return {
239
+ bundleMessageId: bundleResult.id,
240
+ noticeMessageId,
241
+ messagesCount: messages.length,
242
+ historyReceivers: audience.historyReceivers,
243
+ nonHistoryReceivers: audience.nonHistoryReceivers
244
+ };
245
+ }
246
+ /**
247
+ * Reads the most recent messages of a group back out of the mailbox store
248
+ * and rebuilds the `WebMessageInfo` shape a bundle carries. Yields nothing
249
+ * when the `messages` store domain is `'none'`.
250
+ */
251
+ async loadGroupHistoryMessages(groupJid, limit, sinceMs) {
252
+ const records = await this.messageStore.listByThread(groupJid, limit);
253
+ const messages = [];
254
+ for (let index = 0; index < records.length; index += 1) {
255
+ const record = records[index];
256
+ if (!record.messageBytes) {
257
+ continue;
258
+ }
259
+ if (sinceMs !== undefined && (record.timestampMs ?? 0) < sinceMs) {
260
+ continue;
261
+ }
262
+ messages[messages.length] = {
263
+ key: {
264
+ id: record.id,
265
+ remoteJid: groupJid,
266
+ fromMe: record.fromMe,
267
+ participant: record.participantJid ?? record.senderJid
268
+ },
269
+ message: _proto_1.proto.Message.decode(record.messageBytes),
270
+ messageTimestamp: Math.floor((record.timestampMs ?? 0) / 1000)
271
+ };
272
+ }
273
+ return messages;
274
+ }
120
275
  /**
121
276
  * Fetches the server-side "reachout" timelock that throttles cold outreach
122
277
  * to non-contacts, returning the active window when enforcement is on.
@@ -17,11 +17,13 @@ import type { SenderKeyManager } from '../../signal/group/SenderKeyManager';
17
17
  import type { SignalSessionResolver } from '../../signal/session/resolver';
18
18
  import type { SignalProtocol } from '../../signal/session/SignalProtocol';
19
19
  import type { SignalAddress } from '../../signal/types';
20
+ import type { WaChatMetadataStore } from '../../store/contracts/chat-metadata.store';
20
21
  import type { WaDeviceListStore } from '../../store/contracts/device-list.store';
21
22
  import type { WaIdentityStore } from '../../store/contracts/identity.store';
22
23
  import type { WaMessageSecretStore } from '../../store/contracts/message-secret.store';
23
24
  import type { WaSessionStore } from '../../store/contracts/session.store';
24
25
  import type { WaSignalStore } from '../../store/contracts/signal.store';
26
+ import type { WaThreadStore } from '../../store/contracts/thread.store';
25
27
  import type { BinaryNode } from '../../transport/types';
26
28
  import type { ServerClock } from '../../util/clock';
27
29
  interface WaMessageDispatchCoordinatorOptions {
@@ -45,6 +47,8 @@ interface WaMessageDispatchCoordinatorOptions {
45
47
  readonly sessionStore: WaSessionStore;
46
48
  readonly identityStore: WaIdentityStore;
47
49
  readonly deviceListStore: WaDeviceListStore;
50
+ readonly threadStore: WaThreadStore;
51
+ readonly chatMetadataStore: WaChatMetadataStore;
48
52
  readonly signalDeviceSync: SignalDeviceSyncApi;
49
53
  readonly messageSecretStore: WaMessageSecretStore;
50
54
  /**
@@ -66,6 +70,32 @@ interface WaMessageDispatchCoordinatorOptions {
66
70
  readonly mobileMessageIdFormat?: () => boolean;
67
71
  readonly serverClock: ServerClock;
68
72
  }
73
+ /** Membership split behind a group-history share, in the group's addressing mode. */
74
+ export interface WaGroupHistoryAudience {
75
+ /** Members the bundle is addressed to. */
76
+ readonly historyReceivers: readonly string[];
77
+ /** Members in the group who are not receiving it. */
78
+ readonly nonHistoryReceivers: readonly string[];
79
+ /** Requested JIDs that are not current members of the group. */
80
+ readonly unknownJids: readonly string[];
81
+ /** Whether the caller asked to share with this account itself. */
82
+ readonly requestedSelf: boolean;
83
+ readonly addressingMode: 'pn' | 'lid';
84
+ }
85
+ /**
86
+ * Narrows a group's participant list to the members a group-history bundle is
87
+ * addressed to, plus the sender so its own devices keep a copy. Receivers are
88
+ * intersected with the live participant list, so a stale receiver entry cannot
89
+ * pull a non-member into the fanout. Any other message keeps the full list.
90
+ *
91
+ * Receivers must be written in the group's addressing mode - a PN entry for a
92
+ * LID-addressed group does not match and is reported through `onDropped`.
93
+ *
94
+ * @throws when no receiver matches a current member. Sending to the sender's
95
+ * own devices alone counts as no match: it would look like a delivered share
96
+ * while nobody received anything.
97
+ */
98
+ export declare function restrictGroupHistoryTargets(message: Proto.IMessage, participantUserJids: readonly string[], senderJid: string, onDropped?: (droppedJids: readonly string[], totalExpected: number) => void): readonly string[];
69
99
  export declare class WaMessageDispatchCoordinator {
70
100
  private readonly deps;
71
101
  private readonly mobileMessageIdFormat;
@@ -74,6 +104,24 @@ export declare class WaMessageDispatchCoordinator {
74
104
  private readonly privacyTokenDedup;
75
105
  private readonly distributionDedup;
76
106
  constructor(options: WaMessageDispatchCoordinatorOptions);
107
+ /**
108
+ * Per-chat disappearing settings from the cache, falling back to the
109
+ * durable thread record on a cold miss and warming the cache with it. A
110
+ * chat with no disappearing mode is cached as expiration `0` rather than
111
+ * left absent, so the archive is not re-read on every send to it; the
112
+ * incoming writers overwrite that entry the moment the chat turns it on.
113
+ * A failing archive degrades the send to "no ephemeral fields" instead of
114
+ * rejecting it - the store is opt-in persistence, not a send dependency.
115
+ */
116
+ private resolveChatMetadata;
117
+ /**
118
+ * Disappearing-message fields for a send into a chat with the mode on, or
119
+ * `{}` when nothing resolves. Both chat kinds report initiator
120
+ * `CHANGED_IN_CHAT` when nothing more specific is stored; the trigger comes
121
+ * from the group metadata for a group and defaults to `CHAT_SETTING` for
122
+ * 1:1, and is omitted when a group reports none.
123
+ */
124
+ private resolveChatEphemeral;
77
125
  publishMessageNode(node: BinaryNode, options?: WaMessagePublishOptions): Promise<WaMessagePublishResult>;
78
126
  publishEncryptedMessage(input: WaEncryptedMessageInput, options?: WaMessagePublishOptions): Promise<WaMessagePublishResult>;
79
127
  publishSignalMessage(input: WaSignalMessagePublishInput, options?: WaMessagePublishOptions): Promise<WaMessagePublishResult>;
@@ -119,6 +167,22 @@ export declare class WaMessageDispatchCoordinator {
119
167
  requestAppStateSyncKeys(keyIds: readonly Uint8Array[]): Promise<readonly string[]>;
120
168
  sendAppStateSyncKeyShare(toDeviceJid: string, keys: readonly WaAppStateSyncKey[], missingKeyIds?: readonly Uint8Array[]): Promise<void>;
121
169
  mutateGroupMetadataCacheFromGroupEvent(event: WaGroupEvent): Promise<void>;
170
+ /**
171
+ * Splits a group's members into the ones a history bundle is being shared
172
+ * with and the rest, normalizing everything to the group's addressing mode
173
+ * so the receiver list written into the proto matches what the fanout can
174
+ * actually resolve. This account is excluded from both lists.
175
+ *
176
+ * Requested JIDs that are not current members come back in `unknownJids`
177
+ * instead of being silently dropped - sharing history with a non-member is
178
+ * a caller mistake worth surfacing.
179
+ */
180
+ resolveGroupHistoryAudience(groupJid: string, requestedJids: readonly string[]): Promise<WaGroupHistoryAudience>;
181
+ /**
182
+ * A group-history bundle goes to the members named in `historyReceivers`
183
+ * rather than to the whole group, so it takes the per-device direct fanout
184
+ * instead of the group sender key.
185
+ */
122
186
  private shouldUseGroupDirectPath;
123
187
  private publishGroupDirectMessage;
124
188
  private buildBotSidecarParticipant;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WaMessageDispatchCoordinator = void 0;
4
+ exports.restrictGroupHistoryTargets = restrictGroupHistoryTargets;
4
5
  const _crypto_1 = require("../../crypto");
5
6
  const primitives_1 = require("../../crypto/core/primitives");
6
7
  const PromiseDedup_1 = require("../../infra/perf/PromiseDedup");
@@ -20,6 +21,58 @@ const binary_1 = require("../../transport/binary");
20
21
  const message_1 = require("../../transport/node/builders/message");
21
22
  const bytes_1 = require("../../util/bytes");
22
23
  const primitives_2 = require("../../util/primitives");
24
+ /**
25
+ * Members a group-history bundle is addressed to. Empty for every other
26
+ * message, which is what keeps the restricted fanout scoped to bundles.
27
+ */
28
+ function resolveGroupHistoryReceivers(message) {
29
+ const metadata = (0, content_1.unwrapMessage)(message).messageHistoryBundle?.messageHistoryMetadata;
30
+ return metadata?.historyReceivers ?? [];
31
+ }
32
+ /**
33
+ * Narrows a group's participant list to the members a group-history bundle is
34
+ * addressed to, plus the sender so its own devices keep a copy. Receivers are
35
+ * intersected with the live participant list, so a stale receiver entry cannot
36
+ * pull a non-member into the fanout. Any other message keeps the full list.
37
+ *
38
+ * Receivers must be written in the group's addressing mode - a PN entry for a
39
+ * LID-addressed group does not match and is reported through `onDropped`.
40
+ *
41
+ * @throws when no receiver matches a current member. Sending to the sender's
42
+ * own devices alone counts as no match: it would look like a delivered share
43
+ * while nobody received anything.
44
+ */
45
+ function restrictGroupHistoryTargets(message, participantUserJids, senderJid, onDropped) {
46
+ const receivers = resolveGroupHistoryReceivers(message);
47
+ if (receivers.length === 0) {
48
+ return participantUserJids;
49
+ }
50
+ const pending = new Set();
51
+ for (let index = 0; index < receivers.length; index += 1) {
52
+ pending.add((0, jid_1.toUserJid)(receivers[index]));
53
+ }
54
+ const senderUserJid = (0, jid_1.toUserJid)(senderJid);
55
+ const targets = [];
56
+ let matchedReceivers = 0;
57
+ for (let index = 0; index < participantUserJids.length; index += 1) {
58
+ const participant = participantUserJids[index];
59
+ const participantUserJid = (0, jid_1.toUserJid)(participant);
60
+ if (pending.delete(participantUserJid)) {
61
+ matchedReceivers += 1;
62
+ targets[targets.length] = participant;
63
+ }
64
+ else if (participantUserJid === senderUserJid) {
65
+ targets[targets.length] = participant;
66
+ }
67
+ }
68
+ if (pending.size > 0 && onDropped) {
69
+ onDropped([...pending], receivers.length);
70
+ }
71
+ if (matchedReceivers === 0) {
72
+ throw new Error('group history bundle resolved no receivers in the group');
73
+ }
74
+ return targets;
75
+ }
23
76
  class WaMessageDispatchCoordinator {
24
77
  constructor(options) {
25
78
  this.icdcDedup = new PromiseDedup_1.PromiseDedup();
@@ -29,6 +82,82 @@ class WaMessageDispatchCoordinator {
29
82
  this.mobileMessageIdFormat = options.mobileMessageIdFormat ?? (() => false);
30
83
  this.serverClock = options.serverClock;
31
84
  }
85
+ /**
86
+ * Per-chat disappearing settings from the cache, falling back to the
87
+ * durable thread record on a cold miss and warming the cache with it. A
88
+ * chat with no disappearing mode is cached as expiration `0` rather than
89
+ * left absent, so the archive is not re-read on every send to it; the
90
+ * incoming writers overwrite that entry the moment the chat turns it on.
91
+ * A failing archive degrades the send to "no ephemeral fields" instead of
92
+ * rejecting it - the store is opt-in persistence, not a send dependency.
93
+ */
94
+ async resolveChatMetadata(chatJid) {
95
+ try {
96
+ const cached = await this.deps.chatMetadataStore.getChatMetadata(chatJid);
97
+ if (cached) {
98
+ return cached;
99
+ }
100
+ const thread = await this.deps.threadStore.getByJid(chatJid);
101
+ const expiration = thread?.ephemeralExpiration ?? 0;
102
+ const settingTimestamp = expiration > 0 ? thread?.ephemeralSettingTimestamp : undefined;
103
+ const snapshot = {
104
+ chatJid,
105
+ ephemeralExpiration: expiration,
106
+ ...(settingTimestamp !== undefined
107
+ ? { ephemeralSettingTimestamp: settingTimestamp }
108
+ : {}),
109
+ updatedAtMs: Date.now()
110
+ };
111
+ await this.deps.chatMetadataStore.upsertChatMetadata(snapshot);
112
+ return snapshot;
113
+ }
114
+ catch (error) {
115
+ this.deps.logger.debug('chat metadata lookup failed, sending without ephemeral fields', {
116
+ jid: chatJid,
117
+ message: (0, primitives_2.toError)(error).message
118
+ });
119
+ return null;
120
+ }
121
+ }
122
+ /**
123
+ * Disappearing-message fields for a send into a chat with the mode on, or
124
+ * `{}` when nothing resolves. Both chat kinds report initiator
125
+ * `CHANGED_IN_CHAT` when nothing more specific is stored; the trigger comes
126
+ * from the group metadata for a group and defaults to `CHAT_SETTING` for
127
+ * 1:1, and is omitted when a group reports none.
128
+ */
129
+ async resolveChatEphemeral(recipientJid) {
130
+ if ((0, jid_1.isGroupJid)(recipientJid)) {
131
+ const settings = await this.deps.groupMetadataCache.resolveEphemeralSettings(recipientJid);
132
+ if (!settings) {
133
+ return {};
134
+ }
135
+ return {
136
+ expirationSeconds: settings.expirationSeconds,
137
+ disappearingModeInitiator: _proto_1.proto.DisappearingMode.Initiator.CHANGED_IN_CHAT,
138
+ ...(settings.trigger !== undefined
139
+ ? {
140
+ disappearingModeTrigger: settings.trigger
141
+ }
142
+ : {})
143
+ };
144
+ }
145
+ const chat = await this.resolveChatMetadata(recipientJid);
146
+ const expiration = chat?.ephemeralExpiration;
147
+ if (expiration === undefined || expiration <= 0) {
148
+ return {};
149
+ }
150
+ const stored = chat?.ephemeralSettingTimestamp;
151
+ const settingTimestamp = stored !== undefined ? (0, constants_1.normalizeEphemeralSettingSeconds)(stored) : undefined;
152
+ return {
153
+ expirationSeconds: expiration,
154
+ disappearingModeInitiator: _proto_1.proto.DisappearingMode.Initiator.CHANGED_IN_CHAT,
155
+ disappearingModeTrigger: _proto_1.proto.DisappearingMode.Trigger.CHAT_SETTING,
156
+ ...(settingTimestamp !== undefined
157
+ ? { ephemeralSettingTimestamp: settingTimestamp }
158
+ : {})
159
+ };
160
+ }
32
161
  async publishMessageNode(node, options = {}) {
33
162
  this.deps.logger.trace('wa client publish message node', {
34
163
  tag: node.tag,
@@ -149,17 +278,34 @@ class WaMessageDispatchCoordinator {
149
278
  this.withResolvedMessageId(options)
150
279
  ]);
151
280
  }
281
+ const isGroupRecipient = (0, jid_1.isGroupJid)(recipientJid);
282
+ const directRecipientJid = isGroupRecipient
283
+ ? recipientJid
284
+ : await this.resolveDirectRecipientLid((0, jid_1.toUserJid)(recipientJid));
152
285
  let optionsCtx = options.contextInfo;
153
286
  if (options.expirationSeconds !== undefined) {
154
287
  optionsCtx = { ...optionsCtx, expirationSeconds: options.expirationSeconds };
155
288
  }
156
- if ((0, jid_1.isGroupJid)(recipientJid) &&
157
- optionsCtx?.expirationSeconds === undefined &&
158
- !options.disableGroupEphemeralAutoInject) {
159
- const cachedEphemeral = await this.deps.groupMetadataCache.resolveEphemeral(recipientJid);
160
- if (cachedEphemeral !== null && cachedEphemeral > 0) {
161
- optionsCtx = { ...optionsCtx, expirationSeconds: cachedEphemeral };
162
- }
289
+ if (options.ephemeralSettingTimestamp !== undefined) {
290
+ optionsCtx = {
291
+ ...optionsCtx,
292
+ ephemeralSettingTimestamp: options.ephemeralSettingTimestamp
293
+ };
294
+ }
295
+ if (options.disappearingModeTrigger !== undefined) {
296
+ optionsCtx = {
297
+ ...optionsCtx,
298
+ disappearingModeTrigger: options.disappearingModeTrigger
299
+ };
300
+ }
301
+ const autoInjectDisabled = isGroupRecipient
302
+ ? options.disableGroupEphemeralAutoInject
303
+ : options.disableDirectEphemeralAutoInject;
304
+ if (optionsCtx?.expirationSeconds === undefined && !autoInjectDisabled) {
305
+ optionsCtx = {
306
+ ...(await this.resolveChatEphemeral(directRecipientJid)),
307
+ ...optionsCtx
308
+ };
163
309
  }
164
310
  const ctx = (0, context_info_1.resolveSendContextInfo)({
165
311
  contentLevel: pickContentContextInfo(content),
@@ -285,9 +431,6 @@ class WaMessageDispatchCoordinator {
285
431
  customNodes: customNodes.length > 0 ? customNodes : undefined,
286
432
  sendOptions
287
433
  };
288
- const directRecipientJid = isGroup
289
- ? recipientJid
290
- : await this.resolveDirectRecipientLid((0, jid_1.toUserJid)(recipientJid));
291
434
  const peerRecipientPn = isGroup
292
435
  ? undefined
293
436
  : await this.resolvePeerRecipientPn((0, jid_1.toUserJid)(recipientJid), directRecipientJid);
@@ -550,9 +693,59 @@ class WaMessageDispatchCoordinator {
550
693
  async mutateGroupMetadataCacheFromGroupEvent(event) {
551
694
  await this.deps.groupMetadataCache.mutateFromGroupEvent(event);
552
695
  }
553
- // noop for now
696
+ /**
697
+ * Splits a group's members into the ones a history bundle is being shared
698
+ * with and the rest, normalizing everything to the group's addressing mode
699
+ * so the receiver list written into the proto matches what the fanout can
700
+ * actually resolve. This account is excluded from both lists.
701
+ *
702
+ * Requested JIDs that are not current members come back in `unknownJids`
703
+ * instead of being silently dropped - sharing history with a non-member is
704
+ * a caller mistake worth surfacing.
705
+ */
706
+ async resolveGroupHistoryAudience(groupJid, requestedJids) {
707
+ const participants = await this.deps.groupMetadataCache.resolveParticipantUsers(groupJid);
708
+ const meJid = this.requireCurrentMeJid('shareGroupHistory');
709
+ const addressingMode = this.resolveGroupAddressingMode(participants, groupJid);
710
+ const meUserJid = (0, jid_1.toUserJid)(this.resolveSenderForAddressingMode(addressingMode, meJid));
711
+ const pending = new Set();
712
+ for (let index = 0; index < requestedJids.length; index += 1) {
713
+ pending.add((0, jid_1.toUserJid)((0, jid_1.normalizeRecipientJid)(requestedJids[index])));
714
+ }
715
+ const historyReceivers = [];
716
+ const nonHistoryReceivers = [];
717
+ for (let index = 0; index < participants.length; index += 1) {
718
+ const participantUserJid = (0, jid_1.toUserJid)(participants[index]);
719
+ if (participantUserJid === meUserJid) {
720
+ continue;
721
+ }
722
+ if (pending.delete(participantUserJid)) {
723
+ historyReceivers[historyReceivers.length] = participantUserJid;
724
+ }
725
+ else {
726
+ nonHistoryReceivers[nonHistoryReceivers.length] = participantUserJid;
727
+ }
728
+ }
729
+ const requestedSelf = pending.delete(meUserJid);
730
+ const unknownJids = [];
731
+ for (const jid of pending) {
732
+ unknownJids[unknownJids.length] = jid;
733
+ }
734
+ return {
735
+ historyReceivers,
736
+ nonHistoryReceivers,
737
+ unknownJids,
738
+ requestedSelf,
739
+ addressingMode
740
+ };
741
+ }
742
+ /**
743
+ * A group-history bundle goes to the members named in `historyReceivers`
744
+ * rather than to the whole group, so it takes the per-device direct fanout
745
+ * instead of the group sender key.
746
+ */
554
747
  shouldUseGroupDirectPath(message) {
555
- return false;
748
+ return resolveGroupHistoryReceivers(message).length > 0;
556
749
  }
557
750
  async publishGroupDirectMessage(groupJid, envelope, retryContext = {}) {
558
751
  const { message, plaintext, type, edit, mediatype, sendOptions } = envelope;
@@ -562,8 +755,16 @@ class WaMessageDispatchCoordinator {
562
755
  : await this.deps.groupMetadataCache.resolveParticipantUsers(groupJid);
563
756
  const addressingMode = retryContext.forceAddressingMode ??
564
757
  this.resolveGroupAddressingMode(participantUserJids, groupJid);
565
- const senderForPhash = this.resolveSenderForAddressingMode(addressingMode, meJid);
566
- const fanoutDeviceJids = await this.deps.fanoutResolver.resolveGroupParticipantDeviceJids(participantUserJids);
758
+ const senderJid = this.resolveSenderForAddressingMode(addressingMode, meJid);
759
+ const targetUserJids = restrictGroupHistoryTargets(message, participantUserJids, senderJid, (droppedJids, totalExpected) => {
760
+ this.deps.logger.warn('group history receivers missing from the participant list', {
761
+ groupJid,
762
+ droppedCount: droppedJids.length,
763
+ totalExpected,
764
+ sample: droppedJids.slice(0, 3)
765
+ });
766
+ });
767
+ const fanoutDeviceJids = await this.deps.fanoutResolver.resolveGroupParticipantDeviceJids(targetUserJids);
567
768
  if (fanoutDeviceJids.length === 0) {
568
769
  throw new Error('group direct send resolved no target devices');
569
770
  }
@@ -618,22 +819,10 @@ class WaMessageDispatchCoordinator {
618
819
  break;
619
820
  }
620
821
  }
621
- const phashTargets = new Array(resolvedFanoutTargets.length + 1);
622
- let phashTargetCount = 0;
623
- for (let index = 0; index < resolvedFanoutTargets.length; index += 1) {
624
- const candidate = resolvedFanoutTargets[index].jid;
625
- if ((0, jid_1.isHostedDeviceJid)(candidate))
626
- continue;
627
- phashTargets[phashTargetCount] = candidate;
628
- phashTargetCount += 1;
629
- }
630
- phashTargets[phashTargetCount] = senderForPhash;
631
- phashTargets.length = phashTargetCount + 1;
632
- const localPhash = (0, phash_1.computePhashV2)(phashTargets);
633
822
  const reportingArtifacts = await this.tryBuildReportingTokenArtifacts({
634
823
  message,
635
824
  stanzaId: sendOptions.id,
636
- senderUserJid: (0, jid_1.toUserJid)(senderForPhash),
825
+ senderUserJid: (0, jid_1.toUserJid)(senderJid),
637
826
  remoteJid: groupJid,
638
827
  context: 'group_direct'
639
828
  });
@@ -645,7 +834,6 @@ class WaMessageDispatchCoordinator {
645
834
  type,
646
835
  id: sendOptions.id,
647
836
  edit,
648
- phash: localPhash,
649
837
  addressingMode,
650
838
  participants,
651
839
  deviceIdentity: shouldAttachDeviceIdentity
@@ -671,7 +859,7 @@ class WaMessageDispatchCoordinator {
671
859
  const ackError = result.ack.error;
672
860
  const serverPhash = result.ack.phash;
673
861
  const serverAddressingMode = result.ack.addressingMode;
674
- const hasPhashMismatch = !!serverPhash && serverPhash !== localPhash;
862
+ const hasPhashMismatch = !!serverPhash;
675
863
  const hasAddressingMismatch = !!serverAddressingMode && serverAddressingMode !== addressingMode;
676
864
  const hasAddressingError = ackError === constants_1.WA_NACK_REASONS.STALE_GROUP_ADDRESSING_MODE;
677
865
  if (!retryContext.retried &&
@@ -679,7 +867,6 @@ class WaMessageDispatchCoordinator {
679
867
  this.deps.logger.warn('group direct publish acknowledged with mismatch metadata', {
680
868
  id: result.id,
681
869
  groupJid,
682
- localPhash,
683
870
  serverPhash,
684
871
  localAddressingMode: addressingMode,
685
872
  serverAddressingMode,