@zapo-js/fake-server 0.3.0 → 1.0.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 (298) hide show
  1. package/README.md +3 -3
  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 +201 -1
  8. package/dist/api/FakeWaServer.d.ts +11 -1
  9. package/dist/api/FakeWaServer.js +34 -2
  10. package/dist/api/PreKeyDispenser.d.ts +0 -1
  11. package/dist/api/PreKeyDispenser.js +0 -1
  12. package/dist/api/Scenario.d.ts +0 -1
  13. package/dist/api/Scenario.js +0 -1
  14. package/dist/api/ServerRegistries.d.ts +0 -1
  15. package/dist/api/ServerRegistries.js +0 -1
  16. package/dist/api/iq-handlers.d.ts +0 -1
  17. package/dist/api/iq-handlers.js +0 -1
  18. package/dist/{cli.d.ts → bin.d.ts} +0 -1
  19. package/dist/{cli.js → bin.js} +1 -2
  20. package/dist/esm/api/AppStateSyncManager.js +146 -0
  21. package/dist/esm/api/FakePairingDriver.js +31 -0
  22. package/dist/esm/api/FakePeer.js +604 -0
  23. package/dist/esm/api/FakeWaServer.js +680 -0
  24. package/dist/esm/api/PreKeyDispenser.js +93 -0
  25. package/dist/esm/api/Scenario.js +32 -0
  26. package/dist/esm/api/ServerRegistries.js +261 -0
  27. package/dist/esm/api/iq-handlers.js +492 -0
  28. package/dist/esm/bin.js +246 -0
  29. package/dist/esm/index.js +26 -0
  30. package/dist/esm/infra/WaFakeConnection.js +49 -0
  31. package/dist/esm/infra/WaFakeConnectionPipeline.js +223 -0
  32. package/dist/esm/infra/WaFakeFrameSocket.js +117 -0
  33. package/dist/esm/infra/WaFakeMediaHttpsServer.js +126 -0
  34. package/dist/esm/infra/WaFakeNoiseHandshake.js +67 -0
  35. package/dist/esm/infra/WaFakeTransport.js +31 -0
  36. package/dist/esm/infra/WaFakeWsServer.js +96 -0
  37. package/dist/esm/package.json +3 -0
  38. package/dist/esm/protocol/auth/cert-chain.js +39 -0
  39. package/dist/esm/protocol/auth/client-payload-validate.js +55 -0
  40. package/dist/esm/protocol/auth/fake-primary-device.js +47 -0
  41. package/dist/esm/protocol/auth/pair-device.js +87 -0
  42. package/dist/esm/protocol/auth/success-node.js +26 -0
  43. package/dist/esm/protocol/iq/abprops.js +30 -0
  44. package/dist/esm/protocol/iq/appstate-sync.js +146 -0
  45. package/dist/esm/protocol/iq/business.js +59 -0
  46. package/dist/esm/protocol/iq/dirty-bits.js +19 -0
  47. package/dist/esm/protocol/iq/group-ops.js +154 -0
  48. package/dist/esm/protocol/iq/newsletter.js +14 -0
  49. package/dist/esm/protocol/iq/prekey-fetch.js +88 -0
  50. package/dist/esm/protocol/iq/privacy-token.js +26 -0
  51. package/dist/esm/protocol/iq/privacy.js +123 -0
  52. package/dist/esm/protocol/iq/profile.js +72 -0
  53. package/dist/esm/protocol/iq/router.js +111 -0
  54. package/dist/esm/protocol/iq/usync.js +64 -0
  55. package/dist/esm/protocol/push/app-state-key-share.js +26 -0
  56. package/dist/esm/protocol/push/call-failure.js +27 -0
  57. package/dist/esm/protocol/push/chatstate.js +23 -0
  58. package/dist/esm/protocol/push/error-stanza.js +13 -0
  59. package/dist/esm/protocol/push/history-sync.js +111 -0
  60. package/dist/esm/protocol/push/message.js +38 -0
  61. package/dist/esm/protocol/push/notification.js +29 -0
  62. package/dist/esm/protocol/push/presence.js +14 -0
  63. package/dist/esm/protocol/push/receipt.js +21 -0
  64. package/dist/esm/protocol/signal/fake-app-state-crypto.js +196 -0
  65. package/dist/esm/protocol/signal/fake-peer-double-ratchet.js +398 -0
  66. package/dist/esm/protocol/signal/fake-peer-group-recv-session.js +120 -0
  67. package/dist/esm/protocol/signal/fake-peer-identity.js +8 -0
  68. package/dist/esm/protocol/signal/fake-peer-key-bundle.js +25 -0
  69. package/dist/esm/protocol/signal/fake-sender-key.js +83 -0
  70. package/dist/esm/protocol/signal/prekey-upload.js +107 -0
  71. package/dist/esm/protocol/stream/stream-error.js +56 -0
  72. package/dist/esm/state/fake-app-state-collection.js +134 -0
  73. package/dist/esm/state/fake-media-store.js +53 -0
  74. package/dist/esm/transport/codec.js +8 -0
  75. package/dist/esm/transport/crypto.js +8 -0
  76. package/dist/esm/transport/protos.js +7 -0
  77. package/dist/esm/transport/util.js +8 -0
  78. package/dist/index.d.ts +0 -1
  79. package/dist/index.js +0 -1
  80. package/dist/infra/WaFakeConnection.d.ts +0 -1
  81. package/dist/infra/WaFakeConnection.js +0 -1
  82. package/dist/infra/WaFakeConnectionPipeline.d.ts +0 -1
  83. package/dist/infra/WaFakeConnectionPipeline.js +27 -24
  84. package/dist/infra/WaFakeFrameSocket.d.ts +0 -1
  85. package/dist/infra/WaFakeFrameSocket.js +0 -1
  86. package/dist/infra/WaFakeMediaHttpsServer.d.ts +0 -1
  87. package/dist/infra/WaFakeMediaHttpsServer.js +0 -1
  88. package/dist/infra/WaFakeNoiseHandshake.d.ts +8 -10
  89. package/dist/infra/WaFakeNoiseHandshake.js +19 -24
  90. package/dist/infra/WaFakeTransport.d.ts +2 -4
  91. package/dist/infra/WaFakeTransport.js +2 -1
  92. package/dist/infra/WaFakeWsServer.d.ts +0 -1
  93. package/dist/infra/WaFakeWsServer.js +0 -1
  94. package/dist/protocol/auth/cert-chain.d.ts +0 -1
  95. package/dist/protocol/auth/cert-chain.js +0 -1
  96. package/dist/protocol/auth/client-payload-validate.d.ts +0 -1
  97. package/dist/protocol/auth/client-payload-validate.js +0 -1
  98. package/dist/protocol/auth/fake-primary-device.d.ts +0 -1
  99. package/dist/protocol/auth/fake-primary-device.js +1 -3
  100. package/dist/protocol/auth/pair-device.d.ts +0 -1
  101. package/dist/protocol/auth/pair-device.js +4 -16
  102. package/dist/protocol/auth/success-node.d.ts +0 -1
  103. package/dist/protocol/auth/success-node.js +0 -1
  104. package/dist/protocol/iq/abprops.d.ts +0 -1
  105. package/dist/protocol/iq/abprops.js +0 -1
  106. package/dist/protocol/iq/appstate-sync.d.ts +0 -1
  107. package/dist/protocol/iq/appstate-sync.js +0 -1
  108. package/dist/protocol/iq/business.d.ts +0 -1
  109. package/dist/protocol/iq/business.js +0 -1
  110. package/dist/protocol/iq/dirty-bits.d.ts +0 -1
  111. package/dist/protocol/iq/dirty-bits.js +0 -1
  112. package/dist/protocol/iq/group-ops.d.ts +0 -1
  113. package/dist/protocol/iq/group-ops.js +0 -1
  114. package/dist/protocol/iq/newsletter.d.ts +0 -1
  115. package/dist/protocol/iq/newsletter.js +0 -1
  116. package/dist/protocol/iq/prekey-fetch.d.ts +0 -1
  117. package/dist/protocol/iq/prekey-fetch.js +0 -1
  118. package/dist/protocol/iq/privacy-token.d.ts +0 -1
  119. package/dist/protocol/iq/privacy-token.js +0 -1
  120. package/dist/protocol/iq/privacy.d.ts +0 -1
  121. package/dist/protocol/iq/privacy.js +15 -16
  122. package/dist/protocol/iq/profile.d.ts +0 -1
  123. package/dist/protocol/iq/profile.js +0 -1
  124. package/dist/protocol/iq/router.d.ts +0 -1
  125. package/dist/protocol/iq/router.js +0 -1
  126. package/dist/protocol/iq/usync.d.ts +0 -1
  127. package/dist/protocol/iq/usync.js +0 -1
  128. package/dist/protocol/push/app-state-key-share.d.ts +0 -1
  129. package/dist/protocol/push/app-state-key-share.js +0 -1
  130. package/dist/protocol/push/call-failure.d.ts +0 -1
  131. package/dist/protocol/push/call-failure.js +0 -1
  132. package/dist/protocol/push/chatstate.d.ts +0 -1
  133. package/dist/protocol/push/chatstate.js +0 -1
  134. package/dist/protocol/push/error-stanza.d.ts +0 -1
  135. package/dist/protocol/push/error-stanza.js +0 -1
  136. package/dist/protocol/push/history-sync.d.ts +0 -1
  137. package/dist/protocol/push/history-sync.js +0 -1
  138. package/dist/protocol/push/message.d.ts +0 -1
  139. package/dist/protocol/push/message.js +0 -1
  140. package/dist/protocol/push/notification.d.ts +0 -1
  141. package/dist/protocol/push/notification.js +0 -1
  142. package/dist/protocol/push/presence.d.ts +0 -1
  143. package/dist/protocol/push/presence.js +0 -1
  144. package/dist/protocol/push/receipt.d.ts +0 -1
  145. package/dist/protocol/push/receipt.js +0 -1
  146. package/dist/protocol/signal/fake-app-state-crypto.d.ts +7 -9
  147. package/dist/protocol/signal/fake-app-state-crypto.js +31 -33
  148. package/dist/protocol/signal/fake-peer-double-ratchet.d.ts +0 -1
  149. package/dist/protocol/signal/fake-peer-double-ratchet.js +23 -28
  150. package/dist/protocol/signal/fake-peer-group-recv-session.d.ts +0 -1
  151. package/dist/protocol/signal/fake-peer-group-recv-session.js +6 -10
  152. package/dist/protocol/signal/fake-peer-identity.d.ts +0 -1
  153. package/dist/protocol/signal/fake-peer-identity.js +0 -1
  154. package/dist/protocol/signal/fake-peer-key-bundle.d.ts +0 -1
  155. package/dist/protocol/signal/fake-peer-key-bundle.js +0 -1
  156. package/dist/protocol/signal/fake-sender-key.d.ts +0 -1
  157. package/dist/protocol/signal/fake-sender-key.js +5 -9
  158. package/dist/protocol/signal/prekey-upload.d.ts +0 -1
  159. package/dist/protocol/signal/prekey-upload.js +0 -1
  160. package/dist/protocol/stream/stream-error.d.ts +0 -1
  161. package/dist/protocol/stream/stream-error.js +0 -1
  162. package/dist/state/fake-app-state-collection.d.ts +0 -1
  163. package/dist/state/fake-app-state-collection.js +0 -1
  164. package/dist/state/fake-media-store.d.ts +0 -1
  165. package/dist/state/fake-media-store.js +2 -2
  166. package/dist/transport/codec.d.ts +2 -3
  167. package/dist/transport/codec.js +2 -3
  168. package/dist/transport/crypto.d.ts +5 -7
  169. package/dist/transport/crypto.js +6 -11
  170. package/dist/transport/protos.d.ts +2 -4
  171. package/dist/transport/protos.js +2 -4
  172. package/dist/transport/util.d.ts +8 -0
  173. package/dist/transport/util.js +15 -0
  174. package/package.json +21 -6
  175. package/dist/api/AppStateSyncManager.d.ts.map +0 -1
  176. package/dist/api/AppStateSyncManager.js.map +0 -1
  177. package/dist/api/FakePairingDriver.d.ts.map +0 -1
  178. package/dist/api/FakePairingDriver.js.map +0 -1
  179. package/dist/api/FakePeer.d.ts.map +0 -1
  180. package/dist/api/FakePeer.js.map +0 -1
  181. package/dist/api/FakeWaServer.d.ts.map +0 -1
  182. package/dist/api/FakeWaServer.js.map +0 -1
  183. package/dist/api/PreKeyDispenser.d.ts.map +0 -1
  184. package/dist/api/PreKeyDispenser.js.map +0 -1
  185. package/dist/api/Scenario.d.ts.map +0 -1
  186. package/dist/api/Scenario.js.map +0 -1
  187. package/dist/api/ServerRegistries.d.ts.map +0 -1
  188. package/dist/api/ServerRegistries.js.map +0 -1
  189. package/dist/api/iq-handlers.d.ts.map +0 -1
  190. package/dist/api/iq-handlers.js.map +0 -1
  191. package/dist/cli.d.ts.map +0 -1
  192. package/dist/cli.js.map +0 -1
  193. package/dist/index.d.ts.map +0 -1
  194. package/dist/index.js.map +0 -1
  195. package/dist/infra/WaFakeConnection.d.ts.map +0 -1
  196. package/dist/infra/WaFakeConnection.js.map +0 -1
  197. package/dist/infra/WaFakeConnectionPipeline.d.ts.map +0 -1
  198. package/dist/infra/WaFakeConnectionPipeline.js.map +0 -1
  199. package/dist/infra/WaFakeFrameSocket.d.ts.map +0 -1
  200. package/dist/infra/WaFakeFrameSocket.js.map +0 -1
  201. package/dist/infra/WaFakeMediaHttpsServer.d.ts.map +0 -1
  202. package/dist/infra/WaFakeMediaHttpsServer.js.map +0 -1
  203. package/dist/infra/WaFakeNoiseHandshake.d.ts.map +0 -1
  204. package/dist/infra/WaFakeNoiseHandshake.js.map +0 -1
  205. package/dist/infra/WaFakeTransport.d.ts.map +0 -1
  206. package/dist/infra/WaFakeTransport.js.map +0 -1
  207. package/dist/infra/WaFakeWsServer.d.ts.map +0 -1
  208. package/dist/infra/WaFakeWsServer.js.map +0 -1
  209. package/dist/protocol/auth/cert-chain.d.ts.map +0 -1
  210. package/dist/protocol/auth/cert-chain.js.map +0 -1
  211. package/dist/protocol/auth/client-payload-validate.d.ts.map +0 -1
  212. package/dist/protocol/auth/client-payload-validate.js.map +0 -1
  213. package/dist/protocol/auth/fake-primary-device.d.ts.map +0 -1
  214. package/dist/protocol/auth/fake-primary-device.js.map +0 -1
  215. package/dist/protocol/auth/pair-device.d.ts.map +0 -1
  216. package/dist/protocol/auth/pair-device.js.map +0 -1
  217. package/dist/protocol/auth/success-node.d.ts.map +0 -1
  218. package/dist/protocol/auth/success-node.js.map +0 -1
  219. package/dist/protocol/iq/abprops.d.ts.map +0 -1
  220. package/dist/protocol/iq/abprops.js.map +0 -1
  221. package/dist/protocol/iq/appstate-sync.d.ts.map +0 -1
  222. package/dist/protocol/iq/appstate-sync.js.map +0 -1
  223. package/dist/protocol/iq/business.d.ts.map +0 -1
  224. package/dist/protocol/iq/business.js.map +0 -1
  225. package/dist/protocol/iq/dirty-bits.d.ts.map +0 -1
  226. package/dist/protocol/iq/dirty-bits.js.map +0 -1
  227. package/dist/protocol/iq/group-ops.d.ts.map +0 -1
  228. package/dist/protocol/iq/group-ops.js.map +0 -1
  229. package/dist/protocol/iq/newsletter.d.ts.map +0 -1
  230. package/dist/protocol/iq/newsletter.js.map +0 -1
  231. package/dist/protocol/iq/prekey-fetch.d.ts.map +0 -1
  232. package/dist/protocol/iq/prekey-fetch.js.map +0 -1
  233. package/dist/protocol/iq/privacy-token.d.ts.map +0 -1
  234. package/dist/protocol/iq/privacy-token.js.map +0 -1
  235. package/dist/protocol/iq/privacy.d.ts.map +0 -1
  236. package/dist/protocol/iq/privacy.js.map +0 -1
  237. package/dist/protocol/iq/profile.d.ts.map +0 -1
  238. package/dist/protocol/iq/profile.js.map +0 -1
  239. package/dist/protocol/iq/router.d.ts.map +0 -1
  240. package/dist/protocol/iq/router.js.map +0 -1
  241. package/dist/protocol/iq/usync.d.ts.map +0 -1
  242. package/dist/protocol/iq/usync.js.map +0 -1
  243. package/dist/protocol/push/app-state-key-share.d.ts.map +0 -1
  244. package/dist/protocol/push/app-state-key-share.js.map +0 -1
  245. package/dist/protocol/push/call-failure.d.ts.map +0 -1
  246. package/dist/protocol/push/call-failure.js.map +0 -1
  247. package/dist/protocol/push/chatstate.d.ts.map +0 -1
  248. package/dist/protocol/push/chatstate.js.map +0 -1
  249. package/dist/protocol/push/error-stanza.d.ts.map +0 -1
  250. package/dist/protocol/push/error-stanza.js.map +0 -1
  251. package/dist/protocol/push/history-sync.d.ts.map +0 -1
  252. package/dist/protocol/push/history-sync.js.map +0 -1
  253. package/dist/protocol/push/message.d.ts.map +0 -1
  254. package/dist/protocol/push/message.js.map +0 -1
  255. package/dist/protocol/push/notification.d.ts.map +0 -1
  256. package/dist/protocol/push/notification.js.map +0 -1
  257. package/dist/protocol/push/presence.d.ts.map +0 -1
  258. package/dist/protocol/push/presence.js.map +0 -1
  259. package/dist/protocol/push/receipt.d.ts.map +0 -1
  260. package/dist/protocol/push/receipt.js.map +0 -1
  261. package/dist/protocol/signal/fake-app-state-crypto.d.ts.map +0 -1
  262. package/dist/protocol/signal/fake-app-state-crypto.js.map +0 -1
  263. package/dist/protocol/signal/fake-peer-double-ratchet.d.ts.map +0 -1
  264. package/dist/protocol/signal/fake-peer-double-ratchet.js.map +0 -1
  265. package/dist/protocol/signal/fake-peer-group-recv-session.d.ts.map +0 -1
  266. package/dist/protocol/signal/fake-peer-group-recv-session.js.map +0 -1
  267. package/dist/protocol/signal/fake-peer-identity.d.ts.map +0 -1
  268. package/dist/protocol/signal/fake-peer-identity.js.map +0 -1
  269. package/dist/protocol/signal/fake-peer-key-bundle.d.ts.map +0 -1
  270. package/dist/protocol/signal/fake-peer-key-bundle.js.map +0 -1
  271. package/dist/protocol/signal/fake-peer-recv-session.d.ts +0 -60
  272. package/dist/protocol/signal/fake-peer-recv-session.d.ts.map +0 -1
  273. package/dist/protocol/signal/fake-peer-recv-session.js +0 -284
  274. package/dist/protocol/signal/fake-peer-recv-session.js.map +0 -1
  275. package/dist/protocol/signal/fake-peer-session.d.ts +0 -87
  276. package/dist/protocol/signal/fake-peer-session.d.ts.map +0 -1
  277. package/dist/protocol/signal/fake-peer-session.js +0 -207
  278. package/dist/protocol/signal/fake-peer-session.js.map +0 -1
  279. package/dist/protocol/signal/fake-sender-key.d.ts.map +0 -1
  280. package/dist/protocol/signal/fake-sender-key.js.map +0 -1
  281. package/dist/protocol/signal/prekey-upload.d.ts.map +0 -1
  282. package/dist/protocol/signal/prekey-upload.js.map +0 -1
  283. package/dist/protocol/stream/stream-error.d.ts.map +0 -1
  284. package/dist/protocol/stream/stream-error.js.map +0 -1
  285. package/dist/state/fake-app-state-collection.d.ts.map +0 -1
  286. package/dist/state/fake-app-state-collection.js.map +0 -1
  287. package/dist/state/fake-media-store.d.ts.map +0 -1
  288. package/dist/state/fake-media-store.js.map +0 -1
  289. package/dist/transport/codec.d.ts.map +0 -1
  290. package/dist/transport/codec.js.map +0 -1
  291. package/dist/transport/crypto.d.ts.map +0 -1
  292. package/dist/transport/crypto.js.map +0 -1
  293. package/dist/transport/index.d.ts +0 -4
  294. package/dist/transport/index.d.ts.map +0 -1
  295. package/dist/transport/index.js +0 -20
  296. package/dist/transport/index.js.map +0 -1
  297. package/dist/transport/protos.d.ts.map +0 -1
  298. package/dist/transport/protos.js.map +0 -1
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+ /** Standalone CLI for `@zapo-js/fake-server`. */
3
+ import { FakeWaServer } from './api/FakeWaServer.js';
4
+ function parseArgs(argv) {
5
+ let host = '127.0.0.1';
6
+ let port = 0;
7
+ let path = '/ws/chat';
8
+ const peerJids = [];
9
+ const groupSpecs = [];
10
+ let log = false;
11
+ let quiet = false;
12
+ let json = false;
13
+ let help = false;
14
+ for (let index = 0; index < argv.length; index += 1) {
15
+ const arg = argv[index];
16
+ const next = () => {
17
+ const value = argv[index + 1];
18
+ if (value === undefined) {
19
+ throw new Error(`missing value for ${arg}`);
20
+ }
21
+ index += 1;
22
+ return value;
23
+ };
24
+ switch (arg) {
25
+ case '-h':
26
+ case '--help':
27
+ help = true;
28
+ break;
29
+ case '--host':
30
+ host = next();
31
+ break;
32
+ case '--port':
33
+ port = Number.parseInt(next(), 10);
34
+ if (!Number.isFinite(port) || port < 0 || port > 65535) {
35
+ throw new Error(`invalid --port: ${argv[index]}`);
36
+ }
37
+ break;
38
+ case '--path':
39
+ path = next();
40
+ break;
41
+ case '--peer':
42
+ peerJids.push(next());
43
+ break;
44
+ case '--group':
45
+ groupSpecs.push(next());
46
+ break;
47
+ case '--log':
48
+ log = true;
49
+ break;
50
+ case '--quiet':
51
+ quiet = true;
52
+ break;
53
+ case '--json':
54
+ json = true;
55
+ break;
56
+ default:
57
+ throw new Error(`unknown flag: ${arg}`);
58
+ }
59
+ }
60
+ return { host, port, path, peerJids, groupSpecs, log, quiet, json, help };
61
+ }
62
+ function printHelp() {
63
+ process.stdout.write(`@zapo-js/fake-server – standalone fake WhatsApp Web server
64
+
65
+ USAGE
66
+ npx @zapo-js/fake-server [flags]
67
+
68
+ FLAGS
69
+ --host <host> Bind host (default: 127.0.0.1)
70
+ --port <port> WebSocket listener port (default: random)
71
+ --path <path> WebSocket upgrade path (default: /ws/chat)
72
+ --peer <jid> Pre-create a fake peer; can be repeated
73
+ --group <spec> Pre-create a fake group; spec format
74
+ <group-jid>=<peer-jid>,<peer-jid>,...
75
+ All peers must already be passed via --peer.
76
+ --log Print every captured inbound stanza
77
+ --quiet Suppress the startup info banner
78
+ --json Print connection info as JSON
79
+ -h | --help Show this help
80
+
81
+ The server stays up until SIGINT (Ctrl+C).
82
+ `);
83
+ }
84
+ function parseGroupSpec(spec) {
85
+ const eqIdx = spec.indexOf('=');
86
+ if (eqIdx < 0) {
87
+ throw new Error(`invalid --group spec '${spec}': expected '<group-jid>=<peer-jid>,<peer-jid>,...'`);
88
+ }
89
+ const groupJid = spec.slice(0, eqIdx).trim();
90
+ const participantsRaw = spec.slice(eqIdx + 1);
91
+ const participantJids = participantsRaw
92
+ .split(',')
93
+ .map((jid) => jid.trim())
94
+ .filter((jid) => jid.length > 0);
95
+ if (!groupJid || participantJids.length === 0) {
96
+ throw new Error(`invalid --group spec '${spec}': empty group jid or participant list`);
97
+ }
98
+ return { groupJid, participantJids };
99
+ }
100
+ async function main() {
101
+ let args;
102
+ try {
103
+ args = parseArgs(process.argv.slice(2));
104
+ }
105
+ catch (error) {
106
+ process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`);
107
+ process.stderr.write('run with --help to see usage.\n');
108
+ process.exit(2);
109
+ }
110
+ if (args.help) {
111
+ printHelp();
112
+ process.exit(0);
113
+ }
114
+ const parsedGroups = args.groupSpecs.map(parseGroupSpec);
115
+ for (const group of parsedGroups) {
116
+ for (const jid of group.participantJids) {
117
+ if (!args.peerJids.includes(jid)) {
118
+ process.stderr.write(`error: --group '${group.groupJid}' references peer '${jid}' that was not declared via --peer\n`);
119
+ process.exit(2);
120
+ }
121
+ }
122
+ }
123
+ const server = await FakeWaServer.start({
124
+ host: args.host,
125
+ port: args.port,
126
+ path: args.path
127
+ });
128
+ let setupComplete = false;
129
+ const setupPromise = new Promise((resolve, reject) => {
130
+ server.onAuthenticatedPipeline(async (pipeline) => {
131
+ if (setupComplete)
132
+ return;
133
+ setupComplete = true;
134
+ try {
135
+ const peers = new Map();
136
+ for (const jid of args.peerJids) {
137
+ const peer = await server.createFakePeer({ jid }, pipeline);
138
+ peers.set(jid, peer);
139
+ process.stdout.write(`[fake-server] created peer ${jid}\n`);
140
+ }
141
+ for (const group of parsedGroups) {
142
+ const participants = group.participantJids.map((jid) => {
143
+ const peer = peers.get(jid);
144
+ if (!peer) {
145
+ throw new Error(`internal: peer ${jid} not found for group ${group.groupJid}`);
146
+ }
147
+ return peer;
148
+ });
149
+ server.createFakeGroup({
150
+ groupJid: group.groupJid,
151
+ participants
152
+ });
153
+ process.stdout.write(`[fake-server] created group ${group.groupJid} with ${participants.length} participants\n`);
154
+ }
155
+ resolve();
156
+ }
157
+ catch (error) {
158
+ reject(error instanceof Error ? error : new Error(String(error)));
159
+ }
160
+ });
161
+ });
162
+ setupPromise.catch((error) => {
163
+ process.stderr.write(`error: ${error.message}\n`);
164
+ process.exitCode = 1;
165
+ });
166
+ if (args.log) {
167
+ server.onPipeline((pipeline) => {
168
+ pipeline.setEvents({
169
+ onStanza: (node) => {
170
+ process.stdout.write(`[wire] ${node.tag}${node.attrs.id ? ` id=${node.attrs.id}` : ''}${node.attrs.type ? ` type=${node.attrs.type}` : ''}\n`);
171
+ }
172
+ });
173
+ });
174
+ }
175
+ if (!args.quiet) {
176
+ const noiseRoot = server.noiseRootCa;
177
+ if (args.json) {
178
+ process.stdout.write(JSON.stringify({
179
+ url: server.url,
180
+ host: server.host,
181
+ port: server.port,
182
+ path: args.path,
183
+ noiseRootCa: {
184
+ serial: noiseRoot.serial,
185
+ publicKeyHex: bytesToHex(noiseRoot.publicKey)
186
+ },
187
+ peers: args.peerJids,
188
+ groups: parsedGroups
189
+ }, null, 2) + '\n');
190
+ }
191
+ else {
192
+ const banner = [
193
+ '',
194
+ '┌─────────────────────────────────────────────────────────────',
195
+ '│ @zapo-js/fake-server is up',
196
+ '├─────────────────────────────────────────────────────────────',
197
+ `│ url ${server.url}`,
198
+ `│ host ${server.host}`,
199
+ `│ port ${server.port}`,
200
+ `│ path ${args.path}`,
201
+ `│ noise root ca serial=${noiseRoot.serial} pub=${bytesToHex(noiseRoot.publicKey).slice(0, 32)}…`,
202
+ `│ peers ${args.peerJids.length === 0 ? '(none)' : args.peerJids.join(', ')}`,
203
+ `│ groups ${parsedGroups.length === 0 ? '(none)' : parsedGroups.map((g) => g.groupJid).join(', ')}`,
204
+ '├─────────────────────────────────────────────────────────────',
205
+ '│ Wire your WaClient with:',
206
+ '│ chatSocketUrls: [server.url]',
207
+ '│ testHooks: { noiseRootCa: <hex above as Uint8Array> }',
208
+ '│ Hit Ctrl+C to stop.',
209
+ '└─────────────────────────────────────────────────────────────',
210
+ ''
211
+ ];
212
+ process.stdout.write(banner.join('\n'));
213
+ }
214
+ }
215
+ let stopping = false;
216
+ const shutdown = async (signal) => {
217
+ if (stopping)
218
+ return;
219
+ stopping = true;
220
+ process.stdout.write(`\n[fake-server] received ${signal}, shutting down…\n`);
221
+ try {
222
+ await server.stop();
223
+ process.stdout.write('[fake-server] stopped cleanly\n');
224
+ process.exit(0);
225
+ }
226
+ catch (error) {
227
+ process.stderr.write(`error during shutdown: ${error instanceof Error ? error.message : String(error)}\n`);
228
+ process.exit(1);
229
+ }
230
+ };
231
+ process.on('SIGINT', () => void shutdown('SIGINT'));
232
+ process.on('SIGTERM', () => void shutdown('SIGTERM'));
233
+ await new Promise(() => undefined);
234
+ }
235
+ function bytesToHex(bytes) {
236
+ let out = '';
237
+ for (let index = 0; index < bytes.byteLength; index += 1) {
238
+ const value = bytes[index];
239
+ out += value < 16 ? `0${value.toString(16)}` : value.toString(16);
240
+ }
241
+ return out;
242
+ }
243
+ main().catch((error) => {
244
+ process.stderr.write(`fatal: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
245
+ process.exit(1);
246
+ });
@@ -0,0 +1,26 @@
1
+ export { FakePairingDriver } from './api/FakePairingDriver.js';
2
+ export { FakePeer } from './api/FakePeer.js';
3
+ export { FakeWaServer } from './api/FakeWaServer.js';
4
+ export { IqExpectation, Scenario } from './api/Scenario.js';
5
+ export { WaFakeConnection } from './infra/WaFakeConnection.js';
6
+ export { buildAppStateSyncFullResult, buildAppStateSyncResult, buildServerSyncNotification, parseAppStateSyncRequest } from './protocol/iq/appstate-sync.js';
7
+ export { buildIqError, buildIqResult } from './protocol/iq/router.js';
8
+ export { buildCall, buildFailure } from './protocol/push/call-failure.js';
9
+ export { buildChatstate } from './protocol/push/chatstate.js';
10
+ export { buildIncomingErrorStanza } from './protocol/push/error-stanza.js';
11
+ export { buildAppStateSyncKeyShareMessage } from './protocol/push/app-state-key-share.js';
12
+ export { buildHistorySyncExternalMessage, buildHistorySyncMessage, encodeHistorySyncPlaintext } from './protocol/push/history-sync.js';
13
+ export { APP_STATE_EMPTY_LT_HASH, FakeAppStateCrypto } from './protocol/signal/fake-app-state-crypto.js';
14
+ export { FakeAppStateCollection } from './state/fake-app-state-collection.js';
15
+ export { FakeMediaStore } from './state/fake-media-store.js';
16
+ export { buildMessage } from './protocol/push/message.js';
17
+ export { buildGroupNotification, buildNotification } from './protocol/push/notification.js';
18
+ export { buildIncomingPresence } from './protocol/push/presence.js';
19
+ export { buildReceipt } from './protocol/push/receipt.js';
20
+ export { generateFakePeerIdentity } from './protocol/signal/fake-peer-identity.js';
21
+ export { FakePeerDoubleRatchet } from './protocol/signal/fake-peer-double-ratchet.js';
22
+ export { FakeSenderKey } from './protocol/signal/fake-sender-key.js';
23
+ export { buildAdvSignedDeviceIdentity, generateFakePrimaryDevice } from './protocol/auth/fake-primary-device.js';
24
+ export { buildPairDeviceIq, buildPairSuccessIq, parsePairingQrString } from './protocol/auth/pair-device.js';
25
+ export { parsePreKeyUploadIq, PreKeyUploadParseError } from './protocol/signal/prekey-upload.js';
26
+ export { buildStreamErrorAck, buildStreamErrorCode, buildStreamErrorDeviceRemoved, buildStreamErrorReplaced, buildStreamErrorXmlNotWellFormed } from './protocol/stream/stream-error.js';
@@ -0,0 +1,49 @@
1
+ export class WaFakeConnection {
2
+ constructor(id, socket) {
3
+ this.handlers = {};
4
+ this.currentState = 'open';
5
+ this.id = id;
6
+ this.socket = socket;
7
+ this.bindSocketEvents();
8
+ }
9
+ get state() {
10
+ return this.currentState;
11
+ }
12
+ setHandlers(handlers) {
13
+ this.handlers = handlers;
14
+ }
15
+ sendFrame(frame) {
16
+ if (this.currentState !== 'open') {
17
+ throw new Error(`cannot send frame on connection in state "${this.currentState}"`);
18
+ }
19
+ this.socket.send(frame);
20
+ }
21
+ close(code = 1000, reason = '') {
22
+ if (this.currentState === 'closed' || this.currentState === 'closing') {
23
+ return;
24
+ }
25
+ this.currentState = 'closing';
26
+ this.socket.close(code, reason);
27
+ }
28
+ bindSocketEvents() {
29
+ this.socket.on('message', (data, isBinary) => {
30
+ if (!isBinary) {
31
+ this.handlers.onError?.(new Error('received unexpected text frame'));
32
+ return;
33
+ }
34
+ const frame = data instanceof Uint8Array
35
+ ? data
36
+ : Array.isArray(data)
37
+ ? new Uint8Array(Buffer.concat(data))
38
+ : new Uint8Array(data);
39
+ this.handlers.onFrame?.(frame);
40
+ });
41
+ this.socket.on('close', (code, reasonBuf) => {
42
+ this.currentState = 'closed';
43
+ this.handlers.onClose?.({ code, reason: reasonBuf.toString('utf8') });
44
+ });
45
+ this.socket.on('error', (error) => {
46
+ this.handlers.onError?.(error);
47
+ });
48
+ }
49
+ }
@@ -0,0 +1,223 @@
1
+ /** Per-connection lifecycle pipeline (Noise handshake + stanza loop). */
2
+ import { buildFakeCertChain } from '../protocol/auth/cert-chain.js';
3
+ import { parseClientPayload } from '../protocol/auth/client-payload-validate.js';
4
+ import { buildSuccessNode } from '../protocol/auth/success-node.js';
5
+ import { decodeBinaryNodeStanza, encodeBinaryNodeStanza } from '../transport/codec.js';
6
+ import { X25519 } from '../transport/crypto.js';
7
+ import { proto } from '../transport/protos.js';
8
+ import { WaFakeFrameSocket } from './WaFakeFrameSocket.js';
9
+ import { WaFakeNoiseHandshake } from './WaFakeNoiseHandshake.js';
10
+ import { WaFakeTransport } from './WaFakeTransport.js';
11
+ const NOISE_XX_NAME = new TextEncoder().encode('Noise_XX_25519_AESGCM_SHA256\0\0\0\0');
12
+ const NOISE_IK_NAME = new TextEncoder().encode('Noise_IK_25519_AESGCM_SHA256\0\0\0\0');
13
+ const PROLOGUE = new Uint8Array([0x57, 0x41, 0x06, 0x03]);
14
+ export class WaFakeConnectionPipeline {
15
+ constructor(config) {
16
+ this.events = {};
17
+ this.state = { kind: 'awaiting_prologue' };
18
+ this.chain = Promise.resolve();
19
+ this.config = config;
20
+ this.frameSocket = new WaFakeFrameSocket(config.connection);
21
+ this.frameSocket.setHandlers({
22
+ onPrologue: () => this.onPrologue(),
23
+ onFrame: (frame) => this.scheduleFrame(frame),
24
+ onClose: (info) => {
25
+ this.state = { kind: 'closed' };
26
+ this.events.onClose?.(info);
27
+ },
28
+ onError: (error) => this.events.onError?.(error)
29
+ });
30
+ }
31
+ setEvents(events) {
32
+ this.events = events;
33
+ }
34
+ isAuthenticated() {
35
+ return this.state.kind === 'authenticated';
36
+ }
37
+ async sendStanza(node) {
38
+ if (this.state.kind !== 'authenticated') {
39
+ throw new Error(`cannot send stanza while pipeline is in state "${this.state.kind}"`);
40
+ }
41
+ const stanza = encodeBinaryNodeStanza(node);
42
+ // Keep outbound encrypted frames in FIFO order across concurrent callers.
43
+ const ciphertext = await this.state.transport.encryptFrame(stanza);
44
+ this.frameSocket.sendFrame(ciphertext);
45
+ }
46
+ onPrologue() {
47
+ if (this.state.kind !== 'awaiting_prologue') {
48
+ this.events.onError?.(new Error('received second prologue from client'));
49
+ return;
50
+ }
51
+ this.state = { kind: 'awaiting_client_hello' };
52
+ }
53
+ scheduleFrame(frame) {
54
+ this.chain = this.chain.then(() => this.handleFrame(frame));
55
+ }
56
+ async handleFrame(frame) {
57
+ try {
58
+ switch (this.state.kind) {
59
+ case 'awaiting_client_hello':
60
+ await this.handleClientHello(frame);
61
+ return;
62
+ case 'awaiting_client_finish':
63
+ await this.handleClientFinish(frame, this.state.handshake, this.state.serverEphemeralKeyPair);
64
+ return;
65
+ case 'authenticated':
66
+ await this.handleAuthenticatedFrame(frame, this.state.transport);
67
+ return;
68
+ default:
69
+ this.events.onError?.(new Error(`unexpected frame in state "${this.state.kind}"`));
70
+ }
71
+ }
72
+ catch (error) {
73
+ this.fail(error);
74
+ }
75
+ }
76
+ async handleClientHello(frame) {
77
+ const parsed = proto.HandshakeMessage.decode(frame);
78
+ const clientHello = parsed.clientHello;
79
+ if (!clientHello?.ephemeral) {
80
+ throw new Error('ClientHello missing ephemeral');
81
+ }
82
+ // IK includes encrypted static+payload in ClientHello; XX does not.
83
+ if (clientHello.static && clientHello.payload) {
84
+ await this.handleIkClientHello(clientHello);
85
+ }
86
+ else {
87
+ await this.handleXxClientHello(clientHello.ephemeral);
88
+ }
89
+ }
90
+ async handleXxClientHello(clientEphemeralPub) {
91
+ const handshake = new WaFakeNoiseHandshake();
92
+ handshake.start(NOISE_XX_NAME, PROLOGUE);
93
+ handshake.authenticate(clientEphemeralPub);
94
+ const serverEphemeral = await X25519.generateKeyPair();
95
+ handshake.authenticate(serverEphemeral.pubKey);
96
+ handshake.mixIntoKey(await X25519.scalarMult(serverEphemeral.privKey, clientEphemeralPub));
97
+ const encryptedServerStatic = handshake.encrypt(this.config.serverStaticKeyPair.pubKey);
98
+ handshake.mixIntoKey(await X25519.scalarMult(this.config.serverStaticKeyPair.privKey, clientEphemeralPub));
99
+ const certChain = await buildFakeCertChain({
100
+ root: this.config.rootCa,
101
+ leafKey: this.config.serverStaticKeyPair.pubKey
102
+ });
103
+ const encryptedCertPayload = handshake.encrypt(certChain.encoded);
104
+ const serverHello = proto.HandshakeMessage.encode({
105
+ serverHello: {
106
+ ephemeral: serverEphemeral.pubKey,
107
+ static: encryptedServerStatic,
108
+ payload: encryptedCertPayload
109
+ }
110
+ }).finish();
111
+ this.frameSocket.sendFrame(serverHello);
112
+ this.state = {
113
+ kind: 'awaiting_client_finish',
114
+ handshake,
115
+ serverEphemeralKeyPair: serverEphemeral,
116
+ clientEphemeralPub
117
+ };
118
+ }
119
+ /** Server-side IK path used when the client already cached our static key. */
120
+ async handleIkClientHello(clientHello) {
121
+ if (!clientHello.ephemeral || !clientHello.static || !clientHello.payload) {
122
+ throw new Error('IK ClientHello missing ephemeral/static/payload');
123
+ }
124
+ const clientEphemeralPub = clientHello.ephemeral;
125
+ const encryptedClientStatic = clientHello.static;
126
+ const encryptedClientPayload = clientHello.payload;
127
+ const handshake = new WaFakeNoiseHandshake();
128
+ handshake.start(NOISE_IK_NAME, PROLOGUE);
129
+ handshake.authenticate(this.config.serverStaticKeyPair.pubKey);
130
+ handshake.authenticate(clientEphemeralPub);
131
+ handshake.mixIntoKey(await X25519.scalarMult(this.config.serverStaticKeyPair.privKey, clientEphemeralPub));
132
+ const clientStaticKey = handshake.decrypt(encryptedClientStatic);
133
+ handshake.mixIntoKey(await X25519.scalarMult(this.config.serverStaticKeyPair.privKey, clientStaticKey));
134
+ const clientPayloadBytes = handshake.decrypt(encryptedClientPayload);
135
+ const clientPayload = parseClientPayload(clientPayloadBytes);
136
+ const serverEphemeral = await X25519.generateKeyPair();
137
+ handshake.authenticate(serverEphemeral.pubKey);
138
+ const [dhEphem, dhStatic] = await Promise.all([
139
+ X25519.scalarMult(serverEphemeral.privKey, clientEphemeralPub),
140
+ X25519.scalarMult(serverEphemeral.privKey, clientStaticKey)
141
+ ]);
142
+ handshake.mixIntoKey(dhEphem);
143
+ handshake.mixIntoKey(dhStatic);
144
+ const certChain = await buildFakeCertChain({
145
+ root: this.config.rootCa,
146
+ leafKey: this.config.serverStaticKeyPair.pubKey
147
+ });
148
+ const encryptedCertPayload = handshake.encrypt(certChain.encoded);
149
+ const serverHello = proto.HandshakeMessage.encode({
150
+ serverHello: {
151
+ ephemeral: serverEphemeral.pubKey,
152
+ payload: encryptedCertPayload
153
+ }
154
+ }).finish();
155
+ this.frameSocket.sendFrame(serverHello);
156
+ const keys = handshake.finish();
157
+ const transport = new WaFakeTransport({
158
+ recvKey: keys.recvKey,
159
+ sendKey: keys.sendKey
160
+ });
161
+ this.state = { kind: 'authenticated', transport };
162
+ await this.sendStanza(buildSuccessNode(this.config.successNodeAttributes));
163
+ this.events.onAuthenticated?.({ clientPayload, clientStaticKey });
164
+ }
165
+ async handleClientFinish(frame, handshake, serverEphemeralKeyPair) {
166
+ const parsed = proto.HandshakeMessage.decode(frame);
167
+ const clientFinish = parsed.clientFinish;
168
+ if (!clientFinish?.static || !clientFinish.payload) {
169
+ throw new Error('ClientFinish missing static/payload');
170
+ }
171
+ const clientStaticKey = handshake.decrypt(clientFinish.static);
172
+ handshake.mixIntoKey(await X25519.scalarMult(serverEphemeralKeyPair.privKey, clientStaticKey));
173
+ const clientPayloadBytes = handshake.decrypt(clientFinish.payload);
174
+ const clientPayload = parseClientPayload(clientPayloadBytes);
175
+ const keys = handshake.finish();
176
+ const transport = new WaFakeTransport({
177
+ recvKey: keys.recvKey,
178
+ sendKey: keys.sendKey
179
+ });
180
+ this.state = { kind: 'authenticated', transport };
181
+ await this.sendStanza(buildSuccessNode(this.config.successNodeAttributes));
182
+ this.events.onAuthenticated?.({ clientPayload, clientStaticKey });
183
+ }
184
+ async handleAuthenticatedFrame(frame, transport) {
185
+ const stanzaBytes = await transport.decryptFrame(frame);
186
+ const node = await decodeBinaryNodeStanza(stanzaBytes);
187
+ this.events.onStanza?.(node);
188
+ if (node.tag === 'iq') {
189
+ const response = await this.config.iqRouter.route(node);
190
+ if (response !== null) {
191
+ await this.sendStanza(response);
192
+ }
193
+ else {
194
+ this.events.onUnhandledStanza?.(node);
195
+ }
196
+ return;
197
+ }
198
+ // Mirror the minimal ack shape that resolves the lib pending publish.
199
+ if (node.tag === 'message' && node.attrs.id) {
200
+ const messageType = node.attrs.type ?? 'text';
201
+ const from = node.attrs.to ?? 's.whatsapp.net';
202
+ const ackAttrs = {
203
+ id: node.attrs.id,
204
+ class: 'receipt',
205
+ type: messageType,
206
+ from
207
+ };
208
+ if (node.attrs.participant) {
209
+ ackAttrs.participant = node.attrs.participant;
210
+ }
211
+ await this.sendStanza({
212
+ tag: 'ack',
213
+ attrs: ackAttrs
214
+ });
215
+ }
216
+ }
217
+ fail(error) {
218
+ const err = error instanceof Error ? error : new Error(String(error));
219
+ this.events.onError?.(err);
220
+ this.state = { kind: 'closed' };
221
+ this.config.connection.close(1011, err.message.slice(0, 120));
222
+ }
223
+ }
@@ -0,0 +1,117 @@
1
+ /** WhatsApp frame codec (prologue + 3-byte length-prefixed frames). */
2
+ const PROLOGUE_W = 0x57;
3
+ const PROLOGUE_A = 0x41;
4
+ const ROUTING_MAGIC = 0xed;
5
+ const VERSION_HEADER_LENGTH = 4;
6
+ const FRAME_LENGTH_PREFIX_BYTES = 3;
7
+ const MAX_FRAME_LENGTH = 1 << 24;
8
+ export class WaFakeFrameSocket {
9
+ constructor(connection) {
10
+ this.handlers = {};
11
+ this.inboundBuffer = new Uint8Array(0);
12
+ this.prologueParsed = false;
13
+ this.connection = connection;
14
+ this.connection.setHandlers({
15
+ onFrame: (bytes) => this.handleInboundBytes(bytes),
16
+ onClose: (info) => this.handlers.onClose?.(info),
17
+ onError: (error) => this.handlers.onError?.(error)
18
+ });
19
+ }
20
+ setHandlers(handlers) {
21
+ this.handlers = handlers;
22
+ }
23
+ sendFrame(body) {
24
+ if (body.byteLength >= MAX_FRAME_LENGTH) {
25
+ throw new Error(`frame too large: ${body.byteLength} bytes`);
26
+ }
27
+ const out = new Uint8Array(FRAME_LENGTH_PREFIX_BYTES + body.byteLength);
28
+ out[0] = (body.byteLength >> 16) & 0xff;
29
+ out[1] = (body.byteLength >> 8) & 0xff;
30
+ out[2] = body.byteLength & 0xff;
31
+ out.set(body, FRAME_LENGTH_PREFIX_BYTES);
32
+ this.connection.sendFrame(out);
33
+ }
34
+ handleInboundBytes(chunk) {
35
+ try {
36
+ this.inboundBuffer = concatBytes(this.inboundBuffer, chunk);
37
+ if (!this.prologueParsed) {
38
+ if (!this.tryParsePrologue()) {
39
+ return;
40
+ }
41
+ }
42
+ this.drainFrames();
43
+ }
44
+ catch (error) {
45
+ this.handlers.onError?.(error instanceof Error ? error : new Error(String(error)));
46
+ }
47
+ }
48
+ tryParsePrologue() {
49
+ const buffer = this.inboundBuffer;
50
+ let offset = 0;
51
+ let routingInfo = null;
52
+ if (buffer.byteLength === 0) {
53
+ return false;
54
+ }
55
+ if (buffer[0] === ROUTING_MAGIC) {
56
+ if (buffer.byteLength < 4) {
57
+ return false;
58
+ }
59
+ const routingLen = (buffer[1] << 16) | (buffer[2] << 8) | buffer[3];
60
+ if (buffer.byteLength < 4 + routingLen + VERSION_HEADER_LENGTH) {
61
+ return false;
62
+ }
63
+ routingInfo = buffer.slice(4, 4 + routingLen);
64
+ offset = 4 + routingLen;
65
+ }
66
+ else if (buffer.byteLength < VERSION_HEADER_LENGTH) {
67
+ return false;
68
+ }
69
+ const versionStart = offset;
70
+ const w = buffer[versionStart];
71
+ const a = buffer[versionStart + 1];
72
+ if (w !== PROLOGUE_W || a !== PROLOGUE_A) {
73
+ throw new Error(`invalid prologue magic: expected "WA", got ${hex(w)} ${hex(a)}`);
74
+ }
75
+ const protocolVersion = buffer[versionStart + 2];
76
+ const dictVersion = buffer[versionStart + 3];
77
+ offset = versionStart + VERSION_HEADER_LENGTH;
78
+ this.inboundBuffer = buffer.slice(offset);
79
+ this.prologueParsed = true;
80
+ this.handlers.onPrologue?.({ protocolVersion, dictVersion, routingInfo });
81
+ return true;
82
+ }
83
+ drainFrames() {
84
+ while (true) {
85
+ const buffer = this.inboundBuffer;
86
+ if (buffer.byteLength < FRAME_LENGTH_PREFIX_BYTES) {
87
+ return;
88
+ }
89
+ const length = (buffer[0] << 16) | (buffer[1] << 8) | buffer[2];
90
+ if (length >= MAX_FRAME_LENGTH) {
91
+ throw new Error(`frame too large: ${length} bytes`);
92
+ }
93
+ const total = FRAME_LENGTH_PREFIX_BYTES + length;
94
+ if (buffer.byteLength < total) {
95
+ return;
96
+ }
97
+ const body = buffer.slice(FRAME_LENGTH_PREFIX_BYTES, total);
98
+ this.inboundBuffer = buffer.slice(total);
99
+ this.handlers.onFrame?.(body);
100
+ }
101
+ }
102
+ }
103
+ function concatBytes(a, b) {
104
+ if (a.byteLength === 0)
105
+ return b;
106
+ if (b.byteLength === 0)
107
+ return a;
108
+ const out = new Uint8Array(a.byteLength + b.byteLength);
109
+ out.set(a, 0);
110
+ out.set(b, a.byteLength);
111
+ return out;
112
+ }
113
+ function hex(byte) {
114
+ if (byte === undefined)
115
+ return 'undefined';
116
+ return `0x${byte.toString(16).padStart(2, '0')}`;
117
+ }