import { timingSafeEqual } from 'node:crypto';
/**
 * PBKDF2 iteration count. Bumped from 100,000 (insufficient by 2026 standards)
 * to 600,000 to align with OWASP/NIST 2023+ guidance for PBKDF2-HMAC-SHA256.
 * The hash format embeds the iteration count, so existing 100k hashes still
 * verify correctly — `verifyPassword` reads the count from the stored string,
 * and `needsRehash` lets callers (login.ts) opportunistically upgrade old
 * hashes when the user signs in with the correct password.
 */
export const PBKDF2_ITERATIONS = 600_000;
/** Hash a password using Web Crypto API (PBKDF2-HMAC-SHA256). */
export async function hashPassword(password) {
    const salt = crypto.getRandomValues(new Uint8Array(16));
    const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveBits']);
    const derived = await crypto.subtle.deriveBits({ name: 'PBKDF2', salt, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' }, key, 256);
    const saltHex = Buffer.from(salt).toString('hex');
    const hashHex = Buffer.from(derived).toString('hex');
    return `pbkdf2:${PBKDF2_ITERATIONS}:${saltHex}:${hashHex}`;
}
/**
 * Verify a password against its stored hash.
 *
 * Reads the iteration count from the stored hash so old hashes (100k from
 * pre-2026 deployments) and current hashes (600k) both verify. Pair with
 * `needsRehash` at the login site to opportunistically upgrade old hashes.
 */
export async function verifyPassword(password, storedHash) {
    const parts = storedHash.split(':');
    if (parts.length !== 4)
        return false;
    const [, iterStr, saltHex, hashHex] = parts;
    if (!iterStr || !saltHex || !hashHex)
        return false;
    const iterations = parseInt(iterStr, 10);
    // Bound the iteration count: refuse anything below 10k (almost certainly
    // a corrupted hash) or above 5M (DoS guard — an attacker who controls a
    // user's stored hash could otherwise pin a worker for tens of seconds per
    // login attempt).
    if (!Number.isFinite(iterations) || iterations < 10_000 || iterations > 5_000_000) {
        return false;
    }
    const salt = Buffer.from(saltHex, 'hex');
    const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveBits']);
    const derived = await crypto.subtle.deriveBits({ name: 'PBKDF2', salt, iterations, hash: 'SHA-256' }, key, 256);
    const derivedBuf = Buffer.from(derived);
    const storedBuf = Buffer.from(hashHex, 'hex');
    if (derivedBuf.length !== storedBuf.length)
        return false;
    return timingSafeEqual(derivedBuf, storedBuf);
}
/**
 * Returns true when the stored hash uses fewer iterations than current policy.
 *
 * Login flow should: verify -> if `needsRehash` -> hash again with current
 * params -> persist. This upgrades old hashes silently as users sign in.
 */
export function needsRehash(storedHash) {
    const parts = storedHash.split(':');
    if (parts.length !== 4)
        return false;
    const iterStr = parts[1];
    const iterations = iterStr ? parseInt(iterStr, 10) : 0;
    if (!Number.isFinite(iterations))
        return false;
    return iterations < PBKDF2_ITERATIONS;
}
/**
 * A stable dummy PBKDF2 hash used to keep login response time roughly constant
 * regardless of whether the email exists. When the user is not found, the
 * login handler still runs `verifyPassword(submittedPassword, dummyHash)` so
 * the timing channel that distinguished "no user" from "wrong password"
 * disappears.
 *
 * Initialised eagerly at module load via a top-level Promise. Without this,
 * the very first call to `compareToDummyHash` after a cold start would have
 * to run `hashPassword` (600k iterations) *and* `verifyPassword` (another
 * 600k iterations) — roughly 2× the latency of a normal verify, which
 * recreates the user-enumeration timing channel this defense is meant to
 * close. The Promise is awaited inside `compareToDummyHash`, so callers
 * never see a partial hash.
 *
 * Why a Promise instead of a string returned from a top-level await:
 *   - The Web Crypto PBKDF2 derivation is asynchronous; we can't compute it
 *     synchronously at module top level without forcing every importer to
 *     also support top-level await.
 *   - Storing the in-flight Promise lets the work start at module load and
 *     overlap with the first request, rather than blocking on it.
 */
const _dummyHashPromise = (async () => {
    const dummyPlaintext = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('hex');
    return hashPassword(dummyPlaintext);
})();
// Surface uncaught rejections so that — if the eager hash ever fails — we
// see it in logs at startup, not on the first login. Without this handler,
// Node would emit an unhandledRejection warning the moment a request hits
// `compareToDummyHash`.
_dummyHashPromise.catch((err) => {
    // eslint-disable-next-line no-console
    console.error('[actuate][auth] failed to precompute dummy login hash:', err);
});
/**
 * Returns a PBKDF2 verification result against the module-level dummy hash.
 * The hash is shared across all calls (and across all unknown emails) — it
 * doesn't matter that it's deterministic per-process, because the comparison
 * itself takes constant time and the attacker only learns "not the dummy
 * hash" — which they already know.
 *
 * The boolean return is meaningless for callers and is intentionally always
 * `false` in practice; it exists so the type matches `verifyPassword`.
 */
export async function compareToDummyHash(password) {
    const dummyHash = await _dummyHashPromise;
    return verifyPassword(password, dummyHash);
}
/**
 * Test-only escape hatch that resolves when the eager dummy-hash
 * precompute has finished. Production callers MUST NOT use this — they
 * should call `compareToDummyHash`, which awaits the same promise
 * internally. Exported solely so timing-sensitive tests can measure
 * verify-only latency without the variable cost of the in-flight
 * eager init.
 *
 * @internal
 */
export function _waitForDummyHashReady() {
    return _dummyHashPromise.then(() => undefined);
}
/** Validate a password against the configured policy rules. */
export function validatePasswordPolicy(password, policy) {
    const errors = [];
    if (policy.minLength && password.length < policy.minLength) {
        errors.push(`Password must be at least ${policy.minLength} characters`);
    }
    if (policy.requireUppercase && !/[A-Z]/.test(password)) {
        errors.push('Password must contain an uppercase letter');
    }
    if (policy.requireLowercase && !/[a-z]/.test(password)) {
        errors.push('Password must contain a lowercase letter');
    }
    if (policy.requireNumbers && !/\d/.test(password)) {
        errors.push('Password must contain a digit');
    }
    if (policy.requireSpecialChars && !/[^a-zA-Z0-9]/.test(password)) {
        errors.push('Password must contain a special character');
    }
    return { valid: errors.length === 0, errors };
}
/** Check if a password has appeared in a known data breach (k-anonymity). */
export { checkBreached as checkPasswordBreach } from '../security/breach-check.js';
//# sourceMappingURL=password.js.map