@orkestrel/middleware 0.0.19 → 0.0.21
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 +105 -87
- 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 +106 -88
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +49 -47
- 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 +49 -47
- package/dist/src/server/index.js.map +1 -1
- package/package.json +21 -22
package/dist/src/core/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { integerShape, isBoolean, isFiniteNumber, isFunction, isRecord, isString, jsonShape, stringShape } from "@orkestrel/contract";
|
|
1
|
+
import { integerShape, isArray, isBoolean, isError, isFiniteNumber, isFunction, isInstance, isInteger, isNumber, isObject, isRecord, isString, jsonShape, stringShape } from "@orkestrel/contract";
|
|
2
2
|
import { HTTPError, clearCookie, computeBodyETag, computeClientKey, isCompressibleType, isHTTPError, isValidRequestId, matchesETag, mergeVary, negotiateEncoding, readSignedCookie, resolveOrigin, resolveSecure, resolveSecurityHeader, signToken, verifyToken, writeSignedCookie } from "@orkestrel/server";
|
|
3
3
|
import { linkSignal } from "@orkestrel/abort";
|
|
4
4
|
import { createBudget } from "@orkestrel/budget";
|
|
5
5
|
import { createTimeout } from "@orkestrel/timeout";
|
|
6
6
|
//#region src/core/constants.ts
|
|
7
|
-
/** Holds the default minimum buffered body size
|
|
7
|
+
/** Holds `1024`, the default minimum buffered body size in bytes `createCompression` will compress. */
|
|
8
8
|
var DEFAULT_COMPRESSION_THRESHOLD = 1024;
|
|
9
9
|
/**
|
|
10
|
-
* Lists the default content-codings `createCompression` offers
|
|
11
|
-
* intersected at construction with what the runtime's
|
|
12
|
-
* actually supports.
|
|
10
|
+
* Lists `['gzip', 'deflate']`, the default content-codings `createCompression` offers in
|
|
11
|
+
* preference order — intersected at construction with what the runtime's
|
|
12
|
+
* `CompressionStream` actually supports.
|
|
13
13
|
*
|
|
14
14
|
* @remarks
|
|
15
15
|
* The shipped `@orkestrel/server` peer's {@link Encoding} union is
|
|
@@ -19,28 +19,29 @@ var DEFAULT_COMPRESSION_THRESHOLD = 1024;
|
|
|
19
19
|
* admits; a node-face brotli variant, if one ships, extends this list there.
|
|
20
20
|
*/
|
|
21
21
|
var DEFAULT_COMPRESSION_ENCODINGS = Object.freeze(["gzip", "deflate"]);
|
|
22
|
-
/** Holds the default `X-Frame-Options` value `createSecurity` sets. */
|
|
22
|
+
/** Holds `'DENY'`, the default `X-Frame-Options` value `createSecurity` sets. */
|
|
23
23
|
var DEFAULT_FRAME_OPTIONS = "DENY";
|
|
24
24
|
/**
|
|
25
|
-
* Holds
|
|
26
|
-
* `
|
|
25
|
+
* Holds `"default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'"`,
|
|
26
|
+
* the default `Content-Security-Policy` value `createSecurity` sets — a custom
|
|
27
|
+
* `csp` option replaces this wholesale, never merges.
|
|
27
28
|
*/
|
|
28
29
|
var DEFAULT_CSP = "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'";
|
|
29
|
-
/** Holds the default `Referrer-Policy` value `createSecurity` sets. */
|
|
30
|
+
/** Holds `'strict-origin-when-cross-origin'`, the default `Referrer-Policy` value `createSecurity` sets. */
|
|
30
31
|
var DEFAULT_REFERRER_POLICY = "strict-origin-when-cross-origin";
|
|
31
|
-
/** Holds the default `Permissions-Policy` value `createSecurity` sets. */
|
|
32
|
+
/** Holds `'camera=(), microphone=(), geolocation=()'`, the default `Permissions-Policy` value `createSecurity` sets. */
|
|
32
33
|
var DEFAULT_PERMISSIONS_POLICY = "camera=(), microphone=(), geolocation=()";
|
|
33
|
-
/** Holds the default `Cross-Origin-Opener-Policy` value `createSecurity` sets. */
|
|
34
|
+
/** Holds `'same-origin'`, the default `Cross-Origin-Opener-Policy` value `createSecurity` sets. */
|
|
34
35
|
var DEFAULT_COOP = "same-origin";
|
|
35
|
-
/** Holds the default `Cross-Origin-Resource-Policy` value `createSecurity` sets. */
|
|
36
|
+
/** Holds `'same-origin'`, the default `Cross-Origin-Resource-Policy` value `createSecurity` sets. */
|
|
36
37
|
var DEFAULT_CORP = "same-origin";
|
|
37
|
-
/** Holds the default `Origin-Agent-Cluster` value `createSecurity` sets. */
|
|
38
|
+
/** Holds `'?1'`, the default `Origin-Agent-Cluster` value `createSecurity` sets. */
|
|
38
39
|
var DEFAULT_CLUSTER = "?1";
|
|
39
|
-
/** Holds the value `createSecurity` sets for `Cross-Origin-Embedder-Policy` when `coep: true`. */
|
|
40
|
+
/** Holds `'require-corp'`, the value `createSecurity` sets for `Cross-Origin-Embedder-Policy` when `coep: true`. */
|
|
40
41
|
var DEFAULT_COEP = "require-corp";
|
|
41
|
-
/** Holds the value `createSecurity` sets for `Strict-Transport-Security` when `hsts: true`. */
|
|
42
|
+
/** Holds `'max-age=31536000; includeSubDomains'`, the value `createSecurity` sets for `Strict-Transport-Security` when `hsts: true`. */
|
|
42
43
|
var DEFAULT_HSTS = "max-age=31536000; includeSubDomains";
|
|
43
|
-
/** Names the default header `createSecurity` mints
|
|
44
|
+
/** Names `'x-request-id'`, the default header `createSecurity` mints or echoes a request identifier into. */
|
|
44
45
|
var DEFAULT_IDENTIFIER_HEADER = "x-request-id";
|
|
45
46
|
/** Lists the default methods `createCors` advertises on a preflight response. */
|
|
46
47
|
var DEFAULT_CORS_METHODS = Object.freeze([
|
|
@@ -53,29 +54,29 @@ var DEFAULT_CORS_METHODS = Object.freeze([
|
|
|
53
54
|
]);
|
|
54
55
|
/** Lists the default headers `createCors` advertises on a preflight response. */
|
|
55
56
|
var DEFAULT_CORS_HEADERS = Object.freeze(["Content-Type", "Authorization"]);
|
|
56
|
-
/** Holds the default response status `createDeadline` returns when its deadline fires first. */
|
|
57
|
+
/** Holds `503`, the default response status `createDeadline` returns when its deadline fires first. */
|
|
57
58
|
var DEFAULT_DEADLINE_STATUS = 503;
|
|
58
|
-
/** Names the default header `createBearer` reads the token from. */
|
|
59
|
+
/** Names `'authorization'`, the default header `createBearer` reads the token from. */
|
|
59
60
|
var DEFAULT_BEARER_HEADER = "authorization";
|
|
60
|
-
/** Names the default scheme prefix `createBearer` strips before verification. */
|
|
61
|
+
/** Names `'Bearer'`, the default scheme prefix `createBearer` strips before verification. */
|
|
61
62
|
var DEFAULT_BEARER_SCHEME = "Bearer";
|
|
62
|
-
/** Holds the default maximum number of distinct rate-limit keys `createLimiter` tracks before LRU eviction. */
|
|
63
|
+
/** Holds `10_000`, the default maximum number of distinct rate-limit keys `createLimiter` tracks before LRU eviction. */
|
|
63
64
|
var DEFAULT_LIMITER_CAPACITY = 1e4;
|
|
64
|
-
/** Holds the default maximum number of distinct session ids `createMemorySessionStore` tracks before LRU (by last write) eviction. */
|
|
65
|
+
/** Holds `10_000`, the default maximum number of distinct session ids `createMemorySessionStore` tracks before LRU (by last write) eviction. */
|
|
65
66
|
var DEFAULT_SESSION_CAPACITY = 1e4;
|
|
66
|
-
/** Holds the default 429 body message `createLimiter` sends when a key is over budget. */
|
|
67
|
+
/** Holds `'rate limit exceeded'`, the default 429 body message `createLimiter` sends when a key is over budget. */
|
|
67
68
|
var DEFAULT_LIMITER_MESSAGE = "rate limit exceeded";
|
|
68
|
-
/** Names the default cookie `createCookieTransport` writes the signed session id under. */
|
|
69
|
+
/** Names `'session'`, the default cookie `createCookieTransport` writes the signed session id under. */
|
|
69
70
|
var DEFAULT_SESSION_COOKIE = "session";
|
|
70
|
-
/** Names the default header `createHeaderTransport` carries the session id in. */
|
|
71
|
+
/** Names `'session-id'`, the default header `createHeaderTransport` carries the session id in. */
|
|
71
72
|
var DEFAULT_SESSION_HEADER = "session-id";
|
|
72
|
-
/** Names the default signed cookie `createCSRF` writes the CSRF token under. */
|
|
73
|
+
/** Names `'csrf'`, the default signed cookie `createCSRF` writes the CSRF token under. */
|
|
73
74
|
var DEFAULT_CSRF_COOKIE = "csrf";
|
|
74
|
-
/** Names the default header `createCSRF` reads a mutating request's submitted token from. */
|
|
75
|
+
/** Names `'x-csrf-token'`, the default header `createCSRF` reads a mutating request's submitted token from. */
|
|
75
76
|
var DEFAULT_CSRF_HEADER = "x-csrf-token";
|
|
76
|
-
/** Names the default body field `createCSRF` falls back to reading a mutating request's submitted token from. */
|
|
77
|
+
/** Names `'_csrf'`, the default body field `createCSRF` falls back to reading a mutating request's submitted token from. */
|
|
77
78
|
var DEFAULT_CSRF_FIELD = "_csrf";
|
|
78
|
-
/** Lists the default methods `createCSRF` treats as safe (mint instead of verify). */
|
|
79
|
+
/** Lists `['GET', 'HEAD', 'OPTIONS']`, the default methods `createCSRF` treats as safe (mint instead of verify). */
|
|
79
80
|
var DEFAULT_CSRF_SAFE_METHODS = Object.freeze([
|
|
80
81
|
"GET",
|
|
81
82
|
"HEAD",
|
|
@@ -174,7 +175,7 @@ function buildRateLimitPolicyField(max, window) {
|
|
|
174
175
|
* never claims full CIDR generality beyond IPv4.
|
|
175
176
|
*
|
|
176
177
|
* @remarks
|
|
177
|
-
* An IPv6 `trusted` roster entry is compared as an
|
|
178
|
+
* An IPv6 `trusted` roster entry is compared as an exact string — it must be
|
|
178
179
|
* supplied in canonical form (no zero-compression normalization, no case
|
|
179
180
|
* folding) by the caller; this function performs no IPv6 normalization of
|
|
180
181
|
* its own.
|
|
@@ -197,24 +198,24 @@ function matchesTrustedEntry(address, entry) {
|
|
|
197
198
|
const networkParts = network.split(".");
|
|
198
199
|
const addressParts = address.split(".");
|
|
199
200
|
if (networkParts.length !== 4 || addressParts.length !== 4) return false;
|
|
200
|
-
if (!
|
|
201
|
+
if (!isInteger(bits) || bits < 8 || bits > 32) return false;
|
|
201
202
|
const networkOctets = networkParts.map(Number);
|
|
202
203
|
const addressOctets = addressParts.map(Number);
|
|
203
|
-
if (networkOctets.some((value) => !
|
|
204
|
-
if (addressOctets.some((value) => !
|
|
204
|
+
if (networkOctets.some((value) => !isInteger(value) || value < 0 || value > 255)) return false;
|
|
205
|
+
if (addressOctets.some((value) => !isInteger(value) || value < 0 || value > 255)) return false;
|
|
205
206
|
const networkInt = (networkOctets[0] ?? 0) << 24 | (networkOctets[1] ?? 0) << 16 | (networkOctets[2] ?? 0) << 8 | (networkOctets[3] ?? 0);
|
|
206
207
|
const addressInt = (addressOctets[0] ?? 0) << 24 | (addressOctets[1] ?? 0) << 16 | (addressOctets[2] ?? 0) << 8 | (addressOctets[3] ?? 0);
|
|
207
208
|
const mask = bits === 32 ? -1 : ~((1 << 32 - bits) - 1);
|
|
208
209
|
return (networkInt & mask) === (addressInt & mask);
|
|
209
210
|
}
|
|
210
211
|
/**
|
|
211
|
-
* Walks `X-Forwarded-For` right-to-left and resolves the first
|
|
212
|
+
* Walks `X-Forwarded-For` right-to-left and resolves the first untrusted hop
|
|
212
213
|
* address — `createForwarded`'s core algorithm.
|
|
213
214
|
*
|
|
214
215
|
* @remarks
|
|
215
216
|
* Parses `X-Forwarded-For` only. With `proxies` set, trusts exactly that
|
|
216
217
|
* many hops counted from the right (the closest to this server) and returns
|
|
217
|
-
* the next one left of them; with `trusted` set, trusts every
|
|
218
|
+
* the next one left of them; with `trusted` set, trusts every consecutive
|
|
218
219
|
* hop from the right that matches one of the roster
|
|
219
220
|
* ({@link matchesTrustedEntry}) and returns the first hop that does not. If
|
|
220
221
|
* the rightmost hop (the immediate sender) does not match the roster, the
|
|
@@ -224,7 +225,7 @@ function matchesTrustedEntry(address, entry) {
|
|
|
224
225
|
* socket peer).
|
|
225
226
|
*
|
|
226
227
|
* @param header - The raw `X-Forwarded-For` header value (comma-separated hops), if present
|
|
227
|
-
* @param trust - The {@link ForwardedOptions} form in force — a trusted hop
|
|
228
|
+
* @param trust - The {@link ForwardedOptions} form in force — a trusted hop count or a `trusted` CIDR/exact roster
|
|
228
229
|
* @returns The first untrusted hop address, or `undefined` when none qualifies
|
|
229
230
|
*
|
|
230
231
|
* @example
|
|
@@ -298,16 +299,17 @@ async function compressBytes(bytes, encoding) {
|
|
|
298
299
|
return new Uint8Array(compressed);
|
|
299
300
|
}
|
|
300
301
|
/**
|
|
301
|
-
* Checks whether a response
|
|
302
|
+
* Checks whether a response must skip the compression and ETag buffering pipeline
|
|
302
303
|
* — the shared cheap-skip predicate both batteries apply before ever touching
|
|
303
|
-
* `response.arrayBuffer()
|
|
304
|
+
* `response.arrayBuffer()`, true for a `HEAD` request, a `204`/`304` or
|
|
305
|
+
* otherwise bodyless response, an `event-stream` response, and a response
|
|
306
|
+
* already carrying the header the caller is about to set.
|
|
304
307
|
*
|
|
305
308
|
* @remarks
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
* ETag battery).
|
|
309
|
+
* Buffering an `event-stream` response would hang the connection, so SSE skips
|
|
310
|
+
* whatever its size. `skipHeader` is the header whose presence already answers
|
|
311
|
+
* the question — `Content-Encoding` for compression, `ETag` for the ETag
|
|
312
|
+
* battery.
|
|
311
313
|
*
|
|
312
314
|
* @param method - The request's HTTP method
|
|
313
315
|
* @param response - The candidate response
|
|
@@ -346,7 +348,7 @@ function isCompressionNegotiated(encoding) {
|
|
|
346
348
|
}
|
|
347
349
|
/**
|
|
348
350
|
* Resolves an opt-in, value-bearing security header — `string | boolean`
|
|
349
|
-
* (default
|
|
351
|
+
* (off by default, `true` uses the secure default), the shape `createSecurity`'s
|
|
350
352
|
* `coep`/`hsts` options use, distinct from the plain value-or-`false` shape
|
|
351
353
|
* `resolveSecurityHeader` (the peer substrate) handles.
|
|
352
354
|
*
|
|
@@ -400,7 +402,7 @@ function rebuildResponse(body, response, headers) {
|
|
|
400
402
|
* even when a later skip declines to compress) → `negotiateEncoding` over
|
|
401
403
|
* `options.encodings` → {@link isCompressionNegotiated} → `isCompressibleType`
|
|
402
404
|
* on `Content-Type` → a fast skip when the response already carries a
|
|
403
|
-
* numeric `Content-Length`
|
|
405
|
+
* numeric `Content-Length` below `options.threshold` (avoids buffering a
|
|
404
406
|
* body known too small to be worth compressing) → buffer through
|
|
405
407
|
* `response.arrayBuffer()` → a threshold passthrough when the buffered size
|
|
406
408
|
* is still below `options.threshold` → `options.compress` → set
|
|
@@ -435,7 +437,7 @@ async function compressResponse(request, context, response, options) {
|
|
|
435
437
|
const declaredLength = response.headers.get("content-length");
|
|
436
438
|
if (declaredLength !== null) {
|
|
437
439
|
const declared = Number(declaredLength);
|
|
438
|
-
if (
|
|
440
|
+
if (isFiniteNumber(declared) && declared < options.threshold) return response;
|
|
439
441
|
}
|
|
440
442
|
const buffer = await response.arrayBuffer();
|
|
441
443
|
if (buffer.byteLength < options.threshold) return rebuildResponse(buffer, response);
|
|
@@ -462,7 +464,7 @@ function transferSessionState(from, to) {
|
|
|
462
464
|
for (const [key, value] of from.state) to.set(key, value);
|
|
463
465
|
}
|
|
464
466
|
/**
|
|
465
|
-
* Determines whether a request is a CORS
|
|
467
|
+
* Determines whether a request is a CORS preflight — an `OPTIONS` request
|
|
466
468
|
* carrying an `Access-Control-Request-Method` header.
|
|
467
469
|
*
|
|
468
470
|
* @param method - The request's HTTP method
|
|
@@ -569,7 +571,7 @@ function validateSessionLimits(limits) {
|
|
|
569
571
|
* @remarks
|
|
570
572
|
* `state` is built on a null-prototype object (`Object.create(null)`), never
|
|
571
573
|
* a `{}` literal — a session key literally named `__proto__` must round-trip
|
|
572
|
-
* as an
|
|
574
|
+
* as an own enumerable property instead of hitting `Object.prototype`'s
|
|
573
575
|
* `__proto__` accessor (which would silently drop the entry and risk
|
|
574
576
|
* polluting the shared prototype).
|
|
575
577
|
*
|
|
@@ -592,8 +594,8 @@ function snapshotSession(session) {
|
|
|
592
594
|
* Determines whether a value implements {@link SessionInterface} — a total
|
|
593
595
|
* structural guard: an `id` string, a `state` `Map`, and the `set`, `delete`,
|
|
594
596
|
* and `clear` mutators. Prototype-agnostic — accepts a plain object, a
|
|
595
|
-
* null-prototype object,
|
|
596
|
-
* restored
|
|
597
|
+
* null-prototype object, and a class instance (a real `Session`), because a
|
|
598
|
+
* restored or stored session is routinely a class instance rather than a literal.
|
|
597
599
|
*
|
|
598
600
|
* @param value - The candidate value
|
|
599
601
|
* @returns True if `value` is shaped like a {@link SessionInterface}; false otherwise
|
|
@@ -605,14 +607,14 @@ function snapshotSession(session) {
|
|
|
605
607
|
* ```
|
|
606
608
|
*/
|
|
607
609
|
function isSession(value) {
|
|
608
|
-
if (
|
|
610
|
+
if (!isObject(value)) return false;
|
|
609
611
|
const id = Reflect.get(value, "id");
|
|
610
612
|
const state = Reflect.get(value, "state");
|
|
611
|
-
if (!isString(id) || !(state
|
|
613
|
+
if (!isString(id) || !isInstance(state, Map)) return false;
|
|
612
614
|
const set = Reflect.get(value, "set");
|
|
613
615
|
const remove = Reflect.get(value, "delete");
|
|
614
616
|
const clear = Reflect.get(value, "clear");
|
|
615
|
-
return
|
|
617
|
+
return isFunction(set) && isFunction(remove) && isFunction(clear);
|
|
616
618
|
}
|
|
617
619
|
/**
|
|
618
620
|
* Determines whether a value implements {@link SessionControlInterface} — a
|
|
@@ -628,7 +630,7 @@ function isSession(value) {
|
|
|
628
630
|
*/
|
|
629
631
|
function isSessionControl(value) {
|
|
630
632
|
if (!isRecord(value)) return false;
|
|
631
|
-
return
|
|
633
|
+
return isFunction(value.regenerate) && isFunction(value.destroy);
|
|
632
634
|
}
|
|
633
635
|
/**
|
|
634
636
|
* Determines whether a value is one staged {@link MultipartFile} record — a
|
|
@@ -652,7 +654,7 @@ function isSessionControl(value) {
|
|
|
652
654
|
*/
|
|
653
655
|
function isMultipartFile(value) {
|
|
654
656
|
if (!isRecord(value)) return false;
|
|
655
|
-
return isString(value.field) && isString(value.name) &&
|
|
657
|
+
return isString(value.field) && isString(value.name) && isNumber(value.size) && isString(value.mime) && isBoolean(value.validated) && isString(value.status) && isString(value.path);
|
|
656
658
|
}
|
|
657
659
|
/**
|
|
658
660
|
* Determines whether a value implements {@link MultipartBody} — a total
|
|
@@ -671,7 +673,7 @@ function isMultipartBody(value) {
|
|
|
671
673
|
if (!isRecord(value)) return false;
|
|
672
674
|
if (!isRecord(value.files) || !isRecord(value.fields)) return false;
|
|
673
675
|
for (const entries of Object.values(value.files)) {
|
|
674
|
-
if (!
|
|
676
|
+
if (!isArray(entries)) return false;
|
|
675
677
|
for (const entry of entries) if (!isMultipartFile(entry)) return false;
|
|
676
678
|
}
|
|
677
679
|
for (const fieldValue of Object.values(value.fields)) if (!isString(fieldValue)) return false;
|
|
@@ -681,7 +683,7 @@ function isMultipartBody(value) {
|
|
|
681
683
|
//#region src/core/shapers.ts
|
|
682
684
|
/**
|
|
683
685
|
* Holds the `@orkestrel/database` column shape for a
|
|
684
|
-
* {@link import('./types.js').SessionRow} table
|
|
686
|
+
* {@link import('./types.js').SessionRow} table. Pass it as-is to
|
|
685
687
|
* `createDatabase({ tables: { sessions: sessionColumns } })` so an app
|
|
686
688
|
* declaring a durable session table never hand-writes the shape.
|
|
687
689
|
*
|
|
@@ -760,7 +762,7 @@ var Session = class {
|
|
|
760
762
|
* @remarks
|
|
761
763
|
* `get` evicts a session whose idle time (`now - seen >= ttl`) or
|
|
762
764
|
* absolute lifetime (`now - created >= lifetime`) has elapsed — the
|
|
763
|
-
* lifetime check fires
|
|
765
|
+
* lifetime check fires even if the session was continuously touched, because
|
|
764
766
|
* `created` is stamped once at the first `set` and preserved across every
|
|
765
767
|
* later re-`set` of the same id. A live read touches `seen`. `delete` of
|
|
766
768
|
* an absent id is a no-op.
|
|
@@ -788,7 +790,7 @@ var MemorySessionStore = class {
|
|
|
788
790
|
#evict;
|
|
789
791
|
constructor(options) {
|
|
790
792
|
validateSessionLimits(options);
|
|
791
|
-
if (options?.capacity !== void 0 && (!
|
|
793
|
+
if (options?.capacity !== void 0 && (!isInteger(options.capacity) || options.capacity <= 0)) throw new TypeError("MemorySessionStore requires options.capacity to be a positive integer when provided");
|
|
792
794
|
if (options?.evict !== void 0 && !isFunction(options.evict)) throw new TypeError("MemorySessionStore requires options.evict to be a function when provided");
|
|
793
795
|
this.#entries = /* @__PURE__ */ new Map();
|
|
794
796
|
this.#ttl = options?.ttl;
|
|
@@ -875,7 +877,7 @@ var MemorySessionStore = class {
|
|
|
875
877
|
* {@link MemorySessionStore}. `delete` of an absent id is a no-op (the
|
|
876
878
|
* table's `remove` contract).
|
|
877
879
|
*
|
|
878
|
-
* A malformed-snapshot or failed-guard `undefined`
|
|
880
|
+
* A malformed-snapshot or failed-guard `undefined` leaves the row in place —
|
|
879
881
|
* unlike the expired path, which removes it. This is deliberate: a
|
|
880
882
|
* caller-contextual guard may reject a session that is still perfectly
|
|
881
883
|
* valid for another flow reading the same table (a differently-shaped `S`,
|
|
@@ -945,9 +947,20 @@ var DatabaseSessionStore = class {
|
|
|
945
947
|
* @returns A `MiddlewareHandler<TState>`
|
|
946
948
|
* @throws {TypeError} When `options.expose` or `options.report` is malformed
|
|
947
949
|
*
|
|
948
|
-
* @example
|
|
950
|
+
* @example Mount a battery
|
|
949
951
|
* ```ts
|
|
952
|
+
* import { createBoundary, createSecurity } from '@orkestrel/middleware'
|
|
953
|
+
* import type { IdentifierState } from '@orkestrel/middleware'
|
|
954
|
+
* import { compose } from '@orkestrel/server'
|
|
955
|
+
*
|
|
956
|
+
* interface State extends IdentifierState {}
|
|
957
|
+
*
|
|
950
958
|
* const boundary = createBoundary({ expose: false })
|
|
959
|
+
* const security = createSecurity({ hsts: true })
|
|
960
|
+
*
|
|
961
|
+
* const handle = compose<State>([boundary, security], async (_request, context) => {
|
|
962
|
+
* return Response.json({ identifier: context.state.identifier })
|
|
963
|
+
* })
|
|
951
964
|
* ```
|
|
952
965
|
*/
|
|
953
966
|
function createBoundary(options) {
|
|
@@ -963,7 +976,7 @@ function createBoundary(options) {
|
|
|
963
976
|
report(error);
|
|
964
977
|
} catch {}
|
|
965
978
|
if (isHTTPError(error)) return new Response(error.message, { status: error.status });
|
|
966
|
-
const message = expose ? error
|
|
979
|
+
const message = expose ? isError(error) ? error.message : String(error) : "internal server error";
|
|
967
980
|
return new Response(message, { status: 500 });
|
|
968
981
|
}
|
|
969
982
|
};
|
|
@@ -1022,7 +1035,7 @@ function createTelemetry(options) {
|
|
|
1022
1035
|
function createCompression(options) {
|
|
1023
1036
|
if (options?.threshold !== void 0 && (!isFiniteNumber(options.threshold) || options.threshold < 0)) throw new TypeError("CompressionOptions.threshold must be a finite number when provided");
|
|
1024
1037
|
if (options?.filter !== void 0 && !isFunction(options.filter)) throw new TypeError("CompressionOptions.filter must be a function when provided");
|
|
1025
|
-
if (options?.encodings !== void 0 && !
|
|
1038
|
+
if (options?.encodings !== void 0 && !isArray(options.encodings)) throw new TypeError("CompressionOptions.encodings must be an array when provided");
|
|
1026
1039
|
const threshold = options?.threshold ?? 1024;
|
|
1027
1040
|
const encodings = detectEncodings(options?.encodings ?? DEFAULT_COMPRESSION_ENCODINGS);
|
|
1028
1041
|
const filter = options?.filter;
|
|
@@ -1036,7 +1049,8 @@ function createCompression(options) {
|
|
|
1036
1049
|
};
|
|
1037
1050
|
}
|
|
1038
1051
|
/**
|
|
1039
|
-
* Creates the security-headers + request-identifier battery
|
|
1052
|
+
* Creates the security-headers + request-identifier battery — sets each documented
|
|
1053
|
+
* header default, and mints or echoes a request identifier.
|
|
1040
1054
|
*
|
|
1041
1055
|
* @typeParam TState - The consumer's opaque per-request state type, must carry {@link IdentifierState}
|
|
1042
1056
|
* @param options - See {@link SecurityOptions}
|
|
@@ -1098,7 +1112,8 @@ function createSecurity(options) {
|
|
|
1098
1112
|
};
|
|
1099
1113
|
}
|
|
1100
1114
|
/**
|
|
1101
|
-
* Creates the Cross-Origin Resource Sharing battery
|
|
1115
|
+
* Creates the Cross-Origin Resource Sharing battery — answers a preflight itself, and
|
|
1116
|
+
* reflects an allow-listed origin or serves the configured wildcard.
|
|
1102
1117
|
*
|
|
1103
1118
|
* @typeParam TState - The consumer's opaque per-request state type
|
|
1104
1119
|
* @param options - See {@link CorsOptions}
|
|
@@ -1111,13 +1126,13 @@ function createSecurity(options) {
|
|
|
1111
1126
|
* ```
|
|
1112
1127
|
*/
|
|
1113
1128
|
function createCors(options) {
|
|
1114
|
-
if (options?.origin !== void 0 && !isString(options.origin) && !
|
|
1115
|
-
if (options?.methods !== void 0 && !
|
|
1116
|
-
if (options?.headers !== void 0 && !
|
|
1129
|
+
if (options?.origin !== void 0 && !isString(options.origin) && !isArray(options.origin)) throw new TypeError("CorsOptions.origin must be a string or string array when provided");
|
|
1130
|
+
if (options?.methods !== void 0 && !isArray(options.methods)) throw new TypeError("CorsOptions.methods must be an array when provided");
|
|
1131
|
+
if (options?.headers !== void 0 && !isArray(options.headers)) throw new TypeError("CorsOptions.headers must be an array when provided");
|
|
1117
1132
|
const origin = options?.origin ?? "*";
|
|
1118
1133
|
const methods = options?.methods ?? DEFAULT_CORS_METHODS;
|
|
1119
1134
|
const headers = options?.headers ?? DEFAULT_CORS_HEADERS;
|
|
1120
|
-
const reflecting =
|
|
1135
|
+
const reflecting = isArray(origin);
|
|
1121
1136
|
return async (request, context, next) => {
|
|
1122
1137
|
const requestOrigin = request.headers.get("origin") ?? void 0;
|
|
1123
1138
|
const allowOrigin = resolveOrigin(origin, requestOrigin);
|
|
@@ -1147,7 +1162,7 @@ function createCors(options) {
|
|
|
1147
1162
|
* @throws {TypeError} When `options.ms` or `options.status` is malformed
|
|
1148
1163
|
*
|
|
1149
1164
|
* @remarks
|
|
1150
|
-
*
|
|
1165
|
+
* Mount this battery outside `createBody` in the chain — it reconstructs the inbound
|
|
1151
1166
|
* `Request` (to link its `signal` to the deadline `signal`), which throws if
|
|
1152
1167
|
* the body was already consumed upstream (for example by `createBody`'s cached read).
|
|
1153
1168
|
*
|
|
@@ -1184,7 +1199,8 @@ function createDeadline(options) {
|
|
|
1184
1199
|
};
|
|
1185
1200
|
}
|
|
1186
1201
|
/**
|
|
1187
|
-
* Creates the trusted-proxy client-IP resolver battery
|
|
1202
|
+
* Creates the trusted-proxy client-IP resolver battery — walks `X-Forwarded-For` past
|
|
1203
|
+
* the hops its options declare trusted.
|
|
1188
1204
|
*
|
|
1189
1205
|
* @typeParam TState - The consumer's opaque per-request state type, must carry {@link ClientState} and {@link ConnectionState}
|
|
1190
1206
|
* @param options - See {@link ForwardedOptions}
|
|
@@ -1202,10 +1218,10 @@ function createForwarded(options) {
|
|
|
1202
1218
|
if (!isRecord(options) || hasProxies === hasTrusted) throw new TypeError("ForwardedOptions requires exactly one of proxies or trusted");
|
|
1203
1219
|
if (hasProxies) {
|
|
1204
1220
|
const proxies = options.proxies;
|
|
1205
|
-
if (!
|
|
1221
|
+
if (!isInteger(proxies) || proxies < 1) throw new TypeError("ForwardedOptions.proxies must be a positive integer");
|
|
1206
1222
|
} else {
|
|
1207
1223
|
const trusted = options.trusted;
|
|
1208
|
-
if (!
|
|
1224
|
+
if (!isArray(trusted)) throw new TypeError("ForwardedOptions.trusted must be an array when provided");
|
|
1209
1225
|
}
|
|
1210
1226
|
const trust = hasProxies ? { proxies: options.proxies } : { trusted: options.trusted };
|
|
1211
1227
|
return async (request, context, next) => {
|
|
@@ -1215,7 +1231,7 @@ function createForwarded(options) {
|
|
|
1215
1231
|
};
|
|
1216
1232
|
}
|
|
1217
1233
|
/**
|
|
1218
|
-
* Creates the dynamic response `ETag` + conditional GET battery.
|
|
1234
|
+
* Creates the dynamic response `ETag` + conditional GET battery (RFC 7232).
|
|
1219
1235
|
*
|
|
1220
1236
|
* @typeParam TState - The consumer's opaque per-request state type
|
|
1221
1237
|
* @param options - See {@link ETagOptions}
|
|
@@ -1253,7 +1269,8 @@ function createETag(options) {
|
|
|
1253
1269
|
};
|
|
1254
1270
|
}
|
|
1255
1271
|
/**
|
|
1256
|
-
* Creates the bearer-token authentication battery
|
|
1272
|
+
* Creates the bearer-token authentication battery — reads the token from its header and
|
|
1273
|
+
* verifies it with `verifyToken`.
|
|
1257
1274
|
*
|
|
1258
1275
|
* @typeParam TState - The consumer's opaque per-request state type, must carry {@link BearerState}
|
|
1259
1276
|
* @param options - See {@link BearerOptions}
|
|
@@ -1267,7 +1284,7 @@ function createETag(options) {
|
|
|
1267
1284
|
* ```
|
|
1268
1285
|
*/
|
|
1269
1286
|
function createBearer(options) {
|
|
1270
|
-
if (!isString(options.secret) && (!
|
|
1287
|
+
if (!isString(options.secret) && (!isArray(options.secret) || !options.secret.every(isString))) throw new TypeError("BearerOptions.secret must be a string or string array");
|
|
1271
1288
|
if (options.header !== void 0 && !isString(options.header)) throw new TypeError("BearerOptions.header must be a string when provided");
|
|
1272
1289
|
if (options.scheme !== void 0 && !isString(options.scheme)) throw new TypeError("BearerOptions.scheme must be a string when provided");
|
|
1273
1290
|
const secret = options.secret;
|
|
@@ -1289,7 +1306,8 @@ function createBearer(options) {
|
|
|
1289
1306
|
};
|
|
1290
1307
|
}
|
|
1291
1308
|
/**
|
|
1292
|
-
* Creates the fixed-window rate-limiting battery
|
|
1309
|
+
* Creates the fixed-window rate-limiting battery — checks a key's budget before
|
|
1310
|
+
* consuming it, so one window admits exactly `max` requests.
|
|
1293
1311
|
*
|
|
1294
1312
|
* @typeParam TState - The consumer's opaque per-request state type, must carry {@link BearerState}, {@link ClientState}, and {@link ConnectionState}
|
|
1295
1313
|
* @param options - See {@link LimiterOptions}
|
|
@@ -1302,9 +1320,9 @@ function createBearer(options) {
|
|
|
1302
1320
|
* ```
|
|
1303
1321
|
*/
|
|
1304
1322
|
function createLimiter(options) {
|
|
1305
|
-
if (!
|
|
1323
|
+
if (!isInteger(options.max) || options.max <= 0) throw new TypeError("LimiterOptions.max must be a positive integer");
|
|
1306
1324
|
if (!isFiniteNumber(options.window) || options.window <= 0) throw new TypeError("LimiterOptions.window must be a positive finite number");
|
|
1307
|
-
if (options.capacity !== void 0 && (!
|
|
1325
|
+
if (options.capacity !== void 0 && (!isInteger(options.capacity) || options.capacity <= 0)) throw new TypeError("LimiterOptions.capacity must be a positive integer when provided");
|
|
1308
1326
|
if (options.key !== void 0 && !isFunction(options.key)) throw new TypeError("LimiterOptions.key must be a function when provided");
|
|
1309
1327
|
if (options.message !== void 0 && !isString(options.message)) throw new TypeError("LimiterOptions.message must be a string when provided");
|
|
1310
1328
|
if (options.clock !== void 0 && !isFunction(options.clock)) throw new TypeError("LimiterOptions.clock must be a function when provided");
|
|
@@ -1378,7 +1396,7 @@ function createLimiter(options) {
|
|
|
1378
1396
|
* The shipped `MiddlewareContext.body()` is a parameterless, server-owned
|
|
1379
1397
|
* cache (`ServerOptions.limit` governs its size cap) — this battery carries
|
|
1380
1398
|
* no `limit`/`decompression` options. `state.body` is
|
|
1381
|
-
* stashed from the
|
|
1399
|
+
* stashed from the same awaited call the 400 check reads — `context.body()`
|
|
1382
1400
|
* is never invoked twice.
|
|
1383
1401
|
*
|
|
1384
1402
|
* @example
|
|
@@ -1494,11 +1512,11 @@ function createSession(options) {
|
|
|
1494
1512
|
* ```
|
|
1495
1513
|
*/
|
|
1496
1514
|
function createCSRF(options) {
|
|
1497
|
-
if (!isString(options.secret) && (!
|
|
1515
|
+
if (!isString(options.secret) && (!isArray(options.secret) || !options.secret.every(isString))) throw new TypeError("CSRFOptions.secret must be a string or string array");
|
|
1498
1516
|
if (options.cookie !== void 0 && !isString(options.cookie)) throw new TypeError("CSRFOptions.cookie must be a string when provided");
|
|
1499
1517
|
if (options.header !== void 0 && !isString(options.header)) throw new TypeError("CSRFOptions.header must be a string when provided");
|
|
1500
1518
|
if (options.field !== void 0 && !isString(options.field)) throw new TypeError("CSRFOptions.field must be a string when provided");
|
|
1501
|
-
if (options.safe !== void 0 && !
|
|
1519
|
+
if (options.safe !== void 0 && !isArray(options.safe)) throw new TypeError("CSRFOptions.safe must be an array when provided");
|
|
1502
1520
|
const secret = options.secret;
|
|
1503
1521
|
const cookieName = options.cookie ?? "csrf";
|
|
1504
1522
|
const header = options.header ?? "x-csrf-token";
|
|
@@ -1537,8 +1555,8 @@ function createCSRF(options) {
|
|
|
1537
1555
|
};
|
|
1538
1556
|
}
|
|
1539
1557
|
/**
|
|
1540
|
-
* Scopes a battery to
|
|
1541
|
-
* steps aside through `next()`.
|
|
1558
|
+
* Scopes a battery to a set of exact pathnames and nowhere else — outside that set
|
|
1559
|
+
* it steps aside through `next()`.
|
|
1542
1560
|
*
|
|
1543
1561
|
* @typeParam TState - The consumer's opaque per-request state type
|
|
1544
1562
|
* @param paths - One pathname, or a set of pathnames, matched exactly against `context.url.pathname`
|
|
@@ -1551,15 +1569,15 @@ function createCSRF(options) {
|
|
|
1551
1569
|
* ```
|
|
1552
1570
|
*/
|
|
1553
1571
|
function only(paths, handler) {
|
|
1554
|
-
const matches = new Set(
|
|
1572
|
+
const matches = new Set(isString(paths) ? [paths] : paths);
|
|
1555
1573
|
return async (request, context, next) => {
|
|
1556
1574
|
if (matches.has(context.url.pathname)) return handler(request, context, next);
|
|
1557
1575
|
return next();
|
|
1558
1576
|
};
|
|
1559
1577
|
}
|
|
1560
1578
|
/**
|
|
1561
|
-
* Scopes a battery to
|
|
1562
|
-
*
|
|
1579
|
+
* Scopes a battery to every pathname outside a set of exact ones — on that set it
|
|
1580
|
+
* steps aside through `next()`.
|
|
1563
1581
|
*
|
|
1564
1582
|
* @typeParam TState - The consumer's opaque per-request state type
|
|
1565
1583
|
* @param paths - One pathname, or a set of pathnames, matched exactly against `context.url.pathname`
|
|
@@ -1572,7 +1590,7 @@ function only(paths, handler) {
|
|
|
1572
1590
|
* ```
|
|
1573
1591
|
*/
|
|
1574
1592
|
function except(paths, handler) {
|
|
1575
|
-
const matches = new Set(
|
|
1593
|
+
const matches = new Set(isString(paths) ? [paths] : paths);
|
|
1576
1594
|
return async (request, context, next) => {
|
|
1577
1595
|
if (matches.has(context.url.pathname)) return next();
|
|
1578
1596
|
return handler(request, context, next);
|
|
@@ -1594,7 +1612,7 @@ function except(paths, handler) {
|
|
|
1594
1612
|
* ```
|
|
1595
1613
|
*/
|
|
1596
1614
|
function createCookieTransport(options) {
|
|
1597
|
-
if (!isString(options.secret) && (!
|
|
1615
|
+
if (!isString(options.secret) && (!isArray(options.secret) || !options.secret.every(isString))) throw new TypeError("CookieTransportOptions.secret must be a string or string array");
|
|
1598
1616
|
if (options.name !== void 0 && !isString(options.name)) throw new TypeError("CookieTransportOptions.name must be a string when provided");
|
|
1599
1617
|
const name = options.name ?? "session";
|
|
1600
1618
|
const secret = options.secret;
|