@copilotkit/aimock 1.43.0 → 1.45.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +46 -0
- package/README.md +14 -2
- package/dist/agui-handler.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +31 -5
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +31 -5
- package/dist/aimock-cli.js.map +1 -1
- package/dist/bedrock-converse.cjs +344 -43
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +345 -44
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +195 -42
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +196 -43
- package/dist/bedrock.js.map +1 -1
- package/dist/chaos.cjs +9 -5
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +10 -7
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +238 -9
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +242 -13
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +159 -72
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +160 -73
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +63 -5
- package/dist/config-loader.cjs.map +1 -1
- package/dist/config-loader.d.cts +37 -4
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts +37 -4
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +63 -5
- package/dist/config-loader.js.map +1 -1
- package/dist/constants.cjs +8 -0
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +6 -2
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +8 -1
- package/dist/constants.js.map +1 -1
- package/dist/echo-text.cjs +65 -0
- package/dist/echo-text.cjs.map +1 -0
- package/dist/echo-text.d.cts +22 -0
- package/dist/echo-text.d.cts.map +1 -0
- package/dist/echo-text.d.ts +22 -0
- package/dist/echo-text.d.ts.map +1 -0
- package/dist/echo-text.js +65 -0
- package/dist/echo-text.js.map +1 -0
- package/dist/embeddings.cjs +18 -1
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +18 -1
- package/dist/embeddings.js.map +1 -1
- package/dist/fakes-plugin.cjs +131 -0
- package/dist/fakes-plugin.cjs.map +1 -0
- package/dist/fakes-plugin.d.cts +23 -0
- package/dist/fakes-plugin.d.cts.map +1 -0
- package/dist/fakes-plugin.d.ts +23 -0
- package/dist/fakes-plugin.d.ts.map +1 -0
- package/dist/fakes-plugin.js +127 -0
- package/dist/fakes-plugin.js.map +1 -0
- package/dist/files.cjs +9 -11
- package/dist/files.cjs.map +1 -1
- package/dist/files.d.cts.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +10 -13
- package/dist/files.js.map +1 -1
- package/dist/fine-tuning.cjs +1 -19
- package/dist/fine-tuning.cjs.map +1 -1
- package/dist/fine-tuning.d.cts.map +1 -1
- package/dist/fine-tuning.d.ts.map +1 -1
- package/dist/fine-tuning.js +2 -20
- package/dist/fine-tuning.js.map +1 -1
- package/dist/fixture-loader-services.cjs +116 -0
- package/dist/fixture-loader-services.cjs.map +1 -0
- package/dist/fixture-loader-services.d.cts +48 -0
- package/dist/fixture-loader-services.d.cts.map +1 -0
- package/dist/fixture-loader-services.d.ts +48 -0
- package/dist/fixture-loader-services.d.ts.map +1 -0
- package/dist/fixture-loader-services.js +114 -0
- package/dist/fixture-loader-services.js.map +1 -0
- package/dist/fixture-loader.cjs +552 -29
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +140 -2
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +140 -2
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +542 -31
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +42 -2
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +43 -3
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +288 -52
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +289 -53
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +234 -38
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +235 -39
- package/dist/gemini.js.map +1 -1
- package/dist/helpers.cjs +1157 -12
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +85 -4
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +85 -4
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +1113 -14
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +3 -2
- package/dist/images.cjs.map +1 -1
- package/dist/images.js +3 -2
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +39 -1
- package/dist/index.d.cts +11 -4
- package/dist/index.d.ts +11 -4
- package/dist/index.js +11 -4
- package/dist/jest.cjs +43 -14
- package/dist/jest.cjs.map +1 -1
- package/dist/jest.d.cts +47 -5
- package/dist/jest.d.cts.map +1 -1
- package/dist/jest.d.ts +47 -5
- package/dist/jest.d.ts.map +1 -1
- package/dist/jest.js +45 -16
- package/dist/jest.js.map +1 -1
- package/dist/journal.cjs +155 -13
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts +54 -4
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts +54 -4
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +155 -13
- package/dist/journal.js.map +1 -1
- package/dist/live-sanitize.cjs +4 -32
- package/dist/live-sanitize.cjs.map +1 -1
- package/dist/live-sanitize.js +4 -32
- package/dist/live-sanitize.js.map +1 -1
- package/dist/llmock.cjs +204 -39
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +51 -1
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +51 -1
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +206 -41
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-fakes-mount.cjs +217 -0
- package/dist/mcp-fakes-mount.cjs.map +1 -0
- package/dist/mcp-fakes-mount.js +211 -0
- package/dist/mcp-fakes-mount.js.map +1 -0
- package/dist/mcp-fakes-report.cjs +111 -0
- package/dist/mcp-fakes-report.cjs.map +1 -0
- package/dist/mcp-fakes-report.d.cts +40 -0
- package/dist/mcp-fakes-report.d.cts.map +1 -0
- package/dist/mcp-fakes-report.d.ts +40 -0
- package/dist/mcp-fakes-report.d.ts.map +1 -0
- package/dist/mcp-fakes-report.js +107 -0
- package/dist/mcp-fakes-report.js.map +1 -0
- package/dist/mcp-fakes.cjs +1942 -0
- package/dist/mcp-fakes.cjs.map +1 -0
- package/dist/mcp-fakes.d.cts +504 -0
- package/dist/mcp-fakes.d.cts.map +1 -0
- package/dist/mcp-fakes.d.ts +504 -0
- package/dist/mcp-fakes.d.ts.map +1 -0
- package/dist/mcp-fakes.js +1937 -0
- package/dist/mcp-fakes.js.map +1 -0
- package/dist/mcp-handler.cjs +534 -27
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.d.cts +49 -0
- package/dist/mcp-handler.d.cts.map +1 -0
- package/dist/mcp-handler.d.ts +49 -0
- package/dist/mcp-handler.d.ts.map +1 -0
- package/dist/mcp-handler.js +530 -28
- package/dist/mcp-handler.js.map +1 -1
- package/dist/mcp-mock.cjs +326 -47
- package/dist/mcp-mock.cjs.map +1 -1
- package/dist/mcp-mock.d.cts +97 -1
- package/dist/mcp-mock.d.cts.map +1 -1
- package/dist/mcp-mock.d.ts +97 -1
- package/dist/mcp-mock.d.ts.map +1 -1
- package/dist/mcp-mock.js +327 -48
- package/dist/mcp-mock.js.map +1 -1
- package/dist/mcp-recorder.cjs +1224 -0
- package/dist/mcp-recorder.cjs.map +1 -0
- package/dist/mcp-recorder.d.cts +108 -0
- package/dist/mcp-recorder.d.cts.map +1 -0
- package/dist/mcp-recorder.d.ts +108 -0
- package/dist/mcp-recorder.d.ts.map +1 -0
- package/dist/mcp-recorder.js +1215 -0
- package/dist/mcp-recorder.js.map +1 -0
- package/dist/mcp-stub.cjs +5 -1
- package/dist/mcp-stub.d.cts +3 -1
- package/dist/mcp-stub.d.ts +3 -1
- package/dist/mcp-stub.js +3 -1
- package/dist/mcp-types.d.cts +6 -0
- package/dist/mcp-types.d.ts +6 -0
- package/dist/message-text.cjs +192 -0
- package/dist/message-text.cjs.map +1 -0
- package/dist/message-text.d.cts +31 -0
- package/dist/message-text.d.cts.map +1 -0
- package/dist/message-text.d.ts +31 -0
- package/dist/message-text.d.ts.map +1 -0
- package/dist/message-text.js +184 -0
- package/dist/message-text.js.map +1 -0
- package/dist/messages.cjs +400 -56
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +401 -58
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +46 -66
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts +14 -6
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts +14 -6
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +32 -40
- package/dist/metrics.js.map +1 -1
- package/dist/misbehavior.cjs +1523 -0
- package/dist/misbehavior.cjs.map +1 -0
- package/dist/misbehavior.d.cts +43 -0
- package/dist/misbehavior.d.cts.map +1 -0
- package/dist/misbehavior.d.ts +43 -0
- package/dist/misbehavior.d.ts.map +1 -0
- package/dist/misbehavior.js +1511 -0
- package/dist/misbehavior.js.map +1 -0
- package/dist/ollama.cjs +315 -113
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +316 -114
- package/dist/ollama.js.map +1 -1
- package/dist/openapi.cjs +3405 -0
- package/dist/openapi.cjs.map +1 -0
- package/dist/openapi.d.cts +30 -0
- package/dist/openapi.d.cts.map +1 -0
- package/dist/openapi.d.ts +30 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +3404 -0
- package/dist/openapi.js.map +1 -0
- package/dist/openrouter-video.cjs +4 -4
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.d.cts.map +1 -1
- package/dist/openrouter-video.d.ts.map +1 -1
- package/dist/openrouter-video.js +4 -4
- package/dist/openrouter-video.js.map +1 -1
- package/dist/record-sanitize.cjs +231 -0
- package/dist/record-sanitize.cjs.map +1 -0
- package/dist/record-sanitize.js +224 -0
- package/dist/record-sanitize.js.map +1 -0
- package/dist/recorder.cjs +115 -6
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +2 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts +2 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +116 -8
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +701 -166
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts +5 -13
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts +5 -13
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +699 -169
- package/dist/responses.js.map +1 -1
- package/dist/route-registry.cjs +951 -0
- package/dist/route-registry.cjs.map +1 -0
- package/dist/route-registry.d.cts +33 -0
- package/dist/route-registry.d.cts.map +1 -0
- package/dist/route-registry.d.ts +33 -0
- package/dist/route-registry.d.ts.map +1 -0
- package/dist/route-registry.js +866 -0
- package/dist/route-registry.js.map +1 -0
- package/dist/router.cjs +24 -2
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +25 -4
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +1159 -264
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +4 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +4 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1067 -172
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +4 -2
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.js +4 -2
- package/dist/speech.js.map +1 -1
- package/dist/stream-collapse.cjs +236 -34
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +39 -4
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +39 -4
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +235 -35
- package/dist/stream-collapse.js.map +1 -1
- package/dist/suite.cjs +1 -1
- package/dist/suite.js +1 -1
- package/dist/transcription.cjs +2 -2
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +2 -2
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs +18 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +516 -5
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +516 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/dist/validate-cli.cjs +123 -33
- package/dist/validate-cli.cjs.map +1 -1
- package/dist/validate-cli.js +125 -35
- package/dist/validate-cli.js.map +1 -1
- package/dist/vector-stores.cjs +1180 -0
- package/dist/vector-stores.cjs.map +1 -0
- package/dist/vector-stores.d.cts +106 -0
- package/dist/vector-stores.d.cts.map +1 -0
- package/dist/vector-stores.d.ts +106 -0
- package/dist/vector-stores.d.ts.map +1 -0
- package/dist/vector-stores.js +1164 -0
- package/dist/vector-stores.js.map +1 -0
- package/dist/vector-types.d.cts.map +1 -1
- package/dist/version.cjs +45 -0
- package/dist/version.cjs.map +1 -0
- package/dist/version.js +44 -0
- package/dist/version.js.map +1 -0
- package/dist/video.cjs +19 -0
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +19 -0
- package/dist/video.js.map +1 -1
- package/dist/vitest.cjs +77 -16
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +38 -5
- package/dist/vitest.d.cts.map +1 -1
- package/dist/vitest.d.ts +38 -5
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +78 -17
- package/dist/vitest.js.map +1 -1
- package/dist/watcher.cjs +6 -2
- package/dist/watcher.cjs.map +1 -1
- package/dist/watcher.js +6 -2
- package/dist/watcher.js.map +1 -1
- package/dist/ws-gemini-live.cjs +210 -101
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts +9 -3
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts +9 -3
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +211 -102
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-realtime.cjs +175 -62
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts +8 -3
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts +8 -3
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +176 -63
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +121 -16
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts +8 -12
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts +8 -12
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +123 -18
- package/dist/ws-responses.js.map +1 -1
- package/package.json +13 -2
- package/skills/write-fixtures/SKILL.md +179 -21
package/dist/files.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files.js","names":[],"sources":["../src/files.ts"],"sourcesContent":["/**\n * OpenAI Files API mock for aimock.\n *\n * Implements the subset of `POST /v1/files`, `GET /v1/files`,\n * `GET /v1/files/{id}`, `GET /v1/files/{id}/content` and\n * `DELETE /v1/files/{id}` that test harnesses need to exercise\n * file-upload flows without touching the real API.\n *\n * Storage is in-memory and per-process (like the video job maps):\n * - metadata lives in `fileStore`\n * - raw bytes live in `fileContents` as `Buffer`s. Bytes are never\n * round-tripped through a utf8 string, so binary uploads come back\n * byte-for-byte identical.\n * - `clearFileStore()` wipes both and is wired into the full reset path\n * (`POST /__aimock/reset` + `LLMock.reset()`)\n *\n * The only memory bound here is per file: {@link FILES_MAX_BYTES} caps one\n * upload. The file COUNT is deliberately unbounded, so total residency grows\n * with the number of uploads until something calls `clearFileStore()` (or the\n * process exits) — a suite that uploads in a loop without resetting will grow\n * this map without limit. That is a real, documented bound, not an OOM defense.\n *\n * The job maps this module otherwise mirrors (fal-audio.ts,\n * openrouter-video.ts, grok-video.ts, byteplus-video.ts) DO cap entry count and\n * FIFO-evict the oldest, and that convention deliberately does NOT apply here.\n * Those hold transient jobs that the real API also expires, so eviction\n * reproduces real behaviour. A file is a durable object: the real API keeps it\n * until the caller deletes it. Silently evicting one would make the mock answer\n * `404 No such file` for a file the caller successfully uploaded and never\n * deleted — an outcome the real API cannot produce, surfacing as an\n * unexplainable flake in the suite under test. Unbounded growth is the honest\n * failure mode; `POST /__aimock/reset` is the lever that reclaims it.\n *\n * Body handling accepts both shapes real SDKs send:\n * - JSON `{ filename, purpose, content }` (test-friendly; `bytes` is an alias\n * of `content`, and exactly one of the two must be present)\n * - `multipart/form-data` with `purpose` + exactly one payload part named\n * `file` (or its `content` alias), framed per RFC 2046 §5.1.1 so a payload\n * containing the boundary token is stored whole rather than truncated at it\n *\n * Both parsers are strict: a malformed body is an OpenAI-shaped\n * `400 invalid_request_error` naming the offending field or part. Nothing is\n * `String()`-coerced and no missing field falls through to a fabricated\n * default, because a mock that quietly stores `\"[object Object]\"` or a 0-byte\n * file teaches the suite under test that a broken upload succeeded.\n *\n * Every branch journals with `service: \"files\"` so\n * `GET /__aimock/journal?service=files` selects exactly this traffic, and with\n * `source: \"internal\"` because — as with fine-tuning — no fixture or proxy\n * ever serves this store. A successful upload journals a SYNTHETIC body\n * describing it (`{ purpose, filename, bytes, content_type, sha256 }`) — never\n * the uploaded octets — exactly as the transcription route journals its\n * multipart audio; a `400` journals the error envelope it answered with, so\n * the reason is readable from the journal; see {@link journalFiles}. Files also\n * honors the inbound API-key boundary via the server dispatch (no bypass),\n * and runs through the chaos gate so retry/backoff suites can inject\n * 500s at the files surface too.\n */\n\nimport { createHash } from \"node:crypto\";\nimport type * as http from \"node:http\";\nimport { flattenHeaders, generateId, isJsonObject } from \"./helpers.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport type { ChaosDefaults, ChatCompletionRequest, JournalBody } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\n\nexport interface FileObject {\n id: string;\n object: \"file\";\n bytes: number;\n created_at: number;\n filename: string;\n purpose: string;\n status: \"uploaded\" | \"processed\" | \"pending\" | \"error\";\n status_details?: string;\n}\n\n/**\n * Purposes accepted on upload — `CreateFileRequest.purpose` in the official\n * `openai/openai-openapi` spec (and the `FilePurpose` type in the SDK).\n * `user_data` is what Responses-API file inputs use, so rejecting it broke the\n * most current flow.\n */\nconst CREATE_PURPOSES = new Set([\n \"assistants\",\n \"batch\",\n \"fine-tune\",\n \"vision\",\n \"user_data\",\n \"evals\",\n]);\n\n/**\n * Exposed for tests and for callers that want to check a value themselves:\n * the enum this mock enforces on upload.\n */\nexport const FILE_CREATE_PURPOSES: readonly string[] = [...CREATE_PURPOSES];\n\nfunction invalidPurpose(purpose: string): string {\n return `Invalid purpose '${purpose}'. Expected one of: ${[...CREATE_PURPOSES].join(\", \")}`;\n}\n\n/**\n * `Content-Transfer-Encoding` values that are identity transforms (RFC 2045\n * §6.2): the part body already *is* the file's octets, so storing it verbatim\n * is correct. Everything else (`base64`, `quoted-printable`, …) would have to\n * be decoded before storage, and is rejected instead — see\n * {@link unsupportedPartEncoding}.\n */\nconst IDENTITY_TRANSFER_ENCODINGS = new Set([\"7bit\", \"8bit\", \"binary\", \"identity\"]);\n\n/**\n * Reject, rather than silently mis-store, a file part that asks for a content\n * encoding this mock does not apply.\n *\n * We deliberately do NOT implement a decoder. RFC 7578 §4.7 deprecates\n * `Content-Transfer-Encoding` in `multipart/form-data` (\"Senders SHOULD NOT\n * generate any parts with a Content-Transfer-Encoding header field. Currently,\n * no deployed implementations that send such bodies have been discovered\"), and\n * a capture of the clients that actually reach this endpoint — curl `-F`,\n * python `requests`, python `httpx` (the OpenAI Python SDK's transport) and\n * undici `FormData` (the OpenAI Node SDK's transport) — confirms none of them\n * emits one. Storing an encoded body as-is is the harmful outcome: `bytes` and\n * `GET /content` would report the *encoded* form and a hash comparison in a\n * test would fail for a reason the caller cannot see. A loud 400 naming the\n * header is the honest answer for a mock.\n *\n * `identity` counts as a no-op alongside the RFC 2045 §6.2 values: it is the\n * registered `Content-Encoding` for \"no transformation\", and a client that\n * spells it on `Content-Transfer-Encoding` means the same thing. Rejecting it\n * named a problem that did not exist.\n *\n * Every part is checked, not only the payload part, and the message names the\n * part it came from. While only the payload part was checked, a `base64`\n * `purpose` part slid through to the enum check and 400'd with\n * `Invalid purpose 'YXNzaXN0YW50cw=='`, pointing the caller at the wrong thing.\n */\nfunction unsupportedPartEncoding(\n header: \"Content-Transfer-Encoding\" | \"Content-Encoding\",\n value: string,\n partName: string,\n): string {\n // The RFC 7578 §4.7 deprecation is specifically about Content-Transfer-\n // Encoding, so only that branch cites it.\n const rationale =\n header === \"Content-Transfer-Encoding\"\n ? ` RFC 7578 section 4.7 deprecates ${header} in multipart/form-data.`\n : \"\";\n return (\n `Unsupported ${header}: '${value}' on the multipart '${partName}' part. ` +\n `aimock stores part bodies verbatim and does not decode them.${rationale} ` +\n `Send the raw bytes instead.`\n );\n}\n\n/**\n * Content types for the handful of extensions test harnesses actually upload.\n * Anything unrecognised falls back to `application/octet-stream` — the safe\n * default for opaque bytes, and what stops a PNG being served (and re-decoded)\n * as UTF-8 text.\n *\n * This is an allowlist of INERT types on purpose. `text/html` is deliberately\n * absent: the mock serves uploaded bytes back from its own origin, so any\n * active type here would let a stored upload script that origin in a browser.\n * HTML uploads round-trip byte-for-byte, they are just served as opaque bytes.\n */\nconst CONTENT_TYPE_BY_EXT: Record<string, string> = {\n json: \"application/json\",\n // UNVERIFIED against the real API, and left alone for that reason.\n // `application/jsonl` is not IANA-registered; the rest of this repo spells\n // line-delimited JSON `application/x-ndjson` (ndjson-writer.ts, recorder.ts,\n // stream-collapse.ts). Neither `openai/openai-openapi` nor the vendored SDK\n // states what `GET /v1/files/{id}/content` returns for a `.jsonl` upload, and\n // no keyless probe of api.openai.com can answer it, so changing this would be\n // swapping one guess for another. Change it only against a real capture.\n jsonl: \"application/jsonl\",\n txt: \"text/plain; charset=utf-8\",\n md: \"text/markdown; charset=utf-8\",\n csv: \"text/csv; charset=utf-8\",\n pdf: \"application/pdf\",\n png: \"image/png\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n wav: \"audio/wav\",\n mp3: \"audio/mpeg\",\n zip: \"application/zip\",\n};\n\nfunction contentTypeForFilename(filename: string): string {\n const dot = filename.lastIndexOf(\".\");\n if (dot === -1) return \"application/octet-stream\";\n const ext = filename.slice(dot + 1).toLowerCase();\n return CONTENT_TYPE_BY_EXT[ext] ?? \"application/octet-stream\";\n}\n\n/**\n * RFC 5987 §3.2.1 `ext-value` percent-encoding for the `filename*` parameter.\n *\n * `encodeURIComponent` leaves `!'()*-._~` unescaped, but `'`, `(`, `)` and `*`\n * are NOT in RFC 5987's `attr-char` set, so they have to be escaped by hand.\n */\nfunction encodeExtValue(value: string): string {\n return encodeURIComponent(value).replace(\n /['()*]/g,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,\n );\n}\n\n/**\n * `Content-Disposition` for the content route (RFC 6266 §4.1).\n *\n * Always `attachment`. {@link CONTENT_TYPE_BY_EXT} is an allowlist of INERT\n * types precisely so a stored upload cannot execute against the mock's own\n * origin, but `application/pdf` was still being served with no disposition at\n * all, which in every mainstream browser means \"render in the built-in PDF\n * viewer\" — an active surface on this origin, exactly what the allowlist's\n * comment claims is excluded. `attachment` closes that without touching the\n * allowlist, and it also stops a `.json`/`.txt`/`.csv` body being rendered\n * inline if someone opens a content URL in a tab.\n *\n * What the REAL API sends here is UNVERIFIED: probing\n * `api.openai.com/v1/files/{id}/content` needs a key, and neither\n * `openai/openai-openapi` nor the SDK documents the response headers. So this\n * is chosen for safety, not fidelity — and it is inert for SDK callers: the\n * vendored SDK's `files.content()` (openai@4.104.0,\n * `resources/files.js:82-88`) issues the GET with `Accept: application/binary`\n * and `__binaryResponse: true`, handing back the raw `Response` without ever\n * reading `Content-Disposition`.\n *\n * Both parameters are built from the STORED filename, which is attacker-shaped\n * input: a multipart `filename=\"...\"` can carry CR/LF and would otherwise\n * inject response headers. The ASCII fallback replaces every non-printable and\n * non-ASCII byte with `_` and backslash-escapes `\"` and `\\` per the\n * quoted-string rule; `filename*` percent-encodes them. Neither can emit a bare\n * CR or LF.\n */\nfunction contentDispositionFor(filename: string): string {\n const asciiFallback = filename.replace(/[^\\x20-\\x7e]/g, \"_\").replace(/([\"\\\\])/g, \"\\\\$1\");\n const base = `attachment; filename=\"${asciiFallback}\"`;\n // Only add the RFC 5987 form when the name actually needs it, so the common\n // ASCII case stays a single plain parameter.\n if (/^[\\x20-\\x7e]*$/.test(filename)) return base;\n return `${base}; filename*=UTF-8''${encodeExtValue(filename)}`;\n}\n\n/**\n * Per-file content cap. An upload whose decoded content is above this gets a\n * `400` with the usual OpenAI-style error envelope, at every wire size a body\n * can have — see {@link FILES_BODY_MAX_BYTES} for how that is kept true\n * without buffering an unbounded body.\n */\nexport const FILES_MAX_BYTES = 10 * 1024 * 1024;\n\n/**\n * Worst-case wire-to-content expansion of a JSON upload.\n *\n * `content` is a JSON string, so one byte of decoded payload can arrive as as\n * many as six wire bytes: a `\\uXXXX` escape is 6 bytes and the cheapest code\n * point one can carry (U+0000–U+007F) is a single UTF-8 byte. Every other\n * code point does better — U+0800–U+FFFF is 3 UTF-8 bytes per 6 wire bytes,\n * a surrogate pair 4 per 12 — and a multipart part is 1:1 plus its envelope.\n * So six is an upper bound on wire → content expansion for both parsers.\n */\nconst FILES_MAX_WIRE_EXPANSION = 6;\n\n/** Slack for the JSON envelope, or for multipart boundaries and part headers. */\nconst FILES_ENVELOPE_HEADROOM_BYTES = 64 * 1024;\n\n/**\n * How much of a Files upload body the route will BUFFER — derived, not picked.\n *\n * `readBodyBuffer` enforces a limit by destroying the socket: no status line,\n * no body, no CORS headers, just an `ECONNRESET`. That made the `400 File\n * content exceeds ... byte cap` branches below unreachable when the two caps\n * were equal (both the 10 MB `DEFAULT_MAX_BODY_BYTES`), and a fixed slice of\n * headroom above the cap only moved the cliff: a `\\u0001`-heavy JSON body or a\n * multipart body far enough over the cap still died on the socket.\n *\n * So the bound is computed from the widest a within-cap payload can be on the\n * wire ({@link FILES_MAX_WIRE_EXPANSION} × the cap, plus envelope headroom).\n * Two consequences, and they are the contract:\n *\n * - No body that could decode to a payload within the cap is ever refused for\n * its size — it fits here by construction, and is answered on content.\n * - A body larger than this is not buffered. The route stops retaining bytes\n * (peak memory stays at this bound), drains the rest, and answers a real\n * `400` with CORS. It is not a socket drop, and it cannot be a false\n * rejection either, by the point above.\n *\n * The global `readBodyBuffer` default is deliberately left alone — every other\n * route depends on it.\n */\nexport const FILES_BODY_MAX_BYTES =\n FILES_MAX_BYTES * FILES_MAX_WIRE_EXPANSION + FILES_ENVELOPE_HEADROOM_BYTES;\n\n/**\n * The one documented socket drop on this route: a DoS backstop.\n *\n * Draining an over-size body is what buys the caller a real `400`, but the\n * draining itself is work an attacker can ask for without limit. Past this\n * the request is simply not worth finishing and the socket is destroyed, the\n * same way every other route treats its own limit. It is deliberately far\n * above {@link FILES_BODY_MAX_BYTES}: a client that lands here is not sending\n * a file that was ever going to be accepted.\n */\nexport const FILES_BODY_DRAIN_MAX_BYTES = FILES_BODY_MAX_BYTES * 2;\n\n/**\n * Stands in for an upload body that went past {@link FILES_BODY_MAX_BYTES} and\n * was therefore drained instead of buffered. The route hands this to\n * {@link handleFilesCreate} in place of the bytes, so the over-size answer is\n * the same `400` envelope — journalled, CORS-headed, chaos-gated — as every\n * other rejection here, rather than a second error path in the server.\n */\nexport const FILES_BODY_OVERSIZED = Symbol(\"files-body-oversized\");\n\n/** The `400` for a body too large to be a within-cap upload. */\nfunction oversizedBody(): string {\n 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)`;\n}\n\ninterface StoredContent {\n bytes: Buffer;\n}\n\nconst fileStore = new Map<string, FileObject>();\nconst fileContents = new Map<string, StoredContent>();\n\nexport function clearFileStore(): void {\n fileStore.clear();\n fileContents.clear();\n}\n\nexport function getFileStoreSize(): number {\n return fileStore.size;\n}\n\n/**\n * The stored bytes for one file, as the very `Buffer` this module holds — so a\n * test can assert on what it RETAINS (`buffer.byteLength`), not just on what it\n * serves back. `GET /v1/files/{id}/content` copies onto the wire, so it cannot\n * tell an owned buffer from a `subarray` pinning a 60 MB request body.\n */\nexport function getStoredFileBytes(id: string): Buffer | undefined {\n return fileContents.get(id)?.bytes;\n}\n\n/**\n * Register one file in the store and return its wire object. This is the one\n * insert path: `POST /v1/files` uses it, and so do server-side producers of\n * files (the Batches mock minting `output_file_id` / `error_file_id`), so a\n * file minted internally is retrievable through `GET /v1/files/{id}` and\n * `/content` exactly like an upload. `content` must be an OWNED buffer (see\n * {@link ParsedUpload.content}) — it is stored as-is, and it must already be\n * within {@link FILES_MAX_BYTES}: the cap the upload route enforces holds for\n * every file in the store, so a producer that cannot fit under it fails its\n * own operation instead of storing the file.\n */\nexport function storeFile(file: {\n purpose: string;\n filename: string;\n content: Buffer;\n}): FileObject {\n if (file.content.length > FILES_MAX_BYTES) {\n throw new RangeError(`File content exceeds ${FILES_MAX_BYTES} byte cap`);\n }\n const id = generateId(\"file\");\n const obj: FileObject = {\n id,\n object: \"file\",\n bytes: file.content.length,\n created_at: Math.floor(Date.now() / 1000),\n filename: file.filename,\n purpose: file.purpose,\n status: \"processed\",\n };\n fileStore.set(id, obj);\n // Stored as-is: an owned buffer of exactly the file's bytes, so residency\n // here is the file's size and nothing more.\n fileContents.set(id, { bytes: file.content });\n return obj;\n}\n\n/**\n * Synthetic journal body for an upload — the SAME convention\n * {@link handleTranscription} uses for `multipart/form-data` audio: the entry\n * carries a small object describing the upload that was parsed out of the\n * body, never the body itself. Raw file octets are NEVER journalled, in any\n * encoding: they can be megabytes of binary, they would blow the journal's\n * 64 KB body cap on anything real, and a base64 round-trip of them is not\n * something a test wants to diff. `sha256` is the byte identity a caller\n * asserts on instead.\n *\n * `content_type` is derived from the stored filename via\n * {@link contentTypeForFilename} — the same decision `GET /v1/files/{id}/content`\n * serves the bytes back with — rather than from the client-declared part\n * header, which this mock deliberately ignores everywhere else.\n *\n * `model`/`messages` are the inert `ChatCompletionRequest` floor the journal's\n * body type requires (the same `{ model: \"\", messages: [] }` the server uses\n * for its own bodyless synthetic entries); `_endpointType` names the surface.\n */\nfunction filesUploadBody(upload: ParsedUpload): ChatCompletionRequest {\n return {\n model: \"\",\n messages: [],\n _endpointType: \"files\",\n purpose: upload.purpose,\n filename: upload.filename,\n bytes: upload.content.length,\n content_type: contentTypeForFilename(upload.filename),\n sha256: createHash(\"sha256\").update(upload.content).digest(\"hex\"),\n };\n}\n\n/**\n * Journal one files-route request.\n *\n * The rule for `body`: an entry carries the {@link filesUploadBody} descriptor\n * exactly when a request had an upload payload that parsed — i.e. a successful\n * `POST /v1/files` — and the {@link invalidRequest} envelope exactly when the\n * request was rejected with a `400` (see {@link rejectFiles}), so the reason\n * is readable from the journal without the wire response. Every other files\n * entry (GET one, GET content, list, DELETE, a chaos-faulted request, a 404)\n * has no payload to describe and stays `null`, matching the bodyless routes in\n * {@link handleTranscription}. Raw uploaded bytes never appear.\n *\n * `service: \"files\"` is set on every branch so\n * `GET /__aimock/journal?service=files` selects exactly this traffic, and\n * `source: \"internal\"` because this store is aimock's own — the same tag every\n * fine-tuning entry carries, success and error alike.\n *\n * Call it AFTER the response has been written: a `writeHead` that throws must\n * not leave behind an entry for a response the client never received.\n */\nfunction journalFiles(\n journal: Journal,\n method: string,\n path: string,\n headers: Record<string, string>,\n status: number,\n body: JournalBody | null = null,\n): void {\n journal.add({\n method,\n path,\n headers,\n body,\n service: \"files\",\n response: { status, fixture: null, source: \"internal\" },\n });\n}\n\n/** Keep the entire warning bounded and on one physical line, including the URL. */\nfunction filesRejectionWarning(message: string): string {\n const maxLength = 1_024;\n let safe = \"\";\n for (const character of message) {\n const code = character.charCodeAt(0);\n const escaped =\n code < 32 || (code >= 127 && code <= 159) || code === 0x2028 || code === 0x2029\n ? `\\\\u${code.toString(16).padStart(4, \"0\")}`\n : character;\n // Reserve the truncation marker and never cut through an escape sequence.\n if (safe.length + escaped.length > maxLength - 3) return `${safe}...`;\n safe += escaped;\n }\n return safe;\n}\n\n/**\n * Answer a files-route `400`: log it at `warn` (the level the server uses for\n * a request it refused), journal the {@link invalidRequest} envelope as the\n * entry's body so the reason survives in the journal, and write that same\n * envelope to the wire. One helper so the three can never disagree.\n */\nfunction rejectFiles(\n res: http.ServerResponse,\n journal: Journal,\n logger: Logger,\n method: string,\n path: string,\n headers: Record<string, string>,\n message: string,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n const envelope = invalidRequest(message);\n logger.warn(filesRejectionWarning(`Files mock: rejected ${method} ${path} with 400: ${message}`));\n writeJson(res, 400, envelope, setCorsHeaders);\n journalFiles(journal, method, path, headers, 400, envelope);\n}\n\nfunction writeJson(\n res: http.ServerResponse,\n status: number,\n payload: unknown,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction invalidRequest(message: string): { error: { message: string; type: string } } {\n return { error: { message, type: \"invalid_request_error\" } };\n}\n\n/**\n * Name a rejected value's JSON type for the 400 message. The point is that the\n * caller sees *what* they sent instead of silently getting `String(value)`\n * stored as the file's content.\n *\n * The listed cases are exhaustive for the one caller. `JSON.parse` only ever\n * yields null, boolean, number, string, array or plain object — never\n * `undefined`, `function`, `symbol` or `bigint` — and the caller has already\n * excluded `string` (it is the accepted type) and `undefined` (that is the\n * separate \"required\" 400). So everything reaching the last line is an object.\n */\nfunction jsonTypeOf(value: unknown): string {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"an array\";\n if (typeof value === \"number\") return \"a number\";\n if (typeof value === \"boolean\") return \"a boolean\";\n return \"an object\";\n}\n\ninterface ParsedUpload {\n filename: string;\n purpose: string;\n /**\n * The upload's bytes, and an OWNED buffer — never a `subarray` of the\n * request body. Both parsers guarantee it (`Buffer.from(content, \"utf8\")` on\n * the JSON path, an explicit copy on the multipart path) because this is\n * what {@link handleFilesCreate} stores, and a view would keep the entire\n * request body resident for the lifetime of the file.\n */\n content: Buffer;\n}\n\n/**\n * A high surrogate not followed by a low one, or a low surrogate not preceded\n * by a high one — i.e. a JS string that is not well-formed UTF-16 and therefore\n * has no UTF-8 encoding.\n *\n * `Buffer.from(s, \"utf8\")` does not fail on these: it substitutes U+FFFD (EF BF\n * BD), silently and permanently. That breaks the one guarantee an upload mock\n * owes its caller — `GET /v1/files/{id}/content` returns what was uploaded — and\n * it breaks it invisibly, three bytes at a time, in a test whose hash\n * comparison then fails for no visible reason. The byte-for-byte promise this\n * module documents is made for the multipart path (which is scanned as latin1\n * and never decoded); the JSON path carries *text*, so text that cannot be\n * encoded is a 400, not a lossy store.\n */\nconst LONE_SURROGATE_RE = /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/;\n\n/**\n * Longest `filename` this mock will store, in UTF-8 bytes.\n *\n * NOT the real API's limit: `openai/openai-openapi` gives `OpenAIFile.filename`\n * as a bare `type: string` with no `maxLength`, and `CreateFileRequest` has no\n * `filename` property at all. So the bound comes from the one thing here a\n * filename can break: `GET /v1/files/{id}/content` echoes the stored name into\n * `Content-Disposition` twice — quoted ASCII (where `\"` and `\\` double) and\n * percent-encoded per RFC 5987 (where a byte can triple) — so ~5 bytes of\n * header per byte of name. 1024 bytes of name is ~5 KiB of header, well inside\n * Node's 16 KiB default `--max-http-header-size`.\n *\n * Unbounded, a 20 KB filename uploaded `200` and then made the content route\n * unreadable by undici: `UND_ERR_HEADERS_OVERFLOW`, no status, no envelope, no\n * CORS — the OpenAI SDK could not fetch a file it had just uploaded.\n */\nexport const FILES_MAX_FILENAME_BYTES = 1024;\n\n/**\n * The filename rules, in one place because both parsers must answer the same.\n *\n * They used to disagree: a name carrying NUL, CR or LF was a `400` as a\n * multipart parameter and a `200` as a JSON field, stored and echoed back\n * verbatim. Returns an error message, or `undefined` if the name is fine.\n */\nfunction filenameError(filename: string): string | undefined {\n // A filename is metadata that is echoed back on every file object, so it must\n // not carry control characters.\n if (CONTROL_CHARS.test(filename)) {\n return \"Invalid parameter: 'filename' must not contain control characters\";\n }\n // An unpaired surrogate has no UTF-8 encoding, so `encodeURIComponent` throws\n // on it when the content route builds its `filename*` parameter: the name\n // uploaded with a 200 and then made `GET /v1/files/{id}/content` a 500.\n if (LONE_SURROGATE_RE.test(filename)) {\n return \"Invalid parameter: 'filename' must not contain an unpaired UTF-16 surrogate\";\n }\n const byteLength = Buffer.byteLength(filename, \"utf8\");\n if (byteLength > FILES_MAX_FILENAME_BYTES) {\n return (\n `Invalid parameter: 'filename' is ${byteLength} UTF-8 bytes, above the ` +\n `${FILES_MAX_FILENAME_BYTES} byte cap (a longer name makes ` +\n `GET /v1/files/{id}/content unfetchable: its Content-Disposition header ` +\n `would exceed the HTTP header limit)`\n );\n }\n return undefined;\n}\n\n/** The deterministic name given to an upload that arrived without one. */\nfunction synthesizedFilename(content: Buffer): string {\n return `upload-${createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 24)}.bin`;\n}\n\nfunction parseJsonUpload(raw: Buffer): ParsedUpload | { error: string } {\n // Decoded FATALLY, like `filename*` on the multipart path: `raw.toString(\"utf8\")`\n // substitutes U+FFFD for an invalid sequence, so a body carrying a raw 0xFF\n // inside `content` was stored (with a 200) as bytes the client never sent.\n let text: string;\n try {\n text = new TextDecoder(\"utf-8\", { fatal: true }).decode(raw);\n } catch {\n return { error: \"Invalid request: body is not valid UTF-8\" };\n }\n let body: unknown;\n try {\n body = JSON.parse(text) as unknown;\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"unknown\";\n return { error: `Malformed JSON: ${detail}` };\n }\n if (!isJsonObject(body)) {\n return { error: \"Request body must be a JSON object\" };\n }\n const rawFilename = body[\"filename\"];\n const purpose = body[\"purpose\"];\n if (typeof rawFilename !== \"string\") {\n return { error: \"Invalid parameter: 'filename' must be a string\" };\n }\n const filenameProblem = filenameError(rawFilename);\n if (filenameProblem !== undefined) return { error: filenameProblem };\n // Blank is no filename at all, exactly as on the multipart path; the\n // synthesized name below is the same pure function of the content there.\n const filename = rawFilename.trim() === \"\" ? undefined : rawFilename;\n if (typeof purpose !== \"string\" || purpose.length === 0) {\n return { error: \"Invalid parameter: 'purpose' must be a non-empty string\" };\n }\n if (!CREATE_PURPOSES.has(purpose)) {\n return { error: invalidPurpose(purpose) };\n }\n // `bytes` is a documented alias of `content`, but it is ALSO the name of the\n // numeric byte-count on a returned file object — so a caller that echoes a\n // `FileObject` back at the upload route would otherwise have `42` stored as\n // its content. Both spellings on one body are ambiguous, so they are rejected\n // rather than silently resolved by precedence, and neither is coerced: a\n // non-string payload is a 400, never `String(value)`.\n const hasContent = body[\"content\"] !== undefined;\n const hasBytesAlias = body[\"bytes\"] !== undefined;\n if (hasContent && hasBytesAlias) {\n return {\n error:\n \"Invalid parameter: 'content' and its alias 'bytes' are mutually exclusive; send exactly one\",\n };\n }\n const field = hasContent ? \"content\" : \"bytes\";\n const content = hasContent ? body[\"content\"] : body[\"bytes\"];\n if (content === undefined) {\n return { error: \"Invalid parameter: 'content' is required (or its alias 'bytes')\" };\n }\n if (typeof content !== \"string\") {\n return {\n error: `Invalid parameter: '${field}' must be a string, got ${jsonTypeOf(content)}`,\n };\n }\n if (LONE_SURROGATE_RE.test(content)) {\n return {\n error:\n `Invalid parameter: '${field}' contains an unpaired UTF-16 surrogate and is not valid text. ` +\n `aimock would have to store it as U+FFFD, so the bytes served back by ` +\n `GET /v1/files/{id}/content would not be the bytes you sent. Send binary ` +\n `as a multipart/form-data upload, which is stored verbatim.`,\n };\n }\n const bytes = Buffer.from(content, \"utf8\");\n if (bytes.length > FILES_MAX_BYTES) {\n return { error: `File content exceeds ${FILES_MAX_BYTES} byte cap` };\n }\n return { filename: filename ?? synthesizedFilename(bytes), purpose, content: bytes };\n}\n\n/**\n * One parsed `multipart/form-data` body part: its headers (field names\n * lower-cased, values trimmed) and its body as the exact octets that sat\n * between the two boundary delimiter lines.\n */\ninterface MultipartPart {\n /** The `name` parameter of `Content-Disposition`. Always present. */\n name: string;\n /** The `filename`/`filename*` parameter, or `undefined` when absent. */\n filename?: string;\n headers: Map<string, string>;\n body: Buffer;\n}\n\nconst HEADER_TERMINATOR = Buffer.from(\"\\r\\n\\r\\n\");\n\n/** RFC 9110 `token` characters — the legal set for a header field name. */\nconst TOKEN_CHAR = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\n\n/** C0 controls plus DEL — never legitimate in a filename we echo back. */\n// eslint-disable-next-line no-control-regex\nconst CONTROL_CHARS = /[\u0000-\u001f]/;\n\n/**\n * Is this request body `multipart/form-data`?\n *\n * Compares the media type's *essence* (RFC 9110 §8.3.1: everything before the\n * first `;`), not a substring of the whole header. A substring test routed\n * `application/json; note=\"multipart/form-data\"` — and anything else that\n * merely mentions the token in a parameter — into the multipart parser, which\n * then 400'd about a missing `boundary` directive on a perfectly good JSON\n * body.\n */\nfunction isMultipartFormData(contentType: string | undefined): boolean {\n if (contentType === undefined) return false;\n const semi = contentType.indexOf(\";\");\n const essence = (semi === -1 ? contentType : contentType.slice(0, semi)).trim().toLowerCase();\n return essence === \"multipart/form-data\";\n}\n\n/**\n * Locate the next *boundary delimiter line* at or after `from`.\n *\n * RFC 2046 §5.1.1 defines the delimiter as `CRLF \"--\" boundary` followed by\n * either transport-padding + CRLF (another part follows) or `--` (the close\n * delimiter). Both halves matter:\n *\n * - The **leading CRLF is part of the delimiter**, not of the preceding body.\n * The previous implementation split on a bare `--boundary`, so a payload\n * whose bytes happened to contain that sequence was truncated at it and\n * stored short — with a 200, silently, which is the worst possible answer\n * from a module whose contract is byte-for-byte storage.\n * - An occurrence **not followed by** CRLF or `--` is not a delimiter line at\n * all, so it is ordinary body content and the scan continues past it. That\n * is what keeps a payload containing `CRLF--boundary` plus arbitrary bytes\n * byte-exact. (undici's `Response.formData()` rejects such a body outright;\n * see the note on {@link parseMultipartUpload}.)\n */\nfunction nextDelimiter(\n buf: Buffer,\n delimiter: Buffer,\n from: number,\n): { at: number; next: number; closing: boolean } | undefined {\n let at = buf.indexOf(delimiter, from);\n while (at !== -1) {\n const tail = delimiterTail(buf, at + delimiter.length);\n if (tail) return { at, ...tail };\n at = buf.indexOf(delimiter, at + 1);\n }\n return undefined;\n}\n\n/**\n * Classify what follows a `CRLF \"--\" boundary` match: transport-padding + CRLF\n * (another part follows), `--` (the close delimiter), or neither — in which\n * case the match is not a delimiter line at all and the caller keeps scanning.\n * Split out of {@link nextDelimiter} so {@link parseMultipartUpload} can apply\n * the same rule to an opening delimiter whose leading CRLF is not present.\n */\nfunction delimiterTail(buf: Buffer, after: number): { next: number; closing: boolean } | undefined {\n if (buf[after] === 0x2d && buf[after + 1] === 0x2d) return { next: after + 2, closing: true };\n // transport-padding = *LWSP-char\n let j = after;\n while (buf[j] === 0x20 || buf[j] === 0x09) j += 1;\n if (buf[j] === 0x0d && buf[j + 1] === 0x0a) return { next: j + 2, closing: false };\n return undefined;\n}\n\n/**\n * Parse a header parameter list (`value; attr=token; attr=\"quoted\"`).\n *\n * Hand-rolled *matching* is what broke the old header handling: `name` and\n * `filename` were matched with regexes against the whole header block, so a\n * `[^\"]+` capture ran across a CRLF and swallowed the next header line (an\n * upload whose filename parameter embedded a CRLF plus `X-Evil: y.txt` was\n * accepted with a 200 and the injected header text stored as the filename),\n * and the `\\s*` in the encoding regexes did the same. Walking one header's\n * value with a real quoted-string scanner cannot cross a line, because the\n * value it is handed never contains one.\n *\n * A repeated attribute is an error rather than a last-one-wins overwrite.\n * `filename=\"a.txt\"; filename=\"b.png\"` decided both the name echoed on the file\n * object and the `Content-Type` served back by `GET /content` (a text upload\n * came back as `image/png`), and RFC 7578 §4.2 gives no precedence rule to pick\n * a winner by — the same reason duplicate `purpose` and payload parts are\n * refused below. Ambiguity is never resolved by precedence in this module.\n */\ntype HeaderParams = { value: string; params: Map<string, string> };\ntype HeaderParamsError = { error: \"malformed\" } | { error: \"duplicate\"; param: string };\n\nfunction parseHeaderParams(value: string): HeaderParams | HeaderParamsError {\n const params = new Map<string, string>();\n const semi = value.indexOf(\";\");\n const head = (semi === -1 ? value : value.slice(0, semi)).trim();\n let i = semi === -1 ? value.length : semi;\n while (i < value.length) {\n i += 1; // consume the ';'\n while (value[i] === \" \" || value[i] === \"\\t\") i += 1;\n if (i >= value.length) break; // trailing ';'\n const eq = value.indexOf(\"=\", i);\n if (eq === -1) return MALFORMED_HEADER; // a parameter with no value\n const attr = value.slice(i, eq).trim().toLowerCase();\n if (!TOKEN_CHAR.test(attr)) return MALFORMED_HEADER;\n if (params.has(attr)) return { error: \"duplicate\", param: attr };\n i = eq + 1;\n if (value[i] === '\"') {\n i += 1;\n let out = \"\";\n for (;;) {\n if (i >= value.length) return MALFORMED_HEADER; // unterminated quoted-string\n const ch = value[i];\n if (ch === \"\\\\\" && i + 1 < value.length) {\n out += value[i + 1];\n i += 2;\n continue;\n }\n if (ch === '\"') {\n i += 1;\n break;\n }\n out += ch;\n i += 1;\n }\n params.set(attr, out);\n while (value[i] === \" \" || value[i] === \"\\t\") i += 1;\n if (i < value.length && value[i] !== \";\") return MALFORMED_HEADER;\n } else {\n const end = value.indexOf(\";\", i);\n params.set(attr, (end === -1 ? value.slice(i) : value.slice(i, end)).trim());\n i = end === -1 ? value.length : end;\n }\n }\n return { value: head, params };\n}\n\nconst MALFORMED_HEADER: HeaderParamsError = { error: \"malformed\" };\n\n/** Render a {@link parseHeaderParams} failure as a 400 message naming the header. */\nfunction headerParamError(err: HeaderParamsError, header: string, where: string): string {\n return err.error === \"duplicate\"\n ? `Invalid request: duplicate '${err.param}' parameter on the ${header} header in ${where}`\n : `Invalid request: malformed ${header} header in ${where}`;\n}\n\n/**\n * Decode an RFC 5987 `ext-value` (`charset'language'pct-encoded`), the form a\n * `filename*` parameter takes. RFC 6266 §4.3 says a recipient that understands\n * it SHOULD prefer it over the plain `filename`, so {@link parsePart} does.\n * A client that sends only `filename*=` therefore keeps the name it sent\n * instead of receiving a synthesized one.\n */\nfunction decodeExtValue(raw: string): string | undefined {\n const first = raw.indexOf(\"'\");\n const second = raw.indexOf(\"'\", first + 1);\n if (first === -1 || second === -1) return undefined;\n const charset = raw.slice(0, first).toLowerCase();\n if (charset !== \"utf-8\" && charset !== \"iso-8859-1\") return undefined;\n const octets: number[] = [];\n const pct = raw.slice(second + 1);\n for (let i = 0; i < pct.length; i += 1) {\n if (pct[i] === \"%\") {\n const hex = pct.slice(i + 1, i + 3);\n if (!/^[0-9A-Fa-f]{2}$/.test(hex)) return undefined;\n octets.push(parseInt(hex, 16));\n i += 2;\n } else {\n const code = pct.charCodeAt(i);\n if (code > 0x7f) return undefined;\n octets.push(code);\n }\n }\n const bytes = Buffer.from(octets);\n // latin1 maps every octet, so only the utf-8 branch can fail — and it must\n // fail LOUDLY. `Buffer.toString(\"utf8\")` substitutes U+FFFD for an invalid\n // sequence, so `filename*=UTF-8''%FF%FE.txt` was stored (with a 200) under a\n // name the client never sent, while the neighbouring malformed-parameter\n // cases got a 400. A name that cannot be decoded is a malformed parameter.\n if (charset === \"iso-8859-1\") return bytes.toString(\"latin1\");\n try {\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n } catch {\n return undefined;\n }\n}\n\n/** Turn one part's raw octets into headers + body, or name why it is malformed. */\nfunction parsePart(partBytes: Buffer, index: number): MultipartPart | { error: string } {\n const where = `multipart part #${index + 1}`;\n let headerText: string;\n let bodyStart: number;\n if (partBytes[0] === 0x0d && partBytes[1] === 0x0a) {\n // `body-part = MIME-part-headers CRLF *OCTET` with an empty header block.\n headerText = \"\";\n bodyStart = 2;\n } else {\n const end = partBytes.indexOf(HEADER_TERMINATOR);\n if (end === -1) {\n // Skipping such a part would report the NEXT missing thing instead: a\n // body whose parts use bare LF separators would be answered \"multipart\n // 'purpose' field is required\" even though `purpose` was sent. The 400\n // names the part that is actually malformed.\n return {\n error: `Invalid request: malformed ${where} — its headers are not terminated by a blank line`,\n };\n }\n headerText = partBytes.subarray(0, end).toString(\"utf8\");\n bodyStart = end + 4;\n }\n\n const headers = new Map<string, string>();\n if (headerText !== \"\") {\n for (const line of headerText.split(\"\\r\\n\")) {\n const colon = line.indexOf(\":\");\n const fieldName = colon === -1 ? \"\" : line.slice(0, colon);\n if (!TOKEN_CHAR.test(fieldName)) {\n return { error: `Invalid request: malformed header line in ${where}` };\n }\n const field = fieldName.toLowerCase();\n // Last-one-wins on a repeated header defeated the very contracts this\n // parser exists to enforce: `Content-Transfer-Encoding: base64` followed\n // by `Content-Transfer-Encoding: 7bit` sailed past the encoding check\n // and stored the literal base64 TEXT as the file's octets (a 200, four\n // bytes of `aGk=`), and a repeated `Content-Disposition` could pair one\n // part's filename with another's rule. RFC 9110 §5.3 only permits a\n // repeated field for a list-valued one, which none of these are, so\n // there is nothing to combine and no defensible precedence.\n if (headers.has(field)) {\n return { error: `Invalid request: duplicate '${fieldName.trim()}' header in ${where}` };\n }\n headers.set(field, line.slice(colon + 1).trim());\n }\n }\n\n const disposition = headers.get(\"content-disposition\");\n if (disposition === undefined) {\n return { error: `Invalid request: ${where} has no Content-Disposition header` };\n }\n const parsed = parseHeaderParams(disposition);\n if (\"error\" in parsed) {\n return { error: headerParamError(parsed, \"Content-Disposition\", where) };\n }\n // RFC 7578 §4.2: \"Each part MUST contain a Content-Disposition header field\n // where the disposition type is 'form-data'.\" Any other type was accepted\n // unchecked, so `Content-Disposition: attachment; name=\"file\"` — which is a\n // response-side disposition, not a form part — uploaded a file.\n const dispositionType = parsed.value.toLowerCase();\n if (dispositionType !== \"form-data\") {\n return {\n error:\n `Invalid request: ${where} has Content-Disposition type '${parsed.value}'; ` +\n `RFC 7578 section 4.2 requires 'form-data'`,\n };\n }\n const name = parsed.params.get(\"name\");\n if (name === undefined) {\n // RFC 7578 §4.2 makes `name` mandatory on every form-data part.\n return {\n error: `Invalid request: ${where} has no 'name' parameter on its Content-Disposition header`,\n };\n }\n\n const star = parsed.params.get(\"filename*\");\n const filename = star !== undefined ? decodeExtValue(star) : parsed.params.get(\"filename\");\n if (star !== undefined && filename === undefined) {\n return { error: `Invalid request: malformed 'filename*' parameter in ${where}` };\n }\n\n return { name, filename, headers, body: partBytes.subarray(bodyStart) };\n}\n\n/**\n * Read a part's content encoding header. An empty value reads as absent: the\n * old regex used `\\s*` after the colon, which matches a CRLF, so an empty\n * `Content-Transfer-Encoding:` captured the *next* header line and 400'd\n * naming `content-type: image/png` as the unsupported encoding.\n */\nfunction partEncoding(part: MultipartPart, header: string): string | undefined {\n const value = part.headers.get(header)?.trim().toLowerCase();\n return value === undefined || value === \"\" ? undefined : value;\n}\n\n/**\n * Parse a `multipart/form-data` upload body.\n *\n * ## Why this is not `Response.formData()`\n *\n * Node's built-in (undici) `Response.formData()` is a spec-compliant multipart\n * parser and was evaluated first, per the \"use the platform, do not hand-roll\n * a codec\" rule. It fails three of this module's requirements:\n *\n * 1. **Byte fidelity.** It throws `expected CRLF` on any body whose payload\n * contains the boundary token, including the RFC-legal bare `--boundary`\n * with no preceding CRLF. Those bytes are storable and this module promises\n * to store them.\n * 2. **Filename-less payload parts.** A part with no `filename` parameter comes\n * back as a *string*, decoded as UTF-8, so binary octets are destroyed\n * (`00 ff fe ...` becomes `00 efbfbd efbfbd ...`). Such a part is legal\n * input here and already round-tripped byte-exact.\n * 3. **Part headers.** `File` exposes only `name`, `type` and `size`. There is\n * no way to see a part's `Content-Transfer-Encoding`, so the \"reject an\n * encoding we do not apply\" contract above could not be implemented at all\n * and a `base64` part would be stored in its encoded form — the exact bug\n * that contract exists to prevent.\n *\n * So the framing stays local, but it is written to RFC 2046 §5.1.1 and\n * RFC 7578 rather than to a `split()`: see {@link nextDelimiter} for the\n * delimiter rule and {@link parsePart} for the header rule.\n *\n * Bytes never round-trip through a string. Part bodies are `subarray`s of the\n * request buffer, so what is parsed is literally what arrived on the wire — but\n * the payload part is COPIED out of that view before it is returned, because a\n * `subarray` retains its whole backing buffer (see the copy below). The scan\n * itself never copies the body either, so peak residency during a parse is one\n * request body, not two.\n */\nfunction parseMultipartUpload(\n rawBytes: Buffer,\n contentType: string | undefined,\n): ParsedUpload | { error: string } {\n // Parsed as a real parameter list rather than with a first-match regex: the\n // regex read `boundary=` out of ANOTHER parameter's quoted string, so\n // `multipart/form-data; name=\"x boundary=FAKE\"; boundary=REAL` searched for\n // the decoy and 400'd a perfectly good body as having \"no boundary\n // delimiter\". `extractBoundary` (transcription.ts) keeps its regex: its\n // callers hand it whole request bodies scanned as text, not this one header.\n const parsedType = parseHeaderParams(contentType ?? \"\");\n if (\"error\" in parsedType) {\n return { error: headerParamError(parsedType, \"Content-Type\", \"the request\") };\n }\n const boundary = parsedType.params.get(\"boundary\");\n if (!boundary) {\n // Without a boundary there is no way to find the part edges, so every\n // earlier best-effort path here silently stored a 0-byte file and threw\n // the caller's bytes away. A multipart body that cannot be parsed is a\n // client error, not an empty upload.\n return {\n error: \"Invalid request: multipart/form-data Content-Type is missing a 'boundary' directive\",\n };\n }\n\n // Everything before the opening delimiter is the preamble. That delimiter\n // alone has no preceding CRLF when the preamble is empty; matching it used\n // to mean parsing `Buffer.concat([CRLF, rawBytes])`, a second copy of the\n // whole request body for the sake of two octets. So it is special-cased and\n // the scan runs over the request buffer in place.\n const buf = rawBytes;\n const delimiter = Buffer.from(`\\r\\n--${boundary}`, \"latin1\");\n const dashBoundary = Buffer.from(`--${boundary}`, \"latin1\");\n const opening = buf.subarray(0, dashBoundary.length).equals(dashBoundary)\n ? delimiterTail(buf, dashBoundary.length)\n : undefined;\n // `at` is only ever read as the END of the preceding part's body, which the\n // opening delimiter has none of, so 0 stands in for it.\n let delim = opening ? { at: 0, ...opening } : nextDelimiter(buf, delimiter, 0);\n if (!delim) {\n return { error: \"Invalid request: multipart body contains no boundary delimiter\" };\n }\n\n const parts: MultipartPart[] = [];\n while (!delim.closing) {\n const start = delim.next;\n const following = nextDelimiter(buf, delimiter, start);\n if (!following) {\n return {\n error: \"Invalid request: multipart body ended before its closing boundary delimiter\",\n };\n }\n const part = parsePart(buf.subarray(start, following.at), parts.length);\n if (\"error\" in part) return part;\n parts.push(part);\n delim = following;\n }\n\n // Encoding first, and across every part: the message must name the part that\n // actually declared the encoding, rather than let an encoded value fall\n // through to a downstream check that reports something else entirely.\n for (const part of parts) {\n const transfer = partEncoding(part, \"content-transfer-encoding\");\n if (transfer !== undefined && !IDENTITY_TRANSFER_ENCODINGS.has(transfer)) {\n return { error: unsupportedPartEncoding(\"Content-Transfer-Encoding\", transfer, part.name) };\n }\n const encoding = partEncoding(part, \"content-encoding\");\n if (encoding !== undefined && encoding !== \"identity\") {\n return { error: unsupportedPartEncoding(\"Content-Encoding\", encoding, part.name) };\n }\n }\n\n // A part whose `name` is none of `purpose`/`file`/`content` is IGNORED, not\n // rejected. That is deliberate, and it is the JSON path's rule too: an\n // unknown key on a JSON upload body is ignored there (only `filename`,\n // `purpose` and `content`/`bytes` are read), so the two body shapes answer\n // the same request the same way. The vendored spec surface gives no reason to\n // be stricter either — `FileCreateParams` in openai 4.104.0\n // (`resources/files.d.ts`) declares exactly `file` and `purpose` and says\n // nothing about rejecting extras, so a 400 here would be this mock inventing\n // a rejection it cannot show the real API makes. Every part is still parsed\n // and still encoding-checked above, so an ignored part cannot smuggle\n // anything past a contract — it only fails to contribute.\n const purposeParts = parts.filter((p) => p.name === \"purpose\");\n if (purposeParts.length > 1) {\n // Last-one-wins silently picked a purpose the caller never meant to send.\n // There is no defensible precedence, so this gets the same answer that\n // duplicate payload parts already got.\n return {\n error: \"Invalid request: multipart body has more than one 'purpose' part; send exactly one\",\n };\n }\n const payloadParts = parts.filter((p) => p.name === \"file\" || p.name === \"content\");\n if (payloadParts.length > 1) {\n // Two payload parts means the filename from one can be paired with the\n // bytes of the other. There is no defensible precedence, so reject.\n const [first, second] = payloadParts;\n return {\n error: `Invalid request: multipart body has more than one payload part ('${first.name}' and '${second.name}'); send exactly one 'file' or 'content' part`,\n };\n }\n\n // NOT trimmed: `\" batch \"` is a `400` here exactly as it is on the JSON path.\n const purpose = purposeParts[0]?.body.toString(\"utf8\");\n if (!purpose) {\n return { error: \"Invalid parameter: multipart 'purpose' field is required\" };\n }\n if (!CREATE_PURPOSES.has(purpose)) {\n return { error: invalidPurpose(purpose) };\n }\n const payload = payloadParts[0];\n if (payload === undefined) {\n return {\n error: \"Invalid request: multipart body must include a 'file' part (or its 'content' alias)\",\n };\n }\n\n // The payload part's own Content-Type header is intentionally ignored: it is\n // client-controlled and must never decide what we serve back. Real SDKs send\n // `application/octet-stream` for every binary part anyway, so the filename is\n // both the safer and the more accurate signal.\n if (payload.body.length > FILES_MAX_BYTES) {\n return { error: `File content exceeds ${FILES_MAX_BYTES} byte cap` };\n }\n // Copy, do not hand out the view. Every part body above is a `subarray` of\n // the request buffer, and a `subarray` keeps its whole backing `ArrayBuffer`\n // alive — so storing one made a 2-byte file pin its entire request body, up\n // to `FILES_BODY_MAX_BYTES` (~62.9 MB), for as long as the file existed.\n // That silently falsified the per-file memory bound this module documents at\n // the top. `Buffer.alloc` + `copy` rather than `Buffer.from(view)` because\n // the latter draws small allocations from the shared `Buffer` pool,\n // which is a smaller version of the same aliasing. The copy is bounded by\n // the cap just checked, and it is the only copy of the payload made here.\n const content = Buffer.alloc(payload.body.length);\n payload.body.copy(content);\n\n // CR and LF can no longer reach the filename here (a header value cannot span\n // a line), but NUL and friends still can — and the JSON path has no such\n // structural protection, which is why the check is shared.\n let filename = payload.filename;\n if (filename !== undefined) {\n const problem = filenameError(filename);\n if (problem !== undefined) return { error: problem };\n }\n // A blank or whitespace-only filename is no filename at all: `\" \"` is\n // neither a usable name nor worth a 400, so it falls through to the\n // synthesized name below.\n if (filename !== undefined && filename.trim() === \"\") filename = undefined;\n\n if (filename === undefined) {\n // A filename-less `file` part is legal input: `CreateFileRequest` in the\n // `openai/openai-openapi` spec declares `file` as `type: string, format:\n // binary` with no `filename` property at all — the name only ever rides in\n // `Content-Disposition`, and the spec never requires it. The *response*\n // schema (`OpenAIFile`) does require `filename`, so the faithful answer is\n // to synthesize one rather than reject with a 400.\n //\n // Deterministic means a pure function of the uploaded bytes: the same\n // upload always yields the same name (so a test can assert on it, across\n // processes and across `clearFileStore()`), and different bytes yield\n // different names (a 96-bit SHA-256 prefix, so collisions are not a\n // practical concern). It is deliberately NOT a clock or a counter — both\n // would make two identical uploads disagree.\n filename = synthesizedFilename(content);\n }\n return { filename, purpose, content };\n}\n\nexport async function handleFilesCreate(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: Buffer | typeof FILES_BODY_OVERSIZED,\n journal: Journal,\n defaults: {\n logger: Logger;\n chaos?: ChaosDefaults;\n registry?: MetricsRegistry;\n },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? \"/v1/files\";\n const method = req.method ?? \"POST\";\n setCorsHeaders(res);\n\n if (\n await applyChaosAsync(\n res,\n null,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"files\",\n },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n ) {\n return;\n }\n\n // After the chaos gate on purpose: an over-size body is still a request the\n // fault injector is entitled to answer for.\n if (raw === FILES_BODY_OVERSIZED) {\n rejectFiles(\n res,\n journal,\n defaults.logger,\n method,\n path,\n flattenHeaders(req.headers),\n oversizedBody(),\n setCorsHeaders,\n );\n return;\n }\n\n const contentType = Array.isArray(req.headers[\"content-type\"])\n ? req.headers[\"content-type\"][0]\n : req.headers[\"content-type\"];\n const parsed = isMultipartFormData(contentType)\n ? parseMultipartUpload(raw, contentType)\n : // Non-multipart bodies are JSON, which is utf8 by definition (RFC 8259).\n parseJsonUpload(raw);\n\n if (\"error\" in parsed) {\n rejectFiles(\n res,\n journal,\n defaults.logger,\n method,\n path,\n flattenHeaders(req.headers),\n parsed.error,\n setCorsHeaders,\n );\n return;\n }\n\n const obj = storeFile(parsed);\n const { id, bytes } = obj;\n\n defaults.logger.debug(`Files mock: stored ${id} (${parsed.filename}, ${bytes} bytes)`);\n writeJson(res, 200, obj, setCorsHeaders);\n journalFiles(journal, method, path, flattenHeaders(req.headers), 200, filesUploadBody(parsed));\n}\n\n/**\n * `GET /v1/files` paging bounds, taken from the `openai/openai-openapi` spec\n * (`operationId: listFiles`): \"Limit can range between 1 and 10,000, and the\n * default is 10,000.\" Files is the odd one out among the list surfaces — most\n * default to 20 — so the default is spelled out here rather than shared.\n */\nexport const FILES_LIST_MAX_LIMIT = 10000;\nexport const FILES_LIST_DEFAULT_LIMIT = FILES_LIST_MAX_LIMIT;\n\n/**\n * An unsigned decimal integer literal and nothing else — no sign, no exponent,\n * no radix prefix, no surrounding whitespace. See {@link parseListQuery} for\n * what each of those would otherwise be silently accepted as.\n *\n * A long digit run is still safe to hand to `Number()`: it stays finite and\n * integral, so it fails the range check below rather than the type check.\n */\n// TODO(integration): dedupe strict integer parsers — the fine-tuning and chaos\n// surfaces each grew their own local copy of this on separate branches.\nconst DECIMAL_INTEGER_RE = /^[0-9]+$/;\n\ninterface FilesListQuery {\n limit: number;\n order: \"asc\" | \"desc\";\n after?: string;\n purpose?: string;\n}\n\n/**\n * The single non-empty value of a list query parameter, or the 400 it earns.\n *\n * ONE rule for all four of `limit`, `order`, `after` and `purpose`, because the\n * three ways of reading them used to disagree on the same URL:\n *\n * - REPEATED is a 400. `URLSearchParams.get` answers with the FIRST of a\n * repeated parameter and launders the rest, so `?limit=1&limit=abc` paged\n * at 1 and never looked at `abc` — a caller that built its URL twice over\n * got a silent success for a value it never meant. `listFiles` declares\n * each of the four as a single scalar and gives no combining rule for a\n * repeat, so a repeat is a caller error. Matches the multipart body, where\n * two `purpose` parts are already a 400 rather than a last-one-wins.\n * - PRESENT-BUT-EMPTY is a 400. `?order=` was already one (no member of the\n * enum is \"\"), but `?after=` meant \"no cursor\" and `?purpose=` meant \"no\n * filter\" and handed back the WHOLE store — the exact opposite of the\n * narrowing the caller asked for. An empty string is not an object id and\n * not a purpose any stored file can carry, and `?x=` is what building a\n * URL from a partly-undefined params object emits, which is a bug worth\n * showing. Omitting the parameter is how you say \"no filter\".\n * - ABSENT is `null`, and each caller applies its own default.\n *\n * Both 400s are OURS, not the vendor's: the spec states neither. They are the\n * mock's standing bias towards failing loudly over answering a question the\n * caller did not ask.\n */\nfunction singleParam(\n params: URLSearchParams,\n name: string,\n): { ok: true; value: string | null } | { ok: false; error: string } {\n const all = params.getAll(name);\n if (all.length > 1) {\n return {\n ok: false,\n error: `Invalid parameter: '${name}' was given ${all.length} times; it takes a single value`,\n };\n }\n if (all.length === 0) return { ok: true, value: null };\n if (all[0] === \"\") {\n return {\n ok: false,\n error: `Invalid parameter: '${name}' was given an empty value; omit it instead`,\n };\n }\n return { ok: true, value: all[0] };\n}\n\n/**\n * Parse the `limit` / `order` / `after` / `purpose` parameters off a list URL.\n *\n * Shapes come from `operationId: listFiles` in `openai/openai-openapi` and from\n * `FileListParams extends CursorPageParams` in the vendored SDK (openai\n * 4.104.0 — `resources/files.d.ts` gives `order?: 'asc' | 'desc'` and\n * `purpose?: string`; `pagination.d.ts` gives `after?: string` and\n * `limit?: number`). `order` defaults to `desc`, so the newest file comes back\n * first.\n *\n * All four are read here, through {@link singleParam}, so there is exactly one\n * place that decides what a repeated or empty list parameter means. `purpose`\n * used to be read separately in `src/server.ts` and passed in already parsed,\n * which is how it ended up with neither rule.\n */\nfunction parseListQuery(rawUrl: string): FilesListQuery | { error: string } {\n // Everything after the first `?` is query data, read as-is. `new URL(target,\n // base).searchParams` cannot be used here: it applies the fragment rule, and\n // a request target has no fragment component (RFC 9112 section 3.2), so an\n // origin server sees `?purpose=batch#frag` as the purpose `batch#frag` — not\n // as `batch` plus a fragment nobody sent. Truncating at the `#` turns a value\n // that matches nothing into a plausible-looking one that matches files.\n // `new URLSearchParams(string)` cannot throw, so no parse failure is hidden.\n const queryStart = rawUrl.indexOf(\"?\");\n const params = new URLSearchParams(queryStart === -1 ? \"\" : rawUrl.slice(queryStart + 1));\n\n const rawOrder = singleParam(params, \"order\");\n if (!rawOrder.ok) return { error: rawOrder.error };\n const orderRaw = rawOrder.value;\n if (orderRaw !== null && orderRaw !== \"asc\" && orderRaw !== \"desc\") {\n return { error: \"Invalid parameter: 'order' must be one of: asc, desc\" };\n }\n\n const rawLimit = singleParam(params, \"limit\");\n if (!rawLimit.ok) return { error: rawLimit.error };\n let limit = FILES_LIST_DEFAULT_LIMIT;\n if (rawLimit.value !== null) {\n // The digit gate runs BEFORE Number(), because Number() is far wider than\n // the \"integer between 1 and 10,000\" this branch's own error message\n // promises: it reads `0x10` as 16, `1e3` as 1000, and strips whitespace and\n // a leading sign, so `\" 5\"`, `\"5\\n\"` and `\"+5\"` are all 5. Every one of\n // those is `Number.isInteger` and inside the range, so without the gate\n // `?limit=0x10` would page silently at 16 items. A caller sending any of\n // them has a bug, and a mock's job is to show it.\n //\n // Reject, never clamp: an out-of-range limit gets the same 400 as a\n // malformed one, so a suite asserting on page size is never quietly handed\n // a different size. (`?limit=` no longer reaches here — the empty value is\n // already a 400 in singleParam, with the same message every parameter\n // gets.)\n const parsed = DECIMAL_INTEGER_RE.test(rawLimit.value) ? Number(rawLimit.value) : Number.NaN;\n if (!Number.isInteger(parsed) || parsed < 1 || parsed > FILES_LIST_MAX_LIMIT) {\n return {\n error: `Invalid parameter: 'limit' must be an integer between 1 and ${FILES_LIST_MAX_LIMIT}`,\n };\n }\n limit = parsed;\n }\n\n const rawAfter = singleParam(params, \"after\");\n if (!rawAfter.ok) return { error: rawAfter.error };\n\n const rawPurpose = singleParam(params, \"purpose\");\n if (!rawPurpose.ok) return { error: rawPurpose.error };\n\n return {\n limit,\n order: orderRaw ?? \"desc\",\n after: rawAfter.value ?? undefined,\n purpose: rawPurpose.value ?? undefined,\n };\n}\n\nexport async function handleFilesList(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n defaults: { logger: Logger; chaos?: ChaosDefaults; registry?: MetricsRegistry },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? \"/v1/files\";\n const method = req.method ?? \"GET\";\n setCorsHeaders(res);\n\n if (\n await applyChaosAsync(\n res,\n null,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: null, service: \"files\" },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n ) {\n return;\n }\n\n const query = parseListQuery(path);\n if (\"error\" in query) {\n rejectFiles(\n res,\n journal,\n defaults.logger,\n method,\n path,\n flattenHeaders(req.headers),\n query.error,\n setCorsHeaders,\n );\n return;\n }\n\n let data = [...fileStore.values()];\n if (query.purpose !== undefined) {\n // `purpose` is an UNVALIDATED free-form string here, on purpose. The real\n // API does not constrain it: `listFiles` declares the query parameter as a\n // bare `type: string` with no `enum` in `openai/openai-openapi` (contrast\n // `order`, which carries `enum: [asc, desc]` in the very same parameter\n // list), and the SDK types it `purpose?: string` on `FileListParams` while\n // typing create as `purpose: FilePurpose` (openai@4.104.0,\n // resources/files.d.ts). So an unknown or server-side-only purpose is not a\n // 400: it simply matches no stored file and yields `{object: \"list\", data:\n // []}` with a 200, which is what this filter does.\n // {@link CREATE_PURPOSES} is not consulted.\n //\n // The one shape that is NOT a 200-with-an-empty-list is `?purpose=` with no\n // value: an empty string is not a purpose any stored file can carry, so\n // {@link singleParam} has already turned it into a 400 above, under the\n // same rule the other three list parameters get. Before that, its falsiness\n // skipped this filter entirely and the caller got the whole store back.\n data = data.filter((f) => f.purpose === query.purpose);\n }\n // `fileStore` iterates in creation order and `Array.prototype.sort` is stable,\n // so sorting on `created_at` alone still yields a *total* ascending order even\n // though the second-granularity timestamps tie constantly in a fast test. The\n // `desc` view is that exact order reversed — not a flipped comparator, which\n // would leave tied files in ascending order inside the descending page and\n // break the \"every file exactly once\" property of a cursor walk.\n data.sort((a, b) => a.created_at - b.created_at);\n if (query.order === \"desc\") data.reverse();\n\n let start = 0;\n if (query.after !== undefined) {\n const idx = data.findIndex((f) => f.id === query.after);\n if (idx === -1) {\n // The spec is silent on unknown cursors, so the mock fails loudly:\n // silently restarting at page 1 turns a caller's paging bug into an\n // infinite loop instead of a test failure. Note a cursor is resolved\n // against *this* listing, so a file excluded by `purpose` is unknown here.\n rejectFiles(\n res,\n journal,\n defaults.logger,\n method,\n path,\n flattenHeaders(req.headers),\n `Invalid parameter: 'after' cursor '${query.after}' does not match any file in this listing`,\n setCorsHeaders,\n );\n return;\n }\n start = idx + 1;\n }\n\n const page = data.slice(start, start + query.limit);\n writeJson(\n res,\n 200,\n {\n object: \"list\",\n data: page,\n first_id: page[0]?.id ?? null,\n last_id: page[page.length - 1]?.id ?? null,\n has_more: start + page.length < data.length,\n },\n setCorsHeaders,\n );\n journalFiles(journal, method, path, flattenHeaders(req.headers), 200);\n}\n\nexport async function handleFilesRetrieve(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fileId: string,\n journal: Journal,\n defaults: { logger: Logger; chaos?: ChaosDefaults; registry?: MetricsRegistry },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? `/v1/files/${fileId}`;\n const method = req.method ?? \"GET\";\n // CORS before the chaos gate: applyChaosAsync short-circuits the response, so a\n // faulted retrieve would otherwise reach the browser with zero\n // Access-Control-* headers and be swallowed as a CORS error instead of the\n // 500 the chaos suite is trying to exercise. Create/list already did this.\n setCorsHeaders(res);\n\n if (\n await applyChaosAsync(\n res,\n null,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: null, service: \"files\" },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n ) {\n return;\n }\n\n const found = fileStore.get(fileId);\n if (!found) {\n writeJson(res, 404, invalidRequest(`No such file: ${fileId}`), setCorsHeaders);\n journalFiles(journal, method, path, flattenHeaders(req.headers), 404);\n return;\n }\n writeJson(res, 200, found, setCorsHeaders);\n journalFiles(journal, method, path, flattenHeaders(req.headers), 200);\n}\n\nexport async function handleFilesContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fileId: string,\n journal: Journal,\n defaults: { logger: Logger; chaos?: ChaosDefaults; registry?: MetricsRegistry },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? `/v1/files/${fileId}/content`;\n const method = req.method ?? \"GET\";\n // CORS before the chaos gate — see handleFilesRetrieve.\n setCorsHeaders(res);\n\n if (\n await applyChaosAsync(\n res,\n null,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: null, service: \"files\" },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n ) {\n return;\n }\n\n const found = fileStore.get(fileId);\n const content = fileContents.get(fileId);\n if (!found || content === undefined) {\n writeJson(res, 404, invalidRequest(`No such file: ${fileId}`), setCorsHeaders);\n journalFiles(journal, method, path, flattenHeaders(req.headers), 404);\n return;\n }\n res.writeHead(200, {\n // Derived from the STORED filename, never from the client-declared part\n // header — see CONTENT_TYPE_BY_EXT. nosniff stops a browser second-\n // guessing that decision and rendering opaque bytes as active content.\n \"Content-Type\": contentTypeForFilename(found.filename),\n \"Content-Length\": String(content.bytes.length),\n \"X-Content-Type-Options\": \"nosniff\",\n // Served as a download, never rendered inline — see contentDispositionFor.\n \"Content-Disposition\": contentDispositionFor(found.filename),\n });\n res.end(content.bytes);\n journalFiles(journal, method, path, flattenHeaders(req.headers), 200);\n}\n\nexport async function handleFilesDelete(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fileId: string,\n journal: Journal,\n defaults: { logger: Logger; chaos?: ChaosDefaults; registry?: MetricsRegistry },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? `/v1/files/${fileId}`;\n const method = req.method ?? \"DELETE\";\n // CORS before the chaos gate — see handleFilesRetrieve.\n setCorsHeaders(res);\n\n if (\n await applyChaosAsync(\n res,\n null,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: null, service: \"files\" },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n ) {\n return;\n }\n\n const found = fileStore.get(fileId);\n if (!found) {\n writeJson(res, 404, invalidRequest(`No such file: ${fileId}`), setCorsHeaders);\n journalFiles(journal, method, path, flattenHeaders(req.headers), 404);\n return;\n }\n fileStore.delete(fileId);\n fileContents.delete(fileId);\n writeJson(res, 200, { id: fileId, object: \"file\", deleted: true }, setCorsHeaders);\n journalFiles(journal, method, path, flattenHeaders(req.headers), 200);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"files.js","names":[],"sources":["../src/files.ts"],"sourcesContent":["/**\n * OpenAI Files API mock for aimock.\n *\n * Implements the subset of `POST /v1/files`, `GET /v1/files`,\n * `GET /v1/files/{id}`, `GET /v1/files/{id}/content` and\n * `DELETE /v1/files/{id}` that test harnesses need to exercise\n * file-upload flows without touching the real API.\n *\n * Storage is in-memory and per-process (like the video job maps):\n * - metadata lives in `fileStore`\n * - raw bytes live in `fileContents` as `Buffer`s. Bytes are never\n * round-tripped through a utf8 string, so binary uploads come back\n * byte-for-byte identical.\n * - `clearFileStore()` wipes both and is wired into the full reset path\n * (`POST /__aimock/reset` + `LLMock.reset()`)\n *\n * The only memory bound here is per file: {@link FILES_MAX_BYTES} caps one\n * upload. The file COUNT is deliberately unbounded, so total residency grows\n * with the number of uploads until something calls `clearFileStore()` (or the\n * process exits) — a suite that uploads in a loop without resetting will grow\n * this map without limit. That is a real, documented bound, not an OOM defense.\n *\n * The job maps this module otherwise mirrors (fal-audio.ts,\n * openrouter-video.ts, grok-video.ts, byteplus-video.ts) DO cap entry count and\n * FIFO-evict the oldest, and that convention deliberately does NOT apply here.\n * Those hold transient jobs that the real API also expires, so eviction\n * reproduces real behaviour. A file is a durable object: the real API keeps it\n * until the caller deletes it. Silently evicting one would make the mock answer\n * `404 No such file` for a file the caller successfully uploaded and never\n * deleted — an outcome the real API cannot produce, surfacing as an\n * unexplainable flake in the suite under test. Unbounded growth is the honest\n * failure mode; `POST /__aimock/reset` is the lever that reclaims it.\n *\n * Body handling accepts both shapes real SDKs send:\n * - JSON `{ filename, purpose, content }` (test-friendly; `bytes` is an alias\n * of `content`, and exactly one of the two must be present)\n * - `multipart/form-data` with `purpose` + exactly one payload part named\n * `file` (or its `content` alias), framed per RFC 2046 §5.1.1 so a payload\n * containing the boundary token is stored whole rather than truncated at it\n *\n * Both parsers are strict: a malformed body is an OpenAI-shaped\n * `400 invalid_request_error` naming the offending field or part. Nothing is\n * `String()`-coerced and no missing field falls through to a fabricated\n * default, because a mock that quietly stores `\"[object Object]\"` or a 0-byte\n * file teaches the suite under test that a broken upload succeeded.\n *\n * Every branch journals with `service: \"files\"` so\n * `GET /__aimock/journal?service=files` selects exactly this traffic, and with\n * `source: \"internal\"` because — as with fine-tuning — no fixture or proxy\n * ever serves this store. A successful upload journals a SYNTHETIC body\n * describing it (`{ purpose, filename, bytes, content_type, sha256 }`) — never\n * the uploaded octets — exactly as the transcription route journals its\n * multipart audio; a `400` journals the error envelope it answered with, so\n * the reason is readable from the journal; see {@link journalFiles}. Files also\n * honors the inbound API-key boundary via the server dispatch (no bypass),\n * and runs through the chaos gate so retry/backoff suites can inject\n * 500s at the files surface too.\n */\n\nimport { createHash } from \"node:crypto\";\nimport type * as http from \"node:http\";\nimport { flattenHeaders, generateId, isJsonObject, parseStrictIntegerText } from \"./helpers.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { removeVectorStoreFile } from \"./vector-stores.js\";\nimport type { ChaosDefaults, ChatCompletionRequest, JournalBody } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\n\nexport interface FileObject {\n id: string;\n object: \"file\";\n bytes: number;\n created_at: number;\n filename: string;\n purpose: string;\n status: \"uploaded\" | \"processed\" | \"pending\" | \"error\";\n status_details?: string;\n}\n\n/**\n * Purposes accepted on upload — `CreateFileRequest.purpose` in the official\n * `openai/openai-openapi` spec (and the `FilePurpose` type in the SDK).\n * `user_data` is what Responses-API file inputs use, so rejecting it broke the\n * most current flow.\n */\nconst CREATE_PURPOSES = new Set([\n \"assistants\",\n \"batch\",\n \"fine-tune\",\n \"vision\",\n \"user_data\",\n \"evals\",\n]);\n\n/**\n * Exposed for tests and for callers that want to check a value themselves:\n * the enum this mock enforces on upload.\n */\nexport const FILE_CREATE_PURPOSES: readonly string[] = [...CREATE_PURPOSES];\n\nfunction invalidPurpose(purpose: string): string {\n return `Invalid purpose '${purpose}'. Expected one of: ${[...CREATE_PURPOSES].join(\", \")}`;\n}\n\n/**\n * `Content-Transfer-Encoding` values that are identity transforms (RFC 2045\n * §6.2): the part body already *is* the file's octets, so storing it verbatim\n * is correct. Everything else (`base64`, `quoted-printable`, …) would have to\n * be decoded before storage, and is rejected instead — see\n * {@link unsupportedPartEncoding}.\n */\nconst IDENTITY_TRANSFER_ENCODINGS = new Set([\"7bit\", \"8bit\", \"binary\", \"identity\"]);\n\n/**\n * Reject, rather than silently mis-store, a file part that asks for a content\n * encoding this mock does not apply.\n *\n * We deliberately do NOT implement a decoder. RFC 7578 §4.7 deprecates\n * `Content-Transfer-Encoding` in `multipart/form-data` (\"Senders SHOULD NOT\n * generate any parts with a Content-Transfer-Encoding header field. Currently,\n * no deployed implementations that send such bodies have been discovered\"), and\n * a capture of the clients that actually reach this endpoint — curl `-F`,\n * python `requests`, python `httpx` (the OpenAI Python SDK's transport) and\n * undici `FormData` (the OpenAI Node SDK's transport) — confirms none of them\n * emits one. Storing an encoded body as-is is the harmful outcome: `bytes` and\n * `GET /content` would report the *encoded* form and a hash comparison in a\n * test would fail for a reason the caller cannot see. A loud 400 naming the\n * header is the honest answer for a mock.\n *\n * `identity` counts as a no-op alongside the RFC 2045 §6.2 values: it is the\n * registered `Content-Encoding` for \"no transformation\", and a client that\n * spells it on `Content-Transfer-Encoding` means the same thing. Rejecting it\n * named a problem that did not exist.\n *\n * Every part is checked, not only the payload part, and the message names the\n * part it came from. While only the payload part was checked, a `base64`\n * `purpose` part slid through to the enum check and 400'd with\n * `Invalid purpose 'YXNzaXN0YW50cw=='`, pointing the caller at the wrong thing.\n */\nfunction unsupportedPartEncoding(\n header: \"Content-Transfer-Encoding\" | \"Content-Encoding\",\n value: string,\n partName: string,\n): string {\n // The RFC 7578 §4.7 deprecation is specifically about Content-Transfer-\n // Encoding, so only that branch cites it.\n const rationale =\n header === \"Content-Transfer-Encoding\"\n ? ` RFC 7578 section 4.7 deprecates ${header} in multipart/form-data.`\n : \"\";\n return (\n `Unsupported ${header}: '${value}' on the multipart '${partName}' part. ` +\n `aimock stores part bodies verbatim and does not decode them.${rationale} ` +\n `Send the raw bytes instead.`\n );\n}\n\n/**\n * Content types for the handful of extensions test harnesses actually upload.\n * Anything unrecognised falls back to `application/octet-stream` — the safe\n * default for opaque bytes, and what stops a PNG being served (and re-decoded)\n * as UTF-8 text.\n *\n * This is an allowlist of INERT types on purpose. `text/html` is deliberately\n * absent: the mock serves uploaded bytes back from its own origin, so any\n * active type here would let a stored upload script that origin in a browser.\n * HTML uploads round-trip byte-for-byte, they are just served as opaque bytes.\n */\nconst CONTENT_TYPE_BY_EXT: Record<string, string> = {\n json: \"application/json\",\n // UNVERIFIED against the real API, and left alone for that reason.\n // `application/jsonl` is not IANA-registered; the rest of this repo spells\n // line-delimited JSON `application/x-ndjson` (ndjson-writer.ts, recorder.ts,\n // stream-collapse.ts). Neither `openai/openai-openapi` nor the vendored SDK\n // states what `GET /v1/files/{id}/content` returns for a `.jsonl` upload, and\n // no keyless probe of api.openai.com can answer it, so changing this would be\n // swapping one guess for another. Change it only against a real capture.\n jsonl: \"application/jsonl\",\n txt: \"text/plain; charset=utf-8\",\n md: \"text/markdown; charset=utf-8\",\n csv: \"text/csv; charset=utf-8\",\n pdf: \"application/pdf\",\n png: \"image/png\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n wav: \"audio/wav\",\n mp3: \"audio/mpeg\",\n zip: \"application/zip\",\n};\n\nfunction contentTypeForFilename(filename: string): string {\n const dot = filename.lastIndexOf(\".\");\n if (dot === -1) return \"application/octet-stream\";\n const ext = filename.slice(dot + 1).toLowerCase();\n return CONTENT_TYPE_BY_EXT[ext] ?? \"application/octet-stream\";\n}\n\n/**\n * RFC 5987 §3.2.1 `ext-value` percent-encoding for the `filename*` parameter.\n *\n * `encodeURIComponent` leaves `!'()*-._~` unescaped, but `'`, `(`, `)` and `*`\n * are NOT in RFC 5987's `attr-char` set, so they have to be escaped by hand.\n */\nfunction encodeExtValue(value: string): string {\n return encodeURIComponent(value).replace(\n /['()*]/g,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,\n );\n}\n\n/**\n * `Content-Disposition` for the content route (RFC 6266 §4.1).\n *\n * Always `attachment`. {@link CONTENT_TYPE_BY_EXT} is an allowlist of INERT\n * types precisely so a stored upload cannot execute against the mock's own\n * origin, but `application/pdf` was still being served with no disposition at\n * all, which in every mainstream browser means \"render in the built-in PDF\n * viewer\" — an active surface on this origin, exactly what the allowlist's\n * comment claims is excluded. `attachment` closes that without touching the\n * allowlist, and it also stops a `.json`/`.txt`/`.csv` body being rendered\n * inline if someone opens a content URL in a tab.\n *\n * What the REAL API sends here is UNVERIFIED: probing\n * `api.openai.com/v1/files/{id}/content` needs a key, and neither\n * `openai/openai-openapi` nor the SDK documents the response headers. So this\n * is chosen for safety, not fidelity — and it is inert for SDK callers: the\n * vendored SDK's `files.content()` (openai@4.104.0,\n * `resources/files.js:82-88`) issues the GET with `Accept: application/binary`\n * and `__binaryResponse: true`, handing back the raw `Response` without ever\n * reading `Content-Disposition`.\n *\n * Both parameters are built from the STORED filename, which is attacker-shaped\n * input: a multipart `filename=\"...\"` can carry CR/LF and would otherwise\n * inject response headers. The ASCII fallback replaces every non-printable and\n * non-ASCII byte with `_` and backslash-escapes `\"` and `\\` per the\n * quoted-string rule; `filename*` percent-encodes them. Neither can emit a bare\n * CR or LF.\n */\nfunction contentDispositionFor(filename: string): string {\n const asciiFallback = filename.replace(/[^\\x20-\\x7e]/g, \"_\").replace(/([\"\\\\])/g, \"\\\\$1\");\n const base = `attachment; filename=\"${asciiFallback}\"`;\n // Only add the RFC 5987 form when the name actually needs it, so the common\n // ASCII case stays a single plain parameter.\n if (/^[\\x20-\\x7e]*$/.test(filename)) return base;\n return `${base}; filename*=UTF-8''${encodeExtValue(filename)}`;\n}\n\n/**\n * Per-file content cap. An upload whose decoded content is above this gets a\n * `400` with the usual OpenAI-style error envelope, at every wire size a body\n * can have — see {@link FILES_BODY_MAX_BYTES} for how that is kept true\n * without buffering an unbounded body.\n */\nexport const FILES_MAX_BYTES = 10 * 1024 * 1024;\n\n/**\n * Worst-case wire-to-content expansion of a JSON upload.\n *\n * `content` is a JSON string, so one byte of decoded payload can arrive as as\n * many as six wire bytes: a `\\uXXXX` escape is 6 bytes and the cheapest code\n * point one can carry (U+0000–U+007F) is a single UTF-8 byte. Every other\n * code point does better — U+0800–U+FFFF is 3 UTF-8 bytes per 6 wire bytes,\n * a surrogate pair 4 per 12 — and a multipart part is 1:1 plus its envelope.\n * So six is an upper bound on wire → content expansion for both parsers.\n */\nconst FILES_MAX_WIRE_EXPANSION = 6;\n\n/** Slack for the JSON envelope, or for multipart boundaries and part headers. */\nconst FILES_ENVELOPE_HEADROOM_BYTES = 64 * 1024;\n\n/**\n * How much of a Files upload body the route will BUFFER — derived, not picked.\n *\n * `readBodyBuffer` enforces a limit by destroying the socket: no status line,\n * no body, no CORS headers, just an `ECONNRESET`. That made the `400 File\n * content exceeds ... byte cap` branches below unreachable when the two caps\n * were equal (both the 10 MB `DEFAULT_MAX_BODY_BYTES`), and a fixed slice of\n * headroom above the cap only moved the cliff: a `\\u0001`-heavy JSON body or a\n * multipart body far enough over the cap still died on the socket.\n *\n * So the bound is computed from the widest a within-cap payload can be on the\n * wire ({@link FILES_MAX_WIRE_EXPANSION} × the cap, plus envelope headroom).\n * Two consequences, and they are the contract:\n *\n * - No body that could decode to a payload within the cap is ever refused for\n * its size — it fits here by construction, and is answered on content.\n * - A body larger than this is not buffered. The route stops retaining bytes\n * (peak memory stays at this bound), drains the rest, and answers a real\n * `400` with CORS. It is not a socket drop, and it cannot be a false\n * rejection either, by the point above.\n *\n * The global `readBodyBuffer` default is deliberately left alone — every other\n * route depends on it.\n */\nexport const FILES_BODY_MAX_BYTES =\n FILES_MAX_BYTES * FILES_MAX_WIRE_EXPANSION + FILES_ENVELOPE_HEADROOM_BYTES;\n\n/**\n * The one documented socket drop on this route: a DoS backstop.\n *\n * Draining an over-size body is what buys the caller a real `400`, but the\n * draining itself is work an attacker can ask for without limit. Past this\n * the request is simply not worth finishing and the socket is destroyed, the\n * same way every other route treats its own limit. It is deliberately far\n * above {@link FILES_BODY_MAX_BYTES}: a client that lands here is not sending\n * a file that was ever going to be accepted.\n */\nexport const FILES_BODY_DRAIN_MAX_BYTES = FILES_BODY_MAX_BYTES * 2;\n\n/**\n * Stands in for an upload body that went past {@link FILES_BODY_MAX_BYTES} and\n * was therefore drained instead of buffered. The route hands this to\n * {@link handleFilesCreate} in place of the bytes, so the over-size answer is\n * the same `400` envelope — journalled, CORS-headed, chaos-gated — as every\n * other rejection here, rather than a second error path in the server.\n */\nexport const FILES_BODY_OVERSIZED = Symbol(\"files-body-oversized\");\n\n/** The `400` for a body too large to be a within-cap upload. */\nfunction oversizedBody(): string {\n 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)`;\n}\n\ninterface StoredContent {\n bytes: Buffer;\n}\n\nconst fileStore = new Map<string, FileObject>();\nconst fileContents = new Map<string, StoredContent>();\n\nexport function clearFileStore(): void {\n fileStore.clear();\n fileContents.clear();\n}\n\nexport function getFileStoreSize(): number {\n return fileStore.size;\n}\n\n/** The original upload name, without exposing mutable file metadata. */\nexport function getStoredFileName(id: string): string | undefined {\n return fileStore.get(id)?.filename;\n}\n\n/**\n * The stored bytes for one file, as the very `Buffer` this module holds — so a\n * test can assert on what it RETAINS (`buffer.byteLength`), not just on what it\n * serves back. `GET /v1/files/{id}/content` copies onto the wire, so it cannot\n * tell an owned buffer from a `subarray` pinning a 60 MB request body.\n */\nexport function getStoredFileBytes(id: string): Buffer | undefined {\n return fileContents.get(id)?.bytes;\n}\n\n/**\n * Register one file in the store and return its wire object. This is the one\n * insert path: `POST /v1/files` uses it, and so do server-side producers of\n * files (the Batches mock minting `output_file_id` / `error_file_id`), so a\n * file minted internally is retrievable through `GET /v1/files/{id}` and\n * `/content` exactly like an upload. `content` must be an OWNED buffer (see\n * {@link ParsedUpload.content}) — it is stored as-is, and it must already be\n * within {@link FILES_MAX_BYTES}: the cap the upload route enforces holds for\n * every file in the store, so a producer that cannot fit under it fails its\n * own operation instead of storing the file.\n */\nexport function storeFile(file: {\n purpose: string;\n filename: string;\n content: Buffer;\n}): FileObject {\n if (file.content.length > FILES_MAX_BYTES) {\n throw new RangeError(`File content exceeds ${FILES_MAX_BYTES} byte cap`);\n }\n const id = generateId(\"file\");\n const obj: FileObject = {\n id,\n object: \"file\",\n bytes: file.content.length,\n created_at: Math.floor(Date.now() / 1000),\n filename: file.filename,\n purpose: file.purpose,\n status: \"processed\",\n };\n fileStore.set(id, obj);\n // Stored as-is: an owned buffer of exactly the file's bytes, so residency\n // here is the file's size and nothing more.\n fileContents.set(id, { bytes: file.content });\n return obj;\n}\n\n/**\n * Synthetic journal body for an upload — the SAME convention\n * {@link handleTranscription} uses for `multipart/form-data` audio: the entry\n * carries a small object describing the upload that was parsed out of the\n * body, never the body itself. Raw file octets are NEVER journalled, in any\n * encoding: they can be megabytes of binary, they would blow the journal's\n * 64 KB body cap on anything real, and a base64 round-trip of them is not\n * something a test wants to diff. `sha256` is the byte identity a caller\n * asserts on instead.\n *\n * `content_type` is derived from the stored filename via\n * {@link contentTypeForFilename} — the same decision `GET /v1/files/{id}/content`\n * serves the bytes back with — rather than from the client-declared part\n * header, which this mock deliberately ignores everywhere else.\n *\n * `model`/`messages` are the inert `ChatCompletionRequest` floor the journal's\n * body type requires (the same `{ model: \"\", messages: [] }` the server uses\n * for its own bodyless synthetic entries); `_endpointType` names the surface.\n */\nfunction filesUploadBody(upload: ParsedUpload): ChatCompletionRequest {\n return {\n model: \"\",\n messages: [],\n _endpointType: \"files\",\n purpose: upload.purpose,\n filename: upload.filename,\n bytes: upload.content.length,\n content_type: contentTypeForFilename(upload.filename),\n sha256: createHash(\"sha256\").update(upload.content).digest(\"hex\"),\n };\n}\n\n/**\n * Journal one files-route request.\n *\n * The rule for `body`: an entry carries the {@link filesUploadBody} descriptor\n * exactly when a request had an upload payload that parsed — i.e. a successful\n * `POST /v1/files` — and the {@link invalidRequest} envelope exactly when the\n * request was rejected with a `400` (see {@link rejectFiles}), so the reason\n * is readable from the journal without the wire response. Every other files\n * entry (GET one, GET content, list, DELETE, a chaos-faulted request, a 404)\n * has no payload to describe and stays `null`, matching the bodyless routes in\n * {@link handleTranscription}. Raw uploaded bytes never appear.\n *\n * `service: \"files\"` is set on every branch so\n * `GET /__aimock/journal?service=files` selects exactly this traffic, and\n * `source: \"internal\"` because this store is aimock's own — the same tag every\n * fine-tuning entry carries, success and error alike.\n *\n * Call it AFTER the response has been written: a `writeHead` that throws must\n * not leave behind an entry for a response the client never received.\n */\nfunction journalFiles(\n journal: Journal,\n method: string,\n path: string,\n headers: Record<string, string>,\n status: number,\n body: JournalBody | null = null,\n): void {\n journal.add({\n method,\n path,\n headers,\n body,\n service: \"files\",\n response: { status, fixture: null, source: \"internal\" },\n });\n}\n\n/** Keep the entire warning bounded and on one physical line, including the URL. */\nfunction filesRejectionWarning(message: string): string {\n const maxLength = 1_024;\n let safe = \"\";\n for (const character of message) {\n const code = character.charCodeAt(0);\n const escaped =\n code < 32 || (code >= 127 && code <= 159) || code === 0x2028 || code === 0x2029\n ? `\\\\u${code.toString(16).padStart(4, \"0\")}`\n : character;\n // Reserve the truncation marker and never cut through an escape sequence.\n if (safe.length + escaped.length > maxLength - 3) return `${safe}...`;\n safe += escaped;\n }\n return safe;\n}\n\n/**\n * Answer a files-route `400`: log it at `warn` (the level the server uses for\n * a request it refused), journal the {@link invalidRequest} envelope as the\n * entry's body so the reason survives in the journal, and write that same\n * envelope to the wire. One helper so the three can never disagree.\n */\nfunction rejectFiles(\n res: http.ServerResponse,\n journal: Journal,\n logger: Logger,\n method: string,\n path: string,\n headers: Record<string, string>,\n message: string,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n const envelope = invalidRequest(message);\n logger.warn(filesRejectionWarning(`Files mock: rejected ${method} ${path} with 400: ${message}`));\n writeJson(res, 400, envelope, setCorsHeaders);\n journalFiles(journal, method, path, headers, 400, envelope);\n}\n\nfunction writeJson(\n res: http.ServerResponse,\n status: number,\n payload: unknown,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction invalidRequest(message: string): { error: { message: string; type: string } } {\n return { error: { message, type: \"invalid_request_error\" } };\n}\n\n/**\n * Name a rejected value's JSON type for the 400 message. The point is that the\n * caller sees *what* they sent instead of silently getting `String(value)`\n * stored as the file's content.\n *\n * The listed cases are exhaustive for the one caller. `JSON.parse` only ever\n * yields null, boolean, number, string, array or plain object — never\n * `undefined`, `function`, `symbol` or `bigint` — and the caller has already\n * excluded `string` (it is the accepted type) and `undefined` (that is the\n * separate \"required\" 400). So everything reaching the last line is an object.\n */\nfunction jsonTypeOf(value: unknown): string {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"an array\";\n if (typeof value === \"number\") return \"a number\";\n if (typeof value === \"boolean\") return \"a boolean\";\n return \"an object\";\n}\n\ninterface ParsedUpload {\n filename: string;\n purpose: string;\n /**\n * The upload's bytes, and an OWNED buffer — never a `subarray` of the\n * request body. Both parsers guarantee it (`Buffer.from(content, \"utf8\")` on\n * the JSON path, an explicit copy on the multipart path) because this is\n * what {@link handleFilesCreate} stores, and a view would keep the entire\n * request body resident for the lifetime of the file.\n */\n content: Buffer;\n}\n\n/**\n * A high surrogate not followed by a low one, or a low surrogate not preceded\n * by a high one — i.e. a JS string that is not well-formed UTF-16 and therefore\n * has no UTF-8 encoding.\n *\n * `Buffer.from(s, \"utf8\")` does not fail on these: it substitutes U+FFFD (EF BF\n * BD), silently and permanently. That breaks the one guarantee an upload mock\n * owes its caller — `GET /v1/files/{id}/content` returns what was uploaded — and\n * it breaks it invisibly, three bytes at a time, in a test whose hash\n * comparison then fails for no visible reason. The byte-for-byte promise this\n * module documents is made for the multipart path (which is scanned as latin1\n * and never decoded); the JSON path carries *text*, so text that cannot be\n * encoded is a 400, not a lossy store.\n */\nconst LONE_SURROGATE_RE = /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/;\n\n/**\n * Longest `filename` this mock will store, in UTF-8 bytes.\n *\n * NOT the real API's limit: `openai/openai-openapi` gives `OpenAIFile.filename`\n * as a bare `type: string` with no `maxLength`, and `CreateFileRequest` has no\n * `filename` property at all. So the bound comes from the one thing here a\n * filename can break: `GET /v1/files/{id}/content` echoes the stored name into\n * `Content-Disposition` twice — quoted ASCII (where `\"` and `\\` double) and\n * percent-encoded per RFC 5987 (where a byte can triple) — so ~5 bytes of\n * header per byte of name. 1024 bytes of name is ~5 KiB of header, well inside\n * Node's 16 KiB default `--max-http-header-size`.\n *\n * Unbounded, a 20 KB filename uploaded `200` and then made the content route\n * unreadable by undici: `UND_ERR_HEADERS_OVERFLOW`, no status, no envelope, no\n * CORS — the OpenAI SDK could not fetch a file it had just uploaded.\n */\nexport const FILES_MAX_FILENAME_BYTES = 1024;\n\n/**\n * The filename rules, in one place because both parsers must answer the same.\n *\n * They used to disagree: a name carrying NUL, CR or LF was a `400` as a\n * multipart parameter and a `200` as a JSON field, stored and echoed back\n * verbatim. Returns an error message, or `undefined` if the name is fine.\n */\nfunction filenameError(filename: string): string | undefined {\n // A filename is metadata that is echoed back on every file object, so it must\n // not carry control characters.\n if (CONTROL_CHARS.test(filename)) {\n return \"Invalid parameter: 'filename' must not contain control characters\";\n }\n // An unpaired surrogate has no UTF-8 encoding, so `encodeURIComponent` throws\n // on it when the content route builds its `filename*` parameter: the name\n // uploaded with a 200 and then made `GET /v1/files/{id}/content` a 500.\n if (LONE_SURROGATE_RE.test(filename)) {\n return \"Invalid parameter: 'filename' must not contain an unpaired UTF-16 surrogate\";\n }\n const byteLength = Buffer.byteLength(filename, \"utf8\");\n if (byteLength > FILES_MAX_FILENAME_BYTES) {\n return (\n `Invalid parameter: 'filename' is ${byteLength} UTF-8 bytes, above the ` +\n `${FILES_MAX_FILENAME_BYTES} byte cap (a longer name makes ` +\n `GET /v1/files/{id}/content unfetchable: its Content-Disposition header ` +\n `would exceed the HTTP header limit)`\n );\n }\n return undefined;\n}\n\n/** The deterministic name given to an upload that arrived without one. */\nfunction synthesizedFilename(content: Buffer): string {\n return `upload-${createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 24)}.bin`;\n}\n\nfunction parseJsonUpload(raw: Buffer): ParsedUpload | { error: string } {\n // Decoded FATALLY, like `filename*` on the multipart path: `raw.toString(\"utf8\")`\n // substitutes U+FFFD for an invalid sequence, so a body carrying a raw 0xFF\n // inside `content` was stored (with a 200) as bytes the client never sent.\n let text: string;\n try {\n text = new TextDecoder(\"utf-8\", { fatal: true }).decode(raw);\n } catch {\n return { error: \"Invalid request: body is not valid UTF-8\" };\n }\n let body: unknown;\n try {\n body = JSON.parse(text) as unknown;\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"unknown\";\n return { error: `Malformed JSON: ${detail}` };\n }\n if (!isJsonObject(body)) {\n return { error: \"Request body must be a JSON object\" };\n }\n const rawFilename = body[\"filename\"];\n const purpose = body[\"purpose\"];\n if (typeof rawFilename !== \"string\") {\n return { error: \"Invalid parameter: 'filename' must be a string\" };\n }\n const filenameProblem = filenameError(rawFilename);\n if (filenameProblem !== undefined) return { error: filenameProblem };\n // Blank is no filename at all, exactly as on the multipart path; the\n // synthesized name below is the same pure function of the content there.\n const filename = rawFilename.trim() === \"\" ? undefined : rawFilename;\n if (typeof purpose !== \"string\" || purpose.length === 0) {\n return { error: \"Invalid parameter: 'purpose' must be a non-empty string\" };\n }\n if (!CREATE_PURPOSES.has(purpose)) {\n return { error: invalidPurpose(purpose) };\n }\n // `bytes` is a documented alias of `content`, but it is ALSO the name of the\n // numeric byte-count on a returned file object — so a caller that echoes a\n // `FileObject` back at the upload route would otherwise have `42` stored as\n // its content. Both spellings on one body are ambiguous, so they are rejected\n // rather than silently resolved by precedence, and neither is coerced: a\n // non-string payload is a 400, never `String(value)`.\n const hasContent = body[\"content\"] !== undefined;\n const hasBytesAlias = body[\"bytes\"] !== undefined;\n if (hasContent && hasBytesAlias) {\n return {\n error:\n \"Invalid parameter: 'content' and its alias 'bytes' are mutually exclusive; send exactly one\",\n };\n }\n const field = hasContent ? \"content\" : \"bytes\";\n const content = hasContent ? body[\"content\"] : body[\"bytes\"];\n if (content === undefined) {\n return { error: \"Invalid parameter: 'content' is required (or its alias 'bytes')\" };\n }\n if (typeof content !== \"string\") {\n return {\n error: `Invalid parameter: '${field}' must be a string, got ${jsonTypeOf(content)}`,\n };\n }\n if (LONE_SURROGATE_RE.test(content)) {\n return {\n error:\n `Invalid parameter: '${field}' contains an unpaired UTF-16 surrogate and is not valid text. ` +\n `aimock would have to store it as U+FFFD, so the bytes served back by ` +\n `GET /v1/files/{id}/content would not be the bytes you sent. Send binary ` +\n `as a multipart/form-data upload, which is stored verbatim.`,\n };\n }\n const bytes = Buffer.from(content, \"utf8\");\n if (bytes.length > FILES_MAX_BYTES) {\n return { error: `File content exceeds ${FILES_MAX_BYTES} byte cap` };\n }\n return { filename: filename ?? synthesizedFilename(bytes), purpose, content: bytes };\n}\n\n/**\n * One parsed `multipart/form-data` body part: its headers (field names\n * lower-cased, values trimmed) and its body as the exact octets that sat\n * between the two boundary delimiter lines.\n */\ninterface MultipartPart {\n /** The `name` parameter of `Content-Disposition`. Always present. */\n name: string;\n /** The `filename`/`filename*` parameter, or `undefined` when absent. */\n filename?: string;\n headers: Map<string, string>;\n body: Buffer;\n}\n\nconst HEADER_TERMINATOR = Buffer.from(\"\\r\\n\\r\\n\");\n\n/** RFC 9110 `token` characters — the legal set for a header field name. */\nconst TOKEN_CHAR = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\n\n/** C0 controls plus DEL — never legitimate in a filename we echo back. */\n// eslint-disable-next-line no-control-regex\nconst CONTROL_CHARS = /[\u0000-\u001f]/;\n\n/**\n * Is this request body `multipart/form-data`?\n *\n * Compares the media type's *essence* (RFC 9110 §8.3.1: everything before the\n * first `;`), not a substring of the whole header. A substring test routed\n * `application/json; note=\"multipart/form-data\"` — and anything else that\n * merely mentions the token in a parameter — into the multipart parser, which\n * then 400'd about a missing `boundary` directive on a perfectly good JSON\n * body.\n */\nfunction isMultipartFormData(contentType: string | undefined): boolean {\n if (contentType === undefined) return false;\n const semi = contentType.indexOf(\";\");\n const essence = (semi === -1 ? contentType : contentType.slice(0, semi)).trim().toLowerCase();\n return essence === \"multipart/form-data\";\n}\n\n/**\n * Locate the next *boundary delimiter line* at or after `from`.\n *\n * RFC 2046 §5.1.1 defines the delimiter as `CRLF \"--\" boundary` followed by\n * either transport-padding + CRLF (another part follows) or `--` (the close\n * delimiter). Both halves matter:\n *\n * - The **leading CRLF is part of the delimiter**, not of the preceding body.\n * The previous implementation split on a bare `--boundary`, so a payload\n * whose bytes happened to contain that sequence was truncated at it and\n * stored short — with a 200, silently, which is the worst possible answer\n * from a module whose contract is byte-for-byte storage.\n * - An occurrence **not followed by** CRLF or `--` is not a delimiter line at\n * all, so it is ordinary body content and the scan continues past it. That\n * is what keeps a payload containing `CRLF--boundary` plus arbitrary bytes\n * byte-exact. (undici's `Response.formData()` rejects such a body outright;\n * see the note on {@link parseMultipartUpload}.)\n */\nfunction nextDelimiter(\n buf: Buffer,\n delimiter: Buffer,\n from: number,\n): { at: number; next: number; closing: boolean } | undefined {\n let at = buf.indexOf(delimiter, from);\n while (at !== -1) {\n const tail = delimiterTail(buf, at + delimiter.length);\n if (tail) return { at, ...tail };\n at = buf.indexOf(delimiter, at + 1);\n }\n return undefined;\n}\n\n/**\n * Classify what follows a `CRLF \"--\" boundary` match: transport-padding + CRLF\n * (another part follows), `--` (the close delimiter), or neither — in which\n * case the match is not a delimiter line at all and the caller keeps scanning.\n * Split out of {@link nextDelimiter} so {@link parseMultipartUpload} can apply\n * the same rule to an opening delimiter whose leading CRLF is not present.\n */\nfunction delimiterTail(buf: Buffer, after: number): { next: number; closing: boolean } | undefined {\n if (buf[after] === 0x2d && buf[after + 1] === 0x2d) return { next: after + 2, closing: true };\n // transport-padding = *LWSP-char\n let j = after;\n while (buf[j] === 0x20 || buf[j] === 0x09) j += 1;\n if (buf[j] === 0x0d && buf[j + 1] === 0x0a) return { next: j + 2, closing: false };\n return undefined;\n}\n\n/**\n * Parse a header parameter list (`value; attr=token; attr=\"quoted\"`).\n *\n * Hand-rolled *matching* is what broke the old header handling: `name` and\n * `filename` were matched with regexes against the whole header block, so a\n * `[^\"]+` capture ran across a CRLF and swallowed the next header line (an\n * upload whose filename parameter embedded a CRLF plus `X-Evil: y.txt` was\n * accepted with a 200 and the injected header text stored as the filename),\n * and the `\\s*` in the encoding regexes did the same. Walking one header's\n * value with a real quoted-string scanner cannot cross a line, because the\n * value it is handed never contains one.\n *\n * A repeated attribute is an error rather than a last-one-wins overwrite.\n * `filename=\"a.txt\"; filename=\"b.png\"` decided both the name echoed on the file\n * object and the `Content-Type` served back by `GET /content` (a text upload\n * came back as `image/png`), and RFC 7578 §4.2 gives no precedence rule to pick\n * a winner by — the same reason duplicate `purpose` and payload parts are\n * refused below. Ambiguity is never resolved by precedence in this module.\n */\ntype HeaderParams = { value: string; params: Map<string, string> };\ntype HeaderParamsError = { error: \"malformed\" } | { error: \"duplicate\"; param: string };\n\nfunction parseHeaderParams(value: string): HeaderParams | HeaderParamsError {\n const params = new Map<string, string>();\n const semi = value.indexOf(\";\");\n const head = (semi === -1 ? value : value.slice(0, semi)).trim();\n let i = semi === -1 ? value.length : semi;\n while (i < value.length) {\n i += 1; // consume the ';'\n while (value[i] === \" \" || value[i] === \"\\t\") i += 1;\n if (i >= value.length) break; // trailing ';'\n const eq = value.indexOf(\"=\", i);\n if (eq === -1) return MALFORMED_HEADER; // a parameter with no value\n const attr = value.slice(i, eq).trim().toLowerCase();\n if (!TOKEN_CHAR.test(attr)) return MALFORMED_HEADER;\n if (params.has(attr)) return { error: \"duplicate\", param: attr };\n i = eq + 1;\n if (value[i] === '\"') {\n i += 1;\n let out = \"\";\n for (;;) {\n if (i >= value.length) return MALFORMED_HEADER; // unterminated quoted-string\n const ch = value[i];\n if (ch === \"\\\\\" && i + 1 < value.length) {\n out += value[i + 1];\n i += 2;\n continue;\n }\n if (ch === '\"') {\n i += 1;\n break;\n }\n out += ch;\n i += 1;\n }\n params.set(attr, out);\n while (value[i] === \" \" || value[i] === \"\\t\") i += 1;\n if (i < value.length && value[i] !== \";\") return MALFORMED_HEADER;\n } else {\n const end = value.indexOf(\";\", i);\n params.set(attr, (end === -1 ? value.slice(i) : value.slice(i, end)).trim());\n i = end === -1 ? value.length : end;\n }\n }\n return { value: head, params };\n}\n\nconst MALFORMED_HEADER: HeaderParamsError = { error: \"malformed\" };\n\n/** Render a {@link parseHeaderParams} failure as a 400 message naming the header. */\nfunction headerParamError(err: HeaderParamsError, header: string, where: string): string {\n return err.error === \"duplicate\"\n ? `Invalid request: duplicate '${err.param}' parameter on the ${header} header in ${where}`\n : `Invalid request: malformed ${header} header in ${where}`;\n}\n\n/**\n * Decode an RFC 5987 `ext-value` (`charset'language'pct-encoded`), the form a\n * `filename*` parameter takes. RFC 6266 §4.3 says a recipient that understands\n * it SHOULD prefer it over the plain `filename`, so {@link parsePart} does.\n * A client that sends only `filename*=` therefore keeps the name it sent\n * instead of receiving a synthesized one.\n */\nfunction decodeExtValue(raw: string): string | undefined {\n const first = raw.indexOf(\"'\");\n const second = raw.indexOf(\"'\", first + 1);\n if (first === -1 || second === -1) return undefined;\n const charset = raw.slice(0, first).toLowerCase();\n if (charset !== \"utf-8\" && charset !== \"iso-8859-1\") return undefined;\n const octets: number[] = [];\n const pct = raw.slice(second + 1);\n for (let i = 0; i < pct.length; i += 1) {\n if (pct[i] === \"%\") {\n const hex = pct.slice(i + 1, i + 3);\n if (!/^[0-9A-Fa-f]{2}$/.test(hex)) return undefined;\n octets.push(parseInt(hex, 16));\n i += 2;\n } else {\n const code = pct.charCodeAt(i);\n if (code > 0x7f) return undefined;\n octets.push(code);\n }\n }\n const bytes = Buffer.from(octets);\n // latin1 maps every octet, so only the utf-8 branch can fail — and it must\n // fail LOUDLY. `Buffer.toString(\"utf8\")` substitutes U+FFFD for an invalid\n // sequence, so `filename*=UTF-8''%FF%FE.txt` was stored (with a 200) under a\n // name the client never sent, while the neighbouring malformed-parameter\n // cases got a 400. A name that cannot be decoded is a malformed parameter.\n if (charset === \"iso-8859-1\") return bytes.toString(\"latin1\");\n try {\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n } catch {\n return undefined;\n }\n}\n\n/** Turn one part's raw octets into headers + body, or name why it is malformed. */\nfunction parsePart(partBytes: Buffer, index: number): MultipartPart | { error: string } {\n const where = `multipart part #${index + 1}`;\n let headerText: string;\n let bodyStart: number;\n if (partBytes[0] === 0x0d && partBytes[1] === 0x0a) {\n // `body-part = MIME-part-headers CRLF *OCTET` with an empty header block.\n headerText = \"\";\n bodyStart = 2;\n } else {\n const end = partBytes.indexOf(HEADER_TERMINATOR);\n if (end === -1) {\n // Skipping such a part would report the NEXT missing thing instead: a\n // body whose parts use bare LF separators would be answered \"multipart\n // 'purpose' field is required\" even though `purpose` was sent. The 400\n // names the part that is actually malformed.\n return {\n error: `Invalid request: malformed ${where} — its headers are not terminated by a blank line`,\n };\n }\n headerText = partBytes.subarray(0, end).toString(\"utf8\");\n bodyStart = end + 4;\n }\n\n const headers = new Map<string, string>();\n if (headerText !== \"\") {\n for (const line of headerText.split(\"\\r\\n\")) {\n const colon = line.indexOf(\":\");\n const fieldName = colon === -1 ? \"\" : line.slice(0, colon);\n if (!TOKEN_CHAR.test(fieldName)) {\n return { error: `Invalid request: malformed header line in ${where}` };\n }\n const field = fieldName.toLowerCase();\n // Last-one-wins on a repeated header defeated the very contracts this\n // parser exists to enforce: `Content-Transfer-Encoding: base64` followed\n // by `Content-Transfer-Encoding: 7bit` sailed past the encoding check\n // and stored the literal base64 TEXT as the file's octets (a 200, four\n // bytes of `aGk=`), and a repeated `Content-Disposition` could pair one\n // part's filename with another's rule. RFC 9110 §5.3 only permits a\n // repeated field for a list-valued one, which none of these are, so\n // there is nothing to combine and no defensible precedence.\n if (headers.has(field)) {\n return { error: `Invalid request: duplicate '${fieldName.trim()}' header in ${where}` };\n }\n headers.set(field, line.slice(colon + 1).trim());\n }\n }\n\n const disposition = headers.get(\"content-disposition\");\n if (disposition === undefined) {\n return { error: `Invalid request: ${where} has no Content-Disposition header` };\n }\n const parsed = parseHeaderParams(disposition);\n if (\"error\" in parsed) {\n return { error: headerParamError(parsed, \"Content-Disposition\", where) };\n }\n // RFC 7578 §4.2: \"Each part MUST contain a Content-Disposition header field\n // where the disposition type is 'form-data'.\" Any other type was accepted\n // unchecked, so `Content-Disposition: attachment; name=\"file\"` — which is a\n // response-side disposition, not a form part — uploaded a file.\n const dispositionType = parsed.value.toLowerCase();\n if (dispositionType !== \"form-data\") {\n return {\n error:\n `Invalid request: ${where} has Content-Disposition type '${parsed.value}'; ` +\n `RFC 7578 section 4.2 requires 'form-data'`,\n };\n }\n const name = parsed.params.get(\"name\");\n if (name === undefined) {\n // RFC 7578 §4.2 makes `name` mandatory on every form-data part.\n return {\n error: `Invalid request: ${where} has no 'name' parameter on its Content-Disposition header`,\n };\n }\n\n const star = parsed.params.get(\"filename*\");\n const filename = star !== undefined ? decodeExtValue(star) : parsed.params.get(\"filename\");\n if (star !== undefined && filename === undefined) {\n return { error: `Invalid request: malformed 'filename*' parameter in ${where}` };\n }\n\n return { name, filename, headers, body: partBytes.subarray(bodyStart) };\n}\n\n/**\n * Read a part's content encoding header. An empty value reads as absent: the\n * old regex used `\\s*` after the colon, which matches a CRLF, so an empty\n * `Content-Transfer-Encoding:` captured the *next* header line and 400'd\n * naming `content-type: image/png` as the unsupported encoding.\n */\nfunction partEncoding(part: MultipartPart, header: string): string | undefined {\n const value = part.headers.get(header)?.trim().toLowerCase();\n return value === undefined || value === \"\" ? undefined : value;\n}\n\n/**\n * Parse a `multipart/form-data` upload body.\n *\n * ## Why this is not `Response.formData()`\n *\n * Node's built-in (undici) `Response.formData()` is a spec-compliant multipart\n * parser and was evaluated first, per the \"use the platform, do not hand-roll\n * a codec\" rule. It fails three of this module's requirements:\n *\n * 1. **Byte fidelity.** It throws `expected CRLF` on any body whose payload\n * contains the boundary token, including the RFC-legal bare `--boundary`\n * with no preceding CRLF. Those bytes are storable and this module promises\n * to store them.\n * 2. **Filename-less payload parts.** A part with no `filename` parameter comes\n * back as a *string*, decoded as UTF-8, so binary octets are destroyed\n * (`00 ff fe ...` becomes `00 efbfbd efbfbd ...`). Such a part is legal\n * input here and already round-tripped byte-exact.\n * 3. **Part headers.** `File` exposes only `name`, `type` and `size`. There is\n * no way to see a part's `Content-Transfer-Encoding`, so the \"reject an\n * encoding we do not apply\" contract above could not be implemented at all\n * and a `base64` part would be stored in its encoded form — the exact bug\n * that contract exists to prevent.\n *\n * So the framing stays local, but it is written to RFC 2046 §5.1.1 and\n * RFC 7578 rather than to a `split()`: see {@link nextDelimiter} for the\n * delimiter rule and {@link parsePart} for the header rule.\n *\n * Bytes never round-trip through a string. Part bodies are `subarray`s of the\n * request buffer, so what is parsed is literally what arrived on the wire — but\n * the payload part is COPIED out of that view before it is returned, because a\n * `subarray` retains its whole backing buffer (see the copy below). The scan\n * itself never copies the body either, so peak residency during a parse is one\n * request body, not two.\n */\nfunction parseMultipartUpload(\n rawBytes: Buffer,\n contentType: string | undefined,\n): ParsedUpload | { error: string } {\n // Parsed as a real parameter list rather than with a first-match regex: the\n // regex read `boundary=` out of ANOTHER parameter's quoted string, so\n // `multipart/form-data; name=\"x boundary=FAKE\"; boundary=REAL` searched for\n // the decoy and 400'd a perfectly good body as having \"no boundary\n // delimiter\". `extractBoundary` (transcription.ts) keeps its regex: its\n // callers hand it whole request bodies scanned as text, not this one header.\n const parsedType = parseHeaderParams(contentType ?? \"\");\n if (\"error\" in parsedType) {\n return { error: headerParamError(parsedType, \"Content-Type\", \"the request\") };\n }\n const boundary = parsedType.params.get(\"boundary\");\n if (!boundary) {\n // Without a boundary there is no way to find the part edges, so every\n // earlier best-effort path here silently stored a 0-byte file and threw\n // the caller's bytes away. A multipart body that cannot be parsed is a\n // client error, not an empty upload.\n return {\n error: \"Invalid request: multipart/form-data Content-Type is missing a 'boundary' directive\",\n };\n }\n\n // Everything before the opening delimiter is the preamble. That delimiter\n // alone has no preceding CRLF when the preamble is empty; matching it used\n // to mean parsing `Buffer.concat([CRLF, rawBytes])`, a second copy of the\n // whole request body for the sake of two octets. So it is special-cased and\n // the scan runs over the request buffer in place.\n const buf = rawBytes;\n const delimiter = Buffer.from(`\\r\\n--${boundary}`, \"latin1\");\n const dashBoundary = Buffer.from(`--${boundary}`, \"latin1\");\n const opening = buf.subarray(0, dashBoundary.length).equals(dashBoundary)\n ? delimiterTail(buf, dashBoundary.length)\n : undefined;\n // `at` is only ever read as the END of the preceding part's body, which the\n // opening delimiter has none of, so 0 stands in for it.\n let delim = opening ? { at: 0, ...opening } : nextDelimiter(buf, delimiter, 0);\n if (!delim) {\n return { error: \"Invalid request: multipart body contains no boundary delimiter\" };\n }\n\n const parts: MultipartPart[] = [];\n while (!delim.closing) {\n const start = delim.next;\n const following = nextDelimiter(buf, delimiter, start);\n if (!following) {\n return {\n error: \"Invalid request: multipart body ended before its closing boundary delimiter\",\n };\n }\n const part = parsePart(buf.subarray(start, following.at), parts.length);\n if (\"error\" in part) return part;\n parts.push(part);\n delim = following;\n }\n\n // Encoding first, and across every part: the message must name the part that\n // actually declared the encoding, rather than let an encoded value fall\n // through to a downstream check that reports something else entirely.\n for (const part of parts) {\n const transfer = partEncoding(part, \"content-transfer-encoding\");\n if (transfer !== undefined && !IDENTITY_TRANSFER_ENCODINGS.has(transfer)) {\n return { error: unsupportedPartEncoding(\"Content-Transfer-Encoding\", transfer, part.name) };\n }\n const encoding = partEncoding(part, \"content-encoding\");\n if (encoding !== undefined && encoding !== \"identity\") {\n return { error: unsupportedPartEncoding(\"Content-Encoding\", encoding, part.name) };\n }\n }\n\n // A part whose `name` is none of `purpose`/`file`/`content` is IGNORED, not\n // rejected. That is deliberate, and it is the JSON path's rule too: an\n // unknown key on a JSON upload body is ignored there (only `filename`,\n // `purpose` and `content`/`bytes` are read), so the two body shapes answer\n // the same request the same way. The vendored spec surface gives no reason to\n // be stricter either — `FileCreateParams` in openai 4.104.0\n // (`resources/files.d.ts`) declares exactly `file` and `purpose` and says\n // nothing about rejecting extras, so a 400 here would be this mock inventing\n // a rejection it cannot show the real API makes. Every part is still parsed\n // and still encoding-checked above, so an ignored part cannot smuggle\n // anything past a contract — it only fails to contribute.\n const purposeParts = parts.filter((p) => p.name === \"purpose\");\n if (purposeParts.length > 1) {\n // Last-one-wins silently picked a purpose the caller never meant to send.\n // There is no defensible precedence, so this gets the same answer that\n // duplicate payload parts already got.\n return {\n error: \"Invalid request: multipart body has more than one 'purpose' part; send exactly one\",\n };\n }\n const payloadParts = parts.filter((p) => p.name === \"file\" || p.name === \"content\");\n if (payloadParts.length > 1) {\n // Two payload parts means the filename from one can be paired with the\n // bytes of the other. There is no defensible precedence, so reject.\n const [first, second] = payloadParts;\n return {\n error: `Invalid request: multipart body has more than one payload part ('${first.name}' and '${second.name}'); send exactly one 'file' or 'content' part`,\n };\n }\n\n // NOT trimmed: `\" batch \"` is a `400` here exactly as it is on the JSON path.\n const purpose = purposeParts[0]?.body.toString(\"utf8\");\n if (!purpose) {\n return { error: \"Invalid parameter: multipart 'purpose' field is required\" };\n }\n if (!CREATE_PURPOSES.has(purpose)) {\n return { error: invalidPurpose(purpose) };\n }\n const payload = payloadParts[0];\n if (payload === undefined) {\n return {\n error: \"Invalid request: multipart body must include a 'file' part (or its 'content' alias)\",\n };\n }\n\n // The payload part's own Content-Type header is intentionally ignored: it is\n // client-controlled and must never decide what we serve back. Real SDKs send\n // `application/octet-stream` for every binary part anyway, so the filename is\n // both the safer and the more accurate signal.\n if (payload.body.length > FILES_MAX_BYTES) {\n return { error: `File content exceeds ${FILES_MAX_BYTES} byte cap` };\n }\n // Copy, do not hand out the view. Every part body above is a `subarray` of\n // the request buffer, and a `subarray` keeps its whole backing `ArrayBuffer`\n // alive — so storing one made a 2-byte file pin its entire request body, up\n // to `FILES_BODY_MAX_BYTES` (~62.9 MB), for as long as the file existed.\n // That silently falsified the per-file memory bound this module documents at\n // the top. `Buffer.alloc` + `copy` rather than `Buffer.from(view)` because\n // the latter draws small allocations from the shared `Buffer` pool,\n // which is a smaller version of the same aliasing. The copy is bounded by\n // the cap just checked, and it is the only copy of the payload made here.\n const content = Buffer.alloc(payload.body.length);\n payload.body.copy(content);\n\n // CR and LF can no longer reach the filename here (a header value cannot span\n // a line), but NUL and friends still can — and the JSON path has no such\n // structural protection, which is why the check is shared.\n let filename = payload.filename;\n if (filename !== undefined) {\n const problem = filenameError(filename);\n if (problem !== undefined) return { error: problem };\n }\n // A blank or whitespace-only filename is no filename at all: `\" \"` is\n // neither a usable name nor worth a 400, so it falls through to the\n // synthesized name below.\n if (filename !== undefined && filename.trim() === \"\") filename = undefined;\n\n if (filename === undefined) {\n // A filename-less `file` part is legal input: `CreateFileRequest` in the\n // `openai/openai-openapi` spec declares `file` as `type: string, format:\n // binary` with no `filename` property at all — the name only ever rides in\n // `Content-Disposition`, and the spec never requires it. The *response*\n // schema (`OpenAIFile`) does require `filename`, so the faithful answer is\n // to synthesize one rather than reject with a 400.\n //\n // Deterministic means a pure function of the uploaded bytes: the same\n // upload always yields the same name (so a test can assert on it, across\n // processes and across `clearFileStore()`), and different bytes yield\n // different names (a 96-bit SHA-256 prefix, so collisions are not a\n // practical concern). It is deliberately NOT a clock or a counter — both\n // would make two identical uploads disagree.\n filename = synthesizedFilename(content);\n }\n return { filename, purpose, content };\n}\n\nexport async function handleFilesCreate(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: Buffer | typeof FILES_BODY_OVERSIZED,\n journal: Journal,\n defaults: {\n logger: Logger;\n chaos?: ChaosDefaults;\n registry?: MetricsRegistry;\n },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? \"/v1/files\";\n const method = req.method ?? \"POST\";\n setCorsHeaders(res);\n\n if (\n await applyChaosAsync(\n res,\n null,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"files\",\n },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n ) {\n return;\n }\n\n // After the chaos gate on purpose: an over-size body is still a request the\n // fault injector is entitled to answer for.\n if (raw === FILES_BODY_OVERSIZED) {\n rejectFiles(\n res,\n journal,\n defaults.logger,\n method,\n path,\n flattenHeaders(req.headers),\n oversizedBody(),\n setCorsHeaders,\n );\n return;\n }\n\n const contentType = Array.isArray(req.headers[\"content-type\"])\n ? req.headers[\"content-type\"][0]\n : req.headers[\"content-type\"];\n const parsed = isMultipartFormData(contentType)\n ? parseMultipartUpload(raw, contentType)\n : // Non-multipart bodies are JSON, which is utf8 by definition (RFC 8259).\n parseJsonUpload(raw);\n\n if (\"error\" in parsed) {\n rejectFiles(\n res,\n journal,\n defaults.logger,\n method,\n path,\n flattenHeaders(req.headers),\n parsed.error,\n setCorsHeaders,\n );\n return;\n }\n\n const obj = storeFile(parsed);\n const { id, bytes } = obj;\n\n defaults.logger.debug(`Files mock: stored ${id} (${parsed.filename}, ${bytes} bytes)`);\n writeJson(res, 200, obj, setCorsHeaders);\n journalFiles(journal, method, path, flattenHeaders(req.headers), 200, filesUploadBody(parsed));\n}\n\n/**\n * `GET /v1/files` paging bounds, taken from the `openai/openai-openapi` spec\n * (`operationId: listFiles`): \"Limit can range between 1 and 10,000, and the\n * default is 10,000.\" Files is the odd one out among the list surfaces — most\n * default to 20 — so the default is spelled out here rather than shared.\n */\nexport const FILES_LIST_MAX_LIMIT = 10000;\nexport const FILES_LIST_DEFAULT_LIMIT = FILES_LIST_MAX_LIMIT;\n\ninterface FilesListQuery {\n limit: number;\n order: \"asc\" | \"desc\";\n after?: string;\n purpose?: string;\n}\n\n/**\n * The single non-empty value of a list query parameter, or the 400 it earns.\n *\n * ONE rule for all four of `limit`, `order`, `after` and `purpose`, because the\n * three ways of reading them used to disagree on the same URL:\n *\n * - REPEATED is a 400. `URLSearchParams.get` answers with the FIRST of a\n * repeated parameter and launders the rest, so `?limit=1&limit=abc` paged\n * at 1 and never looked at `abc` — a caller that built its URL twice over\n * got a silent success for a value it never meant. `listFiles` declares\n * each of the four as a single scalar and gives no combining rule for a\n * repeat, so a repeat is a caller error. Matches the multipart body, where\n * two `purpose` parts are already a 400 rather than a last-one-wins.\n * - PRESENT-BUT-EMPTY is a 400. `?order=` was already one (no member of the\n * enum is \"\"), but `?after=` meant \"no cursor\" and `?purpose=` meant \"no\n * filter\" and handed back the WHOLE store — the exact opposite of the\n * narrowing the caller asked for. An empty string is not an object id and\n * not a purpose any stored file can carry, and `?x=` is what building a\n * URL from a partly-undefined params object emits, which is a bug worth\n * showing. Omitting the parameter is how you say \"no filter\".\n * - ABSENT is `null`, and each caller applies its own default.\n *\n * Both 400s are OURS, not the vendor's: the spec states neither. They are the\n * mock's standing bias towards failing loudly over answering a question the\n * caller did not ask.\n */\nfunction singleParam(\n params: URLSearchParams,\n name: string,\n): { ok: true; value: string | null } | { ok: false; error: string } {\n const all = params.getAll(name);\n if (all.length > 1) {\n return {\n ok: false,\n error: `Invalid parameter: '${name}' was given ${all.length} times; it takes a single value`,\n };\n }\n if (all.length === 0) return { ok: true, value: null };\n if (all[0] === \"\") {\n return {\n ok: false,\n error: `Invalid parameter: '${name}' was given an empty value; omit it instead`,\n };\n }\n return { ok: true, value: all[0] };\n}\n\n/**\n * Parse the `limit` / `order` / `after` / `purpose` parameters off a list URL.\n *\n * Shapes come from `operationId: listFiles` in `openai/openai-openapi` and from\n * `FileListParams extends CursorPageParams` in the vendored SDK (openai\n * 4.104.0 — `resources/files.d.ts` gives `order?: 'asc' | 'desc'` and\n * `purpose?: string`; `pagination.d.ts` gives `after?: string` and\n * `limit?: number`). `order` defaults to `desc`, so the newest file comes back\n * first.\n *\n * All four are read here, through {@link singleParam}, so there is exactly one\n * place that decides what a repeated or empty list parameter means. `purpose`\n * used to be read separately in `src/server.ts` and passed in already parsed,\n * which is how it ended up with neither rule.\n */\nfunction parseListQuery(rawUrl: string): FilesListQuery | { error: string } {\n // Everything after the first `?` is query data, read as-is. `new URL(target,\n // base).searchParams` cannot be used here: it applies the fragment rule, and\n // a request target has no fragment component (RFC 9112 section 3.2), so an\n // origin server sees `?purpose=batch#frag` as the purpose `batch#frag` — not\n // as `batch` plus a fragment nobody sent. Truncating at the `#` turns a value\n // that matches nothing into a plausible-looking one that matches files.\n // `new URLSearchParams(string)` cannot throw, so no parse failure is hidden.\n const queryStart = rawUrl.indexOf(\"?\");\n const params = new URLSearchParams(queryStart === -1 ? \"\" : rawUrl.slice(queryStart + 1));\n\n const rawOrder = singleParam(params, \"order\");\n if (!rawOrder.ok) return { error: rawOrder.error };\n const orderRaw = rawOrder.value;\n if (orderRaw !== null && orderRaw !== \"asc\" && orderRaw !== \"desc\") {\n return { error: \"Invalid parameter: 'order' must be one of: asc, desc\" };\n }\n\n const rawLimit = singleParam(params, \"limit\");\n if (!rawLimit.ok) return { error: rawLimit.error };\n let limit = FILES_LIST_DEFAULT_LIMIT;\n if (rawLimit.value !== null) {\n // The digit gate runs BEFORE Number(), because Number() is far wider than\n // the \"integer between 1 and 10,000\" this branch's own error message\n // promises: it reads `0x10` as 16, `1e3` as 1000, and strips whitespace and\n // a leading sign, so `\" 5\"`, `\"5\\n\"` and `\"+5\"` are all 5. Every one of\n // those is `Number.isInteger` and inside the range, so without the gate\n // `?limit=0x10` would page silently at 16 items. A caller sending any of\n // them has a bug, and a mock's job is to show it.\n //\n // Reject, never clamp: an out-of-range limit gets the same 400 as a\n // malformed one, so a suite asserting on page size is never quietly handed\n // a different size. (`?limit=` no longer reaches here — the empty value is\n // already a 400 in singleParam, with the same message every parameter\n // gets.)\n const parsed = parseStrictIntegerText(rawLimit.value);\n if (parsed === null || parsed < 1 || parsed > FILES_LIST_MAX_LIMIT) {\n return {\n error: `Invalid parameter: 'limit' must be an integer between 1 and ${FILES_LIST_MAX_LIMIT}`,\n };\n }\n limit = parsed;\n }\n\n const rawAfter = singleParam(params, \"after\");\n if (!rawAfter.ok) return { error: rawAfter.error };\n\n const rawPurpose = singleParam(params, \"purpose\");\n if (!rawPurpose.ok) return { error: rawPurpose.error };\n\n return {\n limit,\n order: orderRaw ?? \"desc\",\n after: rawAfter.value ?? undefined,\n purpose: rawPurpose.value ?? undefined,\n };\n}\n\nexport async function handleFilesList(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n defaults: { logger: Logger; chaos?: ChaosDefaults; registry?: MetricsRegistry },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? \"/v1/files\";\n const method = req.method ?? \"GET\";\n setCorsHeaders(res);\n\n if (\n await applyChaosAsync(\n res,\n null,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: null, service: \"files\" },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n ) {\n return;\n }\n\n const query = parseListQuery(path);\n if (\"error\" in query) {\n rejectFiles(\n res,\n journal,\n defaults.logger,\n method,\n path,\n flattenHeaders(req.headers),\n query.error,\n setCorsHeaders,\n );\n return;\n }\n\n let data = [...fileStore.values()];\n if (query.purpose !== undefined) {\n // `purpose` is an UNVALIDATED free-form string here, on purpose. The real\n // API does not constrain it: `listFiles` declares the query parameter as a\n // bare `type: string` with no `enum` in `openai/openai-openapi` (contrast\n // `order`, which carries `enum: [asc, desc]` in the very same parameter\n // list), and the SDK types it `purpose?: string` on `FileListParams` while\n // typing create as `purpose: FilePurpose` (openai@4.104.0,\n // resources/files.d.ts). So an unknown or server-side-only purpose is not a\n // 400: it simply matches no stored file and yields `{object: \"list\", data:\n // []}` with a 200, which is what this filter does.\n // {@link CREATE_PURPOSES} is not consulted.\n //\n // The one shape that is NOT a 200-with-an-empty-list is `?purpose=` with no\n // value: an empty string is not a purpose any stored file can carry, so\n // {@link singleParam} has already turned it into a 400 above, under the\n // same rule the other three list parameters get. Before that, its falsiness\n // skipped this filter entirely and the caller got the whole store back.\n data = data.filter((f) => f.purpose === query.purpose);\n }\n // `fileStore` iterates in creation order and `Array.prototype.sort` is stable,\n // so sorting on `created_at` alone still yields a *total* ascending order even\n // though the second-granularity timestamps tie constantly in a fast test. The\n // `desc` view is that exact order reversed — not a flipped comparator, which\n // would leave tied files in ascending order inside the descending page and\n // break the \"every file exactly once\" property of a cursor walk.\n data.sort((a, b) => a.created_at - b.created_at);\n if (query.order === \"desc\") data.reverse();\n\n let start = 0;\n if (query.after !== undefined) {\n const idx = data.findIndex((f) => f.id === query.after);\n if (idx === -1) {\n // The spec is silent on unknown cursors, so the mock fails loudly:\n // silently restarting at page 1 turns a caller's paging bug into an\n // infinite loop instead of a test failure. Note a cursor is resolved\n // against *this* listing, so a file excluded by `purpose` is unknown here.\n rejectFiles(\n res,\n journal,\n defaults.logger,\n method,\n path,\n flattenHeaders(req.headers),\n `Invalid parameter: 'after' cursor '${query.after}' does not match any file in this listing`,\n setCorsHeaders,\n );\n return;\n }\n start = idx + 1;\n }\n\n const page = data.slice(start, start + query.limit);\n writeJson(\n res,\n 200,\n {\n object: \"list\",\n data: page,\n first_id: page[0]?.id ?? null,\n last_id: page[page.length - 1]?.id ?? null,\n has_more: start + page.length < data.length,\n },\n setCorsHeaders,\n );\n journalFiles(journal, method, path, flattenHeaders(req.headers), 200);\n}\n\nexport async function handleFilesRetrieve(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fileId: string,\n journal: Journal,\n defaults: { logger: Logger; chaos?: ChaosDefaults; registry?: MetricsRegistry },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? `/v1/files/${fileId}`;\n const method = req.method ?? \"GET\";\n // CORS before the chaos gate: applyChaosAsync short-circuits the response, so a\n // faulted retrieve would otherwise reach the browser with zero\n // Access-Control-* headers and be swallowed as a CORS error instead of the\n // 500 the chaos suite is trying to exercise. Create/list already did this.\n setCorsHeaders(res);\n\n if (\n await applyChaosAsync(\n res,\n null,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: null, service: \"files\" },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n ) {\n return;\n }\n\n const found = fileStore.get(fileId);\n if (!found) {\n writeJson(res, 404, invalidRequest(`No such file: ${fileId}`), setCorsHeaders);\n journalFiles(journal, method, path, flattenHeaders(req.headers), 404);\n return;\n }\n writeJson(res, 200, found, setCorsHeaders);\n journalFiles(journal, method, path, flattenHeaders(req.headers), 200);\n}\n\nexport async function handleFilesContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fileId: string,\n journal: Journal,\n defaults: { logger: Logger; chaos?: ChaosDefaults; registry?: MetricsRegistry },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? `/v1/files/${fileId}/content`;\n const method = req.method ?? \"GET\";\n // CORS before the chaos gate — see handleFilesRetrieve.\n setCorsHeaders(res);\n\n if (\n await applyChaosAsync(\n res,\n null,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: null, service: \"files\" },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n ) {\n return;\n }\n\n const found = fileStore.get(fileId);\n const content = fileContents.get(fileId);\n if (!found || content === undefined) {\n writeJson(res, 404, invalidRequest(`No such file: ${fileId}`), setCorsHeaders);\n journalFiles(journal, method, path, flattenHeaders(req.headers), 404);\n return;\n }\n res.writeHead(200, {\n // Derived from the STORED filename, never from the client-declared part\n // header — see CONTENT_TYPE_BY_EXT. nosniff stops a browser second-\n // guessing that decision and rendering opaque bytes as active content.\n \"Content-Type\": contentTypeForFilename(found.filename),\n \"Content-Length\": String(content.bytes.length),\n \"X-Content-Type-Options\": \"nosniff\",\n // Served as a download, never rendered inline — see contentDispositionFor.\n \"Content-Disposition\": contentDispositionFor(found.filename),\n });\n res.end(content.bytes);\n journalFiles(journal, method, path, flattenHeaders(req.headers), 200);\n}\n\nexport async function handleFilesDelete(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fileId: string,\n journal: Journal,\n defaults: { logger: Logger; chaos?: ChaosDefaults; registry?: MetricsRegistry },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? `/v1/files/${fileId}`;\n const method = req.method ?? \"DELETE\";\n // CORS before the chaos gate — see handleFilesRetrieve.\n setCorsHeaders(res);\n\n if (\n await applyChaosAsync(\n res,\n null,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: null, service: \"files\" },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n ) {\n return;\n }\n\n const found = fileStore.get(fileId);\n if (!found) {\n writeJson(res, 404, invalidRequest(`No such file: ${fileId}`), setCorsHeaders);\n journalFiles(journal, method, path, flattenHeaders(req.headers), 404);\n return;\n }\n fileStore.delete(fileId);\n fileContents.delete(fileId);\n removeVectorStoreFile(fileId);\n writeJson(res, 200, { id: fileId, object: \"file\", deleted: true }, setCorsHeaders);\n journalFiles(journal, method, path, flattenHeaders(req.headers), 200);\n}\n"],"mappings":"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"}
|
package/dist/fine-tuning.cjs
CHANGED
|
@@ -297,24 +297,6 @@ function singleParam(params, name) {
|
|
|
297
297
|
};
|
|
298
298
|
}
|
|
299
299
|
/**
|
|
300
|
-
* A plain decimal integer, or null. Deliberately stricter than `Number()`,
|
|
301
|
-
* which also accepts hex (`0x10` → 16), exponent notation (`1e2` → 100), a
|
|
302
|
-
* leading sign (`+5`), a trailing `.0` and surrounding whitespace — none of
|
|
303
|
-
* which is the "integer" the spec's `type: integer` or our own error message
|
|
304
|
-
* describes, and all of which used to be honoured silently at some other
|
|
305
|
-
* number than the one the caller wrote.
|
|
306
|
-
*
|
|
307
|
-
* Kept local rather than shared with the chaos config's own number parsing:
|
|
308
|
-
* that one must accept fractional rates and so goes through `Number()`, which
|
|
309
|
-
* is precisely the behaviour this function exists to refuse. Merging the two
|
|
310
|
-
* would mean widening the lenient one, not renaming this one.
|
|
311
|
-
*/
|
|
312
|
-
function parseDecimalInteger(raw) {
|
|
313
|
-
if (!/^\d+$/.test(raw)) return null;
|
|
314
|
-
const n = Number(raw);
|
|
315
|
-
return Number.isSafeInteger(n) ? n : null;
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
300
|
* Cursor page over an array already ordered newest-first, mirroring the
|
|
319
301
|
* `after`/`limit` params both fine-tuning list endpoints accept. `has_more`
|
|
320
302
|
* reports whether anything was left behind, so it is never a constant.
|
|
@@ -351,7 +333,7 @@ function paginate(items, url) {
|
|
|
351
333
|
const rawLimit = readLimit.value;
|
|
352
334
|
let limit = DEFAULT_PAGE_LIMIT;
|
|
353
335
|
if (rawLimit !== null) {
|
|
354
|
-
const n =
|
|
336
|
+
const n = require_helpers.parseStrictIntegerText(rawLimit);
|
|
355
337
|
if (n === null || n < 1 || n > MAX_PAGE_LIMIT) return {
|
|
356
338
|
ok: false,
|
|
357
339
|
message: `Invalid parameter: 'limit' must be an integer between 1 and ${MAX_PAGE_LIMIT}, got '${rawParam(url, "limit") ?? rawLimit}'`
|