// Runbook section 1 (docs/guides/slice-1-identities.md) via the admin API. // Prints names, ids and HTTP statuses only. No secret reaches argv, stdout or a URL. import fs from "node:fs"; import crypto from "node:crypto"; import os from "node:os"; const BASE = "https://git.mosaicstack.dev/api/v1"; const REPO = "mosaicstack/stack"; const S = `${os.homedir()}/.config/mosaic-dev/secrets/mosaic-stack`; const DATE = new Date().toISOString().slice(0, 10); const ADMIN_FILE = `${os.homedir()}/.mosaic/fleet/agents/jarvis/secrets/gitea-mosaicstack-jarvis.token`; const st = fs.lstatSync(ADMIN_FILE); if (!st.isFile() || (st.mode & 0o077) !== 0) throw new Error("admin token file refused"); const adminTok = fs.readFileSync(ADMIN_FILE, "utf8").trim(); if (!/^[0-9a-f]{40}$/.test(adminTok)) throw new Error("admin token shape refused"); const sd = fs.lstatSync(S); if (!sd.isDirectory() || (sd.mode & 0o077) !== 0) throw new Error("secrets dir refused"); const ROLES = [ { r: "pm", perm: "write", scopes: ["write:issue", "read:repository", "read:user"] }, { r: "cto", perm: "write", scopes: ["write:issue", "write:repository", "read:user"] }, { r: "coder", perm: "write", scopes: ["write:issue", "write:repository", "read:user"] }, { r: "reviewer", perm: "read", scopes: ["write:issue", "write:repository", "read:user"] }, ]; async function call(method, path, body, auth = `token ${adminTok}`) { const res = await fetch(`${BASE}/${path}`, { method, headers: { Authorization: auth, "Content-Type": "application/json", Accept: "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), }); const text = await res.text(); let json = null; try { json = text ? JSON.parse(text) : null; } catch {} return { status: res.status, json }; } const only = process.argv.slice(2); for (const { r, perm, scopes } of ROLES) { if (only.length && !only.includes(r)) continue; const u = `mosaic-stack-${r}-bot`; const file = `${S}/${r}-gitea.token`; if (fs.existsSync(file)) { console.log(`${u}: token file exists, skipped`); continue; } let pw = crypto.randomBytes(36).toString("base64url"); const got = await call("GET", `users/${u}`); let res; if (got.status === 404) { res = await call("POST", "admin/users", { username: u, full_name: `mosaic-stack ${r} bot`, email: `${u}@noreply.mosaicstack.dev`, password: pw, must_change_password: false, send_notify: false, restricted: true, visibility: "private", }); console.log(`${u}: create HTTP ${res.status} id=${res.json?.id} admin=${res.json?.is_admin} restricted=${res.json?.restricted}`); if (res.status !== 201) { console.log(` message: ${res.json?.message}`); process.exit(1); } } else if (got.status === 200) { res = await call("PATCH", `admin/users/${u}`, { login_name: u, source_id: 0, password: pw, must_change_password: false }); console.log(`${u}: exists id=${got.json?.id}, password reset HTTP ${res.status}`); if (res.status !== 200) process.exit(1); } else { console.log(`${u}: lookup HTTP ${got.status}`); process.exit(1); } res = await call("PUT", `repos/${REPO}/collaborators/${u}`, { permission: perm }); console.log(`${u}: collaborator ${perm} HTTP ${res.status}`); if (res.status !== 204) process.exit(1); const basic = "Basic " + Buffer.from(`${u}:${pw}`).toString("base64"); pw = null; res = await call("POST", `users/${u}/tokens`, { name: `mosaic-stack-${r}-${DATE}`, scopes }, basic); const tok = res.json?.sha1; console.log(`${u}: token "mosaic-stack-${r}-${DATE}" HTTP ${res.status} id=${res.json?.id} scopes=${(res.json?.scopes || []).join(",")}`); if (res.status !== 201 || typeof tok !== "string" || tok.length === 0) { console.log(` message: ${res.json?.message}`); process.exit(1); } fs.writeFileSync(file, tok, { flag: "wx", mode: 0o600 }); }