@c9up/ream 0.2.29 → 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 (220) hide show
  1. package/README.md +5 -6
  2. package/dist/Codemods.d.ts +8 -0
  3. package/dist/Codemods.d.ts.map +1 -1
  4. package/dist/Codemods.js +29 -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 +5 -0
  10. package/dist/Ignitor.d.ts.map +1 -1
  11. package/dist/Ignitor.js +12 -1
  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 +38 -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 +10 -0
  26. package/dist/bodyparser/qsParse.d.ts.map +1 -1
  27. package/dist/bodyparser/qsParse.js +27 -12
  28. package/dist/bodyparser/qsParse.js.map +1 -1
  29. package/dist/console/prompts.d.ts.map +1 -1
  30. package/dist/console/prompts.js +3 -1
  31. package/dist/console/prompts.js.map +1 -1
  32. package/dist/container/Container.d.ts.map +1 -1
  33. package/dist/container/Container.js +61 -71
  34. package/dist/container/Container.js.map +1 -1
  35. package/dist/events/Emitter.d.ts +12 -0
  36. package/dist/events/Emitter.d.ts.map +1 -1
  37. package/dist/events/Emitter.js +36 -9
  38. package/dist/events/Emitter.js.map +1 -1
  39. package/dist/events/helix/helpers.d.ts.map +1 -1
  40. package/dist/events/helix/helpers.js +19 -9
  41. package/dist/events/helix/helpers.js.map +1 -1
  42. package/dist/graphql/GraphQLEngine.d.ts +8 -0
  43. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  44. package/dist/graphql/GraphQLEngine.js +77 -11
  45. package/dist/graphql/GraphQLEngine.js.map +1 -1
  46. package/dist/helpers/MessageBuilder.d.ts +4 -2
  47. package/dist/helpers/MessageBuilder.d.ts.map +1 -1
  48. package/dist/helpers/MessageBuilder.js +4 -2
  49. package/dist/helpers/MessageBuilder.js.map +1 -1
  50. package/dist/helpers/errors.d.ts +11 -0
  51. package/dist/helpers/errors.d.ts.map +1 -0
  52. package/dist/helpers/errors.js +17 -0
  53. package/dist/helpers/errors.js.map +1 -0
  54. package/dist/helpers/napi-loader.d.ts.map +1 -1
  55. package/dist/helpers/napi-loader.js +3 -2
  56. package/dist/helpers/napi-loader.js.map +1 -1
  57. package/dist/http/HttpContext.d.ts +20 -2
  58. package/dist/http/HttpContext.d.ts.map +1 -1
  59. package/dist/http/HttpContext.js +10 -4
  60. package/dist/http/HttpContext.js.map +1 -1
  61. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  62. package/dist/http/RedirectBuilder.js +4 -0
  63. package/dist/http/RedirectBuilder.js.map +1 -1
  64. package/dist/http/Request.d.ts.map +1 -1
  65. package/dist/http/Request.js +15 -29
  66. package/dist/http/Request.js.map +1 -1
  67. package/dist/http/Response.d.ts +7 -4
  68. package/dist/http/Response.d.ts.map +1 -1
  69. package/dist/http/Response.js +14 -6
  70. package/dist/http/Response.js.map +1 -1
  71. package/dist/http/permissions.d.ts +14 -8
  72. package/dist/http/permissions.d.ts.map +1 -1
  73. package/dist/http/permissions.js +26 -16
  74. package/dist/http/permissions.js.map +1 -1
  75. package/dist/index.d.ts +2 -2
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +1 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/middleware/Pipeline.js +1 -1
  80. package/dist/middleware/Pipeline.js.map +1 -1
  81. package/dist/native/generated.d.ts +16 -30
  82. package/dist/native/generated.d.ts.map +1 -1
  83. package/dist/profiler/Profiler.d.ts.map +1 -1
  84. package/dist/profiler/Profiler.js +4 -1
  85. package/dist/profiler/Profiler.js.map +1 -1
  86. package/dist/profiler/client.generated.d.ts +5 -0
  87. package/dist/profiler/client.generated.d.ts.map +1 -0
  88. package/dist/profiler/client.generated.js +6 -0
  89. package/dist/profiler/client.generated.js.map +1 -0
  90. package/dist/profiler/collectors.d.ts +92 -13
  91. package/dist/profiler/collectors.d.ts.map +1 -1
  92. package/dist/profiler/collectors.js +106 -19
  93. package/dist/profiler/collectors.js.map +1 -1
  94. package/dist/profiler/endpoint.d.ts +2 -1
  95. package/dist/profiler/endpoint.d.ts.map +1 -1
  96. package/dist/profiler/endpoint.js +25 -1
  97. package/dist/profiler/endpoint.js.map +1 -1
  98. package/dist/profiler/index.d.ts +8 -4
  99. package/dist/profiler/index.d.ts.map +1 -1
  100. package/dist/profiler/index.js +11 -4
  101. package/dist/profiler/index.js.map +1 -1
  102. package/dist/profiler/middleware.d.ts.map +1 -1
  103. package/dist/profiler/middleware.js +76 -10
  104. package/dist/profiler/middleware.js.map +1 -1
  105. package/dist/profiler/preview.d.ts +17 -0
  106. package/dist/profiler/preview.d.ts.map +1 -0
  107. package/dist/profiler/preview.js +100 -0
  108. package/dist/profiler/preview.js.map +1 -0
  109. package/dist/profiler/toolbar.d.ts +15 -23
  110. package/dist/profiler/toolbar.d.ts.map +1 -1
  111. package/dist/profiler/toolbar.js +25 -91
  112. package/dist/profiler/toolbar.js.map +1 -1
  113. package/dist/profiler/types.d.ts +6 -1
  114. package/dist/profiler/types.d.ts.map +1 -1
  115. package/dist/router/Router.d.ts +6 -0
  116. package/dist/router/Router.d.ts.map +1 -1
  117. package/dist/router/Router.js +115 -12
  118. package/dist/router/Router.js.map +1 -1
  119. package/dist/rpc/RpcRouter.js +3 -3
  120. package/dist/rpc/RpcRouter.js.map +1 -1
  121. package/dist/scheduler/Scheduler.js +1 -1
  122. package/dist/scheduler/Scheduler.js.map +1 -1
  123. package/dist/security/CookieSigner.d.ts.map +1 -1
  124. package/dist/security/CookieSigner.js +15 -5
  125. package/dist/security/CookieSigner.js.map +1 -1
  126. package/dist/security/SignedUrl.d.ts.map +1 -1
  127. package/dist/security/SignedUrl.js +10 -18
  128. package/dist/security/SignedUrl.js.map +1 -1
  129. package/dist/server/Server.d.ts +4 -1
  130. package/dist/server/Server.d.ts.map +1 -1
  131. package/dist/server/Server.js.map +1 -1
  132. package/dist/server/nativeLimits.d.ts +9 -1
  133. package/dist/server/nativeLimits.d.ts.map +1 -1
  134. package/dist/server/nativeLimits.js +8 -0
  135. package/dist/server/nativeLimits.js.map +1 -1
  136. package/dist/session/Session.d.ts.map +1 -1
  137. package/dist/session/Session.js +13 -4
  138. package/dist/session/Session.js.map +1 -1
  139. package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
  140. package/dist/session/drivers/MemoryDriver.js +14 -2
  141. package/dist/session/drivers/MemoryDriver.js.map +1 -1
  142. package/dist/storage/StaticMiddleware.d.ts.map +1 -1
  143. package/dist/storage/StaticMiddleware.js +20 -9
  144. package/dist/storage/StaticMiddleware.js.map +1 -1
  145. package/dist/testing/ApiResponse.d.ts.map +1 -1
  146. package/dist/testing/ApiResponse.js +7 -1
  147. package/dist/testing/ApiResponse.js.map +1 -1
  148. package/dist/testing/TestClient.d.ts.map +1 -1
  149. package/dist/testing/TestClient.js +16 -4
  150. package/dist/testing/TestClient.js.map +1 -1
  151. package/dist/utils/Macroable.d.ts +1 -1
  152. package/dist/utils/Macroable.js +2 -2
  153. package/dist/utils/Macroable.js.map +1 -1
  154. package/events.darwin-arm64.node +0 -0
  155. package/events.darwin-x64.node +0 -0
  156. package/events.linux-arm64-gnu.node +0 -0
  157. package/events.linux-x64-gnu.node +0 -0
  158. package/events.win32-x64-msvc.node +0 -0
  159. package/index.darwin-arm64.node +0 -0
  160. package/index.darwin-x64.node +0 -0
  161. package/index.linux-arm64-gnu.node +0 -0
  162. package/index.linux-x64-gnu.node +0 -0
  163. package/index.win32-x64-msvc.node +0 -0
  164. package/package.json +1 -2
  165. package/scheduler.darwin-arm64.node +0 -0
  166. package/scheduler.darwin-x64.node +0 -0
  167. package/scheduler.linux-arm64-gnu.node +0 -0
  168. package/scheduler.linux-x64-gnu.node +0 -0
  169. package/scheduler.win32-x64-msvc.node +0 -0
  170. package/src/Codemods.ts +49 -3
  171. package/src/HttpKernel.ts +18 -7
  172. package/src/Ignitor.ts +21 -4
  173. package/src/bodyparser/BodyParserMiddleware.ts +42 -16
  174. package/src/bodyparser/MultipartFile.ts +46 -67
  175. package/src/bodyparser/parseSize.ts +17 -17
  176. package/src/bodyparser/qsParse.ts +28 -13
  177. package/src/console/prompts.ts +3 -1
  178. package/src/container/Container.ts +97 -81
  179. package/src/events/Emitter.ts +37 -10
  180. package/src/events/helix/helpers.ts +24 -19
  181. package/src/graphql/GraphQLEngine.ts +89 -13
  182. package/src/helpers/MessageBuilder.ts +4 -2
  183. package/src/helpers/errors.ts +16 -0
  184. package/src/helpers/napi-loader.ts +3 -2
  185. package/src/http/HttpContext.ts +20 -4
  186. package/src/http/RedirectBuilder.ts +3 -0
  187. package/src/http/Request.ts +15 -29
  188. package/src/http/Response.ts +15 -7
  189. package/src/http/permissions.ts +31 -15
  190. package/src/index.ts +2 -1
  191. package/src/middleware/Pipeline.ts +1 -1
  192. package/src/native/generated.ts +18 -42
  193. package/src/profiler/Profiler.ts +4 -1
  194. package/src/profiler/client/toolbar.css +595 -0
  195. package/src/profiler/client/toolbar.js +1299 -0
  196. package/src/profiler/client.generated.ts +7 -0
  197. package/src/profiler/collectors.ts +156 -19
  198. package/src/profiler/endpoint.ts +27 -1
  199. package/src/profiler/index.ts +17 -3
  200. package/src/profiler/middleware.ts +74 -9
  201. package/src/profiler/preview.ts +104 -0
  202. package/src/profiler/toolbar.ts +28 -91
  203. package/src/profiler/types.ts +6 -1
  204. package/src/router/Router.ts +136 -13
  205. package/src/rpc/RpcRouter.ts +4 -4
  206. package/src/scheduler/Scheduler.ts +1 -1
  207. package/src/security/CookieSigner.ts +13 -5
  208. package/src/security/SignedUrl.ts +12 -16
  209. package/src/server/Server.ts +4 -1
  210. package/src/server/nativeLimits.ts +18 -1
  211. package/src/session/Session.ts +13 -4
  212. package/src/session/drivers/MemoryDriver.ts +15 -2
  213. package/src/storage/StaticMiddleware.ts +18 -9
  214. package/src/testing/ApiResponse.ts +7 -1
  215. package/src/testing/TestClient.ts +14 -5
  216. package/src/utils/Macroable.ts +2 -2
  217. package/scripts/build-napi-types.mjs +0 -69
  218. package/scripts/copy-napi.mjs +0 -121
  219. package/scripts/generate-mime.mjs +0 -119
  220. package/scripts/generate-napi-types.mjs +0 -187
