@c9up/ream 0.1.34 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Application.d.ts +68 -0
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +142 -6
- package/dist/Application.js.map +1 -1
- package/dist/HealthCheck.d.ts +14 -7
- package/dist/HealthCheck.d.ts.map +1 -1
- package/dist/HealthCheck.js +21 -12
- package/dist/HealthCheck.js.map +1 -1
- package/dist/HttpKernel.d.ts +12 -0
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +15 -4
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +8 -0
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +11 -0
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +13 -2
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +50 -30
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts +65 -6
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +100 -7
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/console/BaseCommand.d.ts +8 -1
- package/dist/console/BaseCommand.d.ts.map +1 -1
- package/dist/console/BaseCommand.js +1 -1
- package/dist/console/BaseCommand.js.map +1 -1
- package/dist/console/Kernel.d.ts +15 -0
- package/dist/console/Kernel.d.ts.map +1 -1
- package/dist/console/Kernel.js +17 -0
- package/dist/console/Kernel.js.map +1 -1
- package/dist/directories.d.ts +52 -0
- package/dist/directories.d.ts.map +1 -0
- package/dist/directories.js +38 -0
- package/dist/directories.js.map +1 -0
- package/dist/events/Emitter.d.ts +49 -6
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +117 -7
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/EventsBuffer.d.ts +47 -0
- package/dist/events/EventsBuffer.d.ts.map +1 -0
- package/dist/events/EventsBuffer.js +107 -0
- package/dist/events/EventsBuffer.js.map +1 -0
- package/dist/events/index.d.ts +2 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +1 -0
- package/dist/events/index.js.map +1 -1
- package/dist/health/BaseCheck.d.ts +19 -0
- package/dist/health/BaseCheck.d.ts.map +1 -0
- package/dist/health/BaseCheck.js +24 -0
- package/dist/health/BaseCheck.js.map +1 -0
- package/dist/health/HealthChecks.d.ts +15 -0
- package/dist/health/HealthChecks.d.ts.map +1 -0
- package/dist/health/HealthChecks.js +69 -0
- package/dist/health/HealthChecks.js.map +1 -0
- package/dist/health/Result.d.ts +27 -0
- package/dist/health/Result.d.ts.map +1 -0
- package/dist/health/Result.js +55 -0
- package/dist/health/Result.js.map +1 -0
- package/dist/health/checks/DiskSpaceCheck.d.ts +29 -0
- package/dist/health/checks/DiskSpaceCheck.d.ts.map +1 -0
- package/dist/health/checks/DiskSpaceCheck.js +58 -0
- package/dist/health/checks/DiskSpaceCheck.js.map +1 -0
- package/dist/health/checks/MemoryHeapCheck.d.ts +21 -0
- package/dist/health/checks/MemoryHeapCheck.d.ts.map +1 -0
- package/dist/health/checks/MemoryHeapCheck.js +55 -0
- package/dist/health/checks/MemoryHeapCheck.js.map +1 -0
- package/dist/health/checks/MemoryRSSCheck.d.ts +21 -0
- package/dist/health/checks/MemoryRSSCheck.d.ts.map +1 -0
- package/dist/health/checks/MemoryRSSCheck.js +55 -0
- package/dist/health/checks/MemoryRSSCheck.js.map +1 -0
- package/dist/health/checks/memory.d.ts +24 -0
- package/dist/health/checks/memory.d.ts.map +1 -0
- package/dist/health/checks/memory.js +88 -0
- package/dist/health/checks/memory.js.map +1 -0
- package/dist/health/index.d.ts +9 -0
- package/dist/health/index.d.ts.map +1 -0
- package/dist/health/index.js +8 -0
- package/dist/health/index.js.map +1 -0
- package/dist/health/tracing.d.ts +12 -0
- package/dist/health/tracing.d.ts.map +1 -0
- package/dist/health/tracing.js +11 -0
- package/dist/health/tracing.js.map +1 -0
- package/dist/health/types.d.ts +51 -0
- package/dist/health/types.d.ts.map +1 -0
- package/dist/health/types.js +5 -0
- package/dist/health/types.js.map +1 -0
- package/dist/helpers/bytes.d.ts +28 -0
- package/dist/helpers/bytes.d.ts.map +1 -0
- package/dist/helpers/bytes.js +63 -0
- package/dist/helpers/bytes.js.map +1 -0
- package/dist/helpers/string.d.ts +4 -0
- package/dist/helpers/string.d.ts.map +1 -1
- package/dist/helpers/string.js +2 -0
- package/dist/helpers/string.js.map +1 -1
- package/dist/http/HttpContext.d.ts +30 -2
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +73 -3
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/Request.d.ts +42 -3
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +51 -7
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +82 -2
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +240 -13
- package/dist/http/Response.js.map +1 -1
- package/dist/http/SseStream.d.ts +10 -0
- package/dist/http/SseStream.d.ts.map +1 -1
- package/dist/http/SseStream.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/migrations/MigrationRegistry.d.ts +28 -0
- package/dist/migrations/MigrationRegistry.d.ts.map +1 -0
- package/dist/migrations/MigrationRegistry.js +41 -0
- package/dist/migrations/MigrationRegistry.js.map +1 -0
- package/dist/migrations/types.d.ts +64 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +15 -0
- package/dist/migrations/types.js.map +1 -0
- package/dist/router/Router.d.ts +112 -9
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +206 -28
- package/dist/router/Router.js.map +1 -1
- package/dist/security/CookieSigner.d.ts +43 -4
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +68 -10
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/session/Session.d.ts +46 -1
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +57 -4
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +19 -9
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/drivers/CookieDriver.d.ts +1 -1
- package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
- package/dist/session/drivers/CookieDriver.js +3 -3
- package/dist/session/drivers/CookieDriver.js.map +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
- package/dist/session/drivers/DatabaseDriver.js +2 -2
- package/dist/session/drivers/DatabaseDriver.js.map +1 -1
- package/dist/session/drivers/FileDriver.d.ts +1 -1
- package/dist/session/drivers/FileDriver.d.ts.map +1 -1
- package/dist/session/drivers/FileDriver.js +7 -5
- package/dist/session/drivers/FileDriver.js.map +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +1 -1
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts +1 -1
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +11 -4
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +5 -2
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Application.ts +169 -6
- package/src/HealthCheck.ts +27 -14
- package/src/HttpKernel.ts +27 -4
- package/src/Ignitor.ts +17 -0
- package/src/bodyparser/BodyParserMiddleware.ts +70 -36
- package/src/bodyparser/MultipartFile.ts +125 -8
- package/src/console/BaseCommand.ts +8 -1
- package/src/console/Kernel.ts +18 -0
- package/src/directories.ts +81 -0
- package/src/events/Emitter.ts +129 -13
- package/src/events/EventsBuffer.ts +131 -0
- package/src/events/index.ts +7 -0
- package/src/health/BaseCheck.ts +30 -0
- package/src/health/HealthChecks.ts +83 -0
- package/src/health/Result.ts +69 -0
- package/src/health/checks/DiskSpaceCheck.ts +69 -0
- package/src/health/checks/MemoryHeapCheck.ts +68 -0
- package/src/health/checks/MemoryRSSCheck.ts +68 -0
- package/src/health/checks/memory.ts +115 -0
- package/src/health/index.ts +13 -0
- package/src/health/tracing.ts +15 -0
- package/src/health/types.ts +51 -0
- package/src/helpers/bytes.ts +71 -0
- package/src/helpers/string.ts +2 -0
- package/src/http/HttpContext.ts +78 -4
- package/src/http/Request.ts +62 -10
- package/src/http/Response.ts +283 -14
- package/src/http/SseStream.ts +10 -0
- package/src/index.ts +27 -1
- package/src/migrations/MigrationRegistry.ts +49 -0
- package/src/migrations/types.ts +68 -0
- package/src/router/Router.ts +241 -32
- package/src/security/CookieSigner.ts +92 -18
- package/src/session/Session.ts +71 -5
- package/src/session/SessionMiddleware.ts +19 -9
- package/src/session/drivers/CookieDriver.ts +4 -4
- package/src/session/drivers/DatabaseDriver.ts +3 -3
- package/src/session/drivers/FileDriver.ts +8 -6
- package/src/session/drivers/MemoryDriver.ts +2 -2
- package/src/session/drivers/RedisDriver.ts +12 -5
- package/tsconfig.app.json +13 -13
- package/tsconfig.base.json +19 -0
- package/tsconfig.client.json +16 -0
- package/tsconfig.package.json +15 -0
package/src/router/Router.ts
CHANGED
|
@@ -57,8 +57,24 @@ export type LazyControllerAction = [loader: LazyControllerImport, method: string
|
|
|
57
57
|
*/
|
|
58
58
|
export type RouteHandler = RouteHandlerFunction | ControllerAction | LazyControllerAction | string
|
|
59
59
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Param matcher — a bare RegExp, or the AdonisJS `{ match, cast }` shape.
|
|
62
|
+
*
|
|
63
|
+
* `cast` turns the captured string into the value the handler wants, so
|
|
64
|
+
* `where('id', matchers.number())` gives `params.id` as a NUMBER. Ream carried
|
|
65
|
+
* only the pattern, so a matched param stayed a string and every controller
|
|
66
|
+
* re-parsed it. `pattern` is ream's older spelling of `match`, still accepted.
|
|
67
|
+
*/
|
|
68
|
+
export type ParamMatcher =
|
|
69
|
+
| RegExp
|
|
70
|
+
| {
|
|
71
|
+
/** AdonisJS spelling. */
|
|
72
|
+
match?: RegExp
|
|
73
|
+
/** Ream's older spelling of {@link match}. */
|
|
74
|
+
pattern?: RegExp
|
|
75
|
+
/** Casts the captured segment before it reaches `ctx.params`. */
|
|
76
|
+
cast?: (value: string) => unknown
|
|
77
|
+
}
|
|
62
78
|
|
|
63
79
|
export interface RouteDefinition {
|
|
64
80
|
method: string
|
|
@@ -166,17 +182,22 @@ export interface MatchResult {
|
|
|
166
182
|
// ─── Matchers ───────────────────────────────────────────────
|
|
167
183
|
|
|
168
184
|
export const matchers = {
|
|
169
|
-
/** Match numeric params only. */
|
|
185
|
+
/** Match numeric params only, and hand the handler a number. */
|
|
170
186
|
number(): ParamMatcher {
|
|
171
|
-
return {
|
|
187
|
+
return { match: /^\d+$/, cast: (value) => Number(value) }
|
|
172
188
|
},
|
|
173
189
|
/** Match UUID v4 params only. */
|
|
174
190
|
uuid(): ParamMatcher {
|
|
175
|
-
return {
|
|
191
|
+
return {
|
|
192
|
+
match: /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
193
|
+
// AdonisJS lowercases it, so a route keyed on the param is stable
|
|
194
|
+
// whatever case the client sent.
|
|
195
|
+
cast: (value) => value.toLowerCase(),
|
|
196
|
+
}
|
|
176
197
|
},
|
|
177
198
|
/** Match slug params only (lowercase alphanumeric + hyphens). */
|
|
178
199
|
slug(): ParamMatcher {
|
|
179
|
-
return {
|
|
200
|
+
return { match: /^[a-z0-9]+(?:-[a-z0-9]+)*$/ }
|
|
180
201
|
},
|
|
181
202
|
}
|
|
182
203
|
|
|
@@ -190,15 +211,43 @@ export const matchers = {
|
|
|
190
211
|
* anchoring here reproduces AdonisJS's *effective* full-segment semantics.
|
|
191
212
|
*/
|
|
192
213
|
function compileMatcher(matcher: ParamMatcher | RegExp | string): ParamMatcher {
|
|
193
|
-
if (typeof matcher === 'string') return {
|
|
194
|
-
return matcher instanceof RegExp ? {
|
|
214
|
+
if (typeof matcher === 'string') return { match: new RegExp(`^(?:${matcher})$`) }
|
|
215
|
+
return matcher instanceof RegExp ? { match: matcher } : matcher
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Run each matcher's `cast` over the params it matched.
|
|
220
|
+
*
|
|
221
|
+
* The wildcard holds several segments, so the cast applies to each of them.
|
|
222
|
+
* A matcher without a cast leaves its param untouched.
|
|
223
|
+
*/
|
|
224
|
+
function applyCasts(params: MatchedParams, matchers: Record<string, ParamMatcher>): MatchedParams {
|
|
225
|
+
let out: MatchedParams | undefined
|
|
226
|
+
for (const [param, value] of Object.entries(params)) {
|
|
227
|
+
const matcher = matchers[param]
|
|
228
|
+
const cast = matcher instanceof RegExp ? undefined : matcher?.cast
|
|
229
|
+
if (!cast) continue
|
|
230
|
+
out ??= { ...params }
|
|
231
|
+
// The cast's return is the caller's claim about its own param; the router
|
|
232
|
+
// only carries it.
|
|
233
|
+
out[param] = (
|
|
234
|
+
Array.isArray(value) ? value.map((v) => cast(v)) : cast(value)
|
|
235
|
+
) as MatchedParams[string]
|
|
236
|
+
}
|
|
237
|
+
return out ?? params
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The RegExp a matcher tests with, whichever spelling it used. */
|
|
241
|
+
function matcherPattern(matcher: ParamMatcher): RegExp | undefined {
|
|
242
|
+
if (matcher instanceof RegExp) return matcher
|
|
243
|
+
return matcher.match ?? matcher.pattern
|
|
195
244
|
}
|
|
196
245
|
|
|
197
246
|
// ─── RouteBuilder ───────────────────────────────────────────
|
|
198
247
|
|
|
199
248
|
/**
|
|
200
249
|
* Fluent route builder — mutates the already-registered route definition(s).
|
|
201
|
-
* Backed by an array so a multi-verb `router.route(['GET','POST'], …)` returns
|
|
250
|
+
* Backed by an array so a multi-verb `router.route(pattern, ['GET','POST'], …)` returns
|
|
202
251
|
* ONE builder that fans every mutation across all its verbs; the single-verb
|
|
203
252
|
* helpers (`get`/`post`/…) back it with a one-element array.
|
|
204
253
|
*/
|
|
@@ -808,14 +857,16 @@ export class Router extends Macroable {
|
|
|
808
857
|
// ─── Route registration ───────────────────────────────────
|
|
809
858
|
|
|
810
859
|
/**
|
|
811
|
-
* Register a route for one verb or several
|
|
812
|
-
*
|
|
813
|
-
*
|
|
860
|
+
* Register a route for one verb or several — `route(pattern, methods,
|
|
861
|
+
* handler)`, the AdonisJS argument order. Ream took `(method, path)`, so a
|
|
862
|
+
* call copied from an AdonisJS app registered the pattern as the verb.
|
|
863
|
+
*
|
|
864
|
+
* A multi-verb call returns a single builder that configures every verb.
|
|
814
865
|
*/
|
|
815
|
-
route(
|
|
816
|
-
const
|
|
817
|
-
const defs =
|
|
818
|
-
const def = this.#createDefinition(verb,
|
|
866
|
+
route(pattern: string, methods: string | string[], handler: RouteHandler): RouteBuilder {
|
|
867
|
+
const verbs = Array.isArray(methods) ? methods : [methods]
|
|
868
|
+
const defs = verbs.map((verb) => {
|
|
869
|
+
const def = this.#createDefinition(verb, pattern, handler)
|
|
819
870
|
this.#routes.push(def)
|
|
820
871
|
return def
|
|
821
872
|
})
|
|
@@ -824,36 +875,36 @@ export class Router extends Macroable {
|
|
|
824
875
|
}
|
|
825
876
|
|
|
826
877
|
get(path: string, handler: RouteHandler): RouteBuilder {
|
|
827
|
-
return this.route('GET',
|
|
878
|
+
return this.route(path, 'GET', handler)
|
|
828
879
|
}
|
|
829
880
|
|
|
830
881
|
post(path: string, handler: RouteHandler): RouteBuilder {
|
|
831
|
-
return this.route('POST',
|
|
882
|
+
return this.route(path, 'POST', handler)
|
|
832
883
|
}
|
|
833
884
|
|
|
834
885
|
put(path: string, handler: RouteHandler): RouteBuilder {
|
|
835
|
-
return this.route('PUT',
|
|
886
|
+
return this.route(path, 'PUT', handler)
|
|
836
887
|
}
|
|
837
888
|
|
|
838
889
|
patch(path: string, handler: RouteHandler): RouteBuilder {
|
|
839
|
-
return this.route('PATCH',
|
|
890
|
+
return this.route(path, 'PATCH', handler)
|
|
840
891
|
}
|
|
841
892
|
|
|
842
893
|
delete(path: string, handler: RouteHandler): RouteBuilder {
|
|
843
|
-
return this.route('DELETE',
|
|
894
|
+
return this.route(path, 'DELETE', handler)
|
|
844
895
|
}
|
|
845
896
|
|
|
846
897
|
head(path: string, handler: RouteHandler): RouteBuilder {
|
|
847
|
-
return this.route('HEAD',
|
|
898
|
+
return this.route(path, 'HEAD', handler)
|
|
848
899
|
}
|
|
849
900
|
|
|
850
901
|
options(path: string, handler: RouteHandler): RouteBuilder {
|
|
851
|
-
return this.route('OPTIONS',
|
|
902
|
+
return this.route(path, 'OPTIONS', handler)
|
|
852
903
|
}
|
|
853
904
|
|
|
854
905
|
/** Register a route for all HTTP methods. */
|
|
855
906
|
any(path: string, handler: RouteHandler): RouteBuilder {
|
|
856
|
-
return this.route('*',
|
|
907
|
+
return this.route(path, '*', handler)
|
|
857
908
|
}
|
|
858
909
|
|
|
859
910
|
// ─── Resource routes ──────────────────────────────────────
|
|
@@ -1096,16 +1147,16 @@ export class Router extends Macroable {
|
|
|
1096
1147
|
// separate gates meant the global always had the final say, so a route
|
|
1097
1148
|
// could never loosen it — a global `id: /^[0-9]+$/` plus a route-level
|
|
1098
1149
|
// slug matcher 404'd instead of matching.
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
...this.#globalMatchers,
|
|
1102
|
-
...route.matchers,
|
|
1103
|
-
})
|
|
1104
|
-
) {
|
|
1150
|
+
const effective = { ...this.#globalMatchers, ...route.matchers }
|
|
1151
|
+
if (!this.#validateMatchers(params, effective)) {
|
|
1105
1152
|
continue
|
|
1106
1153
|
}
|
|
1107
1154
|
|
|
1108
|
-
|
|
1155
|
+
// A matcher's `cast` turns the captured segment into the value the
|
|
1156
|
+
// handler wants — `where('id', matchers.number())` hands `params.id`
|
|
1157
|
+
// a NUMBER, as in AdonisJS. Ream carried only the pattern, so every
|
|
1158
|
+
// controller re-parsed a string the router had already validated.
|
|
1159
|
+
return { route, params: applyCasts(params, effective) }
|
|
1109
1160
|
}
|
|
1110
1161
|
}
|
|
1111
1162
|
|
|
@@ -1270,6 +1321,96 @@ export class Router extends Macroable {
|
|
|
1270
1321
|
return this.#routes.filter((route) => !route.deleted)
|
|
1271
1322
|
}
|
|
1272
1323
|
|
|
1324
|
+
/**
|
|
1325
|
+
* A fluent URL builder (AdonisJS `router.builder()` / `urlBuilder`).
|
|
1326
|
+
*
|
|
1327
|
+
* `urlFor` takes everything at once; this is the form upstream gives you
|
|
1328
|
+
* when the pieces arrive separately — params here, query string there, a
|
|
1329
|
+
* prefix from config:
|
|
1330
|
+
*
|
|
1331
|
+
* ```ts
|
|
1332
|
+
* router.builder().params({ id: '1' }).qs({ tab: 'posts' }).make('users.show')
|
|
1333
|
+
* router.builder().prefixUrl('https://acme.test').makeSigned('invite', { expiresIn: '1h' })
|
|
1334
|
+
* ```
|
|
1335
|
+
*/
|
|
1336
|
+
builder(): UrlBuilder {
|
|
1337
|
+
return new UrlBuilder(this)
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/** AdonisJS' property name for {@link builder}. */
|
|
1341
|
+
get urlBuilder(): UrlBuilder {
|
|
1342
|
+
return this.builder()
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* A builder that prefixes every URL with `domain` (AdonisJS
|
|
1347
|
+
* `builderForDomain`).
|
|
1348
|
+
*
|
|
1349
|
+
* The domain is a prefix here, not a lookup key: routes are matched by host
|
|
1350
|
+
* at request time, and a named route is unique across the table.
|
|
1351
|
+
*/
|
|
1352
|
+
builderForDomain(domain: string): UrlBuilder {
|
|
1353
|
+
return this.builder().prefixUrl(domain)
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* The parameter names a route pattern declares, in order (AdonisJS
|
|
1358
|
+
* `parsePattern`).
|
|
1359
|
+
*
|
|
1360
|
+
* `:id` and `:slug?` both count; the optional marker is reported rather than
|
|
1361
|
+
* being part of the name.
|
|
1362
|
+
*/
|
|
1363
|
+
parsePattern(pattern: string): Array<{ name: string; optional: boolean }> {
|
|
1364
|
+
const tokens: Array<{ name: string; optional: boolean }> = []
|
|
1365
|
+
for (const match of pattern.matchAll(/:([A-Za-z_][\w]*)(\?)?/g)) {
|
|
1366
|
+
tokens.push({ name: match[1] ?? '', optional: match[2] === '?' })
|
|
1367
|
+
}
|
|
1368
|
+
return tokens
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* Every route, grouped by the domain it answers on (AdonisJS `toJSON`).
|
|
1373
|
+
*
|
|
1374
|
+
* Routes registered without a domain land under `root`, as upstream keys
|
|
1375
|
+
* them. This is what a route list — `ream inspect`, an OpenAPI generator, a
|
|
1376
|
+
* debug screen — reads.
|
|
1377
|
+
*/
|
|
1378
|
+
toJSON(): Record<string, RouteDefinition[]> {
|
|
1379
|
+
const byDomain: Record<string, RouteDefinition[]> = {}
|
|
1380
|
+
for (const route of this.getRoutes()) {
|
|
1381
|
+
const key = route.domain ?? 'root'
|
|
1382
|
+
const bucket = byDomain[key] ?? []
|
|
1383
|
+
bucket.push(route)
|
|
1384
|
+
byDomain[key] = bucket
|
|
1385
|
+
}
|
|
1386
|
+
return byDomain
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Whether any route is scoped to a domain (AdonisJS `usingDomains`).
|
|
1391
|
+
*
|
|
1392
|
+
* Lets the matcher skip host resolution entirely when nothing asked for it.
|
|
1393
|
+
*/
|
|
1394
|
+
get usingDomains(): boolean {
|
|
1395
|
+
return this.getRoutes().some((route) => route.domain !== undefined)
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Freeze the route table and build the lookup index (AdonisJS `commit`).
|
|
1400
|
+
*
|
|
1401
|
+
* The index is built lazily on the first `match()`, so this is only needed
|
|
1402
|
+
* to pay that cost up front — at the end of boot rather than inside the
|
|
1403
|
+
* first request.
|
|
1404
|
+
*/
|
|
1405
|
+
commit(): void {
|
|
1406
|
+
this.#buildIndex()
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/** Whether the lookup index has been built (AdonisJS `commited`). */
|
|
1410
|
+
get commited(): boolean {
|
|
1411
|
+
return !this.#indexDirty
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1273
1414
|
/**
|
|
1274
1415
|
* Clear all registered routes AND registries (used by hot-reload). Router
|
|
1275
1416
|
* middleware, named middleware, and global matchers are reset too — the
|
|
@@ -1302,7 +1443,8 @@ export class Router extends Macroable {
|
|
|
1302
1443
|
// The wildcard holds several segments; every one has to satisfy the
|
|
1303
1444
|
// matcher, or a `/assets/*` guard would pass on its first segment alone.
|
|
1304
1445
|
const values = Array.isArray(value) ? value : [value]
|
|
1305
|
-
const regex = matcher
|
|
1446
|
+
const regex = matcherPattern(matcher)
|
|
1447
|
+
if (!regex) continue
|
|
1306
1448
|
// A user-supplied /g or /y regex is stateful (`lastIndex` advances on
|
|
1307
1449
|
// match) — without a reset, the same URL alternates match/no-match
|
|
1308
1450
|
// across requests. Reset before every test.
|
|
@@ -1387,3 +1529,70 @@ function matchDomain(pattern: string, host: string): boolean {
|
|
|
1387
1529
|
}
|
|
1388
1530
|
return false
|
|
1389
1531
|
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* Builds a URL for a named route, one piece at a time (AdonisJS `UrlBuilder`).
|
|
1535
|
+
*
|
|
1536
|
+
* Every method returns the builder, and `make` / `makeSigned` are the two ways
|
|
1537
|
+
* out. It defers to {@link Router.urlFor} and {@link Router.makeSignedUrl}, so
|
|
1538
|
+
* a missing param fails the same way whichever spelling you used.
|
|
1539
|
+
*/
|
|
1540
|
+
export class UrlBuilder {
|
|
1541
|
+
readonly #router: Router
|
|
1542
|
+
#params: Record<string, string> = {}
|
|
1543
|
+
#qs: Record<string, string> = {}
|
|
1544
|
+
#prefix = ''
|
|
1545
|
+
#lookup = true
|
|
1546
|
+
|
|
1547
|
+
constructor(router: Router) {
|
|
1548
|
+
this.#router = router
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/** Route params, by name. */
|
|
1552
|
+
params(params: Record<string, string>): this {
|
|
1553
|
+
this.#params = { ...this.#params, ...params }
|
|
1554
|
+
return this
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/** Query-string params appended to the built URL. */
|
|
1558
|
+
qs(qs: Record<string, string>): this {
|
|
1559
|
+
this.#qs = { ...this.#qs, ...qs }
|
|
1560
|
+
return this
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/** A prefix — a domain, a base path — put in front of the result. */
|
|
1564
|
+
prefixUrl(prefix: string): this {
|
|
1565
|
+
this.#prefix = prefix.replace(/\/$/, '')
|
|
1566
|
+
return this
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* Treat what `make()` receives as a literal path instead of a route name
|
|
1571
|
+
* (AdonisJS `disableRouteLookup`).
|
|
1572
|
+
*/
|
|
1573
|
+
disableRouteLookup(): this {
|
|
1574
|
+
this.#lookup = false
|
|
1575
|
+
return this
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/** The URL. */
|
|
1579
|
+
make(identifier: string): string {
|
|
1580
|
+
const path = this.#lookup ? this.#router.urlFor(identifier, this.#params) : identifier
|
|
1581
|
+
const query = new URLSearchParams(this.#qs).toString()
|
|
1582
|
+
return `${this.#prefix}${path}${query ? `?${query}` : ''}`
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* The URL with an HMAC signature (AdonisJS `makeSigned`).
|
|
1587
|
+
*
|
|
1588
|
+
* The query string is folded into the signature, so a link cannot be
|
|
1589
|
+
* re-pointed by editing it.
|
|
1590
|
+
*/
|
|
1591
|
+
makeSigned(identifier: string, options?: SignedUrlOptions): string {
|
|
1592
|
+
const signed = this.#router.makeSignedUrl(identifier, this.#params, {
|
|
1593
|
+
...options,
|
|
1594
|
+
qs: { ...this.#qs, ...options?.qs },
|
|
1595
|
+
})
|
|
1596
|
+
return `${this.#prefix}${signed}`
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
@@ -40,8 +40,12 @@ export class E_INSECURE_APP_KEY extends Error {
|
|
|
40
40
|
|
|
41
41
|
/** The envelope carried inside an encrypted or signed value. */
|
|
42
42
|
interface Envelope {
|
|
43
|
-
/**
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The payload. Any JSON value, not just a string: AdonisJS' verifier signs
|
|
45
|
+
* whatever you hand it, and a caller signing `{ id: 1 }` should not have to
|
|
46
|
+
* stringify it first.
|
|
47
|
+
*/
|
|
48
|
+
m: unknown
|
|
45
49
|
/** Epoch-ms expiry, when one was given. */
|
|
46
50
|
e?: number
|
|
47
51
|
/** The purpose it was sealed for, when one was given. */
|
|
@@ -69,25 +73,36 @@ export class CookieSigner {
|
|
|
69
73
|
* another — a password-reset token presented as a session cookie fails here
|
|
70
74
|
* rather than being honoured.
|
|
71
75
|
*/
|
|
72
|
-
#seal(value:
|
|
76
|
+
#seal(value: unknown, expiresIn?: number, purpose?: string): string {
|
|
73
77
|
const envelope: Envelope = { m: value }
|
|
74
78
|
if (expiresIn !== undefined) envelope.e = Date.now() + expiresIn
|
|
75
79
|
if (purpose !== undefined) envelope.p = purpose
|
|
76
80
|
return JSON.stringify(envelope)
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Unwrap a sealed payload, refusing an expired one or the wrong purpose.
|
|
85
|
+
*
|
|
86
|
+
* Anything that is not an envelope is refused rather than handed back.
|
|
87
|
+
* `#seal` only ever produces one, so a non-envelope did not come from this
|
|
88
|
+
* class — and returning it would skip the expiry and purpose checks below,
|
|
89
|
+
* which is precisely the downgrade those checks exist to prevent. Failing
|
|
90
|
+
* closed costs a value nothing here produced; failing open costs the
|
|
91
|
+
* guarantee that a token sealed for one use cannot be replayed into another.
|
|
92
|
+
*/
|
|
93
|
+
#open(raw: string, purpose?: string): unknown {
|
|
94
|
+
let parsed: unknown
|
|
82
95
|
try {
|
|
83
|
-
|
|
84
|
-
if (typeof parsed !== 'object' || parsed === null) return null
|
|
85
|
-
envelope = parsed as Envelope
|
|
96
|
+
parsed = JSON.parse(raw)
|
|
86
97
|
} catch {
|
|
87
|
-
|
|
88
|
-
return raw
|
|
98
|
+
return null
|
|
89
99
|
}
|
|
90
|
-
if (typeof
|
|
100
|
+
if (typeof parsed !== 'object' || parsed === null) return null
|
|
101
|
+
const envelope = parsed as Envelope
|
|
102
|
+
// The KEY must be there, whatever it holds — that is what distinguishes an
|
|
103
|
+
// envelope from an arbitrary JSON document, and a signed `null` or `0` is a
|
|
104
|
+
// legitimate payload.
|
|
105
|
+
if (!('m' in envelope)) return null
|
|
91
106
|
if (envelope.e !== undefined && envelope.e < Date.now()) return null
|
|
92
107
|
if ((envelope.p ?? undefined) !== purpose) return null
|
|
93
108
|
return envelope.m
|
|
@@ -99,27 +114,86 @@ export class CookieSigner {
|
|
|
99
114
|
* `expiresIn` is in milliseconds; `purpose` scopes the signature so it cannot
|
|
100
115
|
* be replayed somewhere else.
|
|
101
116
|
*/
|
|
102
|
-
sign(value:
|
|
117
|
+
sign(value: unknown, expiresIn?: number, purpose?: string): string {
|
|
103
118
|
const sealed = Buffer.from(this.#seal(value, expiresIn, purpose)).toString('base64url')
|
|
104
119
|
const sig = hmacSign(sealed, this.#secret)
|
|
105
120
|
return `${sealed}.${sig}`
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
/** Verify and unwrap, returning null when tampered, expired, or mis-purposed. */
|
|
109
|
-
unsign(signed: string, purpose?: string):
|
|
124
|
+
unsign<T = string>(signed: string, purpose?: string): T | null {
|
|
110
125
|
const lastDot = signed.lastIndexOf('.')
|
|
111
126
|
if (lastDot === -1) return null
|
|
112
127
|
const sealed = signed.slice(0, lastDot)
|
|
113
128
|
const sig = signed.slice(lastDot + 1)
|
|
114
129
|
if (!hmacVerify(sealed, sig, this.#secret)) return null
|
|
115
130
|
try {
|
|
116
|
-
return this.#open(Buffer.from(sealed, 'base64url').toString('utf8'), purpose)
|
|
131
|
+
return this.#open(Buffer.from(sealed, 'base64url').toString('utf8'), purpose) as T | null
|
|
117
132
|
} catch {
|
|
118
133
|
return null
|
|
119
134
|
}
|
|
120
135
|
}
|
|
121
136
|
|
|
122
|
-
|
|
137
|
+
/**
|
|
138
|
+
* The cipher in use (AdonisJS `Encryption.algorithm`).
|
|
139
|
+
*
|
|
140
|
+
* `aes-256-gcm`, not upstream's `aes-256-cbc` — see the deviation at the top
|
|
141
|
+
* of this file.
|
|
142
|
+
*/
|
|
143
|
+
get algorithm(): 'aes-256-gcm' {
|
|
144
|
+
return 'aes-256-gcm'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* base64url helpers (AdonisJS `Encryption.base64`).
|
|
149
|
+
*
|
|
150
|
+
* url-safe and unpadded, which is what every value this class produces uses
|
|
151
|
+
* — a signed or encrypted payload travels in a cookie or a URL, where `+`,
|
|
152
|
+
* `/` and `=` do not survive. `decode` answers `null` on anything that does
|
|
153
|
+
* not round-trip rather than handing back mangled bytes.
|
|
154
|
+
*/
|
|
155
|
+
get base64(): {
|
|
156
|
+
encode(value: string): string
|
|
157
|
+
decode(value: string): string | null
|
|
158
|
+
urlEncode(value: string): string
|
|
159
|
+
urlDecode(value: string): string | null
|
|
160
|
+
} {
|
|
161
|
+
const encode = (value: string) => Buffer.from(value, 'utf8').toString('base64url')
|
|
162
|
+
const decode = (value: string): string | null => {
|
|
163
|
+
const out = Buffer.from(value, 'base64url').toString('utf8')
|
|
164
|
+
return Buffer.from(out, 'utf8').toString('base64url') === value ? out : null
|
|
165
|
+
}
|
|
166
|
+
return { encode, decode, urlEncode: encode, urlDecode: decode }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Sign and verify WITHOUT encrypting (AdonisJS `Encryption.verifier`).
|
|
171
|
+
*
|
|
172
|
+
* The same `sign` / `unsign` this class already exposes, under the name and
|
|
173
|
+
* shape upstream puts them: an app calling `encryption.verifier.sign(...)`
|
|
174
|
+
* found nothing here.
|
|
175
|
+
*/
|
|
176
|
+
get verifier(): {
|
|
177
|
+
sign(payload: unknown, expiresIn?: number, purpose?: string): string
|
|
178
|
+
unsign<T = string>(payload: string, purpose?: string): T | null
|
|
179
|
+
} {
|
|
180
|
+
return {
|
|
181
|
+
sign: (payload, expiresIn, purpose) => this.sign(payload, expiresIn, purpose),
|
|
182
|
+
unsign: <T>(payload: string, purpose?: string) => this.unsign<T>(payload, purpose),
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Another signer on a different secret (AdonisJS `Encryption.child`).
|
|
188
|
+
*
|
|
189
|
+
* What key rotation is built on: keep the old secret's signer around to read
|
|
190
|
+
* values already in the wild while the new one writes.
|
|
191
|
+
*/
|
|
192
|
+
child(secret?: string): CookieSigner {
|
|
193
|
+
return new CookieSigner(secret ?? this.#secret)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
encrypt(value: unknown, expiresIn?: number, purpose?: string): string {
|
|
123
197
|
const iv = randomBytes(12)
|
|
124
198
|
const cipher = createCipheriv('aes-256-gcm', this.#keyBuffer, iv)
|
|
125
199
|
const sealed = this.#seal(value, expiresIn, purpose)
|
|
@@ -128,7 +202,7 @@ export class CookieSigner {
|
|
|
128
202
|
return `${iv.toString('base64url')}.${encrypted.toString('base64url')}.${tag.toString('base64url')}`
|
|
129
203
|
}
|
|
130
204
|
|
|
131
|
-
decrypt(encrypted: string, purpose?: string):
|
|
205
|
+
decrypt<T = string>(encrypted: string, purpose?: string): T | null {
|
|
132
206
|
const parts = encrypted.split('.')
|
|
133
207
|
if (parts.length !== 3) return null
|
|
134
208
|
try {
|
|
@@ -138,7 +212,7 @@ export class CookieSigner {
|
|
|
138
212
|
const decipher = createDecipheriv('aes-256-gcm', this.#keyBuffer, iv)
|
|
139
213
|
decipher.setAuthTag(tag)
|
|
140
214
|
const raw = Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8')
|
|
141
|
-
return this.#open(raw, purpose)
|
|
215
|
+
return this.#open(raw, purpose) as T | null
|
|
142
216
|
} catch {
|
|
143
217
|
return null
|
|
144
218
|
}
|
package/src/session/Session.ts
CHANGED
|
@@ -13,7 +13,16 @@
|
|
|
13
13
|
import type { SessionRedisClientSource } from './drivers/RedisDriver.js'
|
|
14
14
|
|
|
15
15
|
export interface SessionDriver {
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The stored session, or `null` when the store has no entry for this id
|
|
18
|
+
* (AdonisJS `SessionStoreContract.read`).
|
|
19
|
+
*
|
|
20
|
+
* `null` and `{}` mean different things: the first is "no session here",
|
|
21
|
+
* the second "a session that happens to be empty". Without the difference a
|
|
22
|
+
* cookie whose server-side row has expired looked like a live session, and
|
|
23
|
+
* `commit()` touched a row that was not there.
|
|
24
|
+
*/
|
|
25
|
+
read(sessionId: string): Promise<Record<string, unknown> | null>
|
|
17
26
|
write(sessionId: string, data: Record<string, unknown>, ttl: number): Promise<void>
|
|
18
27
|
destroy(sessionId: string): Promise<void>
|
|
19
28
|
touch(sessionId: string, ttl: number): Promise<void>
|
|
@@ -51,6 +60,14 @@ export function supportsTagging(driver: SessionDriver): driver is SessionDriverW
|
|
|
51
60
|
)
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The session key the flash bag travels in.
|
|
65
|
+
*
|
|
66
|
+
* One constant rather than three literals: the read, the delete and the write
|
|
67
|
+
* below all have to agree, and they had to agree by hand.
|
|
68
|
+
*/
|
|
69
|
+
const FLASH_KEY = '__flash'
|
|
70
|
+
|
|
54
71
|
/** Where `flashAll` / `flashOnly` / `flashExcept` put the request input. */
|
|
55
72
|
const FLASH_INPUT_KEY = 'input'
|
|
56
73
|
|
|
@@ -175,9 +192,9 @@ export class Session {
|
|
|
175
192
|
*/
|
|
176
193
|
#hydrate(data: Record<string, unknown>): void {
|
|
177
194
|
this.#data = { ...data }
|
|
178
|
-
const flash = data
|
|
195
|
+
const flash = data[FLASH_KEY]
|
|
179
196
|
this.#previousFlash = isPlainRecord(flash) ? { ...flash } : {}
|
|
180
|
-
delete this.#data
|
|
197
|
+
delete this.#data[FLASH_KEY]
|
|
181
198
|
}
|
|
182
199
|
|
|
183
200
|
/** Current session id. Mutated by `regenerate()`. */
|
|
@@ -437,7 +454,7 @@ export class Session {
|
|
|
437
454
|
toJSON(): Record<string, unknown> {
|
|
438
455
|
const result = { ...this.#data }
|
|
439
456
|
if (Object.keys(this.#flashData).length > 0) {
|
|
440
|
-
result
|
|
457
|
+
result[FLASH_KEY] = this.#flashData
|
|
441
458
|
}
|
|
442
459
|
return result
|
|
443
460
|
}
|
|
@@ -472,6 +489,51 @@ export class Session {
|
|
|
472
489
|
* has no read-only session mode, and reporting `true` would tell a caller
|
|
473
490
|
* its writes were dropped when they were not.
|
|
474
491
|
*/
|
|
492
|
+
/**
|
|
493
|
+
* The options this session was seated with (AdonisJS `config`).
|
|
494
|
+
*
|
|
495
|
+
* What `setStore` was told: whether the session is fresh, and how long it
|
|
496
|
+
* lives. Read-only — changing it after the fact would not move the entry
|
|
497
|
+
* already written.
|
|
498
|
+
*/
|
|
499
|
+
get config(): { fresh: boolean; ttl: number } {
|
|
500
|
+
return { fresh: this.#fresh, ttl: this.#ttl }
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* The key the flash bag is stored under inside the session (AdonisJS
|
|
505
|
+
* `flashKey`).
|
|
506
|
+
*
|
|
507
|
+
* Exposed so a store that has to reason about the payload — a migration, a
|
|
508
|
+
* debug screen — does not have to hardcode the string.
|
|
509
|
+
*/
|
|
510
|
+
get flashKey(): string {
|
|
511
|
+
return FLASH_KEY
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* What has been flashed for the NEXT request (AdonisJS
|
|
516
|
+
* `responseFlashMessages`).
|
|
517
|
+
*
|
|
518
|
+
* A copy: flashing goes through `flash()`, which is where the value checks
|
|
519
|
+
* live. `flashMessages` is the other half — what the PREVIOUS request left
|
|
520
|
+
* for this one.
|
|
521
|
+
*/
|
|
522
|
+
get responseFlashMessages(): Record<string, unknown> {
|
|
523
|
+
return { ...this.#flashData }
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Whether `regenerate()` ran this request (AdonisJS `hasRegeneratedSession`).
|
|
528
|
+
*
|
|
529
|
+
* The id changed, so anything holding the old one — a cookie already
|
|
530
|
+
* written, an external store keyed by session id — is stale. It was tracked
|
|
531
|
+
* internally and never exposed, so only this class could act on it.
|
|
532
|
+
*/
|
|
533
|
+
get hasRegeneratedSession(): boolean {
|
|
534
|
+
return this.#regenerated
|
|
535
|
+
}
|
|
536
|
+
|
|
475
537
|
get readonly(): boolean {
|
|
476
538
|
return false
|
|
477
539
|
}
|
|
@@ -496,7 +558,11 @@ export class Session {
|
|
|
496
558
|
async initiate(): Promise<void> {
|
|
497
559
|
if (this.#initiated || this.#driver === undefined) return
|
|
498
560
|
this.#initiated = true
|
|
499
|
-
|
|
561
|
+
const stored = await this.#driver.read(this.#sessionId)
|
|
562
|
+
// The store is the authority on existence: a cookie can outlive the row it
|
|
563
|
+
// points at, and that session is fresh however old its id is.
|
|
564
|
+
if (stored === null) this.#fresh = true
|
|
565
|
+
this.#hydrate(stored ?? {})
|
|
500
566
|
}
|
|
501
567
|
|
|
502
568
|
/**
|