// Darkwing, S3: rehearse packages/tasks/live/run.mjs on the scratch container. 127.0.0.1 only. // Builds the runbook's install (svc owner, bots it owns, a label it owns, scoped tokens), then runs // the live script with the tokens in its environment. Passwords and tokens stay in memory. import { randomBytes } from "node:crypto"; import { writeFileSync } from "node:fs"; import { spawnSync } from "node:child_process"; const [ORIGIN, RUN, OUT] = process.argv.slice(2); if (!/^http:\/\/127\.0\.0\.1:\d+$/.test(ORIGIN)) throw new Error("scratch origin on 127.0.0.1 only"); const BASE = ORIGIN + "/api/v2"; const secrets = []; async function api(method, path, body, auth) { const headers = { "Content-Type": "application/json" }; if (auth) headers.Authorization = `Bearer ${auth}`; const r = await fetch(BASE + path, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) }); const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {} return { status: r.status, json }; } const must = (r, l) => { if (r.status >= 300) throw new Error(`${l}: ${r.status} ${r.json?.code ?? ""}`); return r.json; }; const password = randomBytes(18).toString("base64url"); secrets.push(password); must(await api("POST", "/register", { username: "svc-demo", email: "svc-demo@example.invalid", password }), "register"); const O = must(await api("POST", "/login", { username: "svc-demo", password }), "login").token; secrets.push(O); const P = must(await api("POST", "/projects", { title: "demo" }, O), "project").id; const K = must(await api("GET", `/projects/${P}/views`, undefined, O), "views").items.find((v) => v.view_kind === "kanban").id; const rename = { "To-Do": "todo", Doing: "in-progress", Done: "done" }; for (const b of must(await api("GET", `/projects/${P}/views/${K}/buckets`, undefined, O), "buckets").items) must(await api("PUT", `/projects/${P}/views/${K}/buckets/${b.id}`, { title: rename[b.title] }, O), "rename"); for (const title of ["in-review", "blocked"]) must(await api("POST", `/projects/${P}/views/${K}/buckets`, { title }, O), title); const label = must(await api("POST", "/labels", { title: "slice-1" }, O), "label").id; const SCOPES = { sync: { projects: ["read_one", "views_buckets", "views_buckets_tasks_get"], projects_views: ["read_all"], tasks: ["read_all", "read_one"], tasks_comments: ["read_all"] }, pm: { tasks: ["read_one", "create", "update"], tasks_assignees: ["create", "delete"], tasks_relations: ["create", "delete"], tasks_labels: ["create", "delete"], tasks_comments: ["create"], labels: ["read_all"], projects: ["views_buckets_tasks"] }, worker: { tasks: ["read_one", "update"], tasks_comments: ["create"], projects: ["views_buckets_tasks"] }, }; const env = { ...process.env }; const setup = { business: "demo", baseUrl: ORIGIN, project: P, roles: {}, labels: { "slice-1": label } }; for (const r of ["sync", "pm", "coder", "reviewer"]) { const b = must(await api("POST", "/user/bots", { username: `bot-demo-${r}`, name: `demo ${r}` }, O), `bot ${r}`); must(await api("POST", `/projects/${P}/users`, { username: b.username, permission: r === "sync" ? 0 : 1 }, O), `share ${r}`); const t = await api("POST", "/tokens", { title: `s3-live-${r}`, owner_id: b.id, expires_at: "2030-01-01T00:00:00Z", permissions: SCOPES[r] ?? SCOPES.worker }, O); if (t.status !== 201) throw new Error(`mint ${r} ${t.status}`); secrets.push(t.json.token); env["VK_LIVE_" + r.toUpperCase()] = t.json.token; const entry = { botId: b.id, env: "VK_LIVE_" + r.toUpperCase(), expires: "2029-12-31" }; if (r === "sync") setup.sync = entry; else setup.roles[r] = entry; } writeFileSync(`${OUT}/setup.json`, JSON.stringify(setup, null, 1) + "\n", { mode: 0o600 }); const run = spawnSync(process.execPath, [RUN, `${OUT}/setup.json`, OUT, "--base", ORIGIN], { env, encoding: "utf8" }); const all = (run.stdout ?? "") + (run.stderr ?? ""); if (secrets.some((s) => all.includes(s))) { console.error("secret in the run output; not shown"); process.exit(3); } process.stdout.write(all); console.log("exit", run.status); process.exit(run.status ?? 1);