// services/voicePin/generatePin.js // Crypto-random PIN generation with configurable length and passcode rules. import { randomInt } from 'node:crypto'; const DEFAULT_MIN = 6; const DEFAULT_MAX = 6; function digitPin(length) { let pin = ''; for (let i = 0; i < length; i += 1) { pin += String(randomInt(0, 10)); } return pin; } function hasSequentialDigits(pin, maxRun = 3) { for (let i = 0; i <= pin.length - maxRun; i += 1) { const slice = pin.slice(i, i + maxRun); const asc = slice.split('').every((d, idx) => idx === 0 || Number(d) === Number(slice[idx - 1]) + 1); const desc = slice.split('').every((d, idx) => idx === 0 || Number(d) === Number(slice[idx - 1]) - 1); if (asc || desc) return true; } return false; } function isAllSameDigit(pin) { return /^(\d)\1+$/.test(pin); } function violatesRules(pin, rules = {}) { const length = pin.length; const minLength = rules.minLength ?? rules.length?.min ?? DEFAULT_MIN; const maxLength = rules.maxLength ?? rules.length?.max ?? DEFAULT_MAX; if (length < minLength || length > maxLength) return true; if (rules.disallowSequential === true && hasSequentialDigits(pin)) return true; if (rules.disallowRepeating === true && isAllSameDigit(pin)) return true; if (rules.disallowSameAsExtension === true && rules.extension && pin === String(rules.extension)) { return true; } return false; } /** * Generate a numeric PIN that satisfies the provided rules. * * @param {object} [opts] * @param {number} [opts.minLength] * @param {number} [opts.maxLength] * @param {object} [opts.rules] full passcodeRules object from Webex * @param {number} [opts.maxAttempts] * @returns {string} */ export function generatePin(opts = {}) { const rules = opts.rules || {}; const minLength = opts.minLength ?? rules.length?.min ?? DEFAULT_MIN; const maxLength = opts.maxLength ?? rules.length?.max ?? minLength; const targetLength = Math.max(minLength, Math.min(maxLength, opts.length ?? minLength)); const maxAttempts = opts.maxAttempts ?? 50; const mergedRules = { ...rules, minLength, maxLength, extension: opts.extension ?? rules.extension, }; for (let attempt = 0; attempt < maxAttempts; attempt += 1) { const pin = digitPin(targetLength); if (!violatesRules(pin, mergedRules)) return pin; } throw new Error(`Could not generate a compliant PIN after ${maxAttempts} attempts`); }