@zapo-js/fake-server 0.3.0 → 1.1.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 (301) hide show
  1. package/README.md +96 -8
  2. package/dist/api/AppStateSyncManager.d.ts +0 -1
  3. package/dist/api/AppStateSyncManager.js +0 -1
  4. package/dist/api/FakePairingDriver.d.ts +0 -1
  5. package/dist/api/FakePairingDriver.js +0 -1
  6. package/dist/api/FakePeer.d.ts +77 -4
  7. package/dist/api/FakePeer.js +208 -2
  8. package/dist/api/FakeServerSession.d.ts +67 -0
  9. package/dist/api/FakeServerSession.js +293 -0
  10. package/dist/api/FakeWaServer.d.ts +77 -31
  11. package/dist/api/FakeWaServer.js +140 -259
  12. package/dist/api/PreKeyDispenser.d.ts +0 -1
  13. package/dist/api/PreKeyDispenser.js +0 -1
  14. package/dist/api/Scenario.d.ts +0 -1
  15. package/dist/api/Scenario.js +0 -1
  16. package/dist/api/ServerRegistries.d.ts +0 -1
  17. package/dist/api/ServerRegistries.js +0 -1
  18. package/dist/api/iq-handlers.d.ts +1 -1
  19. package/dist/api/iq-handlers.js +23 -1
  20. package/dist/{cli.d.ts → bin.d.ts} +0 -1
  21. package/dist/bin.js +296 -0
  22. package/dist/esm/api/AppStateSyncManager.js +146 -0
  23. package/dist/esm/api/FakePairingDriver.js +31 -0
  24. package/dist/esm/api/FakePeer.js +610 -0
  25. package/dist/esm/api/FakeServerSession.js +287 -0
  26. package/dist/esm/api/FakeWaServer.js +528 -0
  27. package/dist/esm/api/PreKeyDispenser.js +93 -0
  28. package/dist/esm/api/Scenario.js +32 -0
  29. package/dist/esm/api/ServerRegistries.js +261 -0
  30. package/dist/esm/api/iq-handlers.js +515 -0
  31. package/dist/{cli.js → esm/bin.js} +70 -25
  32. package/dist/esm/index.js +26 -0
  33. package/dist/esm/infra/WaFakeConnection.js +49 -0
  34. package/dist/esm/infra/WaFakeConnectionPipeline.js +235 -0
  35. package/dist/esm/infra/WaFakeFrameSocket.js +117 -0
  36. package/dist/esm/infra/WaFakeMediaHttpsServer.js +126 -0
  37. package/dist/esm/infra/WaFakeNoiseHandshake.js +67 -0
  38. package/dist/esm/infra/WaFakeTransport.js +31 -0
  39. package/dist/esm/infra/WaFakeWsServer.js +96 -0
  40. package/dist/esm/package.json +3 -0
  41. package/dist/esm/protocol/auth/cert-chain.js +50 -0
  42. package/dist/esm/protocol/auth/client-payload-validate.js +55 -0
  43. package/dist/esm/protocol/auth/fake-primary-device.js +47 -0
  44. package/dist/esm/protocol/auth/pair-device.js +87 -0
  45. package/dist/esm/protocol/auth/success-node.js +26 -0
  46. package/dist/esm/protocol/iq/abprops.js +30 -0
  47. package/dist/esm/protocol/iq/appstate-sync.js +146 -0
  48. package/dist/esm/protocol/iq/business.js +59 -0
  49. package/dist/esm/protocol/iq/dirty-bits.js +19 -0
  50. package/dist/esm/protocol/iq/group-ops.js +154 -0
  51. package/dist/esm/protocol/iq/newsletter.js +14 -0
  52. package/dist/esm/protocol/iq/prekey-fetch.js +88 -0
  53. package/dist/esm/protocol/iq/privacy-token.js +26 -0
  54. package/dist/esm/protocol/iq/privacy.js +123 -0
  55. package/dist/esm/protocol/iq/profile.js +72 -0
  56. package/dist/esm/protocol/iq/router.js +117 -0
  57. package/dist/esm/protocol/iq/usync.js +64 -0
  58. package/dist/esm/protocol/push/app-state-key-share.js +26 -0
  59. package/dist/esm/protocol/push/call-failure.js +27 -0
  60. package/dist/esm/protocol/push/chatstate.js +23 -0
  61. package/dist/esm/protocol/push/error-stanza.js +13 -0
  62. package/dist/esm/protocol/push/history-sync.js +111 -0
  63. package/dist/esm/protocol/push/message.js +38 -0
  64. package/dist/esm/protocol/push/notification.js +29 -0
  65. package/dist/esm/protocol/push/presence.js +14 -0
  66. package/dist/esm/protocol/push/receipt.js +21 -0
  67. package/dist/esm/protocol/signal/fake-app-state-crypto.js +196 -0
  68. package/dist/esm/protocol/signal/fake-peer-double-ratchet.js +398 -0
  69. package/dist/esm/protocol/signal/fake-peer-group-recv-session.js +120 -0
  70. package/dist/esm/protocol/signal/fake-peer-identity.js +8 -0
  71. package/dist/esm/protocol/signal/fake-peer-key-bundle.js +25 -0
  72. package/dist/esm/protocol/signal/fake-sender-key.js +83 -0
  73. package/dist/esm/protocol/signal/prekey-upload.js +107 -0
  74. package/dist/esm/protocol/stream/stream-error.js +56 -0
  75. package/dist/esm/state/fake-app-state-collection.js +134 -0
  76. package/dist/esm/state/fake-media-store.js +53 -0
  77. package/dist/esm/transport/codec.js +8 -0
  78. package/dist/esm/transport/crypto.js +8 -0
  79. package/dist/esm/transport/protos.js +7 -0
  80. package/dist/esm/transport/util.js +8 -0
  81. package/dist/index.d.ts +2 -3
  82. package/dist/index.js +2 -2
  83. package/dist/infra/WaFakeConnection.d.ts +0 -1
  84. package/dist/infra/WaFakeConnection.js +0 -1
  85. package/dist/infra/WaFakeConnectionPipeline.d.ts +10 -3
  86. package/dist/infra/WaFakeConnectionPipeline.js +47 -32
  87. package/dist/infra/WaFakeFrameSocket.d.ts +0 -1
  88. package/dist/infra/WaFakeFrameSocket.js +0 -1
  89. package/dist/infra/WaFakeMediaHttpsServer.d.ts +0 -1
  90. package/dist/infra/WaFakeMediaHttpsServer.js +0 -1
  91. package/dist/infra/WaFakeNoiseHandshake.d.ts +8 -10
  92. package/dist/infra/WaFakeNoiseHandshake.js +19 -24
  93. package/dist/infra/WaFakeTransport.d.ts +2 -4
  94. package/dist/infra/WaFakeTransport.js +2 -1
  95. package/dist/infra/WaFakeWsServer.d.ts +0 -1
  96. package/dist/infra/WaFakeWsServer.js +0 -1
  97. package/dist/protocol/auth/cert-chain.d.ts +4 -1
  98. package/dist/protocol/auth/cert-chain.js +13 -3
  99. package/dist/protocol/auth/client-payload-validate.d.ts +0 -1
  100. package/dist/protocol/auth/client-payload-validate.js +0 -1
  101. package/dist/protocol/auth/fake-primary-device.d.ts +0 -1
  102. package/dist/protocol/auth/fake-primary-device.js +1 -3
  103. package/dist/protocol/auth/pair-device.d.ts +0 -1
  104. package/dist/protocol/auth/pair-device.js +4 -16
  105. package/dist/protocol/auth/success-node.d.ts +0 -1
  106. package/dist/protocol/auth/success-node.js +0 -1
  107. package/dist/protocol/iq/abprops.d.ts +0 -1
  108. package/dist/protocol/iq/abprops.js +0 -1
  109. package/dist/protocol/iq/appstate-sync.d.ts +0 -1
  110. package/dist/protocol/iq/appstate-sync.js +0 -1
  111. package/dist/protocol/iq/business.d.ts +0 -1
  112. package/dist/protocol/iq/business.js +0 -1
  113. package/dist/protocol/iq/dirty-bits.d.ts +0 -1
  114. package/dist/protocol/iq/dirty-bits.js +0 -1
  115. package/dist/protocol/iq/group-ops.d.ts +0 -1
  116. package/dist/protocol/iq/group-ops.js +0 -1
  117. package/dist/protocol/iq/newsletter.d.ts +0 -1
  118. package/dist/protocol/iq/newsletter.js +0 -1
  119. package/dist/protocol/iq/prekey-fetch.d.ts +0 -1
  120. package/dist/protocol/iq/prekey-fetch.js +0 -1
  121. package/dist/protocol/iq/privacy-token.d.ts +0 -1
  122. package/dist/protocol/iq/privacy-token.js +0 -1
  123. package/dist/protocol/iq/privacy.d.ts +0 -1
  124. package/dist/protocol/iq/privacy.js +15 -16
  125. package/dist/protocol/iq/profile.d.ts +0 -1
  126. package/dist/protocol/iq/profile.js +0 -1
  127. package/dist/protocol/iq/router.d.ts +7 -2
  128. package/dist/protocol/iq/router.js +8 -3
  129. package/dist/protocol/iq/usync.d.ts +0 -1
  130. package/dist/protocol/iq/usync.js +0 -1
  131. package/dist/protocol/push/app-state-key-share.d.ts +0 -1
  132. package/dist/protocol/push/app-state-key-share.js +0 -1
  133. package/dist/protocol/push/call-failure.d.ts +0 -1
  134. package/dist/protocol/push/call-failure.js +0 -1
  135. package/dist/protocol/push/chatstate.d.ts +0 -1
  136. package/dist/protocol/push/chatstate.js +0 -1
  137. package/dist/protocol/push/error-stanza.d.ts +0 -1
  138. package/dist/protocol/push/error-stanza.js +0 -1
  139. package/dist/protocol/push/history-sync.d.ts +0 -1
  140. package/dist/protocol/push/history-sync.js +0 -1
  141. package/dist/protocol/push/message.d.ts +0 -1
  142. package/dist/protocol/push/message.js +0 -1
  143. package/dist/protocol/push/notification.d.ts +0 -1
  144. package/dist/protocol/push/notification.js +0 -1
  145. package/dist/protocol/push/presence.d.ts +0 -1
  146. package/dist/protocol/push/presence.js +0 -1
  147. package/dist/protocol/push/receipt.d.ts +0 -1
  148. package/dist/protocol/push/receipt.js +0 -1
  149. package/dist/protocol/signal/fake-app-state-crypto.d.ts +7 -9
  150. package/dist/protocol/signal/fake-app-state-crypto.js +31 -33
  151. package/dist/protocol/signal/fake-peer-double-ratchet.d.ts +0 -1
  152. package/dist/protocol/signal/fake-peer-double-ratchet.js +23 -28
  153. package/dist/protocol/signal/fake-peer-group-recv-session.d.ts +0 -1
  154. package/dist/protocol/signal/fake-peer-group-recv-session.js +6 -10
  155. package/dist/protocol/signal/fake-peer-identity.d.ts +0 -1
  156. package/dist/protocol/signal/fake-peer-identity.js +0 -1
  157. package/dist/protocol/signal/fake-peer-key-bundle.d.ts +0 -1
  158. package/dist/protocol/signal/fake-peer-key-bundle.js +0 -1
  159. package/dist/protocol/signal/fake-sender-key.d.ts +0 -1
  160. package/dist/protocol/signal/fake-sender-key.js +5 -9
  161. package/dist/protocol/signal/prekey-upload.d.ts +0 -1
  162. package/dist/protocol/signal/prekey-upload.js +0 -1
  163. package/dist/protocol/stream/stream-error.d.ts +0 -1
  164. package/dist/protocol/stream/stream-error.js +0 -1
  165. package/dist/state/fake-app-state-collection.d.ts +0 -1
  166. package/dist/state/fake-app-state-collection.js +0 -1
  167. package/dist/state/fake-media-store.d.ts +0 -1
  168. package/dist/state/fake-media-store.js +2 -2
  169. package/dist/transport/codec.d.ts +2 -3
  170. package/dist/transport/codec.js +2 -3
  171. package/dist/transport/crypto.d.ts +5 -7
  172. package/dist/transport/crypto.js +6 -11
  173. package/dist/transport/protos.d.ts +2 -4
  174. package/dist/transport/protos.js +2 -4
  175. package/dist/transport/util.d.ts +8 -0
  176. package/dist/transport/util.js +16 -0
  177. package/package.json +26 -6
  178. package/dist/api/AppStateSyncManager.d.ts.map +0 -1
  179. package/dist/api/AppStateSyncManager.js.map +0 -1
  180. package/dist/api/FakePairingDriver.d.ts.map +0 -1
  181. package/dist/api/FakePairingDriver.js.map +0 -1
  182. package/dist/api/FakePeer.d.ts.map +0 -1
  183. package/dist/api/FakePeer.js.map +0 -1
  184. package/dist/api/FakeWaServer.d.ts.map +0 -1
  185. package/dist/api/FakeWaServer.js.map +0 -1
  186. package/dist/api/PreKeyDispenser.d.ts.map +0 -1
  187. package/dist/api/PreKeyDispenser.js.map +0 -1
  188. package/dist/api/Scenario.d.ts.map +0 -1
  189. package/dist/api/Scenario.js.map +0 -1
  190. package/dist/api/ServerRegistries.d.ts.map +0 -1
  191. package/dist/api/ServerRegistries.js.map +0 -1
  192. package/dist/api/iq-handlers.d.ts.map +0 -1
  193. package/dist/api/iq-handlers.js.map +0 -1
  194. package/dist/cli.d.ts.map +0 -1
  195. package/dist/cli.js.map +0 -1
  196. package/dist/index.d.ts.map +0 -1
  197. package/dist/index.js.map +0 -1
  198. package/dist/infra/WaFakeConnection.d.ts.map +0 -1
  199. package/dist/infra/WaFakeConnection.js.map +0 -1
  200. package/dist/infra/WaFakeConnectionPipeline.d.ts.map +0 -1
  201. package/dist/infra/WaFakeConnectionPipeline.js.map +0 -1
  202. package/dist/infra/WaFakeFrameSocket.d.ts.map +0 -1
  203. package/dist/infra/WaFakeFrameSocket.js.map +0 -1
  204. package/dist/infra/WaFakeMediaHttpsServer.d.ts.map +0 -1
  205. package/dist/infra/WaFakeMediaHttpsServer.js.map +0 -1
  206. package/dist/infra/WaFakeNoiseHandshake.d.ts.map +0 -1
  207. package/dist/infra/WaFakeNoiseHandshake.js.map +0 -1
  208. package/dist/infra/WaFakeTransport.d.ts.map +0 -1
  209. package/dist/infra/WaFakeTransport.js.map +0 -1
  210. package/dist/infra/WaFakeWsServer.d.ts.map +0 -1
  211. package/dist/infra/WaFakeWsServer.js.map +0 -1
  212. package/dist/protocol/auth/cert-chain.d.ts.map +0 -1
  213. package/dist/protocol/auth/cert-chain.js.map +0 -1
  214. package/dist/protocol/auth/client-payload-validate.d.ts.map +0 -1
  215. package/dist/protocol/auth/client-payload-validate.js.map +0 -1
  216. package/dist/protocol/auth/fake-primary-device.d.ts.map +0 -1
  217. package/dist/protocol/auth/fake-primary-device.js.map +0 -1
  218. package/dist/protocol/auth/pair-device.d.ts.map +0 -1
  219. package/dist/protocol/auth/pair-device.js.map +0 -1
  220. package/dist/protocol/auth/success-node.d.ts.map +0 -1
  221. package/dist/protocol/auth/success-node.js.map +0 -1
  222. package/dist/protocol/iq/abprops.d.ts.map +0 -1
  223. package/dist/protocol/iq/abprops.js.map +0 -1
  224. package/dist/protocol/iq/appstate-sync.d.ts.map +0 -1
  225. package/dist/protocol/iq/appstate-sync.js.map +0 -1
  226. package/dist/protocol/iq/business.d.ts.map +0 -1
  227. package/dist/protocol/iq/business.js.map +0 -1
  228. package/dist/protocol/iq/dirty-bits.d.ts.map +0 -1
  229. package/dist/protocol/iq/dirty-bits.js.map +0 -1
  230. package/dist/protocol/iq/group-ops.d.ts.map +0 -1
  231. package/dist/protocol/iq/group-ops.js.map +0 -1
  232. package/dist/protocol/iq/newsletter.d.ts.map +0 -1
  233. package/dist/protocol/iq/newsletter.js.map +0 -1
  234. package/dist/protocol/iq/prekey-fetch.d.ts.map +0 -1
  235. package/dist/protocol/iq/prekey-fetch.js.map +0 -1
  236. package/dist/protocol/iq/privacy-token.d.ts.map +0 -1
  237. package/dist/protocol/iq/privacy-token.js.map +0 -1
  238. package/dist/protocol/iq/privacy.d.ts.map +0 -1
  239. package/dist/protocol/iq/privacy.js.map +0 -1
  240. package/dist/protocol/iq/profile.d.ts.map +0 -1
  241. package/dist/protocol/iq/profile.js.map +0 -1
  242. package/dist/protocol/iq/router.d.ts.map +0 -1
  243. package/dist/protocol/iq/router.js.map +0 -1
  244. package/dist/protocol/iq/usync.d.ts.map +0 -1
  245. package/dist/protocol/iq/usync.js.map +0 -1
  246. package/dist/protocol/push/app-state-key-share.d.ts.map +0 -1
  247. package/dist/protocol/push/app-state-key-share.js.map +0 -1
  248. package/dist/protocol/push/call-failure.d.ts.map +0 -1
  249. package/dist/protocol/push/call-failure.js.map +0 -1
  250. package/dist/protocol/push/chatstate.d.ts.map +0 -1
  251. package/dist/protocol/push/chatstate.js.map +0 -1
  252. package/dist/protocol/push/error-stanza.d.ts.map +0 -1
  253. package/dist/protocol/push/error-stanza.js.map +0 -1
  254. package/dist/protocol/push/history-sync.d.ts.map +0 -1
  255. package/dist/protocol/push/history-sync.js.map +0 -1
  256. package/dist/protocol/push/message.d.ts.map +0 -1
  257. package/dist/protocol/push/message.js.map +0 -1
  258. package/dist/protocol/push/notification.d.ts.map +0 -1
  259. package/dist/protocol/push/notification.js.map +0 -1
  260. package/dist/protocol/push/presence.d.ts.map +0 -1
  261. package/dist/protocol/push/presence.js.map +0 -1
  262. package/dist/protocol/push/receipt.d.ts.map +0 -1
  263. package/dist/protocol/push/receipt.js.map +0 -1
  264. package/dist/protocol/signal/fake-app-state-crypto.d.ts.map +0 -1
  265. package/dist/protocol/signal/fake-app-state-crypto.js.map +0 -1
  266. package/dist/protocol/signal/fake-peer-double-ratchet.d.ts.map +0 -1
  267. package/dist/protocol/signal/fake-peer-double-ratchet.js.map +0 -1
  268. package/dist/protocol/signal/fake-peer-group-recv-session.d.ts.map +0 -1
  269. package/dist/protocol/signal/fake-peer-group-recv-session.js.map +0 -1
  270. package/dist/protocol/signal/fake-peer-identity.d.ts.map +0 -1
  271. package/dist/protocol/signal/fake-peer-identity.js.map +0 -1
  272. package/dist/protocol/signal/fake-peer-key-bundle.d.ts.map +0 -1
  273. package/dist/protocol/signal/fake-peer-key-bundle.js.map +0 -1
  274. package/dist/protocol/signal/fake-peer-recv-session.d.ts +0 -60
  275. package/dist/protocol/signal/fake-peer-recv-session.d.ts.map +0 -1
  276. package/dist/protocol/signal/fake-peer-recv-session.js +0 -284
  277. package/dist/protocol/signal/fake-peer-recv-session.js.map +0 -1
  278. package/dist/protocol/signal/fake-peer-session.d.ts +0 -87
  279. package/dist/protocol/signal/fake-peer-session.d.ts.map +0 -1
  280. package/dist/protocol/signal/fake-peer-session.js +0 -207
  281. package/dist/protocol/signal/fake-peer-session.js.map +0 -1
  282. package/dist/protocol/signal/fake-sender-key.d.ts.map +0 -1
  283. package/dist/protocol/signal/fake-sender-key.js.map +0 -1
  284. package/dist/protocol/signal/prekey-upload.d.ts.map +0 -1
  285. package/dist/protocol/signal/prekey-upload.js.map +0 -1
  286. package/dist/protocol/stream/stream-error.d.ts.map +0 -1
  287. package/dist/protocol/stream/stream-error.js.map +0 -1
  288. package/dist/state/fake-app-state-collection.d.ts.map +0 -1
  289. package/dist/state/fake-app-state-collection.js.map +0 -1
  290. package/dist/state/fake-media-store.d.ts.map +0 -1
  291. package/dist/state/fake-media-store.js.map +0 -1
  292. package/dist/transport/codec.d.ts.map +0 -1
  293. package/dist/transport/codec.js.map +0 -1
  294. package/dist/transport/crypto.d.ts.map +0 -1
  295. package/dist/transport/crypto.js.map +0 -1
  296. package/dist/transport/index.d.ts +0 -4
  297. package/dist/transport/index.d.ts.map +0 -1
  298. package/dist/transport/index.js +0 -20
  299. package/dist/transport/index.js.map +0 -1
  300. package/dist/transport/protos.d.ts.map +0 -1
  301. package/dist/transport/protos.js.map +0 -1
