@orkestrel/middleware 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,13 @@
1
1
  import { ConnectionInfo } from '@orkestrel/server';
2
2
  import { CookieOptions } from '@orkestrel/server';
3
3
  import { Encoding } from '@orkestrel/server';
4
+ import { Guard } from '@orkestrel/contract';
5
+ import { JSONShape } from '@orkestrel/contract';
4
6
  import { MiddlewareContext } from '@orkestrel/server';
5
7
  import { MiddlewareHandler } from '@orkestrel/server';
8
+ import { NumberShape } from '@orkestrel/contract';
9
+ import { StringShape } from '@orkestrel/contract';
10
+ import { TableInterface } from '@orkestrel/database';
6
11
  import { TokenSecret } from '@orkestrel/server';
7
12
 
8
13
  /**
@@ -37,6 +42,19 @@ export declare interface BearerState {
37
42
  token?: string;
38
43
  }
39
44
 
45
+ /**
46
+ * The body state slice `createBody` stashes.
47
+ *
48
+ * @remarks
49
+ * `body` holds the same value the cached `context.body()` resolved to
50
+ * (`undefined` when the request declared no body, or a body `createBody`
51
+ * did not reject) — a mid-handler read without a second `context.body()`
52
+ * await.
53
+ */
54
+ export declare interface BodyState {
55
+ body?: unknown;
56
+ }
57
+
40
58
  /**
41
59
  * Options for `createBoundary` — the outermost error-rendering battery.
42
60
  *
@@ -254,9 +272,10 @@ export declare function createBearer<TState extends BearerState>(options: Bearer
254
272
 
255
273
  /**
256
274
  * The body-driving battery — eagerly awaits the cached `context.body()` so
257
- * its throws (or a malformed-JSON `undefined`) surface before the handler runs.
275
+ * its throws (or a malformed-JSON `undefined`) surface before the handler
276
+ * runs, and stashes the resolved value onto {@link BodyState.body}.
258
277
  *
259
- * @typeParam TState - The consumer's opaque per-request state type
278
+ * @typeParam TState - The consumer's opaque per-request state type, must carry {@link BodyState}
260
279
  * @returns A `MiddlewareHandler<TState>`
261
280
  * @throws {HTTPError} `400` when the request declares `application/json` and the body resolves `undefined`
262
281
  *
@@ -264,14 +283,16 @@ export declare function createBearer<TState extends BearerState>(options: Bearer
264
283
  * The shipped `MiddlewareContext.body()` is a parameterless, server-owned
265
284
  * cache (`ServerOptions.limit` governs its size cap) — this battery carries
266
285
  * no `limit`/`decompression` options (a deliberate break from the deleted old
267
- * `createBodyParser` surface, which configured them itself).
286
+ * `createBodyParser` surface, which configured them itself). `state.body` is
287
+ * stashed from the SAME awaited call the 400 check reads — `context.body()`
288
+ * is never invoked twice.
268
289
  *
269
290
  * @example
270
291
  * ```ts
271
292
  * const body = createBody()
272
293
  * ```
273
294
  */
274
- export declare function createBody<TState>(): MiddlewareHandler<TState>;
295
+ export declare function createBody<TState extends BodyState = BodyState>(): MiddlewareHandler<TState>;
275
296
 
276
297
  /**
277
298
  * The outermost error-rendering battery — catches a downstream throw and
@@ -351,6 +372,32 @@ export declare function createCors<TState>(options?: CorsOptions): MiddlewareHan
351
372
  */
352
373
  export declare function createCSRF<TState extends CSRFState & SessionState & ConnectionState>(options: CSRFOptions): MiddlewareHandler<TState>;
353
374
 
