import { spawnSync } from "node:child_process"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { createPasswordHash } from "./password-hash.mjs"; const usage = `Usage: EDGE_EVER_PASSWORD='' bun run auth:reset-password -- --remote --username EDGE_EVER_PASSWORD='' bun run auth:reset-password -- --local --username For a scoped instance, also set EDGE_EVER_INSTANCE, for example EDGE_EVER_INSTANCE=demo.`; export const parseResetPasswordArgs = (args) => { let target; let username; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--remote" || arg === "--local") { const nextTarget = arg.slice(2); if (target && target !== nextTarget) { throw new Error("Choose exactly one of --remote or --local."); } target = nextTarget; continue; } if (arg === "--username") { username = args[index + 1]; index += 1; continue; } throw new Error(`Unknown argument: ${arg}`); } if (!target) { throw new Error("Choose --remote or --local."); } if (!username?.trim()) { throw new Error("Provide --username ."); } return { target, username: username.trim() }; }; const sqlString = (value) => `'${value.replaceAll("'", "''")}'`; export const buildUserLookupSql = (username) => `SELECT id FROM users WHERE username = ${sqlString(username)} AND is_disabled = 0 LIMIT 1`; export const buildPasswordHashLookupSql = (username) => `SELECT password_hash FROM users WHERE username = ${sqlString(username)} AND is_disabled = 0 LIMIT 1`; export const buildPasswordResetSql = (username, passwordHash) => { const escapedUsername = sqlString(username); const escapedHash = sqlString(passwordHash); return `UPDATE users SET password_hash = ${escapedHash}, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE username = ${escapedUsername} AND is_disabled = 0; UPDATE sessions SET revoked_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE user_id IN ( SELECT id FROM users WHERE username = ${escapedUsername} AND is_disabled = 0 ) AND revoked_at IS NULL; `; }; export const parseD1Rows = (output) => { const parsed = JSON.parse(output); const results = Array.isArray(parsed) ? parsed : [parsed]; return results.flatMap((result) => result?.results ?? []); }; const runWrangler = (args) => { const result = spawnSync( process.execPath, [resolve("scripts/run-wrangler.mjs"), ...args], { encoding: "utf8", env: process.env }, ); if (result.status !== 0) { if (result.stdout) process.stdout.write(result.stdout); if (result.stderr) process.stderr.write(result.stderr); throw new Error(`Wrangler exited with status ${result.status ?? 1}.`); } return result.stdout; }; const main = () => { let options; try { options = parseResetPasswordArgs(process.argv.slice(2)); } catch (error) { console.error(error.message); console.error(usage); process.exit(1); } const password = process.env.EDGE_EVER_PASSWORD; if (!password || password.length < 8 || password.length > 512) { console.error("EDGE_EVER_PASSWORD must contain 8-512 characters."); console.error(usage); process.exit(1); } const targetFlag = `--${options.target}`; const lookupOutput = runWrangler([ "d1", "execute", "DB", targetFlag, "--command", buildUserLookupSql(options.username), "--json", ]); if (parseD1Rows(lookupOutput).length === 0) { throw new Error(`Enabled user not found: ${options.username}`); } const temporaryDirectory = mkdtempSync(join(tmpdir(), "edgeever-password-reset-")); const sqlPath = join(temporaryDirectory, "reset-password.sql"); try { const passwordHash = createPasswordHash(password); writeFileSync(sqlPath, buildPasswordResetSql(options.username, passwordHash), { mode: 0o600, }); runWrangler(["d1", "execute", "DB", targetFlag, "--file", sqlPath, "--yes"]); const verificationOutput = runWrangler([ "d1", "execute", "DB", targetFlag, "--command", buildPasswordHashLookupSql(options.username), "--json", ]); const [updatedUser] = parseD1Rows(verificationOutput); if (updatedUser?.password_hash !== passwordHash) { throw new Error(`Password reset verification failed: ${options.username}`); } } finally { rmSync(temporaryDirectory, { recursive: true, force: true }); } console.log(`Password reset for ${options.username} (${options.target}); existing sessions revoked.`); }; if (import.meta.main) { try { main(); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } }