@copilotkit/aimock 1.42.0 → 1.43.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 (341) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +56 -0
  4. package/README.md +102 -16
  5. package/dist/agui-mock.cjs +1 -1
  6. package/dist/agui-mock.cjs.map +1 -1
  7. package/dist/agui-mock.d.cts.map +1 -1
  8. package/dist/agui-mock.d.ts.map +1 -1
  9. package/dist/agui-mock.js +1 -1
  10. package/dist/agui-mock.js.map +1 -1
  11. package/dist/agui-recorder.cjs +33 -11
  12. package/dist/agui-recorder.cjs.map +1 -1
  13. package/dist/agui-recorder.d.cts +10 -1
  14. package/dist/agui-recorder.d.cts.map +1 -1
  15. package/dist/agui-recorder.d.ts +10 -1
  16. package/dist/agui-recorder.d.ts.map +1 -1
  17. package/dist/agui-recorder.js +33 -11
  18. package/dist/agui-recorder.js.map +1 -1
  19. package/dist/agui-types.d.cts +1 -0
  20. package/dist/agui-types.d.cts.map +1 -1
  21. package/dist/agui-types.d.ts +1 -0
  22. package/dist/agui-types.d.ts.map +1 -1
  23. package/dist/aimock-cli.cjs +38 -10
  24. package/dist/aimock-cli.cjs.map +1 -1
  25. package/dist/aimock-cli.d.cts +5 -0
  26. package/dist/aimock-cli.d.cts.map +1 -1
  27. package/dist/aimock-cli.d.ts +5 -0
  28. package/dist/aimock-cli.d.ts.map +1 -1
  29. package/dist/aimock-cli.js +38 -10
  30. package/dist/aimock-cli.js.map +1 -1
  31. package/dist/batches.cjs +458 -0
  32. package/dist/batches.cjs.map +1 -0
  33. package/dist/batches.d.cts +57 -0
  34. package/dist/batches.d.cts.map +1 -0
  35. package/dist/batches.d.ts +57 -0
  36. package/dist/batches.d.ts.map +1 -0
  37. package/dist/batches.js +453 -0
  38. package/dist/batches.js.map +1 -0
  39. package/dist/bedrock-converse.cjs +12 -8
  40. package/dist/bedrock-converse.cjs.map +1 -1
  41. package/dist/bedrock-converse.d.cts.map +1 -1
  42. package/dist/bedrock-converse.d.ts.map +1 -1
  43. package/dist/bedrock-converse.js +14 -10
  44. package/dist/bedrock-converse.js.map +1 -1
  45. package/dist/bedrock.cjs +10 -8
  46. package/dist/bedrock.cjs.map +1 -1
  47. package/dist/bedrock.d.cts.map +1 -1
  48. package/dist/bedrock.d.ts.map +1 -1
  49. package/dist/bedrock.js +12 -10
  50. package/dist/bedrock.js.map +1 -1
  51. package/dist/byteplus-video.cjs +3 -3
  52. package/dist/byteplus-video.cjs.map +1 -1
  53. package/dist/byteplus-video.js +4 -4
  54. package/dist/byteplus-video.js.map +1 -1
  55. package/dist/chaos.cjs +622 -72
  56. package/dist/chaos.cjs.map +1 -1
  57. package/dist/chaos.d.cts +164 -7
  58. package/dist/chaos.d.cts.map +1 -1
  59. package/dist/chaos.d.ts +164 -7
  60. package/dist/chaos.d.ts.map +1 -1
  61. package/dist/chaos.js +613 -74
  62. package/dist/chaos.js.map +1 -1
  63. package/dist/cli.cjs +42 -23
  64. package/dist/cli.cjs.map +1 -1
  65. package/dist/cli.js +42 -23
  66. package/dist/cli.js.map +1 -1
  67. package/dist/cohere.cjs +10 -8
  68. package/dist/cohere.cjs.map +1 -1
  69. package/dist/cohere.d.cts.map +1 -1
  70. package/dist/cohere.d.ts.map +1 -1
  71. package/dist/cohere.js +12 -10
  72. package/dist/cohere.js.map +1 -1
  73. package/dist/config-loader.d.cts.map +1 -1
  74. package/dist/config-loader.d.ts.map +1 -1
  75. package/dist/convert.cjs +1 -1
  76. package/dist/convert.cjs.map +1 -1
  77. package/dist/convert.js +1 -1
  78. package/dist/convert.js.map +1 -1
  79. package/dist/elevenlabs-audio.cjs +3 -3
  80. package/dist/elevenlabs-audio.cjs.map +1 -1
  81. package/dist/elevenlabs-audio.js +4 -4
  82. package/dist/elevenlabs-audio.js.map +1 -1
  83. package/dist/elevenlabs-voice.cjs +1053 -0
  84. package/dist/elevenlabs-voice.cjs.map +1 -0
  85. package/dist/elevenlabs-voice.d.cts +72 -0
  86. package/dist/elevenlabs-voice.d.cts.map +1 -0
  87. package/dist/elevenlabs-voice.d.ts +72 -0
  88. package/dist/elevenlabs-voice.d.ts.map +1 -0
  89. package/dist/elevenlabs-voice.js +1048 -0
  90. package/dist/elevenlabs-voice.js.map +1 -0
  91. package/dist/embeddings.cjs +8 -4
  92. package/dist/embeddings.cjs.map +1 -1
  93. package/dist/embeddings.d.cts.map +1 -1
  94. package/dist/embeddings.d.ts.map +1 -1
  95. package/dist/embeddings.js +9 -5
  96. package/dist/embeddings.js.map +1 -1
  97. package/dist/fal-audio.cjs +3 -3
  98. package/dist/fal-audio.cjs.map +1 -1
  99. package/dist/fal-audio.js +4 -4
  100. package/dist/fal-audio.js.map +1 -1
  101. package/dist/fal.cjs +28 -4
  102. package/dist/fal.cjs.map +1 -1
  103. package/dist/fal.d.cts +16 -1
  104. package/dist/fal.d.cts.map +1 -1
  105. package/dist/fal.d.ts +16 -1
  106. package/dist/fal.d.ts.map +1 -1
  107. package/dist/fal.js +28 -5
  108. package/dist/fal.js.map +1 -1
  109. package/dist/files.cjs +1071 -0
  110. package/dist/files.cjs.map +1 -0
  111. package/dist/files.d.cts +68 -0
  112. package/dist/files.d.cts.map +1 -0
  113. package/dist/files.d.ts +68 -0
  114. package/dist/files.d.ts.map +1 -0
  115. package/dist/files.js +1059 -0
  116. package/dist/files.js.map +1 -0
  117. package/dist/fine-tuning.cjs +1051 -0
  118. package/dist/fine-tuning.cjs.map +1 -0
  119. package/dist/fine-tuning.d.cts +96 -0
  120. package/dist/fine-tuning.d.cts.map +1 -0
  121. package/dist/fine-tuning.d.ts +96 -0
  122. package/dist/fine-tuning.d.ts.map +1 -0
  123. package/dist/fine-tuning.js +1044 -0
  124. package/dist/fine-tuning.js.map +1 -0
  125. package/dist/fixture-loader.cjs +0 -0
  126. package/dist/fixture-loader.cjs.map +1 -1
  127. package/dist/fixture-loader.d.cts +37 -5
  128. package/dist/fixture-loader.d.cts.map +1 -1
  129. package/dist/fixture-loader.d.ts +37 -5
  130. package/dist/fixture-loader.d.ts.map +1 -1
  131. package/dist/fixture-loader.js +0 -0
  132. package/dist/fixture-loader.js.map +1 -1
  133. package/dist/gemini-embeddings.cjs +2 -2
  134. package/dist/gemini-embeddings.cjs.map +1 -1
  135. package/dist/gemini-embeddings.js +3 -3
  136. package/dist/gemini-embeddings.js.map +1 -1
  137. package/dist/gemini-interactions.cjs +13 -7
  138. package/dist/gemini-interactions.cjs.map +1 -1
  139. package/dist/gemini-interactions.d.cts.map +1 -1
  140. package/dist/gemini-interactions.d.ts.map +1 -1
  141. package/dist/gemini-interactions.js +15 -9
  142. package/dist/gemini-interactions.js.map +1 -1
  143. package/dist/gemini.cjs +11 -7
  144. package/dist/gemini.cjs.map +1 -1
  145. package/dist/gemini.d.cts.map +1 -1
  146. package/dist/gemini.d.ts.map +1 -1
  147. package/dist/gemini.js +13 -9
  148. package/dist/gemini.js.map +1 -1
  149. package/dist/grok-video.cjs +14 -14
  150. package/dist/grok-video.cjs.map +1 -1
  151. package/dist/grok-video.js +15 -15
  152. package/dist/grok-video.js.map +1 -1
  153. package/dist/helpers.cjs +229 -7
  154. package/dist/helpers.cjs.map +1 -1
  155. package/dist/helpers.d.cts +38 -2
  156. package/dist/helpers.d.cts.map +1 -1
  157. package/dist/helpers.d.ts +38 -2
  158. package/dist/helpers.d.ts.map +1 -1
  159. package/dist/helpers.js +221 -8
  160. package/dist/helpers.js.map +1 -1
  161. package/dist/images.cjs +54 -123
  162. package/dist/images.cjs.map +1 -1
  163. package/dist/images.d.cts.map +1 -1
  164. package/dist/images.d.ts.map +1 -1
  165. package/dist/images.js +55 -124
  166. package/dist/images.js.map +1 -1
  167. package/dist/index.cjs +41 -8
  168. package/dist/index.d.cts +12 -7
  169. package/dist/index.d.ts +12 -7
  170. package/dist/index.js +14 -10
  171. package/dist/jest.cjs +14 -7
  172. package/dist/jest.cjs.map +1 -1
  173. package/dist/jest.d.cts +3 -2
  174. package/dist/jest.d.cts.map +1 -1
  175. package/dist/jest.d.ts +3 -2
  176. package/dist/jest.d.ts.map +1 -1
  177. package/dist/jest.js +14 -7
  178. package/dist/jest.js.map +1 -1
  179. package/dist/journal.cjs +41 -4
  180. package/dist/journal.cjs.map +1 -1
  181. package/dist/journal.d.cts +44 -3
  182. package/dist/journal.d.cts.map +1 -1
  183. package/dist/journal.d.ts +44 -3
  184. package/dist/journal.d.ts.map +1 -1
  185. package/dist/journal.js +41 -5
  186. package/dist/journal.js.map +1 -1
  187. package/dist/live-fixture.cjs +1192 -0
  188. package/dist/live-fixture.cjs.map +1 -0
  189. package/dist/live-fixture.js +1187 -0
  190. package/dist/live-fixture.js.map +1 -0
  191. package/dist/live-recorder.cjs +256 -0
  192. package/dist/live-recorder.cjs.map +1 -0
  193. package/dist/live-recorder.js +254 -0
  194. package/dist/live-recorder.js.map +1 -0
  195. package/dist/live-replay.cjs +286 -0
  196. package/dist/live-replay.cjs.map +1 -0
  197. package/dist/live-replay.js +285 -0
  198. package/dist/live-replay.js.map +1 -0
  199. package/dist/live-sanitize.cjs +196 -0
  200. package/dist/live-sanitize.cjs.map +1 -0
  201. package/dist/live-sanitize.js +196 -0
  202. package/dist/live-sanitize.js.map +1 -0
  203. package/dist/live-types.d.cts +71 -0
  204. package/dist/live-types.d.cts.map +1 -0
  205. package/dist/live-types.d.ts +71 -0
  206. package/dist/live-types.d.ts.map +1 -0
  207. package/dist/llmock.cjs +52 -41
  208. package/dist/llmock.cjs.map +1 -1
  209. package/dist/llmock.d.cts +16 -5
  210. package/dist/llmock.d.cts.map +1 -1
  211. package/dist/llmock.d.ts +16 -5
  212. package/dist/llmock.d.ts.map +1 -1
  213. package/dist/llmock.js +53 -42
  214. package/dist/llmock.js.map +1 -1
  215. package/dist/messages.cjs +11 -7
  216. package/dist/messages.cjs.map +1 -1
  217. package/dist/messages.d.cts.map +1 -1
  218. package/dist/messages.d.ts.map +1 -1
  219. package/dist/messages.js +13 -9
  220. package/dist/messages.js.map +1 -1
  221. package/dist/metrics.cjs +258 -5
  222. package/dist/metrics.cjs.map +1 -1
  223. package/dist/metrics.d.cts +3 -1
  224. package/dist/metrics.d.cts.map +1 -1
  225. package/dist/metrics.d.ts +3 -1
  226. package/dist/metrics.d.ts.map +1 -1
  227. package/dist/metrics.js +255 -6
  228. package/dist/metrics.js.map +1 -1
  229. package/dist/moderation.cjs +10 -1
  230. package/dist/moderation.cjs.map +1 -1
  231. package/dist/moderation.d.cts.map +1 -1
  232. package/dist/moderation.d.ts.map +1 -1
  233. package/dist/moderation.js +10 -1
  234. package/dist/moderation.js.map +1 -1
  235. package/dist/ollama.cjs +5 -5
  236. package/dist/ollama.cjs.map +1 -1
  237. package/dist/ollama.js +6 -6
  238. package/dist/ollama.js.map +1 -1
  239. package/dist/openrouter-video.cjs +5 -5
  240. package/dist/openrouter-video.cjs.map +1 -1
  241. package/dist/openrouter-video.js +6 -6
  242. package/dist/openrouter-video.js.map +1 -1
  243. package/dist/recorder.cjs +45 -9
  244. package/dist/recorder.cjs.map +1 -1
  245. package/dist/recorder.d.cts.map +1 -1
  246. package/dist/recorder.d.ts.map +1 -1
  247. package/dist/recorder.js +43 -11
  248. package/dist/recorder.js.map +1 -1
  249. package/dist/responses.cjs +4 -3
  250. package/dist/responses.cjs.map +1 -1
  251. package/dist/responses.js +5 -5
  252. package/dist/responses.js.map +1 -1
  253. package/dist/router.cjs +20 -33
  254. package/dist/router.cjs.map +1 -1
  255. package/dist/router.d.cts +4 -1
  256. package/dist/router.d.cts.map +1 -1
  257. package/dist/router.d.ts +4 -1
  258. package/dist/router.d.ts.map +1 -1
  259. package/dist/router.js +20 -34
  260. package/dist/router.js.map +1 -1
  261. package/dist/server.cjs +701 -625
  262. package/dist/server.cjs.map +1 -1
  263. package/dist/server.d.cts +1 -0
  264. package/dist/server.d.cts.map +1 -1
  265. package/dist/server.d.ts +1 -0
  266. package/dist/server.d.ts.map +1 -1
  267. package/dist/server.js +704 -628
  268. package/dist/server.js.map +1 -1
  269. package/dist/speech.cjs +2 -2
  270. package/dist/speech.cjs.map +1 -1
  271. package/dist/speech.js +3 -3
  272. package/dist/speech.js.map +1 -1
  273. package/dist/transcription.cjs +4 -4
  274. package/dist/transcription.cjs.map +1 -1
  275. package/dist/transcription.js +5 -5
  276. package/dist/transcription.js.map +1 -1
  277. package/dist/types.d.cts +119 -16
  278. package/dist/types.d.cts.map +1 -1
  279. package/dist/types.d.ts +119 -16
  280. package/dist/types.d.ts.map +1 -1
  281. package/dist/validate-cli.cjs +925 -0
  282. package/dist/validate-cli.cjs.map +1 -0
  283. package/dist/validate-cli.js +924 -0
  284. package/dist/validate-cli.js.map +1 -0
  285. package/dist/vector-types.d.cts.map +1 -1
  286. package/dist/vector-types.d.ts.map +1 -1
  287. package/dist/veo-video.cjs +13 -13
  288. package/dist/veo-video.cjs.map +1 -1
  289. package/dist/veo-video.js +14 -14
  290. package/dist/veo-video.js.map +1 -1
  291. package/dist/video.cjs +4 -4
  292. package/dist/video.cjs.map +1 -1
  293. package/dist/video.d.cts +1 -1
  294. package/dist/video.d.cts.map +1 -1
  295. package/dist/video.d.ts +1 -1
  296. package/dist/video.d.ts.map +1 -1
  297. package/dist/video.js +5 -5
  298. package/dist/video.js.map +1 -1
  299. package/dist/vitest.cjs +13 -6
  300. package/dist/vitest.cjs.map +1 -1
  301. package/dist/vitest.d.cts +2 -1
  302. package/dist/vitest.d.cts.map +1 -1
  303. package/dist/vitest.d.ts +2 -1
  304. package/dist/vitest.d.ts.map +1 -1
  305. package/dist/vitest.js +14 -7
  306. package/dist/vitest.js.map +1 -1
  307. package/dist/watcher.cjs +2 -1
  308. package/dist/watcher.cjs.map +1 -1
  309. package/dist/watcher.js +2 -1
  310. package/dist/watcher.js.map +1 -1
  311. package/dist/ws-framing.cjs +150 -35
  312. package/dist/ws-framing.cjs.map +1 -1
  313. package/dist/ws-framing.d.cts +18 -3
  314. package/dist/ws-framing.d.cts.map +1 -1
  315. package/dist/ws-framing.d.ts +18 -3
  316. package/dist/ws-framing.d.ts.map +1 -1
  317. package/dist/ws-framing.js +151 -36
  318. package/dist/ws-framing.js.map +1 -1
  319. package/dist/ws-gemini-live.cjs +2 -2
  320. package/dist/ws-gemini-live.d.ts +2 -2
  321. package/dist/ws-gemini-live.js +2 -2
  322. package/dist/ws-live.cjs +401 -0
  323. package/dist/ws-live.cjs.map +1 -0
  324. package/dist/ws-live.js +400 -0
  325. package/dist/ws-live.js.map +1 -0
  326. package/dist/ws-realtime.cjs +46 -2
  327. package/dist/ws-realtime.cjs.map +1 -1
  328. package/dist/ws-realtime.d.cts.map +1 -1
  329. package/dist/ws-realtime.d.ts +2 -2
  330. package/dist/ws-realtime.d.ts.map +1 -1
  331. package/dist/ws-realtime.js +46 -2
  332. package/dist/ws-realtime.js.map +1 -1
  333. package/dist/ws-responses.cjs +2 -2
  334. package/dist/ws-responses.js +2 -2
  335. package/dist/ws-upstream.cjs +118 -0
  336. package/dist/ws-upstream.cjs.map +1 -0
  337. package/dist/ws-upstream.js +115 -0
  338. package/dist/ws-upstream.js.map +1 -0
  339. package/fixtures/openai-live-client.json +390 -0
  340. package/fixtures/openai-live-managed.json +709 -0
  341. package/package.json +1 -1