package/README.md CHANGED
@@ -1,6 +1,14 @@
1
1
  # @zapo-js/fake-server
2
2
 
3
- In-process fake WhatsApp Web server that drives the real `zapo-js` `WaClient` end-to-end — full Noise XX/IK handshake, QR pairing, Signal Protocol (X3DH + Double Ratchet), SenderKey for groups, media upload/download over self-signed HTTPS, app-state sync — all without touching WhatsApp servers.
3
+ In-process fake WhatsApp Web server that drives the real `zapo-js` `WaClient` end-to-end - full Noise XX/IK handshake, QR pairing, Signal Protocol (X3DH + Double Ratchet), SenderKey for groups, media upload/download over self-signed HTTPS, app-state sync - all without touching WhatsApp servers.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i -D @zapo-js/fake-server zapo-js
9
+ ```
10
+
11
+ `zapo-js` is a peer dependency - the fake server reuses the core's binary codec, crypto, and protos. Everything below works from any project; nothing requires this monorepo.
4
12
 
5
13
  ## Quick start
6
14
 
@@ -29,8 +37,8 @@ await server.stop()
29
37
  ```text
30
38
  src/
31
39
  ├── api/ # Public-facing API
32
- │ ├── FakeWaServer.ts # Main facade — WS server, IQ router, registries, lifecycle
33
- │ ├── FakePeer.ts # Simulated WhatsApp peer — Signal crypto, send/recv, groups
40
+ │ ├── FakeWaServer.ts # Main facade – WS server, IQ router, registries, lifecycle
41
+ │ ├── FakePeer.ts # Simulated WhatsApp peer – Signal crypto, send/recv, groups
34
42
  │ ├── FakePairingDriver.ts # QR pairing flow orchestrator
