euredact 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -16
- package/dist/cjs/cache.js +5 -0
- package/dist/cjs/cloud/client.js +26 -2
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/rules/countries/index.js +73 -8
- package/dist/cjs/rules/cues.js +21 -2
- package/dist/cjs/sdk.js +385 -64
- package/dist/esm/cache.d.ts +2 -0
- package/dist/esm/cache.js +5 -1
- package/dist/esm/cloud/client.js +26 -2
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/rules/countries/index.js +73 -8
- package/dist/esm/rules/cues.d.ts +13 -1
- package/dist/esm/rules/cues.js +21 -2
- package/dist/esm/sdk.d.ts +121 -1
- package/dist/esm/sdk.js +381 -63
- package/dist/esm/types.d.ts +30 -0
- package/package.json +6 -3
|
@@ -28,6 +28,41 @@ function p(entityType, pattern, validator = null, description = "", contextKeywo
|
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
29
|
// Secret context keywords (consolidated, reused across SECRET patterns)
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
|
+
/**
|
|
32
|
+
* Words that name a travel document, in the official languages of the 31
|
|
33
|
+
* countries this engine covers, plus English.
|
|
34
|
+
*
|
|
35
|
+
* Shared by every passport pattern. Before this existed each country kept its
|
|
36
|
+
* own list, so a passport was only recognised when its shape and its label
|
|
37
|
+
* came from the same country: `Reisepass: CA1234567` was missed because the
|
|
38
|
+
* German pattern rejects that alphabet while the Dutch pattern, whose shape
|
|
39
|
+
* fits, had never heard of "Reisepass" (issue rules-engine#23).
|
|
40
|
+
*
|
|
41
|
+
* The bare Scandinavian "pass", Finnish "passi" and Latvian "pase" are
|
|
42
|
+
* omitted deliberately: context matching is substring, not word-boundary, so
|
|
43
|
+
* they fire inside "password", "passenger", "passive" and "phase". Only the
|
|
44
|
+
* compound forms are listed. Mirrors PASSPORT_CONTEXT in Python.
|
|
45
|
+
*/
|
|
46
|
+
const PASSPORT_CONTEXT = [
|
|
47
|
+
"passport", "passport no", "passport number", "travel document",
|
|
48
|
+
"reisepass", "passnummer", "reisepassnummer", "reisepass-nr", "pass nr",
|
|
49
|
+
"reisedokument",
|
|
50
|
+
"paspoort", "paspoortnummer", "reisdocument", "identiteitsbewijs",
|
|
51
|
+
"passeport", "numéro de passeport", "document de voyage",
|
|
52
|
+
"passaporto", "numero di passaporto", "documento di viaggio",
|
|
53
|
+
"pasaporte", "número de pasaporte", "documento de viaje",
|
|
54
|
+
"passaporte", "número do passaporte",
|
|
55
|
+
"passnr", "passets nummer", "passin numero", "passinumero",
|
|
56
|
+
"passi number", "passinumber",
|
|
57
|
+
"vegabréf", "vegabréfsnúmer",
|
|
58
|
+
"paso numeris", "pases numurs",
|
|
59
|
+
"paszport", "numer paszportu", "cestovní pas", "číslo pasu",
|
|
60
|
+
"cestovný pas", "potni list", "številka potnega lista",
|
|
61
|
+
"putovnica", "broj putovnice",
|
|
62
|
+
"útlevél", "útlevélszám", "pașaport", "numărul pașaportului",
|
|
63
|
+
"паспорт", "номер на паспорт", "διαβατήριο",
|
|
64
|
+
"αριθμός διαβατηρίου", "passaport",
|
|
65
|
+
];
|
|
31
66
|
const SECRET_CONTEXT = [
|
|
32
67
|
// English
|
|
33
68
|
"key", "token", "secret", "password", "credential",
|
|
@@ -96,7 +131,14 @@ const SHARED = {
|
|
|
96
131
|
// is Unicode-aware) matched local parts like "petras_ž@..." while this one
|
|
97
132
|
// did not. \p{L}\p{N} with the `u` flag restores parity; the leading guard
|
|
98
133
|
// is a Unicode-safe word boundary.
|
|
99
|
-
|
|
134
|
+
// The apostrophe is allowed only between word characters: it belongs in a
|
|
135
|
+
// local part, but putting it in the class would let the match absorb a
|
|
136
|
+
// quote from the surrounding text, while leaving it out made
|
|
137
|
+
// `johno'neill@outlook.ie` mask as `johno'[EMAIL]` (issue rules-engine#10).
|
|
138
|
+
// The second lookbehind refuses to start a match after `<word-char>'`,
|
|
139
|
+
// which is a match already in progress; without it every apostrophe was a
|
|
140
|
+
// fresh start offset and the scan went quadratic. Mirrors Python.
|
|
141
|
+
p(EntityType.EMAIL, String.raw `(?<![\p{L}\p{N}._%+\-])(?<![\p{L}\p{N}._%+\-]')[\p{L}\p{N}._%+\-]+(?:'[\p{L}\p{N}._%+\-]+)*@(?:[\p{L}\p{N}\-]+\.)+\p{L}{2,}\b`),
|
|
100
142
|
// IBAN (any country) — structure + mod-97 only. Country-specific IBAN
|
|
101
143
|
// patterns still exist and win on identical spans; this one guarantees an
|
|
102
144
|
// IBAN is never missed just because its country was not requested.
|
|
@@ -106,7 +148,22 @@ const SHARED = {
|
|
|
106
148
|
// "+43 664 8213 907" was invisible to the Austrian pattern, which expects
|
|
107
149
|
// an unbroken subscriber number. A leading "+" is self-identifying, so
|
|
108
150
|
// this is deliberately not country-gated.
|
|
109
|
-
|
|
151
|
+
// Parentheses are matched as a pair inside one group: `\(?...\)?` let the
|
|
152
|
+
// closing parenthesis of the surrounding text join the number, so
|
|
153
|
+
// "Call (+32 475 12 34 56) today." also masked the ")" (issue
|
|
154
|
+
// rules-engine#3). Mirrors the Python pattern character for character.
|
|
155
|
+
p(EntityType.PHONE, String.raw `\+\d{1,3}(?:[\s\-/]?(?:\(\d{1,4}\)|\d{1,4})){2,7}`, "e164", "International phone number (E.164) — any country"),
|
|
156
|
+
// Passport (any country). The per-country patterns cover BE, DE, FR and NL
|
|
157
|
+
// only; the other 26 had none. Same argument as the E.164 phone rule: one
|
|
158
|
+
// country-independent shape, with the label carrying the precision.
|
|
159
|
+
// Shapes from the project canon (prompts/pii_definitions.md, PASSPORT):
|
|
160
|
+
// EU is 1-2 letters + 7 digits, UK is 9 digits optionally GBR-prefixed.
|
|
161
|
+
// The canon's position is that the EU converged on one shape, so national
|
|
162
|
+
// regexes would contradict it as well as being unverifiable. An earlier
|
|
163
|
+
// draft required a leading letter and missed all-numeric passports, the
|
|
164
|
+
// UK's own format among them; requiring exactly nine digits for the
|
|
165
|
+
// numeric form rejects eight-digit dates on shape (issue rules-engine#23).
|
|
166
|
+
p(EntityType.PASSPORT, String.raw `\b(?:GBR\d{9}|[A-Z]{1,2}\d{7}|\d{9})\b`, null, "Passport number — any country, label-gated", PASSPORT_CONTEXT, true),
|
|
110
167
|
p(EntityType.BIC, String.raw `\b[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?\b`, "bic"),
|
|
111
168
|
// Space-separated form: "RZBA AT WW", "NICA BE BB". Written as a separate
|
|
112
169
|
// pattern requiring a space between *every* group, rather than making the
|
|
@@ -117,7 +174,15 @@ const SHARED = {
|
|
|
117
174
|
p(EntityType.CREDIT_CARD, String.raw `\b(?:4[0-9]{3}[\s\-]?[0-9]{4}[\s\-]?[0-9]{4}[\s\-]?[0-9]{4}|5[1-5][0-9]{2}[\s\-]?[0-9]{4}[\s\-]?[0-9]{4}[\s\-]?[0-9]{4}|3[47][0-9]{2}[\s\-]?[0-9]{6}[\s\-]?[0-9]{5})\b`, "luhn"),
|
|
118
175
|
p(EntityType.VIN, String.raw `\b[A-HJ-NPR-Z0-9]{17}\b`, "vin"),
|
|
119
176
|
p(EntityType.IP_ADDRESS, String.raw `\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b`),
|
|
120
|
-
|
|
177
|
+
// Branch order is load-bearing: alternation is leftmost-first, not
|
|
178
|
+
// longest-match, so the form that consumes most is tried first, and the
|
|
179
|
+
// IPv4-embedded branches lead because every group of a dotted quad is
|
|
180
|
+
// also valid hex. `\b` is the wrong anchor around ":" (a non-word
|
|
181
|
+
// character), which is why `::1` and `2001:db8::` were not detected at
|
|
182
|
+
// all. A bare "::" is deliberately not matched. Mirrors the Python
|
|
183
|
+
// pattern character for character -- see the comment there
|
|
184
|
+
// (issue rules-engine#5).
|
|
185
|
+
p(EntityType.IPV6_ADDRESS, String.raw `(?<![0-9A-Za-z_:.])(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])|(?:[0-9A-Fa-f]{1,4}:){1,5}:(?:[0-9A-Fa-f]{1,4}:)*(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])|::(?:[0-9A-Fa-f]{1,4}:)*(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])|(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}(?::[0-9A-Fa-f]{1,4}){1,7}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|:(?::[0-9A-Fa-f]{1,4}){1,7})(?:%[0-9A-Za-z][0-9A-Za-z._-]*)?(?![0-9A-Za-z_:])`),
|
|
121
186
|
p(EntityType.MAC_ADDRESS, String.raw `\b(?:[0-9a-fA-F]{2}[:\-]){5}[0-9a-fA-F]{2}\b`, null, "colon/dash"),
|
|
122
187
|
p(EntityType.MAC_ADDRESS, String.raw `\b[0-9a-fA-F]{4}\.[0-9a-fA-F]{4}\.[0-9a-fA-F]{4}\b`, null, "Cisco"),
|
|
123
188
|
p(EntityType.IMEI, String.raw `\b\d{15}\b`, "imei"),
|
|
@@ -192,7 +257,7 @@ const NL = {
|
|
|
192
257
|
p(EntityType.PHONE, String.raw `\b06[\s\-]?\d{2}[\s\-]?\d{2}[\s\-]?\d{2}[\s\-]?\d{2}\b`),
|
|
193
258
|
p(EntityType.PHONE, String.raw `\b0[1-9]\d{1,2}[\s\-]?\d{2,3}[\s\-]?\d{2}[\s\-]?\d{2}\b`),
|
|
194
259
|
p(EntityType.PHONE, String.raw `\+31\s?[1-9]\d{0,2}[\s\-]?\d{3,4}[\s\-]?\d{3,4}\b`),
|
|
195
|
-
p(EntityType.PASSPORT, String.raw `\b[A-Z][A-Z0-9]{8}\b`, null, "",
|
|
260
|
+
p(EntityType.PASSPORT, String.raw `\b[A-Z][A-Z0-9]{8}\b`, null, "", PASSPORT_CONTEXT, true),
|
|
196
261
|
p(EntityType.LICENSE_PLATE, String.raw `\b(?:[A-Z]{2}[\-\s]?\d{3}[\-\s]?[A-Z]|\d[\-\s]?[A-Z]{3}[\-\s]?\d{2}|\d{2}[\-\s]?[A-Z]{3}[\-\s]?\d|[A-Z]{2}[\-\s]?\d{2}[\-\s]?[A-Z]{2}|\d{2}[\-\s]?[A-Z]{2}[\-\s]?\d{2}|\d[\-\s]?[A-Z]{2}[\-\s]?\d{3})\b`),
|
|
197
262
|
p(EntityType.POSTAL_CODE, String.raw `\b[1-9]\d{3}\s?[A-Z]{2}\b`),
|
|
198
263
|
],
|
|
@@ -207,7 +272,7 @@ const BE = {
|
|
|
207
272
|
p(EntityType.CHAMBER_OF_COMMERCE, String.raw `\b0\d{3}\.?\d{3}\.?\d{3}\b`, null, "", ["KBO", "BCE", "ondernemingsnummer", "numéro d'entreprise", "enterprise number", "bedrijfsnummer", "Kruispuntbank", "Banque-Carrefour", "Banque Carrefour", "Ondernemingen", "Entreprises", "onder nummer"], true),
|
|
208
273
|
p(EntityType.PHONE, String.raw `\b0[1-9]\d{0,2}[/\s.\-]?\d{2,3}[.\s\-]?\d{2,3}[.\s\-]?\d{2,3}\b`),
|
|
209
274
|
p(EntityType.PHONE, String.raw `\+32\s?\d{1,3}[\s.\-]?\d{2,3}[\s.\-]?\d{2}[\s.\-]?\d{2}`),
|
|
210
|
-
p(EntityType.PASSPORT, String.raw `\b[A-Z]{2}\d{6}\b`, null, "",
|
|
275
|
+
p(EntityType.PASSPORT, String.raw `\b[A-Z]{2}\d{6}\b`, null, "", PASSPORT_CONTEXT, true),
|
|
211
276
|
p(EntityType.DRIVERS_LICENSE, String.raw `\b\d{10}\b`, null, "", ["rijbewijs", "permis de conduire", "driving licence", "driving license", "rijbewijsnummer"], true),
|
|
212
277
|
p(EntityType.LICENSE_PLATE, String.raw `\b[12][\-\s]?[A-Z]{3}[\-\s]?\d{3}\b`),
|
|
213
278
|
p(EntityType.POSTAL_CODE, String.raw `\b[1-9]\d{3}\b`, null, "", ["postcode", "code postal", "postnummer", "postal code", "zip", "B-", "adres", "adresse", "wonende", "woonplaats", "rue", "straat", "laan", "avenue", "boulevard", "plein", "steenweg", "chaussée", "domicilié", "gedomicilieerd", "Levering:", "siège"], true),
|
|
@@ -221,12 +286,12 @@ const DE = {
|
|
|
221
286
|
patterns: [
|
|
222
287
|
p(EntityType.TAX_ID, String.raw `\b[1-9]\d{10}\b`, "german_tax_id"),
|
|
223
288
|
p(EntityType.TAX_ID, String.raw `\b[1-9]\d[\s.]?\d{3}[\s.]?\d{3}[\s.]?\d{3}\b`, "german_tax_id"),
|
|
224
|
-
p(EntityType.NATIONAL_ID, String.raw `\b[CFGHJKLMNPRTVWXYZ][0-
|
|
289
|
+
p(EntityType.NATIONAL_ID, String.raw `\b[CFGHJKLMNPRTVWXYZ][0-9CFGHJKLMNPRTVWXYZ]{8}\d?\b`, null, "", ["Personalausweis", "Personalausweisnummer", "Ausweis", "Ausweisnummer", "Ausweis-Nr", "identity card", "Identitätskarte", "Perso", "PA-Nummer"], true),
|
|
225
290
|
p(EntityType.IBAN, String.raw `\bDE\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{2}\b`, "iban"),
|
|
226
291
|
p(EntityType.VAT, String.raw `\bDE\s?\d{9}\b`, "vat_de"),
|
|
227
292
|
p(EntityType.PHONE, String.raw `\b0[1-9]\d{1,4}[\s/\-]?\d{3,8}\b`),
|
|
228
293
|
p(EntityType.PHONE, String.raw `\+49\s?\d{2,5}[\s/\-]?\d{3,8}\b`),
|
|
229
|
-
p(EntityType.PASSPORT, String.raw `\b[CFGHJK][0-
|
|
294
|
+
p(EntityType.PASSPORT, String.raw `\b[CFGHJK][0-9CFGHJKLMNPRTVWXYZ]{8}\d?\b`, null, "", PASSPORT_CONTEXT, true),
|
|
230
295
|
// The separator after the city code is mandatory. When it was optional, a
|
|
231
296
|
// contiguous letter run split across both groups and the hyphen was
|
|
232
297
|
// consumed as the *second* separator, so any "LETTERS-DIGITS" token
|
|
@@ -287,7 +352,7 @@ const FR = {
|
|
|
287
352
|
p(EntityType.VAT, String.raw `\bFR\s?[0-9A-HJ-NP-Z]{2}\s?\d{9}\b`, "vat_fr"),
|
|
288
353
|
p(EntityType.PHONE, String.raw `\b0[1-9][\s.\-]?\d{2}[\s.\-]?\d{2}[\s.\-]?\d{2}[\s.\-]?\d{2}\b`),
|
|
289
354
|
p(EntityType.PHONE, String.raw `\+33\s?[1-9][\s.\-]?\d{2}[\s.\-]?\d{2}[\s.\-]?\d{2}[\s.\-]?\d{2}`),
|
|
290
|
-
p(EntityType.PASSPORT, String.raw `\b\d{2}[A-Z]{2}\d{5}\b`, null, "",
|
|
355
|
+
p(EntityType.PASSPORT, String.raw `\b\d{2}[A-Z]{2}\d{5}\b`, null, "", PASSPORT_CONTEXT, true),
|
|
291
356
|
p(EntityType.LICENSE_PLATE, String.raw `\b[A-Z]{2}[\-\s]?\d{3}[\-\s]?[A-Z]{2}\b`),
|
|
292
357
|
p(EntityType.POSTAL_CODE, String.raw `\b(?:0[1-9]|[1-8]\d|9[0-5]|97[1-6])\d{3}\b`, null, "", ["code postal", "CP", "postal code", "postcode", "adresse", "domicilié", "résidant", "rue", "avenue", "boulevard", "place", "chemin", "allée", "impasse", "ville"], true),
|
|
293
358
|
p(EntityType.CHAMBER_OF_COMMERCE, String.raw `\b\d{3}\s?\d{3}\s?\d{3}\b`, null, "", ["SIREN", "siren", "RCS", "entreprise", "immatricul", "numéro d'entreprise", "registre du commerce"], true),
|
package/dist/esm/rules/cues.d.ts
CHANGED
|
@@ -64,8 +64,20 @@ import { EntityType } from "../types.js";
|
|
|
64
64
|
* How far back a cue may sit. Short on purpose: a cue is only evidence about
|
|
65
65
|
* the value it introduces, which is the lesson of the postal-code year defect,
|
|
66
66
|
* where a cue 150 characters away licensed every number in the window.
|
|
67
|
+
*
|
|
68
|
+
* This bounds how long a *label* may be, not how far it may sit from the
|
|
69
|
+
* value: every pattern is anchored with `$`, so the label still has to reach
|
|
70
|
+
* the span through a run-on or one qualifier word.
|
|
71
|
+
*
|
|
72
|
+
* 32 was too short for German tax labels, the longest the engine carries.
|
|
73
|
+
* "Steuerliche Identifikationsnummer " is 34 characters and
|
|
74
|
+
* "Steueridentifikationsnummer lautet: " is 36, so the label's own start sat
|
|
75
|
+
* outside the window and `(?<![A-Za-z0-9_])` had nothing to anchor against,
|
|
76
|
+
* leaving a critical-tier identifier in the clear (rules-engine#26). 44
|
|
77
|
+
* admits the longest with a qualifier: "Steuerliche Identifikationsnummer
|
|
78
|
+
* lautet: " is 42.
|
|
67
79
|
*/
|
|
68
|
-
export declare const CUE_WINDOW =
|
|
80
|
+
export declare const CUE_WINDOW = 44;
|
|
69
81
|
/**
|
|
70
82
|
* (entity type, label pattern). Ordered: the first match wins, so the narrower
|
|
71
83
|
* label comes first where two could read the same text.
|
package/dist/esm/rules/cues.js
CHANGED
|
@@ -64,8 +64,20 @@ import { EntityType } from "../types.js";
|
|
|
64
64
|
* How far back a cue may sit. Short on purpose: a cue is only evidence about
|
|
65
65
|
* the value it introduces, which is the lesson of the postal-code year defect,
|
|
66
66
|
* where a cue 150 characters away licensed every number in the window.
|
|
67
|
+
*
|
|
68
|
+
* This bounds how long a *label* may be, not how far it may sit from the
|
|
69
|
+
* value: every pattern is anchored with `$`, so the label still has to reach
|
|
70
|
+
* the span through a run-on or one qualifier word.
|
|
71
|
+
*
|
|
72
|
+
* 32 was too short for German tax labels, the longest the engine carries.
|
|
73
|
+
* "Steuerliche Identifikationsnummer " is 34 characters and
|
|
74
|
+
* "Steueridentifikationsnummer lautet: " is 36, so the label's own start sat
|
|
75
|
+
* outside the window and `(?<![A-Za-z0-9_])` had nothing to anchor against,
|
|
76
|
+
* leaving a critical-tier identifier in the clear (rules-engine#26). 44
|
|
77
|
+
* admits the longest with a qualifier: "Steuerliche Identifikationsnummer
|
|
78
|
+
* lautet: " is 42.
|
|
67
79
|
*/
|
|
68
|
-
export const CUE_WINDOW =
|
|
80
|
+
export const CUE_WINDOW = 44;
|
|
69
81
|
/**
|
|
70
82
|
* (entity type, label pattern). Ordered: the first match wins, so the narrower
|
|
71
83
|
* label comes first where two could read the same text.
|
|
@@ -88,8 +100,15 @@ export const CUES = [
|
|
|
88
100
|
/(?<![A-Za-z0-9_])(?:ssn|social\s*security|sozialversicherungsnummer|svnr|sofi)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
89
101
|
// Tax number. Greece's ΑΦΜ is issued by the tax authority; the identity-card
|
|
90
102
|
// number is the ΑΔΤ, above.
|
|
103
|
+
//
|
|
104
|
+
// The German long forms are spelled out rather than left to the run-on after
|
|
105
|
+
// `steuer-?id`. A cue may use its run-on *or* one qualifier word, never both,
|
|
106
|
+
// and "Steuer-IdNr lautet:" needs both: the run-on for "Nr" and the qualifier
|
|
107
|
+
// for "lautet". Naming the whole label leaves the qualifier free.
|
|
108
|
+
// "Steuerliche Identifikationsnummer" matched nothing at all — it is two
|
|
109
|
+
// words, and `steuer-?id` cannot reach across "liche " (rules-engine#26).
|
|
91
110
|
[EntityType.TAX_ID,
|
|
92
|
-
/(?<![A-Za-z0-9_])(?:αφμ|α\.φ\.μ\.|steuer-?id|steuernummer|st\.?-?nr|stnr|tin|finanzamt\s+ist|tax\s*(?:id|no|number)|daňové\s*číslo|numer\s*podatkowy)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
111
|
+
/(?<![A-Za-z0-9_])(?:αφμ|α\.φ\.μ\.|steuerliche\s*identifikations-?nummer|steuer-?identifikations-?nummer|steuer-?id-?nr\.?|steuer-?id|steuernummer|st\.?-?nr|stnr|tin|finanzamt\s+ist|tax\s*(?:id|no|number)|daňové\s*číslo|numer\s*podatkowy)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
93
112
|
// Health insurance: the insured person.
|
|
94
113
|
[EntityType.HEALTH_INSURANCE,
|
|
95
114
|
/(?<![A-Za-z0-9_])(?:versichertennummer|krankenversicherung|kvnr|kv-?nr|mutualité|ziekenfonds|aansluitingsnummer|medical\s*card(?:\s*(?:no|number))?)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
package/dist/esm/sdk.d.ts
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
import { DocumentContext } from "./rules/context.js";
|
|
2
|
-
import { type RedactResult } from "./types.js";
|
|
2
|
+
import { type Detection, type RedactResult } from "./types.js";
|
|
3
|
+
/** Returns the label that replaces `det`; `slice` is the exact text it covers. */
|
|
4
|
+
type LabelFor = (det: Detection, slice: string) => string;
|
|
5
|
+
/**
|
|
6
|
+
* Splice a label over every detection and return the masked text.
|
|
7
|
+
*
|
|
8
|
+
* `detections` must be sorted by `(start, -end)`.
|
|
9
|
+
*
|
|
10
|
+
* Labels are resolved right-to-left because the referential mapper numbers
|
|
11
|
+
* each entity type in call order, and that order is part of the output
|
|
12
|
+
* contract. The string itself is then assembled in a single forward pass:
|
|
13
|
+
* rebuilding it per detection copied the whole document each time, which is
|
|
14
|
+
* O(document x detections) — 1.68 s of pure copying on a 1 MB document with
|
|
15
|
+
* 15,000 detections, versus 2 ms here.
|
|
16
|
+
*
|
|
17
|
+
* Spans from the rule engine are deduplicated and non-overlapping. Spans from
|
|
18
|
+
* elsewhere (the cloud service) are only sorted, so a span may start behind
|
|
19
|
+
* the cursor. Its uncovered tail is still masked rather than dropped: dropping
|
|
20
|
+
* the span would leave those characters in the clear, and splicing it whole
|
|
21
|
+
* would corrupt the label already emitted over its head.
|
|
22
|
+
*/
|
|
23
|
+
export declare function applyReplacements(text: string, detections: Detection[], labelFor: LabelFor): string;
|
|
24
|
+
/**
|
|
25
|
+
* Characters a token suffix is drawn from. No vowels, so a suffix never spells
|
|
26
|
+
* a word; no 0/1/I/O, so it survives being read back by a person; no
|
|
27
|
+
* underscore, so the type prefix stays unambiguous.
|
|
28
|
+
*/
|
|
29
|
+
export declare const TOKEN_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
30
|
+
export declare const TOKEN_SUFFIX_LENGTH = 4;
|
|
31
|
+
/**
|
|
32
|
+
* Maps each distinct PII value in one call to a reversible token.
|
|
33
|
+
*
|
|
34
|
+
* A token is `TYPE_XXXX`: the entity type, then `TOKEN_SUFFIX_LENGTH` random
|
|
35
|
+
* characters from `TOKEN_ALPHABET`. The same value gets the same token within
|
|
36
|
+
* the call, so relationships survive; across calls it gets a different one, so
|
|
37
|
+
* two tokenized documents never reveal a shared value.
|
|
38
|
+
*
|
|
39
|
+
* Nothing is kept beyond the call. The mapping goes to the caller in
|
|
40
|
+
* `RedactResult.tokens`, and only the caller can turn it back into text with
|
|
41
|
+
* `restore()`.
|
|
42
|
+
*
|
|
43
|
+
* Tokens are also kept clear of any token-shaped string already in the
|
|
44
|
+
* document. A caller that redacts an LLM's reply to a tokenized prompt has
|
|
45
|
+
* exactly such a document, and a fresh token colliding with an old one would
|
|
46
|
+
* make `restore()` put the wrong value back.
|
|
47
|
+
*/
|
|
48
|
+
export declare class TokenMapper {
|
|
49
|
+
private byValue;
|
|
50
|
+
private taken;
|
|
51
|
+
/** Token -> original value, for `RedactResult.tokens`. */
|
|
52
|
+
readonly tokens: Record<string, string>;
|
|
53
|
+
constructor(text: string, detections: Detection[]);
|
|
54
|
+
/** Return the token for `value`, minting one on first sight. */
|
|
55
|
+
getToken: (det: Detection, value: string) => string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Put the original values back into `text`.
|
|
59
|
+
*
|
|
60
|
+
* `tokens` is `RedactResult.tokens` from the `redact(text, { tokenize: true })`
|
|
61
|
+
* call that produced the text this one derives from — typically the reply an
|
|
62
|
+
* LLM wrote to the tokenized prompt. Every occurrence of every token is
|
|
63
|
+
* replaced; a token an LLM glued to other characters (`EMAIL_K7Q2s`) is still
|
|
64
|
+
* restored, since leaving a token behind is the worse failure.
|
|
65
|
+
*/
|
|
66
|
+
export declare function restore(text: string, tokens: Record<string, string>): string;
|
|
3
67
|
export interface RedactOptions {
|
|
4
68
|
/**
|
|
5
69
|
* Scope. Detections attributed elsewhere are flagged `outOfScope`, never
|
|
@@ -24,6 +88,22 @@ export interface RedactOptions {
|
|
|
24
88
|
chunkOffset?: number;
|
|
25
89
|
mode?: string;
|
|
26
90
|
referentialIntegrity?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Replace each value with a reversible token (`EMAIL_K7Q2`) and return the
|
|
93
|
+
* token -> value mapping in `RedactResult.tokens`; see `restore()`. Tokens
|
|
94
|
+
* are unique to the call. Cannot be combined with `referentialIntegrity`.
|
|
95
|
+
*/
|
|
96
|
+
tokenize?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Values never to redact, matched whole and case-insensitively against each
|
|
99
|
+
* detection. Merged with the instance's allowlist.
|
|
100
|
+
*/
|
|
101
|
+
allowlist?: string[] | null;
|
|
102
|
+
/**
|
|
103
|
+
* Domains whose addresses are never redacted, e.g. `["acme.be"]`. Applies to
|
|
104
|
+
* EMAIL and URL only, and covers subdomains. Merged with the instance list.
|
|
105
|
+
*/
|
|
106
|
+
allowlistDomains?: string[] | null;
|
|
27
107
|
detectDates?: boolean;
|
|
28
108
|
cache?: boolean;
|
|
29
109
|
}
|
|
@@ -32,13 +112,52 @@ export declare class EuRedact {
|
|
|
32
112
|
private cache;
|
|
33
113
|
private referentialMapper;
|
|
34
114
|
private maxInputLength;
|
|
115
|
+
private allowlist;
|
|
116
|
+
private allowlistDomains;
|
|
117
|
+
/**
|
|
118
|
+
* @param options.maxInputLength Longest document `redact` accepts, in characters.
|
|
119
|
+
* @param options.allowlist Values never to redact, for every call on this
|
|
120
|
+
* instance — an organisation's own name, its own addresses. Merged with
|
|
121
|
+
* the per-call `allowlist`. Matched whole, case-insensitively.
|
|
122
|
+
*/
|
|
35
123
|
constructor(options?: {
|
|
36
124
|
maxInputLength?: number;
|
|
125
|
+
allowlist?: string[] | null;
|
|
126
|
+
allowlistDomains?: string[] | null;
|
|
37
127
|
});
|
|
38
128
|
addCustomPattern(name: string, pattern: string): void;
|
|
39
129
|
/** Clear the result cache and referential integrity mappings.
|
|
40
130
|
* Call this in long-running processes to free PII from memory. */
|
|
41
131
|
clear(): void;
|
|
132
|
+
/** The instance allowlist merged with a call's, in comparison form. */
|
|
133
|
+
private allowedFor;
|
|
134
|
+
/** The instance domain list merged with a call's. */
|
|
135
|
+
private domainsFor;
|
|
136
|
+
/** The label function for one call, given its output options. */
|
|
137
|
+
private labelFor;
|
|
138
|
+
/**
|
|
139
|
+
* Send a document to the cloud tier.
|
|
140
|
+
*
|
|
141
|
+
* Options the service cannot honour throw rather than being ignored. Silently
|
|
142
|
+
* dropping one would mean returning a result that does not match what was
|
|
143
|
+
* asked for — which, for anything that changes which spans come back, is
|
|
144
|
+
* under-redaction wearing a plausible face.
|
|
145
|
+
*/
|
|
146
|
+
private redactViaCloud;
|
|
147
|
+
/**
|
|
148
|
+
* Rebuild the masked text from the service's spans, in place.
|
|
149
|
+
*
|
|
150
|
+
* The service masks with bracketed labels only. Any other output option is
|
|
151
|
+
* applied here, from its spans: the service builds its `redacted_text` from
|
|
152
|
+
* exactly the spans it returns (entities it reports but cannot place are
|
|
153
|
+
* listed separately and never applied), so nothing it masked is lost by
|
|
154
|
+
* masking again from the same spans.
|
|
155
|
+
*
|
|
156
|
+
* The service reports offsets in code points and this SDK slices UTF-16
|
|
157
|
+
* units, so the slice check is what stands between an emoji in the document
|
|
158
|
+
* and a rebuilt text with the wrong characters masked.
|
|
159
|
+
*/
|
|
160
|
+
private remaskCloudResult;
|
|
42
161
|
redact(text: string, options?: RedactOptions): RedactResult;
|
|
43
162
|
/**
|
|
44
163
|
* Redact, awaiting the cloud tier when `mode: "cloud"` is asked for.
|
|
@@ -49,3 +168,4 @@ export declare class EuRedact {
|
|
|
49
168
|
redactAsync(text: string, options?: RedactOptions): Promise<RedactResult>;
|
|
50
169
|
redactBatch(texts: string[], options?: RedactOptions): RedactResult[];
|
|
51
170
|
}
|
|
171
|
+
export {};
|