euredact 0.2.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 CHANGED
@@ -166,7 +166,7 @@ interface Detection {
166
166
  String enum with all supported PII categories:
167
167
 
168
168
  ```
169
- NAME ADDRESS IBAN BIC
169
+ NAME ADDRESS BANK_ACCOUNT BIC
170
170
  CREDIT_CARD PHONE EMAIL DOB
171
171
  DATE_OF_DEATH NATIONAL_ID SSN TAX_ID
172
172
  PASSPORT DRIVERS_LICENSE RESIDENCE_PERMIT LICENSE_PLATE
@@ -178,6 +178,76 @@ SOCIAL_HANDLE SECRET OTHER
178
178
 
179
179
  For custom patterns, `entityType` is a plain string (e.g. `"EMPLOYEE_ID"`).
180
180
 
181
+ ## Country codes
182
+
183
+ `countries` accepts **ISO 3166-1 alpha-2** codes. The two EU/VAT spellings are
184
+ accepted as equivalents: `GB`/`UK` and `GR`/`EL`. Codes are case-insensitive
185
+ and whitespace-tolerant.
186
+
187
+ An **unrecognised** code does not throw — it logs an `[euredact]` warning and
188
+ detection continues with the shared, country-independent patterns. Throwing on
189
+ an unknown locale invites callers to wrap the call in `try/catch` and skip
190
+ redaction entirely, failing open with unredacted PII.
191
+
192
+ ## Country-independent detection
193
+
194
+ `countries` narrows which country-specific patterns run. It does **not** gate
195
+ identifiers that carry their own evidence:
196
+
197
+ | Type | Why it ignores `countries` |
198
+ |---|---|
199
+ | `BANK_ACCOUNT` (IBAN) | carries an ISO 3166 country code and a mod-97 checksum |
200
+ | `PHONE` (with `+` prefix) | an E.164 country prefix is self-identifying |
201
+ | `EMAIL`, `CREDIT_CARD`, `VIN`, `IMEI`, `IP_ADDRESS`, `UUID`, `SECRET` | no national form to gate on |
202
+
203
+ ```ts
204
+ redact("Rekening: BE68 5390 0754 7034", { countries: ["AT"] });
205
+ // -> 'Rekening: [BANK_ACCOUNT]'
206
+ ```
207
+
208
+ ## BIC detection
209
+
210
+ BIC is the only bank identifier here with **no check digit**, and characters
211
+ 5-6 of ordinary uppercase words are frequently valid ISO 3166 country codes
212
+ (`DRINGEND` -> `GE`, `HOSPITAL` -> `IT`). Detection is therefore gated:
213
+
214
+ | Stage | Condition | Result |
215
+ |---|---|---|
216
+ | Gate 0 | the token also occurs as an ordinary lowercase word in the same document | never emitted |
217
+ | Tier 1 | registry hit on the BIC6 institution+country prefix | emitted |
218
+ | Gate 2 | heading / shouted-word shape | never emitted |
219
+ | Tier 2 | `BIC`/`SWIFT` keyword, an IBAN, or a bank block in the enclosing line, record or paragraph | emitted |
220
+ | — | none of the above | never emitted |
221
+
222
+ The package bundles **no licensed BIC data** — only a small seed list of BIC6
223
+ prefixes compiled from publicly published bank data. Deployments holding a
224
+ licensed directory install it at startup:
225
+
226
+ ```ts
227
+ import { setBicRegistry } from "euredact";
228
+
229
+ setBicRegistry(["ABNANL2A", "INGBNL2A", "BBRUBE"]); // iterable
230
+ setBicRegistry(bic => myDirectory.has(bic)); // membership callable
231
+ setBicRegistry(null); // remove
232
+ ```
233
+
234
+ The registry is an **accept** signal, never a filter: a code missing from it
235
+ falls through to the context gate and is still detected when banking context is
236
+ present. A stale list costs a little recall on bare, contextless BICs — it
237
+ never causes a leak. Annual review is sufficient.
238
+
239
+ ## `IBAN` is now `BANK_ACCOUNT`
240
+
241
+ The canonical type name is `BANK_ACCOUNT`; `IBAN` was a legacy alias.
242
+ `detection.entityType` is now `"BANK_ACCOUNT"` and the placeholder written into
243
+ redacted text is `[BANK_ACCOUNT]`.
244
+
245
+ `EntityType.IBAN` is kept as an alias with the same value, so
246
+ `EntityType.IBAN === EntityType.BANK_ACCOUNT` and code referring to the member
247
+ keeps working. Code matching the *string* `"IBAN"` — or the `[IBAN]`
248
+ placeholder — must be updated. `LEGACY_TYPE_ALIASES` publishes the mapping.
249
+
250
+
181
251
  ## Custom Patterns
182
252
 
183
253
  Register domain-specific PII patterns at runtime:
package/dist/cjs/cache.js CHANGED
@@ -21,10 +21,10 @@ class ResultCache {
21
21
  key(text, countries, mode) {
22
22
  const sorted = [...countries].sort();
23
23
  const raw = `${text}|${sorted.join("|")}|${mode}`;
24
- // Use multiple FNV-1a hashes on different slices for reduced collisions
24
+ // Include raw length to reduce collision risk alongside dual FNV-1a hashes
25
25
  const h1 = fnv1aHash(raw);
26
26
  const h2 = fnv1aHash(raw.slice(Math.floor(raw.length / 2)) + raw.slice(0, Math.floor(raw.length / 2)));
27
- return h1 + h2;
27
+ return `${raw.length}:${h1}${h2}`;
28
28
  }
29
29
  get(key) {
30
30
  if (!this.enabled)
package/dist/cjs/index.js CHANGED
@@ -1,17 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.COUNTRY_CONFIGS = exports.EuRedact = exports.DetectionSource = exports.EntityType = void 0;
3
+ exports.SEED_BIC6_PREFIXES = exports.getBicRegistry = exports.setBicRegistry = exports.COUNTRY_CONFIGS = exports.EuRedact = exports.LEGACY_TYPE_ALIASES = exports.DetectionSource = exports.EntityType = void 0;
4
4
  exports.availableCountries = availableCountries;
5
5
  exports.redact = redact;
6
6
  exports.addCustomPattern = addCustomPattern;
7
+ exports.clear = clear;
7
8
  exports.redactBatch = redactBatch;
8
9
  var types_js_1 = require("./types.js");
9
10
  Object.defineProperty(exports, "EntityType", { enumerable: true, get: function () { return types_js_1.EntityType; } });
10
11
  Object.defineProperty(exports, "DetectionSource", { enumerable: true, get: function () { return types_js_1.DetectionSource; } });
12
+ Object.defineProperty(exports, "LEGACY_TYPE_ALIASES", { enumerable: true, get: function () { return types_js_1.LEGACY_TYPE_ALIASES; } });
11
13
  var sdk_js_1 = require("./sdk.js");
12
14
  Object.defineProperty(exports, "EuRedact", { enumerable: true, get: function () { return sdk_js_1.EuRedact; } });
13
15
  var index_js_1 = require("./rules/countries/index.js");
14
16
  Object.defineProperty(exports, "COUNTRY_CONFIGS", { enumerable: true, get: function () { return index_js_1.COUNTRY_CONFIGS; } });
17
+ var bicRegistry_js_1 = require("./rules/bicRegistry.js");
18
+ Object.defineProperty(exports, "setBicRegistry", { enumerable: true, get: function () { return bicRegistry_js_1.setBicRegistry; } });
19
+ Object.defineProperty(exports, "getBicRegistry", { enumerable: true, get: function () { return bicRegistry_js_1.getBicRegistry; } });
20
+ Object.defineProperty(exports, "SEED_BIC6_PREFIXES", { enumerable: true, get: function () { return bicRegistry_js_1.SEED_BIC6_PREFIXES; } });
15
21
  const sdk_js_2 = require("./sdk.js");
16
22
  const index_js_2 = require("./rules/countries/index.js");
17
23
  let _instance = null;
@@ -29,6 +35,9 @@ function redact(text, options) {
29
35
  function addCustomPattern(name, pattern) {
30
36
  getInstance().addCustomPattern(name, pattern);
31
37
  }
38
+ function clear() {
39
+ getInstance().clear();
40
+ }
32
41
  function redactBatch(texts, options) {
33
42
  return getInstance().redactBatch(texts, options);
34
43
  }
@@ -9,27 +9,25 @@ function normalize(text) {
9
9
  return [normalized, buildOffsetMapping(text, normalized)];
10
10
  }
11
11
  function buildOffsetMapping(original, normalized) {
12
- const nfdOriginal = original.normalize("NFD");
13
- const nfdNormalized = normalized.normalize("NFD");
14
- // Build: normalized index -> original index via NFD as intermediate
12
+ // Precompute cumulative NFD lengths for normalized characters
13
+ const normNfdCum = [0];
14
+ for (let i = 0; i < normalized.length; i++) {
15
+ normNfdCum.push(normNfdCum[i] + normalized[i].normalize("NFD").length);
16
+ }
17
+ // Precompute cumulative NFD lengths for original characters
18
+ const origNfdCum = [0];
19
+ for (let i = 0; i < original.length; i++) {
20
+ origNfdCum.push(origNfdCum[i] + original[i].normalize("NFD").length);
21
+ }
22
+ // Two-pointer scan: map each normalized position to its original position
15
23
  const normToOrig = [];
24
+ let origPtr = 0;
16
25
  for (let normIdx = 0; normIdx < normalized.length; normIdx++) {
17
- const nfdOfNormChar = normalized[normIdx].normalize("NFD");
18
- let nfdStart = 0;
19
- for (let i = 0; i < normIdx; i++) {
20
- nfdStart += normalized[i].normalize("NFD").length;
21
- }
22
- let targetOrig = 0;
23
- let running = 0;
24
- for (let oi = 0; oi < original.length; oi++) {
25
- const nfdOc = original[oi].normalize("NFD");
26
- if (running + nfdOc.length > nfdStart) {
27
- targetOrig = oi;
28
- break;
29
- }
30
- running += nfdOc.length;
26
+ const nfdStart = normNfdCum[normIdx];
27
+ while (origPtr + 1 < original.length && origNfdCum[origPtr + 1] <= nfdStart) {
28
+ origPtr++;
31
29
  }
32
- normToOrig.push(targetOrig);
30
+ normToOrig.push(origPtr);
33
31
  }
34
32
  return normToOrig;
35
33
  }
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ /**
3
+ * BIC registry — tier-1 membership testing for ISO 9362 bank identifier codes.
4
+ *
5
+ * BIC is the only bank identifier in the supported set with **no check digit**
6
+ * (IBAN has mod-97, VAT has country-specific checksums). Structure alone cannot
7
+ * separate a genuine BIC from an ordinary 8- or 11-character uppercase word,
8
+ * because characters 5-6 of everyday words are frequently valid ISO 3166
9
+ * country codes (`DRINGEND` -> `GE`, `HOSPITAL` -> `IT`). Registry membership
10
+ * is therefore the strongest precision signal available.
11
+ *
12
+ * Two sources are consulted, in order:
13
+ *
14
+ * 1. A deployment-supplied registry, installed via {@link setBicRegistry}.
15
+ * This is the path for anyone holding a licensed directory (SWIFTRef).
16
+ * 2. {@link SEED_BIC6_PREFIXES} — a small bundled set of institution+country
17
+ * prefixes for major European banks, compiled from publicly published bank
18
+ * data. No licensed directory is bundled.
19
+ *
20
+ * Matching is on the **BIC6 prefix** (4-letter institution + 2-letter country),
21
+ * so any location and branch code on a known institution is accepted. The
22
+ * prefix is the stable part of a BIC; branch codes churn as branches open and
23
+ * close.
24
+ *
25
+ * The registry is used as an *accept* signal, never as a filter: a candidate
26
+ * that misses it falls through to the context gate in `suppressors.ts` and is
27
+ * still detected when banking context is present. A stale list therefore costs
28
+ * a little recall on bare, contextless BICs — it never causes a leak. Annual
29
+ * review is sufficient.
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.SEED_BIC6_PREFIXES = void 0;
33
+ exports.setBicRegistry = setBicRegistry;
34
+ exports.getBicRegistry = getBicRegistry;
35
+ exports.isRegisteredBic = isRegisteredBic;
36
+ // BIC6 prefixes (institution + country) of major European institutions,
37
+ // compiled from publicly published bank data — not from a licensed directory.
38
+ // Kept deliberately small: this covers the banks that appear bare and
39
+ // contextless in real documents. Anything outside this list is still detected
40
+ // via the context gate.
41
+ exports.SEED_BIC6_PREFIXES = new Set([
42
+ // --- Netherlands ---
43
+ "ABNANL", // ABN AMRO
44
+ "INGBNL", // ING
45
+ "RABONL", // Rabobank
46
+ "SNSBNL", // SNS Bank
47
+ "TRIONL", // Triodos
48
+ "BUNQNL", // bunq
49
+ "KNABNL", // Knab
50
+ "ASNBNL", // ASN Bank
51
+ "RBRBNL", // RegioBank
52
+ "FVLBNL", // Van Lanschot
53
+ "BITSNL", // BinckBank
54
+ "DEUTNL", // Deutsche Bank NL
55
+ // --- Belgium ---
56
+ "BBRUBE", // BNP Paribas Fortis
57
+ "GEBABE", // BNP Paribas Fortis (legacy)
58
+ "KREDBE", // KBC
59
+ "GKCCBE", // Belfius
60
+ "BPOTBE", // bpost bank
61
+ "ARSPBE", // Argenta
62
+ "AXABBE", // AXA Bank Belgium
63
+ "CREGBE", // Belfius (legacy)
64
+ "NICABE", // Crelan
65
+ "TRIOBE", // Triodos Belgium
66
+ "CTBKBE", // Banca Monte Paschi Belgio
67
+ "JVBABE", // Van Breda
68
+ // --- Germany ---
69
+ "DEUTDE", // Deutsche Bank
70
+ "COBADE", // Commerzbank
71
+ "PBNKDE", // Postbank
72
+ "GENODE", // Volksbanken / Raiffeisen
73
+ "MALADE", // Sparkassen
74
+ "HELADE", // Helaba / Sparkassen
75
+ "BYLADE", // BayernLB / Sparkassen
76
+ "SOLADE", // Sparkassen (Baden-Wurttemberg)
77
+ "WELADE", // Sparkassen (Westfalen)
78
+ "NOLADE", // Norddeutsche Landesbank
79
+ "SPKKDE", // Sparkasse
80
+ "HYVEDE", // UniCredit / HypoVereinsbank
81
+ "DRESDE", // Dresdner (legacy Commerzbank)
82
+ "INGDDE", // ING-DiBa
83
+ "NTSBDE", // N26
84
+ "SFRTDE", // Sutor / fintech BaaS
85
+ "TEDADE", // Telda / BaaS
86
+ // --- France ---
87
+ "BNPAFR", // BNP Paribas
88
+ "SOGEFR", // Societe Generale
89
+ "AGRIFR", // Credit Agricole
90
+ "CCBPFR", // Banque Populaire
91
+ "CEPAFR", // Caisse d'Epargne
92
+ "CMCIFR", // Credit Mutuel / CIC
93
+ "PSSTFR", // La Banque Postale
94
+ "BREDFR", // BRED Banque Populaire
95
+ "CRLYFR", // LCL
96
+ "HSBCFR", // HSBC France
97
+ "TRZOFR", // Treezor
98
+ "QNTOFR", // Qonto
99
+ // --- Austria ---
100
+ "GIBAAT", // Erste Bank / Sparkassen
101
+ "BKAUAT", // UniCredit Bank Austria
102
+ "RZBAAT", // Raiffeisen Bank International
103
+ "RLNWAT", // Raiffeisen NO-Wien
104
+ "OBKLAT", // Oberbank
105
+ "BAWAAT", // BAWAG P.S.K.
106
+ "VBOEAT", // Volksbank
107
+ "HYPTAT", // Hypo Tirol
108
+ "SPIHAT", // Sparkasse
109
+ // --- Luxembourg ---
110
+ "BCEELU", // Banque et Caisse d'Epargne de l'Etat
111
+ "BILLLU", // Banque Internationale a Luxembourg
112
+ "BGLLLU", // BGL BNP Paribas
113
+ "CCRALU", // Banque Raiffeisen
114
+ "BLUXLU", // Banque de Luxembourg
115
+ // --- Ireland / UK ---
116
+ "AIBKIE", // Allied Irish Banks
117
+ "BOFIIE", // Bank of Ireland
118
+ "ULSBIE", // Ulster Bank
119
+ "CITIIE", // Citibank Ireland
120
+ "BARCGB", // Barclays
121
+ "HBUKGB", // HSBC UK
122
+ "MIDLGB", // HSBC (legacy Midland)
123
+ "LOYDGB", // Lloyds
124
+ "NWBKGB", // NatWest
125
+ "RBOSGB", // Royal Bank of Scotland
126
+ "TSBSGB", // TSB
127
+ "REVOGB", // Revolut
128
+ "MONZGB", // Monzo
129
+ "STARGB", // Starling
130
+ "CHASGB", // JP Morgan Chase UK
131
+ // --- Nordics ---
132
+ "NDEADK", // Nordea Denmark
133
+ "DABADK", // Danske Bank
134
+ "JYBADK", // Jyske Bank
135
+ "SYBKDK", // Sydbank
136
+ "NDEASE", // Nordea Sweden
137
+ "ESSESE", // SEB
138
+ "HANDSE", // Handelsbanken
139
+ "SWEDSE", // Swedbank
140
+ "DNBANO", // DNB
141
+ "NDEANO", // Nordea Norway
142
+ "NDEAFI", // Nordea Finland
143
+ "OKOYFI", // OP Corporate Bank
144
+ "HELSFI", // Aktia
145
+ "NBIIIS", // Landsbankinn (NBIIISRE)
146
+ "ESJAIS", // Arion Bank (ESJAISRE)
147
+ "GLITIS", // Islandsbanki (GLITISRE)
148
+ "MPBAIS", // Kvika banki (MPBAISRE)
149
+ "KBNONO", // BNbank ASA (KBNONO22)
150
+ "PRBONO", // Boligbanken ASA (PRBONO22)
151
+ "AARBDE", // Aareal Bank AG (AARBDE5W)
152
+ // --- Southern / Central Europe ---
153
+ "UNCRIT", // UniCredit Italy
154
+ "BCITIT", // Intesa Sanpaolo
155
+ "PASCIT", // Monte dei Paschi
156
+ "BPMOIT", // Banco BPM
157
+ "CRPPIT", // Cassa di Risparmio
158
+ "BSCHES", // Banco Santander
159
+ "BBVAES", // BBVA
160
+ "CAIXES", // CaixaBank
161
+ "SABBES", // Banco Sabadell
162
+ "BKBKES", // Bankinter
163
+ "BCOMPT", // Millennium BCP
164
+ "CGDIPT", // Caixa Geral de Depositos
165
+ "BPIPPT", // Banco BPI
166
+ "NOVBPT", // Novo Banco
167
+ "BPKOPL", // PKO Bank Polski
168
+ "BREXPL", // mBank
169
+ "INGBPL", // ING Bank Slaski
170
+ "WBKPPL", // Santander Bank Polska
171
+ "KOMBCZ", // Komercni banka
172
+ "CEKOCZ", // CSOB
173
+ "GIBACZ", // Ceska sporitelna
174
+ "TATRSK", // Tatra banka
175
+ "SUBASK", // Vseobecna uverova banka
176
+ "GIBASK", // Slovenska sporitelna
177
+ "OTPVHU", // OTP Bank
178
+ "BUDAHU", // Budapest Bank
179
+ "MKKBHU", // MKB Bank
180
+ // --- Switzerland ---
181
+ "UBSWCH", // UBS
182
+ "CRESCH", // Credit Suisse
183
+ "POFICH", // PostFinance
184
+ "ZKBKCH", // Zurcher Kantonalbank
185
+ "RAIFCH", // Raiffeisen Switzerland
186
+ "MIGRCH", // Migros Bank
187
+ ]);
188
+ let provider = null;
189
+ /**
190
+ * Install a deployment-supplied BIC registry, consulted ahead of the seed list.
191
+ *
192
+ * Accepts a membership callable, any iterable of BIC strings, or `null` to
193
+ * remove a previously installed registry. Entries may be full BIC8/BIC11 codes
194
+ * or bare BIC6 prefixes; both are matched, case-insensitively and ignoring
195
+ * spaces.
196
+ */
197
+ function setBicRegistry(registry) {
198
+ if (registry === null) {
199
+ provider = null;
200
+ return;
201
+ }
202
+ if (typeof registry === "function") {
203
+ provider = registry;
204
+ return;
205
+ }
206
+ const known = new Set();
207
+ for (const entry of registry) {
208
+ const e = String(entry).trim();
209
+ if (e && !e.startsWith("#"))
210
+ known.add(e.replace(/ /g, "").toUpperCase());
211
+ }
212
+ provider = (bic) => known.has(bic);
213
+ }
214
+ /** Return the installed registry provider, or null if none is installed. */
215
+ function getBicRegistry() {
216
+ return provider;
217
+ }
218
+ /**
219
+ * Tier 1: is `candidate` a BIC of a known institution?
220
+ *
221
+ * Checks the deployment-supplied registry first (full code, then BIC6 prefix),
222
+ * then the bundled seed prefixes.
223
+ */
224
+ function isRegisteredBic(candidate) {
225
+ const c = candidate.replace(/ /g, "").toUpperCase();
226
+ if (c.length !== 8 && c.length !== 11)
227
+ return false;
228
+ const prefix = c.slice(0, 6);
229
+ if (provider !== null) {
230
+ try {
231
+ if (provider(c) || provider(prefix))
232
+ return true;
233
+ }
234
+ catch {
235
+ // a broken provider must not break detection
236
+ }
237
+ }
238
+ return exports.SEED_BIC6_PREFIXES.has(prefix);
239
+ }
@@ -2,6 +2,26 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.COUNTRY_CONFIGS = void 0;
4
4
  const types_js_1 = require("../../types.js");
5
+ const validators_js_1 = require("../validators.js");
6
+ /**
7
+ * Build a country-independent IBAN pattern from the ISO 13616 length table.
8
+ *
9
+ * An IBAN carries its own country code and a mod-97 checksum, so it is
10
+ * self-identifying and must never depend on the caller's `countries` setting —
11
+ * a Belgian account in an Austrian file is still an account.
12
+ *
13
+ * Each alternative pins the *exact* character count for its country, which
14
+ * stops the match absorbing a following word: without it, greedy matching
15
+ * turns `BE68 5390 0754 7034 KBC` into one over-long candidate that fails
16
+ * mod-97 and is lost entirely.
17
+ */
18
+ function buildIbanPattern() {
19
+ const alternatives = Object.entries(validators_js_1.IBAN_LENGTHS)
20
+ .sort(([a], [b]) => a.localeCompare(b))
21
+ .map(([code, length]) => `${code}\\d{2}(?:[ \\-]?[A-Z0-9]){${length - 4}}`);
22
+ return String.raw `\b(?:` + alternatives.join("|") + String.raw `)\b`;
23
+ }
24
+ const GENERIC_IBAN = buildIbanPattern();
5
25
  /**
6
26
  * Helper to create a PatternDef with sensible defaults.
7
27
  */
@@ -9,6 +29,45 @@ function p(entityType, pattern, validator = null, description = "", contextKeywo
9
29
  return { entityType, pattern, validator, description, contextKeywords, requiresContext };
10
30
  }
11
31
  // ---------------------------------------------------------------------------
32
+ // Secret context keywords (consolidated, reused across SECRET patterns)
33
+ // ---------------------------------------------------------------------------
34
+ const SECRET_CONTEXT = [
35
+ // English
36
+ "key", "token", "secret", "password", "credential",
37
+ "api_key", "apikey", "api-key", "auth", "bearer",
38
+ "aws_secret_access_key", "access_key", "private_key",
39
+ "AES_KEY", "ENCRYPTION_KEY", "SIGNING_KEY",
40
+ "ACCOUNT_SID", "AUTH_TOKEN", "TWILIO",
41
+ "RAILS_SECRET", "SECRET_KEY_BASE", "DATABASE_URL",
42
+ "NPM_TOKEN", "DIGITALOCEAN", "GOOGLE_API",
43
+ "DB_PASS", "DB_PASSWORD", "DATASOURCE_PASSWORD",
44
+ "JWT_SIGNING", "passwd", "DISCORD_TOKEN",
45
+ // Dutch
46
+ "wachtwoord", "sleutel", "geheim",
47
+ // French
48
+ "mot de passe", "mot_de_passe", "clé", "jeton",
49
+ // German
50
+ "Passwort", "Kennwort", "Schlüssel", "Geheimnis",
51
+ // Italian
52
+ "chiave", "segreto",
53
+ // Spanish
54
+ "contraseña", "clave",
55
+ // Portuguese
56
+ "senha", "chave",
57
+ // Polish
58
+ "hasło", "klucz",
59
+ // Czech/Slovak
60
+ "heslo", "kľúč",
61
+ // Hungarian
62
+ "jelszó", "kulcs",
63
+ // Romanian
64
+ "parola",
65
+ // Nordic
66
+ "lösenord", "nøkkel", "nyckel", "avain", "lykill",
67
+ // Greek
68
+ "κωδικός",
69
+ ];
70
+ // ---------------------------------------------------------------------------
12
71
  // DOB context keywords (reused across SHARED patterns)
13
72
  // ---------------------------------------------------------------------------
14
73
  const DOB_CONTEXT = [
@@ -36,8 +95,28 @@ const SHARED = {
36
95
  code: "SHARED",
37
96
  name: "Shared EU Patterns",
38
97
  patterns: [
39
- p(types_js_1.EntityType.EMAIL, String.raw `\b[\w._%+\-]+@[\w.\-]+\.[a-zA-Z]{2,}\b`),
98
+ // JavaScript's \w and \b are ASCII-only, so the Python engine's \w (which
99
+ // is Unicode-aware) matched local parts like "petras_ž@..." while this one
100
+ // did not. \p{L}\p{N} with the `u` flag restores parity; the leading guard
101
+ // is a Unicode-safe word boundary.
102
+ p(types_js_1.EntityType.EMAIL, String.raw `(?<![\p{L}\p{N}._%+\-])[\p{L}\p{N}._%+\-]+@(?:[\p{L}\p{N}\-]+\.)+\p{L}{2,}\b`),
103
+ // IBAN (any country) — structure + mod-97 only. Country-specific IBAN
104
+ // patterns still exist and win on identical spans; this one guarantees an
105
+ // IBAN is never missed just because its country was not requested.
106
+ p(types_js_1.EntityType.BANK_ACCOUNT, GENERIC_IBAN, "iban", "IBAN — any country (ISO 13616, mod-97 validated)"),
107
+ // International phone (E.164, any country). Runs alongside the per-country
108
+ // patterns, which each hard-code a single grouping and miss the rest:
109
+ // "+43 664 8213 907" was invisible to the Austrian pattern, which expects
110
+ // an unbroken subscriber number. A leading "+" is self-identifying, so
111
+ // this is deliberately not country-gated.
112
+ p(types_js_1.EntityType.PHONE, String.raw `\+\d{1,3}(?:[\s\-/]?\(?\d{1,4}\)?){2,7}`, "e164", "International phone number (E.164) — any country"),
40
113
  p(types_js_1.EntityType.BIC, String.raw `\b[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?\b`, "bic"),
114
+ // Space-separated form: "RZBA AT WW", "NICA BE BB". Written as a separate
115
+ // pattern requiring a space between *every* group, rather than making the
116
+ // spaces optional above — optional spaces would let "GEBABEBB KBC" match
117
+ // as one 11-character code and claim the following word. Only a literal
118
+ // space is allowed, so a match cannot span a line break.
119
+ p(types_js_1.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)"),
41
120
  p(types_js_1.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"),
42
121
  p(types_js_1.EntityType.VIN, String.raw `\b[A-HJ-NPR-Z0-9]{17}\b`, "vin"),
43
122
  p(types_js_1.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`),
@@ -48,7 +127,7 @@ const SHARED = {
48
127
  p(types_js_1.EntityType.IMEI, String.raw `\b\d{2}[\-\s]\d{6}[\-\s]\d{6}[\-\s]\d\b`, "imei", "formatted"),
49
128
  p(types_js_1.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,}))`),
50
129
  p(types_js_1.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`),
