@c9up/ream 0.1.25 → 0.1.27

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 (302) hide show
  1. package/README.md +65 -0
  2. package/dist/ConfigLoader.d.ts +46 -5
  3. package/dist/ConfigLoader.d.ts.map +1 -1
  4. package/dist/ConfigLoader.js +60 -9
  5. package/dist/ConfigLoader.js.map +1 -1
  6. package/dist/HttpKernel.d.ts.map +1 -1
  7. package/dist/HttpKernel.js +46 -6
  8. package/dist/HttpKernel.js.map +1 -1
  9. package/dist/Ignitor.d.ts +115 -7
  10. package/dist/Ignitor.d.ts.map +1 -1
  11. package/dist/Ignitor.js +254 -40
  12. package/dist/Ignitor.js.map +1 -1
  13. package/dist/console/BaseCommand.d.ts +217 -0
  14. package/dist/console/BaseCommand.d.ts.map +1 -0
  15. package/dist/console/BaseCommand.js +340 -0
  16. package/dist/console/BaseCommand.js.map +1 -0
  17. package/dist/console/Console.d.ts +86 -0
  18. package/dist/console/Console.d.ts.map +1 -0
  19. package/dist/console/Console.js +134 -0
  20. package/dist/console/Console.js.map +1 -0
  21. package/dist/console/ExceptionHandler.d.ts +28 -0
  22. package/dist/console/ExceptionHandler.d.ts.map +1 -0
  23. package/dist/console/ExceptionHandler.js +76 -0
  24. package/dist/console/ExceptionHandler.js.map +1 -0
  25. package/dist/console/HelpCommand.d.ts +16 -0
  26. package/dist/console/HelpCommand.d.ts.map +1 -0
  27. package/dist/console/HelpCommand.js +40 -0
  28. package/dist/console/HelpCommand.js.map +1 -0
  29. package/dist/console/IndexGenerator.d.ts +24 -0
  30. package/dist/console/IndexGenerator.d.ts.map +1 -0
  31. package/dist/console/IndexGenerator.js +65 -0
  32. package/dist/console/IndexGenerator.js.map +1 -0
  33. package/dist/console/Kernel.d.ts +298 -0
  34. package/dist/console/Kernel.d.ts.map +1 -0
  35. package/dist/console/Kernel.js +1120 -0
  36. package/dist/console/Kernel.js.map +1 -0
  37. package/dist/console/ListCommand.d.ts +18 -0
  38. package/dist/console/ListCommand.d.ts.map +1 -0
  39. package/dist/console/ListCommand.js +60 -0
  40. package/dist/console/ListCommand.js.map +1 -0
  41. package/dist/console/assertions.d.ts +20 -0
  42. package/dist/console/assertions.d.ts.map +1 -0
  43. package/dist/console/assertions.js +84 -0
  44. package/dist/console/assertions.js.map +1 -0
  45. package/dist/console/cliui.d.ts +248 -0
  46. package/dist/console/cliui.d.ts.map +1 -0
  47. package/dist/console/cliui.js +587 -0
  48. package/dist/console/cliui.js.map +1 -0
  49. package/dist/console/decorators.d.ts +90 -0
  50. package/dist/console/decorators.d.ts.map +1 -0
  51. package/dist/console/decorators.js +142 -0
  52. package/dist/console/decorators.js.map +1 -0
  53. package/dist/console/index.d.ts +32 -0
  54. package/dist/console/index.d.ts.map +1 -0
  55. package/dist/console/index.js +24 -0
  56. package/dist/console/index.js.map +1 -0
  57. package/dist/console/loaders.d.ts +52 -0
  58. package/dist/console/loaders.d.ts.map +1 -0
  59. package/dist/console/loaders.js +131 -0
  60. package/dist/console/loaders.js.map +1 -0
  61. package/dist/console/parser.d.ts +86 -0
  62. package/dist/console/parser.d.ts.map +1 -0
  63. package/dist/console/parser.js +440 -0
  64. package/dist/console/parser.js.map +1 -0
  65. package/dist/console/prompts.d.ts +124 -0
  66. package/dist/console/prompts.d.ts.map +1 -0
  67. package/dist/console/prompts.js +356 -0
  68. package/dist/console/prompts.js.map +1 -0
  69. package/dist/console/tracing.d.ts +17 -0
  70. package/dist/console/tracing.d.ts.map +1 -0
  71. package/dist/console/tracing.js +11 -0
  72. package/dist/console/tracing.js.map +1 -0
  73. package/dist/console/types.d.ts +255 -0
  74. package/dist/console/types.d.ts.map +1 -0
  75. package/dist/console/types.js +25 -0
  76. package/dist/console/types.js.map +1 -0
  77. package/dist/console/ui.d.ts +20 -0
  78. package/dist/console/ui.d.ts.map +1 -0
  79. package/dist/console/ui.js +31 -0
  80. package/dist/console/ui.js.map +1 -0
  81. package/dist/console/utils.d.ts +27 -0
  82. package/dist/console/utils.d.ts.map +1 -0
  83. package/dist/console/utils.js +56 -0
  84. package/dist/console/utils.js.map +1 -0
  85. package/dist/container/Container.d.ts +23 -3
  86. package/dist/container/Container.d.ts.map +1 -1
  87. package/dist/container/Container.js +177 -70
  88. package/dist/container/Container.js.map +1 -1
  89. package/dist/container/types.d.ts +14 -2
  90. package/dist/container/types.d.ts.map +1 -1
  91. package/dist/decorators/Service.d.ts +7 -0
  92. package/dist/decorators/Service.d.ts.map +1 -1
  93. package/dist/decorators/Service.js +23 -3
  94. package/dist/decorators/Service.js.map +1 -1
  95. package/dist/env/Env.d.ts +29 -4
  96. package/dist/env/Env.d.ts.map +1 -1
  97. package/dist/env/Env.js +27 -4
  98. package/dist/env/Env.js.map +1 -1
  99. package/dist/env/Secret.d.ts +11 -0
  100. package/dist/env/Secret.d.ts.map +1 -0
  101. package/dist/env/Secret.js +36 -0
  102. package/dist/env/Secret.js.map +1 -0
  103. package/dist/env/interpolate.d.ts +26 -0
  104. package/dist/env/interpolate.d.ts.map +1 -0
  105. package/dist/env/interpolate.js +48 -0
  106. package/dist/env/interpolate.js.map +1 -0
  107. package/dist/env/loadEnvFiles.d.ts.map +1 -1
  108. package/dist/env/loadEnvFiles.js +14 -4
  109. package/dist/env/loadEnvFiles.js.map +1 -1
  110. package/dist/env/schema.d.ts +23 -6
  111. package/dist/env/schema.d.ts.map +1 -1
  112. package/dist/env/schema.js +67 -33
  113. package/dist/env/schema.js.map +1 -1
  114. package/dist/errors/ReamError.d.ts +25 -0
  115. package/dist/errors/ReamError.d.ts.map +1 -1
  116. package/dist/errors/ReamError.js +5 -0
  117. package/dist/errors/ReamError.js.map +1 -1
  118. package/dist/errors/prettyPrintError.d.ts +9 -0
  119. package/dist/errors/prettyPrintError.d.ts.map +1 -0
  120. package/dist/errors/prettyPrintError.js +22 -0
  121. package/dist/errors/prettyPrintError.js.map +1 -0
  122. package/dist/events/Emitter.d.ts +1 -1
  123. package/dist/events/Emitter.d.ts.map +1 -1
  124. package/dist/events/Emitter.js +1 -1
  125. package/dist/events/Emitter.js.map +1 -1
  126. package/dist/events/EventsProvider.d.ts +2 -2
  127. package/dist/events/EventsProvider.d.ts.map +1 -1
  128. package/dist/events/EventsProvider.js +3 -3
  129. package/dist/events/EventsProvider.js.map +1 -1
  130. package/dist/graphql/GraphQLEngine.d.ts +1 -1
  131. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  132. package/dist/graphql/GraphQLEngine.js +2 -2
  133. package/dist/graphql/GraphQLEngine.js.map +1 -1
  134. package/dist/graphql/GraphQLProvider.d.ts.map +1 -1
  135. package/dist/graphql/GraphQLProvider.js +7 -3
  136. package/dist/graphql/GraphQLProvider.js.map +1 -1
  137. package/dist/http/Exception.d.ts +63 -1
  138. package/dist/http/Exception.d.ts.map +1 -1
  139. package/dist/http/Exception.js +185 -48
  140. package/dist/http/Exception.js.map +1 -1
  141. package/dist/http/HttpContext.d.ts +53 -7
  142. package/dist/http/HttpContext.d.ts.map +1 -1
  143. package/dist/http/HttpContext.js +56 -15
  144. package/dist/http/HttpContext.js.map +1 -1
  145. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  146. package/dist/http/RedirectBuilder.js +4 -1
  147. package/dist/http/RedirectBuilder.js.map +1 -1
  148. package/dist/http/Request.d.ts +141 -10
  149. package/dist/http/Request.d.ts.map +1 -1
  150. package/dist/http/Request.js +244 -34
  151. package/dist/http/Request.js.map +1 -1
  152. package/dist/http/Response.d.ts +176 -9
  153. package/dist/http/Response.d.ts.map +1 -1
  154. package/dist/http/Response.js +404 -16
  155. package/dist/http/Response.js.map +1 -1
  156. package/dist/index.d.ts +19 -8
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +20 -5
  159. package/dist/index.js.map +1 -1
  160. package/dist/middleware/Pipeline.d.ts +29 -8
  161. package/dist/middleware/Pipeline.d.ts.map +1 -1
  162. package/dist/middleware/Pipeline.js +40 -2
  163. package/dist/middleware/Pipeline.js.map +1 -1
  164. package/dist/openapi/OpenApiProvider.js +4 -4
  165. package/dist/openapi/OpenApiProvider.js.map +1 -1
  166. package/dist/router/Router.d.ts +223 -31
  167. package/dist/router/Router.d.ts.map +1 -1
  168. package/dist/router/Router.js +520 -102
  169. package/dist/router/Router.js.map +1 -1
  170. package/dist/rpc/RpcProvider.d.ts +1 -0
  171. package/dist/rpc/RpcProvider.d.ts.map +1 -1
  172. package/dist/rpc/RpcProvider.js +7 -3
  173. package/dist/rpc/RpcProvider.js.map +1 -1
  174. package/dist/rpc/RpcRouter.d.ts +2 -2
  175. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  176. package/dist/rpc/RpcRouter.js +17 -5
  177. package/dist/rpc/RpcRouter.js.map +1 -1
  178. package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
  179. package/dist/scheduler/ScheduleProvider.js +7 -15
  180. package/dist/scheduler/ScheduleProvider.js.map +1 -1
  181. package/dist/server/Server.d.ts +17 -4
  182. package/dist/server/Server.d.ts.map +1 -1
  183. package/dist/server/Server.js +41 -7
  184. package/dist/server/Server.js.map +1 -1
  185. package/dist/services/app.d.ts +1 -1
  186. package/dist/services/app.js +1 -1
  187. package/dist/services/console.d.ts +21 -0
  188. package/dist/services/console.d.ts.map +1 -0
  189. package/dist/services/console.js +35 -0
  190. package/dist/services/console.js.map +1 -0
  191. package/dist/session/SessionMiddleware.js +3 -3
  192. package/dist/session/SessionMiddleware.js.map +1 -1
  193. package/dist/storage/StaticProvider.js +1 -1
  194. package/dist/storage/StaticProvider.js.map +1 -1
  195. package/dist/testing/ApiResponse.d.ts +218 -0
  196. package/dist/testing/ApiResponse.d.ts.map +1 -0
  197. package/dist/testing/ApiResponse.js +576 -0
  198. package/dist/testing/ApiResponse.js.map +1 -0
  199. package/dist/testing/RequestBuilder.d.ts +155 -35
  200. package/dist/testing/RequestBuilder.d.ts.map +1 -1
  201. package/dist/testing/RequestBuilder.js +455 -109
  202. package/dist/testing/RequestBuilder.js.map +1 -1
  203. package/dist/testing/TestClient.d.ts +53 -37
  204. package/dist/testing/TestClient.d.ts.map +1 -1
  205. package/dist/testing/TestClient.js +84 -52
  206. package/dist/testing/TestClient.js.map +1 -1
  207. package/dist/utils/Macroable.d.ts +30 -0
  208. package/dist/utils/Macroable.d.ts.map +1 -0
  209. package/dist/utils/Macroable.js +43 -0
  210. package/dist/utils/Macroable.js.map +1 -0
  211. package/dist/utils/inflect.d.ts +17 -0
  212. package/dist/utils/inflect.d.ts.map +1 -0
  213. package/dist/utils/inflect.js +32 -0
  214. package/dist/utils/inflect.js.map +1 -0
  215. package/dist/utils/objectPath.d.ts +57 -0
  216. package/dist/utils/objectPath.d.ts.map +1 -0
  217. package/dist/utils/objectPath.js +141 -0
  218. package/dist/utils/objectPath.js.map +1 -0
  219. package/events.darwin-arm64.node +0 -0
  220. package/events.darwin-x64.node +0 -0
  221. package/events.linux-arm64-gnu.node +0 -0
  222. package/events.linux-x64-gnu.node +0 -0
  223. package/events.win32-x64-msvc.node +0 -0
  224. package/index.darwin-arm64.node +0 -0
  225. package/index.darwin-x64.node +0 -0
  226. package/index.linux-arm64-gnu.node +0 -0
  227. package/index.linux-x64-gnu.node +0 -0
  228. package/index.win32-x64-msvc.node +0 -0
  229. package/package.json +20 -2
  230. package/scheduler.darwin-arm64.node +0 -0
  231. package/scheduler.darwin-x64.node +0 -0
  232. package/scheduler.linux-arm64-gnu.node +0 -0
  233. package/scheduler.linux-x64-gnu.node +0 -0
  234. package/scheduler.win32-x64-msvc.node +0 -0
  235. package/src/ConfigLoader.ts +72 -9
  236. package/src/HttpKernel.ts +53 -15
  237. package/src/Ignitor.ts +364 -45
  238. package/src/console/BaseCommand.ts +467 -0
  239. package/src/console/Console.ts +174 -0
  240. package/src/console/ExceptionHandler.ts +91 -0
  241. package/src/console/HelpCommand.ts +37 -0
  242. package/src/console/IndexGenerator.ts +79 -0
  243. package/src/console/Kernel.ts +1382 -0
  244. package/src/console/ListCommand.ts +55 -0
  245. package/src/console/assertions.ts +114 -0
  246. package/src/console/cliui.ts +756 -0
  247. package/src/console/decorators.ts +199 -0
  248. package/src/console/index.ts +67 -0
  249. package/src/console/loaders.ts +145 -0
  250. package/src/console/parser.ts +598 -0
  251. package/src/console/prompts.ts +548 -0
  252. package/src/console/tracing.ts +22 -0
  253. package/src/console/types.ts +280 -0
  254. package/src/console/ui.ts +37 -0
  255. package/src/console/utils.ts +69 -0
  256. package/src/container/Container.ts +198 -72
  257. package/src/container/types.ts +15 -2
  258. package/src/decorators/Service.ts +28 -3
  259. package/src/env/Env.ts +49 -5
  260. package/src/env/Secret.ts +41 -0
  261. package/src/env/interpolate.ts +54 -0
  262. package/src/env/loadEnvFiles.ts +12 -4
  263. package/src/env/schema.ts +109 -35
  264. package/src/errors/ReamError.ts +31 -0
  265. package/src/errors/prettyPrintError.ts +20 -0
  266. package/src/events/Emitter.ts +2 -2
  267. package/src/events/EventsProvider.ts +5 -5
  268. package/src/graphql/GraphQLEngine.ts +4 -4
  269. package/src/graphql/GraphQLProvider.ts +7 -3
  270. package/src/http/Exception.ts +214 -37
  271. package/src/http/HttpContext.ts +87 -18
  272. package/src/http/RedirectBuilder.ts +4 -1
  273. package/src/http/Request.ts +264 -34
  274. package/src/http/Response.ts +478 -27
  275. package/src/index.ts +41 -6
  276. package/src/middleware/Pipeline.ts +77 -6
  277. package/src/openapi/OpenApiProvider.ts +4 -4
  278. package/src/router/Router.ts +631 -113
  279. package/src/rpc/RpcProvider.ts +6 -2
  280. package/src/rpc/RpcRouter.ts +22 -7
  281. package/src/scheduler/ScheduleProvider.ts +7 -15
  282. package/src/server/Server.ts +49 -10
  283. package/src/services/app.ts +1 -1
  284. package/src/services/console.ts +42 -0
  285. package/src/session/SessionMiddleware.ts +4 -4
  286. package/src/storage/StaticProvider.ts +1 -1
  287. package/src/testing/ApiResponse.ts +691 -0
  288. package/src/testing/RequestBuilder.ts +577 -137
  289. package/src/testing/TestClient.ts +110 -77
  290. package/src/utils/Macroable.ts +54 -0
  291. package/src/utils/inflect.ts +29 -0
  292. package/src/utils/objectPath.ts +142 -0
  293. package/dist/console/CommandRunner.d.ts +0 -18
  294. package/dist/console/CommandRunner.d.ts.map +0 -1
  295. package/dist/console/CommandRunner.js +0 -65
  296. package/dist/console/CommandRunner.js.map +0 -1
  297. package/dist/decorators/Lazy.d.ts +0 -19
  298. package/dist/decorators/Lazy.d.ts.map +0 -1
  299. package/dist/decorators/Lazy.js +0 -53
  300. package/dist/decorators/Lazy.js.map +0 -1
  301. package/src/console/CommandRunner.ts +0 -82
  302. package/src/decorators/Lazy.ts +0 -57
