euredact 0.3.8 → 0.4.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 +114 -13
- package/dist/cjs/cloud/client.js +219 -0
- package/dist/cjs/cloud/config.js +57 -0
- package/dist/cjs/cloud/errors.js +55 -0
- package/dist/cjs/cloud/index.js +31 -0
- package/dist/cjs/index.js +22 -3
- package/dist/cjs/rules/cues.js +101 -29
- package/dist/cjs/rules/engine.js +51 -4
- package/dist/cjs/rules/suppressors.js +48 -1
- package/dist/cjs/sdk.js +92 -0
- package/dist/cjs/types.js +45 -1
- package/dist/esm/cloud/client.d.ts +42 -0
- package/dist/esm/cloud/client.js +215 -0
- package/dist/esm/cloud/config.d.ts +43 -0
- package/dist/esm/cloud/config.js +51 -0
- package/dist/esm/cloud/errors.d.ts +32 -0
- package/dist/esm/cloud/errors.js +48 -0
- package/dist/esm/cloud/index.d.ts +19 -0
- package/dist/esm/cloud/index.js +19 -0
- package/dist/esm/index.d.ts +9 -1
- package/dist/esm/index.js +11 -1
- package/dist/esm/rules/cues.d.ts +42 -12
- package/dist/esm/rules/cues.js +100 -28
- package/dist/esm/rules/engine.d.ts +32 -0
- package/dist/esm/rules/engine.js +51 -5
- package/dist/esm/rules/suppressors.js +48 -1
- package/dist/esm/sdk.d.ts +7 -0
- package/dist/esm/sdk.js +59 -0
- package/dist/esm/types.d.ts +33 -1
- package/dist/esm/types.js +44 -1
- package/package.json +4 -3
package/dist/cjs/rules/cues.js
CHANGED
|
@@ -34,18 +34,34 @@
|
|
|
34
34
|
* the *tail* of an ordinary word. "NIS" matched "tālru**nis**", the Latvian
|
|
35
35
|
* for telephone, and suppressed 653 real phone numbers; "iva" matched
|
|
36
36
|
* "Pr**iva**t:" and read a personal e-mail address as a VAT number.
|
|
37
|
-
* *
|
|
38
|
-
*
|
|
37
|
+
* * Neither the qualifier nor the run-on may use `\w`. The qualifier is a
|
|
38
|
+
* negated ASCII class, fixed for this in 0.3.8; `[^\W\d_]` would reject
|
|
39
|
+
* "εταιρειας" here for the same ASCII reason. The run-on was *not* fixed then
|
|
40
|
+
* and went on diverging for another release: Slovak "telefón 0956550012"
|
|
41
|
+
* needs it to absorb "efón" after `tel`, and Bulgarian
|
|
42
|
+
* "пощенски кодове 4000-4999" needs "ове" after `пощенски код`. Python's
|
|
43
|
+
* Unicode `\w` did; this SDK's ASCII `\w` did not, so one engine said `PHONE`
|
|
44
|
+
* and the other `NATIONAL_ID` on the same document. They were the last two
|
|
45
|
+
* type divergences on the corpus, and only the type-aware half of
|
|
46
|
+
* `make parity` could see them.
|
|
39
47
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
48
|
+
* The run-on is spelled `(?:[A-Za-z0-9_]|[^\x00-\x7F])` — an ASCII word
|
|
49
|
+
* character, or any non-ASCII character — rather than as a negated class of
|
|
50
|
+
* separators. A negated class also admits `'`, `%`, `&`, `@` and the rest of
|
|
51
|
+
* ASCII punctuation, which `\w` never did, and widening it that far was
|
|
52
|
+
* measured at 46 extra false positives with hints and 57 blind over the
|
|
53
|
+
* 152,300-document corpus. This is `\w` plus the non-ASCII letters `\w`
|
|
54
|
+
* should have matched all along, and nothing else.
|
|
55
|
+
*
|
|
56
|
+
* Every label ends in `(?:<run-on>|\s+<one word>)`: the label either runs on
|
|
57
|
+
* into a longer word ("telefoon" → "telefoonnummer") **or** takes one qualifier
|
|
58
|
+
* word naming whose identifier it is ("ΑΦΜ εταιρειας:", "Tel Nr:") — never both.
|
|
43
59
|
* Allowing both let "nie" grow into the surname "Nieminen" and then swallow the
|
|
44
60
|
* real label in "Matti Nieminen\nDOB: 08.05.1994", reading a date of birth as a
|
|
45
61
|
* Spanish NIE: 52 false positives across the corpus.
|
|
46
62
|
*/
|
|
47
63
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
-
exports.CUE_TARGETS = exports.CUES = exports.CUE_WINDOW = void 0;
|
|
64
|
+
exports.RESCUE_TARGETS = exports.CUE_TARGETS = exports.CUES = exports.CUE_WINDOW = void 0;
|
|
49
65
|
exports.cuedType = cuedType;
|
|
50
66
|
exports.retypedBy = retypedBy;
|
|
51
67
|
const types_js_1 = require("../types.js");
|
|
@@ -54,7 +70,7 @@ const types_js_1 = require("../types.js");
|
|
|
54
70
|
* the value it introduces, which is the lesson of the postal-code year defect,
|
|
55
71
|
* where a cue 150 characters away licensed every number in the window.
|
|
56
72
|
*/
|
|
57
|
-
exports.CUE_WINDOW =
|
|
73
|
+
exports.CUE_WINDOW = 32;
|
|
58
74
|
/**
|
|
59
75
|
* (entity type, label pattern). Ordered: the first match wins, so the narrower
|
|
60
76
|
* label comes first where two could read the same text.
|
|
@@ -64,46 +80,63 @@ exports.CUES = [
|
|
|
64
80
|
// front of a span that no phone pattern matched says the document is laid out
|
|
65
81
|
// unusually, not that the value is a phone number.
|
|
66
82
|
[types_js_1.EntityType.PHONE,
|
|
67
|
-
/(?<![A-Za-z0-9_])(?:phone|tel|telephone|téléphone|telefoon|telefon|telefono|teléfono|tlf|mobil|mobile|gsm|handy|sími|puh|móvil|cell|tālrunis|τηλέφωνο|τηλ)(
|
|
83
|
+
/(?<![A-Za-z0-9_])(?:phone|tel|telephone|téléphone|telefoon|telefon|telefono|teléfono|tlf|mobil|mobile|gsm|handy|sími|puh|móvil|cell|tālrunis|τηλέφωνο|τηλ)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
68
84
|
// National identity number.
|
|
69
85
|
[types_js_1.EntityType.NATIONAL_ID,
|
|
70
|
-
/(?<![A-Za-z0-9_])(?:bsn|personnummer|rijksregisternummer|nationaal\s*nummer|numéro\s*national|national\s*(?:id|number)|identiteitsnummer|henkilötunnus|kennitala|cpr(?:-?nummer)?|nif|nie|dni|rr|nn|nir|insz|niss|nis|matricule|ausweisnummer|personalausweis|osobní\s*číslo|ЕГН|EGN)(
|
|
86
|
+
/(?<![A-Za-z0-9_])(?:bsn|burgerservicenummer(?:\s*\(bsn\))?|personnummer|rijksregisternummer|nationaal\s*nummer|numéro\s*national|national\s*(?:id|number)|identiteitsnummer|henkilötunnus|kennitala|cpr(?:-?nummer)?|nif|nie|dni|rr|nn|nir|insz|niss|nis|matricule|ausweisnummer|personalausweis|osobní\s*číslo|ЕГН|EGN)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
71
87
|
// Cyprus: "ID number" in both official languages, plus the Greek abbreviation
|
|
72
88
|
// ΑΔΤ (Αστυνομική Ταυτότητα). Neither has a phone reading.
|
|
73
89
|
[types_js_1.EntityType.NATIONAL_ID,
|
|
74
|
-
/(?<![A-Za-z0-9_])(?:αρ\.?\s*ταυτότητας|ταυτότητας|αδτ|kimlik\s*numaras[ıi])(
|
|
90
|
+
/(?<![A-Za-z0-9_])(?:αρ\.?\s*ταυτότητας|ταυτότητας|αδτ|kimlik\s*numaras[ıi])(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
75
91
|
// Social security.
|
|
76
92
|
[types_js_1.EntityType.SSN,
|
|
77
|
-
/(?<![A-Za-z0-9_])(?:ssn|social\s*security|sozialversicherungsnummer|svnr|sofi)(
|
|
93
|
+
/(?<![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],
|
|
78
94
|
// Tax number. Greece's ΑΦΜ is issued by the tax authority; the identity-card
|
|
79
|
-
// number is the ΑΔΤ, above.
|
|
80
|
-
// cued ΑΦΜ, which is wrong independently of the phone problem.
|
|
95
|
+
// number is the ΑΔΤ, above.
|
|
81
96
|
[types_js_1.EntityType.TAX_ID,
|
|
82
|
-
/(?<![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)(
|
|
83
|
-
// Health insurance.
|
|
97
|
+
/(?<![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],
|
|
98
|
+
// Health insurance: the insured person.
|
|
84
99
|
[types_js_1.EntityType.HEALTH_INSURANCE,
|
|
85
|
-
/(?<![A-Za-z0-9_])(?:versichertennummer|krankenversicherung|kvnr|kv-?nr|mutualité|ziekenfonds|aansluitingsnummer)(
|
|
86
|
-
//
|
|
100
|
+
/(?<![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],
|
|
101
|
+
// Healthcare provider: the clinician or practice, not the patient — the Dutch
|
|
102
|
+
// AGB-code, the German LANR, the UK GMC number.
|
|
103
|
+
[types_js_1.EntityType.HEALTHCARE_PROVIDER,
|
|
104
|
+
/(?<![A-Za-z0-9_])(?:agb-?code|lanr|bsnr|gmc(?:\s*(?:no|number|reg))?|big-?nummer|zorgverlener(?:snummer)?)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
105
|
+
// Company / trade register. "Companies House" and "Company Registration
|
|
106
|
+
// Number" are written out: the tail allows a run-on *or* one qualifier word,
|
|
107
|
+
// never a second word, so a three-word label needs to be spelled.
|
|
87
108
|
[types_js_1.EntityType.CHAMBER_OF_COMMERCE,
|
|
88
|
-
/(?<![A-Za-z0-9_])(?:siren|siret|kbo|bce|kvk|ondernemingsnummer|ondernemingen\s+onder\s+nummer|kruispuntbank|numéro\s*d'entreprise|enterprise\s*number|handelsregister|company\s*(?:no|number|reg)|organisationsnummer|orgnr|i[čc]o|identifikační\s*číslo|ЕИК|eik|bulstat|cvr(?:-?nummer)?|virksomhedsnummer)(
|
|
109
|
+
/(?<![A-Za-z0-9_])(?:siren|siret|kbo|bce|kvk|ondernemingsnummer|ondernemingen\s+onder\s+nummer|kruispuntbank|numéro\s*d'entreprise|enterprise\s*number|handelsregister|companies\s*house(?:\s*(?:registration|reg|no|number))?|company\s*registration(?:\s*(?:number|no))?|company\s*(?:no|number|reg)|organisationsnummer|orgnr|i[čc]o|identifikační\s*číslo|ЕИК|eik|bulstat|cvr(?:-?nummer)?|virksomhedsnummer)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
89
110
|
// VAT.
|
|
90
111
|
[types_js_1.EntityType.VAT,
|
|
91
|
-
/(?<![A-Za-z0-9_])(?:vat|btw|tva|ust|iva|moms|alv|mwst|dič|vsk)(?:[
|
|
112
|
+
/(?<![A-Za-z0-9_])(?:vat|btw|tva|ust|iva|moms|alv|mwst|dič|vsk)(?:(?:[A-Za-z0-9_.\-]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
113
|
+
// Bank account. A UK sort code is NN-NN-NN, also a licence-plate shape, so
|
|
114
|
+
// "sort code 20-45-91" was typed LICENSE_PLATE with the label in front of it.
|
|
115
|
+
// Retype target only — a failed mod-97 is never rescued by a label.
|
|
116
|
+
[types_js_1.EntityType.BANK_ACCOUNT,
|
|
117
|
+
/(?<![A-Za-z0-9_])(?:sort\s*code|account\s*(?:no|number)|rekeningnummer|kontonummer|numéro\s*de\s*compte|sort-?code)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
118
|
+
// Passport.
|
|
119
|
+
[types_js_1.EntityType.PASSPORT,
|
|
120
|
+
/(?<![A-Za-z0-9_])(?:passport(?:\s*(?:no|number))?|paspoort(?:nummer)?|reisepass(?:nummer)?|passeport|passnummer)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
92
121
|
// BIC / SWIFT.
|
|
93
122
|
[types_js_1.EntityType.BIC,
|
|
94
|
-
/(?<![A-Za-z0-9_])(?:bic|swift)[
|
|
123
|
+
/(?<![A-Za-z0-9_])(?:bic|swift)(?:[A-Za-z0-9_\/]|[^\x00-\x7F])*\s*[:.\-]?\s*\(?\s*$/i],
|
|
95
124
|
// Postal code. A postal code loses every contested span on priority (it is
|
|
96
125
|
// the weakest evidence the engine has), so without a cue entry
|
|
97
126
|
// "Cod postal: 040171" was handed to a German phone pattern.
|
|
98
127
|
[types_js_1.EntityType.POSTAL_CODE,
|
|
99
|
-
/(?<![A-Za-z0-9_])(?:cod\s*po[sș]tal|postnummer|postnr|postleitzahl|plz
|
|
128
|
+
/(?<![A-Za-z0-9_])(?:cod\s*po[sș]tal|postnummer|postnr|postleitzahl|plz(?:-?bereiche?)?|postcode|code\s*postal|codice\s*postale|kod\s*pocztowy|ps[čc]|τ\.?κ\.?|пощенски\s*код)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
100
129
|
// Internal / employee identifier. The canon calls these INTERNAL_ID and
|
|
101
130
|
// annotates them with an LLM, so the engine has no pattern for them — but it
|
|
102
131
|
// does have a phone pattern that was happily claiming them. The cue is the
|
|
103
132
|
// only thing that identifies one, which is why the engine emits this type
|
|
104
133
|
// from here and nowhere else.
|
|
105
134
|
[types_js_1.EntityType.INTERNAL_ID,
|
|
106
|
-
/(?<![A-Za-z0-9_])(?:medarbejdernummer|ansattnummer|anställningsnummer|personalnummer|personeelsnummer|matricule\s*salarié|employee\s*(?:no|number|id)|badge
|
|
135
|
+
/(?<![A-Za-z0-9_])(?:medarbejdernummer|ansattnummer|anställningsnummer|personalnummer|personeelsnummer|matricule\s*salarié|employee\s*(?:no|number|id)|badge(?:\s*(?:no|number))?|betriebsstätten(?:nr|nummer)|grundstücks(?:nr|nummer)|dossier-?(?:nr|nummer)|aktenzeichen|osobní\s*(?:číslo\s*)?zaměstnance)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
136
|
+
// Credential. Typed SECRET because that is the type this SDK has; the
|
|
137
|
+
// training pipeline's report asks for CREDENTIAL, which does not exist here.
|
|
138
|
+
[types_js_1.EntityType.SECRET,
|
|
139
|
+
/(?<![A-Za-z0-9_])(?:tan-?activatiecode|activatiecode|activation\s*code|verificatiecode|verification\s*code|otp|pincode)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
107
140
|
];
|
|
108
141
|
/**
|
|
109
142
|
* The entity type a label immediately before `start` names, or null.
|
|
@@ -129,22 +162,56 @@ const RETYPABLE = new Set([
|
|
|
129
162
|
types_js_1.EntityType.PHONE, types_js_1.EntityType.POSTAL_CODE, types_js_1.EntityType.LICENSE_PLATE,
|
|
130
163
|
]);
|
|
131
164
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
165
|
+
* Retypable, but only when the document gives its country no support at all.
|
|
166
|
+
*
|
|
167
|
+
* These carry a checksum, so normally the pattern outranks any label. But a
|
|
168
|
+
* checksum only says the digits fit *some* national scheme, and a weak one fits
|
|
169
|
+
* by luck — the same reasoning that demotes an uncorroborated validated
|
|
170
|
+
* candidate to priority 1 in `engine.ts`. At a country score of 0.0 the engine
|
|
171
|
+
* is already saying "this passed a Czech checksum in a document with nothing
|
|
172
|
+
* Czech about it", and an explicit "Passport No.:" is the better evidence.
|
|
173
|
+
*
|
|
174
|
+
* Restricted to zero country support on purpose: a Dutch BSN in a Dutch
|
|
175
|
+
* document scores above zero and stays a NATIONAL_ID whatever label sits near
|
|
176
|
+
* it, so this cannot relabel a domestic identifier — only the coincidental
|
|
177
|
+
* foreign ones, which is where "Passport No.: 512847603" and
|
|
178
|
+
* "Betriebsstättennr. 260326822" both landed, each reported as Czech.
|
|
179
|
+
*/
|
|
180
|
+
const RETYPABLE_UNCORROBORATED = new Set([types_js_1.EntityType.NATIONAL_ID]);
|
|
181
|
+
/**
|
|
182
|
+
* Types a label can assert on its own.
|
|
135
183
|
*
|
|
136
184
|
* PHONE is absent on purpose: "Tel:" in front of something no phone pattern
|
|
137
185
|
* matched means the document is laid out oddly, not that the value is a phone
|
|
138
|
-
* number. So are EMAIL,
|
|
186
|
+
* number. So are EMAIL, DOB, VIN and the rest — those carry their own
|
|
139
187
|
* structure, and a nearby word is not entitled to overrule it.
|
|
188
|
+
*/
|
|
189
|
+
exports.CUE_TARGETS = new Set([
|
|
190
|
+
types_js_1.EntityType.NATIONAL_ID, types_js_1.EntityType.SSN, types_js_1.EntityType.TAX_ID,
|
|
191
|
+
types_js_1.EntityType.HEALTH_INSURANCE, types_js_1.EntityType.HEALTHCARE_PROVIDER,
|
|
192
|
+
types_js_1.EntityType.CHAMBER_OF_COMMERCE, types_js_1.EntityType.VAT, types_js_1.EntityType.POSTAL_CODE,
|
|
193
|
+
types_js_1.EntityType.PASSPORT, types_js_1.EntityType.INTERNAL_ID, types_js_1.EntityType.BANK_ACCOUNT,
|
|
194
|
+
types_js_1.EntityType.SECRET,
|
|
195
|
+
]);
|
|
196
|
+
/**
|
|
197
|
+
* Which of those a label may also *rescue* — re-admit after its checksum
|
|
198
|
+
* failed. Until 0.3.9 this was the same set as CUE_TARGETS and it cannot stay
|
|
199
|
+
* that way: re-typing a span the engine already decided to mask is cheap to be
|
|
200
|
+
* wrong about, while rescuing one asserts that a value which failed its check
|
|
201
|
+
* digit is a real identifier anyway.
|
|
140
202
|
*
|
|
141
203
|
* BIC's absence is what keeps the rescue honest. Its validator is a *registry
|
|
142
204
|
* lookup*, not a checksum, so a failure means "no such bank", which no label can
|
|
143
205
|
* talk you out of. Rescuing it read 34 unlisted codes behind a "BIC:" label as
|
|
144
206
|
* real ones. The same reasoning keeps CREDIT_CARD and BANK_ACCOUNT out: Luhn and
|
|
145
207
|
* mod-97 are strong enough that a failure really does mean "not one of these".
|
|
208
|
+
* BANK_ACCOUNT joined CUE_TARGETS in 0.3.9 so "sort code 20-45-91" stops being a
|
|
209
|
+
* LICENSE_PLATE, and is kept out here for exactly the 0.3.8 reason.
|
|
210
|
+
*
|
|
211
|
+
* HEALTHCARE_PROVIDER and SECRET are absent because neither is checksummed;
|
|
212
|
+
* there is no failed validation for a label to overrule in the first place.
|
|
146
213
|
*/
|
|
147
|
-
exports.
|
|
214
|
+
exports.RESCUE_TARGETS = new Set([
|
|
148
215
|
types_js_1.EntityType.NATIONAL_ID, types_js_1.EntityType.SSN, types_js_1.EntityType.TAX_ID,
|
|
149
216
|
types_js_1.EntityType.HEALTH_INSURANCE, types_js_1.EntityType.CHAMBER_OF_COMMERCE,
|
|
150
217
|
types_js_1.EntityType.VAT, types_js_1.EntityType.POSTAL_CODE, types_js_1.EntityType.PASSPORT,
|
|
@@ -157,9 +224,14 @@ exports.CUE_TARGETS = new Set([
|
|
|
157
224
|
* without them fired 52 times and got roughly 30 of those wrong — EMAIL read as
|
|
158
225
|
* VAT, a date of birth as a phone number, a VIN as VAT — against 20 real fixes.
|
|
159
226
|
* With them: 14 re-typings, every one of them PHONE to a structured identifier.
|
|
227
|
+
*
|
|
228
|
+
* `countryScore` is the document's support for the country the winning pattern
|
|
229
|
+
* came from; see RETYPABLE_UNCORROBORATED.
|
|
160
230
|
*/
|
|
161
|
-
function retypedBy(text, start, entityType) {
|
|
162
|
-
|
|
231
|
+
function retypedBy(text, start, entityType, countryScore) {
|
|
232
|
+
const type = String(entityType);
|
|
233
|
+
if (!RETYPABLE.has(type)
|
|
234
|
+
&& !(RETYPABLE_UNCORROBORATED.has(type) && countryScore === 0))
|
|
163
235
|
return null;
|
|
164
236
|
const cued = cuedType(text, start);
|
|
165
237
|
if (cued === null || cued === entityType || !exports.CUE_TARGETS.has(String(cued)))
|
package/dist/cjs/rules/engine.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.RuleEngine = exports.COUNTRY_CODE_ALIASES = void 0;
|
|
37
37
|
exports.resolveCountryCode = resolveCountryCode;
|
|
38
|
+
exports.tieKey = tieKey;
|
|
38
39
|
const types_js_1 = require("../types.js");
|
|
39
40
|
const matchers_js_1 = require("./matchers.js");
|
|
40
41
|
const suppressors_js_1 = require("./suppressors.js");
|
|
@@ -81,6 +82,48 @@ function resolveCountryCode(countryCode) {
|
|
|
81
82
|
code = exports.COUNTRY_CODE_ALIASES[code] ?? code;
|
|
82
83
|
return code in index_js_1.COUNTRY_CONFIGS && code !== "SHARED" ? code : null;
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Last-resort ordering for candidates every other field has tied on.
|
|
87
|
+
*
|
|
88
|
+
* Reaching this means the engine has no evidence left: same span, no cue, same
|
|
89
|
+
* priority, the same (usually zero) country score, and the same declared scope.
|
|
90
|
+
* Something still has to win, and before this the winner was whichever candidate
|
|
91
|
+
* happened to be generated first — which is country *registration* order, and
|
|
92
|
+
* the two SDKs do not share one. Python registers alphabetically
|
|
93
|
+
* (`AT, BE, BG, CH, CY, CZ, ...`); this SDK uses a curated order
|
|
94
|
+
* (`NL, BE, DE, AT, CH, FR, ..., PT, LU, PL, ..., CZ`). So a bare digit run that
|
|
95
|
+
* several national schemes accept was filed as a Czech `NATIONAL_ID` by Python
|
|
96
|
+
* and a Portuguese `PHONE` here — identical characters masked, contradictory
|
|
97
|
+
* labels, and `make parity` compared only characters, so it stayed green through
|
|
98
|
+
* the whole of 0.3.8.
|
|
99
|
+
*
|
|
100
|
+
* The order reproduces what Python already did rather than inventing a new one,
|
|
101
|
+
* because every accuracy figure this engine has published was measured with
|
|
102
|
+
* Python resolving these ties by its registration order. Ordering on the entity
|
|
103
|
+
* type instead was tried first and re-tuned the corpus twice over: it turned the
|
|
104
|
+
* German `Steuerß12345678911` from `TAX_ID`/DE into `NATIONAL_ID`/EL, and moved
|
|
105
|
+
* 820 further spans from `DOB` to `DATE_OF_DEATH`. So the key is deliberately
|
|
106
|
+
* not total — candidates from the same country are left to the stable sort,
|
|
107
|
+
* which preserves the order that country's pattern list was written in, and both
|
|
108
|
+
* SDKs write theirs in the same order.
|
|
109
|
+
*
|
|
110
|
+
* Taken from the **pattern**, never the finished detection: this SDK re-types a
|
|
111
|
+
* cued candidate before the sort and Python re-types after it, so keying on the
|
|
112
|
+
* final type would reintroduce the divergence in a subtler form.
|
|
113
|
+
*
|
|
114
|
+
* Kept character-for-character in step with `tie_key` in `rules/engine.py`.
|
|
115
|
+
*/
|
|
116
|
+
function tieKey(countryCode) {
|
|
117
|
+
// "SHARED" and "CUSTOM" are not countries and sort as themselves under a
|
|
118
|
+
// plain alphabetical key — landing between PT and SK, rather than ahead of
|
|
119
|
+
// every country the way the registry loads them. The bucket restores that.
|
|
120
|
+
const bucket = countryCode === "SHARED" ? "0" : countryCode === "CUSTOM" ? "1" : "2";
|
|
121
|
+
// NUL separator, written as an escape so this file stays plain ASCII, and
|
|
122
|
+
// matching Python byte for byte. It must sort below every character a code
|
|
123
|
+
// or type name can contain, or a code that is a prefix of another would
|
|
124
|
+
// order differently in the two SDKs.
|
|
125
|
+
return `${bucket}\u0000${countryCode}`;
|
|
126
|
+
}
|
|
84
127
|
const warnedCountries = new Set();
|
|
85
128
|
function warnUnknownCountry(code) {
|
|
86
129
|
if (warnedCountries.has(code))
|
|
@@ -240,7 +283,7 @@ class RuleEngine {
|
|
|
240
283
|
else if (m.patternDef.validator !== null && !m.patternDef.requiresContext) {
|
|
241
284
|
failedSpans.push({ start: m.start, end: m.end, code: m.countryCode,
|
|
242
285
|
etype: String(m.patternDef.entityType) });
|
|
243
|
-
if (cues_js_1.
|
|
286
|
+
if (cues_js_1.RESCUE_TARGETS.has(String(m.patternDef.entityType))
|
|
244
287
|
&& (0, cues_js_1.cuedType)(text, m.start) === m.patternDef.entityType)
|
|
245
288
|
rescued.push(m);
|
|
246
289
|
}
|
|
@@ -452,7 +495,7 @@ class RuleEngine {
|
|
|
452
495
|
// the value is filed as a Cypriot identity number. Dropping it also clears
|
|
453
496
|
// outOfScope, which would otherwise claim the detection belongs to a
|
|
454
497
|
// country the caller did not declare while naming no country at all.
|
|
455
|
-
const retyped = (0, cues_js_1.retypedBy)(text, match.start, entityType);
|
|
498
|
+
const retyped = (0, cues_js_1.retypedBy)(text, match.start, entityType, countryScores.get(match.countryCode) ?? 0);
|
|
456
499
|
if (retyped !== null) {
|
|
457
500
|
entityType = retyped;
|
|
458
501
|
country = null;
|
|
@@ -466,6 +509,7 @@ class RuleEngine {
|
|
|
466
509
|
length: match.end - match.start,
|
|
467
510
|
countryScore: countryScores.get(match.countryCode) ?? 0,
|
|
468
511
|
inScope: declared === null || declared.has(match.countryCode) ? 1 : 0,
|
|
512
|
+
tie: tieKey(match.countryCode),
|
|
469
513
|
tier: 0,
|
|
470
514
|
det: {
|
|
471
515
|
entityType,
|
|
@@ -504,6 +548,7 @@ class RuleEngine {
|
|
|
504
548
|
length: match.end - match.start,
|
|
505
549
|
countryScore: countryScores.get(match.countryCode) ?? 0,
|
|
506
550
|
inScope: declared === null || declared.has(match.countryCode) ? 1 : 0,
|
|
551
|
+
tie: tieKey(match.countryCode),
|
|
507
552
|
tier: 0,
|
|
508
553
|
det: {
|
|
509
554
|
entityType: match.patternDef.entityType,
|
|
@@ -519,7 +564,8 @@ class RuleEngine {
|
|
|
519
564
|
}
|
|
520
565
|
for (const d of (0, structural_js_1.detectStructuralDob)(text)) {
|
|
521
566
|
candidates.push({ blindPriority: 1, cue: 0, priority: 1, length: d.end - d.start,
|
|
522
|
-
countryScore: 0, inScope: 1, tier: 0, det: d
|
|
567
|
+
countryScore: 0, inScope: 1, tier: 0, det: d,
|
|
568
|
+
tie: tieKey(d.country ?? "") });
|
|
523
569
|
}
|
|
524
570
|
const detections = this.deduplicate(candidates);
|
|
525
571
|
detections.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
@@ -587,7 +633,8 @@ class RuleEngine {
|
|
|
587
633
|
b.cue - a.cue || // local cue } country-aware
|
|
588
634
|
b.priority - a.priority || // country priority } — decides the
|
|
589
635
|
b.countryScore - a.countryScore || // country evidence } LABEL only
|
|
590
|
-
b.inScope - a.inScope
|
|
636
|
+
b.inScope - a.inScope || // declared scope }
|
|
637
|
+
(a.tie < b.tie ? -1 : a.tie > b.tie ? 1 : 0)); // total } — see tieKey
|
|
591
638
|
const result = [];
|
|
592
639
|
const occupied = new Set();
|
|
593
640
|
for (const { det } of sorted) {
|
|
@@ -347,6 +347,53 @@ function suppressYearAsPostal(text, match) {
|
|
|
347
347
|
// Suppress: recent years without postal context are almost never postal codes
|
|
348
348
|
return true;
|
|
349
349
|
}
|
|
350
|
+
// ── Postal code: disqualified by the word in front of it ───────────────
|
|
351
|
+
//
|
|
352
|
+
// The mirror image of `rules/cues.ts`. There a label promotes a type; here a
|
|
353
|
+
// word refuses one. Both were reported together: a bare four-digit run becomes
|
|
354
|
+
// a POSTAL_CODE as soon as *any* real postal code establishes the country,
|
|
355
|
+
// which is to say in almost every real document, because POSTAL_CONTEXT_NEAR
|
|
356
|
+
// above then rescues it from suppressYearAsPostal. Measured on the training
|
|
357
|
+
// corpus: 55 bare years in prose ("Opgericht in 2016", "Fondée en 2017") plus
|
|
358
|
+
// telephone extensions ("(toest. 3841)", "(poste interne 3318)", "(ext. 2219)").
|
|
359
|
+
/**
|
|
360
|
+
* Prepositions that can only be temporal. Nothing is ever located "since 2018",
|
|
361
|
+
* so these disqualify a postal code on their own.
|
|
362
|
+
*/
|
|
363
|
+
const YEAR_WORD_BEFORE = /(?<![A-Za-z0-9_])(?:sinds|since|depuis|seit|siden|sedan|desde|vuonna|anno|dal)\s+$/i;
|
|
364
|
+
/**
|
|
365
|
+
* "in", "en" and "im" are *both* temporal and locative, and in exactly the
|
|
366
|
+
* countries whose postal codes are year-shaped. Belgian 2000 is Antwerp and
|
|
367
|
+
* 2018 is one of its districts, so "Rustige ligging in 2018, vlakbij openbaar
|
|
368
|
+
* vervoer" is an address, not a date — an earlier version of this rule
|
|
369
|
+
* suppressed it and lost a real postal code, which is the worse error for a
|
|
370
|
+
* redaction tool. So an ambiguous preposition needs a founding or payment
|
|
371
|
+
* participle in front of it, which is what every case in the report had:
|
|
372
|
+
* "Opgericht in 2016", "Fondée en 2017", "versé en 2025".
|
|
373
|
+
*/
|
|
374
|
+
const YEAR_VERB_BEFORE = /(?<![A-Za-z0-9_])(?:opgericht|gesticht|opgestart|founded|established|created|fond[ée]e?|cr[ée][ée]e?|gegründet|gegruendet|errichtet|grundlagt|grundad|perustettu|fundada|fundado|fondata|costituita|vers[ée]|betaald|uitbetaald|ausgezahlt|paid|geboren|born)\s+(?:in|en|im|op|the)\s+$/i;
|
|
375
|
+
/**
|
|
376
|
+
* A capitalised word immediately after keeps the candidate: "in 2000 Antwerpen"
|
|
377
|
+
* really is a postal code, and so is the "AZ" of a Dutch "1105 AZ". Without
|
|
378
|
+
* this the rule would suppress the very addresses it is meant to leave alone.
|
|
379
|
+
*/
|
|
380
|
+
const TOWN_AFTER = /^\s*[A-ZÀ-ÞĀ-Ž]/;
|
|
381
|
+
/**
|
|
382
|
+
* A telephone extension marker. "poste interne 3318" puts one qualifier word
|
|
383
|
+
* between the marker and the number, so allow exactly one.
|
|
384
|
+
*/
|
|
385
|
+
const EXTENSION_BEFORE = /(?<![A-Za-z0-9_])(?:toest(?:el)?|ext|extension|poste|durchwahl|doorkiesnummer|tst|nebenstelle)\.?\s*(?:[a-zà-ÿ]{2,10}\s*)?[:.]?\s*$/i;
|
|
386
|
+
/** Suppress a postal code the word in front of it rules out. */
|
|
387
|
+
function suppressPostalAfterDisqualifyingWord(text, match) {
|
|
388
|
+
if (match.patternDef.entityType !== types_js_1.EntityType.POSTAL_CODE)
|
|
389
|
+
return false;
|
|
390
|
+
const before = text.slice(Math.max(0, match.start - 24), match.start);
|
|
391
|
+
if (EXTENSION_BEFORE.test(before))
|
|
392
|
+
return true;
|
|
393
|
+
return RECENT_YEAR.test(match.text.trim())
|
|
394
|
+
&& (YEAR_WORD_BEFORE.test(before) || YEAR_VERB_BEFORE.test(before))
|
|
395
|
+
&& !TOWN_AFTER.test(text.slice(match.end, match.end + 3));
|
|
396
|
+
}
|
|
350
397
|
function suppressPhoneServiceNumber(_text, match) {
|
|
351
398
|
if (match.patternDef.entityType !== types_js_1.EntityType.PHONE)
|
|
352
399
|
return false;
|
|
@@ -796,7 +843,7 @@ const TYPE_SUPPRESSORS = {
|
|
|
796
843
|
[types_js_1.EntityType.NATIONAL_ID]: [suppressCurrency, suppressUnits, suppressReference, suppressLegal, suppressMath, suppressNatidAsPassport, suppressSeNatidAsOrg],
|
|
797
844
|
[types_js_1.EntityType.SSN]: [suppressCurrency, suppressUnits, suppressReference, suppressMath],
|
|
798
845
|
[types_js_1.EntityType.TAX_ID]: [suppressCurrency, suppressUnits, suppressReference, suppressMath, suppressTaxidAsIpAddress],
|
|
799
|
-
[types_js_1.EntityType.POSTAL_CODE]: [suppressCurrency, suppressUnits, suppressMath, suppressLegal, suppressYearAsPostal, suppressPostalInsideIban, suppressPostalAsHouseNumber, suppressPostalInLongerIdentifier, suppressReference],
|
|
846
|
+
[types_js_1.EntityType.POSTAL_CODE]: [suppressCurrency, suppressUnits, suppressMath, suppressLegal, suppressYearAsPostal, suppressPostalAfterDisqualifyingWord, suppressPostalInsideIban, suppressPostalAsHouseNumber, suppressPostalInLongerIdentifier, suppressReference],
|
|
800
847
|
[types_js_1.EntityType.BIC]: [suppressBicWithoutEvidence],
|
|
801
848
|
[types_js_1.EntityType.BANK_ACCOUNT]: [suppressReference],
|
|
802
849
|
[types_js_1.EntityType.LICENSE_PLATE]: [suppressPlateInCompound, suppressDePlateUnknownDistrict, suppressPlateAsCurrencyAmount],
|
package/dist/cjs/sdk.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.EuRedact = void 0;
|
|
4
37
|
const cache_js_1 = require("./cache.js");
|
|
@@ -48,6 +81,40 @@ class ReferentialMapper {
|
|
|
48
81
|
}
|
|
49
82
|
}
|
|
50
83
|
const DEFAULT_MAX_INPUT_LENGTH = 10_485_760; // ~10 MB of text
|
|
84
|
+
/**
|
|
85
|
+
* Send a document to the cloud tier.
|
|
86
|
+
*
|
|
87
|
+
* Options the service cannot honour throw rather than being ignored. Silently
|
|
88
|
+
* dropping one would mean returning a result that does not match what was
|
|
89
|
+
* asked for — which, for anything that changes which spans come back, is
|
|
90
|
+
* under-redaction wearing a plausible face.
|
|
91
|
+
*/
|
|
92
|
+
async function redactViaCloud(text, options) {
|
|
93
|
+
const { CloudClient } = await Promise.resolve().then(() => __importStar(require("./cloud/client.js")));
|
|
94
|
+
const countries = options.countries ?? null;
|
|
95
|
+
if (!countries || countries.length !== 1) {
|
|
96
|
+
throw new Error('cloud mode needs exactly one country, e.g. { countries: ["BE"] }. The ' +
|
|
97
|
+
"model is trained and evaluated per country, so a multi-country request " +
|
|
98
|
+
"has no defined behaviour.");
|
|
99
|
+
}
|
|
100
|
+
if (options.countryHint) {
|
|
101
|
+
throw new Error("countryHint is not supported in cloud mode");
|
|
102
|
+
}
|
|
103
|
+
if (options.context || options.chunkOffset) {
|
|
104
|
+
throw new Error("context/chunkOffset are not supported in cloud mode: the model has never " +
|
|
105
|
+
"seen a chunk boundary, so the service rejects oversized input rather " +
|
|
106
|
+
"than splitting it");
|
|
107
|
+
}
|
|
108
|
+
if (options.referentialIntegrity) {
|
|
109
|
+
throw new Error("referentialIntegrity is not supported in cloud mode");
|
|
110
|
+
}
|
|
111
|
+
// detectDates is deliberately NOT forwarded. The service always runs its
|
|
112
|
+
// rules engine with dates on, because that is what the model was trained
|
|
113
|
+
// against; the caller's value cannot change that. It is the one ignored
|
|
114
|
+
// option that is safe to ignore — it can only cause MORE to be detected,
|
|
115
|
+
// never less, so it cannot produce under-redaction.
|
|
116
|
+
return new CloudClient().redact(text, { country: countries[0] });
|
|
117
|
+
}
|
|
51
118
|
/**
|
|
52
119
|
* Reject a bare string where a list of country codes is expected.
|
|
53
120
|
*
|
|
@@ -95,6 +162,19 @@ class EuRedact {
|
|
|
95
162
|
redact(text, options = {}) {
|
|
96
163
|
checkCountryArg(options.countries, "countries");
|
|
97
164
|
checkCountryArg(options.countryHint, "countryHint");
|
|
165
|
+
const requestedMode = options.mode ?? "rules";
|
|
166
|
+
if (requestedMode === "cloud") {
|
|
167
|
+
// Cannot be served synchronously: the cloud tier is a network call, and
|
|
168
|
+
// returning rules-only output instead would be the silent under-redaction
|
|
169
|
+
// this guard exists to prevent. Point at the async entry point rather
|
|
170
|
+
// than inventing a Promise-or-value union return type.
|
|
171
|
+
throw new Error('mode: "cloud" is asynchronous — use redactAsync(text, { mode: "cloud" }) ' +
|
|
172
|
+
"(or EuRedact#redactAsync). redact() is synchronous and can only serve " +
|
|
173
|
+
'mode: "rules".');
|
|
174
|
+
}
|
|
175
|
+
if (requestedMode !== "rules") {
|
|
176
|
+
throw new Error(`unknown mode ${JSON.stringify(requestedMode)}: expected "rules" or "cloud"`);
|
|
177
|
+
}
|
|
98
178
|
if (text.length > this.maxInputLength) {
|
|
99
179
|
throw new Error(`Input text length (${text.length.toLocaleString()} chars) exceeds the maximum ` +
|
|
100
180
|
`(${this.maxInputLength.toLocaleString()} chars). Split the input or ` +
|
|
@@ -178,6 +258,18 @@ class EuRedact {
|
|
|
178
258
|
}
|
|
179
259
|
return result;
|
|
180
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Redact, awaiting the cloud tier when `mode: "cloud"` is asked for.
|
|
263
|
+
*
|
|
264
|
+
* `mode: "rules"` resolves immediately with exactly what `redact()` returns,
|
|
265
|
+
* so callers that may or may not use the cloud tier can hold one code path.
|
|
266
|
+
*/
|
|
267
|
+
async redactAsync(text, options = {}) {
|
|
268
|
+
const mode = options.mode ?? "rules";
|
|
269
|
+
if (mode !== "cloud")
|
|
270
|
+
return this.redact(text, options);
|
|
271
|
+
return redactViaCloud(text, options);
|
|
272
|
+
}
|
|
181
273
|
redactBatch(texts, options = {}) {
|
|
182
274
|
// Generation is country-blind, so every country's patterns are loaded
|
|
183
275
|
// regardless of what the caller declared.
|
package/dist/cjs/types.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DetectionSource = exports.LEGACY_TYPE_ALIASES = exports.EntityType = void 0;
|
|
4
|
+
exports.canonicalType = canonicalType;
|
|
4
5
|
var EntityType;
|
|
5
6
|
(function (EntityType) {
|
|
6
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Canonical name for a person's name. `NAME` is kept below as a legacy alias
|
|
9
|
+
* with the same value, exactly as `IBAN` aliases `BANK_ACCOUNT`, so
|
|
10
|
+
* `EntityType.NAME === EntityType.PERSON_NAME` and existing code keeps
|
|
11
|
+
* working — but the emitted value, and therefore the `[PERSON_NAME]`
|
|
12
|
+
* placeholder written into redacted text, is canonical.
|
|
13
|
+
*
|
|
14
|
+
* Renamed rather than translated at the cloud boundary: two names for one
|
|
15
|
+
* type is how a whole category goes missing when someone filters on the
|
|
16
|
+
* spelling they happened to know. Nothing could have depended on the old
|
|
17
|
+
* value — the type is cloud-only and the cloud tier was stubbed, so it was
|
|
18
|
+
* never emitted.
|
|
19
|
+
*/
|
|
20
|
+
EntityType["PERSON_NAME"] = "PERSON_NAME";
|
|
21
|
+
/** @deprecated legacy alias of {@link EntityType.PERSON_NAME} */
|
|
22
|
+
EntityType["NAME"] = "PERSON_NAME";
|
|
7
23
|
EntityType["ADDRESS"] = "ADDRESS";
|
|
8
24
|
/**
|
|
9
25
|
* Canonical name for a bank account identifier. `IBAN` is kept below as a
|
|
@@ -51,6 +67,20 @@ var EntityType;
|
|
|
51
67
|
EntityType["UUID"] = "UUID";
|
|
52
68
|
EntityType["SOCIAL_HANDLE"] = "SOCIAL_HANDLE";
|
|
53
69
|
EntityType["SECRET"] = "SECRET";
|
|
70
|
+
// ── [CLOUD EXTENSION] ────────────────────────────────────────────────────
|
|
71
|
+
// Types only the cloud tier can detect. The rule engine never emits these —
|
|
72
|
+
// there is no shape to match on — which is exactly why they exist: the
|
|
73
|
+
// model's job is what the rules cannot catch. Named to match the detection
|
|
74
|
+
// canon the service is trained and evaluated against.
|
|
75
|
+
EntityType["ORGANISATION_NAME"] = "ORGANISATION_NAME";
|
|
76
|
+
EntityType["JOB_TITLE"] = "JOB_TITLE";
|
|
77
|
+
EntityType["MEDICAL_CONDITION"] = "MEDICAL_CONDITION";
|
|
78
|
+
EntityType["SENSITIVE_ATTRIBUTE"] = "SENSITIVE_ATTRIBUTE";
|
|
79
|
+
EntityType["BIOMETRIC_REF"] = "BIOMETRIC_REF";
|
|
80
|
+
EntityType["FINANCIAL_AMOUNT"] = "FINANCIAL_AMOUNT";
|
|
81
|
+
EntityType["QUASI_IDENTIFIER"] = "QUASI_IDENTIFIER";
|
|
82
|
+
EntityType["CREDENTIAL"] = "CREDENTIAL";
|
|
83
|
+
EntityType["URL"] = "URL";
|
|
54
84
|
EntityType["OTHER"] = "OTHER";
|
|
55
85
|
})(EntityType || (exports.EntityType = EntityType = {}));
|
|
56
86
|
/**
|
|
@@ -60,7 +90,21 @@ var EntityType;
|
|
|
60
90
|
*/
|
|
61
91
|
exports.LEGACY_TYPE_ALIASES = {
|
|
62
92
|
IBAN: "BANK_ACCOUNT",
|
|
93
|
+
NAME: "PERSON_NAME",
|
|
94
|
+
STREET_ADDRESS: "ADDRESS",
|
|
95
|
+
NATIONALITY_ETHNICITY: "SENSITIVE_ATTRIBUTE",
|
|
63
96
|
};
|
|
97
|
+
/**
|
|
98
|
+
* Resolve a possibly-legacy type name to its canonical form.
|
|
99
|
+
*
|
|
100
|
+
* Mirrors `EntityType._missing_` in the Python SDK. An unrecognised name is
|
|
101
|
+
* returned unchanged rather than dropped: a client one release behind the
|
|
102
|
+
* service must not silently lose a whole category of PII.
|
|
103
|
+
*/
|
|
104
|
+
function canonicalType(name) {
|
|
105
|
+
const upper = String(name ?? "").trim().toUpperCase();
|
|
106
|
+
return exports.LEGACY_TYPE_ALIASES[upper] ?? upper;
|
|
107
|
+
}
|
|
64
108
|
var DetectionSource;
|
|
65
109
|
(function (DetectionSource) {
|
|
66
110
|
DetectionSource["RULES"] = "rules";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [CLOUD EXTENSION] The cloud client.
|
|
3
|
+
*
|
|
4
|
+
* Speaks to the euRedact inference service: a deterministic rules engine
|
|
5
|
+
* followed by a fine-tuned model asked only *what did the rules miss?* The
|
|
6
|
+
* service returns both the redacted document and located spans, so this
|
|
7
|
+
* module's job is transport and translation, not detection.
|
|
8
|
+
*
|
|
9
|
+
* Three things it hides from the caller:
|
|
10
|
+
*
|
|
11
|
+
* - **Whether the document finished inside the service's sync window.** A short
|
|
12
|
+
* document comes back on the original request; a long one, or one arriving
|
|
13
|
+
* while a GPU node is cold, returns a job handle and is polled. Callers
|
|
14
|
+
* should never write that branch, so `redact()` resolves either way.
|
|
15
|
+
* - **Retries.** Every request carries an `Idempotency-Key`, so a retry after a
|
|
16
|
+
* timeout cannot produce a second job or a second usage row.
|
|
17
|
+
* - **`Retry-After`.** When the service says how long to wait, that is obeyed
|
|
18
|
+
* rather than second-guessed with a backoff curve.
|
|
19
|
+
*
|
|
20
|
+
* Zero dependencies, like the rest of the package: this uses the platform's
|
|
21
|
+
* own `fetch`.
|
|
22
|
+
*/
|
|
23
|
+
import { type CloudConfig } from "./config.js";
|
|
24
|
+
import { type RedactResult } from "../types.js";
|
|
25
|
+
export interface CloudRedactOptions {
|
|
26
|
+
country: string;
|
|
27
|
+
language?: string;
|
|
28
|
+
priority?: "interactive" | "batch";
|
|
29
|
+
rulesOnly?: boolean;
|
|
30
|
+
idempotencyKey?: string;
|
|
31
|
+
/** Injectable for tests. Defaults to the platform `fetch`. */
|
|
32
|
+
fetchImpl?: typeof fetch;
|
|
33
|
+
}
|
|
34
|
+
export declare class CloudClient {
|
|
35
|
+
readonly config: CloudConfig;
|
|
36
|
+
constructor(config?: CloudConfig | null);
|
|
37
|
+
private headers;
|
|
38
|
+
private raiseFor;
|
|
39
|
+
redact(text: string, options: CloudRedactOptions): Promise<RedactResult>;
|
|
40
|
+
private poll;
|
|
41
|
+
private withTimeout;
|
|
42
|
+
}
|