@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
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The limits the native server applies at the socket, read from the
3
+ * application's configuration.
4
+ *
5
+ * - Body limits come from `config/bodyparser.ts`: the native server enforces
6
+ * each section's `limit` WHILE reading the body, so nothing past it is ever
7
+ * buffered. The request still reaches routing, without its body and marked
8
+ * `bodyTooLarge`; reading the body then throws the 413 the body parser
9
+ * raises. The body parser keeps its own check; the two read one config.
10
+ * - Timeouts and the connection cap come from `config/app.ts` → `http`, where
11
+ * an AdonisJS app sets `headersTimeout`, `requestTimeout` and
12
+ * `keepAliveTimeout` for Node's server. `maxConnections` has no AdonisJS
13
+ * option (Node's `server.maxConnections` is left unbounded there); it is a
14
+ * deliberate DoS bound, and past it new clients wait in the listen backlog.
15
+ *
16
+ * Why the body limits are applied natively at all (NAPI constraint): the
17
+ * native server hands JS a request whose body is already read, where AdonisJS
18
+ * leaves the body in Node's stream until the parser pulls it. Without a limit
19
+ * at the read, every body is buffered before any JS-side limit can apply.
20
+ */
21
+
22
+ import type { BodyParserConfig } from '../bodyparser/BodyParserMiddleware.js'
23
+ import { parseSize } from '../bodyparser/parseSize.js'
24
+ import { ReamError } from '../errors/ReamError.js'
25
+
26
+ /** One body-parser section as the native server takes it (bytes). */
27
+ export interface NativeBodyLimit {
28
+ limit?: number
29
+ types?: string[]
30
+ }
31
+
32
+ /**
33
+ * The multipart section, with the part counts the native parser enforces
34
+ * while it parses: it stops at the first field or file past its bound.
35
+ */
36
+ export interface NativeMultipartLimit extends NativeBodyLimit {
37
+ maxFields?: number
38
+ maxFiles?: number
39
+ }
40
+
41
+ /** Body limits per body-parser section; an omitted section keeps its default. */
42
+ export interface NativeBodyLimits {
43
+ json?: NativeBodyLimit
44
+ form?: NativeBodyLimit
45
+ raw?: NativeBodyLimit
46
+ multipart?: NativeMultipartLimit
47
+ }
48
+
49
+ /** Timeouts in milliseconds (`0` disables one) and the connection cap (`0` = unbounded). */
50
+ export interface NativeConnectionConfig {
51
+ headersTimeout?: number
52
+ requestTimeout?: number
53
+ keepAliveTimeout?: number
54
+ maxConnections?: number
55
+ }
56
+
57
+ const SECTIONS = ['json', 'form', 'raw', 'multipart'] as const
58
+
59
+ /** Translate `config/bodyparser.ts` into the native server's body limits. */
60
+ export function nativeBodyLimits(config: BodyParserConfig): NativeBodyLimits {
61
+ const limits: NativeBodyLimits = {}
62
+ for (const section of SECTIONS) {
63
+ const given = config[section]
64
+ if (!given) continue
65
+ const native: NativeBodyLimit = {}
66
+ if (given.limit !== undefined) native.limit = parseSize(given.limit)
67
+ if (given.types !== undefined) native.types = [...given.types]
68
+ limits[section] = native
69
+ }
70
+ const multipart = config.multipart
71
+ if (multipart) {
72
+ limits.multipart = {
73
+ ...limits.multipart,
74
+ ...(multipart.maxFields !== undefined && { maxFields: multipart.maxFields }),
75
+ ...(multipart.maxFiles !== undefined && { maxFiles: multipart.maxFiles }),
76
+ }
77
+ }
78
+ return limits
79
+ }
80
+
81
+ const CONNECTION_KEYS = [
82
+ 'headersTimeout',
83
+ 'requestTimeout',
84
+ 'keepAliveTimeout',
85
+ 'maxConnections',
86
+ ] as const
87
+
88
+ const MAX_U32 = 2 ** 32 - 1
89
+
90
+ /**
91
+ * Pick the connection settings out of the `http` block of `config/app.ts`.
92
+ * Throws on a value that is not a whole number of milliseconds (or
93
+ * connections) the native server can hold, rather than letting it wrap.
94
+ */
95
+ export function nativeConnectionConfig(http: unknown): NativeConnectionConfig {
96
+ const config: NativeConnectionConfig = {}
97
+ if (typeof http !== 'object' || http === null) return config
98
+ for (const key of CONNECTION_KEYS) {
99
+ const value: unknown = Reflect.get(http, key)
100
+ if (value === undefined) continue
101
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > MAX_U32) {
102
+ throw new ReamError(
103
+ 'E_IGNITOR_INVALID_HTTP_CONFIG',
104
+ `\`http.${key}\` must be a whole number between 0 and ${MAX_U32}, received ${String(value)}.`,
105
+ { hint: 'Timeouts are in milliseconds; 0 disables the bound.' },
106
+ )
107
+ }
108
+ config[key] = value
109
+ }
110
+ return config
111
+ }
@@ -10,6 +10,7 @@
10
10
  * Backed by pluggable drivers (cookie, memory, redis).
