@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.
- package/dist/ConfigLoader.d.ts +46 -5
- package/dist/ConfigLoader.d.ts.map +1 -1
- package/dist/ConfigLoader.js +60 -9
- package/dist/ConfigLoader.js.map +1 -1
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +46 -6
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +21 -3
- package/dist/Ignitor.js.map +1 -1
- package/dist/container/Container.d.ts +23 -3
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +139 -60
- package/dist/container/Container.js.map +1 -1
- package/dist/env/Env.d.ts +29 -4
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +27 -4
- package/dist/env/Env.js.map +1 -1
- package/dist/env/Secret.d.ts +11 -0
- package/dist/env/Secret.d.ts.map +1 -0
- package/dist/env/Secret.js +36 -0
- package/dist/env/Secret.js.map +1 -0
- package/dist/env/interpolate.d.ts +26 -0
- package/dist/env/interpolate.d.ts.map +1 -0
- package/dist/env/interpolate.js +48 -0
- package/dist/env/interpolate.js.map +1 -0
- package/dist/env/loadEnvFiles.d.ts.map +1 -1
- package/dist/env/loadEnvFiles.js +14 -4
- package/dist/env/loadEnvFiles.js.map +1 -1
- package/dist/env/schema.d.ts +23 -6
- package/dist/env/schema.d.ts.map +1 -1
- package/dist/env/schema.js +67 -33
- package/dist/env/schema.js.map +1 -1
- package/dist/events/Emitter.d.ts +1 -1
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +1 -1
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/EventsProvider.d.ts +2 -2
- package/dist/events/EventsProvider.d.ts.map +1 -1
- package/dist/events/EventsProvider.js +3 -3
- package/dist/events/EventsProvider.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +1 -1
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +2 -2
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/graphql/GraphQLProvider.d.ts.map +1 -1
- package/dist/graphql/GraphQLProvider.js +7 -3
- package/dist/graphql/GraphQLProvider.js.map +1 -1
- package/dist/http/Exception.d.ts +63 -1
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +185 -48
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/HttpContext.d.ts +53 -7
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +56 -15
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +4 -1
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts +141 -10
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +244 -34
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +176 -9
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +404 -16
- package/dist/http/Response.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.js +1 -1
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/openapi/OpenApiProvider.js +4 -4
- package/dist/openapi/OpenApiProvider.js.map +1 -1
- package/dist/router/Router.d.ts +223 -31
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +520 -102
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcProvider.d.ts +1 -0
- package/dist/rpc/RpcProvider.d.ts.map +1 -1
- package/dist/rpc/RpcProvider.js +7 -3
- package/dist/rpc/RpcProvider.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +2 -2
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +5 -4
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
- package/dist/scheduler/ScheduleProvider.js +7 -15
- package/dist/scheduler/ScheduleProvider.js.map +1 -1
- package/dist/server/Server.d.ts +17 -4
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js +41 -7
- package/dist/server/Server.js.map +1 -1
- package/dist/session/SessionMiddleware.js +3 -3
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/storage/StaticProvider.js +1 -1
- package/dist/storage/StaticProvider.js.map +1 -1
- package/dist/testing/RequestBuilder.d.ts +60 -0
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +180 -1
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/testing/TestClient.d.ts +22 -1
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +40 -0
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +30 -0
- package/dist/utils/Macroable.d.ts.map +1 -0
- package/dist/utils/Macroable.js +43 -0
- package/dist/utils/Macroable.js.map +1 -0
- package/dist/utils/inflect.d.ts +17 -0
- package/dist/utils/inflect.d.ts.map +1 -0
- package/dist/utils/inflect.js +32 -0
- package/dist/utils/inflect.js.map +1 -0
- package/dist/utils/objectPath.d.ts +57 -0
- package/dist/utils/objectPath.d.ts.map +1 -0
- package/dist/utils/objectPath.js +141 -0
- package/dist/utils/objectPath.js.map +1 -0
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +6 -1
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/ConfigLoader.ts +72 -9
- package/src/HttpKernel.ts +53 -15
- package/src/Ignitor.ts +24 -3
- package/src/container/Container.ts +145 -61
- package/src/env/Env.ts +49 -5
- package/src/env/Secret.ts +41 -0
- package/src/env/interpolate.ts +54 -0
- package/src/env/loadEnvFiles.ts +12 -4
- package/src/env/schema.ts +109 -35
- package/src/events/Emitter.ts +2 -2
- package/src/events/EventsProvider.ts +5 -5
- package/src/graphql/GraphQLEngine.ts +4 -4
- package/src/graphql/GraphQLProvider.ts +7 -3
- package/src/http/Exception.ts +214 -37
- package/src/http/HttpContext.ts +87 -18
- package/src/http/RedirectBuilder.ts +4 -1
- package/src/http/Request.ts +264 -34
- package/src/http/Response.ts +478 -27
- package/src/index.ts +11 -2
- package/src/middleware/Pipeline.ts +1 -1
- package/src/openapi/OpenApiProvider.ts +4 -4
- package/src/router/Router.ts +631 -113
- package/src/rpc/RpcProvider.ts +6 -2
- package/src/rpc/RpcRouter.ts +7 -6
- package/src/scheduler/ScheduleProvider.ts +7 -15
- package/src/server/Server.ts +49 -10
- package/src/session/SessionMiddleware.ts +4 -4
- package/src/storage/StaticProvider.ts +1 -1
- package/src/testing/RequestBuilder.ts +218 -1
- package/src/testing/TestClient.ts +57 -1
- package/src/utils/Macroable.ts +54 -0
- package/src/utils/inflect.ts +29 -0
- package/src/utils/objectPath.ts +142 -0
- package/dist/decorators/Lazy.d.ts +0 -19
- package/dist/decorators/Lazy.d.ts.map +0 -1
- package/dist/decorators/Lazy.js +0 -53
- package/dist/decorators/Lazy.js.map +0 -1
- package/src/decorators/Lazy.ts +0 -57
package/src/http/Response.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
116
|
-
|
|
117
|
-
|
|
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',
|
|
120
|
-
this.#headers['content-type'] =
|
|
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
|
-
/**
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
//
|
|
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
|
|
287
|
-
//
|
|
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.
|
|
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',
|