@staix/agent-console 0.0.0-stage → 0.1.1

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 (43) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +354 -2
  4. package/bin/acon.js +8 -0
  5. package/package.json +38 -4
  6. package/shell/asset-viewer.css +5 -0
  7. package/shell/asset-viewer.html +11 -0
  8. package/shell/asset-viewer.js +23 -0
  9. package/shell/icon.svg +1 -0
  10. package/shell/index.html +53 -0
  11. package/shell/shell.css +534 -0
  12. package/shell/shell.js +2214 -0
  13. package/shell/space-view.js +164 -0
  14. package/shell/space.js +241 -0
  15. package/shell/space3d.js +364 -0
  16. package/shell/static.js +111 -0
  17. package/shell/vendor/BufferGeometryUtils.js +1523 -0
  18. package/shell/vendor/GLTFLoader.js +5006 -0
  19. package/shell/vendor/NOTICE +30 -0
  20. package/shell/vendor/SkeletonUtils.js +496 -0
  21. package/shell/vendor/manifest.json +42 -0
  22. package/shell/vendor/mermaid/NOTICE +25 -0
  23. package/shell/vendor/mermaid/manifest.json +19 -0
  24. package/shell/vendor/mermaid/mermaid.js +3580 -0
  25. package/shell/vendor/three.core.js +61162 -0
  26. package/shell/vendor/three.module.js +19537 -0
  27. package/shell/visual-frame.css +5 -0
  28. package/shell/visual-frame.html +14 -0
  29. package/shell/visual-frame.js +54 -0
  30. package/src/assets.ts +200 -0
  31. package/src/canonical.ts +16 -0
  32. package/src/cli.ts +246 -0
  33. package/src/conformance.ts +299 -0
  34. package/src/contract.ts +276 -0
  35. package/src/install.ts +248 -0
  36. package/src/pets.ts +63 -0
  37. package/src/plugins.ts +357 -0
  38. package/src/serve.ts +184 -0
  39. package/src/server.ts +462 -0
  40. package/src/setup.ts +74 -0
  41. package/src/space.ts +106 -0
  42. package/src/static.ts +102 -0
  43. package/test/fixtures/review/cases.json +726 -0
