#!/usr/bin/env node import crypto from "crypto"; import pg from "pg"; const { Pool } = pg; const SCRYPT_PARAMS = { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }; const SCRYPT_KEYLEN = 32; const PASSWORD_PREFIX = "scrypt:v1:"; function hashPassword(password) { const salt = crypto.randomBytes(16); return new Promise((resolve, reject) => { crypto.scrypt(password, salt, SCRYPT_KEYLEN, SCRYPT_PARAMS, (err, derived) => { if (err) { reject(err); return; } resolve( `${PASSWORD_PREFIX}${salt.toString("base64url")}:${derived.toString("base64url")}`, ); }); }); } async function main() { const email = process.env.LOCAL_AUTH_EMAIL?.trim().toLowerCase(); const password = process.env.LOCAL_AUTH_PASSWORD; const revokeSessions = process.env.LOCAL_AUTH_REVOKE_SESSIONS !== "0"; const databaseUrl = process.env.DATABASE_URL; if (!databaseUrl) { throw new Error("DATABASE_URL is required"); } if (!email) { throw new Error("LOCAL_AUTH_EMAIL is required"); } if (!password || password.length < 8) { throw new Error("LOCAL_AUTH_PASSWORD must be at least 8 characters"); } const pool = new Pool({ connectionString: databaseUrl }); const client = await pool.connect(); try { const passwordHash = await hashPassword(password); const updateResult = await client.query( `UPDATE auth.users SET password_hash = $1, updated_at = now() WHERE lower(email) = lower($2) RETURNING id, email`, [passwordHash, email], ); if (updateResult.rowCount === 0) { throw new Error(`No user found for ${email}`); } const userId = updateResult.rows[0].id; if (revokeSessions) { await client.query(`DELETE FROM auth.sessions WHERE user_id = $1`, [userId]); } console.log(`Reset password for ${email}`); if (revokeSessions) { console.log("Revoked existing sessions"); } } finally { client.release(); await pool.end(); } } main().catch((err) => { console.error(err.message || err); process.exit(1); });