/* Create an operator for the operations console. * * OPERATOR_PASSWORD='…' node scripts/create-operator.cjs kyle@threadcount.tech "Kyle" OWNER * * The password comes from the environment and never from an argument: argv is visible to every * process on the box through `ps`, and this is the break-glass credential for the whole platform. * * Seeds, never resets. It refuses if the address already has an operator, so re-running it cannot * quietly change a password. TOTP is enrolled at first sign-in; nothing here sets it. The address * must be one the Cloudflare Access policy on ops.threadcount.tech allows, or the row is an * operator who cannot reach the door. */ require("dotenv/config"); const bcrypt = require("bcryptjs"); const { PrismaClient } = require("@prisma/client"); const { PrismaPg } = require("@prisma/adapter-pg"); const [emailArg, nameArg, roleArg] = process.argv.slice(2); const email = String(emailArg || "").trim().toLowerCase(); const name = String(nameArg || "").trim(); const role = String(roleArg || "SUPPORT").trim().toUpperCase(); const password = process.env.OPERATOR_PASSWORD || ""; function die(msg) { console.error(msg); process.exit(1); } if (!email || !email.includes("@")) die("usage: OPERATOR_PASSWORD='…' node scripts/create-operator.cjs [OWNER|SUPPORT]"); if (!name) die("a display name is required"); if (role !== "OWNER" && role !== "SUPPORT") die("role must be OWNER or SUPPORT"); // Eight, the product's own minimum for a coordinator account (components/AuthForm.tsx). Not a // stricter number invented here: the operator's real protection is the second factor enrolled at // first sign-in and Cloudflare Access in front of the door, not the length of the fire-escape key. if (password.length < 8) die("OPERATOR_PASSWORD must be set in the environment and be at least 8 characters"); const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL, max: 1 }) }); (async () => { const existing = await prisma.operator.findUnique({ where: { email }, select: { id: true } }); if (existing) die(`an operator already exists for ${email} — this script seeds, it does not reset`); // Cost 12, the same as a coordinator account at signup. const passwordHash = await bcrypt.hash(password, 12); const o = await prisma.operator.create({ data: { email, name, role, passwordHash }, select: { id: true, email: true, role: true } }); console.log(`created operator ${o.email} (${o.role}) id=${o.id}`); console.log("TOTP is enrolled at first sign-in."); })() .catch((e) => die(e && e.message ? e.message : String(e))) .finally(() => prisma.$disconnect());