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
package/README.md CHANGED
@@ -41,8 +41,8 @@ npm install zapo-js
41
41
  ```
42
42
 
43
43
  Zero mandatory runtime dependencies. Pick the optional packages you need
44
- on top: a persistent store and (optionally) the media processor for
45
- thumbnails / voice-note metadata.
44
+ on top: a persistent store, the media processor for thumbnails /
45
+ voice-note metadata, and the native crypto accelerator.
46
46
 
47
47
  ```bash
48
48
  # Persistent store - choose one
@@ -56,10 +56,21 @@ npm install @zapo-js/store-sqlite better-sqlite3
56
56
  npm install @zapo-js/media-utils sharp
57
57
  # plus a system `ffmpeg` + `ffprobe` on PATH (see media-utils README)
58
58
 
59
+ # Optional - Rust crypto accelerator, no code change required
60
+ npm install @zapo-js/native
61
+
59
62
  # Optional - structured logging
60
63
  npm install pino pino-pretty
61
64
  ```
62
65
 
66
+ `@zapo-js/native` is picked up automatically once installed: the core
67
+ resolves it on first use and falls back to the JS implementation when it is
68
+ absent, so nothing else changes. It accelerates the two primitives that
69
+ dominate the messaging hot path, X25519 ECDH and XEdDSA sign / verify. The
70
+ published build is the WASM one (`ZAPO_NATIVE_BACKEND=auto` tries the NAPI
71
+ addon first, then WASM); the compiled NAPI addon is not on npm yet and has
72
+ to be built from `packages/native`. Set `ZAPO_NATIVE_BACKEND=js` to opt out.
73
+
63
74
  ## Quick Start
64
75
 
65
76
  ```ts
@@ -116,16 +127,19 @@ The core lives at the repo root (`zapo-js`). Optional packages live in
116
127
  [`packages/`](packages/) and ship under the `@zapo-js/*` scope. Install
117
128
  only what you need.
118
129
 
119
- | Package | Peer dependency | Purpose |
120
- | ---------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
121
- | [`@zapo-js/store-sqlite`](packages/store-sqlite) | `better-sqlite3` | SQLite persistent store (single-process bots, dev sessions, small-to-medium prod). |
122
- | [`@zapo-js/store-redis`](packages/store-redis) | `ioredis` | Redis-backed store with native TTL eviction. |
123
- | [`@zapo-js/store-mongo`](packages/store-mongo) | `mongodb` | MongoDB-backed store with TTL-index eviction. |
124
- | [`@zapo-js/store-mysql`](packages/store-mysql) | `mysql2` | MySQL / MariaDB-backed store with background cleanup poller. |
125
- | [`@zapo-js/store-postgres`](packages/store-postgres) | `pg` | PostgreSQL-backed store with background cleanup poller. |
126
- | [`@zapo-js/media-utils`](packages/media-utils) | `sharp` + `ffmpeg` | `WaMediaProcessor`: thumbnails, waveforms, voice-note normalization. |
127
- | [`@zapo-js/fake-server`](packages/fake-server) | (none) | In-process fake WhatsApp Web server for end-to-end testing. |
128
- | [`@zapo-js/mcp-server`](packages/mcp-server) | `@modelcontextprotocol/sdk` | **Dev-only.** MCP server exposing multi-session `WaClient`s as dynamic tools for an LLM agent (Claude Code / Cursor / etc.). Not for production. |
130
+ | Package | Peer dependency | Purpose |
131
+ | ---------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
132
+ | [`@zapo-js/store-sqlite`](packages/store-sqlite) | `better-sqlite3` | SQLite persistent store (single-process bots, dev sessions, small-to-medium prod). |
133
+ | [`@zapo-js/store-redis`](packages/store-redis) | `ioredis` | Redis-backed store with native TTL eviction. |
134
+ | [`@zapo-js/store-mongo`](packages/store-mongo) | `mongodb` | MongoDB-backed store with TTL-index eviction. |
135
+ | [`@zapo-js/store-mysql`](packages/store-mysql) | `mysql2` | MySQL / MariaDB-backed store with background cleanup poller. |
136
+ | [`@zapo-js/store-postgres`](packages/store-postgres) | `pg` | PostgreSQL-backed store with background cleanup poller. |
137
+ | [`@zapo-js/media-utils`](packages/media-utils) | `sharp` + `file-type` + `ffmpeg` | `WaMediaProcessor`: thumbnails, waveforms, voice-note normalization. |
138
+ | [`@zapo-js/native`](packages/native) | (none) | Rust crypto accelerator for the messaging hot path (X25519 ECDH, XEdDSA sign / verify). Auto-detected once installed; ships as a WASM build. |
139
+ | [`@zapo-js/voip`](packages/voip) | `@roamhq/wrtc` + `libmlow-wasm` | `client.voip` plugin for calls: MLow/WASM codec, SRTP, STUN, WebRTC/SCTP relay transport. |
140
+ | [`@zapo-js/wam`](packages/wam) | (none) | `client.wam` plugin emitting the client-side WAM telemetry batches WA Web sends, for wire parity and anti-fingerprinting. |
141
+ | [`@zapo-js/fake-server`](packages/fake-server) | (none) | In-process fake WhatsApp Web server for end-to-end testing. |
142
+ | [`@zapo-js/mcp-server`](packages/mcp-server) | `@modelcontextprotocol/sdk` | **Dev-only.** MCP server exposing multi-session `WaClient`s as dynamic tools for an LLM agent (Claude Code / Cursor / etc.). Not for production. |
129
143
 