375
+ /**
376
+ * Create a {@link DatabaseSessionStore} as a {@link SessionStoreInterface} —
377
+ * the durable counterpart to `createMemorySessionStore`, over a caller-opened
378
+ * `@orkestrel/database` table (declare it with {@link sessionColumns}).
379
+ *
380
+ * @typeParam S - The session data payload type
381
+ * @param table - The backing `TableInterface<SessionRow>`
382
+ * @param is - A {@link Guard} narrowing a restored snapshot to `S`
383
+ * @param options - The idle `ttl` / absolute `lifetime` thresholds
384
+ * @returns A {@link SessionStoreInterface}
385
+ *
386
+ * @remarks
387
+ * This factory only wraps `new DatabaseSessionStore(...)` — it never opens a
388
+ * database or driver itself; the caller owns that lifecycle and passes in an
389
+ * already-open table.
390
+ *
391
+ * @example
392
+ * ```ts
393
+ * const store = createDatabaseSessionStore(db.table('sessions'), isSession, { ttl: 60_000 })
394
+ * ```
395
+ */
396
+ export declare function createDatabaseSessionStore<S extends SessionInterface = Session>(table: TableInterface<SessionRow>, is: Guard<S>, options?: {
397
+ readonly ttl?: number;
398
+ readonly lifetime?: number;
399
+ }): SessionStoreInterface<S>;
400
+
354
401
  /**
355
402
  * The application-level per-request deadline battery.
356
403
  *
@@ -532,6 +579,49 @@ export declare interface CSRFState {
532
579
  csrf?: string;
533
580
  }
534
581
 
582
+ /**
583
+ * A durable {@link SessionStoreInterface} over an `@orkestrel/database`
584
+ * table — the same idle-timeout + absolute-lifetime contract as
585
+ * {@link MemorySessionStore}, backed by a caller-supplied `TableInterface`
586
+ * instead of an in-process `Map`.
587
+ *
588
+ * @typeParam S - The session data payload type
589
+ *
590
+ * @remarks
591
+ * `get` reads the row, evicts (removes the row) once `sessionExpired`
592
+ * reports either threshold elapsed, then rebuilds the session via
593
+ * {@link restoreSession} — a malformed snapshot or one that fails the
594
+ * caller's `is` guard resolves `undefined` rather than throwing. A live read
595
+ * touches `lastSeen`. `set` preserves an existing row's `createdAt` across a
596
+ * re-`set` of the same id (stamped once at the first `set`), mirroring
597
+ * {@link MemorySessionStore}. `delete` of an absent id is a no-op (the
598
+ * table's `remove` contract).
599
+ *
600
+ * A malformed-snapshot or failed-guard `undefined` LEAVES the row in place —
601
+ * unlike the expired path, which removes it. This is deliberate: a
602
+ * caller-contextual `is` guard may reject a session that is still perfectly
603
+ * valid for another flow reading the same table (a differently-shaped `S`,
604
+ * a stricter guard mid-rollout), so `get` never destroys data on a guard
605
+ * miss. A row that no caller's guard ever accepts again self-heals once its
606
+ * `ttl`/`lifetime` elapses on a later `get`.
607
+ *
608
+ * @example
609
+ * ```ts
610
+ * const store = new DatabaseSessionStore(table, isSession, { ttl: 60_000 })
611
+ * await store.set('abc', new Session('abc'), Date.now())
612
+ * ```
613
+ */
614
+ export declare class DatabaseSessionStore<S extends SessionInterface = Session> implements SessionStoreInterface<S> {
615
+ #private;
616
+ constructor(table: TableInterface<SessionRow>, is: Guard<S>, options?: {
617
+ readonly ttl?: number;
618
+ readonly lifetime?: number;
619
+ });
620
+ get(id: string, now: number): Promise<S | undefined>;
621
+ set(id: string, session: S, now: number): Promise<void>;
622
+ delete(id: string): Promise<void>;
623
+ }
624
+
535
625
  /**
536
626
  * Options for `createDeadline` — the application-level per-request deadline.
537
627
  *
@@ -695,6 +785,22 @@ export declare interface ETagOptions {
695
785
  readonly weak?: boolean;
696
786
  }
697
787
 
788
+ /**
789
+ * Scope a battery to run everywhere EXCEPT a set of exact pathnames — there
790
+ * it steps aside via `next()`.
791
+ *
792
+ * @typeParam TState - The consumer's opaque per-request state type
793
+ * @param paths - One pathname, or a set of pathnames, matched exactly against `context.url.pathname`
794
+ * @param handler - The battery to run when `paths` does not match
795
+ * @returns A `MiddlewareHandler<TState>`
796
+ *
797
+ * @example
798
+ * ```ts
799
+ * const scoped = except('/health', createTelemetry({ record }))
800
+ * ```
801
+ */
802
+ export declare function except<TState>(paths: string | readonly string[], handler: MiddlewareHandler<TState>): MiddlewareHandler<TState>;
803
+
698
804
  /**
699
805
  * Options for `createForwarded` — the trusted-proxy client-IP resolver.
700
806
  *
@@ -812,7 +918,10 @@ export declare function isPreflight(method: string, headers: Headers): boolean;
812
918
 
813
919
  /**
814
920
  * Determine whether a value implements {@link SessionInterface} — a total
815
- * structural guard (§14): an `id` string plus a `data` `Map`.
921
+ * structural guard (§14): an `id` string plus a `data` `Map`. Prototype-agnostic
922
+ * — accepts a plain object, a null-prototype object, AND a class instance
923
+ * (a real `Session`), since a restored/stored session is routinely a class
924
+ * instance, not a literal.
816
925
  *
817
926
  * @param value - The candidate value
818
927
  * @returns `true` when `value` is shaped like a {@link SessionInterface}
@@ -820,6 +929,7 @@ export declare function isPreflight(method: string, headers: Headers): boolean;
820
929
  * @example
821
930
  * ```ts
822
931
  * isSession({ id: 'a', data: new Map() }) // true
932
+ * isSession(new Session('a')) // true
823
933
  * ```
824
934
  */
825
935
  export declare function isSession(value: unknown): value is SessionInterface;
@@ -1007,6 +1117,22 @@ export declare interface MultipartState {
1007
1117
  multipart?: MultipartBody;
1008
1118
  }
1009
1119
 
1120
+ /**
1121
+ * Scope a battery to run ONLY on a set of exact pathnames — elsewhere it
1122
+ * steps aside via `next()`.
1123
+ *
1124
+ * @typeParam TState - The consumer's opaque per-request state type
1125
+ * @param paths - One pathname, or a set of pathnames, matched exactly against `context.url.pathname`
1126
+ * @param handler - The battery to run when `paths` matches
1127
+ * @returns A `MiddlewareHandler<TState>`
1128
+ *
1129
+ * @example
1130
+ * ```ts
1131
+ * const scoped = only('/admin', createBearer({ secret }))
1132
+ * ```
1133
+ */
1134
+ export declare function only<TState>(paths: string | readonly string[], handler: MiddlewareHandler<TState>): MiddlewareHandler<TState>;
1135
+
1010
1136
  /**
1011
1137
  * Rebuild a `Response` around a replacement body while preserving its
1012
1138
  * status/statusText — the buffered-response reconstruction shared by the
@@ -1099,6 +1225,21 @@ export declare function resolveKey(state: BearerState & ClientState & Connection
1099
1225
  */
1100
1226
  export declare function resolveOptInHeader(value: string | boolean | undefined, fallback: string): string | undefined;
1101
1227
 
