#!/usr/bin/env -S npx tsx --tsconfig tsconfig.scripts.json /** * Seed the first bootstrap invite (OPERATOR.md ยง 7). * * Tessera is invite-only โ€” no one can sign in until at least one invite has * been minted *and* accepted. The admin UI is unreachable until that first * sign-in, so the very first invite has to be inserted out-of-band. This * script does that via `wrangler d1 execute` โ€” no D1 binding required. * * Pair the run with `BOOTSTRAP_ADMIN_EMAIL` set to the same address in * wrangler.jsonc `vars` (remote) or .dev.vars (local) so first signup * matching that email gets auto-promoted to role=admin by the Better Auth * databaseHooks.user.create.before hook. * * Usage: * npm run db:seed-initial-user -- --email ops@limic.dev * npm run db:seed-initial-user:local -- --email ops@example.com * * After the operator accepts the invite, remove BOOTSTRAP_ADMIN_EMAIL and * redeploy โ€” subsequent signups must not auto-promote. */ import { execFile as execFileCallback } from "node:child_process"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import process from "node:process"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; import { encodeBase64Url, sha256 } from "@/worker/services/crypto"; const execFile = promisify(execFileCallback); const DEFAULT_DATABASE = "tessera-prod"; const DEFAULT_EXPIRY_DAYS = 7; const MAX_EXPIRY_DAYS = 90; const DEFAULT_REMOTE_ISSUER = "https://auth.limic.dev"; const DEFAULT_LOCAL_ISSUER = "http://localhost:5174"; const BOOTSTRAP_CREATED_BY = "bootstrap"; const INVITE_TOKEN_BYTES = 32; const INVITE_ID_BYTES = 12; const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(SCRIPT_DIR, ".."); const HELP_TEXT = `Seed the first bootstrap invite for tessera. Usage: npm run db:seed-initial-user -- --email [options] npm run db:seed-initial-user:local -- --email [options] Options: --local Seed the local D1 database (default for the :local script) --remote Seed the remote D1 database (default for the bare script) --email EMAIL Operator email; bound to the invite and matched by BOOTSTRAP_ADMIN_EMAIL for auto-promotion (required) --database NAME D1 database binding/name (default: ${DEFAULT_DATABASE}) --expires-in-days N Invite TTL in days (default: ${DEFAULT_EXPIRY_DAYS}, max: ${MAX_EXPIRY_DAYS}) --issuer URL Base URL used to print the invite link (default: ${DEFAULT_REMOTE_ISSUER} for --remote, ${DEFAULT_LOCAL_ISSUER} for --local) --force Replace existing unused bootstrap invites --dry-run Build the SQL but do not execute it --print-sql Print the generated SQL --json Emit a machine-readable JSON payload --help Show this help text `; interface Options { mode: "local" | "remote"; email: string; database: string; expiresInDays: number; issuer: string; force: boolean; dryRun: boolean; printSql: boolean; json: boolean; } interface ParsedArguments { options: Options | null; requestedHelp: boolean; } interface WranglerStatementResult { success: boolean; results?: Array>; error?: string; } interface SeedBootstrapInviteJsonOutput { mode: Options["mode"]; database: string; inviteId: string; email: string; token: string; inviteUrl: string; createdAt: string; expiresAt: string; createdBy: string; dryRun: boolean; } class CliError extends Error {} const printHelp = (): void => { process.stdout.write(HELP_TEXT); }; const escapeSqlString = (value: string): string => `'${value.replace(/'/g, "''")}'`; const parseNumberFlag = (name: string, rawValue: string): number => { const value = Number(rawValue); if (!Number.isFinite(value)) { throw new CliError(`${name} must be a valid number.`); } return value; }; const parseArguments = (argv: string[]): ParsedArguments => { if (argv.length === 0) { return { options: null, requestedHelp: false }; } let mode: Options["mode"] | null = null; let email: string | null = null; let database = DEFAULT_DATABASE; let expiresInDays = DEFAULT_EXPIRY_DAYS; let issuer: string | null = null; let force = false; let dryRun = false; let printSql = false; let json = false; let requestedHelp = false; for (let index = 0; index < argv.length; index += 1) { const argument = argv[index]; switch (argument) { case "--help": case "-h": requestedHelp = true; break; case "--local": if (mode && mode !== "local") { throw new CliError("Specify exactly one of --local or --remote."); } mode = "local"; break; case "--remote": if (mode && mode !== "remote") { throw new CliError("Specify exactly one of --local or --remote."); } mode = "remote"; break; case "--force": force = true; break; case "--dry-run": dryRun = true; break; case "--print-sql": printSql = true; break; case "--json": json = true; break; case "--email": case "--database": case "--expires-in-days": case "--issuer": { const rawValue = argv[index + 1]; if (!rawValue || rawValue.startsWith("--")) { throw new CliError(`${argument} requires a value.`); } index += 1; if (argument === "--email") { email = rawValue; } else if (argument === "--database") { database = rawValue; } else if (argument === "--expires-in-days") { expiresInDays = parseNumberFlag(argument, rawValue); } else { issuer = rawValue; } break; } default: throw new CliError(`Unknown argument: ${argument}`); } } if (requestedHelp) { return { options: null, requestedHelp: true }; } if (!mode) { return { options: null, requestedHelp: false }; } if (!email) { throw new CliError("--email is required."); } const normalizedEmail = email.trim().toLowerCase(); if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) { throw new CliError(`--email "${email}" is not a valid email address.`); } if (!database.trim()) { throw new CliError("--database cannot be empty."); } if (!Number.isFinite(expiresInDays) || expiresInDays <= 0 || expiresInDays > MAX_EXPIRY_DAYS) { throw new CliError(`--expires-in-days must be between 1 and ${MAX_EXPIRY_DAYS}.`); } return { options: { mode, email: normalizedEmail, database: database.trim(), expiresInDays, issuer: issuer ?? (mode === "remote" ? DEFAULT_REMOTE_ISSUER : DEFAULT_LOCAL_ISSUER), force, dryRun, printSql, json, }, requestedHelp: false, }; }; const createWranglerArgs = ( options: Pick, payload: { command: string; file?: never } | { command?: never; file: string }, json: boolean, ): string[] => { const args = [ "wrangler", "d1", "execute", options.database, options.mode === "local" ? "--local" : "--remote", "--yes", ]; if ("file" in payload && payload.file !== undefined) { args.push("--file", payload.file); } else { args.push("--command", payload.command); } if (json) { args.push("--json"); } return args; }; const formatCommandFailure = (error: unknown): string => { if (error && typeof error === "object" && "stderr" in error) { const stderr = String((error as { stderr?: unknown }).stderr ?? "").trim(); const stdout = String((error as { stdout?: unknown }).stdout ?? "").trim(); if (stderr) return stderr; if (stdout) return stdout; } return error instanceof Error ? error.message : String(error); }; const runWranglerJson = async ( options: Pick, command: string, ): Promise => { const args = createWranglerArgs(options, { command }, true); let stdout: string; try { ({ stdout } = await execFile("npx", args, { cwd: REPO_ROOT, maxBuffer: 10 * 1024 * 1024, })); } catch (error) { throw new CliError(formatCommandFailure(error)); } let parsed: unknown; try { parsed = JSON.parse(stdout); } catch (error) { throw new CliError( `Failed to parse Wrangler JSON output: ${error instanceof Error ? error.message : String(error)}`, ); } if (!Array.isArray(parsed)) { throw new CliError("Wrangler returned an unexpected JSON payload."); } const statements = parsed as WranglerStatementResult[]; const failed = statements.find((statement) => !statement.success); if (failed) { throw new CliError(failed.error ?? "Wrangler reported a failed statement."); } return statements; }; const runWranglerFile = async ( options: Pick, file: string, quiet: boolean, ): Promise => { const args = createWranglerArgs(options, { file }, false); try { const { stdout, stderr } = await execFile("npx", args, { cwd: REPO_ROOT, maxBuffer: 10 * 1024 * 1024, }); if (!quiet && stdout.trim()) process.stdout.write(`${stdout.trim()}\n`); if (!quiet && stderr.trim()) process.stderr.write(`${stderr.trim()}\n`); } catch (error) { throw new CliError(formatCommandFailure(error)); } }; const getNumericCell = (statements: WranglerStatementResult[], statementIndex: number, key: string): number => { const rawValue = statements[statementIndex]?.results?.[0]?.[key]; if (typeof rawValue === "number") return rawValue; if (typeof rawValue === "string" && rawValue.length > 0) { const parsed = Number(rawValue); if (Number.isFinite(parsed)) return parsed; } throw new CliError(`Wrangler response did not include a numeric ${key} value.`); }; const buildBootstrapInviteSql = (params: { inviteId: string; tokenHash: string; email: string; createdAt: string; expiresAt: string; replaceExisting: boolean; }): string => { const lines: string[] = []; if (params.replaceExisting) { lines.push( `DELETE FROM invites WHERE created_by = ${escapeSqlString(BOOTSTRAP_CREATED_BY)} AND consumed_at IS NULL;`, ); } lines.push( `INSERT INTO invites (id, token_hash, email, created_by, created_at, expires_at) VALUES (${escapeSqlString(params.inviteId)}, ${escapeSqlString(params.tokenHash)}, ${escapeSqlString(params.email)}, ${escapeSqlString(BOOTSTRAP_CREATED_BY)}, ${escapeSqlString(params.createdAt)}, ${escapeSqlString(params.expiresAt)});`, ); return `${lines.join("\n")}\n`; }; const main = async (): Promise => { const { options, requestedHelp } = parseArguments(process.argv.slice(2)); if (requestedHelp) { printHelp(); return; } if (!options) { printHelp(); throw new CliError("Missing required mode flag. Specify either --local or --remote."); } const nowIso = new Date().toISOString(); const preflight = await runWranglerJson( options, [ `SELECT COUNT(*) AS userCount FROM users`, `SELECT COUNT(*) AS activeBootstrapInviteCount FROM invites WHERE created_by = ${escapeSqlString(BOOTSTRAP_CREATED_BY)} AND consumed_at IS NULL AND expires_at > ${escapeSqlString(nowIso)}`, ].join("; "), ); const userCount = getNumericCell(preflight, 0, "userCount"); const activeBootstrapInviteCount = getNumericCell(preflight, 1, "activeBootstrapInviteCount"); if (userCount > 0) { throw new CliError( `Refusing to seed a bootstrap invite because ${userCount} user row${userCount === 1 ? "" : "s"} already exist. Mint future invites through /admin/invites.`, ); } if (activeBootstrapInviteCount > 0 && !options.force) { throw new CliError( `Refusing to seed: ${activeBootstrapInviteCount} unused bootstrap invite${activeBootstrapInviteCount === 1 ? " already exists" : "s already exist"}. Re-run with --force to replace.`, ); } const tokenBytes = crypto.getRandomValues(new Uint8Array(INVITE_TOKEN_BYTES)); const token = encodeBase64Url(tokenBytes); const tokenHash = await sha256(token); const idBytes = crypto.getRandomValues(new Uint8Array(INVITE_ID_BYTES)); const inviteId = `inv_${encodeBase64Url(idBytes)}`; const createdAtDate = new Date(); const expiresAtDate = new Date(createdAtDate.getTime() + options.expiresInDays * 86_400_000); const createdAt = createdAtDate.toISOString(); const expiresAt = expiresAtDate.toISOString(); const sql = buildBootstrapInviteSql({ inviteId, tokenHash, email: options.email, createdAt, expiresAt, replaceExisting: options.force && activeBootstrapInviteCount > 0, }); if (options.printSql) { process.stdout.write(sql); } if (!options.dryRun) { const tempDirectory = await mkdtemp(join(tmpdir(), "tessera-bootstrap-invite-")); const sqlFile = join(tempDirectory, "seed-bootstrap-invite.sql"); try { await writeFile(sqlFile, sql, "utf8"); await runWranglerFile(options, sqlFile, options.json); } finally { await rm(tempDirectory, { recursive: true, force: true }); } } const inviteUrl = `${options.issuer.replace(/\/+$/, "")}/invite/${token}`; if (options.json) { const output: SeedBootstrapInviteJsonOutput = { mode: options.mode, database: options.database, inviteId, email: options.email, token, inviteUrl, createdAt, expiresAt, createdBy: BOOTSTRAP_CREATED_BY, dryRun: options.dryRun, }; process.stdout.write(`${JSON.stringify(output)}\n`); return; } const lines = [ options.dryRun ? "Bootstrap invite dry run complete. No database changes were made." : "Bootstrap invite seeded successfully.", `Mode: ${options.mode}`, `Database: ${options.database}`, `Invite ID: ${inviteId}`, `Email: ${options.email}`, `Created at: ${createdAt}`, `Expires at: ${expiresAt}`, `Invite URL: ${inviteUrl}`, "", `Next: set BOOTSTRAP_ADMIN_EMAIL=${options.email} in ${options.mode === "remote" ? "wrangler.jsonc vars" : ".dev.vars"}, open the invite URL, then remove BOOTSTRAP_ADMIN_EMAIL and redeploy.`, options.dryRun ? "Warning: this dry-run token was not inserted and will not work." : "Warning: treat this token as a one-time secret until it is redeemed or expires.", ]; process.stdout.write(`${lines.join("\n")}\n`); }; await main().catch((error: unknown) => { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = 1; });