@orkestrel/middleware 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,13 @@
1
1
  import { integerShape, isBoolean, isFiniteNumber, isFunction, isRecord, isString, jsonShape, stringShape } from "@orkestrel/contract";
2
- import { HTTPError, clearCookie, clientRateKey, computeBodyETag, isCompressibleType, isHTTPError, isValidRequestId, matchesETag, mergeVary, negotiateEncoding, readSignedCookie, resolveOrigin, resolveSecure, resolveSecurityHeader, signToken, verifyToken, writeSignedCookie } from "@orkestrel/server";
2
+ import { HTTPError, clearCookie, computeBodyETag, computeClientKey, isCompressibleType, isHTTPError, isValidRequestId, matchesETag, mergeVary, negotiateEncoding, readSignedCookie, resolveOrigin, resolveSecure, resolveSecurityHeader, signToken, verifyToken, writeSignedCookie } from "@orkestrel/server";
3
3
  import { linkSignal } from "@orkestrel/abort";
4
4
  import { createBudget } from "@orkestrel/budget";
5
5
  import { createTimeout } from "@orkestrel/timeout";
6
6
  //#region src/core/constants.ts
7
- /** Default minimum buffered body size (bytes) `createCompression` will compress. */
7
+ /** Holds the default minimum buffered body size (bytes) `createCompression` will compress. */
8
8
  var DEFAULT_COMPRESSION_THRESHOLD = 1024;
9
9
  /**
10
- * Default content-codings `createCompression` offers, in preference order —
10
+ * Lists the default content-codings `createCompression` offers, in preference order —
11
11
  * intersected at construction with what the runtime's `CompressionStream`
12
12
  * actually supports.
13
13
  *
@@ -19,30 +19,30 @@ 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
- /** Default `X-Frame-Options` value `createSecurity` sets. */
22
+ /** Holds the default `X-Frame-Options` value `createSecurity` sets. */
23
23
  var DEFAULT_FRAME_OPTIONS = "DENY";
24
24
  /**
25
- * Default `Content-Security-Policy` value `createSecurity` sets — a custom
25
+ * Holds the default `Content-Security-Policy` value `createSecurity` sets — a custom
26
26
  * `csp` option REPLACES this wholesale, never merges.
27
27
  */
28
28
  var DEFAULT_CSP = "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'";
29
- /** Default `Referrer-Policy` value `createSecurity` sets. */
29
+ /** Holds the default `Referrer-Policy` value `createSecurity` sets. */
30
30
  var DEFAULT_REFERRER_POLICY = "strict-origin-when-cross-origin";
31
- /** Default `Permissions-Policy` value `createSecurity` sets. */
31
+ /** Holds the default `Permissions-Policy` value `createSecurity` sets. */
32
32
  var DEFAULT_PERMISSIONS_POLICY = "camera=(), microphone=(), geolocation=()";
33
- /** Default `Cross-Origin-Opener-Policy` value `createSecurity` sets. */
33
+ /** Holds the default `Cross-Origin-Opener-Policy` value `createSecurity` sets. */
34
34
  var DEFAULT_COOP = "same-origin";
35
- /** Default `Cross-Origin-Resource-Policy` value `createSecurity` sets. */
35
+ /** Holds the default `Cross-Origin-Resource-Policy` value `createSecurity` sets. */
36
36
  var DEFAULT_CORP = "same-origin";
37
- /** Default `Origin-Agent-Cluster` value `createSecurity` sets. */
37
+ /** Holds the default `Origin-Agent-Cluster` value `createSecurity` sets. */
38
38
  var DEFAULT_CLUSTER = "?1";
39
- /** Value `createSecurity` sets for `Cross-Origin-Embedder-Policy` when `coep: true`. */
39
+ /** Holds the value `createSecurity` sets for `Cross-Origin-Embedder-Policy` when `coep: true`. */
40
40
  var DEFAULT_COEP = "require-corp";
41
- /** Value `createSecurity` sets for `Strict-Transport-Security` when `hsts: true`. */
41
+ /** Holds the value `createSecurity` sets for `Strict-Transport-Security` when `hsts: true`. */
42
42
  var DEFAULT_HSTS = "max-age=31536000; includeSubDomains";
43
- /** Default header `createSecurity` mints/echoes a request identifier into. */
43
+ /** Names the default header `createSecurity` mints/echoes a request identifier into. */
44
44
  var DEFAULT_IDENTIFIER_HEADER = "x-request-id";
