@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.
@@ -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 (bytes) `createCompression` will compress. */
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, in preference order —
11
- * intersected at construction with what the runtime's `CompressionStream`
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 the default `Content-Security-Policy` value `createSecurity` sets — a custom
26
- * `csp` option REPLACES this wholesale, never merges.
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/echoes a request identifier into. */
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 EXACT string — it must be
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 (!Number.isInteger(bits) || bits < 8 || bits > 32) return false;
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) => !Number.isInteger(value) || value < 0 || value > 255)) return false;
204
- if (addressOctets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) return false;
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 UNTRUSTED hop
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 CONSECUTIVE
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 COUNT or a `trusted` CIDR/exact roster
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 is eligible for the compression/ETag buffering pipeline
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
- * Skips a `HEAD` request, a `204`/`304` or otherwise bodyless response, an
307
- * `event-stream` response (SSE — buffering would hang the connection), and a
308
- * response that already carries the header the caller is about to set
309
- * (`skipHeader`, for example `Content-Encoding` for compression, `ETag` for the
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 OFF, `true` uses the secure default), the shape `createSecurity`'s
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` BELOW `options.threshold` (avoids buffering a
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 (Number.isFinite(declared) && declared < options.threshold) return response;
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 PREFLIGHT — an `OPTIONS` request
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 OWN enumerable property instead of hitting `Object.prototype`'s
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, AND a class instance (a real `Session`), since a
596
- * restored/stored session is routinely a class instance, not a literal.
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 (typeof value !== "object" || value === null) return false;
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 instanceof Map)) return false;
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 typeof set === "function" && typeof remove === "function" && typeof clear === "function";
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 typeof value.regenerate === "function" && typeof value.destroy === "function";
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) && typeof value.size === "number" && isString(value.mime) && typeof value.validated === "boolean" && isString(value.status) && isString(value.path);
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 (!Array.isArray(entries)) return false;
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 — pass as-is to
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 EVEN IF the session was continuously touched, since
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 && (!isFiniteNumber(options.capacity) || !Number.isInteger(options.capacity) || options.capacity <= 0)) throw new TypeError("MemorySessionStore requires options.capacity to be a positive integer when provided");
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` LEAVES the row in place —
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 instanceof Error ? error.message : String(error) : "internal server 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 && !Array.isArray(options.encodings)) throw new TypeError("CompressionOptions.encodings must be an array when provided");
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) && !Array.isArray(options.origin)) throw new TypeError("CorsOptions.origin must be a string or string array when provided");
1115
- if (options?.methods !== void 0 && !Array.isArray(options.methods)) throw new TypeError("CorsOptions.methods must be an array when provided");
1116
- if (options?.headers !== void 0 && !Array.isArray(options.headers)) throw new TypeError("CorsOptions.headers must be an array when provided");
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 = Array.isArray(origin);
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
- * MUST sit OUTSIDE `createBody` in the chain — it reconstructs the inbound
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 (!isFiniteNumber(proxies) || !Number.isInteger(proxies) || proxies < 1) throw new TypeError("ForwardedOptions.proxies must be a positive integer");
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 (!Array.isArray(trusted)) throw new TypeError("ForwardedOptions.trusted must be an array when provided");
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) && (!Array.isArray(options.secret) || !options.secret.every(isString))) throw new TypeError("BearerOptions.secret must be a string or string array");
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 (!isFiniteNumber(options.max) || !Number.isInteger(options.max) || options.max <= 0) throw new TypeError("LimiterOptions.max must be a positive integer");
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 && (!isFiniteNumber(options.capacity) || !Number.isInteger(options.capacity) || options.capacity <= 0)) throw new TypeError("LimiterOptions.capacity must be a positive integer when provided");
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 SAME awaited call the 400 check reads — `context.body()`
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) && (!Array.isArray(options.secret) || !options.secret.every(isString))) throw new TypeError("CSRFOptions.secret must be a string or string array");
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 && !Array.isArray(options.safe)) throw new TypeError("CSRFOptions.safe must be an array when provided");
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 run ONLY on a set of exact pathnames — elsewhere it
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(typeof paths === "string" ? [paths] : paths);
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 run everywhere EXCEPT a set of exact pathnames — there
1562
- * it steps aside through `next()`.
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(typeof paths === "string" ? [paths] : paths);
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) && (!Array.isArray(options.secret) || !options.secret.every(isString))) throw new TypeError("CookieTransportOptions.secret must be a string or string array");
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;