import { env } from '$env/dynamic/private'; import argon2 from 'argon2'; import { db } from './db/index.js'; export interface LocalCredentialRecord { userId: string; username: string; fullName: string; email: string | null; passwordHash: string; } export const LOCAL_AUTH_MIN_PASSWORD_LENGTH = 12; const getNumberEnv = (value: string | undefined, fallback: number): number => { const parsed = Number.parseInt(value ?? '', 10); return Number.isFinite(parsed) ? parsed : fallback; }; export function getLocalCredentialByIdentifier(identifier: string): LocalCredentialRecord | null { const normalized = identifier.trim(); if (!normalized) return null; return ( db.get( `SELECT users.id as userId, users.username as username, users.full_name as fullName, users.email as email, local_credentials.password_hash as passwordHash FROM users INNER JOIN local_credentials ON local_credentials.user_id = users.id WHERE lower(users.username) = lower(?) OR (users.email IS NOT NULL AND lower(users.email) = lower(?)) LIMIT 1`, [normalized, normalized] ) ?? null ); } export async function hashLocalPassword(password: string): Promise { const trimmed = password.trim(); if (trimmed.length < LOCAL_AUTH_MIN_PASSWORD_LENGTH) { throw new Error(`Password must be at least ${LOCAL_AUTH_MIN_PASSWORD_LENGTH} characters`); } const memoryCost = getNumberEnv(env.LOCAL_AUTH_ARGON2_MEMORY_KB, 65536); const timeCost = getNumberEnv(env.LOCAL_AUTH_ARGON2_TIME_COST, 3); const parallelism = getNumberEnv(env.LOCAL_AUTH_ARGON2_PARALLELISM, 1); return argon2.hash(trimmed, { type: argon2.argon2id, memoryCost, timeCost, parallelism }); } export async function setLocalCredentialPassword(userId: string, password: string): Promise { const passwordHash = await hashLocalPassword(password); db.run( `INSERT INTO local_credentials (user_id, password_hash) VALUES (?, ?) ON CONFLICT(user_id) DO UPDATE SET password_hash = excluded.password_hash, updated_at = datetime('now')`, [userId, passwordHash] ); }