1228
+ /**
1229
+ * Rebuild a `Session` from an untrusted snapshot value (the inverse of
1230
+ * {@link snapshotSession}) — a durable store's `get` deserialization step.
1231
+ *
1232
+ * @param value - The candidate snapshot, of unknown shape
1233
+ * @returns A rebuilt `Session`, or `undefined` when `value` is malformed
1234
+ *
1235
+ * @example
1236
+ * ```ts
1237
+ * restoreSession({ id: 'abc', data: { userId: 'u_1' } }) // Session { id: 'abc', data: Map }
1238
+ * restoreSession({ id: 1 }) // undefined
1239
+ * ```
1240
+ */
1241
+ export declare function restoreSession(value: unknown): Session | undefined;
1242
+
1102
1243
  /**
1103
1244
  * `createSecurity`'s `identifier` sub-option — request-id minting/echo
1104
1245
  * policy, or `false` to disable the feature entirely.
@@ -1170,6 +1311,31 @@ export declare class Session implements SessionInterface {
1170
1311
  constructor(id: string);
1171
1312
  }
1172
1313
 
1314
+ /**
1315
+ * The `@orkestrel/database` column shape for a {@link SessionRow} table — pass
1316
+ * as-is to `createDatabase({ tables: { sessions: sessionColumns } })` so an
1317
+ * app declaring a durable session table never hand-writes the shape.
1318
+ *
1319
+ * @remarks
1320
+ * `lastSeen`/`createdAt` are `integerShape({ min: 0 })` — the table VALIDATES
1321
+ * them as integers, so `DatabaseSessionStore`'s `now` clock must yield
1322
+ * integer milliseconds (`Date.now()`, the implicit default `createSession`
1323
+ * clock). A fractional clock (`performance.now()`) fails the write with a
1324
+ * validation error; `MemorySessionStore` carries no such column shape and
1325
+ * accepts a fractional clock without complaint.
1326
+ *
1327
+ * @example
1328
+ * ```ts
1329
+ * const db = createDatabase({ driver, tables: { sessions: sessionColumns } })
1330
+ * ```
1331
+ */
1332
+ export declare const sessionColumns: {
1333
+ id: StringShape;
1334
+ session: JSONShape;
1335
+ lastSeen: NumberShape;
1336
+ createdAt: NumberShape;
1337
+ };
1338
+
1173
1339
  /**
1174
1340
  * The mid-handler control handle `createSession` stashes alongside the
1175
1341
  * session itself — the OWASP anti-fixation / logout primitives.
@@ -1185,6 +1351,28 @@ export declare interface SessionControlInterface {
1185
1351
  destroy(): void;
1186
1352
  }
1187
1353
 
1354
+ /**
1355
+ * Whether a session has aged past its idle timeout or absolute lifetime as
1356
+ * of `now` — the pure expiry predicate `MemorySessionStore` delegates to.
1357
+ *
1358
+ * @param cursors - The session's `lastSeen` (idle) and `createdAt` (absolute) instants
1359
+ * @param now - The current instant (same clock unit as `cursors`)
1360
+ * @param limits - The optional `ttl` (idle) and `lifetime` (absolute) thresholds
1361
+ * @returns `true` when either configured threshold has elapsed
1362
+ *
1363
+ * @example
1364
+ * ```ts
1365
+ * sessionExpired({ lastSeen: 0, createdAt: 0 }, 1_000, { ttl: 500 }) // true
1366
+ * ```
1367
+ */
1368
+ export declare function sessionExpired(cursors: {
1369
+ readonly lastSeen: number;
1370
+ readonly createdAt: number;
1371
+ }, now: number, limits: {
1372
+ readonly ttl?: number;
1373
+ readonly lifetime?: number;
1374
+ }): boolean;
1375
+
1188
1376
  /**
1189
1377
  * A server-managed session's public surface — an id and its mutable data bag.
1190
1378
  *
@@ -1241,6 +1429,18 @@ export declare interface SessionOptions<S, TState = unknown> {
1241
1429
  readonly clock?: () => number;
1242
1430
  }
1243
1431
 
1432
+ /**
1433
+ * One persisted session row — an opaque snapshot column plus the store-owned
1434
+ * idle/absolute-lifetime cursors, the shape a {@link DatabaseSessionStore}'s
1435
+ * backing table holds.
1436
+ */
1437
+ export declare interface SessionRow {
1438
+ readonly id: string;
1439
+ readonly session: unknown;
1440
+ readonly lastSeen: number;
1441
+ readonly createdAt: number;
1442
+ }
1443
+
1244
1444
  /**
1245
1445
  * The session state slice `createSession` stashes.
1246
1446
  *
@@ -1290,6 +1490,30 @@ export declare interface SessionTransport {
1290
1490
  clear(response: Response): void;
1291
1491
  }
1292
1492
 
1493
+ /**
1494
+ * Snapshot a session's `data` Map into a plain, serializable record — the
1495
+ * projection a durable store's `set` writes to disk.
1496
+ *
1497
+ * @param session - The session to snapshot
1498
+ * @returns A plain-object copy of `session.data`, keyed alongside `session.id`
1499
+ *
1500
+ * @remarks
1501
+ * `data` is built on a null-prototype object (`Object.create(null)`), never
1502
+ * a `{}` literal — a session key literally named `__proto__` must round-trip
1503
+ * as an OWN enumerable property instead of hitting `Object.prototype`'s
1504
+ * `__proto__` accessor (which would silently drop the entry and risk
1505
+ * polluting the shared prototype).
1506
+ *
1507
+ * @example
1508
+ * ```ts
1509
+ * snapshotSession(session) // { id: 'abc', data: { userId: 'u_1' } }
1510
+ * ```
1511
+ */
1512
+ export declare function snapshotSession(session: SessionInterface): {
1513
+ readonly id: string;
1514
+ readonly data: Record<string, unknown>;
1515
+ };
1516
+
1293
1517
  /**
1294
1518
  * One access-log-style entry `createTelemetry` records after a response
1295
1519
  * settles — the access-log/timing seam's payload shape.
@@ -1,8 +1,13 @@
1
1
  import { ConnectionInfo } from '@orkestrel/server';
2
2
  import { CookieOptions } from '@orkestrel/server';
3
3
  import { Encoding } from '@orkestrel/server';
4
+ import { Guard } from '@orkestrel/contract';
5
+ import { JSONShape } from '@orkestrel/contract';
4
6
  import { MiddlewareContext } from '@orkestrel/server';
5
7
  import { MiddlewareHandler } from '@orkestrel/server';
8
+ import { NumberShape } from '@orkestrel/contract';
9
+ import { StringShape } from '@orkestrel/contract';
10
+ import { TableInterface } from '@orkestrel/database';
6
11
  import { TokenSecret } from '@orkestrel/server';
7
12
 
8
13
  /**
@@ -37,6 +42,19 @@ export declare interface BearerState {
37
42
  token?: string;
38
43
  }
39
44
 
45
+ /**
46
+ * The body state slice `createBody` stashes.
47
+ *
48
+ * @remarks
49
+ * `body` holds the same value the cached `context.body()` resolved to
50
+ * (`undefined` when the request declared no body, or a body `createBody`
51
+ * did not reject) — a mid-handler read without a second `context.body()`
52
+ * await.
53
+ */
54
+ export declare interface BodyState {
55
+ body?: unknown;
56
+ }
57
+
40
58
  /**
41
59
  * Options for `createBoundary` — the outermost error-rendering battery.
42
60
  *
@@ -254,9 +272,10 @@ export declare function createBearer<TState extends BearerState>(options: Bearer
254
272
 
255
273
  /**
256
274
  * The body-driving battery — eagerly awaits the cached `context.body()` so
257
- * its throws (or a malformed-JSON `undefined`) surface before the handler runs.
275
+ * its throws (or a malformed-JSON `undefined`) surface before the handler
276
+ * runs, and stashes the resolved value onto {@link BodyState.body}.
258
277
  *
259
- * @typeParam TState - The consumer's opaque per-request state type
278
+ * @typeParam TState - The consumer's opaque per-request state type, must carry {@link BodyState}
260
279
  * @returns A `MiddlewareHandler<TState>`
261
280
  * @throws {HTTPError} `400` when the request declares `application/json` and the body resolves `undefined`
262
281
  *
@@ -264,14 +283,16 @@ export declare function createBearer<TState extends BearerState>(options: Bearer
264
283
  * The shipped `MiddlewareContext.body()` is a parameterless, server-owned
265
284
  * cache (`ServerOptions.limit` governs its size cap) — this battery carries
266
285
  * no `limit`/`decompression` options (a deliberate break from the deleted old
267
- * `createBodyParser` surface, which configured them itself).
286
+ * `createBodyParser` surface, which configured them itself). `state.body` is
287
+ * stashed from the SAME awaited call the 400 check reads — `context.body()`
288
+ * is never invoked twice.
268
289
  *
269
290
  * @example
270
291
  * ```ts
271
292
  * const body = createBody()
272
293
  * ```
273
294
  */