@@ -0,0 +1,104 @@
1
+ /**
2
+ * A bounded, redacted copy of a value, for a profiler panel.
3
+ *
4
+ * What the profiler keeps crosses a wire and sits in memory for the rest of the
5
+ * dev session, so a payload is never stored as given: an event can carry a
6
+ * whole model graph, a session a megabyte of flash data, a body an upload.
7
+ * Depth, keys, items and string length are all capped, cycles are cut, and the
8
+ * value of a key that names a secret is replaced rather than shown — the
9
+ * profiler is the page a developer screenshares.
10
+ */
11
+
12
+ /** A key whose value is never shown: credentials, and what carries them. */
13
+ const SECRET_KEY =
14
+ /pass(word|phrase)?|secret|token|authorization|cookie|api[-_]?key|private[-_]?key|csrf|xsrf|credential/i
15
+
16
+ const MAX_DEPTH = 4
17
+ const MAX_KEYS = 40
18
+ const MAX_ITEMS = 25
19
+ const MAX_STRING = 400
20
+
21
+ /** What a redacted value becomes. */
22
+ export const REDACTED = '[redacted]'
23
+
24
+ /** Whether a key's value is hidden. */
25
+ export function isSecretKey(key: string): boolean {
26
+ return SECRET_KEY.test(key)
27
+ }
28
+
29
+ /** A JSON-safe preview of `value`. */
30
+ export function preview(value: unknown): unknown {
31
+ return walk(value, 0, new WeakSet())
32
+ }
33
+
34
+ function walk(value: unknown, depth: number, seen: WeakSet<object>): unknown {
35
+ if (value === null || value === undefined) return value ?? null
36
+ switch (typeof value) {
37
+ case 'string':
38
+ return value.length > MAX_STRING
39
+ ? `${value.slice(0, MAX_STRING)}… (${value.length} chars)`
40
+ : value
41
+ case 'number':
42
+ return Number.isFinite(value) ? value : String(value)
43
+ case 'boolean':
44
+ return value
45
+ case 'bigint':
46
+ return `${value}n`
47
+ case 'symbol':
48
+ return value.toString()
49
+ case 'function':
50
+ return `[function ${value.name || 'anonymous'}]`
51
+ }
52
+ if (typeof value !== 'object') return String(value)
53
+ if (value instanceof Date)
54
+ return Number.isNaN(value.getTime()) ? 'Invalid Date' : value.toISOString()
55
+ if (value instanceof Error) return { name: value.name, message: value.message }
56
+ if (Buffer.isBuffer(value)) return `[Buffer ${value.length} bytes]`
57
+ if (seen.has(value)) return '[circular]'
58
+ if (depth >= MAX_DEPTH)
59
+ return Array.isArray(value) ? `[array ${value.length}]` : `[${typeName(value)}]`
60
+ seen.add(value)
61
+
62
+ if (Array.isArray(value)) {
63
+ const items = value.slice(0, MAX_ITEMS).map((item) => walk(item, depth + 1, seen))
64
+ if (value.length > MAX_ITEMS) items.push(`… ${value.length - MAX_ITEMS} more`)
65
+ return items
66
+ }
67
+ if (value instanceof Map) {
68
+ return walk(
69
+ Object.fromEntries([...value.entries()].map(([k, v]) => [String(k), v])),
70
+ depth,
71
+ seen,
72
+ )
73
+ }
74
+ if (value instanceof Set) return walk([...value], depth, seen)
75
+
76
+ // A model or entity usually serialises itself the way it wants to be seen.
77
+ const toJSON = Reflect.get(value, 'toJSON')
78
+ const source: unknown = typeof toJSON === 'function' ? safeCall(() => toJSON.call(value)) : value
79
+ if (source !== value) return walk(source, depth, seen)
80
+
81
+ const out: Record<string, unknown> = {}
82
+ const name = typeName(value)
83
+ if (name !== 'Object') out['@type'] = name
84
+ const keys = Object.keys(value)
85
+ for (const key of keys.slice(0, MAX_KEYS)) {
86
+ out[key] = isSecretKey(key) ? REDACTED : walk(Reflect.get(value, key), depth + 1, seen)
87
+ }
88
+ if (keys.length > MAX_KEYS) out['…'] = `${keys.length - MAX_KEYS} more keys`
89
+ return out
90
+ }
91
+
92
+ function typeName(value: object): string {
93
+ const ctor = Reflect.get(value, 'constructor')
94
+ const name = typeof ctor === 'function' ? Reflect.get(ctor, 'name') : undefined
95
+ return typeof name === 'string' && name !== '' ? name : 'Object'
96
+ }
97
+
98
+ function safeCall(fn: () => unknown): unknown {
99
+ try {
100
+ return fn()
101
+ } catch {
102
+ return '[unserialisable]'
103
+ }
104
+ }
@@ -1,106 +1,43 @@
1
1
  /**
2
- * The bar at the bottom of the page.
2
+ * The tag that loads the debug toolbar into an HTML page.
3
3
  *
4
- * It carries its own token inline rather than reading `X-Debug-Token` off the
5
- * response: a script in a page cannot see the headers of the navigation that
6
- * delivered it. Symfony injects the token for the same reason.
4
+ * A `<script src>` with the request's profile token in a data attribute, and
5
+ * nothing inline. The bar itself — `client/toolbar.js` and `client/toolbar.css`
6
+ * — is served by the profiler endpoint from the same origin, so a policy of
7
+ * `script-src 'self'` / `style-src 'self'` runs and styles it, and a
8
+ * nonce-only policy runs it through the nonce stamped here. An inline script,
9
+ * which is what this used to be, needed the nonce and nothing else would do.
7
10
  *
8
- * Everything lives in a shadow root — your stylesheet must not reach the bar,
9
- * and the bar must not reach your page. It fetches its numbers once, on load,
10
- * and then does nothing: a dev tool that polls is a dev tool that shows up in
11
- * its own history.
12
- *
13
- * A string rather than a stringified function, as the hot-reload script is:
14
- * this package compiles without the DOM library, deliberately — it is a server
15
- * — so browser code here is text, and its only inputs are escaped below.
16
- *
17
- * It styles its nodes through the CSSOM — `node.style.cssText` — rather than
18
- * with a `<style>` element. Under a CSP with `style-src 'self'` (or only a
19
- * `default-src`, which is the common case) a stylesheet inserted at runtime is
20
- * inline style and is dropped, with no error anywhere: the bar renders as a
21
- * stack of grey text and nothing says why. Setting properties on an element is
22
- * not inline style and is never blocked, which is also how the host below has
23
- * always been positioned.
11
+ * The token travels in the page rather than being read off the response: a
12
+ * script cannot see the headers of the navigation that delivered it. Symfony
13
+ * injects its token for the same reason.
24
14
  */