11
11
  */
12
12
 
13
+ import type { CookieOptions } from '../http/Response.js'
13
14
  import type { SessionRedisClientSource } from './drivers/RedisDriver.js'
14
15
  import {
15
16
  E_SESSION_NOT_READY,
@@ -154,8 +155,23 @@ export interface SessionConfig {
154
155
  client?: SessionRedisClientSource
155
156
  /** `redis` driver only — key prefix. Defaults to `ream:session:`. */
156
157
  prefix?: string
158
+ /**
159
+ * Attributes of the session cookie, for every store. Defaults:
160
+ * `{ path: '/', httpOnly: true, sameSite: 'lax' }`, and `secure` on when
161
+ * `NODE_ENV` is production. Its lifetime comes from {@link age} and
162
+ * {@link clearWithBrowser}, not from here. `httpOnly: false` is refused: a
163
+ * script able to read the session cookie is what turns an XSS into an
164
+ * account takeover.
165
+ */
166
+ cookie?: SessionCookieOptions
157
167
  }
158
168
 
169
+ /** The session cookie attributes an application may set. */
170
+ export type SessionCookieOptions = Pick<
171
+ CookieOptions,
172
+ 'path' | 'httpOnly' | 'secure' | 'sameSite' | 'domain' | 'partitioned' | 'priority'
173
+ >
174
+
159
175
  /**
160
176
  * Generate a fresh session ID. Format mirrors AdonisJS / express-session —
161
177
  * 32 bytes of base64url entropy (~256 bits). Defined here so `regenerate()`
@@ -272,19 +288,28 @@ export class Session {
272
288
  // The store is untyped by nature, so `T` is the CALLER's claim about what
273
289
  // it put there — one assertion from `unknown`, which is what `T` means
274
290
  // here. Going through `any` first widened nothing and hid the intent.
275
- if (key in this.#data) return this.#data[key] as T
291
+ // OWN keys only: `in` walked the prototype, so an empty session answered
292
+ // `get('toString')` with a function and `has('constructor')` with true.
293
+ if (Object.hasOwn(this.#data, key)) return this.#data[key] as T
276
294
  return defaultValue as T
277
295
  }
278
296
 
279
297
  /** Set a session value. */
280
298
  put(key: string, value: unknown): void {
281
- this.#data[key] = value
299
+ // Defined, not assigned: `put('__proto__', x)` stores a key like any
300
+ // other instead of replacing the session data's prototype.
301
+ Object.defineProperty(this.#data, key, {
302
+ value,
303
+ writable: true,
304
+ enumerable: true,
305
+ configurable: true,
306
+ })
282
307
  this.#dirty = true
283
308
  }
284
309
 
285
310
  /** Check if a key exists. */
