@c9up/ream 0.2.1 → 0.2.2
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/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +15 -1
- package/dist/Ignitor.js.map +1 -1
- package/dist/errors/aggregate.d.ts +24 -0
- package/dist/errors/aggregate.d.ts.map +1 -0
- package/dist/errors/aggregate.js +24 -0
- package/dist/errors/aggregate.js.map +1 -0
- package/dist/events/services/main.d.ts.map +1 -1
- package/dist/events/services/main.js +3 -10
- package/dist/events/services/main.js.map +1 -1
- package/dist/http/Exception.d.ts +76 -6
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +178 -85
- package/dist/http/Exception.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/router/Router.js +1 -1
- package/dist/router/Router.js.map +1 -1
- package/dist/services/app.d.ts.map +1 -1
- package/dist/services/app.js +3 -21
- package/dist/services/app.js.map +1 -1
- package/dist/services/console.d.ts.map +1 -1
- package/dist/services/console.js +3 -19
- package/dist/services/console.js.map +1 -1
- package/dist/services/createServiceProxy.d.ts +19 -0
- package/dist/services/createServiceProxy.d.ts.map +1 -0
- package/dist/services/createServiceProxy.js +44 -0
- package/dist/services/createServiceProxy.js.map +1 -0
- package/dist/services/router.d.ts.map +1 -1
- package/dist/services/router.js +3 -12
- package/dist/services/router.js.map +1 -1
- package/dist/services/server.d.ts.map +1 -1
- package/dist/services/server.js +3 -22
- package/dist/services/server.js.map +1 -1
- package/dist/types/container.d.ts +64 -0
- package/dist/types/container.d.ts.map +1 -0
- package/dist/types/container.js +23 -0
- package/dist/types/container.js.map +1 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +9 -1
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Ignitor.ts +15 -1
- package/src/errors/aggregate.ts +33 -0
- package/src/events/services/main.ts +6 -12
- package/src/http/Exception.ts +225 -99
- package/src/index.ts +15 -2
- package/src/router/Router.ts +1 -1
- package/src/services/app.ts +6 -23
- package/src/services/console.ts +6 -21
- package/src/services/createServiceProxy.ts +47 -0
- package/src/services/router.ts +6 -14
- package/src/services/server.ts +6 -24
- package/src/types/container.ts +66 -0
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Populated by `EventsProvider.boot()`.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { createServiceProxy } from '../../services/createServiceProxy.js'
|
|
12
13
|
import type { Emitter } from '../Emitter.js'
|
|
13
14
|
|
|
14
15
|
let instance: Emitter | undefined
|
|
@@ -23,17 +24,10 @@ export function getEmitter(): Emitter | undefined {
|
|
|
23
24
|
return instance
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
const emitter: Emitter =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
'Check that `@c9up/ream/events/provider` is listed in your reamrc.ts providers.',
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
const value = Reflect.get(instance, prop, instance)
|
|
35
|
-
return typeof value === 'function' ? value.bind(instance) : value
|
|
36
|
-
},
|
|
37
|
-
})
|
|
27
|
+
const emitter: Emitter = createServiceProxy<Emitter>(
|
|
28
|
+
() => instance,
|
|
29
|
+
'[ream:events] Emitter singleton accessed before EventsProvider.boot() ran. ' +
|
|
30
|
+
'Check that `@c9up/ream/events/provider` is listed in your reamrc.ts providers.',
|
|
31
|
+
)
|
|
38
32
|
|
|
39
33
|
export default emitter
|
package/src/http/Exception.ts
CHANGED
|
@@ -154,15 +154,16 @@ export class E_VALIDATION_ERROR extends Exception {
|
|
|
154
154
|
static override status = 422
|
|
155
155
|
static override code = 'E_VALIDATION_ERROR'
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Per-field failures. Named `messages` because that is what VineJS, rune and
|
|
159
|
+
* AdonisJS's own validation renderers read — a handler copied from an Adonis
|
|
160
|
+
* app reaches for `error.messages`.
|
|
161
|
+
*/
|
|
162
|
+
messages: unknown[]
|
|
163
|
+
|
|
164
|
+
constructor(messages: unknown[]) {
|
|
165
|
+
super('Validation failure', { status: 422, code: 'E_VALIDATION_ERROR' })
|
|
166
|
+
this.messages = messages
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
|
|
@@ -233,8 +234,34 @@ export class E_HTTP_REQUEST_ABORTED extends Exception {
|
|
|
233
234
|
* 2. Else → this.handle() → content negotiation (JSON or HTML)
|
|
234
235
|
* 3. Then → this.report() → logging/monitoring
|
|
235
236
|
*/
|
|
237
|
+
/**
|
|
238
|
+
* A thrown value, normalised into the shape every renderer reads.
|
|
239
|
+
*
|
|
240
|
+
* AdonisJS hands this to `renderError*` / `renderValidationError*` and to the
|
|
241
|
+
* status-page renderers, so an app that overrides one of them receives the same
|
|
242
|
+
* object it would there — a plain record, never an `Error` subclass, because
|
|
243
|
+
* what is thrown is not always one.
|
|
244
|
+
*/
|
|
245
|
+
export type HttpError = {
|
|
246
|
+
message: string
|
|
247
|
+
status: number
|
|
248
|
+
code: string
|
|
249
|
+
stack?: string
|
|
250
|
+
cause?: unknown
|
|
251
|
+
/** Per-field failures, on a validation error (VineJS / rune shape). */
|
|
252
|
+
messages?: unknown
|
|
253
|
+
/** Extra detail an exception chose to carry. */
|
|
254
|
+
errors?: unknown
|
|
255
|
+
/** Set when the thrown value handles its own response. */
|
|
256
|
+
handle?: (error: unknown, ctx: HttpContext) => Promise<void> | void
|
|
257
|
+
/** Set when the thrown value reports itself. */
|
|
258
|
+
report?: (error: unknown, ctx: HttpContext) => Promise<void> | void
|
|
259
|
+
/** A hint appended to the message, when the exception carries one. */
|
|
260
|
+
help?: string
|
|
261
|
+
}
|
|
262
|
+
|
|
236
263
|
/** Renders an HTML error page for a status (AdonisJS `StatusPageRenderer`). */
|
|
237
|
-
export type StatusPageRenderer = (error:
|
|
264
|
+
export type StatusPageRenderer = (error: HttpError, ctx: HttpContext) => Promise<string> | string
|
|
238
265
|
|
|
239
266
|
/** A constructor usable in `instanceof` for {@link ExceptionHandler.ignoreExceptions}. */
|
|
240
267
|
export type ExceptionClass = new (...args: never[]) => Error
|
|
@@ -268,18 +295,17 @@ export class ExceptionHandler extends Macroable {
|
|
|
268
295
|
}
|
|
269
296
|
|
|
270
297
|
/** Log level for an error by status: 5xx→error, 4xx→warn, else info (AdonisJS). */
|
|
271
|
-
protected getErrorLogLevel(
|
|
272
|
-
if (status >= 500) return 'error'
|
|
273
|
-
if (status >= 400) return 'warn'
|
|
298
|
+
protected getErrorLogLevel(error: HttpError): LogLevel {
|
|
299
|
+
if (error.status >= 500) return 'error'
|
|
300
|
+
if (error.status >= 400) return 'warn'
|
|
274
301
|
return 'info'
|
|
275
302
|
}
|
|
276
303
|
|
|
277
304
|
/** Whether an error should be reported — honours every ignore list (AdonisJS `shouldReport`). */
|
|
278
|
-
protected shouldReport(error:
|
|
305
|
+
protected shouldReport(error: HttpError): boolean {
|
|
279
306
|
if (!this.reportErrors) return false
|
|
280
|
-
|
|
281
|
-
if (this.
|
|
282
|
-
if (this.ignoreCodes.includes(code)) return false
|
|
307
|
+
if (this.ignoreStatuses.includes(error.status)) return false
|
|
308
|
+
if (this.ignoreCodes.includes(error.code)) return false
|
|
283
309
|
if (this.ignoreExceptions.some((exception) => error instanceof exception)) return false
|
|
284
310
|
return true
|
|
285
311
|
}
|
|
@@ -297,95 +323,185 @@ export class ExceptionHandler extends Macroable {
|
|
|
297
323
|
}
|
|
298
324
|
|
|
299
325
|
/** Convert an exception to an HTTP response. */
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
this.#sendJsonError(error, ctx)
|
|
321
|
-
}
|
|
326
|
+
/**
|
|
327
|
+
* Normalise any thrown value into an {@link HttpError}.
|
|
328
|
+
*
|
|
329
|
+
* Everything downstream — the renderers, the status pages, the report path —
|
|
330
|
+
* reads this shape, so a handler override never has to re-sniff whether it
|
|
331
|
+
* was handed an `Exception`, a plain object, or a string.
|
|
332
|
+
*/
|
|
333
|
+
protected toHttpError(error: unknown): HttpError {
|
|
334
|
+
// AdonisJS returns the thrown object ITSELF, filled in — not a copy. That
|
|
335
|
+
// identity is load-bearing: `ignoreExceptions` matches with `instanceof`,
|
|
336
|
+
// and `handle`/`report` have to stay bound to their own instance. A copy
|
|
337
|
+
// would quietly break both.
|
|
338
|
+
const source: object =
|
|
339
|
+
typeof error === 'object' && error !== null ? error : new Error(String(error))
|
|
340
|
+
const { status, code } = extractErrorMeta(error)
|
|
341
|
+
const message =
|
|
342
|
+
'message' in source && typeof source.message === 'string' && source.message.length > 0
|
|
343
|
+
? source.message
|
|
344
|
+
: 'Internal server error'
|
|
345
|
+
return Object.assign(source, { status, code, message })
|
|
322
346
|
}
|
|
323
347
|
|
|
324
|
-
/**
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
348
|
+
/**
|
|
349
|
+
* The message a client is allowed to see.
|
|
350
|
+
*
|
|
351
|
+
* DEVIATION (named): outside debug mode ream only echoes the message of a
|
|
352
|
+
* deliberate `Exception`; anything else collapses to a generic line. AdonisJS
|
|
353
|
+
* echoes `error.message` whatever was thrown, which leaks the text of driver
|
|
354
|
+
* and library errors to the client.
|
|
355
|
+
*/
|
|
356
|
+
protected publicMessage(error: HttpError, ctx: HttpContext): string {
|
|
357
|
+
if (this.isDebuggingEnabled(ctx)) return error.message
|
|
358
|
+
if (error.code !== 'E_UNKNOWN') return error.message
|
|
328
359
|
return 'An internal error occurred'
|
|
329
360
|
}
|
|
330
361
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const payload: Record<string, unknown> = {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
payload.stack = error.stack
|
|
362
|
+
/** Render an error as JSON. */
|
|
363
|
+
async renderErrorAsJSON(error: HttpError, ctx: HttpContext): Promise<void> {
|
|
364
|
+
const payload: Record<string, unknown> = {
|
|
365
|
+
code: error.code,
|
|
366
|
+
message: this.publicMessage(error, ctx),
|
|
337
367
|
}
|
|
338
|
-
|
|
368
|
+
if (error.help) payload.help = error.help
|
|
369
|
+
if (this.isDebuggingEnabled(ctx) && error.stack) payload.stack = error.stack
|
|
370
|
+
ctx.response.status(error.status).json({ error: payload })
|
|
339
371
|
}
|
|
340
372
|
|
|
341
|
-
|
|
342
|
-
|
|
373
|
+
/** Render an error as a JSON:API document. */
|
|
374
|
+
async renderErrorAsJSONAPI(error: HttpError, ctx: HttpContext): Promise<void> {
|
|
343
375
|
ctx.response
|
|
344
|
-
.status(status)
|
|
376
|
+
.status(error.status)
|
|
345
377
|
.type('application/vnd.api+json')
|
|
346
|
-
.json({
|
|
378
|
+
.json({
|
|
379
|
+
errors: [
|
|
380
|
+
{
|
|
381
|
+
title: this.publicMessage(error, ctx),
|
|
382
|
+
code: error.code,
|
|
383
|
+
status: String(error.status),
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
})
|
|
347
387
|
}
|
|
348
388
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
// A registered status page wins (production browser error pages).
|
|
389
|
+
/** Render an error as HTML, preferring a registered status page. */
|
|
390
|
+
async renderErrorAsHTML(error: HttpError, ctx: HttpContext): Promise<void> {
|
|
352
391
|
if (this.renderStatusPages) {
|
|
353
|
-
const renderer = this.#expandStatusPages()[status]
|
|
392
|
+
const renderer = this.#expandStatusPages()[error.status]
|
|
354
393
|
if (renderer) {
|
|
355
394
|
const html = await renderer(error, ctx)
|
|
356
|
-
ctx.response.status(status).type('text/html; charset=utf-8').send(html)
|
|
395
|
+
ctx.response.status(error.status).type('text/html; charset=utf-8').send(html)
|
|
357
396
|
return
|
|
358
397
|
}
|
|
359
398
|
}
|
|
360
|
-
const message = this
|
|
399
|
+
const message = this.publicMessage(error, ctx)
|
|
361
400
|
ctx.response
|
|
362
|
-
.status(status)
|
|
401
|
+
.status(error.status)
|
|
363
402
|
.type('text/html; charset=utf-8')
|
|
364
403
|
.send(
|
|
365
|
-
`<!DOCTYPE html><html><head><title>Error ${status}</title></head>` +
|
|
366
|
-
`<body><h1>${status}</h1><p>${escapeHtml(message)}</p></body></html>`,
|
|
404
|
+
`<!DOCTYPE html><html><head><title>Error ${error.status}</title></head>` +
|
|
405
|
+
`<body><h1>${error.status}</h1><p>${escapeHtml(message)}</p></body></html>`,
|
|
367
406
|
)
|
|
368
407
|
}
|
|
369
408
|
|
|
409
|
+
/** Render per-field validation failures as JSON. */
|
|
410
|
+
async renderValidationErrorAsJSON(error: HttpError, ctx: HttpContext): Promise<void> {
|
|
411
|
+
ctx.response.status(error.status).json({ errors: error.messages })
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Render per-field validation failures as a JSON:API document. */
|
|
415
|
+
async renderValidationErrorAsJSONAPI(error: HttpError, ctx: HttpContext): Promise<void> {
|
|
416
|
+
const messages = Array.isArray(error.messages) ? error.messages : []
|
|
417
|
+
ctx.response
|
|
418
|
+
.status(error.status)
|
|
419
|
+
.type('application/vnd.api+json')
|
|
420
|
+
.json({ errors: messages.map(toJsonApiValidationError) })
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Render per-field validation failures as HTML. */
|
|
424
|
+
async renderValidationErrorAsHTML(error: HttpError, ctx: HttpContext): Promise<void> {
|
|
425
|
+
const messages = Array.isArray(error.messages) ? error.messages : []
|
|
426
|
+
ctx.response
|
|
427
|
+
.status(error.status)
|
|
428
|
+
.type('text/html; charset=utf-8')
|
|
429
|
+
.send(
|
|
430
|
+
messages
|
|
431
|
+
.map((message) => {
|
|
432
|
+
const { field, text } = readValidationMessage(message)
|
|
433
|
+
return `${escapeHtml(field)} - ${escapeHtml(text)}`
|
|
434
|
+
})
|
|
435
|
+
.join('<br />'),
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Pick a renderer by content negotiation.
|
|
441
|
+
*
|
|
442
|
+
* DEVIATION (named): ream lists `json` FIRST, so a request with no or `*` /`*`
|
|
443
|
+
* Accept defaults to JSON (API-first). AdonisJS lists `html` first
|
|
444
|
+
* (full-stack default). Explicit `text/html` / `vnd.api+json` are honoured
|
|
445
|
+
* identically.
|
|
446
|
+
*/
|
|
447
|
+
async renderError(error: HttpError, ctx: HttpContext): Promise<void> {
|
|
448
|
+
switch (ctx.request.accepts(['json', 'html', 'application/vnd.api+json'])) {
|
|
449
|
+
case 'application/vnd.api+json':
|
|
450
|
+
return this.renderErrorAsJSONAPI(error, ctx)
|
|
451
|
+
case 'html':
|
|
452
|
+
return this.renderErrorAsHTML(error, ctx)
|
|
453
|
+
default:
|
|
454
|
+
return this.renderErrorAsJSON(error, ctx)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Pick a validation renderer by content negotiation. */
|
|
459
|
+
async renderValidationError(error: HttpError, ctx: HttpContext): Promise<void> {
|
|
460
|
+
switch (ctx.request.accepts(['json', 'html', 'application/vnd.api+json'])) {
|
|
461
|
+
case 'application/vnd.api+json':
|
|
462
|
+
return this.renderValidationErrorAsJSONAPI(error, ctx)
|
|
463
|
+
case 'html':
|
|
464
|
+
return this.renderValidationErrorAsHTML(error, ctx)
|
|
465
|
+
default:
|
|
466
|
+
return this.renderValidationErrorAsJSON(error, ctx)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Convert an exception to an HTTP response. */
|
|
471
|
+
async handle(error: unknown, ctx: HttpContext): Promise<void> {
|
|
472
|
+
const httpError = this.toHttpError(error)
|
|
473
|
+
|
|
474
|
+
// Self-handled errors take over entirely (duck-typed, per AdonisJS — any
|
|
475
|
+
// thrown value exposing a `handle()` method, not just `Exception` instances).
|
|
476
|
+
if (typeof httpError.handle === 'function') {
|
|
477
|
+
await httpError.handle(httpError, ctx)
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Validation failures have per-field detail and their own renderers. Keyed
|
|
482
|
+
// on the code plus the presence of `messages`, exactly as AdonisJS does, so
|
|
483
|
+
// rune's and VineJS's errors both land here.
|
|
484
|
+
if (httpError.code === 'E_VALIDATION_ERROR' && httpError.messages !== undefined) {
|
|
485
|
+
await this.renderValidationError(httpError, ctx)
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
await this.renderError(httpError, ctx)
|
|
490
|
+
}
|
|
491
|
+
|
|
370
492
|
/** Log/report an exception. Override for custom monitoring. */
|
|
371
493
|
async report(error: unknown, ctx: HttpContext): Promise<void> {
|
|
372
|
-
|
|
494
|
+
const httpError = this.toHttpError(error)
|
|
495
|
+
if (!this.shouldReport(httpError)) return
|
|
373
496
|
|
|
374
497
|
// Self-reported exceptions handle their own reporting.
|
|
375
|
-
if (
|
|
376
|
-
await
|
|
498
|
+
if (typeof httpError.report === 'function') {
|
|
499
|
+
await httpError.report(httpError, ctx)
|
|
377
500
|
return
|
|
378
501
|
}
|
|
379
502
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const level = this.getErrorLogLevel(status)
|
|
383
|
-
const context = this.context(ctx)
|
|
384
|
-
if (error instanceof Error) {
|
|
385
|
-
console[level](`[${new Date().toISOString()}] ${error.message}`, context, error.stack)
|
|
386
|
-
} else {
|
|
387
|
-
console[level](`[${new Date().toISOString()}] Unknown error:`, error, context)
|
|
388
|
-
}
|
|
503
|
+
const level = this.getErrorLogLevel(httpError)
|
|
504
|
+
ctx.logger[level](httpError.message, this.context(ctx))
|
|
389
505
|
}
|
|
390
506
|
|
|
391
507
|
/** Provide additional context for error reports. Override for custom data. */
|
|
@@ -399,6 +515,40 @@ export class ExceptionHandler extends Macroable {
|
|
|
399
515
|
}
|
|
400
516
|
}
|
|
401
517
|
|
|
518
|
+
/**
|
|
519
|
+
* The fields a validation message carries (VineJS / rune `RuneErrorNode`).
|
|
520
|
+
*
|
|
521
|
+
* Read defensively: the messages come from whichever validator the app wired,
|
|
522
|
+
* and a renderer must not throw while rendering someone else's error.
|
|
523
|
+
*/
|
|
524
|
+
function readValidationMessage(message: unknown): {
|
|
525
|
+
field: string
|
|
526
|
+
text: string
|
|
527
|
+
rule: string
|
|
528
|
+
meta: unknown
|
|
529
|
+
} {
|
|
530
|
+
if (message === null || typeof message !== 'object') {
|
|
531
|
+
return { field: '', text: String(message), rule: '', meta: undefined }
|
|
532
|
+
}
|
|
533
|
+
const field = 'field' in message && typeof message.field === 'string' ? message.field : ''
|
|
534
|
+
const text = 'message' in message && typeof message.message === 'string' ? message.message : ''
|
|
535
|
+
const rule = 'rule' in message && typeof message.rule === 'string' ? message.rule : ''
|
|
536
|
+
const meta = 'meta' in message ? message.meta : undefined
|
|
537
|
+
return { field, text, rule, meta }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** One validation message, as a JSON:API error object (AdonisJS shape). */
|
|
541
|
+
function toJsonApiValidationError(message: unknown): Record<string, unknown> {
|
|
542
|
+
const { field, text, rule, meta } = readValidationMessage(message)
|
|
543
|
+
const error: Record<string, unknown> = {
|
|
544
|
+
title: text,
|
|
545
|
+
code: rule,
|
|
546
|
+
source: { pointer: field },
|
|
547
|
+
}
|
|
548
|
+
if (meta !== undefined) error.meta = meta
|
|
549
|
+
return error
|
|
550
|
+
}
|
|
551
|
+
|
|
402
552
|
/** Extract status + code from an Exception, ReamError, or duck-typed error. */
|
|
403
553
|
function extractErrorMeta(error: unknown): { status: number; code: string } {
|
|
404
554
|
let status = 500
|
|
@@ -413,30 +563,6 @@ function extractErrorMeta(error: unknown): { status: number; code: string } {
|
|
|
413
563
|
return { status, code }
|
|
414
564
|
}
|
|
415
565
|
|
|
416
|
-
/** True when a thrown value self-handles via a `handle()` method (AdonisJS duck-type). */
|
|
417
|
-
function isSelfHandling(
|
|
418
|
-
error: unknown,
|
|
419
|
-
): error is { handle: (error: unknown, ctx: HttpContext) => Promise<void> | void } {
|
|
420
|
-
return (
|
|
421
|
-
error !== null &&
|
|
422
|
-
typeof error === 'object' &&
|
|
423
|
-
'handle' in error &&
|
|
424
|
-
typeof error.handle === 'function'
|
|
425
|
-
)
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/** True when a thrown value self-reports via a `report()` method. */
|
|
429
|
-
function isSelfReporting(
|
|
430
|
-
error: unknown,
|
|
431
|
-
): error is { report: (error: unknown, ctx: HttpContext) => Promise<void> | void } {
|
|
432
|
-
return (
|
|
433
|
-
error !== null &&
|
|
434
|
-
typeof error === 'object' &&
|
|
435
|
-
'report' in error &&
|
|
436
|
-
typeof error.report === 'function'
|
|
437
|
-
)
|
|
438
|
-
}
|
|
439
|
-
|
|
440
566
|
/** Expand a status-page key into a per-code map: `'500..599'` → {500,…,599}, `'404'` → {404}. */
|
|
441
567
|
function parseStatusRange(
|
|
442
568
|
range: string,
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export {
|
|
|
40
40
|
createError,
|
|
41
41
|
E_FORBIDDEN,
|
|
42
42
|
E_HTTP_EXCEPTION,
|
|
43
|
+
E_HTTP_REQUEST_ABORTED,
|
|
43
44
|
E_ROUTE_NOT_FOUND,
|
|
44
45
|
E_ROW_NOT_FOUND,
|
|
45
46
|
E_UNAUTHORIZED,
|
|
@@ -48,6 +49,7 @@ export {
|
|
|
48
49
|
type ExceptionClass,
|
|
49
50
|
type ExceptionConstructor,
|
|
50
51
|
ExceptionHandler,
|
|
52
|
+
type HttpError,
|
|
51
53
|
InvalidArgumentsException,
|
|
52
54
|
RuntimeException,
|
|
53
55
|
type StatusPageRenderer,
|
|
@@ -58,6 +60,8 @@ export { RedirectBuilder } from './http/RedirectBuilder.js'
|
|
|
58
60
|
export type { RawRequest } from './http/Request.js'
|
|
59
61
|
export { Request } from './http/Request.js'
|
|
60
62
|
export { Response } from './http/Response.js'
|
|
63
|
+
export type { SseStreamOptions } from './http/SseStream.js'
|
|
64
|
+
export { SseStream } from './http/SseStream.js'
|
|
61
65
|
export { type GetterFn, Macroable, type MacroFn } from './utils/Macroable.js'
|
|
62
66
|
|
|
63
67
|
// ─── Session ────────────────────────────────────────────────
|
|
@@ -77,6 +81,8 @@ export type {
|
|
|
77
81
|
SessionRedisClientSource,
|
|
78
82
|
} from './session/drivers/RedisDriver.js'
|
|
79
83
|
export { RedisDriver as SessionRedisDriver } from './session/drivers/RedisDriver.js'
|
|
84
|
+
export type { ValuePath } from './session/ReadOnlyValuesStore.js'
|
|
85
|
+
export { ReadOnlyValuesStore } from './session/ReadOnlyValuesStore.js'
|
|
80
86
|
export type {
|
|
81
87
|
SessionConfig,
|
|
82
88
|
SessionDriver,
|
|
@@ -139,7 +145,11 @@ export { StaticProvider } from './storage/StaticProvider.js'
|
|
|
139
145
|
|
|
140
146
|
// Request-filter security (CSRF, XSS, rate-limit, CORS, headers, path/param)
|
|
141
147
|
// lives in @c9up/blackhole. ream core keeps only the crypto primitives.
|
|
142
|
-
export {
|
|
148
|
+
export {
|
|
149
|
+
CookieSigner,
|
|
150
|
+
E_INSECURE_APP_KEY,
|
|
151
|
+
E_MISSING_APP_KEY,
|
|
152
|
+
} from './security/CookieSigner.js'
|
|
143
153
|
export {
|
|
144
154
|
constantTimeEq,
|
|
145
155
|
hasNativeCrypto,
|
|
@@ -179,6 +189,7 @@ export {
|
|
|
179
189
|
matchers,
|
|
180
190
|
OnRouteBuilder,
|
|
181
191
|
RouteBuilder,
|
|
192
|
+
RouteResource,
|
|
182
193
|
Router,
|
|
183
194
|
UrlBuilder,
|
|
184
195
|
} from './router/Router.js'
|
|
@@ -238,8 +249,10 @@ export { ConsoleKernel, defineConfig, Ignitor, prettyPrintError } from './Ignito
|
|
|
238
249
|
|
|
239
250
|
// ─── Providers ──────────────────────────────────────────────
|
|
240
251
|
|
|
252
|
+
export { errors } from './errors/aggregate.js'
|
|
241
253
|
export type { AppContext, ProviderContract } from './Provider.js'
|
|
242
254
|
export { Provider } from './Provider.js'
|
|
255
|
+
export type { ContainerBinding, ContainerBindings } from './types/container.js'
|
|
243
256
|
|
|
244
257
|
// ─── Codemods ───────────────────────────────────────────────
|
|
245
258
|
|
|
@@ -349,4 +362,4 @@ export {
|
|
|
349
362
|
StatsTracker,
|
|
350
363
|
} from './scheduler/index.js'
|
|
351
364
|
|
|
352
|
-
export const VERSION = '0.
|
|
365
|
+
export const VERSION = '0.2.2'
|
package/src/router/Router.ts
CHANGED
|
@@ -1236,7 +1236,7 @@ export class Router extends Macroable {
|
|
|
1236
1236
|
makeSignedUrl(name: string, params?: Record<string, string>, options?: SignedUrlOptions): string {
|
|
1237
1237
|
if (!this.#signedUrl) {
|
|
1238
1238
|
throw new Error(
|
|
1239
|
-
'[
|
|
1239
|
+
'[E_MISSING_APP_KEY] makeSignedUrl() requires an APP_KEY-backed signer. Set APP_KEY (>= 16 chars) so the encryption service is registered.',
|
|
1240
1240
|
)
|
|
1241
1241
|
}
|
|
1242
1242
|
const path = this.urlFor(name, params)
|
package/src/services/app.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { Application } from '../Application.js'
|
|
11
|
+
import { createServiceProxy } from './createServiceProxy.js'
|
|
11
12
|
|
|
12
13
|
let instance: Application | undefined
|
|
13
14
|
|
|
@@ -25,28 +26,10 @@ export function clearApp(app: Application): void {
|
|
|
25
26
|
if (instance === app) instance = undefined
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
const app: Application =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// `import { setX } from '.../services/x'` into a crash at import time, far
|
|
34
|
-
// from any real use. They are not members of what this stands in for, so
|
|
35
|
-
// answer undefined and let a genuine access be the one that reports.
|
|
36
|
-
if (typeof prop === 'symbol' || prop === 'then') {
|
|
37
|
-
return undefined
|
|
38
|
-
}
|
|
39
|
-
if (!instance) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
'Application accessed before initialization. ' +
|
|
42
|
-
'Ensure this code runs during or after the boot phase.',
|
|
43
|
-
)
|
|
44
|
-
}
|
|
45
|
-
// Bind methods so private-field writes inside the class resolve
|
|
46
|
-
// against the real instance brand instead of the Proxy.
|
|
47
|
-
const value = Reflect.get(instance, prop, instance)
|
|
48
|
-
return typeof value === 'function' ? value.bind(instance) : value
|
|
49
|
-
},
|
|
50
|
-
})
|
|
29
|
+
const app: Application = createServiceProxy<Application>(
|
|
30
|
+
() => instance,
|
|
31
|
+
'Application accessed before initialization. ' +
|
|
32
|
+
'Ensure this code runs during or after the boot phase.',
|
|
33
|
+
)
|
|
51
34
|
|
|
52
35
|
export default app
|
package/src/services/console.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { Console } from '../console/Console.js'
|
|
12
|
+
import { createServiceProxy } from './createServiceProxy.js'
|
|
12
13
|
|
|
13
14
|
let instance: Console | undefined
|
|
14
15
|
|
|
@@ -26,26 +27,10 @@ export function clearConsole(consoleApp: Console): void {
|
|
|
26
27
|
if (instance === consoleApp) instance = undefined
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
const consoleApp: Console =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// `import { setX } from '.../services/x'` into a crash at import time, far
|
|
35
|
-
// from any real use. They are not members of what this stands in for, so
|
|
36
|
-
// answer undefined and let a genuine access be the one that reports.
|
|
37
|
-
if (typeof prop === 'symbol' || prop === 'then') {
|
|
38
|
-
return undefined
|
|
39
|
-
}
|
|
40
|
-
if (!instance) {
|
|
41
|
-
throw new Error(
|
|
42
|
-
'Console accessed before initialization. ' +
|
|
43
|
-
'Ensure this code runs during or after the boot phase.',
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
const value = Reflect.get(instance, prop, instance)
|
|
47
|
-
return typeof value === 'function' ? value.bind(instance) : value
|
|
48
|
-
},
|
|
49
|
-
})
|
|
30
|
+
const consoleApp: Console = createServiceProxy<Console>(
|
|
31
|
+
() => instance,
|
|
32
|
+
'Console accessed before initialization. ' +
|
|
33
|
+
'Ensure this code runs during or after the boot phase.',
|
|
34
|
+
)
|
|
50
35
|
|
|
51
36
|
export default consoleApp
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one implementation behind every `@c9up/ream/services/*` accessor.
|
|
3
|
+
*
|
|
4
|
+
* Each accessor stands in for a singleton that only exists once the Ignitor has
|
|
5
|
+
* built it, so the module-level export is a proxy that forwards to whatever the
|
|
6
|
+
* boot phase installed. Written out per service, the proxy was copied five
|
|
7
|
+
* times and the `symbol`/`then` guard below reached only three of them — a fix
|
|
8
|
+
* that has to be applied by hand five times is a fix that will be missed.
|
|
9
|
+
*
|
|
10
|
+
* @see services/app.ts for the accessor shape this backs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build the proxy an accessor module default-exports.
|
|
15
|
+
*
|
|
16
|
+
* @param read Returns the live singleton, or `undefined` before boot.
|
|
17
|
+
* @param notReady Message thrown when something reads the accessor too early.
|
|
18
|
+
*/
|
|
19
|
+
export function createServiceProxy<T extends object>(
|
|
20
|
+
read: () => T | undefined,
|
|
21
|
+
notReady: string,
|
|
22
|
+
): T {
|
|
23
|
+
// The target is never read through — every trap forwards to `read()`.
|
|
24
|
+
const target: T = Object.create(null)
|
|
25
|
+
return new Proxy(target, {
|
|
26
|
+
get(_target, prop) {
|
|
27
|
+
// A module loader inspects what it imports before anyone uses it: it
|
|
28
|
+
// reads `then` to decide whether the namespace is thenable, and various
|
|
29
|
+
// symbols for interop and formatting. Throwing on those turns a plain
|
|
30
|
+
// `import { setX } from '.../services/x'` into a crash at import time,
|
|
31
|
+
// far from any real use. They are not members of what this stands in
|
|
32
|
+
// for, so answer undefined and let a genuine access be the one that
|
|
33
|
+
// reports.
|
|
34
|
+
if (typeof prop === 'symbol' || prop === 'then') {
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
const instance = read()
|
|
38
|
+
if (!instance) {
|
|
39
|
+
throw new Error(notReady)
|
|
40
|
+
}
|
|
41
|
+
// Bind methods so private-field writes inside the class resolve against
|
|
42
|
+
// the real instance brand instead of the Proxy.
|
|
43
|
+
const value = Reflect.get(instance, prop, instance)
|
|
44
|
+
return typeof value === 'function' ? value.bind(instance) : value
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
}
|