25
15
 
26
16
  import { PROFILER_PATH } from './Profiler.js'
27
17
 
18
+ /** Where the toolbar's own files are served, under the profiler's path. */
19
+ export const PROFILER_ASSETS_PATH = `${PROFILER_PATH}/_assets`
20
+
28
21
  /**
29
22
  * The `<script>` to append to an HTML response.
30
23
  *
31
24
  * `token` is generated by the profiler (hex) and `nonce` by the response, so
32
- * neither is user input; both are still put through `JSON.stringify` rather
33
- * than interpolated raw, because "cannot contain a quote" is an invariant that
34
- * holds until someone changes how one of them is made.
25
+ * neither is user input; both are still attribute-escaped, because "cannot
26
+ * contain a quote" is an invariant that holds until someone changes how one of
27
+ * them is made.
35
28
  */
36
29
  export function toolbarScript(token: string, nonce?: string): string {
37
- const attribute = nonce === undefined ? '' : ` nonce="${nonce.replace(/"/g, '&quot;')}"`
38
- const config = JSON.stringify({ token, path: PROFILER_PATH })
39
- return `<script${attribute}>(${BAR})(${config})</script>`
30
+ const attribute = nonce === undefined ? '' : ` nonce="${escapeAttribute(nonce)}"`
31
+ return (
32
+ `<script src="${PROFILER_ASSETS_PATH}/toolbar.js" data-token="${escapeAttribute(token)}"` +
33
+ ` data-path="${PROFILER_PATH}"${attribute}></script>`
34
+ )
40
35
  }
