@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.
- package/README.md +5 -6
- package/dist/Codemods.d.ts +24 -0
- package/dist/Codemods.d.ts.map +1 -1
- package/dist/Codemods.js +211 -1
- package/dist/Codemods.js.map +1 -1
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +17 -5
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +15 -2
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +33 -23
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +4 -4
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +41 -11
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts +21 -15
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +40 -58
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/bodyparser/parseSize.d.ts +7 -4
- package/dist/bodyparser/parseSize.d.ts.map +1 -1
- package/dist/bodyparser/parseSize.js +12 -17
- package/dist/bodyparser/parseSize.js.map +1 -1
- package/dist/bodyparser/qsParse.d.ts +20 -1
- package/dist/bodyparser/qsParse.d.ts.map +1 -1
- package/dist/bodyparser/qsParse.js +78 -25
- package/dist/bodyparser/qsParse.js.map +1 -1
- package/dist/codemods/source.d.ts +57 -0
- package/dist/codemods/source.d.ts.map +1 -0
- package/dist/codemods/source.js +282 -0
- package/dist/codemods/source.js.map +1 -0
- package/dist/console/prompts.d.ts.map +1 -1
- package/dist/console/prompts.js +3 -1
- package/dist/console/prompts.js.map +1 -1
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +61 -71
- package/dist/container/Container.js.map +1 -1
- package/dist/events/Emitter.d.ts +12 -0
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +36 -9
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/helix/helpers.d.ts.map +1 -1
- package/dist/events/helix/helpers.js +19 -9
- package/dist/events/helix/helpers.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +8 -0
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +79 -12
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/helpers/MessageBuilder.d.ts +4 -2
- package/dist/helpers/MessageBuilder.d.ts.map +1 -1
- package/dist/helpers/MessageBuilder.js +4 -2
- package/dist/helpers/MessageBuilder.js.map +1 -1
- package/dist/helpers/errors.d.ts +11 -0
- package/dist/helpers/errors.d.ts.map +1 -0
- package/dist/helpers/errors.js +17 -0
- package/dist/helpers/errors.js.map +1 -0
- package/dist/helpers/napi-loader.d.ts.map +1 -1
- package/dist/helpers/napi-loader.js +3 -2
- package/dist/helpers/napi-loader.js.map +1 -1
- package/dist/http/Exception.d.ts +14 -1
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +10 -2
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/HttpContext.d.ts +31 -2
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +12 -5
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts +9 -3
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +43 -37
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts +42 -17
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +80 -58
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +7 -4
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +14 -6
- package/dist/http/Response.js.map +1 -1
- package/dist/http/SseStream.js +6 -7
- package/dist/http/SseStream.js.map +1 -1
- package/dist/http/permissions.d.ts +25 -0
- package/dist/http/permissions.d.ts.map +1 -0
- package/dist/http/permissions.js +38 -0
- package/dist/http/permissions.js.map +1 -0
- package/dist/http-client/HttpClient.d.ts +111 -0
- package/dist/http-client/HttpClient.d.ts.map +1 -0
- package/dist/http-client/HttpClient.js +386 -0
- package/dist/http-client/HttpClient.js.map +1 -0
- package/dist/http-client/addresses.d.ts +14 -0
- package/dist/http-client/addresses.d.ts.map +1 -0
- package/dist/http-client/addresses.js +57 -0
- package/dist/http-client/addresses.js.map +1 -0
- package/dist/http-client/fake.d.ts +50 -0
- package/dist/http-client/fake.d.ts.map +1 -0
- package/dist/http-client/fake.js +95 -0
- package/dist/http-client/fake.js.map +1 -0
- package/dist/http-client/index.d.ts +7 -0
- package/dist/http-client/index.d.ts.map +1 -0
- package/dist/http-client/index.js +7 -0
- package/dist/http-client/index.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.d.ts.map +1 -1
- package/dist/middleware/Pipeline.js +6 -3
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/native/generated.d.ts +93 -36
- package/dist/native/generated.d.ts.map +1 -1
- package/dist/profiler/Profiler.d.ts.map +1 -1
- package/dist/profiler/Profiler.js +4 -1
- package/dist/profiler/Profiler.js.map +1 -1
- package/dist/profiler/client.generated.d.ts +5 -0
- package/dist/profiler/client.generated.d.ts.map +1 -0
- package/dist/profiler/client.generated.js +6 -0
- package/dist/profiler/client.generated.js.map +1 -0
- package/dist/profiler/collectors.d.ts +92 -13
- package/dist/profiler/collectors.d.ts.map +1 -1
- package/dist/profiler/collectors.js +106 -19
- package/dist/profiler/collectors.js.map +1 -1
- package/dist/profiler/endpoint.d.ts +2 -1
- package/dist/profiler/endpoint.d.ts.map +1 -1
- package/dist/profiler/endpoint.js +25 -1
- package/dist/profiler/endpoint.js.map +1 -1
- package/dist/profiler/index.d.ts +8 -4
- package/dist/profiler/index.d.ts.map +1 -1
- package/dist/profiler/index.js +11 -4
- package/dist/profiler/index.js.map +1 -1
- package/dist/profiler/middleware.d.ts.map +1 -1
- package/dist/profiler/middleware.js +76 -10
- package/dist/profiler/middleware.js.map +1 -1
- package/dist/profiler/preview.d.ts +17 -0
- package/dist/profiler/preview.d.ts.map +1 -0
- package/dist/profiler/preview.js +100 -0
- package/dist/profiler/preview.js.map +1 -0
- package/dist/profiler/toolbar.d.ts +15 -23
- package/dist/profiler/toolbar.d.ts.map +1 -1
- package/dist/profiler/toolbar.js +25 -91
- package/dist/profiler/toolbar.js.map +1 -1
- package/dist/profiler/types.d.ts +6 -1
- package/dist/profiler/types.d.ts.map +1 -1
- package/dist/router/Router.d.ts +6 -0
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +115 -12
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcProvider.d.ts +6 -0
- package/dist/rpc/RpcProvider.d.ts.map +1 -1
- package/dist/rpc/RpcProvider.js +4 -1
- package/dist/rpc/RpcProvider.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +27 -14
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +97 -40
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/Scheduler.js +1 -1
- package/dist/scheduler/Scheduler.js.map +1 -1
- package/dist/scheduler/locks/RedisLockBackend.d.ts.map +1 -1
- package/dist/scheduler/locks/RedisLockBackend.js +13 -2
- package/dist/scheduler/locks/RedisLockBackend.js.map +1 -1
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +32 -8
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/security/SignedUrl.d.ts.map +1 -1
- package/dist/security/SignedUrl.js +16 -19
- package/dist/security/SignedUrl.js.map +1 -1
- package/dist/server/Server.d.ts +44 -12
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js +31 -10
- package/dist/server/Server.js.map +1 -1
- package/dist/server/nativeGrpc.d.ts +27 -0
- package/dist/server/nativeGrpc.d.ts.map +1 -0
- package/dist/server/nativeGrpc.js +47 -0
- package/dist/server/nativeGrpc.js.map +1 -0
- package/dist/server/nativeLimits.d.ts +57 -0
- package/dist/server/nativeLimits.d.ts.map +1 -0
- package/dist/server/nativeLimits.js +75 -0
- package/dist/server/nativeLimits.js.map +1 -0
- package/dist/session/Session.d.ts +12 -0
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +13 -4
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +28 -9
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/config.d.ts +5 -0
- package/dist/session/config.d.ts.map +1 -1
- package/dist/session/config.js +8 -0
- package/dist/session/config.js.map +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +26 -7
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/session/errors.d.ts +17 -0
- package/dist/session/errors.d.ts.map +1 -1
- package/dist/session/errors.js +21 -0
- package/dist/session/errors.js.map +1 -1
- package/dist/session/templateTags.d.ts +14 -0
- package/dist/session/templateTags.d.ts.map +1 -1
- package/dist/session/templateTags.js +45 -17
- package/dist/session/templateTags.js.map +1 -1
- package/dist/storage/StaticMiddleware.d.ts.map +1 -1
- package/dist/storage/StaticMiddleware.js +49 -27
- package/dist/storage/StaticMiddleware.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +7 -1
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +16 -4
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +1 -1
- package/dist/utils/Macroable.js +2 -2
- package/dist/utils/Macroable.js.map +1 -1
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +11 -3
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Codemods.ts +314 -3
- package/src/HttpKernel.ts +18 -7
- package/src/Ignitor.ts +58 -26
- package/src/bodyparser/BodyParserMiddleware.ts +45 -16
- package/src/bodyparser/MultipartFile.ts +46 -67
- package/src/bodyparser/parseSize.ts +17 -17
- package/src/bodyparser/qsParse.ts +96 -26
- package/src/codemods/source.ts +303 -0
- package/src/console/prompts.ts +3 -1
- package/src/container/Container.ts +97 -81
- package/src/events/Emitter.ts +37 -10
- package/src/events/helix/helpers.ts +24 -19
- package/src/graphql/GraphQLEngine.ts +91 -14
- package/src/helpers/MessageBuilder.ts +4 -2
- package/src/helpers/errors.ts +16 -0
- package/src/helpers/napi-loader.ts +3 -2
- package/src/http/Exception.ts +17 -2
- package/src/http/HttpContext.ts +34 -5
- package/src/http/RedirectBuilder.ts +51 -37
- package/src/http/Request.ts +102 -58
- package/src/http/Response.ts +15 -7
- package/src/http/SseStream.ts +6 -7
- package/src/http/permissions.ts +47 -0
- package/src/http-client/HttpClient.ts +505 -0
- package/src/http-client/addresses.ts +58 -0
- package/src/http-client/fake.ts +141 -0
- package/src/http-client/index.ts +21 -0
- package/src/index.ts +6 -1
- package/src/middleware/Pipeline.ts +7 -3
- package/src/native/generated.ts +106 -48
- package/src/profiler/Profiler.ts +4 -1
- package/src/profiler/client/toolbar.css +595 -0
- package/src/profiler/client/toolbar.js +1299 -0
- package/src/profiler/client.generated.ts +7 -0
- package/src/profiler/collectors.ts +156 -19
- package/src/profiler/endpoint.ts +27 -1
- package/src/profiler/index.ts +17 -3
- package/src/profiler/middleware.ts +74 -9
- package/src/profiler/preview.ts +104 -0
- package/src/profiler/toolbar.ts +28 -91
- package/src/profiler/types.ts +6 -1
- package/src/router/Router.ts +136 -13
- package/src/rpc/RpcProvider.ts +10 -1
- package/src/rpc/RpcRouter.ts +129 -45
- package/src/scheduler/Scheduler.ts +1 -1
- package/src/scheduler/locks/RedisLockBackend.ts +12 -4
- package/src/security/CookieSigner.ts +30 -8
- package/src/security/SignedUrl.ts +18 -17
- package/src/server/Server.ts +60 -12
- package/src/server/nativeGrpc.ts +59 -0
- package/src/server/nativeLimits.ts +111 -0
- package/src/session/Session.ts +29 -4
- package/src/session/SessionMiddleware.ts +30 -10
- package/src/session/config.ts +7 -0
- package/src/session/drivers/MemoryDriver.ts +26 -7
- package/src/session/errors.ts +31 -0
- package/src/session/templateTags.ts +57 -0
- package/src/storage/StaticMiddleware.ts +46 -27
- package/src/testing/ApiResponse.ts +7 -1
- package/src/testing/TestClient.ts +14 -5
- package/src/utils/Macroable.ts +2 -2
- package/scripts/build-napi-types.mjs +0 -69
- package/scripts/copy-napi.mjs +0 -121
- package/scripts/generate-mime.mjs +0 -119
- 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
|
+
}
|
package/src/profiler/toolbar.ts
CHANGED
|
@@ -1,106 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The tag that loads the debug toolbar into an HTML page.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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('&', '&')
|
|
40
|
+
.replaceAll('"', '"')
|
|
41
|
+
.replaceAll('<', '<')
|
|
42
|
+
.replaceAll('>', '>')
|
|
43
|
+
}
|
package/src/profiler/types.ts
CHANGED
|
@@ -45,8 +45,13 @@ export type ProfileSummary = Omit<Profile, 'data'> & {
|
|
|
45
45
|
*/
|
|
46
46
|
export interface CollectionScope {
|
|
47
47
|
token: string
|
|
48
|
-
/**
|
|
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
|
/**
|
package/src/router/Router.ts
CHANGED
|
@@ -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
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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('*.')) {
|
package/src/rpc/RpcProvider.ts
CHANGED
|
@@ -39,6 +39,12 @@ export interface RpcConfig {
|
|
|
39
39
|
* it as public and never populates `ctx.auth` (every guarded method 401s).
|
|
40
40
|
*/
|
|
41
41
|
guards?: string[]
|
|
42
|
+
/**
|
|
43
|
+
* Media types the endpoint accepts a request body under; any other is
|
|
44
|
+
* answered 415. Default: `application/json`, `application/json-rpc`,
|
|
45
|
+
* `application/jsonrequest`.
|
|
46
|
+
*/
|
|
47
|
+
contentTypes?: string[]
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
export class RpcProvider extends Provider {
|
|
@@ -47,7 +53,10 @@ export class RpcProvider extends Provider {
|
|
|
47
53
|
|
|
48
54
|
constructor(app: AppContext, options: RpcProviderOptions = {}) {
|
|
49
55
|
super(app)
|
|
50
|
-
|
|
56
|
+
// Config is loaded before providers are constructed.
|
|
57
|
+
this.rpc =
|
|
58
|
+
options.router ??
|
|
59
|
+
new RpcRouter({ contentTypes: app.config.get<RpcConfig>('rpc')?.contentTypes })
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
override register(): void {
|