Files
go-otel/scripts/dev-auth.mjs
T
matsandClaude Sonnet 5 369c7a21ad
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 20s
dev-auth: use form encoding for device start/poll, JSON for refresh
Verified end to end against users.k6n.net: device grant approved in a
browser, refresh token stored 0600, access token carries aud
[dev-console, https://otel.k6n.net].

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 09:14:44 +00:00

95 lines
3.8 KiB
JavaScript

#!/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/<client>.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
//
// Env: AUTH_DEV_BASE (issuer, default https://users.k6n.net),
// AUTH_DEVICE_CLIENT_ID (default dev-console),
// 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 cmds = {
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");
process.exit(2);
}
cmd().catch((e) => {
console.error(e.message);
process.exit(1);
});