stuntdouble 0.1.2 → 0.1.3

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.
@@ -1,238 +0,0 @@
1
- import {
2
- ActorSchema,
3
- ChecklistSchema,
4
- effectiveStatus,
5
- readJson,
6
- readRuns,
7
- writeFileAtomic
8
- } from "./chunk-JAB46QV7.js";
9
-
10
- // src/ui-server.ts
11
- import { randomBytes, timingSafeEqual } from "crypto";
12
- import { watch } from "fs";
13
- import { readdir, readFile, realpath } from "fs/promises";
14
- import { createServer } from "http";
15
- import { basename, extname, join, resolve, sep } from "path";
16
- var FIRST_PORT = 4747;
17
- var PORT_TRIES = 20;
18
- var TYPES = {
19
- ".png": "image/png",
20
- ".json": "application/json",
21
- ".jsonl": "text/plain; charset=utf-8"
22
- };
23
- var PAGE_HEADERS = {
24
- "Content-Security-Policy": "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src 'self'; frame-ancestors 'none'; form-action 'none'"
25
- };
26
- function send(res, status, body, type = "text/plain; charset=utf-8", headers = {}) {
27
- res.writeHead(status, {
28
- "Content-Type": type,
29
- "Cache-Control": "no-store",
30
- "X-Content-Type-Options": "nosniff",
31
- "Referrer-Policy": "no-referrer",
32
- ...headers
33
- });
34
- res.end(body);
35
- }
36
- var json = (res, data) => send(res, 200, JSON.stringify(data), "application/json");
37
- function sameSecret(given, token) {
38
- const a = Buffer.from(given);
39
- const b = Buffer.from(token);
40
- return a.length === b.length && timingSafeEqual(a, b);
41
- }
42
- function cookie(req, name) {
43
- for (const part of (req.headers.cookie ?? "").split(";")) {
44
- const [key, ...value] = part.trim().split("=");
45
- if (key === name) return value.join("=");
46
- }
47
- return void 0;
48
- }
49
- async function confined(root, rel) {
50
- try {
51
- const realRoot = await realpath(root);
52
- const real = await realpath(resolve(root, rel));
53
- return real.startsWith(realRoot + sep) ? real : null;
54
- } catch {
55
- return null;
56
- }
57
- }
58
- async function definitions(root, folder) {
59
- const schema = folder === "checklists" ? ChecklistSchema : ActorSchema;
60
- const names = await readdir(join(root, folder)).catch(() => []);
61
- return Promise.all(
62
- names.filter((n) => n.endsWith(".json")).sort().map(async (name) => {
63
- const slug = basename(name, ".json");
64
- try {
65
- return { slug, data: await readJson(join(root, folder, name), schema, name) };
66
- } catch (err) {
67
- return { slug, error: err.message };
68
- }
69
- })
70
- );
71
- }
72
- async function readBody(req) {
73
- let body = "";
74
- for await (const chunk of req) {
75
- body += chunk;
76
- if (body.length > 1e4) throw new Error("Body too large");
77
- }
78
- return body;
79
- }
80
- async function startUi(options) {
81
- const root = resolve(options.root);
82
- const token = randomBytes(24).toString("base64url");
83
- const clients = /* @__PURE__ */ new Set();
84
- let port = 0;
85
- async function handle(req, res) {
86
- const host = req.headers.host ?? "";
87
- if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`) {
88
- return send(res, 403, "Forbidden");
89
- }
90
- const url = new URL(req.url ?? "/", `http://${host}`);
91
- if (url.pathname === "/__stuntdouble") return json(res, { stuntdouble: true });
92
- const cookieName = `stuntdouble_${port}`;
93
- const key = url.searchParams.get("t");
94
- if (key !== null) {
95
- if (!sameSecret(key, token)) return send(res, 403, "This link is out of date.");
96
- url.searchParams.delete("t");
97
- return send(res, 302, "", void 0, {
98
- Location: `${url.pathname}${url.search}`,
99
- "Set-Cookie": `${cookieName}=${token}; HttpOnly; SameSite=Strict; Path=/`
100
- });
101
- }
102
- const signedIn = cookie(req, cookieName);
103
- if (!signedIn || !sameSecret(signedIn, token)) {
104
- return send(
105
- res,
106
- 403,
107
- "Open the viewer from the link Stunt Double printed: it carries the key."
108
- );
109
- }
110
- const path = url.pathname;
111
- if (req.method === "POST" && path === "/api/stop") {
112
- const origin = req.headers.origin;
113
- if (origin && origin !== `http://${host}`) return send(res, 403, "Forbidden");
114
- const { run } = JSON.parse(await readBody(req));
115
- const entry = (await readRuns(root)).find((e) => e.id === run);
116
- if (!entry || !("run" in entry) || effectiveStatus(entry.run) !== "running") {
117
- return send(res, 409, "That run is not going.");
118
- }
119
- await writeFileAtomic(join(root, ".live", "stop"), entry.id);
120
- return send(res, 204, "");
121
- }
122
- if (req.method !== "GET") return send(res, 405, "Method not allowed");
123
- if (path === "/" || path === "/definitions" || /^\/runs\/[^/]+$/.test(path)) {
124
- const page = await readFile(new URL("./ui.html", import.meta.url));
125
- return send(res, 200, page, "text/html; charset=utf-8", PAGE_HEADERS);
126
- }
127
- if (path === "/api/runs") {
128
- return json(
129
- res,
130
- (await readRuns(root)).map(
131
- (e) => "run" in e ? { ...e.run, status: effectiveStatus(e.run) } : e
132
- )
133
- );
134
- }
135
- const runMatch = path.match(/^\/api\/runs\/([^/]+)$/);
136
- if (runMatch) {
137
- const id = decodeURIComponent(runMatch[1]);
138
- const entry = (await readRuns(root)).find((e) => e.id === id);
139
- if (!entry) return send(res, 404, "No such run");
140
- const activity = await readFile(join(root, "runs", entry.id, "activity.jsonl"), "utf8").then(
141
- (text) => text.split("\n").filter(Boolean).map((line) => JSON.parse(line))
142
- ).catch(() => []);
143
- const stop = await readFile(join(root, ".live", "stop"), "utf8").catch(() => "");
144
- return json(res, {
145
- ...entry,
146
- ..."run" in entry ? { run: { ...entry.run, status: effectiveStatus(entry.run) } } : {},
147
- activity,
148
- stop_requested: stop.trim() === entry.id
149
- });
150
- }
151
- if (path === "/api/definitions") {
152
- return json(res, {
153
- checklists: await definitions(root, "checklists"),
154
- actors: await definitions(root, "actors")
155
- });
156
- }
157
- if (path === "/api/events") {
158
- res.writeHead(200, {
159
- "Content-Type": "text/event-stream",
160
- "Cache-Control": "no-store",
161
- "X-Content-Type-Options": "nosniff"
162
- });
163
- res.write(": connected\n\n");
164
- clients.add(res);
165
- req.on("close", () => clients.delete(res));
166
- return;
167
- }
168
- if (path.startsWith("/files/")) {
169
- const rel = decodeURIComponent(path.slice("/files/".length));
170
- const file = await confined(root, rel);
171
- if (!file) return send(res, 403, "Outside .stuntdouble/");
172
- const type = TYPES[extname(file)];
173
- if (!type) return send(res, 404, "Not found");
174
- const cache = rel.includes("/shots/") ? "private, max-age=86400" : "no-store";
175
- return send(res, 200, await readFile(file), type, { "Cache-Control": cache });
176
- }
177
- return send(res, 404, "Not found");
178
- }
179
- const server = createServer((req, res) => {
180
- handle(req, res).catch(() => {
181
- if (res.headersSent) res.end();
182
- else send(res, 500, "Something went wrong reading .stuntdouble/");
183
- });
184
- });
185
- const first = options.port ?? FIRST_PORT;
186
- for (let candidate = first; ; candidate++) {
187
- try {
188
- await new Promise((ok, fail) => {
189
- server.once("error", fail);
190
- server.listen(candidate, "127.0.0.1", () => {
191
- server.off("error", fail);
192
- ok();
193
- });
194
- });
195
- break;
196
- } catch (err) {
197
- const busy = err.code === "EADDRINUSE";
198
- if (!busy || candidate >= first + PORT_TRIES) throw err;
199
- }
200
- }
201
- port = server.address().port;
202
- let watcher = null;
203
- let pending = /* @__PURE__ */ new Set();
204
- let timer;
205
- try {
206
- watcher = watch(root, { recursive: true }, (_event, name) => {
207
- if (!name || name.endsWith(".tmp")) return;
208
- pending.add(String(name).split(sep).join("/"));
209
- timer ??= setTimeout(() => {
210
- const data = `data: ${JSON.stringify({ paths: [...pending] })}
211
-
212
- `;
213
- for (const client of clients) client.write(data);
214
- pending = /* @__PURE__ */ new Set();
215
- timer = void 0;
216
- }, 150);
217
- });
218
- } catch {
219
- }
220
- return {
221
- port,
222
- token,
223
- link: (path = "/") => `http://127.0.0.1:${port}${path}?t=${token}`,
224
- close: async () => {
225
- watcher?.close();
226
- clearTimeout(timer);
227
- for (const client of clients) client.end();
228
- server.closeAllConnections();
229
- await new Promise((ok) => server.close(() => ok()));
230
- }
231
- };
232
- }
233
-
234
- export {
235
- FIRST_PORT,
236
- startUi
237
- };
238
- //# sourceMappingURL=chunk-VWTYG7G4.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/ui-server.ts"],"sourcesContent":["// The local viewer: one HTML page over the files in `.stuntdouble/`, with live\n// updates while a run is going and a Stop button.\n//\n// It listens on loopback only, and still trusts nothing that can reach\n// loopback: a page in the person's browser can, and so can a DNS rebinding\n// attack. So every request must name this server in its Host header, and\n// everything but the identity probe needs the per-start key, which the printed\n// link carries once and an HttpOnly, SameSite=Strict cookie carries after\n// that. No CORS headers are ever sent, and files are served from inside\n// `.stuntdouble/` only.\n\nimport { randomBytes, timingSafeEqual } from 'node:crypto';\nimport { watch, type FSWatcher } from 'node:fs';\nimport { readdir, readFile, realpath } from 'node:fs/promises';\nimport { createServer, type IncomingMessage, type ServerResponse } from 'node:http';\nimport type { AddressInfo } from 'node:net';\nimport { basename, extname, join, resolve, sep } from 'node:path';\n\nimport {\n ActorSchema,\n ChecklistSchema,\n effectiveStatus,\n readJson,\n readRuns,\n writeFileAtomic,\n} from './files.ts';\n\nexport const FIRST_PORT = 4747;\nconst PORT_TRIES = 20;\n\nexport type UiServer = {\n port: number;\n token: string;\n /** An address that signs the browser in, opening at `path`. */\n link(path?: string): string;\n close(): Promise<void>;\n};\n\nconst TYPES: Record<string, string> = {\n '.png': 'image/png',\n '.json': 'application/json',\n '.jsonl': 'text/plain; charset=utf-8',\n};\n\nconst PAGE_HEADERS = {\n 'Content-Security-Policy':\n \"default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src 'self'; frame-ancestors 'none'; form-action 'none'\",\n};\n\nfunction send(\n res: ServerResponse,\n status: number,\n body: string | Uint8Array,\n type = 'text/plain; charset=utf-8',\n headers: Record<string, string> = {}\n): void {\n res.writeHead(status, {\n 'Content-Type': type,\n 'Cache-Control': 'no-store',\n 'X-Content-Type-Options': 'nosniff',\n 'Referrer-Policy': 'no-referrer',\n ...headers,\n });\n res.end(body);\n}\n\nconst json = (res: ServerResponse, data: unknown) =>\n send(res, 200, JSON.stringify(data), 'application/json');\n\nfunction sameSecret(given: string, token: string): boolean {\n const a = Buffer.from(given);\n const b = Buffer.from(token);\n return a.length === b.length && timingSafeEqual(a, b);\n}\n\nfunction cookie(req: IncomingMessage, name: string): string | undefined {\n for (const part of (req.headers.cookie ?? '').split(';')) {\n const [key, ...value] = part.trim().split('=');\n if (key === name) return value.join('=');\n }\n return undefined;\n}\n\n/** The real path of `rel` under `root`, or null when it would leave it (`..`, a symlink). */\nasync function confined(root: string, rel: string): Promise<string | null> {\n try {\n const realRoot = await realpath(root);\n const real = await realpath(resolve(root, rel));\n return real.startsWith(realRoot + sep) ? real : null;\n } catch {\n return null;\n }\n}\n\nasync function definitions(root: string, folder: 'checklists' | 'actors') {\n const schema = folder === 'checklists' ? ChecklistSchema : ActorSchema;\n const names = await readdir(join(root, folder)).catch(() => [] as string[]);\n return Promise.all(\n names\n .filter((n) => n.endsWith('.json'))\n .sort()\n .map(async (name) => {\n const slug = basename(name, '.json');\n try {\n return { slug, data: await readJson(join(root, folder, name), schema, name) };\n } catch (err) {\n return { slug, error: (err as Error).message };\n }\n })\n );\n}\n\nasync function readBody(req: IncomingMessage): Promise<string> {\n let body = '';\n for await (const chunk of req) {\n body += chunk;\n if (body.length > 10_000) throw new Error('Body too large');\n }\n return body;\n}\n\nexport async function startUi(options: { root: string; port?: number }): Promise<UiServer> {\n const root = resolve(options.root);\n const token = randomBytes(24).toString('base64url');\n const clients = new Set<ServerResponse>();\n let port = 0;\n\n async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const host = req.headers.host ?? '';\n if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`) {\n return send(res, 403, 'Forbidden');\n }\n const url = new URL(req.url ?? '/', `http://${host}`);\n if (url.pathname === '/__stuntdouble') return json(res, { stuntdouble: true });\n\n // Cookies are per host, not per port, so each server names its own.\n const cookieName = `stuntdouble_${port}`;\n const key = url.searchParams.get('t');\n if (key !== null) {\n if (!sameSecret(key, token)) return send(res, 403, 'This link is out of date.');\n url.searchParams.delete('t');\n return send(res, 302, '', undefined, {\n Location: `${url.pathname}${url.search}`,\n 'Set-Cookie': `${cookieName}=${token}; HttpOnly; SameSite=Strict; Path=/`,\n });\n }\n const signedIn = cookie(req, cookieName);\n if (!signedIn || !sameSecret(signedIn, token)) {\n return send(\n res,\n 403,\n 'Open the viewer from the link Stunt Double printed: it carries the key.'\n );\n }\n\n const path = url.pathname;\n if (req.method === 'POST' && path === '/api/stop') {\n const origin = req.headers.origin;\n if (origin && origin !== `http://${host}`) return send(res, 403, 'Forbidden');\n const { run } = JSON.parse(await readBody(req)) as { run?: unknown };\n const entry = (await readRuns(root)).find((e) => e.id === run);\n if (!entry || !('run' in entry) || effectiveStatus(entry.run) !== 'running') {\n return send(res, 409, 'That run is not going.');\n }\n await writeFileAtomic(join(root, '.live', 'stop'), entry.id);\n return send(res, 204, '');\n }\n if (req.method !== 'GET') return send(res, 405, 'Method not allowed');\n\n if (path === '/' || path === '/definitions' || /^\\/runs\\/[^/]+$/.test(path)) {\n const page = await readFile(new URL('./ui.html', import.meta.url));\n return send(res, 200, page, 'text/html; charset=utf-8', PAGE_HEADERS);\n }\n if (path === '/api/runs') {\n return json(\n res,\n (await readRuns(root)).map((e) =>\n 'run' in e ? { ...e.run, status: effectiveStatus(e.run) } : e\n )\n );\n }\n const runMatch = path.match(/^\\/api\\/runs\\/([^/]+)$/);\n if (runMatch) {\n const id = decodeURIComponent(runMatch[1]!);\n const entry = (await readRuns(root)).find((e) => e.id === id);\n if (!entry) return send(res, 404, 'No such run');\n const activity = await readFile(join(root, 'runs', entry.id, 'activity.jsonl'), 'utf8')\n .then((text) =>\n text\n .split('\\n')\n .filter(Boolean)\n .map((line) => JSON.parse(line))\n )\n .catch(() => []);\n const stop = await readFile(join(root, '.live', 'stop'), 'utf8').catch(() => '');\n return json(res, {\n ...entry,\n ...('run' in entry ? { run: { ...entry.run, status: effectiveStatus(entry.run) } } : {}),\n activity,\n stop_requested: stop.trim() === entry.id,\n });\n }\n if (path === '/api/definitions') {\n return json(res, {\n checklists: await definitions(root, 'checklists'),\n actors: await definitions(root, 'actors'),\n });\n }\n if (path === '/api/events') {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-store',\n 'X-Content-Type-Options': 'nosniff',\n });\n res.write(': connected\\n\\n');\n clients.add(res);\n req.on('close', () => clients.delete(res));\n return;\n }\n if (path.startsWith('/files/')) {\n const rel = decodeURIComponent(path.slice('/files/'.length));\n const file = await confined(root, rel);\n if (!file) return send(res, 403, 'Outside .stuntdouble/');\n const type = TYPES[extname(file)];\n if (!type) return send(res, 404, 'Not found');\n // A verdict's screenshot never changes once written; the live frame does.\n const cache = rel.includes('/shots/') ? 'private, max-age=86400' : 'no-store';\n return send(res, 200, await readFile(file), type, { 'Cache-Control': cache });\n }\n return send(res, 404, 'Not found');\n }\n\n const server = createServer((req, res) => {\n handle(req, res).catch(() => {\n if (res.headersSent) res.end();\n else send(res, 500, 'Something went wrong reading .stuntdouble/');\n });\n });\n\n const first = options.port ?? FIRST_PORT;\n for (let candidate = first; ; candidate++) {\n try {\n await new Promise<void>((ok, fail) => {\n server.once('error', fail);\n server.listen(candidate, '127.0.0.1', () => {\n server.off('error', fail);\n ok();\n });\n });\n break;\n } catch (err) {\n const busy = (err as NodeJS.ErrnoException).code === 'EADDRINUSE';\n if (!busy || candidate >= first + PORT_TRIES) throw err;\n }\n }\n port = (server.address() as AddressInfo).port;\n\n // Every change under .stuntdouble/ is announced, and the page refetches what\n // it shows. This works the same whether the run is in this process or not.\n let watcher: FSWatcher | null = null;\n let pending = new Set<string>();\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n watcher = watch(root, { recursive: true }, (_event, name) => {\n if (!name || name.endsWith('.tmp')) return;\n pending.add(String(name).split(sep).join('/'));\n timer ??= setTimeout(() => {\n const data = `data: ${JSON.stringify({ paths: [...pending] })}\\n\\n`;\n for (const client of clients) client.write(data);\n pending = new Set();\n timer = undefined;\n }, 150);\n });\n } catch {\n // No .stuntdouble/ yet: the viewer shows the empty state without live updates.\n }\n\n return {\n port,\n token,\n link: (path = '/') => `http://127.0.0.1:${port}${path}?t=${token}`,\n close: async () => {\n watcher?.close();\n clearTimeout(timer);\n for (const client of clients) client.end();\n server.closeAllConnections();\n await new Promise<void>((ok) => server.close(() => ok()));\n },\n };\n}\n"],"mappings":"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","names":[]}