collabSupport/services/voicePin/generatePin.js
jmcqueen 2ec2b7a486 Add /resetvmpin and /resetvppin with confirmation cards for Webex PIN resets.
Chat-only commands reset voicemail mailbox and voice portal passcodes via telephony_config_write, with extension lookup and unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 16:34:09 -04:00

76 lines
2.4 KiB
JavaScript

// 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`);
}