274
- export declare function createBody<TState>(): MiddlewareHandler<TState>;
295
+ export declare function createBody<TState extends BodyState = BodyState>(): MiddlewareHandler<TState>;
275
296
 
276
297
  /**
277
298
  * The outermost error-rendering battery — catches a downstream throw and
@@ -351,6 +372,32 @@ export declare function createCors<TState>(options?: CorsOptions): MiddlewareHan
351
372
  */
352
373
  export declare function createCSRF<TState extends CSRFState & SessionState & ConnectionState>(options: CSRFOptions): MiddlewareHandler<TState>;
353
374
 
375
+ /**
376
+ * Create a {@link DatabaseSessionStore} as a {@link SessionStoreInterface} —
377
+ * the durable counterpart to `createMemorySessionStore`, over a caller-opened
378
+ * `@orkestrel/database` table (declare it with {@link sessionColumns}).
379
+ *
380
+ * @typeParam S - The session data payload type
381
+ * @param table - The backing `TableInterface<SessionRow>`
382
+ * @param is - A {@link Guard} narrowing a restored snapshot to `S`
383
+ * @param options - The idle `ttl` / absolute `lifetime` thresholds
384
+ * @returns A {@link SessionStoreInterface}
385
+ *
386
+ * @remarks
387
+ * This factory only wraps `new DatabaseSessionStore(...)` — it never opens a
388
+ * database or driver itself; the caller owns that lifecycle and passes in an
389
+ * already-open table.
390
+ *
391
+ * @example
392
+ * ```ts
393
+ * const store = createDatabaseSessionStore(db.table('sessions'), isSession, { ttl: 60_000 })
394
+ * ```
395
+ */
396
+ export declare function createDatabaseSessionStore<S extends SessionInterface = Session>(table: TableInterface<SessionRow>, is: Guard<S>, options?: {
397
+ readonly ttl?: number;
398
+ readonly lifetime?: number;
399
+ }): SessionStoreInterface<S>;
400
+
354
401
  /**
355
402
  * The application-level per-request deadline battery.
356
403
  *
@@ -532,6 +579,49 @@ export declare interface CSRFState {
532
579
  csrf?: string;
533
580
  }
534
581
 
582
+ /**
583
+ * A durable {@link SessionStoreInterface} over an `@orkestrel/database`
584
+ * table — the same idle-timeout + absolute-lifetime contract as
585
+ * {@link MemorySessionStore}, backed by a caller-supplied `TableInterface`
586
+ * instead of an in-process `Map`.
587
+ *
588
+ * @typeParam S - The session data payload type
589
+ *
590
+ * @remarks
591
+ * `get` reads the row, evicts (removes the row) once `sessionExpired`
592
+ * reports either threshold elapsed, then rebuilds the session via
593
+ * {@link restoreSession} — a malformed snapshot or one that fails the
594
+ * caller's `is` guard resolves `undefined` rather than throwing. A live read
595
+ * touches `lastSeen`. `set` preserves an existing row's `createdAt` across a
596
+ * re-`set` of the same id (stamped once at the first `set`), mirroring
597
+ * {@link MemorySessionStore}. `delete` of an absent id is a no-op (the
598
+ * table's `remove` contract).
599
+ *
600
+ * A malformed-snapshot or failed-guard `undefined` LEAVES the row in place —
601
+ * unlike the expired path, which removes it. This is deliberate: a
602
+ * caller-contextual `is` guard may reject a session that is still perfectly
603
+ * valid for another flow reading the same table (a differently-shaped `S`,
604
+ * a stricter guard mid-rollout), so `get` never destroys data on a guard
605
+ * miss. A row that no caller's guard ever accepts again self-heals once its
606
+ * `ttl`/`lifetime` elapses on a later `get`.
607
+ *
608
+ * @example
609
+ * ```ts
610
+ * const store = new DatabaseSessionStore(table, isSession, { ttl: 60_000 })
611
+ * await store.set('abc', new Session('abc'), Date.now())
612
+ * ```
613
+ */
614
+ export declare class DatabaseSessionStore<S extends SessionInterface = Session> implements SessionStoreInterface<S> {
615
+ #private;
616
+ constructor(table: TableInterface<SessionRow>, is: Guard<S>, options?: {
617
+ readonly ttl?: number;
618
+ readonly lifetime?: number;
619
+ });
620
+ get(id: string, now: number): Promise<S | undefined>;
621
+ set(id: string, session: S, now: number): Promise<void>;
622
+ delete(id: string): Promise<void>;
623
+ }
624
+
535
625
  /**
536
626
  * Options for `createDeadline` — the application-level per-request deadline.
537
627
  *
@@ -695,6 +785,22 @@ export declare interface ETagOptions {
695
785
  readonly weak?: boolean;
696
786
  }
697
787
 
788
+ /**
789
+ * Scope a battery to run everywhere EXCEPT a set of exact pathnames — there
790
+ * it steps aside via `next()`.
791
+ *
792
+ * @typeParam TState - The consumer's opaque per-request state type
793
+ * @param paths - One pathname, or a set of pathnames, matched exactly against `context.url.pathname`
794
+ * @param handler - The battery to run when `paths` does not match
795
+ * @returns A `MiddlewareHandler<TState>`
796
+ *
797
+ * @example
798
+ * ```ts
799
+ * const scoped = except('/health', createTelemetry({ record }))
800
+ * ```
801
+ */
802
+ export declare function except<TState>(paths: string | readonly string[], handler: MiddlewareHandler<TState>): MiddlewareHandler<TState>;
803
+
698
804
  /**
699
805
  * Options for `createForwarded` — the trusted-proxy client-IP resolver.
700
806
  *
@@ -812,7 +918,10 @@ export declare function isPreflight(method: string, headers: Headers): boolean;
812
918
 
813
919
  /**
814
920
  * Determine whether a value implements {@link SessionInterface} — a total
815
- * structural guard (§14): an `id` string plus a `data` `Map`.
921
+ * structural guard (§14): an `id` string plus a `data` `Map`. Prototype-agnostic
922
+ * — accepts a plain object, a null-prototype object, AND a class instance
923
+ * (a real `Session`), since a restored/stored session is routinely a class
924
+ * instance, not a literal.
816
925
  *
817
926
  * @param value - The candidate value
818
927
  * @returns `true` when `value` is shaped like a {@link SessionInterface}
@@ -820,6 +929,7 @@ export declare function isPreflight(method: string, headers: Headers): boolean;
820
929
  * @example
821
930
  * ```ts
822
931
  * isSession({ id: 'a', data: new Map() }) // true
932
+ * isSession(new Session('a')) // true
823
933
  * ```
824
934
  */
