/** Creates/rotates the two application roles. Usage: DATABASE_URL_ADMIN=postgres://@host/db pnpm db:roles [--rotate] * Prints the generated passwords ONCE to stdout (write them into .env) — never stored anywhere else. */ import "./_env"; import postgres from "postgres"; import { randomBytes } from "node:crypto"; const admin = process.env.DATABASE_URL_ADMIN; if (!admin) { console.error("DATABASE_URL_ADMIN required (superuser/owner connection)"); process.exit(1); } const sql = postgres(admin, { max: 1 }); const rotate = process.argv.includes("--rotate"); const pw = () => randomBytes(24).toString("base64url"); const out: Record = {}; for (const role of ["tools_app", "tools_reader"]) { const exists = (await sql`SELECT 1 FROM pg_roles WHERE rolname=${role}`).length > 0; if (!exists) { const p = pw(); await sql.unsafe(`CREATE ROLE ${role} LOGIN PASSWORD '${p}' NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT`); out[role] = p; } else if (rotate) { const p = pw(); await sql.unsafe(`ALTER ROLE ${role} PASSWORD '${p}'`); out[role] = p; } } await sql.unsafe(`ALTER ROLE tools_reader RESET default_transaction_read_only`); // reader may INSERT into error_reports/search_events (explicit grants); everything else is denied by privileges await sql.unsafe(`ALTER ROLE tools_reader SET statement_timeout = '5s'`); await sql.unsafe(`ALTER ROLE tools_app SET statement_timeout = '60s'`); const db = (await sql`SELECT current_database() AS d`)[0].d; await sql.unsafe(`GRANT CONNECT ON DATABASE "${db}" TO tools_app, tools_reader`); console.log(Object.keys(out).length ? JSON.stringify(out, null, 2) : "roles exist (use --rotate to rotate passwords)"); await sql.end();