@c9up/ream 0.1.24 → 0.1.26
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/ConfigLoader.d.ts +46 -5
- package/dist/ConfigLoader.d.ts.map +1 -1
- package/dist/ConfigLoader.js +60 -9
- package/dist/ConfigLoader.js.map +1 -1
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +46 -6
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +21 -3
- package/dist/Ignitor.js.map +1 -1
- package/dist/container/Container.d.ts +23 -3
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +139 -60
- package/dist/container/Container.js.map +1 -1
- package/dist/env/Env.d.ts +29 -4
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +27 -4
- package/dist/env/Env.js.map +1 -1
- package/dist/env/Secret.d.ts +11 -0
- package/dist/env/Secret.d.ts.map +1 -0
- package/dist/env/Secret.js +36 -0
- package/dist/env/Secret.js.map +1 -0
- package/dist/env/interpolate.d.ts +26 -0
- package/dist/env/interpolate.d.ts.map +1 -0
- package/dist/env/interpolate.js +48 -0
- package/dist/env/interpolate.js.map +1 -0
- package/dist/env/loadEnvFiles.d.ts.map +1 -1
- package/dist/env/loadEnvFiles.js +14 -4
- package/dist/env/loadEnvFiles.js.map +1 -1
- package/dist/env/schema.d.ts +23 -6
- package/dist/env/schema.d.ts.map +1 -1
- package/dist/env/schema.js +67 -33
- package/dist/env/schema.js.map +1 -1
- package/dist/events/Emitter.d.ts +1 -1
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +1 -1
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/EventsProvider.d.ts +2 -2
- package/dist/events/EventsProvider.d.ts.map +1 -1
- package/dist/events/EventsProvider.js +3 -3
- package/dist/events/EventsProvider.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +1 -1
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +2 -2
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/graphql/GraphQLProvider.d.ts.map +1 -1
- package/dist/graphql/GraphQLProvider.js +7 -3
- package/dist/graphql/GraphQLProvider.js.map +1 -1
- package/dist/http/Exception.d.ts +63 -1
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +185 -48
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/HttpContext.d.ts +56 -9
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +56 -15
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +4 -1
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts +141 -10
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +244 -34
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +176 -9
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +431 -19
- package/dist/http/Response.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.js +1 -1
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/openapi/OpenApiProvider.js +4 -4
- package/dist/openapi/OpenApiProvider.js.map +1 -1
- package/dist/router/Router.d.ts +223 -31
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +520 -102
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcProvider.d.ts +1 -0
- package/dist/rpc/RpcProvider.d.ts.map +1 -1
- package/dist/rpc/RpcProvider.js +7 -3
- package/dist/rpc/RpcProvider.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +2 -2
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +5 -4
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
- package/dist/scheduler/ScheduleProvider.js +7 -15
- package/dist/scheduler/ScheduleProvider.js.map +1 -1
- package/dist/server/Server.d.ts +17 -4
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js +41 -7
- package/dist/server/Server.js.map +1 -1
- package/dist/session/SessionMiddleware.js +3 -3
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/storage/StaticProvider.js +1 -1
- package/dist/storage/StaticProvider.js.map +1 -1
- package/dist/testing/RequestBuilder.d.ts +60 -0
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +180 -1
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/testing/TestClient.d.ts +22 -1
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +40 -0
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +30 -0
- package/dist/utils/Macroable.d.ts.map +1 -0
- package/dist/utils/Macroable.js +43 -0
- package/dist/utils/Macroable.js.map +1 -0
- package/dist/utils/inflect.d.ts +17 -0
- package/dist/utils/inflect.d.ts.map +1 -0
- package/dist/utils/inflect.js +32 -0
- package/dist/utils/inflect.js.map +1 -0
- package/dist/utils/objectPath.d.ts +57 -0
- package/dist/utils/objectPath.d.ts.map +1 -0
- package/dist/utils/objectPath.js +141 -0
- package/dist/utils/objectPath.js.map +1 -0
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- 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 +6 -1
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/ConfigLoader.ts +72 -9
- package/src/HttpKernel.ts +53 -15
- package/src/Ignitor.ts +24 -3
- package/src/container/Container.ts +145 -61
- package/src/env/Env.ts +49 -5
- package/src/env/Secret.ts +41 -0
- package/src/env/interpolate.ts +54 -0
- package/src/env/loadEnvFiles.ts +12 -4
- package/src/env/schema.ts +109 -35
- package/src/events/Emitter.ts +2 -2
- package/src/events/EventsProvider.ts +5 -5
- package/src/graphql/GraphQLEngine.ts +4 -4
- package/src/graphql/GraphQLProvider.ts +7 -3
- package/src/http/Exception.ts +214 -37
- package/src/http/HttpContext.ts +93 -20
- package/src/http/RedirectBuilder.ts +4 -1
- package/src/http/Request.ts +264 -34
- package/src/http/Response.ts +504 -30
- package/src/index.ts +11 -2
- package/src/middleware/Pipeline.ts +1 -1
- package/src/openapi/OpenApiProvider.ts +4 -4
- package/src/router/Router.ts +631 -113
- package/src/rpc/RpcProvider.ts +6 -2
- package/src/rpc/RpcRouter.ts +7 -6
- package/src/scheduler/ScheduleProvider.ts +7 -15
- package/src/server/Server.ts +49 -10
- package/src/session/SessionMiddleware.ts +4 -4
- package/src/storage/StaticProvider.ts +1 -1
- package/src/testing/RequestBuilder.ts +218 -1
- package/src/testing/TestClient.ts +57 -1
- package/src/utils/Macroable.ts +54 -0
- package/src/utils/inflect.ts +29 -0
- package/src/utils/objectPath.ts +142 -0
- package/dist/decorators/Lazy.d.ts +0 -19
- package/dist/decorators/Lazy.d.ts.map +0 -1
- package/dist/decorators/Lazy.js +0 -53
- package/dist/decorators/Lazy.js.map +0 -1
- package/src/decorators/Lazy.ts +0 -57
package/src/http/Exception.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
165
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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.
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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:
|
|
326
|
+
ctx.response.status(status).json({ error: payload })
|
|
198
327
|
}
|
|
199
328
|
|
|
200
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
|
373
|
+
console[level](`[${new Date().toISOString()}] ${error.message}`, context, error.stack)
|
|
227
374
|
} else {
|
|
228
|
-
console
|
|
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
|
-
/**
|
|
258
|
-
function
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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, '&')
|
package/src/http/HttpContext.ts
CHANGED
|
@@ -10,15 +10,23 @@
|
|
|
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'
|
|
15
|
+
import type { Session } from '../session/Session.js'
|
|
16
|
+
import { Macroable } from '../utils/Macroable.js'
|
|
13
17
|
import type { RouteUrlResolver } from './RedirectBuilder.js'
|
|
14
18
|
import { RedirectBuilder } from './RedirectBuilder.js'
|
|
15
19
|
import type { RawRequest } from './Request.js'
|
|
16
20
|
import { Request } from './Request.js'
|
|
17
21
|
import { Response } from './Response.js'
|
|
18
|
-
import type { Session } from '../session/Session.js'
|
|
19
22
|
|
|
20
23
|
export interface AuthState {
|
|
21
|
-
|
|
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
|
/**
|
|
@@ -60,9 +81,13 @@ export interface Authorizer {
|
|
|
60
81
|
* exposing it under the Adonis name with `.make()` keeps the public contract
|
|
61
82
|
* aligned so a future scoped resolver is a non-breaking swap.
|
|
62
83
|
*/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
84
|
+
// File-local: the app container's minimal resolver shape. Not exported — the
|
|
85
|
+
// public resolver contract is the events `ContainerResolver`; each subsystem
|
|
86
|
+
// declares its own structurally-compatible interface to stay decoupled (same
|
|
87
|
+
// pattern as warden/blackhole middleware), so nothing cross-imports it.
|
|
88
|
+
interface ContainerResolver {
|
|
89
|
+
/** Resolve/construct a service by token (class, string, or symbol). Async (AdonisJS parity). */
|
|
90
|
+
make<T>(token: ServiceToken): Promise<T>
|
|
66
91
|
}
|
|
67
92
|
|
|
68
93
|
/**
|
|
@@ -80,7 +105,7 @@ export interface ContextLogger {
|
|
|
80
105
|
}
|
|
81
106
|
|
|
82
107
|
/** A logger that can spawn a request-scoped child (e.g. spectrum's `child()`). */
|
|
83
|
-
interface ChildLoggerSource extends ContextLogger {
|
|
108
|
+
export interface ChildLoggerSource extends ContextLogger {
|
|
84
109
|
child?(options: { correlationId?: string; module?: string }): ContextLogger
|
|
85
110
|
}
|
|
86
111
|
|
|
@@ -100,17 +125,16 @@ function consoleLogger(correlationId: string): ContextLogger {
|
|
|
100
125
|
}
|
|
101
126
|
}
|
|
102
127
|
|
|
103
|
-
/**
|
|
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
|
+
*/
|
|
104
134
|
function resolveRequestLogger(
|
|
105
|
-
|
|
135
|
+
base: ChildLoggerSource | undefined,
|
|
106
136
|
correlationId: string,
|
|
107
137
|
): ContextLogger {
|
|
108
|
-
let base: ChildLoggerSource | undefined
|
|
109
|
-
try {
|
|
110
|
-
base = resolver?.make<ChildLoggerSource>('logger')
|
|
111
|
-
} catch {
|
|
112
|
-
base = undefined
|
|
113
|
-
}
|
|
114
138
|
if (!base) return consoleLogger(correlationId)
|
|
115
139
|
return typeof base.child === 'function' ? base.child({ correlationId }) : base
|
|
116
140
|
}
|
|
@@ -134,7 +158,7 @@ export interface RouteInfo {
|
|
|
134
158
|
/** Ambient per-request context store (AdonisJS `HttpContext` ALS accessor). */
|
|
135
159
|
const httpContextStorage = new AsyncLocalStorage<HttpContext>()
|
|
136
160
|
|
|
137
|
-
export class HttpContext {
|
|
161
|
+
export class HttpContext extends Macroable {
|
|
138
162
|
/**
|
|
139
163
|
* @internal Run `fn` with `ctx` as the ambient HttpContext, so code deep in
|
|
140
164
|
* the call stack can reach it via {@link HttpContext.get}/{@link getOrFail}
|
|
@@ -161,6 +185,15 @@ export class HttpContext {
|
|
|
161
185
|
return ctx
|
|
162
186
|
}
|
|
163
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
|
+
|
|
164
197
|
/** Unique request/correlation ID. */
|
|
165
198
|
readonly id: string
|
|
166
199
|
|
|
@@ -177,7 +210,7 @@ export class HttpContext {
|
|
|
177
210
|
readonly route: RouteInfo
|
|
178
211
|
|
|
179
212
|
/** Authentication state — populated by auth middleware. */
|
|
180
|
-
auth: AuthState = {
|
|
213
|
+
auth: AuthState = { isAuthenticated: false }
|
|
181
214
|
|
|
182
215
|
/**
|
|
183
216
|
* Request locale — the primary subtag of the first `Accept-Language` entry
|
|
@@ -223,21 +256,50 @@ export class HttpContext {
|
|
|
223
256
|
/** Lazily-built per-request logger (see {@link logger}). */
|
|
224
257
|
#logger?: ContextLogger
|
|
225
258
|
|
|
259
|
+
/** App base logger, resolved once (async) by HttpKernel and injected. */
|
|
260
|
+
#baseLogger?: ChildLoggerSource
|
|
261
|
+
|
|
226
262
|
/** Route URL resolver for redirect().toRoute(). */
|
|
227
263
|
#routeUrlResolver?: RouteUrlResolver
|
|
228
264
|
|
|
229
265
|
/**
|
|
230
|
-
* Per-request logger (AdonisJS `ctx.logger`)
|
|
231
|
-
*
|
|
232
|
-
*
|
|
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.
|
|
233
269
|
*/
|
|
234
270
|
get logger(): ContextLogger {
|
|
235
271
|
if (!this.#logger) {
|
|
236
|
-
this.#logger = resolveRequestLogger(this
|
|
272
|
+
this.#logger = resolveRequestLogger(this.#baseLogger, this.id)
|
|
237
273
|
}
|
|
238
274
|
return this.#logger
|
|
239
275
|
}
|
|
240
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
|
+
|
|
241
303
|
constructor(
|
|
242
304
|
id: string,
|
|
243
305
|
rawRequest: RawRequest,
|
|
@@ -245,12 +307,23 @@ export class HttpContext {
|
|
|
245
307
|
route: RouteInfo,
|
|
246
308
|
containerResolver?: ContainerResolver,
|
|
247
309
|
) {
|
|
310
|
+
super()
|
|
248
311
|
this.id = id
|
|
249
312
|
this.params = params
|
|
250
313
|
this.route = route
|
|
251
314
|
this.containerResolver = containerResolver
|
|
252
315
|
this.request = new Request(rawRequest, params)
|
|
253
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).
|
|
254
327
|
this.locale = parseAcceptLanguage(this.request.header('accept-language')) ?? 'en'
|
|
255
328
|
|
|
256
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
|
-
|
|
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)
|