- PatternDef.validate + Luhn-gated credit-card detection; de-overlap the generic phone/date/IP patterns; presets.swiss unchanged (production behavior) - strip g/y flags from nameHint so .test() is stateless (latent footgun) - openAICompatibleProvider: bounded retry on transient failures (network / timeout / 429 / 5xx), configurable via retries + retryDelayMs - eslint + prettier + vitest coverage (97%); CI runs lint/format/coverage - docs: README badges + new-option docs, SECURITY.md, issue/PR templates 26 tests passing; build emits ESM+CJS+types. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
56 lines
2.2 KiB
TypeScript
56 lines
2.2 KiB
TypeScript
import type { PatternDef } from './types.js';
|
||
|
||
/**
|
||
* Swiss preset — the structured identifiers most relevant to the Swiss nLPD.
|
||
* Order matters: specific patterns (AVS, IBAN) come before generic ones.
|
||
*/
|
||
const swiss: PatternDef[] = [
|
||
{ tag: 'AVS', re: /\b756\.\d{4}\.\d{4}\.\d{2}\b/g }, // Swiss social security (AVS/AHV)
|
||
{ tag: 'IBAN', re: /\bCH\d{2}[0-9A-Z]{17}\b/gi }, // Swiss IBAN
|
||
{ tag: 'EMAIL', re: /\b[\w.+-]+@[\w-]+\.[\w.-]+\b/g },
|
||
{ tag: 'TEL', re: /(?:\+41|0)(?:[\s.-]?\d){9}\b/g }, // Swiss phone number
|
||
{ tag: 'DATE', re: /\b\d{1,2}[./-]\d{1,2}[./-]\d{2,4}\b/g },
|
||
];
|
||
|
||
/** Luhn check on a credit-card candidate (ignores spaces/dashes); 13–19 digits. */
|
||
function luhnValid(value: string): boolean {
|
||
const digits = value.replace(/\D/g, '');
|
||
if (digits.length < 13 || digits.length > 19) return false;
|
||
let sum = 0;
|
||
let double = false;
|
||
for (let i = digits.length - 1; i >= 0; i--) {
|
||
let d = digits.charCodeAt(i) - 48; // '0' === 48
|
||
if (double) {
|
||
d *= 2;
|
||
if (d > 9) d -= 9;
|
||
}
|
||
sum += d;
|
||
double = !double;
|
||
}
|
||
return sum % 10 === 0;
|
||
}
|
||
|
||
/**
|
||
* Generic preset — locale-agnostic identifiers useful as a starting point
|
||
* anywhere. Best-effort: extend or compose with your own {@link PatternDef}s.
|
||
*
|
||
* Order matters (patterns run in sequence on the already-anonymized text):
|
||
* specific identifiers claim their matches before broader ones, and dates are
|
||
* claimed before phone numbers so they don't get mis-tagged as `TEL`.
|
||
*/
|
||
const generic: PatternDef[] = [
|
||
{ tag: 'EMAIL', re: /\b[\w.+-]+@[\w-]+\.[\w.-]+\b/g },
|
||
{ tag: 'IBAN', re: /\b[A-Z]{2}\d{2}[0-9A-Z]{11,30}\b/g },
|
||
{ tag: 'IPV4', re: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g },
|
||
// 13–19 digits in optional space/dash groups, gated by Luhn to avoid eating
|
||
// arbitrary long digit runs (account numbers, ids).
|
||
{ tag: 'CREDIT_CARD', re: /\b\d(?:[ -]?\d){12,18}\b/g, validate: luhnValid },
|
||
{ tag: 'DATE', re: /\b\d{1,2}[./-]\d{1,2}[./-]\d{2,4}\b/g },
|
||
// Requires grouped/separated digits (or a leading +country) so plain integers
|
||
// aren't mistaken for phone numbers.
|
||
{ tag: 'TEL', re: /(?:\+\d{1,3}[\s.-]?)?(?:\(?\d{2,4}\)?[\s.-]){2,3}\d{2,4}/g },
|
||
];
|
||
|
||
/** Built-in pattern sets for the regex fallback. */
|
||
export const presets = { swiss, generic };
|