@copilotkit/aimock 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (368) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.claude-plugin/plugin.json +12 -0
  3. package/LICENSE +21 -0
  4. package/README.md +82 -0
  5. package/dist/_virtual/_rolldown/runtime.cjs +29 -0
  6. package/dist/a2a-handler.cjs +203 -0
  7. package/dist/a2a-handler.cjs.map +1 -0
  8. package/dist/a2a-handler.js +199 -0
  9. package/dist/a2a-handler.js.map +1 -0
  10. package/dist/a2a-mock.cjs +292 -0
  11. package/dist/a2a-mock.cjs.map +1 -0
  12. package/dist/a2a-mock.d.cts +41 -0
  13. package/dist/a2a-mock.d.cts.map +1 -0
  14. package/dist/a2a-mock.d.ts +41 -0
  15. package/dist/a2a-mock.d.ts.map +1 -0
  16. package/dist/a2a-mock.js +290 -0
  17. package/dist/a2a-mock.js.map +1 -0
  18. package/dist/a2a-stub.cjs +4 -0
  19. package/dist/a2a-stub.d.cts +3 -0
  20. package/dist/a2a-stub.d.ts +3 -0
  21. package/dist/a2a-stub.js +3 -0
  22. package/dist/a2a-types.d.cts +68 -0
  23. package/dist/a2a-types.d.cts.map +1 -0
  24. package/dist/a2a-types.d.ts +68 -0
  25. package/dist/a2a-types.d.ts.map +1 -0
  26. package/dist/aimock-cli.cjs +112 -0
  27. package/dist/aimock-cli.cjs.map +1 -0
  28. package/dist/aimock-cli.d.cts +19 -0
  29. package/dist/aimock-cli.d.cts.map +1 -0
  30. package/dist/aimock-cli.d.ts +19 -0
  31. package/dist/aimock-cli.d.ts.map +1 -0
  32. package/dist/aimock-cli.js +110 -0
  33. package/dist/aimock-cli.js.map +1 -0
  34. package/dist/aws-event-stream.cjs +117 -0
  35. package/dist/aws-event-stream.cjs.map +1 -0
  36. package/dist/aws-event-stream.d.cts +38 -0
  37. package/dist/aws-event-stream.d.cts.map +1 -0
  38. package/dist/aws-event-stream.d.ts +38 -0
  39. package/dist/aws-event-stream.d.ts.map +1 -0
  40. package/dist/aws-event-stream.js +114 -0
  41. package/dist/aws-event-stream.js.map +1 -0
  42. package/dist/bedrock-converse.cjs +445 -0
  43. package/dist/bedrock-converse.cjs.map +1 -0
  44. package/dist/bedrock-converse.d.cts +50 -0
  45. package/dist/bedrock-converse.d.cts.map +1 -0
  46. package/dist/bedrock-converse.d.ts +50 -0
  47. package/dist/bedrock-converse.d.ts.map +1 -0
  48. package/dist/bedrock-converse.js +443 -0
  49. package/dist/bedrock-converse.js.map +1 -0
  50. package/dist/bedrock.cjs +557 -0
  51. package/dist/bedrock.cjs.map +1 -0
  52. package/dist/bedrock.d.cts +41 -0
  53. package/dist/bedrock.d.cts.map +1 -0
  54. package/dist/bedrock.d.ts +41 -0
  55. package/dist/bedrock.d.ts.map +1 -0
  56. package/dist/bedrock.js +553 -0
  57. package/dist/bedrock.js.map +1 -0
  58. package/dist/chaos.cjs +114 -0
  59. package/dist/chaos.cjs.map +1 -0
  60. package/dist/chaos.d.cts +27 -0
  61. package/dist/chaos.d.cts.map +1 -0
  62. package/dist/chaos.d.ts +27 -0
  63. package/dist/chaos.d.ts.map +1 -0
  64. package/dist/chaos.js +113 -0
  65. package/dist/chaos.js.map +1 -0
  66. package/dist/cli.cjs +268 -0
  67. package/dist/cli.cjs.map +1 -0
  68. package/dist/cli.d.cts +1 -0
  69. package/dist/cli.d.ts +1 -0
  70. package/dist/cli.js +268 -0
  71. package/dist/cli.js.map +1 -0
  72. package/dist/cohere.cjs +434 -0
  73. package/dist/cohere.cjs.map +1 -0
  74. package/dist/cohere.d.cts +34 -0
  75. package/dist/cohere.d.cts.map +1 -0
  76. package/dist/cohere.d.ts +34 -0
  77. package/dist/cohere.d.ts.map +1 -0
  78. package/dist/cohere.js +433 -0
  79. package/dist/cohere.js.map +1 -0
  80. package/dist/config-loader.cjs +111 -0
  81. package/dist/config-loader.cjs.map +1 -0
  82. package/dist/config-loader.d.cts +100 -0
  83. package/dist/config-loader.d.cts.map +1 -0
  84. package/dist/config-loader.d.ts +100 -0
  85. package/dist/config-loader.d.ts.map +1 -0
  86. package/dist/config-loader.js +107 -0
  87. package/dist/config-loader.js.map +1 -0
  88. package/dist/embeddings.cjs +150 -0
  89. package/dist/embeddings.cjs.map +1 -0
  90. package/dist/embeddings.d.cts +12 -0
  91. package/dist/embeddings.d.cts.map +1 -0
  92. package/dist/embeddings.d.ts +12 -0
  93. package/dist/embeddings.d.ts.map +1 -0
  94. package/dist/embeddings.js +150 -0
  95. package/dist/embeddings.js.map +1 -0
  96. package/dist/fixture-loader.cjs +269 -0
  97. package/dist/fixture-loader.cjs.map +1 -0
  98. package/dist/fixture-loader.d.cts +17 -0
  99. package/dist/fixture-loader.d.cts.map +1 -0
  100. package/dist/fixture-loader.d.ts +17 -0
  101. package/dist/fixture-loader.d.ts.map +1 -0
  102. package/dist/fixture-loader.js +265 -0
  103. package/dist/fixture-loader.js.map +1 -0
  104. package/dist/gemini.cjs +403 -0
  105. package/dist/gemini.cjs.map +1 -0
  106. package/dist/gemini.d.cts +10 -0
  107. package/dist/gemini.d.cts.map +1 -0
  108. package/dist/gemini.d.ts +10 -0
  109. package/dist/gemini.d.ts.map +1 -0
  110. package/dist/gemini.js +403 -0
  111. package/dist/gemini.js.map +1 -0
  112. package/dist/helpers.cjs +276 -0
  113. package/dist/helpers.cjs.map +1 -0
  114. package/dist/helpers.d.cts +39 -0
  115. package/dist/helpers.d.cts.map +1 -0
  116. package/dist/helpers.d.ts +39 -0
  117. package/dist/helpers.d.ts.map +1 -0
  118. package/dist/helpers.js +259 -0
  119. package/dist/helpers.js.map +1 -0
  120. package/dist/index.cjs +113 -0
  121. package/dist/index.d.cts +42 -0
  122. package/dist/index.d.ts +42 -0
  123. package/dist/index.js +39 -0
  124. package/dist/interruption.cjs +40 -0
  125. package/dist/interruption.cjs.map +1 -0
  126. package/dist/interruption.d.cts +15 -0
  127. package/dist/interruption.d.cts.map +1 -0
  128. package/dist/interruption.d.ts +15 -0
  129. package/dist/interruption.d.ts.map +1 -0
  130. package/dist/interruption.js +39 -0
  131. package/dist/interruption.js.map +1 -0
  132. package/dist/journal.cjs +65 -0
  133. package/dist/journal.cjs.map +1 -0
  134. package/dist/journal.d.cts +23 -0
  135. package/dist/journal.d.cts.map +1 -0
  136. package/dist/journal.d.ts +23 -0
  137. package/dist/journal.d.ts.map +1 -0
  138. package/dist/journal.js +65 -0
  139. package/dist/journal.js.map +1 -0
  140. package/dist/jsonrpc.cjs +91 -0
  141. package/dist/jsonrpc.cjs.map +1 -0
  142. package/dist/jsonrpc.d.cts +24 -0
  143. package/dist/jsonrpc.d.cts.map +1 -0
  144. package/dist/jsonrpc.d.ts +24 -0
  145. package/dist/jsonrpc.d.ts.map +1 -0
  146. package/dist/jsonrpc.js +90 -0
  147. package/dist/jsonrpc.js.map +1 -0
  148. package/dist/llmock.cjs +223 -0
  149. package/dist/llmock.cjs.map +1 -0
  150. package/dist/llmock.d.cts +70 -0
  151. package/dist/llmock.d.cts.map +1 -0
  152. package/dist/llmock.d.ts +70 -0
  153. package/dist/llmock.d.ts.map +1 -0
  154. package/dist/llmock.js +223 -0
  155. package/dist/llmock.js.map +1 -0
  156. package/dist/logger.cjs +29 -0
  157. package/dist/logger.cjs.map +1 -0
  158. package/dist/logger.d.cts +14 -0
  159. package/dist/logger.d.cts.map +1 -0
  160. package/dist/logger.d.ts +14 -0
  161. package/dist/logger.d.ts.map +1 -0
  162. package/dist/logger.js +28 -0
  163. package/dist/logger.js.map +1 -0
  164. package/dist/mcp-handler.cjs +189 -0
  165. package/dist/mcp-handler.cjs.map +1 -0
  166. package/dist/mcp-handler.js +188 -0
  167. package/dist/mcp-handler.js.map +1 -0
  168. package/dist/mcp-mock.cjs +169 -0
  169. package/dist/mcp-mock.cjs.map +1 -0
  170. package/dist/mcp-mock.d.cts +40 -0
  171. package/dist/mcp-mock.d.cts.map +1 -0
  172. package/dist/mcp-mock.d.ts +40 -0
  173. package/dist/mcp-mock.d.ts.map +1 -0
  174. package/dist/mcp-mock.js +167 -0
  175. package/dist/mcp-mock.js.map +1 -0
  176. package/dist/mcp-stub.cjs +4 -0
  177. package/dist/mcp-stub.d.cts +3 -0
  178. package/dist/mcp-stub.d.ts +3 -0
  179. package/dist/mcp-stub.js +3 -0
  180. package/dist/mcp-types.d.cts +65 -0
  181. package/dist/mcp-types.d.cts.map +1 -0
  182. package/dist/mcp-types.d.ts +65 -0
  183. package/dist/mcp-types.d.ts.map +1 -0
  184. package/dist/messages.cjs +489 -0
  185. package/dist/messages.cjs.map +1 -0
  186. package/dist/messages.d.cts +10 -0
  187. package/dist/messages.d.cts.map +1 -0
  188. package/dist/messages.d.ts +10 -0
  189. package/dist/messages.d.ts.map +1 -0
  190. package/dist/messages.js +489 -0
  191. package/dist/messages.js.map +1 -0
  192. package/dist/metrics.cjs +160 -0
  193. package/dist/metrics.cjs.map +1 -0
  194. package/dist/metrics.d.cts +24 -0
  195. package/dist/metrics.d.cts.map +1 -0
  196. package/dist/metrics.d.ts +24 -0
  197. package/dist/metrics.d.ts.map +1 -0
  198. package/dist/metrics.js +158 -0
  199. package/dist/metrics.js.map +1 -0
  200. package/dist/moderation.cjs +91 -0
  201. package/dist/moderation.cjs.map +1 -0
  202. package/dist/moderation.d.cts +23 -0
  203. package/dist/moderation.d.cts.map +1 -0
  204. package/dist/moderation.d.ts +23 -0
  205. package/dist/moderation.d.ts.map +1 -0
  206. package/dist/moderation.js +91 -0
  207. package/dist/moderation.js.map +1 -0
  208. package/dist/ndjson-writer.cjs +31 -0
  209. package/dist/ndjson-writer.cjs.map +1 -0
  210. package/dist/ndjson-writer.d.cts +17 -0
  211. package/dist/ndjson-writer.d.cts.map +1 -0
  212. package/dist/ndjson-writer.d.ts +17 -0
  213. package/dist/ndjson-writer.d.ts.map +1 -0
  214. package/dist/ndjson-writer.js +31 -0
  215. package/dist/ndjson-writer.js.map +1 -0
  216. package/dist/ollama.cjs +519 -0
  217. package/dist/ollama.cjs.map +1 -0
  218. package/dist/ollama.d.cts +34 -0
  219. package/dist/ollama.d.cts.map +1 -0
  220. package/dist/ollama.d.ts +34 -0
  221. package/dist/ollama.d.ts.map +1 -0
  222. package/dist/ollama.js +517 -0
  223. package/dist/ollama.js.map +1 -0
  224. package/dist/recorder.cjs +311 -0
  225. package/dist/recorder.cjs.map +1 -0
  226. package/dist/recorder.d.cts +23 -0
  227. package/dist/recorder.d.cts.map +1 -0
  228. package/dist/recorder.d.ts +23 -0
  229. package/dist/recorder.d.ts.map +1 -0
  230. package/dist/recorder.js +305 -0
  231. package/dist/recorder.js.map +1 -0
  232. package/dist/rerank.cjs +71 -0
  233. package/dist/rerank.cjs.map +1 -0
  234. package/dist/rerank.d.cts +22 -0
  235. package/dist/rerank.d.cts.map +1 -0
  236. package/dist/rerank.d.ts +22 -0
  237. package/dist/rerank.d.ts.map +1 -0
  238. package/dist/rerank.js +71 -0
  239. package/dist/rerank.js.map +1 -0
  240. package/dist/responses.cjs +637 -0
  241. package/dist/responses.cjs.map +1 -0
  242. package/dist/responses.d.cts +16 -0
  243. package/dist/responses.d.cts.map +1 -0
  244. package/dist/responses.d.ts +16 -0
  245. package/dist/responses.d.ts.map +1 -0
  246. package/dist/responses.js +634 -0
  247. package/dist/responses.js.map +1 -0
  248. package/dist/router.cjs +68 -0
  249. package/dist/router.cjs.map +1 -0
  250. package/dist/router.d.cts +16 -0
  251. package/dist/router.d.cts.map +1 -0
  252. package/dist/router.d.ts +16 -0
  253. package/dist/router.d.ts.map +1 -0
  254. package/dist/router.js +65 -0
  255. package/dist/router.js.map +1 -0
  256. package/dist/search.cjs +59 -0
  257. package/dist/search.cjs.map +1 -0
  258. package/dist/search.d.cts +23 -0
  259. package/dist/search.d.cts.map +1 -0
  260. package/dist/search.d.ts +23 -0
  261. package/dist/search.d.ts.map +1 -0
  262. package/dist/search.js +59 -0
  263. package/dist/search.js.map +1 -0
  264. package/dist/server.cjs +935 -0
  265. package/dist/server.cjs.map +1 -0
  266. package/dist/server.d.cts +28 -0
  267. package/dist/server.d.cts.map +1 -0
  268. package/dist/server.d.ts +28 -0
  269. package/dist/server.d.ts.map +1 -0
  270. package/dist/server.js +933 -0
  271. package/dist/server.js.map +1 -0
  272. package/dist/sse-writer.cjs +59 -0
  273. package/dist/sse-writer.cjs.map +1 -0
  274. package/dist/sse-writer.d.cts +19 -0
  275. package/dist/sse-writer.d.cts.map +1 -0
  276. package/dist/sse-writer.d.ts +19 -0
  277. package/dist/sse-writer.d.ts.map +1 -0
  278. package/dist/sse-writer.js +55 -0
  279. package/dist/sse-writer.js.map +1 -0
  280. package/dist/stream-collapse.cjs +496 -0
  281. package/dist/stream-collapse.cjs.map +1 -0
  282. package/dist/stream-collapse.d.cts +70 -0
  283. package/dist/stream-collapse.d.cts.map +1 -0
  284. package/dist/stream-collapse.d.ts +70 -0
  285. package/dist/stream-collapse.d.ts.map +1 -0
  286. package/dist/stream-collapse.js +489 -0
  287. package/dist/stream-collapse.js.map +1 -0
  288. package/dist/suite.cjs +46 -0
  289. package/dist/suite.cjs.map +1 -0
  290. package/dist/suite.d.cts +31 -0
  291. package/dist/suite.d.cts.map +1 -0
  292. package/dist/suite.d.ts +31 -0
  293. package/dist/suite.d.ts.map +1 -0
  294. package/dist/suite.js +46 -0
  295. package/dist/suite.js.map +1 -0
  296. package/dist/types.d.cts +243 -0
  297. package/dist/types.d.cts.map +1 -0
  298. package/dist/types.d.ts +243 -0
  299. package/dist/types.d.ts.map +1 -0
  300. package/dist/url.cjs +21 -0
  301. package/dist/url.cjs.map +1 -0
  302. package/dist/url.d.cts +16 -0
  303. package/dist/url.d.cts.map +1 -0
  304. package/dist/url.d.ts +16 -0
  305. package/dist/url.d.ts.map +1 -0
  306. package/dist/url.js +20 -0
  307. package/dist/url.js.map +1 -0
  308. package/dist/vector-handler.cjs +239 -0
  309. package/dist/vector-handler.cjs.map +1 -0
  310. package/dist/vector-handler.js +238 -0
  311. package/dist/vector-handler.js.map +1 -0
  312. package/dist/vector-mock.cjs +229 -0
  313. package/dist/vector-mock.cjs.map +1 -0
  314. package/dist/vector-mock.d.cts +39 -0
  315. package/dist/vector-mock.d.cts.map +1 -0
  316. package/dist/vector-mock.d.ts +39 -0
  317. package/dist/vector-mock.d.ts.map +1 -0
  318. package/dist/vector-mock.js +227 -0
  319. package/dist/vector-mock.js.map +1 -0
  320. package/dist/vector-stub.cjs +4 -0
  321. package/dist/vector-stub.d.cts +3 -0
  322. package/dist/vector-stub.d.ts +3 -0
  323. package/dist/vector-stub.js +3 -0
  324. package/dist/vector-types.d.cts +32 -0
  325. package/dist/vector-types.d.cts.map +1 -0
  326. package/dist/vector-types.d.ts +32 -0
  327. package/dist/vector-types.d.ts.map +1 -0
  328. package/dist/watcher.cjs +59 -0
  329. package/dist/watcher.cjs.map +1 -0
  330. package/dist/watcher.js +58 -0
  331. package/dist/watcher.js.map +1 -0
  332. package/dist/ws-framing.cjs +187 -0
  333. package/dist/ws-framing.cjs.map +1 -0
  334. package/dist/ws-framing.d.cts +26 -0
  335. package/dist/ws-framing.d.cts.map +1 -0
  336. package/dist/ws-framing.d.ts +26 -0
  337. package/dist/ws-framing.d.ts.map +1 -0
  338. package/dist/ws-framing.js +184 -0
  339. package/dist/ws-framing.js.map +1 -0
  340. package/dist/ws-gemini-live.cjs +364 -0
  341. package/dist/ws-gemini-live.cjs.map +1 -0
  342. package/dist/ws-gemini-live.d.cts +18 -0
  343. package/dist/ws-gemini-live.d.cts.map +1 -0
  344. package/dist/ws-gemini-live.d.ts +18 -0
  345. package/dist/ws-gemini-live.d.ts.map +1 -0
  346. package/dist/ws-gemini-live.js +364 -0
  347. package/dist/ws-gemini-live.js.map +1 -0
  348. package/dist/ws-realtime.cjs +435 -0
  349. package/dist/ws-realtime.cjs.map +1 -0
  350. package/dist/ws-realtime.d.cts +17 -0
  351. package/dist/ws-realtime.d.cts.map +1 -0
  352. package/dist/ws-realtime.d.ts +17 -0
  353. package/dist/ws-realtime.d.ts.map +1 -0
  354. package/dist/ws-realtime.js +435 -0
  355. package/dist/ws-realtime.js.map +1 -0
  356. package/dist/ws-responses.cjs +164 -0
  357. package/dist/ws-responses.cjs.map +1 -0
  358. package/dist/ws-responses.d.cts +18 -0
  359. package/dist/ws-responses.d.cts.map +1 -0
  360. package/dist/ws-responses.d.ts +18 -0
  361. package/dist/ws-responses.d.ts.map +1 -0
  362. package/dist/ws-responses.js +164 -0
  363. package/dist/ws-responses.js.map +1 -0
  364. package/fixtures/example-greeting.json +12 -0
  365. package/fixtures/example-multi-turn.json +14 -0
  366. package/fixtures/example-tool-call.json +15 -0
  367. package/package.json +118 -0
  368. package/skills/write-fixtures/SKILL.md +625 -0
