@c9up/ream 0.1.25 → 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 +53 -7
  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 +404 -16
  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 +87 -18
  150. package/src/http/RedirectBuilder.ts +4 -1
  151. package/src/http/Request.ts +264 -34
  152. package/src/http/Response.ts +478 -27
  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,
@@ -47,7 +54,15 @@ function safeStringify(value: unknown): string {
47
54
  })
48
55
  }
49
56
 
50
- export class Response {
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 {
51
66
  #status = 200
52
67
  #headers: Record<string, string> = {}
53
68
  #cookies: string[] = []
@@ -56,6 +71,8 @@ export class Response {
56
71
  #redirectBuilderFactory?: () => RedirectBuilder
57
72
  #streamBackend?: StreamBackend
58
73
  #streamId?: string
74
+ #request?: { method(): string; header(key: string): string | undefined }
75
+ #cookieSigner?: CookieSigner
59
76
 
60
77
  /**
61
78
  * CSP nonce for this request (AdonisJS idiom: `response.nonce`). Seeded by
@@ -112,12 +129,54 @@ export class Response {
112
129
  return this
113
130
  }
114
131
 
115
- /** Set the Content-Type header. Chainable. */
116
- type(contentType: string): this {
117
- // 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
118
177
  // response-splitting hole just because it writes a fixed header key.
119
- assertNoCRLF('content-type', contentType)
120
- this.#headers['content-type'] = contentType
178
+ assertNoCRLF('content-type', value)
179
+ this.#headers['content-type'] = value
121
180
  return this
122
181
  }
123
182
 
@@ -130,6 +189,27 @@ export class Response {
130
189
  this.#finished = true
131
190
  }
132
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});`
210
+ this.#finished = true
211
+ }
212
+
133
213
  /** Send a response body. Auto-detects content type if not set. */
134
214
  send(data: unknown): void {
135
215
  if (typeof data === 'string') {
@@ -186,6 +266,254 @@ export class Response {
186
266
  this.#finished = true
187
267
  }
188
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
+
189
517
  // ─── Redirect ─────────────────────────────────────────────
190
518
 
191
519
  /** Get a redirect builder. */
@@ -199,6 +527,103 @@ export class Response {
199
527
  }
200
528
 
201
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
+
202
627
  setRedirectFactory(factory: () => RedirectBuilder): void {
203
628
  this.#redirectBuilderFactory = factory
204
629
  }
@@ -254,18 +679,41 @@ export class Response {
254
679
 
255
680
  // ─── Cookies ──────────────────────────────────────────────
256
681
 
257
- /** Set a response cookie. */
258
- cookie(
259
- name: string,
260
- value: string,
261
- options?: {
262
- maxAge?: number
263
- path?: string
264
- httpOnly?: boolean
265
- secure?: boolean
266
- sameSite?: 'lax' | 'strict' | 'none'
267
- },
268
- ): 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 {
269
717
  // SameSite=None cookies REQUIRE the Secure attribute — modern browsers
270
718
  // (Chrome 80+, Firefox, Safari) silently reject the cookie otherwise, so
271
719
  // a missing Secure here turns OAuth callbacks / iframe sessions into a
@@ -276,17 +724,12 @@ export class Response {
276
724
  )
277
725
  }
278
726
  const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`]
279
- // `maxAge: 0` is the RFC 6265 "delete-now" signal used by logout
280
- // flows. A truthiness check would skip it and leave the cookie
281
- // alive for the session — explicit `!== undefined` covers both
282
- // 0 and negative values that browsers also treat as immediate
283
- // 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.
284
729
  if (options?.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`)
285
730
  if (options?.path) {
286
- // `Path` is concatenated raw (encodeURIComponent would mangle the `/`
287
- // separators a path legitimately contains), so it needs the same
288
- // CRLF/NUL guard as header()/append() — otherwise a controlled path
289
- // 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).
290
733
  assertNoCRLF(`Set-Cookie path for '${name}'`, options.path)
291
734
  parts.push(`Path=${options.path}`)
292
735
  }
@@ -297,6 +740,14 @@ export class Response {
297
740
  return this
298
741
  }
299
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
+
300
751
  // ─── Internals (used by HttpKernel for NAPI serialization) ─
301
752
 
302
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',