@warlock.js/devtools 5.25.0
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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/cjs/index.cjs +694 -0
- package/cjs/index.cjs.map +1 -0
- package/cjs/server-DHYSVive.cjs +286 -0
- package/cjs/server-DHYSVive.cjs.map +1 -0
- package/esm/caller-location.mjs +33 -0
- package/esm/caller-location.mjs.map +1 -0
- package/esm/collector.mjs +185 -0
- package/esm/collector.mjs.map +1 -0
- package/esm/current-request-id.mjs +24 -0
- package/esm/current-request-id.mjs.map +1 -0
- package/esm/devtools-connector.d.mts +26 -0
- package/esm/devtools-connector.d.mts.map +1 -0
- package/esm/devtools-connector.mjs +112 -0
- package/esm/devtools-connector.mjs.map +1 -0
- package/esm/index.d.mts +3 -0
- package/esm/index.mjs +3 -0
- package/esm/n-plus-one.mjs +36 -0
- package/esm/n-plus-one.mjs.map +1 -0
- package/esm/options.mjs +19 -0
- package/esm/options.mjs.map +1 -0
- package/esm/query-shape.mjs +27 -0
- package/esm/query-shape.mjs.map +1 -0
- package/esm/safe-value.mjs +63 -0
- package/esm/safe-value.mjs.map +1 -0
- package/esm/server/background-routes.mjs +9 -0
- package/esm/server/background-routes.mjs.map +1 -0
- package/esm/server/explain-route.mjs +37 -0
- package/esm/server/explain-route.mjs.map +1 -0
- package/esm/server/guard.mjs +30 -0
- package/esm/server/guard.mjs.map +1 -0
- package/esm/server/index.mjs +37 -0
- package/esm/server/index.mjs.map +1 -0
- package/esm/server/log-routes.mjs +12 -0
- package/esm/server/log-routes.mjs.map +1 -0
- package/esm/server/mail-routes.mjs +27 -0
- package/esm/server/mail-routes.mjs.map +1 -0
- package/esm/server/requests-routes.mjs +21 -0
- package/esm/server/requests-routes.mjs.map +1 -0
- package/esm/server/route-routes.mjs +9 -0
- package/esm/server/route-routes.mjs.map +1 -0
- package/esm/server/stream-route.mjs +32 -0
- package/esm/server/stream-route.mjs.map +1 -0
- package/esm/server/ui-assets.mjs +38 -0
- package/esm/server/ui-assets.mjs.map +1 -0
- package/esm/sources/cache-source.mjs +51 -0
- package/esm/sources/cache-source.mjs.map +1 -0
- package/esm/sources/log-source.mjs +46 -0
- package/esm/sources/log-source.mjs.map +1 -0
- package/esm/sources/mail-source.mjs +48 -0
- package/esm/sources/mail-source.mjs.map +1 -0
- package/esm/sources/query-source.mjs +52 -0
- package/esm/sources/query-source.mjs.map +1 -0
- package/esm/sources/tracing-source.mjs +47 -0
- package/esm/sources/tracing-source.mjs.map +1 -0
- package/esm/types.d.mts +213 -0
- package/esm/types.d.mts.map +1 -0
- package/esm/ui-root.mjs +40 -0
- package/esm/ui-root.mjs.map +1 -0
- package/llms-full.txt +78 -0
- package/llms.txt +9 -0
- package/package.json +52 -0
- package/skills/devtools-overview/SKILL.md +68 -0
- package/ui/app.css +354 -0
- package/ui/app.js +520 -0
- package/ui/index.html +24 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"explain-route.mjs","names":[],"sources":["../../../../../../../devtools/src/server/explain-route.ts"],"sourcesContent":["import type { FastifyInstance } from \"fastify\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\n\n/**\n * The value the logger's redaction substitutes for a censored leaf\n * (`logger/src/redact/redact.ts`, `DEFAULT_CENSOR`). Not exported from\n * `@warlock.js/logger`, so matched by literal here.\n */\nconst REDACTED_PLACEHOLDER = \"[REDACTED]\";\n\nfunction isExplainableSql(sql: string | undefined): boolean {\n return /^\\s*(select|with)\\b/i.test(sql ?? \"\");\n}\n\n/** Registers the EXPLAIN route: Postgres `SELECT`/`WITH` queries only. */\nexport function registerExplainRoute(server: FastifyInstance, deps: MountDevtoolsRoutesDeps): void {\n server.post<{ Params: { requestId: string; queryId: string } }>(\n \"/api/requests/:requestId/queries/:queryId/explain\",\n async (request, reply) => {\n const found = deps.collector.getRequest(request.params.requestId);\n const query = found?.queries.find((item) => item.id === request.params.queryId);\n\n if (!query) {\n reply.code(404).send();\n return;\n }\n\n if (query.driver !== \"postgres\" || !isExplainableSql(query.sql) || !deps.explain) {\n reply.code(400).send({ error: \"Only Postgres SELECT or WITH queries can be explained\" });\n return;\n }\n\n if (query.bindings?.some((binding) => binding === REDACTED_PLACEHOLDER)) {\n reply.code(400).send({ error: \"bindings redacted\" });\n return;\n }\n\n try {\n const plan = await deps.explain(query);\n return { plan };\n } catch (error) {\n reply.code(500).send({ error: error instanceof Error ? error.message : String(error) });\n }\n },\n );\n}\n"],"mappings":";;;;;;AAQA,MAAM,uBAAuB;AAE7B,SAAS,iBAAiB,KAAkC;CAC1D,OAAO,uBAAuB,KAAK,OAAO,EAAE;AAC9C;;AAGA,SAAgB,qBAAqB,QAAyB,MAAqC;CACjG,OAAO,KACL,qDACA,OAAO,SAAS,UAAU;EAExB,MAAM,QADQ,KAAK,UAAU,WAAW,QAAQ,OAAO,SACrC,CAAC,EAAE,QAAQ,MAAM,SAAS,KAAK,OAAO,QAAQ,OAAO,OAAO;EAE9E,IAAI,CAAC,OAAO;GACV,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;GACrB;EACF;EAEA,IAAI,MAAM,WAAW,cAAc,CAAC,iBAAiB,MAAM,GAAG,KAAK,CAAC,KAAK,SAAS;GAChF,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,wDAAwD,CAAC;GACvF;EACF;EAEA,IAAI,MAAM,UAAU,MAAM,YAAY,YAAY,oBAAoB,GAAG;GACvE,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,oBAAoB,CAAC;GACnD;EACF;EAEA,IAAI;GAEF,OAAO,EAAE,YADU,KAAK,QAAQ,KAAK,EACvB;EAChB,SAAS,OAAO;GACd,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,CAAC;EACxF;CACF,CACF;AACF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region ../devtools/src/server/guard.ts
|
|
2
|
+
const LOOPBACK_ADDRESSES = new Set([
|
|
3
|
+
"127.0.0.1",
|
|
4
|
+
"::1",
|
|
5
|
+
"::ffff:127.0.0.1"
|
|
6
|
+
]);
|
|
7
|
+
/**
|
|
8
|
+
* Whether `address` is a loopback address. Deliberately reads
|
|
9
|
+
* `request.socket.remoteAddress` rather than `request.ip`, which honours
|
|
10
|
+
* `X-Forwarded-For` when the app trusts a proxy — a header any client can
|
|
11
|
+
* set.
|
|
12
|
+
*/
|
|
13
|
+
function isLoopbackAddress(address) {
|
|
14
|
+
return address !== void 0 && LOOPBACK_ADDRESSES.has(address);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Registers the dev-only, loopback-only guard as an `onRequest` hook on
|
|
18
|
+
* `server`. Callers must register this inside an encapsulated Fastify
|
|
19
|
+
* context (a plugin/prefix) so the hook never reaches routes outside the
|
|
20
|
+
* dashboard.
|
|
21
|
+
*/
|
|
22
|
+
function registerDevtoolsGuard(server, deps) {
|
|
23
|
+
server.addHook("onRequest", async (request, reply) => {
|
|
24
|
+
if (!deps.isDevelopment() || !isLoopbackAddress(request.socket.remoteAddress)) return reply.code(404).send();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
export { registerDevtoolsGuard };
|
|
30
|
+
//# sourceMappingURL=guard.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.mjs","names":[],"sources":["../../../../../../../devtools/src/server/guard.ts"],"sourcesContent":["import type { FastifyInstance, FastifyReply, FastifyRequest } from \"fastify\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\n\nconst LOOPBACK_ADDRESSES = new Set([\"127.0.0.1\", \"::1\", \"::ffff:127.0.0.1\"]);\n\n/**\n * Whether `address` is a loopback address. Deliberately reads\n * `request.socket.remoteAddress` rather than `request.ip`, which honours\n * `X-Forwarded-For` when the app trusts a proxy — a header any client can\n * set.\n */\nfunction isLoopbackAddress(address: string | undefined): boolean {\n return address !== undefined && LOOPBACK_ADDRESSES.has(address);\n}\n\n/**\n * Registers the dev-only, loopback-only guard as an `onRequest` hook on\n * `server`. Callers must register this inside an encapsulated Fastify\n * context (a plugin/prefix) so the hook never reaches routes outside the\n * dashboard.\n */\nexport function registerDevtoolsGuard(\n server: FastifyInstance,\n deps: MountDevtoolsRoutesDeps,\n): void {\n server.addHook(\"onRequest\", async (request: FastifyRequest, reply: FastifyReply) => {\n if (!deps.isDevelopment() || !isLoopbackAddress(request.socket.remoteAddress)) {\n // Returning the reply is what stops the lifecycle in an async hook;\n // without it Fastify still runs the handler after the 404 is sent.\n return reply.code(404).send();\n }\n });\n}\n"],"mappings":";AAGA,MAAM,qBAAqB,IAAI,IAAI;CAAC;CAAa;CAAO;AAAkB,CAAC;;;;;;;AAQ3E,SAAS,kBAAkB,SAAsC;CAC/D,OAAO,YAAY,UAAa,mBAAmB,IAAI,OAAO;AAChE;;;;;;;AAQA,SAAgB,sBACd,QACA,MACM;CACN,OAAO,QAAQ,aAAa,OAAO,SAAyB,UAAwB;EAClF,IAAI,CAAC,KAAK,cAAc,KAAK,CAAC,kBAAkB,QAAQ,OAAO,aAAa,GAG1E,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;CAEhC,CAAC;AACH"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { registerBackgroundRoutes } from "./background-routes.mjs";
|
|
2
|
+
import { registerExplainRoute } from "./explain-route.mjs";
|
|
3
|
+
import { registerDevtoolsGuard } from "./guard.mjs";
|
|
4
|
+
import { registerLogRoutes } from "./log-routes.mjs";
|
|
5
|
+
import { registerMailRoutes } from "./mail-routes.mjs";
|
|
6
|
+
import { registerRequestsRoutes } from "./requests-routes.mjs";
|
|
7
|
+
import { registerRouteRoutes } from "./route-routes.mjs";
|
|
8
|
+
import { registerStreamRoute } from "./stream-route.mjs";
|
|
9
|
+
import { registerUiAssets } from "./ui-assets.mjs";
|
|
10
|
+
|
|
11
|
+
//#region ../devtools/src/server/index.ts
|
|
12
|
+
/**
|
|
13
|
+
* Mounts the devtools dashboard and its API under `deps.options.path` on
|
|
14
|
+
* `server`. Every route is registered inside a single encapsulated Fastify
|
|
15
|
+
* context so the dev-only, loopback-only guard never reaches the app's
|
|
16
|
+
* other routes.
|
|
17
|
+
*/
|
|
18
|
+
function mountDevtoolsRoutes(server, deps) {
|
|
19
|
+
server.register(async (instance) => {
|
|
20
|
+
registerDevtoolsGuard(instance, deps);
|
|
21
|
+
instance.setErrorHandler((error, _request, reply) => {
|
|
22
|
+
reply.code(error.statusCode ?? 500).send({ error: error.message });
|
|
23
|
+
});
|
|
24
|
+
registerUiAssets(instance);
|
|
25
|
+
registerRequestsRoutes(instance, deps);
|
|
26
|
+
registerBackgroundRoutes(instance, deps);
|
|
27
|
+
registerMailRoutes(instance, deps);
|
|
28
|
+
registerLogRoutes(instance, deps);
|
|
29
|
+
registerRouteRoutes(instance, deps);
|
|
30
|
+
registerExplainRoute(instance, deps);
|
|
31
|
+
registerStreamRoute(instance, deps);
|
|
32
|
+
}, { prefix: deps.options.path });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { mountDevtoolsRoutes };
|
|
37
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../devtools/src/server/index.ts"],"sourcesContent":["import type { FastifyError, FastifyInstance } from \"fastify\";\nimport { registerBackgroundRoutes } from \"./background-routes\";\nimport { registerExplainRoute } from \"./explain-route\";\nimport { registerDevtoolsGuard } from \"./guard\";\nimport { registerLogRoutes } from \"./log-routes\";\nimport { registerMailRoutes } from \"./mail-routes\";\nimport { registerRequestsRoutes } from \"./requests-routes\";\nimport { registerRouteRoutes } from \"./route-routes\";\nimport { registerStreamRoute } from \"./stream-route\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\nimport { registerUiAssets } from \"./ui-assets\";\n\nexport type { DevtoolsRouteRow, MountDevtoolsRoutesDeps } from \"./types\";\n\n/**\n * Mounts the devtools dashboard and its API under `deps.options.path` on\n * `server`. Every route is registered inside a single encapsulated Fastify\n * context so the dev-only, loopback-only guard never reaches the app's\n * other routes.\n */\nexport function mountDevtoolsRoutes(server: FastifyInstance, deps: MountDevtoolsRoutesDeps): void {\n server.register(\n async (instance) => {\n registerDevtoolsGuard(instance, deps);\n\n instance.setErrorHandler((error: FastifyError, _request, reply) => {\n reply.code(error.statusCode ?? 500).send({ error: error.message });\n });\n\n registerUiAssets(instance);\n registerRequestsRoutes(instance, deps);\n registerBackgroundRoutes(instance, deps);\n registerMailRoutes(instance, deps);\n registerLogRoutes(instance, deps);\n registerRouteRoutes(instance, deps);\n registerExplainRoute(instance, deps);\n registerStreamRoute(instance, deps);\n },\n { prefix: deps.options.path },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAoBA,SAAgB,oBAAoB,QAAyB,MAAqC;CAChG,OAAO,SACL,OAAO,aAAa;EAClB,sBAAsB,UAAU,IAAI;EAEpC,SAAS,iBAAiB,OAAqB,UAAU,UAAU;GACjE,MAAM,KAAK,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,MAAM,QAAQ,CAAC;EACnE,CAAC;EAED,iBAAiB,QAAQ;EACzB,uBAAuB,UAAU,IAAI;EACrC,yBAAyB,UAAU,IAAI;EACvC,mBAAmB,UAAU,IAAI;EACjC,kBAAkB,UAAU,IAAI;EAChC,oBAAoB,UAAU,IAAI;EAClC,qBAAqB,UAAU,IAAI;EACnC,oBAAoB,UAAU,IAAI;CACpC,GACA,EAAE,QAAQ,KAAK,QAAQ,KAAK,CAC9B;AACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region ../devtools/src/server/log-routes.ts
|
|
2
|
+
/** Registers the log listing route, filterable by `level` and `module`. */
|
|
3
|
+
function registerLogRoutes(server, deps) {
|
|
4
|
+
server.get("/api/logs", async (request) => deps.collector.listLogs({
|
|
5
|
+
level: request.query.level,
|
|
6
|
+
module: request.query.module
|
|
7
|
+
}));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
export { registerLogRoutes };
|
|
12
|
+
//# sourceMappingURL=log-routes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-routes.mjs","names":[],"sources":["../../../../../../../devtools/src/server/log-routes.ts"],"sourcesContent":["import type { FastifyInstance } from \"fastify\";\nimport type { DevtoolsLogEntry } from \"../types\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\n\ntype LogsQuery = { level?: DevtoolsLogEntry[\"level\"]; module?: string };\n\n/** Registers the log listing route, filterable by `level` and `module`. */\nexport function registerLogRoutes(server: FastifyInstance, deps: MountDevtoolsRoutesDeps): void {\n server.get<{ Querystring: LogsQuery }>(\"/api/logs\", async (request) =>\n deps.collector.listLogs({ level: request.query.level, module: request.query.module }),\n );\n}\n"],"mappings":";;AAOA,SAAgB,kBAAkB,QAAyB,MAAqC;CAC9F,OAAO,IAAgC,aAAa,OAAO,YACzD,KAAK,UAAU,SAAS;EAAE,OAAO,QAAQ,MAAM;EAAO,QAAQ,QAAQ,MAAM;CAAO,CAAC,CACtF;AACF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region ../devtools/src/server/mail-routes.ts
|
|
2
|
+
const MAIL_HTML_CSP = "sandbox; default-src 'none'; img-src data: https: http:; style-src 'unsafe-inline'";
|
|
3
|
+
/** Registers the mailbox list, single-mail detail, and mail HTML routes. */
|
|
4
|
+
function registerMailRoutes(server, deps) {
|
|
5
|
+
server.get("/api/mails", async () => deps.collector.listMails());
|
|
6
|
+
server.get("/api/mails/:id", async (request, reply) => {
|
|
7
|
+
const mail = deps.collector.getMail(request.params.id);
|
|
8
|
+
if (!mail) {
|
|
9
|
+
reply.code(404).send();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const { html, ...rest } = mail;
|
|
13
|
+
return rest;
|
|
14
|
+
});
|
|
15
|
+
server.get("/api/mails/:id/html", async (request, reply) => {
|
|
16
|
+
const mail = deps.collector.getMail(request.params.id);
|
|
17
|
+
if (!mail || !mail.html) {
|
|
18
|
+
reply.code(404).send();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
reply.header("Content-Security-Policy", MAIL_HTML_CSP).header("X-Content-Type-Options", "nosniff").header("Content-Type", "text/html; charset=utf-8").send(mail.html);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { registerMailRoutes };
|
|
27
|
+
//# sourceMappingURL=mail-routes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail-routes.mjs","names":[],"sources":["../../../../../../../devtools/src/server/mail-routes.ts"],"sourcesContent":["import type { FastifyInstance } from \"fastify\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\n\nconst MAIL_HTML_CSP =\n \"sandbox; default-src 'none'; img-src data: https: http:; style-src 'unsafe-inline'\";\n\n/** Registers the mailbox list, single-mail detail, and mail HTML routes. */\nexport function registerMailRoutes(server: FastifyInstance, deps: MountDevtoolsRoutesDeps): void {\n server.get(\"/api/mails\", async () => deps.collector.listMails());\n\n server.get<{ Params: { id: string } }>(\"/api/mails/:id\", async (request, reply) => {\n const mail = deps.collector.getMail(request.params.id);\n\n if (!mail) {\n reply.code(404).send();\n return;\n }\n\n const { html, ...rest } = mail;\n return rest;\n });\n\n server.get<{ Params: { id: string } }>(\"/api/mails/:id/html\", async (request, reply) => {\n const mail = deps.collector.getMail(request.params.id);\n\n if (!mail || !mail.html) {\n reply.code(404).send();\n return;\n }\n\n reply\n .header(\"Content-Security-Policy\", MAIL_HTML_CSP)\n .header(\"X-Content-Type-Options\", \"nosniff\")\n .header(\"Content-Type\", \"text/html; charset=utf-8\")\n .send(mail.html);\n });\n}\n"],"mappings":";AAGA,MAAM,gBACJ;;AAGF,SAAgB,mBAAmB,QAAyB,MAAqC;CAC/F,OAAO,IAAI,cAAc,YAAY,KAAK,UAAU,UAAU,CAAC;CAE/D,OAAO,IAAgC,kBAAkB,OAAO,SAAS,UAAU;EACjF,MAAM,OAAO,KAAK,UAAU,QAAQ,QAAQ,OAAO,EAAE;EAErD,IAAI,CAAC,MAAM;GACT,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;GACrB;EACF;EAEA,MAAM,EAAE,MAAM,GAAG,SAAS;EAC1B,OAAO;CACT,CAAC;CAED,OAAO,IAAgC,uBAAuB,OAAO,SAAS,UAAU;EACtF,MAAM,OAAO,KAAK,UAAU,QAAQ,QAAQ,OAAO,EAAE;EAErD,IAAI,CAAC,QAAQ,CAAC,KAAK,MAAM;GACvB,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;GACrB;EACF;EAEA,MACG,OAAO,2BAA2B,aAAa,CAAC,CAChD,OAAO,0BAA0B,SAAS,CAAC,CAC3C,OAAO,gBAAgB,0BAA0B,CAAC,CAClD,KAAK,KAAK,IAAI;CACnB,CAAC;AACH"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region ../devtools/src/server/requests-routes.ts
|
|
2
|
+
/** Registers the request list, single-request detail, and clear routes. */
|
|
3
|
+
function registerRequestsRoutes(server, deps) {
|
|
4
|
+
server.get("/api/requests", async () => deps.collector.listRequests());
|
|
5
|
+
server.get("/api/requests/:id", async (request, reply) => {
|
|
6
|
+
const found = deps.collector.getRequest(request.params.id);
|
|
7
|
+
if (!found) {
|
|
8
|
+
reply.code(404).send();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
return found;
|
|
12
|
+
});
|
|
13
|
+
server.delete("/api/requests", async (_request, reply) => {
|
|
14
|
+
deps.collector.clear();
|
|
15
|
+
reply.code(204).send();
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { registerRequestsRoutes };
|
|
21
|
+
//# sourceMappingURL=requests-routes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requests-routes.mjs","names":[],"sources":["../../../../../../../devtools/src/server/requests-routes.ts"],"sourcesContent":["import type { FastifyInstance } from \"fastify\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\n\n/** Registers the request list, single-request detail, and clear routes. */\nexport function registerRequestsRoutes(\n server: FastifyInstance,\n deps: MountDevtoolsRoutesDeps,\n): void {\n server.get(\"/api/requests\", async () => deps.collector.listRequests());\n\n server.get<{ Params: { id: string } }>(\"/api/requests/:id\", async (request, reply) => {\n const found = deps.collector.getRequest(request.params.id);\n\n if (!found) {\n reply.code(404).send();\n return;\n }\n\n return found;\n });\n\n server.delete(\"/api/requests\", async (_request, reply) => {\n deps.collector.clear();\n reply.code(204).send();\n });\n}\n"],"mappings":";;AAIA,SAAgB,uBACd,QACA,MACM;CACN,OAAO,IAAI,iBAAiB,YAAY,KAAK,UAAU,aAAa,CAAC;CAErE,OAAO,IAAgC,qBAAqB,OAAO,SAAS,UAAU;EACpF,MAAM,QAAQ,KAAK,UAAU,WAAW,QAAQ,OAAO,EAAE;EAEzD,IAAI,CAAC,OAAO;GACV,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;GACrB;EACF;EAEA,OAAO;CACT,CAAC;CAED,OAAO,OAAO,iBAAiB,OAAO,UAAU,UAAU;EACxD,KAAK,UAAU,MAAM;EACrB,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;CACvB,CAAC;AACH"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region ../devtools/src/server/route-routes.ts
|
|
2
|
+
/** Registers the route map, sourced from core's router via `deps.listRoutes`. */
|
|
3
|
+
function registerRouteRoutes(server, deps) {
|
|
4
|
+
server.get("/api/routes", async () => deps.listRoutes ? deps.listRoutes() : []);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
//#endregion
|
|
8
|
+
export { registerRouteRoutes };
|
|
9
|
+
//# sourceMappingURL=route-routes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-routes.mjs","names":[],"sources":["../../../../../../../devtools/src/server/route-routes.ts"],"sourcesContent":["import type { FastifyInstance } from \"fastify\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\n\n/** Registers the route map, sourced from core's router via `deps.listRoutes`. */\nexport function registerRouteRoutes(server: FastifyInstance, deps: MountDevtoolsRoutesDeps): void {\n server.get(\"/api/routes\", async () => (deps.listRoutes ? deps.listRoutes() : []));\n}\n"],"mappings":";;AAIA,SAAgB,oBAAoB,QAAyB,MAAqC;CAChG,OAAO,IAAI,eAAe,YAAa,KAAK,aAAa,KAAK,WAAW,IAAI,CAAC,CAAE;AAClF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region ../devtools/src/server/stream-route.ts
|
|
2
|
+
const PING_INTERVAL_MS = 15e3;
|
|
3
|
+
/**
|
|
4
|
+
* Registers the SSE stream: one `event: <type>\ndata: <json>\n\n` frame per
|
|
5
|
+
* collector event, plus a `: ping` comment every 15s to keep intermediaries
|
|
6
|
+
* from closing an idle connection. The reply is hijacked so Fastify never
|
|
7
|
+
* tries to serialize or close the response itself.
|
|
8
|
+
*/
|
|
9
|
+
function registerStreamRoute(server, deps) {
|
|
10
|
+
server.get("/api/stream", async (request, reply) => {
|
|
11
|
+
reply.hijack();
|
|
12
|
+
const res = reply.raw;
|
|
13
|
+
res.writeHead(200, {
|
|
14
|
+
"Content-Type": "text/event-stream",
|
|
15
|
+
"Cache-Control": "no-store",
|
|
16
|
+
Connection: "keep-alive"
|
|
17
|
+
});
|
|
18
|
+
const dispose = deps.collector.subscribe((event) => {
|
|
19
|
+
res.write(`event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`);
|
|
20
|
+
});
|
|
21
|
+
const pingTimer = setInterval(() => res.write(": ping\n\n"), PING_INTERVAL_MS);
|
|
22
|
+
pingTimer.unref();
|
|
23
|
+
request.raw.on("close", () => {
|
|
24
|
+
clearInterval(pingTimer);
|
|
25
|
+
dispose();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { registerStreamRoute };
|
|
32
|
+
//# sourceMappingURL=stream-route.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-route.mjs","names":[],"sources":["../../../../../../../devtools/src/server/stream-route.ts"],"sourcesContent":["import type { FastifyInstance } from \"fastify\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\n\nconst PING_INTERVAL_MS = 15_000;\n\n/**\n * Registers the SSE stream: one `event: <type>\\ndata: <json>\\n\\n` frame per\n * collector event, plus a `: ping` comment every 15s to keep intermediaries\n * from closing an idle connection. The reply is hijacked so Fastify never\n * tries to serialize or close the response itself.\n */\nexport function registerStreamRoute(server: FastifyInstance, deps: MountDevtoolsRoutesDeps): void {\n server.get(\"/api/stream\", async (request, reply) => {\n reply.hijack();\n const res = reply.raw;\n\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-store\",\n Connection: \"keep-alive\",\n });\n\n const dispose = deps.collector.subscribe((event) => {\n res.write(`event: ${event.type}\\ndata: ${JSON.stringify(event.data)}\\n\\n`);\n });\n\n const pingTimer = setInterval(() => res.write(\": ping\\n\\n\"), PING_INTERVAL_MS);\n pingTimer.unref();\n\n request.raw.on(\"close\", () => {\n clearInterval(pingTimer);\n dispose();\n });\n });\n}\n"],"mappings":";AAGA,MAAM,mBAAmB;;;;;;;AAQzB,SAAgB,oBAAoB,QAAyB,MAAqC;CAChG,OAAO,IAAI,eAAe,OAAO,SAAS,UAAU;EAClD,MAAM,OAAO;EACb,MAAM,MAAM,MAAM;EAElB,IAAI,UAAU,KAAK;GACjB,gBAAgB;GAChB,iBAAiB;GACjB,YAAY;EACd,CAAC;EAED,MAAM,UAAU,KAAK,UAAU,WAAW,UAAU;GAClD,IAAI,MAAM,UAAU,MAAM,KAAK,UAAU,KAAK,UAAU,MAAM,IAAI,EAAE,KAAK;EAC3E,CAAC;EAED,MAAM,YAAY,kBAAkB,IAAI,MAAM,YAAY,GAAG,gBAAgB;EAC7E,UAAU,MAAM;EAEhB,QAAQ,IAAI,GAAG,eAAe;GAC5B,cAAc,SAAS;GACvB,QAAQ;EACV,CAAC;CACH,CAAC;AACH"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { resolveUiRoot } from "../ui-root.mjs";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
//#region ../devtools/src/server/ui-assets.ts
|
|
6
|
+
const ASSET_CONTENT_TYPES = {
|
|
7
|
+
"app.js": "text/javascript; charset=utf-8",
|
|
8
|
+
"app.css": "text/css; charset=utf-8"
|
|
9
|
+
};
|
|
10
|
+
function sendFile(reply, file, contentType) {
|
|
11
|
+
reply.header("Cache-Control", "no-store").header("Content-Type", contentType).send(readFileSync(file));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Serves the static dashboard: `index.html` at the dashboard root and the
|
|
15
|
+
* two named assets. The asset name is matched against a fixed whitelist —
|
|
16
|
+
* never joined into a filesystem path — so no request can traverse outside
|
|
17
|
+
* `ui/`.
|
|
18
|
+
*/
|
|
19
|
+
function registerUiAssets(server) {
|
|
20
|
+
const serveIndex = (_request, reply) => {
|
|
21
|
+
const uiRoot = resolveUiRoot();
|
|
22
|
+
sendFile(reply, path.join(uiRoot, "index.html"), "text/html; charset=utf-8");
|
|
23
|
+
};
|
|
24
|
+
server.get("/", serveIndex);
|
|
25
|
+
server.get("/assets/:name", (request, reply) => {
|
|
26
|
+
const contentType = ASSET_CONTENT_TYPES[request.params.name];
|
|
27
|
+
if (!contentType) {
|
|
28
|
+
reply.code(404).header("Cache-Control", "no-store").send();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const uiRoot = resolveUiRoot();
|
|
32
|
+
sendFile(reply, path.join(uiRoot, request.params.name), contentType);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { registerUiAssets };
|
|
38
|
+
//# sourceMappingURL=ui-assets.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui-assets.mjs","names":[],"sources":["../../../../../../../devtools/src/server/ui-assets.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { FastifyInstance, FastifyReply } from \"fastify\";\nimport { resolveUiRoot } from \"../ui-root\";\n\nconst ASSET_CONTENT_TYPES: Record<string, string> = {\n \"app.js\": \"text/javascript; charset=utf-8\",\n \"app.css\": \"text/css; charset=utf-8\",\n};\n\nfunction sendFile(reply: FastifyReply, file: string, contentType: string): void {\n reply\n .header(\"Cache-Control\", \"no-store\")\n .header(\"Content-Type\", contentType)\n .send(readFileSync(file));\n}\n\n/**\n * Serves the static dashboard: `index.html` at the dashboard root and the\n * two named assets. The asset name is matched against a fixed whitelist —\n * never joined into a filesystem path — so no request can traverse outside\n * `ui/`.\n */\nexport function registerUiAssets(server: FastifyInstance): void {\n const serveIndex = (_request: unknown, reply: FastifyReply): void => {\n const uiRoot = resolveUiRoot();\n sendFile(reply, path.join(uiRoot, \"index.html\"), \"text/html; charset=utf-8\");\n };\n\n server.get(\"/\", serveIndex);\n\n server.get<{ Params: { name: string } }>(\"/assets/:name\", (request, reply) => {\n const contentType = ASSET_CONTENT_TYPES[request.params.name];\n\n if (!contentType) {\n reply.code(404).header(\"Cache-Control\", \"no-store\").send();\n return;\n }\n\n const uiRoot = resolveUiRoot();\n sendFile(reply, path.join(uiRoot, request.params.name), contentType);\n });\n}\n"],"mappings":";;;;;AAKA,MAAM,sBAA8C;CAClD,UAAU;CACV,WAAW;AACb;AAEA,SAAS,SAAS,OAAqB,MAAc,aAA2B;CAC9E,MACG,OAAO,iBAAiB,UAAU,CAAC,CACnC,OAAO,gBAAgB,WAAW,CAAC,CACnC,KAAK,aAAa,IAAI,CAAC;AAC5B;;;;;;;AAQA,SAAgB,iBAAiB,QAA+B;CAC9D,MAAM,cAAc,UAAmB,UAA8B;EACnE,MAAM,SAAS,cAAc;EAC7B,SAAS,OAAO,KAAK,KAAK,QAAQ,YAAY,GAAG,0BAA0B;CAC7E;CAEA,OAAO,IAAI,KAAK,UAAU;CAE1B,OAAO,IAAkC,kBAAkB,SAAS,UAAU;EAC5E,MAAM,cAAc,oBAAoB,QAAQ,OAAO;EAEvD,IAAI,CAAC,aAAa;GAChB,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,iBAAiB,UAAU,CAAC,CAAC,KAAK;GACzD;EACF;EAEA,MAAM,SAAS,cAAc;EAC7B,SAAS,OAAO,KAAK,KAAK,QAAQ,QAAQ,OAAO,IAAI,GAAG,WAAW;CACrE,CAAC;AACH"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { currentRequestId } from "../current-request-id.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../devtools/src/sources/cache-source.ts
|
|
4
|
+
/** Cache events the dashboard's waterfall and background bucket care about. */
|
|
5
|
+
const EVENT_TYPES = [
|
|
6
|
+
"hit",
|
|
7
|
+
"miss",
|
|
8
|
+
"set",
|
|
9
|
+
"removed",
|
|
10
|
+
"invalidated",
|
|
11
|
+
"flushed"
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Feeds cache hits, misses and mutations, from `@warlock.js/cache`, into the
|
|
15
|
+
* collector. A no-op when `@warlock.js/cache` isn't installed — it is an
|
|
16
|
+
* optional peer.
|
|
17
|
+
*/
|
|
18
|
+
async function attachCacheSource(collector) {
|
|
19
|
+
let cache;
|
|
20
|
+
try {
|
|
21
|
+
({cache} = await import("@warlock.js/cache"));
|
|
22
|
+
} catch {
|
|
23
|
+
return () => {};
|
|
24
|
+
}
|
|
25
|
+
const handlers = EVENT_TYPES.map((type) => {
|
|
26
|
+
const handler = (data) => {
|
|
27
|
+
const operation = {
|
|
28
|
+
type,
|
|
29
|
+
driver: data.driver,
|
|
30
|
+
key: data.key,
|
|
31
|
+
tags: data.tags,
|
|
32
|
+
keys: data.keys,
|
|
33
|
+
at: Date.now(),
|
|
34
|
+
durationMs: data.durationMs
|
|
35
|
+
};
|
|
36
|
+
collector.addCacheOperation(currentRequestId(), operation);
|
|
37
|
+
};
|
|
38
|
+
cache.on(type, handler);
|
|
39
|
+
return {
|
|
40
|
+
type,
|
|
41
|
+
handler
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
return () => {
|
|
45
|
+
for (const { type, handler } of handlers) cache.off(type, handler);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
export { attachCacheSource };
|
|
51
|
+
//# sourceMappingURL=cache-source.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-source.mjs","names":[],"sources":["../../../../../../../devtools/src/sources/cache-source.ts"],"sourcesContent":["import { currentRequestId } from \"../current-request-id\";\nimport type { DevtoolsCacheOperation, DevtoolsCollector, DevtoolsDisposer } from \"../types\";\n\n/** Cache events the dashboard's waterfall and background bucket care about. */\nconst EVENT_TYPES = [\"hit\", \"miss\", \"set\", \"removed\", \"invalidated\", \"flushed\"] as const;\n\n/** The shape `cache.on(event, handler)` hands the handler. */\ntype CacheEventData = {\n key?: string;\n driver: string;\n tags?: string[];\n keys?: string[];\n durationMs?: number;\n};\n\n/**\n * Feeds cache hits, misses and mutations, from `@warlock.js/cache`, into the\n * collector. A no-op when `@warlock.js/cache` isn't installed — it is an\n * optional peer.\n */\nexport async function attachCacheSource(collector: DevtoolsCollector): Promise<DevtoolsDisposer> {\n let cache: {\n on(event: (typeof EVENT_TYPES)[number], handler: (data: CacheEventData) => void): unknown;\n off(event: (typeof EVENT_TYPES)[number], handler: (data: CacheEventData) => void): unknown;\n };\n\n try {\n ({ cache } = await import(\"@warlock.js/cache\"));\n } catch {\n return () => {};\n }\n\n const handlers = EVENT_TYPES.map((type) => {\n const handler = (data: CacheEventData) => {\n const operation: DevtoolsCacheOperation = {\n type,\n driver: data.driver,\n key: data.key,\n tags: data.tags,\n keys: data.keys,\n at: Date.now(),\n durationMs: data.durationMs,\n };\n\n collector.addCacheOperation(currentRequestId(), operation);\n };\n\n cache.on(type, handler);\n\n return { type, handler };\n });\n\n return () => {\n for (const { type, handler } of handlers) cache.off(type, handler);\n };\n}\n"],"mappings":";;;;AAIA,MAAM,cAAc;CAAC;CAAO;CAAQ;CAAO;CAAW;CAAe;AAAS;;;;;;AAgB9E,eAAsB,kBAAkB,WAAyD;CAC/F,IAAI;CAKJ,IAAI;EACF,CAAC,CAAE,SAAU,MAAM,OAAO;CAC5B,QAAQ;EACN,aAAa,CAAC;CAChB;CAEA,MAAM,WAAW,YAAY,KAAK,SAAS;EACzC,MAAM,WAAW,SAAyB;GACxC,MAAM,YAAoC;IACxC;IACA,QAAQ,KAAK;IACb,KAAK,KAAK;IACV,MAAM,KAAK;IACX,MAAM,KAAK;IACX,IAAI,KAAK,IAAI;IACb,YAAY,KAAK;GACnB;GAEA,UAAU,kBAAkB,iBAAiB,GAAG,SAAS;EAC3D;EAEA,MAAM,GAAG,MAAM,OAAO;EAEtB,OAAO;GAAE;GAAM;EAAQ;CACzB,CAAC;CAED,aAAa;EACX,KAAK,MAAM,EAAE,MAAM,aAAa,UAAU,MAAM,IAAI,MAAM,OAAO;CACnE;AACF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { currentRequestId } from "../current-request-id.mjs";
|
|
2
|
+
import { toSafeValue } from "../safe-value.mjs";
|
|
3
|
+
import { LogChannel, log } from "@warlock.js/logger";
|
|
4
|
+
|
|
5
|
+
//#region ../devtools/src/sources/log-source.ts
|
|
6
|
+
/** `LoggingData.type` includes `fatal`, which devtools folds into `error`. */
|
|
7
|
+
function toDevtoolsLevel(type) {
|
|
8
|
+
return type === "fatal" ? "error" : type;
|
|
9
|
+
}
|
|
10
|
+
var DevtoolsLogChannel = class extends LogChannel {
|
|
11
|
+
constructor(collector) {
|
|
12
|
+
super();
|
|
13
|
+
this.collector = collector;
|
|
14
|
+
this.name = "devtools";
|
|
15
|
+
this.active = true;
|
|
16
|
+
}
|
|
17
|
+
log(data) {
|
|
18
|
+
if (!this.active) return;
|
|
19
|
+
const entry = {
|
|
20
|
+
level: toDevtoolsLevel(data.type),
|
|
21
|
+
module: data.module,
|
|
22
|
+
action: data.action,
|
|
23
|
+
message: data.message,
|
|
24
|
+
at: Date.now(),
|
|
25
|
+
context: toSafeValue(data.context)
|
|
26
|
+
};
|
|
27
|
+
this.collector.addLog(currentRequestId(), entry);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Feeds every log entry, from `@warlock.js/logger`, into the collector by
|
|
32
|
+
* registering an additional channel. `Logger` has no way to remove a
|
|
33
|
+
* channel, so the disposer flag-ignores further entries instead — the
|
|
34
|
+
* channel stays registered for the process's lifetime.
|
|
35
|
+
*/
|
|
36
|
+
async function attachLogSource(collector) {
|
|
37
|
+
const channel = new DevtoolsLogChannel(collector);
|
|
38
|
+
log.addChannel(channel);
|
|
39
|
+
return () => {
|
|
40
|
+
channel.active = false;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
export { attachLogSource };
|
|
46
|
+
//# sourceMappingURL=log-source.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-source.mjs","names":[],"sources":["../../../../../../../devtools/src/sources/log-source.ts"],"sourcesContent":["import { LogChannel, log, type LoggingData } from \"@warlock.js/logger\";\nimport { currentRequestId } from \"../current-request-id\";\nimport { toSafeValue } from \"../safe-value\";\nimport type { DevtoolsCollector, DevtoolsDisposer, DevtoolsLogEntry } from \"../types\";\n\n/** `LoggingData.type` includes `fatal`, which devtools folds into `error`. */\nfunction toDevtoolsLevel(type: LoggingData[\"type\"]): DevtoolsLogEntry[\"level\"] {\n return type === \"fatal\" ? \"error\" : type;\n}\n\nclass DevtoolsLogChannel extends LogChannel {\n public override name = \"devtools\";\n public active = true;\n\n public constructor(private readonly collector: DevtoolsCollector) {\n super();\n }\n\n public log(data: LoggingData): void {\n if (!this.active) return;\n\n // Already passed through the logger's redaction floor (`Logger.log`\n // applies it before invoking any channel). It is still app data, and can\n // hold live objects (a request, its socket), so a JSON-safe snapshot is\n // kept instead of the object itself.\n const entry: DevtoolsLogEntry = {\n level: toDevtoolsLevel(data.type),\n module: data.module,\n action: data.action,\n message: data.message,\n at: Date.now(),\n context: toSafeValue(data.context),\n };\n\n this.collector.addLog(currentRequestId(), entry);\n }\n}\n\n/**\n * Feeds every log entry, from `@warlock.js/logger`, into the collector by\n * registering an additional channel. `Logger` has no way to remove a\n * channel, so the disposer flag-ignores further entries instead — the\n * channel stays registered for the process's lifetime.\n */\nexport async function attachLogSource(collector: DevtoolsCollector): Promise<DevtoolsDisposer> {\n const channel = new DevtoolsLogChannel(collector);\n\n log.addChannel(channel);\n\n return () => {\n channel.active = false;\n };\n}\n"],"mappings":";;;;;;AAMA,SAAS,gBAAgB,MAAsD;CAC7E,OAAO,SAAS,UAAU,UAAU;AACtC;AAEA,IAAM,qBAAN,cAAiC,WAAW;CAI1C,AAAO,YAAY,AAAiB,WAA8B;EAChE,MAAM;EAD4B;cAHb;gBACP;CAIhB;CAEA,AAAO,IAAI,MAAyB;EAClC,IAAI,CAAC,KAAK,QAAQ;EAMlB,MAAM,QAA0B;GAC9B,OAAO,gBAAgB,KAAK,IAAI;GAChC,QAAQ,KAAK;GACb,QAAQ,KAAK;GACb,SAAS,KAAK;GACd,IAAI,KAAK,IAAI;GACb,SAAS,YAAY,KAAK,OAAO;EACnC;EAEA,KAAK,UAAU,OAAO,iBAAiB,GAAG,KAAK;CACjD;AACF;;;;;;;AAQA,eAAsB,gBAAgB,WAAyD;CAC7F,MAAM,UAAU,IAAI,mBAAmB,SAAS;CAEhD,IAAI,WAAW,OAAO;CAEtB,aAAa;EACX,QAAQ,SAAS;CACnB;AACF"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { currentRequestId } from "../current-request-id.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../devtools/src/sources/mail-source.ts
|
|
4
|
+
function addressToString(address) {
|
|
5
|
+
if (!address) return void 0;
|
|
6
|
+
return typeof address === "string" ? address : `${address.name} <${address.address}>`;
|
|
7
|
+
}
|
|
8
|
+
function attachmentSize(attachment) {
|
|
9
|
+
if (typeof attachment.content === "string") return Buffer.byteLength(attachment.content);
|
|
10
|
+
if (Buffer.isBuffer(attachment.content)) return attachment.content.length;
|
|
11
|
+
}
|
|
12
|
+
function toDevtoolsAttachment(attachment) {
|
|
13
|
+
return {
|
|
14
|
+
filename: attachment.filename,
|
|
15
|
+
contentType: attachment.contentType,
|
|
16
|
+
size: attachmentSize(attachment)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Feeds mails captured in development mode, from core's `mailEvents`, into
|
|
21
|
+
* the collector. The captured attachments carry their content, but only
|
|
22
|
+
* metadata is stored — the bytes are never kept.
|
|
23
|
+
*/
|
|
24
|
+
async function attachMailSource(collector) {
|
|
25
|
+
const { mailEvents } = await import("@warlock.js/core");
|
|
26
|
+
const subscription = mailEvents.onCaptured((mail) => {
|
|
27
|
+
const { normalized } = mail;
|
|
28
|
+
collector.addMail({
|
|
29
|
+
id: mail.id,
|
|
30
|
+
requestId: currentRequestId(),
|
|
31
|
+
capturedAt: mail.timestamp.getTime(),
|
|
32
|
+
from: addressToString(normalized.from),
|
|
33
|
+
to: normalized.to,
|
|
34
|
+
cc: normalized.cc,
|
|
35
|
+
bcc: normalized.bcc,
|
|
36
|
+
subject: normalized.subject,
|
|
37
|
+
html: normalized.html,
|
|
38
|
+
text: normalized.text,
|
|
39
|
+
headers: normalized.headers,
|
|
40
|
+
attachments: normalized.attachments.map(toDevtoolsAttachment)
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
return () => subscription.unsubscribe();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { attachMailSource };
|
|
48
|
+
//# sourceMappingURL=mail-source.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail-source.mjs","names":[],"sources":["../../../../../../../devtools/src/sources/mail-source.ts"],"sourcesContent":["import type { CapturedMail, MailAddress, MailAttachment } from \"@warlock.js/core\";\nimport { currentRequestId } from \"../current-request-id\";\nimport type { DevtoolsCollector, DevtoolsDisposer, DevtoolsMailAttachment } from \"../types\";\n\nfunction addressToString(address: MailAddress | undefined): string | undefined {\n if (!address) return undefined;\n\n return typeof address === \"string\" ? address : `${address.name} <${address.address}>`;\n}\n\nfunction attachmentSize(attachment: MailAttachment): number | undefined {\n if (typeof attachment.content === \"string\") return Buffer.byteLength(attachment.content);\n if (Buffer.isBuffer(attachment.content)) return attachment.content.length;\n\n return undefined;\n}\n\nfunction toDevtoolsAttachment(attachment: MailAttachment): DevtoolsMailAttachment {\n return {\n filename: attachment.filename,\n contentType: attachment.contentType,\n size: attachmentSize(attachment),\n };\n}\n\n/**\n * Feeds mails captured in development mode, from core's `mailEvents`, into\n * the collector. The captured attachments carry their content, but only\n * metadata is stored — the bytes are never kept.\n */\nexport async function attachMailSource(collector: DevtoolsCollector): Promise<DevtoolsDisposer> {\n const { mailEvents } = await import(\"@warlock.js/core\");\n\n const subscription = mailEvents.onCaptured((mail: CapturedMail) => {\n const { normalized } = mail;\n\n collector.addMail({\n id: mail.id,\n requestId: currentRequestId(),\n capturedAt: mail.timestamp.getTime(),\n from: addressToString(normalized.from),\n to: normalized.to,\n cc: normalized.cc,\n bcc: normalized.bcc,\n subject: normalized.subject,\n html: normalized.html,\n text: normalized.text,\n headers: normalized.headers,\n attachments: normalized.attachments.map(toDevtoolsAttachment),\n });\n });\n\n return () => subscription.unsubscribe();\n}\n"],"mappings":";;;AAIA,SAAS,gBAAgB,SAAsD;CAC7E,IAAI,CAAC,SAAS,OAAO;CAErB,OAAO,OAAO,YAAY,WAAW,UAAU,GAAG,QAAQ,KAAK,IAAI,QAAQ,QAAQ;AACrF;AAEA,SAAS,eAAe,YAAgD;CACtE,IAAI,OAAO,WAAW,YAAY,UAAU,OAAO,OAAO,WAAW,WAAW,OAAO;CACvF,IAAI,OAAO,SAAS,WAAW,OAAO,GAAG,OAAO,WAAW,QAAQ;AAGrE;AAEA,SAAS,qBAAqB,YAAoD;CAChF,OAAO;EACL,UAAU,WAAW;EACrB,aAAa,WAAW;EACxB,MAAM,eAAe,UAAU;CACjC;AACF;;;;;;AAOA,eAAsB,iBAAiB,WAAyD;CAC9F,MAAM,EAAE,eAAe,MAAM,OAAO;CAEpC,MAAM,eAAe,WAAW,YAAY,SAAuB;EACjE,MAAM,EAAE,eAAe;EAEvB,UAAU,QAAQ;GAChB,IAAI,KAAK;GACT,WAAW,iBAAiB;GAC5B,YAAY,KAAK,UAAU,QAAQ;GACnC,MAAM,gBAAgB,WAAW,IAAI;GACrC,IAAI,WAAW;GACf,IAAI,WAAW;GACf,KAAK,WAAW;GAChB,SAAS,WAAW;GACpB,MAAM,WAAW;GACjB,MAAM,WAAW;GACjB,SAAS,WAAW;GACpB,aAAa,WAAW,YAAY,IAAI,oBAAoB;EAC9D,CAAC;CACH,CAAC;CAED,aAAa,aAAa,YAAY;AACxC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { currentRequestId } from "../current-request-id.mjs";
|
|
2
|
+
import { toSafeValue } from "../safe-value.mjs";
|
|
3
|
+
import { captureCallerLocation } from "../caller-location.mjs";
|
|
4
|
+
import { applyRedact, log } from "@warlock.js/logger";
|
|
5
|
+
|
|
6
|
+
//#region ../devtools/src/sources/query-source.ts
|
|
7
|
+
/** Redacts query bindings through the logger's redaction rules. */
|
|
8
|
+
function redactBindings(bindings) {
|
|
9
|
+
if (!bindings) return bindings;
|
|
10
|
+
return applyRedact({
|
|
11
|
+
type: "debug",
|
|
12
|
+
module: "devtools",
|
|
13
|
+
action: "query",
|
|
14
|
+
message: void 0,
|
|
15
|
+
context: { bindings }
|
|
16
|
+
}, log.getRedact()).context.bindings;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Feeds every settled database query, from cascade's data source registry,
|
|
20
|
+
* into the collector. A no-op when `@warlock.js/cascade` isn't installed —
|
|
21
|
+
* it is an optional peer.
|
|
22
|
+
*/
|
|
23
|
+
async function attachQuerySource(collector) {
|
|
24
|
+
let dataSourceRegistry;
|
|
25
|
+
try {
|
|
26
|
+
({dataSourceRegistry} = await import("@warlock.js/cascade"));
|
|
27
|
+
} catch {
|
|
28
|
+
return () => {};
|
|
29
|
+
}
|
|
30
|
+
const listener = (event) => {
|
|
31
|
+
collector.addQuery(currentRequestId(), {
|
|
32
|
+
connection: event.connection,
|
|
33
|
+
driver: event.driver,
|
|
34
|
+
sql: event.sql,
|
|
35
|
+
bindings: toSafeValue(redactBindings(event.bindings)),
|
|
36
|
+
collection: event.collection,
|
|
37
|
+
command: event.command,
|
|
38
|
+
pipeline: event.pipeline,
|
|
39
|
+
startedAt: event.startedAt,
|
|
40
|
+
durationMs: event.durationMs,
|
|
41
|
+
rowCount: event.rowCount,
|
|
42
|
+
error: event.error === void 0 ? void 0 : event.error instanceof Error ? event.error.message : String(event.error),
|
|
43
|
+
caller: captureCallerLocation()
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
dataSourceRegistry.on("query", listener);
|
|
47
|
+
return () => dataSourceRegistry.off("query", listener);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//#endregion
|
|
51
|
+
export { attachQuerySource };
|
|
52
|
+
//# sourceMappingURL=query-source.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-source.mjs","names":[],"sources":["../../../../../../../devtools/src/sources/query-source.ts"],"sourcesContent":["import { applyRedact, log } from \"@warlock.js/logger\";\nimport { captureCallerLocation } from \"../caller-location\";\nimport { currentRequestId } from \"../current-request-id\";\nimport { toSafeValue } from \"../safe-value\";\nimport type { DevtoolsCollector, DevtoolsDisposer } from \"../types\";\n\n/** The shape of the event `dataSourceRegistry` emits for `\"query\"`. */\ntype QueryEvent = {\n connection: string;\n driver: \"postgres\" | \"mongodb\";\n sql?: string;\n bindings?: unknown[];\n collection?: string;\n command?: string;\n pipeline?: unknown;\n durationMs: number;\n startedAt: number;\n rowCount?: number;\n error?: unknown;\n};\n\n/** Redacts query bindings through the logger's redaction rules. */\nfunction redactBindings(bindings: unknown[] | undefined): unknown[] | undefined {\n if (!bindings) return bindings;\n\n const redacted = applyRedact(\n {\n type: \"debug\",\n module: \"devtools\",\n action: \"query\",\n message: undefined,\n context: { bindings },\n },\n log.getRedact(),\n );\n\n return (redacted.context as { bindings: unknown[] }).bindings;\n}\n\n/**\n * Feeds every settled database query, from cascade's data source registry,\n * into the collector. A no-op when `@warlock.js/cascade` isn't installed —\n * it is an optional peer.\n */\nexport async function attachQuerySource(collector: DevtoolsCollector): Promise<DevtoolsDisposer> {\n let dataSourceRegistry: {\n on(event: \"query\", listener: (event: QueryEvent) => void): void;\n off(event: \"query\", listener: (event: QueryEvent) => void): void;\n };\n\n try {\n ({ dataSourceRegistry } = await import(\"@warlock.js/cascade\"));\n } catch {\n return () => {};\n }\n\n const listener = (event: QueryEvent) => {\n collector.addQuery(currentRequestId(), {\n connection: event.connection,\n driver: event.driver,\n sql: event.sql,\n bindings: toSafeValue(redactBindings(event.bindings)) as unknown[] | undefined,\n collection: event.collection,\n command: event.command,\n pipeline: event.pipeline,\n startedAt: event.startedAt,\n durationMs: event.durationMs,\n rowCount: event.rowCount,\n error:\n event.error === undefined\n ? undefined\n : event.error instanceof Error\n ? event.error.message\n : String(event.error),\n caller: captureCallerLocation(),\n });\n };\n\n dataSourceRegistry.on(\"query\", listener);\n\n return () => dataSourceRegistry.off(\"query\", listener);\n}\n"],"mappings":";;;;;;;AAsBA,SAAS,eAAe,UAAwD;CAC9E,IAAI,CAAC,UAAU,OAAO;CAatB,OAXiB,YACf;EACE,MAAM;EACN,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,SAAS,EAAE,SAAS;CACtB,GACA,IAAI,UAAU,CAGD,CAAC,CAAC,QAAoC;AACvD;;;;;;AAOA,eAAsB,kBAAkB,WAAyD;CAC/F,IAAI;CAKJ,IAAI;EACF,CAAC,CAAE,sBAAuB,MAAM,OAAO;CACzC,QAAQ;EACN,aAAa,CAAC;CAChB;CAEA,MAAM,YAAY,UAAsB;EACtC,UAAU,SAAS,iBAAiB,GAAG;GACrC,YAAY,MAAM;GAClB,QAAQ,MAAM;GACd,KAAK,MAAM;GACX,UAAU,YAAY,eAAe,MAAM,QAAQ,CAAC;GACpD,YAAY,MAAM;GAClB,SAAS,MAAM;GACf,UAAU,MAAM;GAChB,WAAW,MAAM;GACjB,YAAY,MAAM;GAClB,UAAU,MAAM;GAChB,OACE,MAAM,UAAU,SACZ,SACA,MAAM,iBAAiB,QACrB,MAAM,MAAM,UACZ,OAAO,MAAM,KAAK;GAC1B,QAAQ,sBAAsB;EAChC,CAAC;CACH;CAEA,mBAAmB,GAAG,SAAS,QAAQ;CAEvC,aAAa,mBAAmB,IAAI,SAAS,QAAQ;AACvD"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { toSafeValue } from "../safe-value.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../devtools/src/sources/tracing-source.ts
|
|
4
|
+
/**
|
|
5
|
+
* Feeds request timelines from core's tracing hooks into the collector: one
|
|
6
|
+
* `startRequest`/`endRequest` pair per HTTP request, with every named phase
|
|
7
|
+
* in between. Requests to the dashboard's own path are never recorded, so
|
|
8
|
+
* the dashboard never watches itself.
|
|
9
|
+
*/
|
|
10
|
+
async function attachTracingSource(collector, options) {
|
|
11
|
+
const { registerTracingHooks } = await import("@warlock.js/core");
|
|
12
|
+
const isDashboardPath = (path) => path.startsWith(options.path);
|
|
13
|
+
return registerTracingHooks({
|
|
14
|
+
onRequestStart(ctx) {
|
|
15
|
+
if (isDashboardPath(ctx.path)) return;
|
|
16
|
+
collector.startRequest({
|
|
17
|
+
id: ctx.requestId,
|
|
18
|
+
method: ctx.method,
|
|
19
|
+
path: ctx.path,
|
|
20
|
+
route: ctx.route,
|
|
21
|
+
startedAt: performance.timeOrigin + performance.now()
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
onPhase(ctx, phase) {
|
|
25
|
+
if (isDashboardPath(ctx.path)) return;
|
|
26
|
+
collector.addPhase(ctx.requestId, {
|
|
27
|
+
name: phase.name,
|
|
28
|
+
durationMs: phase.durationMs,
|
|
29
|
+
startedAt: phase.startedAt ?? performance.timeOrigin + performance.now() - phase.durationMs,
|
|
30
|
+
attrs: phase.attrs === void 0 ? void 0 : toSafeValue(phase.attrs)
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
onRequestEnd(ctx, result) {
|
|
34
|
+
if (isDashboardPath(ctx.path)) return;
|
|
35
|
+
collector.endRequest(ctx.requestId, {
|
|
36
|
+
status: result.status,
|
|
37
|
+
durationMs: result.durationMs,
|
|
38
|
+
error: result.error === void 0 ? void 0 : result.error instanceof Error ? result.error.message : String(result.error),
|
|
39
|
+
route: ctx.route
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
export { attachTracingSource };
|
|
47
|
+
//# sourceMappingURL=tracing-source.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracing-source.mjs","names":[],"sources":["../../../../../../../devtools/src/sources/tracing-source.ts"],"sourcesContent":["import type { TracingContext, TracingPhaseInfo, TracingRequestEndInfo } from \"@warlock.js/core\";\nimport { toSafeValue } from \"../safe-value\";\nimport type { DevtoolsCollector, DevtoolsDisposer, ResolvedDevtoolsOptions } from \"../types\";\n\n/**\n * Feeds request timelines from core's tracing hooks into the collector: one\n * `startRequest`/`endRequest` pair per HTTP request, with every named phase\n * in between. Requests to the dashboard's own path are never recorded, so\n * the dashboard never watches itself.\n */\nexport async function attachTracingSource(\n collector: DevtoolsCollector,\n options: ResolvedDevtoolsOptions,\n): Promise<DevtoolsDisposer> {\n const { registerTracingHooks } = await import(\"@warlock.js/core\");\n\n const isDashboardPath = (path: string): boolean => path.startsWith(options.path);\n\n return registerTracingHooks({\n onRequestStart(ctx: TracingContext) {\n if (isDashboardPath(ctx.path)) return;\n\n collector.startRequest({\n id: ctx.requestId,\n method: ctx.method,\n path: ctx.path,\n route: ctx.route,\n startedAt: performance.timeOrigin + performance.now(),\n });\n },\n onPhase(ctx: TracingContext, phase: TracingPhaseInfo) {\n if (isDashboardPath(ctx.path)) return;\n\n collector.addPhase(ctx.requestId, {\n name: phase.name,\n durationMs: phase.durationMs,\n startedAt: phase.startedAt ?? performance.timeOrigin + performance.now() - phase.durationMs,\n attrs: phase.attrs === undefined ? undefined : (toSafeValue(phase.attrs) as Record<string, unknown>),\n });\n },\n onRequestEnd(ctx: TracingContext, result: TracingRequestEndInfo) {\n if (isDashboardPath(ctx.path)) return;\n\n collector.endRequest(ctx.requestId, {\n status: result.status,\n durationMs: result.durationMs,\n error:\n result.error === undefined\n ? undefined\n : result.error instanceof Error\n ? result.error.message\n : String(result.error),\n route: ctx.route,\n });\n },\n });\n}\n"],"mappings":";;;;;;;;;AAUA,eAAsB,oBACpB,WACA,SAC2B;CAC3B,MAAM,EAAE,yBAAyB,MAAM,OAAO;CAE9C,MAAM,mBAAmB,SAA0B,KAAK,WAAW,QAAQ,IAAI;CAE/E,OAAO,qBAAqB;EAC1B,eAAe,KAAqB;GAClC,IAAI,gBAAgB,IAAI,IAAI,GAAG;GAE/B,UAAU,aAAa;IACrB,IAAI,IAAI;IACR,QAAQ,IAAI;IACZ,MAAM,IAAI;IACV,OAAO,IAAI;IACX,WAAW,YAAY,aAAa,YAAY,IAAI;GACtD,CAAC;EACH;EACA,QAAQ,KAAqB,OAAyB;GACpD,IAAI,gBAAgB,IAAI,IAAI,GAAG;GAE/B,UAAU,SAAS,IAAI,WAAW;IAChC,MAAM,MAAM;IACZ,YAAY,MAAM;IAClB,WAAW,MAAM,aAAa,YAAY,aAAa,YAAY,IAAI,IAAI,MAAM;IACjF,OAAO,MAAM,UAAU,SAAY,SAAa,YAAY,MAAM,KAAK;GACzE,CAAC;EACH;EACA,aAAa,KAAqB,QAA+B;GAC/D,IAAI,gBAAgB,IAAI,IAAI,GAAG;GAE/B,UAAU,WAAW,IAAI,WAAW;IAClC,QAAQ,OAAO;IACf,YAAY,OAAO;IACnB,OACE,OAAO,UAAU,SACb,SACA,OAAO,iBAAiB,QACtB,OAAO,MAAM,UACb,OAAO,OAAO,KAAK;IAC3B,OAAO,IAAI;GACb,CAAC;EACH;CACF,CAAC;AACH"}
|