import * as crypto from 'crypto'; // Excludes 0/O, 1/I/L to avoid look-alike confusion when a code is // hand-written, read aloud, or typed from a printed fallback under a QR // code. 31 symbols * 8 chars ≈ 39.6 bits of entropy - effectively // unguessable combined with rate-limiting on the redeem endpoint. const CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'; const CODE_LENGTH = 8; /** * Generates a single random code. Uses crypto.randomInt (uniform, no * modulo bias) rather than Math.random() since these gate a real-world * concert invitation. */ export const generateCode = (): string => { let code = ''; for (let i = 0; i < CODE_LENGTH; i++) { code += CODE_ALPHABET[crypto.randomInt(CODE_ALPHABET.length)]; } return code; }; /** * Generates a code guaranteed not to collide with any row `existingCodes` * already contains, tries up to `maxAttempts` times before giving up. * Collisions are astronomically unlikely at this entropy - this exists as * a correctness backstop, not because collisions are expected. */ export const generateUniqueCode = (existingCodes: Set, maxAttempts = 20): string => { for (let i = 0; i < maxAttempts; i++) { const candidate = generateCode(); if (!existingCodes.has(candidate)) { existingCodes.add(candidate); return candidate; } } throw new Error('Could not generate a unique voucher code after ' + maxAttempts + ' attempts'); };