Skip to content

Instantly share code, notes, and snippets.

@TotalLag
Created March 28, 2026 22:58
Show Gist options
  • Select an option

  • Save TotalLag/82917b2cb4146ab2f97080ac7867c2b5 to your computer and use it in GitHub Desktop.

Select an option

Save TotalLag/82917b2cb4146ab2f97080ac7867c2b5 to your computer and use it in GitHub Desktop.
Node script to reset account passwords on Paperclip
#!/usr/bin/env node
/**
* Paperclip Password Reset Script
*
* Resets a user's password by email address.
* Connects directly to the embedded Postgres DB.
*
* Usage:
* bun add -d pg @noble/hashes
* bun run reset-password.js <email> <new-password>
*
* Example:
* bun run reset-password.js user@example.com MyNewPassword123
*/
import { Pool } from 'pg';
import { scryptAsync } from '@noble/hashes/scrypt.js';
import { bytesToHex, randomBytes } from '@noble/hashes/utils.js';
// === Configuration ===
// Embedded Postgres connection - matches embedded-postgres defaults
const DB_CONFIG = {
host: 'localhost',
port: 54329,
database: 'paperclip',
user: 'paperclip',
password: 'paperclip',
max: 1,
idleTimeoutMillis: 5000,
connectionTimeoutMillis: 5000,
};
// Scrypt params - must match better-auth's defaults (N:16384, r:16, p:1, dkLen:64)
const SCRYPT_PARAMS = { N: 16384, r: 16, p: 1, dkLen: 64 };
/**
* Hash a password using scrypt with the same params as better-auth
* @param {string} password
* @returns {Promise<string>} salt:hash format
*/
async function hashPassword(password) {
const salt = bytesToHex(randomBytes(16));
const key = await scryptAsync(password.normalize('NFKC'), salt, {
...SCRYPT_PARAMS,
maxmem: 128 * SCRYPT_PARAMS.N * SCRYPT_PARAMS.r * 2,
});
return `${salt}:${bytesToHex(key)}`;
}
/**
* Reset password for a user by email
* @param {string} email
* @param {string} newPassword
*/
async function resetPassword(email, newPassword) {
const pool = new Pool(DB_CONFIG);
try {
// Connect and verify
const client = await pool.connect();
console.log('✓ Connected to embedded Postgres on port 54329\n');
client.release();
// Look up user by email
const userResult = await pool.query(
`SELECT id, name, email FROM "user" WHERE email = $1`,
[email]
);
if (userResult.rows.length === 0) {
console.error(`✗ No user found with email: ${email}`);
process.exit(1);
}
const user = userResult.rows[0];
console.log(`✓ Found user:`);
console.log(` ID: ${user.id}`);
console.log(` Name: ${user.name}`);
console.log(` Email: ${user.email}\n`);
// Generate new password hash
const passwordHash = await hashPassword(newPassword);
console.log(`✓ Generated new scrypt hash for password: ${newPassword}`);
// Update the account table (snake_case column names)
const updateResult = await pool.query(
`UPDATE account
SET password = $1, updated_at = NOW()
WHERE user_id = $2
RETURNING id, user_id, provider_id`,
[passwordHash, user.id]
);
if (updateResult.rows.length === 0) {
// Try to find what accounts exist for this user
const accountCheck = await pool.query(
`SELECT id, provider_id FROM account WHERE user_id = $1`,
[user.id]
);
if (accountCheck.rows.length === 0) {
console.error(`✗ No account found for user ID: ${user.id}`);
console.log('\nNote: This user may have been created via OAuth/single sign-on');
console.log('and does not have a password-based account.');
process.exit(1);
}
console.log(`✗ Account exists but is not a credential provider account:`);
accountCheck.rows.forEach(acc => {
console.log(` Account ID: ${acc.id}, Provider: ${acc.provider_id}`);
});
process.exit(1);
}
const updatedAccount = updateResult.rows[0];
console.log(`\n✓ Password updated successfully!`);
console.log(` Account ID: ${updatedAccount.id}`);
console.log(` Provider: ${updatedAccount.provider_id}`);
console.log(` Updated at: ${new Date().toISOString()}`);
console.log(`\n========================================`);
console.log(`Password reset complete!`);
console.log(`User can now log in with:`);
console.log(` Email: ${email}`);
console.log(` Password: ${newPassword}`);
console.log(`========================================`);
} catch (error) {
console.error(`✗ Error: ${error.message}`);
if (error.code === 'ECONNREFUSED') {
console.error(`\nIs the embedded Postgres running?`);
console.error(`Check if process is listening on port 54329:`);
console.error(` lsof -i :54329`);
} else if (error.code === '28P01') {
console.error(`\nAuthentication failed. Check Postgres credentials.`);
}
process.exit(1);
} finally {
await pool.end();
}
}
// === CLI Entry Point ===
const args = process.argv.slice(2);
if (args.length < 2) {
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ Paperclip Password Reset Script ║
╚══════════════════════════════════════════════════════════════╝
Usage:
bun run reset-password.js <email> <new-password>
Example:
bun run reset-password.js user@example.com MyNewPassword123
Prerequisites:
bun add -d pg @noble/hashes
Notes:
- Connects to embedded Postgres on port 54329
- Uses scrypt hashing (same as better-auth)
- Updates the account table for the user
`);
process.exit(1);
}
const [email, password] = args;
if (!email.includes('@')) {
console.error(`✗ Invalid email address: ${email}`);
process.exit(1);
}
if (password.length < 8) {
console.warn(`⚠ Warning: Password is less than 8 characters`);
}
resetPassword(email, password).catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment