export interface GeneratorOptions { length: number; includeUppercase: boolean; includeLowercase: boolean; includeNumbers: boolean; includeSymbols: boolean; /** Characters to exclude from every pool — honors site forbidden-char lists. */ forbiddenChars?: string; } /** * Generate a cryptographically random password. */ export function generatePassword(options: Partial = {}): string { const { length = 20, includeUppercase = true, includeLowercase = true, includeNumbers = true, includeSymbols = true, forbiddenChars = "", } = options; const forbidden = new Set(forbiddenChars); const filter = (pool: string): string => forbidden.size === 0 ? pool : Array.from(pool).filter((c) => !forbidden.has(c)).join(""); let chars = ""; if (includeLowercase) chars += filter("abcdefghijklmnopqrstuvwxyz"); if (includeUppercase) chars += filter("ABCDEFGHIJKLMNOPQRSTUVWXYZ"); if (includeNumbers) chars += filter("0123456789"); if (includeSymbols) chars += filter("!@#$%^&*"); if (chars.length === 0) chars = filter("abcdefghijklmnopqrstuvwxyz") || "abcdefghijklmnopqrstuvwxyz"; // Rejection sampling to eliminate modulo bias: discard CSPRNG values in the // final partial bucket so every character is exactly uniform over `chars` // (audit INFO — `% chars.length` alone slightly favours low indices when the // alphabet size does not divide 2^32). const limit = Math.floor(0x1_0000_0000 / chars.length) * chars.length; let password = ""; while (password.length < length) { const batch = crypto.getRandomValues(new Uint32Array(length - password.length)); for (let i = 0; i < batch.length && password.length < length; i++) { const v = batch[i]!; if (v >= limit) continue; // would bias the modulo — draw again password += chars[v % chars.length]; } } return password; }