@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -0
  4. package/cjs/index.cjs +694 -0
  5. package/cjs/index.cjs.map +1 -0
  6. package/cjs/server-DHYSVive.cjs +286 -0
  7. package/cjs/server-DHYSVive.cjs.map +1 -0
  8. package/esm/caller-location.mjs +33 -0
  9. package/esm/caller-location.mjs.map +1 -0
  10. package/esm/collector.mjs +185 -0
  11. package/esm/collector.mjs.map +1 -0
  12. package/esm/current-request-id.mjs +24 -0
  13. package/esm/current-request-id.mjs.map +1 -0
  14. package/esm/devtools-connector.d.mts +26 -0
  15. package/esm/devtools-connector.d.mts.map +1 -0
  16. package/esm/devtools-connector.mjs +112 -0
  17. package/esm/devtools-connector.mjs.map +1 -0
  18. package/esm/index.d.mts +3 -0
  19. package/esm/index.mjs +3 -0
  20. package/esm/n-plus-one.mjs +36 -0
  21. package/esm/n-plus-one.mjs.map +1 -0
  22. package/esm/options.mjs +19 -0
  23. package/esm/options.mjs.map +1 -0
  24. package/esm/query-shape.mjs +27 -0
  25. package/esm/query-shape.mjs.map +1 -0
  26. package/esm/safe-value.mjs +63 -0
  27. package/esm/safe-value.mjs.map +1 -0
  28. package/esm/server/background-routes.mjs +9 -0
  29. package/esm/server/background-routes.mjs.map +1 -0
  30. package/esm/server/explain-route.mjs +37 -0
  31. package/esm/server/explain-route.mjs.map +1 -0
  32. package/esm/server/guard.mjs +30 -0
  33. package/esm/server/guard.mjs.map +1 -0
  34. package/esm/server/index.mjs +37 -0
  35. package/esm/server/index.mjs.map +1 -0
  36. package/esm/server/log-routes.mjs +12 -0
  37. package/esm/server/log-routes.mjs.map +1 -0
  38. package/esm/server/mail-routes.mjs +27 -0
  39. package/esm/server/mail-routes.mjs.map +1 -0
  40. package/esm/server/requests-routes.mjs +21 -0
  41. package/esm/server/requests-routes.mjs.map +1 -0
  42. package/esm/server/route-routes.mjs +9 -0
  43. package/esm/server/route-routes.mjs.map +1 -0
  44. package/esm/server/stream-route.mjs +32 -0
  45. package/esm/server/stream-route.mjs.map +1 -0
  46. package/esm/server/ui-assets.mjs +38 -0
  47. package/esm/server/ui-assets.mjs.map +1 -0
  48. package/esm/sources/cache-source.mjs +51 -0
  49. package/esm/sources/cache-source.mjs.map +1 -0
  50. package/esm/sources/log-source.mjs +46 -0
  51. package/esm/sources/log-source.mjs.map +1 -0
  52. package/esm/sources/mail-source.mjs +48 -0
  53. package/esm/sources/mail-source.mjs.map +1 -0
  54. package/esm/sources/query-source.mjs +52 -0
  55. package/esm/sources/query-source.mjs.map +1 -0
  56. package/esm/sources/tracing-source.mjs +47 -0
  57. package/esm/sources/tracing-source.mjs.map +1 -0
  58. package/esm/types.d.mts +213 -0
  59. package/esm/types.d.mts.map +1 -0
  60. package/esm/ui-root.mjs +40 -0
  61. package/esm/ui-root.mjs.map +1 -0
  62. package/llms-full.txt +78 -0
  63. package/llms.txt +9 -0
  64. package/package.json +52 -0
  65. package/skills/devtools-overview/SKILL.md +68 -0
  66. package/ui/app.css +354 -0
  67. package/ui/app.js +520 -0
  68. 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"}