zapo-js 1.6.2 → 1.7.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.
Files changed (218) hide show
  1. package/README.md +26 -12
  2. package/dist/abprops-spec.d.ts +10 -0
  3. package/dist/abprops-spec.js +25 -0
  4. package/dist/appstate/crypto/WaAppStateCrypto.js +4 -6
  5. package/dist/auth/WaAuthClient.d.ts +14 -1
  6. package/dist/auth/WaAuthClient.js +22 -1
  7. package/dist/auth/credentials-flow.d.ts +3 -1
  8. package/dist/auth/credentials-flow.js +3 -0
  9. package/dist/auth/pairing/WaPairingFlow.d.ts +5 -0
  10. package/dist/auth/pairing/WaPairingFlow.js +66 -14
  11. package/dist/auth/types.d.ts +13 -0
  12. package/dist/client/WaClient.d.ts +11 -0
  13. package/dist/client/WaClient.js +60 -0
  14. package/dist/client/WaClientFactory.d.ts +2 -2
  15. package/dist/client/WaClientFactory.js +53 -13
  16. package/dist/client/coordinators/WaAbPropsCoordinator.d.ts +2 -2
  17. package/dist/client/coordinators/WaAbPropsCoordinator.js +16 -4
  18. package/dist/client/coordinators/WaIncomingNodeCoordinator.d.ts +2 -1
  19. package/dist/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
  20. package/dist/client/coordinators/WaMessageCoordinator.d.ts +96 -0
  21. package/dist/client/coordinators/WaMessageCoordinator.js +163 -8
  22. package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +64 -0
  23. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +252 -57
  24. package/dist/client/coordinators/WaPrivacyCoordinator.d.ts +97 -19
  25. package/dist/client/coordinators/WaPrivacyCoordinator.js +152 -101
  26. package/dist/client/coordinators/WaRetryCoordinator.d.ts +22 -0
  27. package/dist/client/coordinators/WaRetryCoordinator.js +74 -11
  28. package/dist/client/events/dirty.d.ts +3 -0
  29. package/dist/client/events/dirty.js +12 -7
  30. package/dist/client/events/offline.d.ts +9 -0
  31. package/dist/client/events/offline.js +95 -0
  32. package/dist/client/events/privacy.d.ts +45 -0
  33. package/dist/client/events/privacy.js +130 -0
  34. package/dist/client/messaging/group-metadata.d.ts +12 -0
  35. package/dist/client/messaging/group-metadata.js +24 -8
  36. package/dist/client/messaging/link-preview.d.ts +7 -0
  37. package/dist/client/messaging/link-preview.js +27 -0
  38. package/dist/client/messaging/messages.js +16 -12
  39. package/dist/client/newsletter/content.d.ts +4 -1
  40. package/dist/client/newsletter/content.js +43 -18
  41. package/dist/client/newsletter/discovery.js +4 -2
  42. package/dist/client/newsletter/messaging.d.ts +2 -0
  43. package/dist/client/newsletter/messaging.js +2 -1
  44. package/dist/client/newsletter/mex.d.ts +2 -2
  45. package/dist/client/persistence/WriteBehindPersistence.js +2 -1
  46. package/dist/client/persistence/ephemeral-setting.d.ts +42 -0
  47. package/dist/client/persistence/ephemeral-setting.js +120 -0
  48. package/dist/client/persistence/group-history.d.ts +29 -0
  49. package/dist/client/persistence/group-history.js +149 -0
  50. package/dist/client/persistence/history-blob.d.ts +25 -0
  51. package/dist/client/persistence/history-blob.js +38 -0
  52. package/dist/client/persistence/history-sync.d.ts +20 -0
  53. package/dist/client/persistence/history-sync.js +165 -71
  54. package/dist/client/plugins/types.d.ts +10 -2
  55. package/dist/client/types.d.ts +150 -16
  56. package/dist/crypto/core/index.d.ts +1 -1
  57. package/dist/crypto/core/index.js +2 -1
  58. package/dist/crypto/core/primitives.d.ts +19 -0
  59. package/dist/crypto/core/primitives.js +48 -0
  60. package/dist/crypto/core/xeddsa.js +35 -4
  61. package/dist/crypto/curves/X25519.js +29 -2
  62. package/dist/crypto/nativeBackend.d.ts +20 -0
  63. package/dist/crypto/nativeBackend.js +95 -0
  64. package/dist/esm/abprops-spec.js +15 -0
  65. package/dist/esm/appstate/crypto/WaAppStateCrypto.js +4 -6
  66. package/dist/esm/auth/WaAuthClient.js +22 -1
  67. package/dist/esm/auth/credentials-flow.js +3 -0
  68. package/dist/esm/auth/pairing/WaPairingFlow.js +67 -15
  69. package/dist/esm/client/WaClient.js +60 -0
  70. package/dist/esm/client/WaClientFactory.js +54 -14
  71. package/dist/esm/client/coordinators/WaAbPropsCoordinator.js +17 -5
  72. package/dist/esm/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
  73. package/dist/esm/client/coordinators/WaMessageCoordinator.js +166 -11
  74. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +254 -60
  75. package/dist/esm/client/coordinators/WaPrivacyCoordinator.js +145 -94
  76. package/dist/esm/client/coordinators/WaRetryCoordinator.js +74 -11
  77. package/dist/esm/client/events/dirty.js +12 -7
  78. package/dist/esm/client/events/offline.js +92 -0
  79. package/dist/esm/client/events/privacy.js +123 -0
  80. package/dist/esm/client/messaging/group-metadata.js +25 -9
  81. package/dist/esm/client/messaging/link-preview.js +29 -2
  82. package/dist/esm/client/messaging/messages.js +16 -12
  83. package/dist/esm/client/newsletter/content.js +44 -19
  84. package/dist/esm/client/newsletter/discovery.js +4 -2
  85. package/dist/esm/client/newsletter/messaging.js +2 -1
  86. package/dist/esm/client/persistence/WriteBehindPersistence.js +2 -1
  87. package/dist/esm/client/persistence/ephemeral-setting.js +116 -0
  88. package/dist/esm/client/persistence/group-history.js +145 -0
  89. package/dist/esm/client/persistence/history-blob.js +34 -0
  90. package/dist/esm/client/persistence/history-sync.js +157 -65
  91. package/dist/esm/crypto/core/index.js +1 -1
  92. package/dist/esm/crypto/core/primitives.js +47 -0
  93. package/dist/esm/crypto/core/xeddsa.js +36 -5
  94. package/dist/esm/crypto/curves/X25519.js +29 -2
  95. package/dist/esm/crypto/nativeBackend.js +93 -0
  96. package/dist/esm/index.js +3 -1
  97. package/dist/esm/media/crypto/WaMediaCrypto.js +38 -19
  98. package/dist/esm/message/context-info.js +39 -0
  99. package/dist/esm/message/crypto/reporting-token.js +38 -216
  100. package/dist/esm/message/encode/content.js +73 -12
  101. package/dist/esm/message/encode/media-payload.js +3 -1
  102. package/dist/esm/message/kinds/group-history.js +36 -0
  103. package/dist/esm/message/primitives/incoming.js +32 -13
  104. package/dist/esm/protocol/abprops.js +18 -162
  105. package/dist/esm/protocol/constants.js +4 -3
  106. package/dist/esm/protocol/defaults.js +7 -0
  107. package/dist/esm/protocol/device-identity.js +10 -5
  108. package/dist/esm/protocol/jid.js +83 -36
  109. package/dist/esm/protocol/media.js +1 -0
  110. package/dist/esm/protocol/message.js +8 -0
  111. package/dist/esm/protocol/notification.js +1 -0
  112. package/dist/esm/protocol/privacy.js +70 -4
  113. package/dist/esm/signal/group/SenderKeyChain.js +1 -2
  114. package/dist/esm/signal/group/SenderKeyManager.js +1 -7
  115. package/dist/esm/signal/session/SignalProtocol.js +22 -21
  116. package/dist/esm/signal/session/SignalRatchet.js +20 -17
  117. package/dist/esm/signal/session/encoding.js +25 -3
  118. package/dist/esm/signal/session/resolver.js +11 -5
  119. package/dist/esm/store/contracts/chat-metadata.store.js +1 -0
  120. package/dist/esm/store/createStore.js +19 -2
  121. package/dist/esm/store/index.js +1 -0
  122. package/dist/esm/store/memory/chat-metadata.store.js +66 -0
  123. package/dist/esm/store/noop.store.js +8 -0
  124. package/dist/esm/transport/binary/constants.js +19 -0
  125. package/dist/esm/transport/binary/decoder.js +17 -19
  126. package/dist/esm/transport/binary/encoder.js +61 -18
  127. package/dist/esm/transport/node/builders/global.js +20 -0
  128. package/dist/esm/transport/node/builders/message.js +32 -15
  129. package/dist/esm/transport/node/builders/privacy.js +63 -4
  130. package/dist/esm/transport/noise/WaClientPayload.js +43 -22
  131. package/dist/esm/transport/noise/WaFrameCodec.js +14 -4
  132. package/dist/esm/util/coercion.js +12 -4
  133. package/dist/esm/util/index.js +1 -1
  134. package/dist/esm/util/protoscan.js +181 -0
  135. package/dist/esm/util/runtime.js +34 -0
  136. package/dist/index.d.ts +11 -6
  137. package/dist/index.js +15 -2
  138. package/dist/media/crypto/WaMediaCrypto.js +36 -17
  139. package/dist/media/types.d.ts +1 -1
  140. package/dist/message/context-info.d.ts +10 -0
  141. package/dist/message/context-info.js +40 -0
  142. package/dist/message/crypto/reporting-token.d.ts +3 -1
  143. package/dist/message/crypto/reporting-token.js +38 -216
  144. package/dist/message/encode/content.d.ts +24 -4
  145. package/dist/message/encode/content.js +74 -12
  146. package/dist/message/encode/media-payload.d.ts +5 -2
  147. package/dist/message/encode/media-payload.js +3 -1
  148. package/dist/message/kinds/group-history.d.ts +30 -0
  149. package/dist/message/kinds/group-history.js +40 -0
  150. package/dist/message/primitives/incoming.d.ts +5 -0
  151. package/dist/message/primitives/incoming.js +32 -13
  152. package/dist/message/types.d.ts +10 -0
  153. package/dist/protocol/abprops.d.ts +27 -140
  154. package/dist/protocol/abprops.js +18 -163
  155. package/dist/protocol/constants.d.ts +4 -3
  156. package/dist/protocol/constants.js +10 -3
  157. package/dist/protocol/defaults.d.ts +7 -0
  158. package/dist/protocol/defaults.js +7 -0
  159. package/dist/protocol/device-identity.d.ts +14 -4
  160. package/dist/protocol/device-identity.js +9 -4
  161. package/dist/protocol/jid.d.ts +4 -1
  162. package/dist/protocol/jid.js +83 -36
  163. package/dist/protocol/media.d.ts +1 -0
  164. package/dist/protocol/media.js +1 -0
  165. package/dist/protocol/message.d.ts +6 -0
  166. package/dist/protocol/message.js +9 -0
  167. package/dist/protocol/notification.d.ts +1 -0
  168. package/dist/protocol/notification.js +1 -0
  169. package/dist/protocol/privacy.d.ts +52 -0
  170. package/dist/protocol/privacy.js +71 -5
  171. package/dist/signal/group/SenderKeyChain.d.ts +1 -1
  172. package/dist/signal/group/SenderKeyChain.js +1 -2
  173. package/dist/signal/group/SenderKeyManager.js +1 -7
  174. package/dist/signal/session/SignalProtocol.d.ts +13 -0
  175. package/dist/signal/session/SignalProtocol.js +22 -21
  176. package/dist/signal/session/SignalRatchet.d.ts +4 -4
  177. package/dist/signal/session/SignalRatchet.js +19 -16
  178. package/dist/signal/session/SignalSession.d.ts +8 -1
  179. package/dist/signal/session/encoding.d.ts +5 -0
  180. package/dist/signal/session/encoding.js +25 -3
  181. package/dist/signal/session/resolver.js +11 -5
  182. package/dist/store/contracts/chat-metadata.store.d.ts +34 -0
  183. package/dist/store/contracts/chat-metadata.store.js +2 -0
  184. package/dist/store/contracts/group-metadata.store.d.ts +7 -0
  185. package/dist/store/contracts/thread.store.d.ts +6 -0
  186. package/dist/store/createStore.d.ts +10 -2
  187. package/dist/store/createStore.js +18 -1
  188. package/dist/store/index.d.ts +3 -1
  189. package/dist/store/index.js +3 -1
  190. package/dist/store/memory/chat-metadata.store.d.ts +26 -0
  191. package/dist/store/memory/chat-metadata.store.js +70 -0
  192. package/dist/store/noop.store.d.ts +2 -0
  193. package/dist/store/noop.store.js +9 -1
  194. package/dist/store/types.d.ts +190 -39
  195. package/dist/transport/binary/constants.d.ts +8 -0
  196. package/dist/transport/binary/constants.js +20 -1
  197. package/dist/transport/binary/decoder.js +17 -19
  198. package/dist/transport/binary/encoder.js +60 -17
  199. package/dist/transport/node/builders/global.d.ts +8 -0
  200. package/dist/transport/node/builders/global.js +21 -0
  201. package/dist/transport/node/builders/message.js +32 -15
  202. package/dist/transport/node/builders/privacy.d.ts +26 -2
  203. package/dist/transport/node/builders/privacy.js +63 -3
  204. package/dist/transport/noise/WaClientPayload.js +43 -22
  205. package/dist/transport/noise/WaFrameCodec.d.ts +1 -0
  206. package/dist/transport/noise/WaFrameCodec.js +14 -4
  207. package/dist/transport/noise/types.d.ts +17 -0
  208. package/dist/util/coercion.d.ts +6 -0
  209. package/dist/util/coercion.js +13 -4
  210. package/dist/util/index.d.ts +1 -1
  211. package/dist/util/index.js +2 -1
  212. package/dist/util/protoscan.d.ts +33 -0
  213. package/dist/util/protoscan.js +186 -0
  214. package/dist/util/runtime.d.ts +12 -0
  215. package/dist/util/runtime.js +35 -0
  216. package/package.json +3 -1
  217. package/spec/abprops/index.d.ts +4402 -0
  218. package/spec/abprops/index.js +4372 -0
