@orkestrel/middleware 0.0.18 → 0.0.19
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/README.md +4 -4
- package/dist/src/core/index.cjs +357 -294
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +390 -290
- package/dist/src/core/index.d.ts +390 -290
- package/dist/src/core/index.js +355 -293
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +472 -410
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +290 -208
- package/dist/src/server/index.d.ts +290 -208
- package/dist/src/server/index.js +464 -405
- package/dist/src/server/index.js.map +1 -1
- package/package.json +18 -19
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["#defaultDirectory","#reader","#signal","#abort","#boundary","#limits","#allowed","#directory","#staged","#files","#fields","#wakeReader","#createDirectory","#buffer","#pull","#fileCount","#fieldCount","#cleanup","#ended","#totalBytes"],"sources":["../../../src/server/constants.ts","../../../src/server/errors.ts","../../../src/server/MultipartParser.ts","../../../src/server/helpers.ts","../../../src/server/middlewares.ts"],"sourcesContent":["import type { MultipartReason } from './types.js'\n\n// ============================================================================\n// @orkestrel/middleware/server — node-face defaults (AGENTS §5\n// constants.ts). Not named in the original dispatch's owned-file list, but\n// added per AGENTS §5 (constants are centralized, never inlined) — the\n// natural completion of the pattern U1 already established on the core\n// face. Documented as a builder latitude decision, not a deviation.\n// ============================================================================\n\n/** The HTTP status `createMultipart` renders for each {@link MultipartReason}. */\nexport const MULTIPART_REASON_STATUS: Readonly<Record<MultipartReason, number>> = Object.freeze({\n\tlimit: 413,\n\tmalformed: 400,\n\trejected: 415,\n})\n\n/** `createStatic`'s default directory-index filename. */\nexport const DEFAULT_STATIC_INDEX = 'index.html'\n\n/** `createStatic`'s `fallback: true` default excluded path prefix. */\nexport const DEFAULT_STATIC_FALLBACK_EXCLUDE = '/api'\n\n/** The MIME type served when a file extension has no known mapping. */\nexport const DEFAULT_CONTENT_TYPE = 'application/octet-stream'\n\n/** `createMultipart`'s default per-file byte-size cap. */\nexport const DEFAULT_MULTIPART_FILE = 10_485_760\n\n/** `createMultipart`'s default maximum file-part count. */\nexport const DEFAULT_MULTIPART_FILES = 10\n\n/** `createMultipart`'s default per-field byte-size cap. */\nexport const DEFAULT_MULTIPART_FIELD = 65_536\n\n/** `createMultipart`'s default maximum field-part count. */\nexport const DEFAULT_MULTIPART_FIELDS = 100\n\n/** `createMultipart`'s default combined request-body byte-size cap. */\nexport const DEFAULT_MULTIPART_TOTAL = 52_428_800\n\n/** The maximum bytes a single multipart part's header block may occupy before it is malformed. */\nexport const MULTIPART_MAX_HEADER_BLOCK = 16_384\n\n/** The maximum bytes scanned before the first multipart boundary is found before it is malformed. */\nexport const MULTIPART_MAX_PREAMBLE = 65_536\n\n/**\n * Windows reserved device-name stems (CVE-2025-27210) — matched\n * case-insensitively against the segment's stem (before its first `.`).\n */\nexport const RESERVED_DEVICE_NAMES: ReadonlySet<string> = Object.freeze(\n\tnew Set([\n\t\t'CON',\n\t\t'PRN',\n\t\t'AUX',\n\t\t'NUL',\n\t\t'COM1',\n\t\t'COM2',\n\t\t'COM3',\n\t\t'COM4',\n\t\t'COM5',\n\t\t'COM6',\n\t\t'COM7',\n\t\t'COM8',\n\t\t'COM9',\n\t\t'LPT1',\n\t\t'LPT2',\n\t\t'LPT3',\n\t\t'LPT4',\n\t\t'LPT5',\n\t\t'LPT6',\n\t\t'LPT7',\n\t\t'LPT8',\n\t\t'LPT9',\n\t]),\n)\n\n/** File-extension (lowercase, with leading `.`) → MIME type lookup table for static serving. */\nexport const EXTENSION_TYPES: Readonly<Record<string, string>> = Object.freeze({\n\t'.html': 'text/html; charset=utf-8',\n\t'.htm': 'text/html; charset=utf-8',\n\t'.css': 'text/css; charset=utf-8',\n\t'.js': 'text/javascript; charset=utf-8',\n\t'.mjs': 'text/javascript; charset=utf-8',\n\t'.json': 'application/json; charset=utf-8',\n\t'.txt': 'text/plain; charset=utf-8',\n\t'.xml': 'application/xml; charset=utf-8',\n\t'.svg': 'image/svg+xml',\n\t'.png': 'image/png',\n\t'.jpg': 'image/jpeg',\n\t'.jpeg': 'image/jpeg',\n\t'.gif': 'image/gif',\n\t'.webp': 'image/webp',\n\t'.ico': 'image/x-icon',\n\t'.pdf': 'application/pdf',\n\t'.zip': 'application/zip',\n\t'.woff': 'font/woff',\n\t'.woff2': 'font/woff2',\n\t'.wasm': 'application/wasm',\n})\n","import type { MultipartReason } from './types.js'\nimport { MULTIPART_REASON_STATUS } from './constants.js'\n\n// ============================================================================\n// @orkestrel/middleware/server — MultipartError (AGENTS §5 errors.ts).\n// Modeled on the peer `@orkestrel/server` HTTPError class shape (status +\n// message + optional context) — MultipartError additionally carries the\n// `reason` axis createMultipart's caller narrows on, mapped to its HTTP\n// status via {@link MULTIPART_REASON_STATUS}: 'limit' → 413, 'malformed' →\n// 400, 'rejected' → 415.\n// ============================================================================\n\n/**\n * An error `createMultipart` throws when a streamed multipart request fails\n * a mid-stream limit, is structurally malformed, or has a file whose sniffed\n * bytes are rejected by the configured `allowed` MIME list.\n *\n * @remarks\n * Carries the HTTP `status` derived from `reason` (limit → 413, malformed →\n * 400, rejected → 415) and an optional `context` record. Rendered by\n * `createBoundary` like any other `HTTPError`-shaped throw. Narrow a caught\n * value with {@link isMultipartError}.\n *\n * @example\n * ```ts\n * import { MultipartError } from '@orkestrel/middleware/server'\n *\n * throw new MultipartError('limit', 'too many files')\n * ```\n */\nexport class MultipartError extends Error {\n\treadonly status: number\n\treadonly reason: MultipartReason\n\treadonly context?: Readonly<Record<string, unknown>>\n\n\tconstructor(\n\t\treason: MultipartReason,\n\t\tmessage: string,\n\t\tcontext?: Readonly<Record<string, unknown>>,\n\t) {\n\t\tsuper(message)\n\t\tthis.status = MULTIPART_REASON_STATUS[reason]\n\t\tthis.reason = reason\n\t\tif (context !== undefined) this.context = context\n\t\tObject.defineProperty(this, Symbol.for('@orkestrel/middleware.MultipartError'), {\n\t\t\tvalue: true,\n\t\t})\n\t}\n}\n\n/**\n * Narrow an unknown caught value to a {@link MultipartError}.\n *\n * @remarks\n * Structural, not `instanceof` — tests that `value` is a non-null object\n * carrying the module-scope brand, a numeric `status`, and a `reason` in the\n * parser's set of reason strings (`'limit' | 'malformed' | 'rejected'`).\n * Total: never throws, returns `false` for any off-shape input.\n *\n * @param value - The value to test (typically a `catch` binding)\n * @returns `true` when `value` is a {@link MultipartError}\n *\n * @example\n * ```ts\n * import { isMultipartError } from '@orkestrel/middleware/server'\n *\n * try {\n * \tawait parse(request)\n * } catch (error) {\n * \tif (isMultipartError(error)) console.log(error.status, error.reason)\n * }\n * ```\n */\nexport function isMultipartError(value: unknown): value is MultipartError {\n\tif (typeof value !== 'object' || value === null) return false\n\tif (!(Symbol.for('@orkestrel/middleware.MultipartError') in value)) return false\n\tif (!('status' in value) || !('reason' in value)) return false\n\tif (typeof value.status !== 'number') return false\n\tif (value.reason !== 'limit' && value.reason !== 'malformed' && value.reason !== 'rejected')\n\t\treturn false\n\treturn true\n}\n","import type { MultipartBody, MultipartFile } from '@src/core'\nimport type { MultipartLimits } from './types.js'\nimport { randomUUID } from 'node:crypto'\nimport { chmod, mkdtemp, open, unlink } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport { isDangerousKey } from '@orkestrel/server'\nimport {\n\tDEFAULT_CONTENT_TYPE,\n\tMULTIPART_MAX_HEADER_BLOCK,\n\tMULTIPART_MAX_PREAMBLE,\n} from './constants.js'\nimport { MultipartError } from './errors.js'\nimport { createUploadedFile, detectMIME, parsePartHeaders } from './helpers.js'\n\nexport class MultipartParser {\n\tstatic #defaultDirectory: Promise<string> | undefined\n\treadonly #reader: ReadableStreamDefaultReader<Uint8Array>\n\treadonly #signal: AbortSignal\n\treadonly #abort: () => void\n\treadonly #boundary: string\n\treadonly #limits: Required<MultipartLimits>\n\treadonly #allowed: readonly string[] | undefined\n\treadonly #directory: string\n\treadonly #staged: string[] = []\n\treadonly #files: Record<string, MultipartFile[]> = Object.create(null)\n\treadonly #fields: Record<string, string> = Object.create(null)\n\t#buffer = Buffer.alloc(0)\n\t#ended = false\n\t#fileCount = 0\n\t#fieldCount = 0\n\t#totalBytes = 0\n\n\tconstructor(\n\t\tstream: ReadableStream<Uint8Array>,\n\t\tsignal: AbortSignal,\n\t\tboundary: string,\n\t\tlimits: Required<MultipartLimits>,\n\t\tallowed: readonly string[] | undefined,\n\t\tdirectory: string,\n\t) {\n\t\tthis.#reader = stream.getReader()\n\t\tthis.#signal = signal\n\t\tthis.#abort = this.#wakeReader.bind(this)\n\t\tthis.#boundary = boundary\n\t\tthis.#limits = limits\n\t\tthis.#allowed = allowed\n\t\tthis.#directory = directory\n\t}\n\n\tstatic directory(): Promise<string> {\n\t\tif (MultipartParser.#defaultDirectory === undefined)\n\t\t\tMultipartParser.#defaultDirectory = MultipartParser.#createDirectory()\n\t\treturn MultipartParser.#defaultDirectory\n\t}\n\n\tasync parse(): Promise<MultipartBody> {\n\t\tthis.#signal.addEventListener('abort', this.#abort, { once: true })\n\t\ttry {\n\t\t\tconst openMarker = Buffer.from(`--${this.#boundary}`)\n\t\t\tlet preambleScanned = 0\n\t\t\tlet index = this.#buffer.indexOf(openMarker)\n\t\t\twhile (index === -1) {\n\t\t\t\tconst carry = openMarker.length - 1\n\t\t\t\tif (this.#buffer.length > carry) {\n\t\t\t\t\tconst drop = this.#buffer.length - carry\n\t\t\t\t\tpreambleScanned += drop\n\t\t\t\t\tif (preambleScanned > MULTIPART_MAX_PREAMBLE)\n\t\t\t\t\t\tthrow new MultipartError('malformed', 'multipart preamble too large')\n\t\t\t\t\tthis.#buffer = this.#buffer.subarray(drop)\n\t\t\t\t}\n\t\t\t\tif (!(await this.#pull()))\n\t\t\t\t\tthrow new MultipartError('malformed', 'missing multipart boundary')\n\t\t\t\tindex = this.#buffer.indexOf(openMarker)\n\t\t\t}\n\t\t\tif (preambleScanned + index > MULTIPART_MAX_PREAMBLE)\n\t\t\t\tthrow new MultipartError('malformed', 'multipart preamble too large')\n\t\t\tthis.#buffer = this.#buffer.subarray(index + openMarker.length)\n\n\t\t\tfor (;;) {\n\t\t\t\twhile (this.#buffer.length < 2)\n\t\t\t\t\tif (!(await this.#pull()))\n\t\t\t\t\t\tthrow new MultipartError('malformed', 'unterminated multipart boundary')\n\t\t\t\tif (this.#buffer[0] === 0x2d && this.#buffer[1] === 0x2d) break\n\t\t\t\tif (this.#buffer[0] !== 0x0d || this.#buffer[1] !== 0x0a)\n\t\t\t\t\tthrow new MultipartError('malformed', 'malformed multipart boundary')\n\t\t\t\tthis.#buffer = this.#buffer.subarray(2)\n\n\t\t\t\tlet headerEnd = this.#buffer.indexOf('\\r\\n\\r\\n')\n\t\t\t\twhile (headerEnd === -1) {\n\t\t\t\t\tif (this.#buffer.length > MULTIPART_MAX_HEADER_BLOCK)\n\t\t\t\t\t\tthrow new MultipartError('malformed', 'multipart header block too large')\n\t\t\t\t\tif (!(await this.#pull()))\n\t\t\t\t\t\tthrow new MultipartError('malformed', 'unterminated multipart part headers')\n\t\t\t\t\theaderEnd = this.#buffer.indexOf('\\r\\n\\r\\n')\n\t\t\t\t}\n\t\t\t\tif (headerEnd > MULTIPART_MAX_HEADER_BLOCK)\n\t\t\t\t\tthrow new MultipartError('malformed', 'multipart header block too large')\n\t\t\t\tconst headerBlock = this.#buffer.subarray(0, headerEnd).toString('utf8')\n\t\t\t\tthis.#buffer = this.#buffer.subarray(headerEnd + 4)\n\t\t\t\tconst { name, filename, contentType } = parsePartHeaders(headerBlock)\n\t\t\t\tif (name === undefined) throw new MultipartError('malformed', 'multipart part missing name')\n\n\t\t\t\tconst partDelimiter = Buffer.from(`\\r\\n--${this.#boundary}`)\n\n\t\t\t\tif (filename !== undefined) {\n\t\t\t\t\tif (filename !== '') {\n\t\t\t\t\t\tthis.#fileCount += 1\n\t\t\t\t\t\tif (this.#fileCount > this.#limits.files)\n\t\t\t\t\t\t\tthrow new MultipartError('limit', 'too many multipart files')\n\t\t\t\t\t}\n\t\t\t\t\tconst path = join(this.#directory, randomUUID())\n\t\t\t\t\tthis.#staged.push(path)\n\t\t\t\t\tconst handle = await open(path, 'w', 0o600)\n\t\t\t\t\tlet size = 0\n\t\t\t\t\tlet head = Buffer.alloc(0)\n\t\t\t\t\ttry {\n\t\t\t\t\t\tfor (;;) {\n\t\t\t\t\t\t\tconst boundaryIndex = this.#buffer.indexOf(partDelimiter)\n\t\t\t\t\t\t\tif (boundaryIndex === -1) {\n\t\t\t\t\t\t\t\tconst safeLength = Math.max(0, this.#buffer.length - (partDelimiter.length - 1))\n\t\t\t\t\t\t\t\tif (safeLength > 0) {\n\t\t\t\t\t\t\t\t\tconst chunk = this.#buffer.subarray(0, safeLength)\n\t\t\t\t\t\t\t\t\tsize += chunk.length\n\t\t\t\t\t\t\t\t\tif (size > this.#limits.file)\n\t\t\t\t\t\t\t\t\t\tthrow new MultipartError('limit', 'multipart file exceeds size limit')\n\t\t\t\t\t\t\t\t\tif (head.length < 16)\n\t\t\t\t\t\t\t\t\t\thead = Buffer.concat([head, chunk.subarray(0, 16 - head.length)])\n\t\t\t\t\t\t\t\t\tawait handle.write(chunk)\n\t\t\t\t\t\t\t\t\tthis.#buffer = this.#buffer.subarray(safeLength)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif (!(await this.#pull()))\n\t\t\t\t\t\t\t\t\tthrow new MultipartError('malformed', 'unterminated multipart file part')\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst chunk = this.#buffer.subarray(0, boundaryIndex)\n\t\t\t\t\t\t\tsize += chunk.length\n\t\t\t\t\t\t\tif (size > this.#limits.file)\n\t\t\t\t\t\t\t\tthrow new MultipartError('limit', 'multipart file exceeds size limit')\n\t\t\t\t\t\t\tif (head.length < 16)\n\t\t\t\t\t\t\t\thead = Buffer.concat([head, chunk.subarray(0, 16 - head.length)])\n\t\t\t\t\t\t\tawait handle.write(chunk)\n\t\t\t\t\t\t\tthis.#buffer = this.#buffer.subarray(boundaryIndex + 2)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tawait handle.close()\n\t\t\t\t\t}\n\t\t\t\t\tif (filename === '' && size === 0) {\n\t\t\t\t\t\tawait unlink(path)\n\t\t\t\t\t\tthis.#staged.splice(this.#staged.indexOf(path), 1)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (filename === '') {\n\t\t\t\t\t\t\tthis.#fileCount += 1\n\t\t\t\t\t\t\tif (this.#fileCount > this.#limits.files)\n\t\t\t\t\t\t\t\tthrow new MultipartError('limit', 'too many multipart files')\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst detected = detectMIME(head)\n\t\t\t\t\t\tconst declared = contentType ?? DEFAULT_CONTENT_TYPE\n\t\t\t\t\t\tconst validated = detected !== undefined && detected === declared\n\t\t\t\t\t\tif (this.#allowed !== undefined) {\n\t\t\t\t\t\t\tconst acceptable = detected !== undefined && this.#allowed.includes(detected)\n\t\t\t\t\t\t\tif (!acceptable)\n\t\t\t\t\t\t\t\tthrow new MultipartError('rejected', 'multipart file failed type validation')\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (isDangerousKey(name)) {\n\t\t\t\t\t\t\tawait unlink(path)\n\t\t\t\t\t\t\tthis.#staged.splice(this.#staged.indexOf(path), 1)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tconst record = createUploadedFile({\n\t\t\t\t\t\t\t\tfield: name,\n\t\t\t\t\t\t\t\tname: filename,\n\t\t\t\t\t\t\t\tsize,\n\t\t\t\t\t\t\t\tmime: detected ?? declared,\n\t\t\t\t\t\t\t\tvalidated,\n\t\t\t\t\t\t\t\tstatus: 'staged',\n\t\t\t\t\t\t\t\tpath,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\tconst existing = this.#files[name]\n\t\t\t\t\t\t\tif (existing === undefined) this.#files[name] = [record]\n\t\t\t\t\t\t\telse existing.push(record)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tthis.#fieldCount += 1\n\t\t\t\t\tif (this.#fieldCount > this.#limits.fields)\n\t\t\t\t\t\tthrow new MultipartError('limit', 'too many multipart fields')\n\t\t\t\t\tlet value = Buffer.alloc(0)\n\t\t\t\t\tfor (;;) {\n\t\t\t\t\t\tconst boundaryIndex = this.#buffer.indexOf(partDelimiter)\n\t\t\t\t\t\tif (boundaryIndex === -1) {\n\t\t\t\t\t\t\tconst safeLength = Math.max(0, this.#buffer.length - (partDelimiter.length - 1))\n\t\t\t\t\t\t\tif (safeLength > 0) {\n\t\t\t\t\t\t\t\tvalue = Buffer.concat([value, this.#buffer.subarray(0, safeLength)])\n\t\t\t\t\t\t\t\tif (value.length > this.#limits.field)\n\t\t\t\t\t\t\t\t\tthrow new MultipartError('limit', 'multipart field exceeds size limit')\n\t\t\t\t\t\t\t\tthis.#buffer = this.#buffer.subarray(safeLength)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!(await this.#pull()))\n\t\t\t\t\t\t\t\tthrow new MultipartError('malformed', 'unterminated multipart field part')\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvalue = Buffer.concat([value, this.#buffer.subarray(0, boundaryIndex)])\n\t\t\t\t\t\tif (value.length > this.#limits.field)\n\t\t\t\t\t\t\tthrow new MultipartError('limit', 'multipart field exceeds size limit')\n\t\t\t\t\t\tthis.#buffer = this.#buffer.subarray(boundaryIndex + 2)\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tif (!isDangerousKey(name)) this.#fields[name] = value.toString('utf8')\n\t\t\t\t}\n\n\t\t\t\twhile (this.#buffer.length < openMarker.length)\n\t\t\t\t\tif (!(await this.#pull()))\n\t\t\t\t\t\tthrow new MultipartError('malformed', 'unterminated multipart boundary')\n\t\t\t\tthis.#buffer = this.#buffer.subarray(openMarker.length)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tawait this.#cleanup()\n\t\t\tawait this.#reader.cancel().catch(() => {})\n\t\t\tthrow error\n\t\t} finally {\n\t\t\tthis.#signal.removeEventListener('abort', this.#abort)\n\t\t\tif (!this.#ended) await this.#reader.cancel().catch(() => {})\n\t\t}\n\n\t\treturn {\n\t\t\tfiles: Object.freeze(this.#files),\n\t\t\tfields: Object.freeze(this.#fields),\n\t\t}\n\t}\n\n\tstatic async #createDirectory(): Promise<string> {\n\t\tconst path = await mkdtemp(join(tmpdir(), 'orkestrel-multipart-'))\n\t\tawait chmod(path, 0o700)\n\t\treturn path\n\t}\n\n\tasync #cleanup(): Promise<void> {\n\t\tfor (const path of this.#staged) {\n\t\t\ttry {\n\t\t\t\tawait unlink(path)\n\t\t\t} catch {\n\t\t\t\t// Already gone — cleanup is best-effort.\n\t\t\t}\n\t\t}\n\t}\n\n\tasync #pull(): Promise<boolean> {\n\t\tif (this.#signal.aborted) throw new MultipartError('malformed', 'request aborted mid-upload')\n\t\tif (this.#ended) return false\n\t\tconst { done, value } = await this.#reader.read()\n\t\tif (this.#signal.aborted) throw new MultipartError('malformed', 'request aborted mid-upload')\n\t\tif (done) {\n\t\t\tthis.#ended = true\n\t\t\treturn false\n\t\t}\n\t\tthis.#totalBytes += value.byteLength\n\t\tif (this.#totalBytes > this.#limits.total)\n\t\t\tthrow new MultipartError('limit', 'multipart body exceeds total limit')\n\t\tthis.#buffer = Buffer.concat([\n\t\t\tthis.#buffer,\n\t\t\tBuffer.from(value.buffer, value.byteOffset, value.byteLength),\n\t\t])\n\t\treturn true\n\t}\n\n\t#wakeReader(): void {\n\t\tthis.#reader.cancel().catch(() => {})\n\t}\n}\n","import type { MultipartBody } from '@src/core'\nimport type {\n\tMultipartLimits,\n\tMultipartOptions,\n\tPartHeaders,\n\tUploadedFileInput,\n\tUploadedFileInterface,\n} from './types.js'\nimport type { FileHandle } from 'node:fs/promises'\nimport type { Encoding } from '@orkestrel/server'\nimport { createReadStream } from 'node:fs'\nimport { copyFile, readFile, rename, unlink } from 'node:fs/promises'\nimport { extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path'\nimport { promisify } from 'node:util'\nimport { deflate as zlibDeflate, gzip as zlibGzip } from 'node:zlib'\nimport { isRecord } from '@orkestrel/contract'\nimport {\n\tDEFAULT_CONTENT_TYPE,\n\tDEFAULT_MULTIPART_FIELD,\n\tDEFAULT_MULTIPART_FIELDS,\n\tDEFAULT_MULTIPART_FILE,\n\tDEFAULT_MULTIPART_FILES,\n\tDEFAULT_MULTIPART_TOTAL,\n\tEXTENSION_TYPES,\n\tRESERVED_DEVICE_NAMES,\n} from './constants.js'\nimport { MultipartError } from './errors.js'\nimport { MultipartParser } from './MultipartParser.js'\n\n// ============================================================================\n// @orkestrel/middleware/server — node-face pure/near-pure leaves (AGENTS §5\n// helpers.ts). The static-file traversal guard, the reserved-device-name and\n// dotfile screens, the extension/MIME lookup, the file ETag formula, the\n// magic-byte MIME sniffer, the multipart boundary parser, the streaming\n// multipart state machine, and the post-parse uploaded-file operations.\n// ============================================================================\n\n/**\n * Whether `pathname` is `prefix` itself or lies under it on a SEGMENT\n * boundary — the shared under-path test `resolveStaticPath`'s prefix strip\n * and `createStatic`'s SPA-fallback `exclude` both apply, so `exclude:\n * '/api'` matches `/api` and `/api/x` but never `/apifoo`.\n *\n * @param pathname - The request pathname to test\n * @param prefix - The path prefix to test against\n * @returns `true` when `pathname` equals `prefix` or starts with `prefix` + `/`\n *\n * @example\n * ```ts\n * isUnderPath('/api/x', '/api') // true\n * isUnderPath('/apifoo', '/api') // false\n * ```\n */\nexport function isUnderPath(pathname: string, prefix: string): boolean {\n\tif (pathname === prefix) return true\n\tconst boundary = prefix.endsWith('/') ? prefix : `${prefix}/`\n\treturn pathname.startsWith(boundary)\n}\n\n/**\n * Resolve the fixed SPA shell path when a static-file miss is eligible for\n * fallback.\n *\n * @param root - The configured static root\n * @param index - The configured shell filename\n * @param exclude - The URL prefix excluded from fallback\n * @param method - The request method\n * @param pathname - The request pathname\n * @param accept - The request's `Accept` header value\n * @returns The fixed shell path, or `undefined` when fallback is ineligible\n *\n * @example\n * ```ts\n * resolveStaticFallbackPath('/srv/public', 'index.html', '/api', 'GET', '/dashboard', 'text/html')\n * // '/srv/public/index.html'\n * ```\n */\nexport function resolveStaticFallbackPath(\n\troot: string,\n\tindex: string,\n\texclude: string,\n\tmethod: string,\n\tpathname: string,\n\taccept: string,\n): string | undefined {\n\tif (method !== 'GET') return undefined\n\tif (extname(pathname) !== '') return undefined\n\tif (!accept.includes('text/html') && !accept.includes('*/*')) return undefined\n\tif (isUnderPath(pathname, exclude)) return undefined\n\treturn join(root, index)\n}\n\n/**\n * Whether `child` is `parent` itself or lies inside it on-disk — the\n * FILESYSTEM containment predicate `createStatic` applies to `fs.realpath`\n * output (never to a URL pathname — that is {@link isUnderPath}'s job).\n *\n * @remarks\n * Argument order is `(child, parent)` — deliberately the OPPOSITE conceptual\n * order from {@link isUnderPath}`(pathname, prefix)`, so a call site cannot\n * casually swap one predicate in for the other. Built on `path.relative`,\n * this is separator-correct on both POSIX (`/`) and win32 (`\\`) — unlike a\n * hardcoded `${parent}/` boundary check, which silently fails to match every\n * realpath on Windows — and case-folds on win32 because `path.relative` does.\n *\n * @param child - The absolute on-disk path to test\n * @param parent - The absolute on-disk directory it must lie under\n * @returns `true` when `child` equals `parent` or resolves inside it\n *\n * @example\n * ```ts\n * isContainedPath('/srv/public/a.html', '/srv/public') // true\n * isContainedPath('/srv/other/a.html', '/srv/public') // false\n * ```\n */\nexport function isContainedPath(child: string, parent: string): boolean {\n\tif (child === parent) return true\n\tconst rel = relative(parent, child)\n\treturn rel.length > 0 && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)\n}\n\n/**\n * Resolve a request pathname to an on-disk path UNDER `root`, or `undefined`\n * when it cannot — the traversal guard, EXACT algorithm and order (PROPOSAL\n * §4.14): strip `prefix` on a segment boundary → `decodeURIComponent` (a\n * malformed escape refuses, never throws) → reject a NUL byte → strip the\n * leading path separator FIRST (so a leading `..` survives `normalize` as a\n * genuine climbing segment) → `normalize` → refuse any Windows reserved-\n * device-name segment ({@link isReservedDeviceName}) → `resolve` and require\n * the result under `root`.\n *\n * @param root - The absolute root directory every result must resolve under\n * @param prefix - An optional URL path prefix stripped on a segment boundary\n * @param pathname - The raw request pathname\n * @returns The resolved absolute path under `root`, or `undefined` when the\n * request does not resolve (out of prefix, malformed escape, NUL byte,\n * reserved device name, or an attempted escape from `root`)\n *\n * @example\n * ```ts\n * resolveStaticPath('/srv/public', '/api', '/api/../../etc/passwd') // undefined\n * ```\n */\nexport function resolveStaticPath(\n\troot: string,\n\tprefix: string | undefined,\n\tpathname: string,\n): string | undefined {\n\tlet remainder = pathname\n\tif (prefix !== undefined) {\n\t\tif (!isUnderPath(pathname, prefix)) return undefined\n\t\tremainder = pathname === prefix ? '/' : pathname.slice(prefix.length)\n\t}\n\tlet decoded: string\n\ttry {\n\t\tdecoded = decodeURIComponent(remainder)\n\t} catch {\n\t\treturn undefined\n\t}\n\tif (decoded.includes('\\0')) return undefined\n\tconst stripped = decoded.replace(/^[/\\\\]+/, '')\n\tconst normalized = normalize(stripped)\n\tconst segments = normalized.split(/[/\\\\]+/).filter((segment) => segment.length > 0)\n\tfor (const segment of segments) if (isReservedDeviceName(segment)) return undefined\n\tconst resolved = resolve(root, normalized)\n\tif (resolved === root || resolved.startsWith(`${root}${sep}`)) return resolved\n\treturn undefined\n}\n\n/**\n * Whether a path segment is a Windows reserved device name (CVE-2025-27210).\n *\n * @remarks\n * Normalizes superscript digits (`¹²³` → `123`) first, strips trailing dots\n * and spaces (Windows drops them), takes the STEM before the first `.`,\n * upper-cases it, and tests it against {@link RESERVED_DEVICE_NAMES}\n * (`CON PRN AUX NUL COM1-9 LPT1-9`) — an exact-stem match only, so\n * `console.js` and `nullable.css` are never flagged.\n *\n * @param segment - One path segment (no separators)\n * @returns `true` when `segment` names a reserved device\n *\n * @example\n * ```ts\n * isReservedDeviceName('NUL.json') // true\n * isReservedDeviceName('nullable.css') // false\n * isReservedDeviceName('CON¹') // true\n * ```\n */\nexport function isReservedDeviceName(segment: string): boolean {\n\tconst superscripted = segment.replace(/[¹²³]/g, (digit) =>\n\t\tdigit === '¹' ? '1' : digit === '²' ? '2' : '3',\n\t)\n\tconst trimmed = superscripted.replace(/[. ]+$/, '')\n\tconst stem = trimmed.split('.')[0]\n\tif (stem === undefined || stem.length === 0) return false\n\treturn RESERVED_DEVICE_NAMES.has(stem.toUpperCase())\n}\n\n/**\n * Whether a relative path (already resolved under a static root) has any\n * segment starting with `.` — a dotfile or dot-directory.\n *\n * @param relativePath - A path relative to the static root\n * @returns `true` when any segment starts with `.`\n *\n * @example\n * ```ts\n * isDotfilePath('.env') // true\n * isDotfilePath('a/.git/config') // true\n * ```\n */\nexport function isDotfilePath(relativePath: string): boolean {\n\treturn relativePath.split(/[/\\\\]+/).some((segment) => segment.startsWith('.'))\n}\n\n/**\n * Look up the MIME type for a static file path by its extension.\n *\n * @param pathname - The file's path (only its extension is read)\n * @returns The mapped MIME type, or {@link DEFAULT_CONTENT_TYPE} when unknown\n *\n * @example\n * ```ts\n * lookupContentType('/a/b.css') // 'text/css; charset=utf-8'\n * ```\n */\nexport function lookupContentType(pathname: string): string {\n\tconst extension = extname(pathname).toLowerCase()\n\tconst mapped = EXTENSION_TYPES[extension]\n\treturn mapped ?? DEFAULT_CONTENT_TYPE\n}\n\n/**\n * Compute a static file's weak ETag from its size and modification time.\n *\n * @param size - The file's byte size\n * @param mtimeMs - The file's modification time in milliseconds\n * @returns A weak entity-tag `W/\"<size>-<floor(mtimeMs)>\"`\n *\n * @example\n * ```ts\n * computeFileETag(1024, 1700000000123.4) // 'W/\"1024-1700000000123\"'\n * ```\n */\nexport function computeFileETag(size: number, mtimeMs: number): string {\n\treturn `W/\"${size}-${Math.floor(mtimeMs)}\"`\n}\n\n/**\n * Compress response bytes with Node's guaranteed zlib gzip/deflate codecs.\n *\n * @param bytes - The uncompressed response bytes\n * @param encoding - The negotiated actionable coding\n * @returns The compressed bytes\n *\n * @example\n * ```ts\n * const bytes = new TextEncoder().encode('compress me')\n * const compressed = await compressNodeBytes(bytes, 'deflate')\n * ```\n */\nexport async function compressNodeBytes(\n\tbytes: Uint8Array<ArrayBuffer>,\n\tencoding: Exclude<Encoding, 'identity'>,\n): Promise<Uint8Array<ArrayBuffer>> {\n\tconst compressed =\n\t\tencoding === 'gzip' ? await promisify(zlibGzip)(bytes) : await promisify(zlibDeflate)(bytes)\n\treturn Uint8Array.from(compressed)\n}\n\n/**\n * Sniff a MIME type from a file's leading bytes against a small magic-byte\n * table (jpeg, png, gif87a/89a, webp, pdf, zip) — the SNIFF-AUTHORITATIVE\n * signal `createMultipart`'s type validation rests on, never the declared\n * `Content-Type`.\n *\n * @param head - The file's first bytes (16 is sufficient for every signature)\n * @returns The detected MIME type, or `undefined` when no signature matches\n *\n * @example\n * ```ts\n * detectMIME(Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) // 'image/png'\n * ```\n */\nexport function detectMIME(head: Uint8Array): string | undefined {\n\tif (matchesBytes(head, [0xff, 0xd8, 0xff])) return 'image/jpeg'\n\tif (matchesBytes(head, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return 'image/png'\n\tif (matchesBytes(head, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61])) return 'image/gif'\n\tif (matchesBytes(head, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) return 'image/gif'\n\tif (\n\t\tmatchesBytes(head, [0x52, 0x49, 0x46, 0x46]) &&\n\t\tmatchesBytes(head, [0x57, 0x45, 0x42, 0x50], 8)\n\t)\n\t\treturn 'image/webp'\n\tif (matchesBytes(head, [0x25, 0x50, 0x44, 0x46, 0x2d])) return 'application/pdf'\n\tif (\n\t\tmatchesBytes(head, [0x50, 0x4b, 0x03, 0x04]) ||\n\t\tmatchesBytes(head, [0x50, 0x4b, 0x05, 0x06]) ||\n\t\tmatchesBytes(head, [0x50, 0x4b, 0x07, 0x08])\n\t)\n\t\treturn 'application/zip'\n\treturn undefined\n}\n\n/**\n * Whether `bytes` contains `signature` at the requested offset.\n *\n * @param bytes - The bytes to inspect\n * @param signature - The exact byte sequence to match\n * @param offset - The starting byte offset, defaulting to zero\n * @returns `true` when the complete signature matches\n *\n * @example\n * ```ts\n * matchesBytes(Uint8Array.from([0, 0x50, 0x4b]), [0x50, 0x4b], 1) // true\n * ```\n */\nexport function matchesBytes(bytes: Uint8Array, signature: readonly number[], offset = 0): boolean {\n\tif (bytes.length < offset + signature.length) return false\n\tfor (let index = 0; index < signature.length; index += 1)\n\t\tif (bytes[offset + index] !== signature[index]) return false\n\treturn true\n}\n\n/**\n * Extract the `boundary` parameter from a `Content-Type` header, or\n * `undefined` when the request is not `multipart/form-data`.\n *\n * @param contentType - The request's `Content-Type` header value, if present\n * @returns The multipart boundary token, or `undefined` for a non-multipart\n * (or malformed/boundary-less) content type\n *\n * @example\n * ```ts\n * multipartBoundary('multipart/form-data; boundary=abc123') // 'abc123'\n * multipartBoundary('application/json') // undefined\n * ```\n */\nexport function multipartBoundary(contentType: string | null): string | undefined {\n\tif (contentType === null) return undefined\n\tconst [type, ...params] = contentType.split(';').map((part) => part.trim())\n\tif (type === undefined || type.toLowerCase() !== 'multipart/form-data') return undefined\n\tfor (const param of params) {\n\t\tconst equals = param.indexOf('=')\n\t\tif (equals === -1) continue\n\t\tconst key = param.slice(0, equals).trim().toLowerCase()\n\t\tif (key !== 'boundary') continue\n\t\tlet value = param.slice(equals + 1).trim()\n\t\tif (value.startsWith('\"') && value.endsWith('\"') && value.length >= 2)\n\t\t\tvalue = value.slice(1, -1)\n\t\treturn value.length > 0 ? value : undefined\n\t}\n\treturn undefined\n}\n\n/**\n * Resolve `createMultipart`'s effective {@link MultipartLimits}, applying\n * every documented default.\n *\n * @param limits - The caller's partial limits\n * @returns The fully-resolved limits\n */\nexport function resolveMultipartLimits(\n\tlimits: MultipartLimits | undefined,\n): Required<MultipartLimits> {\n\treturn {\n\t\tfile: limits?.file ?? DEFAULT_MULTIPART_FILE,\n\t\tfiles: limits?.files ?? DEFAULT_MULTIPART_FILES,\n\t\tfield: limits?.field ?? DEFAULT_MULTIPART_FIELD,\n\t\tfields: limits?.fields ?? DEFAULT_MULTIPART_FIELDS,\n\t\ttotal: limits?.total ?? DEFAULT_MULTIPART_TOTAL,\n\t}\n}\n\n/**\n * Resolve `parseMultipartRequest`'s default staging directory when the\n * caller did not configure one — a process-owned directory created ONCE\n * (lazily, memoized across calls) via `mkdtemp` under `os.tmpdir()` and\n * locked to mode `0o700`.\n *\n * @returns The absolute path of the process-owned staging directory\n *\n * @example\n * ```ts\n * const directory = await resolveDefaultDirectory()\n * ```\n */\nexport function resolveDefaultDirectory(): Promise<string> {\n\treturn MultipartParser.directory()\n}\n\n/**\n * Parse one multipart part's raw header block into its `name` (from\n * `Content-Disposition`), optional `filename`, and optional `Content-Type`.\n *\n * @param block - The raw header block for one multipart part (before the\n * terminating blank line)\n * @returns The parsed `name`, `filename`, and `contentType` (each\n * `undefined` when absent)\n *\n * @example\n * ```ts\n * parsePartHeaders('Content-Disposition: form-data; name=\"title\"')\n * // { name: 'title', filename: undefined, contentType: undefined }\n * ```\n */\nexport function parsePartHeaders(block: string): PartHeaders {\n\tlet name: string | undefined\n\tlet filename: string | undefined\n\tlet contentType: string | undefined\n\tfor (const line of block.split('\\r\\n')) {\n\t\tconst colon = line.indexOf(':')\n\t\tif (colon === -1) continue\n\t\tconst key = line.slice(0, colon).trim().toLowerCase()\n\t\tconst value = line.slice(colon + 1).trim()\n\t\tif (key === 'content-disposition') {\n\t\t\tconst nameMatch = /;\\s*name=\"([^\"]*)\"/.exec(value)\n\t\t\tconst filenameMatch = /;\\s*filename=\"([^\"]*)\"/.exec(value)\n\t\t\tif (nameMatch !== null) name = nameMatch[1]\n\t\t\tif (filenameMatch !== null) filename = filenameMatch[1]\n\t\t} else if (key === 'content-type') {\n\t\t\tcontentType = value\n\t\t}\n\t}\n\treturn { name, filename, contentType }\n}\n\n/**\n * Stream-parse a `multipart/form-data` request into its files and fields —\n * the mid-stream state machine `createMultipart` drives (PROPOSAL §4.15).\n *\n * @remarks\n * Reads `request.body` chunk by chunk via its `ReadableStream` reader —\n * NEVER buffers the whole body — enforcing every {@link MultipartLimits} cap\n * the instant it is exceeded (reading stops, every already-staged temp file\n * is deleted, throws {@link MultipartError} with reason `'limit'`). Each file\n * part streams to `join(directory, randomUUID())` — the client's declared\n * filename is METADATA ONLY, never a path component. A field OR file part\n * named `__proto__` / `constructor` / `prototype` is silently skipped and\n * never keyed onto the returned {@link MultipartBody} (a skipped file's\n * staged temp file is unlinked immediately, since it can never be\n * referenced). A file part with an empty declared filename (`filename=\"\"`)\n * AND a zero-byte body — the browser convention for an unselected optional\n * `<input type=\"file\">` — is a silent no-op: its temp file is unlinked, it is\n * never counted against the `files` limit, and it never runs the `allowed`\n * check. A malformed\n * structure (missing/unterminated boundary, nameless part, an oversized\n * header block, or a preamble exceeding {@link MULTIPART_MAX_PREAMBLE} before\n * the first boundary) throws with reason `'malformed'`. A file is accepted\n * against the configured `allowed` MIME list iff its SNIFFED bytes detect a\n * type present in the list — sniff-authoritative, independent of whether the\n * declared `Content-Type` matches (that agreement is exposed separately as\n * `validated`); otherwise throws with reason `'rejected'`. A\n * request abort mid-upload triggers the same fail-closed cleanup as a limit\n * breach. Returns `undefined` for a non-multipart request (untouched).\n *\n * @param request - The incoming multipart request\n * @param options - See {@link MultipartOptions}\n * @returns The parsed {@link MultipartBody}, or `undefined` when the request\n * is not `multipart/form-data`\n * @throws {MultipartError} On any limit breach, malformed structure, or\n * rejected file type\n *\n * @example\n * ```ts\n * const body = await parseMultipartRequest(request, { allowed: ['image/png'] })\n * ```\n */\nexport async function parseMultipartRequest(\n\trequest: Request,\n\toptions: MultipartOptions = {},\n): Promise<MultipartBody | undefined> {\n\tconst boundary = multipartBoundary(request.headers.get('content-type'))\n\tif (boundary === undefined) return undefined\n\tif (request.body === null) throw new MultipartError('malformed', 'multipart request has no body')\n\n\tconst limits = resolveMultipartLimits(options.limits)\n\tconst allowed = options.allowed\n\tconst directory = options.directory ?? (await resolveDefaultDirectory())\n\treturn new MultipartParser(\n\t\trequest.body,\n\t\trequest.signal,\n\t\tboundary,\n\t\tlimits,\n\t\tallowed,\n\t\tdirectory,\n\t).parse()\n}\n\n/**\n * Build a frozen {@link UploadedFileInterface} record.\n *\n * @param input - Every field of the record\n * @returns A frozen {@link UploadedFileInterface}\n *\n * @example\n * ```ts\n * createUploadedFile({ field: 'avatar', name: 'a.png', size: 1024, mime: 'image/png', validated: true, status: 'staged', path: '/tmp/x' })\n * ```\n */\nexport function createUploadedFile(input: UploadedFileInput): UploadedFileInterface {\n\treturn Object.freeze({ ...input })\n}\n\n/**\n * Best-effort unlink every still-`'staged'` file in a parsed\n * {@link MultipartBody} — the fail-closed cleanup `createMultipart` runs when\n * its downstream handler throws, mirroring `parseMultipartRequest`'s own\n * cleanup pattern (a missing file is already gone; failures are swallowed).\n *\n * @param body - The parsed multipart body to clean up\n * @returns A promise that resolves once every staged file has been attempted\n *\n * @example\n * ```ts\n * await unlinkStagedFiles(body)\n * ```\n */\nexport async function unlinkStagedFiles(body: MultipartBody): Promise<void> {\n\tfor (const records of Object.values(body.files)) {\n\t\tfor (const file of records) {\n\t\t\tif (file.status !== 'staged') continue\n\t\t\ttry {\n\t\t\t\tawait unlink(file.path)\n\t\t\t} catch {\n\t\t\t\t// Already gone — cleanup is best-effort.\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Adapt a `node:fs` read stream over `path` (or an already-open\n * `FileHandle`) into a DOM-compatible `ReadableStream<Uint8Array>` — the\n * single shared node↔web stream bridge every static-file and uploaded-file\n * response body routes through.\n *\n * @remarks\n * PULL-driven, not push-driven: the underlying node stream's async iterator\n * is only advanced (`iterator.next()`) from inside `pull(controller)`, which\n * the web `ReadableStream` invokes exactly when its internal queue has room\n * for more data. Exactly one disk chunk is read and enqueued per `pull` —\n * never more — so a slow or stalled consumer (a stalled HTTP connection)\n * simply stops triggering `pull` calls and the source stops reading ahead;\n * this is genuine consumer backpressure, not the \"naturally backpressured\"\n * `for await`/`enqueue` pattern (which does not block on a slow consumer at\n * all, since `enqueue` returns synchronously). The controller is closed on\n * iterator completion and errored (never thrown into the process) on a\n * mid-stream read failure. Cancelling the returned `ReadableStream` (e.g. the\n * consumer aborts the response) calls the iterator's `return()`, which\n * destroys the underlying node read stream so the file descriptor is\n * released. When `path` is a `FileHandle`, `FileHandle.createReadStream`'s\n * default `autoClose` closes the handle on every terminal path (end, error,\n * or `destroy()` via the iterator's `return()`) — the caller never needs a\n * separate `handle.close()` for a handle passed here.\n *\n * @param source - The absolute on-disk file path to stream, or an already-open\n * `FileHandle` (e.g. one already `fstat`'d so the served bytes match the\n * headers computed from that same `fstat`)\n * @param range - An optional inclusive byte range (`start`/`end`, both\n * 0-indexed and inclusive, matching `node:fs`'s `createReadStream` options)\n * @returns A `ReadableStream<Uint8Array>` valid as a fetch `BodyInit`\n *\n * @example\n * ```ts\n * new Response(streamFile('/srv/public/index.html'))\n * ```\n */\nexport function streamFile(\n\tsource: string | FileHandle,\n\trange?: { readonly start: number; readonly end: number },\n): ReadableStream<Uint8Array> {\n\tconst stream =\n\t\ttypeof source === 'string'\n\t\t\t? range === undefined\n\t\t\t\t? createReadStream(source)\n\t\t\t\t: createReadStream(source, { start: range.start, end: range.end })\n\t\t\t: range === undefined\n\t\t\t\t? source.createReadStream()\n\t\t\t\t: source.createReadStream({ start: range.start, end: range.end })\n\tconst iterator: AsyncIterator<unknown> = stream[Symbol.asyncIterator]()\n\treturn new ReadableStream<Uint8Array>({\n\t\tasync pull(controller) {\n\t\t\ttry {\n\t\t\t\tconst { done, value } = await iterator.next()\n\t\t\t\tif (done) {\n\t\t\t\t\tcontroller.close()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!(value instanceof Uint8Array)) {\n\t\t\t\t\tawait iterator.return?.()\n\t\t\t\t\tcontroller.error(new TypeError('streamFile: read stream yielded a non-Uint8Array chunk'))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tcontroller.enqueue(value)\n\t\t\t} catch (error) {\n\t\t\t\tawait iterator.return?.()\n\t\t\t\tcontroller.error(error)\n\t\t\t}\n\t\t},\n\t\tasync cancel() {\n\t\t\tawait iterator.return?.()\n\t\t},\n\t})\n}\n\n/**\n * Open a staged/moved uploaded file as a web `ReadableStream`.\n *\n * @param file - The {@link UploadedFileInterface} record to stream\n * @returns A `ReadableStream<Uint8Array>` over the file's current on-disk path\n *\n * @example\n * ```ts\n * new Response(streamUploadedFile(file))\n * ```\n */\nexport function streamUploadedFile(file: UploadedFileInterface): ReadableStream<Uint8Array> {\n\treturn streamFile(file.path)\n}\n\n/**\n * Read a staged/moved uploaded file's full contents into memory.\n *\n * @param file - The {@link UploadedFileInterface} record to read\n * @returns The file's bytes\n *\n * @example\n * ```ts\n * const bytes = await readUploadedFile(file)\n * ```\n */\nexport async function readUploadedFile(file: UploadedFileInterface): Promise<Uint8Array> {\n\treturn readFile(file.path)\n}\n\n/**\n * Move a staged uploaded file to its final `destination`.\n *\n * @remarks\n * Attempts a `rename` first; on a cross-device error (`EXDEV`) falls back to\n * `copyFile` + `unlink`. Returns a new frozen record with `status: 'moved'`\n * and `path: destination` — the input record is never mutated.\n *\n * @param file - The {@link UploadedFileInterface} record to move\n * @param destination - The final on-disk path\n * @returns A new {@link UploadedFileInterface} record reflecting the move\n *\n * @example\n * ```ts\n * const moved = await moveUploadedFile(file, '/var/uploads/final.png')\n * ```\n */\nexport async function moveUploadedFile(\n\tfile: UploadedFileInterface,\n\tdestination: string,\n): Promise<UploadedFileInterface> {\n\ttry {\n\t\tawait rename(file.path, destination)\n\t} catch (error) {\n\t\tif (isRecord(error) && error.code === 'EXDEV') {\n\t\t\tawait copyFile(file.path, destination)\n\t\t\tawait unlink(file.path)\n\t\t} else {\n\t\t\tthrow error\n\t\t}\n\t}\n\treturn createUploadedFile({\n\t\tfield: file.field,\n\t\tname: file.name,\n\t\tsize: file.size,\n\t\tmime: file.mime,\n\t\tvalidated: file.validated,\n\t\tstatus: 'moved',\n\t\tpath: destination,\n\t})\n}\n","import type { MultipartState } from '@src/core'\nimport type {\n\tAssetOptions,\n\tMultipartOptions,\n\tNodeCompressionOptions,\n\tStaticOptions,\n} from './types.js'\nimport type { Stats } from 'node:fs'\nimport type { FileHandle } from 'node:fs/promises'\nimport { DEFAULT_COMPRESSION_THRESHOLD, compressResponse } from '@src/core'\nimport { open, realpath, stat } from 'node:fs/promises'\nimport { join, relative, resolve } from 'node:path'\nimport { brotliDecompressSync } from 'node:zlib'\nimport type { Encoding, MiddlewareHandler } from '@orkestrel/server'\nimport {\n\tcomputeBodyETag,\n\tHTTPError,\n\tmatchesETag,\n\tnegotiateEncoding,\n\tparseRange,\n} from '@orkestrel/server'\nimport {\n\tisArrayBuffer,\n\tisFiniteNumber,\n\tisFunction,\n\tisObject,\n\tisString,\n\tisUint8Array,\n} from '@orkestrel/contract'\nimport { DEFAULT_STATIC_FALLBACK_EXCLUDE, DEFAULT_STATIC_INDEX } from './constants.js'\nimport { isMultipartError } from './errors.js'\nimport {\n\tcomputeFileETag,\n\tcompressNodeBytes,\n\tisContainedPath,\n\tisDotfilePath,\n\tlookupContentType,\n\tparseMultipartRequest,\n\tresolveStaticFallbackPath,\n\tresolveStaticPath,\n\tstreamFile,\n\tunlinkStagedFiles,\n} from './helpers.js'\n\n// ============================================================================\n// @orkestrel/middleware/server — node-face battery factories (AGENTS §5\n// middlewares.ts). createAssets (in-memory asset serving), createStatic\n// (node:fs static file serving), and createMultipart (node:fs/os/crypto\n// streaming multipart uploads) — the three node-bound batteries — plus\n// createCompression, the node-`zlib`\n// guaranteed-availability sibling of the core face's `CompressionStream`-\n// feature-detected battery (separate package entry point, ruling H).\n// ============================================================================\n\n/**\n * Serve validated in-memory assets with identity/Brotli negotiation.\n *\n * @remarks\n * Only `GET` and `HEAD` are served. `/` resolves to `index.html`. Every other\n * key is decoded once, must remain relative, and refuses empty, dot, climbing,\n * backslash, and dotfile segments before the source is read. Successful source\n * values are copied and cached by key. A Brotli value is decompressed once for\n * the identity representation; both representations share an ETag computed\n * from those identity bytes. Responses always vary on `Accept-Encoding`.\n * Range requests are intentionally served as full responses because an asset\n * source exposes complete in-memory representations rather than file handles.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param options - See {@link AssetOptions}\n * @returns A `MiddlewareHandler<TState>`\n * @throws {TypeError} When `options.source` does not implement\n * {@link AssetSourceInterface}, or when it returns malformed asset data\n * @throws {Error} When a source marks bytes as Brotli but they cannot be decompressed\n *\n * @example\n * ```ts\n * import { createAssets } from '@orkestrel/middleware/server'\n *\n * const assets = createAssets({\n * \tsource: {\n * \t\tread: (path) =>\n * \t\t\tpath === 'index.html'\n * \t\t\t\t? { body: new TextEncoder().encode('home') }\n * \t\t\t\t: undefined,\n * \t},\n * })\n * ```\n */\nexport function createAssets<TState>(options: AssetOptions): MiddlewareHandler<TState> {\n\tif (!isObject(options?.source) || !('read' in options.source) || !isFunction(options.source.read))\n\t\tthrow new TypeError('AssetOptions.source must implement AssetSourceInterface')\n\tconst identities = new Map<string, Uint8Array<ArrayBuffer>>()\n\tconst brotlis = new Map<string, Uint8Array<ArrayBuffer>>()\n\tconst tags = new Map<string, Promise<string>>()\n\n\treturn async (request, context, next) => {\n\t\tif (context.method !== 'GET' && context.method !== 'HEAD') return next()\n\t\tlet pathname: string\n\t\ttry {\n\t\t\tpathname = decodeURIComponent(context.url.pathname)\n\t\t} catch {\n\t\t\treturn next()\n\t\t}\n\t\tconst key = pathname === '/' ? DEFAULT_STATIC_INDEX : pathname.slice(1)\n\t\tif (\n\t\t\tkey.length === 0 ||\n\t\t\tkey.includes('\\\\') ||\n\t\t\tkey\n\t\t\t\t.split('/')\n\t\t\t\t.some((segment) => segment.length === 0 || segment === '.' || segment === '..') ||\n\t\t\tisDotfilePath(key)\n\t\t)\n\t\t\treturn next()\n\n\t\tlet identity = identities.get(key)\n\t\tlet brotli = brotlis.get(key)\n\t\tif (identity === undefined) {\n\t\t\tconst asset = options.source.read(key)\n\t\t\tif (asset === undefined) return next()\n\t\t\tif (!isObject(asset)) throw new TypeError('AssetSourceInterface.read must return an Asset')\n\t\t\tconst body = asset.body\n\t\t\tconst encoding = asset.encoding\n\t\t\tif (\n\t\t\t\t(!isArrayBuffer(body) && !isUint8Array(body)) ||\n\t\t\t\t(encoding !== undefined && encoding !== 'br')\n\t\t\t)\n\t\t\t\tthrow new TypeError('AssetSourceInterface.read must return an Asset')\n\t\t\tconst content = isArrayBuffer(body)\n\t\t\t\t? Uint8Array.from(new Uint8Array(body))\n\t\t\t\t: Uint8Array.from(body)\n\t\t\tif (encoding === 'br') {\n\t\t\t\tbrotli = content\n\t\t\t\tidentity = Uint8Array.from(brotliDecompressSync(content))\n\t\t\t\tbrotlis.set(key, brotli)\n\t\t\t} else {\n\t\t\t\tidentity = content\n\t\t\t}\n\t\t\tidentities.set(key, identity)\n\t\t}\n\n\t\tlet pending = tags.get(key)\n\t\tif (pending === undefined) {\n\t\t\tpending = computeBodyETag(identity)\n\t\t\ttags.set(key, pending)\n\t\t}\n\t\tconst etag = await pending\n\t\tlet body = identity\n\t\tlet encoding: 'br' | undefined\n\t\tif (\n\t\t\tbrotli !== undefined &&\n\t\t\tnegotiateEncoding(request.headers.get('accept-encoding') ?? '', ['br']) === 'br'\n\t\t) {\n\t\t\tbody = brotli\n\t\t\tencoding = 'br'\n\t\t}\n\t\tconst headers = new Headers({\n\t\t\t'content-type': lookupContentType(key),\n\t\t\tetag,\n\t\t\tvary: 'accept-encoding',\n\t\t})\n\t\tif (encoding !== undefined) headers.set('content-encoding', encoding)\n\t\tconst cached = request.headers.get('if-none-match')\n\t\tif (cached !== null && matchesETag(cached, etag))\n\t\t\treturn new Response(null, { status: 304, headers })\n\t\theaders.set('content-length', String(body.byteLength))\n\t\treturn new Response(context.method === 'HEAD' ? undefined : body, { headers })\n\t}\n}\n\n/**\n * Serve static files from `options.root` over `node:fs` — the node-bound\n * static-file battery (PROPOSAL §4.14).\n *\n * @remarks\n * Containment is enforced on CANONICAL paths, not merely the lexically\n * resolved one: `options.root` is canonicalized once (memoized) and every\n * request's candidate path is re-canonicalized (`fs.realpath`) before it is\n * served, so a symlink whose target escapes `root` is refused (falls through\n * to `next()`) even though the lexical path resolved inside `root`. A\n * symlink that resolves to a target still INSIDE `root` is unaffected and\n * still serves normally. A dangling symlink (`realpath` throws `ENOENT`) or\n * any other `realpath` failure is treated as a miss — this battery never\n * throws or 500s on a symlink surprise. On a streamed response (a 200 or 206\n * that carries a file body), the open `FileHandle` is owned by the\n * `Response` body and is released only once that body is fully read or\n * cancelled — Node HTTP servers do this automatically when sending the\n * response, but a caller that holds an unread `Response` (e.g. in a test)\n * must cancel its body to release the handle promptly.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param options - See {@link StaticOptions}\n * @returns A `MiddlewareHandler<TState>`\n * @throws {TypeError} When `options.root` is not a non-empty string\n *\n * @example\n * ```ts\n * import { createStatic } from '@orkestrel/middleware/server'\n *\n * const serveFiles = createStatic({ root: '/srv/public', fallback: true })\n * ```\n */\nexport function createStatic<TState>(options: StaticOptions): MiddlewareHandler<TState> {\n\tif (!isString(options.root) || options.root.length === 0)\n\t\tthrow new TypeError('createStatic requires options.root to be a non-empty string')\n\tconst root = resolve(options.root)\n\tconst index = options.index ?? DEFAULT_STATIC_INDEX\n\tconst dotfiles = options.dotfiles ?? 'ignore'\n\tconst useETag = options.etag ?? true\n\tconst fallback =\n\t\toptions.fallback === true\n\t\t\t? { exclude: DEFAULT_STATIC_FALLBACK_EXCLUDE }\n\t\t\t: options.fallback === false || options.fallback === undefined\n\t\t\t\t? undefined\n\t\t\t\t: { exclude: options.fallback.exclude ?? DEFAULT_STATIC_FALLBACK_EXCLUDE }\n\n\tlet canonicalRootPromise: Promise<string> | undefined\n\n\treturn async (request, context, next) => {\n\t\tif (context.method !== 'GET' && context.method !== 'HEAD') return next()\n\n\t\tconst target = resolveStaticPath(root, options.prefix, context.url.pathname)\n\t\tif (target === undefined) return next()\n\n\t\tconst relativePath = relative(root, target)\n\t\tif (relativePath.length > 0 && isDotfilePath(relativePath)) {\n\t\t\tif (dotfiles === 'deny') throw new HTTPError(403, 'forbidden')\n\t\t\tif (dotfiles === 'ignore') return next()\n\t\t}\n\n\t\tlet resolvedPath = target\n\t\tlet fallbackNeeded = false\n\t\ttry {\n\t\t\tif (canonicalRootPromise === undefined) canonicalRootPromise = realpath(root)\n\t\t\tconst [rootReal, targetReal] = await Promise.all([canonicalRootPromise, realpath(target)])\n\t\t\tif (!isContainedPath(targetReal, rootReal)) return next()\n\t\t\tresolvedPath = targetReal\n\t\t} catch {\n\t\t\tfallbackNeeded = true\n\t\t}\n\n\t\t// Directory-detection only — routing decision, not the served file's\n\t\t// facts. The bytes streamed and the headers computed for them come\n\t\t// from a single `fstat` on an opened handle below, closing the\n\t\t// stat-to-stream TOCTOU (a file replaced between \"check\" and \"serve\"\n\t\t// can no longer yield a 200 with stale headers over swapped bytes).\n\t\tlet directoryInfo: Awaited<ReturnType<typeof stat>> | undefined\n\t\tif (!fallbackNeeded) {\n\t\t\ttry {\n\t\t\t\tdirectoryInfo = await stat(resolvedPath)\n\t\t\t} catch {\n\t\t\t\tfallbackNeeded = true\n\t\t\t}\n\t\t}\n\n\t\tif (directoryInfo?.isDirectory()) {\n\t\t\tresolvedPath = join(resolvedPath, index)\n\t\t\ttry {\n\t\t\t\tif (canonicalRootPromise === undefined) canonicalRootPromise = realpath(root)\n\t\t\t\tconst [rootReal, indexReal] = await Promise.all([\n\t\t\t\t\tcanonicalRootPromise,\n\t\t\t\t\trealpath(resolvedPath),\n\t\t\t\t])\n\t\t\t\tif (isContainedPath(indexReal, rootReal)) resolvedPath = indexReal\n\t\t\t\telse fallbackNeeded = true\n\t\t\t} catch {\n\t\t\t\tfallbackNeeded = true\n\t\t\t}\n\t\t}\n\n\t\tlet handle: FileHandle | undefined\n\t\tlet info: Stats | undefined\n\t\tif (!fallbackNeeded) {\n\t\t\ttry {\n\t\t\t\thandle = await open(resolvedPath, 'r')\n\t\t\t} catch {\n\t\t\t\tfallbackNeeded = true\n\t\t\t}\n\t\t}\n\t\tif (handle !== undefined) {\n\t\t\ttry {\n\t\t\t\tinfo = await handle.stat()\n\t\t\t} catch {\n\t\t\t\tawait handle.close()\n\t\t\t\thandle = undefined\n\t\t\t\tfallbackNeeded = true\n\t\t\t}\n\t\t}\n\t\tif (handle !== undefined && info !== undefined && !info.isFile()) {\n\t\t\tawait handle.close()\n\t\t\thandle = undefined\n\t\t\tinfo = undefined\n\t\t\tfallbackNeeded = true\n\t\t}\n\n\t\tif (fallbackNeeded) {\n\t\t\tconst shellPath = resolveStaticFallbackPath(\n\t\t\t\troot,\n\t\t\t\tindex,\n\t\t\t\tfallback?.exclude ?? DEFAULT_STATIC_FALLBACK_EXCLUDE,\n\t\t\t\tcontext.method,\n\t\t\t\tcontext.url.pathname,\n\t\t\t\trequest.headers.get('accept') ?? '',\n\t\t\t)\n\t\t\tif (fallback === undefined || shellPath === undefined) return next()\n\t\t\tlet shellReal: string\n\t\t\ttry {\n\t\t\t\tif (canonicalRootPromise === undefined) canonicalRootPromise = realpath(root)\n\t\t\t\tconst [rootReal, candidate] = await Promise.all([canonicalRootPromise, realpath(shellPath)])\n\t\t\t\tif (!isContainedPath(candidate, rootReal)) return next()\n\t\t\t\tshellReal = candidate\n\t\t\t} catch {\n\t\t\t\treturn next()\n\t\t\t}\n\t\t\tlet shellHandle: FileHandle\n\t\t\ttry {\n\t\t\t\tshellHandle = await open(shellReal, 'r')\n\t\t\t} catch {\n\t\t\t\treturn next()\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst body = streamFile(shellHandle)\n\t\t\t\treturn new Response(body, {\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: new Headers({ 'content-type': lookupContentType(shellReal) }),\n\t\t\t\t})\n\t\t\t} catch (error) {\n\t\t\t\tawait shellHandle.close().catch(() => {})\n\t\t\t\tthrow error\n\t\t\t}\n\t\t}\n\n\t\tif (handle === undefined || info === undefined) return next()\n\n\t\tlet streaming = false\n\t\ttry {\n\t\t\tconst headers = new Headers({\n\t\t\t\t'content-type': lookupContentType(resolvedPath),\n\t\t\t\t'accept-ranges': 'bytes',\n\t\t\t})\n\t\t\tif (options.cache !== undefined) headers.set('cache-control', `max-age=${options.cache}`)\n\n\t\t\tif (useETag) {\n\t\t\t\tconst etag = computeFileETag(info.size, info.mtimeMs)\n\t\t\t\theaders.set('etag', etag)\n\t\t\t\tconst ifNoneMatch = request.headers.get('if-none-match')\n\t\t\t\tif (ifNoneMatch !== null && matchesETag(ifNoneMatch, etag)) {\n\t\t\t\t\tawait handle.close()\n\t\t\t\t\treturn new Response(null, { status: 304, headers })\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (context.method === 'HEAD') {\n\t\t\t\tawait handle.close()\n\t\t\t\theaders.set('content-length', String(info.size))\n\t\t\t\treturn new Response(null, { status: 200, headers })\n\t\t\t}\n\n\t\t\tconst rangeHeader = request.headers.get('range')\n\t\t\tconst range = parseRange(rangeHeader === null ? undefined : rangeHeader, info.size)\n\t\t\tif (range === undefined) {\n\t\t\t\theaders.set('content-length', String(info.size))\n\t\t\t\tconst body = streamFile(handle)\n\t\t\t\tstreaming = true\n\t\t\t\treturn new Response(body, { status: 200, headers })\n\t\t\t}\n\t\t\tif (!range.satisfiable) {\n\t\t\t\tawait handle.close()\n\t\t\t\theaders.set('content-range', `bytes */${info.size}`)\n\t\t\t\treturn new Response(null, { status: 416, headers })\n\t\t\t}\n\t\t\theaders.set('content-range', `bytes ${range.start}-${range.end}/${info.size}`)\n\t\t\theaders.set('content-length', String(range.end - range.start + 1))\n\t\t\tconst body = streamFile(handle, { start: range.start, end: range.end })\n\t\t\tstreaming = true\n\t\t\treturn new Response(body, { status: 206, headers })\n\t\t} catch (error) {\n\t\t\tif (!streaming) await handle.close().catch(() => {})\n\t\t\tthrow error\n\t\t}\n\t}\n}\n\n/**\n * Parse a streamed `multipart/form-data` request body and stash its\n * {@link MultipartBody} on `context.state.multipart` — the node-bound\n * streaming multipart battery (PROPOSAL §4.15, ruling C).\n *\n * @remarks\n * A non-multipart request passes through untouched. Consumes `request.body`\n * as a stream — `context.body()` must not be called for a request this\n * battery has processed (the underlying stream is exhausted). Every\n * {@link MultipartError} this battery's parser throws is re-thrown as an\n * {@link HTTPError} carrying the same status/message, so `createBoundary`\n * (or any HTTPError-aware renderer) maps it correctly without depending on\n * this node face's error type. Fail-closed on the DOWNSTREAM handler too: if\n * `next()` throws, every still-`'staged'` uploaded file is unlinked\n * (best-effort) before the error is re-thrown, so an unhandled downstream\n * failure never leaks temp files. A normal return leaves staged files\n * untouched — the downstream handler owns moving/reading them.\n *\n * @typeParam TState - The consumer's state type, extending {@link MultipartState}\n * @param options - See {@link MultipartOptions}\n * @returns A `MiddlewareHandler<TState>`\n * @throws {HTTPError} When the underlying parse throws a {@link MultipartError}\n * (limit breach → 413, malformed structure → 400, rejected file type → 415)\n *\n * @example\n * ```ts\n * import { createMultipart } from '@orkestrel/middleware/server'\n *\n * const uploads = createMultipart({ allowed: ['image/png', 'image/jpeg'] })\n * ```\n */\nexport function createMultipart<TState extends MultipartState>(\n\toptions: MultipartOptions = {},\n): MiddlewareHandler<TState> {\n\treturn async (request, context, next) => {\n\t\tlet body: Awaited<ReturnType<typeof parseMultipartRequest>>\n\t\ttry {\n\t\t\tbody = await parseMultipartRequest(request, options)\n\t\t} catch (error) {\n\t\t\tif (isMultipartError(error)) throw new HTTPError(error.status, error.message, error.context)\n\t\t\tthrow error\n\t\t}\n\t\tif (body === undefined) return next()\n\t\tObject.assign(context.state, { multipart: body })\n\t\ttry {\n\t\t\treturn await next()\n\t\t} catch (error) {\n\t\t\tawait unlinkStagedFiles(body)\n\t\t\tthrow error\n\t\t}\n\t}\n}\n\n/**\n * Compress response bodies via `node:zlib` — the node-bound sibling of the\n * core face's `CompressionStream`-feature-detected `createCompression`,\n * guaranteed available on any Node runtime rather than dependent on the\n * WHATWG `CompressionStream` global (PROPOSAL §4.3, ruling J). Ships as a\n * SEPARATE package entry point (`@orkestrel/middleware/server`) from the core\n * face's `createCompression`, so the shared name is unambiguous per\n * consumer import path (ruling H).\n *\n * @remarks\n * Peer-type limitation (same one U1 recorded on the core face): the shipped\n * `@orkestrel/server` `Encoding` union is `'gzip' | 'deflate' | 'identity'`\n * — it does not include `'br'`, so this battery cannot honestly type or\n * negotiate a guaranteed brotli coding despite `node:zlib` shipping\n * `brotliCompress`. It guarantees `gzip`/`deflate` via `node:zlib` (never\n * feature-detected — always available) and negotiates only those.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param options - See {@link NodeCompressionOptions}\n * @returns A `MiddlewareHandler<TState>`\n * @throws {TypeError} When `options.threshold` is provided and is not a\n * finite number, or `options.filter` is provided and is not a function\n *\n * @example\n * ```ts\n * import { createCompression } from '@orkestrel/middleware/server'\n *\n * const compress = createCompression({ threshold: 512 })\n * ```\n */\nexport function createCompression<TState>(\n\toptions?: NodeCompressionOptions,\n): MiddlewareHandler<TState> {\n\tif (options?.threshold !== undefined && !isFiniteNumber(options.threshold))\n\t\tthrow new TypeError('NodeCompressionOptions.threshold must be a finite number when provided')\n\tif (options?.filter !== undefined && !isFunction(options.filter))\n\t\tthrow new TypeError('NodeCompressionOptions.filter must be a function when provided')\n\tconst threshold = options?.threshold ?? DEFAULT_COMPRESSION_THRESHOLD\n\tconst filter = options?.filter\n\tconst encodings: readonly Encoding[] = ['gzip', 'deflate']\n\treturn async (request, context, next) => {\n\t\tconst response = await next()\n\t\treturn compressResponse(request, context, response, {\n\t\t\tthreshold,\n\t\t\t...(filter !== undefined ? { filter } : {}),\n\t\t\tencodings,\n\t\t\tcompress: compressNodeBytes,\n\t\t})\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["#defaultDirectory","#reader","#signal","#abort","#boundary","#limits","#allowed","#directory","#staged","#files","#fields","#wakeReader","#createDirectory","#buffer","#pull","#consumeFile","#consumeField","#cleanup","#ended","#fileCount","#scan","#discard","#fieldCount","#totalBytes"],"sources":["../../../src/server/constants.ts","../../../src/server/errors.ts","../../../src/server/helpers.ts","../../../src/server/MultipartParser.ts","../../../src/server/parsers.ts","../../../src/server/middlewares.ts"],"sourcesContent":["import type { MultipartErrorCode, StaticOptions } from './types.js'\nimport type { Encoding } from '@orkestrel/server'\n\n/** Holds the HTTP status `createMultipart` renders for each {@link MultipartErrorCode}. */\nexport const MULTIPART_STATUS: Readonly<Record<MultipartErrorCode, number>> = Object.freeze({\n\tlimit: 413,\n\tmalformed: 400,\n\trejected: 415,\n})\n\n/**\n * Holds the `Symbol.for` brand {@link MultipartError} carries so\n * {@link isMultipartError} recognizes an instance across duplicate copies of\n * this package — a registry symbol rather than a module-local `Symbol()`,\n * which would mint an unequal symbol per copy.\n */\nexport const MULTIPART_ERROR_BRAND: unique symbol = Symbol.for(\n\t'@orkestrel/middleware.MultipartError',\n)\n\n/** Names `createStatic`'s default directory-index filename. */\nexport const DEFAULT_STATIC_INDEX = 'index.html'\n\n/** Names `createStatic`'s `fallback: true` default excluded path prefix. */\nexport const DEFAULT_STATIC_FALLBACK_EXCLUDE = '/api'\n\n/** Names `createStatic`'s default policy for a path carrying a dotfile segment. */\nexport const DEFAULT_STATIC_DOTFILES: NonNullable<StaticOptions['dotfiles']> = 'ignore'\n\n/**\n * Lists the content-codings the node face's `createCompression` offers — the two\n * `node:zlib` guarantees on every Node runtime, so this face never\n * feature-detects.\n */\nexport const NODE_COMPRESSION_ENCODINGS: readonly Encoding[] = Object.freeze(['gzip', 'deflate'])\n\n/** Names the MIME type served when a file extension has no known mapping. */\nexport const DEFAULT_CONTENT_TYPE = 'application/octet-stream'\n\n/** Holds `createMultipart`'s default per-file byte-size cap. */\nexport const DEFAULT_MULTIPART_FILE_SIZE = 10_485_760\n\n/** Holds `createMultipart`'s default maximum file-part count. */\nexport const DEFAULT_MULTIPART_FILE_COUNT = 10\n\n/** Holds `createMultipart`'s default per-field byte-size cap. */\nexport const DEFAULT_MULTIPART_FIELD_SIZE = 65_536\n\n/** Holds `createMultipart`'s default maximum field-part count. */\nexport const DEFAULT_MULTIPART_FIELD_COUNT = 100\n\n/** Holds `createMultipart`'s default combined request-body byte-size cap. */\nexport const DEFAULT_MULTIPART_TOTAL = 52_428_800\n\n/** Holds the maximum bytes a single multipart part's header block may occupy before it is malformed. */\nexport const MULTIPART_MAX_HEADER_BLOCK = 16_384\n\n/** Holds the maximum bytes scanned before the first multipart boundary is found before it is malformed. */\nexport const MULTIPART_MAX_PREAMBLE = 65_536\n\n/**\n * Lists the Windows reserved device-name stems (CVE-2025-27210) — matched\n * case-insensitively against the segment's stem (before its first `.`).\n */\nexport const RESERVED_DEVICE_NAMES: ReadonlySet<string> = Object.freeze(\n\tnew Set([\n\t\t'CON',\n\t\t'PRN',\n\t\t'AUX',\n\t\t'NUL',\n\t\t'COM1',\n\t\t'COM2',\n\t\t'COM3',\n\t\t'COM4',\n\t\t'COM5',\n\t\t'COM6',\n\t\t'COM7',\n\t\t'COM8',\n\t\t'COM9',\n\t\t'LPT1',\n\t\t'LPT2',\n\t\t'LPT3',\n\t\t'LPT4',\n\t\t'LPT5',\n\t\t'LPT6',\n\t\t'LPT7',\n\t\t'LPT8',\n\t\t'LPT9',\n\t]),\n)\n\n/** Holds the file-extension (lowercase, with leading `.`) → MIME type lookup table for static serving. */\nexport const EXTENSION_TYPES: Readonly<Record<string, string>> = Object.freeze({\n\t'.html': 'text/html; charset=utf-8',\n\t'.htm': 'text/html; charset=utf-8',\n\t'.css': 'text/css; charset=utf-8',\n\t'.js': 'text/javascript; charset=utf-8',\n\t'.mjs': 'text/javascript; charset=utf-8',\n\t'.json': 'application/json; charset=utf-8',\n\t'.txt': 'text/plain; charset=utf-8',\n\t'.xml': 'application/xml; charset=utf-8',\n\t'.svg': 'image/svg+xml',\n\t'.png': 'image/png',\n\t'.jpg': 'image/jpeg',\n\t'.jpeg': 'image/jpeg',\n\t'.gif': 'image/gif',\n\t'.webp': 'image/webp',\n\t'.ico': 'image/x-icon',\n\t'.pdf': 'application/pdf',\n\t'.zip': 'application/zip',\n\t'.woff': 'font/woff',\n\t'.woff2': 'font/woff2',\n\t'.wasm': 'application/wasm',\n})\n","import type { MultipartErrorCode } from './types.js'\nimport { HTTPError } from '@orkestrel/server'\nimport { MULTIPART_ERROR_BRAND, MULTIPART_STATUS } from './constants.js'\n\n/**\n * Represents an error `createMultipart` throws when a streamed multipart request fails\n * a mid-stream limit, is structurally malformed, or has a file whose sniffed\n * bytes are rejected by the configured `allowed` MIME list.\n *\n * @remarks\n * Extends the peer `HTTPError`, which already publishes the `status`,\n * `context`, and brand members every fleet error of this shape carries, and\n * adds the machine-readable `code` axis a caller narrows on. `status` is\n * derived from `code` through {@link MULTIPART_STATUS} (limit → 413, malformed →\n * 400, rejected → 415), so `createBoundary` — or any other `isHTTPError`-aware\n * renderer — maps it without knowing this face's error type. Narrow a caught\n * value to the richer type with {@link isMultipartError}.\n *\n * @example\n * ```ts\n * import { MultipartError } from '@orkestrel/middleware/server'\n *\n * throw new MultipartError('limit', 'too many files')\n * ```\n */\nexport class MultipartError extends HTTPError {\n\treadonly code: MultipartErrorCode\n\treadonly [MULTIPART_ERROR_BRAND] = true\n\n\tconstructor(\n\t\tcode: MultipartErrorCode,\n\t\tmessage: string,\n\t\tcontext?: Readonly<Record<string, unknown>>,\n\t) {\n\t\tsuper(MULTIPART_STATUS[code], message, context)\n\t\tthis.name = 'MultipartError'\n\t\tthis.code = code\n\t}\n}\n\n/**\n * Narrows an unknown caught value to a {@link MultipartError}.\n *\n * @remarks\n * Structural, not `instanceof` — tests that `value` is a non-null object\n * carrying {@link MULTIPART_ERROR_BRAND}, a numeric `status`, and a `code`\n * in the parser's {@link MultipartErrorCode} set (`'limit' | 'malformed' | 'rejected'`).\n * Total: never throws, returns `false` for any off-shape input.\n *\n * @param value - The value to test (typically a `catch` binding)\n * @returns True if `value` is a {@link MultipartError}; false otherwise\n *\n * @example\n * ```ts\n * import { isMultipartError } from '@orkestrel/middleware/server'\n *\n * try {\n * \tawait parse(request)\n * } catch (error) {\n * \tif (isMultipartError(error)) console.log(error.status, error.code)\n * }\n * ```\n */\nexport function isMultipartError(value: unknown): value is MultipartError {\n\tif (typeof value !== 'object' || value === null) return false\n\tif (!(MULTIPART_ERROR_BRAND in value)) return false\n\tif (!('status' in value) || !('code' in value)) return false\n\tif (typeof value.status !== 'number') return false\n\tif (value.code !== 'limit' && value.code !== 'malformed' && value.code !== 'rejected')\n\t\treturn false\n\treturn true\n}\n","import type { MultipartBody } from '@src/core'\nimport type {\n\tByteRange,\n\tMultipartLimits,\n\tMultipartLimitsInput,\n\tPartHeaders,\n\tUploadedFile,\n} from './types.js'\nimport type { FileHandle } from 'node:fs/promises'\nimport type { Encoding } from '@orkestrel/server'\nimport { createReadStream } from 'node:fs'\nimport { copyFile, readFile, realpath, rename, unlink } from 'node:fs/promises'\nimport { extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path'\nimport { promisify } from 'node:util'\nimport { deflate as zlibDeflate, gzip as zlibGzip } from 'node:zlib'\nimport { isError } from '@orkestrel/contract'\nimport {\n\tDEFAULT_CONTENT_TYPE,\n\tDEFAULT_MULTIPART_FIELD_SIZE,\n\tDEFAULT_MULTIPART_FIELD_COUNT,\n\tDEFAULT_MULTIPART_FILE_SIZE,\n\tDEFAULT_MULTIPART_FILE_COUNT,\n\tDEFAULT_MULTIPART_TOTAL,\n\tEXTENSION_TYPES,\n\tRESERVED_DEVICE_NAMES,\n} from './constants.js'\n\n/**\n * Checks whether `pathname` is `prefix` itself or lies under it on a SEGMENT\n * boundary — the shared under-path test `resolveStaticPath`'s prefix strip\n * and `createStatic`'s SPA-fallback `exclude` both apply, so `exclude:\n * '/api'` matches `/api` and `/api/x` but never `/apifoo`.\n *\n * @param pathname - The request pathname to test\n * @param prefix - The path prefix to test against\n * @returns True if `pathname` equals `prefix` or starts with `prefix` + `/`; false otherwise\n *\n * @example\n * ```ts\n * isUnderPath('/api/x', '/api') // true\n * isUnderPath('/apifoo', '/api') // false\n * ```\n */\nexport function isUnderPath(pathname: string, prefix: string): boolean {\n\tif (pathname === prefix) return true\n\tconst boundary = prefix.endsWith('/') ? prefix : `${prefix}/`\n\treturn pathname.startsWith(boundary)\n}\n\n/**\n * Resolves the fixed SPA shell path when a static-file miss is eligible for\n * fallback.\n *\n * @remarks\n * `GET` and `HEAD` are both eligible, and resolve the SAME shell: `HEAD` is\n * defined as `GET` without a body (RFC 9110 §9.3.2), so a navigation probe\n * that answered `404` while its `GET` answered `200` would report a resource\n * the very next request serves.\n *\n * @param root - The configured static root\n * @param index - The configured shell filename\n * @param exclude - The URL prefix excluded from fallback\n * @param method - The request method; only `GET` and `HEAD` are eligible\n * @param pathname - The request pathname\n * @param accept - The request's `Accept` header value\n * @returns The fixed shell path, or `undefined` when fallback is ineligible\n *\n * @example\n * ```ts\n * resolveStaticFallbackPath('/srv/public', 'index.html', '/api', 'GET', '/dashboard', 'text/html')\n * // '/srv/public/index.html'\n * ```\n */\nexport function resolveStaticFallbackPath(\n\troot: string,\n\tindex: string,\n\texclude: string,\n\tmethod: string,\n\tpathname: string,\n\taccept: string,\n): string | undefined {\n\tif (method !== 'GET' && method !== 'HEAD') return undefined\n\tif (extname(pathname) !== '') return undefined\n\tif (!accept.includes('text/html') && !accept.includes('*/*')) return undefined\n\tif (isUnderPath(pathname, exclude)) return undefined\n\treturn join(root, index)\n}\n\n/**\n * Checks whether `child` is `parent` itself or lies inside it on-disk — the\n * FILESYSTEM containment predicate `createStatic` applies to `fs.realpath`\n * output (never to a URL pathname — that is {@link isUnderPath}'s job).\n *\n * @remarks\n * Argument order is `(child, parent)` — deliberately the OPPOSITE conceptual\n * order from {@link isUnderPath}`(pathname, prefix)`, so a call site cannot\n * casually swap one predicate in for the other. Built on `path.relative`,\n * this is separator-correct on both POSIX (`/`) and win32 (`\\`) — unlike a\n * hardcoded `${parent}/` boundary check, which silently fails to match every\n * realpath on Windows — and case-folds on win32 because `path.relative` does.\n *\n * @param child - The absolute on-disk path to test\n * @param parent - The absolute on-disk directory it must lie under\n * @returns True if `child` equals `parent` or resolves inside it; false otherwise\n *\n * @example\n * ```ts\n * isContainedPath('/srv/public/a.html', '/srv/public') // true\n * isContainedPath('/srv/other/a.html', '/srv/public') // false\n * ```\n */\nexport function isContainedPath(child: string, parent: string): boolean {\n\tif (child === parent) return true\n\tconst rel = relative(parent, child)\n\treturn rel.length > 0 && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)\n}\n\n/**\n * Resolves a request pathname to an on-disk path UNDER `root`, or `undefined`\n * when it cannot — the traversal guard, whose algorithm and order are exact:\n * strip `prefix` on a segment boundary → `decodeURIComponent` (a\n * malformed escape refuses, never throws) → reject a NUL byte → strip the\n * leading path separator FIRST (so a leading `..` survives `normalize` as a\n * genuine climbing segment) → `normalize` → refuse any Windows reserved-\n * device-name segment ({@link isReservedDeviceName}) → `resolve` and require\n * the result under `root`.\n *\n * @param root - The absolute root directory every result must resolve under\n * @param prefix - An optional URL path prefix stripped on a segment boundary\n * @param pathname - The raw request pathname\n * @returns The resolved absolute path under `root`, or `undefined` when the\n * request does not resolve (out of prefix, malformed escape, NUL byte,\n * reserved device name, or an attempted escape from `root`)\n *\n * @example\n * ```ts\n * resolveStaticPath('/srv/public', '/api', '/api/../../etc/passwd') // undefined\n * ```\n */\nexport function resolveStaticPath(\n\troot: string,\n\tprefix: string | undefined,\n\tpathname: string,\n): string | undefined {\n\tlet remainder = pathname\n\tif (prefix !== undefined) {\n\t\tif (!isUnderPath(pathname, prefix)) return undefined\n\t\tremainder = pathname === prefix ? '/' : pathname.slice(prefix.length)\n\t}\n\tlet decoded: string\n\ttry {\n\t\tdecoded = decodeURIComponent(remainder)\n\t} catch {\n\t\treturn undefined\n\t}\n\tif (decoded.includes('\\0')) return undefined\n\tconst stripped = decoded.replace(/^[/\\\\]+/, '')\n\tconst normalized = normalize(stripped)\n\tconst segments = normalized.split(/[/\\\\]+/).filter((segment) => segment.length > 0)\n\tfor (const segment of segments) if (isReservedDeviceName(segment)) return undefined\n\tconst resolved = resolve(root, normalized)\n\treturn isContainedPath(resolved, root) ? resolved : undefined\n}\n\n/**\n * Canonicalizes `candidate` and returns it only when it lies inside `rootReal`\n * — the shared realpath-then-contain step `createStatic` applies to a\n * directory index and to its SPA shell.\n *\n * @remarks\n * Total: a `realpath` failure (a dangling symlink, a missing file, a\n * permission refusal) and an escape from `rootReal` both resolve `undefined`,\n * so a caller that treats those two outcomes identically needs no `try`. A\n * caller that must tell them apart keeps its own explicit branch instead.\n *\n * @param candidate - The on-disk path to canonicalize\n * @param rootReal - The already-canonical root the result must lie under\n * @returns The canonical path inside `rootReal`, or `undefined`\n *\n * @example\n * ```ts\n * await resolveContainedRealPath('/srv/public/index.html', '/srv/public')\n * // '/srv/public/index.html'\n * ```\n */\nexport async function resolveContainedRealPath(\n\tcandidate: string,\n\trootReal: string,\n): Promise<string | undefined> {\n\tlet real: string\n\ttry {\n\t\treal = await realpath(candidate)\n\t} catch {\n\t\treturn undefined\n\t}\n\treturn isContainedPath(real, rootReal) ? real : undefined\n}\n\n/**\n * Checks whether a path segment is a Windows reserved device name (CVE-2025-27210).\n *\n * @remarks\n * Normalizes superscript digits (`¹²³` → `123`) first, strips trailing dots\n * and spaces (Windows drops them), takes the STEM before the first `.`,\n * upper-cases it, and tests it against {@link RESERVED_DEVICE_NAMES}\n * (`CON PRN AUX NUL COM1-9 LPT1-9`) — an exact-stem match only, so\n * `console.js` and `nullable.css` are never flagged.\n *\n * @param segment - One path segment (no separators)\n * @returns True if `segment` names a reserved device; false otherwise\n *\n * @example\n * ```ts\n * isReservedDeviceName('NUL.json') // true\n * isReservedDeviceName('nullable.css') // false\n * isReservedDeviceName('CON¹') // true\n * ```\n */\nexport function isReservedDeviceName(segment: string): boolean {\n\tconst superscripted = segment.replace(/[¹²³]/g, (digit) =>\n\t\tdigit === '¹' ? '1' : digit === '²' ? '2' : '3',\n\t)\n\tconst trimmed = superscripted.replace(/[. ]+$/, '')\n\tconst stem = trimmed.split('.')[0]\n\tif (stem === undefined || stem.length === 0) return false\n\treturn RESERVED_DEVICE_NAMES.has(stem.toUpperCase())\n}\n\n/**\n * Checks whether a relative path (already resolved under a static root) has any\n * segment starting with `.` — a dotfile or dot-directory.\n *\n * @param relativePath - A path relative to the static root\n * @returns True if any segment starts with `.`; false otherwise\n *\n * @example\n * ```ts\n * isDotfilePath('.env') // true\n * isDotfilePath('a/.git/config') // true\n * ```\n */\nexport function isDotfilePath(relativePath: string): boolean {\n\treturn relativePath.split(/[/\\\\]+/).some((segment) => segment.startsWith('.'))\n}\n\n/**\n * Looks up the MIME type for a static file path by its extension.\n *\n * @param pathname - The file's path (only its extension is read)\n * @returns The mapped MIME type, or {@link DEFAULT_CONTENT_TYPE} when unknown\n *\n * @example\n * ```ts\n * lookupContentType('/a/b.css') // 'text/css; charset=utf-8'\n * ```\n */\nexport function lookupContentType(pathname: string): string {\n\tconst extension = extname(pathname).toLowerCase()\n\tconst mapped = EXTENSION_TYPES[extension]\n\treturn mapped ?? DEFAULT_CONTENT_TYPE\n}\n\n/**\n * Computes a static file's weak ETag from its size and modification time.\n *\n * @param size - The file's byte size\n * @param mtimeMs - The file's modification time in milliseconds\n * @returns A weak entity-tag `W/\"<size>-<floor(mtimeMs)>\"`\n *\n * @example\n * ```ts\n * computeFileETag(1024, 1700000000123.4) // 'W/\"1024-1700000000123\"'\n * ```\n */\nexport function computeFileETag(size: number, mtimeMs: number): string {\n\treturn `W/\"${size}-${Math.floor(mtimeMs)}\"`\n}\n\n/**\n * Compresses response bytes with Node's guaranteed zlib gzip/deflate codecs.\n *\n * @param bytes - The uncompressed response bytes\n * @param encoding - The negotiated actionable coding\n * @returns The compressed bytes\n *\n * @example\n * ```ts\n * const bytes = new TextEncoder().encode('compress me')\n * const compressed = await compressNodeBytes(bytes, 'deflate')\n * ```\n */\nexport async function compressNodeBytes(\n\tbytes: Uint8Array<ArrayBuffer>,\n\tencoding: Exclude<Encoding, 'identity'>,\n): Promise<Uint8Array<ArrayBuffer>> {\n\tconst compressed =\n\t\tencoding === 'gzip' ? await promisify(zlibGzip)(bytes) : await promisify(zlibDeflate)(bytes)\n\treturn Uint8Array.from(compressed)\n}\n\n/**\n * Sniffs a MIME type from a file's leading bytes against a small magic-byte\n * table (jpeg, png, gif87a/89a, webp, pdf, zip).\n *\n * @remarks\n * `createMultipart`'s `allowed` check reads this signal alone and never the\n * declared `Content-Type`, so a file whose bytes match no signature can never\n * be placed on an `allowed` list. The record's `mime` field is a different\n * question: it falls back to the declared `Content-Type` (then to\n * {@link DEFAULT_CONTENT_TYPE}) when nothing sniffs, and its `validated` flag\n * reports whether the sniffed and declared types agreed.\n *\n * @param head - The file's first bytes (16 is sufficient for every signature)\n * @returns The detected MIME type, or `undefined` when no signature matches\n *\n * @example\n * ```ts\n * detectMIME(Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) // 'image/png'\n * ```\n */\nexport function detectMIME(head: Uint8Array): string | undefined {\n\tif (matchesBytes(head, [0xff, 0xd8, 0xff])) return 'image/jpeg'\n\tif (matchesBytes(head, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return 'image/png'\n\tif (matchesBytes(head, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61])) return 'image/gif'\n\tif (matchesBytes(head, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) return 'image/gif'\n\tif (\n\t\tmatchesBytes(head, [0x52, 0x49, 0x46, 0x46]) &&\n\t\tmatchesBytes(head, [0x57, 0x45, 0x42, 0x50], 8)\n\t)\n\t\treturn 'image/webp'\n\tif (matchesBytes(head, [0x25, 0x50, 0x44, 0x46, 0x2d])) return 'application/pdf'\n\tif (\n\t\tmatchesBytes(head, [0x50, 0x4b, 0x03, 0x04]) ||\n\t\tmatchesBytes(head, [0x50, 0x4b, 0x05, 0x06]) ||\n\t\tmatchesBytes(head, [0x50, 0x4b, 0x07, 0x08])\n\t)\n\t\treturn 'application/zip'\n\treturn undefined\n}\n\n/**\n * Checks whether `bytes` contains `signature` at the requested offset.\n *\n * @param bytes - The bytes to inspect\n * @param signature - The exact byte sequence to match\n * @param offset - The starting byte offset, defaulting to zero\n * @returns True if the complete signature matches; false otherwise\n *\n * @example\n * ```ts\n * matchesBytes(Uint8Array.from([0, 0x50, 0x4b]), [0x50, 0x4b], 1) // true\n * ```\n */\nexport function matchesBytes(bytes: Uint8Array, signature: readonly number[], offset = 0): boolean {\n\tif (bytes.length < offset + signature.length) return false\n\tfor (let index = 0; index < signature.length; index += 1)\n\t\tif (bytes[offset + index] !== signature[index]) return false\n\treturn true\n}\n\n/**\n * Extracts the `boundary` parameter from a `Content-Type` header, or\n * `undefined` when the request is not `multipart/form-data`.\n *\n * @param contentType - The request's `Content-Type` header value, if present\n * @returns The multipart boundary token, or `undefined` for a non-multipart\n * (or malformed/boundary-less) content type\n *\n * @example\n * ```ts\n * extractMultipartBoundary('multipart/form-data; boundary=abc123') // 'abc123'\n * extractMultipartBoundary('application/json') // undefined\n * ```\n */\nexport function extractMultipartBoundary(contentType: string | null): string | undefined {\n\tif (contentType === null) return undefined\n\tconst [type, ...params] = contentType.split(';').map((part) => part.trim())\n\tif (type === undefined || type.toLowerCase() !== 'multipart/form-data') return undefined\n\tfor (const param of params) {\n\t\tconst equals = param.indexOf('=')\n\t\tif (equals === -1) continue\n\t\tconst key = param.slice(0, equals).trim().toLowerCase()\n\t\tif (key !== 'boundary') continue\n\t\tlet value = param.slice(equals + 1).trim()\n\t\tif (value.startsWith('\"') && value.endsWith('\"') && value.length >= 2)\n\t\t\tvalue = value.slice(1, -1)\n\t\treturn value.length > 0 ? value : undefined\n\t}\n\treturn undefined\n}\n\n/**\n * Resolves `createMultipart`'s effective {@link MultipartLimits}, applying\n * every documented default to an omitted leaf.\n *\n * @param limits - The caller's partial limits\n * @returns The fully-resolved limits\n *\n * @example\n * ```ts\n * resolveMultipartLimits({ file: { size: 1_048_576 } })\n * ```\n */\nexport function resolveMultipartLimits(limits: MultipartLimitsInput | undefined): MultipartLimits {\n\treturn {\n\t\tfile: {\n\t\t\tsize: limits?.file?.size ?? DEFAULT_MULTIPART_FILE_SIZE,\n\t\t\tcount: limits?.file?.count ?? DEFAULT_MULTIPART_FILE_COUNT,\n\t\t},\n\t\tfield: {\n\t\t\tsize: limits?.field?.size ?? DEFAULT_MULTIPART_FIELD_SIZE,\n\t\t\tcount: limits?.field?.count ?? DEFAULT_MULTIPART_FIELD_COUNT,\n\t\t},\n\t\ttotal: limits?.total ?? DEFAULT_MULTIPART_TOTAL,\n\t}\n}\n\n/**\n * Parses one multipart part's raw header block into its `name` (from\n * `Content-Disposition`), optional `filename`, and optional `Content-Type`.\n *\n * @param block - The raw header block for one multipart part (before the\n * terminating blank line)\n * @returns The parsed `name`, `filename`, and `mime` (each `undefined` when\n * absent)\n *\n * @example\n * ```ts\n * parsePartHeaders('Content-Disposition: form-data; name=\"title\"')\n * // { name: 'title', filename: undefined, mime: undefined }\n * ```\n */\nexport function parsePartHeaders(block: string): PartHeaders {\n\tlet name: string | undefined\n\tlet filename: string | undefined\n\tlet mime: string | undefined\n\tfor (const line of block.split('\\r\\n')) {\n\t\tconst colon = line.indexOf(':')\n\t\tif (colon === -1) continue\n\t\tconst key = line.slice(0, colon).trim().toLowerCase()\n\t\tconst value = line.slice(colon + 1).trim()\n\t\tif (key === 'content-disposition') {\n\t\t\tconst nameMatch = /;\\s*name=\"([^\"]*)\"/.exec(value)\n\t\t\tconst filenameMatch = /;\\s*filename=\"([^\"]*)\"/.exec(value)\n\t\t\tif (nameMatch !== null) name = nameMatch[1]\n\t\t\tif (filenameMatch !== null) filename = filenameMatch[1]\n\t\t} else if (key === 'content-type') {\n\t\t\tmime = value\n\t\t}\n\t}\n\treturn { name, filename, mime }\n}\n\n/**\n * Builds a frozen {@link UploadedFile} record.\n *\n * @param input - Every field of the record\n * @returns A frozen {@link UploadedFile}\n *\n * @example\n * ```ts\n * createUploadedFile({ field: 'avatar', name: 'a.png', size: 1024, mime: 'image/png', validated: true, status: 'staged', path: '/tmp/x' })\n * ```\n */\nexport function createUploadedFile(input: UploadedFile): UploadedFile {\n\treturn Object.freeze({ ...input })\n}\n\n/**\n * Attempts to unlink every still-`'staged'` file in a parsed\n * {@link MultipartBody} — the fail-closed cleanup `createMultipart` runs when\n * its downstream handler throws, mirroring `parseMultipartRequest`'s own\n * cleanup pattern (a missing file is already gone; failures are swallowed).\n *\n * @param body - The parsed multipart body to clean up\n * @returns A promise that resolves once every staged file has been attempted\n *\n * @example\n * ```ts\n * await unlinkStagedFiles(body)\n * ```\n */\nexport async function unlinkStagedFiles(body: MultipartBody): Promise<void> {\n\tfor (const records of Object.values(body.files)) {\n\t\tfor (const file of records) {\n\t\t\tif (file.status !== 'staged') continue\n\t\t\ttry {\n\t\t\t\tawait unlink(file.path)\n\t\t\t} catch {\n\t\t\t\t// Already gone — cleanup is best-effort.\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Adapts a `node:fs` read stream over a file path (or an already-open\n * `FileHandle`) into a DOM-compatible `ReadableStream<Uint8Array>` — the\n * single shared node↔web stream bridge every static-file and uploaded-file\n * response body routes through.\n *\n * @remarks\n * PULL-driven, not push-driven: the underlying node stream's async iterator\n * is only advanced (`iterator.next()`) from inside `pull(controller)`, which\n * the web `ReadableStream` invokes exactly when its internal queue has room\n * for more data. Exactly one disk chunk is read and enqueued per `pull` —\n * never more — so a slow or stalled consumer (a stalled HTTP connection)\n * stops triggering `pull` calls and the source stops reading ahead;\n * this is genuine consumer backpressure, not the \"naturally backpressured\"\n * `for await`/`enqueue` pattern (which does not block on a slow consumer at\n * all, because `enqueue` returns synchronously). The controller is closed on\n * iterator completion and errored (never thrown into the process) on a\n * mid-stream read failure. Cancelling the returned `ReadableStream` (for\n * example the consumer aborts the response) calls the iterator's `return()`, which\n * destroys the underlying node read stream so the file descriptor is\n * released. When `source` is a `FileHandle`, `FileHandle.createReadStream`'s\n * default `autoClose` closes the handle on every terminal path (end, error,\n * or `destroy()` through the iterator's `return()`) — the caller never needs a\n * separate `handle.close()` for a `FileHandle` passed as `source`.\n *\n * @param source - The absolute on-disk file path to stream, or an already-open\n * `FileHandle` (for example one already `fstat`'d so the served bytes match the\n * headers computed from that same `fstat`)\n * @param range - An optional inclusive byte range; see {@link ByteRange}\n * @returns A `ReadableStream<Uint8Array>` valid as a fetch `BodyInit`\n *\n * @example\n * ```ts\n * new Response(streamFile('/srv/public/index.html'))\n * ```\n */\nexport function streamFile(\n\tsource: string | FileHandle,\n\trange?: ByteRange,\n): ReadableStream<Uint8Array> {\n\tconst stream =\n\t\ttypeof source === 'string'\n\t\t\t? range === undefined\n\t\t\t\t? createReadStream(source)\n\t\t\t\t: createReadStream(source, { start: range.start, end: range.end })\n\t\t\t: range === undefined\n\t\t\t\t? source.createReadStream()\n\t\t\t\t: source.createReadStream({ start: range.start, end: range.end })\n\tconst iterator: AsyncIterator<unknown> = stream[Symbol.asyncIterator]()\n\treturn new ReadableStream<Uint8Array>({\n\t\tasync pull(controller) {\n\t\t\ttry {\n\t\t\t\tconst { done, value } = await iterator.next()\n\t\t\t\tif (done) {\n\t\t\t\t\tcontroller.close()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!(value instanceof Uint8Array)) {\n\t\t\t\t\tawait iterator.return?.()\n\t\t\t\t\tcontroller.error(new TypeError('streamFile: read stream yielded a non-Uint8Array chunk'))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tcontroller.enqueue(value)\n\t\t\t} catch (error) {\n\t\t\t\tawait iterator.return?.()\n\t\t\t\tcontroller.error(error)\n\t\t\t}\n\t\t},\n\t\tasync cancel() {\n\t\t\tawait iterator.return?.()\n\t\t},\n\t})\n}\n\n/**\n * Opens a staged/moved uploaded file as a web `ReadableStream`.\n *\n * @param file - The {@link UploadedFile} record to stream\n * @returns A `ReadableStream<Uint8Array>` over the file's current on-disk path\n *\n * @example\n * ```ts\n * new Response(streamUploadedFile(file))\n * ```\n */\nexport function streamUploadedFile(file: UploadedFile): ReadableStream<Uint8Array> {\n\treturn streamFile(file.path)\n}\n\n/**\n * Reads a staged/moved uploaded file's full contents into memory.\n *\n * @param file - The {@link UploadedFile} record to read\n * @returns The file's bytes\n *\n * @example\n * ```ts\n * const bytes = await readUploadedFile(file)\n * ```\n */\nexport async function readUploadedFile(file: UploadedFile): Promise<Uint8Array> {\n\treturn readFile(file.path)\n}\n\n/**\n * Moves a staged uploaded file to its final `destination`.\n *\n * @remarks\n * Attempts a `rename` first; on a cross-device error (`EXDEV`) falls back to\n * `copyFile` + `unlink`. Returns a new frozen record with `status: 'moved'`\n * and `path: destination` — the input record is never mutated. The suite\n * drives the `EXDEV` fallback only on a host whose device probe finds a\n * second filesystem, so a single-device host leaves that branch unproven.\n *\n * @param file - The {@link UploadedFile} record to move\n * @param destination - The final on-disk path\n * @returns A new {@link UploadedFile} record reflecting the move\n *\n * @example\n * ```ts\n * const moved = await moveUploadedFile(file, '/var/uploads/final.png')\n * ```\n */\nexport async function moveUploadedFile(\n\tfile: UploadedFile,\n\tdestination: string,\n): Promise<UploadedFile> {\n\ttry {\n\t\tawait rename(file.path, destination)\n\t} catch (error) {\n\t\t// `isRecord` refuses a class instance, and every `node:fs` rejection is\n\t\t// an `Error` instance, so the cross-device branch must narrow with\n\t\t// `isError` to be reachable at all.\n\t\tif (isError(error) && 'code' in error && error.code === 'EXDEV') {\n\t\t\tawait copyFile(file.path, destination)\n\t\t\tawait unlink(file.path)\n\t\t} else {\n\t\t\tthrow error\n\t\t}\n\t}\n\treturn createUploadedFile({\n\t\tfield: file.field,\n\t\tname: file.name,\n\t\tsize: file.size,\n\t\tmime: file.mime,\n\t\tvalidated: file.validated,\n\t\tstatus: 'moved',\n\t\tpath: destination,\n\t})\n}\n","import type { MultipartBody, MultipartFile } from '@src/core'\nimport type { MultipartLimits } from './types.js'\nimport { randomUUID } from 'node:crypto'\nimport { chmod, mkdtemp, open, unlink } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport { isDangerousKey } from '@orkestrel/server'\nimport {\n\tDEFAULT_CONTENT_TYPE,\n\tMULTIPART_MAX_HEADER_BLOCK,\n\tMULTIPART_MAX_PREAMBLE,\n} from './constants.js'\nimport { MultipartError } from './errors.js'\nimport { createUploadedFile, detectMIME, parsePartHeaders } from './helpers.js'\n\nexport class MultipartParser {\n\tstatic #defaultDirectory: Promise<string> | undefined\n\treadonly #reader: ReadableStreamDefaultReader<Uint8Array>\n\treadonly #signal: AbortSignal\n\treadonly #abort: () => void\n\treadonly #boundary: string\n\treadonly #limits: MultipartLimits\n\treadonly #allowed: readonly string[] | undefined\n\treadonly #directory: string\n\treadonly #staged: string[] = []\n\treadonly #files: Record<string, MultipartFile[]> = Object.create(null)\n\treadonly #fields: Record<string, string> = Object.create(null)\n\t#buffer = Buffer.alloc(0)\n\t#ended = false\n\t#fileCount = 0\n\t#fieldCount = 0\n\t#totalBytes = 0\n\n\tconstructor(\n\t\tstream: ReadableStream<Uint8Array>,\n\t\tsignal: AbortSignal,\n\t\tboundary: string,\n\t\tlimits: MultipartLimits,\n\t\tallowed: readonly string[] | undefined,\n\t\tdirectory: string,\n\t) {\n\t\tthis.#reader = stream.getReader()\n\t\tthis.#signal = signal\n\t\tthis.#abort = this.#wakeReader.bind(this)\n\t\tthis.#boundary = boundary\n\t\tthis.#limits = limits\n\t\tthis.#allowed = allowed\n\t\tthis.#directory = directory\n\t}\n\n\tstatic directory(): Promise<string> {\n\t\tif (MultipartParser.#defaultDirectory === undefined)\n\t\t\tMultipartParser.#defaultDirectory = MultipartParser.#createDirectory()\n\t\treturn MultipartParser.#defaultDirectory\n\t}\n\n\tasync parse(): Promise<MultipartBody> {\n\t\tthis.#signal.addEventListener('abort', this.#abort, { once: true })\n\t\ttry {\n\t\t\tconst openMarker = Buffer.from(`--${this.#boundary}`)\n\t\t\tlet preambleScanned = 0\n\t\t\tlet index = this.#buffer.indexOf(openMarker)\n\t\t\twhile (index === -1) {\n\t\t\t\tconst carry = openMarker.length - 1\n\t\t\t\tif (this.#buffer.length > carry) {\n\t\t\t\t\tconst drop = this.#buffer.length - carry\n\t\t\t\t\tpreambleScanned += drop\n\t\t\t\t\tif (preambleScanned > MULTIPART_MAX_PREAMBLE)\n\t\t\t\t\t\tthrow new MultipartError('malformed', 'multipart preamble too large')\n\t\t\t\t\tthis.#buffer = this.#buffer.subarray(drop)\n\t\t\t\t}\n\t\t\t\tif (!(await this.#pull()))\n\t\t\t\t\tthrow new MultipartError('malformed', 'missing multipart boundary')\n\t\t\t\tindex = this.#buffer.indexOf(openMarker)\n\t\t\t}\n\t\t\tif (preambleScanned + index > MULTIPART_MAX_PREAMBLE)\n\t\t\t\tthrow new MultipartError('malformed', 'multipart preamble too large')\n\t\t\tthis.#buffer = this.#buffer.subarray(index + openMarker.length)\n\n\t\t\tfor (;;) {\n\t\t\t\twhile (this.#buffer.length < 2)\n\t\t\t\t\tif (!(await this.#pull()))\n\t\t\t\t\t\tthrow new MultipartError('malformed', 'unterminated multipart boundary')\n\t\t\t\tif (this.#buffer[0] === 0x2d && this.#buffer[1] === 0x2d) break\n\t\t\t\tif (this.#buffer[0] !== 0x0d || this.#buffer[1] !== 0x0a)\n\t\t\t\t\tthrow new MultipartError('malformed', 'malformed multipart boundary')\n\t\t\t\tthis.#buffer = this.#buffer.subarray(2)\n\n\t\t\t\tlet headerEnd = this.#buffer.indexOf('\\r\\n\\r\\n')\n\t\t\t\twhile (headerEnd === -1) {\n\t\t\t\t\tif (this.#buffer.length > MULTIPART_MAX_HEADER_BLOCK)\n\t\t\t\t\t\tthrow new MultipartError('malformed', 'multipart header block too large')\n\t\t\t\t\tif (!(await this.#pull()))\n\t\t\t\t\t\tthrow new MultipartError('malformed', 'unterminated multipart part headers')\n\t\t\t\t\theaderEnd = this.#buffer.indexOf('\\r\\n\\r\\n')\n\t\t\t\t}\n\t\t\t\tif (headerEnd > MULTIPART_MAX_HEADER_BLOCK)\n\t\t\t\t\tthrow new MultipartError('malformed', 'multipart header block too large')\n\t\t\t\tconst headerBlock = this.#buffer.subarray(0, headerEnd).toString('utf8')\n\t\t\t\tthis.#buffer = this.#buffer.subarray(headerEnd + 4)\n\t\t\t\tconst { name, filename, mime } = parsePartHeaders(headerBlock)\n\t\t\t\tif (name === undefined) throw new MultipartError('malformed', 'multipart part missing name')\n\n\t\t\t\tconst partDelimiter = Buffer.from(`\\r\\n--${this.#boundary}`)\n\n\t\t\t\tif (filename !== undefined) await this.#consumeFile(name, filename, mime, partDelimiter)\n\t\t\t\telse await this.#consumeField(name, partDelimiter)\n\n\t\t\t\twhile (this.#buffer.length < openMarker.length)\n\t\t\t\t\tif (!(await this.#pull()))\n\t\t\t\t\t\tthrow new MultipartError('malformed', 'unterminated multipart boundary')\n\t\t\t\tthis.#buffer = this.#buffer.subarray(openMarker.length)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tawait this.#cleanup()\n\t\t\tawait this.#reader.cancel().catch(() => {})\n\t\t\tthrow error\n\t\t} finally {\n\t\t\tthis.#signal.removeEventListener('abort', this.#abort)\n\t\t\tif (!this.#ended) await this.#reader.cancel().catch(() => {})\n\t\t}\n\n\t\tconst files: Record<string, readonly MultipartFile[]> = Object.create(null)\n\t\tfor (const [field, records] of Object.entries(this.#files))\n\t\t\tfiles[field] = Object.freeze([...records])\n\t\tconst fields: Record<string, string> = Object.assign(Object.create(null), this.#fields)\n\t\treturn {\n\t\t\tfiles: Object.freeze(files),\n\t\t\tfields: Object.freeze(fields),\n\t\t}\n\t}\n\n\t// Streams one file part to disk, screens its sniffed type, and keys its\n\t// record onto the parsed body.\n\tasync #consumeFile(\n\t\tname: string,\n\t\tfilename: string,\n\t\tmime: string | undefined,\n\t\tdelimiter: Buffer,\n\t): Promise<void> {\n\t\tif (filename !== '') {\n\t\t\tthis.#fileCount += 1\n\t\t\tif (this.#fileCount > this.#limits.file.count)\n\t\t\t\tthrow new MultipartError('limit', 'too many multipart files')\n\t\t}\n\t\tconst path = join(this.#directory, randomUUID())\n\t\tthis.#staged.push(path)\n\t\tconst handle = await open(path, 'w', 0o600)\n\t\tlet size = 0\n\t\tlet head = Buffer.alloc(0)\n\t\ttry {\n\t\t\tawait this.#scan(delimiter, 'unterminated multipart file part', async (chunk) => {\n\t\t\t\tsize += chunk.length\n\t\t\t\tif (size > this.#limits.file.size)\n\t\t\t\t\tthrow new MultipartError('limit', 'multipart file exceeds size limit')\n\t\t\t\tif (head.length < 16) head = Buffer.concat([head, chunk.subarray(0, 16 - head.length)])\n\t\t\t\tawait handle.write(chunk)\n\t\t\t})\n\t\t} finally {\n\t\t\tawait handle.close()\n\t\t}\n\t\tif (filename === '' && size === 0) {\n\t\t\tawait this.#discard(path)\n\t\t\treturn\n\t\t}\n\t\tif (filename === '') {\n\t\t\tthis.#fileCount += 1\n\t\t\tif (this.#fileCount > this.#limits.file.count)\n\t\t\t\tthrow new MultipartError('limit', 'too many multipart files')\n\t\t}\n\t\tconst detected = detectMIME(head)\n\t\tconst declared = mime ?? DEFAULT_CONTENT_TYPE\n\t\tconst validated = detected !== undefined && detected === declared\n\t\tif (this.#allowed !== undefined) {\n\t\t\tconst acceptable = detected !== undefined && this.#allowed.includes(detected)\n\t\t\tif (!acceptable) throw new MultipartError('rejected', 'multipart file failed type validation')\n\t\t}\n\t\tif (isDangerousKey(name)) {\n\t\t\tawait this.#discard(path)\n\t\t\treturn\n\t\t}\n\t\tconst record = createUploadedFile({\n\t\t\tfield: name,\n\t\t\tname: filename,\n\t\t\tsize,\n\t\t\tmime: detected ?? declared,\n\t\t\tvalidated,\n\t\t\tstatus: 'staged',\n\t\t\tpath,\n\t\t})\n\t\tconst existing = this.#files[name]\n\t\tif (existing === undefined) this.#files[name] = [record]\n\t\telse existing.push(record)\n\t}\n\n\t// Accumulates one text field part and keys it onto the parsed body.\n\tasync #consumeField(name: string, delimiter: Buffer): Promise<void> {\n\t\tthis.#fieldCount += 1\n\t\tif (this.#fieldCount > this.#limits.field.count)\n\t\t\tthrow new MultipartError('limit', 'too many multipart fields')\n\t\tlet value = Buffer.alloc(0)\n\t\tawait this.#scan(delimiter, 'unterminated multipart field part', (chunk) => {\n\t\t\tvalue = Buffer.concat([value, chunk])\n\t\t\tif (value.length > this.#limits.field.size)\n\t\t\t\tthrow new MultipartError('limit', 'multipart field exceeds size limit')\n\t\t})\n\t\tif (!isDangerousKey(name)) this.#fields[name] = value.toString('utf8')\n\t}\n\n\t// Drains one part's bytes into `sink` up to `delimiter`, carrying back the\n\t// partial tail that could still be the delimiter's own prefix, and leaves\n\t// the buffer positioned past the delimiter's leading CRLF.\n\tasync #scan(\n\t\tdelimiter: Buffer,\n\t\tunterminated: string,\n\t\tsink: (chunk: Buffer) => void | Promise<void>,\n\t): Promise<void> {\n\t\tfor (;;) {\n\t\t\tconst boundaryIndex = this.#buffer.indexOf(delimiter)\n\t\t\tif (boundaryIndex === -1) {\n\t\t\t\tconst safeLength = Math.max(0, this.#buffer.length - (delimiter.length - 1))\n\t\t\t\tif (safeLength > 0) {\n\t\t\t\t\tawait sink(this.#buffer.subarray(0, safeLength))\n\t\t\t\t\tthis.#buffer = this.#buffer.subarray(safeLength)\n\t\t\t\t}\n\t\t\t\tif (!(await this.#pull())) throw new MultipartError('malformed', unterminated)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tawait sink(this.#buffer.subarray(0, boundaryIndex))\n\t\t\tthis.#buffer = this.#buffer.subarray(boundaryIndex + 2)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Unlinks one staged temp file best-effort and drops it from the staged\n\t// roster, leaving the roster untouched when the path is not on it.\n\tasync #discard(path: string): Promise<void> {\n\t\ttry {\n\t\t\tawait unlink(path)\n\t\t} catch {\n\t\t\t// Already gone — cleanup is best-effort.\n\t\t}\n\t\tconst index = this.#staged.indexOf(path)\n\t\tif (index !== -1) this.#staged.splice(index, 1)\n\t}\n\n\tstatic async #createDirectory(): Promise<string> {\n\t\tconst path = await mkdtemp(join(tmpdir(), 'orkestrel-multipart-'))\n\t\tawait chmod(path, 0o700)\n\t\treturn path\n\t}\n\n\tasync #cleanup(): Promise<void> {\n\t\tfor (const path of this.#staged) {\n\t\t\ttry {\n\t\t\t\tawait unlink(path)\n\t\t\t} catch {\n\t\t\t\t// Already gone — cleanup is best-effort.\n\t\t\t}\n\t\t}\n\t}\n\n\tasync #pull(): Promise<boolean> {\n\t\tif (this.#signal.aborted) throw new MultipartError('malformed', 'request aborted mid-upload')\n\t\tif (this.#ended) return false\n\t\tconst { done, value } = await this.#reader.read()\n\t\tif (this.#signal.aborted) throw new MultipartError('malformed', 'request aborted mid-upload')\n\t\tif (done) {\n\t\t\tthis.#ended = true\n\t\t\treturn false\n\t\t}\n\t\tthis.#totalBytes += value.byteLength\n\t\tif (this.#totalBytes > this.#limits.total)\n\t\t\tthrow new MultipartError('limit', 'multipart body exceeds total limit')\n\t\tthis.#buffer = Buffer.concat([\n\t\t\tthis.#buffer,\n\t\t\tBuffer.from(value.buffer, value.byteOffset, value.byteLength),\n\t\t])\n\t\treturn true\n\t}\n\n\t#wakeReader(): void {\n\t\tthis.#reader.cancel().catch(() => {})\n\t}\n}\n","import type { MultipartBody } from '@src/core'\nimport type { MultipartOptions } from './types.js'\nimport { MultipartError } from './errors.js'\nimport { extractMultipartBoundary, resolveMultipartLimits } from './helpers.js'\nimport { MultipartParser } from './MultipartParser.js'\n\n/**\n * Stream-parses a `multipart/form-data` request into its files and fields —\n * the mid-stream state machine `createMultipart` drives.\n *\n * @remarks\n * Reads `request.body` chunk by chunk through its `ReadableStream` reader —\n * NEVER buffers the whole body — enforcing every {@link MultipartLimits} cap\n * the instant it is exceeded (reading stops, every already-staged temp file\n * is deleted, throws {@link MultipartError} with code `'limit'`). Each file\n * part streams to `join(directory, randomUUID())` — the client's declared\n * filename is METADATA ONLY, never a path component. A field OR file part\n * named `__proto__` / `constructor` / `prototype` is silently skipped and\n * never keyed onto the returned {@link MultipartBody} (a skipped file's\n * staged temp file is unlinked immediately, since it can never be\n * referenced). A file part with an empty declared filename (`filename=\"\"`)\n * AND a zero-byte body — the browser convention for an unselected optional\n * `<input type=\"file\">` — is a silent no-op: its temp file is unlinked, it is\n * never counted against the `file.count` limit, and it never runs the\n * `allowed` check. A malformed\n * structure (missing/unterminated boundary, nameless part, an oversized\n * header block, or a preamble exceeding {@link MULTIPART_MAX_PREAMBLE} before\n * the first boundary) throws with code `'malformed'`. A file is accepted\n * against the configured `allowed` MIME list iff its SNIFFED bytes detect a\n * type present in the list — sniff-authoritative, independent of whether the\n * declared `Content-Type` matches (that agreement is exposed separately as\n * `validated`); otherwise throws with code `'rejected'`. A\n * request abort mid-upload triggers the same fail-closed cleanup as a limit\n * breach. Returns `undefined` for a non-multipart request (untouched).\n *\n * Staging defaults to a process-owned directory created ONCE (lazily,\n * memoized across calls) with `mkdtemp` under `os.tmpdir()` and locked to\n * mode `0o700`; `options.directory` overrides it.\n *\n * @param request - The incoming multipart request\n * @param options - See {@link MultipartOptions}\n * @returns The parsed {@link MultipartBody}, or `undefined` when the request\n * is not `multipart/form-data`\n * @throws {MultipartError} On any limit breach, malformed structure, or\n * rejected file type\n *\n * @example\n * ```ts\n * const body = await parseMultipartRequest(request, { allowed: ['image/png'] })\n * ```\n */\nexport async function parseMultipartRequest(\n\trequest: Request,\n\toptions: MultipartOptions = {},\n): Promise<MultipartBody | undefined> {\n\tconst boundary = extractMultipartBoundary(request.headers.get('content-type'))\n\tif (boundary === undefined) return undefined\n\tif (request.body === null) throw new MultipartError('malformed', 'multipart request has no body')\n\n\tconst limits = resolveMultipartLimits(options.limits)\n\tconst allowed = options.allowed\n\tconst directory = options.directory ?? (await MultipartParser.directory())\n\treturn new MultipartParser(\n\t\trequest.body,\n\t\trequest.signal,\n\t\tboundary,\n\t\tlimits,\n\t\tallowed,\n\t\tdirectory,\n\t).parse()\n}\n","import type { MultipartBody, MultipartState } from '@src/core'\nimport type {\n\tAssetOptions,\n\tMultipartOptions,\n\tNodeCompressionOptions,\n\tStaticOptions,\n} from './types.js'\nimport type { Stats } from 'node:fs'\nimport type { FileHandle } from 'node:fs/promises'\nimport type { MiddlewareHandler } from '@orkestrel/server'\nimport { DEFAULT_COMPRESSION_THRESHOLD, compressResponse } from '@src/core'\nimport { open, realpath, stat } from 'node:fs/promises'\nimport { join, relative, resolve } from 'node:path'\nimport { brotliDecompressSync } from 'node:zlib'\nimport {\n\tcomputeBodyETag,\n\tHTTPError,\n\tmatchesETag,\n\tnegotiateEncoding,\n\tparseRange,\n} from '@orkestrel/server'\nimport {\n\tisArrayBuffer,\n\tisFiniteNumber,\n\tisFunction,\n\tisObject,\n\tisString,\n\tisUint8Array,\n} from '@orkestrel/contract'\nimport {\n\tDEFAULT_STATIC_DOTFILES,\n\tDEFAULT_STATIC_FALLBACK_EXCLUDE,\n\tDEFAULT_STATIC_INDEX,\n\tNODE_COMPRESSION_ENCODINGS,\n} from './constants.js'\nimport { isMultipartError } from './errors.js'\nimport {\n\tcomputeFileETag,\n\tcompressNodeBytes,\n\tisContainedPath,\n\tisDotfilePath,\n\tlookupContentType,\n\tresolveContainedRealPath,\n\tresolveStaticFallbackPath,\n\tresolveStaticPath,\n\tstreamFile,\n\tunlinkStagedFiles,\n} from './helpers.js'\nimport { parseMultipartRequest } from './parsers.js'\n\n/**\n * Serves validated in-memory assets with identity/Brotli negotiation.\n *\n * @remarks\n * Only `GET` and `HEAD` are served. `/` resolves to `index.html`. Every other\n * key is decoded once, must remain relative, and refuses empty, dot, climbing,\n * backslash, and dotfile segments before the source is read. Successful source\n * values are copied and cached by key. A Brotli value is decompressed once for\n * the identity representation; both representations share an ETag computed\n * from those identity bytes. Responses always vary on `Accept-Encoding`.\n * Range requests are intentionally served as full responses because an asset\n * source exposes complete in-memory representations rather than file handles.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param options - See {@link AssetOptions}\n * @returns A `MiddlewareHandler<TState>`\n * @throws {TypeError} When `options.source` does not implement\n * {@link AssetSourceInterface}, or when it returns malformed asset data\n * @throws {Error} When a source marks bytes as Brotli but they cannot be decompressed\n *\n * @example\n * ```ts\n * import { createAssets } from '@orkestrel/middleware/server'\n *\n * const assets = createAssets({\n * \tsource: {\n * \t\tread: (path) =>\n * \t\t\tpath === 'index.html'\n * \t\t\t\t? { body: new TextEncoder().encode('home') }\n * \t\t\t\t: undefined,\n * \t},\n * })\n * ```\n */\nexport function createAssets<TState>(options: AssetOptions): MiddlewareHandler<TState> {\n\tif (!isObject(options?.source) || !('read' in options.source) || !isFunction(options.source.read))\n\t\tthrow new TypeError('AssetOptions.source must implement AssetSourceInterface')\n\tconst identities = new Map<string, Uint8Array<ArrayBuffer>>()\n\tconst brotlis = new Map<string, Uint8Array<ArrayBuffer>>()\n\tconst tags = new Map<string, Promise<string>>()\n\n\treturn async (request, context, next) => {\n\t\tif (context.method !== 'GET' && context.method !== 'HEAD') return next()\n\t\tlet pathname: string\n\t\ttry {\n\t\t\tpathname = decodeURIComponent(context.url.pathname)\n\t\t} catch {\n\t\t\treturn next()\n\t\t}\n\t\tconst key = pathname === '/' ? DEFAULT_STATIC_INDEX : pathname.slice(1)\n\t\tif (\n\t\t\tkey.length === 0 ||\n\t\t\tkey.includes('\\\\') ||\n\t\t\tkey\n\t\t\t\t.split('/')\n\t\t\t\t.some((segment) => segment.length === 0 || segment === '.' || segment === '..') ||\n\t\t\tisDotfilePath(key)\n\t\t)\n\t\t\treturn next()\n\n\t\tlet identity = identities.get(key)\n\t\tlet brotli = brotlis.get(key)\n\t\tif (identity === undefined) {\n\t\t\tconst asset = options.source.read(key)\n\t\t\tif (asset === undefined) return next()\n\t\t\tif (!isObject(asset)) throw new TypeError('AssetSourceInterface.read must return an Asset')\n\t\t\tconst body = asset.body\n\t\t\tconst encoding = asset.encoding\n\t\t\tif (\n\t\t\t\t(!isArrayBuffer(body) && !isUint8Array(body)) ||\n\t\t\t\t(encoding !== undefined && encoding !== 'br')\n\t\t\t)\n\t\t\t\tthrow new TypeError('AssetSourceInterface.read must return an Asset')\n\t\t\tconst content = isArrayBuffer(body)\n\t\t\t\t? Uint8Array.from(new Uint8Array(body))\n\t\t\t\t: Uint8Array.from(body)\n\t\t\tif (encoding === 'br') {\n\t\t\t\tbrotli = content\n\t\t\t\tidentity = Uint8Array.from(brotliDecompressSync(content))\n\t\t\t\tbrotlis.set(key, brotli)\n\t\t\t} else {\n\t\t\t\tidentity = content\n\t\t\t}\n\t\t\tidentities.set(key, identity)\n\t\t}\n\n\t\tlet pending = tags.get(key)\n\t\tif (pending === undefined) {\n\t\t\tpending = computeBodyETag(identity)\n\t\t\ttags.set(key, pending)\n\t\t}\n\t\tconst etag = await pending\n\t\tlet body = identity\n\t\tlet encoding: 'br' | undefined\n\t\tif (\n\t\t\tbrotli !== undefined &&\n\t\t\tnegotiateEncoding(request.headers.get('accept-encoding') ?? '', ['br']) === 'br'\n\t\t) {\n\t\t\tbody = brotli\n\t\t\tencoding = 'br'\n\t\t}\n\t\tconst headers = new Headers({\n\t\t\t'content-type': lookupContentType(key),\n\t\t\tetag,\n\t\t\tvary: 'accept-encoding',\n\t\t})\n\t\tif (encoding !== undefined) headers.set('content-encoding', encoding)\n\t\tconst cached = request.headers.get('if-none-match')\n\t\tif (cached !== null && matchesETag(cached, etag))\n\t\t\treturn new Response(null, { status: 304, headers })\n\t\theaders.set('content-length', String(body.byteLength))\n\t\treturn new Response(context.method === 'HEAD' ? undefined : body, { headers })\n\t}\n}\n\n/**\n * Serves static files from `options.root` over `node:fs` — the node-bound\n * static-file battery.\n *\n * @remarks\n * Containment is enforced on CANONICAL paths, not merely the lexically\n * resolved one: `options.root` is canonicalized once (memoized) and every\n * request's candidate path is re-canonicalized (`fs.realpath`) before it is\n * served, so a symlink whose target escapes `root` is refused (falls through\n * to `next()`) even though the lexical path resolved inside `root`. A\n * symlink that resolves to a target still INSIDE `root` is unaffected and\n * still serves normally. A dangling symlink (`realpath` throws `ENOENT`) or\n * any other `realpath` failure is treated as a miss — this battery never\n * throws or 500s on a symlink surprise. On a streamed response (a 200 or 206\n * that carries a file body), the open `FileHandle` is owned by the\n * `Response` body and is released only once that body is fully read or\n * cancelled — Node HTTP servers do this automatically when sending the\n * response, but a caller that holds an unread `Response` (for example in a test)\n * must cancel its body to release the handle promptly.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param options - See {@link StaticOptions}\n * @returns A `MiddlewareHandler<TState>`\n * @throws {TypeError} When `options.root` is not a non-empty string\n *\n * @example\n * ```ts\n * import { createStatic } from '@orkestrel/middleware/server'\n *\n * const serveFiles = createStatic({ root: '/srv/public', fallback: true })\n * ```\n */\nexport function createStatic<TState>(options: StaticOptions): MiddlewareHandler<TState> {\n\tif (!isString(options.root) || options.root.length === 0)\n\t\tthrow new TypeError('createStatic requires options.root to be a non-empty string')\n\tconst root = resolve(options.root)\n\tconst index = options.index ?? DEFAULT_STATIC_INDEX\n\tconst dotfiles = options.dotfiles ?? DEFAULT_STATIC_DOTFILES\n\tconst useETag = options.etag ?? true\n\tconst fallback =\n\t\toptions.fallback === true\n\t\t\t? { exclude: DEFAULT_STATIC_FALLBACK_EXCLUDE }\n\t\t\t: options.fallback === false || options.fallback === undefined\n\t\t\t\t? undefined\n\t\t\t\t: { exclude: options.fallback.exclude ?? DEFAULT_STATIC_FALLBACK_EXCLUDE }\n\n\tlet canonicalRootPromise: Promise<string> | undefined\n\n\treturn async (request, context, next) => {\n\t\tif (context.method !== 'GET' && context.method !== 'HEAD') return next()\n\n\t\tconst target = resolveStaticPath(root, options.prefix, context.url.pathname)\n\t\tif (target === undefined) return next()\n\n\t\tconst relativePath = relative(root, target)\n\t\tif (relativePath.length > 0 && isDotfilePath(relativePath)) {\n\t\t\tif (dotfiles === 'deny') throw new HTTPError(403, 'forbidden')\n\t\t\tif (dotfiles === 'ignore') return next()\n\t\t}\n\n\t\tlet resolvedPath = target\n\t\tlet fallbackNeeded = false\n\t\ttry {\n\t\t\tif (canonicalRootPromise === undefined) canonicalRootPromise = realpath(root)\n\t\t\tconst [rootReal, targetReal] = await Promise.all([canonicalRootPromise, realpath(target)])\n\t\t\tif (!isContainedPath(targetReal, rootReal)) return next()\n\t\t\tresolvedPath = targetReal\n\t\t} catch {\n\t\t\tfallbackNeeded = true\n\t\t}\n\n\t\t// Directory-detection only — routing decision, not the served file's\n\t\t// facts. The bytes streamed and the headers computed for them come\n\t\t// from a single `fstat` on a handle opened later in this function,\n\t\t// closing the stat-to-stream TOCTOU (a file replaced between \"check\"\n\t\t// and \"serve\" can no longer yield a 200 with stale headers over\n\t\t// swapped bytes).\n\t\tlet directoryInfo: Stats | undefined\n\t\tif (!fallbackNeeded) {\n\t\t\ttry {\n\t\t\t\tdirectoryInfo = await stat(resolvedPath)\n\t\t\t} catch {\n\t\t\t\tfallbackNeeded = true\n\t\t\t}\n\t\t}\n\n\t\tif (directoryInfo?.isDirectory()) {\n\t\t\tresolvedPath = join(resolvedPath, index)\n\t\t\ttry {\n\t\t\t\tif (canonicalRootPromise === undefined) canonicalRootPromise = realpath(root)\n\t\t\t\tconst rootReal = await canonicalRootPromise\n\t\t\t\tconst indexReal = await resolveContainedRealPath(resolvedPath, rootReal)\n\t\t\t\tif (indexReal === undefined) fallbackNeeded = true\n\t\t\t\telse resolvedPath = indexReal\n\t\t\t} catch {\n\t\t\t\tfallbackNeeded = true\n\t\t\t}\n\t\t}\n\n\t\tlet handle: FileHandle | undefined\n\t\tlet info: Stats | undefined\n\t\tif (!fallbackNeeded) {\n\t\t\ttry {\n\t\t\t\thandle = await open(resolvedPath, 'r')\n\t\t\t} catch {\n\t\t\t\tfallbackNeeded = true\n\t\t\t}\n\t\t}\n\t\tif (handle !== undefined) {\n\t\t\ttry {\n\t\t\t\tinfo = await handle.stat()\n\t\t\t} catch {\n\t\t\t\tawait handle.close()\n\t\t\t\thandle = undefined\n\t\t\t\tfallbackNeeded = true\n\t\t\t}\n\t\t}\n\t\tif (handle !== undefined && info !== undefined && !info.isFile()) {\n\t\t\tawait handle.close()\n\t\t\thandle = undefined\n\t\t\tinfo = undefined\n\t\t\tfallbackNeeded = true\n\t\t}\n\n\t\tif (fallbackNeeded) {\n\t\t\tconst shellPath = resolveStaticFallbackPath(\n\t\t\t\troot,\n\t\t\t\tindex,\n\t\t\t\tfallback?.exclude ?? DEFAULT_STATIC_FALLBACK_EXCLUDE,\n\t\t\t\tcontext.method,\n\t\t\t\tcontext.url.pathname,\n\t\t\t\trequest.headers.get('accept') ?? '',\n\t\t\t)\n\t\t\tif (fallback === undefined || shellPath === undefined) return next()\n\t\t\tlet shellReal: string\n\t\t\ttry {\n\t\t\t\tif (canonicalRootPromise === undefined) canonicalRootPromise = realpath(root)\n\t\t\t\tconst rootReal = await canonicalRootPromise\n\t\t\t\tconst candidate = await resolveContainedRealPath(shellPath, rootReal)\n\t\t\t\tif (candidate === undefined) return next()\n\t\t\t\tshellReal = candidate\n\t\t\t} catch {\n\t\t\t\treturn next()\n\t\t\t}\n\t\t\tlet shellHandle: FileHandle\n\t\t\ttry {\n\t\t\t\tshellHandle = await open(shellReal, 'r')\n\t\t\t} catch {\n\t\t\t\treturn next()\n\t\t\t}\n\t\t\tlet shellInfo: Stats\n\t\t\ttry {\n\t\t\t\tshellInfo = await shellHandle.stat()\n\t\t\t} catch {\n\t\t\t\tawait shellHandle.close().catch(() => {})\n\t\t\t\treturn next()\n\t\t\t}\n\t\t\tif (!shellInfo.isFile()) {\n\t\t\t\tawait shellHandle.close().catch(() => {})\n\t\t\t\treturn next()\n\t\t\t}\n\t\t\t// The shell answers through the SAME response block the primary path\n\t\t\t// uses, so `cache`, `etag`, conditional revalidation, `HEAD`, and\n\t\t\t// ranges are computed from the shell handle's own `fstat` and cannot\n\t\t\t// diverge between a served file and the shell served in its place.\n\t\t\tresolvedPath = shellReal\n\t\t\thandle = shellHandle\n\t\t\tinfo = shellInfo\n\t\t}\n\n\t\tif (handle === undefined || info === undefined) return next()\n\n\t\tlet streaming = false\n\t\ttry {\n\t\t\tconst headers = new Headers({\n\t\t\t\t'content-type': lookupContentType(resolvedPath),\n\t\t\t\t'accept-ranges': 'bytes',\n\t\t\t})\n\t\t\tif (options.cache !== undefined) headers.set('cache-control', `max-age=${options.cache}`)\n\n\t\t\tif (useETag) {\n\t\t\t\tconst etag = computeFileETag(info.size, info.mtimeMs)\n\t\t\t\theaders.set('etag', etag)\n\t\t\t\tconst ifNoneMatch = request.headers.get('if-none-match')\n\t\t\t\tif (ifNoneMatch !== null && matchesETag(ifNoneMatch, etag)) {\n\t\t\t\t\tawait handle.close()\n\t\t\t\t\treturn new Response(null, { status: 304, headers })\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (context.method === 'HEAD') {\n\t\t\t\tawait handle.close()\n\t\t\t\theaders.set('content-length', String(info.size))\n\t\t\t\treturn new Response(null, { status: 200, headers })\n\t\t\t}\n\n\t\t\tconst rangeHeader = request.headers.get('range')\n\t\t\tconst range = parseRange(rangeHeader === null ? undefined : rangeHeader, info.size)\n\t\t\tif (range === undefined) {\n\t\t\t\theaders.set('content-length', String(info.size))\n\t\t\t\tconst body = streamFile(handle)\n\t\t\t\tstreaming = true\n\t\t\t\treturn new Response(body, { status: 200, headers })\n\t\t\t}\n\t\t\tif (!range.satisfiable) {\n\t\t\t\tawait handle.close()\n\t\t\t\theaders.set('content-range', `bytes */${info.size}`)\n\t\t\t\treturn new Response(null, { status: 416, headers })\n\t\t\t}\n\t\t\theaders.set('content-range', `bytes ${range.start}-${range.end}/${info.size}`)\n\t\t\theaders.set('content-length', String(range.end - range.start + 1))\n\t\t\tconst body = streamFile(handle, { start: range.start, end: range.end })\n\t\t\tstreaming = true\n\t\t\treturn new Response(body, { status: 206, headers })\n\t\t} catch (error) {\n\t\t\tif (!streaming) await handle.close().catch(() => {})\n\t\t\tthrow error\n\t\t}\n\t}\n}\n\n/**\n * Parses a streamed `multipart/form-data` request body and stashes its\n * {@link MultipartBody} on `context.state.multipart` — the node-bound\n * streaming multipart battery.\n *\n * @remarks\n * A non-multipart request passes through untouched. Consumes `request.body`\n * as a stream — `context.body()` must not be called for a request this\n * battery has processed (the underlying stream is exhausted). Every\n * {@link MultipartError} this battery's parser throws is re-thrown as an\n * {@link HTTPError} carrying the same status/message, so `createBoundary`\n * (or any HTTPError-aware renderer) maps it correctly without depending on\n * this node face's error type. Fail-closed on the DOWNSTREAM handler too: if\n * `next()` throws, every still-`'staged'` uploaded file is unlinked\n * (best-effort) before the error is re-thrown, so an unhandled downstream\n * failure never leaks temp files. A normal return leaves staged files\n * untouched — the downstream handler owns moving/reading them.\n *\n * @typeParam TState - The consumer's state type, extending {@link MultipartState}\n * @param options - See {@link MultipartOptions}\n * @returns A `MiddlewareHandler<TState>`\n * @throws {HTTPError} When the underlying parse throws a {@link MultipartError}\n * (limit breach → 413, malformed structure → 400, rejected file type → 415)\n *\n * @example\n * ```ts\n * import { createMultipart } from '@orkestrel/middleware/server'\n *\n * const uploads = createMultipart({ allowed: ['image/png', 'image/jpeg'] })\n * ```\n */\nexport function createMultipart<TState extends MultipartState>(\n\toptions: MultipartOptions = {},\n): MiddlewareHandler<TState> {\n\treturn async (request, context, next) => {\n\t\tlet body: MultipartBody | undefined\n\t\ttry {\n\t\t\tbody = await parseMultipartRequest(request, options)\n\t\t} catch (error) {\n\t\t\tif (isMultipartError(error)) throw new HTTPError(error.status, error.message, error.context)\n\t\t\tthrow error\n\t\t}\n\t\tif (body === undefined) return next()\n\t\tObject.assign(context.state, { multipart: body })\n\t\ttry {\n\t\t\treturn await next()\n\t\t} catch (error) {\n\t\t\tawait unlinkStagedFiles(body)\n\t\t\tthrow error\n\t\t}\n\t}\n}\n\n/**\n * Compresses response bodies through `node:zlib` — the node-bound sibling of\n * the core face's `CompressionStream`-feature-detected `createCompression`,\n * guaranteed available on any Node runtime rather than dependent on the\n * WHATWG `CompressionStream` global. Ships as a SEPARATE package entry point\n * (`@orkestrel/middleware/server`) from the core face's `createCompression`,\n * so the shared name is unambiguous per consumer import path.\n *\n * @remarks\n * Peer-type limitation, the same one the core face carries: the shipped\n * `@orkestrel/server` `Encoding` union is `'gzip' | 'deflate' | 'identity'`\n * — it does not include `'br'`, so this battery cannot honestly type or\n * negotiate a guaranteed brotli coding despite `node:zlib` shipping\n * `brotliCompress`. It guarantees `gzip`/`deflate` through `node:zlib` (never\n * feature-detected — always available) and negotiates only those.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param options - See {@link NodeCompressionOptions}\n * @returns A `MiddlewareHandler<TState>`\n * @throws {TypeError} When `options.threshold` is provided and is not a\n * finite number, or `options.filter` is provided and is not a function\n *\n * @example\n * ```ts\n * import { createCompression } from '@orkestrel/middleware/server'\n *\n * const compress = createCompression({ threshold: 512 })\n * ```\n */\nexport function createCompression<TState>(\n\toptions?: NodeCompressionOptions,\n): MiddlewareHandler<TState> {\n\tif (options?.threshold !== undefined && !isFiniteNumber(options.threshold))\n\t\tthrow new TypeError('NodeCompressionOptions.threshold must be a finite number when provided')\n\tif (options?.filter !== undefined && !isFunction(options.filter))\n\t\tthrow new TypeError('NodeCompressionOptions.filter must be a function when provided')\n\tconst threshold = options?.threshold ?? DEFAULT_COMPRESSION_THRESHOLD\n\tconst filter = options?.filter\n\tconst encodings = NODE_COMPRESSION_ENCODINGS\n\treturn async (request, context, next) => {\n\t\tconst response = await next()\n\t\treturn compressResponse(request, context, response, {\n\t\t\tthreshold,\n\t\t\t...(filter !== undefined ? { filter } : {}),\n\t\t\tencodings,\n\t\t\tcompress: compressNodeBytes,\n\t\t})\n\t}\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orkestrel/middleware",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "Batteries for the @orkestrel/server middleware seam — boundary, telemetry, compression, security, CORS, rate limiting, sessions, CSRF, static files, and multipart uploads as typed factories. Part of the @orkestrel line.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"compression",
|
|
@@ -87,32 +87,31 @@
|
|
|
87
87
|
"test:setup": "vitest run --config vite.config.ts --no-cache --reporter=dot --project setup"
|
|
88
88
|
},
|
|
89
89
|
"dependencies": {
|
|
90
|
-
"@orkestrel/abort": "^0.0.
|
|
91
|
-
"@orkestrel/budget": "^0.0.
|
|
92
|
-
"@orkestrel/contract": "^0.0.
|
|
93
|
-
"@orkestrel/timeout": "^0.0.
|
|
90
|
+
"@orkestrel/abort": "^0.0.9",
|
|
91
|
+
"@orkestrel/budget": "^0.0.9",
|
|
92
|
+
"@orkestrel/contract": "^0.0.16",
|
|
93
|
+
"@orkestrel/timeout": "^0.0.9"
|
|
94
94
|
},
|
|
95
95
|
"devDependencies": {
|
|
96
96
|
"@microsoft/api-extractor": "^7.59.0",
|
|
97
|
-
"@orkestrel/database": "^0.0.
|
|
98
|
-
"@orkestrel/guide": "^0.0.
|
|
99
|
-
"@orkestrel/probe": "^0.0.
|
|
100
|
-
"@orkestrel/router": "^0.0.
|
|
101
|
-
"@orkestrel/scaffold": "^0.0.
|
|
102
|
-
"@orkestrel/server": "^0.0.
|
|
103
|
-
"@orkestrel/test": "^0.0.
|
|
104
|
-
"@types/node": "^26.4.
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"oxlint": "^1.80.0",
|
|
97
|
+
"@orkestrel/database": "^0.0.13",
|
|
98
|
+
"@orkestrel/guide": "^0.0.17",
|
|
99
|
+
"@orkestrel/probe": "^0.0.11",
|
|
100
|
+
"@orkestrel/router": "^0.0.13",
|
|
101
|
+
"@orkestrel/scaffold": "^0.0.62",
|
|
102
|
+
"@orkestrel/server": "^0.0.18",
|
|
103
|
+
"@orkestrel/test": "^0.0.13",
|
|
104
|
+
"@types/node": "^26.4.1",
|
|
105
|
+
"oxfmt": "^0.66.0",
|
|
106
|
+
"oxlint": "^1.81.0",
|
|
108
107
|
"typescript": "^6.0.3",
|
|
109
108
|
"vite": "^8.2.2",
|
|
110
|
-
"vite-plugin-dts": "^5.0
|
|
109
|
+
"vite-plugin-dts": "^5.1.0",
|
|
111
110
|
"vitest": "^4.1.11"
|
|
112
111
|
},
|
|
113
112
|
"peerDependencies": {
|
|
114
|
-
"@orkestrel/database": "^0.0.
|
|
115
|
-
"@orkestrel/server": "^0.0.
|
|
113
|
+
"@orkestrel/database": "^0.0.13",
|
|
114
|
+
"@orkestrel/server": "^0.0.18"
|
|
116
115
|
},
|
|
117
116
|
"peerDependenciesMeta": {
|
|
118
117
|
"@orkestrel/database": {
|