@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.
@@ -4,12 +4,12 @@ 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.
@@ -208,13 +209,13 @@ function matchesTrustedEntry(address, entry) {
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
@@ -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
@@ -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.
@@ -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) {
@@ -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}
@@ -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}
@@ -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}
@@ -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}
@@ -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
@@ -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`
@@ -1558,8 +1576,8 @@ function only(paths, handler) {
1558
1576
  };
1559
1577
  }
1560
1578
  /**
1561
- * Scopes a battery to run everywhere EXCEPT a set of exact pathnamesthere
1562
- * it steps aside through `next()`.
1579
+ * Scopes a battery to every pathname outside a set of exact oneson 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`