825
935
  export declare function isSession(value: unknown): value is SessionInterface;
@@ -1007,6 +1117,22 @@ export declare interface MultipartState {
1007
1117
  multipart?: MultipartBody;
1008
1118
  }
1009
1119
 
1120
+ /**
1121
+ * Scope a battery to run ONLY on a set of exact pathnames — elsewhere it
1122
+ * steps aside via `next()`.
1123
+ *
1124
+ * @typeParam TState - The consumer's opaque per-request state type
1125
+ * @param paths - One pathname, or a set of pathnames, matched exactly against `context.url.pathname`
1126
+ * @param handler - The battery to run when `paths` matches
1127
+ * @returns A `MiddlewareHandler<TState>`
1128
+ *
1129
+ * @example
1130
+ * ```ts
1131
+ * const scoped = only('/admin', createBearer({ secret }))
1132
+ * ```
1133
+ */
1134
+ export declare function only<TState>(paths: string | readonly string[], handler: MiddlewareHandler<TState>): MiddlewareHandler<TState>;
1135
+
1010
1136
  /**
1011
1137
  * Rebuild a `Response` around a replacement body while preserving its
1012
1138
  * status/statusText — the buffered-response reconstruction shared by the
@@ -1099,6 +1225,21 @@ export declare function resolveKey(state: BearerState & ClientState & Connection
1099
1225
  */