51
- p(types_js_1.EntityType.SOCIAL_HANDLE, String.raw `(?<!\w)@[a-zA-Z][a-zA-Z0-9_.]{1,29}\b`),
130
+ p(types_js_1.EntityType.SOCIAL_HANDLE, String.raw `(?<![\p{L}\p{N}_])@\p{L}[\p{L}\p{N}_.]{1,29}\b`),
52
131
  // --- Secret / API Key (known prefixes — always active) ---
53
132
  p(types_js_1.EntityType.SECRET, String.raw `\bAKIA[A-Z0-9]{16}\b`, null, "AWS Access Key ID"),
54
133
  p(types_js_1.EntityType.SECRET, String.raw `\bgh[poas]_[a-zA-Z0-9]{36,}\b`, null, "GitHub token"),
@@ -70,35 +149,18 @@ const SHARED = {
70
149
  p(types_js_1.EntityType.SECRET, String.raw `\bxapp-\d[\d\-]{20,}`, null, "Slack app numeric token"),
71
150
  p(types_js_1.EntityType.SECRET, String.raw `\bxapp-[a-zA-Z0-9\-]{20,}`, null, "Slack app token"),
72
151
  p(types_js_1.EntityType.SECRET, String.raw `\bdapi[a-f0-9]{32,}\b`, null, "Databricks PAT"),
152
+ // Discord bot token (3-segment base64 with numeric first segment)
153
+ p(types_js_1.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"),
154
+ // PEM private key headers
155
+ p(types_js_1.EntityType.SECRET, String.raw `-----BEGIN\s(?:RSA\s)?PRIVATE\sKEY-----[\s\S]{0,16384}?-----END\s(?:RSA\s)?PRIVATE\sKEY-----`, null, "PEM private key"),
156
+ // AWS Secret Access Key (40 chars, base64 with +/ after an AKIA line)
157
+ p(types_js_1.EntityType.SECRET, String.raw `(?<=aws_secret_access_key\s{0,4}[:=]\s{0,4})[A-Za-z0-9+/]{40}`, null, "AWS Secret Access Key"),
73
158
  // --- Secret / API Key (connection strings with embedded credentials) ---
74
- p(types_js_1.EntityType.SECRET, String.raw `(?:mongodb|mysql|postgres(?:ql)?|redis|amqp|rabbitmq):\/\/[^\s:]+:[^\s@]+@[^\s]+`, null, "Connection string with credentials"),
159
+ p(types_js_1.EntityType.SECRET, String.raw `(?:mongodb|mysql|postgres(?:ql)?|redis|amqp|rabbitmq):\/\/[^\s:]{1,256}:[^\s@]{1,256}@[^\s]+`, null, "Connection string with credentials"),
75
160
  // --- Secret / API Key (assignment-based: KEY=value or KEY: value) ---
76
- p(types_js_1.EntityType.SECRET, String.raw `(?<=[:=] )\S{8,}|(?<=[:=])\S{8,}`, "high_entropy", "Assigned secret value", [
77
- "key", "token", "secret", "password", "credential",
78
- "api_key", "apikey", "api-key", "auth", "bearer",
79
- "aws_secret_access_key", "access_key", "private_key",
80
- "wachtwoord", "mot de passe", "Passwort", "Schlüssel",
81
- "clé", "sleutel", "lösenord", "hasło", "heslo",
82
- "jelszó", "contraseña", "senha", "parola",
83
- "AES_KEY", "ENCRYPTION_KEY", "SIGNING_KEY",
84
- "ACCOUNT_SID", "AUTH_TOKEN", "TWILIO",
85
- "RAILS_SECRET", "SECRET_KEY_BASE", "DATABASE_URL",
86
- "NPM_TOKEN", "DIGITALOCEAN", "GOOGLE_API",
87
- "kľúč", "nøkkel", "nyckel", "avain", "lykill",
88
- "DB_PASS", "DB_PASSWORD", "DATASOURCE_PASSWORD",
89
- "JWT_SIGNING", "SIGNING_KEY",
90
- "passwd", "Kennwort",
91
- ], true),
161
+ p(types_js_1.EntityType.SECRET, String.raw `(?<=[:=] )\S{8,}|(?<=[:=])\S{8,}`, "high_entropy", "Assigned secret value", SECRET_CONTEXT, true),
92
162
  // --- Secret / API Key (entropy-based fallback for longer tokens) ---
93
- p(types_js_1.EntityType.SECRET, String.raw `\b[A-Za-z0-9_\-+/]{24,}[A-Za-z0-9_\-+/=]*\b`, "high_entropy", "High-entropy token", [
94
- "key", "token", "secret", "password", "credential",
95
- "api_key", "apikey", "api-key", "auth", "bearer",
96
- "aws_secret_access_key", "access_key", "private_key",
97
- "wachtwoord", "mot de passe", "Passwort", "Schlüssel",
98
- "clé", "sleutel", "lösenord", "hasło", "heslo",
99
- "jelszó", "contraseña", "senha", "parola",
100
- "kľúč", "nøkkel", "nyckel", "avain", "lykill",
101
- ], true),
163
+ p(types_js_1.EntityType.SECRET, String.raw `\b[A-Za-z0-9_\-+/]{24,}[A-Za-z0-9_\-+/=]*\b`, "high_entropy", "High-entropy token", SECRET_CONTEXT, true),
102
164
  p(types_js_1.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),
103
165
  p(types_js_1.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),
104
166
  p(types_js_1.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),
@@ -172,6 +234,11 @@ const AT = {
172
234
  p(types_js_1.EntityType.IBAN, String.raw `\bAT\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b`, "iban"),
173
235
  p(types_js_1.EntityType.VAT, String.raw `\bATU\d{8}\b`),
174
236
  p(types_js_1.EntityType.PHONE, String.raw `\b0\d{3,4}[\s\-]?\d{5,8}\b`),
237
+ // Short area codes (Vienna is "01") split across three groups:
238
+ // "01 53460 2215". The pattern above requires 3-4 digits after the trunk
239
+ // 0, so it never matched these. Both separators are mandatory here, which
240
+ // keeps "01 2025" (a date fragment) out.
241
+ p(types_js_1.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"),
175
242
  p(types_js_1.EntityType.PHONE, String.raw `\+43\s?\d{3,4}[\s\-]?\d{5,8}`),
176
243
  p(types_js_1.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),
177
244
  p(types_js_1.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),