@c9up/ream 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/ConfigLoader.d.ts +46 -5
  2. package/dist/ConfigLoader.d.ts.map +1 -1
  3. package/dist/ConfigLoader.js +60 -9
  4. package/dist/ConfigLoader.js.map +1 -1
  5. package/dist/HttpKernel.d.ts.map +1 -1
  6. package/dist/HttpKernel.js +46 -6
  7. package/dist/HttpKernel.js.map +1 -1
  8. package/dist/Ignitor.d.ts.map +1 -1
  9. package/dist/Ignitor.js +21 -3
  10. package/dist/Ignitor.js.map +1 -1
  11. package/dist/container/Container.d.ts +23 -3
  12. package/dist/container/Container.d.ts.map +1 -1
  13. package/dist/container/Container.js +139 -60
  14. package/dist/container/Container.js.map +1 -1
  15. package/dist/env/Env.d.ts +29 -4
  16. package/dist/env/Env.d.ts.map +1 -1
  17. package/dist/env/Env.js +27 -4
  18. package/dist/env/Env.js.map +1 -1
  19. package/dist/env/Secret.d.ts +11 -0
  20. package/dist/env/Secret.d.ts.map +1 -0
  21. package/dist/env/Secret.js +36 -0
  22. package/dist/env/Secret.js.map +1 -0
  23. package/dist/env/interpolate.d.ts +26 -0
  24. package/dist/env/interpolate.d.ts.map +1 -0
  25. package/dist/env/interpolate.js +48 -0
  26. package/dist/env/interpolate.js.map +1 -0
  27. package/dist/env/loadEnvFiles.d.ts.map +1 -1
  28. package/dist/env/loadEnvFiles.js +14 -4
  29. package/dist/env/loadEnvFiles.js.map +1 -1
  30. package/dist/env/schema.d.ts +23 -6
  31. package/dist/env/schema.d.ts.map +1 -1
  32. package/dist/env/schema.js +67 -33
  33. package/dist/env/schema.js.map +1 -1
  34. package/dist/events/Emitter.d.ts +1 -1
  35. package/dist/events/Emitter.d.ts.map +1 -1
  36. package/dist/events/Emitter.js +1 -1
  37. package/dist/events/Emitter.js.map +1 -1
  38. package/dist/events/EventsProvider.d.ts +2 -2
  39. package/dist/events/EventsProvider.d.ts.map +1 -1
  40. package/dist/events/EventsProvider.js +3 -3
  41. package/dist/events/EventsProvider.js.map +1 -1
  42. package/dist/graphql/GraphQLEngine.d.ts +1 -1
  43. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  44. package/dist/graphql/GraphQLEngine.js +2 -2
  45. package/dist/graphql/GraphQLEngine.js.map +1 -1
  46. package/dist/graphql/GraphQLProvider.d.ts.map +1 -1
  47. package/dist/graphql/GraphQLProvider.js +7 -3
  48. package/dist/graphql/GraphQLProvider.js.map +1 -1
  49. package/dist/http/Exception.d.ts +63 -1
  50. package/dist/http/Exception.d.ts.map +1 -1
  51. package/dist/http/Exception.js +185 -48
  52. package/dist/http/Exception.js.map +1 -1
  53. package/dist/http/HttpContext.d.ts +56 -9
  54. package/dist/http/HttpContext.d.ts.map +1 -1
  55. package/dist/http/HttpContext.js +56 -15
  56. package/dist/http/HttpContext.js.map +1 -1
  57. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  58. package/dist/http/RedirectBuilder.js +4 -1
  59. package/dist/http/RedirectBuilder.js.map +1 -1
  60. package/dist/http/Request.d.ts +141 -10
  61. package/dist/http/Request.d.ts.map +1 -1
  62. package/dist/http/Request.js +244 -34
  63. package/dist/http/Request.js.map +1 -1
  64. package/dist/http/Response.d.ts +176 -9
  65. package/dist/http/Response.d.ts.map +1 -1
  66. package/dist/http/Response.js +431 -19
  67. package/dist/http/Response.js.map +1 -1
  68. package/dist/index.d.ts +5 -4
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4 -3
  71. package/dist/index.js.map +1 -1
  72. package/dist/middleware/Pipeline.js +1 -1
  73. package/dist/middleware/Pipeline.js.map +1 -1
  74. package/dist/openapi/OpenApiProvider.js +4 -4
  75. package/dist/openapi/OpenApiProvider.js.map +1 -1
  76. package/dist/router/Router.d.ts +223 -31
  77. package/dist/router/Router.d.ts.map +1 -1
  78. package/dist/router/Router.js +520 -102
  79. package/dist/router/Router.js.map +1 -1
  80. package/dist/rpc/RpcProvider.d.ts +1 -0
  81. package/dist/rpc/RpcProvider.d.ts.map +1 -1
  82. package/dist/rpc/RpcProvider.js +7 -3
  83. package/dist/rpc/RpcProvider.js.map +1 -1
  84. package/dist/rpc/RpcRouter.d.ts +2 -2
  85. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  86. package/dist/rpc/RpcRouter.js +5 -4
  87. package/dist/rpc/RpcRouter.js.map +1 -1
  88. package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
  89. package/dist/scheduler/ScheduleProvider.js +7 -15
  90. package/dist/scheduler/ScheduleProvider.js.map +1 -1
  91. package/dist/server/Server.d.ts +17 -4
  92. package/dist/server/Server.d.ts.map +1 -1
  93. package/dist/server/Server.js +41 -7
  94. package/dist/server/Server.js.map +1 -1
  95. package/dist/session/SessionMiddleware.js +3 -3
  96. package/dist/session/SessionMiddleware.js.map +1 -1
  97. package/dist/storage/StaticProvider.js +1 -1
  98. package/dist/storage/StaticProvider.js.map +1 -1
  99. package/dist/testing/RequestBuilder.d.ts +60 -0
  100. package/dist/testing/RequestBuilder.d.ts.map +1 -1
  101. package/dist/testing/RequestBuilder.js +180 -1
  102. package/dist/testing/RequestBuilder.js.map +1 -1
  103. package/dist/testing/TestClient.d.ts +22 -1
  104. package/dist/testing/TestClient.d.ts.map +1 -1
  105. package/dist/testing/TestClient.js +40 -0
  106. package/dist/testing/TestClient.js.map +1 -1
  107. package/dist/utils/Macroable.d.ts +30 -0
  108. package/dist/utils/Macroable.d.ts.map +1 -0
  109. package/dist/utils/Macroable.js +43 -0
  110. package/dist/utils/Macroable.js.map +1 -0
  111. package/dist/utils/inflect.d.ts +17 -0
  112. package/dist/utils/inflect.d.ts.map +1 -0
  113. package/dist/utils/inflect.js +32 -0
  114. package/dist/utils/inflect.js.map +1 -0
  115. package/dist/utils/objectPath.d.ts +57 -0
  116. package/dist/utils/objectPath.d.ts.map +1 -0
  117. package/dist/utils/objectPath.js +141 -0
  118. package/dist/utils/objectPath.js.map +1 -0
  119. package/events.darwin-arm64.node +0 -0
  120. package/events.darwin-x64.node +0 -0
  121. package/events.linux-arm64-gnu.node +0 -0
  122. package/events.linux-x64-gnu.node +0 -0
  123. package/events.win32-x64-msvc.node +0 -0
  124. package/index.darwin-arm64.node +0 -0
  125. package/index.darwin-x64.node +0 -0
  126. package/index.linux-arm64-gnu.node +0 -0
  127. package/index.linux-x64-gnu.node +0 -0
  128. package/index.win32-x64-msvc.node +0 -0
  129. package/package.json +6 -1
  130. package/scheduler.darwin-arm64.node +0 -0
  131. package/scheduler.darwin-x64.node +0 -0
  132. package/scheduler.linux-arm64-gnu.node +0 -0
  133. package/scheduler.linux-x64-gnu.node +0 -0
  134. package/scheduler.win32-x64-msvc.node +0 -0
  135. package/src/ConfigLoader.ts +72 -9
  136. package/src/HttpKernel.ts +53 -15
  137. package/src/Ignitor.ts +24 -3
  138. package/src/container/Container.ts +145 -61
  139. package/src/env/Env.ts +49 -5
  140. package/src/env/Secret.ts +41 -0
  141. package/src/env/interpolate.ts +54 -0
  142. package/src/env/loadEnvFiles.ts +12 -4
  143. package/src/env/schema.ts +109 -35
  144. package/src/events/Emitter.ts +2 -2
  145. package/src/events/EventsProvider.ts +5 -5
  146. package/src/graphql/GraphQLEngine.ts +4 -4
  147. package/src/graphql/GraphQLProvider.ts +7 -3
  148. package/src/http/Exception.ts +214 -37
  149. package/src/http/HttpContext.ts +93 -20
  150. package/src/http/RedirectBuilder.ts +4 -1
  151. package/src/http/Request.ts +264 -34
  152. package/src/http/Response.ts +504 -30
  153. package/src/index.ts +11 -2
  154. package/src/middleware/Pipeline.ts +1 -1
  155. package/src/openapi/OpenApiProvider.ts +4 -4
  156. package/src/router/Router.ts +631 -113
  157. package/src/rpc/RpcProvider.ts +6 -2
  158. package/src/rpc/RpcRouter.ts +7 -6
  159. package/src/scheduler/ScheduleProvider.ts +7 -15
  160. package/src/server/Server.ts +49 -10
  161. package/src/session/SessionMiddleware.ts +4 -4
  162. package/src/storage/StaticProvider.ts +1 -1
  163. package/src/testing/RequestBuilder.ts +218 -1
  164. package/src/testing/TestClient.ts +57 -1
  165. package/src/utils/Macroable.ts +54 -0
  166. package/src/utils/inflect.ts +29 -0
  167. package/src/utils/objectPath.ts +142 -0
  168. package/dist/decorators/Lazy.d.ts +0 -19
  169. package/dist/decorators/Lazy.d.ts.map +0 -1
  170. package/dist/decorators/Lazy.js +0 -53
  171. package/dist/decorators/Lazy.js.map +0 -1
  172. package/src/decorators/Lazy.ts +0 -57
