@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.
- 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 +56 -9
- 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 +431 -19
- 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 +93 -20
- package/src/http/RedirectBuilder.ts +4 -1
- package/src/http/Request.ts +264 -34
- package/src/http/Response.ts +504 -30
- 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,
|
|
@@ -29,7 +36,33 @@ function assertNoCRLF(name: string, value: string): void {
|
|
|
29
36
|
}
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
|
|
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
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
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',
|
|
102
|
-
this.#headers['content-type'] =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
/**
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
//
|
|
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
|
|
264
|
-
//
|
|
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.
|
|
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',
|