35
43
  │ └── Scenario.ts # Declarative test scenario DSL
36
44
  ├── protocol/ # Protocol-layer builders, parsers, crypto
@@ -50,7 +58,7 @@ src/
50
58
  │ ├── fake-media-store.ts # In-memory media blob store
51
59
  │ └── fake-app-state-collection.ts # App-state patch/snapshot provider
52
60
  ├── transport/ # Re-exports from zapo-js (codec, crypto, protos)
53
- └── __tests__/ # Cross-check test suite (147 tests)
61
+ └── __tests__/ # Cross-check test suite
54
62
  └── helpers/ # Shared test utilities (zapo-client factory)
55
63
 
56
64
  bench/
@@ -63,14 +71,63 @@ bench/
63
71
 
64
72
  ### FakeWaServer
65
73
 
66
- Central facade. Manages the WS listener, noise handshake, IQ router, and all state registries:
74
+ Central facade. Manages the WS listener, noise handshake, IQ router, and all state registries.
75
+
76
+ Constructor options (`FakeWaServer.start({...})` / `new FakeWaServer({...})`):
77
+
78
+ ```ts
79
+ const server = await FakeWaServer.start({
80
+ host: '127.0.0.1', // default
81
+ port: 5222, // default: random free port
82
+ path: '/ws/chat', // default
83
+ successNodeAttributes: {
84
+ // stamped on every post-handshake <success/>
85
+ lid: '5511999@lid',
86
+ displayName: 'Fake Display'
87
+ },
88
+ defaultIqHandlers: false // start with an empty IQ router (default: true)
89
+ })
90
+ ```
67
91
 