package/src/static.ts ADDED
@@ -0,0 +1,102 @@
1
+ import { lstat, mkdir, mkdtemp, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { isLoopback } from "./serve.ts";
4
+ import { FRAME_CSP } from "./server.ts";
5
+
6
+ export interface StaticNode { id: string; url: string }
7
+ export interface StaticConfig { api: 1; nodes: StaticNode[] }
8
+ export interface StaticBuild { out: string; config: StaticConfig }
9
+ const NODE_ID = /^[a-z][a-z0-9-]{0,31}$/;
10
+ const MAX_NODES = 32;
11
+
12
+ /** Build configuration contains public origins only, never credentials, paths or plugin records. */
13
+ export function parseStaticNodes(raw: string): StaticConfig {
14
+ if (!raw || raw.length > 8192) throw new Error("--nodes needs 1-32 id=https://origin entries; pass --nodes desk=https://node.example.com");
15
+ const parts = raw.split(",");
16
+ if (parts.length > MAX_NODES) throw new Error("--nodes has more than 32 nodes; split the build into at most 32 nodes");
17
+ const ids = new Set<string>();
18
+ const nodes = parts.map((part): StaticNode => {
19
+ const equal = part.indexOf("=");
20
+ const id = part.slice(0, equal);
21
+ const address = part.slice(equal + 1);
22
+ if (equal < 1 || !NODE_ID.test(id) || ids.has(id)) throw new Error("--nodes needs unique lowercase node ids (1-32 letters, digits or hyphens, starting with a letter); correct the id=origin entries");
23
+ let url: URL;
24
+ try { url = new URL(address); }
25
+ catch { throw new Error("--nodes contains an invalid URL; use an exact https origin such as desk=https://node.example.com"); }
26
+ if (address.length > 1024 || url.username || url.password || url.search || url.hash || url.origin !== address.replace(/\/$/, "") ||
27
+ (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback(url.hostname)))) {
28
+ throw new Error("--nodes accepts exact https origins without credentials, paths, queries or fragments (http only for loopback); correct the node URL before building");
29
+ }
30
+ ids.add(id);
31
+ return { id, url: url.origin };
32
+ });
33
+ return { api: 1, nodes };
34
+ }
35
+
36
+ export function staticCsp(config: StaticConfig): string {
37
+ const origins = [...new Set(config.nodes.map((node) => node.url))].join(" ");
38
+ return `default-src 'none'; script-src 'self' ${origins}; style-src 'self'; connect-src 'self' ${origins}; img-src 'self' data: ${origins}; base-uri 'none'; form-action 'none'; frame-src 'self'; frame-ancestors 'none'`;
39
+ }
40
+
41
+ const FILES: [string, string][] = [
42
+ ...["shell.js", "static.js", "shell.css", "space.js", "space-view.js", "space3d.js", "icon.svg", "asset-viewer.html", "asset-viewer.js", "asset-viewer.css"].map((file): [string, string] => [file, file]),
43
+ ...["three.module.js", "three.core.js", "GLTFLoader.js", "BufferGeometryUtils.js", "SkeletonUtils.js", "NOTICE", "manifest.json"].map((file): [string, string] => [`vendor/${file}`, `vendor/${file}`]),
44
+ ["visual-frame.html", "visual/frame.html"], ["visual-frame.css", "visual/frame.css"], ["visual-frame.js", "visual/frame.js"],
45
+ ["vendor/mermaid/mermaid.js", "visual/mermaid.js"], ["vendor/mermaid/NOTICE", "visual/NOTICE"], ["vendor/mermaid/manifest.json", "visual/manifest.json"],
46
+ ];
47
+ export const VIEWER_CSP = "default-src 'none'; script-src 'self'; style-src 'self'; img-src blob:; frame-src blob:; base-uri 'none'; form-action 'none'; frame-ancestors 'self'";
48
+ const BASE_HEADERS = {
49
+ "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "X-Content-Type-Options": "nosniff",
50
+ };
51
+ const headerLines = (headers: Record<string, string>) => Object.entries(headers).map(([key, value]) => ` ${key}: ${value}`).join("\n");
52
+
53
+ /** Write a fixed, credential-free shell into a new destination, publishing only a complete staged tree. */
54
+ export async function buildStatic(raw: string, out = "dist"): Promise<StaticBuild> {
55
+ const config = parseStaticNodes(raw);
56
+ const target = resolve(out);
57
+ try { await lstat(target); throw new Error("static output already exists; choose a new --out directory or move the existing output first"); }
58
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
59
+ await mkdir(dirname(target), { recursive: true });
60
+ const parent = await realpath(dirname(target));
61
+ const destination = join(parent, target.slice(dirname(target).length + 1));
62
+ const stage = await mkdtemp(join(parent, ".acon-static-"));
63
+ try {
64
+ const csp = staticCsp(config);
65
+ // frame-ancestors is header-only; browsers report it as ignored in a meta policy.
66
+ const metaCsp = csp.replace("; frame-ancestors 'none'", "");
67
+ const shellRoot = new URL("../shell/", import.meta.url);
68
+ const source = await readFile(new URL("index.html", shellRoot), "utf8");
69
+ const index = source.replace('<html lang="en">', '<html lang="en" data-static="true">').replace('<meta charset="utf-8">', `<meta charset="utf-8">\n<meta http-equiv="Content-Security-Policy" content="${metaCsp}">`);
70
+ await writeFile(join(stage, "index.html"), index);
71
+ for (const [sourceName, destinationName] of FILES) {
72
+ const sourceUrl = new URL(sourceName, shellRoot);
73
+ const info = await lstat(sourceUrl);
74
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error("a static shell source is not a regular file; reinstall the console package before building");
75
+ const file = join(stage, destinationName);
76
+ await mkdir(dirname(file), { recursive: true });
77
+ await writeFile(file, await readFile(sourceUrl));
78
+ }
79
+ await writeFile(join(stage, "config.json"), `${JSON.stringify(config, null, 2)}\n`);
80
+ // Keep document policies disjoint: Workers joins duplicate matching header values, so a broad CSP would
81
+ // also restrict the sandboxed frame's separate measuring policy.
82
+ const documents = ["/", "/index.html"];
83
+ const frameDocuments = ["/visual/frame", "/visual/frame.html"];
84
+ const viewerDocuments = ["/asset-viewer", "/asset-viewer.html"];
85
+ const headers = `/*\n${headerLines(BASE_HEADERS)}\n\n${documents.map((path) => `${path}\n${headerLines({ "Content-Security-Policy": csp, "X-Frame-Options": "DENY" })}`).join("\n\n")}\n\n${frameDocuments.map((path) => `${path}\n${headerLines({ "Content-Security-Policy": FRAME_CSP, "X-Frame-Options": "SAMEORIGIN" })}`).join("\n\n")}\n\n${viewerDocuments.map((path) => `${path}\n${headerLines({ "Content-Security-Policy": VIEWER_CSP, "X-Frame-Options": "SAMEORIGIN" })}`).join("\n\n")}\n`;
86
+ await writeFile(join(stage, "_headers"), headers);
87
+ await writeFile(join(stage, "vercel.json"), `${JSON.stringify({ version: 2, cleanUrls: true, headers: [
88
+ { source: "/(.*)", headers: Object.entries(BASE_HEADERS).map(([key, value]) => ({ key, value })) },
89
+ ...documents.map((path) => ({ source: path, headers: [{ key: "Content-Security-Policy", value: csp }, { key: "X-Frame-Options", value: "DENY" }] })),
90
+ ...frameDocuments.map((path) => ({ source: path, headers: [{ key: "Content-Security-Policy", value: FRAME_CSP }, { key: "X-Frame-Options", value: "SAMEORIGIN" }] })),
91
+ ...viewerDocuments.map((path) => ({ source: path, headers: [{ key: "Content-Security-Policy", value: VIEWER_CSP }, { key: "X-Frame-Options", value: "SAMEORIGIN" }] })),
92
+ ] }, null, 2)}\n`);
93
+ await writeFile(join(stage, "wrangler.jsonc"), `${JSON.stringify({ name: "acon-static-shell", compatibility_date: "2026-11-04", assets: { directory: ".", html_handling: "auto-trailing-slash", not_found_handling: "none" } }, null, 2)}\n`);
94
+ await writeFile(join(stage, ".assetsignore"), "vercel.json\nwrangler.jsonc\n.assetsignore\n");
95
+ // rename refuses a nonempty directory, file or symlink that appeared while staging; no existing content is removed.
96
+ await rename(stage, destination);
97
+ return { out: destination, config };
98
+ } catch (error) {
99
+ await rm(stage, { recursive: true, force: true });
100
+ throw error;
101
+ }
102
+ }