@@ -7,6 +7,13 @@
7
7
  * @implements FR21
8
8
  */
9
9
 
10
+ import { readFileSync } from 'node:fs'
11
+ import { basename } from 'node:path'
12
+ import etag from 'etag'
13
+ import { contentType } from 'mime-types'
14
+ import type { CookieSigner } from '../security/CookieSigner.js'
15
+ import { Macroable } from '../utils/Macroable.js'
16
+ import { E_HTTP_REQUEST_ABORTED } from './Exception.js'
10
17
  import type { RedirectBuilder } from './RedirectBuilder.js'
11
18
  import {
12
19
  openSseStream,
@@ -29,7 +36,33 @@ function assertNoCRLF(name: string, value: string): void {
29
36
  }
30
37
  }
31
38
 
32
- export class Response {
39
+ /**
40
+ * JSON.stringify with AdonisJS-parity safety: `BigInt` values are emitted as
41
+ * strings (native `JSON.stringify` throws on them) and circular references are
42
+ * dropped instead of throwing — so `response.json()` / an object `send()` never
43
+ * blows up on a payload the raw serializer can't handle.
44
+ */
45
+ function safeStringify(value: unknown): string {
46
+ const seen = new WeakSet<object>()
47
+ return JSON.stringify(value, (_key, val) => {
48
+ if (typeof val === 'bigint') return val.toString()
49
+ if (typeof val === 'object' && val !== null) {
50
+ if (seen.has(val)) return undefined
51
+ seen.add(val)
52
+ }
53
+ return val
54
+ })
55
+ }
56
+
57
+ export interface CookieOptions {
58
+ maxAge?: number
59
+ path?: string
60
+ httpOnly?: boolean
61
+ secure?: boolean
62
+ sameSite?: 'lax' | 'strict' | 'none'
63
+ }
64
+
65
+ export class Response extends Macroable {
33
66
  #status = 200
34
67
  #headers: Record<string, string> = {}
35
68
  #cookies: string[] = []
@@ -38,6 +71,8 @@ export class Response {
38
71
  #redirectBuilderFactory?: () => RedirectBuilder
39
72
  #streamBackend?: StreamBackend
40
73
  #streamId?: string
74
+ #request?: { method(): string; header(key: string): string | undefined }
75
+ #cookieSigner?: CookieSigner
41
76
 
42
77
  /**
43
78
  * CSP nonce for this request (AdonisJS idiom: `response.nonce`). Seeded by
@@ -94,12 +129,54 @@ export class Response {
94
129
  return this
95
130
  }
96
131
 
97
- /** Set the Content-Type header. Chainable. */
98
- type(contentType: string): this {
99
- // Same CRLF/NUL guard as header() — `type(userValue)` must not be a
132
+ /**
133
+ * Set a header only if it has not been set yet (AdonisJS `safeHeader`) —
134
+ * the idiom for defaulting a header from middleware without clobbering a
135
+ * value a downstream handler already chose.
136
+ */
137
+ safeHeader(key: string, value: string): this {
138
+ if (!this.getHeader(key)) this.header(key, value)
139
+ return this
140
+ }
141
+
142
+ /** Append field(s) to the `Vary` header, de-duplicated (AdonisJS parity). */
143
+ vary(field: string | string[]): this {
144
+ const fields = Array.isArray(field) ? field : [field]
145
+ const current = this.getHeader('vary')
146
+ const existing = current
147
+ ? current
148
+ .split(',')
149
+ .map((f) => f.trim())
150
+ .filter(Boolean)
151
+ : []
152
+ for (const f of fields) if (!existing.includes(f)) existing.push(f)
153
+ this.header('Vary', existing.join(', '))
154
+ return this
155
+ }
156
+
157
+ /** Set the `Location` header without issuing a redirect (AdonisJS parity). */
158
+ location(url: string): this {
159
+ this.header('Location', url)
160
+ return this
161
+ }
162
+
163
+ /**
164
+ * Set the Content-Type header. Chainable. AdonisJS parity
165
+ * (`response.type(type, charset?)`): `type` may be a full MIME type OR a file
166
+ * extension — `type('txt')` → `text/plain; charset=utf-8`, `type('json')` →
167
+ * `application/json; charset=utf-8` — resolved via `mime-types` exactly like
168
+ * Adonis. An optional `charset` is appended before resolution.
169
+ */
170
+ type(type: string, charset?: string): this {
171
+ const input = charset ? `${type}; charset=${charset}` : type
172
+ // `contentType()` returns `false` for an unrecognised value — fall back to
173
+ // the raw input rather than writing `content-type: false`.
174
+ const resolved = contentType(input)
175
+ const value = resolved === false ? input : resolved
176
+ // Same CRLF/NUL guard as header() — a resolved type must never be a
100
177
  // response-splitting hole just because it writes a fixed header key.
101
- assertNoCRLF('content-type', contentType)
102
- this.#headers['content-type'] = contentType
178
+ assertNoCRLF('content-type', value)
179
+ this.#headers['content-type'] = value
103
180
  return this
104
181
  }
105
182
 
@@ -108,7 +185,28 @@ export class Response {
108
185
  /** Send a JSON response. Sets content-type and stringifies. */
109
186
  json(data: unknown): void {
110
187
  this.#headers['content-type'] = 'application/json'
111
- this.#body = JSON.stringify(data)
188
+ this.#body = safeStringify(data)
189
+ this.#finished = true
190
+ }
191
+
192
+ /**
193
+ * Send a JSONP response (AdonisJS parity) — wraps the JSON body in a
194
+ * `callbackName(...)` call served as `text/javascript`. The callback name is
195
+ * sanitised to identifier-safe characters and U+2028 / U+2029 are escaped
196
+ * (valid in JSON, but break JS), so neither the callback nor the payload can
197
+ * inject script.
198
+ */
199
+ jsonp(body: unknown, callbackName = 'callback'): void {
200
+ // Sanitise the callback name to identifier-safe characters — the JSONP XSS
201
+ // guard (an attacker-controlled callback must not inject script).
202
+ const safeCallback = callbackName.replace(/[^\w$.]/g, '')
203
+ // Escape U+2028 / U+2029 — valid inside JSON strings but line terminators in
204
+ // JS, so they'd break the wrapped payload in a <script> context (Adonis parity).
205
+ const json = safeStringify(body).replace(/[\u2028\u2029]/g, (c) =>
206
+ c === '\u2028' ? '\\u2028' : '\\u2029',
207
+ )
208
+ this.#headers['content-type'] = 'text/javascript; charset=utf-8'
209
+ this.#body = `/**/ typeof ${safeCallback} === 'function' && ${safeCallback}(${json});`
112
210
  this.#finished = true
113
211
  }
114
212
 
@@ -116,7 +214,12 @@ export class Response {
116
214
  send(data: unknown): void {
117
215
  if (typeof data === 'string') {
118
216
  if (!this.#headers['content-type']) {
119
- this.#headers['content-type'] = 'text/html; charset=utf-8'
217
+ // AdonisJS parity: an HTML-looking string (opens with `<`) is served as
218
+ // `text/html`; a plain string as `text/plain` — not everything forced to
219
+ // HTML (which mislabels plain text and JSON/CSV/robots.txt bodies).
220
+ this.#headers['content-type'] = data.trimStart().startsWith('<')
221
+ ? 'text/html; charset=utf-8'
222
+ : 'text/plain; charset=utf-8'
120
223
  }
121
224
  this.#body = data
122
225
  } else if (Buffer.isBuffer(data)) {
@@ -127,7 +230,7 @@ export class Response {
127
230
  return
128
231
  } else if (typeof data === 'object' && data !== null) {
129
232
  this.#headers['content-type'] = 'application/json'
130
- this.#body = JSON.stringify(data)
233
+ this.#body = safeStringify(data)
131
234
  } else if (data !== undefined && data !== null) {
132
235
  if (!this.#headers['content-type']) {
133
236
  this.#headers['content-type'] = 'text/plain'
@@ -163,6 +266,254 @@ export class Response {
163
266
  this.#finished = true
164
267
  }
165
268
 
269
+ // ─── Descriptive status methods (AdonisJS parity) ────────
270
+ // Each sets the status then sends the (optional) body — mirrors
271
+ // `@adonisjs/http-server` Response (e.g. `response.notFound(body)`).
272
+
273
+ /** 100 Continue. */
274
+ continue(): void {
275
+ this.status(100)
276
+ }
277
+
278
+ /** 101 Switching Protocols. */
279
+ switchingProtocols(): void {
280
+ this.status(101)
281
+ }
282
+
283
+ /** 200 ok. */
284
+ ok(body?: unknown): void {
285
+ this.status(200)
286
+ this.send(body)
287
+ }
288
+
289
+ /** 201 created. */
290
+ created(body?: unknown): void {
291
+ this.status(201)
292
+ this.send(body)
293
+ }
294
+
295
+ /** 202 accepted. */
296
+ accepted(body?: unknown): void {
297
+ this.status(202)
298
+ this.send(body)
299
+ }
300
+
301
+ /** 203 nonAuthoritativeInformation. */
302
+ nonAuthoritativeInformation(body?: unknown): void {
303
+ this.status(203)
304
+ this.send(body)
305
+ }
306
+
307
+ /** 205 resetContent. */
308
+ resetContent(body?: unknown): void {
309
+ this.status(205)
310
+ this.send(body)
311
+ }
312
+
313
+ /** 206 partialContent. */
314
+ partialContent(body?: unknown): void {
315
+ this.status(206)
316
+ this.send(body)
317
+ }
318
+
319
+ /** 300 multipleChoices. */
320
+ multipleChoices(body?: unknown): void {
321
+ this.status(300)
322
+ this.send(body)
323
+ }
324
+
325
+ /** 301 movedPermanently. */
326
+ movedPermanently(body?: unknown): void {
327
+ this.status(301)
328
+ this.send(body)
329
+ }
330
+
331
+ /** 302 movedTemporarily. */
332
+ movedTemporarily(body?: unknown): void {
333
+ this.status(302)
334
+ this.send(body)
335
+ }
336
+
337
+ /** 303 seeOther. */
338
+ seeOther(body?: unknown): void {
339
+ this.status(303)
340
+ this.send(body)
341
+ }
342
+
343
+ /** 304 notModified. */
344
+ notModified(body?: unknown): void {
345
+ this.status(304)
346
+ this.send(body)
347
+ }
348
+
349
+ /** 305 useProxy. */
350
+ useProxy(body?: unknown): void {
351
+ this.status(305)
352
+ this.send(body)
353
+ }
354
+
355
+ /** 307 temporaryRedirect. */
356
+ temporaryRedirect(body?: unknown): void {
357
+ this.status(307)
358
+ this.send(body)
359
+ }
360
+
361
+ /** 400 badRequest. */
362
+ badRequest(body?: unknown): void {
363
+ this.status(400)
364
+ this.send(body)
365
+ }
366
+
367
+ /** 401 unauthorized. */
368
+ unauthorized(body?: unknown): void {
369
+ this.status(401)
370
+ this.send(body)
371
+ }
372
+
373
+ /** 402 paymentRequired. */
374
+ paymentRequired(body?: unknown): void {
375
+ this.status(402)
376
+ this.send(body)
377
+ }
378
+
379
+ /** 403 forbidden. */
380
+ forbidden(body?: unknown): void {
381
+ this.status(403)
382
+ this.send(body)
383
+ }
384
+
385
+ /** 404 notFound. */
386
+ notFound(body?: unknown): void {
387
+ this.status(404)
388
+ this.send(body)
389
+ }
390
+
391
+ /** 405 methodNotAllowed. */
392
+ methodNotAllowed(body?: unknown): void {
393
+ this.status(405)
394
+ this.send(body)
395
+ }
396
+
397
+ /** 406 notAcceptable. */
398
+ notAcceptable(body?: unknown): void {
399
+ this.status(406)
400
+ this.send(body)
401
+ }
402
+
403
+ /** 407 proxyAuthenticationRequired. */
404
+ proxyAuthenticationRequired(body?: unknown): void {
405
+ this.status(407)
406
+ this.send(body)
407
+ }
408
+
409
+ /** 408 requestTimeout. */
410
+ requestTimeout(body?: unknown): void {
411
+ this.status(408)
412
+ this.send(body)
413
+ }
414
+
415
+ /** 409 conflict. */
416
+ conflict(body?: unknown): void {
417
+ this.status(409)
418
+ this.send(body)
419
+ }
420
+
421
+ /** 410 gone. */
422
+ gone(body?: unknown): void {
423
+ this.status(410)
424
+ this.send(body)
425
+ }
426
+
427
+ /** 411 lengthRequired. */
428
+ lengthRequired(body?: unknown): void {
429
+ this.status(411)
430
+ this.send(body)
431
+ }
432
+
433
+ /** 412 preconditionFailed. */
434
+ preconditionFailed(body?: unknown): void {
435
+ this.status(412)
436
+ this.send(body)
437
+ }
438
+
439
+ /** 413 requestEntityTooLarge. */
440
+ requestEntityTooLarge(body?: unknown): void {
441
+ this.status(413)
442
+ this.send(body)
443
+ }
444
+
445
+ /** 414 requestUriTooLong. */
446
+ requestUriTooLong(body?: unknown): void {
447
+ this.status(414)
448
+ this.send(body)
449
+ }
450
+
451
+ /** 415 unsupportedMediaType. */
452
+ unsupportedMediaType(body?: unknown): void {
453
+ this.status(415)
454
+ this.send(body)
455
+ }
456
+
457
+ /** 416 requestedRangeNotSatisfiable. */
458
+ requestedRangeNotSatisfiable(body?: unknown): void {
459
+ this.status(416)
460
+ this.send(body)
461
+ }
462
+
463
+ /** 417 expectationFailed. */
464
+ expectationFailed(body?: unknown): void {
465
+ this.status(417)
466
+ this.send(body)
467
+ }
468
+
469
+ /** 422 unprocessableEntity. */
470
+ unprocessableEntity(body?: unknown): void {
471
+ this.status(422)
472
+ this.send(body)
473
+ }
474
+
475
+ /** 429 tooManyRequests. */
476
+ tooManyRequests(body?: unknown): void {
477
+ this.status(429)
478
+ this.send(body)
479
+ }
480
+
481
+ /** 500 internalServerError. */
482
+ internalServerError(body?: unknown): void {
483
+ this.status(500)
484
+ this.send(body)
485
+ }
486
+
487
+ /** 501 notImplemented. */
488
+ notImplemented(body?: unknown): void {
489
+ this.status(501)
490
+ this.send(body)
491
+ }
492
+
493
+ /** 502 badGateway. */
494
+ badGateway(body?: unknown): void {
495
+ this.status(502)
496
+ this.send(body)
497
+ }
498
+
499
+ /** 503 serviceUnavailable. */
500
+ serviceUnavailable(body?: unknown): void {
501
+ this.status(503)
502
+ this.send(body)
503
+ }
504
+
505
+ /** 504 gatewayTimeout. */
506
+ gatewayTimeout(body?: unknown): void {
507
+ this.status(504)
508
+ this.send(body)
509
+ }
510
+
511
+ /** 505 httpVersionNotSupported. */
512
+ httpVersionNotSupported(body?: unknown): void {
513
+ this.status(505)
514
+ this.send(body)
515
+ }
516
+
166
517
  // ─── Redirect ─────────────────────────────────────────────
167
518
 
168
519
  /** Get a redirect builder. */
@@ -176,6 +527,103 @@ export class Response {
176
527
  }
177
528
 
178
529
  /** @internal Set the redirect builder factory (injected by HttpContext). */
530
+ /** Give the response access to the request (wired by HttpContext) — used by `fresh()`. */
531
+ setRequest(request: { method(): string; header(key: string): string | undefined }): void {
532
+ this.#request = request
533
+ }
534
+
535
+ /** Inject the APP_KEY-backed cookie signer — wired by HttpContext. */
536
+ setCookieSigner(signer: CookieSigner): void {
537
+ this.#cookieSigner = signer
538
+ }
539
+
540
+ /** Set a strong (or weak) `ETag` for the given body (AdonisJS parity, `etag` pkg). */
541
+ setEtag(body: string | Buffer, weak = false): this {
542
+ this.header('Etag', etag(body, { weak }))
543
+ return this
544
+ }
545
+
546
+ /**
547
+ * Whether the client's cached copy is still fresh (AdonisJS `fresh`) — a
548
+ * cacheable method (GET/HEAD) with a 2xx/304 status whose `If-None-Match`
549
+ * matches this response's `ETag`. Lets a handler answer `304 Not Modified`.
550
+ */
551
+ fresh(): boolean {
552
+ const method = this.#request?.method()
553
+ if (method && method !== 'GET' && method !== 'HEAD') return false
554
+ const status = this.#status
555
+ if (!((status >= 200 && status < 300) || status === 304)) return false
556
+ const noneMatch = this.#request?.header('if-none-match')
557
+ const currentEtag = this.getHeader('etag')
558
+ if (!noneMatch || !currentEtag) return false
559
+ if (noneMatch.trim() === '*') return true
560
+ return noneMatch.split(',').some((tag) => {
561
+ const t = tag.trim()
562
+ return t === currentEtag || t === `W/${currentEtag}` || `W/${t}` === currentEtag
563
+ })
564
+ }
565
+
566
+ /**
567
+ * Abort the request with a body + status (AdonisJS parity) — throws
568
+ * `E_HTTP_REQUEST_ABORTED`, which the exception handler renders (a string body
569
+ * verbatim, anything else as JSON).
570
+ */
571
+ abort(body: unknown, status = 400): never {
572
+ throw new E_HTTP_REQUEST_ABORTED(body, status)
573
+ }
574
+
575
+ /** Abort only when `condition` is truthy (AdonisJS `abortIf`). */
576
+ abortIf(
577
+ condition: unknown,
578
+ body: unknown,
579
+ status = 400,
580
+ ): asserts condition is undefined | null | false {
581
+ if (condition) this.abort(body, status)
582
+ }
583
+
584
+ /**
585
+ * Send a file to the client (AdonisJS `download`) — reads the file, infers the
586
+ * content-type from its extension, and sends it as a binary body. On a read
587
+ * error, `errorCallback` may map it to `[message, status?]`, else a 404.
588
+ */
589
+ download(
590
+ filePath: string,
591
+ generateEtag = false,
592
+ errorCallback?: (error: NodeJS.ErrnoException) => [string, number?],
593
+ ): void {
594
+ let content: Buffer
595
+ try {
596
+ content = readFileSync(filePath)
597
+ } catch (error) {
598
+ if (errorCallback) {
599
+ const [message, status] = errorCallback(error as NodeJS.ErrnoException)
600
+ this.status(status ?? 404).send(message)
601
+ return
602
+ }
603
+ this.status(404).send('Not Found')
604
+ return
605
+ }
606
+ const ct = contentType(basename(filePath))
607
+ if (ct) this.#headers['content-type'] = ct
608
+ if (generateEtag) this.setEtag(content)
609
+ this.sendBuffer(content)
610
+ }
611
+
612
+ /**
613
+ * Force a file download with a `Content-Disposition` header (AdonisJS
614
+ * `attachment`). Defaults the filename to the file's basename.
615
+ */
616
+ attachment(
617
+ filePath: string,
618
+ name?: string,
619
+ disposition = 'attachment',
620
+ generateEtag = false,
621
+ ): void {
622
+ const filename = name ?? basename(filePath)
623
+ this.header('Content-Disposition', `${disposition}; filename="${filename}"`)
624
+ this.download(filePath, generateEtag)
625
+ }
626
+
179
627
  setRedirectFactory(factory: () => RedirectBuilder): void {
180
628
  this.#redirectBuilderFactory = factory
181
629
  }
@@ -231,18 +679,41 @@ export class Response {
231
679
 
232
680
  // ─── Cookies ──────────────────────────────────────────────
233
681
 
234
- /** Set a response cookie. */
235
- cookie(
236
- name: string,
237
- value: string,
238
- options?: {
239
- maxAge?: number
240
- path?: string
241
- httpOnly?: boolean
242
- secure?: boolean
243
- sameSite?: 'lax' | 'strict' | 'none'
244
- },
245
- ): this {
682
+ /**
683
+ * Set a SIGNED response cookie (AdonisJS default). The value is HMAC-signed
684
+ * with the app's `APP_KEY` (via the injected signer) so tampering is detected
685
+ * on read; `request.cookie()` verifies + unwraps it. Falls back to a plain
686
+ * cookie when no APP_KEY / encryption service is configured.
687
+ */
688
+ cookie(name: string, value: string, options?: CookieOptions): this {
689
+ const signed = this.#cookieSigner ? this.#cookieSigner.sign(value) : value
690
+ return this.#writeCookie(name, signed, options)
691
+ }
692
+
693
+ /**
694
+ * Set an UNSIGNED response cookie (AdonisJS `plainCookie`) — sent as-is. Use
695
+ * for values you already protect (encrypted session ids, CSRF tokens) or that
696
+ * a client-side script must read.
697
+ */
698
+ plainCookie(name: string, value: string, options?: CookieOptions): this {
699
+ return this.#writeCookie(name, value, options)
700
+ }
701
+
702
+ /**
703
+ * Set an ENCRYPTED response cookie (AdonisJS `encryptedCookie`) — AES-256-GCM
704
+ * encrypted with `APP_KEY`, unreadable + tamper-proof on the client. Requires
705
+ * a configured encryption service (APP_KEY).
706
+ */
707
+ encryptedCookie(name: string, value: string, options?: CookieOptions): this {
708
+ if (!this.#cookieSigner) {
709
+ throw new Error(
710
+ 'encryptedCookie() requires APP_KEY — set it so the encryption service is registered.',
711
+ )
712
+ }
713
+ return this.#writeCookie(name, this.#cookieSigner.encrypt(value), options)
714
+ }
715
+
716
+ #writeCookie(name: string, value: string, options?: CookieOptions): this {
246
717
  // SameSite=None cookies REQUIRE the Secure attribute — modern browsers
247
718
  // (Chrome 80+, Firefox, Safari) silently reject the cookie otherwise, so
248
719
  // a missing Secure here turns OAuth callbacks / iframe sessions into a
@@ -253,17 +724,12 @@ export class Response {
253
724
  )
254
725
  }
255
726
  const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`]
256
- // `maxAge: 0` is the RFC 6265 "delete-now" signal used by logout
257
- // flows. A truthiness check would skip it and leave the cookie
258
- // alive for the session — explicit `!== undefined` covers both
259
- // 0 and negative values that browsers also treat as immediate
260
- // expiry.
727
+ // `maxAge: 0` is the RFC 6265 "delete-now" signal used by logout flows. A
728
+ // truthiness check would skip it; explicit `!== undefined` covers 0 too.
261
729
  if (options?.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`)
262
730
  if (options?.path) {
263
- // `Path` is concatenated raw (encodeURIComponent would mangle the `/`
264
- // separators a path legitimately contains), so it needs the same
265
- // CRLF/NUL guard as header()/append() — otherwise a controlled path
266
- // injects into the `\n`-joined Set-Cookie header (response-splitting).
731
+ // `Path` is concatenated raw (encodeURIComponent would mangle `/`), so it
732
+ // needs the same CRLF/NUL guard as header() (response-splitting).
267
733
  assertNoCRLF(`Set-Cookie path for '${name}'`, options.path)
268
734
  parts.push(`Path=${options.path}`)
269
735
  }
@@ -274,6 +740,14 @@ export class Response {
274
740
  return this
275
741
  }
276
742
 
743
+ /**
744
+ * Expire a cookie immediately (AdonisJS `clearCookie`) — re-sends it with
745
+ * `Max-Age=0`, the RFC 6265 delete-now signal that `cookie()` honours.
746
+ */
747
+ clearCookie(name: string, options?: { path?: string }): this {
748
+ return this.plainCookie(name, '', { ...options, maxAge: 0 })
749
+ }
750
+
277
751
  // ─── Internals (used by HttpKernel for NAPI serialization) ─
278
752
 
279
753
  /** @internal Get the accumulated status code. */
package/src/index.ts CHANGED
@@ -9,14 +9,17 @@ import 'reflect-metadata'
9
9
  // ─── Core ───────────────────────────────────────────────────
10
10
 
11
11
  export { Application } from './Application.js'
12
- export { ConfigStore, defineModuleConfig, env } from './ConfigLoader.js'
12
+ export { ConfigStore, configProvider, defineModuleConfig, env } from './ConfigLoader.js'
13
13
  export { Container } from './container/Container.js'
14
14
  export type { Binding, ServiceFactory, ServiceScope, ServiceToken } from './container/types.js'
15
15
  export { Env, EnvValidationException } from './env/Env.js'
16
16
  export { loadEnvFiles } from './env/loadEnvFiles.js'
17
+ export { Secret } from './env/Secret.js'
17
18
  export {
18
19
  EnvVarError,
20
+ type OptionalCondition,
19
21
  type RequiredNode,
22
+ type SchemaFnOptions,
20
23
  type SchemaNode,
21
24
  type StringOptions,
22
25
  } from './env/schema.js'
@@ -24,6 +27,7 @@ export {
24
27
  // ─── HTTP (new — AdonisJS-compatible) ───────────────────────
25
28
 
26
29
  export {
30
+ createError,
27
31
  E_FORBIDDEN,
28
32
  E_HTTP_EXCEPTION,
29
33
  E_ROUTE_NOT_FOUND,
@@ -31,7 +35,12 @@ export {
31
35
  E_UNAUTHORIZED,
32
36
  E_VALIDATION_ERROR,
33
37
  Exception,
38
+ type ExceptionClass,
39
+ type ExceptionConstructor,
34
40
  ExceptionHandler,
41
+ InvalidArgumentsException,
42
+ RuntimeException,
43
+ type StatusPageRenderer,
35
44
  } from './http/Exception.js'
36
45
  export type { Authorizer, AuthState, RouteInfo } from './http/HttpContext.js'
37
46
  export { HttpContext } from './http/HttpContext.js'
@@ -39,6 +48,7 @@ export { RedirectBuilder } from './http/RedirectBuilder.js'
39
48
  export type { RawRequest } from './http/Request.js'
40
49
  export { Request } from './http/Request.js'
41
50
  export { Response } from './http/Response.js'
51
+ export { type GetterFn, Macroable, type MacroFn } from './utils/Macroable.js'
42
52
 
43
53
  // ─── Session ────────────────────────────────────────────────
44
54
 
@@ -116,7 +126,6 @@ export { SignedUrl } from './security/SignedUrl.js'
116
126
 
117
127
  // ─── Decorators ─────────────────────────────────────────────
118
128
 
119
- export { createLazyProxy, getLazyParams, Lazy } from './decorators/Lazy.js'
120
129
  export {
121
130
  clearServiceRegistry,
122
131
  getServiceMetadata,
@@ -138,7 +138,7 @@ function createGuardMiddleware(
138
138
  return async (ctx, next) => {
139
139
  const needsAuth =
140
140
  guards.length > 0 || (roles && roles.length > 0) || (permissions && permissions.length > 0)
141
- if (needsAuth && !ctx.auth.authenticated) {
141
+ if (needsAuth && !ctx.auth.isAuthenticated) {
142
142
  throw new E_UNAUTHORIZED()
143
143
  }
144
144
 
@@ -47,8 +47,8 @@ export class OpenApiProvider extends Provider {
47
47
  const config = this.app.config.get<OpenApiDocsConfig>('openapi') ?? {}
48
48
  if (config.enabled === false) return
49
49
 
50
- const generator = this.#override ?? this.#buildGenerator(config)
51
- const server = this.app.container.make<Server>('server')
50
+ const generator = this.#override ?? (await this.#buildGenerator(config))
51
+ const server = await this.app.container.make<Server>('server')
52
52
  const middleware = new OpenApiMiddleware({
53
53
  specPath: config.specPath,
54
54
  docsPath: config.docsPath,
@@ -57,8 +57,8 @@ export class OpenApiProvider extends Provider {
57
57
  server.use([(ctx, next) => middleware.handle(ctx, next)])
58
58
  }
59
59
 
60
- #buildGenerator(config: OpenApiDocsConfig): OpenApiGenerator {
61
- const router = this.app.container.make<Router>('router')
60
+ async #buildGenerator(config: OpenApiDocsConfig): Promise<OpenApiGenerator> {
61
+ const router = await this.app.container.make<Router>('router')
62
62
  return new OpenApiGenerator(router, {
63
63
  title: config.title ?? 'API',
64
64
  version: config.version ?? '1.0.0',