68
92
  - **Peer registry** (`peerRegistry`): maps device JIDs → `FakePeer` instances. The global `usync` + `prekey-fetch` IQ handlers consult this.
93
+ - **Sessions**: state is grouped per session (see below). A single-client server uses one default session, and the `server.registries` / `server.expectIq(...)` / `server.preKeyDispenser` surface delegates straight to it.
69
94
  - **Group registry** (`groupRegistry`): maps group JIDs → group metadata + participants. The `w:g2` handler serves it.
70
- - **IQ router** (`WaFakeIqRouter`): first-match-wins stanza dispatcher with `{ xmlns, type, childTag }` matchers. ~20 global handlers registered in the constructor cover every IQ the lib emits during normal operation.
95
+ - **IQ router** (`WaFakeIqRouter`): first-match-wins stanza dispatcher with `{ xmlns, type, childTag }` matchers. ~27 global handlers registered in the constructor cover every IQ the lib emits during normal operation (disable them with `defaultIqHandlers: false`). `registerIqHandler(matcher, respond)` registers at high priority and shadows the defaults; a responder that returns `null` falls through to the next matching handler, so you can observe an IQ (capture, assert) and still let the default answer it.
71
96
  - **Prekey dispenser**: hands out unique one-time prekeys from the lib's upload to FakePeers. Resets on each forced refill (`triggerPreKeyUpload({ force: true })`).
72
97
  - **Listener fan-outs**: `onOutboundGroupOp`, `onOutboundPrivacySet`, `onOutboundBlocklistChange`, `onOutboundProfilePictureSet`, `onOutboundStatusSet`, `onLogout`, `onOutboundPrivacyTokenIssue`, `onOutboundDirtyBitsClear`.
73
98
 
99
+ ### Sessions (multiple clients, one server)
100
+
101
+ By default every connection shares one session, so a single `WaClient` sees the whole `FakeWaServer` surface. To run **several clients against one server without them sharing any state**, pass a `sessionKey` resolver: each authenticated connection is bound to an isolated session keyed by the returned id. Sessions never share peers, groups, prekeys, app-state, or captured stanzas.
102
+
103
+ ```ts
104
+ const server = await FakeWaServer.start({
105
+ // Key each connection by its login username; pre-login (registration)
106
+ // connections fall under one transient bucket.
107
+ sessionKey: ({ clientPayload }) =>
108
+ clientPayload.kind === 'login' ? String(clientPayload.username) : 'pending'
109
+ })
110
+
111
+ // After a client authenticates, reach its isolated state through the pipeline:
112
+ const session = server.sessionFor(pipeline) // FakeServerSession
113
+ await session.expectIq({ xmlns: 'passive', type: 'set' })
114
+ session.registries.peerRegistry // this client's peers only
115
+ session.preKeyDispenser.capturedPreKeyBundleSnapshot() // this client's prekeys
116
+
117
+ // Or address a session by id directly:
118
+ const alice = server.session('5511111111111')
119
+
120
+ // Peer creation routes to the connection's session automatically:
121
+ const peer = await server.createFakePeer({ jid: peerJid }, pipeline)
122
+ ```
123
+
124
+ Notes:
125
+
126
+ - The resolver runs once per connection, right after authentication (so `clientPayload` is available). Connections that resolve to the same id reuse one session.
127
+ - `server.registerIqHandler(...)` applies to every session (present and future); `session.registerIqHandler(...)` scopes to one session.
128
+ - The media HTTPS store is process-global and shared across sessions; isolation covers peers/groups/prekeys/app-state/captures, not uploaded media blobs.
129
+ - QR pairing writes to the connection's session too, so pair clients **sequentially** when keying registration connections into a shared bucket.
130
+
74
131
  ### FakePeer