286
311
  has(key: string): boolean {
287
- return key in this.#data
312
+ return Object.hasOwn(this.#data, key)
288
313
  }
289
314
 
290
315
  /** Get all session data. */
@@ -488,7 +513,7 @@ export class Session {
488
513
  old<T = unknown>(key: string, defaultValue?: T): T {
489
514
  // Same contract as `get`: `T` is the caller's claim about a store that is
490
515
  // untyped by nature.
491
- if (key in this.#previousFlash) return this.#previousFlash[key] as T
516
+ if (Object.hasOwn(this.#previousFlash, key)) return this.#previousFlash[key] as T
492
517
  return defaultValue as T
493
518
  }
494
519
 
@@ -10,15 +10,17 @@ import { readModuleConfig } from '../config/moduleConfig.js'
10
10
  import { currentNodeEnv } from '../env/nodeEnv.js'
11
11
  import { durationToSeconds } from '../helpers/duration.js'
12
12
  import type { HttpContext } from '../http/HttpContext.js'
13
+ import type { CookieOptions } from '../http/Response.js'
13
14
  import type { SessionConfigInput } from './config.js'
14
15
  import { CookieDriver } from './drivers/CookieDriver.js'
15
16
  import { DatabaseDriver, type SessionDbConnection } from './drivers/DatabaseDriver.js'
16
17
  import { FileDriver } from './drivers/FileDriver.js'
17
18
  import { MemoryDriver } from './drivers/MemoryDriver.js'
18
19
  import { RedisDriver } from './drivers/RedisDriver.js'
20
+ import { E_SESSION_COOKIE_NOT_HTTP_ONLY, E_SESSION_STORE_NOT_CONFIGURED } from './errors.js'
19
21
  import { quasarConnection } from './quasar.js'
20
22
  import { ReadOnlyValuesStore, type ValuePath } from './ReadOnlyValuesStore.js'
21
- import type { SessionDriver } from './Session.js'
23
+ import type { SessionCookieOptions, SessionDriver } from './Session.js'
22
24
  import { Session } from './Session.js'
23
25
 
24
26
  interface ResolvedSessionConfig {
@@ -27,6 +29,7 @@ interface ResolvedSessionConfig {
27
29
  maxAge: number
28
30
  clearWithBrowser: boolean
29
31
  rolling: boolean
32
+ cookie: SessionCookieOptions
30
33
  }
31
34
 
32
35
  /** Duck-typed check for a connection the database store can drive. */
@@ -61,8 +64,15 @@ export default class SessionMiddleware {
61
64
  // `{ store: 'redis', stores: { redis: { driver: 'redis' } } }` works.
62
65
  const selected = config?.store ?? config?.driver
63
66
  const named = selected ? config?.stores?.[selected] : undefined
67
+ // No implicit store, in any environment — AdonisJS' session config
68
+ // refuses one without `store` too. The memory store lives in one process
69
+ // and is bounded: a restart, a second replica, or enough anonymous
70
+ // requests signs every user out, so it is used only when it is chosen
71
+ // (`store: 'memory'`).
72
+ if (!selected) throw new E_SESSION_STORE_NOT_CONFIGURED()
73
+ if (config?.cookie?.httpOnly === false) throw new E_SESSION_COOKIE_NOT_HTTP_ONLY()
64
74
  this.#config = {
65
- driver: named?.driver ?? selected ?? 'memory',
75
+ driver: named?.driver ?? selected,
66
76
  cookieName: config?.cookieName ?? 'ream_session',
67
77
  // `age` is the AdonisJS key and accepts a duration string.
68
78
  maxAge:
@@ -71,6 +81,7 @@ export default class SessionMiddleware {
71
81
  : (config?.maxAge ?? 7200),
72
82
  clearWithBrowser: config?.clearWithBrowser ?? false,
73
83
  rolling: config?.rolling ?? false,
84
+ cookie: { ...config?.cookie },
74
85
  }
75
86
 
76
87
  if (this.#config.driver === 'cookie') {
@@ -189,11 +200,8 @@ export default class SessionMiddleware {
189
200
  // Already encrypted by the cookie driver; packing it again would
190
201
  // only make the cookie bigger and change nothing.
191
202
  encode: false,
203
+ ...this.#cookieAttributes(isProduction),
192
204
  maxAge: this.#config.clearWithBrowser ? undefined : Math.ceil(remaining / 1000),
193
- path: '/',
194
- httpOnly: true,
195
- sameSite: 'lax',
196
- secure: isProduction,
197
205
  })
198
206
  }
199
207
  return
@@ -242,14 +250,26 @@ export default class SessionMiddleware {
242
250
  // read, before any store lookup, and that an id from another deployment
243
251
  // (a different APP_KEY) cannot be presented here.
244
252
  ctx.response.cookie(cookieName, session.sessionId, {
253
+ ...this.#cookieAttributes(isProduction),
245
254
  maxAge: this.#config.clearWithBrowser ? undefined : maxAge,
246
- path: '/',
247
- httpOnly: true,
248
- sameSite: 'lax',
249
- secure: isProduction,
250
255
  })
251
256
  }
252
257
  }
258
+
259
+ /**
260
+ * The session cookie's attributes: the configured `cookie` over the
261
+ * defaults. `secure` follows the environment unless the config sets it.
262
+ */
263
+ #cookieAttributes(isProduction: boolean): CookieOptions {
264
+ const configured = this.#config.cookie
265
+ return {
266
+ path: '/',
267
+ sameSite: 'lax',
268
+ ...configured,
269
+ httpOnly: true,
270
+ secure: configured.secure ?? isProduction,
271
+ }
272
+ }
253
273
  }
254
274
 
255
275
  function generateSessionId(): string {
@@ -13,6 +13,7 @@
13
13
  * })
14
14
  */
15
15
 
16
+ import { E_SESSION_STORE_NOT_CONFIGURED } from './errors.js'
16
17
  import type { SessionConfig } from './Session.js'
17
18
 
18
19
  /** What a session config file may declare. */
@@ -24,6 +25,12 @@ export type SessionConfigInput = SessionConfig & {
24
25
  secret?: string
25
26
  }
26
27
 
28
+ /**
29
+ * Validate and return a session config. A config without `store` (or its
30
+ * older spelling `driver`) is refused here, at load time, as AdonisJS'
31
+ * `defineConfig` refuses it: there is no implicit store.
32
+ */
27
33
  export function defineConfig(config: SessionConfigInput): SessionConfigInput {
34
+ if (!config.store && !config.driver) throw new E_SESSION_STORE_NOT_CONFIGURED()
28
35
  return config
29
36
  }
@@ -3,14 +3,25 @@ import type { SessionDriverWithTagging, TaggedSession } from '../Session.js'
3
3
  /** Sweep expired entries once the store reaches this size. */
4
4
  const SWEEP_THRESHOLD = 1000
5
5
 
6
+ /**
7
+ * At most one sweep per this many milliseconds.
8
+ *
9
+ * A sweep walks the whole store. Run on every write past the threshold, a
10
+ * flood of anonymous requests turned each write into a 10 000-entry scan —
11
+ * quadratic, and 20 000 writes took seconds. Between two sweeps the cap below
12
+ * still bounds the store, and a read drops the expired entry it lands on.
13
+ */
14
+ const SWEEP_INTERVAL_MS = 1000
15
+
6
16
  /**
7
17
  * Hard cap on live sessions.
8
18
  *
9
19
  * The sweep alone only removes EXPIRED entries, so within the TTL window
10
20
  * nothing bounded growth: anyone able to request a page could mint sessions
11
21
  * until the process ran out of memory, without ever logging in. Past the cap
12
- * the oldest sessions are dropped — a bounded store that forgets is strictly
13
- * better than an unbounded one that dies.
22
+ * the least recently used sessions are dropped (a write, an update or a
23
+ * touch moves a session to the back of the line) — a bounded store that
24
+ * forgets is strictly better than an unbounded one that dies.
14
25
  */
15
26
  const DEFAULT_MAX_ENTRIES = 10_000
16
27
 
@@ -24,6 +35,8 @@ export class MemoryDriver implements SessionDriverWithTagging {
24
35
  /** user id → the sessions tagged with it. */
25
36
  #tags: Map<string, Set<string>> = new Map()
26
37
  readonly #maxEntries: number
38
+ /** When the last sweep ran, epoch milliseconds. */
39
+ #lastSweep = 0
27
40
 
28
41
  constructor(options: MemoryDriverOptions = {}) {
29
42
  this.#maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_ENTRIES)
@@ -39,8 +52,9 @@ export class MemoryDriver implements SessionDriverWithTagging {
39
52
  }
40
53
 
41
54
  async write(sessionId: string, data: Record<string, unknown>, ttl: number): Promise<void> {
42
- if (this.#store.size >= SWEEP_THRESHOLD) {
43
- const now = Date.now()
55
+ const now = Date.now()
56
+ if (this.#store.size >= SWEEP_THRESHOLD && now - this.#lastSweep >= SWEEP_INTERVAL_MS) {
57
+ this.#lastSweep = now
44
58
  for (const [id, entry] of this.#store) {
45
59
  if (entry.expiresAt < now) this.#store.delete(id)
46
60
  }
@@ -78,9 +92,14 @@ export class MemoryDriver implements SessionDriverWithTagging {
78
92
 
79
93
  async touch(sessionId: string, ttl: number): Promise<void> {
80
94
  const entry = this.#store.get(sessionId)
81
- if (entry) {
82
- entry.expiresAt = Date.now() + ttl * 1000
83
- }
95
+ if (!entry || entry.expiresAt < Date.now()) return
96
+ // Re-inserted, not updated in place: eviction drops the head of the map,
97
+ // so a session in use has to move to the tail. Updated in place, a
98
+ // signed-in user who keeps browsing stayed first in line, and ten
99
+ // thousand anonymous requests evicted them.
100
+ entry.expiresAt = Date.now() + ttl * 1000
101
+ this.#store.delete(sessionId)
102
+ this.#store.set(sessionId, entry)
84
103
  }
85
104
 
86
105
  async tag(sessionId: string, userId: string | number): Promise<void> {
@@ -54,3 +54,34 @@ export class E_SESSION_TAGGING_NOT_SUPPORTED extends Exception {
54
54
  )
55
55
  }
56
56
  }
57
+
58
+ /**
59
+ * A session config without a store (AdonisJS raises "Missing store" for the
60
+ * same config). There is no implicit fallback: the memory store is per
61
+ * process and bounded — a restart, a second replica or enough anonymous
62
+ * traffic signs every user out — so it is only used when it is chosen.
63
+ */
64
+ export class E_SESSION_STORE_NOT_CONFIGURED extends Exception {
65
+ static override status = 500
66
+ static override code = 'E_SESSION_STORE_NOT_CONFIGURED'
67
+
68
+ constructor() {
69
+ super(
70
+ "No session store is configured. Set `store` in config/session.ts (redis, database, file or cookie), or `store: 'memory'` for sessions kept in this process only (tests, a single development instance).",
71
+ { status: 500, code: 'E_SESSION_STORE_NOT_CONFIGURED' },
72
+ )
73
+ }
74
+ }
75
+
76
+ /** `cookie.httpOnly: false` — the session id would be readable by any script on the page. */
77
+ export class E_SESSION_COOKIE_NOT_HTTP_ONLY extends Exception {
78
+ static override status = 500
79
+ static override code = 'E_SESSION_COOKIE_NOT_HTTP_ONLY'
80
+
81
+ constructor() {
82
+ super(
83
+ 'The session cookie must be httpOnly: a script able to read it turns any XSS into an account takeover. Remove `cookie.httpOnly: false` from the session config.',
84
+ { status: 500, code: 'E_SESSION_COOKIE_NOT_HTTP_ONLY' },
85
+ )
86
+ }
87
+ }
@@ -1,3 +1,5 @@
1
+ import { ReamError } from '../errors/ReamError.js'
2
+
1
3
  /**
2
4
  * The flash-message tags AdonisJS's session publishes to templates.
3
5
  *
@@ -122,3 +124,58 @@ export function registerSessionTemplateTags(engine: TemplateTagEngine): void {
122
124
  },
123
125
  })
124
126
  }
127
+
128
+ /** Loads the template engine package. Swappable so a test needs no engine. */
129
+ export type EnginePackageLoader = () => Promise<unknown>
130
+
131
+ const loadInker: EnginePackageLoader = () => {
132
+ // Built rather than written inline: inker is an OPTIONAL peer, and a literal
133
+ // `import('@c9up/inker')` makes a bundler resolve it at build time for an
134
+ // application that never installed it. Computed, it only runs behind the
135
+ // `usingInker` flag.
136
+ const specifier = '@c9up/inker'
137
+ return import(/* @vite-ignore */ specifier)
138
+ }
139
+
140
+ function unusable(detail: string): ReamError {
141
+ return new ReamError(
142
+ 'E_SESSION_TEMPLATE_ENGINE_UNUSABLE',
143
+ `The template engine is installed but the session tags cannot be registered: ${detail}`,
144
+ {
145
+ hint: '`@c9up/inker` must default-export its plugin registrar (`inker.use(plugin)`), whose plugins receive an engine with `registerTag()`.',
146
+ },
147
+ )
148
+ }
149
+
150
+ /**
151
+ * Publish the session tags into the template engine, the way AdonisJS's
152
+ * session does with Edge: through the engine package's MODULE SINGLETON
153
+ * (`inker.use(plugin)`), guarded by the host's `usingInker` flag — never by
154
+ * resolving a container binding. `use()` queues the plugin, and the engine
155
+ * runs it before its next render, whenever the engine was built.
156
+ *
157
+ * Returns whether the tags were handed over. A registrar or an engine of the
158
+ * wrong shape is an error, not a skip: a template using `@flashMessage`
159
+ * would otherwise render it as plain text with no warning.
160
+ */
161
+ export async function installSessionTemplateTags(
162
+ usingInker: boolean,
163
+ load: EnginePackageLoader = loadInker,
164
+ ): Promise<boolean> {
165
+ if (!usingInker) return false
166
+ const module: unknown = await load()
167
+ const registrar =
168
+ typeof module === 'object' && module !== null ? Reflect.get(module, 'default') : undefined
169
+ const use = Reflect.get(Object(registrar), 'use')
170
+ if (typeof use !== 'function') throw unusable('its default export has no use()')
171
+ use.call(registrar, (engine: unknown) => {
172
+ const register = Reflect.get(Object(engine), 'registerTag')
173
+ if (typeof register !== 'function') throw unusable('the engine has no registerTag()')
174
+ registerSessionTemplateTags({
175
+ registerTag: (tag) => {
176
+ register.call(engine, tag)
177
+ },
178
+ })
179
+ })
180
+ return true
181
+ }
@@ -147,11 +147,18 @@ export class StaticMiddleware {
147
147
  const method = ctx.request.method()
148
148
  if (method !== 'GET' && method !== 'HEAD') return next()
149
149
 
150
- const reqPath = ctx.request.path()
150
+ // Outside the prefix the request is none of this middleware's business —
151
+ // not even a dot-file refusal, or `{ prefix: '/static', dotFiles: 'deny' }`
152
+ // answers 403 to an application route such as `/.well-known/...`.
153
+ const reqPath = this.#withinPrefix(ctx.request.path())
154
+ if (reqPath === null) return next()
151
155
 
152
156
  // A dotfile under `deny` is answered, not passed on: falling through would
153
- // leak its existence through whatever the router answers next.
154
- if (this.#dotFiles === 'deny' && this.#hasDotSegment(reqPath)) {
157
+ // leak its existence through whatever the router answers next. The check
158
+ // runs on the DECODED path — the name that would be opened — or `/%2eenv`
159
+ // reaches `.env`. A path that does not decode is checked raw; it is never
160
+ // served anyway.
161
+ if (this.#dotFiles === 'deny' && this.#hasDotSegment(decodePath(reqPath) ?? reqPath)) {
155
162
  ctx.response.status(403).send('Forbidden')
156
163
  return
157
164
  }
@@ -324,36 +331,37 @@ export class StaticMiddleware {
324
331
  * from the descriptor: what gets served is then the same file that passed the
325
332
  * checks, which a path re-read after the fact cannot promise.
326
333
  */
334
+ /**
335
+ * The request path below the prefix, or `null` when it lies outside it.
336
+ * Match only when the prefix is followed by `/` or end-of-path — otherwise
337
+ * `/staticx/foo.js` would be intercepted when the configured prefix is
338
+ * `/static`, hijacking unrelated routes.
339
+ */
340
+ #withinPrefix(reqPath: string): string | null {
341
+ if (this.#prefix === '') return reqPath
342
+ if (!(reqPath === this.#prefix || reqPath.startsWith(`${this.#prefix}/`))) return null
343
+ return reqPath.slice(this.#prefix.length) || '/'
344
+ }
345
+
346
+ /** `reqPath` is already relative to the prefix ({@link #withinPrefix}). */
327
347
  async #openServableFile(
328
348
  reqPath: string,
329
349
  ): Promise<{ handle: FileHandle; filePath: string; stat: fs.Stats } | null> {
330
- if (this.#prefix !== '') {
331
- // Match only when the prefix is followed by `/` or end-of-path —
332
- // otherwise `/staticx/foo.js` would be intercepted when the configured
333
- // prefix is `/static`, hijacking unrelated routes.
334
- if (!(reqPath === this.#prefix || reqPath.startsWith(`${this.#prefix}/`))) return null
335
- reqPath = reqPath.slice(this.#prefix.length) || '/'
336
- }
337
-
338
- if (this.#dotFiles === 'ignore' && this.#hasDotSegment(reqPath)) return null
339
-
340
- // The candidates, in upstream's order: the path itself, then the path with
341
- // each fallback extension, then the index files for a directory request.
342
- //
343
- // Percent-decoding happens BEFORE resolution on purpose: `%2e%2e%2f` is
344
- // `../` and has to be normalised away by `path.resolve` like any other
345
- // traversal, not carried through as an opaque segment. `decodeURIComponent`
346
- // THROWS on a malformed sequence (a lone `%`), which would otherwise become
347
- // a 500 on a request anyone can send.
348
- let decoded: string
349
- try {
350
- decoded = decodeURIComponent(reqPath)
351
- } catch {
352
- return null
353
- }
350
+ // Percent-decoding happens BEFORE any check and BEFORE resolution on
351
+ // purpose: the dot-file policy has to see the name that will be opened
352
+ // (`%2eenv` is `.env`), and `%2e%2e%2f` is `../`, which has to be
353
+ // normalised away by `path.resolve` like any other traversal, not carried
354
+ // through as an opaque segment. A malformed sequence (a lone `%`) is not
355
+ // servable.
356
+ const decoded = decodePath(reqPath)
357
+ if (decoded === null) return null
354
358
  // A NUL truncates the path in the syscall layer of some runtimes; Node
355
359
  // rejects it, but refusing here keeps the decision in one place.
356
360
  if (decoded.includes('\0')) return null
361
+ if (this.#dotFiles !== 'allow' && this.#hasDotSegment(decoded)) return null
362
+
363
+ // The candidates, in upstream's order: the path itself, then the path with
364
+ // each fallback extension, then the index files for a directory request.
357
365
  const relative = decoded.replace(/^\/+/, '')
358
366
  const candidates: string[] = []
359
367
  if (relative !== '') {
@@ -367,6 +375,8 @@ export class StaticMiddleware {
367
375
  }
368
376
 
369
377
  for (const candidate of candidates) {
378
+ // A configured index or extension can itself name a dot file.
379
+ if (this.#dotFiles !== 'allow' && this.#hasDotSegment(candidate)) continue
370
380
  const opened = await this.#openInRoot(candidate)
371
381
  if (opened) return opened
372
382
  }
@@ -426,6 +436,15 @@ export class StaticMiddleware {
426
436
  }
427
437
  }
428
438
 
439
+ /** `decodeURIComponent` that answers `null` instead of throwing on a malformed sequence. */
440
+ function decodePath(reqPath: string): string | null {
441
+ try {
442
+ return decodeURIComponent(reqPath)
443
+ } catch {
444
+ return null
445
+ }
446
+ }
447
+
429
448
  /**
430
449
  * Static config with the defaults filled in — `@adonisjs/static`'s
431
450
  * `defineConfig`, value for value.
@@ -78,7 +78,13 @@ export function partialMatch(actual: unknown, expected: unknown): boolean {
78
78
 
79
79
  /** Split a `Set-Cookie` header value into its individual cookie strings. */
80
80
  function splitSetCookie(setCookie: string): string[] {
81
- return setCookie.split(/,(?=\s*[\w-]+=)/).map((c) => c.trim())
81
+ // One cookie per line, as the response parser stores them; a header folded
82
+ // with commas by another source is still split where a new `name=` starts.
83
+ return setCookie
84
+ .split('\n')
85
+ .flatMap((line) => line.split(/,(?=\s*[\w-]+=)/))
86
+ .map((c) => c.trim())
87
+ .filter((c) => c !== '')
82
88
  }
83
89
 
84
90
  /** A parsed response cookie — `name` + `value` plus any attributes present. */
@@ -195,7 +195,7 @@ export class TestClient {
195
195
  m,
196
196
  p,
197
197
  { ...this.#headers, ...init.headers },
198
- init.body.toString('utf8'),
198
+ init.body,
199
199
  init.timeoutMs,
200
200
  )
201
201
  }
@@ -288,7 +288,7 @@ async function sendRequest(
288
288
  method: string,
289
289
  path: string,
290
290
  headers: Dict,
291
- body: string,
291
+ body: Buffer,
292
292
  timeoutMs?: number,
293
293
  ): Promise<TestResponse> {
294
294
  const { connect } = await import('node:net')
@@ -300,7 +300,10 @@ async function sendRequest(
300
300
  .map(([k, v]) => `${k}: ${v}`)
301
301
  .join('\r\n')
302
302
 
303
- const contentLength = Buffer.byteLength(body, 'utf8')
303
+ // The body stays a Buffer end to end: a multipart upload of a PNG or a
304
+ // PDF is not valid UTF-8, and a round trip through a string replaced
305
+ // every such byte with U+FFFD before the server ever saw it.
306
+ const contentLength = body.length
304
307
 
305
308
  let request = `${method} ${path} HTTP/1.1\r\n`
306
309
  request += `Host: localhost:${port}\r\n`
@@ -312,9 +315,9 @@ async function sendRequest(
312
315
  request += `${headerLines}\r\n`
313
316
  }
314
317
  request += '\r\n'
315
- request += body
316
318
 
317
319
  socket.write(request)
320
+ if (contentLength > 0) socket.write(body)
318
321
  })
319
322
 
320
323
  const chunks: Buffer[] = []
@@ -371,13 +374,19 @@ export function parseHttpResponse(raw: string | Buffer): TestResponse {
371
374
  const [, statusDigits] = statusLine.match(/HTTP\/\d+\.\d+ (\d+)/) ?? []
372
375
  const status = statusDigits === undefined ? 0 : Number.parseInt(statusDigits, 10)
373
376
 
377
+ // A repeated header is never overwritten. Set-Cookie lines are joined with
378
+ // `\n`, the way `Response.getHeaders()` hands them to the serializer: a
379
+ // cookie's `Expires` holds a comma, so a comma cannot separate two of them
380
+ // (RFC 6265 §3). Any other field is combined with `, ` (RFC 9110 §5.3).
374
381
  const headers: Dict = {}
375
382
  for (const line of lines.slice(1)) {
376
383
  const colonIdx = line.indexOf(':')
377
384
  if (colonIdx === -1) continue
378
385
  const key = line.slice(0, colonIdx).trim().toLowerCase()
379
386
  const value = line.slice(colonIdx + 1).trim()
380
- headers[key] = value
387
+ const existing = headers[key]
388
+ if (existing === undefined) headers[key] = value
389
+ else headers[key] = `${existing}${key === 'set-cookie' ? '\n' : ', '}${value}`
381
390
  }
382
391
 
383
392
  // RFC 7230 §4.1: when Transfer-Encoding includes `chunked`, the message body
@@ -27,12 +27,12 @@ export type MacroFn = (...args: unknown[]) => unknown
27
27
  /** A getter implementation — declare `this` in your callback to type the receiver. */
28
28
  export type GetterFn = () => unknown
29
29
 
30
- // biome-ignore lint/complexity/noStaticOnlyClass: eight classes extend this one to inherit macro()/getter(); it is a base class, not a namespace.
30
+ // biome-ignore lint/complexity/noStaticOnlyClass: nine classes extend this one to inherit macro()/getter(); it is a base class, not a namespace.
31
31
  export class Macroable {
32
32
  /**
33
33
  * Attach a method to every instance of THIS class (AdonisJS `macro`).
34
34
  *
35
- * `this.prototype`, not `Macroable.prototype`: eight classes extend this one,
35
+ * `this.prototype`, not `Macroable.prototype`: nine classes extend this one,
36
36
  * and defining on the base put every macro on all of them — a
37
37
  * `Response.macro('json', …)` also landed on `Request`, `Router` and
38
38
  * `HttpContext`, and two subclasses declaring the same name clobbered each