1100
1226
  export declare function resolveOptInHeader(value: string | boolean | undefined, fallback: string): string | undefined;
1101
1227
 
1228
+ /**
1229
+ * Rebuild a `Session` from an untrusted snapshot value (the inverse of
1230
+ * {@link snapshotSession}) — a durable store's `get` deserialization step.
1231
+ *
1232
+ * @param value - The candidate snapshot, of unknown shape
1233
+ * @returns A rebuilt `Session`, or `undefined` when `value` is malformed
1234
+ *
1235
+ * @example
1236
+ * ```ts
1237
+ * restoreSession({ id: 'abc', data: { userId: 'u_1' } }) // Session { id: 'abc', data: Map }
1238
+ * restoreSession({ id: 1 }) // undefined
1239
+ * ```
1240
+ */
1241
+ export declare function restoreSession(value: unknown): Session | undefined;
1242
+
1102
1243
  /**
1103
1244
  * `createSecurity`'s `identifier` sub-option — request-id minting/echo
1104
1245
  * policy, or `false` to disable the feature entirely.
@@ -1170,6 +1311,31 @@ export declare class Session implements SessionInterface {
1170
1311
  constructor(id: string);
1171
1312
  }
1172
1313
 
1314
+ /**
1315
+ * The `@orkestrel/database` column shape for a {@link SessionRow} table — pass
1316
+ * as-is to `createDatabase({ tables: { sessions: sessionColumns } })` so an
1317
+ * app declaring a durable session table never hand-writes the shape.
1318
+ *
1319
+ * @remarks
1320
+ * `lastSeen`/`createdAt` are `integerShape({ min: 0 })` — the table VALIDATES
1321
+ * them as integers, so `DatabaseSessionStore`'s `now` clock must yield
1322
+ * integer milliseconds (`Date.now()`, the implicit default `createSession`
1323
+ * clock). A fractional clock (`performance.now()`) fails the write with a
1324
+ * validation error; `MemorySessionStore` carries no such column shape and
1325
+ * accepts a fractional clock without complaint.
1326
+ *
1327
+ * @example
1328
+ * ```ts
1329
+ * const db = createDatabase({ driver, tables: { sessions: sessionColumns } })
1330
+ * ```
1331
+ */
1332
+ export declare const sessionColumns: {
1333
+ id: StringShape;
1334
+ session: JSONShape;
1335
+ lastSeen: NumberShape;
1336
+ createdAt: NumberShape;
1337
+ };
1338
+
1173
1339
  /**
1174
1340
  * The mid-handler control handle `createSession` stashes alongside the
1175
1341
  * session itself — the OWASP anti-fixation / logout primitives.
@@ -1185,6 +1351,28 @@ export declare interface SessionControlInterface {
1185
1351
  destroy(): void;
1186
1352
  }
1187
1353
 
1354
+ /**
1355
+ * Whether a session has aged past its idle timeout or absolute lifetime as
1356
+ * of `now` — the pure expiry predicate `MemorySessionStore` delegates to.
1357
+ *
1358
+ * @param cursors - The session's `lastSeen` (idle) and `createdAt` (absolute) instants
1359
+ * @param now - The current instant (same clock unit as `cursors`)
1360
+ * @param limits - The optional `ttl` (idle) and `lifetime` (absolute) thresholds
1361
+ * @returns `true` when either configured threshold has elapsed
1362
+ *
1363
+ * @example
1364
+ * ```ts
1365
+ * sessionExpired({ lastSeen: 0, createdAt: 0 }, 1_000, { ttl: 500 }) // true
1366
+ * ```
1367
+ */
1368
+ export declare function sessionExpired(cursors: {
1369
+ readonly lastSeen: number;
1370
+ readonly createdAt: number;
1371
+ }, now: number, limits: {
1372
+ readonly ttl?: number;
1373
+ readonly lifetime?: number;
1374
+ }): boolean;
1375
+
1188
1376
  /**
1189
1377
  * A server-managed session's public surface — an id and its mutable data bag.
1190
1378
  *
@@ -1241,6 +1429,18 @@ export declare interface SessionOptions<S, TState = unknown> {
1241
1429
  readonly clock?: () => number;
1242
1430
  }
1243
1431
 
1432
+ /**
1433
+ * One persisted session row — an opaque snapshot column plus the store-owned
1434
+ * idle/absolute-lifetime cursors, the shape a {@link DatabaseSessionStore}'s
1435
+ * backing table holds.
1436
+ */
1437
+ export declare interface SessionRow {
1438
+ readonly id: string;
1439
+ readonly session: unknown;
1440
+ readonly lastSeen: number;
1441
+ readonly createdAt: number;
1442
+ }
1443
+
1244
1444
  /**
1245
1445
  * The session state slice `createSession` stashes.
1246
1446
  *
@@ -1290,6 +1490,30 @@ export declare interface SessionTransport {
1290
1490
  clear(response: Response): void;
1291
1491
  }
1292
1492
 
1493
+ /**
1494
+ * Snapshot a session's `data` Map into a plain, serializable record — the
1495
+ * projection a durable store's `set` writes to disk.
1496
+ *
1497
+ * @param session - The session to snapshot
1498
+ * @returns A plain-object copy of `session.data`, keyed alongside `session.id`
1499
+ *
1500
+ * @remarks
1501
+ * `data` is built on a null-prototype object (`Object.create(null)`), never
1502
+ * a `{}` literal — a session key literally named `__proto__` must round-trip
1503
+ * as an OWN enumerable property instead of hitting `Object.prototype`'s
1504
+ * `__proto__` accessor (which would silently drop the entry and risk
1505
+ * polluting the shared prototype).
1506
+ *
1507
+ * @example
1508
+ * ```ts
1509
+ * snapshotSession(session) // { id: 'abc', data: { userId: 'u_1' } }
1510
+ * ```
1511
+ */
1512
+ export declare function snapshotSession(session: SessionInterface): {
1513
+ readonly id: string;
1514
+ readonly data: Record<string, unknown>;
1515
+ };
1516
+
1293
1517
  /**
1294
1518
  * One access-log-style entry `createTelemetry` records after a response
1295
1519
  * settles — the access-log/timing seam's payload shape.