130
144
  Each package's README has the install + config + integration notes.
131
145
 
@@ -0,0 +1,10 @@
1
+ import { type WaAbPropName } from '../spec/abprops';
2
+ export { WA_ABPROPS, WA_ABPROPS_BY_CODE, WA_ABPROPS_SPECIAL_EARLY, WA_ABPROPS_USED_BEFORE_INIT, WA_GROUP_ABPROPS, WA_GROUP_ABPROPS_BY_CODE } from '../spec/abprops';
3
+ export type { WaAbProp, WaAbPropName, WaAbPropType, WaAbPropValue, WaAbPropValueByName, WaAbPropValueOf, WaGroupAbPropName, WaGroupAbPropValueByName } from '../spec/abprops';
4
+ /**
5
+ * Resolves the client-side prop name for a wire config code.
6
+ *
7
+ * Returns `undefined` for codes this build does not know about - the server
8
+ * advertises props from newer bundles, and WA Web drops those rows too.
9
+ */
10
+ export declare function resolveAbPropNameByCode(configCode: number): WaAbPropName | undefined;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WA_GROUP_ABPROPS_BY_CODE = exports.WA_GROUP_ABPROPS = exports.WA_ABPROPS_USED_BEFORE_INIT = exports.WA_ABPROPS_SPECIAL_EARLY = exports.WA_ABPROPS_BY_CODE = exports.WA_ABPROPS = void 0;
4
+ exports.resolveAbPropNameByCode = resolveAbPropNameByCode;
5
+ const abprops_1 = require("../spec/abprops");
6
+ var abprops_2 = require("../spec/abprops");
7
+ Object.defineProperty(exports, "WA_ABPROPS", { enumerable: true, get: function () { return abprops_2.WA_ABPROPS; } });
8
+ Object.defineProperty(exports, "WA_ABPROPS_BY_CODE", { enumerable: true, get: function () { return abprops_2.WA_ABPROPS_BY_CODE; } });
9
+ Object.defineProperty(exports, "WA_ABPROPS_SPECIAL_EARLY", { enumerable: true, get: function () { return abprops_2.WA_ABPROPS_SPECIAL_EARLY; } });
10
+ Object.defineProperty(exports, "WA_ABPROPS_USED_BEFORE_INIT", { enumerable: true, get: function () { return abprops_2.WA_ABPROPS_USED_BEFORE_INIT; } });
11
+ Object.defineProperty(exports, "WA_GROUP_ABPROPS", { enumerable: true, get: function () { return abprops_2.WA_GROUP_ABPROPS; } });
12
+ Object.defineProperty(exports, "WA_GROUP_ABPROPS_BY_CODE", { enumerable: true, get: function () { return abprops_2.WA_GROUP_ABPROPS_BY_CODE; } });
13
+ // The reverse map is emitted with numeric literal keys, so it cannot be indexed
14
+ // with a plain `number`. Widening it keeps the lookup a single property read and
15
+ // avoids allocating a parallel Map with 2000+ entries at module init.
16
+ const AB_PROP_NAME_BY_CODE = abprops_1.WA_ABPROPS_BY_CODE;
17
+ /**
18
+ * Resolves the client-side prop name for a wire config code.
19
+ *
20
+ * Returns `undefined` for codes this build does not know about - the server
21
+ * advertises props from newer bundles, and WA Web drops those rows too.
22
+ */
23
+ function resolveAbPropNameByCode(configCode) {
24
+ return AB_PROP_NAME_BY_CODE[configCode];
25
+ }
@@ -177,12 +177,10 @@ class WaAppStateCrypto {
177
177
  if (out.byteLength !== left.byteLength) {
178
178
  throw new Error('lt hash output length mismatch');
179
179
  }
180
- const leftView = new DataView(left.buffer, left.byteOffset, left.byteLength);
181
- const rightView = new DataView(right.buffer, right.byteOffset, right.byteLength);
182
- const outView = new DataView(out.buffer, out.byteOffset, out.byteLength);
183
180
  for (let offset = 0; offset < left.byteLength; offset += constants_1.APP_STATE_POINT_SIZE) {
184
- const value = combine(leftView.getUint16(offset, true), rightView.getUint16(offset, true));
185
- outView.setUint16(offset, value & 0xffff, true);
181
+ const value = combine(left[offset] | (left[offset + 1] << 8), right[offset] | (right[offset + 1] << 8));
182
+ out[offset] = value & 0xff;
183
+ out[offset + 1] = (value >>> 8) & 0xff;
186
184
  }
187
185
  return out;
188
186
  }
