@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
|
@@ -10,7 +10,10 @@ let node_crypto = require("node:crypto");
|
|
|
10
10
|
let node_os = require("node:os");
|
|
11
11
|
let _src_core = require("../core/index.cjs");
|
|
12
12
|
//#region src/server/constants.ts
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Holds the HTTP status `createMultipart` renders for each {@link MultipartErrorCode}:
|
|
15
|
+
* `'limit'` is 413, `'malformed'` is 400, and `'rejected'` is 415.
|
|
16
|
+
*/
|
|
14
17
|
var MULTIPART_STATUS = Object.freeze({
|
|
15
18
|
limit: 413,
|
|
16
19
|
malformed: 400,
|
|
@@ -23,33 +26,33 @@ var MULTIPART_STATUS = Object.freeze({
|
|
|
23
26
|
* which would mint an unequal symbol per copy.
|
|
24
27
|
*/
|
|
25
28
|
var MULTIPART_ERROR_BRAND = Symbol.for("@orkestrel/middleware.MultipartError");
|
|
26
|
-
/** Names `createStatic`'s default directory-index filename. */
|
|
29
|
+
/** Names `'index.html'`, `createStatic`'s default directory-index filename. */
|
|
27
30
|
var DEFAULT_STATIC_INDEX = "index.html";
|
|
28
|
-
/** Names `createStatic`'s `fallback: true` default excluded path prefix. */
|
|
31
|
+
/** Names `'/api'`, `createStatic`'s `fallback: true` default excluded path prefix. */
|
|
29
32
|
var DEFAULT_STATIC_FALLBACK_EXCLUDE = "/api";
|
|
30
|
-
/** Names `createStatic`'s default policy for a path carrying a dotfile segment. */
|
|
33
|
+
/** Names `'ignore'`, `createStatic`'s default policy for a path carrying a dotfile segment. */
|
|
31
34
|
var DEFAULT_STATIC_DOTFILES = "ignore";
|
|
32
35
|
/**
|
|
33
|
-
* Lists the content-codings the node face's `createCompression`
|
|
34
|
-
* `node:zlib` guarantees on every Node runtime, so this face never
|
|
36
|
+
* Lists `['gzip', 'deflate']`, the content-codings the node face's `createCompression`
|
|
37
|
+
* offers — what `node:zlib` guarantees on every Node runtime, so this face never
|
|
35
38
|
* feature-detects.
|
|
36
39
|
*/
|
|
37
40
|
var NODE_COMPRESSION_ENCODINGS = Object.freeze(["gzip", "deflate"]);
|
|
38
|
-
/** Names the MIME type served when a file extension has no known mapping. */
|
|
41
|
+
/** Names `'application/octet-stream'`, the MIME type served when a file extension has no known mapping. */
|
|
39
42
|
var DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
40
|
-
/** Holds `createMultipart`'s default per-file byte-size cap. */
|
|
43
|
+
/** Holds `10_485_760`, `createMultipart`'s default per-file byte-size cap. */
|
|
41
44
|
var DEFAULT_MULTIPART_FILE_SIZE = 10485760;
|
|
42
|
-
/** Holds `createMultipart`'s default maximum file-part count. */
|
|
45
|
+
/** Holds `10`, `createMultipart`'s default maximum file-part count. */
|
|
43
46
|
var DEFAULT_MULTIPART_FILE_COUNT = 10;
|
|
44
|
-
/** Holds `createMultipart`'s default per-field byte-size cap. */
|
|
47
|
+
/** Holds `65_536`, `createMultipart`'s default per-field byte-size cap. */
|
|
45
48
|
var DEFAULT_MULTIPART_FIELD_SIZE = 65536;
|
|
46
|
-
/** Holds `createMultipart`'s default maximum field-part count. */
|
|
49
|
+
/** Holds `100`, `createMultipart`'s default maximum field-part count. */
|
|
47
50
|
var DEFAULT_MULTIPART_FIELD_COUNT = 100;
|
|
48
|
-
/** Holds `createMultipart`'s default combined request-body byte-size cap. */
|
|
51
|
+
/** Holds `52_428_800`, `createMultipart`'s default combined request-body byte-size cap. */
|
|
49
52
|
var DEFAULT_MULTIPART_TOTAL = 52428800;
|
|
50
|
-
/** Holds the maximum bytes a single multipart part's header block may occupy before it is malformed. */
|
|
53
|
+
/** Holds `16_384`, the maximum bytes a single multipart part's header block may occupy before it is malformed. */
|
|
51
54
|
var MULTIPART_MAX_HEADER_BLOCK = 16384;
|
|
52
|
-
/** Holds the maximum bytes scanned before the first multipart boundary is found before it is malformed. */
|
|
55
|
+
/** Holds `65_536`, the maximum bytes scanned before the first multipart boundary is found before it is malformed. */
|
|
53
56
|
var MULTIPART_MAX_PREAMBLE = 65536;
|
|
54
57
|
/**
|
|
55
58
|
* Lists the Windows reserved device-name stems (CVE-2025-27210) — matched
|
|
@@ -168,7 +171,7 @@ function isMultipartError(value) {
|
|
|
168
171
|
//#endregion
|
|
169
172
|
//#region src/server/helpers.ts
|
|
170
173
|
/**
|
|
171
|
-
* Checks whether `pathname` is `prefix` itself or lies under it on a
|
|
174
|
+
* Checks whether `pathname` is `prefix` itself or lies under it on a segment
|
|
172
175
|
* boundary — the shared under-path test `resolveStaticPath`'s prefix strip
|
|
173
176
|
* and `createStatic`'s SPA-fallback `exclude` both apply, so `exclude:
|
|
174
177
|
* '/api'` matches `/api` and `/api/x` but never `/apifoo`.
|
|
@@ -193,7 +196,7 @@ function isUnderPath(pathname, prefix) {
|
|
|
193
196
|
* fallback.
|
|
194
197
|
*
|
|
195
198
|
* @remarks
|
|
196
|
-
* `GET` and `HEAD` are both eligible, and resolve the
|
|
199
|
+
* `GET` and `HEAD` are both eligible, and resolve the same shell: `HEAD` is
|
|
197
200
|
* defined as `GET` without a body (RFC 9110 §9.3.2), so a navigation probe
|
|
198
201
|
* that answered `404` while its `GET` answered `200` would report a resource
|
|
199
202
|
* the very next request serves.
|
|
@@ -221,11 +224,11 @@ function resolveStaticFallbackPath(root, index, exclude, method, pathname, accep
|
|
|
221
224
|
}
|
|
222
225
|
/**
|
|
223
226
|
* Checks whether `child` is `parent` itself or lies inside it on-disk — the
|
|
224
|
-
*
|
|
227
|
+
* filesystem containment predicate `createStatic` applies to `fs.realpath`
|
|
225
228
|
* output (never to a URL pathname — that is {@link isUnderPath}'s job).
|
|
226
229
|
*
|
|
227
230
|
* @remarks
|
|
228
|
-
* Argument order is `(child, parent)` — deliberately the
|
|
231
|
+
* Argument order is `(child, parent)` — deliberately the opposite conceptual
|
|
229
232
|
* order from {@link isUnderPath}`(pathname, prefix)`, so a call site cannot
|
|
230
233
|
* casually swap one predicate in for the other. Built on `path.relative`,
|
|
231
234
|
* this is separator-correct on both POSIX (`/`) and win32 (`\`) — unlike a
|
|
@@ -248,14 +251,14 @@ function isContainedPath(child, parent) {
|
|
|
248
251
|
return rel.length > 0 && rel !== ".." && !rel.startsWith(`..${node_path.sep}`) && !(0, node_path.isAbsolute)(rel);
|
|
249
252
|
}
|
|
250
253
|
/**
|
|
251
|
-
* Resolves a request pathname to an on-disk path
|
|
254
|
+
* Resolves a request pathname to an on-disk path under `root`, or `undefined`
|
|
252
255
|
* when it cannot — the traversal guard, whose algorithm and order are exact:
|
|
253
256
|
* strip `prefix` on a segment boundary → `decodeURIComponent` (a
|
|
254
257
|
* malformed escape refuses, never throws) → reject a NUL byte → strip the
|
|
255
|
-
* leading path separator
|
|
256
|
-
* genuine climbing segment) → `normalize` → refuse any Windows
|
|
257
|
-
* device-name segment ({@link isReservedDeviceName}) → `resolve` and
|
|
258
|
-
* the result under `root`.
|
|
258
|
+
* leading path separator first (so a leading `..` survives `normalize` as a
|
|
259
|
+
* genuine climbing segment) → `normalize` → refuse any Windows
|
|
260
|
+
* reserved-device-name segment ({@link isReservedDeviceName}) → `resolve` and
|
|
261
|
+
* require the result under `root`.
|
|
259
262
|
*
|
|
260
263
|
* @param root - The absolute root directory every result must resolve under
|
|
261
264
|
* @param prefix - An optional URL path prefix stripped on a segment boundary
|
|
@@ -324,7 +327,7 @@ async function resolveContainedRealPath(candidate, rootReal) {
|
|
|
324
327
|
*
|
|
325
328
|
* @remarks
|
|
326
329
|
* Normalizes superscript digits (`¹²³` → `123`) first, strips trailing dots
|
|
327
|
-
* and spaces (Windows drops them), takes the
|
|
330
|
+
* and spaces (Windows drops them), takes the stem before the first `.`,
|
|
328
331
|
* upper-cases it, and tests it against {@link RESERVED_DEVICE_NAMES}
|
|
329
332
|
* (`CON PRN AUX NUL COM1-9 LPT1-9`) — an exact-stem match only, so
|
|
330
333
|
* `console.js` and `nullable.css` are never flagged.
|
|
@@ -643,7 +646,7 @@ async function unlinkStagedFiles(body) {
|
|
|
643
646
|
* response body routes through.
|
|
644
647
|
*
|
|
645
648
|
* @remarks
|
|
646
|
-
*
|
|
649
|
+
* Pull-driven, not push-driven: the underlying node stream's async iterator
|
|
647
650
|
* is only advanced (`iterator.next()`) from inside `pull(controller)`, which
|
|
648
651
|
* the web `ReadableStream` invokes exactly when its internal queue has room
|
|
649
652
|
* for more data. Exactly one disk chunk is read and enqueued per `pull` —
|
|
@@ -980,30 +983,30 @@ var MultipartParser = class MultipartParser {
|
|
|
980
983
|
*
|
|
981
984
|
* @remarks
|
|
982
985
|
* Reads `request.body` chunk by chunk through its `ReadableStream` reader —
|
|
983
|
-
*
|
|
986
|
+
* never buffers the whole body — enforcing every {@link MultipartLimits} cap
|
|
984
987
|
* the instant it is exceeded (reading stops, every already-staged temp file
|
|
985
988
|
* is deleted, throws {@link MultipartError} with code `'limit'`). Each file
|
|
986
989
|
* part streams to `join(directory, randomUUID())` — the client's declared
|
|
987
|
-
* filename is
|
|
990
|
+
* filename is metadata only, never a path component. A field or file part
|
|
988
991
|
* named `__proto__` / `constructor` / `prototype` is silently skipped and
|
|
989
992
|
* never keyed onto the returned {@link MultipartBody} (a skipped file's
|
|
990
993
|
* staged temp file is unlinked immediately, since it can never be
|
|
991
994
|
* referenced). A file part with an empty declared filename (`filename=""`)
|
|
992
|
-
*
|
|
995
|
+
* and a zero-byte body — the browser convention for an unselected optional
|
|
993
996
|
* `<input type="file">` — is a silent no-op: its temp file is unlinked, it is
|
|
994
997
|
* never counted against the `file.count` limit, and it never runs the
|
|
995
998
|
* `allowed` check. A malformed
|
|
996
999
|
* structure (missing/unterminated boundary, nameless part, an oversized
|
|
997
1000
|
* header block, or a preamble exceeding {@link MULTIPART_MAX_PREAMBLE} before
|
|
998
1001
|
* the first boundary) throws with code `'malformed'`. A file is accepted
|
|
999
|
-
* against the configured `allowed` MIME list
|
|
1002
|
+
* against the configured `allowed` MIME list exactly when its sniffed bytes detect a
|
|
1000
1003
|
* type present in the list — sniff-authoritative, independent of whether the
|
|
1001
1004
|
* declared `Content-Type` matches (that agreement is exposed separately as
|
|
1002
1005
|
* `validated`); otherwise throws with code `'rejected'`. A
|
|
1003
1006
|
* request abort mid-upload triggers the same fail-closed cleanup as a limit
|
|
1004
1007
|
* breach. Returns `undefined` for a non-multipart request (untouched).
|
|
1005
1008
|
*
|
|
1006
|
-
* Staging defaults to a process-owned directory created
|
|
1009
|
+
* Staging defaults to a process-owned directory created once (lazily,
|
|
1007
1010
|
* memoized across calls) with `mkdtemp` under `os.tmpdir()` and locked to
|
|
1008
1011
|
* mode `0o700`; `options.directory` overrides it.
|
|
1009
1012
|
*
|
|
@@ -1124,16 +1127,16 @@ function createAssets(options) {
|
|
|
1124
1127
|
};
|
|
1125
1128
|
}
|
|
1126
1129
|
/**
|
|
1127
|
-
* Serves static files from `options.root` over `node:fs` — the node-bound
|
|
1128
|
-
*
|
|
1130
|
+
* Serves static files from `options.root` over `node:fs` — the node-bound static-file
|
|
1131
|
+
* battery, answering conditional, ranged, and SPA-fallback requests.
|
|
1129
1132
|
*
|
|
1130
1133
|
* @remarks
|
|
1131
|
-
* Containment is enforced on
|
|
1134
|
+
* Containment is enforced on canonical paths, not merely the lexically
|
|
1132
1135
|
* resolved one: `options.root` is canonicalized once (memoized) and every
|
|
1133
1136
|
* request's candidate path is re-canonicalized (`fs.realpath`) before it is
|
|
1134
1137
|
* served, so a symlink whose target escapes `root` is refused (falls through
|
|
1135
1138
|
* to `next()`) even though the lexical path resolved inside `root`. A
|
|
1136
|
-
* symlink that resolves to a target still
|
|
1139
|
+
* symlink that resolves to a target still inside `root` is unaffected and
|
|
1137
1140
|
* still serves normally. A dangling symlink (`realpath` throws `ENOENT`) or
|
|
1138
1141
|
* any other `realpath` failure is treated as a miss — this battery never
|
|
1139
1142
|
* throws or 500s on a symlink surprise. On a streamed response (a 200 or 206
|
|
@@ -1329,7 +1332,7 @@ function createStatic(options) {
|
|
|
1329
1332
|
* {@link MultipartError} this battery's parser throws is re-thrown as an
|
|
1330
1333
|
* {@link HTTPError} carrying the same status/message, so `createBoundary`
|
|
1331
1334
|
* (or any HTTPError-aware renderer) maps it correctly without depending on
|
|
1332
|
-
* this node face's error type. Fail-closed on the
|
|
1335
|
+
* this node face's error type. Fail-closed on the downstream handler too: if
|
|
1333
1336
|
* `next()` throws, every still-`'staged'` uploaded file is unlinked
|
|
1334
1337
|
* (best-effort) before the error is re-thrown, so an unhandled downstream
|
|
1335
1338
|
* failure never leaks temp files. A normal return leaves staged files
|
|
@@ -1368,12 +1371,11 @@ function createMultipart(options = {}) {
|
|
|
1368
1371
|
};
|
|
1369
1372
|
}
|
|
1370
1373
|
/**
|
|
1371
|
-
* Compresses response bodies through `node:zlib
|
|
1372
|
-
* the
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
* so the shared name is unambiguous per consumer import path.
|
|
1374
|
+
* Compresses response bodies through `node:zlib`, guaranteed on any Node runtime rather
|
|
1375
|
+
* than dependent on the WHATWG `CompressionStream` global. This battery is the
|
|
1376
|
+
* node-bound sibling of the core face's feature-detected `createCompression`, and it
|
|
1377
|
+
* ships from a separate package entry point (`@orkestrel/middleware/server`) so the
|
|
1378
|
+
* shared name is unambiguous per consumer import path.
|
|
1377
1379
|
*
|
|
1378
1380
|
* @remarks
|
|
1379
1381
|
* Peer-type limitation, the same one the core face carries: the shipped
|