@@ -1,11 +1,14 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { createIgnoreKeyFilter, validateIgnoreKey } from './messaging/ignore-key.js';
3
+ import { createEphemeralObserver, persistIncomingEphemeralSetting } from './persistence/ephemeral-setting.js';
4
+ import { runGroupHistoryBundle } from './persistence/group-history.js';
3
5
  import { runHistorySyncNotification } from './persistence/history-sync.js';
4
6
  import { persistIncomingMailboxEntities } from './persistence/mailbox.js';
5
7
  import { WriteBehindPersistence } from './persistence/WriteBehindPersistence.js';
6
8
  import { installWaClientPlugins } from './plugins/install.js';
7
9
  import { buildWaClientDependencies, resolveWaClientBase } from './WaClientFactory.js';
8
10
  import { ConsoleLogger } from '../infra/log/ConsoleLogger.js';
11
+ import { unwrapMessage } from '../message/encode/content.js';
9
12
  import { proto } from '../proto.js';
10
13
  import { WA_DEFAULTS, WA_MESSAGE_TYPES } from '../protocol/constants.js';
11
14
  import { normalizeDeviceJid } from '../protocol/jid.js';
@@ -42,6 +45,10 @@ class WaClientImpl extends EventEmitter {
42
45
  threadStore: this.stores.threads,
43
46
  contactStore: this.stores.contacts
44
47
  }, this.logger, this.options.writeBehind);
