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