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