@c9up/ream 0.1.34 → 0.2.1

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.
Files changed (210) hide show
  1. package/dist/Application.d.ts +68 -0
  2. package/dist/Application.d.ts.map +1 -1
  3. package/dist/Application.js +142 -6
  4. package/dist/Application.js.map +1 -1
  5. package/dist/HealthCheck.d.ts +14 -7
  6. package/dist/HealthCheck.d.ts.map +1 -1
  7. package/dist/HealthCheck.js +21 -12
  8. package/dist/HealthCheck.js.map +1 -1
  9. package/dist/HttpKernel.d.ts +12 -0
  10. package/dist/HttpKernel.d.ts.map +1 -1
  11. package/dist/HttpKernel.js +15 -4
  12. package/dist/HttpKernel.js.map +1 -1
  13. package/dist/Ignitor.d.ts +8 -8
  14. package/dist/Ignitor.d.ts.map +1 -1
  15. package/dist/Ignitor.js +11 -0
  16. package/dist/Ignitor.js.map +1 -1
  17. package/dist/bodyparser/BodyParserMiddleware.d.ts +13 -2
  18. package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
  19. package/dist/bodyparser/BodyParserMiddleware.js +50 -30
  20. package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
  21. package/dist/bodyparser/MultipartFile.d.ts +75 -6
  22. package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
  23. package/dist/bodyparser/MultipartFile.js +128 -8
  24. package/dist/bodyparser/MultipartFile.js.map +1 -1
  25. package/dist/console/BaseCommand.d.ts +8 -1
  26. package/dist/console/BaseCommand.d.ts.map +1 -1
  27. package/dist/console/BaseCommand.js +1 -1
  28. package/dist/console/BaseCommand.js.map +1 -1
  29. package/dist/console/Kernel.d.ts +15 -0
  30. package/dist/console/Kernel.d.ts.map +1 -1
  31. package/dist/console/Kernel.js +17 -0
  32. package/dist/console/Kernel.js.map +1 -1
  33. package/dist/directories.d.ts +52 -0
  34. package/dist/directories.d.ts.map +1 -0
  35. package/dist/directories.js +38 -0
  36. package/dist/directories.js.map +1 -0
  37. package/dist/events/Emitter.d.ts +49 -6
  38. package/dist/events/Emitter.d.ts.map +1 -1
  39. package/dist/events/Emitter.js +117 -7
  40. package/dist/events/Emitter.js.map +1 -1
  41. package/dist/events/EventsBuffer.d.ts +47 -0
  42. package/dist/events/EventsBuffer.d.ts.map +1 -0
  43. package/dist/events/EventsBuffer.js +107 -0
  44. package/dist/events/EventsBuffer.js.map +1 -0
  45. package/dist/events/index.d.ts +2 -0
  46. package/dist/events/index.d.ts.map +1 -1
  47. package/dist/events/index.js +1 -0
  48. package/dist/events/index.js.map +1 -1
  49. package/dist/health/BaseCheck.d.ts +19 -0
  50. package/dist/health/BaseCheck.d.ts.map +1 -0
  51. package/dist/health/BaseCheck.js +24 -0
  52. package/dist/health/BaseCheck.js.map +1 -0
  53. package/dist/health/HealthChecks.d.ts +15 -0
  54. package/dist/health/HealthChecks.d.ts.map +1 -0
  55. package/dist/health/HealthChecks.js +69 -0
  56. package/dist/health/HealthChecks.js.map +1 -0
  57. package/dist/health/Result.d.ts +27 -0
  58. package/dist/health/Result.d.ts.map +1 -0
  59. package/dist/health/Result.js +55 -0
  60. package/dist/health/Result.js.map +1 -0
  61. package/dist/health/checks/DiskSpaceCheck.d.ts +29 -0
  62. package/dist/health/checks/DiskSpaceCheck.d.ts.map +1 -0
  63. package/dist/health/checks/DiskSpaceCheck.js +58 -0
  64. package/dist/health/checks/DiskSpaceCheck.js.map +1 -0
  65. package/dist/health/checks/MemoryHeapCheck.d.ts +21 -0
  66. package/dist/health/checks/MemoryHeapCheck.d.ts.map +1 -0
  67. package/dist/health/checks/MemoryHeapCheck.js +55 -0
  68. package/dist/health/checks/MemoryHeapCheck.js.map +1 -0
  69. package/dist/health/checks/MemoryRSSCheck.d.ts +21 -0
  70. package/dist/health/checks/MemoryRSSCheck.d.ts.map +1 -0
  71. package/dist/health/checks/MemoryRSSCheck.js +55 -0
  72. package/dist/health/checks/MemoryRSSCheck.js.map +1 -0
  73. package/dist/health/checks/memory.d.ts +24 -0
  74. package/dist/health/checks/memory.d.ts.map +1 -0
  75. package/dist/health/checks/memory.js +88 -0
  76. package/dist/health/checks/memory.js.map +1 -0
  77. package/dist/health/index.d.ts +9 -0
  78. package/dist/health/index.d.ts.map +1 -0
  79. package/dist/health/index.js +8 -0
  80. package/dist/health/index.js.map +1 -0
  81. package/dist/health/tracing.d.ts +12 -0
  82. package/dist/health/tracing.d.ts.map +1 -0
  83. package/dist/health/tracing.js +11 -0
  84. package/dist/health/tracing.js.map +1 -0
  85. package/dist/health/types.d.ts +51 -0
  86. package/dist/health/types.d.ts.map +1 -0
  87. package/dist/health/types.js +5 -0
  88. package/dist/health/types.js.map +1 -0
  89. package/dist/helpers/bytes.d.ts +28 -0
  90. package/dist/helpers/bytes.d.ts.map +1 -0
  91. package/dist/helpers/bytes.js +63 -0
  92. package/dist/helpers/bytes.js.map +1 -0
  93. package/dist/helpers/string.d.ts +4 -0
  94. package/dist/helpers/string.d.ts.map +1 -1
  95. package/dist/helpers/string.js +2 -0
  96. package/dist/helpers/string.js.map +1 -1
  97. package/dist/http/HttpContext.d.ts +30 -2
  98. package/dist/http/HttpContext.d.ts.map +1 -1
  99. package/dist/http/HttpContext.js +73 -3
  100. package/dist/http/HttpContext.js.map +1 -1
  101. package/dist/http/Request.d.ts +42 -3
  102. package/dist/http/Request.d.ts.map +1 -1
  103. package/dist/http/Request.js +51 -7
  104. package/dist/http/Request.js.map +1 -1
  105. package/dist/http/Response.d.ts +82 -2
  106. package/dist/http/Response.d.ts.map +1 -1
  107. package/dist/http/Response.js +240 -13
  108. package/dist/http/Response.js.map +1 -1
  109. package/dist/http/SseStream.d.ts +10 -0
  110. package/dist/http/SseStream.d.ts.map +1 -1
  111. package/dist/http/SseStream.js.map +1 -1
  112. package/dist/index.d.ts +7 -2
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +4 -1
  115. package/dist/index.js.map +1 -1
  116. package/dist/migrations/MigrationRegistry.d.ts +28 -0
  117. package/dist/migrations/MigrationRegistry.d.ts.map +1 -0
  118. package/dist/migrations/MigrationRegistry.js +41 -0
  119. package/dist/migrations/MigrationRegistry.js.map +1 -0
  120. package/dist/migrations/types.d.ts +64 -0
  121. package/dist/migrations/types.d.ts.map +1 -0
  122. package/dist/migrations/types.js +15 -0
  123. package/dist/migrations/types.js.map +1 -0
  124. package/dist/router/Router.d.ts +112 -9
  125. package/dist/router/Router.d.ts.map +1 -1
  126. package/dist/router/Router.js +206 -28
  127. package/dist/router/Router.js.map +1 -1
  128. package/dist/security/CookieSigner.d.ts +43 -4
  129. package/dist/security/CookieSigner.d.ts.map +1 -1
  130. package/dist/security/CookieSigner.js +68 -10
  131. package/dist/security/CookieSigner.js.map +1 -1
  132. package/dist/session/Session.d.ts +46 -1
  133. package/dist/session/Session.d.ts.map +1 -1
  134. package/dist/session/Session.js +57 -4
  135. package/dist/session/Session.js.map +1 -1
  136. package/dist/session/SessionMiddleware.d.ts.map +1 -1
  137. package/dist/session/SessionMiddleware.js +19 -9
  138. package/dist/session/SessionMiddleware.js.map +1 -1
  139. package/dist/session/drivers/CookieDriver.d.ts +1 -1
  140. package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
  141. package/dist/session/drivers/CookieDriver.js +3 -3
  142. package/dist/session/drivers/CookieDriver.js.map +1 -1
  143. package/dist/session/drivers/DatabaseDriver.d.ts +1 -1
  144. package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
  145. package/dist/session/drivers/DatabaseDriver.js +2 -2
  146. package/dist/session/drivers/DatabaseDriver.js.map +1 -1
  147. package/dist/session/drivers/FileDriver.d.ts +1 -1
  148. package/dist/session/drivers/FileDriver.d.ts.map +1 -1
  149. package/dist/session/drivers/FileDriver.js +7 -5
  150. package/dist/session/drivers/FileDriver.js.map +1 -1
  151. package/dist/session/drivers/MemoryDriver.d.ts +1 -1
  152. package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
  153. package/dist/session/drivers/MemoryDriver.js +1 -1
  154. package/dist/session/drivers/MemoryDriver.js.map +1 -1
  155. package/dist/session/drivers/RedisDriver.d.ts +1 -1
  156. package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
  157. package/dist/session/drivers/RedisDriver.js +11 -4
  158. package/dist/session/drivers/RedisDriver.js.map +1 -1
  159. package/events.win32-x64-msvc.node +0 -0
  160. package/index.darwin-arm64.node +0 -0
  161. package/index.darwin-x64.node +0 -0
  162. package/index.linux-arm64-gnu.node +0 -0
  163. package/index.linux-x64-gnu.node +0 -0
  164. package/index.win32-x64-msvc.node +0 -0
  165. package/package.json +48 -31
  166. package/scheduler.win32-x64-msvc.node +0 -0
  167. package/src/Application.ts +169 -6
  168. package/src/HealthCheck.ts +27 -14
  169. package/src/HttpKernel.ts +27 -4
  170. package/src/Ignitor.ts +17 -8
  171. package/src/bodyparser/BodyParserMiddleware.ts +70 -36
  172. package/src/bodyparser/MultipartFile.ts +153 -8
  173. package/src/console/BaseCommand.ts +8 -1
  174. package/src/console/Kernel.ts +18 -0
  175. package/src/directories.ts +81 -0
  176. package/src/events/Emitter.ts +129 -13
  177. package/src/events/EventsBuffer.ts +131 -0
  178. package/src/events/index.ts +7 -0
  179. package/src/health/BaseCheck.ts +30 -0
  180. package/src/health/HealthChecks.ts +83 -0
  181. package/src/health/Result.ts +69 -0
  182. package/src/health/checks/DiskSpaceCheck.ts +69 -0
  183. package/src/health/checks/MemoryHeapCheck.ts +68 -0
  184. package/src/health/checks/MemoryRSSCheck.ts +68 -0
  185. package/src/health/checks/memory.ts +115 -0
  186. package/src/health/index.ts +13 -0
  187. package/src/health/tracing.ts +15 -0
  188. package/src/health/types.ts +51 -0
  189. package/src/helpers/bytes.ts +71 -0
  190. package/src/helpers/string.ts +2 -0
  191. package/src/http/HttpContext.ts +78 -4
  192. package/src/http/Request.ts +62 -10
  193. package/src/http/Response.ts +283 -14
  194. package/src/http/SseStream.ts +10 -0
  195. package/src/index.ts +27 -1
  196. package/src/migrations/MigrationRegistry.ts +49 -0
  197. package/src/migrations/types.ts +68 -0
  198. package/src/router/Router.ts +241 -32
  199. package/src/security/CookieSigner.ts +92 -18
  200. package/src/session/Session.ts +71 -5
  201. package/src/session/SessionMiddleware.ts +19 -9
  202. package/src/session/drivers/CookieDriver.ts +4 -4
  203. package/src/session/drivers/DatabaseDriver.ts +3 -3
  204. package/src/session/drivers/FileDriver.ts +8 -6
  205. package/src/session/drivers/MemoryDriver.ts +2 -2
  206. package/src/session/drivers/RedisDriver.ts +12 -5
  207. package/tsconfig.app.json +13 -13
  208. package/tsconfig.base.json +19 -0
  209. package/tsconfig.client.json +16 -0
  210. package/tsconfig.package.json +15 -0
