@c9up/ream 0.2.28 → 0.2.30
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/README.md +5 -6
- package/dist/Codemods.d.ts +24 -0
- package/dist/Codemods.d.ts.map +1 -1
- package/dist/Codemods.js +211 -1
- package/dist/Codemods.js.map +1 -1
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +17 -5
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +15 -2
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +33 -23
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +4 -4
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +41 -11
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts +21 -15
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +40 -58
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/bodyparser/parseSize.d.ts +7 -4
- package/dist/bodyparser/parseSize.d.ts.map +1 -1
- package/dist/bodyparser/parseSize.js +12 -17
- package/dist/bodyparser/parseSize.js.map +1 -1
- package/dist/bodyparser/qsParse.d.ts +20 -1
- package/dist/bodyparser/qsParse.d.ts.map +1 -1
- package/dist/bodyparser/qsParse.js +78 -25
- package/dist/bodyparser/qsParse.js.map +1 -1
- package/dist/codemods/source.d.ts +57 -0
- package/dist/codemods/source.d.ts.map +1 -0
- package/dist/codemods/source.js +282 -0
- package/dist/codemods/source.js.map +1 -0
- package/dist/console/prompts.d.ts.map +1 -1
- package/dist/console/prompts.js +3 -1
- package/dist/console/prompts.js.map +1 -1
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +61 -71
- package/dist/container/Container.js.map +1 -1
- package/dist/events/Emitter.d.ts +12 -0
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +36 -9
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/helix/helpers.d.ts.map +1 -1
- package/dist/events/helix/helpers.js +19 -9
- package/dist/events/helix/helpers.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +8 -0
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +79 -12
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/helpers/MessageBuilder.d.ts +4 -2
- package/dist/helpers/MessageBuilder.d.ts.map +1 -1
- package/dist/helpers/MessageBuilder.js +4 -2
- package/dist/helpers/MessageBuilder.js.map +1 -1
- package/dist/helpers/errors.d.ts +11 -0
- package/dist/helpers/errors.d.ts.map +1 -0
- package/dist/helpers/errors.js +17 -0
- package/dist/helpers/errors.js.map +1 -0
- package/dist/helpers/napi-loader.d.ts.map +1 -1
- package/dist/helpers/napi-loader.js +3 -2
- package/dist/helpers/napi-loader.js.map +1 -1
- package/dist/http/Exception.d.ts +14 -1
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +10 -2
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/HttpContext.d.ts +31 -2
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +12 -5
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts +9 -3
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +43 -37
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts +42 -17
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +80 -58
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +7 -4
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +14 -6
- package/dist/http/Response.js.map +1 -1
- package/dist/http/SseStream.js +6 -7
- package/dist/http/SseStream.js.map +1 -1
- package/dist/http/permissions.d.ts +25 -0
- package/dist/http/permissions.d.ts.map +1 -0
- package/dist/http/permissions.js +38 -0
- package/dist/http/permissions.js.map +1 -0
- package/dist/http-client/HttpClient.d.ts +111 -0
- package/dist/http-client/HttpClient.d.ts.map +1 -0
- package/dist/http-client/HttpClient.js +386 -0
- package/dist/http-client/HttpClient.js.map +1 -0
- package/dist/http-client/addresses.d.ts +14 -0
- package/dist/http-client/addresses.d.ts.map +1 -0
- package/dist/http-client/addresses.js +57 -0
- package/dist/http-client/addresses.js.map +1 -0
- package/dist/http-client/fake.d.ts +50 -0
- package/dist/http-client/fake.d.ts.map +1 -0
- package/dist/http-client/fake.js +95 -0
- package/dist/http-client/fake.js.map +1 -0
- package/dist/http-client/index.d.ts +7 -0
- package/dist/http-client/index.d.ts.map +1 -0
- package/dist/http-client/index.js +7 -0
- package/dist/http-client/index.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.d.ts.map +1 -1
- package/dist/middleware/Pipeline.js +6 -3
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/native/generated.d.ts +93 -36
- package/dist/native/generated.d.ts.map +1 -1
- package/dist/profiler/Profiler.d.ts.map +1 -1
- package/dist/profiler/Profiler.js +4 -1
- package/dist/profiler/Profiler.js.map +1 -1
- package/dist/profiler/client.generated.d.ts +5 -0
- package/dist/profiler/client.generated.d.ts.map +1 -0
- package/dist/profiler/client.generated.js +6 -0
- package/dist/profiler/client.generated.js.map +1 -0
- package/dist/profiler/collectors.d.ts +92 -13
- package/dist/profiler/collectors.d.ts.map +1 -1
- package/dist/profiler/collectors.js +106 -19
- package/dist/profiler/collectors.js.map +1 -1
- package/dist/profiler/endpoint.d.ts +2 -1
- package/dist/profiler/endpoint.d.ts.map +1 -1
- package/dist/profiler/endpoint.js +25 -1
- package/dist/profiler/endpoint.js.map +1 -1
- package/dist/profiler/index.d.ts +8 -4
- package/dist/profiler/index.d.ts.map +1 -1
- package/dist/profiler/index.js +11 -4
- package/dist/profiler/index.js.map +1 -1
- package/dist/profiler/middleware.d.ts.map +1 -1
- package/dist/profiler/middleware.js +76 -10
- package/dist/profiler/middleware.js.map +1 -1
- package/dist/profiler/preview.d.ts +17 -0
- package/dist/profiler/preview.d.ts.map +1 -0
- package/dist/profiler/preview.js +100 -0
- package/dist/profiler/preview.js.map +1 -0
- package/dist/profiler/toolbar.d.ts +15 -23
- package/dist/profiler/toolbar.d.ts.map +1 -1
- package/dist/profiler/toolbar.js +25 -91
- package/dist/profiler/toolbar.js.map +1 -1
- package/dist/profiler/types.d.ts +6 -1
- package/dist/profiler/types.d.ts.map +1 -1
- package/dist/router/Router.d.ts +6 -0
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +115 -12
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcProvider.d.ts +6 -0
- package/dist/rpc/RpcProvider.d.ts.map +1 -1
- package/dist/rpc/RpcProvider.js +4 -1
- package/dist/rpc/RpcProvider.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +27 -14
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +97 -40
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/Scheduler.js +1 -1
- package/dist/scheduler/Scheduler.js.map +1 -1
- package/dist/scheduler/locks/RedisLockBackend.d.ts.map +1 -1
- package/dist/scheduler/locks/RedisLockBackend.js +13 -2
- package/dist/scheduler/locks/RedisLockBackend.js.map +1 -1
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +32 -8
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/security/SignedUrl.d.ts.map +1 -1
- package/dist/security/SignedUrl.js +16 -19
- package/dist/security/SignedUrl.js.map +1 -1
- package/dist/server/Server.d.ts +44 -12
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js +31 -10
- package/dist/server/Server.js.map +1 -1
- package/dist/server/nativeGrpc.d.ts +27 -0
- package/dist/server/nativeGrpc.d.ts.map +1 -0
- package/dist/server/nativeGrpc.js +47 -0
- package/dist/server/nativeGrpc.js.map +1 -0
- package/dist/server/nativeLimits.d.ts +57 -0
- package/dist/server/nativeLimits.d.ts.map +1 -0
- package/dist/server/nativeLimits.js +75 -0
- package/dist/server/nativeLimits.js.map +1 -0
- package/dist/session/Session.d.ts +12 -0
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +13 -4
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +28 -9
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/config.d.ts +5 -0
- package/dist/session/config.d.ts.map +1 -1
- package/dist/session/config.js +8 -0
- package/dist/session/config.js.map +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +26 -7
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/session/errors.d.ts +17 -0
- package/dist/session/errors.d.ts.map +1 -1
- package/dist/session/errors.js +21 -0
- package/dist/session/errors.js.map +1 -1
- package/dist/session/templateTags.d.ts +14 -0
- package/dist/session/templateTags.d.ts.map +1 -1
- package/dist/session/templateTags.js +45 -17
- package/dist/session/templateTags.js.map +1 -1
- package/dist/storage/StaticMiddleware.d.ts.map +1 -1
- package/dist/storage/StaticMiddleware.js +49 -27
- package/dist/storage/StaticMiddleware.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +7 -1
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +16 -4
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +1 -1
- package/dist/utils/Macroable.js +2 -2
- package/dist/utils/Macroable.js.map +1 -1
- 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 +11 -3
- 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/Codemods.ts +314 -3
- package/src/HttpKernel.ts +18 -7
- package/src/Ignitor.ts +58 -26
- package/src/bodyparser/BodyParserMiddleware.ts +45 -16
- package/src/bodyparser/MultipartFile.ts +46 -67
- package/src/bodyparser/parseSize.ts +17 -17
- package/src/bodyparser/qsParse.ts +96 -26
- package/src/codemods/source.ts +303 -0
- package/src/console/prompts.ts +3 -1
- package/src/container/Container.ts +97 -81
- package/src/events/Emitter.ts +37 -10
- package/src/events/helix/helpers.ts +24 -19
- package/src/graphql/GraphQLEngine.ts +91 -14
- package/src/helpers/MessageBuilder.ts +4 -2
- package/src/helpers/errors.ts +16 -0
- package/src/helpers/napi-loader.ts +3 -2
- package/src/http/Exception.ts +17 -2
- package/src/http/HttpContext.ts +34 -5
- package/src/http/RedirectBuilder.ts +51 -37
- package/src/http/Request.ts +102 -58
- package/src/http/Response.ts +15 -7
- package/src/http/SseStream.ts +6 -7
- package/src/http/permissions.ts +47 -0
- package/src/http-client/HttpClient.ts +505 -0
- package/src/http-client/addresses.ts +58 -0
- package/src/http-client/fake.ts +141 -0
- package/src/http-client/index.ts +21 -0
- package/src/index.ts +6 -1
- package/src/middleware/Pipeline.ts +7 -3
- package/src/native/generated.ts +106 -48
- package/src/profiler/Profiler.ts +4 -1
- package/src/profiler/client/toolbar.css +595 -0
- package/src/profiler/client/toolbar.js +1299 -0
- package/src/profiler/client.generated.ts +7 -0
- package/src/profiler/collectors.ts +156 -19
- package/src/profiler/endpoint.ts +27 -1
- package/src/profiler/index.ts +17 -3
- package/src/profiler/middleware.ts +74 -9
- package/src/profiler/preview.ts +104 -0
- package/src/profiler/toolbar.ts +28 -91
- package/src/profiler/types.ts +6 -1
- package/src/router/Router.ts +136 -13
- package/src/rpc/RpcProvider.ts +10 -1
- package/src/rpc/RpcRouter.ts +129 -45
- package/src/scheduler/Scheduler.ts +1 -1
- package/src/scheduler/locks/RedisLockBackend.ts +12 -4
- package/src/security/CookieSigner.ts +30 -8
- package/src/security/SignedUrl.ts +18 -17
- package/src/server/Server.ts +60 -12
- package/src/server/nativeGrpc.ts +59 -0
- package/src/server/nativeLimits.ts +111 -0
- package/src/session/Session.ts +29 -4
- package/src/session/SessionMiddleware.ts +30 -10
- package/src/session/config.ts +7 -0
- package/src/session/drivers/MemoryDriver.ts +26 -7
- package/src/session/errors.ts +31 -0
- package/src/session/templateTags.ts +57 -0
- package/src/storage/StaticMiddleware.ts +46 -27
- package/src/testing/ApiResponse.ts +7 -1
- package/src/testing/TestClient.ts +14 -5
- package/src/utils/Macroable.ts +2 -2
- package/scripts/build-napi-types.mjs +0 -69
- package/scripts/copy-napi.mjs +0 -121
- package/scripts/generate-mime.mjs +0 -119
- package/scripts/generate-napi-types.mjs +0 -187
package/src/events/Emitter.ts
CHANGED
|
@@ -96,6 +96,8 @@ export class Emitter {
|
|
|
96
96
|
#fakedEvents?: Set<BufferedEventName>
|
|
97
97
|
/** Wildcard subscriptions, so `offAny(listener)` can find its Rust handle. */
|
|
98
98
|
readonly #anySubscriptions = new Map<(eventName: string, data: unknown) => void, Set<number>>()
|
|
99
|
+
/** Observers told of every emission as it happens — see {@link tap}. */
|
|
100
|
+
readonly #taps = new Set<(event: string, data: unknown) => void>()
|
|
99
101
|
|
|
100
102
|
constructor(bus: EventBus, resolver?: EmitterResolver) {
|
|
101
103
|
this.#bus = bus
|
|
@@ -227,6 +229,34 @@ export class Emitter {
|
|
|
227
229
|
|
|
228
230
|
// ─── Emit ─────────────────────────────────────────────────
|
|
229
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Watch every emission — string and class-based — as it happens.
|
|
234
|
+
*
|
|
235
|
+
* Development tooling: the profiler's events panel. `onAny` is not enough for
|
|
236
|
+
* it, because a wildcard subscription goes through the native bus and is
|
|
237
|
+
* called later, outside the request that emitted. A tap runs synchronously,
|
|
238
|
+
* in the emitting code's own async context, before any listener.
|
|
239
|
+
*
|
|
240
|
+
* It observes and nothing more: it sees a faked event too, it cannot change
|
|
241
|
+
* or cancel the emission, and a tap that throws is ignored.
|
|
242
|
+
*/
|
|
243
|
+
tap(observer: (event: string, data: unknown) => void): UnsubscribeFunction {
|
|
244
|
+
this.#taps.add(observer)
|
|
245
|
+
return () => {
|
|
246
|
+
this.#taps.delete(observer)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#notifyTaps(event: string, data: unknown): void {
|
|
251
|
+
for (const observer of this.#taps) {
|
|
252
|
+
try {
|
|
253
|
+
observer(event, data)
|
|
254
|
+
} catch {
|
|
255
|
+
// Observation must never break the emission it watches.
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
230
260
|
/**
|
|
231
261
|
* Run an event's listeners, in parallel or one after another.
|
|
232
262
|
*
|
|
@@ -235,10 +265,9 @@ export class Emitter {
|
|
|
235
265
|
* event it does not own.
|
|
236
266
|
*/
|
|
237
267
|
async #dispatchListeners(event: string, data: unknown, serial: boolean): Promise<void> {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
]
|
|
268
|
+
// `onAny` listeners are not here: they subscribe through the native bus,
|
|
269
|
+
// which delivers every matching event to them on its own.
|
|
270
|
+
const listeners = this.#stringListeners.get(event) ?? []
|
|
242
271
|
if (listeners.length === 0) return
|
|
243
272
|
const run = async (fn: ListenerFn): Promise<void> => {
|
|
244
273
|
try {
|
|
@@ -254,17 +283,13 @@ export class Emitter {
|
|
|
254
283
|
await Promise.all(listeners.map(run))
|
|
255
284
|
}
|
|
256
285
|
|
|
257
|
-
/** Listeners registered through `onAny` whose pattern matches `event`. */
|
|
258
|
-
#wildcardListenersFor(_event: string): ListenerFn[] {
|
|
259
|
-
return []
|
|
260
|
-
}
|
|
261
|
-
|
|
262
286
|
/**
|
|
263
287
|
* Emit and run the listeners ONE AT A TIME (AdonisJS `emitSerial`).
|
|
264
288
|
*
|
|
265
289
|
* Use it when order matters — a listener that seeds what the next one reads.
|
|
266
290
|
*/
|
|
267
291
|
async emitSerial(event: string, data: unknown): Promise<void> {
|
|
292
|
+
this.#notifyTaps(event, data)
|
|
268
293
|
if (this.#capture(event, data)) return
|
|
269
294
|
await this.#dispatchListeners(event, data, true)
|
|
270
295
|
this.#publishToBus(event, data)
|
|
@@ -282,6 +307,7 @@ export class Emitter {
|
|
|
282
307
|
* rejects is reported through `emitError`, never left unhandled.
|
|
283
308
|
*/
|
|
284
309
|
async emit(event: string, data: unknown): Promise<void> {
|
|
310
|
+
this.#notifyTaps(event, data)
|
|
285
311
|
if (this.#capture(event, data)) return
|
|
286
312
|
await this.#dispatchListeners(event, data, false)
|
|
287
313
|
this.#publishToBus(event, data)
|
|
@@ -331,11 +357,12 @@ export class Emitter {
|
|
|
331
357
|
*/
|
|
332
358
|
async dispatchEvent<T extends object>(event: T): Promise<void> {
|
|
333
359
|
const EventClass = event.constructor as EventConstructor<T>
|
|
360
|
+
const name = (EventClass as { eventName?: string }).eventName ?? classToEventName(EventClass)
|
|
361
|
+
this.#notifyTaps(name, event)
|
|
334
362
|
// Faked by the class, as AdonisJS does — `fake([TaskDeclared])` catches
|
|
335
363
|
// `new TaskDeclared(...).emit()`.
|
|
336
364
|
if (this.#capture(EventClass, event)) return
|
|
337
365
|
const listeners = this.#classListeners.get(EventClass) ?? []
|
|
338
|
-
const name = (EventClass as { eventName?: string }).eventName ?? classToEventName(EventClass)
|
|
339
366
|
|
|
340
367
|
// Per-listener error isolation — mirrors the string `emit()` contract.
|
|
341
368
|
// A side-effect listener that throws (SMTP down, etc.) MUST NOT abort
|
|
@@ -15,6 +15,16 @@ export interface CollectedEvent {
|
|
|
15
15
|
[key: string]: unknown
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function isCollectedEvent(value: unknown): value is CollectedEvent {
|
|
19
|
+
return (
|
|
20
|
+
typeof value === 'object' &&
|
|
21
|
+
value !== null &&
|
|
22
|
+
'name' in value &&
|
|
23
|
+
typeof value.name === 'string' &&
|
|
24
|
+
'data' in value
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
export interface CollectResult {
|
|
19
29
|
events: CollectedEvent[]
|
|
20
30
|
subId: number
|
|
@@ -34,15 +44,17 @@ interface BusLike {
|
|
|
34
44
|
export function collect(bus: BusLike, pattern: string): CollectResult {
|
|
35
45
|
const events: CollectedEvent[] = []
|
|
36
46
|
const subId = bus.subscribe(pattern, (eventJson: string) => {
|
|
47
|
+
const unreadable: CollectedEvent = {
|
|
48
|
+
_raw: eventJson,
|
|
49
|
+
_parseError: true,
|
|
50
|
+
name: '',
|
|
51
|
+
data: undefined,
|
|
52
|
+
}
|
|
37
53
|
try {
|
|
38
|
-
|
|
54
|
+
const parsed: unknown = JSON.parse(eventJson)
|
|
55
|
+
events.push(isCollectedEvent(parsed) ? parsed : unreadable)
|
|
39
56
|
} catch {
|
|
40
|
-
events.push(
|
|
41
|
-
_raw: eventJson,
|
|
42
|
-
_parseError: true,
|
|
43
|
-
name: '',
|
|
44
|
-
data: undefined,
|
|
45
|
-
} as CollectedEvent)
|
|
57
|
+
events.push(unreadable)
|
|
46
58
|
}
|
|
47
59
|
})
|
|
48
60
|
return { events, subId }
|
|
@@ -233,20 +245,13 @@ function partialMatch(actual: unknown, expected: unknown): boolean {
|
|
|
233
245
|
|
|
234
246
|
if (Array.isArray(expected)) {
|
|
235
247
|
if (!Array.isArray(actual)) return false
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
)
|
|
248
|
+
const items: unknown[] = actual
|
|
249
|
+
return expected.every((item, i) => i < items.length && partialMatch(items[i], item))
|
|
239
250
|
}
|
|
240
251
|
|
|
241
|
-
for (const key of Object.keys(expected
|
|
242
|
-
if (!(key in
|
|
243
|
-
if (
|
|
244
|
-
!partialMatch(
|
|
245
|
-
(actual as Record<string, unknown>)[key],
|
|
246
|
-
(expected as Record<string, unknown>)[key],
|
|
247
|
-
)
|
|
248
|
-
)
|
|
249
|
-
return false
|
|
252
|
+
for (const key of Object.keys(expected)) {
|
|
253
|
+
if (!(key in actual)) return false
|
|
254
|
+
if (!partialMatch(Reflect.get(actual, key), Reflect.get(expected, key))) return false
|
|
250
255
|
}
|
|
251
256
|
return true
|
|
252
257
|
}
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
import * as fs from 'node:fs'
|
|
25
25
|
import { currentNodeEnv } from '../env/nodeEnv.js'
|
|
26
26
|
import { loadNapi } from '../helpers/napi-loader.js'
|
|
27
|
+
import { Exception } from '../http/Exception.js'
|
|
27
28
|
import type { HttpContext } from '../http/HttpContext.js'
|
|
29
|
+
import { holdsPermissions } from '../http/permissions.js'
|
|
28
30
|
|
|
29
31
|
/** Rust parser output (the `ream-graphql` crate via the `index` NAPI binary). */
|
|
30
32
|
interface RustParsedField {
|
|
@@ -169,6 +171,14 @@ export interface GraphQLConfig {
|
|
|
169
171
|
* (default: 1000).
|
|
170
172
|
*/
|
|
171
173
|
maxComplexity?: number
|
|
174
|
+
/**
|
|
175
|
+
* Echo the message of any error a resolver throws to the client. Default:
|
|
176
|
+
* only when NODE_ENV is development. Otherwise only a deliberate
|
|
177
|
+
* `Exception`, a client error (status below 500) or an error flagged
|
|
178
|
+
* `expose: true` keeps its message; anything else reads "Internal server
|
|
179
|
+
* error", and the real error goes to `ctx.logger`.
|
|
180
|
+
*/
|
|
181
|
+
debug?: boolean
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
export interface ResolverOptions {
|
|
@@ -207,6 +217,7 @@ export class GraphQLEngine {
|
|
|
207
217
|
#container?: { make<T>(target: abstract new (...args: never[]) => T): Promise<T> }
|
|
208
218
|
readonly path: string
|
|
209
219
|
#playground: boolean
|
|
220
|
+
readonly #debug: boolean
|
|
210
221
|
#maxQueryBytes: number
|
|
211
222
|
#maxDepth: number
|
|
212
223
|
#maxComplexity: number
|
|
@@ -236,6 +247,7 @@ export class GraphQLEngine {
|
|
|
236
247
|
this.#maxQueryBytes = config.maxQueryBytes ?? 100_000
|
|
237
248
|
this.#maxDepth = config.maxDepth ?? 12
|
|
238
249
|
this.#maxComplexity = config.maxComplexity ?? 1000
|
|
250
|
+
this.#debug = config.debug ?? currentNodeEnv() === 'development'
|
|
239
251
|
}
|
|
240
252
|
|
|
241
253
|
/** Set IoC container for resolver instantiation. */
|
|
@@ -279,8 +291,19 @@ export class GraphQLEngine {
|
|
|
279
291
|
return
|
|
280
292
|
}
|
|
281
293
|
|
|
282
|
-
|
|
283
|
-
|
|
294
|
+
// Only a JSON body is a GraphQL request. A form post, a text/plain body or
|
|
295
|
+
// one with no Content-Type is a CORS simple request: a foreign page sends
|
|
296
|
+
// it with the user's cookies and no preflight, so a mutation would run
|
|
297
|
+
// cross-site. Refused before the body is read.
|
|
298
|
+
const mediaType = (ctx.request.header('content-type')?.split(';')[0] ?? '').trim().toLowerCase()
|
|
299
|
+
if (mediaType !== 'application/json') {
|
|
300
|
+
ctx.response.status(415).json({ errors: [{ message: 'Unsupported Media Type' }] })
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Read from the raw body rather than `request.body()`, so the endpoint
|
|
305
|
+
// works whether or not the body parser runs in front of it.
|
|
306
|
+
const body = parseGraphQLBody(parseJson(ctx.request.raw()))
|
|
284
307
|
if (!body) {
|
|
285
308
|
ctx.response.status(400).json({ errors: [{ message: 'Missing or invalid query' }] })
|
|
286
309
|
return
|
|
@@ -290,11 +313,7 @@ export class GraphQLEngine {
|
|
|
290
313
|
const result = await this.#execute(body, ctx)
|
|
291
314
|
ctx.response.status(200).json(result)
|
|
292
315
|
} catch (err) {
|
|
293
|
-
|
|
294
|
-
currentNodeEnv() !== 'production' && err instanceof Error
|
|
295
|
-
? err.message
|
|
296
|
-
: 'Internal server error'
|
|
297
|
-
ctx.response.status(500).json({ errors: [{ message }] })
|
|
316
|
+
ctx.response.status(500).json({ errors: [{ message: this.#publicMessage(err, ctx) }] })
|
|
298
317
|
}
|
|
299
318
|
}
|
|
300
319
|
|
|
@@ -364,7 +383,7 @@ export class GraphQLEngine {
|
|
|
364
383
|
}
|
|
365
384
|
}
|
|
366
385
|
|
|
367
|
-
const guardError = this.#enforceGuards(entry.options, ctx)
|
|
386
|
+
const guardError = await this.#enforceGuards(entry.options, ctx)
|
|
368
387
|
if (guardError) {
|
|
369
388
|
return { ok: false, error: { message: guardError, path: [field.name] } }
|
|
370
389
|
}
|
|
@@ -395,11 +414,26 @@ export class GraphQLEngine {
|
|
|
395
414
|
// ask for. Empty selection (scalar field) returns the value as-is.
|
|
396
415
|
return { ok: true, value: pruneToSelection(result, field.selection) }
|
|
397
416
|
} catch (err) {
|
|
398
|
-
|
|
399
|
-
return { ok: false, error: { message: msg, path: [field.name] } }
|
|
417
|
+
return { ok: false, error: { message: this.#publicMessage(err, ctx), path: [field.name] } }
|
|
400
418
|
}
|
|
401
419
|
}
|
|
402
420
|
|
|
421
|
+
/**
|
|
422
|
+
* The message a client may see for a thrown error — the rule the HTTP
|
|
423
|
+
* exception handler applies. A driver or library error carries internal
|
|
424
|
+
* hosts, credentials and stored values in its text, so outside debug it is
|
|
425
|
+
* collapsed, and the original is logged for the operator instead.
|
|
426
|
+
*/
|
|
427
|
+
#publicMessage(err: unknown, ctx: HttpContext): string {
|
|
428
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
429
|
+
if (this.#debug || isPublicError(err)) return message
|
|
430
|
+
ctx.logger.error('GraphQL resolver failed', {
|
|
431
|
+
message,
|
|
432
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
433
|
+
})
|
|
434
|
+
return 'Internal server error'
|
|
435
|
+
}
|
|
436
|
+
|
|
403
437
|
/**
|
|
404
438
|
* Build the resolver argument bag: `$variable` references resolve against
|
|
405
439
|
* `request.variables`, then any unreferenced variables are merged in.
|
|
@@ -435,7 +469,7 @@ export class GraphQLEngine {
|
|
|
435
469
|
}
|
|
436
470
|
|
|
437
471
|
/** Enforce guard/role/permission on a resolver. */
|
|
438
|
-
#enforceGuards(options: ResolverOptions, ctx: HttpContext): string | null {
|
|
472
|
+
async #enforceGuards(options: ResolverOptions, ctx: HttpContext): Promise<string | null> {
|
|
439
473
|
const guards = [...(options.guards ?? []), ...(options.guard ? [options.guard] : [])]
|
|
440
474
|
const roles = [...(options.roles ?? []), ...(options.role ? [options.role] : [])]
|
|
441
475
|
const permissions = options.permissions ?? []
|
|
@@ -455,9 +489,9 @@ export class GraphQLEngine {
|
|
|
455
489
|
}
|
|
456
490
|
}
|
|
457
491
|
|
|
492
|
+
// A token-authenticated request also needs the token to allow each one.
|
|
458
493
|
if (permissions.length > 0) {
|
|
459
|
-
|
|
460
|
-
if (!permissions.every((p) => userPerms.includes(p))) {
|
|
494
|
+
if (!(await holdsPermissions(ctx.auth, permissions))) {
|
|
461
495
|
return 'Insufficient permissions'
|
|
462
496
|
}
|
|
463
497
|
}
|
|
@@ -477,7 +511,10 @@ export class GraphQLEngine {
|
|
|
477
511
|
// Full-Rust: the spec-compliant `ream-graphql` parser (graphql-parser
|
|
478
512
|
// crate) rejects invalid queries before any resolver runs. No TS fallback —
|
|
479
513
|
// ream ships the native binary, so when Rust exists the engine is Rust.
|
|
480
|
-
const raw = JSON.parse(graphqlNative().graphqlParse(query))
|
|
514
|
+
const raw: unknown = JSON.parse(graphqlNative().graphqlParse(query))
|
|
515
|
+
if (!isRustParseResult(raw)) {
|
|
516
|
+
return { typeName: 'Query', fields: [], errors: [{ message: 'Unreadable parse result' }] }
|
|
517
|
+
}
|
|
481
518
|
const typeName = OPERATION_TYPE[raw.operation_type] ?? 'Query'
|
|
482
519
|
if (raw.errors.length > 0) {
|
|
483
520
|
return { typeName, fields: [], errors: raw.errors.map((message) => ({ message })) }
|
|
@@ -519,6 +556,22 @@ function isVariablesRecord(value: unknown): value is Record<string, unknown> {
|
|
|
519
556
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
520
557
|
}
|
|
521
558
|
|
|
559
|
+
/** A deliberate `Exception`, a client error, or an error flagged `expose: true`. */
|
|
560
|
+
function isPublicError(err: unknown): boolean {
|
|
561
|
+
if (err instanceof Exception) return true
|
|
562
|
+
if (err === null || typeof err !== 'object') return false
|
|
563
|
+
if ('expose' in err && err.expose === true) return true
|
|
564
|
+
return 'status' in err && typeof err.status === 'number' && err.status < 500
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function parseJson(raw: string): unknown {
|
|
568
|
+
try {
|
|
569
|
+
return JSON.parse(raw)
|
|
570
|
+
} catch {
|
|
571
|
+
return undefined
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
522
575
|
function parseGraphQLBody(body: unknown): GraphQLRequest | null {
|
|
523
576
|
if (!body || typeof body !== 'object' || Array.isArray(body)) return null
|
|
524
577
|
// After typeof check, body is a non-null non-array object; index via 'in' narrowing
|
|
@@ -536,6 +589,30 @@ function parseGraphQLBody(body: unknown): GraphQLRequest | null {
|
|
|
536
589
|
return { query: q, variables, operationName }
|
|
537
590
|
}
|
|
538
591
|
|
|
592
|
+
/** The shape the native parser answers with, checked rather than assumed. */
|
|
593
|
+
function isRustParseResult(value: unknown): value is RustParseResult {
|
|
594
|
+
if (!isRecord(value)) return false
|
|
595
|
+
return (
|
|
596
|
+
typeof value.operation_type === 'string' &&
|
|
597
|
+
(value.operation_name === null || typeof value.operation_name === 'string') &&
|
|
598
|
+
Array.isArray(value.errors) &&
|
|
599
|
+
value.errors.every((error) => typeof error === 'string') &&
|
|
600
|
+
Array.isArray(value.fields) &&
|
|
601
|
+
value.fields.every(isRustParsedField)
|
|
602
|
+
)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function isRustParsedField(value: unknown): value is RustParsedField {
|
|
606
|
+
if (!isRecord(value)) return false
|
|
607
|
+
return (
|
|
608
|
+
typeof value.name === 'string' &&
|
|
609
|
+
(value.alias === null || typeof value.alias === 'string') &&
|
|
610
|
+
isRecord(value.args) &&
|
|
611
|
+
Array.isArray(value.sub_fields) &&
|
|
612
|
+
value.sub_fields.every(isRustParsedField)
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
|
|
539
616
|
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
540
617
|
return typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
541
618
|
}
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* signed email-verification token. Those two claims travel inside the payload,
|
|
8
8
|
* which is why they are added before signing rather than checked after.
|
|
9
9
|
*
|
|
10
|
-
* Ported from `@poppinss/utils
|
|
11
|
-
*
|
|
10
|
+
* Ported from `@poppinss/utils`, as a helper for application code. Ream's own
|
|
11
|
+
* signers do not use it: `CookieSigner` seals its values in a compact envelope
|
|
12
|
+
* of its own (`{ m, e, p }`, an epoch-millisecond expiry), and `SignedUrl`
|
|
13
|
+
* carries the expiry and the purpose as query parameters.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
import { milliseconds } from './duration.js'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A caught value as the `NodeJS.ErrnoException` an error callback takes.
|
|
3
|
+
*
|
|
4
|
+
* `catch` binds `unknown`: an `Error` already is one (every field it adds is
|
|
5
|
+
* optional), and anything else thrown is wrapped, so a callback never reads
|
|
6
|
+
* `.code` or `.message` off a string.
|
|
7
|
+
*/
|
|
8
|
+
export function toErrnoException(error: unknown): NodeJS.ErrnoException {
|
|
9
|
+
return error instanceof Error ? error : new Error(String(error))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The `code` of a caught system error (`ENOENT`, `MODULE_NOT_FOUND`…), if any. */
|
|
13
|
+
export function errorCode(error: unknown): string | undefined {
|
|
14
|
+
if (typeof error !== 'object' || error === null || !('code' in error)) return undefined
|
|
15
|
+
return typeof error.code === 'string' ? error.code : undefined
|
|
16
|
+
}
|
|
@@ -12,6 +12,7 @@ import { dirname, join } from 'node:path'
|
|
|
12
12
|
import { arch, platform } from 'node:process'
|
|
13
13
|
import { fileURLToPath } from 'node:url'
|
|
14
14
|
import { ReamError } from '../errors/ReamError.js'
|
|
15
|
+
import { errorCode } from './errors.js'
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Platform triple suffix (matches napi-rs naming).
|
|
@@ -88,14 +89,14 @@ export function loadNapi<T>(options: LoadNapiOptions): T {
|
|
|
88
89
|
try {
|
|
89
90
|
return req(path) as T
|
|
90
91
|
} catch (e) {
|
|
91
|
-
const code = (e
|
|
92
|
+
const code = errorCode(e)
|
|
92
93
|
if (code === 'MODULE_NOT_FOUND') continue
|
|
93
94
|
// Any other error (SyntaxError, ABI mismatch, permissions,
|
|
94
95
|
// corrupt binary) means the file exists but cannot load. Surface
|
|
95
96
|
// the real cause rather than a generic "not found".
|
|
96
97
|
throw new ReamError(
|
|
97
98
|
`${errorCodePrefix}_NAPI_LOAD_FAILED`,
|
|
98
|
-
`${binaryName} NAPI binary exists at '${path}' but failed to load: ${
|
|
99
|
+
`${binaryName} NAPI binary exists at '${path}' but failed to load: ${e instanceof Error ? e.message : String(e)}`,
|
|
99
100
|
{
|
|
100
101
|
hint: 'Check that the binary was built for the current platform and Node ABI. Rebuild with `pnpm --filter @c9up/ream build:rust`.',
|
|
101
102
|
},
|
package/src/http/Exception.ts
CHANGED
|
@@ -259,6 +259,12 @@ export type HttpError = {
|
|
|
259
259
|
report?: (error: unknown, ctx: HttpContext) => Promise<void> | void
|
|
260
260
|
/** A hint appended to the message, when the exception carries one. */
|
|
261
261
|
help?: string
|
|
262
|
+
/**
|
|
263
|
+
* Set by a thrown value that is not an {@link Exception} but whose message is
|
|
264
|
+
* written for clients (the `http-errors` convention). Shown outside debug
|
|
265
|
+
* mode even on a 5xx.
|
|
266
|
+
*/
|
|
267
|
+
expose?: boolean
|
|
262
268
|
}
|
|
263
269
|
|
|
264
270
|
/** Renders an HTML error page for a status (AdonisJS `StatusPageRenderer`). */
|
|
@@ -352,13 +358,22 @@ export class ExceptionHandler extends Macroable {
|
|
|
352
358
|
* The message a client is allowed to see.
|
|
353
359
|
*
|
|
354
360
|
* DEVIATION (named): outside debug mode ream only echoes the message of a
|
|
355
|
-
* deliberate `Exception
|
|
361
|
+
* deliberate `Exception`, of a client error (status below 500, which a
|
|
362
|
+
* package raises on purpose: 401, 403, 413…), or of an error flagged
|
|
363
|
+
* `expose: true`; anything else collapses to a generic line. AdonisJS
|
|
356
364
|
* echoes `error.message` whatever was thrown, which leaks the text of driver
|
|
357
365
|
* and library errors to the client.
|
|
366
|
+
*
|
|
367
|
+
* A `code` is NOT a reason to show the message: Node system errors
|
|
368
|
+
* (`ECONNREFUSED`, `ENOENT`) and database drivers (`23505`) carry one, and
|
|
369
|
+
* their messages hold internal hosts, file paths, constraint names and
|
|
370
|
+
* stored values. The error itself is left untouched for reporting.
|
|
358
371
|
*/
|
|
359
372
|
protected publicMessage(error: HttpError, ctx: HttpContext): string {
|
|
360
373
|
if (this.isDebuggingEnabled(ctx)) return error.message
|
|
361
|
-
if (error.
|
|
374
|
+
if (error instanceof Exception || error.status < 500 || error.expose === true) {
|
|
375
|
+
return error.message
|
|
376
|
+
}
|
|
362
377
|
return 'An internal error occurred'
|
|
363
378
|
}
|
|
364
379
|
|
package/src/http/HttpContext.ts
CHANGED
|
@@ -33,7 +33,14 @@ export interface AuthState {
|
|
|
33
33
|
id: string
|
|
34
34
|
email?: string
|
|
35
35
|
roles?: string[]
|
|
36
|
+
/** The user's own permissions — never widened by a token's abilities. */
|
|
36
37
|
permissions?: string[]
|
|
38
|
+
/**
|
|
39
|
+
* Set by a token guard (AdonisJS `user.currentAccessToken`): the token this
|
|
40
|
+
* request authenticated with. Its abilities restrict the user's
|
|
41
|
+
* permissions on every `.permission()` gate; they never add to them.
|
|
42
|
+
*/
|
|
43
|
+
currentAccessToken?: AccessTokenAbilities
|
|
37
44
|
[key: string]: unknown
|
|
38
45
|
}
|
|
39
46
|
roles?: string[]
|
|
@@ -48,6 +55,16 @@ export interface AuthState {
|
|
|
48
55
|
check?(): Promise<boolean>
|
|
49
56
|
authenticateUsing?(guards?: string[], options?: { loginRoute?: string }): Promise<void>
|
|
50
57
|
getUserOrFail?(): AuthState['user']
|
|
58
|
+
/**
|
|
59
|
+
* Whether the authenticated user holds every permission in `permissions`,
|
|
60
|
+
* as the auth layer resolves them (warden's authenticator asks its rights
|
|
61
|
+
* resolver and narrows by the current access token). When present, the
|
|
62
|
+
* `.permission()` gates trust this answer instead of the payload's
|
|
63
|
+
* `permissions`, which can be stale or incomplete. `scope` is the auth
|
|
64
|
+
* layer's own notion (warden: `'global'` or a tenant); the gates pass none,
|
|
65
|
+
* so the layer's default applies.
|
|
66
|
+
*/
|
|
67
|
+
hasAllPermissions?(permissions: readonly string[], scope?: unknown): Promise<boolean>
|
|
51
68
|
/**
|
|
52
69
|
* The guard behind `name` (AdonisJS `auth.use('web')`).
|
|
53
70
|
*
|
|
@@ -65,6 +82,11 @@ export interface AuthState {
|
|
|
65
82
|
readonly authenticatedViaGuard?: string
|
|
66
83
|
}
|
|
67
84
|
|
|
85
|
+
/** What a permission gate needs from an access token: whether it allows an ability. */
|
|
86
|
+
export interface AccessTokenAbilities {
|
|
87
|
+
allows(ability: string): boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
68
90
|
/**
|
|
69
91
|
* Per-request authorization entry point — populated by an authorization
|
|
70
92
|
* middleware (e.g. `@c9up/warden`'s Bouncer initializer). A structural
|
|
@@ -393,10 +415,16 @@ export class HttpContext extends Macroable {
|
|
|
393
415
|
this.request.setSignedUrl(signedUrl)
|
|
394
416
|
}
|
|
395
417
|
|
|
396
|
-
/**
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
418
|
+
/**
|
|
419
|
+
* The values the matched route's domain captured from the host:
|
|
420
|
+
* `{ tenant: 'acme' }` for a route on `:tenant.app.test` reached through
|
|
421
|
+
* `acme.app.test`. Empty when the route has no parameterised domain. Set by
|
|
422
|
+
* the HTTP kernel from the route match, as upstream's router does.
|
|
423
|
+
*
|
|
424
|
+
* BREAKING — this used to be the host's labels (`['api']`); that is
|
|
425
|
+
* `ctx.request.subdomains()`, and stays there.
|
|
426
|
+
*/
|
|
427
|
+
subdomains: Record<string, string> = {}
|
|
400
428
|
|
|
401
429
|
/** Stable identifier for the matched route (`METHOD-pattern`) — AdonisJS `ctx.routeKey`. */
|
|
402
430
|
get routeKey(): string {
|
|
@@ -440,8 +468,9 @@ export class HttpContext extends Macroable {
|
|
|
440
468
|
this.response.setRedirectFactory(
|
|
441
469
|
() =>
|
|
442
470
|
new RedirectBuilder(this.response, {
|
|
443
|
-
requestUrl: this.request.url(),
|
|
471
|
+
requestUrl: this.request.url(true),
|
|
444
472
|
requestReferer: this.request.header('referer'),
|
|
473
|
+
requestAuthority: this.request.authority() ?? undefined,
|
|
445
474
|
routeUrlResolver: this.#routeUrlResolver,
|
|
446
475
|
}),
|
|
447
476
|
)
|