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/README.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# euredact
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**European PII detection and redaction for Node.js / TypeScript**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Fast, zero-dependency European PII detection and redaction SDK. Detects and
|
|
8
|
+
redacts personal data across **31 European countries** using rule-based pattern
|
|
9
|
+
matching with checksum validation, context-aware false-positive suppression,
|
|
10
|
+
and priority-based deduplication.
|
|
6
11
|
|
|
7
12
|
## Install
|
|
8
13
|
|
|
@@ -26,64 +31,374 @@ console.log(result.detections);
|
|
|
26
31
|
// [{ entityType: "NATIONAL_ID", text: "123456782", start: 12, end: 21, ... }, ...]
|
|
27
32
|
```
|
|
28
33
|
|
|
29
|
-
##
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **31 European countries** with country-specific patterns
|
|
37
|
+
- **25+ PII entity types:** national IDs, IBANs, phone numbers, email, VAT
|
|
38
|
+
numbers, license plates, credit cards, BIC/SWIFT, VIN, IMEI, GPS coordinates,
|
|
39
|
+
UUIDs, social handles, IP/IPv6, MAC addresses, secrets/API keys, and more
|
|
40
|
+
- **Secret/API key detection:** known-prefix patterns for AWS, GitHub, Stripe,
|
|
41
|
+
OpenAI, Slack, JWT, SendGrid, plus Shannon entropy-based detection for generic
|
|
42
|
+
high-entropy tokens near context keywords
|
|
43
|
+
- **Custom patterns:** register your own regex patterns for domain-specific PII
|
|
44
|
+
- **Checksum validation:** IBAN mod-97, Luhn (credit cards/IMEI), and 30+
|
|
45
|
+
country-specific validators
|
|
46
|
+
- **Priority-aware deduplication:** validated (checksum) > custom > regex-only
|
|
47
|
+
- **Context-aware:** keyword proximity checks and structural detection (JSON field
|
|
48
|
+
names, CSV headers) for ambiguous patterns
|
|
49
|
+
- **Zero runtime dependencies**
|
|
50
|
+
- **ESM and CommonJS** dual-published
|
|
51
|
+
|
|
52
|
+
### Supported Countries
|
|
53
|
+
|
|
54
|
+
| Region | Countries |
|
|
55
|
+
|---|---|
|
|
56
|
+
| Western Europe | AT, BE, CH, DE, FR, LU, NL |
|
|
57
|
+
| Southern Europe | CY, EL, ES, IT, MT, PT |
|
|
58
|
+
| Northern Europe | DK, EE, FI, IS, LT, LV, NO, SE |
|
|
59
|
+
| Eastern Europe | BG, CZ, HR, HU, PL, RO, SI, SK |
|
|
60
|
+
| British Isles | IE, UK |
|
|
61
|
+
|
|
62
|
+
## API Reference
|
|
63
|
+
|
|
64
|
+
### Module-Level Functions
|
|
65
|
+
|
|
66
|
+
#### `redact(text, options?)`
|
|
30
67
|
|
|
31
|
-
|
|
68
|
+
```ts
|
|
69
|
+
function redact(text: string, options?: RedactOptions): RedactResult;
|
|
70
|
+
```
|
|
32
71
|
|
|
33
|
-
|
|
72
|
+
Main entry point. Detects and redacts PII in the given text.
|
|
34
73
|
|
|
35
74
|
```ts
|
|
36
75
|
interface RedactOptions {
|
|
37
|
-
countries?: string[];
|
|
38
|
-
|
|
39
|
-
detectDates?: boolean;
|
|
40
|
-
cache?: boolean;
|
|
76
|
+
countries?: string[] | null; // Country codes (e.g. ["NL", "BE"]) — null loads all
|
|
77
|
+
referentialIntegrity?: boolean; // Replace with consistent labels (default: false)
|
|
78
|
+
detectDates?: boolean; // Include DOB/date-of-death detections (default: false)
|
|
79
|
+
cache?: boolean; // Enable result caching (default: true)
|
|
41
80
|
}
|
|
42
81
|
```
|
|
43
82
|
|
|
44
|
-
|
|
83
|
+
| Parameter | Default | Description |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `countries` | `null` | ISO 3166-1 alpha-2 codes to restrict detection. `null` loads all 31 countries. |
|
|
86
|
+
| `referentialIntegrity` | `false` | Replace PII with consistent labels instead of entity-type labels. |
|
|
87
|
+
| `detectDates` | `false` | Include date-of-birth and date-of-death detections. Off by default. |
|
|
88
|
+
| `cache` | `true` | Cache results for identical inputs. |
|
|
45
89
|
|
|
46
|
-
|
|
90
|
+
#### `redactBatch(texts, options?)`
|
|
47
91
|
|
|
48
|
-
|
|
92
|
+
```ts
|
|
93
|
+
function redactBatch(texts: string[], options?: RedactOptions): RedactResult[];
|
|
94
|
+
```
|
|
49
95
|
|
|
50
|
-
|
|
96
|
+
Process multiple texts efficiently. Loads country configs once. Returns results
|
|
97
|
+
in the same order as the input.
|
|
51
98
|
|
|
52
|
-
|
|
99
|
+
#### `addCustomPattern(name, pattern)`
|
|
53
100
|
|
|
54
|
-
|
|
101
|
+
```ts
|
|
102
|
+
function addCustomPattern(name: string, pattern: string): void;
|
|
103
|
+
```
|
|
55
104
|
|
|
56
|
-
|
|
105
|
+
Register a custom regex pattern. Matches are reported with `name` as the entity
|
|
106
|
+
type. See [Custom Patterns](#custom-patterns) below.
|
|
57
107
|
|
|
58
|
-
|
|
59
|
-
|----------|---------|
|
|
60
|
-
| `NATIONAL_ID` | BSN, Personalausweis, NIR, CPR, personnummer, ... |
|
|
61
|
-
| `IBAN` | Country-specific IBAN with ISO 13616 checksum |
|
|
62
|
-
| `PHONE` | National and international formats per country |
|
|
63
|
-
| `EMAIL` | RFC 5322 simplified |
|
|
64
|
-
| `POSTAL_CODE` | Country-specific formats with context validation |
|
|
65
|
-
| `TAX_ID` | Steuer-ID, NIF, NIP, ... with checksums |
|
|
66
|
-
| `CREDIT_CARD` | Visa, Mastercard, Amex with Luhn check |
|
|
67
|
-
| `LICENSE_PLATE` | Country-specific formats |
|
|
68
|
-
| `PASSPORT` | Context-required per country |
|
|
69
|
-
| `VAT` | EU VAT numbers |
|
|
70
|
-
| `BIC` | SWIFT/BIC codes |
|
|
71
|
-
| `VIN` | ISO 3779 vehicle identification |
|
|
72
|
-
| `IP_ADDRESS` | IPv4 and IPv6 |
|
|
73
|
-
| `MAC_ADDRESS` | Colon, dash, and Cisco dot notation |
|
|
74
|
-
| `UUID` | RFC 4122 versions 1-5 |
|
|
75
|
-
| `IMEI` | 15-digit with Luhn check |
|
|
76
|
-
| `GPS_COORDINATES` | Decimal degrees with 4+ decimal places |
|
|
77
|
-
| `SOCIAL_HANDLE` | @username format |
|
|
78
|
-
| `DOB` | Date of birth (context-required, opt-in) |
|
|
108
|
+
#### `availableCountries()`
|
|
79
109
|
|
|
80
|
-
|
|
110
|
+
```ts
|
|
111
|
+
function availableCountries(): string[];
|
|
112
|
+
```
|
|
81
113
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
|
|
114
|
+
Returns a sorted list of supported ISO country codes.
|
|
115
|
+
|
|
116
|
+
### Instance-Based API (`EuRedact` Class)
|
|
117
|
+
|
|
118
|
+
For applications that need isolated instances with separate caches and custom
|
|
119
|
+
pattern registrations:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { EuRedact } from "euredact";
|
|
123
|
+
|
|
124
|
+
const instance = new EuRedact();
|
|
125
|
+
instance.addCustomPattern("CASE_REF", "CASE-\\d{8}");
|
|
126
|
+
|
|
127
|
+
const result = instance.redact("See CASE-20260401 for details", {
|
|
128
|
+
countries: ["NL", "BE"],
|
|
129
|
+
});
|
|
130
|
+
console.log(result.redactedText);
|
|
131
|
+
// "See [CASE_REF] for details"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The `EuRedact` class exposes: `redact()`, `redactBatch()`, and
|
|
135
|
+
`addCustomPattern()`.
|
|
136
|
+
|
|
137
|
+
### Return Types
|
|
138
|
+
|
|
139
|
+
#### `RedactResult`
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
interface RedactResult {
|
|
143
|
+
redactedText: string; // The input text with PII replaced
|
|
144
|
+
detections: Detection[]; // All PII spans found
|
|
145
|
+
source: string; // Detection backend ("rules")
|
|
146
|
+
degraded: boolean; // True if the engine fell back to a simpler mode
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### `Detection`
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
interface Detection {
|
|
154
|
+
entityType: EntityType | string; // PII category (enum or custom name)
|
|
155
|
+
start: number; // Start offset in the original text
|
|
156
|
+
end: number; // End offset (exclusive)
|
|
157
|
+
text: string; // The matched substring
|
|
158
|
+
source: DetectionSource; // "rules" or "cloud"
|
|
159
|
+
country: string | null; // ISO code or null for shared/custom patterns
|
|
160
|
+
confidence: string; // Confidence level
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### `EntityType`
|
|
165
|
+
|
|
166
|
+
String enum with all supported PII categories:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
NAME ADDRESS BANK_ACCOUNT BIC
|
|
170
|
+
CREDIT_CARD PHONE EMAIL DOB
|
|
171
|
+
DATE_OF_DEATH NATIONAL_ID SSN TAX_ID
|
|
172
|
+
PASSPORT DRIVERS_LICENSE RESIDENCE_PERMIT LICENSE_PLATE
|
|
173
|
+
VIN VAT POSTAL_CODE IP_ADDRESS
|
|
174
|
+
IPV6_ADDRESS MAC_ADDRESS HEALTH_INSURANCE HEALTHCARE_PROVIDER
|
|
175
|
+
CHAMBER_OF_COMMERCE IMEI GPS_COORDINATES UUID
|
|
176
|
+
SOCIAL_HANDLE SECRET OTHER
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
For custom patterns, `entityType` is a plain string (e.g. `"EMPLOYEE_ID"`).
|
|
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
|
+
|
|
251
|
+
## Custom Patterns
|
|
252
|
+
|
|
253
|
+
Register domain-specific PII patterns at runtime:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { addCustomPattern, redact } from "euredact";
|
|
257
|
+
|
|
258
|
+
// Register patterns
|
|
259
|
+
addCustomPattern("EMPLOYEE_ID", "EMP-\\d{6}");
|
|
260
|
+
addCustomPattern("CASE_REF", "CASE-\\d{8}");
|
|
261
|
+
|
|
262
|
+
// They are detected alongside built-in PII
|
|
263
|
+
const result = redact(
|
|
264
|
+
"Employee EMP-123456, email jan@example.com, ref CASE-20260401"
|
|
265
|
+
);
|
|
266
|
+
console.log(result.redactedText);
|
|
267
|
+
// "Employee [EMPLOYEE_ID], email [EMAIL], ref [CASE_REF]"
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### How Custom Patterns Work
|
|
271
|
+
|
|
272
|
+
- `name` becomes the entity type in detections and replacement tags
|
|
273
|
+
- `pattern` is a JavaScript regular expression (same syntax as `RegExp`)
|
|
274
|
+
- Custom patterns are always active regardless of the `countries` option
|
|
275
|
+
- In overlap resolution, custom patterns have higher priority than built-in
|
|
276
|
+
regex-only patterns but lower priority than built-in patterns with a passing
|
|
277
|
+
checksum validator
|
|
278
|
+
|
|
279
|
+
### Instance Isolation
|
|
280
|
+
|
|
281
|
+
Custom patterns registered on the module-level function apply to the shared
|
|
282
|
+
singleton. For isolated registrations, use separate `EuRedact` instances:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { EuRedact } from "euredact";
|
|
286
|
+
|
|
287
|
+
const a = new EuRedact();
|
|
288
|
+
a.addCustomPattern("EMPLOYEE_ID", "EMP-\\d{6}");
|
|
289
|
+
|
|
290
|
+
const b = new EuRedact();
|
|
291
|
+
b.addCustomPattern("CASE_REF", "CASE-\\d{8}");
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Secret and API Key Detection
|
|
295
|
+
|
|
296
|
+
Built-in detection for API keys, tokens, and passwords. Always active -- no
|
|
297
|
+
configuration required.
|
|
298
|
+
|
|
299
|
+
### Known-Prefix Patterns
|
|
300
|
+
|
|
301
|
+
| Pattern | Description |
|
|
302
|
+
|---|---|
|
|
303
|
+
| `AKIA...` | AWS Access Key ID |
|
|
304
|
+
| `ghp_`, `gho_`, `ghs_`, `github_pat_` | GitHub tokens (PAT, OAuth, app, server) |
|
|
305
|
+
| `sk_live_`, `pk_live_`, `sk_test_`, `pk_test_` | Stripe secret and publishable keys |
|
|
306
|
+
| `sk-`, `sk-ant-` | OpenAI and Anthropic API keys |
|
|
307
|
+
| `xoxb-`, `xoxp-`, `xoxa-`, `xoxs-` | Slack tokens |
|
|
308
|
+
| `eyJ...` (3-part base64url) | JWT tokens |
|
|
309
|
+
| `SG.` | SendGrid API keys |
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
const result = redact("My API key is sk-proj-abc123def456ghi789jkl0");
|
|
313
|
+
console.log(result.redactedText);
|
|
314
|
+
// "My API key is [SECRET]"
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Entropy-Based Detection
|
|
318
|
+
|
|
319
|
+
High-entropy strings (32+ alphanumeric characters) near context keywords like
|
|
320
|
+
`key`, `token`, `secret`, `password`, `credential`, `auth`, or `bearer`
|
|
321
|
+
(including 12 European language translations) are flagged via Shannon entropy
|
|
322
|
+
analysis:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
redact("The api_key is xK9mPqR7vLnW2bFjY8cGhT4sDfAeU6iO").redactedText;
|
|
326
|
+
// "The api_key is [SECRET]"
|
|
327
|
+
|
|
328
|
+
// Without a context keyword -- not flagged:
|
|
329
|
+
redact("identifier: xK9mPqR7vLnW2bFjY8cGhT4sDfAeU6iO").redactedText;
|
|
330
|
+
// "identifier: xK9mPqR7vLnW2bFjY8cGhT4sDfAeU6iO"
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Country Hints
|
|
334
|
+
|
|
335
|
+
When you know which countries appear in your data, pass them explicitly:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
const result = redact(text, { countries: ["NL", "BE"] });
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
This restricts pattern matching to Dutch and Belgian rules (plus shared patterns
|
|
342
|
+
and custom patterns). Benefits:
|
|
343
|
+
|
|
344
|
+
- **Fewer false positives.** A 9-digit number that passes the Dutch BSN checksum
|
|
345
|
+
won't also be tested against unrelated country patterns.
|
|
346
|
+
- **Faster.** Fewer patterns to compile and scan.
|
|
347
|
+
|
|
348
|
+
## Referential Integrity
|
|
349
|
+
|
|
350
|
+
When `referentialIntegrity: true`, each unique PII value is mapped to a consistent
|
|
351
|
+
label:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
const result = redact(
|
|
355
|
+
"BSN 123456782 en later weer 123456782",
|
|
356
|
+
{ countries: ["NL"], referentialIntegrity: true }
|
|
357
|
+
);
|
|
358
|
+
console.log(result.redactedText);
|
|
359
|
+
// "BSN NATIONAL_ID_1 en later weer NATIONAL_ID_1"
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Architecture
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
Input text
|
|
366
|
+
|
|
|
367
|
+
v
|
|
368
|
+
[Normalizer] -- Unicode normalization, whitespace cleanup
|
|
369
|
+
|
|
|
370
|
+
v
|
|
371
|
+
[Pass 1: Pattern Matching] -- All country + shared + custom regexes
|
|
372
|
+
|
|
|
373
|
+
v
|
|
374
|
+
[Pass 2a: Validation] -- Checksum validators (mod-97, Luhn, entropy, ...)
|
|
375
|
+
| Failed-validation spans become suppression zones
|
|
376
|
+
v
|
|
377
|
+
[Pass 2b: Suppression] -- Remove false positives (currency amounts, units,
|
|
378
|
+
| references, overlapping matches in suppression zones)
|
|
379
|
+
v
|
|
380
|
+
[Deduplication] -- Priority-aware: validated > custom > regex-only
|
|
381
|
+
| Within same tier, longer span wins
|
|
382
|
+
v
|
|
383
|
+
[Replacement] -- Right-to-left substitution with [ENTITY_TYPE] labels
|
|
384
|
+
| or labels
|
|
385
|
+
v
|
|
386
|
+
RedactResult
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Suppression Zones
|
|
390
|
+
|
|
391
|
+
When a regex matches a pattern that has a checksum validator but the checksum
|
|
392
|
+
fails, the matched span becomes a "suppression zone." Any purely regex-based
|
|
393
|
+
detection fully contained within that zone is suppressed as a false positive.
|
|
394
|
+
|
|
395
|
+
### Deduplication Priority
|
|
396
|
+
|
|
397
|
+
1. **Validated patterns** (checksum passes) -- highest priority
|
|
398
|
+
2. **Custom patterns** (registered via `addCustomPattern()`)
|
|
399
|
+
3. **Regex-only patterns** (no validator) -- lowest priority
|
|
400
|
+
|
|
401
|
+
Within the same tier, the longer span wins.
|
|
87
402
|
|
|
88
403
|
## CommonJS
|
|
89
404
|
|
|
@@ -91,6 +406,14 @@ AT, BE, BG, CH, CY, CZ, DE, DK, EE, EL, ES, FI, FR, HR, HU, IE, IS, IT, LT, LU,
|
|
|
91
406
|
const { redact } = require("euredact");
|
|
92
407
|
```
|
|
93
408
|
|
|
409
|
+
## Performance
|
|
410
|
+
|
|
411
|
+
| Metric | Value |
|
|
412
|
+
|---|---|
|
|
413
|
+
| Latency per redaction | ~0.02 ms |
|
|
414
|
+
| Package size | ~86 KB |
|
|
415
|
+
| Runtime dependencies | 0 |
|
|
416
|
+
|
|
94
417
|
## License
|
|
95
418
|
|
|
96
419
|
Apache-2.0
|
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
|
-
//
|
|
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
|
|
27
|
+
return `${raw.length}:${h1}${h2}`;
|
|
28
28
|
}
|
|
29
29
|
get(key) {
|
|
30
30
|
if (!this.enabled)
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,16 +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
|
+
exports.addCustomPattern = addCustomPattern;
|
|
7
|
+
exports.clear = clear;
|
|
6
8
|
exports.redactBatch = redactBatch;
|
|
7
9
|
var types_js_1 = require("./types.js");
|
|
8
10
|
Object.defineProperty(exports, "EntityType", { enumerable: true, get: function () { return types_js_1.EntityType; } });
|
|
9
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; } });
|
|
10
13
|
var sdk_js_1 = require("./sdk.js");
|
|
11
14
|
Object.defineProperty(exports, "EuRedact", { enumerable: true, get: function () { return sdk_js_1.EuRedact; } });
|
|
12
15
|
var index_js_1 = require("./rules/countries/index.js");
|
|
13
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; } });
|
|
14
21
|
const sdk_js_2 = require("./sdk.js");
|
|
15
22
|
const index_js_2 = require("./rules/countries/index.js");
|
|
16
23
|
let _instance = null;
|
|
@@ -25,6 +32,12 @@ function availableCountries() {
|
|
|
25
32
|
function redact(text, options) {
|
|
26
33
|
return getInstance().redact(text, options);
|
|
27
34
|
}
|
|
35
|
+
function addCustomPattern(name, pattern) {
|
|
36
|
+
getInstance().addCustomPattern(name, pattern);
|
|
37
|
+
}
|
|
38
|
+
function clear() {
|
|
39
|
+
getInstance().clear();
|
|
40
|
+
}
|
|
28
41
|
function redactBatch(texts, options) {
|
|
29
42
|
return getInstance().redactBatch(texts, options);
|
|
30
43
|
}
|
package/dist/cjs/normalizer.js
CHANGED
|
@@ -9,27 +9,25 @@ function normalize(text) {
|
|
|
9
9
|
return [normalized, buildOffsetMapping(text, normalized)];
|
|
10
10
|
}
|
|
11
11
|
function buildOffsetMapping(original, normalized) {
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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(
|
|
30
|
+
normToOrig.push(origPtr);
|
|
33
31
|
}
|
|
34
32
|
return normToOrig;
|
|
35
33
|
}
|