import { randomBytes } from "@noble/ciphers/utils.js"; const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const LOWERCASE = "abcdefghijklmnopqrstuvwxyz"; const NUMBERS = "0123456789"; const SYMBOLS = "!@#$%^&*()_+-=[]{}|;:,.<>?"; const SIMILAR = "il1Lo0O"; export interface GeneratePasswordOptions { /** Output length in characters. */ length: number; includeUppercase?: boolean; includeLowercase?: boolean; includeNumbers?: boolean; includeSymbols?: boolean; /** Drop ambiguous glyphs (`i l 1 L o 0 O`). */ excludeSimilar?: boolean; } /** * Generate a random password using CSPRNG bytes (`@noble/ciphers`) with * rejection sampling so the output distribution stays uniform across * the chosen character set — `b % len` modulo-bias is rejected by * trimming the byte range to a multiple of `len`. * * Throws when every character class is disabled (no charset). */ export function generatePassword(opts: GeneratePasswordOptions): string { let charset = ""; if (opts.includeUppercase ?? true) { charset += UPPERCASE; } if (opts.includeLowercase ?? true) { charset += LOWERCASE; } if (opts.includeNumbers ?? true) { charset += NUMBERS; } if (opts.includeSymbols ?? true) { charset += SYMBOLS; } if (opts.excludeSimilar) { charset = charset .split("") .filter((c) => !SIMILAR.includes(c)) .join(""); } if (charset.length === 0) { throw new Error("generatePassword: at least one character class must be enabled"); } const limit = Math.floor(256 / charset.length) * charset.length; let out = ""; while (out.length < opts.length) { const bytes = randomBytes(opts.length - out.length + 16); for (let i = 0; i < bytes.length && out.length < opts.length; i++) { const b = bytes[i] ?? 0; if (b < limit) { out += charset.charAt(b % charset.length); } } } return out; }