euredact 0.1.0 → 0.3.0
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.
- package/README.md +366 -43
- package/dist/cjs/cache.js +2 -2
- package/dist/cjs/index.js +14 -1
- package/dist/cjs/normalizer.js +16 -18
- package/dist/cjs/rules/bicRegistry.js +239 -0
- package/dist/cjs/rules/countries/index.js +119 -2
- package/dist/cjs/rules/engine.js +180 -27
- package/dist/cjs/rules/matchers.js +8 -1
- package/dist/cjs/rules/suppressors.js +246 -9
- package/dist/cjs/rules/validators.js +92 -12
- package/dist/cjs/sdk.js +30 -6
- package/dist/cjs/types.js +19 -2
- package/dist/esm/cache.js +2 -2
- package/dist/esm/index.d.ts +4 -1
- package/dist/esm/index.js +8 -1
- package/dist/esm/normalizer.js +16 -18
- package/dist/esm/rules/bicRegistry.d.ts +50 -0
- package/dist/esm/rules/bicRegistry.js +233 -0
- package/dist/esm/rules/countries/index.js +119 -2
- package/dist/esm/rules/engine.d.ts +14 -0
- package/dist/esm/rules/engine.js +179 -27
- package/dist/esm/rules/matchers.d.ts +3 -2
- package/dist/esm/rules/matchers.js +8 -1
- package/dist/esm/rules/suppressors.js +246 -9
- package/dist/esm/rules/validators.d.ts +25 -0
- package/dist/esm/rules/validators.js +90 -11
- package/dist/esm/sdk.d.ts +10 -2
- package/dist/esm/sdk.js +30 -6
- package/dist/esm/types.d.ts +18 -3
- package/dist/esm/types.js +18 -1
- package/package.json +8 -4
package/dist/esm/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { EntityType, DetectionSource } from "./types.js";
|
|
1
|
+
export { EntityType, DetectionSource, LEGACY_TYPE_ALIASES } from "./types.js";
|
|
2
2
|
export { EuRedact } from "./sdk.js";
|
|
3
3
|
export { COUNTRY_CONFIGS } from "./rules/countries/index.js";
|
|
4
|
+
export { setBicRegistry, getBicRegistry, SEED_BIC6_PREFIXES } from "./rules/bicRegistry.js";
|
|
4
5
|
import { EuRedact } from "./sdk.js";
|
|
5
6
|
import { COUNTRY_CONFIGS } from "./rules/countries/index.js";
|
|
6
7
|
let _instance = null;
|
|
@@ -15,6 +16,12 @@ export function availableCountries() {
|
|
|
15
16
|
export function redact(text, options) {
|
|
16
17
|
return getInstance().redact(text, options);
|
|
17
18
|
}
|
|
19
|
+
export function addCustomPattern(name, pattern) {
|
|
20
|
+
getInstance().addCustomPattern(name, pattern);
|
|
21
|
+
}
|
|
22
|
+
export function clear() {
|
|
23
|
+
getInstance().clear();
|
|
24
|
+
}
|
|
18
25
|
export function redactBatch(texts, options) {
|
|
19
26
|
return getInstance().redactBatch(texts, options);
|
|
20
27
|
}
|
package/dist/esm/normalizer.js
CHANGED
|
@@ -5,27 +5,25 @@ export function normalize(text) {
|
|
|
5
5
|
return [normalized, buildOffsetMapping(text, normalized)];
|
|
6
6
|
}
|
|
7
7
|
function buildOffsetMapping(original, normalized) {
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
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(
|
|
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
|
-
|
|
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,40 @@ 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 `(
|
|
127
|
+
p(EntityType.SOCIAL_HANDLE, String.raw `(?<![\p{L}\p{N}_])@\p{L}[\p{L}\p{N}_.]{1,29}\b`),
|
|
128
|
+
// --- Secret / API Key (known prefixes — always active) ---
|
|
129
|
+
p(EntityType.SECRET, String.raw `\bAKIA[A-Z0-9]{16}\b`, null, "AWS Access Key ID"),
|
|
130
|
+
p(EntityType.SECRET, String.raw `\bgh[poas]_[a-zA-Z0-9]{36,}\b`, null, "GitHub token"),
|
|
131
|
+
p(EntityType.SECRET, String.raw `\bgithub_pat_[a-zA-Z0-9_]{22,}\b`, null, "GitHub fine-grained PAT"),
|
|
132
|
+
p(EntityType.SECRET, String.raw `\bgl[a-z]t-[a-zA-Z0-9_\-]{20,}\b`, null, "GitLab token"),
|
|
133
|
+
p(EntityType.SECRET, String.raw `\bglpat-[a-zA-Z0-9_\-]{20,}\b`, null, "GitLab PAT"),
|
|
134
|
+
p(EntityType.SECRET, String.raw `\b[sp]k_(?:live|test)_[a-zA-Z0-9]{24,}\b`, null, "Stripe key"),
|
|
135
|
+
p(EntityType.SECRET, String.raw `\bsk-(?:ant-)?[a-zA-Z0-9\-_]{20,}\b`, null, "OpenAI/Anthropic key"),
|
|
136
|
+
p(EntityType.SECRET, String.raw `\bxox[bpas]-\d[\d\-]{10,}`, null, "Slack numeric token"),
|
|
137
|
+
p(EntityType.SECRET, String.raw `\bxox[bpas]-[a-zA-Z0-9\-]{10,}`, null, "Slack token"),
|
|
138
|
+
p(EntityType.SECRET, String.raw `\beyJ[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\b`, null, "JWT token"),
|
|
139
|
+
p(EntityType.SECRET, String.raw `\bSG\.[a-zA-Z0-9_\-]{22}\.[a-zA-Z0-9_\-]{43}\b`, null, "SendGrid key"),
|
|
140
|
+
p(EntityType.SECRET, String.raw `\bhvs\.[a-zA-Z0-9_\-]{20,}\b`, null, "Vault token"),
|
|
141
|
+
p(EntityType.SECRET, String.raw `\bkey-[a-f0-9]{20,}\b`, null, "Mailgun key"),
|
|
142
|
+
p(EntityType.SECRET, String.raw `\bnpm_[a-zA-Z0-9]{36,}\b`, null, "npm token"),
|
|
143
|
+
p(EntityType.SECRET, String.raw `\bAIza[a-zA-Z0-9_\-]{35}\b`, null, "Google API key"),
|
|
144
|
+
p(EntityType.SECRET, String.raw `\bdop_v1_[a-f0-9]{64}\b`, null, "DigitalOcean token"),
|
|
145
|
+
p(EntityType.SECRET, String.raw `\bAC[a-f0-9]{32}\b`, null, "Twilio Account SID"),
|
|
146
|
+
p(EntityType.SECRET, String.raw `\bxapp-\d[\d\-]{20,}`, null, "Slack app numeric token"),
|
|
147
|
+
p(EntityType.SECRET, String.raw `\bxapp-[a-zA-Z0-9\-]{20,}`, null, "Slack app token"),
|
|
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"),
|
|
155
|
+
// --- Secret / API Key (connection strings with embedded 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"),
|
|
157
|
+
// --- Secret / API Key (assignment-based: KEY=value or KEY: value) ---
|
|
158
|
+
p(EntityType.SECRET, String.raw `(?<=[:=] )\S{8,}|(?<=[:=])\S{8,}`, "high_entropy", "Assigned secret value", SECRET_CONTEXT, true),
|
|
159
|
+
// --- Secret / API Key (entropy-based fallback for longer tokens) ---
|
|
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),
|
|
49
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),
|
|
50
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),
|
|
51
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),
|
|
@@ -119,6 +231,11 @@ const AT = {
|
|
|
119
231
|
p(EntityType.IBAN, String.raw `\bAT\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b`, "iban"),
|
|
120
232
|
p(EntityType.VAT, String.raw `\bATU\d{8}\b`),
|
|
121
233
|
p(EntityType.PHONE, String.raw `\b0\d{3,4}[\s\-]?\d{5,8}\b`),
|
|
234
|
+
// Short area codes (Vienna is "01") split across three groups:
|
|
235
|
+
// "01 53460 2215". The pattern above requires 3-4 digits after the trunk
|
|
236
|
+
// 0, so it never matched these. Both separators are mandatory here, which
|
|
237
|
+
// keeps "01 2025" (a date fragment) out.
|
|
238
|
+
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"),
|
|
122
239
|
p(EntityType.PHONE, String.raw `\+43\s?\d{3,4}[\s\-]?\d{5,8}`),
|
|
123
240
|
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),
|
|
124
241
|
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),
|
|
@@ -1,9 +1,23 @@
|
|
|
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;
|
|
13
|
+
addCustomPattern(name: string, pattern: string): void;
|
|
5
14
|
loadCountries(countryCodes: string[] | null): void;
|
|
6
15
|
detect(text: string, countryCodes: string[] | null): Detection[];
|
|
16
|
+
/**
|
|
17
|
+
* Remove overlapping detections with priority-aware resolution.
|
|
18
|
+
* Priority: validated (3) > custom (2) > regex-only (1).
|
|
19
|
+
* Within the same tier, longer span wins.
|
|
20
|
+
*/
|
|
7
21
|
private deduplicate;
|
|
8
22
|
getLoadedCountries(): Set<string>;
|
|
9
23
|
}
|