@aact/view 3.0.0-beta.19

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 ADDED
@@ -0,0 +1,118 @@
1
+ # `@aact/view`
2
+
3
+ Browser-based live viewer for the C4 architecture model that
4
+ [aact](https://github.com/Byndyusoft/aact) parses. The Structurizr
5
+ DSL / C4-PUML / model-json source you point `aact check` at is the
6
+ source of truth; `aact view` renders it as an interactive graph
7
+ that re-layouts every time you save the file.
8
+
9
+ > **Status: experimental.** Ships on the `proposal/aact-view`
10
+ > branch; not yet on a beta release. Expect rough edges around
11
+ > non-default models and watch the CHANGELOG entry when the feature
12
+ > graduates.
13
+
14
+ ## Install
15
+
16
+ `@aact/view` is an optional companion package. Install alongside
17
+ aact in the project you want to inspect:
18
+
19
+ ```bash
20
+ npm install -D @aact/view
21
+ # or
22
+ pnpm add -D @aact/view
23
+ ```
24
+
25
+ The core `aact` package detects `@aact/view` via dynamic import; no
26
+ configuration plumbing.
27
+
28
+ ## Usage
29
+
30
+ ```bash
31
+ npx aact view # uses aact.config.ts in cwd
32
+ npx aact view --port 4321 # pin port (defaults to 3000 with auto-fallback)
33
+ npx aact view --no-open # skip auto-opening the browser
34
+ ```
35
+
36
+ The console prints a URL with a per-session auth token:
37
+
38
+ ```
39
+ ▸ aact view ready at http://localhost:3000/?token=AbCd…
40
+ watching ./architecture.dsl for changes (Ctrl-C to stop)
41
+ ```
42
+
43
+ Open that URL. Saving the source file re-parses through aact-core
44
+ and pushes the new model over WebSocket — the graph re-layouts in
45
+ place.
46
+
47
+ ## What you see
48
+
49
+ The canvas follows the Simon Brown C4 reference palette (Person
50
+ deep blue, System mid blue, Container lighter, Component lightest,
51
+ externals neutral slate). Three view modes in the top bar:
52
+
53
+ - **Drill** — classic C4 levels. Double-click a boundary to descend;
54
+ breadcrumb walks back up.
55
+ - **Expand** — toggle boundaries open inline; parents stay visible
56
+ so cross-context interactions read in one frame.
57
+ - **Flat** — every boundary expanded at once; read-only big-picture
58
+ view.
59
+
60
+ Two more topbar toggles cover edge presentation:
61
+
62
+ - **Edge style** — Curve / Smooth / Step. Personal preference,
63
+ persisted in localStorage.
64
+ - **Edge filter** — All / Cross-BC. In Cross-BC mode intra-boundary
65
+ relations fade to background and inter-context interactions light
66
+ up; useful when an API gateway has many fan-outs and you want to
67
+ see which Bounded Contexts actually talk.
68
+
69
+ Hover a node to highlight only the edges incident to it; everything
70
+ else dims. The right-side details panel shows the selected
71
+ element / boundary's tags, technology, source location (clickable —
72
+ opens your IDE), properties, and outgoing relations.
73
+
74
+ The full visual + interaction spec lives in [`DESIGN.md`](./DESIGN.md).
75
+
76
+ ## Live reload
77
+
78
+ A chokidar watcher debounces source changes by 80ms and coalesces
79
+ in-flight reloads. When the parser fails (broken DSL syntax) the
80
+ last good model stays on screen and the status pill flips to
81
+ "error" with the parser message — restoring the file recovers via
82
+ the next successful broadcast.
83
+
84
+ ## Security
85
+
86
+ `aact view` listens on `localhost` only. Each session generates a
87
+ 24-byte random auth token; `/api/model` and the `/api/ws` upgrade
88
+ require it as either a query string parameter (first navigation) or
89
+ a `HttpOnly` cookie (set on first HTML response). This stops random
90
+ browser tabs / extensions on the same machine from reading your
91
+ architecture graph or following `vscode://file/...` source links.
92
+
93
+ ## What it doesn't do
94
+
95
+ - **Editing.** The viewer is read-only. Source DSL/PUML stays the
96
+ authority; your IDE is the editor.
97
+ - **Per-user layout persistence.** Positions are deterministic from
98
+ the model — every re-parse re-runs ELK so the layout is
99
+ reproducible across machines.
100
+ - **ArchiMate / Deployment view / UML / BPMN.** C4 paradigm only,
101
+ matching aact-core scope.
102
+
103
+ ## Known follow-ups
104
+
105
+ These are flagged but not built:
106
+
107
+ - **ELK in a worker** — layout currently runs on the main thread.
108
+ Sub-100ms on typical C4 models (V < 100), can take 300-500ms at
109
+ V > 500. Canonical fix: ship `elkjs/lib/elk-worker.min.js` as a
110
+ static asset via Vite `?url` + `new ELK({ workerUrl })` so ELK
111
+ spawns its own sub-worker out of the main thread.
112
+ - **Search / filter by name.**
113
+ - **Focus mode** — pick a node, dim all non-1-hop-neighbours.
114
+ - **Export to SVG / PNG.**
115
+
116
+ ## License
117
+
118
+ GPL-3.0, matching aact-core.
@@ -0,0 +1,48 @@
1
+ import type { AactConfig } from "aact";
2
+ /**
3
+ * Inputs the core `aact view` subcommand hands over after loading
4
+ * and validating the user's config. Everything is already resolved
5
+ * — paths are absolute, the customRules array is parsed — so the
6
+ * companion has no business touching the loader pipeline a second
7
+ * time. It just hosts the UI on top of what's already in memory.
8
+ */
9
+ export interface RunWorkbenchOptions {
10
+ /** Loaded + validated `aact.config.ts` payload. */
11
+ readonly config: AactConfig;
12
+ /** Absolute path to the resolved config file, or `null` when the
13
+ * caller (rarely) skipped config discovery. */
14
+ readonly configPath: string | null;
15
+ /** Override port. When omitted, listhen picks the next free
16
+ * port — the workbench prints the URL it actually bound to. */
17
+ readonly port?: number;
18
+ /** Suppress the automatic browser-open. The URL still prints to
19
+ * stdout so CI / headless flows can pick it up. */
20
+ readonly noOpen?: boolean;
21
+ }
22
+ export interface RunWorkbenchResult {
23
+ /** `0` when the user quit cleanly; `2` when the server failed to
24
+ * boot (port collision the picker couldn't escape, file
25
+ * watcher errored, etc.). */
26
+ readonly exitCode: 0 | 2;
27
+ /** URL the workbench bound to, or `null` if the boot failed
28
+ * before binding. Surfaced on the core CLI's `ViewData`
29
+ * envelope so CI / agents can pick up where the session went. */
30
+ readonly url: string | null;
31
+ }
32
+ /**
33
+ * Lifecycle entry point invoked by the core `aact view` subcommand
34
+ * via dynamic import.
35
+ *
36
+ * Boots the local HTTP server (listhen-picked port, browser opens
37
+ * unless `--no-open`), serves the inline workbench page, exposes
38
+ * `GET /api/model` + the `/api/ws` push channel, and wires a
39
+ * chokidar watcher on `config.source.path` so every save triggers
40
+ * a re-load + broadcast.
41
+ *
42
+ * Resolves only when the user terminates the session (Ctrl-C /
43
+ * SIGTERM); the core CLI then propagates the exit code into the
44
+ * envelope. Defensive cleanup: signal handlers tear the watcher
45
+ * + listener down so a hard-killed Node process doesn't leak the
46
+ * port or a stuck file watcher into the editor's `.fseventsd`.
47
+ */
48
+ export declare const runWorkbench: (options: RunWorkbenchOptions) => Promise<RunWorkbenchResult>;
package/dist/index.js ADDED
@@ -0,0 +1,123 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { loadModelFromConfig } from "./load-model.js";
3
+ import { startServer } from "./server.js";
4
+ import { startWatcher } from "./watcher.js";
5
+ const sourceOf = (config) => typeof config.source === "string" ? config.source : config.source.path;
6
+ const createAuthToken = () => randomBytes(24).toString("base64url");
7
+ /** Build a `ModelEnvelope` from a single in-process loadModel call.
8
+ * Mirrors the wider `aact model --json` envelope so the SPA can
9
+ * reuse aact's contract — schemaVersion stays at `1`. */
10
+ const buildEnvelope = async (options, aactVersion) => {
11
+ const startedAt = performance.now();
12
+ const { model, issues } = await loadModelFromConfig(options.config);
13
+ return {
14
+ schemaVersion: 1,
15
+ command: "view",
16
+ ok: true,
17
+ exitCode: 0,
18
+ data: { model, issues: [...issues] },
19
+ diagnostics: [],
20
+ meta: {
21
+ aactVersion,
22
+ durationMs: Math.round(performance.now() - startedAt),
23
+ configPath: options.configPath,
24
+ source: sourceOf(options.config),
25
+ },
26
+ };
27
+ };
28
+ const buildReloadError = (options, error, startedAt) => ({
29
+ message: error instanceof Error ? error.message : String(error),
30
+ source: sourceOf(options.config),
31
+ configPath: options.configPath,
32
+ durationMs: Math.round(performance.now() - startedAt),
33
+ at: new Date().toISOString(),
34
+ });
35
+ /**
36
+ * Lifecycle entry point invoked by the core `aact view` subcommand
37
+ * via dynamic import.
38
+ *
39
+ * Boots the local HTTP server (listhen-picked port, browser opens
40
+ * unless `--no-open`), serves the inline workbench page, exposes
41
+ * `GET /api/model` + the `/api/ws` push channel, and wires a
42
+ * chokidar watcher on `config.source.path` so every save triggers
43
+ * a re-load + broadcast.
44
+ *
45
+ * Resolves only when the user terminates the session (Ctrl-C /
46
+ * SIGTERM); the core CLI then propagates the exit code into the
47
+ * envelope. Defensive cleanup: signal handlers tear the watcher
48
+ * + listener down so a hard-killed Node process doesn't leak the
49
+ * port or a stuck file watcher into the editor's `.fseventsd`.
50
+ */
51
+ export const runWorkbench = async (options) => {
52
+ // Resolve aact version lazily — it's only surfaced on the
53
+ // envelope `meta`, no need to crash the boot if the workspace
54
+ // is mid-rebuild. Falls back to "unknown".
55
+ let aactVersion = "unknown";
56
+ try {
57
+ const mod = (await import("aact"));
58
+ if (typeof mod.aactVersion === "string")
59
+ aactVersion = mod.aactVersion;
60
+ }
61
+ catch {
62
+ // ignore — version is informational only
63
+ }
64
+ let server;
65
+ try {
66
+ const authToken = createAuthToken();
67
+ const envelope = await buildEnvelope(options, aactVersion);
68
+ server = await startServer({
69
+ ...(options.port === undefined ? {} : { port: options.port }),
70
+ ...(options.noOpen === undefined ? {} : { noOpen: options.noOpen }),
71
+ initialEnvelope: envelope,
72
+ authToken,
73
+ });
74
+ // eslint-disable-next-line no-console -- intentional user-facing banner
75
+ console.log(`▸ aact view ready at ${server.url}`);
76
+ if (options.noOpen) {
77
+ // eslint-disable-next-line no-console
78
+ console.log(` open it manually — --no-open suppressed auto-launch`);
79
+ }
80
+ // eslint-disable-next-line no-console
81
+ console.log(` watching ${sourceOf(options.config)} for changes (Ctrl-C to stop)`);
82
+ const watcher = startWatcher({
83
+ paths: [sourceOf(options.config)],
84
+ onChange: async () => {
85
+ const startedAt = performance.now();
86
+ try {
87
+ const next = await buildEnvelope(options, aactVersion);
88
+ server?.broadcast(next);
89
+ // eslint-disable-next-line no-console
90
+ console.log(` ↻ reloaded (${Object.keys(next.data.model.elements).length} elements, ${next.data.issues.length} issues)`);
91
+ }
92
+ catch (error) {
93
+ const viewError = buildReloadError(options, error, startedAt);
94
+ server?.broadcastError(viewError);
95
+ // eslint-disable-next-line no-console
96
+ console.error(` ✗ reload failed: ${viewError.message}`);
97
+ }
98
+ },
99
+ });
100
+ await new Promise((resolve) => {
101
+ const cleanup = (signal) => {
102
+ // eslint-disable-next-line no-console
103
+ console.log(`\n• stopping (${signal})…`);
104
+ process.off("SIGINT", onSigint);
105
+ process.off("SIGTERM", onSigterm);
106
+ void Promise.allSettled([watcher.close(), server?.close()]).then(() => resolve());
107
+ };
108
+ const onSigint = () => cleanup("SIGINT");
109
+ const onSigterm = () => cleanup("SIGTERM");
110
+ process.on("SIGINT", onSigint);
111
+ process.on("SIGTERM", onSigterm);
112
+ });
113
+ return { exitCode: 0, url: server.url };
114
+ }
115
+ catch (error) {
116
+ if (server)
117
+ await server.close().catch(() => { });
118
+ // eslint-disable-next-line no-console
119
+ console.error(`aact view boot failed: ${error instanceof Error ? error.message : String(error)}`);
120
+ return { exitCode: 2, url: server?.url ?? null };
121
+ }
122
+ };
123
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { AactConfig, Model, ModelIssue } from "aact";
2
+ /**
3
+ * Load the normalised Model from `aact.config.ts`'s `source`.
4
+ *
5
+ * The companion deliberately routes through aact's public format
6
+ * registry instead of forking a loader — every format aact already
7
+ * supports (PUML, Structurizr DSL/JSON, model-json) flows through
8
+ * here automatically, and a third-party format added to the registry
9
+ * works in the workbench the moment it's installed.
10
+ *
11
+ * `config.source.path` is already absolute after
12
+ * `loadAndValidateConfig` ran upstream in the core CLI; the
13
+ * companion treats it as read-only data.
14
+ */
15
+ export interface ModelLoadResult {
16
+ readonly model: Model;
17
+ readonly issues: readonly ModelIssue[];
18
+ }
19
+ export declare const loadModelFromConfig: (config: AactConfig) => Promise<ModelLoadResult>;
@@ -0,0 +1,15 @@
1
+ import { canLoad, loadFormat } from "aact";
2
+ export const loadModelFromConfig = async (config) => {
3
+ const source = typeof config.source === "string"
4
+ ? { type: undefined, path: config.source }
5
+ : config.source;
6
+ if (!source.type) {
7
+ throw new Error(`aact config source.type is required for view (got "${source.path}" without type)`);
8
+ }
9
+ const format = await loadFormat(source.type);
10
+ if (!canLoad(format)) {
11
+ throw new Error(`Format "${source.type}" cannot load — load capability missing`);
12
+ }
13
+ return format.load(source.path);
14
+ };
15
+ //# sourceMappingURL=load-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-model.js","sourceRoot":"","sources":["../src/load-model.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAoB3C,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,EACtC,MAAkB,EACQ,EAAE;IAC5B,MAAM,MAAM,GACV,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ;QAC/B,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE;QAC1C,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC;IAEpB,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,sDAAsD,MAAM,CAAC,IAAI,iBAAiB,CACnF,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7C,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CACb,WAAW,MAAM,CAAC,IAAI,yCAAyC,CAChE,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC,CAAC"}
@@ -0,0 +1,84 @@
1
+ import { type Listener } from "listhen";
2
+ import type { ModelLoadResult } from "./load-model.js";
3
+ /**
4
+ * The envelope shape the server pushes over `/api/ws` and returns
5
+ * from `/api/model`. Mirrors aact's `CliEnvelope<ModelData>` enough
6
+ * that the client can treat them interchangeably — the workbench
7
+ * adds no second contract.
8
+ */
9
+ export interface ModelEnvelope {
10
+ readonly schemaVersion: 1;
11
+ readonly command: "view";
12
+ readonly ok: boolean;
13
+ readonly exitCode: 0 | 1 | 2;
14
+ readonly data: {
15
+ readonly model: ModelLoadResult["model"];
16
+ readonly issues: readonly ModelLoadResult["issues"][number][];
17
+ };
18
+ readonly diagnostics: readonly never[];
19
+ readonly meta: {
20
+ readonly aactVersion: string;
21
+ readonly durationMs: number;
22
+ readonly configPath: string | null;
23
+ readonly source: string | null;
24
+ };
25
+ }
26
+ export interface ViewError {
27
+ readonly message: string;
28
+ readonly source: string | null;
29
+ readonly configPath: string | null;
30
+ readonly durationMs: number;
31
+ readonly at: string;
32
+ }
33
+ export type ServerMessage = {
34
+ readonly type: "model-update";
35
+ readonly envelope: ModelEnvelope;
36
+ } | {
37
+ readonly type: "model-error";
38
+ readonly error: ViewError;
39
+ };
40
+ /** Subscriber callback type — the workbench pushes a fresh envelope
41
+ * to every subscriber whenever chokidar reports a source change. */
42
+ export type Subscriber = (message: ServerMessage) => void;
43
+ export interface ServerHandle {
44
+ readonly listener: Listener;
45
+ readonly url: string;
46
+ /** Register a callback that fires on every model update. Returns
47
+ * an unsubscribe function. */
48
+ subscribe(fn: Subscriber): () => void;
49
+ /** Push a fresh envelope to every connected client. */
50
+ broadcast(envelope: ModelEnvelope): void;
51
+ /** Push a reload failure while keeping the latest valid model cached. */
52
+ broadcastError(error: ViewError): void;
53
+ /** Update the cached envelope so new HTTP `/api/model` hits return
54
+ * the latest known model without re-loading from disk. */
55
+ setCurrent(envelope: ModelEnvelope): void;
56
+ close(): Promise<void>;
57
+ }
58
+ export interface ServerOptions {
59
+ readonly port?: number;
60
+ /** Skip `listhen`'s automatic browser open — useful when the
61
+ * workbench is driven from CI or an editor extension. */
62
+ readonly noOpen?: boolean;
63
+ /** Initial envelope to serve immediately. The watcher will
64
+ * replace it via `setCurrent` whenever the source changes. */
65
+ readonly initialEnvelope: ModelEnvelope;
66
+ /** Per-session token protecting local JSON / WS endpoints from
67
+ * unrelated browser pages hitting localhost. */
68
+ readonly authToken: string;
69
+ }
70
+ /**
71
+ * Bring up the local workbench HTTP server.
72
+ *
73
+ * Routes:
74
+ * - `GET /` — inline HTML page (Phase 1 placeholder; the
75
+ * Svelte SPA replaces it in Phase 3).
76
+ * - `GET /api/model` — current `ModelEnvelope`, JSON.
77
+ * - `GET /api/ws` — WebSocket channel pushing `{type:"model-update", envelope}`
78
+ * payloads on every chokidar event.
79
+ *
80
+ * The h3 app keeps a per-client subscriber list keyed by peer; the
81
+ * watcher (in `runWorkbench`) calls `broadcast()` to notify the SPA
82
+ * after each successful `loadModelFromConfig`.
83
+ */
84
+ export declare const startServer: (options: ServerOptions) => Promise<ServerHandle>;
package/dist/server.js ADDED
@@ -0,0 +1,210 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { H3, defineWebSocketHandler, toNodeHandler } from "h3";
5
+ import { listen } from "listhen";
6
+ /**
7
+ * Resolve the bundled SPA dist relative to this module so the path
8
+ * survives both the source tree (`packages/view/dist/ui/`) and the
9
+ * post-publish layout (`<install>/node_modules/@aact/view/dist/ui/`).
10
+ * Computed from `import.meta.url` because aact uses pure ESM —
11
+ * `__dirname` is unavailable.
12
+ */
13
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
14
+ const UI_ROOT = path.resolve(HERE, "ui");
15
+ const MIME = {
16
+ ".html": "text/html; charset=utf-8",
17
+ ".js": "text/javascript; charset=utf-8",
18
+ ".mjs": "text/javascript; charset=utf-8",
19
+ ".css": "text/css; charset=utf-8",
20
+ ".svg": "image/svg+xml",
21
+ ".png": "image/png",
22
+ ".ico": "image/x-icon",
23
+ ".map": "application/json; charset=utf-8",
24
+ ".json": "application/json; charset=utf-8",
25
+ };
26
+ const mimeFor = (file) => MIME[path.extname(file).toLowerCase()] ?? "application/octet-stream";
27
+ const isInside = (root, candidate) => {
28
+ const relative = path.relative(root, candidate);
29
+ return (relative === "" ||
30
+ (!relative.startsWith("..") && !path.isAbsolute(relative)));
31
+ };
32
+ /**
33
+ * Bring up the local workbench HTTP server.
34
+ *
35
+ * Routes:
36
+ * - `GET /` — inline HTML page (Phase 1 placeholder; the
37
+ * Svelte SPA replaces it in Phase 3).
38
+ * - `GET /api/model` — current `ModelEnvelope`, JSON.
39
+ * - `GET /api/ws` — WebSocket channel pushing `{type:"model-update", envelope}`
40
+ * payloads on every chokidar event.
41
+ *
42
+ * The h3 app keeps a per-client subscriber list keyed by peer; the
43
+ * watcher (in `runWorkbench`) calls `broadcast()` to notify the SPA
44
+ * after each successful `loadModelFromConfig`.
45
+ */
46
+ export const startServer = async (options) => {
47
+ let current = options.initialEnvelope;
48
+ const subscribers = new Set();
49
+ const peers = new Set();
50
+ const authCookie = `aact_view_token=${options.authToken}; Path=/; SameSite=Strict; HttpOnly`;
51
+ const hasAuthCookie = (headers) => {
52
+ const raw = headers.get("cookie") ?? "";
53
+ return raw
54
+ .split(";")
55
+ .map((part) => part.trim())
56
+ .some((part) => part === `aact_view_token=${options.authToken}`);
57
+ };
58
+ const isAuthorized = (url, headers) => url.searchParams.get("token") === options.authToken ||
59
+ hasAuthCookie(headers);
60
+ const parseRequestUrl = (input) => new URL(input, "http://localhost");
61
+ const authUrl = (url) => `${url.replace(/\/$/, "")}/?token=${encodeURIComponent(options.authToken)}`;
62
+ const unauthorized = () => new Response("Unauthorized", {
63
+ status: 401,
64
+ headers: { "content-type": "text/plain; charset=utf-8" },
65
+ });
66
+ const htmlHeaders = (contentType) => ({
67
+ "content-type": contentType,
68
+ "set-cookie": authCookie,
69
+ });
70
+ const sendToSubscribers = (message) => {
71
+ for (const fn of subscribers) {
72
+ try {
73
+ fn(message);
74
+ }
75
+ catch {
76
+ // Subscriber threw — don't let one bad listener stop the
77
+ // others from receiving the update.
78
+ }
79
+ }
80
+ };
81
+ const sendToPeers = (message) => {
82
+ const payload = JSON.stringify(message);
83
+ for (const peer of peers) {
84
+ try {
85
+ peer.send(payload);
86
+ }
87
+ catch {
88
+ // Peer dropped mid-broadcast; CrossWS will report `close`
89
+ // / `error` next tick so just skip it here.
90
+ }
91
+ }
92
+ };
93
+ const app = new H3();
94
+ const resolveWebSocketHooks = async (request) => {
95
+ const response = (await app.fetch(request));
96
+ return response.crossws ?? {};
97
+ };
98
+ app.get("/api/model", (event) => {
99
+ const url = parseRequestUrl(event.req.url);
100
+ if (!isAuthorized(url, event.req.headers))
101
+ return unauthorized();
102
+ return current;
103
+ });
104
+ app.get("/api/ws", defineWebSocketHandler({
105
+ upgrade(request) {
106
+ const url = parseRequestUrl(request.url);
107
+ if (!isAuthorized(url, request.headers))
108
+ return unauthorized();
109
+ },
110
+ open(peer) {
111
+ peers.add(peer);
112
+ // Send the latest known envelope right away so a freshly
113
+ // opened tab doesn't have to wait for the next watcher event
114
+ // to populate.
115
+ peer.send(JSON.stringify({ type: "model-update", envelope: current }));
116
+ },
117
+ close(peer) {
118
+ peers.delete(peer);
119
+ },
120
+ error(peer) {
121
+ peers.delete(peer);
122
+ },
123
+ }));
124
+ // Serve the pre-built Svelte SPA from `dist/ui/`. Resolve each
125
+ // request path against the bundle dir, fall back to `index.html`
126
+ // for client-side routes (Svelte Flow drill-down is in-app, but
127
+ // a deep link / refresh hits `/something` and we want to bring
128
+ // the SPA up regardless). Aggressive directory traversal blocked
129
+ // by re-resolving and asserting the path stays under UI_ROOT.
130
+ const serveAsset = async (requestPath) => {
131
+ const clean = requestPath.replace(/^\/+/, "").split("?")[0] ?? "";
132
+ const relative = clean === "" ? "index.html" : clean;
133
+ const candidate = path.resolve(UI_ROOT, relative);
134
+ if (!isInside(UI_ROOT, candidate))
135
+ return null;
136
+ try {
137
+ const body = await readFile(candidate);
138
+ return { body, contentType: mimeFor(candidate) };
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ };
144
+ app.get("/", async () => {
145
+ const asset = await serveAsset("index.html");
146
+ if (!asset) {
147
+ return new Response("aact view UI bundle is missing — run `pnpm --filter @aact/view build:ui`.", {
148
+ status: 500,
149
+ headers: { "content-type": "text/plain; charset=utf-8" },
150
+ });
151
+ }
152
+ return new Response(asset.body, {
153
+ headers: htmlHeaders(asset.contentType),
154
+ });
155
+ });
156
+ app.get("/**:path", async (event) => {
157
+ const url = new URL(event.req.url);
158
+ if (url.pathname.startsWith("/api/")) {
159
+ return new Response("Not Found", { status: 404 });
160
+ }
161
+ const asset = await serveAsset(url.pathname);
162
+ if (asset) {
163
+ return new Response(asset.body, {
164
+ headers: htmlHeaders(asset.contentType),
165
+ });
166
+ }
167
+ // SPA fallback — let the client route in-app.
168
+ const indexAsset = await serveAsset("index.html");
169
+ if (!indexAsset)
170
+ return new Response("Not Found", { status: 404 });
171
+ return new Response(indexAsset.body, {
172
+ headers: htmlHeaders(indexAsset.contentType),
173
+ });
174
+ });
175
+ const listener = await listen(toNodeHandler(app), {
176
+ port: options.port,
177
+ open: options.noOpen ? false : true,
178
+ showURL: false,
179
+ qr: false,
180
+ public: false,
181
+ ws: { resolve: resolveWebSocketHooks },
182
+ });
183
+ const url = authUrl(listener.url);
184
+ return {
185
+ listener,
186
+ url,
187
+ subscribe(fn) {
188
+ subscribers.add(fn);
189
+ return () => subscribers.delete(fn);
190
+ },
191
+ broadcast(envelope) {
192
+ current = envelope;
193
+ const message = { type: "model-update", envelope };
194
+ sendToPeers(message);
195
+ sendToSubscribers(message);
196
+ },
197
+ broadcastError(error) {
198
+ const message = { type: "model-error", error };
199
+ sendToPeers(message);
200
+ sendToSubscribers(message);
201
+ },
202
+ setCurrent(envelope) {
203
+ current = envelope;
204
+ },
205
+ close() {
206
+ return listener.close();
207
+ },
208
+ };
209
+ };
210
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}