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 +71 -1
- package/dist/cjs/cache.js +2 -2
- package/dist/cjs/index.js +10 -1
- package/dist/cjs/normalizer.js +16 -18
- package/dist/cjs/rules/bicRegistry.js +239 -0
- package/dist/cjs/rules/countries/index.js +95 -28
- package/dist/cjs/rules/engine.js +131 -19
- package/dist/cjs/rules/matchers.js +3 -1
- package/dist/cjs/rules/suppressors.js +246 -9
- package/dist/cjs/rules/validators.js +62 -12
- package/dist/cjs/sdk.js +20 -0
- package/dist/cjs/types.js +18 -2
- package/dist/esm/cache.js +2 -2
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +5 -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 +95 -28
- package/dist/esm/rules/engine.d.ts +8 -0
- package/dist/esm/rules/engine.js +130 -19
- package/dist/esm/rules/matchers.d.ts +1 -1
- package/dist/esm/rules/matchers.js +3 -1
- package/dist/esm/rules/suppressors.js +246 -9
- package/dist/esm/rules/validators.d.ts +25 -0
- package/dist/esm/rules/validators.js +60 -11
- package/dist/esm/sdk.d.ts +7 -0
- package/dist/esm/sdk.js +20 -0
- package/dist/esm/types.d.ts +15 -1
- package/dist/esm/types.js +17 -1
- package/package.json +8 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EntityType } from "../types.js";
|
|
2
|
+
import { isRegisteredBic } from "./bicRegistry.js";
|
|
2
3
|
const CONTEXT_CHARS = 150;
|
|
3
4
|
function getContext(text, start, end) {
|
|
4
5
|
const ctxStart = Math.max(0, start - CONTEXT_CHARS);
|
|
@@ -18,12 +19,14 @@ const REFERENCE_BEFORE = /(?:dossier|ref\.?|referentie|reference|référence|fac
|
|
|
18
19
|
const LEGAL_BEFORE = /(?:Art(?:ikel|icle|\.)|§|Artikel|Section|Sectie|Afdeling|paragraaf|Absatz|alinéa|punt|point|Punkt|lid)\s*$/i;
|
|
19
20
|
const MATH_BEFORE = /[=+\-×÷*/]\s*$/;
|
|
20
21
|
const MATH_AFTER = /^\s*[=+\-×÷*/]/;
|
|
22
|
+
// "A-1010", "B-2000", "L-1234", "CH-8000", "D-10115": a country prefix on a
|
|
23
|
+
// postal code, not a minus sign.
|
|
24
|
+
const COUNTRY_PREFIX_HYPHEN = /(?:^|[^A-Za-z0-9])[A-Z]{1,2}-\s*$/;
|
|
21
25
|
const SEQUENTIAL_PATTERNS = /^(?:0{6,}|1234567890?|0123456789|9876543210?|1111111111?|000000000|123456789)$/;
|
|
22
|
-
|
|
23
|
-
const DATE_KEYWORD_NEAR = /(?:jaar|year|année|Jahr|datum|date|Datum|in\s+\d{4}|since|sinds|depuis|seit|født|fødselsdato|fødsel|Fødselsdato|född|födelsedatum|födelsedag|syntynyt|syntymäaika|fæddur|fæðingardagur|\d{2}\.\d{2}\.|(?:januar|februar|marts|april|maj|juni|juli|august|september|oktober|november|december|januari|februari|mars|april|mei|juin|juillet|août|Tiltr[æa]delsesdato|Tiltredelsesdato))/i;
|
|
26
|
+
// YEAR_PATTERN kept for reference but replaced by RECENT_YEAR in suppressYearAsPostal
|
|
24
27
|
const ID_LABEL_BEFORE = /(?:BSN|RR|NN|NIR|INSZ|NISS|NIS|Steuer-?ID|TIN|NIF|NIE|SSN|rijksregisternummer|numéro\s*national|national\s*number|matricule|Ausweisnummer|Personalausweis|Versichertennummer|KVNR|KV-Nr|Steuernummer|St\.\-Nr|StNr|Finanzamt\s+ist|Ondernemingen\s+onder\s+nummer|ondernemingsnummer|numéro\s*d'entreprise|enterprise\s*number|Kruispuntbank)\s*[:.]?\s*$/i;
|
|
25
28
|
const SERVICE_NUMBER = /^0800[\-\s]/;
|
|
26
|
-
const DATE_PATTERN_FULL = /^\d{2}[-/.]\d{2}[-/.]\d{4}$/;
|
|
29
|
+
const DATE_PATTERN_FULL = /^\d{2}[-/.]\d{2}[-/.]\d{4}$|^\d{4}[-/.]\d{2}[-/.]\d{2}$/;
|
|
27
30
|
const PASSPORT_CONTEXT_BEFORE = /(?:Reisepass|passport|passeport|paspoort|Bisheriger\s+Reisepass)\s*(?:Nr\.?|Nummer|nummer|number|n[°o])?\s*[:.]?\s*$/i;
|
|
28
31
|
const SE_ORG_CONTEXT_BEFORE = /(?:org\.?\s*nr\.?|organisationsnummer|organisationsnr|Bolagsverket|företag)\s*[:.]?\s*$/i;
|
|
29
32
|
const NUMERIC_TYPES = new Set([EntityType.PHONE, EntityType.NATIONAL_ID, EntityType.SSN, EntityType.TAX_ID, EntityType.POSTAL_CODE]);
|
|
@@ -63,23 +66,40 @@ function suppressMath(text, match) {
|
|
|
63
66
|
if (!NUMERIC_TYPES.has(match.patternDef.entityType))
|
|
64
67
|
return false;
|
|
65
68
|
const [before, after] = getContext(text, match.start, match.end);
|
|
69
|
+
// A country-prefixed postal code is an address, not a subtraction:
|
|
70
|
+
// "A-1010 Wien", "B-2000 Antwerpen", "D-10115 Berlin".
|
|
71
|
+
if (match.patternDef.entityType === EntityType.POSTAL_CODE && COUNTRY_PREFIX_HYPHEN.test(before)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
66
74
|
return MATH_BEFORE.test(before) || MATH_AFTER.test(after);
|
|
67
75
|
}
|
|
68
76
|
function suppressSequential(_text, match) {
|
|
69
77
|
const clean = match.text.replace(/[\s.\-]/g, "");
|
|
70
78
|
return SEQUENTIAL_PATTERNS.test(clean);
|
|
71
79
|
}
|
|
80
|
+
const RECENT_YEAR = /^(?:19[5-9]\d|20[0-3]\d)$/;
|
|
81
|
+
const POSTAL_CONTEXT_NEAR = /(?:postcode|postal|code\s*postal|PLZ|Postleitzahl|postnummer|postinumero|póstnúmer|zip|straat|straße|strasse|rue\s|via\s|calle\s|rua\s|ulica|utca|street|avenue|laan\s|weg\s|plein|adres|adresse|address|woonplaats|wonende|woonachtig|gevestigd|domicili|demeurant|résidant|residant|bosatt|bopæl|wohnhaft|ansässig|stad\b|ville\b|city\b|Stadt|città|ciudad|cidade|miasto|město|város)/i;
|
|
82
|
+
const DATE_KEYWORD_NEAR = /(?:født|fødselsdato|fødsel|Fødselsdato|född|födelsedatum|födelsedag|syntynyt|syntymäaika|fæddur|fæðingardagur|geboren|geboortedatum|Geburtsdatum|nascido|nacido|data di nascita|nato il|nata il|Tiltr[æa]delsesdato|Tiltredelsesdato|datum|date\b|Datum|jaar|year|année|Jahr|since|sinds|depuis|seit|\d{2}\.\d{2}\.|(?:januar|februar|marts|april|maj|juni|juli|august|september|oktober|november|december|januari|februari|mars|mei|juin|juillet|août))/i;
|
|
72
83
|
function suppressYearAsPostal(text, match) {
|
|
73
84
|
if (match.patternDef.entityType !== EntityType.POSTAL_CODE)
|
|
74
85
|
return false;
|
|
75
86
|
const clean = match.text.trim();
|
|
76
|
-
if (!
|
|
87
|
+
if (!RECENT_YEAR.test(clean))
|
|
88
|
+
return false;
|
|
89
|
+
const [before, after] = getContext(text, match.start, match.end);
|
|
90
|
+
const context = before + after;
|
|
91
|
+
// Keep as postal code if postal/address context nearby
|
|
92
|
+
if (POSTAL_CONTEXT_NEAR.test(context))
|
|
77
93
|
return false;
|
|
94
|
+
// Keep if preceded by comma+space (address pattern: "Amsterdam, 2026")
|
|
78
95
|
const immediateBefore = text.slice(Math.max(0, match.start - 3), match.start);
|
|
79
|
-
if (
|
|
96
|
+
if (/,\s*$/.test(immediateBefore))
|
|
80
97
|
return false;
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
// Suppress if date keyword nearby (birth date, employment date, etc.)
|
|
99
|
+
if (DATE_KEYWORD_NEAR.test(context))
|
|
100
|
+
return true;
|
|
101
|
+
// Suppress: recent years without postal context are almost never postal codes
|
|
102
|
+
return true;
|
|
83
103
|
}
|
|
84
104
|
function suppressPhoneAfterIdLabel(text, match) {
|
|
85
105
|
if (match.patternDef.entityType !== EntityType.PHONE)
|
|
@@ -101,9 +121,13 @@ const NOT_CITY_CODES = new Set([
|
|
|
101
121
|
"ID", "NR", "NO", "ST", "DR", "MR", "MS", "HR", "FR", "IM", "IN", "OR", "IF", "IS", "IT", "AT", "AD", "AG", "AV",
|
|
102
122
|
"BE", "DE", "EU", "NL", "LU", "WS", "SS", "IP",
|
|
103
123
|
]);
|
|
124
|
+
const CURRENCY_PLATE = /^(?:EUR|USD|GBP|CHF|SEK|NOK|DKK|ISK|CZK|PLN|HUF|RON|BGN|HRK)\s/i;
|
|
104
125
|
function suppressPlateInCompound(text, match) {
|
|
105
126
|
if (match.patternDef.entityType !== EntityType.LICENSE_PLATE)
|
|
106
127
|
return false;
|
|
128
|
+
// Suppress currency + number misread as plate (e.g. "EUR 2")
|
|
129
|
+
if (CURRENCY_PLATE.test(match.text))
|
|
130
|
+
return true;
|
|
107
131
|
if (match.start >= 3) {
|
|
108
132
|
const before = text.slice(Math.max(0, match.start - 10), match.start);
|
|
109
133
|
if (/[A-Za-zÄÖÜäöüß]{2,}-$/.test(before))
|
|
@@ -175,6 +199,218 @@ function suppressPostalAsHouseNumber(text, match) {
|
|
|
175
199
|
}
|
|
176
200
|
return false;
|
|
177
201
|
}
|
|
202
|
+
// ── BIC: banking-context cues and heading shapes ────────────────────────
|
|
203
|
+
// \bSWIFT\b also covers SWIFT-Code / SWIFT-BIC / Code SWIFT, and \bBIC\b
|
|
204
|
+
// covers BIC-code / BIC/SWIFT.
|
|
205
|
+
const BIC_KEYWORD = /\b(?:BIC|SWIFT)\b/i;
|
|
206
|
+
const BANK_BLOCK = /\b(?:IBAN|Bankverbindung|Bankgegevens|Bankrekening|Rekening(?:nummer)?|Kontonummer|Konto|Kontoinhaber|Compte|Coordonn[ée]es\s+bancaires|Banque|Account\s+(?:number|holder)|Bankleitzahl|BLZ|Betaalgegevens|Zahlungsdaten)\b/i;
|
|
207
|
+
// A line that is nothing but a BIC/SWIFT label, as used in table and column
|
|
208
|
+
// layouts where the code sits on the following line.
|
|
209
|
+
const BIC_LABEL_LINE = /^\s*(?:BIC|SWIFT|SWIFT[-\s]?BIC|BIC\s?\/\s?SWIFT|SWIFT[-\s]?Code|BIC[-\s]?code|Code\s+SWIFT)\s*:?\s*$/i;
|
|
210
|
+
// An IBAN in the same structural unit: CC + 2 check digits + 2 or more groups.
|
|
211
|
+
const IBAN_SHAPE = /\b[A-Z]{2}\d{2}(?:\s?[A-Z0-9]{4}){2,}/;
|
|
212
|
+
// Largest paragraph still treated as one structural unit. Beyond this the unit
|
|
213
|
+
// falls back to the enclosing line, so a run-on document body cannot lend
|
|
214
|
+
// banking context to a token 2,000 characters away.
|
|
215
|
+
const MAX_UNIT_CHARS = 600;
|
|
216
|
+
function enclosingLine(text, start, end) {
|
|
217
|
+
const lineStart = text.lastIndexOf("\n", start - 1) + 1;
|
|
218
|
+
let lineEnd = text.indexOf("\n", end);
|
|
219
|
+
if (lineEnd === -1)
|
|
220
|
+
lineEnd = text.length;
|
|
221
|
+
return [lineStart, lineEnd];
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Return the enclosing line / record / paragraph around a match.
|
|
225
|
+
*
|
|
226
|
+
* The context window for BIC is scoped structurally, not by character count.
|
|
227
|
+
* A tight character window measured on the corpus rejects 97.3% of dictionary
|
|
228
|
+
* false positives but wrongly discards ~316 real-looking codes, because the
|
|
229
|
+
* banking cue often sits further away in the same record — e.g. a CSV row
|
|
230
|
+
* carrying the IBAN in one field and the BIC several fields later.
|
|
231
|
+
*/
|
|
232
|
+
function structuralUnit(text, start, end) {
|
|
233
|
+
const [lineStart, lineEnd] = enclosingLine(text, start, end);
|
|
234
|
+
let paraStart = lineStart;
|
|
235
|
+
while (paraStart > 0) {
|
|
236
|
+
const prevEnd = paraStart - 1;
|
|
237
|
+
const prevStart = text.lastIndexOf("\n", prevEnd - 1) + 1;
|
|
238
|
+
if (text.slice(prevStart, prevEnd).trim() === "")
|
|
239
|
+
break;
|
|
240
|
+
paraStart = prevStart;
|
|
241
|
+
}
|
|
242
|
+
let paraEnd = lineEnd;
|
|
243
|
+
while (paraEnd < text.length) {
|
|
244
|
+
const nextStart = paraEnd + 1;
|
|
245
|
+
let nextEnd = text.indexOf("\n", nextStart);
|
|
246
|
+
if (nextEnd === -1)
|
|
247
|
+
nextEnd = text.length;
|
|
248
|
+
if (text.slice(nextStart, nextEnd).trim() === "")
|
|
249
|
+
break;
|
|
250
|
+
paraEnd = nextEnd;
|
|
251
|
+
}
|
|
252
|
+
if (paraEnd - paraStart <= MAX_UNIT_CHARS)
|
|
253
|
+
return text.slice(paraStart, paraEnd);
|
|
254
|
+
return text.slice(lineStart, lineEnd);
|
|
255
|
+
}
|
|
256
|
+
function previousNonblankLine(text, lineStart) {
|
|
257
|
+
let pos = lineStart;
|
|
258
|
+
while (pos > 0) {
|
|
259
|
+
const prevEnd = pos - 1;
|
|
260
|
+
const prevStart = text.lastIndexOf("\n", prevEnd - 1) + 1;
|
|
261
|
+
const line = text.slice(prevStart, prevEnd);
|
|
262
|
+
if (line.trim() !== "")
|
|
263
|
+
return line;
|
|
264
|
+
pos = prevStart;
|
|
265
|
+
}
|
|
266
|
+
return "";
|
|
267
|
+
}
|
|
268
|
+
function escapeRe(s) {
|
|
269
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Does `token` also occur as an ordinary lowercase word in this document?
|
|
273
|
+
*
|
|
274
|
+
* `hospital`/`HOSPITAL`, `gegevens`/`GEGEVENS` — a token that appears in
|
|
275
|
+
* ordinary case elsewhere in the same document is a word, not a bank code.
|
|
276
|
+
* Email and domain contexts are excluded, so `ing.nl` does not vouch for a
|
|
277
|
+
* heading. Measured on the corpus this alone identifies ~78% of the BIC false
|
|
278
|
+
* positives with no dictionaries and no labelled data.
|
|
279
|
+
*/
|
|
280
|
+
function occursAsLowercaseWord(text, token) {
|
|
281
|
+
if (!/^[A-Za-z]+$/.test(token))
|
|
282
|
+
return false;
|
|
283
|
+
const lower = token.toLowerCase();
|
|
284
|
+
const capitalised = lower[0].toUpperCase() + lower.slice(1);
|
|
285
|
+
for (const form of [lower, capitalised]) {
|
|
286
|
+
// Cheap pre-check: a plain substring test almost always answers "no".
|
|
287
|
+
if (!text.includes(form))
|
|
288
|
+
continue;
|
|
289
|
+
const re = new RegExp(`\\b${escapeRe(form)}\\b`, "g");
|
|
290
|
+
let m;
|
|
291
|
+
while ((m = re.exec(text)) !== null) {
|
|
292
|
+
const before = m.index > 0 ? text[m.index - 1] : "";
|
|
293
|
+
const after = text.slice(m.index + form.length, m.index + form.length + 8);
|
|
294
|
+
if (before === "@" || (before === "." && m.index >= 2 && /[A-Za-z0-9]/.test(text[m.index - 2])))
|
|
295
|
+
continue;
|
|
296
|
+
if (after.startsWith("@") || /^\.[a-zA-Z]{2,6}\b/.test(after))
|
|
297
|
+
continue;
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Is the candidate positioned as a section heading rather than a value?
|
|
305
|
+
* Headings and shouted words are never bank codes. Lines carrying an explicit
|
|
306
|
+
* BIC/SWIFT keyword are exempt from the all-caps rule, so a genuine
|
|
307
|
+
* `BIC: GEBABEBB` line is not mistaken for a heading.
|
|
308
|
+
*/
|
|
309
|
+
function isHeadingShape(text, start, end, token) {
|
|
310
|
+
const [lineStart, lineEnd] = enclosingLine(text, start, end);
|
|
311
|
+
const line = text.slice(lineStart, lineEnd);
|
|
312
|
+
// The token is the entire line — unless the line above is a bare BIC/SWIFT
|
|
313
|
+
// label, which makes this a labelled value in a table layout, not a heading.
|
|
314
|
+
if (line.trim() === token) {
|
|
315
|
+
return !BIC_LABEL_LINE.test(previousNonblankLine(text, lineStart));
|
|
316
|
+
}
|
|
317
|
+
// The token starts its line and is immediately followed by a colon
|
|
318
|
+
if (text.slice(lineStart, start).trim() === "" && text.slice(end, lineEnd).trimStart().startsWith(":")) {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
// A shouted line: no lowercase, no digits, and no banking keyword
|
|
322
|
+
if (!/[a-z]/.test(line) && !/\d/.test(line) && !BIC_KEYWORD.test(line))
|
|
323
|
+
return true;
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Emit a BIC only on registry membership or banking context.
|
|
328
|
+
*
|
|
329
|
+
* 0. the token also occurs as an ordinary lowercase word here -> reject;
|
|
330
|
+
* 1. registry hit (deployment-supplied, then bundled seed prefixes) -> emit;
|
|
331
|
+
* 2. heading / shouted-word shape -> reject;
|
|
332
|
+
* 3. BIC/SWIFT keyword, IBAN or bank block in the structural unit -> emit;
|
|
333
|
+
*
|
|
334
|
+
* and a bare shape match reaching the end with none of the above is never
|
|
335
|
+
* emitted. Gate 0 outranks every tier below it — no genuine BIC is also an
|
|
336
|
+
* ordinary lowercase word — so it is applied on both emitting paths. It scans
|
|
337
|
+
* the whole document, so it runs only on the paths that would otherwise emit.
|
|
338
|
+
*/
|
|
339
|
+
function suppressBicWithoutEvidence(text, match) {
|
|
340
|
+
if (match.patternDef.entityType !== EntityType.BIC)
|
|
341
|
+
return false;
|
|
342
|
+
const token = match.text.trim();
|
|
343
|
+
if (isRegisteredBic(token))
|
|
344
|
+
return occursAsLowercaseWord(text, token);
|
|
345
|
+
if (isHeadingShape(text, match.start, match.end, token))
|
|
346
|
+
return true;
|
|
347
|
+
// The token itself is blanked out so it cannot vouch for itself.
|
|
348
|
+
const unit = structuralUnit(text, match.start, match.end).split(token).join(" ".repeat(token.length));
|
|
349
|
+
if (BIC_KEYWORD.test(unit) || IBAN_SHAPE.test(unit) || BANK_BLOCK.test(unit)) {
|
|
350
|
+
return occursAsLowercaseWord(text, token);
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
// ── Postal code: digits belonging to a longer identifier ────────────────
|
|
355
|
+
// An identifier label immediately before the digits. A postal code is never
|
|
356
|
+
// introduced this way; an SVNr, policy number or service number always is.
|
|
357
|
+
const ID_CUE_BEFORE = /(?:[\w\-]*Nr|[\w\-]*N[°ºo]|[\w\-]*Nummer|[\w\-]*Numero|[\w\-]*Numéro|No|number|num|Kennzahl|Aktenzeichen|Az|e-?card|Polizze|Police|Policen)\.?\s*:?\s*$/i;
|
|
358
|
+
// An international dialling prefix earlier on the same line, with nothing but
|
|
359
|
+
// number punctuation in between: these digits belong to the phone detector.
|
|
360
|
+
const DIALLING_PREFIX_BEFORE = /\+\d{1,3}[\d\s\-().]*$/;
|
|
361
|
+
// A country prefix on a postal code — "A-1010 Wien", "B-2000", "L-1234".
|
|
362
|
+
const COUNTRY_PREFIXED = /(?:^|[^A-Za-z0-9])[A-Z]{1,2}$/;
|
|
363
|
+
/**
|
|
364
|
+
* Suppress bare digit runs that belong to a longer number, not an address.
|
|
365
|
+
*
|
|
366
|
+
* A bare 4- or 5-digit run is the weakest shape in the engine, and when it
|
|
367
|
+
* cuts into a longer identifier the damage is worse than a plain false
|
|
368
|
+
* positive: `SV-Nummer: [POSTAL_CODE] 040390` leaves half an Austrian social
|
|
369
|
+
* security number exposed with no way to label the remainder.
|
|
370
|
+
*
|
|
371
|
+
* Applies only to digits-only matches, so structured forms keep their own
|
|
372
|
+
* behaviour — NL `1234 AB`, PT `1234-567`, LU `L-1234`.
|
|
373
|
+
*/
|
|
374
|
+
function suppressPostalInLongerIdentifier(text, match) {
|
|
375
|
+
if (match.patternDef.entityType !== EntityType.POSTAL_CODE)
|
|
376
|
+
return false;
|
|
377
|
+
const clean = match.text.trim();
|
|
378
|
+
if (!/^\d+$/.test(clean))
|
|
379
|
+
return false;
|
|
380
|
+
const beforeChar = match.start > 0 ? text[match.start - 1] : "";
|
|
381
|
+
const afterChar = match.end < text.length ? text[match.end] : "";
|
|
382
|
+
const prevPrev = match.start >= 2 ? text[match.start - 2] : "";
|
|
383
|
+
const nextNext = match.end + 1 < text.length ? text[match.end + 1] : "";
|
|
384
|
+
// Directly glued to more digits
|
|
385
|
+
if (/\d/.test(beforeChar) || /\d/.test(afterChar))
|
|
386
|
+
return true;
|
|
387
|
+
// Joined by identifier punctuation: "0456.2398.71-02", "4471/2025".
|
|
388
|
+
// The punctuation only counts when it actually *joins two digit groups* — a
|
|
389
|
+
// trailing period is ordinary sentence punctuation, and treating it as a
|
|
390
|
+
// separator discards every postal code that ends a sentence
|
|
391
|
+
// ("Domicilio: Palma, 13867. Pagos a ...").
|
|
392
|
+
if ("._/".includes(beforeChar) && /\d/.test(prevPrev))
|
|
393
|
+
return true;
|
|
394
|
+
if ("._/".includes(afterChar) && /\d/.test(nextNext))
|
|
395
|
+
return true;
|
|
396
|
+
if (afterChar === "-" && /\d/.test(nextNext))
|
|
397
|
+
return true;
|
|
398
|
+
if (beforeChar === "-" && !COUNTRY_PREFIXED.test(text.slice(0, match.start - 1)))
|
|
399
|
+
return true;
|
|
400
|
+
// A further digit group on the same line: "1268 040390", "1234 5678 925",
|
|
401
|
+
// "+43 664 8213 907". Horizontal whitespace only — a digit on the *next*
|
|
402
|
+
// line is a separate field, not a continuation.
|
|
403
|
+
if (/^[ \t]+\d/.test(text.slice(match.end, match.end + 4)))
|
|
404
|
+
return true;
|
|
405
|
+
// An identifier label introduces the digits: "DiNr. 4471", "Policen-Nr."
|
|
406
|
+
if (ID_CUE_BEFORE.test(text.slice(Math.max(0, match.start - 40), match.start)))
|
|
407
|
+
return true;
|
|
408
|
+
// Digits after an international dialling prefix belong to PHONE
|
|
409
|
+
const [lineStart] = enclosingLine(text, match.start, match.end);
|
|
410
|
+
if (DIALLING_PREFIX_BEFORE.test(text.slice(lineStart, match.start)))
|
|
411
|
+
return true;
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
178
414
|
function suppressRequiresContext(text, match) {
|
|
179
415
|
if (!match.patternDef.requiresContext || match.patternDef.contextKeywords.length === 0)
|
|
180
416
|
return false;
|
|
@@ -187,8 +423,9 @@ const TYPE_SUPPRESSORS = {
|
|
|
187
423
|
[EntityType.NATIONAL_ID]: [suppressCurrency, suppressUnits, suppressReference, suppressLegal, suppressMath, suppressNatidAsPassport, suppressSeNatidAsOrg],
|
|
188
424
|
[EntityType.SSN]: [suppressCurrency, suppressUnits, suppressReference, suppressMath],
|
|
189
425
|
[EntityType.TAX_ID]: [suppressCurrency, suppressUnits, suppressReference, suppressMath],
|
|
190
|
-
[EntityType.POSTAL_CODE]: [suppressCurrency, suppressUnits, suppressMath, suppressLegal, suppressYearAsPostal, suppressPostalInsideIban, suppressPostalAsHouseNumber],
|
|
191
|
-
[EntityType.
|
|
426
|
+
[EntityType.POSTAL_CODE]: [suppressCurrency, suppressUnits, suppressMath, suppressLegal, suppressYearAsPostal, suppressPostalInsideIban, suppressPostalAsHouseNumber, suppressPostalInLongerIdentifier],
|
|
427
|
+
[EntityType.BIC]: [suppressBicWithoutEvidence],
|
|
428
|
+
[EntityType.BANK_ACCOUNT]: [suppressReference],
|
|
192
429
|
[EntityType.LICENSE_PLATE]: [suppressPlateInCompound],
|
|
193
430
|
[EntityType.CHAMBER_OF_COMMERCE]: [suppressReference],
|
|
194
431
|
};
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Total IBAN length per country (ISO 13616 registry). Also used to build the
|
|
3
|
+
* country-independent IBAN pattern in rules/countries, so an IBAN is detected
|
|
4
|
+
* on its own structure rather than on the caller's `countries` setting.
|
|
5
|
+
*/
|
|
6
|
+
export declare const IBAN_LENGTHS: Record<string, number>;
|
|
7
|
+
/** ISO 3166-1 alpha-2 codes, plus XK (Kosovo, user-assigned but in banking use). */
|
|
8
|
+
export declare const ISO_3166_ALPHA2: ReadonlySet<string>;
|
|
1
9
|
export declare function validateIban(candidate: string): boolean;
|
|
2
10
|
export declare function validateBsn(candidate: string): boolean;
|
|
3
11
|
export declare function validateBelgianNn(candidate: string): boolean;
|
|
@@ -10,7 +18,24 @@ export declare function validateVatLu(candidate: string): boolean;
|
|
|
10
18
|
export declare function validateGermanTaxId(candidate: string): boolean;
|
|
11
19
|
export declare function validateFrenchNir(candidate: string): boolean;
|
|
12
20
|
export declare function validateVin(candidate: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* BIC/SWIFT structural validation (ISO 9362): BBBB CC LL (PPP).
|
|
23
|
+
*
|
|
24
|
+
* Necessary but **not sufficient** — BIC has no check digit, and characters
|
|
25
|
+
* 5-6 of ordinary uppercase words are frequently valid country codes
|
|
26
|
+
* (`DRINGEND` -> `GE`, `HOSPITAL` -> `IT`). Emission additionally requires a
|
|
27
|
+
* registry hit or banking context; see `suppressBicWithoutEvidence`.
|
|
28
|
+
*/
|
|
13
29
|
export declare function validateBic(candidate: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* International phone number in E.164 form: `+` then 8-15 digits.
|
|
32
|
+
*
|
|
33
|
+
* A `+` prefix makes a phone number self-identifying, so this backs a
|
|
34
|
+
* country-independent pattern: the per-country patterns each hard-code one
|
|
35
|
+
* grouping and miss the others (`+43 664 8213 907` was invisible because the
|
|
36
|
+
* Austrian pattern expects an unbroken subscriber number).
|
|
37
|
+
*/
|
|
38
|
+
export declare function validateE164(candidate: string): boolean;
|
|
14
39
|
export declare function validateKvk(candidate: string): boolean;
|
|
15
40
|
export declare function validateSwedishPnr(candidate: string): boolean;
|
|
16
41
|
export declare function validateNorwegianFnr(candidate: string): boolean;
|
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
function clean(candidate, chars = /[\s.\-]/g) {
|
|
2
2
|
return candidate.replace(chars, "");
|
|
3
3
|
}
|
|
4
|
+
/**
|
|
5
|
+
* Total IBAN length per country (ISO 13616 registry). Also used to build the
|
|
6
|
+
* country-independent IBAN pattern in rules/countries, so an IBAN is detected
|
|
7
|
+
* on its own structure rather than on the caller's `countries` setting.
|
|
8
|
+
*/
|
|
9
|
+
export const IBAN_LENGTHS = {
|
|
10
|
+
AL: 28, AD: 24, AT: 20, AZ: 28, BH: 22, BY: 28, BE: 16, BA: 20, BR: 29, BG: 22, CR: 22, HR: 21,
|
|
11
|
+
CY: 28, CZ: 24, DK: 18, DO: 28, TL: 23, EE: 20, FO: 18, FI: 18, FR: 27, GE: 22, DE: 22, GI: 23,
|
|
12
|
+
GR: 27, GL: 18, GT: 28, HU: 28, IS: 26, IQ: 23, IE: 22, IL: 23, IT: 27, JO: 30, KZ: 20, XK: 20,
|
|
13
|
+
KW: 30, LV: 21, LB: 28, LI: 21, LT: 20, LU: 20, MT: 31, MR: 27, MU: 30, MC: 27, MD: 24, ME: 22,
|
|
14
|
+
NL: 18, MK: 19, NO: 15, PK: 24, PS: 29, PL: 28, PT: 25, QA: 29, RO: 24, LC: 32, SM: 27, SA: 24,
|
|
15
|
+
RS: 22, SC: 31, SK: 24, SI: 19, ES: 24, SE: 24, CH: 21, TN: 24, TR: 26, UA: 29, AE: 23, GB: 22,
|
|
16
|
+
VA: 22, VG: 24,
|
|
17
|
+
};
|
|
18
|
+
/** ISO 3166-1 alpha-2 codes, plus XK (Kosovo, user-assigned but in banking use). */
|
|
19
|
+
export const ISO_3166_ALPHA2 = new Set([
|
|
20
|
+
"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ",
|
|
21
|
+
"BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS",
|
|
22
|
+
"BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN",
|
|
23
|
+
"CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE",
|
|
24
|
+
"EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF",
|
|
25
|
+
"GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM",
|
|
26
|
+
"HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM",
|
|
27
|
+
"JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC",
|
|
28
|
+
"LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK",
|
|
29
|
+
"ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA",
|
|
30
|
+
"NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG",
|
|
31
|
+
"PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW",
|
|
32
|
+
"SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS",
|
|
33
|
+
"ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO",
|
|
34
|
+
"TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI",
|
|
35
|
+
"VN", "VU", "WF", "WS", "XK", "YE", "YT", "ZA", "ZM", "ZW",
|
|
36
|
+
]);
|
|
4
37
|
export function validateIban(candidate) {
|
|
5
38
|
const c = candidate.replace(/[\s\-]/g, "").toUpperCase();
|
|
6
39
|
if (c.length < 5 || !/^[A-Z]{2}/.test(c) || !/^\d{2}$/.test(c.slice(2, 4)))
|
|
7
40
|
return false;
|
|
8
|
-
const ibanLengths = {
|
|
9
|
-
AL: 28, AD: 24, AT: 20, AZ: 28, BH: 22, BY: 28, BE: 16, BA: 20, BR: 29, BG: 22, CR: 22, HR: 21,
|
|
10
|
-
CY: 28, CZ: 24, DK: 18, DO: 28, TL: 23, EE: 20, FO: 18, FI: 18, FR: 27, GE: 22, DE: 22, GI: 23,
|
|
11
|
-
GR: 27, GL: 18, GT: 28, HU: 28, IS: 26, IQ: 23, IE: 22, IL: 23, IT: 27, JO: 30, KZ: 20, XK: 20,
|
|
12
|
-
KW: 30, LV: 21, LB: 28, LI: 21, LT: 20, LU: 20, MT: 31, MR: 27, MU: 30, MC: 27, MD: 24, ME: 22,
|
|
13
|
-
NL: 18, MK: 19, NO: 15, PK: 24, PS: 29, PL: 28, PT: 25, QA: 29, RO: 24, LC: 32, SM: 27, SA: 24,
|
|
14
|
-
RS: 22, SC: 31, SK: 24, SI: 19, ES: 24, SE: 24, CH: 21, TN: 24, TR: 26, UA: 29, AE: 23, GB: 22,
|
|
15
|
-
VA: 22, VG: 24,
|
|
16
|
-
};
|
|
17
41
|
const country = c.slice(0, 2);
|
|
18
|
-
const expected =
|
|
42
|
+
const expected = IBAN_LENGTHS[country];
|
|
19
43
|
if (expected !== undefined && c.length !== expected)
|
|
20
44
|
return false;
|
|
21
45
|
const rearranged = c.slice(4) + c.slice(0, 4);
|
|
@@ -161,13 +185,22 @@ export function validateVin(candidate) {
|
|
|
161
185
|
return false;
|
|
162
186
|
return !/[IOQ]/.test(c);
|
|
163
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* BIC/SWIFT structural validation (ISO 9362): BBBB CC LL (PPP).
|
|
190
|
+
*
|
|
191
|
+
* Necessary but **not sufficient** — BIC has no check digit, and characters
|
|
192
|
+
* 5-6 of ordinary uppercase words are frequently valid country codes
|
|
193
|
+
* (`DRINGEND` -> `GE`, `HOSPITAL` -> `IT`). Emission additionally requires a
|
|
194
|
+
* registry hit or banking context; see `suppressBicWithoutEvidence`.
|
|
195
|
+
*/
|
|
164
196
|
export function validateBic(candidate) {
|
|
165
197
|
const c = candidate.replace(/ /g, "").toUpperCase();
|
|
166
198
|
if (c.length !== 8 && c.length !== 11)
|
|
167
199
|
return false;
|
|
168
200
|
if (!/^[A-Z]{4}/.test(c))
|
|
169
201
|
return false;
|
|
170
|
-
|
|
202
|
+
// Country code must be a real ISO 3166-1 alpha-2 code
|
|
203
|
+
if (!ISO_3166_ALPHA2.has(c.slice(4, 6)))
|
|
171
204
|
return false;
|
|
172
205
|
if (!/^[A-Z0-9]{2}$/.test(c.slice(6, 8)))
|
|
173
206
|
return false;
|
|
@@ -175,6 +208,21 @@ export function validateBic(candidate) {
|
|
|
175
208
|
return false;
|
|
176
209
|
return true;
|
|
177
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* International phone number in E.164 form: `+` then 8-15 digits.
|
|
213
|
+
*
|
|
214
|
+
* A `+` prefix makes a phone number self-identifying, so this backs a
|
|
215
|
+
* country-independent pattern: the per-country patterns each hard-code one
|
|
216
|
+
* grouping and miss the others (`+43 664 8213 907` was invisible because the
|
|
217
|
+
* Austrian pattern expects an unbroken subscriber number).
|
|
218
|
+
*/
|
|
219
|
+
export function validateE164(candidate) {
|
|
220
|
+
if (!candidate.trimStart().startsWith("+"))
|
|
221
|
+
return false;
|
|
222
|
+
// A trunk prefix written as "(0)" is not part of the E.164 digit count.
|
|
223
|
+
const digits = candidate.replace(/\(\s*0\s*\)/g, "").replace(/\D/g, "");
|
|
224
|
+
return digits.length >= 8 && digits.length <= 15;
|
|
225
|
+
}
|
|
178
226
|
export function validateKvk(candidate) {
|
|
179
227
|
const c = clean(candidate);
|
|
180
228
|
return c.length === 8 && /^\d{8}$/.test(c);
|
|
@@ -621,6 +669,7 @@ export const VALIDATORS = {
|
|
|
621
669
|
french_nir: validateFrenchNir,
|
|
622
670
|
vin: validateVin,
|
|
623
671
|
bic: validateBic,
|
|
672
|
+
e164: validateE164,
|
|
624
673
|
kvk: validateKvk,
|
|
625
674
|
swedish_pnr: validateSwedishPnr,
|
|
626
675
|
norwegian_fnr: validateNorwegianFnr,
|
package/dist/esm/sdk.d.ts
CHANGED
|
@@ -10,7 +10,14 @@ export declare class EuRedact {
|
|
|
10
10
|
private engine;
|
|
11
11
|
private cache;
|
|
12
12
|
private referentialMapper;
|
|
13
|
+
private maxInputLength;
|
|
14
|
+
constructor(options?: {
|
|
15
|
+
maxInputLength?: number;
|
|
16
|
+
});
|
|
13
17
|
addCustomPattern(name: string, pattern: string): void;
|
|
18
|
+
/** Clear the result cache and referential integrity mappings.
|
|
19
|
+
* Call this in long-running processes to free PII from memory. */
|
|
20
|
+
clear(): void;
|
|
14
21
|
redact(text: string, options?: RedactOptions): RedactResult;
|
|
15
22
|
redactBatch(texts: string[], options?: RedactOptions): RedactResult[];
|
|
16
23
|
}
|
package/dist/esm/sdk.js
CHANGED
|
@@ -14,16 +14,36 @@ class ReferentialMapper {
|
|
|
14
14
|
}
|
|
15
15
|
return this.mapping.get(text);
|
|
16
16
|
}
|
|
17
|
+
clear() {
|
|
18
|
+
this.counters.clear();
|
|
19
|
+
this.mapping.clear();
|
|
20
|
+
}
|
|
17
21
|
}
|
|
22
|
+
const DEFAULT_MAX_INPUT_LENGTH = 10_485_760; // ~10 MB of text
|
|
18
23
|
export class EuRedact {
|
|
19
24
|
engine = new RuleEngine();
|
|
20
25
|
cache = new ResultCache();
|
|
21
26
|
referentialMapper = new ReferentialMapper();
|
|
27
|
+
maxInputLength;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.maxInputLength = options?.maxInputLength ?? DEFAULT_MAX_INPUT_LENGTH;
|
|
30
|
+
}
|
|
22
31
|
addCustomPattern(name, pattern) {
|
|
23
32
|
this.engine.addCustomPattern(name, pattern);
|
|
24
33
|
this.cache.clear();
|
|
25
34
|
}
|
|
35
|
+
/** Clear the result cache and referential integrity mappings.
|
|
36
|
+
* Call this in long-running processes to free PII from memory. */
|
|
37
|
+
clear() {
|
|
38
|
+
this.cache.clear();
|
|
39
|
+
this.referentialMapper.clear();
|
|
40
|
+
}
|
|
26
41
|
redact(text, options = {}) {
|
|
42
|
+
if (text.length > this.maxInputLength) {
|
|
43
|
+
throw new Error(`Input text length (${text.length.toLocaleString()} chars) exceeds the maximum ` +
|
|
44
|
+
`(${this.maxInputLength.toLocaleString()} chars). Split the input or ` +
|
|
45
|
+
`increase maxInputLength when constructing EuRedact.`);
|
|
46
|
+
}
|
|
27
47
|
const { countries = null, mode = "rules", referentialIntegrity = false, detectDates = false, cache = true, } = options;
|
|
28
48
|
const [normalizedText, offsetMapping] = normalize(text);
|
|
29
49
|
const countriesTuple = countries
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
export declare enum EntityType {
|
|
2
2
|
NAME = "NAME",
|
|
3
3
|
ADDRESS = "ADDRESS",
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Canonical name for a bank account identifier. `IBAN` is kept below as a
|
|
6
|
+
* legacy alias with the same value, so `EntityType.IBAN === EntityType.BANK_ACCOUNT`
|
|
7
|
+
* and existing code keeps working — but the emitted value, and therefore the
|
|
8
|
+
* `[BANK_ACCOUNT]` placeholder written into redacted text, is canonical.
|
|
9
|
+
*/
|
|
10
|
+
BANK_ACCOUNT = "BANK_ACCOUNT",
|
|
11
|
+
/** @deprecated legacy alias of {@link EntityType.BANK_ACCOUNT} */
|
|
12
|
+
IBAN = "BANK_ACCOUNT",
|
|
5
13
|
BIC = "BIC",
|
|
6
14
|
CREDIT_CARD = "CREDIT_CARD",
|
|
7
15
|
PHONE = "PHONE",
|
|
@@ -31,6 +39,12 @@ export declare enum EntityType {
|
|
|
31
39
|
SECRET = "SECRET",
|
|
32
40
|
OTHER = "OTHER"
|
|
33
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Legacy type names accepted on input and mapped to their canonical form.
|
|
44
|
+
* Kept in sync with the pipeline canon (config/entity_types.json →
|
|
45
|
+
* legacy_aliases). The engine never *emits* these names.
|
|
46
|
+
*/
|
|
47
|
+
export declare const LEGACY_TYPE_ALIASES: Record<string, string>;
|
|
34
48
|
export declare enum DetectionSource {
|
|
35
49
|
RULES = "rules",
|
|
36
50
|
CLOUD = "cloud"
|
package/dist/esm/types.js
CHANGED
|
@@ -2,7 +2,15 @@ export var EntityType;
|
|
|
2
2
|
(function (EntityType) {
|
|
3
3
|
EntityType["NAME"] = "NAME";
|
|
4
4
|
EntityType["ADDRESS"] = "ADDRESS";
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Canonical name for a bank account identifier. `IBAN` is kept below as a
|
|
7
|
+
* legacy alias with the same value, so `EntityType.IBAN === EntityType.BANK_ACCOUNT`
|
|
8
|
+
* and existing code keeps working — but the emitted value, and therefore the
|
|
9
|
+
* `[BANK_ACCOUNT]` placeholder written into redacted text, is canonical.
|
|
10
|
+
*/
|
|
11
|
+
EntityType["BANK_ACCOUNT"] = "BANK_ACCOUNT";
|
|
12
|
+
/** @deprecated legacy alias of {@link EntityType.BANK_ACCOUNT} */
|
|
13
|
+
EntityType["IBAN"] = "BANK_ACCOUNT";
|
|
6
14
|
EntityType["BIC"] = "BIC";
|
|
7
15
|
EntityType["CREDIT_CARD"] = "CREDIT_CARD";
|
|
8
16
|
EntityType["PHONE"] = "PHONE";
|
|
@@ -32,6 +40,14 @@ export var EntityType;
|
|
|
32
40
|
EntityType["SECRET"] = "SECRET";
|
|
33
41
|
EntityType["OTHER"] = "OTHER";
|
|
34
42
|
})(EntityType || (EntityType = {}));
|
|
43
|
+
/**
|
|
44
|
+
* Legacy type names accepted on input and mapped to their canonical form.
|
|
45
|
+
* Kept in sync with the pipeline canon (config/entity_types.json →
|
|
46
|
+
* legacy_aliases). The engine never *emits* these names.
|
|
47
|
+
*/
|
|
48
|
+
export const LEGACY_TYPE_ALIASES = {
|
|
49
|
+
IBAN: "BANK_ACCOUNT",
|
|
50
|
+
};
|
|
35
51
|
export var DetectionSource;
|
|
36
52
|
(function (DetectionSource) {
|
|
37
53
|
DetectionSource["RULES"] = "rules";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "euredact",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "European PII redaction SDK -- fast, zero-dependency, rule-based detection and redaction of personal data across 31 European countries",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -46,15 +46,19 @@
|
|
|
46
46
|
],
|
|
47
47
|
"repository": {
|
|
48
48
|
"type": "git",
|
|
49
|
-
"url": "https://github.com/euredact/euredact"
|
|
49
|
+
"url": "git+https://github.com/euredact/euredact.git"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build:esm": "tsc",
|
|
53
53
|
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
54
54
|
"postbuild:cjs": "echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
|
|
55
55
|
"build": "rm -rf dist && npm run build:esm && npm run build:cjs",
|
|
56
|
-
"test": "
|
|
57
|
-
"
|
|
56
|
+
"test": "npm run test:spec",
|
|
57
|
+
"benchmark": "node --import tsx src/__tests__/benchmark.ts",
|
|
58
|
+
"prepublishOnly": "npm run build",
|
|
59
|
+
"test:spec": "node --import tsx src/__tests__/spec.ts",
|
|
60
|
+
"test:eval": "node --import tsx src/__tests__/run.ts",
|
|
61
|
+
"test:eval:full": "node --max-old-space-size=8192 --import tsx src/__tests__/evalFull.ts"
|
|
58
62
|
},
|
|
59
63
|
"devDependencies": {
|
|
60
64
|
"typescript": "^5.4.0",
|