@c9up/ream 0.2.28 → 0.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/README.md +5 -6
  2. package/dist/Codemods.d.ts +24 -0
  3. package/dist/Codemods.d.ts.map +1 -1
  4. package/dist/Codemods.js +211 -1
  5. package/dist/Codemods.js.map +1 -1
  6. package/dist/HttpKernel.d.ts.map +1 -1
  7. package/dist/HttpKernel.js +17 -5
  8. package/dist/HttpKernel.js.map +1 -1
  9. package/dist/Ignitor.d.ts +15 -2
  10. package/dist/Ignitor.d.ts.map +1 -1
  11. package/dist/Ignitor.js +33 -23
  12. package/dist/Ignitor.js.map +1 -1
  13. package/dist/bodyparser/BodyParserMiddleware.d.ts +4 -4
  14. package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
  15. package/dist/bodyparser/BodyParserMiddleware.js +41 -11
  16. package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
  17. package/dist/bodyparser/MultipartFile.d.ts +21 -15
  18. package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
  19. package/dist/bodyparser/MultipartFile.js +40 -58
  20. package/dist/bodyparser/MultipartFile.js.map +1 -1
  21. package/dist/bodyparser/parseSize.d.ts +7 -4
  22. package/dist/bodyparser/parseSize.d.ts.map +1 -1
  23. package/dist/bodyparser/parseSize.js +12 -17
  24. package/dist/bodyparser/parseSize.js.map +1 -1
  25. package/dist/bodyparser/qsParse.d.ts +20 -1
  26. package/dist/bodyparser/qsParse.d.ts.map +1 -1
  27. package/dist/bodyparser/qsParse.js +78 -25
  28. package/dist/bodyparser/qsParse.js.map +1 -1
  29. package/dist/codemods/source.d.ts +57 -0
  30. package/dist/codemods/source.d.ts.map +1 -0
  31. package/dist/codemods/source.js +282 -0
  32. package/dist/codemods/source.js.map +1 -0
  33. package/dist/console/prompts.d.ts.map +1 -1
  34. package/dist/console/prompts.js +3 -1
  35. package/dist/console/prompts.js.map +1 -1
  36. package/dist/container/Container.d.ts.map +1 -1
  37. package/dist/container/Container.js +61 -71
  38. package/dist/container/Container.js.map +1 -1
  39. package/dist/events/Emitter.d.ts +12 -0
  40. package/dist/events/Emitter.d.ts.map +1 -1
  41. package/dist/events/Emitter.js +36 -9
  42. package/dist/events/Emitter.js.map +1 -1
  43. package/dist/events/helix/helpers.d.ts.map +1 -1
  44. package/dist/events/helix/helpers.js +19 -9
  45. package/dist/events/helix/helpers.js.map +1 -1
  46. package/dist/graphql/GraphQLEngine.d.ts +8 -0
  47. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  48. package/dist/graphql/GraphQLEngine.js +79 -12
  49. package/dist/graphql/GraphQLEngine.js.map +1 -1
  50. package/dist/helpers/MessageBuilder.d.ts +4 -2
  51. package/dist/helpers/MessageBuilder.d.ts.map +1 -1
  52. package/dist/helpers/MessageBuilder.js +4 -2
  53. package/dist/helpers/MessageBuilder.js.map +1 -1
  54. package/dist/helpers/errors.d.ts +11 -0
  55. package/dist/helpers/errors.d.ts.map +1 -0
  56. package/dist/helpers/errors.js +17 -0
  57. package/dist/helpers/errors.js.map +1 -0
  58. package/dist/helpers/napi-loader.d.ts.map +1 -1
  59. package/dist/helpers/napi-loader.js +3 -2
  60. package/dist/helpers/napi-loader.js.map +1 -1
  61. package/dist/http/Exception.d.ts +14 -1
  62. package/dist/http/Exception.d.ts.map +1 -1
  63. package/dist/http/Exception.js +10 -2
  64. package/dist/http/Exception.js.map +1 -1
  65. package/dist/http/HttpContext.d.ts +31 -2
  66. package/dist/http/HttpContext.d.ts.map +1 -1
  67. package/dist/http/HttpContext.js +12 -5
  68. package/dist/http/HttpContext.js.map +1 -1
  69. package/dist/http/RedirectBuilder.d.ts +9 -3
  70. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  71. package/dist/http/RedirectBuilder.js +43 -37
  72. package/dist/http/RedirectBuilder.js.map +1 -1
  73. package/dist/http/Request.d.ts +42 -17
  74. package/dist/http/Request.d.ts.map +1 -1
  75. package/dist/http/Request.js +80 -58
  76. package/dist/http/Request.js.map +1 -1
  77. package/dist/http/Response.d.ts +7 -4
  78. package/dist/http/Response.d.ts.map +1 -1
  79. package/dist/http/Response.js +14 -6
  80. package/dist/http/Response.js.map +1 -1
  81. package/dist/http/SseStream.js +6 -7
  82. package/dist/http/SseStream.js.map +1 -1
  83. package/dist/http/permissions.d.ts +25 -0
  84. package/dist/http/permissions.d.ts.map +1 -0
  85. package/dist/http/permissions.js +38 -0
  86. package/dist/http/permissions.js.map +1 -0
  87. package/dist/http-client/HttpClient.d.ts +111 -0
  88. package/dist/http-client/HttpClient.d.ts.map +1 -0
  89. package/dist/http-client/HttpClient.js +386 -0
  90. package/dist/http-client/HttpClient.js.map +1 -0
  91. package/dist/http-client/addresses.d.ts +14 -0
  92. package/dist/http-client/addresses.d.ts.map +1 -0
  93. package/dist/http-client/addresses.js +57 -0
  94. package/dist/http-client/addresses.js.map +1 -0
  95. package/dist/http-client/fake.d.ts +50 -0
  96. package/dist/http-client/fake.d.ts.map +1 -0
  97. package/dist/http-client/fake.js +95 -0
  98. package/dist/http-client/fake.js.map +1 -0
  99. package/dist/http-client/index.d.ts +7 -0
  100. package/dist/http-client/index.d.ts.map +1 -0
  101. package/dist/http-client/index.js +7 -0
  102. package/dist/http-client/index.js.map +1 -0
  103. package/dist/index.d.ts +4 -3
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +1 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/middleware/Pipeline.d.ts.map +1 -1
  108. package/dist/middleware/Pipeline.js +6 -3
  109. package/dist/middleware/Pipeline.js.map +1 -1
  110. package/dist/native/generated.d.ts +93 -36
  111. package/dist/native/generated.d.ts.map +1 -1
  112. package/dist/profiler/Profiler.d.ts.map +1 -1
  113. package/dist/profiler/Profiler.js +4 -1
  114. package/dist/profiler/Profiler.js.map +1 -1
  115. package/dist/profiler/client.generated.d.ts +5 -0
  116. package/dist/profiler/client.generated.d.ts.map +1 -0
  117. package/dist/profiler/client.generated.js +6 -0
  118. package/dist/profiler/client.generated.js.map +1 -0
  119. package/dist/profiler/collectors.d.ts +92 -13
  120. package/dist/profiler/collectors.d.ts.map +1 -1
  121. package/dist/profiler/collectors.js +106 -19
  122. package/dist/profiler/collectors.js.map +1 -1
  123. package/dist/profiler/endpoint.d.ts +2 -1
  124. package/dist/profiler/endpoint.d.ts.map +1 -1
  125. package/dist/profiler/endpoint.js +25 -1
  126. package/dist/profiler/endpoint.js.map +1 -1
  127. package/dist/profiler/index.d.ts +8 -4
  128. package/dist/profiler/index.d.ts.map +1 -1
  129. package/dist/profiler/index.js +11 -4
  130. package/dist/profiler/index.js.map +1 -1
  131. package/dist/profiler/middleware.d.ts.map +1 -1
  132. package/dist/profiler/middleware.js +76 -10
  133. package/dist/profiler/middleware.js.map +1 -1
  134. package/dist/profiler/preview.d.ts +17 -0
  135. package/dist/profiler/preview.d.ts.map +1 -0
  136. package/dist/profiler/preview.js +100 -0
  137. package/dist/profiler/preview.js.map +1 -0
  138. package/dist/profiler/toolbar.d.ts +15 -23
  139. package/dist/profiler/toolbar.d.ts.map +1 -1
  140. package/dist/profiler/toolbar.js +25 -91
  141. package/dist/profiler/toolbar.js.map +1 -1
  142. package/dist/profiler/types.d.ts +6 -1
  143. package/dist/profiler/types.d.ts.map +1 -1
  144. package/dist/router/Router.d.ts +6 -0
  145. package/dist/router/Router.d.ts.map +1 -1
  146. package/dist/router/Router.js +115 -12
  147. package/dist/router/Router.js.map +1 -1
  148. package/dist/rpc/RpcProvider.d.ts +6 -0
  149. package/dist/rpc/RpcProvider.d.ts.map +1 -1
  150. package/dist/rpc/RpcProvider.js +4 -1
  151. package/dist/rpc/RpcProvider.js.map +1 -1
  152. package/dist/rpc/RpcRouter.d.ts +27 -14
  153. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  154. package/dist/rpc/RpcRouter.js +97 -40
  155. package/dist/rpc/RpcRouter.js.map +1 -1
  156. package/dist/scheduler/Scheduler.js +1 -1
  157. package/dist/scheduler/Scheduler.js.map +1 -1
  158. package/dist/scheduler/locks/RedisLockBackend.d.ts.map +1 -1
  159. package/dist/scheduler/locks/RedisLockBackend.js +13 -2
  160. package/dist/scheduler/locks/RedisLockBackend.js.map +1 -1
  161. package/dist/security/CookieSigner.d.ts.map +1 -1
  162. package/dist/security/CookieSigner.js +32 -8
  163. package/dist/security/CookieSigner.js.map +1 -1
  164. package/dist/security/SignedUrl.d.ts.map +1 -1
  165. package/dist/security/SignedUrl.js +16 -19
  166. package/dist/security/SignedUrl.js.map +1 -1
  167. package/dist/server/Server.d.ts +44 -12
  168. package/dist/server/Server.d.ts.map +1 -1
  169. package/dist/server/Server.js +31 -10
  170. package/dist/server/Server.js.map +1 -1
  171. package/dist/server/nativeGrpc.d.ts +27 -0
  172. package/dist/server/nativeGrpc.d.ts.map +1 -0
  173. package/dist/server/nativeGrpc.js +47 -0
  174. package/dist/server/nativeGrpc.js.map +1 -0
  175. package/dist/server/nativeLimits.d.ts +57 -0
  176. package/dist/server/nativeLimits.d.ts.map +1 -0
  177. package/dist/server/nativeLimits.js +75 -0
  178. package/dist/server/nativeLimits.js.map +1 -0
  179. package/dist/session/Session.d.ts +12 -0
  180. package/dist/session/Session.d.ts.map +1 -1
  181. package/dist/session/Session.js +13 -4
  182. package/dist/session/Session.js.map +1 -1
  183. package/dist/session/SessionMiddleware.d.ts.map +1 -1
  184. package/dist/session/SessionMiddleware.js +28 -9
  185. package/dist/session/SessionMiddleware.js.map +1 -1
  186. package/dist/session/config.d.ts +5 -0
  187. package/dist/session/config.d.ts.map +1 -1
  188. package/dist/session/config.js +8 -0
  189. package/dist/session/config.js.map +1 -1
  190. package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
  191. package/dist/session/drivers/MemoryDriver.js +26 -7
  192. package/dist/session/drivers/MemoryDriver.js.map +1 -1
  193. package/dist/session/errors.d.ts +17 -0
  194. package/dist/session/errors.d.ts.map +1 -1
  195. package/dist/session/errors.js +21 -0
  196. package/dist/session/errors.js.map +1 -1
  197. package/dist/session/templateTags.d.ts +14 -0
  198. package/dist/session/templateTags.d.ts.map +1 -1
  199. package/dist/session/templateTags.js +45 -17
  200. package/dist/session/templateTags.js.map +1 -1
  201. package/dist/storage/StaticMiddleware.d.ts.map +1 -1
  202. package/dist/storage/StaticMiddleware.js +49 -27
  203. package/dist/storage/StaticMiddleware.js.map +1 -1
  204. package/dist/testing/ApiResponse.d.ts.map +1 -1
  205. package/dist/testing/ApiResponse.js +7 -1
  206. package/dist/testing/ApiResponse.js.map +1 -1
  207. package/dist/testing/TestClient.d.ts.map +1 -1
  208. package/dist/testing/TestClient.js +16 -4
  209. package/dist/testing/TestClient.js.map +1 -1
  210. package/dist/utils/Macroable.d.ts +1 -1
  211. package/dist/utils/Macroable.js +2 -2
  212. package/dist/utils/Macroable.js.map +1 -1
  213. package/events.darwin-arm64.node +0 -0
  214. package/events.darwin-x64.node +0 -0
  215. package/events.linux-arm64-gnu.node +0 -0
  216. package/events.linux-x64-gnu.node +0 -0
  217. package/events.win32-x64-msvc.node +0 -0
  218. package/index.darwin-arm64.node +0 -0
  219. package/index.darwin-x64.node +0 -0
  220. package/index.linux-arm64-gnu.node +0 -0
  221. package/index.linux-x64-gnu.node +0 -0
  222. package/index.win32-x64-msvc.node +0 -0
  223. package/package.json +11 -3
  224. package/scheduler.darwin-arm64.node +0 -0
  225. package/scheduler.darwin-x64.node +0 -0
  226. package/scheduler.linux-arm64-gnu.node +0 -0
  227. package/scheduler.linux-x64-gnu.node +0 -0
  228. package/scheduler.win32-x64-msvc.node +0 -0
  229. package/src/Codemods.ts +314 -3
  230. package/src/HttpKernel.ts +18 -7
  231. package/src/Ignitor.ts +58 -26
  232. package/src/bodyparser/BodyParserMiddleware.ts +45 -16
  233. package/src/bodyparser/MultipartFile.ts +46 -67
  234. package/src/bodyparser/parseSize.ts +17 -17
  235. package/src/bodyparser/qsParse.ts +96 -26
  236. package/src/codemods/source.ts +303 -0
  237. package/src/console/prompts.ts +3 -1
  238. package/src/container/Container.ts +97 -81
  239. package/src/events/Emitter.ts +37 -10
  240. package/src/events/helix/helpers.ts +24 -19
  241. package/src/graphql/GraphQLEngine.ts +91 -14
  242. package/src/helpers/MessageBuilder.ts +4 -2
  243. package/src/helpers/errors.ts +16 -0
  244. package/src/helpers/napi-loader.ts +3 -2
  245. package/src/http/Exception.ts +17 -2
  246. package/src/http/HttpContext.ts +34 -5
  247. package/src/http/RedirectBuilder.ts +51 -37
  248. package/src/http/Request.ts +102 -58
  249. package/src/http/Response.ts +15 -7
  250. package/src/http/SseStream.ts +6 -7
  251. package/src/http/permissions.ts +47 -0
  252. package/src/http-client/HttpClient.ts +505 -0
  253. package/src/http-client/addresses.ts +58 -0
  254. package/src/http-client/fake.ts +141 -0
  255. package/src/http-client/index.ts +21 -0
  256. package/src/index.ts +6 -1
  257. package/src/middleware/Pipeline.ts +7 -3
  258. package/src/native/generated.ts +106 -48
  259. package/src/profiler/Profiler.ts +4 -1
  260. package/src/profiler/client/toolbar.css +595 -0
  261. package/src/profiler/client/toolbar.js +1299 -0
  262. package/src/profiler/client.generated.ts +7 -0
  263. package/src/profiler/collectors.ts +156 -19
  264. package/src/profiler/endpoint.ts +27 -1
  265. package/src/profiler/index.ts +17 -3
  266. package/src/profiler/middleware.ts +74 -9
  267. package/src/profiler/preview.ts +104 -0
  268. package/src/profiler/toolbar.ts +28 -91
  269. package/src/profiler/types.ts +6 -1
  270. package/src/router/Router.ts +136 -13
  271. package/src/rpc/RpcProvider.ts +10 -1
  272. package/src/rpc/RpcRouter.ts +129 -45
  273. package/src/scheduler/Scheduler.ts +1 -1
  274. package/src/scheduler/locks/RedisLockBackend.ts +12 -4
  275. package/src/security/CookieSigner.ts +30 -8
  276. package/src/security/SignedUrl.ts +18 -17
  277. package/src/server/Server.ts +60 -12
  278. package/src/server/nativeGrpc.ts +59 -0
  279. package/src/server/nativeLimits.ts +111 -0
  280. package/src/session/Session.ts +29 -4
  281. package/src/session/SessionMiddleware.ts +30 -10
  282. package/src/session/config.ts +7 -0
  283. package/src/session/drivers/MemoryDriver.ts +26 -7
  284. package/src/session/errors.ts +31 -0
  285. package/src/session/templateTags.ts +57 -0
  286. package/src/storage/StaticMiddleware.ts +46 -27
  287. package/src/testing/ApiResponse.ts +7 -1
  288. package/src/testing/TestClient.ts +14 -5
  289. package/src/utils/Macroable.ts +2 -2
  290. package/scripts/build-napi-types.mjs +0 -69
  291. package/scripts/copy-napi.mjs +0 -121
  292. package/scripts/generate-mime.mjs +0 -119
  293. package/scripts/generate-napi-types.mjs +0 -187
