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