@orkestrel/middleware 0.0.18 → 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 +20 -15
- package/dist/src/core/index.cjs +393 -312
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +497 -328
- package/dist/src/core/index.d.ts +497 -328
- package/dist/src/core/index.js +391 -311
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +486 -422
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +316 -232
- package/dist/src/server/index.d.ts +316 -232
- package/dist/src/server/index.js +478 -417
- package/dist/src/server/index.js.map +1 -1
- package/package.json +21 -23
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { Encoding } from '@orkestrel/server';
|
|
2
|
-
import { FileHandle } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import type { Encoding } from '@orkestrel/server';
|
|
2
|
+
import type { FileHandle } from 'node:fs/promises';
|
|
3
|
+
import { HTTPError } from '@orkestrel/server';
|
|
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';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* Describes one in-memory asset representation returned by an {@link AssetSourceInterface}.
|
|
10
11
|
*
|
|
11
12
|
* @remarks
|
|
12
13
|
* - `body` — the representation bytes. `createAssets` copies them before use.
|
|
@@ -19,26 +20,32 @@ export declare interface Asset {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
23
|
+
* Configures `createAssets` — in-memory identity/Brotli asset serving.
|
|
23
24
|
*
|
|
24
|
-
* @param options - See fields below
|
|
25
25
|
* @remarks
|
|
26
|
-
* - `source` — the required in-memory asset reader.
|
|
26
|
+
* - `source` — the required in-memory asset reader. It must answer a bounded
|
|
27
|
+
* key set and return `undefined` for every key outside it, because
|
|
28
|
+
* `createAssets` retains every successful result for the factory's lifetime
|
|
29
|
+
* and evicts nothing. A `source` that synthesizes a representation for an
|
|
30
|
+
* arbitrary key therefore grows that cache without limit under request
|
|
31
|
+
* pressure.
|
|
27
32
|
*/
|
|
28
33
|
export declare interface AssetOptions {
|
|
29
34
|
readonly source: AssetSourceInterface;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
/**
|
|
33
|
-
*
|
|
38
|
+
* Reads in-memory assets by decoded, browser-build-relative path.
|
|
34
39
|
*
|
|
35
40
|
* @remarks
|
|
36
41
|
* A successful result is cached by `createAssets`; later source changes do
|
|
37
42
|
* not alter that path's response. A miss may be read again on a later request.
|
|
43
|
+
* `read` therefore owes a bounded key set: that cache retains every
|
|
44
|
+
* successful result for the factory's lifetime and evicts nothing.
|
|
38
45
|
*/
|
|
39
46
|
export declare interface AssetSourceInterface {
|
|
40
47
|
/**
|
|
41
|
-
*
|
|
48
|
+
* Reads one identity or Brotli asset representation for a validated relative key.
|
|
42
49
|
*
|
|
43
50
|
* @param path - The validated relative asset path
|
|
44
51
|
* @returns The asset, or `undefined` when the path is absent
|
|
@@ -47,7 +54,24 @@ export declare interface AssetSourceInterface {
|
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
/**
|
|
50
|
-
*
|
|
57
|
+
* Describes one inclusive byte range over a file — `streamFile`'s optional
|
|
58
|
+
* `range` argument and the shape `createStatic` builds for a satisfiable
|
|
59
|
+
* `Range` request.
|
|
60
|
+
*
|
|
61
|
+
* @remarks
|
|
62
|
+
* - `start` — the first byte offset served.
|
|
63
|
+
* - `end` — the last byte offset served.
|
|
64
|
+
*
|
|
65
|
+
* Both bounds are 0-indexed and inclusive, matching `node:fs`'s
|
|
66
|
+
* `createReadStream` options.
|
|
67
|
+
*/
|
|
68
|
+
export declare interface ByteRange {
|
|
69
|
+
readonly start: number;
|
|
70
|
+
readonly end: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Compresses response bytes with Node's guaranteed zlib gzip/deflate codecs.
|
|
51
75
|
*
|
|
52
76
|
* @param bytes - The uncompressed response bytes
|
|
53
77
|
* @param encoding - The negotiated actionable coding
|
|
@@ -62,7 +86,7 @@ export declare interface AssetSourceInterface {
|
|
|
62
86
|
export declare function compressNodeBytes(bytes: Uint8Array<ArrayBuffer>, encoding: Exclude<Encoding, 'identity'>): Promise<Uint8Array<ArrayBuffer>>;
|
|
63
87
|
|
|
64
88
|
/**
|
|
65
|
-
*
|
|
89
|
+
* Computes a static file's weak ETag from its size and modification time.
|
|
66
90
|
*
|
|
67
91
|
* @param size - The file's byte size
|
|
68
92
|
* @param mtimeMs - The file's modification time in milliseconds
|
|
@@ -76,7 +100,7 @@ export declare function compressNodeBytes(bytes: Uint8Array<ArrayBuffer>, encodi
|
|
|
76
100
|
export declare function computeFileETag(size: number, mtimeMs: number): string;
|
|
77
101
|
|
|
78
102
|
/**
|
|
79
|
-
*
|
|
103
|
+
* Serves validated in-memory assets with identity/Brotli negotiation.
|
|
80
104
|
*
|
|
81
105
|
* @remarks
|
|
82
106
|
* Only `GET` and `HEAD` are served. `/` resolves to `index.html`. Every other
|
|
@@ -112,20 +136,18 @@ export declare function computeFileETag(size: number, mtimeMs: number): string;
|
|
|
112
136
|
export declare function createAssets<TState>(options: AssetOptions): MiddlewareHandler<TState>;
|
|
113
137
|
|
|
114
138
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* face's `createCompression`, so the shared name is unambiguous per
|
|
121
|
-
* consumer import path (ruling H).
|
|
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.
|
|
122
144
|
*
|
|
123
145
|
* @remarks
|
|
124
|
-
* Peer-type limitation
|
|
146
|
+
* Peer-type limitation, the same one the core face carries: the shipped
|
|
125
147
|
* `@orkestrel/server` `Encoding` union is `'gzip' | 'deflate' | 'identity'`
|
|
126
148
|
* — it does not include `'br'`, so this battery cannot honestly type or
|
|
127
149
|
* negotiate a guaranteed brotli coding despite `node:zlib` shipping
|
|
128
|
-
* `brotliCompress`. It guarantees `gzip`/`deflate`
|
|
150
|
+
* `brotliCompress`. It guarantees `gzip`/`deflate` through `node:zlib` (never
|
|
129
151
|
* feature-detected — always available) and negotiates only those.
|
|
130
152
|
*
|
|
131
153
|
* @typeParam TState - The consumer's opaque per-request state type
|
|
@@ -144,9 +166,9 @@ export declare function createAssets<TState>(options: AssetOptions): MiddlewareH
|
|
|
144
166
|
export declare function createCompression<TState>(options?: NodeCompressionOptions): MiddlewareHandler<TState>;
|
|
145
167
|
|
|
146
168
|
/**
|
|
147
|
-
*
|
|
169
|
+
* Parses a streamed `multipart/form-data` request body and stashes its
|
|
148
170
|
* {@link MultipartBody} on `context.state.multipart` — the node-bound
|
|
149
|
-
* streaming multipart battery
|
|
171
|
+
* streaming multipart battery.
|
|
150
172
|
*
|
|
151
173
|
* @remarks
|
|
152
174
|
* A non-multipart request passes through untouched. Consumes `request.body`
|
|
@@ -155,7 +177,7 @@ export declare function createCompression<TState>(options?: NodeCompressionOptio
|
|
|
155
177
|
* {@link MultipartError} this battery's parser throws is re-thrown as an
|
|
156
178
|
* {@link HTTPError} carrying the same status/message, so `createBoundary`
|
|
157
179
|
* (or any HTTPError-aware renderer) maps it correctly without depending on
|
|
158
|
-
* 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
|
|
159
181
|
* `next()` throws, every still-`'staged'` uploaded file is unlinked
|
|
160
182
|
* (best-effort) before the error is re-thrown, so an unhandled downstream
|
|
161
183
|
* failure never leaks temp files. A normal return leaves staged files
|
|
@@ -177,23 +199,23 @@ export declare function createCompression<TState>(options?: NodeCompressionOptio
|
|
|
177
199
|
export declare function createMultipart<TState extends MultipartState>(options?: MultipartOptions): MiddlewareHandler<TState>;
|
|
178
200
|
|
|
179
201
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
202
|
+
* Serves static files from `options.root` over `node:fs` — the node-bound static-file
|
|
203
|
+
* battery, answering conditional, ranged, and SPA-fallback requests.
|
|
182
204
|
*
|
|
183
205
|
* @remarks
|
|
184
|
-
* Containment is enforced on
|
|
206
|
+
* Containment is enforced on canonical paths, not merely the lexically
|
|
185
207
|
* resolved one: `options.root` is canonicalized once (memoized) and every
|
|
186
208
|
* request's candidate path is re-canonicalized (`fs.realpath`) before it is
|
|
187
209
|
* served, so a symlink whose target escapes `root` is refused (falls through
|
|
188
210
|
* to `next()`) even though the lexical path resolved inside `root`. A
|
|
189
|
-
* symlink that resolves to a target still
|
|
211
|
+
* symlink that resolves to a target still inside `root` is unaffected and
|
|
190
212
|
* still serves normally. A dangling symlink (`realpath` throws `ENOENT`) or
|
|
191
213
|
* any other `realpath` failure is treated as a miss — this battery never
|
|
192
214
|
* throws or 500s on a symlink surprise. On a streamed response (a 200 or 206
|
|
193
215
|
* that carries a file body), the open `FileHandle` is owned by the
|
|
194
216
|
* `Response` body and is released only once that body is fully read or
|
|
195
217
|
* cancelled — Node HTTP servers do this automatically when sending the
|
|
196
|
-
* response, but a caller that holds an unread `Response` (
|
|
218
|
+
* response, but a caller that holds an unread `Response` (for example in a test)
|
|
197
219
|
* must cancel its body to release the handle promptly.
|
|
198
220
|
*
|
|
199
221
|
* @typeParam TState - The consumer's opaque per-request state type
|
|
@@ -211,47 +233,56 @@ export declare function createMultipart<TState extends MultipartState>(options?:
|
|
|
211
233
|
export declare function createStatic<TState>(options: StaticOptions): MiddlewareHandler<TState>;
|
|
212
234
|
|
|
213
235
|
/**
|
|
214
|
-
*
|
|
236
|
+
* Builds a frozen {@link UploadedFile} record.
|
|
215
237
|
*
|
|
216
238
|
* @param input - Every field of the record
|
|
217
|
-
* @returns A frozen {@link
|
|
239
|
+
* @returns A frozen {@link UploadedFile}
|
|
218
240
|
*
|
|
219
241
|
* @example
|
|
220
242
|
* ```ts
|
|
221
243
|
* createUploadedFile({ field: 'avatar', name: 'a.png', size: 1024, mime: 'image/png', validated: true, status: 'staged', path: '/tmp/x' })
|
|
222
244
|
* ```
|
|
223
245
|
*/
|
|
224
|
-
export declare function createUploadedFile(input:
|
|
246
|
+
export declare function createUploadedFile(input: UploadedFile): UploadedFile;
|
|
225
247
|
|
|
226
|
-
/**
|
|
248
|
+
/** Names `'application/octet-stream'`, the MIME type served when a file extension has no known mapping. */
|
|
227
249
|
export declare const DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
228
250
|
|
|
229
|
-
/** `createMultipart`'s default
|
|
230
|
-
export declare const
|
|
251
|
+
/** Holds `100`, `createMultipart`'s default maximum field-part count. */
|
|
252
|
+
export declare const DEFAULT_MULTIPART_FIELD_COUNT = 100;
|
|
231
253
|
|
|
232
|
-
/** `createMultipart`'s default
|
|
233
|
-
export declare const
|
|
254
|
+
/** Holds `65_536`, `createMultipart`'s default per-field byte-size cap. */
|
|
255
|
+
export declare const DEFAULT_MULTIPART_FIELD_SIZE = 65536;
|
|
234
256
|
|
|
235
|
-
/** `createMultipart`'s default
|
|
236
|
-
export declare const
|
|
257
|
+
/** Holds `10`, `createMultipart`'s default maximum file-part count. */
|
|
258
|
+
export declare const DEFAULT_MULTIPART_FILE_COUNT = 10;
|
|
237
259
|
|
|
238
|
-
/** `createMultipart`'s default
|
|
239
|
-
export declare const
|
|
260
|
+
/** Holds `10_485_760`, `createMultipart`'s default per-file byte-size cap. */
|
|
261
|
+
export declare const DEFAULT_MULTIPART_FILE_SIZE = 10485760;
|
|
240
262
|
|
|
241
|
-
/** `createMultipart`'s default combined request-body byte-size cap. */
|
|
263
|
+
/** Holds `52_428_800`, `createMultipart`'s default combined request-body byte-size cap. */
|
|
242
264
|
export declare const DEFAULT_MULTIPART_TOTAL = 52428800;
|
|
243
265
|
|
|
244
|
-
/** `createStatic`'s
|
|
266
|
+
/** Names `'ignore'`, `createStatic`'s default policy for a path carrying a dotfile segment. */
|
|
267
|
+
export declare const DEFAULT_STATIC_DOTFILES: NonNullable<StaticOptions['dotfiles']>;
|
|
268
|
+
|
|
269
|
+
/** Names `'/api'`, `createStatic`'s `fallback: true` default excluded path prefix. */
|
|
245
270
|
export declare const DEFAULT_STATIC_FALLBACK_EXCLUDE = "/api";
|
|
246
271
|
|
|
247
|
-
/** `createStatic`'s default directory-index filename. */
|
|
272
|
+
/** Names `'index.html'`, `createStatic`'s default directory-index filename. */
|
|
248
273
|
export declare const DEFAULT_STATIC_INDEX = "index.html";
|
|
249
274
|
|
|
250
275
|
/**
|
|
251
|
-
*
|
|
252
|
-
* table (jpeg, png, gif87a/89a, webp, pdf, zip)
|
|
253
|
-
*
|
|
254
|
-
*
|
|
276
|
+
* Sniffs a MIME type from a file's leading bytes against a small magic-byte
|
|
277
|
+
* table (jpeg, png, gif87a/89a, webp, pdf, zip).
|
|
278
|
+
*
|
|
279
|
+
* @remarks
|
|
280
|
+
* `createMultipart`'s `allowed` check reads this signal alone and never the
|
|
281
|
+
* declared `Content-Type`, so a file whose bytes match no signature can never
|
|
282
|
+
* be placed on an `allowed` list. The record's `mime` field is a different
|
|
283
|
+
* question: it falls back to the declared `Content-Type` (then to
|
|
284
|
+
* {@link DEFAULT_CONTENT_TYPE}) when nothing sniffs, and its `validated` flag
|
|
285
|
+
* reports whether the sniffed and declared types agreed.
|
|
255
286
|
*
|
|
256
287
|
* @param head - The file's first bytes (16 is sufficient for every signature)
|
|
257
288
|
* @returns The detected MIME type, or `undefined` when no signature matches
|
|
@@ -263,16 +294,32 @@ export declare const DEFAULT_STATIC_INDEX = "index.html";
|
|
|
263
294
|
*/
|
|
264
295
|
export declare function detectMIME(head: Uint8Array): string | undefined;
|
|
265
296
|
|
|
266
|
-
/**
|
|
297
|
+
/** Holds the file-extension (lowercase, with leading `.`) → MIME type lookup table for static serving. */
|
|
267
298
|
export declare const EXTENSION_TYPES: Readonly<Record<string, string>>;
|
|
268
299
|
|
|
269
300
|
/**
|
|
270
|
-
*
|
|
271
|
-
*
|
|
301
|
+
* Extracts the `boundary` parameter from a `Content-Type` header, or
|
|
302
|
+
* `undefined` when the request is not `multipart/form-data`.
|
|
303
|
+
*
|
|
304
|
+
* @param contentType - The request's `Content-Type` header value, if present
|
|
305
|
+
* @returns The multipart boundary token, or `undefined` for a non-multipart
|
|
306
|
+
* (or malformed/boundary-less) content type
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```ts
|
|
310
|
+
* extractMultipartBoundary('multipart/form-data; boundary=abc123') // 'abc123'
|
|
311
|
+
* extractMultipartBoundary('application/json') // undefined
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
314
|
+
export declare function extractMultipartBoundary(contentType: string | null): string | undefined;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Checks whether `child` is `parent` itself or lies inside it on-disk — the
|
|
318
|
+
* filesystem containment predicate `createStatic` applies to `fs.realpath`
|
|
272
319
|
* output (never to a URL pathname — that is {@link isUnderPath}'s job).
|
|
273
320
|
*
|
|
274
321
|
* @remarks
|
|
275
|
-
* Argument order is `(child, parent)` — deliberately the
|
|
322
|
+
* Argument order is `(child, parent)` — deliberately the opposite conceptual
|
|
276
323
|
* order from {@link isUnderPath}`(pathname, prefix)`, so a call site cannot
|
|
277
324
|
* casually swap one predicate in for the other. Built on `path.relative`,
|
|
278
325
|
* this is separator-correct on both POSIX (`/`) and win32 (`\`) — unlike a
|
|
@@ -281,7 +328,7 @@ export declare const EXTENSION_TYPES: Readonly<Record<string, string>>;
|
|
|
281
328
|
*
|
|
282
329
|
* @param child - The absolute on-disk path to test
|
|
283
330
|
* @param parent - The absolute on-disk directory it must lie under
|
|
284
|
-
* @returns
|
|
331
|
+
* @returns True if `child` equals `parent` or resolves inside it; false otherwise
|
|
285
332
|
*
|
|
286
333
|
* @example
|
|
287
334
|
* ```ts
|
|
@@ -292,11 +339,11 @@ export declare const EXTENSION_TYPES: Readonly<Record<string, string>>;
|
|
|
292
339
|
export declare function isContainedPath(child: string, parent: string): boolean;
|
|
293
340
|
|
|
294
341
|
/**
|
|
295
|
-
*
|
|
342
|
+
* Checks whether a relative path (already resolved under a static root) has any
|
|
296
343
|
* segment starting with `.` — a dotfile or dot-directory.
|
|
297
344
|
*
|
|
298
345
|
* @param relativePath - A path relative to the static root
|
|
299
|
-
* @returns
|
|
346
|
+
* @returns True if any segment starts with `.`; false otherwise
|
|
300
347
|
*
|
|
301
348
|
* @example
|
|
302
349
|
* ```ts
|
|
@@ -307,16 +354,16 @@ export declare function isContainedPath(child: string, parent: string): boolean;
|
|
|
307
354
|
export declare function isDotfilePath(relativePath: string): boolean;
|
|
308
355
|
|
|
309
356
|
/**
|
|
310
|
-
*
|
|
357
|
+
* Narrows an unknown caught value to a {@link MultipartError}.
|
|
311
358
|
*
|
|
312
359
|
* @remarks
|
|
313
360
|
* Structural, not `instanceof` — tests that `value` is a non-null object
|
|
314
|
-
* carrying
|
|
315
|
-
* parser's
|
|
361
|
+
* carrying {@link MULTIPART_ERROR_BRAND}, a numeric `status`, and a `code`
|
|
362
|
+
* in the parser's {@link MultipartErrorCode} set (`'limit' | 'malformed' | 'rejected'`).
|
|
316
363
|
* Total: never throws, returns `false` for any off-shape input.
|
|
317
364
|
*
|
|
318
365
|
* @param value - The value to test (typically a `catch` binding)
|
|
319
|
-
* @returns
|
|
366
|
+
* @returns True if `value` is a {@link MultipartError}; false otherwise
|
|
320
367
|
*
|
|
321
368
|
* @example
|
|
322
369
|
* ```ts
|
|
@@ -325,24 +372,24 @@ export declare function isDotfilePath(relativePath: string): boolean;
|
|
|
325
372
|
* try {
|
|
326
373
|
* await parse(request)
|
|
327
374
|
* } catch (error) {
|
|
328
|
-
* if (isMultipartError(error)) console.log(error.status, error.
|
|
375
|
+
* if (isMultipartError(error)) console.log(error.status, error.code)
|
|
329
376
|
* }
|
|
330
377
|
* ```
|
|
331
378
|
*/
|
|
332
379
|
export declare function isMultipartError(value: unknown): value is MultipartError;
|
|
333
380
|
|
|
334
381
|
/**
|
|
335
|
-
*
|
|
382
|
+
* Checks whether a path segment is a Windows reserved device name (CVE-2025-27210).
|
|
336
383
|
*
|
|
337
384
|
* @remarks
|
|
338
385
|
* Normalizes superscript digits (`¹²³` → `123`) first, strips trailing dots
|
|
339
|
-
* and spaces (Windows drops them), takes the
|
|
386
|
+
* and spaces (Windows drops them), takes the stem before the first `.`,
|
|
340
387
|
* upper-cases it, and tests it against {@link RESERVED_DEVICE_NAMES}
|
|
341
388
|
* (`CON PRN AUX NUL COM1-9 LPT1-9`) — an exact-stem match only, so
|
|
342
389
|
* `console.js` and `nullable.css` are never flagged.
|
|
343
390
|
*
|
|
344
391
|
* @param segment - One path segment (no separators)
|
|
345
|
-
* @returns
|
|
392
|
+
* @returns True if `segment` names a reserved device; false otherwise
|
|
346
393
|
*
|
|
347
394
|
* @example
|
|
348
395
|
* ```ts
|
|
@@ -354,14 +401,14 @@ export declare function isMultipartError(value: unknown): value is MultipartErro
|
|
|
354
401
|
export declare function isReservedDeviceName(segment: string): boolean;
|
|
355
402
|
|
|
356
403
|
/**
|
|
357
|
-
*
|
|
404
|
+
* Checks whether `pathname` is `prefix` itself or lies under it on a segment
|
|
358
405
|
* boundary — the shared under-path test `resolveStaticPath`'s prefix strip
|
|
359
406
|
* and `createStatic`'s SPA-fallback `exclude` both apply, so `exclude:
|
|
360
407
|
* '/api'` matches `/api` and `/api/x` but never `/apifoo`.
|
|
361
408
|
*
|
|
362
409
|
* @param pathname - The request pathname to test
|
|
363
410
|
* @param prefix - The path prefix to test against
|
|
364
|
-
* @returns
|
|
411
|
+
* @returns True if `pathname` equals `prefix` or starts with `prefix` + `/`; false otherwise
|
|
365
412
|
*
|
|
366
413
|
* @example
|
|
367
414
|
* ```ts
|
|
@@ -372,7 +419,7 @@ export declare function isReservedDeviceName(segment: string): boolean;
|
|
|
372
419
|
export declare function isUnderPath(pathname: string, prefix: string): boolean;
|
|
373
420
|
|
|
374
421
|
/**
|
|
375
|
-
*
|
|
422
|
+
* Looks up the MIME type for a static file path by its extension.
|
|
376
423
|
*
|
|
377
424
|
* @param pathname - The file's path (only its extension is read)
|
|
378
425
|
* @returns The mapped MIME type, or {@link DEFAULT_CONTENT_TYPE} when unknown
|
|
@@ -385,12 +432,12 @@ export declare function isUnderPath(pathname: string, prefix: string): boolean;
|
|
|
385
432
|
export declare function lookupContentType(pathname: string): string;
|
|
386
433
|
|
|
387
434
|
/**
|
|
388
|
-
*
|
|
435
|
+
* Checks whether `bytes` contains `signature` at the requested offset.
|
|
389
436
|
*
|
|
390
437
|
* @param bytes - The bytes to inspect
|
|
391
438
|
* @param signature - The exact byte sequence to match
|
|
392
439
|
* @param offset - The starting byte offset, defaulting to zero
|
|
393
|
-
* @returns
|
|
440
|
+
* @returns True if the complete signature matches; false otherwise
|
|
394
441
|
*
|
|
395
442
|
* @example
|
|
396
443
|
* ```ts
|
|
@@ -400,59 +447,59 @@ export declare function lookupContentType(pathname: string): string;
|
|
|
400
447
|
export declare function matchesBytes(bytes: Uint8Array, signature: readonly number[], offset?: number): boolean;
|
|
401
448
|
|
|
402
449
|
/**
|
|
403
|
-
*
|
|
450
|
+
* Moves a staged uploaded file to its final `destination`.
|
|
404
451
|
*
|
|
405
452
|
* @remarks
|
|
406
453
|
* Attempts a `rename` first; on a cross-device error (`EXDEV`) falls back to
|
|
407
454
|
* `copyFile` + `unlink`. Returns a new frozen record with `status: 'moved'`
|
|
408
|
-
* and `path: destination` — the input record is never mutated.
|
|
455
|
+
* and `path: destination` — the input record is never mutated. The suite
|
|
456
|
+
* drives the `EXDEV` fallback only on a host whose device probe finds a
|
|
457
|
+
* second filesystem, so a single-device host leaves that branch unproven.
|
|
409
458
|
*
|
|
410
|
-
* @param file - The {@link
|
|
459
|
+
* @param file - The {@link UploadedFile} record to move
|
|
411
460
|
* @param destination - The final on-disk path
|
|
412
|
-
* @returns A new {@link
|
|
461
|
+
* @returns A new {@link UploadedFile} record reflecting the move
|
|
413
462
|
*
|
|
414
463
|
* @example
|
|
415
464
|
* ```ts
|
|
416
465
|
* const moved = await moveUploadedFile(file, '/var/uploads/final.png')
|
|
417
466
|
* ```
|
|
418
467
|
*/
|
|
419
|
-
export declare function moveUploadedFile(file:
|
|
468
|
+
export declare function moveUploadedFile(file: UploadedFile, destination: string): Promise<UploadedFile>;
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Holds the `Symbol.for` brand {@link MultipartError} carries so
|
|
472
|
+
* {@link isMultipartError} recognizes an instance across duplicate copies of
|
|
473
|
+
* this package — a registry symbol rather than a module-local `Symbol()`,
|
|
474
|
+
* which would mint an unequal symbol per copy.
|
|
475
|
+
*/
|
|
476
|
+
export declare const MULTIPART_ERROR_BRAND: unique symbol;
|
|
420
477
|
|
|
421
|
-
/**
|
|
478
|
+
/** Holds `16_384`, the maximum bytes a single multipart part's header block may occupy before it is malformed. */
|
|
422
479
|
export declare const MULTIPART_MAX_HEADER_BLOCK = 16384;
|
|
423
480
|
|
|
424
|
-
/**
|
|
481
|
+
/** Holds `65_536`, the maximum bytes scanned before the first multipart boundary is found before it is malformed. */
|
|
425
482
|
export declare const MULTIPART_MAX_PREAMBLE = 65536;
|
|
426
483
|
|
|
427
|
-
/** The HTTP status `createMultipart` renders for each {@link MultipartReason}. */
|
|
428
|
-
export declare const MULTIPART_REASON_STATUS: Readonly<Record<MultipartReason, number>>;
|
|
429
|
-
|
|
430
484
|
/**
|
|
431
|
-
*
|
|
432
|
-
* `
|
|
433
|
-
*
|
|
434
|
-
* @param contentType - The request's `Content-Type` header value, if present
|
|
435
|
-
* @returns The multipart boundary token, or `undefined` for a non-multipart
|
|
436
|
-
* (or malformed/boundary-less) content type
|
|
437
|
-
*
|
|
438
|
-
* @example
|
|
439
|
-
* ```ts
|
|
440
|
-
* multipartBoundary('multipart/form-data; boundary=abc123') // 'abc123'
|
|
441
|
-
* multipartBoundary('application/json') // undefined
|
|
442
|
-
* ```
|
|
485
|
+
* Holds the HTTP status `createMultipart` renders for each {@link MultipartErrorCode}:
|
|
486
|
+
* `'limit'` is 413, `'malformed'` is 400, and `'rejected'` is 415.
|
|
443
487
|
*/
|
|
444
|
-
export declare
|
|
488
|
+
export declare const MULTIPART_STATUS: Readonly<Record<MultipartErrorCode, number>>;
|
|
445
489
|
|
|
446
490
|
/**
|
|
447
|
-
*
|
|
491
|
+
* Represents an error `createMultipart` throws when a streamed multipart request fails
|
|
448
492
|
* a mid-stream limit, is structurally malformed, or has a file whose sniffed
|
|
449
493
|
* bytes are rejected by the configured `allowed` MIME list.
|
|
450
494
|
*
|
|
451
495
|
* @remarks
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
* `
|
|
455
|
-
*
|
|
496
|
+
* Extends the peer `HTTPError`, which already publishes the `status`,
|
|
497
|
+
* `context`, and brand members every fleet error of this shape carries, and
|
|
498
|
+
* adds the machine-readable `code` axis a caller narrows on. `status` is
|
|
499
|
+
* derived from `code` through {@link MULTIPART_STATUS} (limit → 413, malformed →
|
|
500
|
+
* 400, rejected → 415), so `createBoundary` — or any other `isHTTPError`-aware
|
|
501
|
+
* renderer — maps it without knowing this face's error type. Narrow a caught
|
|
502
|
+
* value to the richer type with {@link isMultipartError}.
|
|
456
503
|
*
|
|
457
504
|
* @example
|
|
458
505
|
* ```ts
|
|
@@ -461,74 +508,107 @@ export declare function multipartBoundary(contentType: string | null): string |
|
|
|
461
508
|
* throw new MultipartError('limit', 'too many files')
|
|
462
509
|
* ```
|
|
463
510
|
*/
|
|
464
|
-
export declare class MultipartError extends
|
|
465
|
-
readonly
|
|
466
|
-
readonly
|
|
467
|
-
|
|
468
|
-
constructor(reason: MultipartReason, message: string, context?: Readonly<Record<string, unknown>>);
|
|
511
|
+
export declare class MultipartError extends HTTPError {
|
|
512
|
+
readonly code: MultipartErrorCode;
|
|
513
|
+
readonly [MULTIPART_ERROR_BRAND] = true;
|
|
514
|
+
constructor(code: MultipartErrorCode, message: string, context?: Readonly<Record<string, unknown>>);
|
|
469
515
|
}
|
|
470
516
|
|
|
471
517
|
/**
|
|
472
|
-
*
|
|
518
|
+
* Names the reason `createMultipart` rejected a request — the machine-readable code
|
|
519
|
+
* {@link MultipartError} carries and maps onto its HTTP status: `'limit'` →
|
|
520
|
+
* 413, `'malformed'` → 400, `'rejected'` → 415.
|
|
521
|
+
*/
|
|
522
|
+
export declare type MultipartErrorCode = 'limit' | 'malformed' | 'rejected';
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Describes the per-category size/count caps `createMultipart` enforces mid-stream — the
|
|
526
|
+
* effective limits, every documented default already applied.
|
|
473
527
|
*
|
|
474
528
|
* @remarks
|
|
475
|
-
* - `file` — the maximum size in bytes of one uploaded file; defaults to
|
|
476
|
-
* {@link
|
|
477
|
-
* - `
|
|
478
|
-
* {@link
|
|
479
|
-
* - `field` — the maximum size in bytes of one text field; defaults to
|
|
480
|
-
* {@link
|
|
481
|
-
* - `
|
|
482
|
-
* {@link
|
|
529
|
+
* - `file.size` — the maximum size in bytes of one uploaded file; defaults to
|
|
530
|
+
* {@link DEFAULT_MULTIPART_FILE_SIZE}.
|
|
531
|
+
* - `file.count` — the maximum number of file parts; defaults to
|
|
532
|
+
* {@link DEFAULT_MULTIPART_FILE_COUNT}.
|
|
533
|
+
* - `field.size` — the maximum size in bytes of one text field; defaults to
|
|
534
|
+
* {@link DEFAULT_MULTIPART_FIELD_SIZE}.
|
|
535
|
+
* - `field.count` — the maximum number of text field parts; defaults to
|
|
536
|
+
* {@link DEFAULT_MULTIPART_FIELD_COUNT}.
|
|
483
537
|
* - `total` — the maximum combined byte size of the whole request body;
|
|
484
538
|
* defaults to {@link DEFAULT_MULTIPART_TOTAL}.
|
|
485
539
|
*/
|
|
486
540
|
export declare interface MultipartLimits {
|
|
487
|
-
readonly file
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
541
|
+
readonly file: {
|
|
542
|
+
readonly size: number;
|
|
543
|
+
readonly count: number;
|
|
544
|
+
};
|
|
545
|
+
readonly field: {
|
|
546
|
+
readonly size: number;
|
|
547
|
+
readonly count: number;
|
|
548
|
+
};
|
|
549
|
+
readonly total: number;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Describes the caller's partial {@link MultipartLimits} — `createMultipart`'s `limits`
|
|
554
|
+
* option, with every member optional.
|
|
555
|
+
*
|
|
556
|
+
* @remarks
|
|
557
|
+
* `resolveMultipartLimits` applies each documented default to an omitted leaf,
|
|
558
|
+
* so a caller states only the caps it wants to move.
|
|
559
|
+
* - `file` — the per-file caps: `size` in bytes, `count` of file parts.
|
|
560
|
+
* - `field` — the per-field caps: `size` in bytes, `count` of text field parts.
|
|
561
|
+
* - `total` — the maximum combined byte size of the whole request body.
|
|
562
|
+
*/
|
|
563
|
+
export declare interface MultipartLimitsInput {
|
|
564
|
+
readonly file?: {
|
|
565
|
+
readonly size?: number;
|
|
566
|
+
readonly count?: number;
|
|
567
|
+
};
|
|
568
|
+
readonly field?: {
|
|
569
|
+
readonly size?: number;
|
|
570
|
+
readonly count?: number;
|
|
571
|
+
};
|
|
491
572
|
readonly total?: number;
|
|
492
573
|
}
|
|
493
574
|
|
|
494
575
|
/**
|
|
495
|
-
*
|
|
576
|
+
* Configures `createMultipart` — node `fs`/`os`/`crypto`-backed streaming
|
|
496
577
|
* multipart upload parsing.
|
|
497
578
|
*
|
|
498
|
-
* @param options - See fields below
|
|
499
579
|
* @remarks
|
|
500
|
-
* - `limits` — see {@link
|
|
501
|
-
* - `allowed` — a MIME allow-list validated against
|
|
580
|
+
* - `limits` — see {@link MultipartLimitsInput}.
|
|
581
|
+
* - `allowed` — a MIME allow-list validated against sniffed (not merely
|
|
502
582
|
* declared) bytes; an empty array allows nothing. Omitted ⇒ no type
|
|
503
583
|
* rejection.
|
|
504
584
|
* - `directory` — the directory staged files are written to; defaults to
|
|
505
585
|
* `os.tmpdir()`.
|
|
506
586
|
*/
|
|
507
587
|
export declare interface MultipartOptions {
|
|
508
|
-
readonly limits?:
|
|
588
|
+
readonly limits?: MultipartLimitsInput;
|
|
509
589
|
readonly allowed?: readonly string[];
|
|
510
590
|
readonly directory?: string;
|
|
511
591
|
}
|
|
512
592
|
|
|
513
593
|
/**
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
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
|
|
596
|
+
* feature-detects.
|
|
517
597
|
*/
|
|
518
|
-
export declare
|
|
598
|
+
export declare const NODE_COMPRESSION_ENCODINGS: readonly Encoding[];
|
|
519
599
|
|
|
520
600
|
/**
|
|
521
|
-
*
|
|
601
|
+
* Configures the node face's `createCompression` — `node:zlib`-backed
|
|
522
602
|
* response compression.
|
|
523
603
|
*
|
|
524
|
-
* @param options - See fields below
|
|
525
604
|
* @remarks
|
|
526
605
|
* - `threshold` — the minimum buffered body size (bytes) worth compressing;
|
|
527
606
|
* defaults to {@link DEFAULT_COMPRESSION_THRESHOLD}.
|
|
528
607
|
* - `filter` — an additional predicate a response must pass before
|
|
529
|
-
* compression is attempted; defaults to always-allow.
|
|
530
|
-
* to
|
|
531
|
-
* type limitation documented on
|
|
608
|
+
* compression is attempted; defaults to always-allow. The offered codings
|
|
609
|
+
* are fixed to {@link NODE_COMPRESSION_ENCODINGS} and are not configurable
|
|
610
|
+
* (see the peer `Encoding` type limitation documented on
|
|
611
|
+
* `createCompression`).
|
|
532
612
|
*/
|
|
533
613
|
export declare interface NodeCompressionOptions {
|
|
534
614
|
readonly threshold?: number;
|
|
@@ -536,34 +616,38 @@ export declare interface NodeCompressionOptions {
|
|
|
536
616
|
}
|
|
537
617
|
|
|
538
618
|
/**
|
|
539
|
-
* Stream-
|
|
540
|
-
* the mid-stream state machine `createMultipart` drives
|
|
619
|
+
* Stream-parses a `multipart/form-data` request into its files and fields —
|
|
620
|
+
* the mid-stream state machine `createMultipart` drives.
|
|
541
621
|
*
|
|
542
622
|
* @remarks
|
|
543
|
-
* Reads `request.body` chunk by chunk
|
|
544
|
-
*
|
|
623
|
+
* Reads `request.body` chunk by chunk through its `ReadableStream` reader —
|
|
624
|
+
* never buffers the whole body — enforcing every {@link MultipartLimits} cap
|
|
545
625
|
* the instant it is exceeded (reading stops, every already-staged temp file
|
|
546
|
-
* is deleted, throws {@link MultipartError} with
|
|
626
|
+
* is deleted, throws {@link MultipartError} with code `'limit'`). Each file
|
|
547
627
|
* part streams to `join(directory, randomUUID())` — the client's declared
|
|
548
|
-
* filename is
|
|
628
|
+
* filename is metadata only, never a path component. A field or file part
|
|
549
629
|
* named `__proto__` / `constructor` / `prototype` is silently skipped and
|
|
550
630
|
* never keyed onto the returned {@link MultipartBody} (a skipped file's
|
|
551
631
|
* staged temp file is unlinked immediately, since it can never be
|
|
552
632
|
* referenced). A file part with an empty declared filename (`filename=""`)
|
|
553
|
-
*
|
|
633
|
+
* and a zero-byte body — the browser convention for an unselected optional
|
|
554
634
|
* `<input type="file">` — is a silent no-op: its temp file is unlinked, it is
|
|
555
|
-
* never counted against the `
|
|
556
|
-
* check. A malformed
|
|
635
|
+
* never counted against the `file.count` limit, and it never runs the
|
|
636
|
+
* `allowed` check. A malformed
|
|
557
637
|
* structure (missing/unterminated boundary, nameless part, an oversized
|
|
558
638
|
* header block, or a preamble exceeding {@link MULTIPART_MAX_PREAMBLE} before
|
|
559
|
-
* the first boundary) throws with
|
|
560
|
-
* against the configured `allowed` MIME list
|
|
639
|
+
* the first boundary) throws with code `'malformed'`. A file is accepted
|
|
640
|
+
* against the configured `allowed` MIME list exactly when its sniffed bytes detect a
|
|
561
641
|
* type present in the list — sniff-authoritative, independent of whether the
|
|
562
642
|
* declared `Content-Type` matches (that agreement is exposed separately as
|
|
563
|
-
* `validated`); otherwise throws with
|
|
643
|
+
* `validated`); otherwise throws with code `'rejected'`. A
|
|
564
644
|
* request abort mid-upload triggers the same fail-closed cleanup as a limit
|
|
565
645
|
* breach. Returns `undefined` for a non-multipart request (untouched).
|
|
566
646
|
*
|
|
647
|
+
* Staging defaults to a process-owned directory created once (lazily,
|
|
648
|
+
* memoized across calls) with `mkdtemp` under `os.tmpdir()` and locked to
|
|
649
|
+
* mode `0o700`; `options.directory` overrides it.
|
|
650
|
+
*
|
|
567
651
|
* @param request - The incoming multipart request
|
|
568
652
|
* @param options - See {@link MultipartOptions}
|
|
569
653
|
* @returns The parsed {@link MultipartBody}, or `undefined` when the request
|
|
@@ -579,41 +663,41 @@ export declare interface NodeCompressionOptions {
|
|
|
579
663
|
export declare function parseMultipartRequest(request: Request, options?: MultipartOptions): Promise<MultipartBody | undefined>;
|
|
580
664
|
|
|
581
665
|
/**
|
|
582
|
-
*
|
|
666
|
+
* Parses one multipart part's raw header block into its `name` (from
|
|
583
667
|
* `Content-Disposition`), optional `filename`, and optional `Content-Type`.
|
|
584
668
|
*
|
|
585
669
|
* @param block - The raw header block for one multipart part (before the
|
|
586
670
|
* terminating blank line)
|
|
587
|
-
* @returns The parsed `name`, `filename`, and `
|
|
588
|
-
*
|
|
671
|
+
* @returns The parsed `name`, `filename`, and `mime` (each `undefined` when
|
|
672
|
+
* absent)
|
|
589
673
|
*
|
|
590
674
|
* @example
|
|
591
675
|
* ```ts
|
|
592
676
|
* parsePartHeaders('Content-Disposition: form-data; name="title"')
|
|
593
|
-
* // { name: 'title', filename: undefined,
|
|
677
|
+
* // { name: 'title', filename: undefined, mime: undefined }
|
|
594
678
|
* ```
|
|
595
679
|
*/
|
|
596
680
|
export declare function parsePartHeaders(block: string): PartHeaders;
|
|
597
681
|
|
|
598
682
|
/**
|
|
599
|
-
*
|
|
683
|
+
* Describes one multipart part's parsed header block — `parsePartHeaders`'s return
|
|
600
684
|
* shape.
|
|
601
685
|
*
|
|
602
686
|
* @remarks
|
|
603
687
|
* - `name` — the `Content-Disposition` `name` parameter, or `undefined` when absent.
|
|
604
688
|
* - `filename` — the `Content-Disposition` `filename` parameter, or `undefined` when absent.
|
|
605
|
-
* - `
|
|
689
|
+
* - `mime` — the part's declared `Content-Type` header value, or `undefined` when absent.
|
|
606
690
|
*/
|
|
607
691
|
export declare interface PartHeaders {
|
|
608
692
|
readonly name: string | undefined;
|
|
609
693
|
readonly filename: string | undefined;
|
|
610
|
-
readonly
|
|
694
|
+
readonly mime: string | undefined;
|
|
611
695
|
}
|
|
612
696
|
|
|
613
697
|
/**
|
|
614
|
-
*
|
|
698
|
+
* Reads a staged/moved uploaded file's full contents into memory.
|
|
615
699
|
*
|
|
616
|
-
* @param file - The {@link
|
|
700
|
+
* @param file - The {@link UploadedFile} record to read
|
|
617
701
|
* @returns The file's bytes
|
|
618
702
|
*
|
|
619
703
|
* @example
|
|
@@ -621,46 +705,65 @@ export declare interface PartHeaders {
|
|
|
621
705
|
* const bytes = await readUploadedFile(file)
|
|
622
706
|
* ```
|
|
623
707
|
*/
|
|
624
|
-
export declare function readUploadedFile(file:
|
|
708
|
+
export declare function readUploadedFile(file: UploadedFile): Promise<Uint8Array>;
|
|
625
709
|
|
|
626
710
|
/**
|
|
627
|
-
* Windows reserved device-name stems (CVE-2025-27210) — matched
|
|
711
|
+
* Lists the Windows reserved device-name stems (CVE-2025-27210) — matched
|
|
628
712
|
* case-insensitively against the segment's stem (before its first `.`).
|
|
629
713
|
*/
|
|
630
714
|
export declare const RESERVED_DEVICE_NAMES: ReadonlySet<string>;
|
|
631
715
|
|
|
632
716
|
/**
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
* locked to mode `0o700`.
|
|
717
|
+
* Canonicalizes `candidate` and returns it only when it lies inside `rootReal`
|
|
718
|
+
* — the shared realpath-then-contain step `createStatic` applies to a
|
|
719
|
+
* directory index and to its SPA shell.
|
|
637
720
|
*
|
|
638
|
-
* @
|
|
721
|
+
* @remarks
|
|
722
|
+
* Total: a `realpath` failure (a dangling symlink, a missing file, a
|
|
723
|
+
* permission refusal) and an escape from `rootReal` both resolve `undefined`,
|
|
724
|
+
* so a caller that treats those two outcomes identically needs no `try`. A
|
|
725
|
+
* caller that must tell them apart keeps its own explicit branch instead.
|
|
726
|
+
*
|
|
727
|
+
* @param candidate - The on-disk path to canonicalize
|
|
728
|
+
* @param rootReal - The already-canonical root the result must lie under
|
|
729
|
+
* @returns The canonical path inside `rootReal`, or `undefined`
|
|
639
730
|
*
|
|
640
731
|
* @example
|
|
641
732
|
* ```ts
|
|
642
|
-
*
|
|
733
|
+
* await resolveContainedRealPath('/srv/public/index.html', '/srv/public')
|
|
734
|
+
* // '/srv/public/index.html'
|
|
643
735
|
* ```
|
|
644
736
|
*/
|
|
645
|
-
export declare function
|
|
737
|
+
export declare function resolveContainedRealPath(candidate: string, rootReal: string): Promise<string | undefined>;
|
|
646
738
|
|
|
647
739
|
/**
|
|
648
|
-
*
|
|
649
|
-
* every documented default.
|
|
740
|
+
* Resolves `createMultipart`'s effective {@link MultipartLimits}, applying
|
|
741
|
+
* every documented default to an omitted leaf.
|
|
650
742
|
*
|
|
651
743
|
* @param limits - The caller's partial limits
|
|
652
744
|
* @returns The fully-resolved limits
|
|
745
|
+
*
|
|
746
|
+
* @example
|
|
747
|
+
* ```ts
|
|
748
|
+
* resolveMultipartLimits({ file: { size: 1_048_576 } })
|
|
749
|
+
* ```
|
|
653
750
|
*/
|
|
654
|
-
export declare function resolveMultipartLimits(limits:
|
|
751
|
+
export declare function resolveMultipartLimits(limits: MultipartLimitsInput | undefined): MultipartLimits;
|
|
655
752
|
|
|
656
753
|
/**
|
|
657
|
-
*
|
|
754
|
+
* Resolves the fixed SPA shell path when a static-file miss is eligible for
|
|
658
755
|
* fallback.
|
|
659
756
|
*
|
|
757
|
+
* @remarks
|
|
758
|
+
* `GET` and `HEAD` are both eligible, and resolve the same shell: `HEAD` is
|
|
759
|
+
* defined as `GET` without a body (RFC 9110 §9.3.2), so a navigation probe
|
|
760
|
+
* that answered `404` while its `GET` answered `200` would report a resource
|
|
761
|
+
* the very next request serves.
|
|
762
|
+
*
|
|
660
763
|
* @param root - The configured static root
|
|
661
764
|
* @param index - The configured shell filename
|
|
662
765
|
* @param exclude - The URL prefix excluded from fallback
|
|
663
|
-
* @param method - The request method
|
|
766
|
+
* @param method - The request method; only `GET` and `HEAD` are eligible
|
|
664
767
|
* @param pathname - The request pathname
|
|
665
768
|
* @param accept - The request's `Accept` header value
|
|
666
769
|
* @returns The fixed shell path, or `undefined` when fallback is ineligible
|
|
@@ -674,14 +777,14 @@ export declare function resolveMultipartLimits(limits: MultipartLimits | undefin
|
|
|
674
777
|
export declare function resolveStaticFallbackPath(root: string, index: string, exclude: string, method: string, pathname: string, accept: string): string | undefined;
|
|
675
778
|
|
|
676
779
|
/**
|
|
677
|
-
*
|
|
678
|
-
* when it cannot — the traversal guard,
|
|
679
|
-
*
|
|
780
|
+
* Resolves a request pathname to an on-disk path under `root`, or `undefined`
|
|
781
|
+
* when it cannot — the traversal guard, whose algorithm and order are exact:
|
|
782
|
+
* strip `prefix` on a segment boundary → `decodeURIComponent` (a
|
|
680
783
|
* malformed escape refuses, never throws) → reject a NUL byte → strip the
|
|
681
|
-
* leading path separator
|
|
682
|
-
* genuine climbing segment) → `normalize` → refuse any Windows
|
|
683
|
-
* device-name segment ({@link isReservedDeviceName}) → `resolve` and
|
|
684
|
-
* 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`.
|
|
685
788
|
*
|
|
686
789
|
* @param root - The absolute root directory every result must resolve under
|
|
687
790
|
* @param prefix - An optional URL path prefix stripped on a segment boundary
|
|
@@ -698,24 +801,29 @@ export declare function resolveStaticFallbackPath(root: string, index: string, e
|
|
|
698
801
|
export declare function resolveStaticPath(root: string, prefix: string | undefined, pathname: string): string | undefined;
|
|
699
802
|
|
|
700
803
|
/**
|
|
701
|
-
*
|
|
804
|
+
* Configures `createStatic` — node `fs`-backed static file serving.
|
|
702
805
|
*
|
|
703
|
-
* @param options - See fields below
|
|
704
806
|
* @remarks
|
|
705
|
-
* - `root` — the directory every request resolves under, resolved
|
|
706
|
-
* construction.
|
|
807
|
+
* - `root` — the required directory every request resolves under, resolved
|
|
808
|
+
* once at construction.
|
|
707
809
|
* - `prefix` — a URL path prefix stripped (on a segment boundary) before
|
|
708
810
|
* resolving under `root`.
|
|
709
|
-
* - `index` — the filename served for a directory hit
|
|
710
|
-
* {@link DEFAULT_STATIC_INDEX}.
|
|
811
|
+
* - `index` — the filename served for a directory hit and by the SPA
|
|
812
|
+
* fallback; defaults to {@link DEFAULT_STATIC_INDEX}. The fallback serves
|
|
813
|
+
* it whatever `dotfiles` is set to, because this path is
|
|
814
|
+
* operator-configured rather than request-derived.
|
|
711
815
|
* - `dotfiles` — the policy for a path with a dotfile segment: `'ignore'`
|
|
712
|
-
* (
|
|
713
|
-
*
|
|
816
|
+
* (falls through to `next()`), `'deny'` (403), or `'allow'` (serves it);
|
|
817
|
+
* defaults to {@link DEFAULT_STATIC_DOTFILES}.
|
|
714
818
|
* - `cache` — `Cache-Control: max-age=<cache>` in seconds, when set.
|
|
715
819
|
* - `etag` — whether to compute and honor a weak file `ETag`; defaults to `true`.
|
|
716
820
|
* - `fallback` — SPA fallback: `false` (default, off), `true` (on, excluding
|
|
717
821
|
* {@link DEFAULT_STATIC_FALLBACK_EXCLUDE}), or `{ exclude }` for a custom
|
|
718
|
-
* excluded prefix.
|
|
822
|
+
* excluded prefix. An eligible `GET` or `HEAD` navigation miss answers with
|
|
823
|
+
* `index` through the same handle-`fstat` header block a directly requested
|
|
824
|
+
* file answers through, so `cache`, `etag`, conditional revalidation,
|
|
825
|
+
* `HEAD`, and ranges are identical on both routes; `index` reaches the
|
|
826
|
+
* client through this route whatever `dotfiles` is set to.
|
|
719
827
|
*/
|
|
720
828
|
export declare interface StaticOptions {
|
|
721
829
|
readonly root: string;
|
|
@@ -730,35 +838,34 @@ export declare interface StaticOptions {
|
|
|
730
838
|
}
|
|
731
839
|
|
|
732
840
|
/**
|
|
733
|
-
*
|
|
841
|
+
* Adapts a `node:fs` read stream over a file path (or an already-open
|
|
734
842
|
* `FileHandle`) into a DOM-compatible `ReadableStream<Uint8Array>` — the
|
|
735
843
|
* single shared node↔web stream bridge every static-file and uploaded-file
|
|
736
844
|
* response body routes through.
|
|
737
845
|
*
|
|
738
846
|
* @remarks
|
|
739
|
-
*
|
|
847
|
+
* Pull-driven, not push-driven: the underlying node stream's async iterator
|
|
740
848
|
* is only advanced (`iterator.next()`) from inside `pull(controller)`, which
|
|
741
849
|
* the web `ReadableStream` invokes exactly when its internal queue has room
|
|
742
850
|
* for more data. Exactly one disk chunk is read and enqueued per `pull` —
|
|
743
851
|
* never more — so a slow or stalled consumer (a stalled HTTP connection)
|
|
744
|
-
*
|
|
852
|
+
* stops triggering `pull` calls and the source stops reading ahead;
|
|
745
853
|
* this is genuine consumer backpressure, not the "naturally backpressured"
|
|
746
854
|
* `for await`/`enqueue` pattern (which does not block on a slow consumer at
|
|
747
|
-
* all,
|
|
855
|
+
* all, because `enqueue` returns synchronously). The controller is closed on
|
|
748
856
|
* iterator completion and errored (never thrown into the process) on a
|
|
749
|
-
* mid-stream read failure. Cancelling the returned `ReadableStream` (
|
|
750
|
-
* consumer aborts the response) calls the iterator's `return()`, which
|
|
857
|
+
* mid-stream read failure. Cancelling the returned `ReadableStream` (for
|
|
858
|
+
* example the consumer aborts the response) calls the iterator's `return()`, which
|
|
751
859
|
* destroys the underlying node read stream so the file descriptor is
|
|
752
|
-
* released. When `
|
|
860
|
+
* released. When `source` is a `FileHandle`, `FileHandle.createReadStream`'s
|
|
753
861
|
* default `autoClose` closes the handle on every terminal path (end, error,
|
|
754
|
-
* or `destroy()`
|
|
755
|
-
* separate `handle.close()` for a
|
|
862
|
+
* or `destroy()` through the iterator's `return()`) — the caller never needs a
|
|
863
|
+
* separate `handle.close()` for a `FileHandle` passed as `source`.
|
|
756
864
|
*
|
|
757
865
|
* @param source - The absolute on-disk file path to stream, or an already-open
|
|
758
|
-
* `FileHandle` (
|
|
866
|
+
* `FileHandle` (for example one already `fstat`'d so the served bytes match the
|
|
759
867
|
* headers computed from that same `fstat`)
|
|
760
|
-
* @param range - An optional inclusive byte range
|
|
761
|
-
* 0-indexed and inclusive, matching `node:fs`'s `createReadStream` options)
|
|
868
|
+
* @param range - An optional inclusive byte range; see {@link ByteRange}
|
|
762
869
|
* @returns A `ReadableStream<Uint8Array>` valid as a fetch `BodyInit`
|
|
763
870
|
*
|
|
764
871
|
* @example
|
|
@@ -766,15 +873,12 @@ export declare interface StaticOptions {
|
|
|
766
873
|
* new Response(streamFile('/srv/public/index.html'))
|
|
767
874
|
* ```
|
|
768
875
|
*/
|
|
769
|
-
export declare function streamFile(source: string | FileHandle, range?:
|
|
770
|
-
readonly start: number;
|
|
771
|
-
readonly end: number;
|
|
772
|
-
}): ReadableStream<Uint8Array>;
|
|
876
|
+
export declare function streamFile(source: string | FileHandle, range?: ByteRange): ReadableStream<Uint8Array>;
|
|
773
877
|
|
|
774
878
|
/**
|
|
775
|
-
*
|
|
879
|
+
* Opens a staged/moved uploaded file as a web `ReadableStream`.
|
|
776
880
|
*
|
|
777
|
-
* @param file - The {@link
|
|
881
|
+
* @param file - The {@link UploadedFile} record to stream
|
|
778
882
|
* @returns A `ReadableStream<Uint8Array>` over the file's current on-disk path
|
|
779
883
|
*
|
|
780
884
|
* @example
|
|
@@ -782,10 +886,10 @@ export declare function streamFile(source: string | FileHandle, range?: {
|
|
|
782
886
|
* new Response(streamUploadedFile(file))
|
|
783
887
|
* ```
|
|
784
888
|
*/
|
|
785
|
-
export declare function streamUploadedFile(file:
|
|
889
|
+
export declare function streamUploadedFile(file: UploadedFile): ReadableStream<Uint8Array>;
|
|
786
890
|
|
|
787
891
|
/**
|
|
788
|
-
*
|
|
892
|
+
* Attempts to unlink every still-`'staged'` file in a parsed
|
|
789
893
|
* {@link MultipartBody} — the fail-closed cleanup `createMultipart` runs when
|
|
790
894
|
* its downstream handler throws, mirroring `parseMultipartRequest`'s own
|
|
791
895
|
* cleanup pattern (a missing file is already gone; failures are swallowed).
|
|
@@ -801,30 +905,7 @@ export declare function streamUploadedFile(file: UploadedFileInterface): Readabl
|
|
|
801
905
|
export declare function unlinkStagedFiles(body: MultipartBody): Promise<void>;
|
|
802
906
|
|
|
803
907
|
/**
|
|
804
|
-
*
|
|
805
|
-
* {@link UploadedFileInterface} record.
|
|
806
|
-
*
|
|
807
|
-
* @remarks
|
|
808
|
-
* - `field` — the multipart field name the file was submitted under.
|
|
809
|
-
* - `name` — the client-declared filename (metadata only).
|
|
810
|
-
* - `size` — the file's byte size.
|
|
811
|
-
* - `mime` — the sniffed MIME type.
|
|
812
|
-
* - `validated` — `true` when the sniffed type matches the declared `Content-Type`.
|
|
813
|
-
* - `status` — see {@link UploadStatus}.
|
|
814
|
-
* - `path` — the file's current on-disk path.
|
|
815
|
-
*/
|
|
816
|
-
export declare interface UploadedFileInput {
|
|
817
|
-
readonly field: string;
|
|
818
|
-
readonly name: string;
|
|
819
|
-
readonly size: number;
|
|
820
|
-
readonly mime: string;
|
|
821
|
-
readonly validated: boolean;
|
|
822
|
-
readonly status: UploadStatus;
|
|
823
|
-
readonly path: string;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
/**
|
|
827
|
-
* One uploaded file's post-parse record — the node-bound, richer sibling of
|
|
908
|
+
* Describes one uploaded file's post-parse record — the node-bound, richer sibling of
|
|
828
909
|
* the pure core's {@link MultipartFile} (identical fields, `status` narrowed
|
|
829
910
|
* to {@link UploadStatus}). Structurally assignable into {@link MultipartFile}
|
|
830
911
|
* so a `createMultipart`-built {@link MultipartBody} satisfies the shared
|
|
@@ -832,21 +913,24 @@ export declare interface UploadedFileInput {
|
|
|
832
913
|
*
|
|
833
914
|
* @remarks
|
|
834
915
|
* - `field` — the multipart field name the file was submitted under.
|
|
835
|
-
* - `name` — the client-declared filename (
|
|
916
|
+
* - `name` — the client-declared filename (metadata only — never used to
|
|
836
917
|
* build a filesystem path).
|
|
837
918
|
* - `size` — the file's byte size.
|
|
838
|
-
* - `mime` — the
|
|
839
|
-
*
|
|
840
|
-
* `
|
|
919
|
+
* - `mime` — the sniffed (magic-byte-detected) MIME type when a signature
|
|
920
|
+
* matches; otherwise the part's declared `Content-Type`; otherwise
|
|
921
|
+
* {@link DEFAULT_CONTENT_TYPE}. Read `validated` to tell which.
|
|
922
|
+
* - `validated` — `true` when a signature matched and the sniffed type equals
|
|
923
|
+
* the declared `Content-Type`, so `mime` is the sniffed fact. `false` means
|
|
924
|
+
* `mime` may be the client-declared value.
|
|
841
925
|
* - `status` — see {@link UploadStatus}.
|
|
842
926
|
* - `path` — the file's current on-disk path.
|
|
843
927
|
*/
|
|
844
|
-
export declare interface
|
|
928
|
+
export declare interface UploadedFile extends Omit<MultipartFile, 'status'> {
|
|
845
929
|
readonly status: UploadStatus;
|
|
846
930
|
}
|
|
847
931
|
|
|
848
932
|
/**
|
|
849
|
-
*
|
|
933
|
+
* Names the lifecycle stage of one staged upload's temp file.
|
|
850
934
|
*
|
|
851
935
|
* @remarks
|
|
852
936
|
* `'staged'` — written to the configured temp directory under a random name,
|