@orkestrel/middleware 0.0.4 → 0.0.6
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/dist/src/core/index.cjs +79 -65
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +40 -23
- package/dist/src/core/index.d.ts +40 -23
- package/dist/src/core/index.js +79 -66
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +340 -254
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +51 -0
- package/dist/src/server/index.d.ts +51 -0
- package/dist/src/server/index.js +338 -255
- package/dist/src/server/index.js.map +1 -1
- package/package.json +24 -22
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/constants.ts","../../../src/server/errors.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\tthis.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 {\n\tMultipartLimits,\n\tMultipartOptions,\n\tPartHeaders,\n\tUploadedFileInput,\n\tUploadedFileInterface,\n} from './types.js'\nimport type { FileHandle } from 'node:fs/promises'\nimport { createReadStream } from 'node:fs'\nimport { chmod, copyFile, mkdtemp, open, readFile, rename, unlink } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path'\nimport { randomUUID } from 'node:crypto'\nimport { isRecord } from '@orkestrel/contract'\nimport { isDangerousKey } from '@orkestrel/server'\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\tMULTIPART_MAX_HEADER_BLOCK,\n\tMULTIPART_MAX_PREAMBLE,\n\tRESERVED_DEVICE_NAMES,\n} from './constants.js'\nimport { MultipartError } from './errors.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 * 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 * 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\tfunction matches(signature: readonly number[], offset = 0): boolean {\n\t\tif (head.length < offset + signature.length) return false\n\t\tfor (let index = 0; index < signature.length; index += 1)\n\t\t\tif (head[offset + index] !== signature[index]) return false\n\t\treturn true\n\t}\n\tif (matches([0xff, 0xd8, 0xff])) return 'image/jpeg'\n\tif (matches([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return 'image/png'\n\tif (matches([0x47, 0x49, 0x46, 0x38, 0x37, 0x61])) return 'image/gif'\n\tif (matches([0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) return 'image/gif'\n\tif (matches([0x52, 0x49, 0x46, 0x46]) && matches([0x57, 0x45, 0x42, 0x50], 8)) return 'image/webp'\n\tif (matches([0x25, 0x50, 0x44, 0x46, 0x2d])) return 'application/pdf'\n\tif (\n\t\tmatches([0x50, 0x4b, 0x03, 0x04]) ||\n\t\tmatches([0x50, 0x4b, 0x05, 0x06]) ||\n\t\tmatches([0x50, 0x4b, 0x07, 0x08])\n\t)\n\t\treturn 'application/zip'\n\treturn undefined\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 * Memoized `Promise` for `parseMultipartRequest`'s lazily-created default\n * staging directory — created once per process via {@link resolveDefaultDirectory}.\n */\nlet defaultDirectory: Promise<string> | undefined\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\tif (defaultDirectory === undefined) {\n\t\tdefaultDirectory = (async () => {\n\t\t\tconst path = await mkdtemp(join(tmpdir(), 'orkestrel-multipart-'))\n\t\t\tawait chmod(path, 0o700)\n\t\t\treturn path\n\t\t})()\n\t}\n\treturn defaultDirectory\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\n\tconst staged: string[] = []\n\tconst files: Record<string, MultipartFile[]> = Object.create(null)\n\tconst fields: Record<string, string> = Object.create(null)\n\tlet fileCount = 0\n\tlet fieldCount = 0\n\tlet totalBytes = 0\n\tlet aborted = false\n\n\tasync function cleanup(): Promise<void> {\n\t\tfor (const path of 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\tconst reader = request.body.getReader()\n\tlet buffer = Buffer.alloc(0)\n\tlet ended = false\n\tconst onAbort = (): void => {\n\t\taborted = true\n\t}\n\trequest.signal.addEventListener('abort', onAbort)\n\n\tasync function pull(): Promise<boolean> {\n\t\tif (aborted || request.signal.aborted)\n\t\t\tthrow new MultipartError('malformed', 'request aborted mid-upload')\n\t\tif (ended) return false\n\t\tconst { done, value } = await reader.read()\n\t\tif (done) {\n\t\t\tended = true\n\t\t\treturn false\n\t\t}\n\t\ttotalBytes += value.byteLength\n\t\tif (totalBytes > limits.total)\n\t\t\tthrow new MultipartError('limit', 'multipart body exceeds total limit')\n\t\tbuffer = Buffer.concat([buffer, Buffer.from(value.buffer, value.byteOffset, value.byteLength)])\n\t\treturn true\n\t}\n\n\ttry {\n\t\tconst openMarker = Buffer.from(`--${boundary}`)\n\t\tlet preambleScanned = 0\n\t\tlet index = buffer.indexOf(openMarker)\n\t\twhile (index === -1) {\n\t\t\tconst carry = openMarker.length - 1\n\t\t\tif (buffer.length > carry) {\n\t\t\t\tconst drop = buffer.length - carry\n\t\t\t\tpreambleScanned += drop\n\t\t\t\tif (preambleScanned > MULTIPART_MAX_PREAMBLE)\n\t\t\t\t\tthrow new MultipartError('malformed', 'multipart preamble too large')\n\t\t\t\tbuffer = buffer.subarray(drop)\n\t\t\t}\n\t\t\tif (!(await pull())) throw new MultipartError('malformed', 'missing multipart boundary')\n\t\t\tindex = buffer.indexOf(openMarker)\n\t\t}\n\t\tbuffer = buffer.subarray(index + openMarker.length)\n\n\t\tfor (;;) {\n\t\t\twhile (buffer.length < 2)\n\t\t\t\tif (!(await pull()))\n\t\t\t\t\tthrow new MultipartError('malformed', 'unterminated multipart boundary')\n\t\t\tif (buffer[0] === 0x2d && buffer[1] === 0x2d) break\n\t\t\tif (buffer[0] !== 0x0d || buffer[1] !== 0x0a)\n\t\t\t\tthrow new MultipartError('malformed', 'malformed multipart boundary')\n\t\t\tbuffer = buffer.subarray(2)\n\n\t\t\tlet headerEnd = buffer.indexOf('\\r\\n\\r\\n')\n\t\t\twhile (headerEnd === -1) {\n\t\t\t\tif (buffer.length > MULTIPART_MAX_HEADER_BLOCK)\n\t\t\t\t\tthrow new MultipartError('malformed', 'multipart header block too large')\n\t\t\t\tif (!(await pull()))\n\t\t\t\t\tthrow new MultipartError('malformed', 'unterminated multipart part headers')\n\t\t\t\theaderEnd = buffer.indexOf('\\r\\n\\r\\n')\n\t\t\t}\n\t\t\tconst headerBlock = buffer.subarray(0, headerEnd).toString('utf8')\n\t\t\tbuffer = buffer.subarray(headerEnd + 4)\n\t\t\tconst { name, filename, contentType } = parsePartHeaders(headerBlock)\n\t\t\tif (name === undefined) throw new MultipartError('malformed', 'multipart part missing name')\n\n\t\t\tconst partDelimiter = Buffer.from(`\\r\\n--${boundary}`)\n\n\t\t\tif (filename !== undefined) {\n\t\t\t\tif (filename !== '') {\n\t\t\t\t\tfileCount += 1\n\t\t\t\t\tif (fileCount > limits.files)\n\t\t\t\t\t\tthrow new MultipartError('limit', 'too many multipart files')\n\t\t\t\t}\n\t\t\t\tconst path = join(directory, randomUUID())\n\t\t\t\tstaged.push(path)\n\t\t\t\tconst handle = await open(path, 'w', 0o600)\n\t\t\t\tlet size = 0\n\t\t\t\tlet head = Buffer.alloc(0)\n\t\t\t\ttry {\n\t\t\t\t\tfor (;;) {\n\t\t\t\t\t\tconst boundaryIndex = buffer.indexOf(partDelimiter)\n\t\t\t\t\t\tif (boundaryIndex === -1) {\n\t\t\t\t\t\t\tconst safeLength = Math.max(0, buffer.length - (partDelimiter.length - 1))\n\t\t\t\t\t\t\tif (safeLength > 0) {\n\t\t\t\t\t\t\t\tconst chunk = buffer.subarray(0, safeLength)\n\t\t\t\t\t\t\t\tsize += chunk.length\n\t\t\t\t\t\t\t\tif (size > limits.file)\n\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\tif (head.length < 16)\n\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\tawait handle.write(chunk)\n\t\t\t\t\t\t\t\tbuffer = buffer.subarray(safeLength)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!(await pull()))\n\t\t\t\t\t\t\t\tthrow new MultipartError('malformed', 'unterminated multipart file part')\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst chunk = buffer.subarray(0, boundaryIndex)\n\t\t\t\t\t\tsize += chunk.length\n\t\t\t\t\t\tif (size > limits.file)\n\t\t\t\t\t\t\tthrow new MultipartError('limit', 'multipart file exceeds size limit')\n\t\t\t\t\t\tif (head.length < 16) head = Buffer.concat([head, chunk.subarray(0, 16 - head.length)])\n\t\t\t\t\t\tawait handle.write(chunk)\n\t\t\t\t\t\tbuffer = buffer.subarray(boundaryIndex + 2)\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t} finally {\n\t\t\t\t\tawait handle.close()\n\t\t\t\t}\n\t\t\t\tif (filename === '' && size === 0) {\n\t\t\t\t\t// Browser empty-optional-file-input convention: an\n\t\t\t\t\t// unselected <input type=\"file\"> still submits a part\n\t\t\t\t\t// with an empty filename and zero-byte body — a no-op,\n\t\t\t\t\t// never counted against the files limit or the allow-list.\n\t\t\t\t\tawait unlink(path)\n\t\t\t\t\tstaged.splice(staged.indexOf(path), 1)\n\t\t\t\t} else {\n\t\t\t\t\tif (filename === '') {\n\t\t\t\t\t\tfileCount += 1\n\t\t\t\t\t\tif (fileCount > 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 detected = detectMIME(head)\n\t\t\t\t\tconst declared = contentType ?? DEFAULT_CONTENT_TYPE\n\t\t\t\t\tconst validated = detected !== undefined && detected === declared\n\t\t\t\t\tif (allowed !== undefined) {\n\t\t\t\t\t\tconst acceptable = detected !== undefined && allowed.includes(detected)\n\t\t\t\t\t\tif (!acceptable)\n\t\t\t\t\t\t\tthrow new MultipartError('rejected', 'multipart file failed type validation')\n\t\t\t\t\t}\n\t\t\t\t\tif (isDangerousKey(name)) {\n\t\t\t\t\t\tawait unlink(path)\n\t\t\t\t\t\tstaged.splice(staged.indexOf(path), 1)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst record = createUploadedFile({\n\t\t\t\t\t\t\tfield: name,\n\t\t\t\t\t\t\tname: filename,\n\t\t\t\t\t\t\tsize,\n\t\t\t\t\t\t\tmime: detected ?? declared,\n\t\t\t\t\t\t\tvalidated,\n\t\t\t\t\t\t\tstatus: 'staged',\n\t\t\t\t\t\t\tpath,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tconst existing = files[name]\n\t\t\t\t\t\tif (existing === undefined) files[name] = [record]\n\t\t\t\t\t\telse existing.push(record)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfieldCount += 1\n\t\t\t\tif (fieldCount > limits.fields)\n\t\t\t\t\tthrow new MultipartError('limit', 'too many multipart fields')\n\t\t\t\tlet value = Buffer.alloc(0)\n\t\t\t\tfor (;;) {\n\t\t\t\t\tconst boundaryIndex = buffer.indexOf(partDelimiter)\n\t\t\t\t\tif (boundaryIndex === -1) {\n\t\t\t\t\t\tconst safeLength = Math.max(0, buffer.length - (partDelimiter.length - 1))\n\t\t\t\t\t\tif (safeLength > 0) {\n\t\t\t\t\t\t\tvalue = Buffer.concat([value, buffer.subarray(0, safeLength)])\n\t\t\t\t\t\t\tif (value.length > limits.field)\n\t\t\t\t\t\t\t\tthrow new MultipartError('limit', 'multipart field exceeds size limit')\n\t\t\t\t\t\t\tbuffer = buffer.subarray(safeLength)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!(await pull()))\n\t\t\t\t\t\t\tthrow new MultipartError('malformed', 'unterminated multipart field part')\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tvalue = Buffer.concat([value, buffer.subarray(0, boundaryIndex)])\n\t\t\t\t\tif (value.length > limits.field)\n\t\t\t\t\t\tthrow new MultipartError('limit', 'multipart field exceeds size limit')\n\t\t\t\t\tbuffer = buffer.subarray(boundaryIndex + 2)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif (!isDangerousKey(name)) fields[name] = value.toString('utf8')\n\t\t\t}\n\n\t\t\twhile (buffer.length < openMarker.length)\n\t\t\t\tif (!(await pull()))\n\t\t\t\t\tthrow new MultipartError('malformed', 'unterminated multipart boundary')\n\t\t\tbuffer = buffer.subarray(openMarker.length)\n\t\t}\n\t} catch (error) {\n\t\tawait cleanup()\n\t\tawait reader.cancel().catch(() => {})\n\t\tthrow error\n\t} finally {\n\t\trequest.signal.removeEventListener('abort', onAbort)\n\t\tif (!ended) await reader.cancel().catch(() => {})\n\t}\n\n\treturn { files: Object.freeze(files), fields: Object.freeze(fields) }\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 { MultipartOptions, NodeCompressionOptions, StaticOptions } from './types.js'\nimport type { FileHandle } from 'node:fs/promises'\nimport { DEFAULT_COMPRESSION_THRESHOLD, compressResponse } from '@src/core'\nimport { open, realpath, stat } from 'node:fs/promises'\nimport { extname, join, relative, resolve } from 'node:path'\nimport { deflate as zlibDeflate, gzip as zlibGzip } from 'node:zlib'\nimport { promisify } from 'node:util'\nimport type { Encoding, MiddlewareHandler } from '@orkestrel/server'\nimport { HTTPError, matchesETag, parseRange } from '@orkestrel/server'\nimport { isFiniteNumber, isFunction, isString } from '@orkestrel/contract'\nimport { DEFAULT_STATIC_FALLBACK_EXCLUDE, DEFAULT_STATIC_INDEX } from './constants.js'\nimport { isMultipartError } from './errors.js'\nimport {\n\tcomputeFileETag,\n\tisContainedPath,\n\tisDotfilePath,\n\tisUnderPath,\n\tlookupContentType,\n\tparseMultipartRequest,\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). createStatic (node:fs static file serving) and\n// createMultipart (node:fs/os/crypto streaming multipart uploads) — the two\n// genuinely node-bound batteries — plus 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 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\tfunction canonicalRoot(): Promise<string> {\n\t\tif (canonicalRootPromise === undefined) canonicalRootPromise = realpath(root)\n\t\treturn canonicalRootPromise\n\t}\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: string\n\t\ttry {\n\t\t\tconst [rootReal, targetReal] = await Promise.all([canonicalRoot(), realpath(target)])\n\t\t\tif (!isContainedPath(targetReal, rootReal)) return next()\n\t\t\tresolvedPath = targetReal\n\t\t} catch {\n\t\t\treturn trySpaFallback()\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>>\n\t\ttry {\n\t\t\tdirectoryInfo = await stat(resolvedPath)\n\t\t} catch {\n\t\t\treturn trySpaFallback()\n\t\t}\n\n\t\tif (directoryInfo.isDirectory()) {\n\t\t\tresolvedPath = join(resolvedPath, index)\n\t\t\ttry {\n\t\t\t\tconst [rootReal, indexReal] = await Promise.all([canonicalRoot(), realpath(resolvedPath)])\n\t\t\t\tif (!isContainedPath(indexReal, rootReal)) return trySpaFallback()\n\t\t\t\tresolvedPath = indexReal\n\t\t\t} catch {\n\t\t\t\treturn trySpaFallback()\n\t\t\t}\n\t\t}\n\n\t\tlet handle: FileHandle\n\t\ttry {\n\t\t\thandle = await open(resolvedPath, 'r')\n\t\t} catch {\n\t\t\treturn trySpaFallback()\n\t\t}\n\t\tlet info: Awaited<ReturnType<FileHandle['stat']>>\n\t\ttry {\n\t\t\tinfo = await handle.stat()\n\t\t} catch {\n\t\t\tawait handle.close()\n\t\t\treturn trySpaFallback()\n\t\t}\n\t\tif (!info.isFile()) {\n\t\t\tawait handle.close()\n\t\t\treturn trySpaFallback()\n\t\t}\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\n\t\tfunction trySpaFallback(): Response | Promise<Response> {\n\t\t\tif (fallback === undefined) return next()\n\t\t\tif (context.method !== 'GET') return next()\n\t\t\tif (extname(context.url.pathname) !== '') return next()\n\t\t\tconst accept = request.headers.get('accept') ?? ''\n\t\t\tif (!accept.includes('text/html') && !accept.includes('*/*')) return next()\n\t\t\tif (isUnderPath(context.url.pathname, fallback.exclude)) return next()\n\t\t\tconst shellPath = join(root, index)\n\t\t\treturn Promise.all([canonicalRoot(), realpath(shellPath)])\n\t\t\t\t.then(([rootReal, shellReal]) => {\n\t\t\t\t\tif (!isContainedPath(shellReal, rootReal)) return next()\n\t\t\t\t\treturn open(shellReal, 'r').then((shellHandle) => {\n\t\t\t\t\t\tlet shellStreaming = false\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst body = streamFile(shellHandle)\n\t\t\t\t\t\t\tshellStreaming = true\n\t\t\t\t\t\t\treturn new Response(body, {\n\t\t\t\t\t\t\t\tstatus: 200,\n\t\t\t\t\t\t\t\theaders: new Headers({ 'content-type': lookupContentType(shellReal) }),\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tif (!shellStreaming) shellHandle.close().catch(() => {})\n\t\t\t\t\t\t\tthrow error\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t\t.catch(() => next())\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\tcontext.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\tconst gzipAsync = promisify(zlibGzip)\n\tconst deflateAsync = promisify(zlibDeflate)\n\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\tfilter,\n\t\t\tencodings,\n\t\t\tcompress: async (bytes, encoding) =>\n\t\t\t\tencoding === 'gzip' ? gzipAsync(bytes) : deflateAsync(bytes),\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","#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 { MultipartOptions, NodeCompressionOptions, StaticOptions } 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 type { Encoding, MiddlewareHandler } from '@orkestrel/server'\nimport { HTTPError, matchesETag, parseRange } from '@orkestrel/server'\nimport { isFiniteNumber, isFunction, isString } 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). createStatic (node:fs static file serving) and\n// createMultipart (node:fs/os/crypto streaming multipart uploads) — the two\n// genuinely node-bound batteries — plus 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 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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orkestrel/middleware",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
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",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"url": "git+https://github.com/orkestrel/middleware.git"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
|
-
"dist",
|
|
26
|
+
"dist/src",
|
|
27
27
|
"README.md"
|
|
28
28
|
],
|
|
29
29
|
"type": "module",
|
|
@@ -57,21 +57,22 @@
|
|
|
57
57
|
"access": "public"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
|
-
"clean": "node -e \"
|
|
60
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
61
61
|
"copy": "node -e \"const fs=require('node:fs'),p=require('node:path'),a=process.argv[1],b=process.argv[2];fs.mkdirSync(p.dirname(b),{recursive:true});fs.cpSync(a,b,{force:true});console.log('Copied: '+a+' to '+b)\"",
|
|
62
|
-
"
|
|
63
|
-
"lint": "oxlint --config .oxlintrc.json --fix .",
|
|
62
|
+
"scaffold": "scaffold",
|
|
63
|
+
"lint": "oxlint --config .oxlintrc.json --fix --deny-warnings .",
|
|
64
64
|
"check": "tsc --noEmit --project tsconfig.json && npm run check:src",
|
|
65
65
|
"check:src": "npm run check:src:core && npm run check:src:server",
|
|
66
66
|
"check:src:core": "tsc --noEmit -p configs/src/tsconfig.core.json",
|
|
67
67
|
"check:src:server": "tsc --noEmit -p configs/src/tsconfig.server.json",
|
|
68
68
|
"format": "oxfmt --config .oxfmtrc.json --write .",
|
|
69
69
|
"format:check": "oxfmt --config .oxfmtrc.json --check .",
|
|
70
|
-
"lint:check": "oxlint --config .oxlintrc.json .",
|
|
71
|
-
"test": "npm run test:src && npm run test:guides",
|
|
70
|
+
"lint:check": "oxlint --config .oxlintrc.json --deny-warnings .",
|
|
71
|
+
"test": "npm run test:src && npm run test:policy && npm run test:guides",
|
|
72
72
|
"test:src": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core --project src:server",
|
|
73
73
|
"test:src:core": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core",
|
|
74
74
|
"test:src:server": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:server",
|
|
75
|
+
"test:policy": "vitest run --config vite.config.ts --no-cache --reporter=dot --project policy",
|
|
75
76
|
"test:guides": "vitest run --config vite.config.ts --reporter=dot --project guides",
|
|
76
77
|
"build": "npm run clean && npm run build:src",
|
|
77
78
|
"build:src": "npm run build:src:core && npm run build:src:server",
|
|
@@ -80,29 +81,30 @@
|
|
|
80
81
|
"prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test"
|
|
81
82
|
},
|
|
82
83
|
"dependencies": {
|
|
83
|
-
"@orkestrel/abort": "^0.0.
|
|
84
|
-
"@orkestrel/budget": "^0.0.
|
|
85
|
-
"@orkestrel/contract": "^0.0.
|
|
86
|
-
"@orkestrel/timeout": "^0.0.
|
|
84
|
+
"@orkestrel/abort": "^0.0.4",
|
|
85
|
+
"@orkestrel/budget": "^0.0.4",
|
|
86
|
+
"@orkestrel/contract": "^0.0.8",
|
|
87
|
+
"@orkestrel/timeout": "^0.0.4"
|
|
87
88
|
},
|
|
88
89
|
"devDependencies": {
|
|
89
|
-
"@microsoft/api-extractor": "^7.58.
|
|
90
|
-
"@orkestrel/database": "^0.0.
|
|
91
|
-
"@orkestrel/guide": "^0.0.
|
|
92
|
-
"@orkestrel/router": "^0.0.
|
|
93
|
-
"@orkestrel/
|
|
94
|
-
"@
|
|
90
|
+
"@microsoft/api-extractor": "^7.58.12",
|
|
91
|
+
"@orkestrel/database": "^0.0.6",
|
|
92
|
+
"@orkestrel/guide": "^0.0.7",
|
|
93
|
+
"@orkestrel/router": "^0.0.6",
|
|
94
|
+
"@orkestrel/scaffold": "^0.0.7",
|
|
95
|
+
"@orkestrel/server": "^0.0.7",
|
|
96
|
+
"@types/node": "^26.1.2",
|
|
95
97
|
"@vitest/browser-playwright": "^4.1.10",
|
|
96
|
-
"oxfmt": "^0.
|
|
97
|
-
"oxlint": "^1.
|
|
98
|
+
"oxfmt": "^0.61.0",
|
|
99
|
+
"oxlint": "^1.76.0",
|
|
98
100
|
"typescript": "^6.0.3",
|
|
99
101
|
"vite": "^8.1.5",
|
|
100
102
|
"vite-plugin-dts": "^5.0.3",
|
|
101
103
|
"vitest": "^4.1.10"
|
|
102
104
|
},
|
|
103
105
|
"peerDependencies": {
|
|
104
|
-
"@orkestrel/database": "^0.0.
|
|
105
|
-
"@orkestrel/server": "^0.0.
|
|
106
|
+
"@orkestrel/database": "^0.0.6",
|
|
107
|
+
"@orkestrel/server": "^0.0.7"
|
|
106
108
|
},
|
|
107
109
|
"peerDependenciesMeta": {
|
|
108
110
|
"@orkestrel/database": {
|
|
@@ -110,6 +112,6 @@
|
|
|
110
112
|
}
|
|
111
113
|
},
|
|
112
114
|
"engines": {
|
|
113
|
-
"node": ">=22"
|
|
115
|
+
"node": ">=22.12.0"
|
|
114
116
|
}
|
|
115
117
|
}
|