75
132
 
76
133
  Simulated WhatsApp peer with real Signal Protocol crypto:
@@ -101,8 +158,10 @@ Every outbound IQ the lib sends during normal operation is handled:
101
158
  | `w:p` / `urn:xmpp:ping` get | `whatsapp-ping` / `xmpp-ping` | Ack |
102
159
  | `encrypt` set | `prekey-upload` | Captures bundle, resets dispenser |
103
160
  | `encrypt` get `<digest>` | `signal-digest` | Returns 404 → forces upload |
161
+ | `encrypt` get `<count>` | `prekey-count` | Serves remaining dispenser prekey count |
104
162
  | `encrypt` set `<rotate>` | `signed-prekey-rotate` | Ack |
105
163
  | `encrypt` get `<key>` | `prekey-fetch` | Serves peer bundles from registry |
164
+ | `passive` set | `passive-mode` | Ack |
106
165
  | `usync` get | `usync` | Resolves device IDs from registry |
107
166
  | `w:m` set `<media_conn>` | `media-conn` | Points lib at fake HTTPS server |
108
167
  | `w:sync:app:state` set | `app-state-sync` | Serves patches/snapshots from providers |
@@ -119,6 +178,16 @@ Every outbound IQ the lib sends during normal operation is handled:
119
178
  | `newsletter` get `<my_addons>` | `newsletter-my-addons` | Ack |
120
179
  | `urn:xmpp:whatsapp:dirty` set | `dirty-bits-clear` | Captures cleared bits |
121
180
 
181
+ ## Using with non-zapo-js clients
182
+
183
+ The fake server speaks the real wire protocol, so other WhatsApp Web libraries (Baileys forks, whatsmeow, ...) can connect to it too. The recipe:
184
+
185
+ 1. **Point the client's WebSocket URL at `server.url`** (the upgrade path is `/ws/chat`, same as production).
186
+ 2. **Trust the fake root CA.** The server signs its Noise cert chain with a per-instance random root; clients that verify the cert signature against WhatsApp's pinned production key must be told to trust `server.noiseRootCa.publicKey` instead (exposed via the CLI's `--json` as `noiseRootCa.publicKeyHex`). The chain carries a valid `notBefore`/`notAfter` window, so clients that enforce validity work out of the box.
187
+ 3. **Post-connect IQs are answered by default**: the `passive` set IQ and the `encrypt` get `<count>` prekey-count query, which Baileys-family and whatsmeow clients block on before reporting the connection as open.
188
+ 4. **Offline drain bulletin**: after every login the server sends `<ib><offline count="0"/></ib>`, which Baileys-family clients require before flushing their buffered events.
189
+ 5. **QR pairing**: in-process, use `server.runPairing(...)` with the client's `advSecretKey` + identity public key; from the standalone CLI, use `--pair <device-jid>` and paste the QR payload the client displays when prompted.
190
+
122
191
  ## Benchmarking
123
192
 
124
193
  ```bash
@@ -161,16 +230,35 @@ node --expose-gc --import tsx packages/fake-server/bench/messaging.bench.ts --cp
161
230
 
162
231
  ## CLI
163
232
 
233
+ The package ships a `fake-wa-server` bin, so the standalone server works from any project (or no project at all):
234
+
235
+ ```bash
236
+ # One-off via npx
237
+ npx @zapo-js/fake-server --port 5222 --peer 5511888@s.whatsapp.net --log
238
+
239
+ # Installed as a dev dependency
240
+ npx fake-wa-server --port 5222 --peer 5511888@s.whatsapp.net --log
241
+
242
+ # First-time QR pairing: prompts on stdin for the QR payload the client displays
243
+ npx fake-wa-server --port 5222 --pair 5511999:1@s.whatsapp.net
244
+
245
+ # Print connection info (url + noise root CA hex) as JSON, for scripting
246
+ npx fake-wa-server --json
247
+ ```
248
+
249
+ From inside this monorepo (runs the source, no build needed):
250
+
164
251
  ```bash
165
- # Standalone server for manual testing
166
252
  npm --workspace=@zapo-js/fake-server run cli -- --port 5222 --peer 5511888@s.whatsapp.net --log
167
253
  ```
168
254
 
255
+ Run with `--help` for the full flag list.
256
+
169
257
  ## Test suite
170
258
 
171
259
  ```bash
172
260
  npm --workspace=@zapo-js/fake-server test
