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