@@ -13,30 +13,39 @@ import type { Response } from './Response.js'
13
13
 
14
14
  export type RouteUrlResolver = (name: string, params?: Record<string, string>) => string
15
15
 
16
- function isSameOriginOrRelative(
17
- referer: string,
18
- requestUrl?: string,
19
- allowedHosts: readonly string[] = [],
16
+ /**
17
+ * AdonisJS `isValidRedirectUrl`: a relative path, or an absolute URL whose host
18
+ * is the request authority or an allowed host.
19
+ */
20
+ function isTrustedRedirect(
21
+ url: string,
22
+ authority: string | undefined,
23
+ allowedHosts: readonly string[],
20
24
  ): boolean {
21
25
  // A relative path is trusted only when it starts with a single "/" and has no
22
26
  // backslash: browsers normalise "\" to "/", so "/\evil.com" would become the
23
27
  // protocol-relative "//evil.com" that a lone `!startsWith("//")` check misses.
24
- if (referer.startsWith('/') && !referer.startsWith('//') && !referer.includes('\\')) return true
25
- let ref: URL
26
- try {
27
- ref = new URL(referer)
28
- } catch {
29
- return false
30
- }
31
- // An explicitly allowed host is trusted whatever the request's own origin is
32
- // — that is what the list is for (AdonisJS `allowedHosts`).
33
- if (allowedHosts.includes(ref.host)) return true
34
- if (!requestUrl) return false
28
+ if (url.startsWith('/') && !url.startsWith('//') && !url.includes('\\')) return true
29
+ let parsed: URL
35
30
  try {
36
- return ref.origin === new URL(requestUrl).origin
31
+ parsed = new URL(url)
37
32
  } catch {
38
33
  return false
39
34
  }
35
+ // Only a web URL is a redirect target: `javascript://app.test/%0aalert(1)`
36
+ // has the right host and is a script, not a page.
37
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false
38
+ if (allowedHosts.includes(parsed.host)) return true
39
+ // Upstream accepts any absolute URL when it knows no host to compare with;
40
+ // refusing it is the safer reading of an attacker-controlled header.
41
+ return authority !== undefined && parsed.host === authority
42
+ }
43
+
44
+ /** The query string of a path or URL, without the "?". */
45
+ function searchOf(url: string | undefined): string {
46
+ if (url === undefined) return ''
47
+ const index = url.indexOf('?')
48
+ return index === -1 ? '' : url.slice(index + 1)
40
49
  }
41
50
 
42
51
  /** The slice of the session the intended-URL methods use. */
@@ -60,6 +69,7 @@ export class RedirectBuilder {
60
69
  #forwardQs = false
61
70
  #requestUrl?: string
62
71
  #requestReferer?: string
72
+ #requestAuthority?: string
63
73
  #routeUrlResolver?: RouteUrlResolver
64
74
  #session?: IntendedUrlStore
65
75
  #isAjax = false
@@ -68,8 +78,14 @@ export class RedirectBuilder {
68
78
  constructor(
69
79
  response: Response,
70
80
  options?: {
81
+ /** The request path with its query string. */
71
82
  requestUrl?: string
72
83
  requestReferer?: string
84
+ /**
85
+ * The request authority (`:authority` or `Host`, never X-Forwarded-Host)
86
+ * an absolute redirect target must match.
87
+ */
88
+ requestAuthority?: string
73
89
  routeUrlResolver?: RouteUrlResolver
74
90
  /** The request session, for the intended-URL methods. */
75
91
  session?: IntendedUrlStore
@@ -82,6 +98,7 @@ export class RedirectBuilder {
82
98
  this.#response = response
83
99
  this.#requestUrl = options?.requestUrl
84
100
  this.#requestReferer = options?.requestReferer
101
+ this.#requestAuthority = options?.requestAuthority
85
102
  this.#routeUrlResolver = options?.routeUrlResolver
86
103
  this.#session = options?.session
87
104
  this.#isAjax = options?.isAjax ?? false
@@ -135,13 +152,9 @@ export class RedirectBuilder {
135
152
 
136
153
  #safeIntended<T extends string | null>(intended: string | null, fallback: T): string | T {
137
154
  if (!intended) return fallback
138
- if (this.#requestUrl && !isSameOriginOrRelative(intended, this.#requestUrl)) {
139
- return fallback
140
- }
141
- // A protocol-relative `//evil.com` is absolute to a browser even though it
142
- // looks relative — reject it whether or not a request URL is known.
143
- if (intended.startsWith('//')) return fallback
144
- return intended
155
+ return isTrustedRedirect(intended, this.#requestAuthority, this.allowedHosts)
156
+ ? intended
157
+ : fallback
145
158
  }
146
159
 
147
160
  /** Set redirect status code. */
@@ -191,7 +204,12 @@ export class RedirectBuilder {
191
204
 
192
205
  /** Redirect to an absolute or relative path. */
193
206
  toPath(path: string): void {
194
- const url = this.#appendQs(path)
207
+ this.#send(path, searchOf(this.#requestUrl))
208
+ }
209
+
210
+ /** Redirect with `withQs()` forwarding `forwardedSearch` (a query string without "?"). */
211
+ #send(path: string, forwardedSearch: string): void {
212
+ const url = this.#appendQs(path, forwardedSearch)
195
213
  this.#response.status(this.#status)
196
214
  this.#response.header('location', url)
197
215
  this.#response.setBody('')
@@ -207,12 +225,13 @@ export class RedirectBuilder {
207
225
  }
208
226
 
209
227
  /**
210
- * Redirect back to the previous page (Referer header).
211
- * Only trusts same-origin or relative referers — external URLs fall back
212
- * to `fallback` to prevent open redirect attacks.
228
+ * Redirect back to the previous page (AdonisJS `back`): the path of
229
+ * {@link getPreviousUrl}, so the redirect never leaves the site; `withQs()`
230
+ * forwards the previous page's query string, not the current request's.
213
231
  */
214
232
  back(fallback = '/'): void {
215
- this.toPath(this.getPreviousUrl(fallback))
233
+ const previous = new URL(this.getPreviousUrl(fallback), 'http://localhost')
234
+ this.#send(previous.pathname, previous.search.slice(1))
216
235
  }
217
236
 
218
237
  /**
@@ -225,7 +244,7 @@ export class RedirectBuilder {
225
244
  */
226
245
  getPreviousUrl(fallback = '/'): string {
227
246
  const referer = this.#requestReferer
228
- if (referer && isSameOriginOrRelative(referer, this.#requestUrl, this.allowedHosts)) {
247
+ if (referer && isTrustedRedirect(referer, this.#requestAuthority, this.allowedHosts)) {
229
248
  return referer
230
249
  }
231
250
  return fallback
@@ -236,15 +255,10 @@ export class RedirectBuilder {
236
255
  * uses, so `withQs()` plus `withQs('page', 2)` overrides an inbound `page`
237
256
  * rather than being overridden by it.
238
257
  */
239
- #appendQs(path: string): string {
258
+ #appendQs(path: string, forwardedSearch: string): string {
240
259
  const params = new URLSearchParams()
241
- if (this.#forwardQs && this.#requestUrl) {
242
- const qsIdx = this.#requestUrl.indexOf('?')
243
- if (qsIdx !== -1) {
244
- for (const [key, value] of new URLSearchParams(this.#requestUrl.slice(qsIdx + 1))) {
245
- params.append(key, value)
246
- }
247
- }
260
+ if (this.#forwardQs) {
261
+ for (const [key, value] of new URLSearchParams(forwardedSearch)) params.append(key, value)
248
262
  }
249
263
  for (const [key, value] of Object.entries(this.#qs)) {
250
264
  if (value === undefined || value === null) continue
@@ -13,6 +13,7 @@ import type { SignedUrl } from '../security/SignedUrl.js'
13
13
  import type { Dict } from '../types/helpers.js'
14
14
  import { Macroable } from '../utils/Macroable.js'
15
15
  import { getPath, omitPaths, pickPaths } from '../utils/objectPath.js'
16
+ import { Exception } from './Exception.js'
16
17
  import type {
17
18
  RequestAwareValidator,
18
19
  RequestValidationOptions,
@@ -37,6 +38,26 @@ export interface RawRequest {
37
38
  * `ip()` accessor falls back to legacy header parsing.
38
39
  */
39
40
  ip?: string
41
+ /**
42
+ * The trusted address chain, closest hop first — the socket peer, then each
43
+ * `X-Forwarded-For` entry the Rust layer walked right to left while the hop
44
+ * before it was a trusted proxy. Its last entry is {@link ip}. Absent in
45
+ * hand-built fixtures, where the chain is the peer alone.
46
+ */
47
+ ips?: string[]
48
+ /**
49
+ * Whether the socket peer is one of the trusted proxies configured with
50
+ * `server.trustedProxies()`, as the Rust layer decided against the same
51
+ * list that resolves {@link ip}. Absent in hand-built fixtures: untrusted.
52
+ */
53
+ peerTrusted?: boolean
54
+ /**
55
+ * The body was over its content type's limit (`config/bodyparser.ts`): the
56
+ * Rust layer did not keep it, and `body` is empty. Reading the body then
57
+ * throws `E_REQUEST_ENTITY_TOO_LARGE` instead of handing back nothing as if
58
+ * nothing had been sent.
59
+ */
60
+ bodyTooLarge?: boolean
40
61
  /**
41
62
  * Connection scheme as seen by the server (`http`/`https`), when the Rust
42
63
  * layer ships it. Defaults to `http` in `protocol()` when absent; the
@@ -72,7 +93,6 @@ export class Request extends Macroable {
72
93
  #cookieSigner?: CookieSigner
73
94
  #signedUrl?: SignedUrl
74
95
  #allowMethodSpoofing = false
75
- #trustProxy = false
76
96
  #routeInfo?: { name?: string; pattern: string; reference?: string }
77
97
  #response?: { fresh(): boolean }
78
98
  #parsedBody: Dict<unknown> | undefined
@@ -245,13 +265,17 @@ export class Request extends Macroable {
245
265
  }
246
266
 
247
267
  /**
248
- * Enable/disable trusting proxy headers (`X-Forwarded-Proto`/`-Host`/`-For`)
249
- * for `protocol()`/`host()`/`ips()`. OFF by default — turning it on when the
250
- * app is NOT behind a trusted proxy lets clients spoof their scheme/host/IP,
251
- * so it's an explicit opt-in (same stance as {@link setMethodSpoofing}).
268
+ * Whether the socket peer is one of the trusted proxies configured with
269
+ * `server.trustedProxies()` — the condition under which `X-Forwarded-Proto`
270
+ * and `X-Forwarded-Host` are believed by {@link protocol} and {@link host}.
271
+ *
272
+ * The decision is the Rust layer's (`peerTrusted`), made once per request
273
+ * against the same list that resolves {@link ip}, so a trusted proxy that
274
+ * forwards the scheme or host without an `X-Forwarded-For` is believed too.
275
+ * A request without the flag — a hand-built fixture — is untrusted.
252
276
  */
253
- setTrustProxy(enabled: boolean): void {
254
- this.#trustProxy = enabled
277
+ #peerIsTrustedProxy(): boolean {
278
+ return this.#raw.peerTrusted === true
255
279
  }
256
280
 
257
281
  /**
@@ -272,11 +296,12 @@ export class Request extends Macroable {
272
296
 
273
297
  /**
274
298
  * The request host including port (AdonisJS `host`). Honours
275
- * `X-Forwarded-Host` ONLY when trust-proxy is enabled; otherwise the `Host`
276
- * header. Returns null when neither is present.
299
+ * `X-Forwarded-Host` ONLY when the request came through a trusted proxy (see
300
+ * `server.trustedProxies()`); otherwise the `Host` header. Returns null when
301
+ * neither is present.
277
302
  */
278
303
  host(): string | null {
279
- if (this.#trustProxy) {
304
+ if (this.#peerIsTrustedProxy()) {
280
305
  const forwarded = this.#raw.headers['x-forwarded-host']
281
306
  // The first hop in the list, named rather than indexed: `split` always
282
307
  // yields one element, and saying so beats being told it might not.
@@ -287,12 +312,15 @@ export class Request extends Macroable {
287
312
  }
288
313
 
289
314
  /**
290
- * The request protocol — `http` or `https` (AdonisJS `protocol`). Honours
291
- * `X-Forwarded-Proto` only under trust-proxy; otherwise the connection scheme
292
- * the Rust layer reported (defaulting to `http`).
315
+ * The request protocol — `http` or `https` (AdonisJS `protocol`). A TLS
316
+ * connection is `https` whatever the headers say. Otherwise
317
+ * `X-Forwarded-Proto` is honoured only when the request came through a
318
+ * trusted proxy (see `server.trustedProxies()`), and the connection scheme
319
+ * the Rust layer reported (defaulting to `http`) is used when it is not.
293
320
  */
294
321
  protocol(): string {
295
- if (this.#trustProxy) {
322
+ if (this.#raw.scheme === 'https') return 'https'
323
+ if (this.#peerIsTrustedProxy()) {
296
324
  const forwarded = this.#raw.headers['x-forwarded-proto']
297
325
  const [first] = forwarded?.split(',') ?? []
298
326
  if (first !== undefined) return first.trim().toLowerCase()
@@ -306,20 +334,15 @@ export class Request extends Macroable {
306
334
  }
307
335
 
308
336
  /**
309
- * The client IP chain (AdonisJS `ips`). Under trust-proxy, the parsed
310
- * `X-Forwarded-For` list (left-most = original client); otherwise just the
311
- * single resolved {@link ip}.
337
+ * The trusted address chain, closest hop first (AdonisJS `ips`): the socket
338
+ * peer, then each `X-Forwarded-For` hop a trusted proxy vouched for. The
339
+ * last entry is {@link ip}. Hops added by an untrusted peer are not in it —
340
+ * the Rust layer stops at the first address it does not trust, so the chain
341
+ * goes past the peer only when {@link peerTrusted} holds.
312
342
  */
313
343
  ips(): string[] {
314
- if (this.#trustProxy) {
315
- const forwarded = this.#raw.headers['x-forwarded-for']
316
- if (forwarded) {
317
- return forwarded
318
- .split(',')
319
- .map((value) => value.trim())
320
- .filter((value) => value.length > 0)
321
- }
322
- }
344
+ const chain = this.#raw.ips
345
+ if (chain && chain.length > 0 && this.#peerIsTrustedProxy()) return [...chain]
323
346
  return [this.ip()]
324
347
  }
325
348
 
@@ -455,7 +478,9 @@ export class Request extends Macroable {
455
478
  encryptedCookie(name: string, defaultValue?: string): string | null
456
479
  encryptedCookie(name: string, defaultValue?: string): string | null {
457
480
  const raw = this.plainCookie<string>(name, undefined, { encoded: false })
458
- const value = raw === null || !this.#cookieSigner ? null : this.#cookieSigner.decrypt(raw)
481
+ // Decrypted for the purpose it was written under — its own name — so a
482
+ // value issued as another encrypted cookie is refused here.
483
+ const value = raw === null || !this.#cookieSigner ? null : this.#cookieSigner.decrypt(raw, name)
459
484
  return value ?? defaultValue ?? null
460
485
  }
461
486
 
@@ -477,17 +502,26 @@ export class Request extends Macroable {
477
502
  return this.#signedUrl.verify(this.url(true), purpose)
478
503
  }
479
504
 
480
- /** Get the raw body as a string (decoded from base64 if binary). */
505
+ /**
506
+ * Get the raw body as a string (decoded from base64 if binary). Throws
507
+ * `E_REQUEST_ENTITY_TOO_LARGE` (413) when the body was over its limit: what
508
+ * the server kept of it is nothing, and nothing is not the body.
509
+ */
481
510
  raw(): string {
482
511
  if (this.#rawBodyOverride !== undefined) return this.#rawBodyOverride
512
+ this.#assertBodyKept()
483
513
  if (this.#raw.bodyEncoding === 'base64') {
484
514
  return Buffer.from(this.#raw.body, 'base64').toString('utf8')
485
515
  }
486
516
  return this.#raw.body
487
517
  }
488
518
 
489
- /** Get the raw body as a Buffer (preserves binary integrity for file uploads). */
519
+ /**
520
+ * Get the raw body as a Buffer (preserves binary integrity for file uploads).
521
+ * Throws `E_REQUEST_ENTITY_TOO_LARGE` (413) when the body was over its limit.
522
+ */
490
523
  rawBuffer(): Buffer {
524
+ this.#assertBodyKept()
491
525
  if (this.#raw.bodyEncoding === 'base64') {
492
526
  return Buffer.from(this.#raw.body, 'base64')
493
527
  }
@@ -716,7 +750,8 @@ export class Request extends Macroable {
716
750
  id: this.id(),
717
751
  url: this.url(true),
718
752
  query: this.parsedUrl().search?.slice(1) ?? '',
719
- body: this.body(),
753
+ // A dump must not throw: an over-limit body is reported as absent.
754
+ body: this.#bodyWasDropped() ? undefined : this.body(),
720
755
  params: this.params(),
721
756
  headers: this.headers(),
722
757
  method: this.method(),
@@ -724,7 +759,9 @@ export class Request extends Macroable {
724
759
  cookies: this.cookies(),
725
760
  hostname: this.hostname(),
726
761
  ip: this.ip(),
727
- subdomains: this.subdomains(),
762
+ // What the route's domain captured (`ctx.subdomains`), as upstream
763
+ // serialises it — not the host's labels, which `subdomains()` returns.
764
+ subdomains: (this.ctx === undefined ? undefined : Reflect.get(this.ctx, 'subdomains')) ?? {},
728
765
  }
729
766
  }
730
767
 
@@ -1062,38 +1099,45 @@ export class Request extends Macroable {
1062
1099
  return this.#validated
1063
1100
  }
1064
1101
 
1065
- #ensureParsedBody(): void {
1066
- if (this.#parsedBody !== undefined) return
1067
- const raw = this.#raw.body
1068
- if (!raw || raw.length === 0) {
1069
- this.#parsedBody = {}
1070
- return
1071
- }
1072
- try {
1073
- const parsed: unknown = JSON.parse(raw)
1074
- this.#parsedBody = isPlainObject(parsed) ? parsed : { _body: parsed }
1075
- } catch {
1076
- this.#parsedBody = {}
1102
+ /** The body was over its limit and nothing has replaced it since. */
1103
+ #bodyWasDropped(): boolean {
1104
+ return (
1105
+ this.#raw.bodyTooLarge === true &&
1106
+ this.#parsedBody === undefined &&
1107
+ this.#rawBodyOverride === undefined
1108
+ )
1109
+ }
1110
+
1111
+ /**
1112
+ * Refuse to read a body the server did not keep. The same 413 the body
1113
+ * parser raises, so a route without it still answers the client properly
1114
+ * the moment it reaches for the body — and a route that never does is not
1115
+ * charged for it.
1116
+ */
1117
+ #assertBodyKept(): void {
1118
+ if (this.#raw.bodyTooLarge === true) {
1119
+ throw new Exception('request entity too large', {
1120
+ status: 413,
1121
+ code: 'E_REQUEST_ENTITY_TOO_LARGE',
1122
+ })
1077
1123
  }
1078
1124
  }
1079
- }
1080
1125
 
1081
- /** Parse a query string (key=value&key2=value2) into an object. */
1082
- /**
1083
- * Test whether an IPv4 address is within a CIDR range. Bare IP entries
1084
- * (`10.0.0.42`) are accepted as `/32`. IPv6 falls back to string equality
1085
- * — a full v6 implementation isn't required by the current security tests.
1086
- */
1087
- /**
1088
- * Type guard for plain JSON-decoded objects. Excludes `null` (which is
1089
- * `typeof === 'object'`) and arrays so consumers can index by string key
1090
- * without a separate runtime check.
1091
- */
1092
- function isPlainObject(value: unknown): value is Dict<unknown> {
1093
- return typeof value === 'object' && value !== null && !Array.isArray(value)
1126
+ /**
1127
+ * The body is whatever the body parser decided it is, and `{}` when no
1128
+ * parser claimed it. Nothing is parsed here: a JSON body is only JSON when
1129
+ * its Content-Type says so. Reading any body as JSON let a foreign page
1130
+ * drive a JSON route with a CORS simple request (no Content-Type, or
1131
+ * `text/plain`), which a browser sends cross-site with the user's cookies
1132
+ * and without a preflight.
1133
+ */
1134
+ #ensureParsedBody(): void {
1135
+ if (this.#parsedBody !== undefined) return
1136
+ this.#assertBodyKept()
1137
+ this.#parsedBody = {}
1138
+ }
1094
1139
  }
1095
1140
 
1096
- /** True for an IPv4 dotted-quad or a (bracketed) IPv6 literal — hosts with no subdomains. */
1097
1141
  /**
1098
1142
  * Headers whose VALUE is a credential. Listing them is useful; printing them
1099
1143
  * hands whoever reads the log a working key.
@@ -13,6 +13,7 @@ import { readFile, stat } from 'node:fs/promises'
13
13
  import { basename } from 'node:path'
14
14
  import { ReamError } from '../errors/ReamError.js'
15
15
  import { durationToSeconds } from '../helpers/duration.js'
16
+ import { toErrnoException } from '../helpers/errors.js'
16
17
  import { etag, matchesIfNoneMatch } from './etag.js'
17
18
  import { contentType } from './mime.js'
18
19
 
@@ -848,7 +849,7 @@ export class Response extends Macroable {
848
849
  size = (await stat(filePath)).size
849
850
  } catch (error) {
850
851
  if (errorCallback) {
851
- const [message, status] = errorCallback(error as NodeJS.ErrnoException)
852
+ const [message, status] = errorCallback(toErrnoException(error))
852
853
  this.status(status ?? 404).send(message)
853
854
  return
854
855
  }
@@ -1002,7 +1003,7 @@ export class Response extends Macroable {
1002
1003
  // The headers are already out, so there is no status left to change: end
1003
1004
  // the body and let the client see a short read. `errorCallback` is still
1004
1005
  // consulted for whatever logging it does.
1005
- errorCallback?.(err as NodeJS.ErrnoException)
1006
+ errorCallback?.(toErrnoException(err))
1006
1007
  } finally {
1007
1008
  // Never rethrows: this runs detached, and an unhandled rejection here
1008
1009
  // would take the process down over one client's download.
@@ -1022,7 +1023,7 @@ export class Response extends Macroable {
1022
1023
  }
1023
1024
  } catch (err) {
1024
1025
  const [message, status] = errorCallback
1025
- ? errorCallback(err as NodeJS.ErrnoException)
1026
+ ? errorCallback(toErrnoException(err))
1026
1027
  : ['Cannot process the request', 500]
1027
1028
  this.status(status ?? 500)
1028
1029
  this.send(message)
@@ -1133,7 +1134,9 @@ export class Response extends Macroable {
1133
1134
  'encryptedCookie() requires APP_KEY — set it so the encryption service is registered.',
1134
1135
  )
1135
1136
  }
1136
- return this.#writeCookie(name, this.#cookieSigner.encrypt(value), options)
1137
+ // The cookie name is the purpose, as for a signed cookie: a value issued
1138
+ // as one encrypted cookie cannot be replayed under another one's name.
1139
+ return this.#writeCookie(name, this.#cookieSigner.encrypt(value, undefined, name), options)
1137
1140
  }
1138
1141
 
1139
1142
  #writeCookie(name: string, value: string, options?: CookieOptions): this {
@@ -1195,10 +1198,15 @@ export class Response extends Macroable {
1195
1198
 
1196
1199
  /**
1197
1200
  * Expire a cookie immediately (AdonisJS `clearCookie`) — re-sends it with
1198
- * `Max-Age=0`, the RFC 6265 delete-now signal that `cookie()` honours.
1201
+ * `Max-Age=0` and an `Expires` in 1970, the RFC 6265 delete-now signals.
1202
+ *
1203
+ * A browser matches the cookie to replace on its name, domain and path, so
1204
+ * a cookie written with a `domain` (or a `path`) is only cleared when the
1205
+ * same ones are passed here; `secure` and `sameSite` are accepted so a
1206
+ * `SameSite=None` cookie can be cleared at all.
1199
1207
  */
1200
- clearCookie(name: string, options?: { path?: string }): this {
1201
- return this.plainCookie(name, '', { ...options, maxAge: 0 })
1208
+ clearCookie(name: string, options?: Omit<CookieOptions, 'maxAge' | 'expires'>): this {
1209
+ return this.plainCookie(name, '', { ...options, maxAge: 0, expires: new Date(0) })
1202
1210
  }
1203
1211
 
1204
1212
  // ─── Internals (used by HttpKernel for NAPI serialization) ─
@@ -253,14 +253,13 @@ function encodeFrame(event: string, data: string, eventId?: string): string {
253
253
  const lines: string[] = []
254
254
  // `event:` and `id:` are single-line tokens — strip CR/LF so a
255
255
  // caller-supplied event name or id can't splice extra SSE frames
256
- // (the same injection `comment()` guards against). `data` is safe:
257
- // it's split on `\n` and each line re-prefixed, so newlines there
258
- // stay inside one logical event.
256
+ // (the same injection `comment()` guards against).
259
257
  if (event) lines.push(`event: ${event.replace(/[\r\n]+/g, ' ')}`)
260
- // Multi-line data is allowed by SSE but each line must repeat the
261
- // `data: ` prefix. Splitting on `\n` keeps the browser's
262
- // reassembly intact (it joins back with `\n`).
263
- for (const line of data.split('\n')) {
258
+ // Multi-line data is allowed but each line must repeat the `data: `
259
+ // prefix. The event-stream parser ends a line on CRLF, LF AND a lone
260
+ // CR, so split on all three: a payload CR left in place would end the
261
+ // line and let the rest of the payload forge its own fields.
262
+ for (const line of data.split(/\r\n|\r|\n/)) {
264
263
  lines.push(`data: ${line}`)
265
264
  }
266
265
  if (eventId) lines.push(`id: ${eventId.replace(/[\r\n]+/g, ' ')}`)
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The permission check shared by the route, RPC and GraphQL gates, so the
3
+ * three cannot drift apart.
4
+ */
5
+
6
+ import type { AuthState } from './HttpContext.js'
7
+
8
+ /**
9
+ * Whether the request's identity holds every permission in `required` — what
10
+ * the route, RPC and GraphQL `.permission()` gates ask.
11
+ *
12
+ * When the auth state exposes `hasAllPermissions` (the auth layer resolves
13
+ * permissions itself, as warden does through its rights resolver), its
14
+ * answer decides: a permission granted only by the resolver passes, one left
15
+ * only in the payload does not. Anything but `true` denies, and an error from
16
+ * the resolver propagates rather than grants. Only an auth state without that
17
+ * method falls back to the payload: top-level `auth.permissions`, or
18
+ * `auth.user.permissions`.
19
+ *
20
+ * When the request was authenticated with a token, each permission must ALSO
21
+ * be allowed by `auth.user.currentAccessToken`: an intersection, never a
22
+ * union. A token scope named like a permission grants nothing the user
23
+ * lacks, and a token limited to `profile` does not carry every permission of
24
+ * its user. A token whose `allows` cannot be called denies.
25
+ */
26
+ export async function holdsPermissions(
27
+ auth: AuthState | undefined,
28
+ required: readonly string[],
29
+ ): Promise<boolean> {
30
+ if (!(await userHolds(auth, required))) return false
31
+ const token = auth?.user?.currentAccessToken
32
+ if (token === undefined || token === null) return true
33
+ return required.every(
34
+ (permission) => typeof token.allows === 'function' && token.allows(permission) === true,
35
+ )
36
+ }
37
+
38
+ async function userHolds(
39
+ auth: AuthState | undefined,
40
+ required: readonly string[],
41
+ ): Promise<boolean> {
42
+ if (typeof auth?.hasAllPermissions === 'function') {
43
+ return (await auth.hasAllPermissions(required)) === true
44
+ }
45
+ const granted = auth?.permissions ?? auth?.user?.permissions ?? []
46
+ return required.every((permission) => granted.includes(permission))
47
+ }