#!/usr/bin/env node // Terminal helper for agent/local development against the protected API. // Device grant (RFC 8628) against the go-auth issuer. The refresh token is // kept in ~/.go-auth/dev/.json (0700 dir, 0600 file). Never commit it. // // node scripts/dev-auth.mjs login approve once in a browser // node scripts/dev-auth.mjs token print a fresh access token // node scripts/dev-auth.mjs whoami print the token claims // node scripts/dev-auth.mjs forget delete the stored refresh token // node scripts/dev-auth.mjs get PATH authenticated GET against the otel API // (signs in first if needed), e.g. get /api/issues // // Env: AUTH_DEV_BASE (issuer, default https://users.k6n.net), // AUTH_DEVICE_CLIENT_ID (default dev-console), // OTEL_URL (API for `get`, default https://otel.k6n.net), // GO_AUTH_TOKEN (explicit token escape hatch; `token` prints it as-is). import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { join } from "node:path"; const BASE = (process.env.AUTH_DEV_BASE ?? "https://users.k6n.net").replace(/\/$/, ""); const CLIENT = process.env.AUTH_DEVICE_CLIENT_ID ?? "dev-console"; const DIR = join(homedir(), ".go-auth", "dev"); const FILE = join(DIR, `${CLIENT}.json`); const GRANT = "urn:ietf:params:oauth:grant-type:device_code"; // RFC 8628 endpoints take form bodies; /device/refresh takes JSON. const post = (path, body, json = false) => fetch(`${BASE}/api/auth${path}`, { method: "POST", headers: { "Content-Type": json ? "application/json" : "application/x-www-form-urlencoded" }, body: json ? JSON.stringify(body) : new URLSearchParams(body), }); async function save(refresh) { await mkdir(DIR, { recursive: true, mode: 0o700 }); await chmod(DIR, 0o700); const tmp = `${FILE}.tmp`; await writeFile(tmp, JSON.stringify({ refresh_token: refresh }), { mode: 0o600 }); await rename(tmp, FILE); } async function login() { const start = await post("/device", { client_id: CLIENT }); if (!start.ok) throw new Error(`device start failed: HTTP ${start.status}`); const d = await start.json(); console.error(`Approve in a browser: ${d.verification_uri_complete ?? d.verification_uri}\nCode: ${d.user_code}`); let interval = (d.interval ?? 5) * 1000; const deadline = Date.now() + d.expires_in * 1000; while (Date.now() < deadline) { await new Promise((r) => setTimeout(r, interval)); const t = await post("/device/token", { grant_type: GRANT, device_code: d.device_code, client_id: CLIENT }); const body = await t.json().catch(() => ({})); if (t.ok) { await save(body.refresh_token); console.error("Signed in; refresh token stored."); return; } if (body.error === "slow_down") interval += 5000; else if (body.error !== "authorization_pending") throw new Error([body.error ?? `HTTP ${t.status}`, body.error_description].filter(Boolean).join(": ")); } throw new Error("device code expired"); } async function token() { if (process.env.GO_AUTH_TOKEN) return process.env.GO_AUTH_TOKEN; let stored; try { stored = JSON.parse(await readFile(FILE, "utf8")); } catch { throw new Error("not signed in: run `node scripts/dev-auth.mjs login`"); } const res = await post("/device/refresh", { refresh_token: stored.refresh_token }, true); if (!res.ok) throw new Error(`refresh failed (HTTP ${res.status}); run login again`); const body = await res.json(); if (body.refresh_token) await save(body.refresh_token); return body.access_token; } const API = (process.env.OTEL_URL ?? "https://otel.k6n.net").replace(/\/$/, ""); async function haveSession() { try { await token(); return true; } catch { return false; } } // Authenticated GET. Signs in first when there is no working session, so an // agent can just run `get /api/issues` and ask the human to approve the link. async function get(path) { if (!path?.startsWith("/")) throw new Error("usage: dev-auth.mjs get /api/..."); if (!(await haveSession())) await login(); const res = await fetch(`${API}${path}`, { headers: { Authorization: `Bearer ${await token()}` } }); const text = await res.text(); if (!res.ok) throw new Error(`HTTP ${res.status} ${text.slice(0, 300)}`); console.log(text); } const cmds = { get: () => get(process.argv[3]), login, token: async () => console.log(await token()), whoami: async () => { const t = await token(); console.log(JSON.stringify(JSON.parse(Buffer.from(t.split(".")[1], "base64url").toString()), null, 2)); }, forget: () => rm(FILE, { force: true }), }; const cmd = cmds[process.argv[2]]; if (!cmd) { console.error("usage: dev-auth.mjs login|token|whoami|forget|get PATH"); process.exit(2); } cmd().catch((e) => { console.error(e.message); process.exit(1); });