package/dist/files.cjs ADDED
@@ -0,0 +1,1071 @@
1
+ const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
+ const require_helpers = require('./helpers.cjs');
3
+ const require_chaos = require('./chaos.cjs');
4
+ let node_crypto = require("node:crypto");
5
+
6
+ //#region src/files.ts
7
+ /**
8
+ * OpenAI Files API mock for aimock.
9
+ *
10
+ * Implements the subset of `POST /v1/files`, `GET /v1/files`,
11
+ * `GET /v1/files/{id}`, `GET /v1/files/{id}/content` and
12
+ * `DELETE /v1/files/{id}` that test harnesses need to exercise
13
+ * file-upload flows without touching the real API.
14
+ *
15
+ * Storage is in-memory and per-process (like the video job maps):
16
+ * - metadata lives in `fileStore`
17
+ * - raw bytes live in `fileContents` as `Buffer`s. Bytes are never
18
+ * round-tripped through a utf8 string, so binary uploads come back
19
+ * byte-for-byte identical.
20
+ * - `clearFileStore()` wipes both and is wired into the full reset path
21
+ * (`POST /__aimock/reset` + `LLMock.reset()`)
22
+ *
23
+ * The only memory bound here is per file: {@link FILES_MAX_BYTES} caps one
24
+ * upload. The file COUNT is deliberately unbounded, so total residency grows
25
+ * with the number of uploads until something calls `clearFileStore()` (or the
26
+ * process exits) — a suite that uploads in a loop without resetting will grow
27
+ * this map without limit. That is a real, documented bound, not an OOM defense.
28
+ *
29
+ * The job maps this module otherwise mirrors (fal-audio.ts,
30
+ * openrouter-video.ts, grok-video.ts, byteplus-video.ts) DO cap entry count and
31
+ * FIFO-evict the oldest, and that convention deliberately does NOT apply here.
32
+ * Those hold transient jobs that the real API also expires, so eviction
33
+ * reproduces real behaviour. A file is a durable object: the real API keeps it
34
+ * until the caller deletes it. Silently evicting one would make the mock answer
35
+ * `404 No such file` for a file the caller successfully uploaded and never
36
+ * deleted — an outcome the real API cannot produce, surfacing as an
37
+ * unexplainable flake in the suite under test. Unbounded growth is the honest
38
+ * failure mode; `POST /__aimock/reset` is the lever that reclaims it.
39
+ *
40
+ * Body handling accepts both shapes real SDKs send:
41
+ * - JSON `{ filename, purpose, content }` (test-friendly; `bytes` is an alias
42
+ * of `content`, and exactly one of the two must be present)
43
+ * - `multipart/form-data` with `purpose` + exactly one payload part named
44
+ * `file` (or its `content` alias), framed per RFC 2046 §5.1.1 so a payload
45
+ * containing the boundary token is stored whole rather than truncated at it
46
+ *
47
+ * Both parsers are strict: a malformed body is an OpenAI-shaped
48
+ * `400 invalid_request_error` naming the offending field or part. Nothing is
49
+ * `String()`-coerced and no missing field falls through to a fabricated
50
+ * default, because a mock that quietly stores `"[object Object]"` or a 0-byte
51
+ * file teaches the suite under test that a broken upload succeeded.
52
+ *
53
+ * Every branch journals with `service: "files"` so
54
+ * `GET /__aimock/journal?service=files` selects exactly this traffic, and with
55
+ * `source: "internal"` because — as with fine-tuning — no fixture or proxy
56
+ * ever serves this store. A successful upload journals a SYNTHETIC body
57
+ * describing it (`{ purpose, filename, bytes, content_type, sha256 }`) — never
58
+ * the uploaded octets — exactly as the transcription route journals its
59
+ * multipart audio; a `400` journals the error envelope it answered with, so
60
+ * the reason is readable from the journal; see {@link journalFiles}. Files also
61
+ * honors the inbound API-key boundary via the server dispatch (no bypass),
62
+ * and runs through the chaos gate so retry/backoff suites can inject
63
+ * 500s at the files surface too.
64
+ */
65
+ /**
66
+ * Purposes accepted on upload — `CreateFileRequest.purpose` in the official
67
+ * `openai/openai-openapi` spec (and the `FilePurpose` type in the SDK).
68
+ * `user_data` is what Responses-API file inputs use, so rejecting it broke the
69
+ * most current flow.
70
+ */
71
+ const CREATE_PURPOSES = new Set([
72
+ "assistants",
73
+ "batch",
74
+ "fine-tune",
75
+ "vision",
76
+ "user_data",
77
+ "evals"
78
+ ]);
79
+ /**
80
+ * Exposed for tests and for callers that want to check a value themselves:
81
+ * the enum this mock enforces on upload.
82
+ */
83
+ const FILE_CREATE_PURPOSES = [...CREATE_PURPOSES];
84
+ function invalidPurpose(purpose) {
85
+ return `Invalid purpose '${purpose}'. Expected one of: ${[...CREATE_PURPOSES].join(", ")}`;
86
+ }
87
+ /**
88
+ * `Content-Transfer-Encoding` values that are identity transforms (RFC 2045
89
+ * §6.2): the part body already *is* the file's octets, so storing it verbatim
90
+ * is correct. Everything else (`base64`, `quoted-printable`, …) would have to
91
+ * be decoded before storage, and is rejected instead — see
92
+ * {@link unsupportedPartEncoding}.
93
+ */
94
+ const IDENTITY_TRANSFER_ENCODINGS = new Set([
95
+ "7bit",
96
+ "8bit",
97
+ "binary",
98
+ "identity"
99
+ ]);
100
+ /**
101
+ * Reject, rather than silently mis-store, a file part that asks for a content
102
+ * encoding this mock does not apply.
103
+ *
104
+ * We deliberately do NOT implement a decoder. RFC 7578 §4.7 deprecates
105
+ * `Content-Transfer-Encoding` in `multipart/form-data` ("Senders SHOULD NOT
106
+ * generate any parts with a Content-Transfer-Encoding header field. Currently,
107
+ * no deployed implementations that send such bodies have been discovered"), and
108
+ * a capture of the clients that actually reach this endpoint — curl `-F`,
109
+ * python `requests`, python `httpx` (the OpenAI Python SDK's transport) and
110
+ * undici `FormData` (the OpenAI Node SDK's transport) — confirms none of them
111
+ * emits one. Storing an encoded body as-is is the harmful outcome: `bytes` and
112
+ * `GET /content` would report the *encoded* form and a hash comparison in a
113
+ * test would fail for a reason the caller cannot see. A loud 400 naming the
114
+ * header is the honest answer for a mock.
115
+ *
116
+ * `identity` counts as a no-op alongside the RFC 2045 §6.2 values: it is the
117
+ * registered `Content-Encoding` for "no transformation", and a client that
118
+ * spells it on `Content-Transfer-Encoding` means the same thing. Rejecting it
119
+ * named a problem that did not exist.
120
+ *
121
+ * Every part is checked, not only the payload part, and the message names the
122
+ * part it came from. While only the payload part was checked, a `base64`
123
+ * `purpose` part slid through to the enum check and 400'd with
124
+ * `Invalid purpose 'YXNzaXN0YW50cw=='`, pointing the caller at the wrong thing.
125
+ */
126
+ function unsupportedPartEncoding(header, value, partName) {
127
+ return `Unsupported ${header}: '${value}' on the multipart '${partName}' part. aimock stores part bodies verbatim and does not decode them.${header === "Content-Transfer-Encoding" ? ` RFC 7578 section 4.7 deprecates ${header} in multipart/form-data.` : ""} Send the raw bytes instead.`;
128
+ }
129
+ /**
130
+ * Content types for the handful of extensions test harnesses actually upload.
131
+ * Anything unrecognised falls back to `application/octet-stream` — the safe
132
+ * default for opaque bytes, and what stops a PNG being served (and re-decoded)
133
+ * as UTF-8 text.
134
+ *
135
+ * This is an allowlist of INERT types on purpose. `text/html` is deliberately
136
+ * absent: the mock serves uploaded bytes back from its own origin, so any
137
+ * active type here would let a stored upload script that origin in a browser.
138
+ * HTML uploads round-trip byte-for-byte, they are just served as opaque bytes.
139
+ */
140
+ const CONTENT_TYPE_BY_EXT = {
141
+ json: "application/json",
142
+ jsonl: "application/jsonl",
143
+ txt: "text/plain; charset=utf-8",
144
+ md: "text/markdown; charset=utf-8",
145
+ csv: "text/csv; charset=utf-8",
146
+ pdf: "application/pdf",
147
+ png: "image/png",
148
+ jpg: "image/jpeg",
149
+ jpeg: "image/jpeg",
150
+ gif: "image/gif",
151
+ webp: "image/webp",
152
+ wav: "audio/wav",
153
+ mp3: "audio/mpeg",
154
+ zip: "application/zip"
155
+ };
156
+ function contentTypeForFilename(filename) {
157
+ const dot = filename.lastIndexOf(".");
158
+ if (dot === -1) return "application/octet-stream";
159
+ return CONTENT_TYPE_BY_EXT[filename.slice(dot + 1).toLowerCase()] ?? "application/octet-stream";
160
+ }
161
+ /**
162
+ * RFC 5987 §3.2.1 `ext-value` percent-encoding for the `filename*` parameter.
163
+ *
164
+ * `encodeURIComponent` leaves `!'()*-._~` unescaped, but `'`, `(`, `)` and `*`
165
+ * are NOT in RFC 5987's `attr-char` set, so they have to be escaped by hand.
166
+ */
167
+ function encodeExtValue(value) {
168
+ return encodeURIComponent(value).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
169
+ }
170
+ /**
171
+ * `Content-Disposition` for the content route (RFC 6266 §4.1).
172
+ *
173
+ * Always `attachment`. {@link CONTENT_TYPE_BY_EXT} is an allowlist of INERT
174
+ * types precisely so a stored upload cannot execute against the mock's own
175
+ * origin, but `application/pdf` was still being served with no disposition at
176
+ * all, which in every mainstream browser means "render in the built-in PDF
177
+ * viewer" — an active surface on this origin, exactly what the allowlist's
178
+ * comment claims is excluded. `attachment` closes that without touching the
179
+ * allowlist, and it also stops a `.json`/`.txt`/`.csv` body being rendered
180
+ * inline if someone opens a content URL in a tab.
181
+ *
182
+ * What the REAL API sends here is UNVERIFIED: probing
183
+ * `api.openai.com/v1/files/{id}/content` needs a key, and neither
184
+ * `openai/openai-openapi` nor the SDK documents the response headers. So this
185
+ * is chosen for safety, not fidelity — and it is inert for SDK callers: the
186
+ * vendored SDK's `files.content()` (openai@4.104.0,
187
+ * `resources/files.js:82-88`) issues the GET with `Accept: application/binary`
188
+ * and `__binaryResponse: true`, handing back the raw `Response` without ever
189
+ * reading `Content-Disposition`.
190
+ *
191
+ * Both parameters are built from the STORED filename, which is attacker-shaped
192
+ * input: a multipart `filename="..."` can carry CR/LF and would otherwise
193
+ * inject response headers. The ASCII fallback replaces every non-printable and
194
+ * non-ASCII byte with `_` and backslash-escapes `"` and `\` per the
195
+ * quoted-string rule; `filename*` percent-encodes them. Neither can emit a bare
196
+ * CR or LF.
197
+ */
198
+ function contentDispositionFor(filename) {
199
+ const base = `attachment; filename="${filename.replace(/[^\x20-\x7e]/g, "_").replace(/(["\\])/g, "\\$1")}"`;
200
+ if (/^[\x20-\x7e]*$/.test(filename)) return base;
201
+ return `${base}; filename*=UTF-8''${encodeExtValue(filename)}`;
202
+ }
203
+ /**
204
+ * Per-file content cap. An upload whose decoded content is above this gets a
205
+ * `400` with the usual OpenAI-style error envelope, at every wire size a body
206
+ * can have — see {@link FILES_BODY_MAX_BYTES} for how that is kept true
207
+ * without buffering an unbounded body.
208
+ */
209
+ const FILES_MAX_BYTES = 10 * 1024 * 1024;
210
+ /**
211
+ * Worst-case wire-to-content expansion of a JSON upload.
212
+ *
213
+ * `content` is a JSON string, so one byte of decoded payload can arrive as as
214
+ * many as six wire bytes: a `\uXXXX` escape is 6 bytes and the cheapest code
215
+ * point one can carry (U+0000–U+007F) is a single UTF-8 byte. Every other
216
+ * code point does better — U+0800–U+FFFF is 3 UTF-8 bytes per 6 wire bytes,
217
+ * a surrogate pair 4 per 12 — and a multipart part is 1:1 plus its envelope.
218
+ * So six is an upper bound on wire → content expansion for both parsers.
219
+ */
220
+ const FILES_MAX_WIRE_EXPANSION = 6;
221
+ /** Slack for the JSON envelope, or for multipart boundaries and part headers. */
222
+ const FILES_ENVELOPE_HEADROOM_BYTES = 64 * 1024;
223
+ /**
224
+ * How much of a Files upload body the route will BUFFER — derived, not picked.
225
+ *
226
+ * `readBodyBuffer` enforces a limit by destroying the socket: no status line,
227
+ * no body, no CORS headers, just an `ECONNRESET`. That made the `400 File
228
+ * content exceeds ... byte cap` branches below unreachable when the two caps
229
+ * were equal (both the 10 MB `DEFAULT_MAX_BODY_BYTES`), and a fixed slice of
230
+ * headroom above the cap only moved the cliff: a `\u0001`-heavy JSON body or a
231
+ * multipart body far enough over the cap still died on the socket.
232
+ *
233
+ * So the bound is computed from the widest a within-cap payload can be on the
234
+ * wire ({@link FILES_MAX_WIRE_EXPANSION} × the cap, plus envelope headroom).
235
+ * Two consequences, and they are the contract:
236
+ *
237
+ * - No body that could decode to a payload within the cap is ever refused for
238
+ * its size — it fits here by construction, and is answered on content.
239
+ * - A body larger than this is not buffered. The route stops retaining bytes
240
+ * (peak memory stays at this bound), drains the rest, and answers a real
241
+ * `400` with CORS. It is not a socket drop, and it cannot be a false
242
+ * rejection either, by the point above.
243
+ *
244
+ * The global `readBodyBuffer` default is deliberately left alone — every other
245
+ * route depends on it.
246
+ */
247
+ const FILES_BODY_MAX_BYTES = FILES_MAX_BYTES * FILES_MAX_WIRE_EXPANSION + FILES_ENVELOPE_HEADROOM_BYTES;
248
+ /**
249
+ * The one documented socket drop on this route: a DoS backstop.
250
+ *
251
+ * Draining an over-size body is what buys the caller a real `400`, but the
252
+ * draining itself is work an attacker can ask for without limit. Past this
253
+ * the request is simply not worth finishing and the socket is destroyed, the
254
+ * same way every other route treats its own limit. It is deliberately far
255
+ * above {@link FILES_BODY_MAX_BYTES}: a client that lands here is not sending
256
+ * a file that was ever going to be accepted.
257
+ */
258
+ const FILES_BODY_DRAIN_MAX_BYTES = FILES_BODY_MAX_BYTES * 2;
259
+ /**
260
+ * Stands in for an upload body that went past {@link FILES_BODY_MAX_BYTES} and
261
+ * was therefore drained instead of buffered. The route hands this to
262
+ * {@link handleFilesCreate} in place of the bytes, so the over-size answer is
263
+ * the same `400` envelope — journalled, CORS-headed, chaos-gated — as every
264
+ * other rejection here, rather than a second error path in the server.
265
+ */
266
+ const FILES_BODY_OVERSIZED = Symbol("files-body-oversized");
267
+ /** The `400` for a body too large to be a within-cap upload. */
268
+ function oversizedBody() {
269
+ return `Request body exceeds the ${FILES_BODY_MAX_BYTES} byte upload limit for this route (the ${FILES_MAX_BYTES} byte content cap plus worst-case wire encoding overhead)`;
270
+ }
271
+ const fileStore = /* @__PURE__ */ new Map();
272
+ const fileContents = /* @__PURE__ */ new Map();
273
+ function clearFileStore() {
274
+ fileStore.clear();
275
+ fileContents.clear();
276
+ }
277
+ /**
278
+ * The stored bytes for one file, as the very `Buffer` this module holds — so a
279
+ * test can assert on what it RETAINS (`buffer.byteLength`), not just on what it
280
+ * serves back. `GET /v1/files/{id}/content` copies onto the wire, so it cannot
281
+ * tell an owned buffer from a `subarray` pinning a 60 MB request body.
282
+ */
283
+ function getStoredFileBytes(id) {
284
+ return fileContents.get(id)?.bytes;
285
+ }
286
+ /**
287
+ * Register one file in the store and return its wire object. This is the one
288
+ * insert path: `POST /v1/files` uses it, and so do server-side producers of
289
+ * files (the Batches mock minting `output_file_id` / `error_file_id`), so a
290
+ * file minted internally is retrievable through `GET /v1/files/{id}` and
291
+ * `/content` exactly like an upload. `content` must be an OWNED buffer (see
292
+ * {@link ParsedUpload.content}) — it is stored as-is, and it must already be
293
+ * within {@link FILES_MAX_BYTES}: the cap the upload route enforces holds for
294
+ * every file in the store, so a producer that cannot fit under it fails its
295
+ * own operation instead of storing the file.
296
+ */
297
+ function storeFile(file) {
298
+ if (file.content.length > FILES_MAX_BYTES) throw new RangeError(`File content exceeds ${FILES_MAX_BYTES} byte cap`);
299
+ const id = require_helpers.generateId("file");
300
+ const obj = {
301
+ id,
302
+ object: "file",
303
+ bytes: file.content.length,
304
+ created_at: Math.floor(Date.now() / 1e3),
305
+ filename: file.filename,
306
+ purpose: file.purpose,
307
+ status: "processed"
308
+ };
309
+ fileStore.set(id, obj);
310
+ fileContents.set(id, { bytes: file.content });
311
+ return obj;
312
+ }
313
+ /**
314
+ * Synthetic journal body for an upload — the SAME convention
315
+ * {@link handleTranscription} uses for `multipart/form-data` audio: the entry
316
+ * carries a small object describing the upload that was parsed out of the
317
+ * body, never the body itself. Raw file octets are NEVER journalled, in any
318
+ * encoding: they can be megabytes of binary, they would blow the journal's
319
+ * 64 KB body cap on anything real, and a base64 round-trip of them is not
320
+ * something a test wants to diff. `sha256` is the byte identity a caller
321
+ * asserts on instead.
322
+ *
323
+ * `content_type` is derived from the stored filename via
324
+ * {@link contentTypeForFilename} — the same decision `GET /v1/files/{id}/content`
325
+ * serves the bytes back with — rather than from the client-declared part
326
+ * header, which this mock deliberately ignores everywhere else.
327
+ *
328
+ * `model`/`messages` are the inert `ChatCompletionRequest` floor the journal's
329
+ * body type requires (the same `{ model: "", messages: [] }` the server uses
330
+ * for its own bodyless synthetic entries); `_endpointType` names the surface.
331
+ */
332
+ function filesUploadBody(upload) {
333
+ return {
334
+ model: "",
335
+ messages: [],
336
+ _endpointType: "files",
337
+ purpose: upload.purpose,
338
+ filename: upload.filename,
339
+ bytes: upload.content.length,
340
+ content_type: contentTypeForFilename(upload.filename),
341
+ sha256: (0, node_crypto.createHash)("sha256").update(upload.content).digest("hex")
342
+ };
343
+ }
344
+ /**
345
+ * Journal one files-route request.
346
+ *
347
+ * The rule for `body`: an entry carries the {@link filesUploadBody} descriptor
348
+ * exactly when a request had an upload payload that parsed — i.e. a successful
349
+ * `POST /v1/files` — and the {@link invalidRequest} envelope exactly when the
350
+ * request was rejected with a `400` (see {@link rejectFiles}), so the reason
351
+ * is readable from the journal without the wire response. Every other files
352
+ * entry (GET one, GET content, list, DELETE, a chaos-faulted request, a 404)
353
+ * has no payload to describe and stays `null`, matching the bodyless routes in
354
+ * {@link handleTranscription}. Raw uploaded bytes never appear.
355
+ *
356
+ * `service: "files"` is set on every branch so
357
+ * `GET /__aimock/journal?service=files` selects exactly this traffic, and
358
+ * `source: "internal"` because this store is aimock's own — the same tag every
359
+ * fine-tuning entry carries, success and error alike.
360
+ *
361
+ * Call it AFTER the response has been written: a `writeHead` that throws must
362
+ * not leave behind an entry for a response the client never received.
363
+ */
364
+ function journalFiles(journal, method, path, headers, status, body = null) {
365
+ journal.add({
366
+ method,
367
+ path,
368
+ headers,
369
+ body,
370
+ service: "files",
371
+ response: {
372
+ status,
373
+ fixture: null,
374
+ source: "internal"
375
+ }
376
+ });
377
+ }
378
+ /** Keep the entire warning bounded and on one physical line, including the URL. */
379
+ function filesRejectionWarning(message) {
380
+ const maxLength = 1024;
381
+ let safe = "";
382
+ for (const character of message) {
383
+ const code = character.charCodeAt(0);
384
+ const escaped = code < 32 || code >= 127 && code <= 159 || code === 8232 || code === 8233 ? `\\u${code.toString(16).padStart(4, "0")}` : character;
385
+ if (safe.length + escaped.length > maxLength - 3) return `${safe}...`;
386
+ safe += escaped;
387
+ }
388
+ return safe;
389
+ }
390
+ /**
391
+ * Answer a files-route `400`: log it at `warn` (the level the server uses for
392
+ * a request it refused), journal the {@link invalidRequest} envelope as the
393
+ * entry's body so the reason survives in the journal, and write that same
394
+ * envelope to the wire. One helper so the three can never disagree.
395
+ */
396
+ function rejectFiles(res, journal, logger, method, path, headers, message, setCorsHeaders) {
397
+ const envelope = invalidRequest(message);
398
+ logger.warn(filesRejectionWarning(`Files mock: rejected ${method} ${path} with 400: ${message}`));
399
+ writeJson(res, 400, envelope, setCorsHeaders);
400
+ journalFiles(journal, method, path, headers, 400, envelope);
401
+ }
402
+ function writeJson(res, status, payload, setCorsHeaders) {
403
+ setCorsHeaders(res);
404
+ res.writeHead(status, { "Content-Type": "application/json" });
405
+ res.end(JSON.stringify(payload));
406
+ }
407
+ function invalidRequest(message) {
408
+ return { error: {
409
+ message,
410
+ type: "invalid_request_error"
411
+ } };
412
+ }
413
+ /**
414
+ * Name a rejected value's JSON type for the 400 message. The point is that the
415
+ * caller sees *what* they sent instead of silently getting `String(value)`
416
+ * stored as the file's content.
417
+ *
418
+ * The listed cases are exhaustive for the one caller. `JSON.parse` only ever
419
+ * yields null, boolean, number, string, array or plain object — never
420
+ * `undefined`, `function`, `symbol` or `bigint` — and the caller has already
421
+ * excluded `string` (it is the accepted type) and `undefined` (that is the
422
+ * separate "required" 400). So everything reaching the last line is an object.
423
+ */
424
+ function jsonTypeOf(value) {
425
+ if (value === null) return "null";
426
+ if (Array.isArray(value)) return "an array";
427
+ if (typeof value === "number") return "a number";
428
+ if (typeof value === "boolean") return "a boolean";
429
+ return "an object";
430
+ }
431
+ /**
432
+ * A high surrogate not followed by a low one, or a low surrogate not preceded
433
+ * by a high one — i.e. a JS string that is not well-formed UTF-16 and therefore
434
+ * has no UTF-8 encoding.
435
+ *
436
+ * `Buffer.from(s, "utf8")` does not fail on these: it substitutes U+FFFD (EF BF
437
+ * BD), silently and permanently. That breaks the one guarantee an upload mock
438
+ * owes its caller — `GET /v1/files/{id}/content` returns what was uploaded — and
439
+ * it breaks it invisibly, three bytes at a time, in a test whose hash
440
+ * comparison then fails for no visible reason. The byte-for-byte promise this
441
+ * module documents is made for the multipart path (which is scanned as latin1
442
+ * and never decoded); the JSON path carries *text*, so text that cannot be
443
+ * encoded is a 400, not a lossy store.
444
+ */
445
+ const LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
446
+ /**
447
+ * Longest `filename` this mock will store, in UTF-8 bytes.
448
+ *
449
+ * NOT the real API's limit: `openai/openai-openapi` gives `OpenAIFile.filename`
450
+ * as a bare `type: string` with no `maxLength`, and `CreateFileRequest` has no
451
+ * `filename` property at all. So the bound comes from the one thing here a
452
+ * filename can break: `GET /v1/files/{id}/content` echoes the stored name into
453
+ * `Content-Disposition` twice — quoted ASCII (where `"` and `\` double) and
454
+ * percent-encoded per RFC 5987 (where a byte can triple) — so ~5 bytes of
455
+ * header per byte of name. 1024 bytes of name is ~5 KiB of header, well inside
456
+ * Node's 16 KiB default `--max-http-header-size`.
457
+ *
458
+ * Unbounded, a 20 KB filename uploaded `200` and then made the content route
459
+ * unreadable by undici: `UND_ERR_HEADERS_OVERFLOW`, no status, no envelope, no
460
+ * CORS — the OpenAI SDK could not fetch a file it had just uploaded.
461
+ */
462
+ const FILES_MAX_FILENAME_BYTES = 1024;
463
+ /**
464
+ * The filename rules, in one place because both parsers must answer the same.
465
+ *
466
+ * They used to disagree: a name carrying NUL, CR or LF was a `400` as a
467
+ * multipart parameter and a `200` as a JSON field, stored and echoed back
468
+ * verbatim. Returns an error message, or `undefined` if the name is fine.
469
+ */
470
+ function filenameError(filename) {
471
+ if (CONTROL_CHARS.test(filename)) return "Invalid parameter: 'filename' must not contain control characters";
472
+ if (LONE_SURROGATE_RE.test(filename)) return "Invalid parameter: 'filename' must not contain an unpaired UTF-16 surrogate";
473
+ const byteLength = Buffer.byteLength(filename, "utf8");
474
+ if (byteLength > FILES_MAX_FILENAME_BYTES) return `Invalid parameter: 'filename' is ${byteLength} UTF-8 bytes, above the ${FILES_MAX_FILENAME_BYTES} byte cap (a longer name makes GET /v1/files/{id}/content unfetchable: its Content-Disposition header would exceed the HTTP header limit)`;
475
+ }
476
+ /** The deterministic name given to an upload that arrived without one. */
477
+ function synthesizedFilename(content) {
478
+ return `upload-${(0, node_crypto.createHash)("sha256").update(content).digest("hex").slice(0, 24)}.bin`;
479
+ }
480
+ function parseJsonUpload(raw) {
481
+ let text;
482
+ try {
483
+ text = new TextDecoder("utf-8", { fatal: true }).decode(raw);
484
+ } catch {
485
+ return { error: "Invalid request: body is not valid UTF-8" };
486
+ }
487
+ let body;
488
+ try {
489
+ body = JSON.parse(text);
490
+ } catch (err) {
491
+ return { error: `Malformed JSON: ${err instanceof Error ? err.message : "unknown"}` };
492
+ }
493
+ if (!require_helpers.isJsonObject(body)) return { error: "Request body must be a JSON object" };
494
+ const rawFilename = body["filename"];
495
+ const purpose = body["purpose"];
496
+ if (typeof rawFilename !== "string") return { error: "Invalid parameter: 'filename' must be a string" };
497
+ const filenameProblem = filenameError(rawFilename);
498
+ if (filenameProblem !== void 0) return { error: filenameProblem };
499
+ const filename = rawFilename.trim() === "" ? void 0 : rawFilename;
500
+ if (typeof purpose !== "string" || purpose.length === 0) return { error: "Invalid parameter: 'purpose' must be a non-empty string" };
501
+ if (!CREATE_PURPOSES.has(purpose)) return { error: invalidPurpose(purpose) };
502
+ const hasContent = body["content"] !== void 0;
503
+ const hasBytesAlias = body["bytes"] !== void 0;
504
+ if (hasContent && hasBytesAlias) return { error: "Invalid parameter: 'content' and its alias 'bytes' are mutually exclusive; send exactly one" };
505
+ const field = hasContent ? "content" : "bytes";
506
+ const content = hasContent ? body["content"] : body["bytes"];
507
+ if (content === void 0) return { error: "Invalid parameter: 'content' is required (or its alias 'bytes')" };
508
+ if (typeof content !== "string") return { error: `Invalid parameter: '${field}' must be a string, got ${jsonTypeOf(content)}` };
509
+ if (LONE_SURROGATE_RE.test(content)) return { error: `Invalid parameter: '${field}' contains an unpaired UTF-16 surrogate and is not valid text. aimock would have to store it as U+FFFD, so the bytes served back by GET /v1/files/{id}/content would not be the bytes you sent. Send binary as a multipart/form-data upload, which is stored verbatim.` };
510
+ const bytes = Buffer.from(content, "utf8");
511
+ if (bytes.length > FILES_MAX_BYTES) return { error: `File content exceeds ${FILES_MAX_BYTES} byte cap` };
512
+ return {
513
+ filename: filename ?? synthesizedFilename(bytes),
514
+ purpose,
515
+ content: bytes
516
+ };
517
+ }
518
+ const HEADER_TERMINATOR = Buffer.from("\r\n\r\n");
519
+ /** RFC 9110 `token` characters — the legal set for a header field name. */
520
+ const TOKEN_CHAR = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
521
+ /** C0 controls plus DEL — never legitimate in a filename we echo back. */
522
+ const CONTROL_CHARS = /[-]/;
523
+ /**
524
+ * Is this request body `multipart/form-data`?
525
+ *
526
+ * Compares the media type's *essence* (RFC 9110 §8.3.1: everything before the
527
+ * first `;`), not a substring of the whole header. A substring test routed
528
+ * `application/json; note="multipart/form-data"` — and anything else that
529
+ * merely mentions the token in a parameter — into the multipart parser, which
530
+ * then 400'd about a missing `boundary` directive on a perfectly good JSON
531
+ * body.
532
+ */
533
+ function isMultipartFormData(contentType) {
534
+ if (contentType === void 0) return false;
535
+ const semi = contentType.indexOf(";");
536
+ return (semi === -1 ? contentType : contentType.slice(0, semi)).trim().toLowerCase() === "multipart/form-data";
537
+ }
538
+ /**
539
+ * Locate the next *boundary delimiter line* at or after `from`.
540
+ *
541
+ * RFC 2046 §5.1.1 defines the delimiter as `CRLF "--" boundary` followed by
542
+ * either transport-padding + CRLF (another part follows) or `--` (the close
543
+ * delimiter). Both halves matter:
544
+ *
545
+ * - The **leading CRLF is part of the delimiter**, not of the preceding body.
546
+ * The previous implementation split on a bare `--boundary`, so a payload
547
+ * whose bytes happened to contain that sequence was truncated at it and
548
+ * stored short — with a 200, silently, which is the worst possible answer
549
+ * from a module whose contract is byte-for-byte storage.
550
+ * - An occurrence **not followed by** CRLF or `--` is not a delimiter line at
551
+ * all, so it is ordinary body content and the scan continues past it. That
552
+ * is what keeps a payload containing `CRLF--boundary` plus arbitrary bytes
553
+ * byte-exact. (undici's `Response.formData()` rejects such a body outright;
554
+ * see the note on {@link parseMultipartUpload}.)
555
+ */
556
+ function nextDelimiter(buf, delimiter, from) {
557
+ let at = buf.indexOf(delimiter, from);
558
+ while (at !== -1) {
559
+ const tail = delimiterTail(buf, at + delimiter.length);
560
+ if (tail) return {
561
+ at,
562
+ ...tail
563
+ };
564
+ at = buf.indexOf(delimiter, at + 1);
565
+ }
566
+ }
567
+ /**
568
+ * Classify what follows a `CRLF "--" boundary` match: transport-padding + CRLF
569
+ * (another part follows), `--` (the close delimiter), or neither — in which
570
+ * case the match is not a delimiter line at all and the caller keeps scanning.
571
+ * Split out of {@link nextDelimiter} so {@link parseMultipartUpload} can apply
572
+ * the same rule to an opening delimiter whose leading CRLF is not present.
573
+ */
574
+ function delimiterTail(buf, after) {
575
+ if (buf[after] === 45 && buf[after + 1] === 45) return {
576
+ next: after + 2,
577
+ closing: true
578
+ };
579
+ let j = after;
580
+ while (buf[j] === 32 || buf[j] === 9) j += 1;
581
+ if (buf[j] === 13 && buf[j + 1] === 10) return {
582
+ next: j + 2,
583
+ closing: false
584
+ };
585
+ }
586
+ function parseHeaderParams(value) {
587
+ const params = /* @__PURE__ */ new Map();
588
+ const semi = value.indexOf(";");
589
+ const head = (semi === -1 ? value : value.slice(0, semi)).trim();
590
+ let i = semi === -1 ? value.length : semi;
591
+ while (i < value.length) {
592
+ i += 1;
593
+ while (value[i] === " " || value[i] === " ") i += 1;
594
+ if (i >= value.length) break;
595
+ const eq = value.indexOf("=", i);
596
+ if (eq === -1) return MALFORMED_HEADER;
597
+ const attr = value.slice(i, eq).trim().toLowerCase();
598
+ if (!TOKEN_CHAR.test(attr)) return MALFORMED_HEADER;
599
+ if (params.has(attr)) return {
600
+ error: "duplicate",
601
+ param: attr
602
+ };
603
+ i = eq + 1;
604
+ if (value[i] === "\"") {
605
+ i += 1;
606
+ let out = "";
607
+ for (;;) {
608
+ if (i >= value.length) return MALFORMED_HEADER;
609
+ const ch = value[i];
610
+ if (ch === "\\" && i + 1 < value.length) {
611
+ out += value[i + 1];
612
+ i += 2;
613
+ continue;
614
+ }
615
+ if (ch === "\"") {
616
+ i += 1;
617
+ break;
618
+ }
619
+ out += ch;
620
+ i += 1;
621
+ }
622
+ params.set(attr, out);
623
+ while (value[i] === " " || value[i] === " ") i += 1;
624
+ if (i < value.length && value[i] !== ";") return MALFORMED_HEADER;
625
+ } else {
626
+ const end = value.indexOf(";", i);
627
+ params.set(attr, (end === -1 ? value.slice(i) : value.slice(i, end)).trim());
628
+ i = end === -1 ? value.length : end;
629
+ }
630
+ }
631
+ return {
632
+ value: head,
633
+ params
634
+ };
635
+ }
636
+ const MALFORMED_HEADER = { error: "malformed" };
637
+ /** Render a {@link parseHeaderParams} failure as a 400 message naming the header. */
638
+ function headerParamError(err, header, where) {
639
+ return err.error === "duplicate" ? `Invalid request: duplicate '${err.param}' parameter on the ${header} header in ${where}` : `Invalid request: malformed ${header} header in ${where}`;
640
+ }
641
+ /**
642
+ * Decode an RFC 5987 `ext-value` (`charset'language'pct-encoded`), the form a
643
+ * `filename*` parameter takes. RFC 6266 §4.3 says a recipient that understands
644
+ * it SHOULD prefer it over the plain `filename`, so {@link parsePart} does.
645
+ * A client that sends only `filename*=` therefore keeps the name it sent
646
+ * instead of receiving a synthesized one.
647
+ */
648
+ function decodeExtValue(raw) {
649
+ const first = raw.indexOf("'");
650
+ const second = raw.indexOf("'", first + 1);
651
+ if (first === -1 || second === -1) return void 0;
652
+ const charset = raw.slice(0, first).toLowerCase();
653
+ if (charset !== "utf-8" && charset !== "iso-8859-1") return void 0;
654
+ const octets = [];
655
+ const pct = raw.slice(second + 1);
656
+ for (let i = 0; i < pct.length; i += 1) if (pct[i] === "%") {
657
+ const hex = pct.slice(i + 1, i + 3);
658
+ if (!/^[0-9A-Fa-f]{2}$/.test(hex)) return void 0;
659
+ octets.push(parseInt(hex, 16));
660
+ i += 2;
661
+ } else {
662
+ const code = pct.charCodeAt(i);
663
+ if (code > 127) return void 0;
664
+ octets.push(code);
665
+ }
666
+ const bytes = Buffer.from(octets);
667
+ if (charset === "iso-8859-1") return bytes.toString("latin1");
668
+ try {
669
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
670
+ } catch {
671
+ return;
672
+ }
673
+ }
674
+ /** Turn one part's raw octets into headers + body, or name why it is malformed. */
675
+ function parsePart(partBytes, index) {
676
+ const where = `multipart part #${index + 1}`;
677
+ let headerText;
678
+ let bodyStart;
679
+ if (partBytes[0] === 13 && partBytes[1] === 10) {
680
+ headerText = "";
681
+ bodyStart = 2;
682
+ } else {
683
+ const end = partBytes.indexOf(HEADER_TERMINATOR);
684
+ if (end === -1) return { error: `Invalid request: malformed ${where} — its headers are not terminated by a blank line` };
685
+ headerText = partBytes.subarray(0, end).toString("utf8");
686
+ bodyStart = end + 4;
687
+ }
688
+ const headers = /* @__PURE__ */ new Map();
689
+ if (headerText !== "") for (const line of headerText.split("\r\n")) {
690
+ const colon = line.indexOf(":");
691
+ const fieldName = colon === -1 ? "" : line.slice(0, colon);
692
+ if (!TOKEN_CHAR.test(fieldName)) return { error: `Invalid request: malformed header line in ${where}` };
693
+ const field = fieldName.toLowerCase();
694
+ if (headers.has(field)) return { error: `Invalid request: duplicate '${fieldName.trim()}' header in ${where}` };
695
+ headers.set(field, line.slice(colon + 1).trim());
696
+ }
697
+ const disposition = headers.get("content-disposition");
698
+ if (disposition === void 0) return { error: `Invalid request: ${where} has no Content-Disposition header` };
699
+ const parsed = parseHeaderParams(disposition);
700
+ if ("error" in parsed) return { error: headerParamError(parsed, "Content-Disposition", where) };
701
+ if (parsed.value.toLowerCase() !== "form-data") return { error: `Invalid request: ${where} has Content-Disposition type '${parsed.value}'; RFC 7578 section 4.2 requires 'form-data'` };
702
+ const name = parsed.params.get("name");
703
+ if (name === void 0) return { error: `Invalid request: ${where} has no 'name' parameter on its Content-Disposition header` };
704
+ const star = parsed.params.get("filename*");
705
+ const filename = star !== void 0 ? decodeExtValue(star) : parsed.params.get("filename");
706
+ if (star !== void 0 && filename === void 0) return { error: `Invalid request: malformed 'filename*' parameter in ${where}` };
707
+ return {
708
+ name,
709
+ filename,
710
+ headers,
711
+ body: partBytes.subarray(bodyStart)
712
+ };
713
+ }
714
+ /**
715
+ * Read a part's content encoding header. An empty value reads as absent: the
716
+ * old regex used `\s*` after the colon, which matches a CRLF, so an empty
717
+ * `Content-Transfer-Encoding:` captured the *next* header line and 400'd
718
+ * naming `content-type: image/png` as the unsupported encoding.
719
+ */
720
+ function partEncoding(part, header) {
721
+ const value = part.headers.get(header)?.trim().toLowerCase();
722
+ return value === void 0 || value === "" ? void 0 : value;
723
+ }
724
+ /**
725
+ * Parse a `multipart/form-data` upload body.
726
+ *
727
+ * ## Why this is not `Response.formData()`
728
+ *
729
+ * Node's built-in (undici) `Response.formData()` is a spec-compliant multipart
730
+ * parser and was evaluated first, per the "use the platform, do not hand-roll
731
+ * a codec" rule. It fails three of this module's requirements:
732
+ *
733
+ * 1. **Byte fidelity.** It throws `expected CRLF` on any body whose payload
734
+ * contains the boundary token, including the RFC-legal bare `--boundary`
735
+ * with no preceding CRLF. Those bytes are storable and this module promises
736
+ * to store them.
737
+ * 2. **Filename-less payload parts.** A part with no `filename` parameter comes
738
+ * back as a *string*, decoded as UTF-8, so binary octets are destroyed
739
+ * (`00 ff fe ...` becomes `00 efbfbd efbfbd ...`). Such a part is legal
740
+ * input here and already round-tripped byte-exact.
741
+ * 3. **Part headers.** `File` exposes only `name`, `type` and `size`. There is
742
+ * no way to see a part's `Content-Transfer-Encoding`, so the "reject an
743
+ * encoding we do not apply" contract above could not be implemented at all
744
+ * and a `base64` part would be stored in its encoded form — the exact bug
745
+ * that contract exists to prevent.
746
+ *
747
+ * So the framing stays local, but it is written to RFC 2046 §5.1.1 and
748
+ * RFC 7578 rather than to a `split()`: see {@link nextDelimiter} for the
749
+ * delimiter rule and {@link parsePart} for the header rule.
750
+ *
751
+ * Bytes never round-trip through a string. Part bodies are `subarray`s of the
752
+ * request buffer, so what is parsed is literally what arrived on the wire — but
753
+ * the payload part is COPIED out of that view before it is returned, because a
754
+ * `subarray` retains its whole backing buffer (see the copy below). The scan
755
+ * itself never copies the body either, so peak residency during a parse is one
756
+ * request body, not two.
757
+ */
758
+ function parseMultipartUpload(rawBytes, contentType) {
759
+ const parsedType = parseHeaderParams(contentType ?? "");
760
+ if ("error" in parsedType) return { error: headerParamError(parsedType, "Content-Type", "the request") };
761
+ const boundary = parsedType.params.get("boundary");
762
+ if (!boundary) return { error: "Invalid request: multipart/form-data Content-Type is missing a 'boundary' directive" };
763
+ const buf = rawBytes;
764
+ const delimiter = Buffer.from(`\r\n--${boundary}`, "latin1");
765
+ const dashBoundary = Buffer.from(`--${boundary}`, "latin1");
766
+ const opening = buf.subarray(0, dashBoundary.length).equals(dashBoundary) ? delimiterTail(buf, dashBoundary.length) : void 0;
767
+ let delim = opening ? {
768
+ at: 0,
769
+ ...opening
770
+ } : nextDelimiter(buf, delimiter, 0);
771
+ if (!delim) return { error: "Invalid request: multipart body contains no boundary delimiter" };
772
+ const parts = [];
773
+ while (!delim.closing) {
774
+ const start = delim.next;
775
+ const following = nextDelimiter(buf, delimiter, start);
776
+ if (!following) return { error: "Invalid request: multipart body ended before its closing boundary delimiter" };
777
+ const part = parsePart(buf.subarray(start, following.at), parts.length);
778
+ if ("error" in part) return part;
779
+ parts.push(part);
780
+ delim = following;
781
+ }
782
+ for (const part of parts) {
783
+ const transfer = partEncoding(part, "content-transfer-encoding");
784
+ if (transfer !== void 0 && !IDENTITY_TRANSFER_ENCODINGS.has(transfer)) return { error: unsupportedPartEncoding("Content-Transfer-Encoding", transfer, part.name) };
785
+ const encoding = partEncoding(part, "content-encoding");
786
+ if (encoding !== void 0 && encoding !== "identity") return { error: unsupportedPartEncoding("Content-Encoding", encoding, part.name) };
787
+ }
788
+ const purposeParts = parts.filter((p) => p.name === "purpose");
789
+ if (purposeParts.length > 1) return { error: "Invalid request: multipart body has more than one 'purpose' part; send exactly one" };
790
+ const payloadParts = parts.filter((p) => p.name === "file" || p.name === "content");
791
+ if (payloadParts.length > 1) {
792
+ const [first, second] = payloadParts;
793
+ return { error: `Invalid request: multipart body has more than one payload part ('${first.name}' and '${second.name}'); send exactly one 'file' or 'content' part` };
794
+ }
795
+ const purpose = purposeParts[0]?.body.toString("utf8");
796
+ if (!purpose) return { error: "Invalid parameter: multipart 'purpose' field is required" };
797
+ if (!CREATE_PURPOSES.has(purpose)) return { error: invalidPurpose(purpose) };
798
+ const payload = payloadParts[0];
799
+ if (payload === void 0) return { error: "Invalid request: multipart body must include a 'file' part (or its 'content' alias)" };
800
+ if (payload.body.length > FILES_MAX_BYTES) return { error: `File content exceeds ${FILES_MAX_BYTES} byte cap` };
801
+ const content = Buffer.alloc(payload.body.length);
802
+ payload.body.copy(content);
803
+ let filename = payload.filename;
804
+ if (filename !== void 0) {
805
+ const problem = filenameError(filename);
806
+ if (problem !== void 0) return { error: problem };
807
+ }
808
+ if (filename !== void 0 && filename.trim() === "") filename = void 0;
809
+ if (filename === void 0) filename = synthesizedFilename(content);
810
+ return {
811
+ filename,
812
+ purpose,
813
+ content
814
+ };
815
+ }
816
+ async function handleFilesCreate(req, res, raw, journal, defaults, setCorsHeaders) {
817
+ const path = req.url ?? "/v1/files";
818
+ const method = req.method ?? "POST";
819
+ setCorsHeaders(res);
820
+ if (await require_chaos.applyChaosAsync(res, null, defaults.chaos, req.headers, req.url, journal, {
821
+ method,
822
+ path,
823
+ headers: require_helpers.flattenHeaders(req.headers),
824
+ body: null,
825
+ service: "files"
826
+ }, "internal", defaults.registry, defaults.logger)) return;
827
+ if (raw === FILES_BODY_OVERSIZED) {
828
+ rejectFiles(res, journal, defaults.logger, method, path, require_helpers.flattenHeaders(req.headers), oversizedBody(), setCorsHeaders);
829
+ return;
830
+ }
831
+ const contentType = Array.isArray(req.headers["content-type"]) ? req.headers["content-type"][0] : req.headers["content-type"];
832
+ const parsed = isMultipartFormData(contentType) ? parseMultipartUpload(raw, contentType) : parseJsonUpload(raw);
833
+ if ("error" in parsed) {
834
+ rejectFiles(res, journal, defaults.logger, method, path, require_helpers.flattenHeaders(req.headers), parsed.error, setCorsHeaders);
835
+ return;
836
+ }
837
+ const obj = storeFile(parsed);
838
+ const { id, bytes } = obj;
839
+ defaults.logger.debug(`Files mock: stored ${id} (${parsed.filename}, ${bytes} bytes)`);
840
+ writeJson(res, 200, obj, setCorsHeaders);
841
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 200, filesUploadBody(parsed));
842
+ }
843
+ /**
844
+ * `GET /v1/files` paging bounds, taken from the `openai/openai-openapi` spec
845
+ * (`operationId: listFiles`): "Limit can range between 1 and 10,000, and the
846
+ * default is 10,000." Files is the odd one out among the list surfaces — most
847
+ * default to 20 — so the default is spelled out here rather than shared.
848
+ */
849
+ const FILES_LIST_MAX_LIMIT = 1e4;
850
+ const FILES_LIST_DEFAULT_LIMIT = FILES_LIST_MAX_LIMIT;
851
+ /**
852
+ * An unsigned decimal integer literal and nothing else — no sign, no exponent,
853
+ * no radix prefix, no surrounding whitespace. See {@link parseListQuery} for
854
+ * what each of those would otherwise be silently accepted as.
855
+ *
856
+ * A long digit run is still safe to hand to `Number()`: it stays finite and
857
+ * integral, so it fails the range check below rather than the type check.
858
+ */
859
+ const DECIMAL_INTEGER_RE = /^[0-9]+$/;
860
+ /**
861
+ * The single non-empty value of a list query parameter, or the 400 it earns.
862
+ *
863
+ * ONE rule for all four of `limit`, `order`, `after` and `purpose`, because the
864
+ * three ways of reading them used to disagree on the same URL:
865
+ *
866
+ * - REPEATED is a 400. `URLSearchParams.get` answers with the FIRST of a
867
+ * repeated parameter and launders the rest, so `?limit=1&limit=abc` paged
868
+ * at 1 and never looked at `abc` — a caller that built its URL twice over
869
+ * got a silent success for a value it never meant. `listFiles` declares
870
+ * each of the four as a single scalar and gives no combining rule for a
871
+ * repeat, so a repeat is a caller error. Matches the multipart body, where
872
+ * two `purpose` parts are already a 400 rather than a last-one-wins.
873
+ * - PRESENT-BUT-EMPTY is a 400. `?order=` was already one (no member of the
874
+ * enum is ""), but `?after=` meant "no cursor" and `?purpose=` meant "no
875
+ * filter" and handed back the WHOLE store — the exact opposite of the
876
+ * narrowing the caller asked for. An empty string is not an object id and
877
+ * not a purpose any stored file can carry, and `?x=` is what building a
878
+ * URL from a partly-undefined params object emits, which is a bug worth
879
+ * showing. Omitting the parameter is how you say "no filter".
880
+ * - ABSENT is `null`, and each caller applies its own default.
881
+ *
882
+ * Both 400s are OURS, not the vendor's: the spec states neither. They are the
883
+ * mock's standing bias towards failing loudly over answering a question the
884
+ * caller did not ask.
885
+ */
886
+ function singleParam(params, name) {
887
+ const all = params.getAll(name);
888
+ if (all.length > 1) return {
889
+ ok: false,
890
+ error: `Invalid parameter: '${name}' was given ${all.length} times; it takes a single value`
891
+ };
892
+ if (all.length === 0) return {
893
+ ok: true,
894
+ value: null
895
+ };
896
+ if (all[0] === "") return {
897
+ ok: false,
898
+ error: `Invalid parameter: '${name}' was given an empty value; omit it instead`
899
+ };
900
+ return {
901
+ ok: true,
902
+ value: all[0]
903
+ };
904
+ }
905
+ /**
906
+ * Parse the `limit` / `order` / `after` / `purpose` parameters off a list URL.
907
+ *
908
+ * Shapes come from `operationId: listFiles` in `openai/openai-openapi` and from
909
+ * `FileListParams extends CursorPageParams` in the vendored SDK (openai
910
+ * 4.104.0 — `resources/files.d.ts` gives `order?: 'asc' | 'desc'` and
911
+ * `purpose?: string`; `pagination.d.ts` gives `after?: string` and
912
+ * `limit?: number`). `order` defaults to `desc`, so the newest file comes back
913
+ * first.
914
+ *
915
+ * All four are read here, through {@link singleParam}, so there is exactly one
916
+ * place that decides what a repeated or empty list parameter means. `purpose`
917
+ * used to be read separately in `src/server.ts` and passed in already parsed,
918
+ * which is how it ended up with neither rule.
919
+ */
920
+ function parseListQuery(rawUrl) {
921
+ const queryStart = rawUrl.indexOf("?");
922
+ const params = new URLSearchParams(queryStart === -1 ? "" : rawUrl.slice(queryStart + 1));
923
+ const rawOrder = singleParam(params, "order");
924
+ if (!rawOrder.ok) return { error: rawOrder.error };
925
+ const orderRaw = rawOrder.value;
926
+ if (orderRaw !== null && orderRaw !== "asc" && orderRaw !== "desc") return { error: "Invalid parameter: 'order' must be one of: asc, desc" };
927
+ const rawLimit = singleParam(params, "limit");
928
+ if (!rawLimit.ok) return { error: rawLimit.error };
929
+ let limit = FILES_LIST_DEFAULT_LIMIT;
930
+ if (rawLimit.value !== null) {
931
+ const parsed = DECIMAL_INTEGER_RE.test(rawLimit.value) ? Number(rawLimit.value) : NaN;
932
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > FILES_LIST_MAX_LIMIT) return { error: `Invalid parameter: 'limit' must be an integer between 1 and ${FILES_LIST_MAX_LIMIT}` };
933
+ limit = parsed;
934
+ }
935
+ const rawAfter = singleParam(params, "after");
936
+ if (!rawAfter.ok) return { error: rawAfter.error };
937
+ const rawPurpose = singleParam(params, "purpose");
938
+ if (!rawPurpose.ok) return { error: rawPurpose.error };
939
+ return {
940
+ limit,
941
+ order: orderRaw ?? "desc",
942
+ after: rawAfter.value ?? void 0,
943
+ purpose: rawPurpose.value ?? void 0
944
+ };
945
+ }
946
+ async function handleFilesList(req, res, journal, defaults, setCorsHeaders) {
947
+ const path = req.url ?? "/v1/files";
948
+ const method = req.method ?? "GET";
949
+ setCorsHeaders(res);
950
+ if (await require_chaos.applyChaosAsync(res, null, defaults.chaos, req.headers, req.url, journal, {
951
+ method,
952
+ path,
953
+ headers: require_helpers.flattenHeaders(req.headers),
954
+ body: null,
955
+ service: "files"
956
+ }, "internal", defaults.registry, defaults.logger)) return;
957
+ const query = parseListQuery(path);
958
+ if ("error" in query) {
959
+ rejectFiles(res, journal, defaults.logger, method, path, require_helpers.flattenHeaders(req.headers), query.error, setCorsHeaders);
960
+ return;
961
+ }
962
+ let data = [...fileStore.values()];
963
+ if (query.purpose !== void 0) data = data.filter((f) => f.purpose === query.purpose);
964
+ data.sort((a, b) => a.created_at - b.created_at);
965
+ if (query.order === "desc") data.reverse();
966
+ let start = 0;
967
+ if (query.after !== void 0) {
968
+ const idx = data.findIndex((f) => f.id === query.after);
969
+ if (idx === -1) {
970
+ rejectFiles(res, journal, defaults.logger, method, path, require_helpers.flattenHeaders(req.headers), `Invalid parameter: 'after' cursor '${query.after}' does not match any file in this listing`, setCorsHeaders);
971
+ return;
972
+ }
973
+ start = idx + 1;
974
+ }
975
+ const page = data.slice(start, start + query.limit);
976
+ writeJson(res, 200, {
977
+ object: "list",
978
+ data: page,
979
+ first_id: page[0]?.id ?? null,
980
+ last_id: page[page.length - 1]?.id ?? null,
981
+ has_more: start + page.length < data.length
982
+ }, setCorsHeaders);
983
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 200);
984
+ }
985
+ async function handleFilesRetrieve(req, res, fileId, journal, defaults, setCorsHeaders) {
986
+ const path = req.url ?? `/v1/files/${fileId}`;
987
+ const method = req.method ?? "GET";
988
+ setCorsHeaders(res);
989
+ if (await require_chaos.applyChaosAsync(res, null, defaults.chaos, req.headers, req.url, journal, {
990
+ method,
991
+ path,
992
+ headers: require_helpers.flattenHeaders(req.headers),
993
+ body: null,
994
+ service: "files"
995
+ }, "internal", defaults.registry, defaults.logger)) return;
996
+ const found = fileStore.get(fileId);
997
+ if (!found) {
998
+ writeJson(res, 404, invalidRequest(`No such file: ${fileId}`), setCorsHeaders);
999
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 404);
1000
+ return;
1001
+ }
1002
+ writeJson(res, 200, found, setCorsHeaders);
1003
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 200);
1004
+ }
1005
+ async function handleFilesContent(req, res, fileId, journal, defaults, setCorsHeaders) {
1006
+ const path = req.url ?? `/v1/files/${fileId}/content`;
1007
+ const method = req.method ?? "GET";
1008
+ setCorsHeaders(res);
1009
+ if (await require_chaos.applyChaosAsync(res, null, defaults.chaos, req.headers, req.url, journal, {
1010
+ method,
1011
+ path,
1012
+ headers: require_helpers.flattenHeaders(req.headers),
1013
+ body: null,
1014
+ service: "files"
1015
+ }, "internal", defaults.registry, defaults.logger)) return;
1016
+ const found = fileStore.get(fileId);
1017
+ const content = fileContents.get(fileId);
1018
+ if (!found || content === void 0) {
1019
+ writeJson(res, 404, invalidRequest(`No such file: ${fileId}`), setCorsHeaders);
1020
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 404);
1021
+ return;
1022
+ }
1023
+ res.writeHead(200, {
1024
+ "Content-Type": contentTypeForFilename(found.filename),
1025
+ "Content-Length": String(content.bytes.length),
1026
+ "X-Content-Type-Options": "nosniff",
1027
+ "Content-Disposition": contentDispositionFor(found.filename)
1028
+ });
1029
+ res.end(content.bytes);
1030
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 200);
1031
+ }
1032
+ async function handleFilesDelete(req, res, fileId, journal, defaults, setCorsHeaders) {
1033
+ const path = req.url ?? `/v1/files/${fileId}`;
1034
+ const method = req.method ?? "DELETE";
1035
+ setCorsHeaders(res);
1036
+ if (await require_chaos.applyChaosAsync(res, null, defaults.chaos, req.headers, req.url, journal, {
1037
+ method,
1038
+ path,
1039
+ headers: require_helpers.flattenHeaders(req.headers),
1040
+ body: null,
1041
+ service: "files"
1042
+ }, "internal", defaults.registry, defaults.logger)) return;
1043
+ if (!fileStore.get(fileId)) {
1044
+ writeJson(res, 404, invalidRequest(`No such file: ${fileId}`), setCorsHeaders);
1045
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 404);
1046
+ return;
1047
+ }
1048
+ fileStore.delete(fileId);
1049
+ fileContents.delete(fileId);
1050
+ writeJson(res, 200, {
1051
+ id: fileId,
1052
+ object: "file",
1053
+ deleted: true
1054
+ }, setCorsHeaders);
1055
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 200);
1056
+ }
1057
+
1058
+ //#endregion
1059
+ exports.FILES_BODY_DRAIN_MAX_BYTES = FILES_BODY_DRAIN_MAX_BYTES;
1060
+ exports.FILES_BODY_MAX_BYTES = FILES_BODY_MAX_BYTES;
1061
+ exports.FILES_BODY_OVERSIZED = FILES_BODY_OVERSIZED;
1062
+ exports.FILES_MAX_BYTES = FILES_MAX_BYTES;
1063
+ exports.clearFileStore = clearFileStore;
1064
+ exports.getStoredFileBytes = getStoredFileBytes;
1065
+ exports.handleFilesContent = handleFilesContent;
1066
+ exports.handleFilesCreate = handleFilesCreate;
1067
+ exports.handleFilesDelete = handleFilesDelete;
1068
+ exports.handleFilesList = handleFilesList;
1069
+ exports.handleFilesRetrieve = handleFilesRetrieve;
1070
+ exports.storeFile = storeFile;
1071
+ //# sourceMappingURL=files.cjs.map