// Random strings over a chosen alphabet: each character drawn independently and uniformly // (randomInts uses rejection sampling, so there is no modulo bias). import { CHARACTER_CLASSES, type CharacterClass } from './password'; import { randomInts } from './random'; export const RANDOM_STRING_MAX_LENGTH = 1024; export type RandomStringOptions = { length: number; classes: Record; useCustom: boolean; /** Extra characters, taken as Unicode code points; duplicates and control characters drop. */ custom: string; }; export class RandomStringOptionsError extends Error { override name = 'RandomStringOptionsError'; } /** The distinct characters the string is drawn from, in a stable order. */ export function randomStringAlphabet(options: RandomStringOptions): string[] { const enabled = (Object.keys(CHARACTER_CLASSES) as CharacterClass[]).filter( (name) => options.classes[name], ); if (enabled.length === 0 && !options.useCustom) { throw new RandomStringOptionsError('Turn on at least one character set.'); } const custom = options.useCustom ? Array.from(options.custom.replace(/\p{Cc}/gu, '')) : []; if (options.useCustom && custom.length === 0) { throw new RandomStringOptionsError('Enter at least one custom character.'); } const alphabet = [ ...new Set([...enabled.flatMap((name) => Array.from(CHARACTER_CLASSES[name])), ...custom]), ]; if (alphabet.length < 2) { throw new RandomStringOptionsError('Use at least two different characters.'); } return alphabet; } function validateLength(length: number) { if (!Number.isInteger(length) || length < 1 || length > RANDOM_STRING_MAX_LENGTH) { throw new RandomStringOptionsError(`Length must be between 1 and ${RANDOM_STRING_MAX_LENGTH}.`); } } export function generateRandomString(options: RandomStringOptions): string { validateLength(options.length); const alphabet = randomStringAlphabet(options); return randomInts(alphabet.length, options.length) .map((index) => alphabet[index]) .join(''); } /** length × log2(alphabet size): exact here, because every character is independent and uniform. */ export function randomStringEntropyBits(options: RandomStringOptions): number { validateLength(options.length); return options.length * Math.log2(randomStringAlphabet(options).length); }