// Sage, 2026-10-11: read-only. svc-mosaic-stack lists its teams and each bot's api tokens (ids, titles, // expiries, scopes). Token values never come back from a list; the output is still checked for secrets. // Usage: node bot-keys.mjs https://tasks.woltje.com SVC_PASSWORD_FILE import { readFileSync } from "node:fs"; const [ORIGIN, PW] = process.argv.slice(2); if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only"); const BASE = ORIGIN + "/api/v2"; const BOTS = { pm: 6, cto: 7, coder: 8, reviewer: 9, sync: 10 }; 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), redirect: "error" }); 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 ?? ""} ${r.json?.message ?? ""}`); return r.json; }; const password = readFileSync(PW, "utf8").trim(); const tok = must(await api("POST", "/login", { username: "svc-mosaic-stack", password }), "login svc").token; if (!tok) throw new Error("login svc: no token field"); const rec = { at: new Date().toISOString(), teams: [], bots: {} }; const teams = must(await api("GET", "/teams?per_page=50", undefined, tok), "teams").items; if (!Array.isArray(teams)) throw new Error("teams: no items array"); for (const t of teams) rec.teams.push({ id: t.id, name: t.name, members: (t.members ?? []).map((m) => [m.username, m.admin]) }); for (const [role, id] of Object.entries(BOTS)) { const l = must(await api("GET", `/tokens?owner_id=${id}&per_page=50`, undefined, tok), `tokens ${role}`).items; if (!Array.isArray(l)) throw new Error(`tokens ${role}: no items array`); rec.bots[role] = l.map((k) => ({ id: k.id, title: k.title, expires: k.expires_at, permissions: k.permissions, hasValue: typeof k.token === "string" && k.token.length > 0 })); } const out = JSON.stringify(rec, null, 1) + "\n"; if ([password, tok].some((s) => out.includes(s))) throw new Error("secret in output; not printed"); process.stdout.write(out);