@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.
- package/dist/src/core/index.cjs +276 -28
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +229 -5
- package/dist/src/core/index.d.ts +229 -5
- package/dist/src/core/index.js +270 -30
- package/dist/src/core/index.js.map +1 -1
- package/package.json +15 -8
|
@@ -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
|
|
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.
|
package/dist/src/core/index.d.ts
CHANGED
|
@@ -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
|
|
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.
|