@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.
Files changed (293) hide show
  1. package/README.md +5 -6
  2. package/dist/Codemods.d.ts +24 -0
  3. package/dist/Codemods.d.ts.map +1 -1
  4. package/dist/Codemods.js +211 -1
  5. package/dist/Codemods.js.map +1 -1
  6. package/dist/HttpKernel.d.ts.map +1 -1
  7. package/dist/HttpKernel.js +17 -5
  8. package/dist/HttpKernel.js.map +1 -1
  9. package/dist/Ignitor.d.ts +15 -2
  10. package/dist/Ignitor.d.ts.map +1 -1
  11. package/dist/Ignitor.js +33 -23
  12. package/dist/Ignitor.js.map +1 -1
  13. package/dist/bodyparser/BodyParserMiddleware.d.ts +4 -4
  14. package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
  15. package/dist/bodyparser/BodyParserMiddleware.js +41 -11
  16. package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
  17. package/dist/bodyparser/MultipartFile.d.ts +21 -15
  18. package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
  19. package/dist/bodyparser/MultipartFile.js +40 -58
  20. package/dist/bodyparser/MultipartFile.js.map +1 -1
  21. package/dist/bodyparser/parseSize.d.ts +7 -4
  22. package/dist/bodyparser/parseSize.d.ts.map +1 -1
  23. package/dist/bodyparser/parseSize.js +12 -17
  24. package/dist/bodyparser/parseSize.js.map +1 -1
  25. package/dist/bodyparser/qsParse.d.ts +20 -1
  26. package/dist/bodyparser/qsParse.d.ts.map +1 -1
  27. package/dist/bodyparser/qsParse.js +78 -25
  28. package/dist/bodyparser/qsParse.js.map +1 -1
  29. package/dist/codemods/source.d.ts +57 -0
  30. package/dist/codemods/source.d.ts.map +1 -0
  31. package/dist/codemods/source.js +282 -0
  32. package/dist/codemods/source.js.map +1 -0
  33. package/dist/console/prompts.d.ts.map +1 -1
  34. package/dist/console/prompts.js +3 -1
  35. package/dist/console/prompts.js.map +1 -1
  36. package/dist/container/Container.d.ts.map +1 -1
  37. package/dist/container/Container.js +61 -71
  38. package/dist/container/Container.js.map +1 -1
  39. package/dist/events/Emitter.d.ts +12 -0
  40. package/dist/events/Emitter.d.ts.map +1 -1
  41. package/dist/events/Emitter.js +36 -9
  42. package/dist/events/Emitter.js.map +1 -1
  43. package/dist/events/helix/helpers.d.ts.map +1 -1
  44. package/dist/events/helix/helpers.js +19 -9
  45. package/dist/events/helix/helpers.js.map +1 -1
  46. package/dist/graphql/GraphQLEngine.d.ts +8 -0
  47. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  48. package/dist/graphql/GraphQLEngine.js +79 -12
  49. package/dist/graphql/GraphQLEngine.js.map +1 -1
  50. package/dist/helpers/MessageBuilder.d.ts +4 -2
  51. package/dist/helpers/MessageBuilder.d.ts.map +1 -1
  52. package/dist/helpers/MessageBuilder.js +4 -2
  53. package/dist/helpers/MessageBuilder.js.map +1 -1
  54. package/dist/helpers/errors.d.ts +11 -0
  55. package/dist/helpers/errors.d.ts.map +1 -0
  56. package/dist/helpers/errors.js +17 -0
  57. package/dist/helpers/errors.js.map +1 -0
  58. package/dist/helpers/napi-loader.d.ts.map +1 -1
  59. package/dist/helpers/napi-loader.js +3 -2
  60. package/dist/helpers/napi-loader.js.map +1 -1
  61. package/dist/http/Exception.d.ts +14 -1
  62. package/dist/http/Exception.d.ts.map +1 -1
  63. package/dist/http/Exception.js +10 -2
  64. package/dist/http/Exception.js.map +1 -1
  65. package/dist/http/HttpContext.d.ts +31 -2
  66. package/dist/http/HttpContext.d.ts.map +1 -1
  67. package/dist/http/HttpContext.js +12 -5
  68. package/dist/http/HttpContext.js.map +1 -1
  69. package/dist/http/RedirectBuilder.d.ts +9 -3
  70. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  71. package/dist/http/RedirectBuilder.js +43 -37
  72. package/dist/http/RedirectBuilder.js.map +1 -1
  73. package/dist/http/Request.d.ts +42 -17
  74. package/dist/http/Request.d.ts.map +1 -1
  75. package/dist/http/Request.js +80 -58
  76. package/dist/http/Request.js.map +1 -1
  77. package/dist/http/Response.d.ts +7 -4
  78. package/dist/http/Response.d.ts.map +1 -1
  79. package/dist/http/Response.js +14 -6
  80. package/dist/http/Response.js.map +1 -1
  81. package/dist/http/SseStream.js +6 -7
  82. package/dist/http/SseStream.js.map +1 -1
  83. package/dist/http/permissions.d.ts +25 -0
  84. package/dist/http/permissions.d.ts.map +1 -0
  85. package/dist/http/permissions.js +38 -0
  86. package/dist/http/permissions.js.map +1 -0
  87. package/dist/http-client/HttpClient.d.ts +111 -0
  88. package/dist/http-client/HttpClient.d.ts.map +1 -0
  89. package/dist/http-client/HttpClient.js +386 -0
  90. package/dist/http-client/HttpClient.js.map +1 -0
  91. package/dist/http-client/addresses.d.ts +14 -0
  92. package/dist/http-client/addresses.d.ts.map +1 -0
  93. package/dist/http-client/addresses.js +57 -0
  94. package/dist/http-client/addresses.js.map +1 -0
  95. package/dist/http-client/fake.d.ts +50 -0
  96. package/dist/http-client/fake.d.ts.map +1 -0
  97. package/dist/http-client/fake.js +95 -0
  98. package/dist/http-client/fake.js.map +1 -0
  99. package/dist/http-client/index.d.ts +7 -0
  100. package/dist/http-client/index.d.ts.map +1 -0
  101. package/dist/http-client/index.js +7 -0
  102. package/dist/http-client/index.js.map +1 -0
  103. package/dist/index.d.ts +4 -3
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +1 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/middleware/Pipeline.d.ts.map +1 -1
  108. package/dist/middleware/Pipeline.js +6 -3
  109. package/dist/middleware/Pipeline.js.map +1 -1
  110. package/dist/native/generated.d.ts +93 -36
  111. package/dist/native/generated.d.ts.map +1 -1
  112. package/dist/profiler/Profiler.d.ts.map +1 -1
  113. package/dist/profiler/Profiler.js +4 -1
  114. package/dist/profiler/Profiler.js.map +1 -1
  115. package/dist/profiler/client.generated.d.ts +5 -0
  116. package/dist/profiler/client.generated.d.ts.map +1 -0
  117. package/dist/profiler/client.generated.js +6 -0
  118. package/dist/profiler/client.generated.js.map +1 -0
  119. package/dist/profiler/collectors.d.ts +92 -13
  120. package/dist/profiler/collectors.d.ts.map +1 -1
  121. package/dist/profiler/collectors.js +106 -19
  122. package/dist/profiler/collectors.js.map +1 -1
  123. package/dist/profiler/endpoint.d.ts +2 -1
  124. package/dist/profiler/endpoint.d.ts.map +1 -1
  125. package/dist/profiler/endpoint.js +25 -1
  126. package/dist/profiler/endpoint.js.map +1 -1
  127. package/dist/profiler/index.d.ts +8 -4
  128. package/dist/profiler/index.d.ts.map +1 -1
  129. package/dist/profiler/index.js +11 -4
  130. package/dist/profiler/index.js.map +1 -1
  131. package/dist/profiler/middleware.d.ts.map +1 -1
  132. package/dist/profiler/middleware.js +76 -10
  133. package/dist/profiler/middleware.js.map +1 -1
  134. package/dist/profiler/preview.d.ts +17 -0
  135. package/dist/profiler/preview.d.ts.map +1 -0
  136. package/dist/profiler/preview.js +100 -0
  137. package/dist/profiler/preview.js.map +1 -0
  138. package/dist/profiler/toolbar.d.ts +15 -23
  139. package/dist/profiler/toolbar.d.ts.map +1 -1
  140. package/dist/profiler/toolbar.js +25 -91
  141. package/dist/profiler/toolbar.js.map +1 -1
  142. package/dist/profiler/types.d.ts +6 -1
  143. package/dist/profiler/types.d.ts.map +1 -1
  144. package/dist/router/Router.d.ts +6 -0
  145. package/dist/router/Router.d.ts.map +1 -1
  146. package/dist/router/Router.js +115 -12
  147. package/dist/router/Router.js.map +1 -1
  148. package/dist/rpc/RpcProvider.d.ts +6 -0
  149. package/dist/rpc/RpcProvider.d.ts.map +1 -1
  150. package/dist/rpc/RpcProvider.js +4 -1
  151. package/dist/rpc/RpcProvider.js.map +1 -1
  152. package/dist/rpc/RpcRouter.d.ts +27 -14
  153. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  154. package/dist/rpc/RpcRouter.js +97 -40
  155. package/dist/rpc/RpcRouter.js.map +1 -1
  156. package/dist/scheduler/Scheduler.js +1 -1
  157. package/dist/scheduler/Scheduler.js.map +1 -1
  158. package/dist/scheduler/locks/RedisLockBackend.d.ts.map +1 -1
  159. package/dist/scheduler/locks/RedisLockBackend.js +13 -2
  160. package/dist/scheduler/locks/RedisLockBackend.js.map +1 -1
  161. package/dist/security/CookieSigner.d.ts.map +1 -1
  162. package/dist/security/CookieSigner.js +32 -8
  163. package/dist/security/CookieSigner.js.map +1 -1
  164. package/dist/security/SignedUrl.d.ts.map +1 -1
  165. package/dist/security/SignedUrl.js +16 -19
  166. package/dist/security/SignedUrl.js.map +1 -1
  167. package/dist/server/Server.d.ts +44 -12
  168. package/dist/server/Server.d.ts.map +1 -1
  169. package/dist/server/Server.js +31 -10
  170. package/dist/server/Server.js.map +1 -1
  171. package/dist/server/nativeGrpc.d.ts +27 -0
  172. package/dist/server/nativeGrpc.d.ts.map +1 -0
  173. package/dist/server/nativeGrpc.js +47 -0
  174. package/dist/server/nativeGrpc.js.map +1 -0
  175. package/dist/server/nativeLimits.d.ts +57 -0
  176. package/dist/server/nativeLimits.d.ts.map +1 -0
  177. package/dist/server/nativeLimits.js +75 -0
  178. package/dist/server/nativeLimits.js.map +1 -0
  179. package/dist/session/Session.d.ts +12 -0
  180. package/dist/session/Session.d.ts.map +1 -1
  181. package/dist/session/Session.js +13 -4
  182. package/dist/session/Session.js.map +1 -1
  183. package/dist/session/SessionMiddleware.d.ts.map +1 -1
  184. package/dist/session/SessionMiddleware.js +28 -9
  185. package/dist/session/SessionMiddleware.js.map +1 -1
  186. package/dist/session/config.d.ts +5 -0
  187. package/dist/session/config.d.ts.map +1 -1
  188. package/dist/session/config.js +8 -0
  189. package/dist/session/config.js.map +1 -1
  190. package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
  191. package/dist/session/drivers/MemoryDriver.js +26 -7
  192. package/dist/session/drivers/MemoryDriver.js.map +1 -1
  193. package/dist/session/errors.d.ts +17 -0
  194. package/dist/session/errors.d.ts.map +1 -1
  195. package/dist/session/errors.js +21 -0
  196. package/dist/session/errors.js.map +1 -1
  197. package/dist/session/templateTags.d.ts +14 -0
  198. package/dist/session/templateTags.d.ts.map +1 -1
  199. package/dist/session/templateTags.js +45 -17
  200. package/dist/session/templateTags.js.map +1 -1
  201. package/dist/storage/StaticMiddleware.d.ts.map +1 -1
  202. package/dist/storage/StaticMiddleware.js +49 -27
  203. package/dist/storage/StaticMiddleware.js.map +1 -1
  204. package/dist/testing/ApiResponse.d.ts.map +1 -1
  205. package/dist/testing/ApiResponse.js +7 -1
  206. package/dist/testing/ApiResponse.js.map +1 -1
  207. package/dist/testing/TestClient.d.ts.map +1 -1
  208. package/dist/testing/TestClient.js +16 -4
  209. package/dist/testing/TestClient.js.map +1 -1
  210. package/dist/utils/Macroable.d.ts +1 -1
  211. package/dist/utils/Macroable.js +2 -2
  212. package/dist/utils/Macroable.js.map +1 -1
  213. package/events.darwin-arm64.node +0 -0
  214. package/events.darwin-x64.node +0 -0
  215. package/events.linux-arm64-gnu.node +0 -0
  216. package/events.linux-x64-gnu.node +0 -0
  217. package/events.win32-x64-msvc.node +0 -0
  218. package/index.darwin-arm64.node +0 -0
  219. package/index.darwin-x64.node +0 -0
  220. package/index.linux-arm64-gnu.node +0 -0
  221. package/index.linux-x64-gnu.node +0 -0
  222. package/index.win32-x64-msvc.node +0 -0
  223. package/package.json +11 -3
  224. package/scheduler.darwin-arm64.node +0 -0
  225. package/scheduler.darwin-x64.node +0 -0
  226. package/scheduler.linux-arm64-gnu.node +0 -0
  227. package/scheduler.linux-x64-gnu.node +0 -0
  228. package/scheduler.win32-x64-msvc.node +0 -0
  229. package/src/Codemods.ts +314 -3
  230. package/src/HttpKernel.ts +18 -7
  231. package/src/Ignitor.ts +58 -26
  232. package/src/bodyparser/BodyParserMiddleware.ts +45 -16
  233. package/src/bodyparser/MultipartFile.ts +46 -67
  234. package/src/bodyparser/parseSize.ts +17 -17
  235. package/src/bodyparser/qsParse.ts +96 -26
  236. package/src/codemods/source.ts +303 -0
  237. package/src/console/prompts.ts +3 -1
  238. package/src/container/Container.ts +97 -81
  239. package/src/events/Emitter.ts +37 -10
  240. package/src/events/helix/helpers.ts +24 -19
  241. package/src/graphql/GraphQLEngine.ts +91 -14
  242. package/src/helpers/MessageBuilder.ts +4 -2
  243. package/src/helpers/errors.ts +16 -0
  244. package/src/helpers/napi-loader.ts +3 -2
  245. package/src/http/Exception.ts +17 -2
  246. package/src/http/HttpContext.ts +34 -5
  247. package/src/http/RedirectBuilder.ts +51 -37
  248. package/src/http/Request.ts +102 -58
  249. package/src/http/Response.ts +15 -7
  250. package/src/http/SseStream.ts +6 -7
  251. package/src/http/permissions.ts +47 -0
  252. package/src/http-client/HttpClient.ts +505 -0
  253. package/src/http-client/addresses.ts +58 -0
  254. package/src/http-client/fake.ts +141 -0
  255. package/src/http-client/index.ts +21 -0
  256. package/src/index.ts +6 -1
  257. package/src/middleware/Pipeline.ts +7 -3
  258. package/src/native/generated.ts +106 -48
  259. package/src/profiler/Profiler.ts +4 -1
  260. package/src/profiler/client/toolbar.css +595 -0
  261. package/src/profiler/client/toolbar.js +1299 -0
  262. package/src/profiler/client.generated.ts +7 -0
  263. package/src/profiler/collectors.ts +156 -19
  264. package/src/profiler/endpoint.ts +27 -1
  265. package/src/profiler/index.ts +17 -3
  266. package/src/profiler/middleware.ts +74 -9
  267. package/src/profiler/preview.ts +104 -0
  268. package/src/profiler/toolbar.ts +28 -91
  269. package/src/profiler/types.ts +6 -1
  270. package/src/router/Router.ts +136 -13
  271. package/src/rpc/RpcProvider.ts +10 -1
  272. package/src/rpc/RpcRouter.ts +129 -45
  273. package/src/scheduler/Scheduler.ts +1 -1
  274. package/src/scheduler/locks/RedisLockBackend.ts +12 -4
  275. package/src/security/CookieSigner.ts +30 -8
  276. package/src/security/SignedUrl.ts +18 -17
  277. package/src/server/Server.ts +60 -12
  278. package/src/server/nativeGrpc.ts +59 -0
  279. package/src/server/nativeLimits.ts +111 -0
  280. package/src/session/Session.ts +29 -4
  281. package/src/session/SessionMiddleware.ts +30 -10
  282. package/src/session/config.ts +7 -0
  283. package/src/session/drivers/MemoryDriver.ts +26 -7
  284. package/src/session/errors.ts +31 -0
  285. package/src/session/templateTags.ts +57 -0
  286. package/src/storage/StaticMiddleware.ts +46 -27
  287. package/src/testing/ApiResponse.ts +7 -1
  288. package/src/testing/TestClient.ts +14 -5
  289. package/src/utils/Macroable.ts +2 -2
  290. package/scripts/build-napi-types.mjs +0 -69
  291. package/scripts/copy-napi.mjs +0 -121
  292. package/scripts/generate-mime.mjs +0 -119
  293. package/scripts/generate-napi-types.mjs +0 -187
@@ -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
- const listeners = [
239
- ...(this.#stringListeners.get(event) ?? []),
240
- ...this.#wildcardListenersFor(event),
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
- events.push(JSON.parse(eventJson) as CollectedEvent)
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
- return expected.every(
237
- (item, i) => i < (actual as unknown[]).length && partialMatch((actual as unknown[])[i], item),
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 as object)) {
242
- if (!(key in (actual as object))) return false
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
- const rawBody = ctx.request.body()
283
- const body = parseGraphQLBody(rawBody)
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
- const message =
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
- const msg = err instanceof Error ? err.message : 'Resolver error'
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
- const userPerms = ctx.auth?.permissions ?? ctx.auth?.user?.permissions ?? []
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)) as RustParseResult
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`; `CookieSigner` and `SignedUrl` sign what this
11
- * produces.
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 as NodeJS.ErrnoException | undefined)?.code
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: ${(e as Error).message}`,
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
  },
@@ -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`; anything else collapses to a generic line. AdonisJS
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.code !== 'E_UNKNOWN') return error.message
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
 
@@ -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
- /** Subdomains of the request host (AdonisJS `ctx.subdomains`). */
397
- get subdomains(): string[] {
398
- return this.request.subdomains()
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
  )