48
+ this.observeEphemeralSetting = createEphemeralObserver({
49
+ logger: this.logger,
50
+ chatMetadataStore: this.stores.chatMetadata
51
+ });
45
52
  const dependencies = buildWaClientDependencies({
46
53
  base,
47
54
  runtime: {
@@ -182,6 +189,7 @@ class WaClientImpl extends EventEmitter {
182
189
  }
183
190
  try {
184
191
  this.emit('message', event);
192
+ this.observeEphemeralSetting(event);
185
193
  void persistIncomingMailboxEntities({
186
194
  logger: this.logger,
187
195
  writeBehind: this.writeBehind,
@@ -207,6 +215,9 @@ class WaClientImpl extends EventEmitter {
207
215
  });
208
216
  });
209
217
  }
218
+ if (this.options.history?.groupBundles === true && event.message && !event.key.fromMe) {
219
+ this.tryProcessGroupHistoryBundle(event);
220
+ }
210
221
  const protocolMessage = event.message?.protocolMessage;
211
222
  if (!protocolMessage) {
212
223
  return;
@@ -258,6 +269,7 @@ class WaClientImpl extends EventEmitter {
258
269
  await runHistorySyncNotification({
259
270
  logger: this.logger,
260
271
  mediaTransfer: this.mediaTransfer,
272
+ chatMetadataStore: this.stores.chatMetadata,
261
273
  writeBehind: this.writeBehind,
262
274
  emitEvent: this.emit.bind(this),
263
275
  onPrivacyTokens: (conversations) => this.deps.trustedContactToken.hydrateFromHistorySync(conversations),
@@ -270,6 +282,16 @@ class WaClientImpl extends EventEmitter {
270
282
  }
271
283
  return;
272
284
  }
285
+ if (protocolType === proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING) {
286
+ persistIncomingEphemeralSetting({
287
+ logger: this.logger,
288
+ writeBehind: this.writeBehind,
289
+ chatMetadataStore: this.stores.chatMetadata,
290
+ event,
291
+ protocolMessage
292
+ });
293
+ return;
294
+ }
273
295
  if (SYNC_RELATED_PROTOCOL_TYPES.has(protocolType)) {
274
296
  this.logger.info('incoming sync-related protocol message', {
275
297
  id: event.key.id,
@@ -288,6 +310,43 @@ class WaClientImpl extends EventEmitter {
288
310
  this.leaveIncomingHandler();
289
311
  }
290
312
  }
313
+ /**
314
+ * Kicks off the group-history bundle download when the incoming message
315
+ * carries one. Fire-and-forget: the blob can be large, and the incoming
316
+ * handler must not stall behind a CDN fetch.
317
+ *
318
+ * The download outlives the handler that started it, so it takes a slot of
319
+ * its own in the drain accounting - `disconnect()` and `clearStoredState()`
320
+ * must not flush or wipe the stores while a bundle is still writing.
321
+ */
322
+ tryProcessGroupHistoryBundle(event) {
323
+ const bundle = unwrapMessage(event.message ?? {}).messageHistoryBundle;
324
+ const groupJid = event.key.remoteJid;
325
+ if (!bundle || !groupJid) {
326
+ return;
327
+ }
328
+ if (!this.tryEnterIncomingHandler()) {
329
+ return;
330
+ }
331
+ const credentials = this.deps.authClient.getCurrentCredentials();
332
+ void runGroupHistoryBundle({
333
+ logger: this.logger,
334
+ mediaTransfer: this.mediaTransfer,
335
+ writeBehind: this.writeBehind,
336
+ emitEvent: this.emit.bind(this),
337
+ meJid: credentials?.meJid,
338
+ meLid: credentials?.meLid,
339
+ getAbPropNumber: (name) => this.deps.abPropsCoordinator.getConfigValue(name)
340
+ }, {
341
+ bundle,
342
+ groupJid,
343
+ senderJid: event.key.participant ?? undefined,
344
+ bundleMessageId: event.key.id,
345
+ sentAtSeconds: event.timestampSeconds
346
+ }).finally(() => {
347
+ this.leaveIncomingHandler();
348
+ });
349
+ }
291
350
  async queryWithContext(context, node, timeoutMs = this.options.iqTimeoutMs ?? WA_DEFAULTS.IQ_TIMEOUT_MS, contextData = {}, options = {}) {
292
351
  return queryNodeWithContext(async (queryNode, queryTimeoutMs) => this.deps.lowLevelCoordinator.query(queryNode, queryTimeoutMs, options), this.logger, context, node, timeoutMs, contextData);
293
352
  }
@@ -362,6 +421,7 @@ class WaClientImpl extends EventEmitter {
362
421
  * your own backoff.
363
422
  */
364
423
  async disconnect() {
424
+ this.deps.privacyCoordinator.stopAccountSyncRefresh();
365
425
  await this.pauseIncomingEventsAndWaitDrain();
366
426
  const writeBehindFlush = await this.writeBehind.flush(this.options.writeBehind?.flushTimeoutMs);
367
427
  if (writeBehindFlush.remaining > 0) {
@@ -55,7 +55,7 @@ import { SignalProtocol } from '../signal/session/SignalProtocol.js';
55
55
  import { WaKeepAlive } from '../transport/keepalive/WaKeepAlive.js';
56
56
  import { buildAckNode } from '../transport/node/builders/global.js';
57
57
  import { buildPresenceNode } from '../transport/node/builders/presence.js';
58
- import { getFirstNodeChild } from '../transport/node/helpers.js';
58
+ import { findNodeChild, getFirstNodeChild } from '../transport/node/helpers.js';
59
59
  import { createUsyncSidGenerator } from '../transport/node/usync.js';
60
60
  import { WaNodeOrchestrator } from '../transport/node/WaNodeOrchestrator.js';
61
61
  import { WaNodeTransport } from '../transport/node/WaNodeTransport.js';
@@ -96,6 +96,7 @@ export function resolveWaClientBase(options, logger) {
96
96
  sessionId,
97
97
  deviceBrowser: device.browser,
98
98
  deviceOsDisplayName: device.osDisplayName,
99
+ deviceOsVersion: device.osVersion ?? undefined,
99
100
  devicePlatform: device.platform,
100
101
  urls: options.urls ?? options.chatSocketUrls ?? WA_DEFAULTS.CHAT_SOCKET_URLS,
101
102
  iqTimeoutMs: options.iqTimeoutMs ?? WA_DEFAULTS.IQ_TIMEOUT_MS,
@@ -120,7 +121,7 @@ function createIncomingNodeRuntime(input) {
120
121
  const { logger, emitEvent, authClient, connectionManager, nodeOrchestrator, streamControl, mediaMessageBuildOptions, retryCoordinator, messageDispatch, sendNode, syncAppState, disconnect, clearStoredCredentials, getCurrentCredentials, handleClientDirtyBits, incomingMessageAckOptions } = input;
121
122
  return {
122
123
  handleStreamControlResult: streamControl.handleStreamControlResult,
123
- persistSuccessAttributes: (attributes) => authClient.persistSuccessAttributes(attributes),
124
+ persistSuccessAttributes: (attributes) => authClient.persistSuccessAttributes(attributes, { countsAsLogin: true }),
124
125
  emitSuccessNode: (node) => emitEvent('debug_connection_success', { node }),
125
126
  updateClockSkewFromSuccess: (serverUnixSeconds) => connectionManager.updateClockSkewFromSuccess(serverUnixSeconds),
126
127
  shouldWarmupMediaConn: () => !!(getCurrentCredentials()?.meJid && connectionManager.isConnected()),
@@ -145,6 +146,7 @@ function createIncomingNodeRuntime(input) {
145
146
  emitIncomingFailure: (event) => emitEvent('stream_failure', event),
146
147
  emitIncomingErrorStanza: (event) => emitEvent('stanza_error', event),
147
148
  emitIncomingNotification: (event) => emitEvent('debug_notification', event),
149
+ emitOfflineThreadMetadata: (event) => emitEvent('offline_thread_metadata', event),
148
150
  emitMexNotification: (event) => emitEvent('mex_notification', event),
149
151
  emitRegistrationCode: (event) => emitEvent('mobile_registration_code', event),
150
152
  emitAccountTakeoverNotice: (event) => emitEvent('mobile_account_takeover_notice', event),
@@ -240,6 +242,15 @@ export function buildWaClientDependencies(input) {
240
242
  allowPrivateHosts: linkPreviewOptions.allowPrivateHosts,
241
243
  proxy: linkPreviewOptions.proxy ?? options.proxy?.linkPreview
242
244
  });
245
+ const createLinkPreviewResolver = (surface) => (content) => resolveLinkPreview(content.text, content.linkPreview, {
246
+ logger,
247
+ mediaTransfer,
248
+ getMediaConn: () => getClientMediaConn(mediaMessageBuildOptions),
249
+ fetcher: linkPreviewFetcher,
250
+ options: linkPreviewOptions,
251
+ serverClock,
252
+ surface
253
+ });
243
254
  const mediaMessageBuildOptions = {
244
255
  logger,
245
256
  mediaTransfer,
@@ -257,14 +268,7 @@ export function buildWaClientDependencies(input) {
257
268
  },
258
269
  serverClock,
259
270
  media: options.media,
260
- linkPreviewResolver: (content) => resolveLinkPreview(content.text, content.linkPreview, {
261
- logger,
262
- mediaTransfer,
263
- getMediaConn: () => getClientMediaConn(mediaMessageBuildOptions),
264
- fetcher: linkPreviewFetcher,
265
- options: linkPreviewOptions,
266
- serverClock
267
- })
271
+ linkPreviewResolver: createLinkPreviewResolver()
268
272
  };
269
273
  const messageClient = new WaMessageClient({
270
274
  logger,
@@ -325,6 +329,7 @@ export function buildWaClientDependencies(input) {
325
329
  const authClient = new WaAuthClient({
326
330
  deviceBrowser: options.deviceBrowser,
327
331
  deviceOsDisplayName: options.deviceOsDisplayName,
332
+ deviceOsVersion: options.deviceOsVersion,
328
333
  devicePlatform: options.devicePlatform,
329
334
  requireFullSync: options.requireFullSync,
330
335
  version: options.version,
@@ -367,12 +372,16 @@ export function buildWaClientDependencies(input) {
367
372
  generateStanzaId: () => messageDispatch.generateOutgoingMessageId(),
368
373
  mediaTransfer,
369
374
  getMediaConn: () => getClientMediaConn(mediaMessageBuildOptions),
375
+ linkPreviewResolver: createLinkPreviewResolver('newsletter'),
370
376
  getAbPropString: (name) => abPropsCoordinator.getConfigValue(name),
371
377
  logger
372
378
  });
373
379
  const privacyCoordinator = createPrivacyCoordinator({
380
+ logger,
374
381
  queryWithContext: runtime.queryWithContext,
375
- resolveUserJidPair: (userJid) => signalDeviceSync.resolveUserJidPair(userJid)
382
+ resolveUserJidPair: (userJid) => signalDeviceSync.resolveUserJidPair(userJid),
383
+ getSelfLid: () => getCurrentCredentials()?.meLid ?? null,
384
+ emitPrivacy: (event) => runtime.emitEvent('privacy', event)
376
385
  });
377
386
  const businessCoordinator = createBusinessCoordinator({
378
387
  queryWithContext: runtime.queryWithContext,
@@ -411,7 +420,8 @@ export function buildWaClientDependencies(input) {
411
420
  }
412
421
  return {
413
422
  participants: participantJids,
414
- ephemeral: metadata.ephemeral
423
+ ephemeral: metadata.ephemeral,
424
+ ephemeralTrigger: metadata.ephemeralTrigger
415
425
  };
416
426
  },
417
427
  logger
@@ -466,6 +476,8 @@ export function buildWaClientDependencies(input) {
466
476
  sessionStore: sessionStore.session,
467
477
  identityStore: sessionStore.identity,
468
478
  deviceListStore: sessionStore.deviceList,
479
+ threadStore: sessionStore.threads,
480
+ chatMetadataStore: sessionStore.chatMetadata,
469
481
  signalDeviceSync,
470
482
  messageSecretStore: sessionStore.messageSecret,
471
483
  persistAllMessageSecrets: options.addons?.persistAllSecrets === true,
@@ -508,7 +520,9 @@ export function buildWaClientDependencies(input) {
508
520
  trustedContactToken,
509
521
  emitAddon: (event) => runtime.emitEvent('message_addon', event),
510
522
  mexSocket: { query: runtime.query },
511
- peerDataOperation
523
+ peerDataOperation,
524
+ isGroupHistorySendEnabled: () => abPropsCoordinator.getConfigValue('group_history_send'),
525
+ getAbPropNumber: (name) => abPropsCoordinator.getConfigValue(name)
512
526
  });
513
527
  const retryCoordinator = new WaRetryCoordinator({
514
528
  logger,
@@ -533,7 +547,8 @@ export function buildWaClientDependencies(input) {
533
547
  .handleIncomingMessageEvent(event)
534
548
  .catch((err) => runtime.handleError(toError(err)));
535
549
  },
536
- isMobilePrimary
550
+ isMobilePrimary,
551
+ getAbPropNumber: (name) => abPropsCoordinator.getConfigValue(name)
537
552
  });
538
553
  const botCoordinator = createBotCoordinator({
539
554
  logger,
@@ -645,6 +660,7 @@ export function buildWaClientDependencies(input) {
645
660
  const disconnectWithClientSideEffects = async (reason, isLogout, code) => {
646
661
  abPropsCoordinator.reset();
647
662
  offlineResume.reset();
663
+ privacyCoordinator.stopAccountSyncRefresh();
648
664
  await connectionManager?.disconnect();
649
665
  runtime.emitEvent('connection', {
650
666
  status: 'close',
@@ -691,6 +707,7 @@ export function buildWaClientDependencies(input) {
691
707
  signalProtocol,
692
708
  senderKeyManager,
693
709
  onDecryptFailure: (context, error) => retryCoordinator.onDecryptFailure(context, error),
710
+ requestPlaceholderResend: (context) => retryCoordinator.onUnavailableMessage(context),
694
711
  emitIncomingMessage: (event) => {
695
712
  void runtime
696
713
  .handleIncomingMessageEvent(event)
@@ -706,6 +723,10 @@ export function buildWaClientDependencies(input) {
706
723
  getCurrentCredentials,
707
724
  syncAppState: runtime.syncAppState,
708
725
  generateUsyncSid,
726
+ syncAccountPrivacy: async () => {
727
+ await privacyCoordinator.refreshFromAccountSync();
728
+ },
729
+ emitBlocklist: (blocklist) => runtime.emitEvent('blocklist', blocklist),
709
730
  newsletterListSubscribed: () => newsletterCoordinator.listSubscribed()
710
731
  }, dirtyBits);
711
732
  const offlineResume = new WaOfflineResumeCoordinator({
@@ -887,6 +908,25 @@ export function buildWaClientDependencies(input) {
887
908
  return false;
888
909
  }
889
910
  });
911
+ incomingNode.registerIncomingHandler({
912
+ tag: WA_NODE_TAGS.NOTIFICATION,
913
+ subtype: WA_NOTIFICATION_TYPES.ACCOUNT_SYNC,
914
+ prepend: true,
915
+ /**
916
+ * The stanza carries the changed `<category>` inline, but it is a
917
+ * trigger only: the payload is a partial view, so the whole set is
918
+ * refetched instead. Observer only - returning false keeps the
919
+ * generic notification handler responsible for the ack and the
920
+ * `debug_notification` emit, leaving wire behavior unchanged.
921
+ */
922
+ // eslint-disable-next-line @typescript-eslint/require-await
923
+ handler: async (node) => {
924
+ if (findNodeChild(node, WA_NODE_TAGS.PRIVACY)) {
925
+ privacyCoordinator.scheduleAccountSyncRefresh();
926
+ }
927
+ return false;
928
+ }
929
+ });
890
930
  incomingNode.registerIncomingHandler({
891
931
  tag: WA_NODE_TAGS.NOTIFICATION,
892
932
  subtype: WA_NOTIFICATION_TYPES.DEVICES,
@@ -1,8 +1,13 @@
1
+ import { resolveAbPropNameByCode, WA_ABPROPS } from '../../abprops-spec.js';
1
2
  import { parseAbPropsIqResult } from '../events/abprops.js';
2
- import { AB_PROP_CONFIGS, resolveAbPropNameByCode, WA_ABPROPS_REFRESH_BOUNDS } from '../../protocol/abprops.js';
3
+ import { WA_ABPROPS_REFRESH_BOUNDS } from '../../protocol/abprops.js';
3
4
  import { WA_DEFAULTS } from '../../protocol/constants.js';
4
5
  import { buildGetAbPropsIq } from '../../transport/node/builders/abprops.js';
5
- import { parseOptionalInt, toError } from '../../util/primitives.js';
6
+ import { toError } from '../../util/primitives.js';
7
+ // The generated table types every entry with literal `code` / `defaultValue`
8
+ // types. Reading through a widened view keeps a lookup by a non-literal name
9
+ // from expanding into a 2000-member union at every call site.
10
+ const AB_PROPS = WA_ABPROPS;
6
11
  export class WaAbPropsCoordinator {
7
12
  constructor(options) {
8
13
  this.logger = options.logger;
@@ -18,7 +23,7 @@ export class WaAbPropsCoordinator {
18
23
  if (cached !== undefined) {
19
24
  return cached;
20
25
  }
21
- return AB_PROP_CONFIGS[name].defaultValue;
26
+ return AB_PROPS[name].defaultValue;
22
27
  }
23
28
  sync() {
24
29
  if (this.syncPromise) {
@@ -105,7 +110,7 @@ export class WaAbPropsCoordinator {
105
110
  if (!name) {
106
111
  continue;
107
112
  }
108
- const config = AB_PROP_CONFIGS[name];
113
+ const config = AB_PROPS[name];
109
114
  const parsed = parseConfigValue(entry.configValue, config.type, config.defaultValue);
110
115
  this.cache.set(name, parsed);
111
116
  applied += 1;
@@ -119,6 +124,8 @@ export class WaAbPropsCoordinator {
119
124
  });
120
125
  }
121
126
  }
127
+ // parseInt, not a digits-only parse: negative values are in band, several props
128
+ // ship -1 as their default. Divergence: WA Web caches NaN, we keep the default.
122
129
  function parseConfigValue(value, type, defaultValue) {
123
130
  if (value === null) {
124
131
  return defaultValue;
@@ -127,7 +134,12 @@ function parseConfigValue(value, type, defaultValue) {
127
134
  return value === '1' || value === 'true' || value === 'True';
128
135
  }
129
136
  if (type === 'int') {
130
- return parseOptionalInt(value) ?? defaultValue;
137
+ const parsed = Number.parseInt(value, 10);
138
+ return Number.isNaN(parsed) ? defaultValue : parsed;
139
+ }
140
+ if (type === 'float') {
141
+ const parsed = Number.parseFloat(value);
142
+ return Number.isNaN(parsed) ? defaultValue : parsed;
131
143
  }
132
144
  return value;
133
145
  }
@@ -1,5 +1,6 @@
1
1
  import { parseChatstateNode } from '../events/chatstate.js';
2
2
  import { buildInboundAck, createIncomingBaseEvent, createIncomingBusinessNotificationHandler, createIncomingCallHandler, createIncomingFailureHandler, createIncomingGroupNotificationHandler, createIncomingNotificationHandler, createIncomingPictureNotificationHandler, createIncomingReceiptHandler, createIncomingRegistrationNotificationHandler, createInfoBulletinNotificationEvent, createUnhandledIncomingNodeEvent, sendSafeAck } from '../events/incoming.js';
3
+ import { parseOfflineThreadMetadata } from '../events/offline.js';
3
4
  import { parsePresenceNode } from '../events/presence.js';
4
5
  import { WA_IQ_TYPES, WA_MESSAGE_TAGS, WA_MESSAGE_TYPES, WA_NODE_TAGS, WA_NOTIFICATION_TYPES, WA_SIGNALING } from '../../protocol/constants.js';
5
6
  import { decodeNodeContentBase64OrBytes, findNodeChild, getNodeChildren, getNodeChildrenByTag } from '../../transport/node/helpers.js';
@@ -378,6 +379,9 @@ export class WaIncomingNodeCoordinator {
378
379
  if (child.tag === 'offline') {
379
380
  this.offlineResume.handleOfflineComplete(parseOptionalInt(child.attrs.count) ?? 0);
380
381
  }
382
+ if (child.tag === 'thread_metadata') {
383
+ this.runtime.emitOfflineThreadMetadata(parseOfflineThreadMetadata(child));
384
+ }
381
385
  handled = true;
382
386
  }
383
387
  }
@@ -6,12 +6,26 @@ import { uploadMedia } from '../messaging/messages.js';
6
6
  import { MEDIA_UPLOAD_PATHS } from '../../media/constants.js';
7
7
  import { buildAddonAdditionalData, decodeAddonPlaintext, decryptAddonPayload, identifyEncryptedAddon, resolveParentMessageSecret, resolvePollOptionNames, shouldUseAddonAdditionalData } from '../../message/crypto/addon-crypto.js';
8
8
  import { unwrapMessage } from '../../message/encode/content.js';
9
+ import { encodeGroupHistoryBundle } from '../../message/kinds/group-history.js';
9
10
  import { proto } from '../../proto.js';
10
- import { applyDeviceToJid, normalizeRecipientJid } from '../../protocol/jid.js';
11
+ import { applyDeviceToJid, isGroupJid, normalizeRecipientJid } from '../../protocol/jid.js';
11
12
  import { runMexQuery } from '../../transport/node/mex/client.js';
12
13
  import { readAllBytes } from '../../util/bytes.js';
13
- import { tryAsNumber, tryAsString } from '../../util/coercion.js';
14
- import { toError } from '../../util/primitives.js';
14
+ import { resolveOptionalPositive, resolvePositive, tryAsNumber, tryAsString } from '../../util/coercion.js';
15
+ import { longToNumber, toError } from '../../util/primitives.js';
16
+ /** MIME type the group-history bundle is uploaded and advertised under. */
17
+ const GROUP_HISTORY_BUNDLE_MIMETYPE = 'application/protobuf';
18
+ /** Oldest `messageTimestamp` across `messages`, in seconds; `undefined` when none carries one. */
19
+ function oldestTimestampSeconds(messages) {
20
+ let oldest;
21
+ for (let index = 0; index < messages.length; index += 1) {
22
+ const timestamp = longToNumber(messages[index].messageTimestamp);
23
+ if (timestamp > 0 && (oldest === undefined || timestamp < oldest)) {
24
+ oldest = timestamp;
25
+ }
26
+ }
27
+ return oldest;
28
+ }
15
29
  function parseReachoutTimelockMexResponse(data) {
16
30
  const root = data?.xwa2_fetch_account_reachout_timelock;
17
31
  return {
@@ -70,6 +84,8 @@ export class WaMessageCoordinator {
70
84
  this.emitAddon = deps.emitAddon;
71
85
  this.mexSocket = deps.mexSocket;
72
86
  this.peerDataOperation = deps.peerDataOperation;
87
+ this.isGroupHistorySendEnabled = deps.isGroupHistorySendEnabled;
88
+ this.getAbPropNumber = deps.getAbPropNumber;
73
89
  }
74
90
  /**
75
91
  * Asks the server to backfill older messages for `chatJid` beyond what
@@ -86,13 +102,7 @@ export class WaMessageCoordinator {
86
102
  */
87
103
  async requestHistorySync(input) {
88
104
  const chatJid = normalizeRecipientJid(input.chatJid);
89
- if (input.count !== undefined) {
90
- if (!Number.isFinite(input.count) ||
91
- !Number.isSafeInteger(input.count) ||
92
- input.count <= 0) {
93
- throw new Error(`invalid count: ${input.count}`);
94
- }
95
- }
105
+ const onDemandMsgCount = resolveOptionalPositive(input.count, 'count');
96
106
  if (input.oldestMsgTimestampMs !== undefined) {
97
107
  if (!Number.isFinite(input.oldestMsgTimestampMs) ||
98
108
  !Number.isSafeInteger(input.oldestMsgTimestampMs) ||
@@ -110,10 +120,155 @@ export class WaMessageCoordinator {
110
120
  ...(input.oldestMsgTimestampMs === undefined
111
121
  ? {}
112
122
  : { oldestMsgTimestampMs: input.oldestMsgTimestampMs }),
113
- ...(input.count === undefined ? {} : { onDemandMsgCount: input.count })
123
+ ...(onDemandMsgCount === undefined ? {} : { onDemandMsgCount })
114
124
  };
115
125
  return this.peerDataOperation.send(proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND, { historySyncOnDemandRequest });
116
126
  }
127
+ /**
128
+ * Shares recent group history with members who joined after the fact - the
129
+ * counterpart of the `group_history_bundle` event on the receiving side.
130
+ *
131
+ * Sends two messages: the bundle itself, encrypted per device and fanned
132
+ * out **only** to `toJids` (never to the whole group), followed by a hidden
133
+ * notice addressed to everyone so other clients can render the "history was
134
+ * shared" marker.
135
+ *
136
+ * WhatsApp gates the sender side per account through the `group_history_send`
137
+ * AB-prop and rejects the stanza with SMAX_INVALID when it is off, so this
138
+ * throws up front rather than spending an upload. Admin-only groups
139
+ * (`memberShareGroupHistoryMode: 'admin_share'`) reject a share from a
140
+ * regular member server-side - read `client.group.queryGroupMetadata()`
141
+ * first if you want to check that too.
142
+ *
143
+ * @throws when the account is not allowed to share group history, when
144
+ * `groupJid` is not a group, when any of `toJids` is not a current member
145
+ * (or is this account), or when there is nothing to bundle.
146
+ * @example
147
+ * ```ts
148
+ * await client.message.shareGroupHistory('12036@g.us', {
149
+ * toJids: ['5511999999999@s.whatsapp.net'],
150
+ * count: 50
151
+ * })
152
+ * ```
153
+ */
154
+ async shareGroupHistory(groupJid, input) {
155
+ const normalizedGroupJid = normalizeRecipientJid(groupJid);
156
+ if (!isGroupJid(normalizedGroupJid)) {
157
+ throw new Error(`shareGroupHistory requires a group jid: ${normalizedGroupJid}`);
158
+ }
159
+ if (input.toJids.length === 0) {
160
+ throw new Error('shareGroupHistory requires at least one recipient');
161
+ }
162
+ const messageLimit = resolvePositive(input.count, this.getAbPropNumber('group_history_message_count_limit'), 'count');
163
+ if (!this.isGroupHistorySendEnabled()) {
164
+ throw new Error('shareGroupHistory is disabled for this account (group_history_send is off)');
165
+ }
166
+ const audience = await this.messageDispatch.resolveGroupHistoryAudience(normalizedGroupJid, input.toJids);
167
+ if (audience.requestedSelf) {
168
+ throw new Error('shareGroupHistory cannot share history with this account itself');
169
+ }
170
+ if (audience.unknownJids.length > 0) {
171
+ throw new Error(`shareGroupHistory recipients are not members of the group (addressing mode: ${audience.addressingMode}): ${audience.unknownJids.join(', ')}`);
172
+ }
173
+ if (audience.historyReceivers.length === 0) {
174
+ throw new Error('shareGroupHistory resolved no recipients');
175
+ }
176
+ const messages = input.messages ??
177
+ (await this.loadGroupHistoryMessages(normalizedGroupJid, messageLimit, input.sinceMs));
178
+ if (messages.length === 0) {
179
+ throw new Error('shareGroupHistory found no messages to share');
180
+ }
181
+ const { compressed } = await encodeGroupHistoryBundle(messages, input.outOfWindowPinnedMessages);
182
+ const upload = await uploadMedia(this.mediaUploadOptions, {
183
+ source: compressed,
184
+ cryptoType: 'group-history',
185
+ uploadPath: MEDIA_UPLOAD_PATHS['group-history'],
186
+ contentType: GROUP_HISTORY_BUNDLE_MIMETYPE,
187
+ sidecar: false,
188
+ logLabel: 'group history bundle upload'
189
+ });
190
+ const oldestInWindow = oldestTimestampSeconds(messages);
191
+ const oldestPin = oldestTimestampSeconds(input.outOfWindowPinnedMessages ?? []);
192
+ const oldestInBundle = oldestPin === undefined
193
+ ? oldestInWindow
194
+ : oldestInWindow === undefined
195
+ ? oldestPin
196
+ : Math.min(oldestInWindow, oldestPin);
197
+ const metadata = {
198
+ historyReceivers: audience.historyReceivers,
199
+ nonHistoryReceivers: audience.nonHistoryReceivers,
200
+ messageCount: messages.length,
201
+ oldestMessageTimestampInWindow: oldestInWindow,
202
+ oldestMessageTimestampInBundle: oldestInBundle
203
+ };
204
+ const bundleResult = await this.send(normalizedGroupJid, {
205
+ messageHistoryBundle: {
206
+ mimetype: GROUP_HISTORY_BUNDLE_MIMETYPE,
207
+ fileSha256: upload.fileSha256,
208
+ fileEncSha256: upload.fileEncSha256,
209
+ mediaKey: upload.mediaKey,
210
+ directPath: upload.directPath,
211
+ mediaKeyTimestamp: this.mediaUploadOptions.serverClock.nowSeconds(),
212
+ messageHistoryMetadata: metadata
213
+ }
214
+ });
215
+ let noticeMessageId;
216
+ try {
217
+ noticeMessageId = (await this.send(normalizedGroupJid, {
218
+ messageHistoryNotice: { messageHistoryMetadata: metadata }
219
+ })).id;
220
+ }
221
+ catch (error) {
222
+ this.logger.warn('group history notice failed after the bundle was delivered', {
223
+ groupJid: normalizedGroupJid,
224
+ id: bundleResult.id,
225
+ receiverCount: audience.historyReceivers.length,
226
+ message: toError(error).message
227
+ });
228
+ }
229
+ this.logger.debug('shared group history', {
230
+ groupJid: normalizedGroupJid,
231
+ id: bundleResult.id,
232
+ messagesCount: messages.length,
233
+ receiverCount: audience.historyReceivers.length
234
+ });
235
+ return {
236
+ bundleMessageId: bundleResult.id,
237
+ noticeMessageId,
238
+ messagesCount: messages.length,
239
+ historyReceivers: audience.historyReceivers,
240
+ nonHistoryReceivers: audience.nonHistoryReceivers
241
+ };
242
+ }
243
+ /**
244
+ * Reads the most recent messages of a group back out of the mailbox store
245
+ * and rebuilds the `WebMessageInfo` shape a bundle carries. Yields nothing
246
+ * when the `messages` store domain is `'none'`.
247
+ */
248
+ async loadGroupHistoryMessages(groupJid, limit, sinceMs) {
249
+ const records = await this.messageStore.listByThread(groupJid, limit);
250
+ const messages = [];
251
+ for (let index = 0; index < records.length; index += 1) {
252
+ const record = records[index];
253
+ if (!record.messageBytes) {
254
+ continue;
255
+ }
256
+ if (sinceMs !== undefined && (record.timestampMs ?? 0) < sinceMs) {
257
+ continue;
258
+ }
259
+ messages[messages.length] = {
260
+ key: {
261
+ id: record.id,
262
+ remoteJid: groupJid,
263
+ fromMe: record.fromMe,
264
+ participant: record.participantJid ?? record.senderJid
265
+ },
266
+ message: proto.Message.decode(record.messageBytes),
267
+ messageTimestamp: Math.floor((record.timestampMs ?? 0) / 1000)
268
+ };
269
+ }
270
+ return messages;
271
+ }
117
272
  /**
118
273
  * Fetches the server-side "reachout" timelock that throttles cold outreach
119
274
  * to non-contacts, returning the active window when enforcement is on.