/** * npm run setup:owner — interactively sets (or resets) the single owner account. * Prompts for a password without echo, stores only its Argon2id hash. Never logs the password. */ import postgres from "postgres"; import { hash } from "@node-rs/argon2"; import readline from "node:readline"; const url = process.env.DATABASE_URL; if (!url) { console.error("DATABASE_URL is required"); process.exit(1); } const sql = postgres(url, { max: 1, onnotice: () => {} }); function ask(q: string, hidden = false): Promise { return new Promise(resolve => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true }); if (hidden) { const anyRl = rl as unknown as { _writeToOutput: (s: string) => void; output: NodeJS.WritableStream }; const orig = anyRl._writeToOutput; anyRl._writeToOutput = (s: string) => { if (s.includes(q)) orig.call(rl, q); else if (s === "\r\n" || s === "\n") orig.call(rl, s); }; } rl.question(q, a => { rl.close(); if (hidden) process.stdout.write("\n"); resolve(a); }); }); } async function main() { // Non-interactive mode for automation: OWNER_USERNAME + OWNER_PASSWORD env (never commit them). if (process.env.OWNER_PASSWORD) { if (process.env.OWNER_PASSWORD.length < 12) { console.error("OWNER_PASSWORD too short"); process.exit(1); } const h = await hash(process.env.OWNER_PASSWORD, { memoryCost: 65536, timeCost: 3, parallelism: 2, algorithm: 2 }); await sql`INSERT INTO owner (id, username, password_hash) VALUES (1, ${process.env.OWNER_USERNAME ?? "simon"}, ${h}) ON CONFLICT (id) DO UPDATE SET username = EXCLUDED.username, password_hash = EXCLUDED.password_hash, updated_at = now()`; await sql`UPDATE sessions SET revoked_at = now() WHERE revoked_at IS NULL`; console.log("Owner saved (non-interactive). All sessions revoked."); await sql.end(); return; } const existing = await sql`SELECT username FROM owner WHERE id = 1`; if (existing.length) console.log(`Owner already configured (${existing[0].username}). Continuing will RESET the password and revoke all sessions.`); const username = (await ask(`Username [${existing[0]?.username ?? "simon"}]: `)).trim() || existing[0]?.username || "simon"; const p1 = await ask("New password (min 12 chars): ", true); if (p1.length < 12) { console.error("Password too short."); process.exit(1); } const p2 = await ask("Confirm password: ", true); if (p1 !== p2) { console.error("Passwords do not match."); process.exit(1); } const h = await hash(p1, { memoryCost: 65536, timeCost: 3, parallelism: 2, algorithm: 2 }); await sql`INSERT INTO owner (id, username, password_hash) VALUES (1, ${username}, ${h}) ON CONFLICT (id) DO UPDATE SET username = EXCLUDED.username, password_hash = EXCLUDED.password_hash, updated_at = now()`; await sql`UPDATE sessions SET revoked_at = now() WHERE revoked_at IS NULL`; console.log(`Owner "${username}" saved (Argon2id). All sessions revoked.`); await sql.end(); } main().catch(e => { console.error(e); process.exit(1); });