@c9up/ream 0.1.32 → 0.1.33
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/Application.d.ts +27 -0
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +29 -0
- package/dist/Application.js.map +1 -1
- package/dist/ErrorBoundary.d.ts.map +1 -1
- package/dist/ErrorBoundary.js +13 -1
- package/dist/ErrorBoundary.js.map +1 -1
- package/dist/HttpKernel.d.ts +8 -0
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +35 -12
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +8 -0
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +39 -0
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +11 -4
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +55 -43
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/qsParse.d.ts +52 -0
- package/dist/bodyparser/qsParse.d.ts.map +1 -0
- package/dist/bodyparser/qsParse.js +161 -0
- package/dist/bodyparser/qsParse.js.map +1 -0
- package/dist/bootstrap/hyper-server.d.ts.map +1 -1
- package/dist/bootstrap/hyper-server.js +2 -8
- package/dist/bootstrap/hyper-server.js.map +1 -1
- package/dist/container/Container.d.ts +41 -0
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +193 -26
- package/dist/container/Container.js.map +1 -1
- package/dist/container/ContainerResolver.d.ts +46 -0
- package/dist/container/ContainerResolver.d.ts.map +1 -0
- package/dist/container/ContainerResolver.js +60 -0
- package/dist/container/ContainerResolver.js.map +1 -0
- package/dist/container/ContextualBindingsBuilder.d.ts +20 -0
- package/dist/container/ContextualBindingsBuilder.d.ts.map +1 -0
- package/dist/container/ContextualBindingsBuilder.js +31 -0
- package/dist/container/ContextualBindingsBuilder.js.map +1 -0
- package/dist/container/types.d.ts +9 -6
- package/dist/container/types.d.ts.map +1 -1
- package/dist/decorators/Service.d.ts +1 -5
- package/dist/decorators/Service.d.ts.map +1 -1
- package/dist/decorators/Service.js +10 -1
- package/dist/decorators/Service.js.map +1 -1
- package/dist/events/Emitter.d.ts +66 -6
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +165 -28
- package/dist/events/Emitter.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +5 -5
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/helpers/duration.d.ts +32 -0
- package/dist/helpers/duration.d.ts.map +1 -0
- package/dist/helpers/duration.js +74 -0
- package/dist/helpers/duration.js.map +1 -0
- package/dist/helpers/napi-loader.d.ts +10 -1
- package/dist/helpers/napi-loader.d.ts.map +1 -1
- package/dist/helpers/napi-loader.js +18 -3
- package/dist/helpers/napi-loader.js.map +1 -1
- package/dist/helpers/types.d.ts +1 -1
- package/dist/helpers/types.d.ts.map +1 -1
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +17 -5
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/HttpContext.d.ts +16 -5
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +4 -2
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts +48 -2
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +95 -14
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts +128 -8
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +259 -8
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +74 -3
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +177 -4
- package/dist/http/Response.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/router/Router.d.ts +36 -7
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +66 -22
- package/dist/router/Router.js.map +1 -1
- package/dist/security/CookieSigner.d.ts +35 -4
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +99 -10
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/server/Server.d.ts +2 -3
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js +30 -13
- package/dist/server/Server.js.map +1 -1
- package/dist/session/ReadOnlyValuesStore.d.ts +24 -0
- package/dist/session/ReadOnlyValuesStore.d.ts.map +1 -0
- package/dist/session/ReadOnlyValuesStore.js +45 -0
- package/dist/session/ReadOnlyValuesStore.js.map +1 -0
- package/dist/session/Session.d.ts +181 -9
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +284 -33
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +87 -33
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts +66 -0
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -0
- package/dist/session/drivers/DatabaseDriver.js +132 -0
- package/dist/session/drivers/DatabaseDriver.js.map +1 -0
- package/dist/session/drivers/FileDriver.d.ts +22 -0
- package/dist/session/drivers/FileDriver.d.ts.map +1 -0
- package/dist/session/drivers/FileDriver.js +66 -0
- package/dist/session/drivers/FileDriver.js.map +1 -0
- package/dist/session/drivers/MemoryDriver.d.ts +10 -2
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +70 -0
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts +10 -2
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +58 -0
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/dist/session/templateTags.d.ts +42 -0
- package/dist/session/templateTags.d.ts.map +1 -0
- package/dist/session/templateTags.js +101 -0
- package/dist/session/templateTags.js.map +1 -0
- package/dist/testing/RequestBuilder.d.ts +38 -1
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +40 -1
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/testing/TestClient.d.ts +3 -1
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +3 -1
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/types/helpers.d.ts +1 -1
- package/dist/types/helpers.d.ts.map +1 -1
- package/dist/utils/Macroable.d.ts +14 -5
- package/dist/utils/Macroable.d.ts.map +1 -1
- package/dist/utils/Macroable.js +20 -18
- package/dist/utils/Macroable.js.map +1 -1
- package/events.win32-x64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Application.ts +39 -0
- package/src/ErrorBoundary.ts +13 -1
- package/src/HttpKernel.ts +45 -19
- package/src/Ignitor.ts +39 -0
- package/src/bodyparser/BodyParserMiddleware.ts +66 -44
- package/src/bodyparser/qsParse.ts +179 -0
- package/src/bootstrap/hyper-server.ts +2 -9
- package/src/container/Container.ts +265 -25
- package/src/container/ContainerResolver.ts +84 -0
- package/src/container/ContextualBindingsBuilder.ts +41 -0
- package/src/container/types.ts +11 -7
- package/src/decorators/Service.ts +16 -3
- package/src/events/Emitter.ts +200 -25
- package/src/graphql/GraphQLEngine.ts +7 -8
- package/src/helpers/duration.ts +71 -0
- package/src/helpers/napi-loader.ts +19 -3
- package/src/helpers/types.ts +4 -1
- package/src/http/Exception.ts +17 -5
- package/src/http/HttpContext.ts +20 -5
- package/src/http/RedirectBuilder.ts +126 -11
- package/src/http/Request.ts +283 -13
- package/src/http/Response.ts +213 -6
- package/src/index.ts +17 -3
- package/src/router/Router.ts +98 -33
- package/src/security/CookieSigner.ts +102 -10
- package/src/server/Server.ts +36 -16
- package/src/session/ReadOnlyValuesStore.ts +52 -0
- package/src/session/Session.ts +364 -37
- package/src/session/SessionMiddleware.ts +93 -30
- package/src/session/drivers/DatabaseDriver.ts +184 -0
- package/src/session/drivers/FileDriver.ts +82 -0
- package/src/session/drivers/MemoryDriver.ts +75 -2
- package/src/session/drivers/RedisDriver.ts +77 -2
- package/src/session/templateTags.ts +124 -0
- package/src/testing/RequestBuilder.ts +76 -0
- package/src/testing/TestClient.ts +10 -2
- package/src/types/helpers.ts +4 -1
- package/src/utils/Macroable.ts +23 -8
- package/dist/utils/inflect.d.ts +0 -17
- package/dist/utils/inflect.d.ts.map +0 -1
- package/dist/utils/inflect.js +0 -32
- package/dist/utils/inflect.js.map +0 -1
- package/src/utils/inflect.ts +0 -29
|
@@ -28,14 +28,23 @@ function isSameOriginOrRelative(referer: string, requestUrl?: string): boolean {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** The slice of the session the intended-URL methods use. */
|
|
32
|
+
export interface IntendedUrlStore {
|
|
33
|
+
setIntendedUrl(url: string): void
|
|
34
|
+
pullIntendedUrl(): string | null
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
export class RedirectBuilder {
|
|
32
38
|
#response: Response
|
|
33
39
|
#status = 302
|
|
34
|
-
#qs: Record<string,
|
|
40
|
+
#qs: Record<string, unknown> = {}
|
|
35
41
|
#forwardQs = false
|
|
36
42
|
#requestUrl?: string
|
|
37
43
|
#requestReferer?: string
|
|
38
44
|
#routeUrlResolver?: RouteUrlResolver
|
|
45
|
+
#session?: IntendedUrlStore
|
|
46
|
+
#isAjax = false
|
|
47
|
+
#hasRoute = true
|
|
39
48
|
|
|
40
49
|
constructor(
|
|
41
50
|
response: Response,
|
|
@@ -43,12 +52,77 @@ export class RedirectBuilder {
|
|
|
43
52
|
requestUrl?: string
|
|
44
53
|
requestReferer?: string
|
|
45
54
|
routeUrlResolver?: RouteUrlResolver
|
|
55
|
+
/** The request session, for the intended-URL methods. */
|
|
56
|
+
session?: IntendedUrlStore
|
|
57
|
+
/** Whether this is an XHR — `withIntendedUrl` skips those. */
|
|
58
|
+
isAjax?: boolean
|
|
59
|
+
/** Whether a route matched — `withIntendedUrl` skips 404s. */
|
|
60
|
+
hasRoute?: boolean
|
|
46
61
|
},
|
|
47
62
|
) {
|
|
48
63
|
this.#response = response
|
|
49
64
|
this.#requestUrl = options?.requestUrl
|
|
50
65
|
this.#requestReferer = options?.requestReferer
|
|
51
66
|
this.#routeUrlResolver = options?.routeUrlResolver
|
|
67
|
+
this.#session = options?.session
|
|
68
|
+
this.#isAjax = options?.isAjax ?? false
|
|
69
|
+
this.#hasRoute = options?.hasRoute ?? true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Remember where the user was heading, then carry on redirecting — AdonisJS'
|
|
74
|
+
* `withIntendedUrl` (a macro `@adonisjs/session` adds to Redirect).
|
|
75
|
+
*
|
|
76
|
+
* Only a GET, non-XHR, route-matched request is stored, exactly as upstream:
|
|
77
|
+
* remembering a POST would replay it as a GET after login, and remembering a
|
|
78
|
+
* 404 would send the user back to a dead end.
|
|
79
|
+
*/
|
|
80
|
+
withIntendedUrl(method = 'GET'): this {
|
|
81
|
+
if (
|
|
82
|
+
this.#session &&
|
|
83
|
+
method.toUpperCase() === 'GET' &&
|
|
84
|
+
!this.#isAjax &&
|
|
85
|
+
this.#hasRoute &&
|
|
86
|
+
this.#requestUrl
|
|
87
|
+
) {
|
|
88
|
+
this.#session.setIntendedUrl(this.#requestUrl)
|
|
89
|
+
}
|
|
90
|
+
return this
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Redirect to the remembered URL, consuming it — AdonisJS' `toIntended`.
|
|
95
|
+
*
|
|
96
|
+
* The stored value is validated before use: it is written from a request URL,
|
|
97
|
+
* but a session store is not a trust boundary, and a redirect to an absolute
|
|
98
|
+
* off-site URL is an open redirect. Anything not same-origin or relative
|
|
99
|
+
* falls back, which is what `back()` already does with the Referer.
|
|
100
|
+
*/
|
|
101
|
+
toIntended(fallback = '/'): void {
|
|
102
|
+
const intended = this.#session?.pullIntendedUrl() ?? null
|
|
103
|
+
this.toPath(this.#safeIntended(intended, fallback))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `toIntended`, falling back to a named route — AdonisJS' `toIntendedRoute`. */
|
|
107
|
+
toIntendedRoute(name: string, params?: Record<string, string>): void {
|
|
108
|
+
const intended = this.#session?.pullIntendedUrl() ?? null
|
|
109
|
+
const safe = intended === null ? null : this.#safeIntended(intended, null)
|
|
110
|
+
if (safe !== null) {
|
|
111
|
+
this.toPath(safe)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
this.toRoute(name, params)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#safeIntended<T extends string | null>(intended: string | null, fallback: T): string | T {
|
|
118
|
+
if (!intended) return fallback
|
|
119
|
+
if (this.#requestUrl && !isSameOriginOrRelative(intended, this.#requestUrl)) {
|
|
120
|
+
return fallback
|
|
121
|
+
}
|
|
122
|
+
// A protocol-relative `//evil.com` is absolute to a browser even though it
|
|
123
|
+
// looks relative — reject it whether or not a request URL is known.
|
|
124
|
+
if (intended.startsWith('//')) return fallback
|
|
125
|
+
return intended
|
|
52
126
|
}
|
|
53
127
|
|
|
54
128
|
/** Set redirect status code. */
|
|
@@ -57,13 +131,42 @@ export class RedirectBuilder {
|
|
|
57
131
|
return this
|
|
58
132
|
}
|
|
59
133
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Query string for the redirect target, in the four shapes AdonisJS accepts:
|
|
136
|
+
*
|
|
137
|
+
* withQs() forward the request's own query string
|
|
138
|
+
* withQs(false) stop forwarding it
|
|
139
|
+
* withQs({ a: 1, b: 2 }) merge these values in
|
|
140
|
+
* withQs('a', 1) set one value
|
|
141
|
+
*
|
|
142
|
+
* The object and name/value forms MERGE, as upstream does — calling it twice
|
|
143
|
+
* adds to what is there rather than replacing it.
|
|
144
|
+
*/
|
|
145
|
+
withQs(): this
|
|
146
|
+
withQs(forward: boolean): this
|
|
147
|
+
withQs(values: Record<string, unknown>): this
|
|
148
|
+
withQs(name: string, value: unknown): this
|
|
149
|
+
withQs(name?: boolean | string | Record<string, unknown>, value?: unknown): this {
|
|
150
|
+
if (name === undefined) {
|
|
65
151
|
this.#forwardQs = true
|
|
152
|
+
return this
|
|
153
|
+
}
|
|
154
|
+
if (typeof name === 'boolean') {
|
|
155
|
+
this.#forwardQs = name
|
|
156
|
+
return this
|
|
66
157
|
}
|
|
158
|
+
if (typeof name === 'string') {
|
|
159
|
+
this.#qs[name] = value
|
|
160
|
+
return this
|
|
161
|
+
}
|
|
162
|
+
Object.assign(this.#qs, name)
|
|
163
|
+
return this
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Drop every value added with {@link withQs}, and stop forwarding. */
|
|
167
|
+
clearQs(): this {
|
|
168
|
+
this.#qs = {}
|
|
169
|
+
this.#forwardQs = false
|
|
67
170
|
return this
|
|
68
171
|
}
|
|
69
172
|
|
|
@@ -98,14 +201,26 @@ export class RedirectBuilder {
|
|
|
98
201
|
}
|
|
99
202
|
}
|
|
100
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Forwarded query string first, explicit values on top — the order AdonisJS
|
|
206
|
+
* uses, so `withQs()` plus `withQs('page', 2)` overrides an inbound `page`
|
|
207
|
+
* rather than being overridden by it.
|
|
208
|
+
*/
|
|
101
209
|
#appendQs(path: string): string {
|
|
102
|
-
|
|
103
|
-
if (this.#
|
|
104
|
-
qs = new URLSearchParams(this.#qs).toString()
|
|
105
|
-
} else if (this.#forwardQs && this.#requestUrl) {
|
|
210
|
+
const params = new URLSearchParams()
|
|
211
|
+
if (this.#forwardQs && this.#requestUrl) {
|
|
106
212
|
const qsIdx = this.#requestUrl.indexOf('?')
|
|
107
|
-
if (qsIdx !== -1)
|
|
213
|
+
if (qsIdx !== -1) {
|
|
214
|
+
for (const [key, value] of new URLSearchParams(this.#requestUrl.slice(qsIdx + 1))) {
|
|
215
|
+
params.append(key, value)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const [key, value] of Object.entries(this.#qs)) {
|
|
220
|
+
if (value === undefined || value === null) continue
|
|
221
|
+
params.set(key, String(value))
|
|
108
222
|
}
|
|
223
|
+
const qs = params.toString()
|
|
109
224
|
if (!qs) return path
|
|
110
225
|
return path.includes('?') ? `${path}&${qs}` : `${path}?${qs}`
|
|
111
226
|
}
|
package/src/http/Request.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { SignedUrl } from '../security/SignedUrl.js'
|
|
|
12
12
|
import type { Dict } from '../types/helpers.js'
|
|
13
13
|
import { Macroable } from '../utils/Macroable.js'
|
|
14
14
|
import { getPath, omitPaths, pickPaths } from '../utils/objectPath.js'
|
|
15
|
+
import { unpackCookieValue } from './Response.js'
|
|
15
16
|
|
|
16
17
|
export interface RawRequest {
|
|
17
18
|
method: string
|
|
@@ -60,7 +61,7 @@ export interface RawRequest {
|
|
|
60
61
|
|
|
61
62
|
export class Request extends Macroable {
|
|
62
63
|
#raw: RawRequest
|
|
63
|
-
#params: Dict
|
|
64
|
+
#params: Dict<string | string[]>
|
|
64
65
|
#cookieSigner?: CookieSigner
|
|
65
66
|
#signedUrl?: SignedUrl
|
|
66
67
|
#allowMethodSpoofing = false
|
|
@@ -71,6 +72,8 @@ export class Request extends Macroable {
|
|
|
71
72
|
#parsedQs: Dict<unknown> | undefined
|
|
72
73
|
#merged: Dict<unknown> | undefined
|
|
73
74
|
#original: Dict<unknown> | undefined
|
|
75
|
+
/** Set by {@link updateRawBody}; `raw()` reports it instead of the wire body. */
|
|
76
|
+
#rawBodyOverride: string | undefined
|
|
74
77
|
#files: Map<string, import('../bodyparser/MultipartFile.js').MultipartFile[]> = new Map()
|
|
75
78
|
#validated: unknown
|
|
76
79
|
|
|
@@ -91,7 +94,7 @@ export class Request extends Macroable {
|
|
|
91
94
|
*/
|
|
92
95
|
csrfProtected?: boolean
|
|
93
96
|
|
|
94
|
-
constructor(raw: RawRequest, params: Dict = {}) {
|
|
97
|
+
constructor(raw: RawRequest, params: Dict<string | string[]> = {}) {
|
|
95
98
|
super()
|
|
96
99
|
this.#raw = raw
|
|
97
100
|
this.#params = params
|
|
@@ -202,6 +205,22 @@ export class Request extends Macroable {
|
|
|
202
205
|
this.#trustProxy = enabled
|
|
203
206
|
}
|
|
204
207
|
|
|
208
|
+
/**
|
|
209
|
+
* The HTTP/2 `:authority` pseudo-header, falling back to `Host` (AdonisJS
|
|
210
|
+
* `authority`).
|
|
211
|
+
*
|
|
212
|
+
* Deliberately does NOT consult `X-Forwarded-Host` and ignores trust-proxy,
|
|
213
|
+
* as Adonis does: no proxy convention forwards the original `:authority`,
|
|
214
|
+
* so honouring a forwarded header here would let a proxy rewrite the value
|
|
215
|
+
* you validate a redirect target against. Use {@link host} when you do want
|
|
216
|
+
* the proxy-aware value.
|
|
217
|
+
*/
|
|
218
|
+
authority(): string | null {
|
|
219
|
+
const pseudo = this.#raw.headers[':authority']
|
|
220
|
+
if (pseudo) return pseudo
|
|
221
|
+
return this.#raw.headers.host ?? null
|
|
222
|
+
}
|
|
223
|
+
|
|
205
224
|
/**
|
|
206
225
|
* The request host including port (AdonisJS `host`). Honours
|
|
207
226
|
* `X-Forwarded-Host` ONLY when trust-proxy is enabled; otherwise the `Host`
|
|
@@ -313,15 +332,32 @@ export class Request extends Macroable {
|
|
|
313
332
|
* (tampered / not signed).
|
|
314
333
|
*/
|
|
315
334
|
cookie(name: string, defaultValue?: string): string | null {
|
|
316
|
-
const raw = this.plainCookie(name)
|
|
335
|
+
const raw = this.plainCookie<string>(name, undefined, { encoded: false })
|
|
317
336
|
const value = raw === null ? null : this.#cookieSigner ? this.#cookieSigner.unsign(raw) : raw
|
|
318
337
|
return value ?? defaultValue ?? null
|
|
319
338
|
}
|
|
320
339
|
|
|
321
|
-
/**
|
|
322
|
-
|
|
340
|
+
/**
|
|
341
|
+
* An unsigned cookie (AdonisJS `plainCookie`), unpacked.
|
|
342
|
+
*
|
|
343
|
+
* `response.plainCookie()` writes a base64url JSON envelope, so this reads
|
|
344
|
+
* the value BACK WITH ITS TYPE — an object stays an object, a number a
|
|
345
|
+
* number. A cookie set by something else, or written with `encode: false`,
|
|
346
|
+
* comes back as the raw string.
|
|
347
|
+
*
|
|
348
|
+
* Pass `encoded: false` to skip unpacking entirely.
|
|
349
|
+
*/
|
|
350
|
+
plainCookie<T = string>(
|
|
351
|
+
name: string,
|
|
352
|
+
defaultValue?: T,
|
|
353
|
+
options?: { encoded?: boolean },
|
|
354
|
+
): T | null {
|
|
323
355
|
const cookies = this.#raw.cookies ?? this.cookies()
|
|
324
|
-
|
|
356
|
+
const raw = cookies[name]
|
|
357
|
+
if (raw === undefined) return defaultValue ?? null
|
|
358
|
+
// Caller's claim about an untyped store, same contract as `input<T>`.
|
|
359
|
+
if (options?.encoded === false) return raw as T
|
|
360
|
+
return unpackCookieValue(raw) as T
|
|
325
361
|
}
|
|
326
362
|
|
|
327
363
|
/**
|
|
@@ -330,7 +366,7 @@ export class Request extends Macroable {
|
|
|
330
366
|
* encryption service.
|
|
331
367
|
*/
|
|
332
368
|
encryptedCookie(name: string, defaultValue?: string): string | null {
|
|
333
|
-
const raw = this.plainCookie(name)
|
|
369
|
+
const raw = this.plainCookie<string>(name, undefined, { encoded: false })
|
|
334
370
|
const value = raw === null || !this.#cookieSigner ? null : this.#cookieSigner.decrypt(raw)
|
|
335
371
|
return value ?? defaultValue ?? null
|
|
336
372
|
}
|
|
@@ -353,6 +389,7 @@ export class Request extends Macroable {
|
|
|
353
389
|
|
|
354
390
|
/** Get the raw body as a string (decoded from base64 if binary). */
|
|
355
391
|
raw(): string {
|
|
392
|
+
if (this.#rawBodyOverride !== undefined) return this.#rawBodyOverride
|
|
356
393
|
if (this.#raw.bodyEncoding === 'base64') {
|
|
357
394
|
return Buffer.from(this.#raw.body, 'base64').toString('utf8')
|
|
358
395
|
}
|
|
@@ -381,16 +418,216 @@ export class Request extends Macroable {
|
|
|
381
418
|
|
|
382
419
|
// ─── Route params ─────────────────────────────────────────
|
|
383
420
|
|
|
384
|
-
/**
|
|
421
|
+
/**
|
|
422
|
+
* Get a single route parameter.
|
|
423
|
+
*
|
|
424
|
+
* The catch-all `*` is an ARRAY of segments (AdonisJS hands it that way), so
|
|
425
|
+
* it is returned joined here — `param('*')` reading `undefined` because the
|
|
426
|
+
* value was not a string would be worse than the path it describes.
|
|
427
|
+
* Use {@link params} to get the segments themselves.
|
|
428
|
+
*/
|
|
385
429
|
param(key: string, defaultValue?: string): string | undefined {
|
|
386
|
-
|
|
430
|
+
const value = this.#params[key]
|
|
431
|
+
if (value === undefined) return defaultValue
|
|
432
|
+
return Array.isArray(value) ? value.join('/') : value
|
|
387
433
|
}
|
|
388
434
|
|
|
389
|
-
/** Get all route parameters. */
|
|
390
|
-
params(): Readonly<Dict
|
|
435
|
+
/** Get all route parameters; `*` holds its segments as an array. */
|
|
436
|
+
params(): Readonly<Dict<string | string[]>> {
|
|
391
437
|
return this.#params
|
|
392
438
|
}
|
|
393
439
|
|
|
440
|
+
// ─── Identity and shape ───────────────────────────────────
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* The request id from `x-request-id` (AdonisJS `id`).
|
|
444
|
+
*
|
|
445
|
+
* Absent when the proxy or client did not set one — it is not invented here,
|
|
446
|
+
* because a correlation id nobody else knows correlates nothing.
|
|
447
|
+
*/
|
|
448
|
+
id(): string | undefined {
|
|
449
|
+
return this.header('x-request-id')
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** The URL split into path and raw query string (AdonisJS `parsedUrl`). */
|
|
453
|
+
parsedUrl(): { pathname: string; search: string; query: string } {
|
|
454
|
+
const query = this.#raw.query ?? ''
|
|
455
|
+
return {
|
|
456
|
+
pathname: this.#raw.path,
|
|
457
|
+
search: query ? `?${query}` : '',
|
|
458
|
+
query,
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** `X-Requested-With: xmlhttprequest` (AdonisJS `ajax`). */
|
|
463
|
+
ajax(): boolean {
|
|
464
|
+
return (this.header('x-requested-with') ?? '').toLowerCase() === 'xmlhttprequest'
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Whether the client sent `X-Pjax` (AdonisJS `pjax`). */
|
|
468
|
+
pjax(): boolean {
|
|
469
|
+
return this.header('x-pjax') !== undefined
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Whether this is a speculative prefetch or prerender rather than a real
|
|
474
|
+
* navigation (AdonisJS `prefetch`).
|
|
475
|
+
*
|
|
476
|
+
* Worth checking before anything with a side effect: a browser may fetch a
|
|
477
|
+
* link the user never clicks, and counting that as a visit — or worse, acting
|
|
478
|
+
* on it — attributes an intention nobody had.
|
|
479
|
+
*/
|
|
480
|
+
prefetch(): boolean {
|
|
481
|
+
const purpose = (
|
|
482
|
+
this.header('sec-purpose') ??
|
|
483
|
+
this.header('purpose') ??
|
|
484
|
+
this.header('x-purpose') ??
|
|
485
|
+
this.header('x-moz') ??
|
|
486
|
+
''
|
|
487
|
+
).toLowerCase()
|
|
488
|
+
return purpose.includes('prefetch') || purpose.includes('prerender')
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* The full URL — `protocol://host/path` (AdonisJS `completeUrl`).
|
|
493
|
+
*
|
|
494
|
+
* Pass `true` to keep the query string.
|
|
495
|
+
*/
|
|
496
|
+
completeUrl(includeQueryString = false): string {
|
|
497
|
+
const path = includeQueryString ? this.url(true) : this.url(false)
|
|
498
|
+
return `${this.protocol()}://${this.host()}${path}`
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Where the user came from, per the `Referer` header — but only when it
|
|
503
|
+
* points somewhere we trust (AdonisJS `getPreviousUrl`).
|
|
504
|
+
*
|
|
505
|
+
* A referrer is attacker-controlled, so redirecting back to it unchecked is
|
|
506
|
+
* an open redirect. The host must be this request's own or one of
|
|
507
|
+
* `allowedHosts`; anything else falls back.
|
|
508
|
+
*/
|
|
509
|
+
getPreviousUrl(allowedHosts: string[] = [], fallback = '/'): string {
|
|
510
|
+
const referer = this.header('referer') ?? this.header('referrer')
|
|
511
|
+
if (!referer) return fallback
|
|
512
|
+
let parsed: URL
|
|
513
|
+
try {
|
|
514
|
+
parsed = new URL(referer)
|
|
515
|
+
} catch {
|
|
516
|
+
return fallback
|
|
517
|
+
}
|
|
518
|
+
const trusted = new Set([this.host(), ...allowedHosts])
|
|
519
|
+
if (!trusted.has(parsed.host)) return fallback
|
|
520
|
+
return `${parsed.pathname}${parsed.search}`
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ─── Content negotiation ──────────────────────────────────
|
|
524
|
+
|
|
525
|
+
/** Every media type the client accepts, best first (AdonisJS `types`). */
|
|
526
|
+
types(): string[] {
|
|
527
|
+
return this.#negotiated('accept')
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Every language the client accepts, best first (AdonisJS `languages`). */
|
|
531
|
+
languages(): string[] {
|
|
532
|
+
return this.#negotiated('accept-language')
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** Every charset the client accepts, best first (AdonisJS `charsets`). */
|
|
536
|
+
charsets(): string[] {
|
|
537
|
+
return this.#negotiated('accept-charset')
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* The best of the offered charsets per `Accept-Charset` (AdonisJS
|
|
542
|
+
* `charset`), or null when none matches. The plural {@link charsets} lists
|
|
543
|
+
* everything the client will take.
|
|
544
|
+
*/
|
|
545
|
+
charset<T extends string>(charsets: T[]): T | null {
|
|
546
|
+
return this.#bestOf('accept-charset', charsets)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Every encoding the client accepts, best first (AdonisJS `encodings`). */
|
|
550
|
+
encodings(): string[] {
|
|
551
|
+
return this.#negotiated('accept-encoding')
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* The best of the offered encodings per `Accept-Encoding` (AdonisJS
|
|
556
|
+
* `encoding`), or null when none matches.
|
|
557
|
+
*/
|
|
558
|
+
encoding<T extends string>(encodings: T[]): T | null {
|
|
559
|
+
return this.#bestOf('accept-encoding', encodings)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* The best of the offered values for one `Accept-*` header (AdonisJS
|
|
564
|
+
* `charset` / `encoding`).
|
|
565
|
+
*
|
|
566
|
+
* Client preference decides: the header's q-order is walked first, and the
|
|
567
|
+
* order the server offered them only breaks a tie within one q-bucket —
|
|
568
|
+
* the same rule {@link accepts} follows. An absent header means the client
|
|
569
|
+
* expressed no preference, so the server's first offer wins.
|
|
570
|
+
*/
|
|
571
|
+
#bestOf<T extends string>(header: string, offered: T[]): T | null {
|
|
572
|
+
const raw = this.#raw.headers[header]
|
|
573
|
+
if (!raw?.trim()) return offered[0] ?? null
|
|
574
|
+
const entries = parseAcceptHeader(raw)
|
|
575
|
+
if (entries.length === 0) return null
|
|
576
|
+
for (const bucket of bucketByQ(entries)) {
|
|
577
|
+
for (const value of offered) {
|
|
578
|
+
for (const entry of bucket) {
|
|
579
|
+
const candidate = entry.value.toLowerCase()
|
|
580
|
+
if (candidate === '*' || candidate === value.toLowerCase()) return value
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return null
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Parse one `Accept-*` header into the client's preference order.
|
|
589
|
+
*
|
|
590
|
+
* Sorted by q descending, ties broken by the order the client wrote them —
|
|
591
|
+
* which is what "preference" means when two entries share a q.
|
|
592
|
+
*/
|
|
593
|
+
#negotiated(header: string): string[] {
|
|
594
|
+
const raw = this.#raw.headers[header]
|
|
595
|
+
if (!raw) return []
|
|
596
|
+
return parseAcceptHeader(raw)
|
|
597
|
+
.sort((a, b) => b.q - a.q || a.index - b.index)
|
|
598
|
+
.map((entry) => entry.value)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ─── Serialization ────────────────────────────────────────
|
|
602
|
+
|
|
603
|
+
/** Every cookie, parsed (AdonisJS `cookiesList`). */
|
|
604
|
+
cookiesList(): Dict {
|
|
605
|
+
return this.cookies()
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* A JSON-safe view of the request (AdonisJS `serialize`), for logs and error
|
|
610
|
+
* reports. Carries no body: it may hold credentials, and a log line is the
|
|
611
|
+
* last place they should land.
|
|
612
|
+
*/
|
|
613
|
+
serialize(): Record<string, unknown> {
|
|
614
|
+
return {
|
|
615
|
+
id: this.id(),
|
|
616
|
+
url: this.url(true),
|
|
617
|
+
method: this.method(),
|
|
618
|
+
protocol: this.protocol(),
|
|
619
|
+
host: this.host(),
|
|
620
|
+
headers: this.headers(),
|
|
621
|
+
qs: this.qs(),
|
|
622
|
+
params: this.params(),
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Same as {@link serialize} — what `JSON.stringify(request)` uses. */
|
|
627
|
+
toJSON(): Record<string, unknown> {
|
|
628
|
+
return this.serialize()
|
|
629
|
+
}
|
|
630
|
+
|
|
394
631
|
// ─── Query string ─────────────────────────────────────────
|
|
395
632
|
|
|
396
633
|
/** Get parsed query string as an object. */
|
|
@@ -500,7 +737,7 @@ export class Request extends Macroable {
|
|
|
500
737
|
* Aliases (`'json'` ↔ `application/json`, `'html'` ↔ `text/html`,
|
|
501
738
|
* `'xml'` ↔ any subtype containing `xml`) are honoured.
|
|
502
739
|
*/
|
|
503
|
-
accepts(types:
|
|
740
|
+
accepts<T extends string>(types: T[]): T | null {
|
|
504
741
|
const header = this.#raw.headers.accept ?? '*/*'
|
|
505
742
|
const entries = parseAcceptHeader(header)
|
|
506
743
|
if (entries.length === 0) return null
|
|
@@ -523,7 +760,7 @@ export class Request extends Macroable {
|
|
|
523
760
|
* primary subtag) matches the highest-q client entry, falling back to
|
|
524
761
|
* `langs[0]` when nothing matches and the client header is empty/absent.
|
|
525
762
|
*/
|
|
526
|
-
language(langs:
|
|
763
|
+
language<T extends string>(langs: T[]): T | null {
|
|
527
764
|
const header = this.#raw.headers['accept-language'] ?? ''
|
|
528
765
|
if (!header.trim()) return langs[0] ?? null
|
|
529
766
|
const entries = parseAcceptHeader(header)
|
|
@@ -601,6 +838,39 @@ export class Request extends Macroable {
|
|
|
601
838
|
this.#merged = undefined // reset merged cache
|
|
602
839
|
}
|
|
603
840
|
|
|
841
|
+
/**
|
|
842
|
+
* Replace the request body (AdonisJS `updateBody`) — what a middleware that
|
|
843
|
+
* sanitizes or normalises input calls. {@link all} is recomputed from the
|
|
844
|
+
* query string and the new body on the next read.
|
|
845
|
+
*
|
|
846
|
+
* {@link original} keeps the first-seen input: flash old-input must show
|
|
847
|
+
* what the user actually sent, not what a middleware rewrote it into.
|
|
848
|
+
*/
|
|
849
|
+
updateBody(body: Dict<unknown>): void {
|
|
850
|
+
this.all() // snapshot the original before it is replaced
|
|
851
|
+
this.setParsedBody(body)
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Replace the query string data (AdonisJS `updateQs`). {@link all} is
|
|
856
|
+
* recomputed on the next read; the raw query string is left alone, so
|
|
857
|
+
* `parsedUrl()` still reports what the client sent.
|
|
858
|
+
*/
|
|
859
|
+
updateQs(data: Dict<unknown>): void {
|
|
860
|
+
this.all()
|
|
861
|
+
this.#parsedQs = { ...data }
|
|
862
|
+
this.#merged = undefined
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Replace the raw body (AdonisJS `updateRawBody`) — what the bodyparser
|
|
867
|
+
* sets when it cannot parse the body, or for multipart. Does NOT reparse:
|
|
868
|
+
* the parsed body is whatever {@link updateBody} last set.
|
|
869
|
+
*/
|
|
870
|
+
updateRawBody(rawBody: string): void {
|
|
871
|
+
this.#rawBodyOverride = rawBody
|
|
872
|
+
}
|
|
873
|
+
|
|
604
874
|
/**
|
|
605
875
|
* @internal Store the validated + transformed payload (called by the
|
|
606
876
|
* validation middleware after a route validator passes).
|