45
- /** Default methods `createCors` advertises on a preflight response. */
45
+ /** Lists the default methods `createCors` advertises on a preflight response. */
46
46
  var DEFAULT_CORS_METHODS = Object.freeze([
47
47
  "GET",
48
48
  "POST",
@@ -51,73 +51,47 @@ var DEFAULT_CORS_METHODS = Object.freeze([
51
51
  "DELETE",
52
52
  "OPTIONS"
53
53
  ]);
54
- /** Default headers `createCors` advertises on a preflight response. */
54
+ /** Lists the default headers `createCors` advertises on a preflight response. */
55
55
  var DEFAULT_CORS_HEADERS = Object.freeze(["Content-Type", "Authorization"]);
56
- /** Default response status `createDeadline` returns when its deadline fires first. */
56
+ /** Holds the default response status `createDeadline` returns when its deadline fires first. */
57
57
  var DEFAULT_DEADLINE_STATUS = 503;
58
- /** Default header `createBearer` reads the token from. */
58
+ /** Names the default header `createBearer` reads the token from. */
59
59
  var DEFAULT_BEARER_HEADER = "authorization";
60
- /** Default scheme prefix `createBearer` strips before verification. */
60
+ /** Names the default scheme prefix `createBearer` strips before verification. */
61
61
  var DEFAULT_BEARER_SCHEME = "Bearer";
62
- /** Default maximum number of distinct rate-limit keys `createLimiter` tracks before LRU eviction. */
62
+ /** Holds the default maximum number of distinct rate-limit keys `createLimiter` tracks before LRU eviction. */
63
63
  var DEFAULT_LIMITER_CAPACITY = 1e4;
64
- /** Default maximum number of distinct session ids `createMemorySessionStore` tracks before LRU (by last write) eviction. */
64
+ /** Holds the default maximum number of distinct session ids `createMemorySessionStore` tracks before LRU (by last write) eviction. */
65
65
  var DEFAULT_SESSION_CAPACITY = 1e4;
66
- /** Default 429 body message `createLimiter` sends when a key is over budget. */
66
+ /** Holds the default 429 body message `createLimiter` sends when a key is over budget. */
67
67
  var DEFAULT_LIMITER_MESSAGE = "rate limit exceeded";
68
- /** Default cookie name `createCookieTransport` writes the signed session id under. */
68
+ /** Names the default cookie `createCookieTransport` writes the signed session id under. */
69
69
  var DEFAULT_SESSION_COOKIE = "session";
70
- /** Default header `createHeaderTransport` carries the session id in. */
70
+ /** Names the default header `createHeaderTransport` carries the session id in. */
71
71
  var DEFAULT_SESSION_HEADER = "session-id";
72
- /** Default signed-cookie name `createCSRF` writes the CSRF token under. */
72
+ /** Names the default signed cookie `createCSRF` writes the CSRF token under. */
73
73
  var DEFAULT_CSRF_COOKIE = "csrf";
74
- /** Default header `createCSRF` reads a mutating request's submitted token from. */
74
+ /** Names the default header `createCSRF` reads a mutating request's submitted token from. */
75
75
  var DEFAULT_CSRF_HEADER = "x-csrf-token";
76
- /** Default body field `createCSRF` falls back to reading a mutating request's submitted token from. */
76
+ /** Names the default body field `createCSRF` falls back to reading a mutating request's submitted token from. */
77
77
  var DEFAULT_CSRF_FIELD = "_csrf";
78
- /** Default methods `createCSRF` treats as safe (mint instead of verify). */
78
+ /** Lists the default methods `createCSRF` treats as safe (mint instead of verify). */
79
79
  var DEFAULT_CSRF_SAFE_METHODS = Object.freeze([
80
80
  "GET",
81
81
  "HEAD",
82
82
  "OPTIONS"
83
83
  ]);
84
84
  //#endregion
85
- //#region src/core/Session.ts
86
- /**
87
- * A server-managed session's default entity — the `create` option's default
88
- * value factory for `createSession` (ruling G: `Session` ships WITHOUT a
89
- * `createSession` factory of its own, since that name belongs to the
90
- * battery).
91
- *
92
- * @remarks
93
- * `data` is a live, mutable `Map` a handler reads/writes directly;
94
- * `createSession` persists it to the configured store on the way out.
95
- *
96
- * @example
97
- * ```ts
98
- * const session = new Session('abc123')
99
- * session.data.set('userId', 'u_1')
100
- * ```
101
- */
102
- var Session = class {
103
- id;
104
- data;
105
- constructor(id) {
106
- this.id = id;
107
- this.data = /* @__PURE__ */ new Map();
108
- }
109
- };
110
- //#endregion
111
85
  //#region src/core/helpers.ts
112
86
  /**
113
- * Derive `createLimiter`'s default rate-limit bucket key from a request's
87
+ * Derives `createLimiter`'s default rate-limit bucket key from a request's
114
88
  * resolved identity facts.
115
89
  *
116
90
  * @remarks
117
91
  * Prefers a verified bearer token ({@link BearerState.token}) as
118
92
  * `token:<value>`; else a resolved client IP ({@link ClientState.client.ip},
119
93
  * set when `createForwarded` is mounted) or the raw socket peer
120
- * ({@link ConnectionState.connection.ip}) collapsed via `clientRateKey`
94
+ * ({@link ConnectionState.connection.ip}) collapsed through `computeClientKey`
121
95
  * (IPv6 to its `/64` network) as `ip:<key>`; else the literal `ip:unknown`.
122
96
  * Never reads `X-Forwarded-For` itself — that trust decision belongs solely
123
97
  * to `createForwarded`.
@@ -134,12 +108,12 @@ var Session = class {
134
108
  function resolveKey(state) {
135
109
  if (state.token !== void 0) return `token:${state.token}`;
136
110
  const ip = state.client?.ip ?? state.connection?.ip;
137
- if (ip !== void 0) return `ip:${clientRateKey(ip)}`;
111
+ if (ip !== void 0) return `ip:${computeClientKey(ip)}`;
138
112
  return "ip:unknown";
139
113
  }
140
114
  /**
141
- * Build the `Retry-After` header value — whole seconds until a window reset,
142
- * floored at a minimum of `1` (ruling I).
115
+ * Builds the `Retry-After` header value — whole seconds until a window reset,
116
+ * floored at a minimum of `1`.
143
117
  *
144
118
  * @param resetAt - The window reset instant (same clock unit as `now`)
145
119
  * @param now - The current instant
@@ -155,8 +129,8 @@ function buildRetryAfter(resetAt, now) {
155
129
  return String(Math.max(1, seconds));
156
130
  }
157
131
  /**
158
- * Build the draft `RateLimit` structured header field (ruling I) emitted
159
- * only when `createLimiter`'s `policy` option is `true`.
132
+ * Builds the draft `RateLimit` structured header field emitted only when
133
+ * `createLimiter`'s `policy` option is `true`.
160
134
  *
161
135
  * @param remaining - The requests still admitted this window
162
136
  * @param resetAt - The window reset instant (same clock unit as `now`)
@@ -172,8 +146,8 @@ function buildRateLimitField(remaining, resetAt, now) {
172
146
  return `"default";r=${remaining};t=${Math.max(1, Math.ceil((resetAt - now) / 1e3))}`;
173
147
  }
174
148
  /**
175
- * Build the draft `RateLimit-Policy` structured header field (ruling I)
176
- * emitted only when `createLimiter`'s `policy` option is `true`.
149
+ * Builds the draft `RateLimit-Policy` structured header field emitted only
150
+ * when `createLimiter`'s `policy` option is `true`.
177
151
  *
178
152
  * @param max - The window's admitted request count
179
153
  * @param window - The window length in milliseconds
@@ -188,7 +162,7 @@ function buildRateLimitPolicyField(max, window) {
188
162
  return `"default";q=${max};w=${Math.ceil(window / 1e3)}`;
189
163
  }
190
164
  /**
191
- * Whether a candidate address is a bare (non-CIDR) trusted-hop match — an
165
+ * Checks whether a candidate address is a bare (non-CIDR) trusted-hop match — an
192
166
  * exact string match, or a simple prefix-CIDR match for IPv4 (`/8`–`/32`).
193
167
  * An IPv6 entry matches by exact string only — there is no IPv6 CIDR
194
168
  * support.
@@ -207,7 +181,7 @@ function buildRateLimitPolicyField(max, window) {
207
181
  *
208
182
  * @param address - The candidate hop address
209
183
  * @param entry - One `trusted` roster entry — an exact address or an IPv4 CIDR
210
- * @returns `true` when `address` is covered by `entry`
184
+ * @returns True if `address` is covered by `entry`; false otherwise
211
185
  *
212
186
  * @example
213
187
  * ```ts
@@ -234,7 +208,7 @@ function matchesTrustedEntry(address, entry) {
234
208
  return (networkInt & mask) === (addressInt & mask);
235
209
  }
236
210
  /**
237
- * Walk `X-Forwarded-For` right-to-left and resolve the first UNTRUSTED hop
211
+ * Walks `X-Forwarded-For` right-to-left and resolves the first UNTRUSTED hop
238
212
  * address — `createForwarded`'s core algorithm.
239
213
  *
240
214
  * @remarks
@@ -250,7 +224,7 @@ function matchesTrustedEntry(address, entry) {
250
224
  * socket peer).
251
225
  *
252
226
  * @param header - The raw `X-Forwarded-For` header value (comma-separated hops), if present
253
- * @param trust - Either a trusted hop COUNT or a `trusted` CIDR/exact roster
227
+ * @param trust - The {@link ForwardedOptions} form in force — a trusted hop COUNT or a `trusted` CIDR/exact roster
254
228
  * @returns The first untrusted hop address, or `undefined` when none qualifies
255
229
  *
256
230
  * @example
@@ -276,9 +250,8 @@ function resolveForwardedFor(header, trust) {
276
250
  }
277
251
  }
278
252
  /**
279
- * Feature-detect which of `candidates` the runtime's `CompressionStream`
280
- * actually supports — `createCompression`'s construction-time intersection
281
- * (ruling J).
253
+ * Feature-detects which of `candidates` the runtime's `CompressionStream`
254
+ * actually supports — `createCompression`'s construction-time intersection.
282
255
  *
283
256
  * @remarks
284
257
  * Probes each candidate with `new CompressionStream(candidate)` inside a
@@ -305,7 +278,7 @@ function detectEncodings(candidates) {
305
278
  return supported;
306
279
  }
307
280
  /**
308
- * Compress bytes with the host-independent `CompressionStream` primitive.
281
+ * Compresses bytes with the host-independent `CompressionStream` primitive.
309
282
  *
310
283
  * @param bytes - The uncompressed response bytes
311
284
  * @param encoding - The negotiated actionable coding
@@ -325,21 +298,21 @@ async function compressBytes(bytes, encoding) {
325
298
  return new Uint8Array(compressed);
326
299
  }
327
300
  /**
328
- * Whether a response is eligible for the compression/ETag buffering pipeline
329
- * (ruling J) — the shared cheap-skip predicate both batteries apply before
330
- * ever touching `response.arrayBuffer()`.
301
+ * Checks whether a response is eligible for the compression/ETag buffering pipeline
302
+ * — the shared cheap-skip predicate both batteries apply before ever touching
303
+ * `response.arrayBuffer()`.
331
304
  *
332
305
  * @remarks
333
306
  * Skips a `HEAD` request, a `204`/`304` or otherwise bodyless response, an
334
307
  * `event-stream` response (SSE — buffering would hang the connection), and a
335
308
  * response that already carries the header the caller is about to set
336
- * (`skipHeader`, e.g. `Content-Encoding` for compression, `ETag` for the
309
+ * (`skipHeader`, for example `Content-Encoding` for compression, `ETag` for the
337
310
  * ETag battery).
338
311
  *
339
312
  * @param method - The request's HTTP method
340
313
  * @param response - The candidate response
341
314
  * @param skipHeader - The response header whose presence means "already handled"
342
- * @returns `true` when the response should be left untouched
315
+ * @returns True if the response must be left untouched; false otherwise
343
316
  *
344
317
  * @example
345
318
  * ```ts
@@ -356,11 +329,11 @@ function isBufferingIneligible(method, response, skipHeader) {
356
329
  return false;
357
330
  }
358
331
  /**
359
- * Whether a negotiated `Accept-Encoding` outcome is worth acting on —
360
- * `createCompression`'s negotiation-eligibility half of ruling J's skip list.
332
+ * Checks whether a negotiated `Accept-Encoding` outcome is worth acting on —
333
+ * `createCompression`'s negotiation-eligibility half of the skip list.
361
334
  *
362
335
  * @param encoding - The negotiated coding, or `undefined` when negotiation failed
363
- * @returns `true` when `encoding` names an actionable, non-`identity` coding
336
+ * @returns True if `encoding` names an actionable, non-`identity` coding; false otherwise
364
337
  *
365
338
  * @example
366
339
  * ```ts
@@ -372,7 +345,7 @@ function isCompressionNegotiated(encoding) {
372
345
  return encoding !== void 0 && encoding !== "identity";
373
346
  }
374
347
  /**
375
- * Resolve an opt-in, value-bearing security header — `string | boolean`
348
+ * Resolves an opt-in, value-bearing security header — `string | boolean`
376
349
  * (default OFF, `true` uses the secure default), the shape `createSecurity`'s
377
350
  * `coep`/`hsts` options use, distinct from the plain value-or-`false` shape
378
351
  * `resolveSecurityHeader` (the peer substrate) handles.
@@ -393,7 +366,7 @@ function resolveOptInHeader(value, fallback) {
393
366
  return value;
394
367
  }
395
368
  /**
396
- * Rebuild a `Response` around a replacement body while preserving its
369
+ * Rebuilds a `Response` around a replacement body while preserving its
397
370
  * status/statusText — the buffered-response reconstruction shared by the
398
371
  * compression and ETag batteries after they have consumed
399
372
  * `response.arrayBuffer()`.
@@ -417,7 +390,7 @@ function rebuildResponse(body, response, headers) {
417
390
  });
418
391
  }
419
392
  /**
420
- * The shared negotiate → skip → threshold → compress → header-set skeleton
393
+ * Runs the shared negotiate → skip → threshold → compress → header-set skeleton
421
394
  * both faces' `createCompression` batteries compose — response-body
422
395
  * compression over a caller-supplied set of feature-detected codings.
423
396
  *
@@ -428,16 +401,16 @@ function rebuildResponse(body, response, headers) {
428
401
  * `options.encodings` → {@link isCompressionNegotiated} → `isCompressibleType`
429
402
  * on `Content-Type` → a fast skip when the response already carries a
430
403
  * numeric `Content-Length` BELOW `options.threshold` (avoids buffering a
431
- * body known too small to be worth compressing) → buffer via
404
+ * body known too small to be worth compressing) → buffer through
432
405
  * `response.arrayBuffer()` → a threshold passthrough when the buffered size
433
406
  * is still below `options.threshold` → `options.compress` → set
434
- * `Content-Encoding` and a fresh `Content-Length` via {@link rebuildResponse}.
407
+ * `Content-Encoding` and a fresh `Content-Length` through {@link rebuildResponse}.
435
408
  * Returns `response` unchanged (aside from the `Vary` stamp) on any skip.
436
409
  *
437
410
  * @param request - The inbound `Request` (read for `Accept-Encoding`)
438
411
  * @param context - The `MiddlewareContext` (read for `context.method`)
439
412
  * @param response - The downstream `Response` to consider compressing
440
- * @param options - The threshold, optional filter, offered encodings, and the runtime's `compress` primitive
413
+ * @param options - See {@link CompressResponseOptions}
441
414
  * @returns The original `response` when skipped, or a new compressed `Response`
442
415
  *
443
416
  * @example
@@ -473,99 +446,28 @@ async function compressResponse(request, context, response, options) {
473
446
  return rebuildResponse(compressed, response, headers);
474
447
  }
475
448
  /**
476
- * Copy every entry of one session's `data` into another — the regenerate
477
- * data-carry `createSession`'s `control.regenerate()` applies (ruling D).
449
+ * Copies every entry of one session's `state` into another — the regenerate
450
+ * state-carry `createSession`'s `control.regenerate()` applies.
478
451
  *
479
- * @param from - The source session whose `data` is copied
480
- * @param to - The destination session `data` is copied into
452
+ * @param from - The source session whose `state` is copied
453
+ * @param to - The destination session the entries are written into, through
454
+ * its own `set` mutator
481
455
  *
482
456
  * @example
483
457
  * ```ts
484
- * transferSessionData(oldSession, newSession)
458
+ * transferSessionState(oldSession, newSession)
485
459
  * ```
486
460
  */
487
- function transferSessionData(from, to) {
488
- for (const [key, value] of from.data) to.data.set(key, value);
461
+ function transferSessionState(from, to) {
462
+ for (const [key, value] of from.state) to.set(key, value);
489
463
  }
490
464
  /**
491
- * Determine whether a value implements {@link SessionInterface}a total
492
- * structural guard (§14): an `id` string plus a `data` `Map`. Prototype-agnostic
493
- * — accepts a plain object, a null-prototype object, AND a class instance
494
- * (a real `Session`), since a restored/stored session is routinely a class
495
- * instance, not a literal.
496
- *
497
- * @param value - The candidate value
498
- * @returns `true` when `value` is shaped like a {@link SessionInterface}
499
- *
500
- * @example
501
- * ```ts
502
- * isSession({ id: 'a', data: new Map() }) // true
503
- * isSession(new Session('a')) // true
504
- * ```
505
- */
506
- function isSession(value) {
507
- if (typeof value !== "object" || value === null) return false;
508
- const id = Reflect.get(value, "id");
509
- const data = Reflect.get(value, "data");
510
- return isString(id) && data instanceof Map;
511
- }
512
- /**
513
- * Determine whether a value implements {@link SessionControlInterface} — a
514
- * total structural guard (§14): callable `regenerate` and `destroy`.
515
- *
516
- * @param value - The candidate value
517
- * @returns `true` when `value` is shaped like a {@link SessionControlInterface}
518
- *
519
- * @example
520
- * ```ts
521
- * isSessionControl({ regenerate() {}, destroy() {} }) // true
522
- * ```
523
- */
524
- function isSessionControl(value) {
525
- if (!isRecord(value)) return false;
526
- return typeof value.regenerate === "function" && typeof value.destroy === "function";
527
- }
528
- /**
529
- * Determine whether a value is one staged {@link MultipartFile} record — a
530
- * total structural guard (§14) checking every required field's shape.
531
- *
532
- * @param value - The candidate value
533
- * @returns `true` when `value` is shaped like a {@link MultipartFile}
534
- */
535
- function isMultipartFile(value) {
536
- if (!isRecord(value)) return false;
537
- return isString(value.field) && isString(value.name) && typeof value.size === "number" && isString(value.mime) && typeof value.validated === "boolean" && isString(value.status) && isString(value.path);
538
- }
539
- /**
540
- * Determine whether a value implements {@link MultipartBody} — a total
541
- * structural guard (§14): `files` keyed by field name to arrays of
542
- * {@link MultipartFile}, and a `fields` string record.
543
- *
544
- * @param value - The candidate value
545
- * @returns `true` when `value` is shaped like a {@link MultipartBody}
546
- *
547
- * @example
548
- * ```ts
549
- * isMultipartBody({ files: {}, fields: { name: 'a' } }) // true
550
- * ```
551
- */
552
- function isMultipartBody(value) {
553
- if (!isRecord(value)) return false;
554
- if (!isRecord(value.files) || !isRecord(value.fields)) return false;
555
- for (const entries of Object.values(value.files)) {
556
- if (!Array.isArray(entries)) return false;
557
- for (const entry of entries) if (!isMultipartFile(entry)) return false;
558
- }
559
- for (const fieldValue of Object.values(value.fields)) if (!isString(fieldValue)) return false;
560
- return true;
561
- }
562
- /**
563
- * Determine whether a request is a CORS PREFLIGHT — an `OPTIONS` request
465
+ * Determines whether a request is a CORS PREFLIGHT an `OPTIONS` request
564
466
  * carrying an `Access-Control-Request-Method` header.
565
467
  *
566
468
  * @param method - The request's HTTP method
567
469
  * @param headers - The request's `Headers`
568
- * @returns `true` when the request is a CORS preflight `createCors` must answer
470
+ * @returns True if the request is a CORS preflight `createCors` must answer; false otherwise
569
471
  *
570
472
  * @example
571
473
  * ```ts
@@ -576,22 +478,22 @@ function isPreflight(method, headers) {
576
478
  return method === "OPTIONS" && headers.has("access-control-request-method");
577
479
  }
578
480
  /**
579
- * A parsed client-info fact for {@link ClientInfo} a leaf shaping helper
580
- * `createForwarded` uses to build its stashed slice.
481
+ * Builds the {@link Client} slice `createForwarded` stashes, from the
482
+ * resolved client IP a leaf shaping helper.
581
483
  *
582
484
  * @param ip - The resolved client IP, if any
583
- * @returns The {@link ClientInfo} slice value
485
+ * @returns The {@link Client} slice value
584
486
  *
585
487
  * @example
586
488
  * ```ts
587
- * buildClientInfo('203.0.113.7') // { ip: '203.0.113.7' }
489
+ * buildClient('203.0.113.7') // { ip: '203.0.113.7' }
588
490
  * ```
589
491
  */
590
- function buildClientInfo(ip) {
492
+ function buildClient(ip) {
591
493
  return { ...ip !== void 0 ? { ip } : {} };
592
494
  }
593
495
  /**
594
- * Constant-time string equality — `createCSRF`'s double-submit token
496
+ * Compares two strings in constant time — `createCSRF`'s double-submit token
595
497
  * comparison, avoiding a timing oracle on the submitted-vs-cookie match.
596
498
  *
597
499
  * @remarks
@@ -603,7 +505,7 @@ function buildClientInfo(ip) {
603
505
  *
604
506
  * @param a - The first string
605
507
  * @param b - The second string
606
- * @returns `true` when `a` and `b` are exactly equal
508
+ * @returns True if `a` and `b` are exactly equal; false otherwise
607
509
  *
608
510
  * @example
609
511
  * ```ts
@@ -618,33 +520,54 @@ function equalsConstantTime(a, b) {
618
520
  return diff === 0;
619
521
  }
620
522
  /**
621
- * Whether a session has aged past its idle timeout or absolute lifetime as
523
+ * Checks whether a session has aged past its idle timeout or absolute lifetime as
622
524
  * of `now` — the pure expiry predicate `MemorySessionStore` delegates to.
623
525
  *
624
- * @param cursors - The session's `lastSeen` (idle) and `createdAt` (absolute) instants
526
+ * @param cursors - See {@link SessionCursors}
625
527
  * @param now - The current instant (same clock unit as `cursors`)
626
- * @param limits - The optional `ttl` (idle) and `lifetime` (absolute) thresholds
627
- * @returns `true` when either configured threshold has elapsed
528
+ * @param limits - See {@link SessionLimits}
529
+ * @returns True if either configured threshold has elapsed; false otherwise
628
530
  *
629
531
  * @example
630
532
  * ```ts
631
- * sessionExpired({ lastSeen: 0, createdAt: 0 }, 1_000, { ttl: 500 }) // true
533
+ * sessionExpired({ seen: 0, created: 0 }, 1_000, { ttl: 500 }) // true
632
534
  * ```
633
535
  */
634
536
  function sessionExpired(cursors, now, limits) {
635
- if (limits.ttl !== void 0 && now - cursors.lastSeen >= limits.ttl) return true;
636
- if (limits.lifetime !== void 0 && now - cursors.createdAt >= limits.lifetime) return true;
537
+ if (limits.ttl !== void 0 && now - cursors.seen >= limits.ttl) return true;
538
+ if (limits.lifetime !== void 0 && now - cursors.created >= limits.lifetime) return true;
637
539
  return false;
638
540
  }
639
541
  /**
640
- * Snapshot a session's `data` Map into a plain, serializable record — the
542
+ * Validates a store's idle and absolute-lifetime thresholds, throwing when
543
+ * either is present and malformed — the shared construction gate
544
+ * {@link MemorySessionStore} and {@link DatabaseSessionStore} both apply, so
545
+ * one malformed `ttl` is refused identically by whichever store receives it.
546
+ *
547
+ * @param limits - See {@link SessionLimits}
548
+ * @returns Nothing; a successful return means both thresholds are usable
549
+ * @throws {TypeError} Thrown when `ttl` or `lifetime` is present and is not a
550
+ * positive finite number
551
+ *
552
+ * @example
553
+ * ```ts
554
+ * validateSessionLimits({ ttl: 60_000 }) // returns; the thresholds are usable
555
+ * ```
556
+ */
557
+ function validateSessionLimits(limits) {
558
+ if (limits?.ttl !== void 0 && (!isFiniteNumber(limits.ttl) || limits.ttl <= 0)) throw new TypeError("SessionLimits.ttl must be a positive finite number when provided");
559
+ if (limits?.lifetime !== void 0 && (!isFiniteNumber(limits.lifetime) || limits.lifetime <= 0)) throw new TypeError("SessionLimits.lifetime must be a positive finite number when provided");
560
+ }
561
+ /**
562
+ * Snapshots a session's `state` into a plain, serializable record — the
641
563
  * projection a durable store's `set` writes to disk.
642
564
  *
643
565
  * @param session - The session to snapshot
644
- * @returns A plain-object copy of `session.data`, keyed alongside `session.id`
566
+ * @returns A {@link SessionSnapshot} whose `state` is a plain-object copy of
567
+ * `session.state`, keyed alongside `session.id`
645
568
  *
646
569
  * @remarks
647
- * `data` is built on a null-prototype object (`Object.create(null)`), never
570
+ * `state` is built on a null-prototype object (`Object.create(null)`), never
648
571
  * a `{}` literal — a session key literally named `__proto__` must round-trip
649
572
  * as an OWN enumerable property instead of hitting `Object.prototype`'s
650
573
  * `__proto__` accessor (which would silently drop the entry and risk
@@ -652,47 +575,118 @@ function sessionExpired(cursors, now, limits) {
652
575
  *
653
576
  * @example
654
577
  * ```ts
655
- * snapshotSession(session) // { id: 'abc', data: { userId: 'u_1' } }
578
+ * snapshotSession(session) // { id: 'abc', state: { userId: 'u_1' } }
656
579
  * ```
657
580
  */
658
581
  function snapshotSession(session) {
659
- const data = Object.create(null);
660
- for (const [key, value] of session.data) data[key] = value;
582
+ const state = Object.create(null);
583
+ for (const [key, value] of session.state) state[key] = value;
661
584
  return {
662
585
  id: session.id,
663
- data
586
+ state
664
587
  };
665
588
  }
589
+ //#endregion
590
+ //#region src/core/validators.ts
666
591
  /**
667
- * Rebuild a `Session` from an untrusted snapshot value (the inverse of
668
- * {@link snapshotSession}) a durable store's `get` deserialization step.
592
+ * Determines whether a value implements {@link SessionInterface} a total
593
+ * structural guard: an `id` string, a `state` `Map`, and the `set`, `delete`,
594
+ * 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.
669
597
  *
670
- * @param value - The candidate snapshot, of unknown shape
671
- * @returns A rebuilt `Session`, or `undefined` when `value` is malformed
598
+ * @param value - The candidate value
599
+ * @returns True if `value` is shaped like a {@link SessionInterface}; false otherwise
672
600
  *
673
601
  * @example
674
602
  * ```ts
675
- * restoreSession({ id: 'abc', data: { userId: 'u_1' } }) // Session { id: 'abc', data: Map }
676
- * restoreSession({ id: 1 }) // undefined
603
+ * isSession(new Session('a')) // true
604
+ * isSession({ id: 'a', state: new Map() }) // false — the mutators are missing
677
605
  * ```
678
606
  */
679
- function restoreSession(value) {
680
- if (!isRecord(value)) return void 0;
681
- if (!isString(value.id) || !isRecord(value.data)) return void 0;
682
- const session = new Session(value.id);
683
- for (const [key, entry] of Object.entries(value.data)) session.data.set(key, entry);
684
- return session;
607
+ function isSession(value) {
608
+ if (typeof value !== "object" || value === null) return false;
609
+ const id = Reflect.get(value, "id");
610
+ const state = Reflect.get(value, "state");
611
+ if (!isString(id) || !(state instanceof Map)) return false;
612
+ const set = Reflect.get(value, "set");
613
+ const remove = Reflect.get(value, "delete");
614
+ const clear = Reflect.get(value, "clear");
615
+ return typeof set === "function" && typeof remove === "function" && typeof clear === "function";
616
+ }
617
+ /**
618
+ * Determines whether a value implements {@link SessionControlInterface} — a
619
+ * total structural guard: callable `regenerate` and `destroy`.
620
+ *
621
+ * @param value - The candidate value
622
+ * @returns True if `value` is shaped like a {@link SessionControlInterface}; false otherwise
623
+ *
624
+ * @example
625
+ * ```ts
626
+ * isSessionControl({ regenerate() {}, destroy() {} }) // true
627
+ * ```
628
+ */
629
+ function isSessionControl(value) {
630
+ if (!isRecord(value)) return false;
631
+ return typeof value.regenerate === "function" && typeof value.destroy === "function";
632
+ }
633
+ /**
634
+ * Determines whether a value is one staged {@link MultipartFile} record — a
635
+ * total structural guard checking every required field's shape.
636
+ *
637
+ * @param value - The candidate value
638
+ * @returns True if `value` is shaped like a {@link MultipartFile}; false otherwise
639
+ *
640
+ * @example
641
+ * ```ts
642
+ * isMultipartFile({
643
+ * field: 'avatar',
644
+ * name: 'a.png',
645
+ * size: 3,
646
+ * mime: 'image/png',
647
+ * validated: true,
648
+ * status: 'staged',
649
+ * path: '/tmp/a',
650
+ * }) // true
651
+ * ```
652
+ */
653
+ function isMultipartFile(value) {
654
+ if (!isRecord(value)) return false;
655
+ return isString(value.field) && isString(value.name) && typeof value.size === "number" && isString(value.mime) && typeof value.validated === "boolean" && isString(value.status) && isString(value.path);
656
+ }
657
+ /**
658
+ * Determines whether a value implements {@link MultipartBody} — a total
659
+ * structural guard: `files` keyed by field name to arrays of
660
+ * {@link MultipartFile}, and a `fields` string record.
661
+ *
662
+ * @param value - The candidate value
663
+ * @returns True if `value` is shaped like a {@link MultipartBody}; false otherwise
664
+ *
665
+ * @example
666
+ * ```ts
667
+ * isMultipartBody({ files: {}, fields: { name: 'a' } }) // true
668
+ * ```
669
+ */
670
+ function isMultipartBody(value) {
671
+ if (!isRecord(value)) return false;
672
+ if (!isRecord(value.files) || !isRecord(value.fields)) return false;
673
+ for (const entries of Object.values(value.files)) {
674
+ if (!Array.isArray(entries)) return false;
675
+ for (const entry of entries) if (!isMultipartFile(entry)) return false;
676
+ }
677
+ for (const fieldValue of Object.values(value.fields)) if (!isString(fieldValue)) return false;
678
+ return true;
685
679
  }
686
680
  //#endregion
687
681
  //#region src/core/shapers.ts
688
682
  /**
689
- * The `@orkestrel/database` column shape for a
683
+ * Holds the `@orkestrel/database` column shape for a
690
684
  * {@link import('./types.js').SessionRow} table — pass as-is to
691
685
  * `createDatabase({ tables: { sessions: sessionColumns } })` so an app
692
686
  * declaring a durable session table never hand-writes the shape.
693
687
  *
694
688
  * @remarks
695
- * `lastSeen`/`createdAt` are `integerShape({ min: 0 })` — the table validates
689
+ * `seen`/`created` are `integerShape({ min: 0 })` — the table validates
696
690
  * them as integers, so
697
691
  * {@link import('./stores/DatabaseSessionStore.js').DatabaseSessionStore}'s
698
692
  * `now` clock must yield integer milliseconds (`Date.now()`, the implicit
@@ -708,24 +702,67 @@ function restoreSession(value) {
708
702
  var sessionColumns = {
709
703
  id: stringShape(),
710
704
  session: jsonShape(),
711
- lastSeen: integerShape({ min: 0 }),
712
- createdAt: integerShape({ min: 0 })
705
+ seen: integerShape({ min: 0 }),
706
+ created: integerShape({ min: 0 })
707
+ };
708
+ //#endregion
709
+ //#region src/core/Session.ts
710
+ /**
711
+ * Represents a server-managed session's default entity — the `create` option's default
712
+ * value for `createSession`. It ships without a bare `create*` factory of its
713
+ * own, because the name `createSession` belongs to the battery;
714
+ * `createRestoredSession` rebuilds one from a stored snapshot.
715
+ *
716
+ * @remarks
717
+ * `state` is a `ReadonlyMap` view over the entity's own `Map`: TypeScript
718
+ * refuses a write through it, and `set`, `delete`, and `clear` are the write
719
+ * path. `createSession` persists the state to the configured store on the way
720
+ * out.
721
+ *
722
+ * @example
723
+ * ```ts
724
+ * const session = new Session('abc123')
725
+ * session.set('userId', 'u_1')
726
+ * ```
727
+ */
728
+ var Session = class {
729
+ #id;
730
+ #state;
731
+ constructor(id) {
732
+ this.#id = id;
733
+ this.#state = /* @__PURE__ */ new Map();
734
+ }
735
+ get id() {
736
+ return this.#id;
737
+ }
738
+ get state() {
739
+ return this.#state;
740
+ }
741
+ set(key, value) {
742
+ this.#state.set(key, value);
743
+ }
744
+ delete(key) {
745
+ return this.#state.delete(key);
746
+ }
747
+ clear() {
748
+ this.#state.clear();
749
+ }
713
750
  };
714
751
  //#endregion
715
752
  //#region src/core/stores/MemorySessionStore.ts
716
753
  /**
717
- * The default in-process {@link SessionStoreInterface} — a `Map`-backed store
754
+ * Implements the default in-process {@link SessionStoreInterface} — a `Map`-backed store
718
755
  * enforcing both an idle timeout and an absolute lifetime, with lazy
719
756
  * (read-time) eviction, a bounded capacity, and no background timers.
720
757
  *
721
- * @typeParam S - The session data payload type
758
+ * @typeParam S - The stored session entity type
722
759
  *
723
760
  * @remarks
724
- * `get` evicts a session whose idle time (`now - lastSeen >= ttl`) or
725
- * absolute lifetime (`now - createdAt >= lifetime`) has elapsed — the
761
+ * `get` evicts a session whose idle time (`now - seen >= ttl`) or
762
+ * absolute lifetime (`now - created >= lifetime`) has elapsed — the
726
763
  * lifetime check fires EVEN IF the session was continuously touched, since
727
- * `createdAt` is stamped once at the first `set` and preserved across every
728
- * later re-`set` of the same id. A live read touches `lastSeen`. `delete` of
764
+ * `created` is stamped once at the first `set` and preserved across every
765
+ * later re-`set` of the same id. A live read touches `seen`. `delete` of
729
766
  * an absent id is a no-op.
730
767
  *
731
768
  * Capacity is enforced as least-recently-used **by last write**: `set`
@@ -740,7 +777,7 @@ var sessionColumns = {
740
777
  * @example
741
778
  * ```ts
742
779
  * const store = new MemorySessionStore({ ttl: 60_000, lifetime: 3_600_000 })
743
- * await store.set('abc', { userId: 'u_1' }, Date.now())
780
+ * await store.set(new Session('abc'), Date.now())
744
781
  * ```
745
782
  */
746
783
  var MemorySessionStore = class {
@@ -750,10 +787,7 @@ var MemorySessionStore = class {
750
787
  #capacity;
751
788
  #evict;
752
789
  constructor(options) {
753
- if (options?.ttl !== void 0 && !isFiniteNumber(options.ttl)) throw new TypeError("MemorySessionStore requires options.ttl to be a finite number when provided");
754
- if (options?.ttl !== void 0 && options.ttl <= 0) throw new TypeError("MemorySessionStore requires options.ttl to be positive when provided");
755
- if (options?.lifetime !== void 0 && !isFiniteNumber(options.lifetime)) throw new TypeError("MemorySessionStore requires options.lifetime to be a finite number when provided");
756
- if (options?.lifetime !== void 0 && options.lifetime <= 0) throw new TypeError("MemorySessionStore requires options.lifetime to be positive when provided");
790
+ validateSessionLimits(options);
757
791
  if (options?.capacity !== void 0 && (!isFiniteNumber(options.capacity) || !Number.isInteger(options.capacity) || options.capacity <= 0)) throw new TypeError("MemorySessionStore requires options.capacity to be a positive integer when provided");
758
792
  if (options?.evict !== void 0 && !isFunction(options.evict)) throw new TypeError("MemorySessionStore requires options.evict to be a function when provided");
759
793
  this.#entries = /* @__PURE__ */ new Map();
@@ -770,18 +804,23 @@ var MemorySessionStore = class {
770
804
  this.#notify(id);
771
805
  return;
772
806
  }
773
- entry.lastSeen = now;
807
+ this.#entries.set(id, {
808
+ session: entry.session,
809
+ seen: now,
810
+ created: entry.created
811
+ });
774
812
  return entry.session;
775
813
  }
776
- async set(id, session, now) {
814
+ async set(session, now) {
815
+ const id = session.id;
777
816
  const existing = this.#entries.get(id);
778
817
  if (existing === void 0) this.#reserve(now);
779
- const createdAt = existing?.createdAt ?? now;
818
+ const created = existing?.created ?? now;
780
819
  this.#entries.delete(id);
781
820
  this.#entries.set(id, {
782
821
  session,
783
- lastSeen: now,
784
- createdAt
822
+ seen: now,
823
+ created
785
824
  });
786
825
  }
787
826
  async delete(id) {
@@ -789,8 +828,8 @@ var MemorySessionStore = class {
789
828
  }
790
829
  #expired(entry, now) {
791
830
  return sessionExpired(entry, now, {
792
- ...this.#ttl !== void 0 ? { ttl: this.#ttl } : {},
793
- ...this.#lifetime !== void 0 ? { lifetime: this.#lifetime } : {}
831
+ ttl: this.#ttl,
832
+ lifetime: this.#lifetime
794
833
  });
795
834
  }
796
835
  #reserve(now) {
@@ -817,26 +856,28 @@ var MemorySessionStore = class {
817
856
  //#endregion
818
857
  //#region src/core/stores/DatabaseSessionStore.ts
819
858
  /**
820
- * A durable {@link SessionStoreInterface} over an `@orkestrel/database`
859
+ * Implements a durable {@link SessionStoreInterface} over an `@orkestrel/database`
821
860
  * table — the same idle-timeout + absolute-lifetime contract as
822
861
  * {@link MemorySessionStore}, backed by a caller-supplied `TableInterface`
823
862
  * instead of an in-process `Map`.
824
863
  *
825
- * @typeParam S - The session data payload type
864
+ * @typeParam S - The stored session entity type
826
865
  *
827
866
  * @remarks
828
867
  * `get` reads the row, evicts (removes the row) once `sessionExpired`
829
- * reports either threshold elapsed, then rebuilds the session via
830
- * {@link restoreSession} — a malformed snapshot or one that fails the
831
- * caller's `is` guard resolves `undefined` rather than throwing. A live read
832
- * touches `lastSeen`. `set` preserves an existing row's `createdAt` across a
868
+ * reports either threshold elapsed, then rebuilds the session through the
869
+ * `restore` step it was constructed with — a malformed snapshot or one that
870
+ * fails the caller's guard resolves `undefined` rather than throwing.
871
+ * `createDatabaseSessionStore` supplies `createRestoredSession` as that step,
872
+ * so a caller reaching the factory never states it. A live read
873
+ * touches `seen`. `set` preserves an existing row's `created` across a
833
874
  * re-`set` of the same id (stamped once at the first `set`), mirroring
834
875
  * {@link MemorySessionStore}. `delete` of an absent id is a no-op (the
835
876
  * table's `remove` contract).
836
877
  *
837
878
  * A malformed-snapshot or failed-guard `undefined` LEAVES the row in place —
838
879
  * unlike the expired path, which removes it. This is deliberate: a
839
- * caller-contextual `is` guard may reject a session that is still perfectly
880
+ * caller-contextual guard may reject a session that is still perfectly
840
881
  * valid for another flow reading the same table (a differently-shaped `S`,
841
882
  * a stricter guard mid-rollout), so `get` never destroys data on a guard
842
883
  * miss. A row that no caller's guard ever accepts again self-heals once its
@@ -844,18 +885,23 @@ var MemorySessionStore = class {
844
885
  *
845
886
  * @example
846
887
  * ```ts
847
- * const store = new DatabaseSessionStore(table, isSession, { ttl: 60_000 })
848
- * await store.set('abc', new Session('abc'), Date.now())
888
+ * const store = new DatabaseSessionStore(table, isSession, createRestoredSession, {
889
+ * ttl: 60_000,
890
+ * })
891
+ * await store.set(new Session('abc'), Date.now())
849
892
  * ```
850
893
  */
851
894
  var DatabaseSessionStore = class {
852
895
  #table;
853
- #is;
896
+ #guard;
897
+ #restore;
854
898
  #ttl;
855
899
  #lifetime;
856
- constructor(table, is, options) {
900
+ constructor(table, guard, restore, options) {
901
+ validateSessionLimits(options);
857
902
  this.#table = table;
858
- this.#is = is;
903
+ this.#guard = guard;
904
+ this.#restore = restore;
859
905
  this.#ttl = options?.ttl;
860
906
  this.#lifetime = options?.lifetime;
861
907
  }
@@ -863,24 +909,25 @@ var DatabaseSessionStore = class {
863
909
  const row = await this.#table.get(id);
864
910
  if (row === void 0) return void 0;
865
911
  if (sessionExpired(row, now, {
866
- ...this.#ttl !== void 0 ? { ttl: this.#ttl } : {},
867
- ...this.#lifetime !== void 0 ? { lifetime: this.#lifetime } : {}
912
+ ttl: this.#ttl,
913
+ lifetime: this.#lifetime
868
914
  })) {
869
915
  await this.#table.remove(id);
870
916
  return;
871
917
  }
872
- const session = restoreSession(row.session);
873
- if (session === void 0 || !this.#is(session)) return void 0;
874
- await this.#table.update(id, { lastSeen: now });
918
+ const session = this.#restore(row.session);
919
+ if (session === void 0 || !this.#guard(session)) return void 0;
920
+ await this.#table.update(id, { seen: now });
875
921
  return session;
876
922
  }
877
- async set(id, session, now) {
878
- const createdAt = (await this.#table.get(id))?.createdAt ?? now;
923
+ async set(session, now) {
924
+ const id = session.id;
925
+ const created = (await this.#table.get(id))?.created ?? now;
879
926
  await this.#table.set({
880
927
  id,
881
928
  session: snapshotSession(session),
882
- lastSeen: now,
883
- createdAt
929
+ seen: now,
930
+ created
884
931
  });
885
932
  }
886
933
  async delete(id) {
@@ -890,7 +937,7 @@ var DatabaseSessionStore = class {
890
937
  //#endregion
891
938
  //#region src/core/middlewares.ts
892
939
  /**
893
- * The outermost error-rendering battery — catches a downstream throw and
940
+ * Creates the outermost error-rendering battery — catches a downstream throw and
894
941
  * renders it as a `Response`.
895
942
  *
896
943
  * @typeParam TState - The consumer's opaque per-request state type
@@ -922,7 +969,7 @@ function createBoundary(options) {
922
969
  };
923
970
  }
924
971
  /**
925
- * The access-log/timing seam — records one {@link TelemetryEntry} per request
972
+ * Creates the access-log/timing seam — records one {@link TelemetryEntry} per request
926
973
  * after the response settles.
927
974
  *
928
975
  * @typeParam TState - The consumer's opaque per-request state type
@@ -959,7 +1006,7 @@ function createTelemetry(options) {
959
1006
  };
960
1007
  }
961
1008
  /**
962
- * Response-body compression — negotiates and compresses a buffered response
1009
+ * Creates the response-body compression battery — negotiates and compresses a buffered response
963
1010
  * body over the runtime's feature-detected `CompressionStream` codings.
964
1011
  *
965
1012
  * @typeParam TState - The consumer's opaque per-request state type
@@ -989,7 +1036,7 @@ function createCompression(options) {
989
1036
  };
990
1037
  }
991
1038
  /**
992
- * Security-headers + request-identifier battery.
1039
+ * Creates the security-headers + request-identifier battery.
993
1040
  *
994
1041
  * @typeParam TState - The consumer's opaque per-request state type, must carry {@link IdentifierState}
995
1042
  * @param options - See {@link SecurityOptions}
@@ -1051,7 +1098,7 @@ function createSecurity(options) {
1051
1098
  };
1052
1099
  }
1053
1100
  /**
1054
- * Cross-Origin Resource Sharing battery.
1101
+ * Creates the Cross-Origin Resource Sharing battery.
1055
1102
  *
1056
1103
  * @typeParam TState - The consumer's opaque per-request state type
1057
1104
  * @param options - See {@link CorsOptions}
@@ -1092,7 +1139,7 @@ function createCors(options) {
1092
1139
  };
1093
1140
  }
1094
1141
  /**
1095
- * The application-level per-request deadline battery.
1142
+ * Creates the application-level per-request deadline battery.
1096
1143
  *
1097
1144
  * @typeParam TState - The consumer's opaque per-request state type
1098
1145
  * @param options - See {@link DeadlineOptions}
@@ -1102,7 +1149,7 @@ function createCors(options) {
1102
1149
  * @remarks
1103
1150
  * MUST sit OUTSIDE `createBody` in the chain — it reconstructs the inbound
1104
1151
  * `Request` (to link its `signal` to the deadline `signal`), which throws if
1105
- * the body was already consumed upstream (e.g. by `createBody`'s cached read).
1152
+ * the body was already consumed upstream (for example by `createBody`'s cached read).
1106
1153
  *
1107
1154
  * @example
1108
1155
  * ```ts
@@ -1137,7 +1184,7 @@ function createDeadline(options) {
1137
1184
  };
1138
1185
  }
1139
1186
  /**
1140
- * The trusted-proxy client-IP resolver battery.
1187
+ * Creates the trusted-proxy client-IP resolver battery.
1141
1188
  *
1142
1189
  * @typeParam TState - The consumer's opaque per-request state type, must carry {@link ClientState} and {@link ConnectionState}
1143
1190
  * @param options - See {@link ForwardedOptions}
@@ -1163,12 +1210,12 @@ function createForwarded(options) {
1163
1210
  const trust = hasProxies ? { proxies: options.proxies } : { trusted: options.trusted };
1164
1211
  return async (request, context, next) => {
1165
1212
  const ip = resolveForwardedFor(request.headers.get("x-forwarded-for") ?? void 0, trust) ?? context.state.connection?.ip;
1166
- Object.assign(context.state, { client: buildClientInfo(ip) });
1213
+ Object.assign(context.state, { client: buildClient(ip) });
1167
1214
  return next();
1168
1215
  };
1169
1216
  }
1170
1217
  /**
1171
- * Dynamic response `ETag` + conditional GET battery.
1218
+ * Creates the dynamic response `ETag` + conditional GET battery.
1172
1219
  *
1173
1220
  * @typeParam TState - The consumer's opaque per-request state type
1174
1221
  * @param options - See {@link ETagOptions}
@@ -1206,7 +1253,7 @@ function createETag(options) {
1206
1253
  };
1207
1254
  }
1208
1255
  /**
1209
- * Bearer-token authentication battery.
1256
+ * Creates the bearer-token authentication battery.
1210
1257
  *
1211
1258
  * @typeParam TState - The consumer's opaque per-request state type, must carry {@link BearerState}
1212
1259
  * @param options - See {@link BearerOptions}
@@ -1242,7 +1289,7 @@ function createBearer(options) {
1242
1289
  };
1243
1290
  }
1244
1291
  /**
1245
- * Fixed-window rate-limiting battery.
1292
+ * Creates the fixed-window rate-limiting battery.
1246
1293
  *
1247
1294
  * @typeParam TState - The consumer's opaque per-request state type, must carry {@link BearerState}, {@link ClientState}, and {@link ConnectionState}
1248
1295
  * @param options - See {@link LimiterOptions}
@@ -1289,7 +1336,7 @@ function createLimiter(options) {
1289
1336
  bucket = {
1290
1337
  budget: createBudget({
1291
1338
  max,
1292
- consume: Number
1339
+ consumer: Number
1293
1340
  }),
1294
1341
  resetAt: now + window
1295
1342
  };
@@ -1319,7 +1366,7 @@ function createLimiter(options) {
1319
1366
  };
1320
1367
  }
1321
1368
  /**
1322
- * The body-driving battery — eagerly awaits the cached `context.body()` so
1369
+ * Creates the body-driving battery — eagerly awaits the cached `context.body()` so
1323
1370
  * its throws (or a malformed-JSON `undefined`) surface before the handler
1324
1371
  * runs, and stashes the resolved value onto {@link BodyState.body}.
1325
1372
  *
@@ -1330,8 +1377,7 @@ function createLimiter(options) {
1330
1377
  * @remarks
1331
1378
  * The shipped `MiddlewareContext.body()` is a parameterless, server-owned
1332
1379
  * cache (`ServerOptions.limit` governs its size cap) — this battery carries
1333
- * no `limit`/`decompression` options (a deliberate break from the deleted old
1334
- * `createBodyParser` surface, which configured them itself). `state.body` is
1380
+ * no `limit`/`decompression` options. `state.body` is
1335
1381
  * stashed from the SAME awaited call the 400 check reads — `context.body()`
1336
1382
  * is never invoked twice.
1337
1383
  *
@@ -1350,7 +1396,7 @@ function createBody() {
1350
1396
  };
1351
1397
  }
1352
1398
  /**
1353
- * The generic session battery — resolves, mints, and persists a session
1399
+ * Creates the generic session battery — resolves, mints, and persists a session
1354
1400
  * across the request, with a mid-handler `regenerate`/`destroy` control handle.
1355
1401
  *
1356
1402
  * @typeParam S - The session entity type the store persists (must implement {@link SessionInterface})
@@ -1358,7 +1404,7 @@ function createBody() {
1358
1404
  * @param options - See {@link SessionOptions}
1359
1405
  * @returns A `MiddlewareHandler<TState>`
1360
1406
  * @throws {TypeError} When any option is malformed
1361
- * @throws {HTTPError} `404` when `require` is set and no session resolves or mints
1407
+ * @throws {HTTPError} `404` when `required` is set and no session resolves or mints
1362
1408
  *
1363
1409
  * @example
1364
1410
  * ```ts
@@ -1366,14 +1412,13 @@ function createBody() {
1366
1412
  * ```
1367
1413
  */
1368
1414
  function createSession(options) {
1369
- if (!isFunction(options.transport.read) || !isFunction(options.transport.write) || !isFunction(options.transport.clear)) throw new TypeError("SessionOptions.transport must implement SessionTransport");
1415
+ if (!isFunction(options.transport.read) || !isFunction(options.transport.write) || !isFunction(options.transport.clear)) throw new TypeError("SessionOptions.transport must implement SessionTransportInterface");
1370
1416
  if (options.store !== void 0 && (!isFunction(options.store.get) || !isFunction(options.store.set) || !isFunction(options.store.delete))) throw new TypeError("SessionOptions.store must implement SessionStoreInterface when provided");
1371
1417
  if (options.ttl !== void 0 && !isFiniteNumber(options.ttl)) throw new TypeError("SessionOptions.ttl must be a finite number when provided");
1372
1418
  if (options.lifetime !== void 0 && !isFiniteNumber(options.lifetime)) throw new TypeError("SessionOptions.lifetime must be a finite number when provided");
1373
1419
  if (options.create !== void 0 && !isFunction(options.create)) throw new TypeError("SessionOptions.create must be a function when provided");
1374
1420
  if (options.mint !== void 0 && !isFunction(options.mint)) throw new TypeError("SessionOptions.mint must be a function when provided");
1375
- if (options.require !== void 0 && !isBoolean(options.require)) throw new TypeError("SessionOptions.require must be a boolean when provided");
1376
- if (options.ends !== void 0 && !isBoolean(options.ends)) throw new TypeError("SessionOptions.ends must be a boolean when provided");
1421
+ if (options.required !== void 0 && !isBoolean(options.required)) throw new TypeError("SessionOptions.required must be a boolean when provided");
1377
1422
  if (options.clock !== void 0 && !isFunction(options.clock)) throw new TypeError("SessionOptions.clock must be a function when provided");
1378
1423
  const transport = options.transport;
1379
1424
  const clock = options.clock ?? Date.now;
@@ -1385,17 +1430,12 @@ function createSession(options) {
1385
1430
  });
1386
1431
  const create = options.create;
1387
1432
  const mint = options.mint;
1388
- const requireSession = options.require ?? false;
1389
- const ends = options.ends ?? false;
1433
+ const requireSession = options.required ?? false;
1390
1434
  return async (request, context, next) => {
1391
1435
  const now = clock();
1392
1436
  const encrypted = context.state.connection?.encrypted ?? false;
1393
1437
  const incomingId = await transport.read(request);
1394
1438
  let session = incomingId !== void 0 ? await store.get(incomingId, now) : void 0;
1395
- if (ends && context.method === "DELETE" && session !== void 0) {
1396
- await store.delete(session.id);
1397
- return new Response(null, { status: 204 });
1398
- }
1399
1439
  let minted = false;
1400
1440
  if (session === void 0) {
1401
1441
  if (mint !== void 0 ? await mint(context) : true) {
@@ -1413,7 +1453,7 @@ function createSession(options) {
1413
1453
  regenerate() {
1414
1454
  if (destroyed) return;
1415
1455
  const newSession = create === void 0 ? new Session(crypto.randomUUID()) : create(crypto.randomUUID());
1416
- transferSessionData(activeSession, newSession);
1456
+ transferSessionState(activeSession, newSession);
1417
1457
  regenerated = newSession;
1418
1458
  },
1419
1459
  destroy() {
@@ -1428,11 +1468,11 @@ function createSession(options) {
1428
1468
  await store.delete(activeSession.id);
1429
1469
  await transport.clear(response);
1430
1470
  } else if (regenerated !== void 0) {
1431
- await store.set(regenerated.id, regenerated, clock());
1471
+ await store.set(regenerated, clock());
1432
1472
  await store.delete(activeSession.id);
1433
1473
  await transport.write(response, regenerated.id, encrypted);
1434
1474
  } else {
1435
- await store.set(activeSession.id, activeSession, clock());
1475
+ await store.set(activeSession, clock());
1436
1476
  if (minted) await transport.write(response, activeSession.id, encrypted);
1437
1477
  }
1438
1478
  }
@@ -1440,7 +1480,7 @@ function createSession(options) {
1440
1480
  };
1441
1481
  }
1442
1482
  /**
1443
- * Session-bound double-submit CSRF protection battery.
1483
+ * Creates the session-bound double-submit CSRF protection battery.
1444
1484
  *
1445
1485
  * @typeParam TState - The consumer's opaque per-request state type, must carry {@link CSRFState}, {@link SessionState}, and {@link ConnectionState}
1446
1486
  * @param options - See {@link CSRFOptions}
@@ -1497,8 +1537,8 @@ function createCSRF(options) {
1497
1537
  };
1498
1538
  }
1499
1539
  /**
1500
- * Scope a battery to run ONLY on a set of exact pathnames — elsewhere it
1501
- * steps aside via `next()`.
1540
+ * Scopes a battery to run ONLY on a set of exact pathnames — elsewhere it
1541
+ * steps aside through `next()`.
1502
1542
  *
1503
1543
  * @typeParam TState - The consumer's opaque per-request state type
1504
1544
  * @param paths - One pathname, or a set of pathnames, matched exactly against `context.url.pathname`
@@ -1518,8 +1558,8 @@ function only(paths, handler) {
1518
1558
  };
1519
1559
  }
1520
1560
  /**
1521
- * Scope a battery to run everywhere EXCEPT a set of exact pathnames — there
1522
- * it steps aside via `next()`.
1561
+ * Scopes a battery to run everywhere EXCEPT a set of exact pathnames — there
1562
+ * it steps aside through `next()`.
1523
1563
  *
1524
1564
  * @typeParam TState - The consumer's opaque per-request state type
1525
1565
  * @param paths - One pathname, or a set of pathnames, matched exactly against `context.url.pathname`
@@ -1541,11 +1581,11 @@ function except(paths, handler) {
1541
1581
  //#endregion
1542
1582
  //#region src/core/factories.ts
1543
1583
  /**
1544
- * Create a signed-cookie {@link SessionTransport} — the session id travels as
1584
+ * Creates a signed-cookie {@link SessionTransportInterface} — the session id travels as
1545
1585
  * a `signToken`-signed cookie value.
1546
1586
  *
1547
1587
  * @param options - See {@link CookieTransportOptions}
1548
- * @returns A {@link SessionTransport}
1588
+ * @returns A {@link SessionTransportInterface}
1549
1589
  * @throws {TypeError} When `options.secret` or `options.name` is malformed
1550
1590
  *
1551
1591
  * @example
@@ -1576,11 +1616,11 @@ function createCookieTransport(options) {
1576
1616
  };
1577
1617
  }
1578
1618
  /**
1579
- * Create a bare-header {@link SessionTransport} — the session id travels
1619
+ * Creates a bare-header {@link SessionTransportInterface} — the session id travels
1580
1620
  * verbatim in a request/response header.
1581
1621
  *
1582
1622
  * @param options - See {@link HeaderTransportOptions}
1583
- * @returns A {@link SessionTransport}
1623
+ * @returns A {@link SessionTransportInterface}
1584
1624
  * @throws {TypeError} When `options.header` is malformed
1585
1625
  *
1586
1626
  * @example
@@ -1604,18 +1644,17 @@ function createHeaderTransport(options) {
1604
1644
  };
1605
1645
  }
1606
1646
  /**
1607
- * Create the default in-process {@link SessionStoreInterface} — a `Map`-backed
1647
+ * Creates the default in-process {@link SessionStoreInterface} — a `Map`-backed
1608
1648
  * store enforcing an idle timeout and an absolute lifetime.
1609
1649
  *
1610
- * @typeParam S - The session data payload type
1650
+ * @typeParam S - The session entity type
1611
1651
  * @param options - See {@link MemorySessionStoreOptions}
1612
1652
  * @returns A {@link SessionStoreInterface}
1613
1653
  * @throws {TypeError} When `options.ttl` or `options.lifetime` is malformed
1614
1654
  *
1615
1655
  * @remarks
1616
- * The {@link Session} entity is the `create` option's default value factory
1617
- * for `createSession` and deliberately ships WITHOUT its own `create*`
1618
- * factory — the name `createSession` belongs to the battery, not this class.
1656
+ * The store holds whatever {@link SessionInterface} entity `createSession`'s
1657
+ * `create` option produced, keyed by that entity's own `id`.
1619
1658
  *
1620
1659
  * @example
1621
1660
  * ```ts
@@ -1626,30 +1665,53 @@ function createMemorySessionStore(options) {
1626
1665
  return new MemorySessionStore(options);
1627
1666
  }
1628
1667
  /**
1629
- * Create a {@link DatabaseSessionStore} as a {@link SessionStoreInterface} —
1668
+ * Creates a {@link DatabaseSessionStore} as a {@link SessionStoreInterface} —
1630
1669
  * the durable counterpart to `createMemorySessionStore`, over a caller-opened
1631
1670
  * `@orkestrel/database` table (declare it with {@link sessionColumns}).
1632
1671
  *
1633
- * @typeParam S - The session data payload type
1672
+ * @typeParam S - The session entity type
1634
1673
  * @param table - The backing `TableInterface<SessionRow>`
1635
- * @param is - A {@link Guard} narrowing a restored snapshot to `S`
1636
- * @param options - The idle `ttl` / absolute `lifetime` thresholds
1674
+ * @param guard - A {@link Guard} narrowing a restored snapshot to `S`
1675
+ * @param options - See {@link SessionLimits}
1637
1676
  * @returns A {@link SessionStoreInterface}
1677
+ * @throws {TypeError} When `options.ttl` or `options.lifetime` is malformed
1638
1678
  *
1639
1679
  * @remarks
1640
1680
  * This factory only wraps `new DatabaseSessionStore(...)` — it never opens a
1641
1681
  * database or driver itself; the caller owns that lifecycle and passes in an
1642
- * already-open table.
1682
+ * already-open table. It supplies {@link createRestoredSession} as the store's
1683
+ * snapshot rebuild step, which is why the store imports nothing from this
1684
+ * file.
1643
1685
  *
1644
1686
  * @example
1645
1687
  * ```ts
1646
1688
  * const store = createDatabaseSessionStore(db.table('sessions'), isSession, { ttl: 60_000 })
1647
1689
  * ```
1648
1690
  */
1649
- function createDatabaseSessionStore(table, is, options) {
1650
- return new DatabaseSessionStore(table, is, options);
1691
+ function createDatabaseSessionStore(table, guard, options) {
1692
+ return new DatabaseSessionStore(table, guard, createRestoredSession, options);
1693
+ }
1694
+ /**
1695
+ * Rebuilds a {@link Session} from an untrusted snapshot value — the inverse of
1696
+ * `snapshotSession` and a durable store's `get` deserialization step.
1697
+ *
1698
+ * @param value - The candidate snapshot, of unknown shape
1699
+ * @returns A rebuilt {@link Session}, or `undefined` when `value` is malformed
1700
+ *
1701
+ * @example
1702
+ * ```ts
1703
+ * createRestoredSession({ id: 'abc', state: { userId: 'u_1' } }) // Session { id: 'abc' }
1704
+ * createRestoredSession({ id: 1 }) // undefined
1705
+ * ```
1706
+ */
1707
+ function createRestoredSession(value) {
1708
+ if (!isRecord(value)) return void 0;
1709
+ if (!isString(value.id) || !isRecord(value.state)) return void 0;
1710
+ const session = new Session(value.id);
1711
+ for (const [key, entry] of Object.entries(value.state)) session.set(key, entry);
1712
+ return session;
1651
1713
  }
1652
1714
  //#endregion
1653
- export { DEFAULT_BEARER_HEADER, DEFAULT_BEARER_SCHEME, DEFAULT_CLUSTER, DEFAULT_COEP, DEFAULT_COMPRESSION_ENCODINGS, DEFAULT_COMPRESSION_THRESHOLD, DEFAULT_COOP, DEFAULT_CORP, DEFAULT_CORS_HEADERS, DEFAULT_CORS_METHODS, DEFAULT_CSP, DEFAULT_CSRF_COOKIE, DEFAULT_CSRF_FIELD, DEFAULT_CSRF_HEADER, DEFAULT_CSRF_SAFE_METHODS, DEFAULT_DEADLINE_STATUS, DEFAULT_FRAME_OPTIONS, DEFAULT_HSTS, DEFAULT_IDENTIFIER_HEADER, DEFAULT_LIMITER_CAPACITY, DEFAULT_LIMITER_MESSAGE, DEFAULT_PERMISSIONS_POLICY, DEFAULT_REFERRER_POLICY, DEFAULT_SESSION_CAPACITY, DEFAULT_SESSION_COOKIE, DEFAULT_SESSION_HEADER, DatabaseSessionStore, MemorySessionStore, Session, buildClientInfo, buildRateLimitField, buildRateLimitPolicyField, buildRetryAfter, compressBytes, compressResponse, createBearer, createBody, createBoundary, createCSRF, createCompression, createCookieTransport, createCors, createDatabaseSessionStore, createDeadline, createETag, createForwarded, createHeaderTransport, createLimiter, createMemorySessionStore, createSecurity, createSession, createTelemetry, detectEncodings, equalsConstantTime, except, isBufferingIneligible, isCompressionNegotiated, isMultipartBody, isMultipartFile, isPreflight, isSession, isSessionControl, matchesTrustedEntry, only, rebuildResponse, resolveForwardedFor, resolveKey, resolveOptInHeader, restoreSession, sessionColumns, sessionExpired, snapshotSession, transferSessionData };
1715
+ export { DEFAULT_BEARER_HEADER, DEFAULT_BEARER_SCHEME, DEFAULT_CLUSTER, DEFAULT_COEP, DEFAULT_COMPRESSION_ENCODINGS, DEFAULT_COMPRESSION_THRESHOLD, DEFAULT_COOP, DEFAULT_CORP, DEFAULT_CORS_HEADERS, DEFAULT_CORS_METHODS, DEFAULT_CSP, DEFAULT_CSRF_COOKIE, DEFAULT_CSRF_FIELD, DEFAULT_CSRF_HEADER, DEFAULT_CSRF_SAFE_METHODS, DEFAULT_DEADLINE_STATUS, DEFAULT_FRAME_OPTIONS, DEFAULT_HSTS, DEFAULT_IDENTIFIER_HEADER, DEFAULT_LIMITER_CAPACITY, DEFAULT_LIMITER_MESSAGE, DEFAULT_PERMISSIONS_POLICY, DEFAULT_REFERRER_POLICY, DEFAULT_SESSION_CAPACITY, DEFAULT_SESSION_COOKIE, DEFAULT_SESSION_HEADER, DatabaseSessionStore, MemorySessionStore, Session, buildClient, buildRateLimitField, buildRateLimitPolicyField, buildRetryAfter, compressBytes, compressResponse, createBearer, createBody, createBoundary, createCSRF, createCompression, createCookieTransport, createCors, createDatabaseSessionStore, createDeadline, createETag, createForwarded, createHeaderTransport, createLimiter, createMemorySessionStore, createRestoredSession, createSecurity, createSession, createTelemetry, detectEncodings, equalsConstantTime, except, isBufferingIneligible, isCompressionNegotiated, isMultipartBody, isMultipartFile, isPreflight, isSession, isSessionControl, matchesTrustedEntry, only, rebuildResponse, resolveForwardedFor, resolveKey, resolveOptInHeader, sessionColumns, sessionExpired, snapshotSession, transferSessionState, validateSessionLimits };
1654
1716
 
1655
1717
  //# sourceMappingURL=index.js.map