Files
selfphoneng/apps/api/src/generator/sanitize.ts
T
iamdoubz b05361e840
ci / check (push) Successful in 1m2s
ci / images (push) Successful in 22s
contacts: caller names on desk phones while they ring (C4)
Spec 11 §6.1: sp-sub-dial-user asks /api/internal/caller-name (func_curl, 2 s at most; this
machine only, with an APP_SECRET-derived key, not logged) for the ringing user's name for the
caller, and sets it on each ringing leg (pre-dial handler, CONNECTEDLINE), never on the call, so
forwards and other members don't see someone else's contacts. Ring group/queue/number prefixes
kept. Contrack lookups never wait (cache, contactsWarm every 5 min). Proven on real Asterisk
with baresip: apps/api/test/asterisk/cnam-test.ts.
2026-10-23 15:45:36 -05:00

77 lines
3.2 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
// Every value that goes into generated Asterisk config passes through one of these (03 §2).
// The API validates with the same rules first, so a throw here means a bug: apply fails loudly.
export class SanitizeError extends Error {}
function check(re: RegExp, kind: string, max = 200) {
return (v: string): string => {
if (typeof v !== 'string' || v.length > max || !re.test(v))
throw new SanitizeError(`${kind}: invalid value ${JSON.stringify(String(v).slice(0, 40))}`);
return v;
};
}
/** Internal numbers, DIDs. */
export const digits = check(/^[0-9]+$/, 'digits', 32);
/** Forward targets, external numbers. */
export const dialable = check(/^[0-9*#+]+$/, 'dialable', 32);
/** Outbound route patterns. */
export const pattern = check(/^[0-9XZN.![\]-]+$/, 'pattern', 32);
/** Office hours times "08:30"; "24:00" is the end of the day. */
export const hhmm = check(/^(?:[01]\d|2[0-3]):[0-5]\d$|^24:00$/, 'time', 5);
/** Day names as GotoIfTime wants them. */
export const weekday = check(/^(?:mon|tue|wed|thu|fri|sat|sun)$/, 'weekday', 3);
/** One IVR key. */
export const ivrDigit = check(/^[0-9*#]$/, 'ivr digit', 1);
/** Sound file names for Playback/Background. */
export const sound = check(/^[a-z0-9/_-]+$/, 'sound', 100);
/** Context-name parts (uuids), MOH class names. */
export const ident = check(/^[a-z0-9-]+$/, 'ident', 64);
/** Generated SIP passwords. */
/** The api on this machine, e.g. http://127.0.0.1:8080/api/internal/caller-name. */
export const localUrl = check(
/^http:\/\/(?:127\.0\.0\.1|localhost):[0-9]{1,5}\/[a-z0-9/_-]+$/,
'local url',
100,
);
export const secret = check(/^[A-Za-z0-9]+$/, 'secret', 64);
/** Trunk usernames (voip.ms sub-accounts like 123456_pbx). */
export const sipUser = check(/^[A-Za-z0-9_.-]+$/, 'sip user', 64);
/** Passwords typed by an admin (trunks): no ';' (comment), no line breaks, no edge spaces. */
export const typedSecret = check(/^[^\s;](?:[^\r\n;]*[^\s;])?$/, 'password', 128);
/** Codec names. */
export const codec = check(/^(?:ulaw|alaw|g722)$/, 'codec', 4);
/** Voicemail / conference PINs. */
export const pin = check(/^[0-9]{4,10}$/, 'pin', 10);
/** IANA time zone names, e.g. America/Chicago, Etc/GMT+6. */
export const timezone = check(/^[A-Za-z][A-Za-z0-9_+/-]*$/, 'timezone', 64);
/** Trunk server host: a hostname or an IPv4 address. */
export const host = check(
/^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)*[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/,
'host',
253,
);
const octet = '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)';
/** IPv4 address. */
export const ipv4 = check(new RegExp(`^${octet}(?:\\.${octet}){3}$`), 'ipv4', 15);
/** IPv4 CIDR, e.g. local nets. */
export const cidr = check(
new RegExp(`^${octet}(?:\\.${octet}){3}/(?:3[0-2]|[12]?\\d)$`),
'cidr',
18,
);
/**
* Caller ID names and labels: removes characters that mean something in dialplan or config
* (" ' \ ; , ( ) [ ] { } $ < > |) and control characters, trims, max 40. Never throws.
*/
export function name(v: string): string {
return String(v)
.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, '') // controls, incl. CR/LF, and invisible format chars
.replace(/["'\\;,()[\]{}$<>|]/g, '')
.trim()
.slice(0, 40)
.trim();
}