41
36
 
42
- /**
43
- * The bar, as it runs in the browser.
44
- *
45
- * Kept on one indented block so what ships is readable in a page's source —
46
- * someone will open the inspector and find this, and a minified blob would
47
- * tell them nothing about where it came from.
48
- */
49
- const BAR = `function(c){
50
- var host=document.createElement('div');
51
- host.style.cssText='position:fixed;left:0;right:0;bottom:0;z-index:2147483000';
52
- var root=host.attachShadow({mode:'closed'});
53
- document.body.appendChild(host);
54
- var bar=document.createElement('div');
55
- bar.style.cssText='display:flex;align-items:stretch;background:#14161a;color:#e6e6e6;'
56
- +'font:12px/1 ui-monospace,Menlo,monospace;border-top:1px solid #262a31;'
57
- +'box-shadow:0 -2px 8px rgba(0,0,0,.4)';
58
- root.appendChild(bar);
59
- var page=c.path+'/'+c.token;
60
- var COLOR={bad:'#ff7b72',warn:'#f0883e',ok:'#7ee787'};
61
- function cell(label,value,tone,plain){
62
- var node=document.createElement('div');
63
- node.style.cssText='padding:7px 11px;white-space:nowrap;'
64
- +(plain?'flex:1;overflow:hidden;text-overflow:ellipsis;cursor:default'
65
- :'border-right:1px solid #21252c;cursor:pointer');
66
- if(label){
67
- var tag=document.createElement('span');
68
- tag.style.cssText='color:#8b949e;margin-right:6px';
69
- tag.textContent=label;
70
- node.appendChild(tag);
71
- }
72
- var text=document.createElement('span');
73
- if(tone){text.style.color=COLOR[tone];}
74
- text.textContent=value;
75
- node.appendChild(text);
76
- if(!plain){
77
- node.addEventListener('mouseenter',function(){node.style.background='#1b1f25'});
78
- node.addEventListener('mouseleave',function(){node.style.background=''});
79
- node.addEventListener('click',function(){window.open(page,'_blank')});
80
- }
81
- bar.appendChild(node);
82
- }
83
- function panel(data,name){
84
- var value=data&&data[name];
85
- return (value&&typeof value==='object')?value:null;
86
- }
87
- fetch(page+'.json',{headers:{accept:'application/json'}})
88
- .then(function(r){return r.ok?r.json():null})
89
- .then(function(p){
90
- if(!p){host.remove();return}
91
- var status=Number(p.status||0);
92
- cell('',String(status),status>=500?'bad':status>=400?'warn':'ok');
93
- cell('time',Number(p.duration||0).toFixed(1)+' ms');
94
- var db=panel(p.data,'database');
95
- if(db){
96
- var total=Number(db.total||0);
97
- cell('db',total+' \u00b7 '+Number(db.duration||0).toFixed(1)+' ms',total>=50?'warn':'');
98
- }
99
- var mem=panel(p.data,'memory');
100
- if(mem){cell('mem',Number(mem.usedMb||0).toFixed(1)+' MB');}
101
- var err=panel(p.data,'exception');
102
- if(err&&typeof err.message==='string'){cell('',err.message,'bad');}
103
- cell('',String(p.url||''),'',true);
104
- })
105
- .catch(function(){host.remove()});
106
- }`
37
+ function escapeAttribute(value: string): string {
38
+ return value
39
+ .replaceAll('&', '&amp;')
40
+ .replaceAll('"', '&quot;')
41
+ .replaceAll('<', '&lt;')
42
+ .replaceAll('>', '&gt;')
43
+ }
@@ -45,8 +45,13 @@ export type ProfileSummary = Omit<Profile, 'data'> & {
45
45
  */
46
46
  export interface CollectionScope {
47
47
  token: string
48
- /** Called by the framework when the request is done. */
48
+ /** When the request arrived, epoch milliseconds. */
49
49
  readonly startedAt: number
50
+ /**
51
+ * Milliseconds since the request arrived, on the monotonic clock. What a
52
+ * collector stamps an entry with, so the timeline can place it.
53
+ */
54
+ elapsed(): number
50
55
  }
51
56
 
52
57
  /**
@@ -186,6 +186,12 @@ export type MatchedParams = Record<string, string | string[]>
186
186
  export interface MatchResult {
187
187
  route: RouteDefinition
188
188
  params: MatchedParams
189
+ /**
190
+ * The values a parameterised domain captured from the host —
191
+ * `{ tenant: 'acme' }` for `:tenant.app.test` on `acme.app.test`. Empty for
192
+ * an exact or wildcard domain, and for a route without one.
193
+ */
194
+ subdomains: Record<string, string>
189
195
  }