@@ -6,6 +6,8 @@
6
6
  * - Built-in E_* exceptions
7
7
  */
8
8
 
9
+ import { format } from 'node:util'
10
+ import { Macroable } from '../utils/Macroable.js'
9
11
  import type { HttpContext } from './HttpContext.js'
10
12
 
11
13
  /**
@@ -29,9 +31,13 @@ import type { HttpContext } from './HttpContext.js'
29
31
  export class Exception extends Error {
30
32
  static status = 500
31
33
  static code = 'E_UNKNOWN'
34
+ /** Optional static remediation hint, copied onto instances (AdonisJS `Exception.help`). */
35
+ static help?: string
32
36
 
33
37
  status: number
34
38
  code: string
39
+ /** Human-readable remediation hint surfaced in debug output (AdonisJS `error.help`). */
40
+ help?: string
35
41
 
36
42
  constructor(message: string, options?: { status?: number; code?: string }) {
37
43
  super(message)
@@ -40,6 +46,7 @@ export class Exception extends Error {
40
46
  const ctor = this.constructor as any as typeof Exception
41
47
  this.status = options?.status ?? ctor.status
42
48
  this.code = options?.code ?? ctor.code
49
+ if (ctor.help !== undefined) this.help = ctor.help
43
50
  }
44
51
 
45
52
  /** Override to self-handle the exception (convert to HTTP response). */
@@ -49,6 +56,42 @@ export class Exception extends Error {
49
56
  report?(error: this, ctx: HttpContext): Promise<void> | void
50
57
  }
51
58
 
59
+ /** A concrete Exception subclass produced by {@link createError}. */
60
+ export interface ExceptionConstructor {
61
+ new (...args: unknown[]): Exception
62
+ status: number
63
+ code: string
64
+ }
65
+
66
+ /**
67
+ * Build a reusable Exception subclass in one line (AdonisJS `createError`).
68
+ * `message` may contain `util.format` placeholders (`%s`, `%d`) filled by the
69
+ * constructor args:
70
+ *
71
+ * const E_RESOURCE_MISSING = createError('Resource %s not found', 'E_RESOURCE_MISSING', 404)
72
+ * throw new E_RESOURCE_MISSING('user-42') // → "Resource user-42 not found"
73
+ */
74
+ export function createError(message: string, code: string, status = 500): ExceptionConstructor {
75
+ return class extends Exception {
76
+ static override status = status
77
+ static override code = code
78
+ constructor(...args: unknown[]) {
79
+ super(format(message, ...args), { status, code })
80
+ }
81
+ }
82
+ }
83
+
84
+ /** A generic 500 runtime error (AdonisJS `RuntimeException`). */
85
+ export class RuntimeException extends Exception {
86
+ static override status = 500
87
+ static override code = 'E_RUNTIME_EXCEPTION'
88
+ }
89
+
90
+ /** Raised when a function receives invalid arguments (AdonisJS `InvalidArgumentsException`). */
91
+ export class InvalidArgumentsException extends RuntimeException {
92
+ static override code = 'E_INVALID_ARGUMENTS'
93
+ }
94
+
52
95
  // ─── Built-in exceptions ──────────────────────────────────
53
96
 
54
97
  export class E_ROUTE_NOT_FOUND extends Exception {
@@ -133,6 +176,34 @@ export class E_HTTP_EXCEPTION extends Exception {
133
176
  }
134
177
  }
135
178
 
179
+ /**
180
+ * Thrown by `response.abort()` / `response.abortIf()` (AdonisJS parity). Carries
181
+ * an arbitrary body that {@link handle} renders as-is — a string verbatim,
182
+ * anything else as JSON.
183
+ */
184
+ export class E_HTTP_REQUEST_ABORTED extends Exception {
185
+ static override status = 400
186
+ static override code = 'E_HTTP_REQUEST_ABORTED'
187
+
188
+ readonly body: unknown
189
+
190
+ constructor(body: unknown, status = 400) {
191
+ super(typeof body === 'string' ? body : 'Request aborted', {
192
+ status,
193
+ code: 'E_HTTP_REQUEST_ABORTED',
194
+ })
195
+ this.body = body
196
+ }
197
+
198
+ override handle(_error: this, ctx: HttpContext): void {
199
+ if (typeof this.body === 'string') {
200
+ ctx.response.status(this.status).send(this.body)
201
+ } else {
202
+ ctx.response.status(this.status).json(this.body)
203
+ }
204
+ }
205
+ }
206
+
136
207
  // ─── ExceptionHandler ─────────────────────────────────────
137
208
 
138
209
  /**
@@ -150,54 +221,131 @@ export class E_HTTP_EXCEPTION extends Exception {
150
221
  * 2. Else → this.handle() → content negotiation (JSON or HTML)
151
222
  * 3. Then → this.report() → logging/monitoring
152
223
  */
153
- export class ExceptionHandler {
224
+ /** Renders an HTML error page for a status (AdonisJS `StatusPageRenderer`). */
225
+ export type StatusPageRenderer = (error: unknown, ctx: HttpContext) => Promise<string> | string
226
+
227
+ /** A constructor usable in `instanceof` for {@link ExceptionHandler.ignoreExceptions}. */
228
+ export type ExceptionClass = new (...args: never[]) => Error
229
+
230
+ /** Reporting log level, keyed to `console` (AdonisJS levels: error/warn/info). */
231
+ type LogLevel = 'error' | 'warn' | 'info'
232
+
233
+ export class ExceptionHandler extends Macroable {
154
234
  protected debug: boolean
235
+ /** Render `statusPages` (browser HTML). Defaults to production only (AdonisJS). */
236
+ protected renderStatusPages: boolean = process.env.NODE_ENV === 'production'
237
+ /** Status → HTML renderer, keys may be single codes or `'500..599'` ranges. */
238
+ protected statusPages: Record<string, StatusPageRenderer> = {}
239
+ /** Master reporting switch (AdonisJS `reportErrors`). */
240
+ protected reportErrors = true
155
241
  protected ignoreStatuses: number[] = [400, 401, 404, 422]
156
242
  protected ignoreCodes: string[] = []
243
+ /** Exception classes never reported (AdonisJS `ignoreExceptions`). */
244
+ protected ignoreExceptions: ExceptionClass[] = []
245
+
246
+ #expandedStatusPages?: Record<number, StatusPageRenderer>
157
247
 
158
248
  constructor(debug = false) {
249
+ super()
159
250
  this.debug = debug
160
251
  }
161
252
 
253
+ /** Whether debug detail is exposed for this request (AdonisJS — override per-ctx). */
254
+ protected isDebuggingEnabled(_ctx: HttpContext): boolean {
255
+ return this.debug
256
+ }
257
+
258
+ /** Log level for an error by status: 5xx→error, 4xx→warn, else info (AdonisJS). */
259
+ protected getErrorLogLevel(status: number): LogLevel {
260
+ if (status >= 500) return 'error'
261
+ if (status >= 400) return 'warn'
262
+ return 'info'
263
+ }
264
+
265
+ /** Whether an error should be reported — honours every ignore list (AdonisJS `shouldReport`). */
266
+ protected shouldReport(error: unknown): boolean {
267
+ if (!this.reportErrors) return false
268
+ const { status, code } = extractErrorMeta(error)
269
+ if (this.ignoreStatuses.includes(status)) return false
270
+ if (this.ignoreCodes.includes(code)) return false
271
+ if (this.ignoreExceptions.some((exception) => error instanceof exception)) return false
272
+ return true
273
+ }
274
+
275
+ /** Expand `statusPages` range keys (`'500..599'`) into a per-code lookup (cached). */
276
+ #expandStatusPages(): Record<number, StatusPageRenderer> {
277
+ if (!this.#expandedStatusPages) {
278
+ const expanded: Record<number, StatusPageRenderer> = {}
279
+ for (const range of Object.keys(this.statusPages)) {
280
+ Object.assign(expanded, parseStatusRange(range, this.statusPages[range]))
281
+ }
282
+ this.#expandedStatusPages = expanded
283
+ }
284
+ return this.#expandedStatusPages
285
+ }
286
+
162
287
  /** Convert an exception to an HTTP response. */
163
288
  async handle(error: unknown, ctx: HttpContext): Promise<void> {
164
- // Self-handled exceptions take over entirely.
165
- if (error instanceof Exception && typeof error.handle === 'function') {
289
+ // Self-handled errors take over entirely (duck-typed, per AdonisJS — any
290
+ // thrown value exposing a `handle()` method, not just `Exception` instances).
291
+ if (isSelfHandling(error)) {
166
292
  await error.handle(error, ctx)
167
293
  return
168
294
  }
169
295
 
170
- const { status, code } = extractErrorMeta(error)
171
- const message = this.#errorMessage(error)
172
- if (wantsJson(ctx)) {
173
- this.#sendJsonError(ctx, status, code, message, error)
174
- } else {
175
- this.#sendHtmlError(ctx, status, message)
296
+ // Negotiation. DEVIATION (named): ream lists `json` FIRST, so a request
297
+ // with no/`*/*` Accept defaults to JSON (API-first). AdonisJS lists `html`
298
+ // first (full-stack default). Explicit `text/html` / `vnd.api+json` are
299
+ // honoured identically.
300
+ switch (ctx.request.accepts(['json', 'html', 'application/vnd.api+json'])) {
301
+ case 'application/vnd.api+json':
302
+ this.#sendJsonApiError(error, ctx)
303
+ return
304
+ case 'html':
305
+ await this.#sendHtmlError(error, ctx)
306
+ return
307
+ default:
308
+ this.#sendJsonError(error, ctx)
176
309
  }
177
310
  }
178
311
 
179
312
  /** Pick the user-facing message — full detail only in debug mode. */
180
- #errorMessage(error: unknown): string {
181
- if (this.debug && error instanceof Error) return error.message
313
+ #errorMessage(error: unknown, ctx: HttpContext): string {
314
+ if (this.isDebuggingEnabled(ctx) && error instanceof Error) return error.message
182
315
  if (error instanceof Exception) return error.message
183
316
  return 'An internal error occurred'
184
317
  }
185
318
 
186
- #sendJsonError(
187
- ctx: HttpContext,
188
- status: number,
189
- code: string,
190
- message: string,
191
- error: unknown,
192
- ): void {
193
- const errorPayload: Record<string, unknown> = { code, message }
194
- if (this.debug && error instanceof Error && error.stack) {
195
- errorPayload.stack = error.stack
319
+ #sendJsonError(error: unknown, ctx: HttpContext): void {
320
+ const { status, code } = extractErrorMeta(error)
321
+ const payload: Record<string, unknown> = { code, message: this.#errorMessage(error, ctx) }
322
+ if (error instanceof Exception && error.help) payload.help = error.help
323
+ if (this.isDebuggingEnabled(ctx) && error instanceof Error && error.stack) {
324
+ payload.stack = error.stack
196
325
  }
197
- ctx.response.status(status).json({ error: errorPayload })
326
+ ctx.response.status(status).json({ error: payload })
198
327
  }
199
328
 
200
- #sendHtmlError(ctx: HttpContext, status: number, message: string): void {
329
+ #sendJsonApiError(error: unknown, ctx: HttpContext): void {
330
+ const { status, code } = extractErrorMeta(error)
331
+ ctx.response
332
+ .status(status)
333
+ .type('application/vnd.api+json')
334
+ .json({ errors: [{ title: this.#errorMessage(error, ctx), code, status: String(status) }] })
335
+ }
336
+
337
+ async #sendHtmlError(error: unknown, ctx: HttpContext): Promise<void> {
338
+ const { status } = extractErrorMeta(error)
339
+ // A registered status page wins (production browser error pages).
340
+ if (this.renderStatusPages) {
341
+ const renderer = this.#expandStatusPages()[status]
342
+ if (renderer) {
343
+ const html = await renderer(error, ctx)
344
+ ctx.response.status(status).type('text/html; charset=utf-8').send(html)
345
+ return
346
+ }
347
+ }
348
+ const message = this.#errorMessage(error, ctx)
201
349
  ctx.response
202
350
  .status(status)
203
351
  .type('text/html; charset=utf-8')
@@ -209,23 +357,22 @@ export class ExceptionHandler {
209
357
 
210
358
  /** Log/report an exception. Override for custom monitoring. */
211
359
  async report(error: unknown, ctx: HttpContext): Promise<void> {
212
- if (error instanceof Exception) {
213
- if (this.ignoreStatuses.includes(error.status)) return
214
- if (this.ignoreCodes.includes(error.code)) return
215
- }
360
+ if (!this.shouldReport(error)) return
216
361
 
217
- // Self-reported exceptions
218
- if (error instanceof Exception && typeof error.report === 'function') {
362
+ // Self-reported exceptions handle their own reporting.
363
+ if (isSelfReporting(error)) {
219
364
  await error.report(error, ctx)
220
365
  return
221
366
  }
222
367
 
223
- // Default: log to stderr
368
+ // Default: log at the status-appropriate level.
369
+ const { status } = extractErrorMeta(error)
370
+ const level = this.getErrorLogLevel(status)
224
371
  const context = this.context(ctx)
225
372
  if (error instanceof Error) {
226
- console.error(`[${new Date().toISOString()}] ${error.message}`, context, error.stack)
373
+ console[level](`[${new Date().toISOString()}] ${error.message}`, context, error.stack)
227
374
  } else {
228
- console.error(`[${new Date().toISOString()}] Unknown error:`, error, context)
375
+ console[level](`[${new Date().toISOString()}] Unknown error:`, error, context)
229
376
  }
230
377
  }
231
378
 
@@ -254,15 +401,45 @@ function extractErrorMeta(error: unknown): { status: number; code: string } {
254
401
  return { status, code }
255
402
  }
256
403
 
257
- /** Content negotiation: JSON unless the client explicitly prefers HTML. */
258
- function wantsJson(ctx: HttpContext): boolean {
404
+ /** True when a thrown value self-handles via a `handle()` method (AdonisJS duck-type). */
405
+ function isSelfHandling(
406
+ error: unknown,
407
+ ): error is { handle: (error: unknown, ctx: HttpContext) => Promise<void> | void } {
259
408
  return (
260
- ctx.request.accepts(['json', 'html']) === 'json' ||
261
- (ctx.request.header('accept')?.includes('application/json') ?? false) ||
262
- !ctx.request.header('accept')?.includes('text/html')
409
+ error !== null &&
410
+ typeof error === 'object' &&
411
+ 'handle' in error &&
412
+ typeof error.handle === 'function'
263
413
  )
264
414
  }
265
415
 
416
+ /** True when a thrown value self-reports via a `report()` method. */
417
+ function isSelfReporting(
418
+ error: unknown,
419
+ ): error is { report: (error: unknown, ctx: HttpContext) => Promise<void> | void } {
420
+ return (
421
+ error !== null &&
422
+ typeof error === 'object' &&
423
+ 'report' in error &&
424
+ typeof error.report === 'function'
425
+ )
426
+ }
427
+
428
+ /** Expand a status-page key into a per-code map: `'500..599'` → {500,…,599}, `'404'` → {404}. */
429
+ function parseStatusRange(
430
+ range: string,
431
+ renderer: StatusPageRenderer,
432
+ ): Record<number, StatusPageRenderer> {
433
+ const parts = range.split('..')
434
+ const min = Number(parts[0])
435
+ const max = Number(parts[1])
436
+ if (parts.length === 1 && !Number.isNaN(min)) return { [min]: renderer }
437
+ if (Number.isNaN(min) || Number.isNaN(max)) return {}
438
+ const result: Record<number, StatusPageRenderer> = {}
439
+ for (let status = min; status <= max; status += 1) result[status] = renderer
440
+ return result
441
+ }
442
+
266
443
  function escapeHtml(str: string): string {
267
444
  return str
268
445
  .replace(/&/g, '&amp;')
@@ -10,7 +10,10 @@
10
10
  import { AsyncLocalStorage } from 'node:async_hooks'
11
11
  import type { ServiceToken } from '../container/types.js'
12
12
  import type { Emitter } from '../events/Emitter.js'
13
+ import type { CookieSigner } from '../security/CookieSigner.js'
14
+ import type { SignedUrl } from '../security/SignedUrl.js'
13
15
  import type { Session } from '../session/Session.js'
16
+ import { Macroable } from '../utils/Macroable.js'
14
17
  import type { RouteUrlResolver } from './RedirectBuilder.js'
15
18
  import { RedirectBuilder } from './RedirectBuilder.js'
16
19
  import type { RawRequest } from './Request.js'
@@ -18,7 +21,12 @@ import { Request } from './Request.js'
18
21
  import { Response } from './Response.js'
19
22
 
20
23
  export interface AuthState {
21
- authenticated: boolean
24
+ /**
25
+ * Whether the request carries an authenticated user (AdonisJS `auth.isAuthenticated`
26
+ * parity, renamed from `authenticated`). Always present — Ream defaults it to
27
+ * `false`; an auth provider (e.g. `@c9up/warden`'s Authenticator) sets it true.
28
+ */
29
+ isAuthenticated: boolean
22
30
  user?: {
23
31
  id: string
24
32
  email?: string
@@ -28,6 +36,19 @@ export interface AuthState {
28
36
  }
29
37
  roles?: string[]
30
38
  permissions?: string[]
39
+ /**
40
+ * Optional AdonisJS `Authenticator` surface — present when an auth provider
41
+ * (Warden) fills the slot with a per-request authenticator, absent for the
42
+ * bare `{ isAuthenticated: false }` default. Methods are OPTIONAL so Ream stays
43
+ * agnostic: a non-Warden host, or the default state, remains a valid AuthState.
44
+ */
45
+ authenticate?(): Promise<void>
46
+ check?(): Promise<boolean>
47
+ authenticateUsing?(guards?: string[], options?: { loginRoute?: string }): Promise<void>
48
+ getUserOrFail?(): AuthState['user']
49
+ use?(name: string): unknown
50
+ readonly authenticationAttempted?: boolean
51
+ readonly authenticatedViaGuard?: string
31
52
  }
32
53
 
33
54
  /**
@@ -65,8 +86,8 @@ export interface Authorizer {
65
86
  // declares its own structurally-compatible interface to stay decoupled (same
66
87
  // pattern as warden/blackhole middleware), so nothing cross-imports it.
67
88
  interface ContainerResolver {
68
- /** Resolve/construct a service by token (class, string, or symbol). */
69
- make<T>(token: ServiceToken): T
89
+ /** Resolve/construct a service by token (class, string, or symbol). Async (AdonisJS parity). */
90
+ make<T>(token: ServiceToken): Promise<T>
70
91
  }
71
92
 
72
93
  /**
@@ -84,7 +105,7 @@ export interface ContextLogger {
84
105
  }
85
106
 
86
107
  /** A logger that can spawn a request-scoped child (e.g. spectrum's `child()`). */
87
- interface ChildLoggerSource extends ContextLogger {
108
+ export interface ChildLoggerSource extends ContextLogger {
88
109
  child?(options: { correlationId?: string; module?: string }): ContextLogger
89
110
  }
90
111
 
@@ -104,17 +125,16 @@ function consoleLogger(correlationId: string): ContextLogger {
104
125
  }
105
126
  }
106
127
 
107
- /** Resolve a request-scoped logger from the container, or fall back to console. */
128
+ /**
129
+ * Build a request-scoped logger from the app's base logger, or fall back to
130
+ * console. The base logger is resolved (async) once by HttpKernel and injected
131
+ * via {@link HttpContext.setBaseLogger}; child-scoping here stays synchronous
132
+ * so `ctx.logger` remains a plain getter.
133
+ */
108
134
  function resolveRequestLogger(
109
- resolver: ContainerResolver | undefined,
135
+ base: ChildLoggerSource | undefined,
110
136
  correlationId: string,
111
137
  ): ContextLogger {
112
- let base: ChildLoggerSource | undefined
113
- try {
114
- base = resolver?.make<ChildLoggerSource>('logger')
115
- } catch {
116
- base = undefined
117
- }
118
138
  if (!base) return consoleLogger(correlationId)
119
139
  return typeof base.child === 'function' ? base.child({ correlationId }) : base
120
140
  }
@@ -138,7 +158,7 @@ export interface RouteInfo {
138
158
  /** Ambient per-request context store (AdonisJS `HttpContext` ALS accessor). */
139
159
  const httpContextStorage = new AsyncLocalStorage<HttpContext>()
140
160
 
141
- export class HttpContext {
161
+ export class HttpContext extends Macroable {
142
162
  /**
143
163
  * @internal Run `fn` with `ctx` as the ambient HttpContext, so code deep in
144
164
  * the call stack can reach it via {@link HttpContext.get}/{@link getOrFail}
@@ -165,6 +185,15 @@ export class HttpContext {
165
185
  return ctx
166
186
  }
167
187
 
188
+ /**
189
+ * Run `fn` with the ambient HttpContext temporarily cleared (AdonisJS
190
+ * `HttpContext.runOutsideContext`) — e.g. to detach background work so it
191
+ * doesn't inherit and pin the request's context.
192
+ */
193
+ static runOutsideContext<T>(fn: () => T): T {
194
+ return httpContextStorage.exit(fn)
195
+ }
196
+
168
197
  /** Unique request/correlation ID. */
169
198
  readonly id: string
170
199
 
@@ -181,7 +210,7 @@ export class HttpContext {
181
210
  readonly route: RouteInfo
182
211
 
183
212
  /** Authentication state — populated by auth middleware. */
184
- auth: AuthState = { authenticated: false }
213
+ auth: AuthState = { isAuthenticated: false }
185
214
 
186
215
  /**
187
216
  * Request locale — the primary subtag of the first `Accept-Language` entry
@@ -227,21 +256,50 @@ export class HttpContext {
227
256
  /** Lazily-built per-request logger (see {@link logger}). */
228
257
  #logger?: ContextLogger
229
258
 
259
+ /** App base logger, resolved once (async) by HttpKernel and injected. */
260
+ #baseLogger?: ChildLoggerSource
261
+
230
262
  /** Route URL resolver for redirect().toRoute(). */
231
263
  #routeUrlResolver?: RouteUrlResolver
232
264
 
233
265
  /**
234
- * Per-request logger (AdonisJS `ctx.logger`). Resolves the container `'logger'`
235
- * (e.g. `@c9up/spectrum`) child-scoped to this request's `id`, or falls back to
236
- * a console logger when none is registered. Built once, on first access.
266
+ * Per-request logger (AdonisJS `ctx.logger`) — the injected app logger (e.g.
267
+ * `@c9up/spectrum`) child-scoped to this request's `id`, or a console logger
268
+ * when none is registered. Built once, on first access.
237
269
  */
238
270
  get logger(): ContextLogger {
239
271
  if (!this.#logger) {
240
- this.#logger = resolveRequestLogger(this.containerResolver, this.id)
272
+ this.#logger = resolveRequestLogger(this.#baseLogger, this.id)
241
273
  }
242
274
  return this.#logger
243
275
  }
244
276
 
277
+ /** @internal Inject the app base logger (resolved async by HttpKernel). */
278
+ setBaseLogger(logger: ChildLoggerSource): void {
279
+ this.#baseLogger = logger
280
+ }
281
+
282
+ /** @internal Inject the APP_KEY cookie signer into request + response (from HttpKernel). */
283
+ setCookieSigner(signer: CookieSigner): void {
284
+ this.response.setCookieSigner(signer)
285
+ this.request.setCookieSigner(signer)
286
+ }
287
+
288
+ /** @internal Inject the APP_KEY signed-URL helper into the request (from HttpKernel). */
289
+ setSignedUrl(signedUrl: SignedUrl): void {
290
+ this.request.setSignedUrl(signedUrl)
291
+ }
292
+
293
+ /** Subdomains of the request host (AdonisJS `ctx.subdomains`). */
294
+ get subdomains(): string[] {
295
+ return this.request.subdomains()
296
+ }
297
+
298
+ /** Stable identifier for the matched route (`METHOD-pattern`) — AdonisJS `ctx.routeKey`. */
299
+ get routeKey(): string {
300
+ return `${this.request.method()}-${this.route.pattern}`
301
+ }
302
+
245
303
  constructor(
246
304
  id: string,
247
305
  rawRequest: RawRequest,
@@ -249,12 +307,23 @@ export class HttpContext {
249
307
  route: RouteInfo,
250
308
  containerResolver?: ContainerResolver,
251
309
  ) {
310
+ super()
252
311
  this.id = id
253
312
  this.params = params
254
313
  this.route = route
255
314
  this.containerResolver = containerResolver
256
315
  this.request = new Request(rawRequest, params)
257
316
  this.response = new Response()
317
+ // Give the response read access to the request (AdonisJS wires them too) —
318
+ // needed by `response.fresh()` for conditional-GET / ETag revalidation.
319
+ this.response.setRequest(this.request)
320
+ // And the reverse, so `request.fresh()`/`stale()` delegate to it, plus the
321
+ // matched-route info for `request.matchesRoute()`.
322
+ this.request.setResponse(this.response)
323
+ this.request.setRouteInfo({ name: route.name, pattern: route.pattern })
324
+ // APP_KEY-backed encryption / signed-URL services + the base logger are
325
+ // resolved asynchronously by HttpKernel and injected via the setters above
326
+ // (the container is async now, so a constructor can't resolve them itself).
258
327
  this.locale = parseAcceptLanguage(this.request.header('accept-language')) ?? 'en'
259
328
 
260
329
  // Wire redirect builder with request context
@@ -14,7 +14,10 @@ import type { Response } from './Response.js'
14
14
  export type RouteUrlResolver = (name: string, params?: Record<string, string>) => string
15
15
 
16
16
  function isSameOriginOrRelative(referer: string, requestUrl?: string): boolean {
17
- if (referer.startsWith('/') && !referer.startsWith('//')) return true
17
+ // A relative path is trusted only when it starts with a single "/" and has no
18
+ // backslash: browsers normalise "\" to "/", so "/\evil.com" would become the
19
+ // protocol-relative "//evil.com" that a lone `!startsWith("//")` check misses.
20
+ if (referer.startsWith('/') && !referer.startsWith('//') && !referer.includes('\\')) return true
18
21
  if (!requestUrl) return false
19
22
  try {
20
23
  const ref = new URL(referer)