@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
295 lines • 9.26 kB
JavaScript
import { PII_TYPES } from '../types.js';
/** Dotted-quad IPv4 with every octet in 0-255 and no leading zeros. */
export function isIPv4(raw) {
const parts = raw.split('.');
return (parts.length === 4 && parts.every((p) => /^(?:0|[1-9]\d{0,2})$/.test(p) && Number(p) <= 255));
}
/** Luhn (mod 10) checksum over a digit string. */
export function luhnValid(digits) {
if (!/^\d+$/.test(digits))
return false;
let sum = 0;
let double = false;
for (let i = digits.length - 1; i >= 0; i--) {
let d = digits.charCodeAt(i) - 48;
if (double) {
d *= 2;
if (d > 9)
d -= 9;
}
sum += d;
double = !double;
}
return sum % 10 === 0;
}
/** Card brands by IIN prefix (digits only). Unknown prefixes are not treated as cards. */
export function cardBrand(digits) {
const p2 = Number(digits.slice(0, 2));
const p3 = Number(digits.slice(0, 3));
const p4 = Number(digits.slice(0, 4));
const p6 = Number(digits.slice(0, 6));
const len = digits.length;
// Brazilian brands first: their ranges overlap Visa/Discover/Mastercard prefixes.
if ([
401178, 401179, 431274, 438935, 451416, 457393, 457631, 457632, 504175, 627780, 636297,
636368,
].includes(p6))
return 'elo';
if ((p6 >= 506699 && p6 <= 506778) ||
(p6 >= 509000 && p6 <= 509999) ||
(p6 >= 650031 && p6 <= 650051))
return 'elo';
if (p6 === 606282 || p4 === 3841)
return 'hipercard';
if (digits.startsWith('4') && (len === 13 || len === 16 || len === 19))
return 'visa';
if (((p2 >= 51 && p2 <= 55) || (p4 >= 2221 && p4 <= 2720)) && len === 16)
return 'mastercard';
if ((p2 === 34 || p2 === 37) && len === 15)
return 'amex';
if ((p4 === 6011 || p2 === 65 || (p3 >= 644 && p3 <= 649)) && len >= 16)
return 'discover';
if ((p2 === 36 || p2 === 38 || (p3 >= 300 && p3 <= 305)) && len >= 14)
return 'diners';
if (p4 >= 3528 && p4 <= 3589 && len >= 16)
return 'jcb';
if ((p2 === 62 || p2 === 81) && len >= 16)
return 'unionpay';
if ((p4 === 5018 || p4 === 5020 || p4 === 5038 || p4 === 6304 || p4 === 6759 || p4 === 6761) &&
len >= 12)
return 'maestro';
return undefined;
}
/** CPF (Brazilian individual taxpayer id): 11 digits, two mod-11 check digits. */
export function cpfValid(raw) {
const d = raw.replace(/\D/g, '');
if (d.length !== 11 || /^(\d)\1{10}$/.test(d))
return false;
const check = (len) => {
let sum = 0;
for (let i = 0; i < len; i++)
sum += (d.charCodeAt(i) - 48) * (len + 1 - i);
const r = (sum * 10) % 11;
return r === 10 ? 0 : r;
};
return check(9) === d.charCodeAt(9) - 48 && check(10) === d.charCodeAt(10) - 48;
}
/**
* CNPJ (Brazilian company id), numeric or the alphanumeric format in use since July 2026: 12
* characters [0-9A-Z] + 2 check digits, each character valued at its ASCII code minus 48.
*/
export function cnpjValid(raw) {
const c = raw.replace(/[.\-/\s]/g, '').toUpperCase();
if (!/^[0-9A-Z]{12}\d{2}$/.test(c) || /^(\d)\1{13}$/.test(c))
return false;
const value = (i) => c.charCodeAt(i) - 48;
const check = (len) => {
const weights = len === 12 ? [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2] : [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
let sum = 0;
for (let i = 0; i < len; i++)
sum += value(i) * (weights[i] ?? 0);
const r = sum % 11;
return r < 2 ? 0 : 11 - r;
};
return check(12) === value(12) && check(13) === value(13);
}
/** US Social Security Number structure (no 000/666/9xx area, no 00 group, no 0000 serial). */
export function ssnValid(raw) {
const m = /^(\d{3})[- ](\d{2})[- ](\d{4})$/.exec(raw);
if (!m)
return false;
const area = m[1] ?? '';
const group = m[2] ?? '';
const serial = m[3] ?? '';
if (area === '000' || area === '666' || area.startsWith('9'))
return false;
if (group === '00' || serial === '0000')
return false;
// Well-known advertising / sample numbers.
if (['078-05-1120', '219-09-9999', '123-45-6789'].includes(`${area}-${group}-${serial}`))
return false;
return true;
}
/** IBAN lengths by country (ISO 13616 registry, main countries). */
const IBAN_LENGTHS = {
AD: 24,
AE: 23,
AT: 20,
BE: 16,
BG: 22,
BH: 22,
BR: 29,
CH: 21,
CR: 22,
CY: 28,
CZ: 24,
DE: 22,
DK: 18,
DO: 28,
EE: 20,
ES: 24,
FI: 18,
FO: 18,
FR: 27,
GB: 22,
GI: 23,
GL: 18,
GR: 27,
GT: 28,
HR: 21,
HU: 28,
IE: 22,
IL: 23,
IS: 26,
IT: 27,
JO: 30,
KW: 30,
KZ: 20,
LB: 28,
LI: 21,
LT: 20,
LU: 20,
LV: 21,
MC: 27,
MD: 24,
ME: 22,
MK: 19,
MR: 27,
MT: 31,
MU: 30,
NL: 18,
NO: 15,
PK: 24,
PL: 28,
PS: 29,
PT: 25,
QA: 29,
RO: 24,
RS: 22,
SA: 24,
SE: 24,
SI: 19,
SK: 24,
SM: 27,
TN: 24,
TR: 26,
UA: 29,
VG: 24,
XK: 20,
};
/** IBAN: known country length and ISO 7064 mod 97-10 checksum. */
export function ibanValid(raw) {
const s = raw.replace(/\s/g, '').toUpperCase();
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(s))
return false;
const expected = IBAN_LENGTHS[s.slice(0, 2)];
if (expected !== undefined && s.length !== expected)
return false;
if (expected === undefined)
return false;
const rearranged = s.slice(4) + s.slice(0, 4);
let rem = 0;
for (const ch of rearranged) {
const v = ch >= 'A' ? String(ch.charCodeAt(0) - 55) : ch;
for (const digit of v)
rem = (rem * 10 + (digit.charCodeAt(0) - 48)) % 97;
}
return rem === 1;
}
/** Phone numbers: international (+CC), Brazilian and North American formats with separators. */
export function phoneValid(raw) {
const digits = raw.replace(/\D/g, '');
if (raw.trim().startsWith('+'))
return digits.length >= 8 && digits.length <= 15;
if (digits.length < 10 || digits.length > 11)
return false;
// Brazil: (11) 91234-5678, 11 91234-5678, (11) 3123-4567
if (/^\(?\d{2}\)?[\s.-]?9?\d{4}[\s.-]\d{4}$/.test(raw.trim()))
return true;
// North America: (415) 555-0132, 415-555-0132, 415.555.0132
if (/^\(?\d{3}\)?[\s.-]?\d{3}[\s.-]\d{4}$/.test(raw.trim()))
return true;
return false;
}
// Boundaries are explicit look-arounds instead of \b so formatted numbers ("(11) 9…") are whole.
const PATTERNS = [
{
type: 'email',
re: /(?<![\w.+-])[A-Za-z0-9](?:[A-Za-z0-9._%+-]{0,63})@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,62})\.)+[A-Za-z]{2,24}(?![\w-])/g,
score: 0.99,
},
{
type: 'iban',
re: /(?<![A-Za-z0-9])[A-Z]{2}\d{2}(?: ?[A-Z0-9]){11,30}(?![A-Za-z0-9])/g,
valid: ibanValid,
score: 0.99,
},
{
type: 'cnpj',
re: /(?<![\w./-])[0-9A-Z]{2}\.?[0-9A-Z]{3}\.?[0-9A-Z]{3}\/?[0-9A-Z]{4}-?\d{2}(?![\w/-])/g,
valid: (m) => cnpjValid(m) && (/^\d/.test(m.replace(/\D/g, '')) || /[./-]/.test(m)) && !/^[A-Z]+$/.test(m),
score: 0.98,
},
{
type: 'cpf',
re: /(?<![\w./-])\d{3}\.?\d{3}\.?\d{3}-?\d{2}(?![\w/-])/g,
valid: cpfValid,
score: 0.95,
},
{
type: 'credit_card',
re: /(?<![\d-])\d(?:[ -]?\d){11,18}(?![\d-])/g,
valid: (m) => {
const digits = m.replace(/\D/g, '');
// Mixed separators ("4111 1111-1111 1111") are unusual for cards; accept one kind.
if (/ /.test(m) && /-/.test(m))
return false;
return digits.length >= 13 && digits.length <= 19 && luhnValid(digits) && !!cardBrand(digits);
},
score: 0.97,
},
{
type: 'ssn',
re: /(?<![\d-])\d{3}[- ]\d{2}[- ]\d{4}(?![\d-])/g,
valid: ssnValid,
score: 0.85,
},
{
type: 'phone',
re: /(?<![\w+])(?:\+\d{1,3}[\s.-]?)?\(?\d{2,4}\)?[\s.-]?\d{3,5}[\s.-]\d{4}(?![\w])/g,
valid: phoneValid,
score: 0.75,
},
{
type: 'ip_address',
re: /(?<![\d.])(?:\d{1,3}\.){3}\d{1,3}(?![\d.])/g,
valid: (m) => isIPv4(m) && !m.startsWith('0.') && m !== '127.0.0.1',
score: 0.6,
},
];
/** PII with format + checksum validation. Overlaps are resolved by the caller (longest, earliest). */
export function detectPii(text, types = PII_TYPES) {
const want = new Set(types);
const out = [];
for (const p of PATTERNS) {
if (!want.has(p.type))
continue;
p.re.lastIndex = 0;
for (let m = p.re.exec(text); m; m = p.re.exec(text)) {
const value = m[0];
if (p.valid && !p.valid(value))
continue;
out.push({
detector: 'pii',
category: `pii.${p.type}`,
start: m.index,
end: m.index + value.length,
score: p.score,
value,
spanned: true,
});
}
}
return out;
}
//# sourceMappingURL=pii.js.map