@@ -0,0 +1,32 @@
1
+ //#region src/vector-types.d.ts
2
+ interface VectorMockOptions {
3
+ port?: number;
4
+ host?: string;
5
+ }
6
+ interface VectorCollection {
7
+ name: string;
8
+ dimension: number;
9
+ vectors: Map<string, VectorEntry>;
10
+ }
11
+ interface VectorEntry {
12
+ id: string;
13
+ values: number[];
14
+ metadata?: Record<string, unknown>;
15
+ }
16
+ interface QueryResult {
17
+ id: string;
18
+ score: number;
19
+ metadata?: Record<string, unknown>;
20
+ values?: number[];
21
+ }
22
+ interface VectorQuery {
23
+ vector?: number[];
24
+ topK?: number;
25
+ filter?: unknown;
26
+ collection: string;
27
+ }
28
+ type QueryHandler = QueryResult[] | ((query: VectorQuery) => QueryResult[]);
29
+ //# sourceMappingURL=vector-types.d.ts.map
30
+ //#endregion
31
+ export { QueryHandler, QueryResult, VectorCollection, VectorEntry, VectorMockOptions, VectorQuery };
32
+ //# sourceMappingURL=vector-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vector-types.d.ts","names":[],"sources":["../src/vector-types.ts"],"sourcesContent":[],"mappings":";UAAiB,iBAAA;EAAA,IAAA,CAAA,EAAA,MAAA;EAKA,IAAA,CAAA,EAAA,MAAA;;AAGM,UAHN,gBAAA,CAGM;MAAZ,EAAA,MAAA;EAAG,SAAA,EAAA,MAAA;EAGG,OAAA,EAHN,GAGM,CAAA,MAAW,EAHL,WAMJ,CAAA;AAGnB;AAOiB,UAbA,WAAA,CAaW;EAOhB,EAAA,EAAA,MAAA;EAAY,MAAA,EAAA,MAAA,EAAA;UAAG,CAAA,EAjBd,MAiBc,CAAA,MAAA,EAAA,OAAA,CAAA;;AAAyC,UAdnD,WAAA,CAcmD;EAAW,EAAA,EAAA,MAAA;;aAXlE;;;UAII,WAAA;;;;;;KAOL,YAAA,GAAe,yBAAyB,gBAAgB"}
@@ -0,0 +1,59 @@
1
+ const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
+ let node_fs = require("node:fs");
3
+
4
+ //#region src/watcher.ts
5
+ const DEBOUNCE_MS = 500;
6
+ function watchFixtures(fixturePath, fixtures, loadFn, opts) {
7
+ const { logger, validate, validateFn } = opts;
8
+ let debounceTimer = null;
9
+ function reload() {
10
+ logger.info(`File changed — reloading fixtures from ${fixturePath}...`);
11
+ let newFixtures;
12
+ try {
13
+ newFixtures = loadFn();
14
+ } catch (err) {
15
+ logger.error("Failed to reload fixtures:", err);
16
+ logger.error("Previous fixtures remain active. Fix the error and save again to retry.");
17
+ return;
18
+ }
19
+ if (newFixtures.length === 0 && fixtures.length > 0) {
20
+ logger.warn("Reload produced 0 fixtures — keeping previous fixtures. Check fixture file for errors.");
21
+ return;
22
+ }
23
+ if (validate && validateFn) {
24
+ const results = validateFn(newFixtures);
25
+ const errors = results.filter((r) => r.severity === "error");
26
+ const warnings = results.filter((r) => r.severity === "warning");
27
+ for (const w of warnings) logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);
28
+ if (errors.length > 0) {
29
+ for (const e of errors) logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);
30
+ logger.error(`${errors.length} validation error(s) — keeping previous fixtures`);
31
+ return;
32
+ }
33
+ }
34
+ fixtures.length = 0;
35
+ fixtures.push(...newFixtures);
36
+ logger.info(`Reloaded ${newFixtures.length} fixture(s)`);
37
+ }
38
+ const watcher = (0, node_fs.watch)(fixturePath, { recursive: true }, () => {
39
+ if (debounceTimer) clearTimeout(debounceTimer);
40
+ debounceTimer = setTimeout(reload, DEBOUNCE_MS);
41
+ });
42
+ watcher.on("error", (err) => {
43
+ if (debounceTimer) clearTimeout(debounceTimer);
44
+ debounceTimer = null;
45
+ try {
46
+ watcher.close();
47
+ } catch {}
48
+ logger.error(`File watcher error on ${fixturePath}: ${err.message}`);
49
+ logger.error("Fixture auto-reload is no longer active. Restart the server to resume watching.");
50
+ });
51
+ return { close() {
52
+ if (debounceTimer) clearTimeout(debounceTimer);
53
+ watcher.close();
54
+ } };
55
+ }
56
+
57
+ //#endregion
58
+ exports.watchFixtures = watchFixtures;
59
+ //# sourceMappingURL=watcher.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher.cjs","names":[],"sources":["../src/watcher.ts"],"sourcesContent":["import { watch, type FSWatcher } from \"node:fs\";\nimport type { Fixture } from \"./types.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { ValidationResult } from \"./fixture-loader.js\";\n\nconst DEBOUNCE_MS = 500;\n\nexport function watchFixtures(\n fixturePath: string,\n fixtures: Fixture[],\n loadFn: () => Fixture[],\n opts: {\n logger: Logger;\n validate?: boolean;\n validateFn?: (fixtures: Fixture[]) => ValidationResult[];\n },\n): { close: () => void } {\n const { logger, validate, validateFn } = opts;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n function reload() {\n logger.info(`File changed — reloading fixtures from ${fixturePath}...`);\n\n let newFixtures: Fixture[];\n try {\n newFixtures = loadFn();\n } catch (err) {\n logger.error(\"Failed to reload fixtures:\", err);\n logger.error(\"Previous fixtures remain active. Fix the error and save again to retry.\");\n return;\n }\n\n if (newFixtures.length === 0 && fixtures.length > 0) {\n logger.warn(\n \"Reload produced 0 fixtures — keeping previous fixtures. Check fixture file for errors.\",\n );\n return;\n }\n\n if (validate && validateFn) {\n const results = validateFn(newFixtures);\n const errors = results.filter((r) => r.severity === \"error\");\n const warnings = results.filter((r) => r.severity === \"warning\");\n\n for (const w of warnings) {\n logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);\n }\n\n if (errors.length > 0) {\n for (const e of errors) {\n logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);\n }\n logger.error(`${errors.length} validation error(s) — keeping previous fixtures`);\n return;\n }\n }\n\n // Replace in-place to preserve array reference identity\n fixtures.length = 0;\n fixtures.push(...newFixtures);\n logger.info(`Reloaded ${newFixtures.length} fixture(s)`);\n }\n\n const watcher: FSWatcher = watch(fixturePath, { recursive: true }, () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(reload, DEBOUNCE_MS);\n });\n\n watcher.on(\"error\", (err: Error) => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = null;\n try {\n watcher.close();\n } catch {\n /* already dead */\n }\n logger.error(`File watcher error on ${fixturePath}: ${err.message}`);\n logger.error(\"Fixture auto-reload is no longer active. Restart the server to resume watching.\");\n });\n\n return {\n close() {\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n },\n };\n}\n"],"mappings":";;;;AAKA,MAAM,cAAc;AAEpB,SAAgB,cACd,aACA,UACA,QACA,MAKuB;CACvB,MAAM,EAAE,QAAQ,UAAU,eAAe;CACzC,IAAI,gBAAsD;CAE1D,SAAS,SAAS;AAChB,SAAO,KAAK,0CAA0C,YAAY,KAAK;EAEvE,IAAI;AACJ,MAAI;AACF,iBAAc,QAAQ;WACf,KAAK;AACZ,UAAO,MAAM,8BAA8B,IAAI;AAC/C,UAAO,MAAM,0EAA0E;AACvF;;AAGF,MAAI,YAAY,WAAW,KAAK,SAAS,SAAS,GAAG;AACnD,UAAO,KACL,yFACD;AACD;;AAGF,MAAI,YAAY,YAAY;GAC1B,MAAM,UAAU,WAAW,YAAY;GACvC,MAAM,SAAS,QAAQ,QAAQ,MAAM,EAAE,aAAa,QAAQ;GAC5D,MAAM,WAAW,QAAQ,QAAQ,MAAM,EAAE,aAAa,UAAU;AAEhE,QAAK,MAAM,KAAK,SACd,QAAO,KAAK,WAAW,EAAE,aAAa,IAAI,EAAE,UAAU;AAGxD,OAAI,OAAO,SAAS,GAAG;AACrB,SAAK,MAAM,KAAK,OACd,QAAO,MAAM,WAAW,EAAE,aAAa,IAAI,EAAE,UAAU;AAEzD,WAAO,MAAM,GAAG,OAAO,OAAO,kDAAkD;AAChF;;;AAKJ,WAAS,SAAS;AAClB,WAAS,KAAK,GAAG,YAAY;AAC7B,SAAO,KAAK,YAAY,YAAY,OAAO,aAAa;;CAG1D,MAAM,6BAA2B,aAAa,EAAE,WAAW,MAAM,QAAQ;AACvE,MAAI,cAAe,cAAa,cAAc;AAC9C,kBAAgB,WAAW,QAAQ,YAAY;GAC/C;AAEF,SAAQ,GAAG,UAAU,QAAe;AAClC,MAAI,cAAe,cAAa,cAAc;AAC9C,kBAAgB;AAChB,MAAI;AACF,WAAQ,OAAO;UACT;AAGR,SAAO,MAAM,yBAAyB,YAAY,IAAI,IAAI,UAAU;AACpE,SAAO,MAAM,kFAAkF;GAC/F;AAEF,QAAO,EACL,QAAQ;AACN,MAAI,cAAe,cAAa,cAAc;AAC9C,UAAQ,OAAO;IAElB"}
@@ -0,0 +1,58 @@
1
+ import { watch } from "node:fs";
2
+
3
+ //#region src/watcher.ts
4
+ const DEBOUNCE_MS = 500;
5
+ function watchFixtures(fixturePath, fixtures, loadFn, opts) {
6
+ const { logger, validate, validateFn } = opts;
7
+ let debounceTimer = null;
8
+ function reload() {
9
+ logger.info(`File changed — reloading fixtures from ${fixturePath}...`);
10
+ let newFixtures;
11
+ try {
12
+ newFixtures = loadFn();
13
+ } catch (err) {
14
+ logger.error("Failed to reload fixtures:", err);
15
+ logger.error("Previous fixtures remain active. Fix the error and save again to retry.");
16
+ return;
17
+ }
18
+ if (newFixtures.length === 0 && fixtures.length > 0) {
19
+ logger.warn("Reload produced 0 fixtures — keeping previous fixtures. Check fixture file for errors.");
20
+ return;
21
+ }
22
+ if (validate && validateFn) {
23
+ const results = validateFn(newFixtures);
24
+ const errors = results.filter((r) => r.severity === "error");
25
+ const warnings = results.filter((r) => r.severity === "warning");
26
+ for (const w of warnings) logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);
27
+ if (errors.length > 0) {
28
+ for (const e of errors) logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);
29
+ logger.error(`${errors.length} validation error(s) — keeping previous fixtures`);
30
+ return;
31
+ }
32
+ }
33
+ fixtures.length = 0;
34
+ fixtures.push(...newFixtures);
35
+ logger.info(`Reloaded ${newFixtures.length} fixture(s)`);
36
+ }
37
+ const watcher = watch(fixturePath, { recursive: true }, () => {
38
+ if (debounceTimer) clearTimeout(debounceTimer);
39
+ debounceTimer = setTimeout(reload, DEBOUNCE_MS);
40
+ });
41
+ watcher.on("error", (err) => {
42
+ if (debounceTimer) clearTimeout(debounceTimer);
43
+ debounceTimer = null;
44
+ try {
45
+ watcher.close();
46
+ } catch {}
47
+ logger.error(`File watcher error on ${fixturePath}: ${err.message}`);
48
+ logger.error("Fixture auto-reload is no longer active. Restart the server to resume watching.");
49
+ });
50
+ return { close() {
51
+ if (debounceTimer) clearTimeout(debounceTimer);
52
+ watcher.close();
53
+ } };
54
+ }
55
+
56
+ //#endregion
57
+ export { watchFixtures };
58
+ //# sourceMappingURL=watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher.js","names":[],"sources":["../src/watcher.ts"],"sourcesContent":["import { watch, type FSWatcher } from \"node:fs\";\nimport type { Fixture } from \"./types.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { ValidationResult } from \"./fixture-loader.js\";\n\nconst DEBOUNCE_MS = 500;\n\nexport function watchFixtures(\n fixturePath: string,\n fixtures: Fixture[],\n loadFn: () => Fixture[],\n opts: {\n logger: Logger;\n validate?: boolean;\n validateFn?: (fixtures: Fixture[]) => ValidationResult[];\n },\n): { close: () => void } {\n const { logger, validate, validateFn } = opts;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n function reload() {\n logger.info(`File changed — reloading fixtures from ${fixturePath}...`);\n\n let newFixtures: Fixture[];\n try {\n newFixtures = loadFn();\n } catch (err) {\n logger.error(\"Failed to reload fixtures:\", err);\n logger.error(\"Previous fixtures remain active. Fix the error and save again to retry.\");\n return;\n }\n\n if (newFixtures.length === 0 && fixtures.length > 0) {\n logger.warn(\n \"Reload produced 0 fixtures — keeping previous fixtures. Check fixture file for errors.\",\n );\n return;\n }\n\n if (validate && validateFn) {\n const results = validateFn(newFixtures);\n const errors = results.filter((r) => r.severity === \"error\");\n const warnings = results.filter((r) => r.severity === \"warning\");\n\n for (const w of warnings) {\n logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);\n }\n\n if (errors.length > 0) {\n for (const e of errors) {\n logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);\n }\n logger.error(`${errors.length} validation error(s) — keeping previous fixtures`);\n return;\n }\n }\n\n // Replace in-place to preserve array reference identity\n fixtures.length = 0;\n fixtures.push(...newFixtures);\n logger.info(`Reloaded ${newFixtures.length} fixture(s)`);\n }\n\n const watcher: FSWatcher = watch(fixturePath, { recursive: true }, () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(reload, DEBOUNCE_MS);\n });\n\n watcher.on(\"error\", (err: Error) => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = null;\n try {\n watcher.close();\n } catch {\n /* already dead */\n }\n logger.error(`File watcher error on ${fixturePath}: ${err.message}`);\n logger.error(\"Fixture auto-reload is no longer active. Restart the server to resume watching.\");\n });\n\n return {\n close() {\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n },\n };\n}\n"],"mappings":";;;AAKA,MAAM,cAAc;AAEpB,SAAgB,cACd,aACA,UACA,QACA,MAKuB;CACvB,MAAM,EAAE,QAAQ,UAAU,eAAe;CACzC,IAAI,gBAAsD;CAE1D,SAAS,SAAS;AAChB,SAAO,KAAK,0CAA0C,YAAY,KAAK;EAEvE,IAAI;AACJ,MAAI;AACF,iBAAc,QAAQ;WACf,KAAK;AACZ,UAAO,MAAM,8BAA8B,IAAI;AAC/C,UAAO,MAAM,0EAA0E;AACvF;;AAGF,MAAI,YAAY,WAAW,KAAK,SAAS,SAAS,GAAG;AACnD,UAAO,KACL,yFACD;AACD;;AAGF,MAAI,YAAY,YAAY;GAC1B,MAAM,UAAU,WAAW,YAAY;GACvC,MAAM,SAAS,QAAQ,QAAQ,MAAM,EAAE,aAAa,QAAQ;GAC5D,MAAM,WAAW,QAAQ,QAAQ,MAAM,EAAE,aAAa,UAAU;AAEhE,QAAK,MAAM,KAAK,SACd,QAAO,KAAK,WAAW,EAAE,aAAa,IAAI,EAAE,UAAU;AAGxD,OAAI,OAAO,SAAS,GAAG;AACrB,SAAK,MAAM,KAAK,OACd,QAAO,MAAM,WAAW,EAAE,aAAa,IAAI,EAAE,UAAU;AAEzD,WAAO,MAAM,GAAG,OAAO,OAAO,kDAAkD;AAChF;;;AAKJ,WAAS,SAAS;AAClB,WAAS,KAAK,GAAG,YAAY;AAC7B,SAAO,KAAK,YAAY,YAAY,OAAO,aAAa;;CAG1D,MAAM,UAAqB,MAAM,aAAa,EAAE,WAAW,MAAM,QAAQ;AACvE,MAAI,cAAe,cAAa,cAAc;AAC9C,kBAAgB,WAAW,QAAQ,YAAY;GAC/C;AAEF,SAAQ,GAAG,UAAU,QAAe;AAClC,MAAI,cAAe,cAAa,cAAc;AAC9C,kBAAgB;AAChB,MAAI;AACF,WAAQ,OAAO;UACT;AAGR,SAAO,MAAM,yBAAyB,YAAY,IAAI,IAAI,UAAU;AACpE,SAAO,MAAM,kFAAkF;GAC/F;AAEF,QAAO,EACL,QAAQ;AACN,MAAI,cAAe,cAAa,cAAc;AAC9C,UAAQ,OAAO;IAElB"}
@@ -0,0 +1,187 @@
1
+ const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
+ let node_crypto = require("node:crypto");
3
+ let node_events = require("node:events");
4
+
5
+ //#region src/ws-framing.ts
6
+ /**
7
+ * Minimal RFC 6455 WebSocket server implementation.
8
+ *
9
+ * Zero dependencies — uses only Node.js builtins (node:crypto, node:events).
10
+ * Supports text frames, ping/pong, close handshake, and client frame unmasking.
11
+ * Designed for a mock server — no extensions, no binary frames, no compression.
12
+ */
13
+ const WS_GUID = "258EAFA5-E914-47DA-95CA-5AB5DC799C07";
14
+ const OP_CONTINUATION = 0;
15
+ const OP_TEXT = 1;
16
+ const OP_CLOSE = 8;
17
+ const OP_PING = 9;
18
+ const OP_PONG = 10;
19
+ var WebSocketConnection = class extends node_events.EventEmitter {
20
+ socket;
21
+ buffer = Buffer.alloc(0);
22
+ closed = false;
23
+ fragments = [];
24
+ constructor(socket) {
25
+ super();
26
+ this.socket = socket;
27
+ socket.on("data", (data) => {
28
+ this.buffer = Buffer.concat([this.buffer, data]);
29
+ this.parseFrames();
30
+ });
31
+ socket.on("close", () => {
32
+ if (!this.closed) {
33
+ this.closed = true;
34
+ this.emit("close", 1006, "Connection lost");
35
+ }
36
+ });
37
+ socket.on("error", (err) => {
38
+ this.emit("error", err);
39
+ });
40
+ }
41
+ send(data) {
42
+ if (this.closed) return;
43
+ const payload = Buffer.from(data, "utf-8");
44
+ this.writeFrame(OP_TEXT, payload);
45
+ }
46
+ close(code = 1e3, reason = "") {
47
+ if (this.closed) return;
48
+ this.closed = true;
49
+ const reasonBuf = Buffer.from(reason, "utf-8");
50
+ const payload = Buffer.alloc(2 + reasonBuf.length);
51
+ payload.writeUInt16BE(code, 0);
52
+ reasonBuf.copy(payload, 2);
53
+ this.writeFrame(OP_CLOSE, payload);
54
+ setTimeout(() => {
55
+ if (!this.socket.destroyed) this.socket.destroy();
56
+ this.emit("close", code, reason);
57
+ }, 100);
58
+ }
59
+ destroy() {
60
+ if (this.closed) return;
61
+ this.closed = true;
62
+ if (!this.socket.destroyed) this.socket.destroy();
63
+ this.emit("close", 1006, "Connection destroyed");
64
+ }
65
+ get isClosed() {
66
+ return this.closed;
67
+ }
68
+ writeFrame(opcode, payload) {
69
+ if (this.socket.destroyed) return;
70
+ const length = payload.length;
71
+ let header;
72
+ if (length < 126) {
73
+ header = Buffer.alloc(2);
74
+ header[0] = 128 | opcode;
75
+ header[1] = length;
76
+ } else if (length < 65536) {
77
+ header = Buffer.alloc(4);
78
+ header[0] = 128 | opcode;
79
+ header[1] = 126;
80
+ header.writeUInt16BE(length, 2);
81
+ } else {
82
+ header = Buffer.alloc(10);
83
+ header[0] = 128 | opcode;
84
+ header[1] = 127;
85
+ header.writeUInt32BE(0, 2);
86
+ header.writeUInt32BE(length, 6);
87
+ }
88
+ try {
89
+ this.socket.write(Buffer.concat([header, payload]));
90
+ } catch (err) {
91
+ if (!this.socket.destroyed) {
92
+ const msg = err instanceof Error ? err.message : String(err);
93
+ console.error(`[LLMock] Unexpected writeFrame error: ${msg}`);
94
+ }
95
+ }
96
+ }
97
+ parseFrames() {
98
+ while (this.buffer.length >= 2 && !this.closed) {
99
+ const byte0 = this.buffer[0];
100
+ const byte1 = this.buffer[1];
101
+ const fin = (byte0 & 128) !== 0;
102
+ const opcode = byte0 & 15;
103
+ const masked = (byte1 & 128) !== 0;
104
+ let payloadLength = byte1 & 127;
105
+ let offset = 2;
106
+ if (payloadLength === 126) {
107
+ if (this.buffer.length < 4) return;
108
+ payloadLength = this.buffer.readUInt16BE(2);
109
+ offset = 4;
110
+ } else if (payloadLength === 127) {
111
+ if (this.buffer.length < 10) return;
112
+ payloadLength = this.buffer.readUInt32BE(6) + this.buffer.readUInt32BE(2) * 4294967296;
113
+ offset = 10;
114
+ }
115
+ const totalFrameSize = offset + (masked ? 4 : 0) + payloadLength;
116
+ if (this.buffer.length < totalFrameSize) return;
117
+ let maskKey = null;
118
+ if (masked) {
119
+ maskKey = this.buffer.subarray(offset, offset + 4);
120
+ offset += 4;
121
+ }
122
+ let payload = this.buffer.subarray(offset, offset + payloadLength);
123
+ if (maskKey) {
124
+ payload = Buffer.from(payload);
125
+ for (let i = 0; i < payload.length; i++) payload[i] ^= maskKey[i % 4];
126
+ }
127
+ this.buffer = this.buffer.subarray(totalFrameSize);
128
+ this.handleFrame(fin, opcode, payload);
129
+ }
130
+ }
131
+ handleFrame(fin, opcode, payload) {
132
+ if (opcode === OP_PING) {
133
+ this.writeFrame(OP_PONG, payload);
134
+ return;
135
+ }
136
+ if (opcode === OP_PONG) return;
137
+ if (opcode === OP_CLOSE) {
138
+ const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1005;
139
+ const reason = payload.length > 2 ? payload.subarray(2).toString("utf-8") : "";
140
+ if (!this.closed) {
141
+ this.closed = true;
142
+ this.writeFrame(OP_CLOSE, payload);
143
+ this.socket.end();
144
+ this.emit("close", code, reason);
145
+ }
146
+ return;
147
+ }
148
+ if (opcode === OP_TEXT || opcode === OP_CONTINUATION) {
149
+ this.fragments.push(payload);
150
+ if (fin) {
151
+ const message = Buffer.concat(this.fragments).toString("utf-8");
152
+ this.fragments = [];
153
+ this.emit("message", message);
154
+ }
155
+ return;
156
+ }
157
+ }
158
+ };
159
+ function computeAcceptKey(wsKey) {
160
+ return (0, node_crypto.createHash)("sha1").update(wsKey + WS_GUID).digest("base64");
161
+ }
162
+ function upgradeToWebSocket(req, socket) {
163
+ const key = req.headers["sec-websocket-key"];
164
+ if (!key) {
165
+ socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
166
+ socket.destroy();
167
+ throw new Error("Missing Sec-WebSocket-Key header");
168
+ }
169
+ let responseHeaders = `HTTP/1.1 101 Switching Protocols\r
170
+ Upgrade: websocket\r
171
+ Connection: Upgrade\r
172
+ Sec-WebSocket-Accept: ${computeAcceptKey(key)}\r\n`;
173
+ const protocol = req.headers["sec-websocket-protocol"];
174
+ if (protocol) {
175
+ const first = protocol.split(",")[0].trim();
176
+ responseHeaders += `Sec-WebSocket-Protocol: ${first}\r\n`;
177
+ }
178
+ responseHeaders += "\r\n";
179
+ socket.write(responseHeaders);
180
+ return new WebSocketConnection(socket);
181
+ }
182
+
183
+ //#endregion
184
+ exports.WebSocketConnection = WebSocketConnection;
185
+ exports.computeAcceptKey = computeAcceptKey;
186
+ exports.upgradeToWebSocket = upgradeToWebSocket;
187
+ //# sourceMappingURL=ws-framing.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-framing.cjs","names":["EventEmitter"],"sources":["../src/ws-framing.ts"],"sourcesContent":["/**\n * Minimal RFC 6455 WebSocket server implementation.\n *\n * Zero dependencies — uses only Node.js builtins (node:crypto, node:events).\n * Supports text frames, ping/pong, close handshake, and client frame unmasking.\n * Designed for a mock server — no extensions, no binary frames, no compression.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { EventEmitter } from \"node:events\";\nimport type * as net from \"node:net\";\nimport type * as http from \"node:http\";\n\nconst WS_GUID = \"258EAFA5-E914-47DA-95CA-5AB5DC799C07\";\n\n// Opcodes\nconst OP_CONTINUATION = 0x0;\nconst OP_TEXT = 0x1;\nconst OP_CLOSE = 0x8;\nconst OP_PING = 0x9;\nconst OP_PONG = 0xa;\n\nexport class WebSocketConnection extends EventEmitter {\n private socket: net.Socket;\n private buffer: Buffer = Buffer.alloc(0);\n private closed = false;\n\n // For fragmented messages (continuation frames)\n private fragments: Buffer[] = [];\n\n constructor(socket: net.Socket) {\n super();\n this.socket = socket;\n\n socket.on(\"data\", (data: Buffer) => {\n this.buffer = Buffer.concat([this.buffer, data]);\n this.parseFrames();\n });\n\n socket.on(\"close\", () => {\n if (!this.closed) {\n this.closed = true;\n this.emit(\"close\", 1006, \"Connection lost\");\n }\n });\n\n socket.on(\"error\", (err: Error) => {\n this.emit(\"error\", err);\n });\n }\n\n send(data: string): void {\n if (this.closed) return;\n const payload = Buffer.from(data, \"utf-8\");\n this.writeFrame(OP_TEXT, payload);\n }\n\n close(code = 1000, reason = \"\"): void {\n if (this.closed) return;\n this.closed = true;\n\n const reasonBuf = Buffer.from(reason, \"utf-8\");\n const payload = Buffer.alloc(2 + reasonBuf.length);\n payload.writeUInt16BE(code, 0);\n reasonBuf.copy(payload, 2);\n this.writeFrame(OP_CLOSE, payload);\n\n // Give the client a moment to receive the close frame before destroying.\n // If writeFrame failed (socket already destroyed), this is a no-op.\n setTimeout(() => {\n if (!this.socket.destroyed) {\n this.socket.destroy();\n }\n // Emit close event for server-initiated closes so listeners\n // (e.g. activeConnections.delete) always fire.\n this.emit(\"close\", code, reason);\n }, 100);\n }\n\n destroy(): void {\n if (this.closed) return;\n this.closed = true;\n if (!this.socket.destroyed) {\n this.socket.destroy();\n }\n this.emit(\"close\", 1006, \"Connection destroyed\");\n }\n\n get isClosed(): boolean {\n return this.closed;\n }\n\n private writeFrame(opcode: number, payload: Buffer): void {\n if (this.socket.destroyed) return;\n\n // Server-to-client frames are NOT masked (per RFC 6455 §5.1)\n const length = payload.length;\n let header: Buffer;\n\n if (length < 126) {\n header = Buffer.alloc(2);\n header[0] = 0x80 | opcode; // FIN + opcode\n header[1] = length;\n } else if (length < 65536) {\n header = Buffer.alloc(4);\n header[0] = 0x80 | opcode;\n header[1] = 126;\n header.writeUInt16BE(length, 2);\n } else {\n header = Buffer.alloc(10);\n header[0] = 0x80 | opcode;\n header[1] = 127;\n header.writeUInt32BE(0, 2);\n header.writeUInt32BE(length, 6);\n }\n\n try {\n this.socket.write(Buffer.concat([header, payload]));\n } catch (err: unknown) {\n // Expected when socket is destroyed between our check and write.\n // Log unexpected errors so they don't vanish silently.\n if (!this.socket.destroyed) {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`[LLMock] Unexpected writeFrame error: ${msg}`);\n }\n }\n }\n\n private parseFrames(): void {\n while (this.buffer.length >= 2 && !this.closed) {\n const byte0 = this.buffer[0];\n const byte1 = this.buffer[1];\n\n const fin = (byte0 & 0x80) !== 0;\n const opcode = byte0 & 0x0f;\n const masked = (byte1 & 0x80) !== 0;\n let payloadLength = byte1 & 0x7f;\n let offset = 2;\n\n if (payloadLength === 126) {\n if (this.buffer.length < 4) return; // need more data\n payloadLength = this.buffer.readUInt16BE(2);\n offset = 4;\n } else if (payloadLength === 127) {\n if (this.buffer.length < 10) return;\n // Read lower 32 bits (upper 32 should be 0 for reasonable payloads)\n payloadLength = this.buffer.readUInt32BE(6) + this.buffer.readUInt32BE(2) * 0x100000000;\n offset = 10;\n }\n\n const maskSize = masked ? 4 : 0;\n const totalFrameSize = offset + maskSize + payloadLength;\n\n if (this.buffer.length < totalFrameSize) return; // need more data\n\n let maskKey: Buffer | null = null;\n if (masked) {\n maskKey = this.buffer.subarray(offset, offset + 4);\n offset += 4;\n }\n\n let payload = this.buffer.subarray(offset, offset + payloadLength);\n\n // Unmask client payload\n if (maskKey) {\n payload = Buffer.from(payload); // copy before mutating\n for (let i = 0; i < payload.length; i++) {\n payload[i] ^= maskKey[i % 4];\n }\n }\n\n // Consume the frame from the buffer\n this.buffer = this.buffer.subarray(totalFrameSize);\n\n this.handleFrame(fin, opcode, payload);\n }\n }\n\n private handleFrame(fin: boolean, opcode: number, payload: Buffer): void {\n // Control frames (opcode >= 0x8) must not be fragmented\n if (opcode === OP_PING) {\n this.writeFrame(OP_PONG, payload);\n return;\n }\n\n if (opcode === OP_PONG) {\n // Ignore unsolicited pongs\n return;\n }\n\n if (opcode === OP_CLOSE) {\n const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1005;\n const reason = payload.length > 2 ? payload.subarray(2).toString(\"utf-8\") : \"\";\n\n if (!this.closed) {\n this.closed = true;\n // Echo close frame back\n this.writeFrame(OP_CLOSE, payload);\n this.socket.end();\n this.emit(\"close\", code, reason);\n }\n // If already closed (server-initiated or duplicate), ignore — the\n // close event was already emitted by close() or the first OP_CLOSE.\n return;\n }\n\n // Text or continuation frames\n if (opcode === OP_TEXT || opcode === OP_CONTINUATION) {\n this.fragments.push(payload);\n\n if (fin) {\n const message = Buffer.concat(this.fragments).toString(\"utf-8\");\n this.fragments = [];\n this.emit(\"message\", message);\n }\n // If !fin, wait for more continuation frames\n return;\n }\n\n // Binary or unknown — just ignore for a mock server\n }\n}\n\nexport function computeAcceptKey(wsKey: string): string {\n return createHash(\"sha1\")\n .update(wsKey + WS_GUID)\n .digest(\"base64\");\n}\n\nexport function upgradeToWebSocket(\n req: http.IncomingMessage,\n socket: net.Socket,\n): WebSocketConnection {\n const key = req.headers[\"sec-websocket-key\"];\n if (!key) {\n socket.write(\"HTTP/1.1 400 Bad Request\\r\\n\\r\\n\");\n socket.destroy();\n throw new Error(\"Missing Sec-WebSocket-Key header\");\n }\n\n const acceptKey = computeAcceptKey(key);\n\n let responseHeaders =\n \"HTTP/1.1 101 Switching Protocols\\r\\n\" +\n \"Upgrade: websocket\\r\\n\" +\n \"Connection: Upgrade\\r\\n\" +\n `Sec-WebSocket-Accept: ${acceptKey}\\r\\n`;\n\n // Echo back requested subprotocol if present\n const protocol = req.headers[\"sec-websocket-protocol\"];\n if (protocol) {\n // Take the first offered protocol\n const first = protocol.split(\",\")[0].trim();\n responseHeaders += `Sec-WebSocket-Protocol: ${first}\\r\\n`;\n }\n\n responseHeaders += \"\\r\\n\";\n\n socket.write(responseHeaders);\n\n return new WebSocketConnection(socket);\n}\n"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import * as http from "node:http";
2
+ import * as net from "node:net";
3
+ import { EventEmitter } from "node:events";
4
+
5
+ //#region src/ws-framing.d.ts
6
+
7
+ declare class WebSocketConnection extends EventEmitter {
8
+ private socket;
9
+ private buffer;
10
+ private closed;
11
+ private fragments;
12
+ constructor(socket: net.Socket);
13
+ send(data: string): void;
14
+ close(code?: number, reason?: string): void;
15
+ destroy(): void;
16
+ get isClosed(): boolean;
17
+ private writeFrame;
18
+ private parseFrames;
19
+ private handleFrame;
20
+ }
21
+ declare function computeAcceptKey(wsKey: string): string;
22
+ declare function upgradeToWebSocket(req: http.IncomingMessage, socket: net.Socket): WebSocketConnection;
23
+ //# sourceMappingURL=ws-framing.d.ts.map
24
+ //#endregion
25
+ export { WebSocketConnection, computeAcceptKey, upgradeToWebSocket };
26
+ //# sourceMappingURL=ws-framing.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-framing.d.cts","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":[],"mappings":";;;;;;AA+NgB,cAzMH,mBAAA,SAA4B,YAAA,CAyMT;EAMhB,QAAA,MAAA;EAAkB,QAAA,MAAA;UACtB,MAAA;UACE,SAAA;aACX,CAAA,MAAA,EA1MmB,GAAA,CAAI,MA0MvB;EAAmB,IAAA,CAAA,IAAA,EAAA,MAAA,CAAA,EAAA,IAAA;;;;;;;;iBATN,gBAAA;iBAMA,kBAAA,MACT,IAAA,CAAK,yBACF,GAAA,CAAI,SACX"}
@@ -0,0 +1,26 @@
1
+ import * as http from "node:http";
2
+ import { EventEmitter } from "node:events";
3
+ import * as net from "node:net";
4
+
5
+ //#region src/ws-framing.d.ts
6
+
7
+ declare class WebSocketConnection extends EventEmitter {
8
+ private socket;
9
+ private buffer;
10
+ private closed;
11
+ private fragments;
12
+ constructor(socket: net.Socket);
13
+ send(data: string): void;
14
+ close(code?: number, reason?: string): void;
15
+ destroy(): void;
16
+ get isClosed(): boolean;
17
+ private writeFrame;
18
+ private parseFrames;
19
+ private handleFrame;
20
+ }
21
+ declare function computeAcceptKey(wsKey: string): string;
22
+ declare function upgradeToWebSocket(req: http.IncomingMessage, socket: net.Socket): WebSocketConnection;
23
+ //# sourceMappingURL=ws-framing.d.ts.map
24
+ //#endregion
25
+ export { WebSocketConnection, computeAcceptKey, upgradeToWebSocket };
26
+ //# sourceMappingURL=ws-framing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-framing.d.ts","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":[],"mappings":";;;;;;AA+NgB,cAzMH,mBAAA,SAA4B,YAAA,CAyMT;EAMhB,QAAA,MAAA;EAAkB,QAAA,MAAA;UACtB,MAAA;UACE,SAAA;aACX,CAAA,MAAA,EA1MmB,GAAA,CAAI,MA0MvB;EAAmB,IAAA,CAAA,IAAA,EAAA,MAAA,CAAA,EAAA,IAAA;;;;;;;;iBATN,gBAAA;iBAMA,kBAAA,MACT,IAAA,CAAK,yBACF,GAAA,CAAI,SACX"}
@@ -0,0 +1,184 @@
1
+ import { createHash } from "node:crypto";
2
+ import { EventEmitter } from "node:events";
3
+
4
+ //#region src/ws-framing.ts
5
+ /**
6
+ * Minimal RFC 6455 WebSocket server implementation.
7
+ *
8
+ * Zero dependencies — uses only Node.js builtins (node:crypto, node:events).
9
+ * Supports text frames, ping/pong, close handshake, and client frame unmasking.
10
+ * Designed for a mock server — no extensions, no binary frames, no compression.
11
+ */
12
+ const WS_GUID = "258EAFA5-E914-47DA-95CA-5AB5DC799C07";
13
+ const OP_CONTINUATION = 0;
14
+ const OP_TEXT = 1;
15
+ const OP_CLOSE = 8;
16
+ const OP_PING = 9;
17
+ const OP_PONG = 10;
18
+ var WebSocketConnection = class extends EventEmitter {
19
+ socket;
20
+ buffer = Buffer.alloc(0);
21
+ closed = false;
22
+ fragments = [];
23
+ constructor(socket) {
24
+ super();
25
+ this.socket = socket;
26
+ socket.on("data", (data) => {
27
+ this.buffer = Buffer.concat([this.buffer, data]);
28
+ this.parseFrames();
29
+ });
30
+ socket.on("close", () => {
31
+ if (!this.closed) {
32
+ this.closed = true;
33
+ this.emit("close", 1006, "Connection lost");
34
+ }
35
+ });
36
+ socket.on("error", (err) => {
37
+ this.emit("error", err);
38
+ });
39
+ }
40
+ send(data) {
41
+ if (this.closed) return;
42
+ const payload = Buffer.from(data, "utf-8");
43
+ this.writeFrame(OP_TEXT, payload);
44
+ }
45
+ close(code = 1e3, reason = "") {
46
+ if (this.closed) return;
47
+ this.closed = true;
48
+ const reasonBuf = Buffer.from(reason, "utf-8");
49
+ const payload = Buffer.alloc(2 + reasonBuf.length);
50
+ payload.writeUInt16BE(code, 0);
51
+ reasonBuf.copy(payload, 2);
52
+ this.writeFrame(OP_CLOSE, payload);
53
+ setTimeout(() => {
54
+ if (!this.socket.destroyed) this.socket.destroy();
55
+ this.emit("close", code, reason);
56
+ }, 100);
57
+ }
58
+ destroy() {
59
+ if (this.closed) return;
60
+ this.closed = true;
61
+ if (!this.socket.destroyed) this.socket.destroy();
62
+ this.emit("close", 1006, "Connection destroyed");
63
+ }
64
+ get isClosed() {
65
+ return this.closed;
66
+ }
67
+ writeFrame(opcode, payload) {
68
+ if (this.socket.destroyed) return;
69
+ const length = payload.length;
70
+ let header;
71
+ if (length < 126) {
72
+ header = Buffer.alloc(2);
73
+ header[0] = 128 | opcode;
74
+ header[1] = length;
75
+ } else if (length < 65536) {
76
+ header = Buffer.alloc(4);
77
+ header[0] = 128 | opcode;
78
+ header[1] = 126;
79
+ header.writeUInt16BE(length, 2);
80
+ } else {
81
+ header = Buffer.alloc(10);
82
+ header[0] = 128 | opcode;
83
+ header[1] = 127;
84
+ header.writeUInt32BE(0, 2);
85
+ header.writeUInt32BE(length, 6);
86
+ }
87
+ try {
88
+ this.socket.write(Buffer.concat([header, payload]));
89
+ } catch (err) {
90
+ if (!this.socket.destroyed) {
91
+ const msg = err instanceof Error ? err.message : String(err);
92
+ console.error(`[LLMock] Unexpected writeFrame error: ${msg}`);
93
+ }
94
+ }
95
+ }
96
+ parseFrames() {
97
+ while (this.buffer.length >= 2 && !this.closed) {
98
+ const byte0 = this.buffer[0];
99
+ const byte1 = this.buffer[1];
100
+ const fin = (byte0 & 128) !== 0;
101
+ const opcode = byte0 & 15;
102
+ const masked = (byte1 & 128) !== 0;
103
+ let payloadLength = byte1 & 127;
104
+ let offset = 2;
105
+ if (payloadLength === 126) {
106
+ if (this.buffer.length < 4) return;
107
+ payloadLength = this.buffer.readUInt16BE(2);
108
+ offset = 4;
109
+ } else if (payloadLength === 127) {
110
+ if (this.buffer.length < 10) return;
111
+ payloadLength = this.buffer.readUInt32BE(6) + this.buffer.readUInt32BE(2) * 4294967296;
112
+ offset = 10;
113
+ }
114
+ const totalFrameSize = offset + (masked ? 4 : 0) + payloadLength;
115
+ if (this.buffer.length < totalFrameSize) return;
116
+ let maskKey = null;
117
+ if (masked) {
118
+ maskKey = this.buffer.subarray(offset, offset + 4);
119
+ offset += 4;
120
+ }
121
+ let payload = this.buffer.subarray(offset, offset + payloadLength);
122
+ if (maskKey) {
123
+ payload = Buffer.from(payload);
124
+ for (let i = 0; i < payload.length; i++) payload[i] ^= maskKey[i % 4];
125
+ }
126
+ this.buffer = this.buffer.subarray(totalFrameSize);
127
+ this.handleFrame(fin, opcode, payload);
128
+ }
129
+ }
130
+ handleFrame(fin, opcode, payload) {
131
+ if (opcode === OP_PING) {
132
+ this.writeFrame(OP_PONG, payload);
133
+ return;
134
+ }
135
+ if (opcode === OP_PONG) return;
136
+ if (opcode === OP_CLOSE) {
137
+ const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1005;
138
+ const reason = payload.length > 2 ? payload.subarray(2).toString("utf-8") : "";
139
+ if (!this.closed) {
140
+ this.closed = true;
141
+ this.writeFrame(OP_CLOSE, payload);
142
+ this.socket.end();
143
+ this.emit("close", code, reason);
144
+ }
145
+ return;
146
+ }
147
+ if (opcode === OP_TEXT || opcode === OP_CONTINUATION) {
148
+ this.fragments.push(payload);
149
+ if (fin) {
150
+ const message = Buffer.concat(this.fragments).toString("utf-8");
151
+ this.fragments = [];
152
+ this.emit("message", message);
153
+ }
154
+ return;
155
+ }
156
+ }
157
+ };
158
+ function computeAcceptKey(wsKey) {
159
+ return createHash("sha1").update(wsKey + WS_GUID).digest("base64");
160
+ }
161
+ function upgradeToWebSocket(req, socket) {
162
+ const key = req.headers["sec-websocket-key"];
163
+ if (!key) {
164
+ socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
165
+ socket.destroy();
166
+ throw new Error("Missing Sec-WebSocket-Key header");
167
+ }
168
+ let responseHeaders = `HTTP/1.1 101 Switching Protocols\r
169
+ Upgrade: websocket\r
170
+ Connection: Upgrade\r
171
+ Sec-WebSocket-Accept: ${computeAcceptKey(key)}\r\n`;
172
+ const protocol = req.headers["sec-websocket-protocol"];
173
+ if (protocol) {
174
+ const first = protocol.split(",")[0].trim();
175
+ responseHeaders += `Sec-WebSocket-Protocol: ${first}\r\n`;
176
+ }
177
+ responseHeaders += "\r\n";
178
+ socket.write(responseHeaders);
179
+ return new WebSocketConnection(socket);
180
+ }
181
+
182
+ //#endregion
183
+ export { WebSocketConnection, computeAcceptKey, upgradeToWebSocket };
184
+ //# sourceMappingURL=ws-framing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-framing.js","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":["/**\n * Minimal RFC 6455 WebSocket server implementation.\n *\n * Zero dependencies — uses only Node.js builtins (node:crypto, node:events).\n * Supports text frames, ping/pong, close handshake, and client frame unmasking.\n * Designed for a mock server — no extensions, no binary frames, no compression.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { EventEmitter } from \"node:events\";\nimport type * as net from \"node:net\";\nimport type * as http from \"node:http\";\n\nconst WS_GUID = \"258EAFA5-E914-47DA-95CA-5AB5DC799C07\";\n\n// Opcodes\nconst OP_CONTINUATION = 0x0;\nconst OP_TEXT = 0x1;\nconst OP_CLOSE = 0x8;\nconst OP_PING = 0x9;\nconst OP_PONG = 0xa;\n\nexport class WebSocketConnection extends EventEmitter {\n private socket: net.Socket;\n private buffer: Buffer = Buffer.alloc(0);\n private closed = false;\n\n // For fragmented messages (continuation frames)\n private fragments: Buffer[] = [];\n\n constructor(socket: net.Socket) {\n super();\n this.socket = socket;\n\n socket.on(\"data\", (data: Buffer) => {\n this.buffer = Buffer.concat([this.buffer, data]);\n this.parseFrames();\n });\n\n socket.on(\"close\", () => {\n if (!this.closed) {\n this.closed = true;\n this.emit(\"close\", 1006, \"Connection lost\");\n }\n });\n\n socket.on(\"error\", (err: Error) => {\n this.emit(\"error\", err);\n });\n }\n\n send(data: string): void {\n if (this.closed) return;\n const payload = Buffer.from(data, \"utf-8\");\n this.writeFrame(OP_TEXT, payload);\n }\n\n close(code = 1000, reason = \"\"): void {\n if (this.closed) return;\n this.closed = true;\n\n const reasonBuf = Buffer.from(reason, \"utf-8\");\n const payload = Buffer.alloc(2 + reasonBuf.length);\n payload.writeUInt16BE(code, 0);\n reasonBuf.copy(payload, 2);\n this.writeFrame(OP_CLOSE, payload);\n\n // Give the client a moment to receive the close frame before destroying.\n // If writeFrame failed (socket already destroyed), this is a no-op.\n setTimeout(() => {\n if (!this.socket.destroyed) {\n this.socket.destroy();\n }\n // Emit close event for server-initiated closes so listeners\n // (e.g. activeConnections.delete) always fire.\n this.emit(\"close\", code, reason);\n }, 100);\n }\n\n destroy(): void {\n if (this.closed) return;\n this.closed = true;\n if (!this.socket.destroyed) {\n this.socket.destroy();\n }\n this.emit(\"close\", 1006, \"Connection destroyed\");\n }\n\n get isClosed(): boolean {\n return this.closed;\n }\n\n private writeFrame(opcode: number, payload: Buffer): void {\n if (this.socket.destroyed) return;\n\n // Server-to-client frames are NOT masked (per RFC 6455 §5.1)\n const length = payload.length;\n let header: Buffer;\n\n if (length < 126) {\n header = Buffer.alloc(2);\n header[0] = 0x80 | opcode; // FIN + opcode\n header[1] = length;\n } else if (length < 65536) {\n header = Buffer.alloc(4);\n header[0] = 0x80 | opcode;\n header[1] = 126;\n header.writeUInt16BE(length, 2);\n } else {\n header = Buffer.alloc(10);\n header[0] = 0x80 | opcode;\n header[1] = 127;\n header.writeUInt32BE(0, 2);\n header.writeUInt32BE(length, 6);\n }\n\n try {\n this.socket.write(Buffer.concat([header, payload]));\n } catch (err: unknown) {\n // Expected when socket is destroyed between our check and write.\n // Log unexpected errors so they don't vanish silently.\n if (!this.socket.destroyed) {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`[LLMock] Unexpected writeFrame error: ${msg}`);\n }\n }\n }\n\n private parseFrames(): void {\n while (this.buffer.length >= 2 && !this.closed) {\n const byte0 = this.buffer[0];\n const byte1 = this.buffer[1];\n\n const fin = (byte0 & 0x80) !== 0;\n const opcode = byte0 & 0x0f;\n const masked = (byte1 & 0x80) !== 0;\n let payloadLength = byte1 & 0x7f;\n let offset = 2;\n\n if (payloadLength === 126) {\n if (this.buffer.length < 4) return; // need more data\n payloadLength = this.buffer.readUInt16BE(2);\n offset = 4;\n } else if (payloadLength === 127) {\n if (this.buffer.length < 10) return;\n // Read lower 32 bits (upper 32 should be 0 for reasonable payloads)\n payloadLength = this.buffer.readUInt32BE(6) + this.buffer.readUInt32BE(2) * 0x100000000;\n offset = 10;\n }\n\n const maskSize = masked ? 4 : 0;\n const totalFrameSize = offset + maskSize + payloadLength;\n\n if (this.buffer.length < totalFrameSize) return; // need more data\n\n let maskKey: Buffer | null = null;\n if (masked) {\n maskKey = this.buffer.subarray(offset, offset + 4);\n offset += 4;\n }\n\n let payload = this.buffer.subarray(offset, offset + payloadLength);\n\n // Unmask client payload\n if (maskKey) {\n payload = Buffer.from(payload); // copy before mutating\n for (let i = 0; i < payload.length; i++) {\n payload[i] ^= maskKey[i % 4];\n }\n }\n\n // Consume the frame from the buffer\n this.buffer = this.buffer.subarray(totalFrameSize);\n\n this.handleFrame(fin, opcode, payload);\n }\n }\n\n private handleFrame(fin: boolean, opcode: number, payload: Buffer): void {\n // Control frames (opcode >= 0x8) must not be fragmented\n if (opcode === OP_PING) {\n this.writeFrame(OP_PONG, payload);\n return;\n }\n\n if (opcode === OP_PONG) {\n // Ignore unsolicited pongs\n return;\n }\n\n if (opcode === OP_CLOSE) {\n const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1005;\n const reason = payload.length > 2 ? payload.subarray(2).toString(\"utf-8\") : \"\";\n\n if (!this.closed) {\n this.closed = true;\n // Echo close frame back\n this.writeFrame(OP_CLOSE, payload);\n this.socket.end();\n this.emit(\"close\", code, reason);\n }\n // If already closed (server-initiated or duplicate), ignore — the\n // close event was already emitted by close() or the first OP_CLOSE.\n return;\n }\n\n // Text or continuation frames\n if (opcode === OP_TEXT || opcode === OP_CONTINUATION) {\n this.fragments.push(payload);\n\n if (fin) {\n const message = Buffer.concat(this.fragments).toString(\"utf-8\");\n this.fragments = [];\n this.emit(\"message\", message);\n }\n // If !fin, wait for more continuation frames\n return;\n }\n\n // Binary or unknown — just ignore for a mock server\n }\n}\n\nexport function computeAcceptKey(wsKey: string): string {\n return createHash(\"sha1\")\n .update(wsKey + WS_GUID)\n .digest(\"base64\");\n}\n\nexport function upgradeToWebSocket(\n req: http.IncomingMessage,\n socket: net.Socket,\n): WebSocketConnection {\n const key = req.headers[\"sec-websocket-key\"];\n if (!key) {\n socket.write(\"HTTP/1.1 400 Bad Request\\r\\n\\r\\n\");\n socket.destroy();\n throw new Error(\"Missing Sec-WebSocket-Key header\");\n }\n\n const acceptKey = computeAcceptKey(key);\n\n let responseHeaders =\n \"HTTP/1.1 101 Switching Protocols\\r\\n\" +\n \"Upgrade: websocket\\r\\n\" +\n \"Connection: Upgrade\\r\\n\" +\n `Sec-WebSocket-Accept: ${acceptKey}\\r\\n`;\n\n // Echo back requested subprotocol if present\n const protocol = req.headers[\"sec-websocket-protocol\"];\n if (protocol) {\n // Take the first offered protocol\n const first = protocol.split(\",\")[0].trim();\n responseHeaders += `Sec-WebSocket-Protocol: ${first}\\r\\n`;\n }\n\n responseHeaders += \"\\r\\n\";\n\n socket.write(responseHeaders);\n\n return new WebSocketConnection(socket);\n}\n"],"mappings":"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"}