190
196
 
191
197
  // ─── Matchers ───────────────────────────────────────────────
@@ -868,6 +874,8 @@ export class Router extends Macroable {
868
874
  #staticIndex: Map<string, RouteDefinition> = new Map()
869
875
  /** Index: parametric routes grouped by method. */
870
876
  #paramIndex: Map<string, RouteDefinition[]> = new Map()
877
+ /** Index: every domain a route is scoped to, compiled, in registration order. */
878
+ #domains: CompiledDomain[] = []
871
879
  /** Index: named routes for O(1) URL generation. */
872
880
  #nameIndex: Map<string, RouteDefinition> = new Map()
873
881
  /** Whether the index needs rebuilding. */
@@ -1144,6 +1152,7 @@ export class Router extends Macroable {
1144
1152
  this.#staticIndex.clear()
1145
1153
  this.#paramIndex.clear()
1146
1154
  this.#nameIndex.clear()
1155
+ this.#domains = []
1147
1156
 
1148
1157
  for (const route of this.#routes) {
1149
1158
  // Soft-deleted routes (resource .only()/.except()/.apiOnly()) never match.
@@ -1179,6 +1188,13 @@ export class Router extends Macroable {
1179
1188
  this.#nameIndex.set(route.name, route)
1180
1189
  }
1181
1190
 
1191
+ if (route.domain !== undefined) {
1192
+ const pattern = route.domain.toLowerCase()
1193
+ if (!this.#domains.some((d) => d.pattern === pattern)) {
1194
+ this.#domains.push(compileDomain(route.domain))
1195
+ }
1196
+ }
1197
+
1182
1198
  const isParam = route.path.includes(':') || route.path.includes('*')
1183
1199
 
1184
1200
  if (!isParam && !route.domain) {
@@ -1221,21 +1237,27 @@ export class Router extends Macroable {
1221
1237
  candidates.push(path.slice(0, -1))
1222
1238
  }
1223
1239
 
1240
+ // The domain the host belongs to decides which routes are candidates, as
1241
+ // upstream does: only that domain's routes when one matches, with no
1242
+ // fallback to the domain-less ones; only the domain-less ones when none
1243
+ // does. A missing host matches no domain, so a domain-scoped route is
1244
+ // never served to a request that simply omits its Host header.
1245
+ const domain = host === undefined ? undefined : this.#matchingDomain(host)
1246
+
1224
1247
  for (const candidatePath of candidates) {
1225
- // 1. Try static exact match (O(1))
1226
- const staticRoute =
1227
- this.#staticIndex.get(`${method}:${candidatePath}`) ??
1228
- this.#staticIndex.get(`*:${candidatePath}`)
1229
- if (staticRoute) {
1230
- if (!staticRoute.domain || !host || matchDomain(staticRoute.domain, host)) {
1231
- return { route: staticRoute, params: {} }
1232
- }
1248
+ // 1. Try static exact match (O(1)). The index holds domain-less routes
1249
+ // only, so it is consulted only when no domain owns the request.
1250
+ if (domain === undefined) {
1251
+ const staticRoute =
1252
+ this.#staticIndex.get(`${method}:${candidatePath}`) ??
1253
+ this.#staticIndex.get(`*:${candidatePath}`)
1254
+ if (staticRoute) return { route: staticRoute, params: {}, subdomains: {} }
1233
1255
  }
1234
1256
 
1235
- // 2. Try parametric routes (only same-method candidates)
1257
+ // 2. Try parametric and domain-scoped routes (only same-method candidates)
1236
1258
  const paramCandidates = this.#paramIndex.get(method) ?? []
1237
1259
  for (const route of paramCandidates) {
1238
- if (route.domain && host && !matchDomain(route.domain, host)) continue
1260
+ if (route.domain?.toLowerCase() !== domain?.pattern) continue
1239
1261
 
1240
1262
  const params = matchPath(route.path, candidatePath)
1241
1263
  if (params === null) continue
@@ -1254,7 +1276,11 @@ export class Router extends Macroable {
1254
1276
  // handler wants — `where('id', matchers.number())` hands `params.id`
1255
1277
  // a NUMBER, as in AdonisJS. Ream carried only the pattern, so every
1256
1278
  // controller re-parsed a string the router had already validated.
1257
- return { route, params: applyCasts(params, effective) }
1279
+ return {
1280
+ route,
1281
+ params: applyCasts(params, effective),
1282
+ subdomains: domain?.subdomains ?? {},
1283
+ }
1258
1284
  }
1259
1285
  }
1260
1286
 
@@ -1268,6 +1294,35 @@ export class Router extends Macroable {
1268
1294
  return undefined
1269
1295
  }
1270
1296
 
1297
+ /**
1298
+ * The registered domain `host` belongs to — its lowercased pattern and the
1299
+ * values its parameters captured — or `undefined` when none covers it.
1300
+ *
1301
+ * NAMED DEVIATION — upstream takes the first registered pattern that
1302
+ * matches, so a wildcard declared before `admin.example.com` swallows the
1303
+ * admin host and its routes become unreachable. Here the most specific kind
1304
+ * wins whatever the order: an exact domain, then a parameterised one (each
1305
+ * parameter fills exactly one label), then a wildcard (any depth). Within a
1306
+ * kind, the first registered still wins.
1307
+ */
1308
+ #matchingDomain(
1309
+ host: string,
1310
+ ): { pattern: string; subdomains: Record<string, string> } | undefined {
1311
+ if (this.#domains.length === 0) return undefined
1312
+ const hostname = hostnameOf(host)
1313
+ const exact = this.#domains.find((d) => d.kind === 'exact' && d.pattern === hostname)
1314
+ if (exact) return { pattern: exact.pattern, subdomains: {} }
1315
+ for (const domain of this.#domains) {
1316
+ if (domain.kind !== 'param') continue
1317
+ const subdomains = matchParamDomain(domain.labels, hostname)
1318
+ if (subdomains) return { pattern: domain.pattern, subdomains }
1319
+ }
1320
+ const wildcard = this.#domains.find(
1321
+ (d) => d.kind === 'wildcard' && matchDomain(d.pattern, host),
1322
+ )
1323
+ return wildcard ? { pattern: wildcard.pattern, subdomains: {} } : undefined
1324
+ }
1325
+
1271
1326
  // ─── URL generation ───────────────────────────────────────
1272
1327
 
1273
1328
  /**
@@ -1637,6 +1692,75 @@ function matchPath(pattern: string, actual: string): MatchedParams | null {
1637
1692
  return params
1638
1693
  }
1639
1694
 
1695
+ /** A route domain, compiled once when the index is built. */
1696
+ interface CompiledDomain {
1697
+ /** The pattern lowercased: what a route's own `domain` is compared to. */
1698
+ pattern: string
1699
+ kind: 'exact' | 'param' | 'wildcard'
1700
+ /**
1701
+ * A parameterised domain's labels: static ones lowercased, `:name` ones
1702
+ * kept as written so the parameter keeps the name the route gave it.
1703
+ */
1704
+ labels: string[]
1705
+ }
1706
+
1707
+ /** What one parameter accepts: a single DNS label, lowercased. */
1708
+ const DOMAIN_PARAM_VALUE = /^[a-z0-9_-]+$/
1709
+
1710
+ function compileDomain(domain: string): CompiledDomain {
1711
+ const pattern = domain.toLowerCase()
1712
+ if (pattern.startsWith('*.')) return { pattern, kind: 'wildcard', labels: [] }
1713
+ const labels = domain
1714
+ .split('.')
1715
+ .map((label) => (label.startsWith(':') ? label : label.toLowerCase()))
1716
+ if (!labels.some((label) => label.startsWith(':'))) return { pattern, kind: 'exact', labels }
1717
+ const names = new Set<string>()
1718
+ for (const label of labels) {
1719
+ if (!label.startsWith(':')) continue
1720
+ const name = label.slice(1)
1721
+ if (name === '' || names.has(name)) {
1722
+ throw new Error(
1723
+ `[E_INVALID_ROUTE_DOMAIN] The route domain "${domain}" has ${name === '' ? 'a parameter without a name' : `the parameter "${name}" twice`}.`,
1724
+ )
1725
+ }
1726
+ names.add(name)
1727
+ }
1728
+ return { pattern, kind: 'param', labels }
1729
+ }
1730
+
1731
+ /**
1732
+ * The values `hostname` gives a parameterised domain's labels, or `null` when
1733
+ * it does not fit them.
1734
+ *
1735
+ * NAMED DEVIATION — upstream's matcher fills a parameter with everything in
1736
+ * front of the static suffix, so `:tenant.app.test` took `a.b.app.test` as the
1737
+ * tenant `a.b` and `.app.test` as an empty one, and a parameter could only
1738
+ * open the domain. Here each parameter is exactly one non-empty label of
1739
+ * letters, digits, `-` or `_`, in any position (`api.:tenant.test`,
1740
+ * `:tenant.:region.app.test`).
1741
+ */
1742
+ function matchParamDomain(labels: string[], hostname: string): Record<string, string> | null {
1743
+ const hostLabels = hostname.split('.')
1744
+ if (hostLabels.length !== labels.length) return null
1745
+ const values: Record<string, string> = {}
1746
+ for (const [index, label] of labels.entries()) {
1747
+ const value = hostLabels[index] ?? ''
1748
+ if (label.startsWith(':')) {
1749
+ if (!DOMAIN_PARAM_VALUE.test(value)) return null
1750
+ values[label.slice(1)] = value
1751
+ } else if (label !== value) {
1752
+ return null
1753
+ }
1754
+ }
1755
+ return values
1756
+ }
1757
+
1758
+ /** The request's host without its port, lowercased (RFC 4343). */
1759
+ function hostnameOf(host: string): string {
1760
+ const [hostname = ''] = host.split(':')
1761
+ return hostname.toLowerCase()
1762
+ }
1763
+
1640
1764
  /**
1641
1765
  * Match a domain pattern against the request's host. Supports wildcards like
1642
1766
  * `*.example.com`.
@@ -1647,8 +1771,7 @@ function matchPath(pattern: string, actual: string): MatchedParams | null {
1647
1771
  * Comparing them verbatim sends that request to a fallback, or to a 404.
1648
1772
  */
1649
1773
  function matchDomain(pattern: string, host: string): boolean {
1650
- const [hostname = ''] = host.split(':') // strip port
1651
- const actualHost = hostname.toLowerCase()
1774
+ const actualHost = hostnameOf(host)
1652
1775
  const wanted = pattern.toLowerCase()
1653
1776
  if (wanted === actualHost) return true
1654
1777
  if (wanted.startsWith('*.')) {
@@ -141,10 +141,10 @@ export interface RpcRouterOptions {
141
141
  * Returns the JSON-RPC error code+message to reject with, or undefined when the
142
142
  * caller is authorized.
143
143
  */
144
- function checkRpcAuthorization(
144
+ async function checkRpcAuthorization(
145
145
  ctx: HttpContext,
146
146
  def: RpcMethodDefinition,
147
- ): { code: number; message: string } | undefined {
147
+ ): Promise<{ code: number; message: string } | undefined> {
148
148
  const needsAuth = def.guards.length > 0 || def.roles.length > 0 || def.permissions.length > 0
149
149
  if (needsAuth && !ctx.auth?.isAuthenticated) {
150
150
  return { code: -32003, message: 'Unauthorized' }
@@ -159,7 +159,7 @@ function checkRpcAuthorization(
159
159
  }
160
160
  // A token-authenticated call also needs the token to allow each permission.
161
161
  if (def.permissions.length > 0) {
162
- if (!holdsPermissions(ctx.auth, def.permissions)) {
162
+ if (!(await holdsPermissions(ctx.auth, def.permissions))) {
163
163
  return { code: -32003, message: 'Insufficient permissions' }
164
164
  }
165
165
  }
@@ -381,7 +381,7 @@ export class RpcRouter {
381
381
  if (!def) return rpcError(-32601, 'Method not found', id)
382
382
 
383
383
  try {
384
- const denied = checkRpcAuthorization(ctx, def)
384
+ const denied = await checkRpcAuthorization(ctx, def)
385
385
  if (denied) return rpcError(denied.code, denied.message, id)
386
386
 
387
387
  // Run declared named middleware (e.g. guards) — they throw to deny, mapped
@@ -144,7 +144,7 @@ function loadNative(): NativeModule {
144
144
  * so scheduler context is never lost.
145
145
  */
146
146
  function translateNativeError(e: unknown): ReamError {
147
- const asError = e as Error
147
+ const asError = e instanceof Error ? e : new Error(String(e))
148
148
  const parsed = ReamError.fromNapi(asError)
149
149
  // `fromNapi` returns an `E_UNKNOWN`-coded error when the message is not
150
150
  // a JSON payload. Preserve that context under a scheduler-specific
@@ -125,14 +125,22 @@ export class CookieSigner {
125
125
  return null
126
126
  }
127
127
  if (typeof parsed !== 'object' || parsed === null) return null
128
- const envelope = parsed as Envelope
129
128
  // The KEY must be there, whatever it holds — that is what distinguishes an
130
129
  // envelope from an arbitrary JSON document, and a signed `null` or `0` is a
131
130
  // legitimate payload.
132
- if (!('m' in envelope)) return null
133
- if (envelope.e !== undefined && envelope.e < Date.now()) return null
134
- if ((envelope.p ?? undefined) !== purpose) return null
135
- return envelope.m
131
+ if (!('m' in parsed)) return null
132
+ // Each field checked for the type `#seal` writes before it is compared: an
133
+ // expiry that is not a number (`"e": "9999"`, `"e": null`) or a purpose
134
+ // that is not a string must not slip past the comparisons below.
135
+ const expiry: unknown = 'e' in parsed ? parsed.e : undefined
136
+ if (expiry !== undefined) {
137
+ if (typeof expiry !== 'number' || !Number.isFinite(expiry)) return null
138
+ if (expiry < Date.now()) return null
139
+ }
140
+ const sealedFor: unknown = 'p' in parsed ? parsed.p : undefined
141
+ if (sealedFor !== undefined && typeof sealedFor !== 'string') return null
142
+ if (sealedFor !== purpose) return null
143
+ return parsed.m
136
144
  }
137
145
 
138
146
  /**
@@ -3,6 +3,7 @@
3
3
  * Uses Rust NAPI via crypto facade when available.
4
4
  */
5
5
 
6
+ import { durationToSeconds } from '../helpers/duration.js'
6
7
  import { constantTimeEq, hmacSign } from './crypto.js'
7
8
 
8
9
  export interface SignedUrlConfig {
@@ -56,22 +57,17 @@ export class SignedUrl {
56
57
  }
57
58
  }
58
59
 
60
+ /**
61
+ * The lifetime of a signed URL in seconds. Read with the duration grammar
62
+ * used everywhere else (`'30 minutes'`, `'1 week'`, `'2h'`), and refused when
63
+ * it cannot be read: signing with a default instead turned "one week" into
64
+ * one hour without a word.
65
+ */
59
66
  function parseExpiry(value: string | number): number {
60
- if (typeof value === 'number') return value
61
- const match = value.match(/^(\d+)(s|m|h|d)$/)
62
- if (!match) return 3600
63
- const [, digits, unit] = match
64
- const num = parseInt(digits ?? '0', 10)
65
- switch (unit) {
66
- case 's':
67
- return num
68
- case 'm':
69
- return num * 60
70
- case 'h':
71
- return num * 3600
72
- case 'd':
73
- return num * 86400
74
- default:
75
- return 3600
67
+ if (typeof value === 'number' && !Number.isFinite(value)) {
68
+ throw new Error(
69
+ `Cannot read ${value} as a signed URL expiry: it must be a finite number of seconds.`,
70
+ )
76
71
  }
72
+ return durationToSeconds(value, 'a signed URL expiry')
77
73
  }
@@ -39,7 +39,10 @@ export interface GrpcCall {
39
39
  path: string
40
40
  /**
41
41
  * Request metadata: the request headers, every value of a repeated one in
42
- * order, names lowercased. Never the client's trailers.
42
+ * order, names lowercased. Never the client's trailers. A `-bin` value is
43
+ * the base64 text that arrived: this is the HTTP/2 header layer, and the
44
+ * gRPC package that registers the handler decodes it, as a gRPC library's
45
+ * metadata class does over Node's HTTP/2 headers.
43
46
  */
44
47
  metadata: Record<string, string[]>
45
48
  /** The request message, still encoded. */
@@ -29,12 +29,21 @@ export interface NativeBodyLimit {
29
29
  types?: string[]
30
30
  }
31
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
+
32
41
  /** Body limits per body-parser section; an omitted section keeps its default. */
33
42
  export interface NativeBodyLimits {
34
43
  json?: NativeBodyLimit
35
44
  form?: NativeBodyLimit
36
45
  raw?: NativeBodyLimit
37
- multipart?: NativeBodyLimit
46
+ multipart?: NativeMultipartLimit
38
47
  }
39
48
 
40
49
  /** Timeouts in milliseconds (`0` disables one) and the connection cap (`0` = unbounded). */
@@ -58,6 +67,14 @@ export function nativeBodyLimits(config: BodyParserConfig): NativeBodyLimits {
58
67
  if (given.types !== undefined) native.types = [...given.types]
59
68
  limits[section] = native
60
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
+ }
61
78
  return limits
62
79
  }
63
80