euredact 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,27 +5,25 @@ export function normalize(text) {
5
5
  return [normalized, buildOffsetMapping(text, normalized)];
6
6
  }
7
7
  function buildOffsetMapping(original, normalized) {
8
- const nfdOriginal = original.normalize("NFD");
9
- const nfdNormalized = normalized.normalize("NFD");
10
- // Build: normalized index -> original index via NFD as intermediate
8
+ // Precompute cumulative NFD lengths for normalized characters
9
+ const normNfdCum = [0];
10
+ for (let i = 0; i < normalized.length; i++) {
11
+ normNfdCum.push(normNfdCum[i] + normalized[i].normalize("NFD").length);
12
+ }
13
+ // Precompute cumulative NFD lengths for original characters
14
+ const origNfdCum = [0];
15
+ for (let i = 0; i < original.length; i++) {
16
+ origNfdCum.push(origNfdCum[i] + original[i].normalize("NFD").length);
17
+ }
18
+ // Two-pointer scan: map each normalized position to its original position
11
19
  const normToOrig = [];
20
+ let origPtr = 0;
12
21
  for (let normIdx = 0; normIdx < normalized.length; normIdx++) {
13
- const nfdOfNormChar = normalized[normIdx].normalize("NFD");
14
- let nfdStart = 0;
15
- for (let i = 0; i < normIdx; i++) {
16
- nfdStart += normalized[i].normalize("NFD").length;
17
- }
18
- let targetOrig = 0;
19
- let running = 0;
20
- for (let oi = 0; oi < original.length; oi++) {
21
- const nfdOc = original[oi].normalize("NFD");
22
- if (running + nfdOc.length > nfdStart) {
23
- targetOrig = oi;
24
- break;
25
- }
26
- running += nfdOc.length;
22
+ const nfdStart = normNfdCum[normIdx];
23
+ while (origPtr + 1 < original.length && origNfdCum[origPtr + 1] <= nfdStart) {
24
+ origPtr++;
27
25
  }
28
- normToOrig.push(targetOrig);
26
+ normToOrig.push(origPtr);
29
27
  }
30
28
  return normToOrig;
31
29
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * BIC registry — tier-1 membership testing for ISO 9362 bank identifier codes.
3
+ *
4
+ * BIC is the only bank identifier in the supported set with **no check digit**
5
+ * (IBAN has mod-97, VAT has country-specific checksums). Structure alone cannot
6
+ * separate a genuine BIC from an ordinary 8- or 11-character uppercase word,
7
+ * because characters 5-6 of everyday words are frequently valid ISO 3166
8
+ * country codes (`DRINGEND` -> `GE`, `HOSPITAL` -> `IT`). Registry membership
9
+ * is therefore the strongest precision signal available.
10
+ *
11
+ * Two sources are consulted, in order:
12
+ *
13
+ * 1. A deployment-supplied registry, installed via {@link setBicRegistry}.
14
+ * This is the path for anyone holding a licensed directory (SWIFTRef).
15
+ * 2. {@link SEED_BIC6_PREFIXES} — a small bundled set of institution+country
16
+ * prefixes for major European banks, compiled from publicly published bank
17
+ * data. No licensed directory is bundled.
18
+ *
19
+ * Matching is on the **BIC6 prefix** (4-letter institution + 2-letter country),
20
+ * so any location and branch code on a known institution is accepted. The
21
+ * prefix is the stable part of a BIC; branch codes churn as branches open and
22
+ * close.
23
+ *
24
+ * The registry is used as an *accept* signal, never as a filter: a candidate
25
+ * that misses it falls through to the context gate in `suppressors.ts` and is
26
+ * still detected when banking context is present. A stale list therefore costs
27
+ * a little recall on bare, contextless BICs — it never causes a leak. Annual
28
+ * review is sufficient.
29
+ */
30
+ export declare const SEED_BIC6_PREFIXES: ReadonlySet<string>;
31
+ /** A membership test over BIC8/BIC11 codes or bare BIC6 prefixes. */
32
+ export type BicRegistryProvider = (bic: string) => boolean;
33
+ /**
34
+ * Install a deployment-supplied BIC registry, consulted ahead of the seed list.
35
+ *
36
+ * Accepts a membership callable, any iterable of BIC strings, or `null` to
37
+ * remove a previously installed registry. Entries may be full BIC8/BIC11 codes
38
+ * or bare BIC6 prefixes; both are matched, case-insensitively and ignoring
39
+ * spaces.
40
+ */
41
+ export declare function setBicRegistry(registry: BicRegistryProvider | Iterable<string> | null): void;
42
+ /** Return the installed registry provider, or null if none is installed. */
43
+ export declare function getBicRegistry(): BicRegistryProvider | null;
44
+ /**
45
+ * Tier 1: is `candidate` a BIC of a known institution?
46
+ *
47
+ * Checks the deployment-supplied registry first (full code, then BIC6 prefix),
48
+ * then the bundled seed prefixes.
49
+ */
50
+ export declare function isRegisteredBic(candidate: string): boolean;
@@ -0,0 +1,233 @@
1
+ /**
2
+ * BIC registry — tier-1 membership testing for ISO 9362 bank identifier codes.
3
+ *
4
+ * BIC is the only bank identifier in the supported set with **no check digit**
5
+ * (IBAN has mod-97, VAT has country-specific checksums). Structure alone cannot
6
+ * separate a genuine BIC from an ordinary 8- or 11-character uppercase word,
7
+ * because characters 5-6 of everyday words are frequently valid ISO 3166
8
+ * country codes (`DRINGEND` -> `GE`, `HOSPITAL` -> `IT`). Registry membership
9
+ * is therefore the strongest precision signal available.
10
+ *
11
+ * Two sources are consulted, in order:
12
+ *
13
+ * 1. A deployment-supplied registry, installed via {@link setBicRegistry}.
14
+ * This is the path for anyone holding a licensed directory (SWIFTRef).
15
+ * 2. {@link SEED_BIC6_PREFIXES} — a small bundled set of institution+country
16
+ * prefixes for major European banks, compiled from publicly published bank
17
+ * data. No licensed directory is bundled.
18
+ *
19
+ * Matching is on the **BIC6 prefix** (4-letter institution + 2-letter country),
20
+ * so any location and branch code on a known institution is accepted. The
21
+ * prefix is the stable part of a BIC; branch codes churn as branches open and
22
+ * close.
23
+ *
24
+ * The registry is used as an *accept* signal, never as a filter: a candidate
25
+ * that misses it falls through to the context gate in `suppressors.ts` and is
26
+ * still detected when banking context is present. A stale list therefore costs
27
+ * a little recall on bare, contextless BICs — it never causes a leak. Annual
28
+ * review is sufficient.
29
+ */
30
+ // BIC6 prefixes (institution + country) of major European institutions,
31
+ // compiled from publicly published bank data — not from a licensed directory.
32
+ // Kept deliberately small: this covers the banks that appear bare and
33
+ // contextless in real documents. Anything outside this list is still detected
34
+ // via the context gate.
35
+ export const SEED_BIC6_PREFIXES = new Set([
36
+ // --- Netherlands ---
37
+ "ABNANL", // ABN AMRO
38
+ "INGBNL", // ING
39
+ "RABONL", // Rabobank
40
+ "SNSBNL", // SNS Bank
41
+ "TRIONL", // Triodos
42
+ "BUNQNL", // bunq
43
+ "KNABNL", // Knab
44
+ "ASNBNL", // ASN Bank
45
+ "RBRBNL", // RegioBank
46
+ "FVLBNL", // Van Lanschot
47
+ "BITSNL", // BinckBank
48
+ "DEUTNL", // Deutsche Bank NL
49
+ // --- Belgium ---
50
+ "BBRUBE", // BNP Paribas Fortis
51
+ "GEBABE", // BNP Paribas Fortis (legacy)
52
+ "KREDBE", // KBC
53
+ "GKCCBE", // Belfius
54
+ "BPOTBE", // bpost bank
55
+ "ARSPBE", // Argenta
56
+ "AXABBE", // AXA Bank Belgium
57
+ "CREGBE", // Belfius (legacy)
58
+ "NICABE", // Crelan
59
+ "TRIOBE", // Triodos Belgium
60
+ "CTBKBE", // Banca Monte Paschi Belgio
61
+ "JVBABE", // Van Breda
62
+ // --- Germany ---
63
+ "DEUTDE", // Deutsche Bank
64
+ "COBADE", // Commerzbank
65
+ "PBNKDE", // Postbank
66
+ "GENODE", // Volksbanken / Raiffeisen
67
+ "MALADE", // Sparkassen
68
+ "HELADE", // Helaba / Sparkassen
69
+ "BYLADE", // BayernLB / Sparkassen
70
+ "SOLADE", // Sparkassen (Baden-Wurttemberg)
71
+ "WELADE", // Sparkassen (Westfalen)
72
+ "NOLADE", // Norddeutsche Landesbank
73
+ "SPKKDE", // Sparkasse
74
+ "HYVEDE", // UniCredit / HypoVereinsbank
75
+ "DRESDE", // Dresdner (legacy Commerzbank)
76
+ "INGDDE", // ING-DiBa
77
+ "NTSBDE", // N26
78
+ "SFRTDE", // Sutor / fintech BaaS
79
+ "TEDADE", // Telda / BaaS
80
+ // --- France ---
81
+ "BNPAFR", // BNP Paribas
82
+ "SOGEFR", // Societe Generale
83
+ "AGRIFR", // Credit Agricole
84
+ "CCBPFR", // Banque Populaire
85
+ "CEPAFR", // Caisse d'Epargne
86
+ "CMCIFR", // Credit Mutuel / CIC
87
+ "PSSTFR", // La Banque Postale
88
+ "BREDFR", // BRED Banque Populaire
89
+ "CRLYFR", // LCL
90
+ "HSBCFR", // HSBC France
91
+ "TRZOFR", // Treezor
92
+ "QNTOFR", // Qonto
93
+ // --- Austria ---
94
+ "GIBAAT", // Erste Bank / Sparkassen
95
+ "BKAUAT", // UniCredit Bank Austria
96
+ "RZBAAT", // Raiffeisen Bank International
97
+ "RLNWAT", // Raiffeisen NO-Wien
98
+ "OBKLAT", // Oberbank
99
+ "BAWAAT", // BAWAG P.S.K.
100
+ "VBOEAT", // Volksbank
101
+ "HYPTAT", // Hypo Tirol
102
+ "SPIHAT", // Sparkasse
103
+ // --- Luxembourg ---
104
+ "BCEELU", // Banque et Caisse d'Epargne de l'Etat
105
+ "BILLLU", // Banque Internationale a Luxembourg
106
+ "BGLLLU", // BGL BNP Paribas
107
+ "CCRALU", // Banque Raiffeisen
108
+ "BLUXLU", // Banque de Luxembourg
109
+ // --- Ireland / UK ---
110
+ "AIBKIE", // Allied Irish Banks
111
+ "BOFIIE", // Bank of Ireland
112
+ "ULSBIE", // Ulster Bank
113
+ "CITIIE", // Citibank Ireland
114
+ "BARCGB", // Barclays
115
+ "HBUKGB", // HSBC UK
116
+ "MIDLGB", // HSBC (legacy Midland)
117
+ "LOYDGB", // Lloyds
118
+ "NWBKGB", // NatWest
119
+ "RBOSGB", // Royal Bank of Scotland
120
+ "TSBSGB", // TSB
121
+ "REVOGB", // Revolut
122
+ "MONZGB", // Monzo
123
+ "STARGB", // Starling
124
+ "CHASGB", // JP Morgan Chase UK
125
+ // --- Nordics ---
126
+ "NDEADK", // Nordea Denmark
127
+ "DABADK", // Danske Bank
128
+ "JYBADK", // Jyske Bank
129
+ "SYBKDK", // Sydbank
130
+ "NDEASE", // Nordea Sweden
131
+ "ESSESE", // SEB
132
+ "HANDSE", // Handelsbanken
133
+ "SWEDSE", // Swedbank
134
+ "DNBANO", // DNB
135
+ "NDEANO", // Nordea Norway
136
+ "NDEAFI", // Nordea Finland
137
+ "OKOYFI", // OP Corporate Bank
138
+ "HELSFI", // Aktia
139
+ "NBIIIS", // Landsbankinn (NBIIISRE)
140
+ "ESJAIS", // Arion Bank (ESJAISRE)
141
+ "GLITIS", // Islandsbanki (GLITISRE)
142
+ "MPBAIS", // Kvika banki (MPBAISRE)
143
+ "KBNONO", // BNbank ASA (KBNONO22)
144
+ "PRBONO", // Boligbanken ASA (PRBONO22)
145
+ "AARBDE", // Aareal Bank AG (AARBDE5W)
146
+ // --- Southern / Central Europe ---
147
+ "UNCRIT", // UniCredit Italy
148
+ "BCITIT", // Intesa Sanpaolo
149
+ "PASCIT", // Monte dei Paschi
150
+ "BPMOIT", // Banco BPM
151
+ "CRPPIT", // Cassa di Risparmio
152
+ "BSCHES", // Banco Santander
153
+ "BBVAES", // BBVA
154
+ "CAIXES", // CaixaBank
155
+ "SABBES", // Banco Sabadell
156
+ "BKBKES", // Bankinter
157
+ "BCOMPT", // Millennium BCP
158
+ "CGDIPT", // Caixa Geral de Depositos
159
+ "BPIPPT", // Banco BPI
160
+ "NOVBPT", // Novo Banco
161
+ "BPKOPL", // PKO Bank Polski
162
+ "BREXPL", // mBank
163
+ "INGBPL", // ING Bank Slaski
164
+ "WBKPPL", // Santander Bank Polska
165
+ "KOMBCZ", // Komercni banka
166
+ "CEKOCZ", // CSOB
167
+ "GIBACZ", // Ceska sporitelna
168
+ "TATRSK", // Tatra banka
169
+ "SUBASK", // Vseobecna uverova banka
170
+ "GIBASK", // Slovenska sporitelna
171
+ "OTPVHU", // OTP Bank
172
+ "BUDAHU", // Budapest Bank
173
+ "MKKBHU", // MKB Bank
174
+ // --- Switzerland ---
175
+ "UBSWCH", // UBS
176
+ "CRESCH", // Credit Suisse
177
+ "POFICH", // PostFinance
178
+ "ZKBKCH", // Zurcher Kantonalbank
179
+ "RAIFCH", // Raiffeisen Switzerland
180
+ "MIGRCH", // Migros Bank
181
+ ]);
182
+ let provider = null;
183
+ /**
184
+ * Install a deployment-supplied BIC registry, consulted ahead of the seed list.
185
+ *
186
+ * Accepts a membership callable, any iterable of BIC strings, or `null` to
187
+ * remove a previously installed registry. Entries may be full BIC8/BIC11 codes
188
+ * or bare BIC6 prefixes; both are matched, case-insensitively and ignoring
189
+ * spaces.
190
+ */
191
+ export function setBicRegistry(registry) {
192
+ if (registry === null) {
193
+ provider = null;
194
+ return;
195
+ }
196
+ if (typeof registry === "function") {
197
+ provider = registry;
198
+ return;
199
+ }
200
+ const known = new Set();
201
+ for (const entry of registry) {
202
+ const e = String(entry).trim();
203
+ if (e && !e.startsWith("#"))
204
+ known.add(e.replace(/ /g, "").toUpperCase());
205
+ }
206
+ provider = (bic) => known.has(bic);
207
+ }
208
+ /** Return the installed registry provider, or null if none is installed. */
209
+ export function getBicRegistry() {
210
+ return provider;
211
+ }
212
+ /**
213
+ * Tier 1: is `candidate` a BIC of a known institution?
214
+ *
215
+ * Checks the deployment-supplied registry first (full code, then BIC6 prefix),
216
+ * then the bundled seed prefixes.
217
+ */
218
+ export function isRegisteredBic(candidate) {
219
+ const c = candidate.replace(/ /g, "").toUpperCase();
220
+ if (c.length !== 8 && c.length !== 11)
221
+ return false;
222
+ const prefix = c.slice(0, 6);
223
+ if (provider !== null) {
224
+ try {
225
+ if (provider(c) || provider(prefix))
226
+ return true;
227
+ }
228
+ catch {
229
+ // a broken provider must not break detection
230
+ }
231
+ }
232
+ return SEED_BIC6_PREFIXES.has(prefix);
233
+ }
@@ -1,4 +1,24 @@
1
1
  import { EntityType } from "../../types.js";
2
+ import { IBAN_LENGTHS } from "../validators.js";
3
+ /**
4
+ * Build a country-independent IBAN pattern from the ISO 13616 length table.
5
+ *
6
+ * An IBAN carries its own country code and a mod-97 checksum, so it is
7
+ * self-identifying and must never depend on the caller's `countries` setting —
8
+ * a Belgian account in an Austrian file is still an account.
9
+ *
10
+ * Each alternative pins the *exact* character count for its country, which
11
+ * stops the match absorbing a following word: without it, greedy matching
12
+ * turns `BE68 5390 0754 7034 KBC` into one over-long candidate that fails
13
+ * mod-97 and is lost entirely.
14
+ */
15
+ function buildIbanPattern() {
16
+ const alternatives = Object.entries(IBAN_LENGTHS)
17
+ .sort(([a], [b]) => a.localeCompare(b))
18
+ .map(([code, length]) => `${code}\\d{2}(?:[ \\-]?[A-Z0-9]){${length - 4}}`);
19
+ return String.raw `\b(?:` + alternatives.join("|") + String.raw `)\b`;
20
+ }
21
+ const GENERIC_IBAN = buildIbanPattern();
2
22
  /**
3
23
  * Helper to create a PatternDef with sensible defaults.
4
24
  */
@@ -6,6 +26,45 @@ function p(entityType, pattern, validator = null, description = "", contextKeywo
6
26
  return { entityType, pattern, validator, description, contextKeywords, requiresContext };
7
27
  }
8
28
  // ---------------------------------------------------------------------------
29
+ // Secret context keywords (consolidated, reused across SECRET patterns)
30
+ // ---------------------------------------------------------------------------
31
+ const SECRET_CONTEXT = [
32
+ // English
33
+ "key", "token", "secret", "password", "credential",
34
+ "api_key", "apikey", "api-key", "auth", "bearer",
35
+ "aws_secret_access_key", "access_key", "private_key",
36
+ "AES_KEY", "ENCRYPTION_KEY", "SIGNING_KEY",
37
+ "ACCOUNT_SID", "AUTH_TOKEN", "TWILIO",
38
+ "RAILS_SECRET", "SECRET_KEY_BASE", "DATABASE_URL",
39
+ "NPM_TOKEN", "DIGITALOCEAN", "GOOGLE_API",
40
+ "DB_PASS", "DB_PASSWORD", "DATASOURCE_PASSWORD",
41
+ "JWT_SIGNING", "passwd", "DISCORD_TOKEN",
42
+ // Dutch
43
+ "wachtwoord", "sleutel", "geheim",
44
+ // French
45
+ "mot de passe", "mot_de_passe", "clé", "jeton",
46
+ // German
47
+ "Passwort", "Kennwort", "Schlüssel", "Geheimnis",
48
+ // Italian
49
+ "chiave", "segreto",
50
+ // Spanish
51
+ "contraseña", "clave",
52
+ // Portuguese
53
+ "senha", "chave",
54
+ // Polish
55
+ "hasło", "klucz",
56
+ // Czech/Slovak
57
+ "heslo", "kľúč",
58
+ // Hungarian
59
+ "jelszó", "kulcs",
60
+ // Romanian
61
+ "parola",
62
+ // Nordic
63
+ "lösenord", "nøkkel", "nyckel", "avain", "lykill",
64
+ // Greek
65
+ "κωδικός",
66
+ ];
67
+ // ---------------------------------------------------------------------------
9
68
  // DOB context keywords (reused across SHARED patterns)
10
69
  // ---------------------------------------------------------------------------
11
70
  const DOB_CONTEXT = [
@@ -33,8 +92,28 @@ const SHARED = {
33
92
  code: "SHARED",
34
93
  name: "Shared EU Patterns",
35
94
  patterns: [
36
- p(EntityType.EMAIL, String.raw `\b[\w._%+\-]+@[\w.\-]+\.[a-zA-Z]{2,}\b`),
95
+ // JavaScript's \w and \b are ASCII-only, so the Python engine's \w (which
96
+ // is Unicode-aware) matched local parts like "petras_ž@..." while this one
97
+ // did not. \p{L}\p{N} with the `u` flag restores parity; the leading guard
98
+ // is a Unicode-safe word boundary.
99
+ p(EntityType.EMAIL, String.raw `(?<![\p{L}\p{N}._%+\-])[\p{L}\p{N}._%+\-]+@(?:[\p{L}\p{N}\-]+\.)+\p{L}{2,}\b`),
100
+ // IBAN (any country) — structure + mod-97 only. Country-specific IBAN
101
+ // patterns still exist and win on identical spans; this one guarantees an
102
+ // IBAN is never missed just because its country was not requested.
103
+ p(EntityType.BANK_ACCOUNT, GENERIC_IBAN, "iban", "IBAN — any country (ISO 13616, mod-97 validated)"),
104
+ // International phone (E.164, any country). Runs alongside the per-country
105
+ // patterns, which each hard-code a single grouping and miss the rest:
106
+ // "+43 664 8213 907" was invisible to the Austrian pattern, which expects
107
+ // an unbroken subscriber number. A leading "+" is self-identifying, so
108
+ // this is deliberately not country-gated.
109
+ p(EntityType.PHONE, String.raw `\+\d{1,3}(?:[\s\-/]?\(?\d{1,4}\)?){2,7}`, "e164", "International phone number (E.164) — any country"),
37
110
  p(EntityType.BIC, String.raw `\b[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?\b`, "bic"),
111
+ // Space-separated form: "RZBA AT WW", "NICA BE BB". Written as a separate
112
+ // pattern requiring a space between *every* group, rather than making the
113
+ // spaces optional above — optional spaces would let "GEBABEBB KBC" match
114
+ // as one 11-character code and claim the following word. Only a literal
115
+ // space is allowed, so a match cannot span a line break.
116
+ p(EntityType.BIC, String.raw `\b[A-Z]{4} [A-Z]{2} [A-Z0-9]{2}(?: [A-Z0-9]{3})?\b`, "bic", "BIC/SWIFT code (space-separated groups)"),
38
117
  p(EntityType.CREDIT_CARD, String.raw `\b(?:4[0-9]{3}[\s\-]?[0-9]{4}[\s\-]?[0-9]{4}[\s\-]?[0-9]{4}|5[1-5][0-9]{2}[\s\-]?[0-9]{4}[\s\-]?[0-9]{4}[\s\-]?[0-9]{4}|3[47][0-9]{2}[\s\-]?[0-9]{6}[\s\-]?[0-9]{5})\b`, "luhn"),
39
118
  p(EntityType.VIN, String.raw `\b[A-HJ-NPR-Z0-9]{17}\b`, "vin"),
40
119
  p(EntityType.IP_ADDRESS, String.raw `\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b`),
@@ -45,7 +124,7 @@ const SHARED = {
45
124
  p(EntityType.IMEI, String.raw `\b\d{2}[\-\s]\d{6}[\-\s]\d{6}[\-\s]\d\b`, "imei", "formatted"),
46
125
  p(EntityType.GPS_COORDINATES, String.raw `-?(?:[1-8]?\d(?:\.\d{4,})|90(?:\.0{4,}))\s*[,;/]\s*-?(?:1[0-7]\d(?:\.\d{4,})|180(?:\.0{4,})|\d{1,2}(?:\.\d{4,}))`),
47
126
  p(EntityType.UUID, String.raw `\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\b`),
48
- p(EntityType.SOCIAL_HANDLE, String.raw `(?<!\w)@[a-zA-Z][a-zA-Z0-9_.]{1,29}\b`),
127
+ p(EntityType.SOCIAL_HANDLE, String.raw `(?<![\p{L}\p{N}_])@\p{L}[\p{L}\p{N}_.]{1,29}\b`),
49
128
  // --- Secret / API Key (known prefixes — always active) ---
50
129
  p(EntityType.SECRET, String.raw `\bAKIA[A-Z0-9]{16}\b`, null, "AWS Access Key ID"),
51
130
  p(EntityType.SECRET, String.raw `\bgh[poas]_[a-zA-Z0-9]{36,}\b`, null, "GitHub token"),
@@ -67,35 +146,18 @@ const SHARED = {
67
146
  p(EntityType.SECRET, String.raw `\bxapp-\d[\d\-]{20,}`, null, "Slack app numeric token"),
68
147
  p(EntityType.SECRET, String.raw `\bxapp-[a-zA-Z0-9\-]{20,}`, null, "Slack app token"),
69
148
  p(EntityType.SECRET, String.raw `\bdapi[a-f0-9]{32,}\b`, null, "Databricks PAT"),
149
+ // Discord bot token (3-segment base64 with numeric first segment)
150
+ p(EntityType.SECRET, String.raw `\b[MN][A-Za-z0-9]{23,}\.[A-Za-z0-9_\-]{6}\.[A-Za-z0-9_\-]{27,}\b`, null, "Discord bot token"),
151
+ // PEM private key headers
152
+ p(EntityType.SECRET, String.raw `-----BEGIN\s(?:RSA\s)?PRIVATE\sKEY-----[\s\S]{0,16384}?-----END\s(?:RSA\s)?PRIVATE\sKEY-----`, null, "PEM private key"),
153
+ // AWS Secret Access Key (40 chars, base64 with +/ after an AKIA line)
154
+ p(EntityType.SECRET, String.raw `(?<=aws_secret_access_key\s{0,4}[:=]\s{0,4})[A-Za-z0-9+/]{40}`, null, "AWS Secret Access Key"),
70
155
  // --- Secret / API Key (connection strings with embedded credentials) ---
71
- p(EntityType.SECRET, String.raw `(?:mongodb|mysql|postgres(?:ql)?|redis|amqp|rabbitmq):\/\/[^\s:]+:[^\s@]+@[^\s]+`, null, "Connection string with credentials"),
156
+ p(EntityType.SECRET, String.raw `(?:mongodb|mysql|postgres(?:ql)?|redis|amqp|rabbitmq):\/\/[^\s:]{1,256}:[^\s@]{1,256}@[^\s]+`, null, "Connection string with credentials"),
72
157
  // --- Secret / API Key (assignment-based: KEY=value or KEY: value) ---
73
- p(EntityType.SECRET, String.raw `(?<=[:=] )\S{8,}|(?<=[:=])\S{8,}`, "high_entropy", "Assigned secret value", [
74
- "key", "token", "secret", "password", "credential",
75
- "api_key", "apikey", "api-key", "auth", "bearer",
76
- "aws_secret_access_key", "access_key", "private_key",
77
- "wachtwoord", "mot de passe", "Passwort", "Schlüssel",
78
- "clé", "sleutel", "lösenord", "hasło", "heslo",
79
- "jelszó", "contraseña", "senha", "parola",
80
- "AES_KEY", "ENCRYPTION_KEY", "SIGNING_KEY",
81
- "ACCOUNT_SID", "AUTH_TOKEN", "TWILIO",
82
- "RAILS_SECRET", "SECRET_KEY_BASE", "DATABASE_URL",
83
- "NPM_TOKEN", "DIGITALOCEAN", "GOOGLE_API",
84
- "kľúč", "nøkkel", "nyckel", "avain", "lykill",
85
- "DB_PASS", "DB_PASSWORD", "DATASOURCE_PASSWORD",
86
- "JWT_SIGNING", "SIGNING_KEY",
87
- "passwd", "Kennwort",
88
- ], true),
158
+ p(EntityType.SECRET, String.raw `(?<=[:=] )\S{8,}|(?<=[:=])\S{8,}`, "high_entropy", "Assigned secret value", SECRET_CONTEXT, true),
89
159
  // --- Secret / API Key (entropy-based fallback for longer tokens) ---
90
- p(EntityType.SECRET, String.raw `\b[A-Za-z0-9_\-+/]{24,}[A-Za-z0-9_\-+/=]*\b`, "high_entropy", "High-entropy token", [
91
- "key", "token", "secret", "password", "credential",
92
- "api_key", "apikey", "api-key", "auth", "bearer",
93
- "aws_secret_access_key", "access_key", "private_key",
94
- "wachtwoord", "mot de passe", "Passwort", "Schlüssel",
95
- "clé", "sleutel", "lösenord", "hasło", "heslo",
96
- "jelszó", "contraseña", "senha", "parola",
97
- "kľúč", "nøkkel", "nyckel", "avain", "lykill",
98
- ], true),
160
+ p(EntityType.SECRET, String.raw `\b[A-Za-z0-9_\-+/]{24,}[A-Za-z0-9_\-+/=]*\b`, "high_entropy", "High-entropy token", SECRET_CONTEXT, true),
99
161
  p(EntityType.DOB, String.raw `\b(?:0[1-9]|[12][0-9]|3[01])[/.\-](?:0[1-9]|1[0-2])[/.\-](?:19|20)\d{2}\b`, null, "DD/MM/YYYY", DOB_CONTEXT, true),
100
162
  p(EntityType.DATE_OF_DEATH, String.raw `\b(?:0[1-9]|[12][0-9]|3[01])[/.\-](?:0[1-9]|1[0-2])[/.\-](?:19|20)\d{2}\b`, null, "", DATE_OF_DEATH_CONTEXT, true),
101
163
  p(EntityType.DOB, String.raw `\b(?:19|20)\d{2}[/.\-](?:0[1-9]|1[0-2])[/.\-](?:0[1-9]|[12][0-9]|3[01])\b`, null, "ISO YYYY-MM-DD", DOB_ISO_CONTEXT, true),
@@ -152,7 +214,13 @@ const DE = {
152
214
  p(EntityType.PHONE, String.raw `\b0[1-9]\d{1,4}[\s/\-]?\d{3,8}\b`),
153
215
  p(EntityType.PHONE, String.raw `\+49\s?\d{2,5}[\s/\-]?\d{3,8}\b`),
154
216
  p(EntityType.PASSPORT, String.raw `\b[CFGHJK][0-9A-Z]{8,9}\b`, null, "", ["Reisepass", "passport", "Passnummer", "Reisepassnummer", "Reisepass Nummer", "Reisepass-Nr", "Pass Nr"], true),
155
- p(EntityType.LICENSE_PLATE, String.raw `\b[A-ZÄÖÜ]{1,3}[\s\-]?[A-Z]{1,2}[\s\-]?\d{1,4}[EH]?\b`),
217
+ // The separator after the city code is mandatory. When it was optional, a
218
+ // contiguous letter run split across both groups and the hyphen was
219
+ // consumed as the *second* separator, so any "LETTERS-DIGITS" token
220
+ // matched: ICD-10 parsed as city "IC" + letters "D" + "-" + "10", and
221
+ // ISO-9001, COVID-19, RFC-2822, POL-2025 likewise. A real plate always
222
+ // separates the city code: "B-AB 1234", "M-XY 99".
223
+ p(EntityType.LICENSE_PLATE, String.raw `\b[A-ZÄÖÜ]{1,3}[\s\-][A-Z]{1,2}[\s\-]?\d{1,4}[EH]?\b`),
156
224
  p(EntityType.POSTAL_CODE, String.raw `\b\d{5}\b`, null, "", ["PLZ", "Postleitzahl", "postal code", "postcode", "Anschrift", "Adresse", "Wohnort", "Ort", "Stadt", "wohnt", "wohnhaft", "Straße", "Str.", "Weg", "Platz", "Allee", "Ring"], true),
157
225
  p(EntityType.CHAMBER_OF_COMMERCE, String.raw `\bHR[AB]\s?\d{4,6}\b`),
158
226
  p(EntityType.SSN, String.raw `\b\d{2}[\s]?\d{6}[\s]?[A-Z][\s]?\d{3}\b`, null, "", ["Rentenversicherung", "RV-Nummer", "Sozialversicherung", "SV-Nummer", "Versicherungsnummer", "Rentenversicherungsnummer", "Sozialversicherungsnummer"], true),
@@ -169,6 +237,11 @@ const AT = {
169
237
  p(EntityType.IBAN, String.raw `\bAT\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b`, "iban"),
170
238
  p(EntityType.VAT, String.raw `\bATU\d{8}\b`),
171
239
  p(EntityType.PHONE, String.raw `\b0\d{3,4}[\s\-]?\d{5,8}\b`),
240
+ // Short area codes (Vienna is "01") split across three groups:
241
+ // "01 53460 2215". The pattern above requires 3-4 digits after the trunk
242
+ // 0, so it never matched these. Both separators are mandatory here, which
243
+ // keeps "01 2025" (a date fragment) out.
244
+ p(EntityType.PHONE, String.raw `\b0\d{1,4}[\s\-/]\d{3,7}[\s\-/]\d{2,6}\b`, null, "Austrian national phone — short area code, grouped"),
172
245
  p(EntityType.PHONE, String.raw `\+43\s?\d{3,4}[\s\-]?\d{5,8}`),
173
246
  p(EntityType.LICENSE_PLATE, String.raw `\b[A-ZÄÖÜ]{1,2}\s?\d{1,5}\s?[A-Z]{1,2}\b`, null, "", ["Kennzeichen", "Nummernschild", "Kfz-Kennzeichen"], true),
174
247
  p(EntityType.POSTAL_CODE, String.raw `\b[1-9]\d{3}\b`, null, "", ["PLZ", "Postleitzahl", "Adresse", "Anschrift", "Straße", "Str.", "Gasse", "Weg", "Platz", "Postal:", "Wohnort"], true),
@@ -0,0 +1,16 @@
1
+ /**
2
+ * German vehicle registration district codes (Unterscheidungszeichen).
3
+ *
4
+ * The set is closed: codes are fixed by the Fahrzeug-Zulassungsverordnung and
5
+ * maintained by the Kraftfahrt-Bundesamt, changing only when a district is
6
+ * created, merged, or a historic code is reinstated. That makes membership a
7
+ * usable precision signal — unlike the open-ended blocklist of standards
8
+ * prefixes it replaces, which only ever catches acronyms someone thought of.
9
+ *
10
+ * Used as a **tier**, not a filter: code on this list -> emit; code absent but
11
+ * a plate cue nearby -> emit; neither -> reject. So a missing code costs
12
+ * recall only where there is also no cue. Some entries are also standards
13
+ * prefixes — DIN is Dinslaken as well as the standards body — which is why the
14
+ * standards guard is kept alongside this one.
15
+ */
16
+ export declare const DE_DISTRICT_CODES: ReadonlySet<string>;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * German vehicle registration district codes (Unterscheidungszeichen).
3
+ *
4
+ * The set is closed: codes are fixed by the Fahrzeug-Zulassungsverordnung and
5
+ * maintained by the Kraftfahrt-Bundesamt, changing only when a district is
6
+ * created, merged, or a historic code is reinstated. That makes membership a
7
+ * usable precision signal — unlike the open-ended blocklist of standards
8
+ * prefixes it replaces, which only ever catches acronyms someone thought of.
9
+ *
10
+ * Used as a **tier**, not a filter: code on this list -> emit; code absent but
11
+ * a plate cue nearby -> emit; neither -> reject. So a missing code costs
12
+ * recall only where there is also no cue. Some entries are also standards
13
+ * prefixes — DIN is Dinslaken as well as the standards body — which is why the
14
+ * standards guard is kept alongside this one.
15
+ */
16
+ export const DE_DISTRICT_CODES = new Set([
17
+ "A", "AA", "AB", "ABG", "AC", "AE", "AH", "AIB", "AIC", "AK", "AL", "ALF",
18
+ "ALS", "ALZ", "AM", "AN", "ANA", "ANG", "ANK", "AP", "APD", "AR", "ARN",
19
+ "ART", "AS", "ASD", "ASL", "ASZ", "AT", "AU", "AUR", "AW", "AZ", "AZE",
20
+ "AÖ", "B", "BA", "BAD", "BAR", "BB", "BBG", "BBL", "BC", "BCH", "BD",
21
+ "BE", "BED", "BEI", "BEL", "BER", "BF", "BG", "BGD", "BGL", "BH", "BI",
22
+ "BID", "BIN", "BIR", "BIT", "BIW", "BK", "BKS", "BL", "BLB", "BLK", "BM",
23
+ "BN", "BNA", "BO", "BOG", "BOH", "BOR", "BOT", "BP", "BR", "BRA", "BRB",
24
+ "BRG", "BRI", "BRK", "BRL", "BRV", "BS", "BSB", "BSK", "BT", "BTF", "BU",
25
+ "BUL", "BUR", "BW", "BWL", "BYL", "BZ", "BZA", "BÖ", "BÜD", "BÜS", "BÜZ",
26
+ "C", "CA", "CAS", "CB", "CE", "CHA", "CHL", "CLP", "CLZ", "CO", "COC",
27
+ "COE", "CR", "CT", "CUX", "CW", "D", "DA", "DAH", "DAN", "DAU", "DB",
28
+ "DBR", "DD", "DE", "DEG", "DEL", "DG", "DGF", "DH", "DI", "DIL", "DIN",
29
+ "DIZ", "DKB", "DL", "DLG", "DM", "DN", "DO", "DON", "DS", "DT", "DU",
30
+ "DUD", "DW", "DZ", "DÜW", "E", "EA", "EB", "EBE", "EBN", "EBS", "ECK",
31
+ "ED", "EE", "EF", "EG", "EH", "EHI", "EI", "EIC", "EIH", "EIL", "EIN",
32
+ "EIS", "EL", "EM", "EMD", "EMS", "EN", "ER", "ERB", "ERH", "ERK", "ES",
33
+ "ESA", "ESB", "ESW", "EU", "EUT", "EW", "F", "FAL", "FB", "FD", "FDB",
34
+ "FDS", "FEU", "FF", "FFB", "FG", "FH", "FI", "FKB", "FL", "FLÖ", "FN",
35
+ "FO", "FOR", "FR", "FRG", "FRI", "FRW", "FS", "FT", "FTL", "FW", "FZ",
36
+ "FÜ", "FÜS", "G", "GA", "GAN", "GAP", "GC", "GD", "GDB", "GE", "GEL",
37
+ "GEM", "GEO", "GER", "GF", "GG", "GHA", "GHC", "GI", "GK", "GL", "GLA",
38
+ "GM", "GMN", "GN", "GNT", "GOA", "GOH", "GP", "GR", "GRA", "GRH", "GRI",
39
+ "GRM", "GRS", "GRZ", "GS", "GT", "GTH", "GUB", "GUN", "GV", "GVM", "GW",
40
+ "GZ", "GÖ", "GÜ", "H", "HA", "HAB", "HAL", "HAM", "HAS", "HB", "HBN",
41
+ "HBS", "HC", "HCH", "HD", "HDH", "HDL", "HE", "HEB", "HEF", "HEI", "HEL",
42
+ "HER", "HET", "HF", "HG", "HGN", "HGW", "HH", "HHM", "HI", "HIG", "HIP",
43
+ "HK", "HL", "HM", "HMU", "HN", "HO", "HOG", "HOH", "HOL", "HOM", "HOR",
44
+ "HOT", "HP", "HR", "HRO", "HS", "HSK", "HST", "HU", "HUS", "HV", "HVL",
45
+ "HW", "HWI", "HX", "HY", "HZ", "HÖS", "HÜN", "IGB", "IK", "IL", "ILL",
46
+ "IN", "IS", "IZ", "J", "JB", "JE", "JEV", "JL", "JÜL", "K", "KA", "KAR",
47
+ "KB", "KC", "KE", "KEH", "KEL", "KEM", "KF", "KG", "KH", "KI", "KIB",
48
+ "KK", "KL", "KLE", "KLT", "KLZ", "KM", "KN", "KO", "KOZ", "KR", "KRU",
49
+ "KS", "KT", "KU", "KUS", "KW", "KY", "KYF", "KÖN", "KÖT", "KÜN", "L",
50
+ "LA", "LAN", "LAT", "LAU", "LB", "LBS", "LBZ", "LC", "LD", "LDK", "LDS",
51
+ "LE", "LEO", "LER", "LEV", "LF", "LG", "LH", "LI", "LIB", "LIF", "LIN",
52
+ "LIP", "LK", "LL", "LM", "LN", "LOH", "LOS", "LP", "LR", "LS", "LSA",
53
+ "LSN", "LSZ", "LU", "LUK", "LWL", "LÖ", "LÖB", "LÜD", "LÜN", "M", "MA",
54
+ "MAB", "MAI", "MAK", "MAL", "MAR", "MB", "MC", "MD", "ME", "MED", "MEG",
55
+ "MEI", "MEK", "MEL", "MEP", "MER", "MES", "MET", "MG", "MGH", "MGN", "MH",
56
+ "MHL", "MI", "MIL", "MK", "MKK", "ML", "MM", "MN", "MO", "MOD", "MOL",
57
+ "MON", "MOS", "MQ", "MR", "MS", "MSP", "MST", "MT", "MTK", "MTL", "MVL",
58
+ "MW", "MY", "MYK", "MZ", "MZG", "MÜ", "MÜB", "MÜL", "MÜN", "MÜR", "N",
59
+ "NAB", "NAI", "NAU", "NB", "ND", "NDH", "NE", "NEA", "NEB", "NEC", "NEN",
60
+ "NES", "NEU", "NEW", "NF", "NH", "NI", "NIB", "NK", "NL", "NM", "NMB",
61
+ "NMS", "NOH", "NOL", "NOM", "NOR", "NP", "NR", "NRW", "NRÜ", "NT", "NU",
62
+ "NVP", "NW", "NWM", "NY", "NZ", "NÖ", "OA", "OAL", "OB", "OBB", "OBG",
63
+ "OC", "OCH", "OD", "OE", "OF", "OG", "OH", "OHA", "OHV", "OHZ", "OK",
64
+ "OL", "OLD", "OP", "OPR", "OR", "OS", "OSL", "OTT", "OTW", "OVI", "OVL",
65
+ "OVP", "OZ", "P", "PA", "PAF", "PAN", "PAR", "PB", "PCH", "PE", "PEG",
66
+ "PER", "PF", "PI", "PIR", "PK", "PL", "PLÖ", "PM", "PN", "PR", "PRÜ",
67
+ "PS", "PW", "PZ", "QFT", "QLB", "R", "RA", "RC", "RD", "RDG", "RE", "REG",
68
+ "REH", "REI", "RG", "RH", "RI", "RID", "RIE", "RL", "RM", "RN", "RO",
69
+ "ROD", "ROF", "ROH", "ROK", "ROL", "ROS", "ROT", "ROW", "RPL", "RS",
70
+ "RSL", "RT", "RU", "RV", "RW", "RWL", "RY", "RZ", "RÜD", "RÜG", "S", "SA",
71
+ "SAB", "SAD", "SAL", "SAN", "SAO", "SAW", "SB", "SBG", "SBK", "SC", "SCZ",
72
+ "SDH", "SDL", "SDT", "SE", "SEB", "SEE", "SEF", "SEL", "SF", "SFA", "SFB",
73
+ "SFT", "SG", "SGH", "SH", "SHA", "SHG", "SHK", "SHL", "SHS", "SI", "SIG",
74
+ "SIM", "SK", "SL", "SLE", "SLF", "SLG", "SLK", "SLN", "SLS", "SLZ", "SLÜ",
75
+ "SM", "SMÜ", "SN", "SNH", "SO", "SOB", "SOG", "SOK", "SOL", "SON", "SP",
76
+ "SPB", "SPN", "SPR", "SR", "SRB", "SRO", "ST", "STA", "STB", "STD", "STE",
77
+ "STH", "STL", "STO", "SU", "SUL", "SW", "SWA", "SWZ", "SY", "SZ", "SZB",
78
+ "SÄK", "SÖM", "SÜW", "TBB", "TE", "TF", "TG", "TGO", "THL", "THW", "TIR",
79
+ "TO", "TP", "TR", "TS", "TT", "TUT", "TÖL", "TÖN", "TÜ", "UE", "UEM",
80
+ "UER", "UFF", "UH", "UL", "UM", "UN", "USI", "V", "VAI", "VB", "VEC",
81
+ "VER", "VIB", "VIE", "VIT", "VK", "VL", "VOF", "VOH", "VR", "VS", "W",
82
+ "WA", "WAF", "WAK", "WAM", "WAN", "WAR", "WAT", "WB", "WBS", "WD", "WDA",
83
+ "WE", "WEB", "WEG", "WEI", "WEL", "WEM", "WEN", "WER", "WES", "WF", "WG",
84
+ "WHV", "WI", "WIL", "WIS", "WIT", "WIZ", "WK", "WL", "WLG", "WM", "WMS",
85
+ "WN", "WND", "WO", "WOB", "WOH", "WOL", "WOR", "WOS", "WR", "WRN", "WS",
86
+ "WSF", "WST", "WSW", "WT", "WTL", "WTM", "WUG", "WUM", "WUN", "WUR", "WW",
87
+ "WZ", "WZL", "WÜ", "X", "Y", "Z", "ZE", "ZEL", "ZI", "ZIG", "ZP", "ZR",
88
+ "ZS", "ZW", "ZZ", "ÖHR", "ÜB",
89
+ ]);
@@ -1,4 +1,12 @@
1
1
  import { type Detection } from "../types.js";
2
+ /**
3
+ * The country modules use the EU/VAT convention (UK, EL) while the public
4
+ * contract is ISO 3166-1 alpha-2 (GB, GR). Accept both spellings so a caller
5
+ * passing correct ISO codes is not punished for it.
6
+ */
7
+ export declare const COUNTRY_CODE_ALIASES: Record<string, string>;
8
+ /** Map a caller-supplied code to a registered one, or null if unknown. */
9
+ export declare function resolveCountryCode(countryCode: string): string | null;
2
10
  export declare class RuleEngine {
3
11
  private matcher;
4
12
  private loadedCountries;