@orkestrel/middleware 0.0.19 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -11
- package/dist/src/core/index.cjs +78 -60
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +150 -81
- package/dist/src/core/index.d.ts +150 -81
- package/dist/src/core/index.js +78 -60
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +43 -41
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +62 -60
- package/dist/src/server/index.d.ts +62 -60
- package/dist/src/server/index.js +43 -41
- package/dist/src/server/index.js.map +1 -1
- package/package.json +21 -22
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Encoding } from '@orkestrel/server';
|
|
2
|
-
import { FileHandle } from 'node:fs/promises';
|
|
1
|
+
import type { Encoding } from '@orkestrel/server';
|
|
2
|
+
import type { FileHandle } from 'node:fs/promises';
|
|
3
3
|
import { HTTPError } from '@orkestrel/server';
|
|
4
|
-
import { MiddlewareHandler } from '@orkestrel/server';
|
|
5
|
-
import { MultipartBody } from '@orkestrel/middleware';
|
|
6
|
-
import { MultipartFile } from '@orkestrel/middleware';
|
|
7
|
-
import { MultipartState } from '@orkestrel/middleware';
|
|
4
|
+
import type { MiddlewareHandler } from '@orkestrel/server';
|
|
5
|
+
import type { MultipartBody } from '@orkestrel/middleware';
|
|
6
|
+
import type { MultipartFile } from '@orkestrel/middleware';
|
|
7
|
+
import type { MultipartState } from '@orkestrel/middleware';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Describes one in-memory asset representation returned by an {@link AssetSourceInterface}.
|
|
@@ -23,7 +23,7 @@ export declare interface Asset {
|
|
|
23
23
|
* Configures `createAssets` — in-memory identity/Brotli asset serving.
|
|
24
24
|
*
|
|
25
25
|
* @remarks
|
|
26
|
-
* - `source` — the required in-memory asset reader. It
|
|
26
|
+
* - `source` — the required in-memory asset reader. It must answer a bounded
|
|
27
27
|
* key set and return `undefined` for every key outside it, because
|
|
28
28
|
* `createAssets` retains every successful result for the factory's lifetime
|
|
29
29
|
* and evicts nothing. A `source` that synthesizes a representation for an
|
|
@@ -40,12 +40,12 @@ export declare interface AssetOptions {
|
|
|
40
40
|
* @remarks
|
|
41
41
|
* A successful result is cached by `createAssets`; later source changes do
|
|
42
42
|
* not alter that path's response. A miss may be read again on a later request.
|
|
43
|
-
* `read` therefore owes a
|
|
43
|
+
* `read` therefore owes a bounded key set: that cache retains every
|
|
44
44
|
* successful result for the factory's lifetime and evicts nothing.
|
|
45
45
|
*/
|
|
46
46
|
export declare interface AssetSourceInterface {
|
|
47
47
|
/**
|
|
48
|
-
* Reads one asset representation.
|
|
48
|
+
* Reads one identity or Brotli asset representation for a validated relative key.
|
|
49
49
|
*
|
|
50
50
|
* @param path - The validated relative asset path
|
|
51
51
|
* @returns The asset, or `undefined` when the path is absent
|
|
@@ -136,12 +136,11 @@ export declare function computeFileETag(size: number, mtimeMs: number): string;
|
|
|
136
136
|
export declare function createAssets<TState>(options: AssetOptions): MiddlewareHandler<TState>;
|
|
137
137
|
|
|
138
138
|
/**
|
|
139
|
-
* Compresses response bodies through `node:zlib
|
|
140
|
-
* the
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* so the shared name is unambiguous per consumer import path.
|
|
139
|
+
* Compresses response bodies through `node:zlib`, guaranteed on any Node runtime rather
|
|
140
|
+
* than dependent on the WHATWG `CompressionStream` global. This battery is the
|
|
141
|
+
* node-bound sibling of the core face's feature-detected `createCompression`, and it
|
|
142
|
+
* ships from a separate package entry point (`@orkestrel/middleware/server`) so the
|
|
143
|
+
* shared name is unambiguous per consumer import path.
|
|
145
144
|
*
|
|
146
145
|
* @remarks
|
|
147
146
|
* Peer-type limitation, the same one the core face carries: the shipped
|
|
@@ -178,7 +177,7 @@ export declare function createCompression<TState>(options?: NodeCompressionOptio
|
|
|
178
177
|
* {@link MultipartError} this battery's parser throws is re-thrown as an
|
|
179
178
|
* {@link HTTPError} carrying the same status/message, so `createBoundary`
|
|
180
179
|
* (or any HTTPError-aware renderer) maps it correctly without depending on
|
|
181
|
-
* this node face's error type. Fail-closed on the
|
|
180
|
+
* this node face's error type. Fail-closed on the downstream handler too: if
|
|
182
181
|
* `next()` throws, every still-`'staged'` uploaded file is unlinked
|
|
183
182
|
* (best-effort) before the error is re-thrown, so an unhandled downstream
|
|
184
183
|
* failure never leaks temp files. A normal return leaves staged files
|
|
@@ -200,16 +199,16 @@ export declare function createCompression<TState>(options?: NodeCompressionOptio
|
|
|
200
199
|
export declare function createMultipart<TState extends MultipartState>(options?: MultipartOptions): MiddlewareHandler<TState>;
|
|
201
200
|
|
|
202
201
|
/**
|
|
203
|
-
* Serves static files from `options.root` over `node:fs` — the node-bound
|
|
204
|
-
*
|
|
202
|
+
* Serves static files from `options.root` over `node:fs` — the node-bound static-file
|
|
203
|
+
* battery, answering conditional, ranged, and SPA-fallback requests.
|
|
205
204
|
*
|
|
206
205
|
* @remarks
|
|
207
|
-
* Containment is enforced on
|
|
206
|
+
* Containment is enforced on canonical paths, not merely the lexically
|
|
208
207
|
* resolved one: `options.root` is canonicalized once (memoized) and every
|
|
209
208
|
* request's candidate path is re-canonicalized (`fs.realpath`) before it is
|
|
210
209
|
* served, so a symlink whose target escapes `root` is refused (falls through
|
|
211
210
|
* to `next()`) even though the lexical path resolved inside `root`. A
|
|
212
|
-
* symlink that resolves to a target still
|
|
211
|
+
* symlink that resolves to a target still inside `root` is unaffected and
|
|
213
212
|
* still serves normally. A dangling symlink (`realpath` throws `ENOENT`) or
|
|
214
213
|
* any other `realpath` failure is treated as a miss — this battery never
|
|
215
214
|
* throws or 500s on a symlink surprise. On a streamed response (a 200 or 206
|
|
@@ -246,31 +245,31 @@ export declare function createStatic<TState>(options: StaticOptions): Middleware
|
|
|
246
245
|
*/
|
|
247
246
|
export declare function createUploadedFile(input: UploadedFile): UploadedFile;
|
|
248
247
|
|
|
249
|
-
/** Names the MIME type served when a file extension has no known mapping. */
|
|
248
|
+
/** Names `'application/octet-stream'`, the MIME type served when a file extension has no known mapping. */
|
|
250
249
|
export declare const DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
251
250
|
|
|
252
|
-
/** Holds `createMultipart`'s default maximum field-part count. */
|
|
251
|
+
/** Holds `100`, `createMultipart`'s default maximum field-part count. */
|
|
253
252
|
export declare const DEFAULT_MULTIPART_FIELD_COUNT = 100;
|
|
254
253
|
|
|
255
|
-
/** Holds `createMultipart`'s default per-field byte-size cap. */
|
|
254
|
+
/** Holds `65_536`, `createMultipart`'s default per-field byte-size cap. */
|
|
256
255
|
export declare const DEFAULT_MULTIPART_FIELD_SIZE = 65536;
|
|
257
256
|
|
|
258
|
-
/** Holds `createMultipart`'s default maximum file-part count. */
|
|
257
|
+
/** Holds `10`, `createMultipart`'s default maximum file-part count. */
|
|
259
258
|
export declare const DEFAULT_MULTIPART_FILE_COUNT = 10;
|
|
260
259
|
|
|
261
|
-
/** Holds `createMultipart`'s default per-file byte-size cap. */
|
|
260
|
+
/** Holds `10_485_760`, `createMultipart`'s default per-file byte-size cap. */
|
|
262
261
|
export declare const DEFAULT_MULTIPART_FILE_SIZE = 10485760;
|
|
263
262
|
|
|
264
|
-
/** Holds `createMultipart`'s default combined request-body byte-size cap. */
|
|
263
|
+
/** Holds `52_428_800`, `createMultipart`'s default combined request-body byte-size cap. */
|
|
265
264
|
export declare const DEFAULT_MULTIPART_TOTAL = 52428800;
|
|
266
265
|
|
|
267
|
-
/** Names `createStatic`'s default policy for a path carrying a dotfile segment. */
|
|
266
|
+
/** Names `'ignore'`, `createStatic`'s default policy for a path carrying a dotfile segment. */
|
|
268
267
|
export declare const DEFAULT_STATIC_DOTFILES: NonNullable<StaticOptions['dotfiles']>;
|
|
269
268
|
|
|
270
|
-
/** Names `createStatic`'s `fallback: true` default excluded path prefix. */
|
|
269
|
+
/** Names `'/api'`, `createStatic`'s `fallback: true` default excluded path prefix. */
|
|
271
270
|
export declare const DEFAULT_STATIC_FALLBACK_EXCLUDE = "/api";
|
|
272
271
|
|
|
273
|
-
/** Names `createStatic`'s default directory-index filename. */
|
|
272
|
+
/** Names `'index.html'`, `createStatic`'s default directory-index filename. */
|
|
274
273
|
export declare const DEFAULT_STATIC_INDEX = "index.html";
|
|
275
274
|
|
|
276
275
|
/**
|
|
@@ -316,11 +315,11 @@ export declare function extractMultipartBoundary(contentType: string | null): st
|
|
|
316
315
|
|
|
317
316
|
/**
|
|
318
317
|
* Checks whether `child` is `parent` itself or lies inside it on-disk — the
|
|
319
|
-
*
|
|
318
|
+
* filesystem containment predicate `createStatic` applies to `fs.realpath`
|
|
320
319
|
* output (never to a URL pathname — that is {@link isUnderPath}'s job).
|
|
321
320
|
*
|
|
322
321
|
* @remarks
|
|
323
|
-
* Argument order is `(child, parent)` — deliberately the
|
|
322
|
+
* Argument order is `(child, parent)` — deliberately the opposite conceptual
|
|
324
323
|
* order from {@link isUnderPath}`(pathname, prefix)`, so a call site cannot
|
|
325
324
|
* casually swap one predicate in for the other. Built on `path.relative`,
|
|
326
325
|
* this is separator-correct on both POSIX (`/`) and win32 (`\`) — unlike a
|
|
@@ -384,7 +383,7 @@ export declare function isMultipartError(value: unknown): value is MultipartErro
|
|
|
384
383
|
*
|
|
385
384
|
* @remarks
|
|
386
385
|
* Normalizes superscript digits (`¹²³` → `123`) first, strips trailing dots
|
|
387
|
-
* and spaces (Windows drops them), takes the
|
|
386
|
+
* and spaces (Windows drops them), takes the stem before the first `.`,
|
|
388
387
|
* upper-cases it, and tests it against {@link RESERVED_DEVICE_NAMES}
|
|
389
388
|
* (`CON PRN AUX NUL COM1-9 LPT1-9`) — an exact-stem match only, so
|
|
390
389
|
* `console.js` and `nullable.css` are never flagged.
|
|
@@ -402,7 +401,7 @@ export declare function isMultipartError(value: unknown): value is MultipartErro
|
|
|
402
401
|
export declare function isReservedDeviceName(segment: string): boolean;
|
|
403
402
|
|
|
404
403
|
/**
|
|
405
|
-
* Checks whether `pathname` is `prefix` itself or lies under it on a
|
|
404
|
+
* Checks whether `pathname` is `prefix` itself or lies under it on a segment
|
|
406
405
|
* boundary — the shared under-path test `resolveStaticPath`'s prefix strip
|
|
407
406
|
* and `createStatic`'s SPA-fallback `exclude` both apply, so `exclude:
|
|
408
407
|
* '/api'` matches `/api` and `/api/x` but never `/apifoo`.
|
|
@@ -476,13 +475,16 @@ export declare function moveUploadedFile(file: UploadedFile, destination: string
|
|
|
476
475
|
*/
|
|
477
476
|
export declare const MULTIPART_ERROR_BRAND: unique symbol;
|
|
478
477
|
|
|
479
|
-
/** Holds the maximum bytes a single multipart part's header block may occupy before it is malformed. */
|
|
478
|
+
/** Holds `16_384`, the maximum bytes a single multipart part's header block may occupy before it is malformed. */
|
|
480
479
|
export declare const MULTIPART_MAX_HEADER_BLOCK = 16384;
|
|
481
480
|
|
|
482
|
-
/** Holds the maximum bytes scanned before the first multipart boundary is found before it is malformed. */
|
|
481
|
+
/** Holds `65_536`, the maximum bytes scanned before the first multipart boundary is found before it is malformed. */
|
|
483
482
|
export declare const MULTIPART_MAX_PREAMBLE = 65536;
|
|
484
483
|
|
|
485
|
-
/**
|
|
484
|
+
/**
|
|
485
|
+
* Holds the HTTP status `createMultipart` renders for each {@link MultipartErrorCode}:
|
|
486
|
+
* `'limit'` is 413, `'malformed'` is 400, and `'rejected'` is 415.
|
|
487
|
+
*/
|
|
486
488
|
export declare const MULTIPART_STATUS: Readonly<Record<MultipartErrorCode, number>>;
|
|
487
489
|
|
|
488
490
|
/**
|
|
@@ -520,7 +522,7 @@ export declare class MultipartError extends HTTPError {
|
|
|
520
522
|
export declare type MultipartErrorCode = 'limit' | 'malformed' | 'rejected';
|
|
521
523
|
|
|
522
524
|
/**
|
|
523
|
-
* Describes the per-category size/count caps `createMultipart` enforces
|
|
525
|
+
* Describes the per-category size/count caps `createMultipart` enforces mid-stream — the
|
|
524
526
|
* effective limits, every documented default already applied.
|
|
525
527
|
*
|
|
526
528
|
* @remarks
|
|
@@ -576,7 +578,7 @@ export declare interface MultipartLimitsInput {
|
|
|
576
578
|
*
|
|
577
579
|
* @remarks
|
|
578
580
|
* - `limits` — see {@link MultipartLimitsInput}.
|
|
579
|
-
* - `allowed` — a MIME allow-list validated against
|
|
581
|
+
* - `allowed` — a MIME allow-list validated against sniffed (not merely
|
|
580
582
|
* declared) bytes; an empty array allows nothing. Omitted ⇒ no type
|
|
581
583
|
* rejection.
|
|
582
584
|
* - `directory` — the directory staged files are written to; defaults to
|
|
@@ -589,8 +591,8 @@ export declare interface MultipartOptions {
|
|
|
589
591
|
}
|
|
590
592
|
|
|
591
593
|
/**
|
|
592
|
-
* Lists the content-codings the node face's `createCompression`
|
|
593
|
-
* `node:zlib` guarantees on every Node runtime, so this face never
|
|
594
|
+
* Lists `['gzip', 'deflate']`, the content-codings the node face's `createCompression`
|
|
595
|
+
* offers — what `node:zlib` guarantees on every Node runtime, so this face never
|
|
594
596
|
* feature-detects.
|
|
595
597
|
*/
|
|
596
598
|
export declare const NODE_COMPRESSION_ENCODINGS: readonly Encoding[];
|
|
@@ -619,30 +621,30 @@ export declare interface NodeCompressionOptions {
|
|
|
619
621
|
*
|
|
620
622
|
* @remarks
|
|
621
623
|
* Reads `request.body` chunk by chunk through its `ReadableStream` reader —
|
|
622
|
-
*
|
|
624
|
+
* never buffers the whole body — enforcing every {@link MultipartLimits} cap
|
|
623
625
|
* the instant it is exceeded (reading stops, every already-staged temp file
|
|
624
626
|
* is deleted, throws {@link MultipartError} with code `'limit'`). Each file
|
|
625
627
|
* part streams to `join(directory, randomUUID())` — the client's declared
|
|
626
|
-
* filename is
|
|
628
|
+
* filename is metadata only, never a path component. A field or file part
|
|
627
629
|
* named `__proto__` / `constructor` / `prototype` is silently skipped and
|
|
628
630
|
* never keyed onto the returned {@link MultipartBody} (a skipped file's
|
|
629
631
|
* staged temp file is unlinked immediately, since it can never be
|
|
630
632
|
* referenced). A file part with an empty declared filename (`filename=""`)
|
|
631
|
-
*
|
|
633
|
+
* and a zero-byte body — the browser convention for an unselected optional
|
|
632
634
|
* `<input type="file">` — is a silent no-op: its temp file is unlinked, it is
|
|
633
635
|
* never counted against the `file.count` limit, and it never runs the
|
|
634
636
|
* `allowed` check. A malformed
|
|
635
637
|
* structure (missing/unterminated boundary, nameless part, an oversized
|
|
636
638
|
* header block, or a preamble exceeding {@link MULTIPART_MAX_PREAMBLE} before
|
|
637
639
|
* the first boundary) throws with code `'malformed'`. A file is accepted
|
|
638
|
-
* against the configured `allowed` MIME list
|
|
640
|
+
* against the configured `allowed` MIME list exactly when its sniffed bytes detect a
|
|
639
641
|
* type present in the list — sniff-authoritative, independent of whether the
|
|
640
642
|
* declared `Content-Type` matches (that agreement is exposed separately as
|
|
641
643
|
* `validated`); otherwise throws with code `'rejected'`. A
|
|
642
644
|
* request abort mid-upload triggers the same fail-closed cleanup as a limit
|
|
643
645
|
* breach. Returns `undefined` for a non-multipart request (untouched).
|
|
644
646
|
*
|
|
645
|
-
* Staging defaults to a process-owned directory created
|
|
647
|
+
* Staging defaults to a process-owned directory created once (lazily,
|
|
646
648
|
* memoized across calls) with `mkdtemp` under `os.tmpdir()` and locked to
|
|
647
649
|
* mode `0o700`; `options.directory` overrides it.
|
|
648
650
|
*
|
|
@@ -753,7 +755,7 @@ export declare function resolveMultipartLimits(limits: MultipartLimitsInput | un
|
|
|
753
755
|
* fallback.
|
|
754
756
|
*
|
|
755
757
|
* @remarks
|
|
756
|
-
* `GET` and `HEAD` are both eligible, and resolve the
|
|
758
|
+
* `GET` and `HEAD` are both eligible, and resolve the same shell: `HEAD` is
|
|
757
759
|
* defined as `GET` without a body (RFC 9110 §9.3.2), so a navigation probe
|
|
758
760
|
* that answered `404` while its `GET` answered `200` would report a resource
|
|
759
761
|
* the very next request serves.
|
|
@@ -775,14 +777,14 @@ export declare function resolveMultipartLimits(limits: MultipartLimitsInput | un
|
|
|
775
777
|
export declare function resolveStaticFallbackPath(root: string, index: string, exclude: string, method: string, pathname: string, accept: string): string | undefined;
|
|
776
778
|
|
|
777
779
|
/**
|
|
778
|
-
* Resolves a request pathname to an on-disk path
|
|
780
|
+
* Resolves a request pathname to an on-disk path under `root`, or `undefined`
|
|
779
781
|
* when it cannot — the traversal guard, whose algorithm and order are exact:
|
|
780
782
|
* strip `prefix` on a segment boundary → `decodeURIComponent` (a
|
|
781
783
|
* malformed escape refuses, never throws) → reject a NUL byte → strip the
|
|
782
|
-
* leading path separator
|
|
783
|
-
* genuine climbing segment) → `normalize` → refuse any Windows
|
|
784
|
-
* device-name segment ({@link isReservedDeviceName}) → `resolve` and
|
|
785
|
-
* the result under `root`.
|
|
784
|
+
* leading path separator first (so a leading `..` survives `normalize` as a
|
|
785
|
+
* genuine climbing segment) → `normalize` → refuse any Windows
|
|
786
|
+
* reserved-device-name segment ({@link isReservedDeviceName}) → `resolve` and
|
|
787
|
+
* require the result under `root`.
|
|
786
788
|
*
|
|
787
789
|
* @param root - The absolute root directory every result must resolve under
|
|
788
790
|
* @param prefix - An optional URL path prefix stripped on a segment boundary
|
|
@@ -802,14 +804,14 @@ export declare function resolveStaticPath(root: string, prefix: string | undefin
|
|
|
802
804
|
* Configures `createStatic` — node `fs`-backed static file serving.
|
|
803
805
|
*
|
|
804
806
|
* @remarks
|
|
805
|
-
* - `root` — the directory every request resolves under, resolved
|
|
806
|
-
* construction.
|
|
807
|
+
* - `root` — the required directory every request resolves under, resolved
|
|
808
|
+
* once at construction.
|
|
807
809
|
* - `prefix` — a URL path prefix stripped (on a segment boundary) before
|
|
808
810
|
* resolving under `root`.
|
|
809
811
|
* - `index` — the filename served for a directory hit and by the SPA
|
|
810
812
|
* fallback; defaults to {@link DEFAULT_STATIC_INDEX}. The fallback serves
|
|
811
|
-
* it whatever `dotfiles` is set to, because this path is
|
|
812
|
-
* configured rather than request-derived.
|
|
813
|
+
* it whatever `dotfiles` is set to, because this path is
|
|
814
|
+
* operator-configured rather than request-derived.
|
|
813
815
|
* - `dotfiles` — the policy for a path with a dotfile segment: `'ignore'`
|
|
814
816
|
* (falls through to `next()`), `'deny'` (403), or `'allow'` (serves it);
|
|
815
817
|
* defaults to {@link DEFAULT_STATIC_DOTFILES}.
|
|
@@ -818,7 +820,7 @@ export declare function resolveStaticPath(root: string, prefix: string | undefin
|
|
|
818
820
|
* - `fallback` — SPA fallback: `false` (default, off), `true` (on, excluding
|
|
819
821
|
* {@link DEFAULT_STATIC_FALLBACK_EXCLUDE}), or `{ exclude }` for a custom
|
|
820
822
|
* excluded prefix. An eligible `GET` or `HEAD` navigation miss answers with
|
|
821
|
-
* `index` through the
|
|
823
|
+
* `index` through the same handle-`fstat` header block a directly requested
|
|
822
824
|
* file answers through, so `cache`, `etag`, conditional revalidation,
|
|
823
825
|
* `HEAD`, and ranges are identical on both routes; `index` reaches the
|
|
824
826
|
* client through this route whatever `dotfiles` is set to.
|
|
@@ -842,7 +844,7 @@ export declare interface StaticOptions {
|
|
|
842
844
|
* response body routes through.
|
|
843
845
|
*
|
|
844
846
|
* @remarks
|
|
845
|
-
*
|
|
847
|
+
* Pull-driven, not push-driven: the underlying node stream's async iterator
|
|
846
848
|
* is only advanced (`iterator.next()`) from inside `pull(controller)`, which
|
|
847
849
|
* the web `ReadableStream` invokes exactly when its internal queue has room
|
|
848
850
|
* for more data. Exactly one disk chunk is read and enqueued per `pull` —
|
|
@@ -911,13 +913,13 @@ export declare function unlinkStagedFiles(body: MultipartBody): Promise<void>;
|
|
|
911
913
|
*
|
|
912
914
|
* @remarks
|
|
913
915
|
* - `field` — the multipart field name the file was submitted under.
|
|
914
|
-
* - `name` — the client-declared filename (
|
|
916
|
+
* - `name` — the client-declared filename (metadata only — never used to
|
|
915
917
|
* build a filesystem path).
|
|
916
918
|
* - `size` — the file's byte size.
|
|
917
|
-
* - `mime` — the
|
|
919
|
+
* - `mime` — the sniffed (magic-byte-detected) MIME type when a signature
|
|
918
920
|
* matches; otherwise the part's declared `Content-Type`; otherwise
|
|
919
921
|
* {@link DEFAULT_CONTENT_TYPE}. Read `validated` to tell which.
|
|
920
|
-
* - `validated` — `true` when a signature matched
|
|
922
|
+
* - `validated` — `true` when a signature matched and the sniffed type equals
|
|
921
923
|
* the declared `Content-Type`, so `mime` is the sniffed fact. `false` means
|
|
922
924
|
* `mime` may be the client-declared value.
|
|
923
925
|
* - `status` — see {@link UploadStatus}.
|
package/dist/src/server/index.js
CHANGED
|
@@ -9,7 +9,10 @@ import { randomUUID } from "node:crypto";
|
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { DEFAULT_COMPRESSION_THRESHOLD, compressResponse } from "../core/index.js";
|
|
11
11
|
//#region src/server/constants.ts
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Holds the HTTP status `createMultipart` renders for each {@link MultipartErrorCode}:
|
|
14
|
+
* `'limit'` is 413, `'malformed'` is 400, and `'rejected'` is 415.
|
|
15
|
+
*/
|
|
13
16
|
var MULTIPART_STATUS = Object.freeze({
|
|
14
17
|
limit: 413,
|
|
15
18
|
malformed: 400,
|
|
@@ -22,33 +25,33 @@ var MULTIPART_STATUS = Object.freeze({
|
|
|
22
25
|
* which would mint an unequal symbol per copy.
|
|
23
26
|
*/
|
|
24
27
|
var MULTIPART_ERROR_BRAND = Symbol.for("@orkestrel/middleware.MultipartError");
|
|
25
|
-
/** Names `createStatic`'s default directory-index filename. */
|
|
28
|
+
/** Names `'index.html'`, `createStatic`'s default directory-index filename. */
|
|
26
29
|
var DEFAULT_STATIC_INDEX = "index.html";
|
|
27
|
-
/** Names `createStatic`'s `fallback: true` default excluded path prefix. */
|
|
30
|
+
/** Names `'/api'`, `createStatic`'s `fallback: true` default excluded path prefix. */
|
|
28
31
|
var DEFAULT_STATIC_FALLBACK_EXCLUDE = "/api";
|
|
29
|
-
/** Names `createStatic`'s default policy for a path carrying a dotfile segment. */
|
|
32
|
+
/** Names `'ignore'`, `createStatic`'s default policy for a path carrying a dotfile segment. */
|
|
30
33
|
var DEFAULT_STATIC_DOTFILES = "ignore";
|
|
31
34
|
/**
|
|
32
|
-
* Lists the content-codings the node face's `createCompression`
|
|
33
|
-
* `node:zlib` guarantees on every Node runtime, so this face never
|
|
35
|
+
* Lists `['gzip', 'deflate']`, the content-codings the node face's `createCompression`
|
|
36
|
+
* offers — what `node:zlib` guarantees on every Node runtime, so this face never
|
|
34
37
|
* feature-detects.
|
|
35
38
|
*/
|
|
36
39
|
var NODE_COMPRESSION_ENCODINGS = Object.freeze(["gzip", "deflate"]);
|
|
37
|
-
/** Names the MIME type served when a file extension has no known mapping. */
|
|
40
|
+
/** Names `'application/octet-stream'`, the MIME type served when a file extension has no known mapping. */
|
|
38
41
|
var DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
39
|
-
/** Holds `createMultipart`'s default per-file byte-size cap. */
|
|
42
|
+
/** Holds `10_485_760`, `createMultipart`'s default per-file byte-size cap. */
|
|
40
43
|
var DEFAULT_MULTIPART_FILE_SIZE = 10485760;
|
|
41
|
-
/** Holds `createMultipart`'s default maximum file-part count. */
|
|
44
|
+
/** Holds `10`, `createMultipart`'s default maximum file-part count. */
|
|
42
45
|
var DEFAULT_MULTIPART_FILE_COUNT = 10;
|
|
43
|
-
/** Holds `createMultipart`'s default per-field byte-size cap. */
|
|
46
|
+
/** Holds `65_536`, `createMultipart`'s default per-field byte-size cap. */
|
|
44
47
|
var DEFAULT_MULTIPART_FIELD_SIZE = 65536;
|
|
45
|
-
/** Holds `createMultipart`'s default maximum field-part count. */
|
|
48
|
+
/** Holds `100`, `createMultipart`'s default maximum field-part count. */
|
|
46
49
|
var DEFAULT_MULTIPART_FIELD_COUNT = 100;
|
|
47
|
-
/** Holds `createMultipart`'s default combined request-body byte-size cap. */
|
|
50
|
+
/** Holds `52_428_800`, `createMultipart`'s default combined request-body byte-size cap. */
|
|
48
51
|
var DEFAULT_MULTIPART_TOTAL = 52428800;
|
|
49
|
-
/** Holds the maximum bytes a single multipart part's header block may occupy before it is malformed. */
|
|
52
|
+
/** Holds `16_384`, the maximum bytes a single multipart part's header block may occupy before it is malformed. */
|
|
50
53
|
var MULTIPART_MAX_HEADER_BLOCK = 16384;
|
|
51
|
-
/** Holds the maximum bytes scanned before the first multipart boundary is found before it is malformed. */
|
|
54
|
+
/** Holds `65_536`, the maximum bytes scanned before the first multipart boundary is found before it is malformed. */
|
|
52
55
|
var MULTIPART_MAX_PREAMBLE = 65536;
|
|
53
56
|
/**
|
|
54
57
|
* Lists the Windows reserved device-name stems (CVE-2025-27210) — matched
|
|
@@ -167,7 +170,7 @@ function isMultipartError(value) {
|
|
|
167
170
|
//#endregion
|
|
168
171
|
//#region src/server/helpers.ts
|
|
169
172
|
/**
|
|
170
|
-
* Checks whether `pathname` is `prefix` itself or lies under it on a
|
|
173
|
+
* Checks whether `pathname` is `prefix` itself or lies under it on a segment
|
|
171
174
|
* boundary — the shared under-path test `resolveStaticPath`'s prefix strip
|
|
172
175
|
* and `createStatic`'s SPA-fallback `exclude` both apply, so `exclude:
|
|
173
176
|
* '/api'` matches `/api` and `/api/x` but never `/apifoo`.
|
|
@@ -192,7 +195,7 @@ function isUnderPath(pathname, prefix) {
|
|
|
192
195
|
* fallback.
|
|
193
196
|
*
|
|
194
197
|
* @remarks
|
|
195
|
-
* `GET` and `HEAD` are both eligible, and resolve the
|
|
198
|
+
* `GET` and `HEAD` are both eligible, and resolve the same shell: `HEAD` is
|
|
196
199
|
* defined as `GET` without a body (RFC 9110 §9.3.2), so a navigation probe
|
|
197
200
|
* that answered `404` while its `GET` answered `200` would report a resource
|
|
198
201
|
* the very next request serves.
|
|
@@ -220,11 +223,11 @@ function resolveStaticFallbackPath(root, index, exclude, method, pathname, accep
|
|
|
220
223
|
}
|
|
221
224
|
/**
|
|
222
225
|
* Checks whether `child` is `parent` itself or lies inside it on-disk — the
|
|
223
|
-
*
|
|
226
|
+
* filesystem containment predicate `createStatic` applies to `fs.realpath`
|
|
224
227
|
* output (never to a URL pathname — that is {@link isUnderPath}'s job).
|
|
225
228
|
*
|
|
226
229
|
* @remarks
|
|
227
|
-
* Argument order is `(child, parent)` — deliberately the
|
|
230
|
+
* Argument order is `(child, parent)` — deliberately the opposite conceptual
|
|
228
231
|
* order from {@link isUnderPath}`(pathname, prefix)`, so a call site cannot
|
|
229
232
|
* casually swap one predicate in for the other. Built on `path.relative`,
|
|
230
233
|
* this is separator-correct on both POSIX (`/`) and win32 (`\`) — unlike a
|
|
@@ -247,14 +250,14 @@ function isContainedPath(child, parent) {
|
|
|
247
250
|
return rel.length > 0 && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
248
251
|
}
|
|
249
252
|
/**
|
|
250
|
-
* Resolves a request pathname to an on-disk path
|
|
253
|
+
* Resolves a request pathname to an on-disk path under `root`, or `undefined`
|
|
251
254
|
* when it cannot — the traversal guard, whose algorithm and order are exact:
|
|
252
255
|
* strip `prefix` on a segment boundary → `decodeURIComponent` (a
|
|
253
256
|
* malformed escape refuses, never throws) → reject a NUL byte → strip the
|
|
254
|
-
* leading path separator
|
|
255
|
-
* genuine climbing segment) → `normalize` → refuse any Windows
|
|
256
|
-
* device-name segment ({@link isReservedDeviceName}) → `resolve` and
|
|
257
|
-
* the result under `root`.
|
|
257
|
+
* leading path separator first (so a leading `..` survives `normalize` as a
|
|
258
|
+
* genuine climbing segment) → `normalize` → refuse any Windows
|
|
259
|
+
* reserved-device-name segment ({@link isReservedDeviceName}) → `resolve` and
|
|
260
|
+
* require the result under `root`.
|
|
258
261
|
*
|
|
259
262
|
* @param root - The absolute root directory every result must resolve under
|
|
260
263
|
* @param prefix - An optional URL path prefix stripped on a segment boundary
|
|
@@ -323,7 +326,7 @@ async function resolveContainedRealPath(candidate, rootReal) {
|
|
|
323
326
|
*
|
|
324
327
|
* @remarks
|
|
325
328
|
* Normalizes superscript digits (`¹²³` → `123`) first, strips trailing dots
|
|
326
|
-
* and spaces (Windows drops them), takes the
|
|
329
|
+
* and spaces (Windows drops them), takes the stem before the first `.`,
|
|
327
330
|
* upper-cases it, and tests it against {@link RESERVED_DEVICE_NAMES}
|
|
328
331
|
* (`CON PRN AUX NUL COM1-9 LPT1-9`) — an exact-stem match only, so
|
|
329
332
|
* `console.js` and `nullable.css` are never flagged.
|
|
@@ -642,7 +645,7 @@ async function unlinkStagedFiles(body) {
|
|
|
642
645
|
* response body routes through.
|
|
643
646
|
*
|
|
644
647
|
* @remarks
|
|
645
|
-
*
|
|
648
|
+
* Pull-driven, not push-driven: the underlying node stream's async iterator
|
|
646
649
|
* is only advanced (`iterator.next()`) from inside `pull(controller)`, which
|
|
647
650
|
* the web `ReadableStream` invokes exactly when its internal queue has room
|
|
648
651
|
* for more data. Exactly one disk chunk is read and enqueued per `pull` —
|
|
@@ -979,30 +982,30 @@ var MultipartParser = class MultipartParser {
|
|
|
979
982
|
*
|
|
980
983
|
* @remarks
|
|
981
984
|
* Reads `request.body` chunk by chunk through its `ReadableStream` reader —
|
|
982
|
-
*
|
|
985
|
+
* never buffers the whole body — enforcing every {@link MultipartLimits} cap
|
|
983
986
|
* the instant it is exceeded (reading stops, every already-staged temp file
|
|
984
987
|
* is deleted, throws {@link MultipartError} with code `'limit'`). Each file
|
|
985
988
|
* part streams to `join(directory, randomUUID())` — the client's declared
|
|
986
|
-
* filename is
|
|
989
|
+
* filename is metadata only, never a path component. A field or file part
|
|
987
990
|
* named `__proto__` / `constructor` / `prototype` is silently skipped and
|
|
988
991
|
* never keyed onto the returned {@link MultipartBody} (a skipped file's
|
|
989
992
|
* staged temp file is unlinked immediately, since it can never be
|
|
990
993
|
* referenced). A file part with an empty declared filename (`filename=""`)
|
|
991
|
-
*
|
|
994
|
+
* and a zero-byte body — the browser convention for an unselected optional
|
|
992
995
|
* `<input type="file">` — is a silent no-op: its temp file is unlinked, it is
|
|
993
996
|
* never counted against the `file.count` limit, and it never runs the
|
|
994
997
|
* `allowed` check. A malformed
|
|
995
998
|
* structure (missing/unterminated boundary, nameless part, an oversized
|
|
996
999
|
* header block, or a preamble exceeding {@link MULTIPART_MAX_PREAMBLE} before
|
|
997
1000
|
* the first boundary) throws with code `'malformed'`. A file is accepted
|
|
998
|
-
* against the configured `allowed` MIME list
|
|
1001
|
+
* against the configured `allowed` MIME list exactly when its sniffed bytes detect a
|
|
999
1002
|
* type present in the list — sniff-authoritative, independent of whether the
|
|
1000
1003
|
* declared `Content-Type` matches (that agreement is exposed separately as
|
|
1001
1004
|
* `validated`); otherwise throws with code `'rejected'`. A
|
|
1002
1005
|
* request abort mid-upload triggers the same fail-closed cleanup as a limit
|
|
1003
1006
|
* breach. Returns `undefined` for a non-multipart request (untouched).
|
|
1004
1007
|
*
|
|
1005
|
-
* Staging defaults to a process-owned directory created
|
|
1008
|
+
* Staging defaults to a process-owned directory created once (lazily,
|
|
1006
1009
|
* memoized across calls) with `mkdtemp` under `os.tmpdir()` and locked to
|
|
1007
1010
|
* mode `0o700`; `options.directory` overrides it.
|
|
1008
1011
|
*
|
|
@@ -1123,16 +1126,16 @@ function createAssets(options) {
|
|
|
1123
1126
|
};
|
|
1124
1127
|
}
|
|
1125
1128
|
/**
|
|
1126
|
-
* Serves static files from `options.root` over `node:fs` — the node-bound
|
|
1127
|
-
*
|
|
1129
|
+
* Serves static files from `options.root` over `node:fs` — the node-bound static-file
|
|
1130
|
+
* battery, answering conditional, ranged, and SPA-fallback requests.
|
|
1128
1131
|
*
|
|
1129
1132
|
* @remarks
|
|
1130
|
-
* Containment is enforced on
|
|
1133
|
+
* Containment is enforced on canonical paths, not merely the lexically
|
|
1131
1134
|
* resolved one: `options.root` is canonicalized once (memoized) and every
|
|
1132
1135
|
* request's candidate path is re-canonicalized (`fs.realpath`) before it is
|
|
1133
1136
|
* served, so a symlink whose target escapes `root` is refused (falls through
|
|
1134
1137
|
* to `next()`) even though the lexical path resolved inside `root`. A
|
|
1135
|
-
* symlink that resolves to a target still
|
|
1138
|
+
* symlink that resolves to a target still inside `root` is unaffected and
|
|
1136
1139
|
* still serves normally. A dangling symlink (`realpath` throws `ENOENT`) or
|
|
1137
1140
|
* any other `realpath` failure is treated as a miss — this battery never
|
|
1138
1141
|
* throws or 500s on a symlink surprise. On a streamed response (a 200 or 206
|
|
@@ -1328,7 +1331,7 @@ function createStatic(options) {
|
|
|
1328
1331
|
* {@link MultipartError} this battery's parser throws is re-thrown as an
|
|
1329
1332
|
* {@link HTTPError} carrying the same status/message, so `createBoundary`
|
|
1330
1333
|
* (or any HTTPError-aware renderer) maps it correctly without depending on
|
|
1331
|
-
* this node face's error type. Fail-closed on the
|
|
1334
|
+
* this node face's error type. Fail-closed on the downstream handler too: if
|
|
1332
1335
|
* `next()` throws, every still-`'staged'` uploaded file is unlinked
|
|
1333
1336
|
* (best-effort) before the error is re-thrown, so an unhandled downstream
|
|
1334
1337
|
* failure never leaks temp files. A normal return leaves staged files
|
|
@@ -1367,12 +1370,11 @@ function createMultipart(options = {}) {
|
|
|
1367
1370
|
};
|
|
1368
1371
|
}
|
|
1369
1372
|
/**
|
|
1370
|
-
* Compresses response bodies through `node:zlib
|
|
1371
|
-
* the
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
* so the shared name is unambiguous per consumer import path.
|
|
1373
|
+
* Compresses response bodies through `node:zlib`, guaranteed on any Node runtime rather
|
|
1374
|
+
* than dependent on the WHATWG `CompressionStream` global. This battery is the
|
|
1375
|
+
* node-bound sibling of the core face's feature-detected `createCompression`, and it
|
|
1376
|
+
* ships from a separate package entry point (`@orkestrel/middleware/server`) so the
|
|
1377
|
+
* shared name is unambiguous per consumer import path.
|
|
1376
1378
|
*
|
|
1377
1379
|
* @remarks
|
|
1378
1380
|
* Peer-type limitation, the same one the core face carries: the shipped
|