@orkestrel/middleware 0.0.18 → 0.0.20

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