@workflow/web 5.0.0-beta.5 → 5.0.0-beta.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/build/client/assets/app-9JVnf-IU.css +1 -0
- package/build/client/assets/arrow-up-right-CSovqivK.js +15 -0
- package/build/client/assets/{entry.client-DtOpLtMQ.js → entry.client-LwI3HNYl.js} +25 -25
- package/build/client/assets/{highlighted-body-B3W2YXNL-L1CrwoL_.js → highlighted-body-B3W2YXNL-BO5_Ufo-.js} +2 -2
- package/build/client/assets/{home-B6UmDDP6.js → home-BbyW_T74.js} +1762 -291
- package/build/client/assets/index-HqNj_XIX.js +200 -0
- package/build/client/assets/{index-BsV8i_Jn.js → index-PFjW8YjQ.js} +26 -26
- package/build/client/assets/loader-circle-BX6ycuOI.js +12 -0
- package/build/client/assets/manifest-34c486ef.js +1 -0
- package/build/client/assets/{mermaid-3ZIDBTTL-BQLU7GUr.js → mermaid-3ZIDBTTL-BKwl_LgA.js} +6469 -4784
- package/build/client/assets/mermaid-3ZIDBTTL-D8CjRQta.css +1 -0
- package/build/client/assets/root-Bl5xphvU.css +1 -0
- package/build/client/assets/{root-Yk3JVwUh.js → root-qx_45_fH.js} +17 -42
- package/build/client/assets/{run-detail-DMUaWXhb.js → run-detail-BoGFG1Fe.js} +2208 -1597
- package/build/client/assets/run-detail-CWoGxu_0.css +1 -0
- package/build/client/assets/{workflow-graph-viewer-yqeN-U_2.js → workflow-graph-viewer-C-1D0sAn.js} +5111 -5254
- package/build/client/assets/zstd-B5R2yJjB.wasm +0 -0
- package/build/client/assets/zstd-browser-decoder-By3hr1F0.js +27 -0
- package/build/server/assets/{server-build-DtcOUEHS.js → app-CBtZR8rR.js} +66453 -43314
- package/build/server/assets/{highlighted-body-B3W2YXNL-CZjJIpdD.js → highlighted-body-B3W2YXNL-BjGiR49i.js} +15 -8
- package/build/server/assets/index-BYoi2wsy.js +1262 -0
- package/build/server/assets/index-BfLBLRRi.js +204 -0
- package/build/server/assets/index-CETb4fkq.js +1164 -0
- package/build/server/assets/index-glWWjG5N.js +173 -0
- package/build/server/assets/{mermaid-3ZIDBTTL-Cc8IPTBj.js → mermaid-3ZIDBTTL-DRi-PBW7.js} +16 -9
- package/build/server/assets/{token-DHnArVm7.js → token-RGgCwymE.js} +5 -2
- package/build/server/assets/{token-util-B3uKeZ0s.js → token-util-wecZ3LiF.js} +5 -2
- package/build/server/assets/websocket-server-DwbEw-Dm.js +3669 -0
- package/build/server/assets/wrapper-B4hIt4qi.js +68 -0
- package/build/server/assets/ws-transport-Dj_kuAkL.js +550 -0
- package/build/server/assets/zstd-browser-decoder-Coywki2m.js +71 -0
- package/build/server/index.js +49 -2
- package/handler.d.ts +22 -0
- package/handler.js +186 -0
- package/package.json +27 -13
- package/registry.d.ts +29 -0
- package/registry.js +89 -0
- package/build/client/assets/encryption-CEh0YXii.js +0 -28
- package/build/client/assets/index-B4xuge0I.js +0 -249
- package/build/client/assets/manifest-e4c1c787.js +0 -1
- package/build/client/assets/mermaid-3ZIDBTTL-DKxHcEOp.css +0 -1
- package/build/client/assets/root-C0Tu5TVN.css +0 -1
- package/build/client/assets/server-build-DqaxIrF0.css +0 -1
- package/build/client/assets/worker-CqHKTA7D.js +0 -115
- package/build/server/assets/app-L1V62FgT.js +0 -9213
- package/build/server/assets/index-BwtNigf1.js +0 -80
- package/build/server/assets/index-meg9qk8y.js +0 -1282
package/build/server/index.js
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRequire as __wkfCreateRequire } from "node:module";
|
|
2
|
+
if (typeof require === "undefined") {
|
|
3
|
+
globalThis.require = __wkfCreateRequire(import.meta.url);
|
|
4
|
+
}
|
|
5
|
+
import { bu, bv } from "./assets/app-CBtZR8rR.js";
|
|
2
6
|
import "express";
|
|
7
|
+
import "node:stream";
|
|
3
8
|
import "stream";
|
|
9
|
+
import "util";
|
|
10
|
+
import "crypto";
|
|
11
|
+
import "async_hooks";
|
|
12
|
+
import "node:crypto";
|
|
13
|
+
import "node:process";
|
|
14
|
+
import "node:path";
|
|
15
|
+
import "node:url";
|
|
16
|
+
import "module";
|
|
17
|
+
import "node:fs/promises";
|
|
18
|
+
import "node:module";
|
|
19
|
+
import "node:util";
|
|
20
|
+
import "node:async_hooks";
|
|
21
|
+
import "node:fs";
|
|
22
|
+
import "node:os";
|
|
23
|
+
import "node:child_process";
|
|
24
|
+
import "node:timers/promises";
|
|
25
|
+
import "node:buffer";
|
|
26
|
+
import "node:http";
|
|
27
|
+
import "node:https";
|
|
28
|
+
import "node:zlib";
|
|
29
|
+
import "events";
|
|
30
|
+
import "node:assert";
|
|
31
|
+
import "node:net";
|
|
32
|
+
import "node:querystring";
|
|
33
|
+
import "node:events";
|
|
34
|
+
import "node:diagnostics_channel";
|
|
35
|
+
import "node:tls";
|
|
36
|
+
import "node:perf_hooks";
|
|
37
|
+
import "node:util/types";
|
|
38
|
+
import "node:sqlite";
|
|
39
|
+
import "node:worker_threads";
|
|
40
|
+
import "node:console";
|
|
41
|
+
import "node:timers";
|
|
42
|
+
import "node:dns";
|
|
43
|
+
import "path";
|
|
44
|
+
import "fs";
|
|
45
|
+
import "constants";
|
|
46
|
+
import "assert";
|
|
47
|
+
import "os";
|
|
48
|
+
import "net";
|
|
49
|
+
import "node:assert/strict";
|
|
4
50
|
export {
|
|
5
|
-
|
|
51
|
+
bu as app,
|
|
52
|
+
bv as createFetchHandler
|
|
6
53
|
};
|
package/handler.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for {@link createWorkflowWebHandler}.
|
|
3
|
+
*/
|
|
4
|
+
export interface CreateWorkflowWebHandlerOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Mount path the dashboard is served under (e.g. `"/_workflow"`). Routing and
|
|
7
|
+
* asset URLs are reprefixed to this base. Defaults to `"/"` (root).
|
|
8
|
+
*/
|
|
9
|
+
basename?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a framework-agnostic Web `Request` -> `Response` handler that serves
|
|
14
|
+
* the Workflow observability UI (static client assets + React Router SSR)
|
|
15
|
+
* in-process, suitable for mounting inside another server under `basename`.
|
|
16
|
+
*
|
|
17
|
+
* Requires `@workflow/web` to have been built (`build/` must exist); throws
|
|
18
|
+
* otherwise.
|
|
19
|
+
*/
|
|
20
|
+
export function createWorkflowWebHandler(
|
|
21
|
+
options?: CreateWorkflowWebHandlerOptions
|
|
22
|
+
): Promise<(request: Request) => Promise<Response>>;
|
package/handler.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic, in-process entry point for @workflow/web.
|
|
3
|
+
*
|
|
4
|
+
* Unlike `server.js` (which starts a standalone Express HTTP server), this
|
|
5
|
+
* exports a single Web-standard fetch handler that another server can mount
|
|
6
|
+
* under an arbitrary base path — e.g. `@workflow/nitro` mounting the dashboard
|
|
7
|
+
* at `/_workflow` without spawning a second server/port.
|
|
8
|
+
*
|
|
9
|
+
* import { createWorkflowWebHandler } from "@workflow/web/handler";
|
|
10
|
+
* const handler = await createWorkflowWebHandler({ basename: "/_workflow" });
|
|
11
|
+
* const response = await handler(request); // (request: Request) => Response
|
|
12
|
+
*
|
|
13
|
+
* The handler serves both the prebuilt static client assets (from `build/client`)
|
|
14
|
+
* and the React Router SSR app. It reads the prebuilt `build/` as-is in every
|
|
15
|
+
* mode (it never runs Vite), so consumers must have built @workflow/web first
|
|
16
|
+
* (the published package ships `build/`).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync } from 'node:fs';
|
|
20
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
23
|
+
import { recordDashboard } from './registry.js';
|
|
24
|
+
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const buildDir = path.resolve(__dirname, 'build');
|
|
27
|
+
const clientDir = path.join(buildDir, 'client');
|
|
28
|
+
const serverEntry = path.join(buildDir, 'server', 'index.js');
|
|
29
|
+
|
|
30
|
+
// Minimal extension -> MIME map for serving the static client build. A wrong
|
|
31
|
+
// Content-Type on the entry module breaks the whole app, so be explicit.
|
|
32
|
+
const MIME_TYPES = {
|
|
33
|
+
'.js': 'text/javascript',
|
|
34
|
+
'.mjs': 'text/javascript',
|
|
35
|
+
'.css': 'text/css',
|
|
36
|
+
'.json': 'application/json',
|
|
37
|
+
'.map': 'application/json',
|
|
38
|
+
'.html': 'text/html',
|
|
39
|
+
'.ico': 'image/x-icon',
|
|
40
|
+
'.svg': 'image/svg+xml',
|
|
41
|
+
'.png': 'image/png',
|
|
42
|
+
'.jpg': 'image/jpeg',
|
|
43
|
+
'.jpeg': 'image/jpeg',
|
|
44
|
+
'.gif': 'image/gif',
|
|
45
|
+
'.webp': 'image/webp',
|
|
46
|
+
'.avif': 'image/avif',
|
|
47
|
+
'.woff': 'font/woff',
|
|
48
|
+
'.woff2': 'font/woff2',
|
|
49
|
+
'.ttf': 'font/ttf',
|
|
50
|
+
'.otf': 'font/otf',
|
|
51
|
+
'.txt': 'text/plain',
|
|
52
|
+
'.wasm': 'application/wasm',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Normalize a mount path: `/` or empty -> "" (root); otherwise strip trailing slash. */
|
|
56
|
+
function normalizeBasename(basename) {
|
|
57
|
+
if (!basename || basename === '/') return '';
|
|
58
|
+
return basename.endsWith('/') ? basename.slice(0, -1) : basename;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// One handler per basename, lazily constructed and memoized so repeated
|
|
62
|
+
// requests reuse the same React Router handler and module import.
|
|
63
|
+
const handlerCache = new Map();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {{ basename?: string }} [options]
|
|
67
|
+
* @returns {Promise<(request: Request) => Promise<Response>>}
|
|
68
|
+
*/
|
|
69
|
+
export async function createWorkflowWebHandler(options = {}) {
|
|
70
|
+
const basename = normalizeBasename(options.basename ?? '/');
|
|
71
|
+
let promise = handlerCache.get(basename);
|
|
72
|
+
if (!promise) {
|
|
73
|
+
promise = buildHandler(basename).catch((err) => {
|
|
74
|
+
handlerCache.delete(basename);
|
|
75
|
+
throw err;
|
|
76
|
+
});
|
|
77
|
+
handlerCache.set(basename, promise);
|
|
78
|
+
}
|
|
79
|
+
return promise;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function buildHandler(basename) {
|
|
83
|
+
if (!existsSync(serverEntry) || !existsSync(clientDir)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
'@workflow/web has not been built (missing build/). ' +
|
|
86
|
+
'Run `pnpm --filter @workflow/web build` before embedding the dashboard.'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const mod = await import(pathToFileURL(serverEntry).href);
|
|
91
|
+
if (typeof mod.createFetchHandler !== 'function') {
|
|
92
|
+
throw new Error(
|
|
93
|
+
'@workflow/web build does not export createFetchHandler; rebuild the package.'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const ssr = mod.createFetchHandler(basename || '/');
|
|
97
|
+
|
|
98
|
+
return async (request) => {
|
|
99
|
+
// Advertise this dashboard so the CLI can defer to it (best-effort, once).
|
|
100
|
+
// Done on first request because that's when the public origin is known.
|
|
101
|
+
try {
|
|
102
|
+
const origin = new URL(request.url).origin;
|
|
103
|
+
recordDashboard({
|
|
104
|
+
url: origin + basename,
|
|
105
|
+
basename,
|
|
106
|
+
world:
|
|
107
|
+
process.env.WORKFLOW_TARGET_WORLD ||
|
|
108
|
+
(process.env.VERCEL_DEPLOYMENT_ID ? 'vercel' : 'local'),
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
// ignore — registration must never affect request handling
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const staticResponse = await tryServeStatic(request, basename);
|
|
115
|
+
if (staticResponse) return staticResponse;
|
|
116
|
+
return ssr(request);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve a request to a path-within-the-client-build (relative to `clientDir`),
|
|
122
|
+
* stripping `basename` and guarding against traversal. Returns null when the
|
|
123
|
+
* request can't map to a client file (so it should fall through to the SSR
|
|
124
|
+
* handler) — e.g. the index, a directory, or an out-of-tree path.
|
|
125
|
+
*/
|
|
126
|
+
function resolveClientFile(request, basename) {
|
|
127
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') return null;
|
|
128
|
+
|
|
129
|
+
let pathname;
|
|
130
|
+
try {
|
|
131
|
+
pathname = decodeURIComponent(new URL(request.url).pathname);
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (basename) {
|
|
137
|
+
if (!pathname.startsWith(`${basename}/`)) return null; // index/other -> SSR
|
|
138
|
+
pathname = pathname.slice(basename.length);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const relative = pathname.replace(/^\/+/, '');
|
|
142
|
+
if (!relative || relative.endsWith('/')) return null;
|
|
143
|
+
|
|
144
|
+
const filePath = path.join(clientDir, relative);
|
|
145
|
+
// Guard against path traversal escaping the client build directory.
|
|
146
|
+
if (filePath !== clientDir && !filePath.startsWith(clientDir + path.sep)) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
return { filePath, relative };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Serve a file from the prebuilt client bundle if the request maps to one;
|
|
154
|
+
* otherwise return null so the request falls through to the SSR handler.
|
|
155
|
+
*/
|
|
156
|
+
async function tryServeStatic(request, basename) {
|
|
157
|
+
const resolved = resolveClientFile(request, basename);
|
|
158
|
+
if (!resolved) return null;
|
|
159
|
+
const { filePath, relative } = resolved;
|
|
160
|
+
|
|
161
|
+
let stats;
|
|
162
|
+
try {
|
|
163
|
+
stats = await stat(filePath);
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
if (!stats.isFile()) return null;
|
|
168
|
+
|
|
169
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
170
|
+
const headers = new Headers({
|
|
171
|
+
'Content-Type': MIME_TYPES[ext] ?? 'application/octet-stream',
|
|
172
|
+
// Hashed assets are content-addressed and safe to cache forever; other
|
|
173
|
+
// client files (e.g. favicon) get a short cache.
|
|
174
|
+
'Cache-Control': relative.startsWith('assets/')
|
|
175
|
+
? 'public, max-age=31536000, immutable'
|
|
176
|
+
: 'public, max-age=3600',
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (request.method === 'HEAD') {
|
|
180
|
+
headers.set('Content-Length', String(stats.size));
|
|
181
|
+
return new Response(null, { status: 200, headers });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const body = await readFile(filePath);
|
|
185
|
+
return new Response(body, { status: 200, headers });
|
|
186
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workflow/web",
|
|
3
3
|
"description": "Workflow Observability UI",
|
|
4
|
-
"version": "5.0.0-beta.
|
|
4
|
+
"version": "5.0.0-beta.50",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"files": [
|
|
8
8
|
"build",
|
|
9
9
|
"server.js",
|
|
10
10
|
"server.d.ts",
|
|
11
|
+
"handler.js",
|
|
12
|
+
"handler.d.ts",
|
|
13
|
+
"registry.js",
|
|
14
|
+
"registry.d.ts",
|
|
11
15
|
"package.json"
|
|
12
16
|
],
|
|
13
17
|
"exports": {
|
|
@@ -15,6 +19,14 @@
|
|
|
15
19
|
"types": "./server.d.ts",
|
|
16
20
|
"default": "./server.js"
|
|
17
21
|
},
|
|
22
|
+
"./handler": {
|
|
23
|
+
"types": "./handler.d.ts",
|
|
24
|
+
"default": "./handler.js"
|
|
25
|
+
},
|
|
26
|
+
"./registry": {
|
|
27
|
+
"types": "./registry.d.ts",
|
|
28
|
+
"default": "./registry.js"
|
|
29
|
+
},
|
|
18
30
|
"./package.json": "./package.json"
|
|
19
31
|
},
|
|
20
32
|
"publishConfig": {
|
|
@@ -27,10 +39,11 @@
|
|
|
27
39
|
"directory": "packages/web"
|
|
28
40
|
},
|
|
29
41
|
"dependencies": {
|
|
30
|
-
"
|
|
42
|
+
"@workflow/world-local": "5.0.0-beta.43",
|
|
43
|
+
"express": "^5.2.1",
|
|
44
|
+
"swr": "^2.3.6"
|
|
31
45
|
},
|
|
32
46
|
"devDependencies": {
|
|
33
|
-
"@testing-library/react": "^16.3.0",
|
|
34
47
|
"@biomejs/biome": "^2.4.4",
|
|
35
48
|
"@radix-ui/react-alert-dialog": "1.1.5",
|
|
36
49
|
"@radix-ui/react-checkbox": "1.1.4",
|
|
@@ -45,12 +58,19 @@
|
|
|
45
58
|
"@react-router/dev": "7.13.1",
|
|
46
59
|
"@react-router/express": "7.13.1",
|
|
47
60
|
"@react-router/node": "7.13.1",
|
|
48
|
-
"@vercel/react-router": "1.2.6",
|
|
49
61
|
"@tailwindcss/vite": "4",
|
|
62
|
+
"@testing-library/react": "^16.3.0",
|
|
50
63
|
"@types/express": "^5",
|
|
51
64
|
"@types/node": "22.19.0",
|
|
52
65
|
"@types/react": "19",
|
|
53
66
|
"@types/react-dom": "19",
|
|
67
|
+
"@vercel/react-router": "1.2.6",
|
|
68
|
+
"@workflow/core": "5.0.0-beta.50",
|
|
69
|
+
"@workflow/errors": "5.0.0-beta.21",
|
|
70
|
+
"@workflow/utils": "5.0.0-beta.10",
|
|
71
|
+
"@workflow/web-shared": "5.0.0-beta.50",
|
|
72
|
+
"@workflow/world": "5.0.0-beta.34",
|
|
73
|
+
"@workflow/world-vercel": "5.0.0-beta.45",
|
|
54
74
|
"@xyflow/react": "12.10.1",
|
|
55
75
|
"cbor-x": "^1",
|
|
56
76
|
"class-variance-authority": "0.7.1",
|
|
@@ -59,6 +79,7 @@
|
|
|
59
79
|
"date-fns": "4.1.0",
|
|
60
80
|
"geist": "^1.7.0",
|
|
61
81
|
"isbot": "^5",
|
|
82
|
+
"jsdom": "^26",
|
|
62
83
|
"lucide-react": "0.575.0",
|
|
63
84
|
"next-themes": "0.4.6",
|
|
64
85
|
"react": "19.1.0",
|
|
@@ -68,16 +89,9 @@
|
|
|
68
89
|
"sonner": "2.0.7",
|
|
69
90
|
"tailwind-merge": "3.5.0",
|
|
70
91
|
"tailwindcss": "4",
|
|
71
|
-
"typescript": "^
|
|
92
|
+
"typescript": "^6.0.3",
|
|
72
93
|
"vite": "^6",
|
|
73
|
-
"vitest": "^3"
|
|
74
|
-
"jsdom": "^26",
|
|
75
|
-
"@workflow/core": "5.0.0-beta.5",
|
|
76
|
-
"@workflow/errors": "5.0.0-beta.2",
|
|
77
|
-
"@workflow/utils": "5.0.0-beta.2",
|
|
78
|
-
"@workflow/web-shared": "5.0.0-beta.5",
|
|
79
|
-
"@workflow/world": "5.0.0-beta.2",
|
|
80
|
-
"@workflow/world-vercel": "5.0.0-beta.4"
|
|
94
|
+
"vitest": "^3.2.7"
|
|
81
95
|
},
|
|
82
96
|
"scripts": {
|
|
83
97
|
"dev": "react-router dev",
|
package/registry.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** A recorded embedded-dashboard instance. */
|
|
2
|
+
export interface DashboardRegistryEntry {
|
|
3
|
+
/** Public base URL the dashboard is reachable at (e.g. `http://localhost:3000/_workflow`). */
|
|
4
|
+
url: string;
|
|
5
|
+
/** Mount path the dashboard is served under (e.g. `/_workflow`; `""` at root). */
|
|
6
|
+
basename: string;
|
|
7
|
+
/** World backend identifier, for display (e.g. `local`, `@workflow/world-postgres`). */
|
|
8
|
+
world: string;
|
|
9
|
+
/** PID of the process hosting the dashboard. */
|
|
10
|
+
pid: number;
|
|
11
|
+
/** ISO timestamp the entry was recorded. */
|
|
12
|
+
startedAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Absolute path of the registry file for the current working directory. */
|
|
16
|
+
export function dashboardRegistryPath(): string;
|
|
17
|
+
|
|
18
|
+
/** Read the registry entries (always returns an array; never throws). */
|
|
19
|
+
export function readDashboardRegistry(): DashboardRegistryEntry[];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Record this process's embedded dashboard (idempotent per process). Entirely
|
|
23
|
+
* best-effort — never throws.
|
|
24
|
+
*/
|
|
25
|
+
export function recordDashboard(entry: {
|
|
26
|
+
url: string;
|
|
27
|
+
basename?: string;
|
|
28
|
+
world?: string;
|
|
29
|
+
}): void;
|
package/registry.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort registry of live, embedded observability dashboards.
|
|
3
|
+
*
|
|
4
|
+
* When the dashboard is embedded in another server (e.g. `@workflow/nitro` at
|
|
5
|
+
* `/_workflow`), the running process records its public URL here. The CLI
|
|
6
|
+
* (`workflow web` / `inspect … --web`) reads this so it can point the user at
|
|
7
|
+
* an already-running dashboard instead of spawning a redundant standalone
|
|
8
|
+
* server on a fixed port.
|
|
9
|
+
*
|
|
10
|
+
* This is a coordination hint, never a source of truth: entries may be stale
|
|
11
|
+
* (a SIGKILL'd dev server can't clean up), so consumers MUST health-check a URL
|
|
12
|
+
* before trusting it. A missing or corrupt registry is treated as "none".
|
|
13
|
+
*
|
|
14
|
+
* The file is keyed by the current working directory so multiple projects don't
|
|
15
|
+
* collide, while multiple dev servers for the same project share one file
|
|
16
|
+
* (keyed by pid within it).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
|
|
24
|
+
/** Absolute path of the registry file for the current working directory. */
|
|
25
|
+
export function dashboardRegistryPath() {
|
|
26
|
+
const key = createHash('sha256')
|
|
27
|
+
.update(process.cwd())
|
|
28
|
+
.digest('hex')
|
|
29
|
+
.slice(0, 16);
|
|
30
|
+
return path.join(os.tmpdir(), 'workflow-observability', `${key}.json`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Read the registry entries (always returns an array; never throws). */
|
|
34
|
+
export function readDashboardRegistry() {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(readFileSync(dashboardRegistryPath(), 'utf8'));
|
|
37
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeEntries(entries) {
|
|
44
|
+
const file = dashboardRegistryPath();
|
|
45
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
46
|
+
writeFileSync(file, JSON.stringify(entries));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let recorded = false;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Record this process's embedded dashboard in the registry (idempotent per
|
|
53
|
+
* process). Call once the public origin is known (e.g. from the first request).
|
|
54
|
+
* Entirely best-effort — never throws.
|
|
55
|
+
*
|
|
56
|
+
* @param {{ url: string, basename?: string, world?: string }} entry
|
|
57
|
+
*/
|
|
58
|
+
export function recordDashboard(entry) {
|
|
59
|
+
if (recorded) return;
|
|
60
|
+
recorded = true;
|
|
61
|
+
try {
|
|
62
|
+
const others = readDashboardRegistry().filter(
|
|
63
|
+
(e) => e && e.pid !== process.pid
|
|
64
|
+
);
|
|
65
|
+
others.push({
|
|
66
|
+
url: entry.url,
|
|
67
|
+
basename: entry.basename ?? '',
|
|
68
|
+
world: entry.world ?? '',
|
|
69
|
+
pid: process.pid,
|
|
70
|
+
startedAt: new Date().toISOString(),
|
|
71
|
+
});
|
|
72
|
+
writeEntries(others);
|
|
73
|
+
// Best-effort cleanup on normal exit. Signal-terminated processes (Ctrl+C,
|
|
74
|
+
// SIGKILL) won't run this — consumers prune via health checks instead. We
|
|
75
|
+
// intentionally don't hook SIGINT/SIGTERM so we don't interfere with the
|
|
76
|
+
// host framework's own shutdown handling.
|
|
77
|
+
process.once('exit', () => {
|
|
78
|
+
try {
|
|
79
|
+
writeEntries(
|
|
80
|
+
readDashboardRegistry().filter((e) => e && e.pid !== process.pid)
|
|
81
|
+
);
|
|
82
|
+
} catch {
|
|
83
|
+
// ignore
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
} catch {
|
|
87
|
+
// best-effort: a failed write must never break request handling
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { W as WorkflowRuntimeError } from "./index-B4xuge0I.js";
|
|
2
|
-
import "./index-BsV8i_Jn.js";
|
|
3
|
-
const NONCE_LENGTH = 12;
|
|
4
|
-
const TAG_LENGTH = 128;
|
|
5
|
-
const KEY_LENGTH = 32;
|
|
6
|
-
async function importKey(raw) {
|
|
7
|
-
if (raw.byteLength !== KEY_LENGTH) {
|
|
8
|
-
throw new WorkflowRuntimeError(`Encryption key must be exactly ${KEY_LENGTH} bytes, got ${raw.byteLength}`);
|
|
9
|
-
}
|
|
10
|
-
return globalThis.crypto.subtle.importKey("raw", raw, "AES-GCM", false, [
|
|
11
|
-
"encrypt",
|
|
12
|
-
"decrypt"
|
|
13
|
-
]);
|
|
14
|
-
}
|
|
15
|
-
async function decrypt(key, data) {
|
|
16
|
-
const minLength = NONCE_LENGTH + TAG_LENGTH / 8;
|
|
17
|
-
if (data.byteLength < minLength) {
|
|
18
|
-
throw new WorkflowRuntimeError(`Encrypted data too short: expected at least ${minLength} bytes, got ${data.byteLength}`);
|
|
19
|
-
}
|
|
20
|
-
const nonce = data.subarray(0, NONCE_LENGTH);
|
|
21
|
-
const ciphertext = data.subarray(NONCE_LENGTH);
|
|
22
|
-
const plaintext = await globalThis.crypto.subtle.decrypt({ name: "AES-GCM", iv: nonce, tagLength: TAG_LENGTH }, key, ciphertext);
|
|
23
|
-
return new Uint8Array(plaintext);
|
|
24
|
-
}
|
|
25
|
-
export {
|
|
26
|
-
decrypt,
|
|
27
|
-
importKey
|
|
28
|
-
};
|