@@ -199,7 +197,7 @@ class WaAppStateCrypto {
199
197
  generateValueMac(valueMacHmacKey, associatedData, iv, cipherText) {
200
198
  const octetLength = new Uint8Array(constants_1.APP_STATE_MAC_OCTET_LENGTH);
201
199
  octetLength[octetLength.length - 1] = associatedData.byteLength & 0xff;
202
- const full = (0, primitives_1.hmacSha512Sign)(valueMacHmacKey, (0, bytes_1.concatBytes)([associatedData, iv, cipherText, octetLength]));
200
+ const full = (0, primitives_1.hmacSha512Sign)(valueMacHmacKey, [associatedData, iv, cipherText, octetLength]);
203
201
  return full.subarray(0, constants_1.APP_STATE_VALUE_MAC_LENGTH);
204
202
  }
205
203
  touchDerivedKeysCacheEntry(cacheKey, keys) {
@@ -52,6 +52,12 @@ export declare class WaAuthClient {
52
52
  private credentials;
53
53
  private versionOverride;
54
54
  private mobileAppVersionOverride;
55
+ /**
56
+ * Successful logins made by this instance, advertised as
57
+ * `connectAttemptCount`. Process-local by design – the persisted
58
+ * counterpart is `credentials.loginCounter`.
59
+ */
60
+ private connectAttemptCount;
55
61
  constructor(options: WaAuthClientOptions, deps: WaAuthClientDeps);
56
62
  /**
57
63
  * Returns a snapshot of auth readiness flags (connection, registration,
@@ -121,8 +127,15 @@ export declare class WaAuthClient {
121
127
  * Persists the per-connection success attributes from the server (LID,
122
128
  * display name, companion key, last-success ts, props versions, ...). Only
123
129
  * persists when at least one attribute actually changed.
130
+ *
131
+ * Pass `countsAsLogin` only for the server `success` node: it advances the
132
+ * login counters advertised as `lc` / `connectAttemptCount`. Callers that
133
+ * merely reuse this to persist a single attribute (a push-name change, for
134
+ * instance) must leave it unset.
124
135
  */
125
- persistSuccessAttributes(attributes: WaSuccessPersistAttributes): Promise<void>;
136
+ persistSuccessAttributes(attributes: WaSuccessPersistAttributes, { countsAsLogin }?: {
137
+ readonly countsAsLogin?: boolean;
138
+ }): Promise<void>;
126
139
  /**
127
140
  * Requests an 8-character pairing code for `phoneNumber` (link-code flow).
128
141
  * Pass `customCode` to suggest a specific code; the server still validates
@@ -23,11 +23,18 @@ class WaAuthClient {
23
23
  constructor(options, deps) {
24
24
  this.versionOverride = null;
25
25
  this.mobileAppVersionOverride = null;
26
+ /**
27
+ * Successful logins made by this instance, advertised as
28
+ * `connectAttemptCount`. Process-local by design – the persisted
29
+ * counterpart is `credentials.loginCounter`.
30
+ */
31
+ this.connectAttemptCount = 0;
26
32
  const device = (0, constants_1.resolveWaDeviceIdentity)(options);
27
33
  this.options = Object.freeze({
28
34
  ...options,
29
35
  deviceBrowser: device.browser,
30
36
  deviceOsDisplayName: device.osDisplayName,
37
+ deviceOsVersion: device.osVersion ?? undefined,
31
38
  devicePlatform: device.platform,
32
39
  requireFullSync: options.requireFullSync
33
40
  });
@@ -125,8 +132,10 @@ class WaAuthClient {
125
132
  const mobileAppVersionOverride = this.mobileAppVersionOverride;
126
133
  this.mobileAppVersionOverride = null;
127
134
  return (0, credentials_flow_1.buildCommsConfig)(this.logger, this.requireCredentials(), socketOptions, {
135
+ connectAttemptCount: this.connectAttemptCount,
128
136
  deviceBrowser: this.options.deviceBrowser,
129
137
  deviceOsDisplayName: this.options.deviceOsDisplayName,
138
+ deviceOsVersion: this.options.deviceOsVersion,
130
139
  requireFullSync: this.options.requireFullSync,
131
140
  version: override ?? this.options.version,
132
141
  mobileTransport: this.options.mobileTransport,
@@ -239,10 +248,19 @@ class WaAuthClient {
239
248
  * Persists the per-connection success attributes from the server (LID,
240
249
  * display name, companion key, last-success ts, props versions, ...). Only
241
250
  * persists when at least one attribute actually changed.
251
+ *
252
+ * Pass `countsAsLogin` only for the server `success` node: it advances the
253
+ * login counters advertised as `lc` / `connectAttemptCount`. Callers that
254
+ * merely reuse this to persist a single attribute (a push-name change, for
255
+ * instance) must leave it unset.
242
256
  */
243
- async persistSuccessAttributes(attributes) {
257
+ async persistSuccessAttributes(attributes, { countsAsLogin = false } = {}) {
258
+ if (countsAsLogin) {
259
+ this.connectAttemptCount += 1;
260
+ }
244
261
  let persistDiff;
245
262
  const computeDiff = (current, next) => ({
263
+ loginCounterChanged: next.loginCounter !== current.loginCounter,
246
264
  lidChanged: next.meLid !== current.meLid,
247
265
  displayNameChanged: next.meDisplayName !== current.meDisplayName,
248
266
  companionChanged: (current.companionEncStatic === undefined) !==
@@ -262,6 +280,9 @@ class WaAuthClient {
262
280
  meDisplayName: attributes.meDisplayName ?? credentials.meDisplayName,
263
281
  companionEncStatic: attributes.companionEncStatic ?? credentials.companionEncStatic,
264
282
  lastSuccessTs: attributes.lastSuccessTs ?? credentials.lastSuccessTs,
283
+ loginCounter: countsAsLogin
284
+ ? (credentials.loginCounter ?? 0) + 1
285
+ : credentials.loginCounter,
265
286
  propsVersion: attributes.propsVersion ?? credentials.propsVersion,
266
287
  abPropsVersion: attributes.abPropsVersion ?? credentials.abPropsVersion,
267
288
  connectionLocation: attributes.connectionLocation ?? credentials.connectionLocation,
@@ -14,7 +14,7 @@ interface WaAuthCredentialsFlowArgs {
14
14
  }
15
15
  export declare function loadOrCreateCredentials(args: WaAuthCredentialsFlowArgs): Promise<WaAuthCredentials>;
16
16
  export declare function persistCredentials(args: WaAuthCredentialsFlowArgs, credentials: WaAuthCredentials): Promise<void>;
17
- export declare function buildCommsConfig(logger: Logger, credentials: WaAuthCredentials, socketOptions: WaAuthSocketOptions, clientOptions: Pick<WaAuthClientOptions, 'deviceBrowser' | 'deviceOsDisplayName' | 'requireFullSync' | 'version' | 'mobileTransport'> & {
17
+ export declare function buildCommsConfig(logger: Logger, credentials: WaAuthCredentials, socketOptions: WaAuthSocketOptions, clientOptions: Pick<WaAuthClientOptions, 'deviceBrowser' | 'deviceOsDisplayName' | 'deviceOsVersion' | 'requireFullSync' | 'version' | 'mobileTransport'> & {
18
18
  readonly noiseTrustedRootCa?: WaNoiseRootCa;
19
19
  readonly disableNoiseCertificateChainVerification?: boolean;
20
20
  /**
@@ -25,5 +25,7 @@ export declare function buildCommsConfig(logger: Logger, credentials: WaAuthCred
25
25
  * sessions.
26
26
  */
27
27
  readonly mobileAppVersionOverride?: string;
28
+ /** Successful logins made by the calling client instance so far. */
29
+ readonly connectAttemptCount?: number;
28
30
  }): Promise<WaCommsConfig>;
29
31
  export {};
@@ -179,6 +179,8 @@ async function buildCommsConfig(logger, credentials, socketOptions, clientOption
179
179
  ? {
180
180
  username: loginIdentity.username,
181
181
  device: loginIdentity.device,
182
+ loginCounter: credentials.loginCounter ?? 0,
183
+ connectAttemptCount: clientOptions.connectAttemptCount ?? 0,
182
184
  deviceBrowser: clientOptions.deviceBrowser,
183
185
  deviceOsDisplayName: clientOptions.deviceOsDisplayName,
184
186
  versionBase
@@ -190,6 +192,7 @@ async function buildCommsConfig(logger, credentials, socketOptions, clientOption
190
192
  signedPreKey: credentials.signedPreKey,
191
193
  deviceBrowser: clientOptions.deviceBrowser,
192
194
  deviceOsDisplayName: clientOptions.deviceOsDisplayName,
195
+ deviceOsVersion: clientOptions.deviceOsVersion,
193
196
  requireFullSync: clientOptions.requireFullSync,
194
197
  versionBase
195
198
  }
@@ -31,6 +31,7 @@ interface WaPairingFlowOptions {
31
31
  export declare class WaPairingFlow {
32
32
  private readonly opts;
33
33
  private pairingSession;
34
+ private pairSuccessInFlight;
34
35
  constructor(options: WaPairingFlowOptions);
35
36
  hasPairingSession(): boolean;
36
37
  clearSession(): void;
@@ -41,7 +42,11 @@ export declare class WaPairingFlow {
41
42
  handleCompanionRegRefreshNotification(node: BinaryNode): Promise<boolean>;
42
43
  private handlePairDevice;
43
44
  private handlePairSuccess;
45
+ private completePairSuccess;
46
+ /** Returns `null` when the primary's account signature does not verify. */
44
47
  private buildPairSuccessResponseIdentity;
48
+ /** Mirrors the `not-authorized` IQ error WhatsApp Web returns on a failed pair-success. */
49
+ private sendPairSuccessValidationError;
45
50
  private handlePrimaryHello;
46
51
  private rotateAdvSecret;
47
52
  private requireCredentials;
@@ -13,8 +13,13 @@ const pairing_1 = require("../../transport/node/builders/pairing");
13
13
  const helpers_1 = require("../../transport/node/helpers");
14
14
  const query_1 = require("../../transport/node/query");
15
15
  const bytes_1 = require("../../util/bytes");
16
+ const primitives_1 = require("../../util/primitives");
17
+ /** Server-enforced bounds on the `pair-success` `device-identity` payload. */
18
+ const PAIR_SUCCESS_DEVICE_IDENTITY_MIN_BYTES = 1;
19
+ const PAIR_SUCCESS_DEVICE_IDENTITY_MAX_BYTES = 500;
16
20
  class WaPairingFlow {
17
21
  constructor(options) {
22
+ this.pairSuccessInFlight = false;
18
23
  this.opts = options;
19
24
  this.pairingSession = null;
20
25
  }
@@ -33,6 +38,7 @@ class WaPairingFlow {
33
38
  const normalizedCustomCode = customCode !== undefined ? (0, pairing_code_crypto_1.normalizeCustomPairingCode)(customCode) : undefined;
34
39
  const credentials = this.requireCredentials();
35
40
  const phoneJid = (0, jid_1.parsePhoneJid)(phoneNumber);
41
+ const createdAtSeconds = Math.floor(Date.now() / 1000);
36
42
  const [companionHello, refreshedCredentials] = await Promise.all([
37
43
  (0, pairing_code_crypto_1.createCompanionHello)({ customCode: normalizedCustomCode }),
38
44
  this.rotateAdvSecret(credentials)
@@ -64,7 +70,7 @@ class WaPairingFlow {
64
70
  pairingCode: companionHello.pairingCode,
65
71
  phoneJid,
66
72
  ref,
67
- createdAtSeconds: Math.floor(Date.now() / 1000),
73
+ createdAtSeconds,
68
74
  companionEphemeralKeyPair: companionHello.companionEphemeralKeyPair,
69
75
  attempts: 0,
70
76
  finished: false
@@ -166,6 +172,23 @@ class WaPairingFlow {
166
172
  this.opts.logger.debug('pair-device refs updated', { refsCount: refs.length });
167
173
  }
168
174
  async handlePairSuccess(iqNode, pairSuccessNode) {
175
+ if (this.pairSuccessInFlight) {
176
+ this.opts.logger.debug('pair-success ignored: already processing');
177
+ return;
178
+ }
179
+ if (this.opts.auth.getCredentials()?.meJid) {
180
+ this.opts.logger.debug('pair-success ignored: session already registered');
181
+ return;
182
+ }
183
+ this.pairSuccessInFlight = true;
184
+ try {
185
+ await this.completePairSuccess(iqNode, pairSuccessNode);
186
+ }
187
+ finally {
188
+ this.pairSuccessInFlight = false;
189
+ }
190
+ }
191
+ async completePairSuccess(iqNode, pairSuccessNode) {
169
192
  this.opts.logger.debug('processing pair-success node');
170
193
  const credentials = this.requireCredentials();
171
194
  const [deviceIdentityNode, deviceNode, platformNode] = (0, helpers_1.findNodeChildrenByTags)(pairSuccessNode, [constants_1.WA_NODE_TAGS.DEVICE_IDENTITY, 'device', constants_1.WA_NODE_TAGS.PLATFORM]);
@@ -177,7 +200,12 @@ class WaPairingFlow {
177
200
  });
178
201
  throw new Error('pair-success stanza is missing required nodes');
179
202
  }
180
- const wrappedIdentity = _proto_1.proto.ADVSignedDeviceIdentityHMAC.decode((0, helpers_1.decodeNodeContentUtf8OrBytes)(deviceIdentityNode.content, 'pair-success.device-identity'));
203
+ const deviceIdentityBytes = (0, helpers_1.decodeNodeContentUtf8OrBytes)(deviceIdentityNode.content, 'pair-success.device-identity');
204
+ if (deviceIdentityBytes.length < PAIR_SUCCESS_DEVICE_IDENTITY_MIN_BYTES ||
205
+ deviceIdentityBytes.length > PAIR_SUCCESS_DEVICE_IDENTITY_MAX_BYTES) {
206
+ throw new Error(`pair-success device-identity must be ${PAIR_SUCCESS_DEVICE_IDENTITY_MIN_BYTES}-${PAIR_SUCCESS_DEVICE_IDENTITY_MAX_BYTES} bytes, got ${deviceIdentityBytes.length}`);
207
+ }
208
+ const wrappedIdentity = _proto_1.proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityBytes);
181
209
  const wrappedDetails = (0, bytes_1.decodeProtoBytes)(wrappedIdentity.details, 'ADVSignedDeviceIdentityHMAC.details');
182
210
  const wrappedHmac = (0, bytes_1.decodeProtoBytes)(wrappedIdentity.hmac, 'ADVSignedDeviceIdentityHMAC.hmac');
183
211
  const accountType = wrappedIdentity.accountType ?? _proto_1.proto.ADVEncryptionType.E2EE;
@@ -194,16 +222,23 @@ class WaPairingFlow {
194
222
  platform: platformNode.attrs.name,
195
223
  isHosted
196
224
  });
225
+ await this.sendPairSuccessValidationError(iqNode);
197
226
  throw new Error('pair-success HMAC validation failed');
198
227
  }
199
228
  }
200
- const { signedIdentity, keyIndex, responseIdentityBytes } = await this.buildPairSuccessResponseIdentity(credentials, wrappedDetails);
229
+ const built = await this.buildPairSuccessResponseIdentity(credentials, wrappedDetails);
230
+ if (!built) {
231
+ await this.sendPairSuccessValidationError(iqNode);
232
+ throw new Error('pair-success account signature validation failed');
233
+ }
234
+ const { signedIdentity, keyIndex, responseIdentityBytes } = built;
201
235
  const nextCredentials = {
202
236
  ...credentials,
203
237
  signedIdentity,
204
238
  meJid: deviceNode.attrs.jid,
205
239
  meLid: deviceNode.attrs.lid,
206
- platform: platformNode.attrs.name
240
+ platform: platformNode.attrs.name,
241
+ loginCounter: 0
207
242
  };
208
243
  await this.opts.auth.updateCredentials(nextCredentials);
209
244
  this.opts.logger.info('pair-success credentials updated', {
@@ -238,6 +273,7 @@ class WaPairingFlow {
238
273
  this.opts.callbacks.emitPaired(nextCredentials);
239
274
  this.opts.logger.debug('pair-success completed and paired event emitted');
240
275
  }
276
+ /** Returns `null` when the primary's account signature does not verify. */
241
277
  async buildPairSuccessResponseIdentity(credentials, wrappedDetails) {
242
278
  const signedIdentity = _proto_1.proto.ADVSignedDeviceIdentity.decode(wrappedDetails);
243
279
  const details = (0, bytes_1.decodeProtoBytes)(signedIdentity.details, 'ADVSignedDeviceIdentity.details');
@@ -253,7 +289,7 @@ class WaPairingFlow {
253
289
  keyIndex: advDeviceIdentity.keyIndex ?? 0,
254
290
  isDeviceHosted
255
291
  });
256
- throw new Error('pair-success account signature validation failed');
292
+ return null;
257
293
  }
258
294
  }
259
295
  signedIdentity.deviceSignature = await (0, WaAdvSignature_1.generateDeviceSignature)(details, localIdentity, accountSignatureKey, false);
@@ -268,19 +304,35 @@ class WaPairingFlow {
268
304
  responseIdentityBytes
269
305
  };
270
306
  }
307
+ /** Mirrors the `not-authorized` IQ error WhatsApp Web returns on a failed pair-success. */
308
+ async sendPairSuccessValidationError(iqNode) {
309
+ try {
310
+ await this.opts.socket.sendNode((0, global_1.buildIqErrorNode)(iqNode, { code: 401, text: 'not-authorized' }));
311
+ }
312
+ catch (error) {
313
+ this.opts.logger.warn('failed to send pair-success error response', {
314
+ message: (0, primitives_1.toError)(error).message
315
+ });
316
+ }
317
+ }
271
318
  async handlePrimaryHello(linkCodeNode) {
272
- const credentials = this.requireCredentials();
319
+ let credentials = this.requireCredentials();
273
320
  const pairingSession = this.pairingSession;
274
- if (!pairingSession || pairingSession.finished) {
321
+ if (!pairingSession) {
275
322
  this.opts.logger.trace('primary_hello ignored: no active session');
276
323
  return;
277
324
  }
278
325
  pairingSession.attempts += 1;
279
326
  this.opts.logger.debug('processing primary_hello', {
280
- attempts: pairingSession.attempts
327
+ attempts: pairingSession.attempts,
328
+ finished: pairingSession.finished
281
329
  });
282
- if (pairingSession.attempts > 3) {
283
- throw new Error('pairing code exceeded maximum primary hello attempts');
330
+ if (pairingSession.finished) {
331
+ if (pairingSession.attempts > constants_1.WA_DEFAULTS.PAIRING_CODE_MAX_PRIMARY_HELLOS) {
332
+ throw new Error('pairing code exceeded maximum primary hello attempts');
333
+ }
334
+ credentials = await this.rotateAdvSecret(credentials);
335
+ pairingSession.finished = false;
284
336
  }
285
337
  const [refNode, wrappedPrimaryNode, primaryIdentityNode] = (0, helpers_1.findNodeChildrenByTags)(linkCodeNode, [
286
338
  constants_1.WA_NODE_TAGS.LINK_CODE_PAIRING_REF,
@@ -309,6 +361,10 @@ class WaPairingFlow {
309
361
  companionEphemeralPrivKey: pairingSession.companionEphemeralKeyPair.privKey,
310
362
  registrationIdentityKeyPair: credentials.registrationInfo.identityKeyPair
311
363
  });
364
+ await this.opts.auth.updateCredentials({
365
+ ...credentials,
366
+ advSecretKey: finish.advSecret
367
+ });
312
368
  const result = await this.opts.socket.query((0, pairing_1.buildCompanionFinishRequestNode)({
313
369
  phoneJid: pairingSession.phoneJid,
314
370
  wrappedKeyBundle: finish.wrappedKeyBundle,
@@ -318,10 +374,6 @@ class WaPairingFlow {
318
374
  if (result.attrs.type === constants_1.WA_IQ_TYPES.ERROR) {
319
375
  throw new Error('companion_finish returned error');
320
376
  }
321
- await this.opts.auth.updateCredentials({
322
- ...credentials,
323
- advSecretKey: finish.advSecret
324
- });
325
377
  pairingSession.finished = true;
326
378
  this.opts.logger.debug('primary_hello completed with companion_finish success');
327
379
  }
@@ -24,6 +24,11 @@ export interface WaAuthCredentials {
24
24
  readonly serverHasPreKeys?: boolean;
25
25
  readonly routingInfo?: Uint8Array;
26
26
  readonly lastSuccessTs?: number;
27
+ /**
28
+ * Successful logins since this device was paired, advertised as `lc` in the
29
+ * noise login payload. Reset to `0` when a new pairing completes.
30
+ */
31
+ readonly loginCounter?: number;
27
32
  readonly propsVersion?: number;
28
33
  readonly abPropsVersion?: number;
29
34
  readonly connectionLocation?: string;
@@ -65,6 +70,14 @@ export interface WaAuthClientOptions {
65
70
  * `'Mac OS'`, `'Linux'`). Defaults to the current runtime's OS.
66
71
  */
67
72
  readonly deviceOsDisplayName?: string;
73
+ /**
74
+ * OS version advertised in `DeviceProps.version` (`'10'`, `'14.6'`, ...).
75
+ * Defaults to the detected runtime OS version. Set this alongside
76
+ * {@link deviceOsDisplayName} when advertising an OS the process is not
77
+ * running on, so the advertised name and version agree. Values that are
78
+ * not dotted-numeric leave the field unset.
79
+ */
80
+ readonly deviceOsVersion?: string;
68
81
  /**
69
82
  * When `true`, request a full history download from the primary device on
70
83
  * pairing instead of just recent messages. Off by default.
@@ -27,6 +27,7 @@ declare class WaClientImpl extends EventEmitter {
27
27
  private readonly appStateSync;
28
28
  private readonly mediaTransfer;
29
29
  private readonly writeBehind;
30
+ private readonly observeEphemeralSetting;
30
31
  private connectPromise;
31
32
  private acceptingIncomingEvents;
32
33
  private activeIncomingHandlers;
@@ -74,6 +75,16 @@ declare class WaClientImpl extends EventEmitter {
74
75
  getClockSkewMs(): number | null;
75
76
  private bindNodeTransportEvents;
76
77
  private handleIncomingMessageEvent;
78
+ /**
79
+ * Kicks off the group-history bundle download when the incoming message
80
+ * carries one. Fire-and-forget: the blob can be large, and the incoming
81
+ * handler must not stall behind a CDN fetch.
82
+ *
83
+ * The download outlives the handler that started it, so it takes a slot of
84
+ * its own in the drain accounting - `disconnect()` and `clearStoredState()`
85
+ * must not flush or wipe the stores while a bundle is still writing.
86
+ */
87
+ private tryProcessGroupHistoryBundle;
77
88
  private queryWithContext;
78
89
  private handleIncomingFrame;
79
90
  /**
@@ -3,12 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WaClient = void 0;
4
4
  const node_events_1 = require("node:events");
5
5
  const ignore_key_1 = require("./messaging/ignore-key");
6
+ const ephemeral_setting_1 = require("./persistence/ephemeral-setting");
7
+ const group_history_1 = require("./persistence/group-history");
6
8
  const history_sync_1 = require("./persistence/history-sync");
7
9
  const mailbox_1 = require("./persistence/mailbox");
8
10
  const WriteBehindPersistence_1 = require("./persistence/WriteBehindPersistence");
9
11
  const install_1 = require("./plugins/install");
10
12
  const WaClientFactory_1 = require("./WaClientFactory");
11
13
  const ConsoleLogger_1 = require("../infra/log/ConsoleLogger");
14
+ const content_1 = require("../message/encode/content");
12
15
  const _proto_1 = require("../proto");
13
16
  const constants_1 = require("../protocol/constants");
14
17
  const jid_1 = require("../protocol/jid");
@@ -45,6 +48,10 @@ class WaClientImpl extends node_events_1.EventEmitter {
45
48
  threadStore: this.stores.threads,
46
49
  contactStore: this.stores.contacts
47
50
  }, this.logger, this.options.writeBehind);
51
+ this.observeEphemeralSetting = (0, ephemeral_setting_1.createEphemeralObserver)({
52
+ logger: this.logger,
53
+ chatMetadataStore: this.stores.chatMetadata
54
+ });
48
55
  const dependencies = (0, WaClientFactory_1.buildWaClientDependencies)({
49
56
  base,
50
57
  runtime: {
@@ -185,6 +192,7 @@ class WaClientImpl extends node_events_1.EventEmitter {
185
192
  }
186
193
  try {
187
194
  this.emit('message', event);
195
+ this.observeEphemeralSetting(event);
188
196
  void (0, mailbox_1.persistIncomingMailboxEntities)({
189
197
  logger: this.logger,
190
198
  writeBehind: this.writeBehind,
@@ -210,6 +218,9 @@ class WaClientImpl extends node_events_1.EventEmitter {
210
218
  });
211
219
  });
212
220
  }
221
+ if (this.options.history?.groupBundles === true && event.message && !event.key.fromMe) {
222
+ this.tryProcessGroupHistoryBundle(event);
223
+ }
213
224
  const protocolMessage = event.message?.protocolMessage;
214
225
  if (!protocolMessage) {
215
226
  return;
@@ -261,6 +272,7 @@ class WaClientImpl extends node_events_1.EventEmitter {
261
272
  await (0, history_sync_1.runHistorySyncNotification)({
262
273
  logger: this.logger,
263
274
  mediaTransfer: this.mediaTransfer,
275
+ chatMetadataStore: this.stores.chatMetadata,
264
276
  writeBehind: this.writeBehind,
265
277
  emitEvent: this.emit.bind(this),
266
278
  onPrivacyTokens: (conversations) => this.deps.trustedContactToken.hydrateFromHistorySync(conversations),
@@ -273,6 +285,16 @@ class WaClientImpl extends node_events_1.EventEmitter {
273
285
  }
274
286
  return;
275
287
  }
288
+ if (protocolType === _proto_1.proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING) {
289
+ (0, ephemeral_setting_1.persistIncomingEphemeralSetting)({
290
+ logger: this.logger,
291
+ writeBehind: this.writeBehind,
292
+ chatMetadataStore: this.stores.chatMetadata,
293
+ event,
294
+ protocolMessage
295
+ });
296
+ return;
297
+ }
276
298
  if (SYNC_RELATED_PROTOCOL_TYPES.has(protocolType)) {
277
299
  this.logger.info('incoming sync-related protocol message', {
278
300
  id: event.key.id,
@@ -291,6 +313,43 @@ class WaClientImpl extends node_events_1.EventEmitter {
291
313
  this.leaveIncomingHandler();
292
314
  }
293
315
  }
316
+ /**
317
+ * Kicks off the group-history bundle download when the incoming message
318
+ * carries one. Fire-and-forget: the blob can be large, and the incoming
319
+ * handler must not stall behind a CDN fetch.
320
+ *
321
+ * The download outlives the handler that started it, so it takes a slot of
322
+ * its own in the drain accounting - `disconnect()` and `clearStoredState()`
323
+ * must not flush or wipe the stores while a bundle is still writing.
324
+ */
325
+ tryProcessGroupHistoryBundle(event) {
326
+ const bundle = (0, content_1.unwrapMessage)(event.message ?? {}).messageHistoryBundle;
327
+ const groupJid = event.key.remoteJid;
328
+ if (!bundle || !groupJid) {
329
+ return;
330
+ }
331
+ if (!this.tryEnterIncomingHandler()) {
332
+ return;
333
+ }
334
+ const credentials = this.deps.authClient.getCurrentCredentials();
335
+ void (0, group_history_1.runGroupHistoryBundle)({
336
+ logger: this.logger,
337
+ mediaTransfer: this.mediaTransfer,
338
+ writeBehind: this.writeBehind,
339
+ emitEvent: this.emit.bind(this),
340
+ meJid: credentials?.meJid,
341
+ meLid: credentials?.meLid,
342
+ getAbPropNumber: (name) => this.deps.abPropsCoordinator.getConfigValue(name)
343
+ }, {
344
+ bundle,
345
+ groupJid,
346
+ senderJid: event.key.participant ?? undefined,
347
+ bundleMessageId: event.key.id,
348
+ sentAtSeconds: event.timestampSeconds
349
+ }).finally(() => {
350
+ this.leaveIncomingHandler();
351
+ });
352
+ }
294
353
  async queryWithContext(context, node, timeoutMs = this.options.iqTimeoutMs ?? constants_1.WA_DEFAULTS.IQ_TIMEOUT_MS, contextData = {}, options = {}) {
295
354
  return (0, query_1.queryWithContext)(async (queryNode, queryTimeoutMs) => this.deps.lowLevelCoordinator.query(queryNode, queryTimeoutMs, options), this.logger, context, node, timeoutMs, contextData);
296
355
  }
@@ -365,6 +424,7 @@ class WaClientImpl extends node_events_1.EventEmitter {
365
424
  * your own backoff.
366
425
  */
367
426
  async disconnect() {
427
+ this.deps.privacyCoordinator.stopAccountSyncRefresh();
368
428
  await this.pauseIncomingEventsAndWaitDrain();
369
429
  const writeBehindFlush = await this.writeBehind.flush(this.options.writeBehind?.flushTimeoutMs);
370
430
  if (writeBehindFlush.remaining > 0) {
@@ -18,7 +18,7 @@ import { WaMobileCoordinator } from './coordinators/WaMobileCoordinator';
18
18
  import { type WaNewsletterCoordinator } from './coordinators/WaNewsletterCoordinator';
19
19
  import { WaPassiveTasksCoordinator } from './coordinators/WaPassiveTasksCoordinator';
20
20
  import { type WaPresenceCoordinator } from './coordinators/WaPresenceCoordinator';
21
- import { type WaPrivacyCoordinator } from './coordinators/WaPrivacyCoordinator';
21
+ import { type WaPrivacyCoordinatorRuntime } from './coordinators/WaPrivacyCoordinator';
22
22
  import { type WaProfileCoordinator } from './coordinators/WaProfileCoordinator';
23
23
  import { WaRetryCoordinator } from './coordinators/WaRetryCoordinator';
24
24
  import { type WaStatusCoordinator } from './coordinators/WaStatusCoordinator';
@@ -110,7 +110,7 @@ export interface WaClientDependencies {
110
110
  readonly statusCoordinator: WaStatusCoordinator;
111
111
  readonly broadcastListCoordinator: WaBroadcastListCoordinator;
112
112
  readonly newsletterCoordinator: WaNewsletterCoordinator;
113
- readonly privacyCoordinator: WaPrivacyCoordinator;
113
+ readonly privacyCoordinator: WaPrivacyCoordinatorRuntime;
114
114
  readonly profileCoordinator: WaProfileCoordinator;
115
115
  readonly businessCoordinator: WaBusinessCoordinator;
116
116
  readonly botCoordinator: WaBotCoordinator;