@@ -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
- /** Param matcher — regex or predefined matcher. */
61
- export type ParamMatcher = RegExp | { pattern: RegExp }
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 { pattern: /^\d+$/ }
187
+ return { match: /^\d+$/, cast: (value) => Number(value) }
172
188
  },
173
189
  /** Match UUID v4 params only. */
174
190
  uuid(): ParamMatcher {
175
- return { pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i }
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 { pattern: /^[a-z0-9]+(?:-[a-z0-9]+)*$/ }
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 { pattern: new RegExp(`^(?:${matcher})$`) }
194
- return matcher instanceof RegExp ? { pattern: matcher } : matcher
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 (AdonisJS `route(pattern, methods)`
812
- * accepts an array). A multi-verb call returns a single builder that
813
- * configures every verb at once.
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(method: string | string[], path: string, handler: RouteHandler): RouteBuilder {
816
- const methods = Array.isArray(method) ? method : [method]
817
- const defs = methods.map((verb) => {
818
- const def = this.#createDefinition(verb, path, handler)
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', path, handler)
878
+ return this.route(path, 'GET', handler)
828
879
  }
829
880
 
830
881
  post(path: string, handler: RouteHandler): RouteBuilder {
831
- return this.route('POST', path, handler)
882
+ return this.route(path, 'POST', handler)
832
883
  }
833
884
 
834
885
  put(path: string, handler: RouteHandler): RouteBuilder {
835
- return this.route('PUT', path, handler)
886
+ return this.route(path, 'PUT', handler)
836
887
  }
837
888
 
838
889
  patch(path: string, handler: RouteHandler): RouteBuilder {
839
- return this.route('PATCH', path, handler)
890
+ return this.route(path, 'PATCH', handler)
840
891
  }
841
892
 
842
893
  delete(path: string, handler: RouteHandler): RouteBuilder {
843
- return this.route('DELETE', path, handler)
894
+ return this.route(path, 'DELETE', handler)
844
895
  }
845
896
 
846
897
  head(path: string, handler: RouteHandler): RouteBuilder {
847
- return this.route('HEAD', path, handler)
898
+ return this.route(path, 'HEAD', handler)
848
899
  }
849
900
 
850
901
  options(path: string, handler: RouteHandler): RouteBuilder {
851
- return this.route('OPTIONS', path, handler)
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('*', path, handler)
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
- if (
1100
- !this.#validateMatchers(params, {
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
- return { route, params }
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 instanceof RegExp ? matcher : matcher.pattern
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
- /** The payload. */
44
- m: string
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: string, expiresIn?: number, purpose?: string): string {
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
- /** Unwrap a sealed payload, refusing an expired one or the wrong purpose. */
80
- #open(raw: string, purpose?: string): string | null {
81
- let envelope: Envelope
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
- const parsed: unknown = JSON.parse(raw)
84
- if (typeof parsed !== 'object' || parsed === null) return null
85
- envelope = parsed as Envelope
96
+ parsed = JSON.parse(raw)
86
97
  } catch {
87
- // Not an envelope — a value sealed before this format existed.
88
- return raw
98
+ return null
89
99
  }
90
- if (typeof envelope.m !== 'string') return raw
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: string, expiresIn?: number, purpose?: string): string {
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): string | null {
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
- encrypt(value: string, expiresIn?: number, purpose?: string): string {
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): string | null {
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
  }
@@ -13,7 +13,16 @@
13
13
  import type { SessionRedisClientSource } from './drivers/RedisDriver.js'
14
14
 
15
15
  export interface SessionDriver {
16
- read(sessionId: string): Promise<Record<string, unknown>>
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.__flash
195
+ const flash = data[FLASH_KEY]
179
196
  this.#previousFlash = isPlainRecord(flash) ? { ...flash } : {}
180
- delete this.#data.__flash
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.__flash = this.#flashData
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
- this.#hydrate(await this.#driver.read(this.#sessionId))
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
  /**