173
- # 147 tests, --test-concurrency=1
261
+ # 164 tests, --test-concurrency=1
174
262
  ```
175
263
 
176
264
  Cross-check tests drive a real `WaClient` against the fake server and assert on both sides (lib emits correct events, peer decrypts correctly). Unit tests validate protocol builders/parsers in isolation.
@@ -24,4 +24,3 @@ export declare class AppStateSyncManager {
24
24
  pushServerSyncNotification(pipeline: WaFakeConnectionPipeline, input: BuildServerSyncNotificationInput): Promise<void>;
25
25
  consumeOutboundAppStatePatches(iq: BinaryNode): Promise<void>;
26
26
  }
27
- //# sourceMappingURL=AppStateSyncManager.d.ts.map
@@ -148,4 +148,3 @@ class AppStateSyncManager {
148
148
  }
149
149
  }
150
150
  exports.AppStateSyncManager = AppStateSyncManager;
151
- //# sourceMappingURL=AppStateSyncManager.js.map
@@ -24,4 +24,3 @@ export declare class FakePairingDriver {
24
24
  constructor(options: FakePairingDriverOptions, deps: FakePairingDriverDeps);
25
25
  run(): Promise<void>;
26
26
  }
27
- //# sourceMappingURL=FakePairingDriver.d.ts.map
@@ -33,4 +33,3 @@ class FakePairingDriver {
33
33
  }
34
34
  }
35
35
  exports.FakePairingDriver = FakePairingDriver;
36
- //# sourceMappingURL=FakePairingDriver.js.map
@@ -12,6 +12,13 @@ export interface CreateFakePeerOptions {
12
12
  readonly displayName?: string;
13
13
  readonly keyBundle?: FakePeerKeyBundle;
14
14
  readonly skipOneTimePreKey?: boolean;
15
+ /**
16
+ * Stash every outbound (id → plaintext + pristine ciphertext) so
17
+ * tests / benches can drive replays via {@link FakePeer.replaySentMessage}.
18
+ * Off by default to avoid inflating the fixture's heap on long
19
+ * memory-focused runs that never replay.
20
+ */
21
+ readonly enableReplayCache?: boolean;
15
22
  }
16
23
  export interface SendMessageOptions {
17
24
  readonly id?: string;
@@ -45,20 +52,57 @@ interface FakePeerDeps {
45
52
  export declare class FakePeer {
46
53
  readonly jid: string;
47
54
  readonly displayName?: string;
48
- readonly keyBundle: FakePeerKeyBundle;
55
+ keyBundle: FakePeerKeyBundle;
49
56
  readonly identity: FakePeerIdentity;
50
57
  private readonly deps;
51
- private readonly ratchet;
58
+ private ratchet;
52
59
  private readonly skipOneTimePreKey;
53
- private readonly reservedOneTimePreKey;
60
+ private reservedOneTimePreKey;
54
61
  private ratchetInitiated;
55
62
  private nextMessageCounter;
63
+ private readonly replayCacheEnabled;
64
+ private readonly sentPlaintextById;
65
+ private readonly sentEncryptedById;
56
66
  private readonly senderKeysByGroup;
57
67
  private readonly groupsBootstrapped;
58
68
  private readonly groupRecvSession;
59
69
  private constructor();
60
70
  static create(options: CreateFakePeerOptions, deps: FakePeerDeps): Promise<FakePeer>;
61
71
  sendMessage(message: proto.IMessage, options?: SendMessageOptions): Promise<void>;
72
+ /**
73
+ * Re-encrypts and re-pushes a previously sent message identified by
74
+ * `originalMsgId`. Used by tests / benches that exercise the lib's
75
+ * incoming-retry path: after the lib emits `<receipt type="retry">`
76
+ * back to this peer, the peer is expected to re-send the same
77
+ * plaintext so the lib can decrypt it successfully on the second
78
+ * attempt.
79
+ *
80
+ * The replay reuses the original stanza id by default so the lib's
81
+ * dedup logic (and tests that wait for a specific id) see the resend
82
+ * as a continuation of the same outbound. Pass `resendId` to use a
83
+ * different id.
84
+ */
85
+ replaySentMessage(originalMsgId: string, options?: {
86
+ readonly resendId?: string;
87
+ }): Promise<void>;
88
+ /**
89
+ * Rotates the peer's prekey material and resets the Signal session
90
+ * state so the lib has to re-run X3DH on the next outbound. Keeps
91
+ * the identity key pair (same user) but issues a new signed prekey
92
+ * + new one-time prekey set. After this call:
93
+ * - `keyBundle` reflects the new material;
94
+ * - the underlying `FakePeerDoubleRatchet` is fresh – the next
95
+ * inbound from the lib must be a `pkmsg` (initial signal
96
+ * message), which the new `keyBundle` is wired to decrypt;
97
+ * - any cached one-time prekey reservation in the dispenser is
98
+ * dropped so subsequent retry-receipts include this peer's new
99
+ * keys directly via {@link sendRetryReceipt}.
100
+ *
101
+ * Use this to simulate the "session lost" scenario that real
102
+ * WhatsApp clients trigger when they need the sender to fully
103
+ * re-bootstrap the Signal session via the retry-receipt path.
104
+ */
105
+ rotateForRetry(): Promise<void>;
62
106
  sendConversation(text: string, options?: SendMessageOptions): Promise<void>;
63
107
  sendMessageEdit(input: {
64
108
  readonly targetMessageId: string;
@@ -131,6 +175,36 @@ export declare class FakePeer {
131
175
  sendAppStateSyncKeyShare(input: BuildAppStateSyncKeyShareInput, options?: SendMessageOptions): Promise<void>;
132
176
  sendGroupConversation(groupJid: string, text: string, options?: SendMessageOptions): Promise<void>;
133
177
  sendGroupMessage(groupJid: string, message: proto.IMessage, options?: SendMessageOptions): Promise<void>;
178
+ /**
179
+ * Sends a `<receipt type="retry">` stanza to the lib asking it to
180
+ * re-encrypt and re-send the message identified by `originalMsgId`.
181
+ * Used by benches / tests that exercise the lib's outbound retry
182
+ * replay path.
183
+ *
184
+ * Shape matches WhatsApp Web's `WAWebRetryRequestParser`:
185
+ * `<receipt id type=retry from t><retry id count?/><registration>4B</registration>[<keys>...</keys>]</receipt>`.
186
+ *
187
+ * When `includeKeys` is true (default), the `<keys>` block carries
188
+ * the peer's current identity + signed prekey + one-time prekey.
189
+ * This forces the lib's `WaRetryCoordinator` to tear down the
190
+ * existing session and re-run X3DH against these fresh keys before
191
+ * re-encrypting the original payload, so the resend lands as a
192
+ * `pkmsg` against the peer's *new* prekey state. Pair this with
193
+ * {@link rotateForRetry} to simulate full session loss + recovery.
194
+ *
195
+ * `<device-identity>` is omitted: it's `maybeChild` in wa-web and
196
+ * also optional in the lib's parser. The fake-peer fixture doesn't
197
+ * have an ADV primary-device to sign one, and the lib's outbound
198
+ * retry replay path does not require it.
199
+ */
200
+ sendRetryReceipt(originalMsgId: string, options?: {
201
+ readonly count?: number;
202
+ readonly receiptId?: string;
203
+ readonly t?: number;
204
+ readonly error?: number;
205
+ readonly includeKeys?: boolean;
206
+ }): Promise<void>;
207
+ private buildRetryKeysNode;
134
208
  expectMessage(options?: ExpectMessageOptions): Promise<ReceivedMessage>;
135
209
  expectGroupMessage(groupJid: string, options?: ExpectGroupMessageOptions): Promise<ReceivedMessage>;
136
210
  private decryptPairwise;
@@ -140,4 +214,3 @@ export declare class FakePeer {
140
214
  private nextId;
141
215
  }
142
216
  export {};
143
- //# sourceMappingURL=FakePeer.d.ts.map
@@ -14,6 +14,8 @@ class FakePeer {
14
14
  constructor(keyBundle, options, deps) {
15
15
  this.ratchetInitiated = false;
16
16
  this.nextMessageCounter = 0;
17
+ this.sentPlaintextById = new Map();
18
+ this.sentEncryptedById = new Map();
17
19
  this.senderKeysByGroup = new Map();
18
20
  this.groupsBootstrapped = new Set();
19
21
  this.groupRecvSession = new fake_peer_group_recv_session_1.FakePeerGroupRecvSession();
@@ -26,6 +28,7 @@ class FakePeer {
26
28
  this.displayName = options.displayName;
27
29
  this.deps = deps;
28
30
  this.skipOneTimePreKey = options.skipOneTimePreKey === true;
31
+ this.replayCacheEnabled = options.enableReplayCache === true;
29
32
  this.reservedOneTimePreKey =
30
33
  !this.skipOneTimePreKey && deps.reserveOneTimePreKey
31
34
  ? deps.reserveOneTimePreKey()
@@ -45,6 +48,18 @@ class FakePeer {
45
48
  : ciphertext;
46
49
  const enc = { type, ciphertext: finalCiphertext };
47
50
  const id = options.id ?? this.nextId();
51
+ if (this.replayCacheEnabled) {
52
+ // Stash the plaintext so the bench / a test can replay this
53
+ // exact payload later in response to an incoming retry
54
+ // receipt from the lib (replaySentMessage). Also stash the
55
+ // PRISTINE ciphertext (pre-tamper) + signal type so a
56
+ // replay can put the original frame back on the wire –
57
+ // matches how a real Signal sender re-sends after a
58
+ // recipient asks for retry: same counter + ratchet key,
59
+ // just an uncorrupted body.
60
+ this.sentPlaintextById.set(id, message);
61
+ this.sentEncryptedById.set(id, { type, ciphertext });
62
+ }
48
63
  const stanza = (0, message_1.buildMessage)({
49
64
  id,
50
65
  from: options.from ?? this.jid,
@@ -56,6 +71,76 @@ class FakePeer {
56
71
  });
57
72
  await this.deps.pushStanza(stanza);
58
73
  }
74
+ /**
75
+ * Re-encrypts and re-pushes a previously sent message identified by
76
+ * `originalMsgId`. Used by tests / benches that exercise the lib's
77
+ * incoming-retry path: after the lib emits `<receipt type="retry">`
78
+ * back to this peer, the peer is expected to re-send the same
79
+ * plaintext so the lib can decrypt it successfully on the second
80
+ * attempt.
81
+ *
82
+ * The replay reuses the original stanza id by default so the lib's
83
+ * dedup logic (and tests that wait for a specific id) see the resend
84
+ * as a continuation of the same outbound. Pass `resendId` to use a
85
+ * different id.
86
+ */
87
+ async replaySentMessage(originalMsgId, options = {}) {
88
+ const stored = this.sentEncryptedById.get(originalMsgId);
89
+ if (stored) {
90
+ // Re-send the pristine ciphertext – same Signal counter +
91
+ // ratchet key as the original send, just without the
92
+ // tamper. The lib's recv chain still has the message key
93
+ // for that counter, so this is the recovery path real
94
+ // WhatsApp peers walk after observing a retry receipt for
95
+ // a frame the recipient failed to decrypt.
96
+ const enc = { type: stored.type, ciphertext: stored.ciphertext };
97
+ const id = options.resendId ?? originalMsgId;
98
+ const stanza = (0, message_1.buildMessage)({
99
+ id,
100
+ from: this.jid,
101
+ type: 'text',
102
+ notify: this.displayName,
103
+ enc: [enc]
104
+ });
105
+ await this.deps.pushStanza(stanza);
106
+ return;
107
+ }
108
+ const message = this.sentPlaintextById.get(originalMsgId);
109
+ if (!message) {
110
+ throw new Error(`FakePeer.replaySentMessage: no sent plaintext for id ${originalMsgId}`);
111
+ }
112
+ await this.sendMessage(message, { id: options.resendId ?? originalMsgId });
113
+ }
114
+ /**
115
+ * Rotates the peer's prekey material and resets the Signal session
116
+ * state so the lib has to re-run X3DH on the next outbound. Keeps
117
+ * the identity key pair (same user) but issues a new signed prekey
118
+ * + new one-time prekey set. After this call:
119
+ * - `keyBundle` reflects the new material;
120
+ * - the underlying `FakePeerDoubleRatchet` is fresh – the next
121
+ * inbound from the lib must be a `pkmsg` (initial signal
122
+ * message), which the new `keyBundle` is wired to decrypt;
123
+ * - any cached one-time prekey reservation in the dispenser is
124
+ * dropped so subsequent retry-receipts include this peer's new
125
+ * keys directly via {@link sendRetryReceipt}.
126
+ *
127
+ * Use this to simulate the "session lost" scenario that real
128
+ * WhatsApp clients trigger when they need the sender to fully
129
+ * re-bootstrap the Signal session via the retry-receipt path.
130
+ */
131
+ async rotateForRetry() {
132
+ const refreshed = await (0, fake_peer_key_bundle_1.generateFakePeerKeyBundle)({
133
+ identityKeyPair: this.keyBundle.identityKeyPair,
134
+ registrationId: this.keyBundle.registrationId,
135
+ signedPreKeyId: this.keyBundle.signedPreKey.id + 1,
136
+ firstOneTimePreKeyId: (this.keyBundle.oneTimePreKeys[this.keyBundle.oneTimePreKeys.length - 1]?.id ?? 0) +
137
+ 1
138
+ });
139
+ this.keyBundle = refreshed;
140
+ this.ratchet = new fake_peer_double_ratchet_1.FakePeerDoubleRatchet(refreshed);
141
+ this.ratchetInitiated = false;
142
+ this.reservedOneTimePreKey = null;
143
+ }
59
144
  sendConversation(text, options = {}) {
60
145
  return this.sendMessage({ conversation: text }, options);
61
146
  }
@@ -234,6 +319,112 @@ class FakePeer {
234
319
  });
235
320
  await this.deps.pushStanza(groupStanza);
236
321
  }
322
+ /**
323
+ * Sends a `<receipt type="retry">` stanza to the lib asking it to
324
+ * re-encrypt and re-send the message identified by `originalMsgId`.
325
+ * Used by benches / tests that exercise the lib's outbound retry
326
+ * replay path.
327
+ *
328
+ * Shape matches WhatsApp Web's `WAWebRetryRequestParser`:
329
+ * `<receipt id type=retry from t><retry id count?/><registration>4B</registration>[<keys>...</keys>]</receipt>`.
330
+ *
331
+ * When `includeKeys` is true (default), the `<keys>` block carries
332
+ * the peer's current identity + signed prekey + one-time prekey.
333
+ * This forces the lib's `WaRetryCoordinator` to tear down the
334
+ * existing session and re-run X3DH against these fresh keys before
335
+ * re-encrypting the original payload, so the resend lands as a
336
+ * `pkmsg` against the peer's *new* prekey state. Pair this with
337
+ * {@link rotateForRetry} to simulate full session loss + recovery.
338
+ *
339
+ * `<device-identity>` is omitted: it's `maybeChild` in wa-web and
340
+ * also optional in the lib's parser. The fake-peer fixture doesn't
341
+ * have an ADV primary-device to sign one, and the lib's outbound
342
+ * retry replay path does not require it.
343
+ */
344
+ async sendRetryReceipt(originalMsgId, options = {}) {
345
+ const receiptId = options.receiptId ?? originalMsgId;
346
+ const retryCount = options.count ?? 1;
347
+ const t = options.t ?? Math.floor(Date.now() / 1000);
348
+ const error = options.error;
349
+ const includeKeys = options.includeKeys !== false;
350
+ const registration = new Uint8Array(4);
351
+ const regId = this.keyBundle.registrationId & 0xffffffff;
352
+ registration[0] = (regId >>> 24) & 0xff;
353
+ registration[1] = (regId >>> 16) & 0xff;
354
+ registration[2] = (regId >>> 8) & 0xff;
355
+ registration[3] = regId & 0xff;
356
+ const retryAttrs = {
357
+ v: '1',
358
+ count: String(retryCount),
359
+ id: originalMsgId,
360
+ t: String(t)
361
+ };
362
+ if (error !== undefined && error !== 0) {
363
+ retryAttrs.error = String(error);
364
+ }
365
+ const content = [
366
+ {
367
+ tag: 'retry',
368
+ attrs: retryAttrs
369
+ },
370
+ {
371
+ tag: 'registration',
372
+ attrs: {},
373
+ content: registration
374
+ }
375
+ ];
376
+ if (includeKeys) {
377
+ content.push(this.buildRetryKeysNode());
378
+ }
379
+ const receipt = {
380
+ tag: 'receipt',
381
+ attrs: {
382
+ id: receiptId,
383
+ from: this.jid,
384
+ type: 'retry',
385
+ t: String(t)
386
+ },
387
+ content
388
+ };
389
+ await this.deps.pushStanza(receipt);
390
+ }
391
+ buildRetryKeysNode() {
392
+ const skey = this.keyBundle.signedPreKey;
393
+ const oneTime = this.keyBundle.oneTimePreKeys[0];
394
+ if (!oneTime) {
395
+ throw new Error('FakePeer.sendRetryReceipt: no one-time prekey available');
396
+ }
397
+ const skeyIdBytes = encodeUint3(skey.id);
398
+ const keyIdBytes = encodeUint3(oneTime.id);
399
+ return {
400
+ tag: 'keys',
401
+ attrs: {},
402
+ content: [
403
+ {
404
+ tag: 'identity',
405
+ attrs: {},
406
+ content: this.keyBundle.identityKeyPair.pubKey
407
+ },
408
+ {
409
+ tag: 'skey',
410
+ attrs: {},
411
+ content: [
412
+ { tag: 'id', attrs: {}, content: skeyIdBytes },
413
+ { tag: 'value', attrs: {}, content: skey.keyPair.pubKey },
414
+ { tag: 'signature', attrs: {}, content: skey.signature }
415
+ ]
416
+ },
417
+ {
418
+ tag: 'key',
419
+ attrs: {},
420
+ content: [
421
+ { tag: 'id', attrs: {}, content: keyIdBytes },
422
+ { tag: 'value', attrs: {}, content: oneTime.keyPair.pubKey }
423
+ ]
424
+ }
425
+ ]
426
+ };
427
+ }
237
428
  expectMessage(options = {}) {
238
429
  const timeoutMs = options.timeoutMs ?? 5000;
239
430
  return new Promise((resolve, reject) => {
@@ -359,7 +550,13 @@ class FakePeer {
359
550
  }
360
551
  nextId() {
361
552
  this.nextMessageCounter += 1;
362
- return `${this.jid}-${Date.now()}-${this.nextMessageCounter}`;
553
+ // WA-style uppercase hex, no '@': strict binary decoders tokenize an
554
+ // id containing '@' as a JID and reject the stanza. The registration
555
+ // id keeps ids from colliding across peers created in the same ms.
556
+ const peerPart = this.keyBundle.registrationId.toString(16).padStart(4, '0');
557
+ const timePart = Date.now().toString(16);
558
+ const counterPart = this.nextMessageCounter.toString(16).padStart(4, '0');
559
+ return `3EB0${peerPart}${timePart}${counterPart}`.toUpperCase();
363
560
  }
364
561
  }
365
562
  exports.FakePeer = FakePeer;
@@ -395,6 +592,16 @@ function findTopLevelEnc(stanza, type) {
395
592
  }
396
593
  return null;
397
594
  }
595
+ function encodeUint3(value) {
596
+ if (!Number.isInteger(value) || value < 0 || value > 0xffffff) {
597
+ throw new Error(`encodeUint3: value out of range: ${value}`);
598
+ }
599
+ const out = new Uint8Array(3);
600
+ out[0] = (value >>> 16) & 0xff;
601
+ out[1] = (value >>> 8) & 0xff;
602
+ out[2] = value & 0xff;
603
+ return out;
604
+ }
398
605
  function encodePlaintextWithPadding(message) {
399
606
  const encoded = protos_1.proto.Message.encode(message).finish();
400
607
  const padLen = 16 - (encoded.byteLength % 16);
@@ -405,4 +612,3 @@ function encodePlaintextWithPadding(message) {
405
612
  }
406
613
  return out;
407
614
  }
408
- //# sourceMappingURL=FakePeer.js.map
@@ -0,0 +1,67 @@
1
+ /** Per-session isolated server state: registries, prekeys, app-state, IQ routing, capture. */
2
+ import type { BuildAbPropsResultInput } from '../protocol/iq/abprops';
3
+ import { type WaFakeIqMatcher, type WaFakeIqResponder, WaFakeIqRouter } from '../protocol/iq/router';
4
+ import type { ClientPreKeyBundle } from '../protocol/signal/prekey-upload';
5
+ import { type BinaryNode } from '../transport/codec';
6
+ import { AppStateSyncManager } from './AppStateSyncManager';
7
+ import { PreKeyDispenser } from './PreKeyDispenser';
8
+ import { ServerRegistries } from './ServerRegistries';
9
+ export interface ExpectIqOptions {
10
+ readonly timeoutMs?: number;
11
+ }
12
+ export interface ExpectStanzaOptions {
13
+ readonly timeoutMs?: number;
14
+ }
15
+ export interface StanzaMatcher {
16
+ readonly tag?: string;
17
+ readonly type?: string;
18
+ readonly xmlns?: string;
19
+ readonly from?: string;
20
+ readonly to?: string;
21
+ readonly childTag?: string;
22
+ }
23
+ /**
24
+ * Shared server-side dependencies a session needs but does not own. The media
25
+ * HTTPS server is process-global (one listener), so its address is injected
26
+ * rather than duplicated per session.
27
+ */
28
+ export interface FakeServerSessionHost {
29
+ requireMediaHttpsInfo(): {
30
+ readonly host: string;
31
+ readonly port: number;
32
+ };
33
+ }
34
+ /**
35
+ * One isolated logical device's worth of server state. Every `WaClient`
36
+ * connected to a `FakeWaServer` is bound to exactly one session; peers,
37
+ * groups, privacy, prekeys, app-state, and captured stanzas never leak across
38
+ * sessions. A single-client server uses one default session, which is why the
39
+ * legacy `FakeWaServer` surface delegates straight through to it.
40
+ */
41
+ export declare class FakeServerSession {
42
+ readonly id: string;
43
+ readonly registries: ServerRegistries;
44
+ readonly preKeyDispenser: PreKeyDispenser;
45
+ readonly appStateSync: AppStateSyncManager;
46
+ readonly iqRouter: WaFakeIqRouter;
47
+ private readonly capturedStanzas;
48
+ private readonly pendingIqExpectations;
49
+ private readonly pendingStanzaExpectations;
50
+ private readonly capturedStanzaListeners;
51
+ private readonly inboundStanzaListeners;
52
+ constructor(id: string, host: FakeServerSessionHost, options: {
53
+ readonly defaultIqHandlers: boolean;
54
+ });
55
+ registerIqHandler(matcher: WaFakeIqMatcher, respond: WaFakeIqResponder, label?: string): () => void;
56
+ routeIq(iq: BinaryNode): Promise<BinaryNode | null>;
57
+ handleCapturedStanza(node: BinaryNode): void;
58
+ subscribeInboundMessages(listener: (node: BinaryNode) => void): () => void;
59
+ onCapturedStanza(listener: (node: BinaryNode) => void): () => void;
60
+ capturedStanzaSnapshot(): readonly BinaryNode[];
61
+ expectIq(matcher: WaFakeIqMatcher, options?: ExpectIqOptions): Promise<BinaryNode>;
62
+ expectStanza(matcher: StanzaMatcher, options?: ExpectStanzaOptions): Promise<BinaryNode>;
63
+ private buildIqHandlerDeps;
64
+ }
65
+ export declare function matchesIq(iq: BinaryNode, matcher: WaFakeIqMatcher): boolean;
66
+ export declare function matchesStanza(node: BinaryNode, matcher: StanzaMatcher): boolean;
67
+ export type { ClientPreKeyBundle, BuildAbPropsResultInput };