Files
stack/agents/sage/work/vikunja-setup/bot-keys.mjs
T
jason.woltjeandClaude Opus 5.5 20735a3706 vikunja: Q58a/Q60 teams 1 and 2, pm key 7 replaces key 2 (sage)
svc-mosaic-stack created mosaic-agents (team 1, bots pm/cto/coder/reviewer)
and mosaic-sync (team 2, bot sync), made jason.woltje team admin, then left
both teams so a team share gives svc no project access. The pm key gained
projects read_all, read_one and update; no key has task or project delete
or a share route (read back via GET /tokens?owner_id). Old pm key 2 revoked,
now 401. Record holds ids and scopes only; checked for secrets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 12:56:04 -05:00

32 lines
2.2 KiB
JavaScript

// 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);