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>
76 lines
2.4 KiB
JavaScript
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`);
|
|
}
|