@orkestrel/middleware 0.0.1

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.
@@ -0,0 +1,1463 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _orkestrel_contract = require("@orkestrel/contract");
3
+ let _orkestrel_server = require("@orkestrel/server");
4
+ let _orkestrel_abort = require("@orkestrel/abort");
5
+ let _orkestrel_budget = require("@orkestrel/budget");
6
+ let _orkestrel_timeout = require("@orkestrel/timeout");
7
+ //#region src/core/constants.ts
8
+ /** Default minimum buffered body size (bytes) `createCompression` will compress. */
9
+ var DEFAULT_COMPRESSION_THRESHOLD = 1024;
10
+ /**
11
+ * Default content-codings `createCompression` offers, in preference order —
12
+ * intersected at construction with what the runtime's `CompressionStream`
13
+ * actually supports.
14
+ *
15
+ * @remarks
16
+ * The shipped `@orkestrel/server` peer's {@link Encoding} union is
17
+ * `'gzip' | 'deflate' | 'identity'` — it does not include `'br'` (see the
18
+ * deviation recorded against this constant in the build report). This
19
+ * default therefore offers every non-`identity` coding the peer's type
20
+ * admits; a node-face brotli variant, if one ships, extends this list there.
21
+ */
22
+ var DEFAULT_COMPRESSION_ENCODINGS = Object.freeze(["gzip", "deflate"]);
23
+ /** Default `X-Frame-Options` value `createSecurity` sets. */
24
+ var DEFAULT_FRAME_OPTIONS = "DENY";
25
+ /**
26
+ * Default `Content-Security-Policy` value `createSecurity` sets — a custom
27
+ * `csp` option REPLACES this wholesale, never merges.
28
+ */
29
+ var DEFAULT_CSP = "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'";
30
+ /** Default `Referrer-Policy` value `createSecurity` sets. */
31
+ var DEFAULT_REFERRER_POLICY = "strict-origin-when-cross-origin";
32
+ /** Default `Permissions-Policy` value `createSecurity` sets. */
33
+ var DEFAULT_PERMISSIONS_POLICY = "camera=(), microphone=(), geolocation=()";
34
+ /** Default `Cross-Origin-Opener-Policy` value `createSecurity` sets. */
35
+ var DEFAULT_COOP = "same-origin";
36
+ /** Default `Cross-Origin-Resource-Policy` value `createSecurity` sets. */
37
+ var DEFAULT_CORP = "same-origin";
38
+ /** Default `Origin-Agent-Cluster` value `createSecurity` sets. */
39
+ var DEFAULT_CLUSTER = "?1";
40
+ /** Value `createSecurity` sets for `Cross-Origin-Embedder-Policy` when `coep: true`. */
41
+ var DEFAULT_COEP = "require-corp";
42
+ /** Value `createSecurity` sets for `Strict-Transport-Security` when `hsts: true`. */
43
+ var DEFAULT_HSTS = "max-age=31536000; includeSubDomains";
44
+ /** Default header `createSecurity` mints/echoes a request identifier into. */
45
+ var DEFAULT_IDENTIFIER_HEADER = "x-request-id";
46
+ /** Default methods `createCors` advertises on a preflight response. */
47
+ var DEFAULT_CORS_METHODS = Object.freeze([
48
+ "GET",
49
+ "POST",
50
+ "PUT",
51
+ "PATCH",
52
+ "DELETE",
53
+ "OPTIONS"
54
+ ]);
55
+ /** Default headers `createCors` advertises on a preflight response. */
56
+ var DEFAULT_CORS_HEADERS = Object.freeze(["Content-Type", "Authorization"]);
57
+ /** Default response status `createDeadline` returns when its deadline fires first. */
58
+ var DEFAULT_DEADLINE_STATUS = 503;
59
+ /** Default header `createBearer` reads the token from. */
60
+ var DEFAULT_BEARER_HEADER = "authorization";
61
+ /** Default scheme prefix `createBearer` strips before verification. */
62
+ var DEFAULT_BEARER_SCHEME = "Bearer";
63
+ /** Default maximum number of distinct rate-limit keys `createLimiter` tracks before LRU eviction. */
64
+ var DEFAULT_LIMITER_CAPACITY = 1e4;
65
+ /** Default maximum number of distinct session ids `createMemorySessionStore` tracks before LRU (by last write) eviction. */
66
+ var DEFAULT_SESSION_CAPACITY = 1e4;
67
+ /** Default 429 body message `createLimiter` sends when a key is over budget. */
68
+ var DEFAULT_LIMITER_MESSAGE = "rate limit exceeded";
69
+ /** Default cookie name `createCookieTransport` writes the signed session id under. */
70
+ var DEFAULT_SESSION_COOKIE = "session";
71
+ /** Default header `createHeaderTransport` carries the session id in. */
72
+ var DEFAULT_SESSION_HEADER = "session-id";
73
+ /** Default signed-cookie name `createCSRF` writes the CSRF token under. */
74
+ var DEFAULT_CSRF_COOKIE = "csrf";
75
+ /** Default header `createCSRF` reads a mutating request's submitted token from. */
76
+ var DEFAULT_CSRF_HEADER = "x-csrf-token";
77
+ /** Default body field `createCSRF` falls back to reading a mutating request's submitted token from. */
78
+ var DEFAULT_CSRF_FIELD = "_csrf";
79
+ /** Default methods `createCSRF` treats as safe (mint instead of verify). */
80
+ var DEFAULT_CSRF_SAFE_METHODS = Object.freeze([
81
+ "GET",
82
+ "HEAD",
83
+ "OPTIONS"
84
+ ]);
85
+ //#endregion
86
+ //#region src/core/helpers.ts
87
+ /**
88
+ * Derive `createLimiter`'s default rate-limit bucket key from a request's
89
+ * resolved identity facts.
90
+ *
91
+ * @remarks
92
+ * Prefers a verified bearer token ({@link BearerState.token}) as
93
+ * `token:<value>`; else a resolved client IP ({@link ClientState.client.ip},
94
+ * set when `createForwarded` is mounted) or the raw socket peer
95
+ * ({@link ConnectionState.connection.ip}) collapsed via `clientRateKey`
96
+ * (IPv6 to its `/64` network) as `ip:<key>`; else the literal `ip:unknown`.
97
+ * Never reads `X-Forwarded-For` itself — that trust decision belongs solely
98
+ * to `createForwarded`.
99
+ *
100
+ * @param state - The slices `createLimiter`'s default key may read
101
+ * @returns The bucket key for the request
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * resolveKey({ token: 'abc' }) // 'token:abc'
106
+ * resolveKey({ client: { ip: '2001:db8::1' } }) // 'ip:2001:db8:0:0::/64'
107
+ * ```
108
+ */
109
+ function resolveKey(state) {
110
+ if (state.token !== void 0) return `token:${state.token}`;
111
+ const ip = state.client?.ip ?? state.connection?.ip;
112
+ if (ip !== void 0) return `ip:${(0, _orkestrel_server.clientRateKey)(ip)}`;
113
+ return "ip:unknown";
114
+ }
115
+ /**
116
+ * Build the `Retry-After` header value — whole seconds until a window reset,
117
+ * floored at a minimum of `1` (ruling I).
118
+ *
119
+ * @param resetAt - The window reset instant (same clock unit as `now`)
120
+ * @param now - The current instant
121
+ * @returns The `Retry-After` value in whole seconds, minimum `1`
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * buildRetryAfter(1_500, 1_000) // '1'
126
+ * ```
127
+ */
128
+ function buildRetryAfter(resetAt, now) {
129
+ const seconds = Math.ceil((resetAt - now) / 1e3);
130
+ return String(Math.max(1, seconds));
131
+ }
132
+ /**
133
+ * Build the draft `RateLimit` structured header field (ruling I) — emitted
134
+ * only when `createLimiter`'s `policy` option is `true`.
135
+ *
136
+ * @param remaining - The requests still admitted this window
137
+ * @param resetAt - The window reset instant (same clock unit as `now`)
138
+ * @param now - The current instant
139
+ * @returns The `RateLimit` header value
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * buildRateLimitField(4, 1_500, 1_000) // '"default";r=4;t=1'
144
+ * ```
145
+ */
146
+ function buildRateLimitField(remaining, resetAt, now) {
147
+ return `"default";r=${remaining};t=${Math.max(1, Math.ceil((resetAt - now) / 1e3))}`;
148
+ }
149
+ /**
150
+ * Build the draft `RateLimit-Policy` structured header field (ruling I) —
151
+ * emitted only when `createLimiter`'s `policy` option is `true`.
152
+ *
153
+ * @param max - The window's admitted request count
154
+ * @param window - The window length in milliseconds
155
+ * @returns The `RateLimit-Policy` header value
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * buildRateLimitPolicyField(10, 60_000) // '"default";q=10;w=60'
160
+ * ```
161
+ */
162
+ function buildRateLimitPolicyField(max, window) {
163
+ return `"default";q=${max};w=${Math.ceil(window / 1e3)}`;
164
+ }
165
+ /**
166
+ * Whether a candidate address is a bare (non-CIDR) trusted-hop match — an
167
+ * exact string match, or a simple prefix-CIDR match for IPv4 (`/8`–`/32`).
168
+ * An IPv6 entry matches by exact string only — there is no IPv6 CIDR
169
+ * support.
170
+ *
171
+ * @remarks
172
+ * Supports exact addresses and dotted-decimal IPv4 CIDR (`10.0.0.0/8`
173
+ * through `/32`) verbatim. An IPv6 entry is matched by exact string only
174
+ * (no CIDR expansion) — documented as the supported subset; `createForwarded`
175
+ * never claims full CIDR generality beyond IPv4.
176
+ *
177
+ * @remarks
178
+ * An IPv6 `trusted` roster entry is compared as an EXACT string — it must be
179
+ * supplied in canonical form (no zero-compression normalization, no case
180
+ * folding) by the caller; this function performs no IPv6 normalization of
181
+ * its own.
182
+ *
183
+ * @param address - The candidate hop address
184
+ * @param entry - One `trusted` roster entry — an exact address or an IPv4 CIDR
185
+ * @returns `true` when `address` is covered by `entry`
186
+ *
187
+ * @example
188
+ * ```ts
189
+ * matchesTrustedEntry('10.1.2.3', '10.0.0.0/8') // true
190
+ * matchesTrustedEntry('192.168.1.1', '10.0.0.0/8') // false
191
+ * ```
192
+ */
193
+ function matchesTrustedEntry(address, entry) {
194
+ const slash = entry.indexOf("/");
195
+ if (slash === -1) return address === entry;
196
+ const network = entry.slice(0, slash);
197
+ const bits = Number(entry.slice(slash + 1));
198
+ const networkParts = network.split(".");
199
+ const addressParts = address.split(".");
200
+ if (networkParts.length !== 4 || addressParts.length !== 4) return false;
201
+ if (!Number.isInteger(bits) || bits < 8 || bits > 32) return false;
202
+ const networkOctets = networkParts.map(Number);
203
+ const addressOctets = addressParts.map(Number);
204
+ if (networkOctets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) return false;
205
+ if (addressOctets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) return false;
206
+ const networkInt = (networkOctets[0] ?? 0) << 24 | (networkOctets[1] ?? 0) << 16 | (networkOctets[2] ?? 0) << 8 | (networkOctets[3] ?? 0);
207
+ const addressInt = (addressOctets[0] ?? 0) << 24 | (addressOctets[1] ?? 0) << 16 | (addressOctets[2] ?? 0) << 8 | (addressOctets[3] ?? 0);
208
+ const mask = bits === 32 ? -1 : ~((1 << 32 - bits) - 1);
209
+ return (networkInt & mask) === (addressInt & mask);
210
+ }
211
+ /**
212
+ * Walk `X-Forwarded-For` right-to-left and resolve the first UNTRUSTED hop
213
+ * address — `createForwarded`'s core algorithm.
214
+ *
215
+ * @remarks
216
+ * Parses `X-Forwarded-For` only. With `proxies` set, trusts exactly that
217
+ * many hops counted from the right (the closest to this server) and returns
218
+ * the next one left of them; with `trusted` set, trusts every CONSECUTIVE
219
+ * hop from the right that matches one of the roster
220
+ * ({@link matchesTrustedEntry}) and returns the first hop that does not. If
221
+ * the rightmost hop (the immediate sender) does not match the roster, the
222
+ * whole header is untrustworthy and this returns `undefined` rather than
223
+ * returning any client-supplied hop value. When every hop is trusted, or the
224
+ * header is absent/empty, returns `undefined` (the caller falls back to the
225
+ * socket peer).
226
+ *
227
+ * @param header - The raw `X-Forwarded-For` header value (comma-separated hops), if present
228
+ * @param trust - Either a trusted hop COUNT or a `trusted` CIDR/exact roster
229
+ * @returns The first untrusted hop address, or `undefined` when none qualifies
230
+ *
231
+ * @example
232
+ * ```ts
233
+ * resolveForwardedFor('203.0.113.7, 10.0.0.1', { proxies: 1 }) // '203.0.113.7'
234
+ * ```
235
+ */
236
+ function resolveForwardedFor(header, trust) {
237
+ if (header === void 0) return void 0;
238
+ const hops = header.split(",").map((hop) => hop.trim()).filter((hop) => hop.length > 0);
239
+ if (hops.length === 0) return void 0;
240
+ if ("proxies" in trust) {
241
+ const index = hops.length - trust.proxies - 1;
242
+ return index >= 0 ? hops[index] : void 0;
243
+ }
244
+ const rightmost = hops[hops.length - 1];
245
+ if (rightmost === void 0) return void 0;
246
+ if (!trust.trusted.some((entry) => matchesTrustedEntry(rightmost, entry))) return void 0;
247
+ for (let index = hops.length - 2; index >= 0; index -= 1) {
248
+ const hop = hops[index];
249
+ if (hop === void 0) return void 0;
250
+ if (!trust.trusted.some((entry) => matchesTrustedEntry(hop, entry))) return hop;
251
+ }
252
+ }
253
+ /**
254
+ * Feature-detect which of `candidates` the runtime's `CompressionStream`
255
+ * actually supports — `createCompression`'s construction-time intersection
256
+ * (ruling J).
257
+ *
258
+ * @remarks
259
+ * Probes each candidate with `new CompressionStream(candidate)` inside a
260
+ * `try`/`catch`; a coding the runtime rejects is dropped silently. `identity`
261
+ * is never probed (it has no `CompressionStream` coding and is always
262
+ * implicitly acceptable to negotiation).
263
+ *
264
+ * @param candidates - The codings to probe, in preference order
265
+ * @returns The subset of `candidates` the runtime's `CompressionStream` supports, in order
266
+ *
267
+ * @example
268
+ * ```ts
269
+ * detectEncodings(['gzip', 'deflate']) // ['gzip', 'deflate'] on a runtime with both
270
+ * ```
271
+ */
272
+ function detectEncodings(candidates) {
273
+ const supported = [];
274
+ for (const candidate of candidates) {
275
+ if (candidate === "identity") continue;
276
+ try {
277
+ if (new CompressionStream(candidate).readable) supported.push(candidate);
278
+ } catch {}
279
+ }
280
+ return supported;
281
+ }
282
+ /**
283
+ * Whether a response is eligible for the compression/ETag buffering pipeline
284
+ * (ruling J) — the shared cheap-skip predicate both batteries apply before
285
+ * ever touching `response.arrayBuffer()`.
286
+ *
287
+ * @remarks
288
+ * Skips a `HEAD` request, a `204`/`304` or otherwise bodyless response, an
289
+ * `event-stream` response (SSE — buffering would hang the connection), and a
290
+ * response that already carries the header the caller is about to set
291
+ * (`skipHeader`, e.g. `Content-Encoding` for compression, `ETag` for the
292
+ * ETag battery).
293
+ *
294
+ * @param method - The request's HTTP method
295
+ * @param response - The candidate response
296
+ * @param skipHeader - The response header whose presence means "already handled"
297
+ * @returns `true` when the response should be left untouched
298
+ *
299
+ * @example
300
+ * ```ts
301
+ * isBufferingIneligible('GET', new Response(null, { status: 204 }), 'content-encoding') // true
302
+ * ```
303
+ */
304
+ function isBufferingIneligible(method, response, skipHeader) {
305
+ if (method === "HEAD") return true;
306
+ if (response.status === 204 || response.status === 304) return true;
307
+ if (response.body === null) return true;
308
+ const contentType = response.headers.get("content-type");
309
+ if (contentType !== null && contentType.toLowerCase().startsWith("text/event-stream")) return true;
310
+ if (response.headers.has(skipHeader)) return true;
311
+ return false;
312
+ }
313
+ /**
314
+ * Whether a negotiated `Accept-Encoding` outcome is worth acting on —
315
+ * `createCompression`'s negotiation-eligibility half of ruling J's skip list.
316
+ *
317
+ * @param encoding - The negotiated coding, or `undefined` when negotiation failed
318
+ * @returns `true` when `encoding` names an actionable, non-`identity` coding
319
+ *
320
+ * @example
321
+ * ```ts
322
+ * isCompressionNegotiated('gzip') // true
323
+ * isCompressionNegotiated(undefined) // false
324
+ * ```
325
+ */
326
+ function isCompressionNegotiated(encoding) {
327
+ return encoding !== void 0 && encoding !== "identity";
328
+ }
329
+ /**
330
+ * Resolve an opt-in, value-bearing security header — `string | boolean`
331
+ * (default OFF, `true` uses the secure default), the shape `createSecurity`'s
332
+ * `coep`/`hsts` options use, distinct from the plain value-or-`false` shape
333
+ * `resolveSecurityHeader` (the peer substrate) handles.
334
+ *
335
+ * @param value - The option value — a `string` override, `true` for the secure default, or `false`/`undefined` to omit
336
+ * @param fallback - The secure-default value used when `value` is `true`
337
+ * @returns The header value to set, or `undefined` to omit the header
338
+ *
339
+ * @example
340
+ * ```ts
341
+ * resolveOptInHeader(true, 'require-corp') // 'require-corp'
342
+ * resolveOptInHeader(undefined, 'require-corp') // undefined — omitted
343
+ * ```
344
+ */
345
+ function resolveOptInHeader(value, fallback) {
346
+ if (value === true) return fallback;
347
+ if (value === false || value === void 0) return void 0;
348
+ return value;
349
+ }
350
+ /**
351
+ * Rebuild a `Response` around a replacement body while preserving its
352
+ * status/statusText — the buffered-response reconstruction shared by the
353
+ * compression and ETag batteries after they have consumed
354
+ * `response.arrayBuffer()`.
355
+ *
356
+ * @param body - The replacement body (already-buffered bytes, or `null`)
357
+ * @param response - The original response whose status/statusText/headers are preserved
358
+ * @param headers - The headers to apply; defaults to a fresh copy of `response.headers`
359
+ * @returns A new `Response` carrying `body` with `response`'s status/statusText
360
+ *
361
+ * @example
362
+ * ```ts
363
+ * rebuildResponse(bytes, response) // same status/statusText, response's headers copied
364
+ * rebuildResponse(bytes, response, headers) // explicit replacement headers
365
+ * ```
366
+ */
367
+ function rebuildResponse(body, response, headers) {
368
+ return new Response(body, {
369
+ status: response.status,
370
+ statusText: response.statusText,
371
+ headers: headers ?? new Headers(response.headers)
372
+ });
373
+ }
374
+ /**
375
+ * The shared negotiate → skip → threshold → compress → header-set skeleton
376
+ * both faces' `createCompression` batteries compose — response-body
377
+ * compression over a caller-supplied set of feature-detected codings.
378
+ *
379
+ * @remarks
380
+ * Decision order: {@link isBufferingIneligible} → `options.filter` → stamp
381
+ * `Vary: Accept-Encoding` (every negotiation-eligible response carries it,
382
+ * even when a later skip declines to compress) → `negotiateEncoding` over
383
+ * `options.encodings` → {@link isCompressionNegotiated} → `isCompressibleType`
384
+ * on `Content-Type` → a fast skip when the response already carries a
385
+ * numeric `Content-Length` BELOW `options.threshold` (avoids buffering a
386
+ * body known too small to be worth compressing) → buffer via
387
+ * `response.arrayBuffer()` → a threshold passthrough when the buffered size
388
+ * is still below `options.threshold` → `options.compress` → set
389
+ * `Content-Encoding` and a fresh `Content-Length` via {@link rebuildResponse}.
390
+ * Returns `response` unchanged (aside from the `Vary` stamp) on any skip.
391
+ *
392
+ * @param request - The inbound `Request` (read for `Accept-Encoding`)
393
+ * @param context - The `MiddlewareContext` (read for `context.method`)
394
+ * @param response - The downstream `Response` to consider compressing
395
+ * @param options - The threshold, optional filter, offered encodings, and the runtime's `compress` primitive
396
+ * @returns The original `response` when skipped, or a new compressed `Response`
397
+ *
398
+ * @example
399
+ * ```ts
400
+ * await compressResponse(request, context, response, {
401
+ * threshold: 1024,
402
+ * encodings: ['gzip'],
403
+ * compress: async (bytes, encoding) => gzip(bytes),
404
+ * })
405
+ * ```
406
+ */
407
+ async function compressResponse(request, context, response, options) {
408
+ if (isBufferingIneligible(context.method, response, "content-encoding")) return response;
409
+ if (options.filter !== void 0 && !options.filter(request, response)) return response;
410
+ response.headers.set("vary", (0, _orkestrel_server.mergeVary)(response.headers.get("vary") ?? void 0, "Accept-Encoding"));
411
+ const acceptEncoding = request.headers.get("accept-encoding");
412
+ if (acceptEncoding === null) return response;
413
+ const negotiated = (0, _orkestrel_server.negotiateEncoding)(acceptEncoding, options.encodings);
414
+ if (!isCompressionNegotiated(negotiated)) return response;
415
+ const contentType = response.headers.get("content-type");
416
+ if (contentType === null || !(0, _orkestrel_server.isCompressibleType)(contentType)) return response;
417
+ const declaredLength = response.headers.get("content-length");
418
+ if (declaredLength !== null) {
419
+ const declared = Number(declaredLength);
420
+ if (Number.isFinite(declared) && declared < options.threshold) return response;
421
+ }
422
+ const buffer = await response.arrayBuffer();
423
+ if (buffer.byteLength < options.threshold) return rebuildResponse(buffer, response);
424
+ const compressed = await options.compress(new Uint8Array(buffer), negotiated);
425
+ const headers = new Headers(response.headers);
426
+ headers.set("content-encoding", negotiated);
427
+ headers.set("content-length", String(compressed.byteLength));
428
+ return rebuildResponse(compressed, response, headers);
429
+ }
430
+ /**
431
+ * Copy every entry of one session's `data` into another — the regenerate
432
+ * data-carry `createSession`'s `control.regenerate()` applies (ruling D).
433
+ *
434
+ * @param from - The source session whose `data` is copied
435
+ * @param to - The destination session `data` is copied into
436
+ *
437
+ * @example
438
+ * ```ts
439
+ * transferSessionData(oldSession, newSession)
440
+ * ```
441
+ */
442
+ function transferSessionData(from, to) {
443
+ for (const [key, value] of from.data) to.data.set(key, value);
444
+ }
445
+ /**
446
+ * Determine whether a value implements {@link SessionInterface} — a total
447
+ * structural guard (§14): an `id` string plus a `data` `Map`.
448
+ *
449
+ * @param value - The candidate value
450
+ * @returns `true` when `value` is shaped like a {@link SessionInterface}
451
+ *
452
+ * @example
453
+ * ```ts
454
+ * isSession({ id: 'a', data: new Map() }) // true
455
+ * ```
456
+ */
457
+ function isSession(value) {
458
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
459
+ return (0, _orkestrel_contract.isString)(value.id) && value.data instanceof Map;
460
+ }
461
+ /**
462
+ * Determine whether a value implements {@link SessionControlInterface} — a
463
+ * total structural guard (§14): callable `regenerate` and `destroy`.
464
+ *
465
+ * @param value - The candidate value
466
+ * @returns `true` when `value` is shaped like a {@link SessionControlInterface}
467
+ *
468
+ * @example
469
+ * ```ts
470
+ * isSessionControl({ regenerate() {}, destroy() {} }) // true
471
+ * ```
472
+ */
473
+ function isSessionControl(value) {
474
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
475
+ return typeof value.regenerate === "function" && typeof value.destroy === "function";
476
+ }
477
+ /**
478
+ * Determine whether a value is one staged {@link MultipartFile} record — a
479
+ * total structural guard (§14) checking every required field's shape.
480
+ *
481
+ * @param value - The candidate value
482
+ * @returns `true` when `value` is shaped like a {@link MultipartFile}
483
+ */
484
+ function isMultipartFile(value) {
485
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
486
+ return (0, _orkestrel_contract.isString)(value.field) && (0, _orkestrel_contract.isString)(value.name) && typeof value.size === "number" && (0, _orkestrel_contract.isString)(value.mime) && typeof value.validated === "boolean" && (0, _orkestrel_contract.isString)(value.status) && (0, _orkestrel_contract.isString)(value.path);
487
+ }
488
+ /**
489
+ * Determine whether a value implements {@link MultipartBody} — a total
490
+ * structural guard (§14): `files` keyed by field name to arrays of
491
+ * {@link MultipartFile}, and a `fields` string record.
492
+ *
493
+ * @param value - The candidate value
494
+ * @returns `true` when `value` is shaped like a {@link MultipartBody}
495
+ *
496
+ * @example
497
+ * ```ts
498
+ * isMultipartBody({ files: {}, fields: { name: 'a' } }) // true
499
+ * ```
500
+ */
501
+ function isMultipartBody(value) {
502
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
503
+ if (!(0, _orkestrel_contract.isRecord)(value.files) || !(0, _orkestrel_contract.isRecord)(value.fields)) return false;
504
+ for (const entries of Object.values(value.files)) {
505
+ if (!Array.isArray(entries)) return false;
506
+ for (const entry of entries) if (!isMultipartFile(entry)) return false;
507
+ }
508
+ for (const fieldValue of Object.values(value.fields)) if (!(0, _orkestrel_contract.isString)(fieldValue)) return false;
509
+ return true;
510
+ }
511
+ /**
512
+ * Determine whether a request is a CORS PREFLIGHT — an `OPTIONS` request
513
+ * carrying an `Access-Control-Request-Method` header.
514
+ *
515
+ * @param method - The request's HTTP method
516
+ * @param headers - The request's `Headers`
517
+ * @returns `true` when the request is a CORS preflight `createCors` must answer
518
+ *
519
+ * @example
520
+ * ```ts
521
+ * isPreflight('OPTIONS', new Headers({ 'access-control-request-method': 'POST' })) // true
522
+ * ```
523
+ */
524
+ function isPreflight(method, headers) {
525
+ return method === "OPTIONS" && headers.has("access-control-request-method");
526
+ }
527
+ /**
528
+ * A parsed client-info fact for {@link ClientInfo} — a leaf shaping helper
529
+ * `createForwarded` uses to build its stashed slice.
530
+ *
531
+ * @param ip - The resolved client IP, if any
532
+ * @returns The {@link ClientInfo} slice value
533
+ *
534
+ * @example
535
+ * ```ts
536
+ * buildClientInfo('203.0.113.7') // { ip: '203.0.113.7' }
537
+ * ```
538
+ */
539
+ function buildClientInfo(ip) {
540
+ return { ip };
541
+ }
542
+ /**
543
+ * Constant-time string equality — `createCSRF`'s double-submit token
544
+ * comparison, avoiding a timing oracle on the submitted-vs-cookie match.
545
+ *
546
+ * @remarks
547
+ * Length-guarded XOR-accumulate over char codes: a length mismatch short
548
+ * circuits (safe — the lengths of two independently-generated tokens are
549
+ * not a useful timing signal), but once lengths match every character is
550
+ * compared with no early return, so a per-character mismatch never affects
551
+ * how long the comparison takes.
552
+ *
553
+ * @param a - The first string
554
+ * @param b - The second string
555
+ * @returns `true` when `a` and `b` are exactly equal
556
+ *
557
+ * @example
558
+ * ```ts
559
+ * equalsConstantTime('abc', 'abc') // true
560
+ * equalsConstantTime('abc', 'abd') // false
561
+ * ```
562
+ */
563
+ function equalsConstantTime(a, b) {
564
+ if (a.length !== b.length) return false;
565
+ let diff = 0;
566
+ for (let index = 0; index < a.length; index += 1) diff |= a.charCodeAt(index) ^ b.charCodeAt(index);
567
+ return diff === 0;
568
+ }
569
+ //#endregion
570
+ //#region src/core/Session.ts
571
+ /**
572
+ * A server-managed session's default entity — the `create` option's default
573
+ * value factory for `createSession` (ruling G: `Session` ships WITHOUT a
574
+ * `createSession` factory of its own, since that name belongs to the
575
+ * battery).
576
+ *
577
+ * @remarks
578
+ * `data` is a live, mutable `Map` a handler reads/writes directly;
579
+ * `createSession` persists it to the configured store on the way out.
580
+ *
581
+ * @example
582
+ * ```ts
583
+ * const session = new Session('abc123')
584
+ * session.data.set('userId', 'u_1')
585
+ * ```
586
+ */
587
+ var Session = class {
588
+ id;
589
+ data;
590
+ constructor(id) {
591
+ this.id = id;
592
+ this.data = /* @__PURE__ */ new Map();
593
+ }
594
+ };
595
+ //#endregion
596
+ //#region src/core/MemorySessionStore.ts
597
+ /**
598
+ * The default in-process {@link SessionStoreInterface} — a `Map`-backed store
599
+ * enforcing both an idle timeout and an absolute lifetime, with lazy
600
+ * (read-time) eviction, a bounded capacity, and no background timers.
601
+ *
602
+ * @typeParam S - The session data payload type
603
+ *
604
+ * @remarks
605
+ * `get` evicts a session whose idle time (`now - lastSeen >= ttl`) or
606
+ * absolute lifetime (`now - createdAt >= lifetime`) has elapsed — the
607
+ * lifetime check fires EVEN IF the session was continuously touched, since
608
+ * `createdAt` is stamped once at the first `set` and preserved across every
609
+ * later re-`set` of the same id. A live read touches `lastSeen`. `delete` of
610
+ * an absent id is a no-op.
611
+ *
612
+ * Capacity is enforced as least-recently-used **by last write**: `set`
613
+ * refreshes an id's recency (deleting then re-inserting so the Map's
614
+ * iteration tail is the most-recently-written id). Inserting a brand-new id
615
+ * once the store is at `capacity` first prunes expired entries; if the store
616
+ * is still full, the least-recently-written id (the Map's head) is evicted.
617
+ * `options.evict` — when provided — is invoked (throw-isolated) with the id
618
+ * on every eviction the store's own policy performs (a capacity eviction or
619
+ * an expired-entry prune), but never for an explicit `delete`.
620
+ *
621
+ * @example
622
+ * ```ts
623
+ * const store = new MemorySessionStore({ ttl: 60_000, lifetime: 3_600_000 })
624
+ * await store.set('abc', { userId: 'u_1' }, Date.now())
625
+ * ```
626
+ */
627
+ var MemorySessionStore = class {
628
+ #entries;
629
+ #ttl;
630
+ #lifetime;
631
+ #capacity;
632
+ #evict;
633
+ constructor(options) {
634
+ if (options?.ttl !== void 0 && !(0, _orkestrel_contract.isFiniteNumber)(options.ttl)) throw new TypeError("MemorySessionStore requires options.ttl to be a finite number when provided");
635
+ if (options?.ttl !== void 0 && options.ttl <= 0) throw new TypeError("MemorySessionStore requires options.ttl to be positive when provided");
636
+ if (options?.lifetime !== void 0 && !(0, _orkestrel_contract.isFiniteNumber)(options.lifetime)) throw new TypeError("MemorySessionStore requires options.lifetime to be a finite number when provided");
637
+ if (options?.lifetime !== void 0 && options.lifetime <= 0) throw new TypeError("MemorySessionStore requires options.lifetime to be positive when provided");
638
+ if (options?.capacity !== void 0 && (!(0, _orkestrel_contract.isFiniteNumber)(options.capacity) || !Number.isInteger(options.capacity) || options.capacity <= 0)) throw new TypeError("MemorySessionStore requires options.capacity to be a positive integer when provided");
639
+ if (options?.evict !== void 0 && !(0, _orkestrel_contract.isFunction)(options.evict)) throw new TypeError("MemorySessionStore requires options.evict to be a function when provided");
640
+ this.#entries = /* @__PURE__ */ new Map();
641
+ this.#ttl = options?.ttl;
642
+ this.#lifetime = options?.lifetime;
643
+ this.#capacity = options?.capacity ?? 1e4;
644
+ this.#evict = options?.evict;
645
+ }
646
+ async get(id, now) {
647
+ const entry = this.#entries.get(id);
648
+ if (entry === void 0) return void 0;
649
+ if (this.#expired(entry, now)) {
650
+ this.#entries.delete(id);
651
+ this.#notify(id);
652
+ return;
653
+ }
654
+ entry.lastSeen = now;
655
+ return entry.session;
656
+ }
657
+ async set(id, session, now) {
658
+ const existing = this.#entries.get(id);
659
+ if (existing === void 0) this.#reserve(now);
660
+ const createdAt = existing?.createdAt ?? now;
661
+ this.#entries.delete(id);
662
+ this.#entries.set(id, {
663
+ session,
664
+ lastSeen: now,
665
+ createdAt
666
+ });
667
+ }
668
+ async delete(id) {
669
+ this.#entries.delete(id);
670
+ }
671
+ #expired(entry, now) {
672
+ if (this.#ttl !== void 0 && now - entry.lastSeen >= this.#ttl) return true;
673
+ if (this.#lifetime !== void 0 && now - entry.createdAt >= this.#lifetime) return true;
674
+ return false;
675
+ }
676
+ #reserve(now) {
677
+ if (this.#entries.size < this.#capacity) return;
678
+ for (const [id, entry] of this.#entries) if (this.#expired(entry, now)) {
679
+ this.#entries.delete(id);
680
+ this.#notify(id);
681
+ }
682
+ if (this.#entries.size >= this.#capacity) {
683
+ const oldest = this.#entries.keys().next().value;
684
+ if (oldest !== void 0) {
685
+ this.#entries.delete(oldest);
686
+ this.#notify(oldest);
687
+ }
688
+ }
689
+ }
690
+ #notify(id) {
691
+ if (this.#evict === void 0) return;
692
+ try {
693
+ this.#evict(id);
694
+ } catch {}
695
+ }
696
+ };
697
+ //#endregion
698
+ //#region src/core/middlewares.ts
699
+ /**
700
+ * The outermost error-rendering battery — catches a downstream throw and
701
+ * renders it as a `Response`.
702
+ *
703
+ * @typeParam TState - The consumer's opaque per-request state type
704
+ * @param options - See {@link BoundaryOptions}
705
+ * @returns A `MiddlewareHandler<TState>`
706
+ * @throws {TypeError} When `options.expose` or `options.report` is malformed
707
+ *
708
+ * @example
709
+ * ```ts
710
+ * const boundary = createBoundary({ expose: false })
711
+ * ```
712
+ */
713
+ function createBoundary(options) {
714
+ if (options?.expose !== void 0 && !(0, _orkestrel_contract.isBoolean)(options.expose)) throw new TypeError("BoundaryOptions.expose must be a boolean when provided");
715
+ if (options?.report !== void 0 && !(0, _orkestrel_contract.isFunction)(options.report)) throw new TypeError("BoundaryOptions.report must be a function when provided");
716
+ const expose = options?.expose ?? false;
717
+ const report = options?.report;
718
+ return async (request, context, next) => {
719
+ try {
720
+ return await next();
721
+ } catch (error) {
722
+ if (report !== void 0) try {
723
+ report(error);
724
+ } catch {}
725
+ if ((0, _orkestrel_server.isHTTPError)(error)) return new Response(error.message, { status: error.status });
726
+ const message = expose ? error instanceof Error ? error.message : String(error) : "internal server error";
727
+ return new Response(message, { status: 500 });
728
+ }
729
+ };
730
+ }
731
+ /**
732
+ * The access-log/timing seam — records one {@link TelemetryEntry} per request
733
+ * after the response settles.
734
+ *
735
+ * @typeParam TState - The consumer's opaque per-request state type
736
+ * @param options - See {@link TelemetryOptions}
737
+ * @returns A `MiddlewareHandler<TState>`
738
+ * @throws {TypeError} When `options.record` is not a function
739
+ *
740
+ * @example
741
+ * ```ts
742
+ * const telemetry = createTelemetry({ record: (entry) => console.log(entry) })
743
+ * ```
744
+ */
745
+ function createTelemetry(options) {
746
+ if (!(0, _orkestrel_contract.isFunction)(options.record)) throw new TypeError("TelemetryOptions.record must be a function");
747
+ const record = options.record;
748
+ return async (request, context, next) => {
749
+ const start = Date.now();
750
+ let response;
751
+ try {
752
+ response = await next();
753
+ return response;
754
+ } finally {
755
+ const duration = Date.now() - start;
756
+ const status = response?.status ?? 500;
757
+ try {
758
+ record({
759
+ method: context.method,
760
+ pathname: context.url.pathname,
761
+ status,
762
+ duration
763
+ });
764
+ } catch {}
765
+ }
766
+ };
767
+ }
768
+ /**
769
+ * Response-body compression — negotiates and compresses a buffered response
770
+ * body over the runtime's feature-detected `CompressionStream` codings.
771
+ *
772
+ * @typeParam TState - The consumer's opaque per-request state type
773
+ * @param options - See {@link CompressionOptions}
774
+ * @returns A `MiddlewareHandler<TState>`
775
+ * @throws {TypeError} When `options.threshold` or `options.filter` is malformed
776
+ *
777
+ * @example
778
+ * ```ts
779
+ * const compression = createCompression({ threshold: 1024 })
780
+ * ```
781
+ */
782
+ function createCompression(options) {
783
+ if (options?.threshold !== void 0 && (!(0, _orkestrel_contract.isFiniteNumber)(options.threshold) || options.threshold < 0)) throw new TypeError("CompressionOptions.threshold must be a finite number when provided");
784
+ if (options?.filter !== void 0 && !(0, _orkestrel_contract.isFunction)(options.filter)) throw new TypeError("CompressionOptions.filter must be a function when provided");
785
+ if (options?.encodings !== void 0 && !Array.isArray(options.encodings)) throw new TypeError("CompressionOptions.encodings must be an array when provided");
786
+ const threshold = options?.threshold ?? 1024;
787
+ const encodings = detectEncodings(options?.encodings ?? DEFAULT_COMPRESSION_ENCODINGS);
788
+ const filter = options?.filter;
789
+ const compress = async (bytes, encoding) => {
790
+ const source = new Response(bytes).body;
791
+ if (source === null) return bytes;
792
+ const compressed = await new Response(source.pipeThrough(new CompressionStream(encoding))).arrayBuffer();
793
+ return new Uint8Array(compressed);
794
+ };
795
+ return async (request, context, next) => {
796
+ return compressResponse(request, context, await next(), {
797
+ threshold,
798
+ filter,
799
+ encodings,
800
+ compress
801
+ });
802
+ };
803
+ }
804
+ /**
805
+ * Security-headers + request-identifier battery.
806
+ *
807
+ * @typeParam TState - The consumer's opaque per-request state type, must carry {@link IdentifierState}
808
+ * @param options - See {@link SecurityOptions}
809
+ * @returns A `MiddlewareHandler<TState>`
810
+ * @throws {TypeError} When any option is malformed
811
+ *
812
+ * @example
813
+ * ```ts
814
+ * const security = createSecurity({ hsts: true })
815
+ * ```
816
+ */
817
+ function createSecurity(options) {
818
+ if (options?.frame !== void 0 && options.frame !== false && options.frame !== "DENY" && options.frame !== "SAMEORIGIN") throw new TypeError("SecurityOptions.frame must be 'DENY', 'SAMEORIGIN', or false when provided");
819
+ if (options?.csp !== void 0 && options.csp !== false && !(0, _orkestrel_contract.isString)(options.csp)) throw new TypeError("SecurityOptions.csp must be a string or false when provided");
820
+ if (options?.referrer !== void 0 && options.referrer !== false && !(0, _orkestrel_contract.isString)(options.referrer)) throw new TypeError("SecurityOptions.referrer must be a string or false when provided");
821
+ if (options?.permissions !== void 0 && options.permissions !== false && !(0, _orkestrel_contract.isString)(options.permissions)) throw new TypeError("SecurityOptions.permissions must be a string or false when provided");
822
+ if (options?.coop !== void 0 && options.coop !== false && !(0, _orkestrel_contract.isString)(options.coop)) throw new TypeError("SecurityOptions.coop must be a string or false when provided");
823
+ if (options?.corp !== void 0 && options.corp !== false && !(0, _orkestrel_contract.isString)(options.corp)) throw new TypeError("SecurityOptions.corp must be a string or false when provided");
824
+ if (options?.cluster !== void 0 && options.cluster !== false && !(0, _orkestrel_contract.isString)(options.cluster)) throw new TypeError("SecurityOptions.cluster must be a string or false when provided");
825
+ if (options?.coep !== void 0 && !(0, _orkestrel_contract.isBoolean)(options.coep) && !(0, _orkestrel_contract.isString)(options.coep)) throw new TypeError("SecurityOptions.coep must be a string or boolean when provided");
826
+ if (options?.hsts !== void 0 && !(0, _orkestrel_contract.isBoolean)(options.hsts) && !(0, _orkestrel_contract.isString)(options.hsts)) throw new TypeError("SecurityOptions.hsts must be a string or boolean when provided");
827
+ if (options?.identifier !== void 0 && options.identifier !== false && !(0, _orkestrel_contract.isRecord)(options.identifier)) throw new TypeError("SecurityOptions.identifier must be an options bag or false when provided");
828
+ if ((0, _orkestrel_contract.isRecord)(options?.identifier) && options.identifier.trust !== void 0 && !(0, _orkestrel_contract.isBoolean)(options.identifier.trust)) throw new TypeError("SecurityOptions.identifier.trust must be a boolean when provided");
829
+ const frame = options?.frame;
830
+ const csp = options?.csp;
831
+ const referrer = options?.referrer;
832
+ const permissions = options?.permissions;
833
+ const coop = options?.coop;
834
+ const corp = options?.corp;
835
+ const cluster = options?.cluster;
836
+ const coep = options?.coep;
837
+ const hsts = options?.hsts;
838
+ const identifierOption = options?.identifier;
839
+ const identifierEnabled = identifierOption !== false;
840
+ const trust = (0, _orkestrel_contract.isRecord)(identifierOption) ? identifierOption.trust === true : false;
841
+ return async (request, context, next) => {
842
+ let identifier;
843
+ if (identifierEnabled) {
844
+ const incoming = request.headers.get(DEFAULT_IDENTIFIER_HEADER);
845
+ identifier = trust && incoming !== null && (0, _orkestrel_server.isValidRequestId)(incoming) ? incoming : crypto.randomUUID();
846
+ context.state.identifier = identifier;
847
+ }
848
+ const response = await next();
849
+ response.headers.set("x-content-type-options", "nosniff");
850
+ const headerTable = [
851
+ ["x-frame-options", (0, _orkestrel_server.resolveSecurityHeader)(frame, DEFAULT_FRAME_OPTIONS)],
852
+ ["content-security-policy", (0, _orkestrel_server.resolveSecurityHeader)(csp, DEFAULT_CSP)],
853
+ ["referrer-policy", (0, _orkestrel_server.resolveSecurityHeader)(referrer, DEFAULT_REFERRER_POLICY)],
854
+ ["permissions-policy", (0, _orkestrel_server.resolveSecurityHeader)(permissions, DEFAULT_PERMISSIONS_POLICY)],
855
+ ["cross-origin-opener-policy", (0, _orkestrel_server.resolveSecurityHeader)(coop, DEFAULT_COOP)],
856
+ ["cross-origin-resource-policy", (0, _orkestrel_server.resolveSecurityHeader)(corp, DEFAULT_CORP)],
857
+ ["origin-agent-cluster", (0, _orkestrel_server.resolveSecurityHeader)(cluster, "?1")],
858
+ ["cross-origin-embedder-policy", resolveOptInHeader(coep, DEFAULT_COEP)],
859
+ ["strict-transport-security", resolveOptInHeader(hsts, DEFAULT_HSTS)]
860
+ ];
861
+ for (const [name, value] of headerTable) if (value !== void 0) response.headers.set(name, value);
862
+ if (identifierEnabled && identifier !== void 0) response.headers.set(DEFAULT_IDENTIFIER_HEADER, identifier);
863
+ return response;
864
+ };
865
+ }
866
+ /**
867
+ * Cross-Origin Resource Sharing battery.
868
+ *
869
+ * @typeParam TState - The consumer's opaque per-request state type
870
+ * @param options - See {@link CorsOptions}
871
+ * @returns A `MiddlewareHandler<TState>`
872
+ * @throws {TypeError} When any option is malformed
873
+ *
874
+ * @example
875
+ * ```ts
876
+ * const cors = createCors({ origin: ['https://app.example'] })
877
+ * ```
878
+ */
879
+ function createCors(options) {
880
+ if (options?.origin !== void 0 && !(0, _orkestrel_contract.isString)(options.origin) && !Array.isArray(options.origin)) throw new TypeError("CorsOptions.origin must be a string or string array when provided");
881
+ if (options?.methods !== void 0 && !Array.isArray(options.methods)) throw new TypeError("CorsOptions.methods must be an array when provided");
882
+ if (options?.headers !== void 0 && !Array.isArray(options.headers)) throw new TypeError("CorsOptions.headers must be an array when provided");
883
+ const origin = options?.origin ?? "*";
884
+ const methods = options?.methods ?? DEFAULT_CORS_METHODS;
885
+ const headers = options?.headers ?? DEFAULT_CORS_HEADERS;
886
+ const reflecting = Array.isArray(origin);
887
+ return async (request, context, next) => {
888
+ const requestOrigin = request.headers.get("origin") ?? void 0;
889
+ const allowOrigin = (0, _orkestrel_server.resolveOrigin)(origin, requestOrigin);
890
+ if (isPreflight(context.method, request.headers)) {
891
+ const preflightHeaders = new Headers();
892
+ if (allowOrigin !== void 0) preflightHeaders.set("access-control-allow-origin", allowOrigin);
893
+ preflightHeaders.set("access-control-allow-methods", methods.join(", "));
894
+ preflightHeaders.set("access-control-allow-headers", headers.join(", "));
895
+ if (reflecting && allowOrigin !== void 0) preflightHeaders.set("vary", (0, _orkestrel_server.mergeVary)(void 0, "Origin"));
896
+ return new Response(null, {
897
+ status: 204,
898
+ headers: preflightHeaders
899
+ });
900
+ }
901
+ const response = await next();
902
+ if (allowOrigin !== void 0) response.headers.set("access-control-allow-origin", allowOrigin);
903
+ if (reflecting && allowOrigin !== void 0) response.headers.set("vary", (0, _orkestrel_server.mergeVary)(response.headers.get("vary") ?? void 0, "Origin"));
904
+ return response;
905
+ };
906
+ }
907
+ /**
908
+ * The application-level per-request deadline battery.
909
+ *
910
+ * @typeParam TState - The consumer's opaque per-request state type
911
+ * @param options - See {@link DeadlineOptions}
912
+ * @returns A `MiddlewareHandler<TState>`
913
+ * @throws {TypeError} When `options.ms` or `options.status` is malformed
914
+ *
915
+ * @remarks
916
+ * MUST sit OUTSIDE `createBody` in the chain — it reconstructs the inbound
917
+ * `Request` (to link its `signal` to the deadline `signal`), which throws if
918
+ * the body was already consumed upstream (e.g. by `createBody`'s cached read).
919
+ *
920
+ * @example
921
+ * ```ts
922
+ * const deadline = createDeadline({ ms: 5_000 })
923
+ * ```
924
+ */
925
+ function createDeadline(options) {
926
+ if (!(0, _orkestrel_contract.isFiniteNumber)(options.ms) || options.ms <= 0) throw new TypeError("DeadlineOptions.ms must be a finite number");
927
+ if (options.status !== void 0 && !(0, _orkestrel_contract.isFiniteNumber)(options.status)) throw new TypeError("DeadlineOptions.status must be a finite number when provided");
928
+ const ms = options.ms;
929
+ const status = options.status ?? 503;
930
+ return async (request, context, next) => {
931
+ const timeout = (0, _orkestrel_timeout.createTimeout)({ ms });
932
+ const linked = (0, _orkestrel_abort.linkSignal)(timeout.signal, request.signal);
933
+ timeout.start();
934
+ const downstream = next(new Request(request, { signal: linked }));
935
+ const deadline = new Promise((resolve) => {
936
+ timeout.signal.addEventListener("abort", () => resolve(new Response(null, { status })), { once: true });
937
+ });
938
+ let deadlineWon = false;
939
+ try {
940
+ return await Promise.race([downstream, deadline.then((response) => {
941
+ deadlineWon = true;
942
+ return response;
943
+ })]);
944
+ } finally {
945
+ timeout.clear();
946
+ downstream.then((response) => {
947
+ if (deadlineWon) response.body?.cancel().catch(() => {});
948
+ }).catch(() => {});
949
+ }
950
+ };
951
+ }
952
+ /**
953
+ * The trusted-proxy client-IP resolver battery.
954
+ *
955
+ * @typeParam TState - The consumer's opaque per-request state type, must carry {@link ClientState} and {@link ConnectionState}
956
+ * @param options - See {@link ForwardedOptions}
957
+ * @returns A `MiddlewareHandler<TState>`
958
+ * @throws {TypeError} When neither or both of `proxies`/`trusted` are provided, or either is malformed
959
+ *
960
+ * @example
961
+ * ```ts
962
+ * const forwarded = createForwarded({ proxies: 1 })
963
+ * ```
964
+ */
965
+ function createForwarded(options) {
966
+ const hasProxies = (0, _orkestrel_contract.isRecord)(options) && "proxies" in options;
967
+ const hasTrusted = (0, _orkestrel_contract.isRecord)(options) && "trusted" in options;
968
+ if (!(0, _orkestrel_contract.isRecord)(options) || hasProxies === hasTrusted) throw new TypeError("ForwardedOptions requires exactly one of proxies or trusted");
969
+ if (hasProxies) {
970
+ const proxies = options.proxies;
971
+ if (!(0, _orkestrel_contract.isFiniteNumber)(proxies) || !Number.isInteger(proxies) || proxies < 1) throw new TypeError("ForwardedOptions.proxies must be a positive integer");
972
+ } else {
973
+ const trusted = options.trusted;
974
+ if (!Array.isArray(trusted)) throw new TypeError("ForwardedOptions.trusted must be an array when provided");
975
+ }
976
+ const trust = hasProxies ? { proxies: options.proxies } : { trusted: options.trusted };
977
+ return async (request, context, next) => {
978
+ const ip = resolveForwardedFor(request.headers.get("x-forwarded-for") ?? void 0, trust) ?? context.state.connection?.ip;
979
+ context.state.client = buildClientInfo(ip);
980
+ return next();
981
+ };
982
+ }
983
+ /**
984
+ * Dynamic response `ETag` + conditional GET battery.
985
+ *
986
+ * @typeParam TState - The consumer's opaque per-request state type
987
+ * @param options - See {@link ETagOptions}
988
+ * @returns A `MiddlewareHandler<TState>`
989
+ * @throws {TypeError} When `options.weak` is not a boolean
990
+ *
991
+ * @example
992
+ * ```ts
993
+ * const etag = createETag({ weak: true })
994
+ * ```
995
+ */
996
+ function createETag(options) {
997
+ if (options?.weak !== void 0 && !(0, _orkestrel_contract.isBoolean)(options.weak)) throw new TypeError("ETagOptions.weak must be a boolean when provided");
998
+ const weak = options?.weak ?? true;
999
+ return async (request, context, next) => {
1000
+ const response = await next();
1001
+ if (context.method !== "GET" || response.status !== 200) return response;
1002
+ if (isBufferingIneligible(context.method, response, "etag")) return response;
1003
+ const buffer = await response.arrayBuffer();
1004
+ const bytes = new Uint8Array(buffer);
1005
+ const etag = await (0, _orkestrel_server.computeBodyETag)(bytes, weak);
1006
+ const ifNoneMatch = request.headers.get("if-none-match");
1007
+ if (ifNoneMatch !== null && (0, _orkestrel_server.matchesETag)(ifNoneMatch, etag)) {
1008
+ const headers = new Headers(response.headers);
1009
+ headers.set("etag", etag);
1010
+ headers.delete("content-length");
1011
+ return new Response(null, {
1012
+ status: 304,
1013
+ headers
1014
+ });
1015
+ }
1016
+ const headers = new Headers(response.headers);
1017
+ headers.set("etag", etag);
1018
+ return rebuildResponse(bytes, response, headers);
1019
+ };
1020
+ }
1021
+ /**
1022
+ * Bearer-token authentication battery.
1023
+ *
1024
+ * @typeParam TState - The consumer's opaque per-request state type, must carry {@link BearerState}
1025
+ * @param options - See {@link BearerOptions}
1026
+ * @returns A `MiddlewareHandler<TState>`
1027
+ * @throws {TypeError} When any option is malformed
1028
+ * @throws {HTTPError} `401` when the token is missing, invalid, or expired
1029
+ *
1030
+ * @example
1031
+ * ```ts
1032
+ * const bearer = createBearer({ secret: 'shh' })
1033
+ * ```
1034
+ */
1035
+ function createBearer(options) {
1036
+ if (!(0, _orkestrel_contract.isString)(options.secret) && (!Array.isArray(options.secret) || !options.secret.every(_orkestrel_contract.isString))) throw new TypeError("BearerOptions.secret must be a string or string array");
1037
+ if (options.header !== void 0 && !(0, _orkestrel_contract.isString)(options.header)) throw new TypeError("BearerOptions.header must be a string when provided");
1038
+ if (options.scheme !== void 0 && !(0, _orkestrel_contract.isString)(options.scheme)) throw new TypeError("BearerOptions.scheme must be a string when provided");
1039
+ const secret = options.secret;
1040
+ const header = (options.header ?? "authorization").toLowerCase();
1041
+ const scheme = options.scheme ?? "Bearer";
1042
+ return async (request, context, next) => {
1043
+ const raw = request.headers.get(header);
1044
+ if (raw === null) throw new _orkestrel_server.HTTPError(401, "missing token");
1045
+ let candidate = raw;
1046
+ if (scheme !== "") {
1047
+ const prefix = `${scheme.toLowerCase()} `;
1048
+ if (!raw.toLowerCase().startsWith(prefix)) throw new _orkestrel_server.HTTPError(401, "missing token");
1049
+ candidate = raw.slice(prefix.length);
1050
+ }
1051
+ const verified = await (0, _orkestrel_server.verifyToken)(candidate, secret);
1052
+ if (verified === void 0) throw new _orkestrel_server.HTTPError(401, "invalid token");
1053
+ context.state.token = verified;
1054
+ return next();
1055
+ };
1056
+ }
1057
+ /**
1058
+ * Fixed-window rate-limiting battery.
1059
+ *
1060
+ * @typeParam TState - The consumer's opaque per-request state type, must carry {@link BearerState}, {@link ClientState}, and {@link ConnectionState}
1061
+ * @param options - See {@link LimiterOptions}
1062
+ * @returns A `MiddlewareHandler<TState>`
1063
+ * @throws {TypeError} When any option is malformed
1064
+ *
1065
+ * @example
1066
+ * ```ts
1067
+ * const limiter = createLimiter({ max: 100, window: 60_000 })
1068
+ * ```
1069
+ */
1070
+ function createLimiter(options) {
1071
+ if (!(0, _orkestrel_contract.isFiniteNumber)(options.max) || !Number.isInteger(options.max) || options.max <= 0) throw new TypeError("LimiterOptions.max must be a positive integer");
1072
+ if (!(0, _orkestrel_contract.isFiniteNumber)(options.window) || options.window <= 0) throw new TypeError("LimiterOptions.window must be a positive finite number");
1073
+ if (options.capacity !== void 0 && (!(0, _orkestrel_contract.isFiniteNumber)(options.capacity) || !Number.isInteger(options.capacity) || options.capacity <= 0)) throw new TypeError("LimiterOptions.capacity must be a positive integer when provided");
1074
+ if (options.key !== void 0 && !(0, _orkestrel_contract.isFunction)(options.key)) throw new TypeError("LimiterOptions.key must be a function when provided");
1075
+ if (options.message !== void 0 && !(0, _orkestrel_contract.isString)(options.message)) throw new TypeError("LimiterOptions.message must be a string when provided");
1076
+ if (options.clock !== void 0 && !(0, _orkestrel_contract.isFunction)(options.clock)) throw new TypeError("LimiterOptions.clock must be a function when provided");
1077
+ if (options.policy !== void 0 && !(0, _orkestrel_contract.isBoolean)(options.policy)) throw new TypeError("LimiterOptions.policy must be a boolean when provided");
1078
+ if (options.evict !== void 0 && !(0, _orkestrel_contract.isFunction)(options.evict)) throw new TypeError("LimiterOptions.evict must be a function when provided");
1079
+ const max = options.max;
1080
+ const window = options.window;
1081
+ const capacity = options.capacity ?? 1e4;
1082
+ const deriveKey = options.key ?? ((context) => resolveKey(context.state));
1083
+ const message = options.message ?? "rate limit exceeded";
1084
+ const clock = options.clock ?? Date.now;
1085
+ const policy = options.policy ?? false;
1086
+ const evict = options.evict;
1087
+ const notify = (evictedKey) => {
1088
+ if (evict === void 0) return;
1089
+ try {
1090
+ evict(evictedKey);
1091
+ } catch {}
1092
+ };
1093
+ const buckets = /* @__PURE__ */ new Map();
1094
+ return async (request, context, next) => {
1095
+ const key = deriveKey(context);
1096
+ const now = clock();
1097
+ let bucket = buckets.get(key);
1098
+ if (bucket === void 0) {
1099
+ if (buckets.size >= capacity) {
1100
+ const oldest = buckets.keys().next().value;
1101
+ if (oldest !== void 0) {
1102
+ buckets.delete(oldest);
1103
+ notify(oldest);
1104
+ }
1105
+ }
1106
+ bucket = {
1107
+ budget: (0, _orkestrel_budget.createBudget)({
1108
+ max,
1109
+ consume: (value) => value
1110
+ }),
1111
+ resetAt: now + window
1112
+ };
1113
+ buckets.set(key, bucket);
1114
+ } else {
1115
+ buckets.delete(key);
1116
+ buckets.set(key, bucket);
1117
+ if (now >= bucket.resetAt) {
1118
+ bucket.budget.clear();
1119
+ bucket.resetAt = now + window;
1120
+ }
1121
+ }
1122
+ if (bucket.budget.exhausted) {
1123
+ const headers = new Headers();
1124
+ headers.set("retry-after", buildRetryAfter(bucket.resetAt, now));
1125
+ if (policy) {
1126
+ headers.set("ratelimit", buildRateLimitField(0, bucket.resetAt, now));
1127
+ headers.set("ratelimit-policy", buildRateLimitPolicyField(max, window));
1128
+ }
1129
+ return new Response(message, {
1130
+ status: 429,
1131
+ headers
1132
+ });
1133
+ }
1134
+ bucket.budget.consume(1);
1135
+ return next();
1136
+ };
1137
+ }
1138
+ /**
1139
+ * The body-driving battery — eagerly awaits the cached `context.body()` so
1140
+ * its throws (or a malformed-JSON `undefined`) surface before the handler runs.
1141
+ *
1142
+ * @typeParam TState - The consumer's opaque per-request state type
1143
+ * @returns A `MiddlewareHandler<TState>`
1144
+ * @throws {HTTPError} `400` when the request declares `application/json` and the body resolves `undefined`
1145
+ *
1146
+ * @remarks
1147
+ * The shipped `MiddlewareContext.body()` is a parameterless, server-owned
1148
+ * cache (`ServerOptions.limit` governs its size cap) — this battery carries
1149
+ * no `limit`/`decompression` options (a deliberate break from the deleted old
1150
+ * `createBodyParser` surface, which configured them itself).
1151
+ *
1152
+ * @example
1153
+ * ```ts
1154
+ * const body = createBody()
1155
+ * ```
1156
+ */
1157
+ function createBody() {
1158
+ return async (request, context, next) => {
1159
+ const body = await context.body();
1160
+ const contentType = request.headers.get("content-type");
1161
+ if (contentType !== null && contentType.toLowerCase().startsWith("application/json") && body === void 0) throw new _orkestrel_server.HTTPError(400, "invalid json");
1162
+ return next();
1163
+ };
1164
+ }
1165
+ /**
1166
+ * The generic session battery — resolves, mints, and persists a session
1167
+ * across the request, with a mid-handler `regenerate`/`destroy` control handle.
1168
+ *
1169
+ * @typeParam S - The session entity type the store persists (must implement {@link SessionInterface})
1170
+ * @typeParam TState - The consumer's opaque per-request state type, must carry {@link SessionState} and {@link ConnectionState}
1171
+ * @param options - See {@link SessionOptions}
1172
+ * @returns A `MiddlewareHandler<TState>`
1173
+ * @throws {TypeError} When any option is malformed
1174
+ * @throws {HTTPError} `404` when `require` is set and no session resolves or mints
1175
+ *
1176
+ * @example
1177
+ * ```ts
1178
+ * const session = createSession({ transport: createHeaderTransport() })
1179
+ * ```
1180
+ */
1181
+ function createSession(options) {
1182
+ if (!(0, _orkestrel_contract.isFunction)(options.transport.read) || !(0, _orkestrel_contract.isFunction)(options.transport.write) || !(0, _orkestrel_contract.isFunction)(options.transport.clear)) throw new TypeError("SessionOptions.transport must implement SessionTransport");
1183
+ if (options.store !== void 0 && (!(0, _orkestrel_contract.isFunction)(options.store.get) || !(0, _orkestrel_contract.isFunction)(options.store.set) || !(0, _orkestrel_contract.isFunction)(options.store.delete))) throw new TypeError("SessionOptions.store must implement SessionStoreInterface when provided");
1184
+ if (options.ttl !== void 0 && !(0, _orkestrel_contract.isFiniteNumber)(options.ttl)) throw new TypeError("SessionOptions.ttl must be a finite number when provided");
1185
+ if (options.lifetime !== void 0 && !(0, _orkestrel_contract.isFiniteNumber)(options.lifetime)) throw new TypeError("SessionOptions.lifetime must be a finite number when provided");
1186
+ if (options.create !== void 0 && !(0, _orkestrel_contract.isFunction)(options.create)) throw new TypeError("SessionOptions.create must be a function when provided");
1187
+ if (options.mint !== void 0 && !(0, _orkestrel_contract.isFunction)(options.mint)) throw new TypeError("SessionOptions.mint must be a function when provided");
1188
+ if (options.require !== void 0 && !(0, _orkestrel_contract.isBoolean)(options.require)) throw new TypeError("SessionOptions.require must be a boolean when provided");
1189
+ if (options.ends !== void 0 && !(0, _orkestrel_contract.isBoolean)(options.ends)) throw new TypeError("SessionOptions.ends must be a boolean when provided");
1190
+ if (options.clock !== void 0 && !(0, _orkestrel_contract.isFunction)(options.clock)) throw new TypeError("SessionOptions.clock must be a function when provided");
1191
+ const transport = options.transport;
1192
+ const clock = options.clock ?? Date.now;
1193
+ const store = options.store ?? new MemorySessionStore({
1194
+ ttl: options.ttl,
1195
+ lifetime: options.lifetime,
1196
+ capacity: options.capacity,
1197
+ evict: options.evict
1198
+ });
1199
+ const create = options.create ?? ((id) => new Session(id));
1200
+ const mint = options.mint;
1201
+ const requireSession = options.require ?? false;
1202
+ const ends = options.ends ?? false;
1203
+ return async (request, context, next) => {
1204
+ const now = clock();
1205
+ const encrypted = context.state.connection?.encrypted ?? false;
1206
+ const incomingId = await transport.read(request);
1207
+ let session = incomingId !== void 0 ? await store.get(incomingId, now) : void 0;
1208
+ if (ends && context.method === "DELETE" && session !== void 0) {
1209
+ await store.delete(session.id);
1210
+ return new Response(null, { status: 204 });
1211
+ }
1212
+ let minted = false;
1213
+ if (session === void 0) {
1214
+ if (mint !== void 0 ? await mint(context) : true) {
1215
+ const id = crypto.randomUUID();
1216
+ session = create(id);
1217
+ minted = true;
1218
+ } else if (requireSession) throw new _orkestrel_server.HTTPError(404, "session required");
1219
+ }
1220
+ let destroyed = false;
1221
+ let regenerated;
1222
+ const activeSession = session;
1223
+ if (activeSession !== void 0) {
1224
+ const control = {
1225
+ regenerate() {
1226
+ if (destroyed) return;
1227
+ const newSession = create(crypto.randomUUID());
1228
+ transferSessionData(activeSession, newSession);
1229
+ regenerated = newSession;
1230
+ },
1231
+ destroy() {
1232
+ destroyed = true;
1233
+ regenerated = void 0;
1234
+ }
1235
+ };
1236
+ context.state.session = activeSession;
1237
+ context.state.control = control;
1238
+ }
1239
+ const response = await next();
1240
+ if (activeSession !== void 0) if (destroyed) {
1241
+ await store.delete(activeSession.id);
1242
+ await transport.clear(response);
1243
+ } else if (regenerated !== void 0) {
1244
+ await store.set(regenerated.id, regenerated, clock());
1245
+ await store.delete(activeSession.id);
1246
+ await transport.write(response, regenerated.id, encrypted);
1247
+ } else {
1248
+ await store.set(activeSession.id, activeSession, clock());
1249
+ if (minted) await transport.write(response, activeSession.id, encrypted);
1250
+ }
1251
+ return response;
1252
+ };
1253
+ }
1254
+ /**
1255
+ * Session-bound double-submit CSRF protection battery.
1256
+ *
1257
+ * @typeParam TState - The consumer's opaque per-request state type, must carry {@link CSRFState}, {@link SessionState}, and {@link ConnectionState}
1258
+ * @param options - See {@link CSRFOptions}
1259
+ * @returns A `MiddlewareHandler<TState>`
1260
+ * @throws {TypeError} When any option is malformed
1261
+ * @throws {HTTPError} `403` when the submitted token is missing, mismatched, or bound to a different session
1262
+ *
1263
+ * @example
1264
+ * ```ts
1265
+ * const csrf = createCSRF({ secret: 'shh' })
1266
+ * ```
1267
+ */
1268
+ function createCSRF(options) {
1269
+ if (!(0, _orkestrel_contract.isString)(options.secret) && (!Array.isArray(options.secret) || !options.secret.every(_orkestrel_contract.isString))) throw new TypeError("CSRFOptions.secret must be a string or string array");
1270
+ if (options.cookie !== void 0 && !(0, _orkestrel_contract.isString)(options.cookie)) throw new TypeError("CSRFOptions.cookie must be a string when provided");
1271
+ if (options.header !== void 0 && !(0, _orkestrel_contract.isString)(options.header)) throw new TypeError("CSRFOptions.header must be a string when provided");
1272
+ if (options.field !== void 0 && !(0, _orkestrel_contract.isString)(options.field)) throw new TypeError("CSRFOptions.field must be a string when provided");
1273
+ if (options.safe !== void 0 && !Array.isArray(options.safe)) throw new TypeError("CSRFOptions.safe must be an array when provided");
1274
+ const secret = options.secret;
1275
+ const cookieName = options.cookie ?? "csrf";
1276
+ const header = options.header ?? "x-csrf-token";
1277
+ const field = options.field ?? "_csrf";
1278
+ const safe = options.safe ?? DEFAULT_CSRF_SAFE_METHODS;
1279
+ return async (request, context, next) => {
1280
+ if (safe.includes(context.method)) {
1281
+ const token = await (0, _orkestrel_server.signToken)(context.state.session?.id ?? crypto.randomUUID(), { secret });
1282
+ context.state.csrf = token;
1283
+ const response = await next();
1284
+ const secure = (0, _orkestrel_server.resolveSecure)(void 0, context.state.connection?.encrypted ?? false);
1285
+ await (0, _orkestrel_server.writeSignedCookie)(response.headers, cookieName, token, secret, {
1286
+ sameSite: "Strict",
1287
+ httpOnly: false,
1288
+ path: "/",
1289
+ secure
1290
+ });
1291
+ return response;
1292
+ }
1293
+ const headerToken = request.headers.get(header);
1294
+ let submitted;
1295
+ if (headerToken !== null) submitted = headerToken;
1296
+ else {
1297
+ const body = await context.body();
1298
+ if ((0, _orkestrel_contract.isRecord)(body) && (0, _orkestrel_contract.isString)(body[field])) submitted = body[field];
1299
+ }
1300
+ if (submitted === void 0) throw new _orkestrel_server.HTTPError(403, "invalid csrf token");
1301
+ const cookieToken = await (0, _orkestrel_server.readSignedCookie)(request, cookieName, secret);
1302
+ if (cookieToken === void 0 || !equalsConstantTime(cookieToken, submitted)) throw new _orkestrel_server.HTTPError(403, "invalid csrf token");
1303
+ const sessionId = context.state.session?.id;
1304
+ if (sessionId !== void 0) {
1305
+ if (await (0, _orkestrel_server.verifyToken)(cookieToken, secret) !== sessionId) throw new _orkestrel_server.HTTPError(403, "invalid csrf token");
1306
+ }
1307
+ return next();
1308
+ };
1309
+ }
1310
+ //#endregion
1311
+ //#region src/core/factories.ts
1312
+ /**
1313
+ * Create a signed-cookie {@link SessionTransport} — the session id travels as
1314
+ * a `signToken`-signed cookie value.
1315
+ *
1316
+ * @param options - See {@link CookieTransportOptions}
1317
+ * @returns A {@link SessionTransport}
1318
+ * @throws {TypeError} When `options.secret` or `options.name` is malformed
1319
+ *
1320
+ * @example
1321
+ * ```ts
1322
+ * const transport = createCookieTransport({ secret: 'shh' })
1323
+ * ```
1324
+ */
1325
+ function createCookieTransport(options) {
1326
+ if (!(0, _orkestrel_contract.isString)(options.secret) && (!Array.isArray(options.secret) || !options.secret.every(_orkestrel_contract.isString))) throw new TypeError("CookieTransportOptions.secret must be a string or string array");
1327
+ if (options.name !== void 0 && !(0, _orkestrel_contract.isString)(options.name)) throw new TypeError("CookieTransportOptions.name must be a string when provided");
1328
+ const name = options.name ?? "session";
1329
+ const secret = options.secret;
1330
+ const cookie = options.cookie;
1331
+ return {
1332
+ read(request) {
1333
+ return (0, _orkestrel_server.readSignedCookie)(request, name, secret);
1334
+ },
1335
+ async write(response, id, encrypted) {
1336
+ const secure = (0, _orkestrel_server.resolveSecure)(cookie?.secure, encrypted);
1337
+ await (0, _orkestrel_server.writeSignedCookie)(response.headers, name, id, secret, {
1338
+ ...cookie,
1339
+ secure
1340
+ });
1341
+ },
1342
+ clear(response) {
1343
+ (0, _orkestrel_server.clearCookie)(response.headers, name, cookie);
1344
+ }
1345
+ };
1346
+ }
1347
+ /**
1348
+ * Create a bare-header {@link SessionTransport} — the session id travels
1349
+ * verbatim in a request/response header.
1350
+ *
1351
+ * @param options - See {@link HeaderTransportOptions}
1352
+ * @returns A {@link SessionTransport}
1353
+ * @throws {TypeError} When `options.header` is malformed
1354
+ *
1355
+ * @example
1356
+ * ```ts
1357
+ * const transport = createHeaderTransport()
1358
+ * ```
1359
+ */
1360
+ function createHeaderTransport(options) {
1361
+ if (options?.header !== void 0 && !(0, _orkestrel_contract.isString)(options.header)) throw new TypeError("HeaderTransportOptions.header must be a string when provided");
1362
+ const header = options?.header ?? "session-id";
1363
+ return {
1364
+ read(request) {
1365
+ return request.headers.get(header) ?? void 0;
1366
+ },
1367
+ write(response, id) {
1368
+ response.headers.set(header, id);
1369
+ },
1370
+ clear(response) {
1371
+ response.headers.delete(header);
1372
+ }
1373
+ };
1374
+ }
1375
+ /**
1376
+ * Create the default in-process {@link SessionStoreInterface} — a `Map`-backed
1377
+ * store enforcing an idle timeout and an absolute lifetime.
1378
+ *
1379
+ * @typeParam S - The session data payload type
1380
+ * @param options - See {@link MemorySessionStoreOptions}
1381
+ * @returns A {@link SessionStoreInterface}
1382
+ * @throws {TypeError} When `options.ttl` or `options.lifetime` is malformed
1383
+ *
1384
+ * @remarks
1385
+ * The {@link Session} entity is the `create` option's default value factory
1386
+ * for `createSession` and deliberately ships WITHOUT its own `create*`
1387
+ * factory — the name `createSession` belongs to the battery, not this class.
1388
+ *
1389
+ * @example
1390
+ * ```ts
1391
+ * const store = createMemorySessionStore({ ttl: 60_000 })
1392
+ * ```
1393
+ */
1394
+ function createMemorySessionStore(options) {
1395
+ return new MemorySessionStore(options);
1396
+ }
1397
+ //#endregion
1398
+ exports.DEFAULT_BEARER_HEADER = DEFAULT_BEARER_HEADER;
1399
+ exports.DEFAULT_BEARER_SCHEME = DEFAULT_BEARER_SCHEME;
1400
+ exports.DEFAULT_CLUSTER = DEFAULT_CLUSTER;
1401
+ exports.DEFAULT_COEP = DEFAULT_COEP;
1402
+ exports.DEFAULT_COMPRESSION_ENCODINGS = DEFAULT_COMPRESSION_ENCODINGS;
1403
+ exports.DEFAULT_COMPRESSION_THRESHOLD = DEFAULT_COMPRESSION_THRESHOLD;
1404
+ exports.DEFAULT_COOP = DEFAULT_COOP;
1405
+ exports.DEFAULT_CORP = DEFAULT_CORP;
1406
+ exports.DEFAULT_CORS_HEADERS = DEFAULT_CORS_HEADERS;
1407
+ exports.DEFAULT_CORS_METHODS = DEFAULT_CORS_METHODS;
1408
+ exports.DEFAULT_CSP = DEFAULT_CSP;
1409
+ exports.DEFAULT_CSRF_COOKIE = DEFAULT_CSRF_COOKIE;
1410
+ exports.DEFAULT_CSRF_FIELD = DEFAULT_CSRF_FIELD;
1411
+ exports.DEFAULT_CSRF_HEADER = DEFAULT_CSRF_HEADER;
1412
+ exports.DEFAULT_CSRF_SAFE_METHODS = DEFAULT_CSRF_SAFE_METHODS;
1413
+ exports.DEFAULT_DEADLINE_STATUS = DEFAULT_DEADLINE_STATUS;
1414
+ exports.DEFAULT_FRAME_OPTIONS = DEFAULT_FRAME_OPTIONS;
1415
+ exports.DEFAULT_HSTS = DEFAULT_HSTS;
1416
+ exports.DEFAULT_IDENTIFIER_HEADER = DEFAULT_IDENTIFIER_HEADER;
1417
+ exports.DEFAULT_LIMITER_CAPACITY = DEFAULT_LIMITER_CAPACITY;
1418
+ exports.DEFAULT_LIMITER_MESSAGE = DEFAULT_LIMITER_MESSAGE;
1419
+ exports.DEFAULT_PERMISSIONS_POLICY = DEFAULT_PERMISSIONS_POLICY;
1420
+ exports.DEFAULT_REFERRER_POLICY = DEFAULT_REFERRER_POLICY;
1421
+ exports.DEFAULT_SESSION_CAPACITY = DEFAULT_SESSION_CAPACITY;
1422
+ exports.DEFAULT_SESSION_COOKIE = DEFAULT_SESSION_COOKIE;
1423
+ exports.DEFAULT_SESSION_HEADER = DEFAULT_SESSION_HEADER;
1424
+ exports.MemorySessionStore = MemorySessionStore;
1425
+ exports.Session = Session;
1426
+ exports.buildClientInfo = buildClientInfo;
1427
+ exports.buildRateLimitField = buildRateLimitField;
1428
+ exports.buildRateLimitPolicyField = buildRateLimitPolicyField;
1429
+ exports.buildRetryAfter = buildRetryAfter;
1430
+ exports.compressResponse = compressResponse;
1431
+ exports.createBearer = createBearer;
1432
+ exports.createBody = createBody;
1433
+ exports.createBoundary = createBoundary;
1434
+ exports.createCSRF = createCSRF;
1435
+ exports.createCompression = createCompression;
1436
+ exports.createCookieTransport = createCookieTransport;
1437
+ exports.createCors = createCors;
1438
+ exports.createDeadline = createDeadline;
1439
+ exports.createETag = createETag;
1440
+ exports.createForwarded = createForwarded;
1441
+ exports.createHeaderTransport = createHeaderTransport;
1442
+ exports.createLimiter = createLimiter;
1443
+ exports.createMemorySessionStore = createMemorySessionStore;
1444
+ exports.createSecurity = createSecurity;
1445
+ exports.createSession = createSession;
1446
+ exports.createTelemetry = createTelemetry;
1447
+ exports.detectEncodings = detectEncodings;
1448
+ exports.equalsConstantTime = equalsConstantTime;
1449
+ exports.isBufferingIneligible = isBufferingIneligible;
1450
+ exports.isCompressionNegotiated = isCompressionNegotiated;
1451
+ exports.isMultipartBody = isMultipartBody;
1452
+ exports.isMultipartFile = isMultipartFile;
1453
+ exports.isPreflight = isPreflight;
1454
+ exports.isSession = isSession;
1455
+ exports.isSessionControl = isSessionControl;
1456
+ exports.matchesTrustedEntry = matchesTrustedEntry;
1457
+ exports.rebuildResponse = rebuildResponse;
1458
+ exports.resolveForwardedFor = resolveForwardedFor;
1459
+ exports.resolveKey = resolveKey;
1460
+ exports.resolveOptInHeader = resolveOptInHeader;
1461
+ exports.transferSessionData = transferSessionData;
1462
+
1463
+ //# sourceMappingURL=index.cjs.map