euredact 0.7.0 → 0.7.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 +52 -5
- package/dist/cjs/rules/countries/index.js +20 -3
- package/dist/cjs/rules/suppressors.js +200 -17
- package/dist/cjs/sdk.js +90 -7
- package/dist/esm/rules/countries/index.js +20 -3
- package/dist/esm/rules/suppressors.js +200 -17
- package/dist/esm/sdk.d.ts +26 -0
- package/dist/esm/sdk.js +90 -8
- package/dist/esm/types.d.ts +6 -4
- package/package.json +3 -2
package/dist/cjs/sdk.js
CHANGED
|
@@ -38,6 +38,7 @@ exports.applyReplacements = applyReplacements;
|
|
|
38
38
|
exports.maskForCloud = maskForCloud;
|
|
39
39
|
exports.ontoOriginal = ontoOriginal;
|
|
40
40
|
exports.restore = restore;
|
|
41
|
+
exports.normalizeExcludeTypes = normalizeExcludeTypes;
|
|
41
42
|
const platform_js_1 = require("./platform.js");
|
|
42
43
|
const cache_js_1 = require("./cache.js");
|
|
43
44
|
const normalizer_js_1 = require("./normalizer.js");
|
|
@@ -394,6 +395,70 @@ function applyAllowlist(text, detections, allowed, domains) {
|
|
|
394
395
|
}
|
|
395
396
|
return [kept, exempted];
|
|
396
397
|
}
|
|
398
|
+
const KNOWN_TYPES = new Set(Object.values(types_js_1.EntityType));
|
|
399
|
+
/**
|
|
400
|
+
* The entity types a caller asked to leave in the text, by canonical name.
|
|
401
|
+
*
|
|
402
|
+
* Names are matched case-insensitively and legacy names resolve (`IBAN` is
|
|
403
|
+
* `BANK_ACCOUNT`, `CREDENTIAL` is `SECRET`). A custom pattern's name counts
|
|
404
|
+
* once it is registered. Anything else throws: a typo in a type meant to be
|
|
405
|
+
* switched off would otherwise switch nothing off (rules-engine#125).
|
|
406
|
+
*/
|
|
407
|
+
function normalizeExcludeTypes(values, customNames = new Set(), param = "excludeTypes") {
|
|
408
|
+
const out = new Set();
|
|
409
|
+
if (values === null || values === undefined)
|
|
410
|
+
return out;
|
|
411
|
+
checkAllowlistArg(values, param);
|
|
412
|
+
for (const v of values) {
|
|
413
|
+
if (typeof v !== "string") {
|
|
414
|
+
throw new TypeError(`${param} entries must be type names, not ${typeof v}`);
|
|
415
|
+
}
|
|
416
|
+
if (customNames.has(v)) {
|
|
417
|
+
out.add(v);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const name = (0, types_js_1.canonicalType)(v);
|
|
421
|
+
if (!KNOWN_TYPES.has(name)) {
|
|
422
|
+
throw new Error(`unknown entity type ${JSON.stringify(v)} in ${param}. Known types: ` +
|
|
423
|
+
[...KNOWN_TYPES].sort().join(", ") +
|
|
424
|
+
(customNames.size ? `; custom patterns: ${[...customNames].sort().join(", ")}` : ""));
|
|
425
|
+
}
|
|
426
|
+
out.add(name);
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Split off the detections of the types the caller excluded. Applied to each
|
|
432
|
+
* span's final type, after ranking: dropping an excluded type's candidates
|
|
433
|
+
* earlier would let the next-best type claim the same span.
|
|
434
|
+
*/
|
|
435
|
+
function applyTypeExclusion(text, detections, excluded) {
|
|
436
|
+
if (excluded.size === 0)
|
|
437
|
+
return [detections, []];
|
|
438
|
+
const kept = [];
|
|
439
|
+
const exempted = [];
|
|
440
|
+
for (const d of detections) {
|
|
441
|
+
const name = String(d.entityType);
|
|
442
|
+
if (excluded.has(name)) {
|
|
443
|
+
exempted.push({
|
|
444
|
+
entityType: d.entityType, start: d.start, end: d.end,
|
|
445
|
+
text: text.slice(d.start, d.end), rule: name, ruleKind: "type",
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
else
|
|
449
|
+
kept.push(d);
|
|
450
|
+
}
|
|
451
|
+
return [kept, exempted];
|
|
452
|
+
}
|
|
453
|
+
/** Allowlist first, then excluded types; every exemption in text order. */
|
|
454
|
+
function exempt(text, detections, allowed, domains, excluded) {
|
|
455
|
+
const [afterValues, byValue] = applyAllowlist(text, detections, allowed, domains);
|
|
456
|
+
const [kept, byType] = applyTypeExclusion(text, afterValues, excluded);
|
|
457
|
+
const exempted = [...byValue, ...byType];
|
|
458
|
+
if (byValue.length && byType.length)
|
|
459
|
+
exempted.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
460
|
+
return [kept, exempted];
|
|
461
|
+
}
|
|
397
462
|
/** Two label schemes for the same spans cannot both apply. */
|
|
398
463
|
function checkLabelOptions(options) {
|
|
399
464
|
if (options.tokenize && options.referentialIntegrity) {
|
|
@@ -474,19 +539,26 @@ class EuRedact {
|
|
|
474
539
|
maxInputLength;
|
|
475
540
|
allowlist;
|
|
476
541
|
allowlistDomains;
|
|
542
|
+
customNames = new Set();
|
|
543
|
+
excludeTypes;
|
|
477
544
|
/**
|
|
478
545
|
* @param options.maxInputLength Longest document `redact` accepts, in characters.
|
|
479
546
|
* @param options.allowlist Values never to redact, for every call on this
|
|
480
547
|
* instance — an organisation's own name, its own addresses. Merged with
|
|
481
548
|
* the per-call `allowlist`. Matched whole, case-insensitively.
|
|
549
|
+
* @param options.excludeTypes Entity types never reported or masked, for
|
|
550
|
+
* every call on this instance, e.g. `["JOB_TITLE", "FINANCIAL_AMOUNT"]`.
|
|
551
|
+
* Merged with the per-call value. Unknown names throw.
|
|
482
552
|
*/
|
|
483
553
|
constructor(options) {
|
|
484
554
|
this.maxInputLength = options?.maxInputLength ?? DEFAULT_MAX_INPUT_LENGTH;
|
|
485
555
|
this.allowlist = normalizeAllowlist(options?.allowlist);
|
|
486
556
|
this.allowlistDomains = normalizeDomains(options?.allowlistDomains);
|
|
557
|
+
this.excludeTypes = normalizeExcludeTypes(options?.excludeTypes);
|
|
487
558
|
}
|
|
488
559
|
addCustomPattern(name, pattern) {
|
|
489
560
|
this.engine.addCustomPattern(name, pattern);
|
|
561
|
+
this.customNames.add(name);
|
|
490
562
|
this.cache.clear();
|
|
491
563
|
}
|
|
492
564
|
/** Clear the result cache and referential integrity mappings.
|
|
@@ -502,6 +574,13 @@ class EuRedact {
|
|
|
502
574
|
allowed.set(k, v);
|
|
503
575
|
return allowed;
|
|
504
576
|
}
|
|
577
|
+
/** The instance's excluded types merged with a call's. */
|
|
578
|
+
excludedFor(options) {
|
|
579
|
+
const excluded = new Set(this.excludeTypes);
|
|
580
|
+
for (const t of normalizeExcludeTypes(options.excludeTypes, this.customNames))
|
|
581
|
+
excluded.add(t);
|
|
582
|
+
return excluded;
|
|
583
|
+
}
|
|
505
584
|
/** The instance domain list merged with a call's. */
|
|
506
585
|
domainsFor(options) {
|
|
507
586
|
const domains = new Map(this.allowlistDomains);
|
|
@@ -558,14 +637,16 @@ class EuRedact {
|
|
|
558
637
|
}
|
|
559
638
|
const allowed = this.allowedFor(options);
|
|
560
639
|
const domains = this.domainsFor(options);
|
|
640
|
+
const excluded = this.excludedFor(options);
|
|
561
641
|
const client = new CloudClient();
|
|
562
642
|
// Dates are always on, whatever detectDates says: that is the rules output
|
|
563
643
|
// the model was trained against, and a date of birth the rules can place
|
|
564
644
|
// has no reason to travel. It is the one ignored option that is safe to
|
|
565
645
|
// ignore — it can only cause MORE to be detected, never less.
|
|
566
646
|
//
|
|
567
|
-
// No allowlist and no tokens on this pass. An exemption
|
|
568
|
-
// caller wants back, not what may leave
|
|
647
|
+
// No allowlist, no excluded types and no tokens on this pass. An exemption
|
|
648
|
+
// says what the caller wants back, not what may leave: an excluded EMAIL is
|
|
649
|
+
// still masked in the request. All three are applied below, after the
|
|
569
650
|
// response, to the merged spans.
|
|
570
651
|
const local = this.redactRules(text, { countries, detectDates: true, cache: options.cache }, new Map(), new Map());
|
|
571
652
|
const [masked, labels] = maskForCloud(text, local.detections);
|
|
@@ -575,7 +656,7 @@ class EuRedact {
|
|
|
575
656
|
country: countries[0], idempotencyKey: options.idempotencyKey,
|
|
576
657
|
});
|
|
577
658
|
// A new array throughout: `local` may be the cached rules result.
|
|
578
|
-
const [detections, exempted] =
|
|
659
|
+
const [detections, exempted] = exempt(text, [...local.detections, ...ontoOriginal(remote.detections, masked, text, labels, CloudError)], allowed, domains, excluded);
|
|
579
660
|
detections.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
580
661
|
const tokenMapper = options.tokenize ? new TokenMapper(text, detections) : null;
|
|
581
662
|
return {
|
|
@@ -594,6 +675,7 @@ class EuRedact {
|
|
|
594
675
|
checkLabelOptions(options);
|
|
595
676
|
const allowed = this.allowedFor(options);
|
|
596
677
|
const domains = this.domainsFor(options);
|
|
678
|
+
const excluded = this.excludedFor(options);
|
|
597
679
|
const requestedMode = options.mode ?? "rules";
|
|
598
680
|
if (requestedMode === "cloud") {
|
|
599
681
|
// Cannot be served synchronously: the cloud tier is a network call, and
|
|
@@ -610,10 +692,10 @@ class EuRedact {
|
|
|
610
692
|
if (options.idempotencyKey !== undefined) {
|
|
611
693
|
throw new Error('idempotencyKey applies to mode: "cloud" only: a rules-only redaction makes no request to deduplicate');
|
|
612
694
|
}
|
|
613
|
-
return this.redactRules(text, options, allowed, domains);
|
|
695
|
+
return this.redactRules(text, options, allowed, domains, excluded);
|
|
614
696
|
}
|
|
615
697
|
/** The local pipeline: normalise, detect, exempt, mask. */
|
|
616
|
-
redactRules(text, options, allowed, domains) {
|
|
698
|
+
redactRules(text, options, allowed, domains, excluded = new Set()) {
|
|
617
699
|
if (text.length > this.maxInputLength) {
|
|
618
700
|
throw new Error(`Input text length (${text.length.toLocaleString()} chars) exceeds the maximum ` +
|
|
619
701
|
`(${this.maxInputLength.toLocaleString()} chars). Split the input or ` +
|
|
@@ -636,7 +718,8 @@ class EuRedact {
|
|
|
636
718
|
// JSON rather than a joined string: an entry may itself contain the
|
|
637
719
|
// separator, and two different lists must never share a key.
|
|
638
720
|
const allowKey = (allowed.size ? JSON.stringify([...allowed.keys()].sort()) : "")
|
|
639
|
-
+ (domains.size ? "|dom=" + JSON.stringify([...domains.keys()].sort()) : "")
|
|
721
|
+
+ (domains.size ? "|dom=" + JSON.stringify([...domains.keys()].sort()) : "")
|
|
722
|
+
+ (excluded.size ? "|excl=" + JSON.stringify([...excluded].sort()) : "");
|
|
640
723
|
const cacheMode = `rules|dates=${detectDates}|hint=${hintKey}|ri=${referentialIntegrity}|tok=${tokenize}|allow=${allowKey}`;
|
|
641
724
|
let cacheKey;
|
|
642
725
|
if (cache) {
|
|
@@ -660,7 +743,7 @@ class EuRedact {
|
|
|
660
743
|
detections = detections.filter(d => !DATE_TYPES.has(d.entityType));
|
|
661
744
|
}
|
|
662
745
|
let exempted;
|
|
663
|
-
[detections, exempted] =
|
|
746
|
+
[detections, exempted] = exempt(text, detections, allowed, domains, excluded);
|
|
664
747
|
detections.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
665
748
|
const tokenMapper = tokenize ? new TokenMapper(text, detections) : null;
|
|
666
749
|
const redacted = applyReplacements(text, detections, this.labelFor(referentialIntegrity, tokenMapper));
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { EntityType } from "../../types.js";
|
|
2
2
|
import { IBAN_LENGTHS } from "../validators.js";
|
|
3
3
|
import { MONTH_NAMES } from "../suppressors.js";
|
|
4
|
+
/** An assigned value: not opened by a quote or bracket, eight characters or
|
|
5
|
+
* more, not ended by punctuation or a closer (rules-engine#35, #126). */
|
|
6
|
+
const ASSIGNED = String.raw `[^\s"'\x60\[\]{}()<>„“”«»‚‘’]\S{6,}[^\s.,;:!?)'"\]}\x60“”»‘’]`;
|
|
7
|
+
/** A quoted assigned value: exactly what the quotes hold (rules-engine#126). */
|
|
8
|
+
const QUOTED_ASSIGNED = ['"', "'", "\x60"].flatMap((q) => [" ", ""].map((gap) => String.raw `(?<=[:=]` + gap + q + String.raw `)[^\s` + q + String.raw `]{8,}(?=` + q + ")")).join("|");
|
|
4
9
|
/** Day, month as a word, year ("4. März 1948"), behind a date label (rules-engine#115). */
|
|
5
10
|
function writtenDatePattern() {
|
|
6
11
|
const names = new Set(MONTH_NAMES.split("|").filter(Boolean));
|
|
@@ -143,6 +148,7 @@ const DOB_CONTEXT = [
|
|
|
143
148
|
"γεννηθείς", "γεννήθηκε", "ημερομηνία γέννησης",
|
|
144
149
|
// West Slavic.
|
|
145
150
|
"urodzony", "urodzona", "data urodzenia",
|
|
151
|
+
"ur.",
|
|
146
152
|
"narozen", "datum narození", "narodený", "dátum narodenia",
|
|
147
153
|
// Hungarian, Romanian, Bulgarian.
|
|
148
154
|
"született", "születési", "născut", "născută", "data nașterii",
|
|
@@ -265,14 +271,20 @@ const SHARED = {
|
|
|
265
271
|
// AWS Secret Access Key (40 chars, base64 with +/ after an AKIA line)
|
|
266
272
|
p(EntityType.SECRET, String.raw `(?<=aws_secret_access_key\s{0,4}[:=]\s{0,4})[A-Za-z0-9+/]{40}`, null, "AWS Secret Access Key"),
|
|
267
273
|
// --- Secret / API Key (connection strings with embedded credentials) ---
|
|
268
|
-
|
|
274
|
+
// The tail stops at a quote (rules-engine#126).
|
|
275
|
+
p(EntityType.SECRET, String.raw `(?:mongodb|mysql|postgres(?:ql)?|redis|amqp|rabbitmq):\/\/[^\s:]{1,256}:[^\s@]{1,256}@[^\s"'\x60]+`, null, "Connection string with credentials"),
|
|
269
276
|
// --- Secret / API Key (assignment-based: KEY=value or KEY: value) ---
|
|
270
277
|
// The final character may not be sentence punctuation. `\S` does not stop
|
|
271
278
|
// at a full stop, so "Reisepass: CA1234567." was claimed as the
|
|
272
279
|
// ten-character span "CA1234567." -- a passport number plus the sentence's
|
|
273
280
|
// period, masked as a credential because a "credentials" word sat within
|
|
274
281
|
// the context window (rules-engine#35).
|
|
275
|
-
|
|
282
|
+
// The value starts after an opening quote or bracket and ends before its
|
|
283
|
+
// closer: `user = "svc_terraform"` was the span `"svc_terraform`
|
|
284
|
+
// (rules-engine#126).
|
|
285
|
+
// A quoted value is what the quotes hold, closing punctuation included.
|
|
286
|
+
p(EntityType.SECRET, QUOTED_ASSIGNED + "|" + [String.raw `(?<=[:=] )`, String.raw `(?<=[:=])`, String.raw `(?<=[:=] ["'\x60\[{(])`, String.raw `(?<=[:=]["'\x60\[{(])`]
|
|
287
|
+
.map((lb) => lb + ASSIGNED).join("|"), "high_entropy", "Assigned secret value", SECRET_CONTEXT, true),
|
|
276
288
|
// --- Secret / API Key (entropy-based fallback for longer tokens) ---
|
|
277
289
|
// Only the *trailing* \b is replaced. It could not match when the token run
|
|
278
290
|
// ended on "-", "+" or "/", so the engine backtracked across two
|
|
@@ -552,7 +564,10 @@ const PT = {
|
|
|
552
564
|
p(EntityType.PHONE, String.raw `\b[29]\d{2}[\s\-]?\d{3}[\s\-]?\d{3}\b`),
|
|
553
565
|
p(EntityType.PHONE, String.raw `\b[29]\d{8}\b`),
|
|
554
566
|
p(EntityType.PHONE, String.raw `\+351\s?[29]\d{2}[\s\-]?\d{3}[\s\-]?\d{3}`),
|
|
555
|
-
|
|
567
|
+
// The four formats Portugal has issued (rules-engine#126).
|
|
568
|
+
p(EntityType.LICENSE_PLATE, String.raw `\b(?:\d{2}-\d{2}-[A-Z]{2}|\d{2}-[A-Z]{2}-\d{2}|[A-Z]{2}-\d{2}-\d{2}|[A-Z]{2}-\d{2}-[A-Z]{2})\b`),
|
|
569
|
+
// Any three pairs, behind a plate label (rules-engine#126).
|
|
570
|
+
p(EntityType.LICENSE_PLATE, String.raw `\b[A-Z0-9]{2}-[A-Z0-9]{2}-[A-Z0-9]{2}\b`, null, "Portuguese license plate — AA-XX-XX, behind its label", ["matrícula", "matricula", "placa", "license plate", "number plate", "plate", "Kennzeichen", "kenteken", "immatriculation", "targa"], true),
|
|
556
571
|
p(EntityType.POSTAL_CODE, String.raw `\b\d{4}-\d{3}\b`),
|
|
557
572
|
],
|
|
558
573
|
};
|
|
@@ -627,6 +642,8 @@ const UK = {
|
|
|
627
642
|
code: "UK",
|
|
628
643
|
name: "United Kingdom",
|
|
629
644
|
patterns: [
|
|
645
|
+
// A sort code behind its label; no longer found through the Portuguese plate shape (rules-engine#126).
|
|
646
|
+
p(EntityType.BANK_ACCOUNT, String.raw `\b\d{2}-\d{2}-\d{2}\b`, null, "UK sort code — NN-NN-NN, behind its label", ["sort code", "sortcode", "sort-code"], true),
|
|
630
647
|
// Healthcare-provider numbers, behind their label: no shape of their own, so without a pattern nothing claimed them, or a phone or a foreign checksum did (canon HEALTHCARE_PROVIDER; rules-engine#111).
|
|
631
648
|
p(EntityType.HEALTHCARE_PROVIDER, String.raw `\b\d{7}\b`, null, "UK GMC number — 7 digits", ["GMC number", "GMC no", "GMC reference", "GMC reg", "GMC:", "GMC-Nr", "General Medical Council"], true),
|
|
632
649
|
// UK NINO: a social-security number, so SSN, as the canon defaults it (rules-engine#47).
|
|
@@ -199,6 +199,27 @@ function suppressSecretOverStructured(text, match) {
|
|
|
199
199
|
* natural word. A random all-letter token would be mixed case and is left alone
|
|
200
200
|
* — losing a real secret is the expensive direction.
|
|
201
201
|
*/
|
|
202
|
+
/**
|
|
203
|
+
* Values an assigned-secret rule finds after a "=" or ":" that are names, not
|
|
204
|
+
* secrets (rules-engine#126): `${VAR}` references, identifiers, hyphenated
|
|
205
|
+
* compounds, image tags, product versions, code calls. Not paths, host names
|
|
206
|
+
* or reference numbers: those carry personal data ("mac-mhartmann88").
|
|
207
|
+
* Kept in sync with the Python SDK's _SECRET_NAME_SHAPES.
|
|
208
|
+
*/
|
|
209
|
+
const SECRET_NAME_SHAPES = new RegExp([
|
|
210
|
+
String.raw `^\$\{[A-Za-z_][A-Za-z0-9_]*(?::?[-=?+][^}]*)?\}?$`,
|
|
211
|
+
String.raw `^[a-z]{2,}(?:_[a-z]{2,})+$`, String.raw `^[A-Z]{2,}(?:_[A-Z]{2,})+$`,
|
|
212
|
+
String.raw `^(?:[A-Z]+[a-z]{2,}){3,}(?:V?\d{1,2})?$`,
|
|
213
|
+
String.raw `^[a-z][a-z0-9._/-]*:(?:\d+(?:\.\d+)*(?:-[a-z0-9]+)*|latest|stable|alpine|slim)$`,
|
|
214
|
+
String.raw `^[A-Za-z][\w.-]*\/\d+(?:\.\d+)+$`,
|
|
215
|
+
String.raw `^[\w.]+\(["'\x60]`,
|
|
216
|
+
].join("|"));
|
|
217
|
+
/** A hyphenated compound of words and acronyms; two capitalised words alone
|
|
218
|
+
* ("Ann-Sophie", "Caron-Cordier") are names and are left alone. */
|
|
219
|
+
const COMPOUND_PART = String.raw `(?:[A-ZÀ-Þ]{2,6}\d{0,3}|[A-Za-zÀ-ÿ][a-zß-ÿ]{2,})`;
|
|
220
|
+
const COMPOUND = new RegExp(String.raw `^` + COMPOUND_PART + String.raw `(?:-` + COMPOUND_PART + String.raw `)+$`);
|
|
221
|
+
const NAME_PAIR = /^[A-ZÀ-Þ][a-zß-ÿ]{2,}-[A-ZÀ-Þ][a-zß-ÿ]{2,}$/;
|
|
222
|
+
const CONNECTION_WITH_REFERENCE = /^[a-z]+:\/\/[^\s:@]+:\$\{[^}]*\}@/;
|
|
202
223
|
function suppressSecretNotASecret(_text, match) {
|
|
203
224
|
if (match.patternDef.entityType !== EntityType.SECRET)
|
|
204
225
|
return false;
|
|
@@ -213,6 +234,10 @@ function suppressSecretNotASecret(_text, match) {
|
|
|
213
234
|
return true;
|
|
214
235
|
if (HYPHENATED_WORDS.test(bare))
|
|
215
236
|
return true;
|
|
237
|
+
if (SECRET_NAME_SHAPES.test(bare) || CONNECTION_WITH_REFERENCE.test(bare))
|
|
238
|
+
return true;
|
|
239
|
+
if (COMPOUND.test(bare) && !NAME_PAIR.test(bare))
|
|
240
|
+
return true;
|
|
216
241
|
const alpha = /^[A-Za-zÀ-ÿ]+$/.test(bare);
|
|
217
242
|
const oneWord = bare === bare.toLowerCase() ||
|
|
218
243
|
(bare.slice(0, 1) === bare.slice(0, 1).toUpperCase() &&
|
|
@@ -354,7 +379,20 @@ function suppressSequential(_text, match) {
|
|
|
354
379
|
// the "]". The opening bracket is still required.
|
|
355
380
|
const PLACEHOLDER = /^(?:\[([A-Z][A-Z0-9_]*)\]?|([A-Z][A-Z0-9_]*?)_(?:\d+|[A-HJ-NP-Z2-9]{4}))$/;
|
|
356
381
|
const KNOWN_TYPE_NAMES = new Set(Object.values(EntityType));
|
|
357
|
-
function suppressRedactionPlaceholder(
|
|
382
|
+
function suppressRedactionPlaceholder(text, match) {
|
|
383
|
+
// The name between the brackets: an assigned value now starts after an
|
|
384
|
+
// opening bracket, so "credentials: [POSTAL_CODE]" offers "POSTAL_CODE" alone
|
|
385
|
+
// (rules-engine#33, #126).
|
|
386
|
+
if (text[match.start - 1] === "[" && text[match.end] === "]" && KNOWN_TYPE_NAMES.has(match.text))
|
|
387
|
+
return true;
|
|
388
|
+
// A placeholder inside the span: on text masked once, a value running across
|
|
389
|
+
// "[IP_ADDRESS]" is two things joined (rules-engine#126).
|
|
390
|
+
// One character either side: a span may start just inside the bracket.
|
|
391
|
+
const around = text.slice(Math.max(0, match.start - 1), match.end + 1);
|
|
392
|
+
for (const m of around.matchAll(/\[([A-Z][A-Z0-9_]*)\]/g)) {
|
|
393
|
+
if (KNOWN_TYPE_NAMES.has(m[1]))
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
358
396
|
const found = PLACEHOLDER.exec(match.text);
|
|
359
397
|
if (found === null)
|
|
360
398
|
return false;
|
|
@@ -502,6 +540,33 @@ const EXTENSION_BEFORE = /(?<![A-Za-z0-9_])(?:toest(?:el)?|ext|extension|poste|d
|
|
|
502
540
|
*/
|
|
503
541
|
const COUNT_LABEL_BEFORE = /(?<![A-Za-z0-9_])(?:employees|headcount|staff|mitarbeiter(?:innen)?|beschäftigte|werknemers|medewerkers|employés|salariés|effectifs?|dipendenti|empleados|pracowników|zaměstnanců)\s*[:=]?\s*$/i;
|
|
504
542
|
const NOT_POSTAL_BEFORE = /(?<![A-Za-z0-9_])(?:(?:ISO(?:\/IEC)?|IEC|EN|DIN(?:\s+EN)?|NEN(?:-EN)?|NF\s+EN|BS(?:\s+EN)?|PN-EN|ÖNORM)[\s\-]*|[A-Za-z]*[a-z][A-Z][A-Za-z]*\s+)$/;
|
|
543
|
+
/**
|
|
544
|
+
* What else a number is, by what touches it (rules-engine#126): a build,
|
|
545
|
+
* version, event, lease, policy or ticket number; a port; an invoice, reference
|
|
546
|
+
* or study-subject number; a process id; a JSON id or port field; an HTTP
|
|
547
|
+
* response size; the hour after a date; a lab reference range; or a quantity
|
|
548
|
+
* with its unit after it. Kept in sync with the Python SDK.
|
|
549
|
+
*/
|
|
550
|
+
const TECH_WORD_BEFORE = /(?<![A-Za-z0-9_])(?:build|version|policy\s+id|ticket(?:\s+[A-Za-z]{2,10})?|event|lease|pid|job|port|rfc|facture|invoice|r[ée]f[ée]rence|n[°º]|tn|sae|uczestnik|subject|narratief|participant)\.?\s*[:#=]?\s*["(«[]?\s*$/i;
|
|
551
|
+
const TECH_SHAPE_BEFORE = new RegExp("(?:" + [
|
|
552
|
+
String.raw `"[A-Za-z_]*(?:id|port)"\s*:\s*"?`, // "parent_id": 4400
|
|
553
|
+
String.raw `(?<![A-Za-z0-9])[A-Z_]*PORT\s*[:=]\s*"?`, // DB_PORT: "5432"
|
|
554
|
+
String.raw `(?:localhost|[a-z0-9\-]+\.[a-z0-9.\-]+):`, // localhost:3000
|
|
555
|
+
String.raw `(?<![\d.])\d{2,5}:`, // "8080:8080"
|
|
556
|
+
String.raw `HTTP\/\d(?:\.\d)?"?\s+\d{3}\s+`, // HTTP/1.1" 200 2048
|
|
557
|
+
String.raw `\d+\.\d+(?:\.\d+)+\s*\(`, // build 2.4.0 (4012)
|
|
558
|
+
String.raw `(?<!\d)\d{1,2}[./]\d{1,2}[./]`, // 03.10.2026 23:50
|
|
559
|
+
String.raw `\((?:N|norm(?:al)?|ref)\s*:?\s*`, // (N : 30-400)
|
|
560
|
+
String.raw `\|`,
|
|
561
|
+
].join("|") + ")$", "i");
|
|
562
|
+
const POSTAL_UNIT_AFTER = /^(?:\s?(?:ms|s|sec|secs|seconds?|secondes?|sekunden|sek|min|bits?|bytes?|kb|mb|gb|tb|kbps|mbps|px|%|records|rows|entries|datensätze|einträge|micrograms|µg|ug|mg|mmol|fl)(?![A-Za-zÀ-ÿ])|\s*→\s*\d+\s*(?:bits?|bytes?)(?![A-Za-z])|:\d|\|)/i;
|
|
563
|
+
const PID_BEFORE = /[A-Za-z0-9_\-]\[$/;
|
|
564
|
+
/** A standard's name as the letters of a lettered postcode: "RFC 5737". Not
|
|
565
|
+
* BS or EN: those are Bristol and Enfield. */
|
|
566
|
+
const STANDARD_LETTERS = /^(?:RFC|ISO|IEC|DIN)\s?\d/;
|
|
567
|
+
/** A year closing a range: "1998–2011". En and em dashes only. */
|
|
568
|
+
const YEAR_RANGE_BEFORE = /(?:19|20)\d\d\s?[–—]\s?$/;
|
|
569
|
+
const YEAR_RANGE_AFTER = /^\s?[–—]\s?(?:19|20)\d\d(?!\d)/;
|
|
505
570
|
/** A thousands group of an amount: the "750" of "£28,750,000". */
|
|
506
571
|
const GROUP_BEFORE = /\d[,.]$/;
|
|
507
572
|
const GROUP_AFTER = /^[,.]\d{3}(?!\d)/;
|
|
@@ -515,10 +580,90 @@ function suppressPostalAfterDisqualifyingWord(text, match) {
|
|
|
515
580
|
return true;
|
|
516
581
|
if (GROUP_BEFORE.test(before) && GROUP_AFTER.test(text.slice(match.end, match.end + 5)))
|
|
517
582
|
return true;
|
|
583
|
+
const after = text.slice(match.end, match.end + 24);
|
|
584
|
+
if (TECH_WORD_BEFORE.test(before) || TECH_SHAPE_BEFORE.test(before) || POSTAL_UNIT_AFTER.test(after)
|
|
585
|
+
|| STANDARD_LETTERS.test(match.text))
|
|
586
|
+
return true;
|
|
587
|
+
if (PID_BEFORE.test(before) && after.startsWith("]"))
|
|
588
|
+
return true;
|
|
589
|
+
if (DATE_YEAR.test(match.text.trim()) && (YEAR_RANGE_BEFORE.test(before) || YEAR_RANGE_AFTER.test(after)))
|
|
590
|
+
return true;
|
|
518
591
|
return RECENT_YEAR.test(match.text.trim())
|
|
519
592
|
&& (YEAR_WORD_BEFORE.test(before) || YEAR_VERB_BEFORE.test(before))
|
|
520
593
|
&& !TOWN_AFTER.test(text.slice(match.end, match.end + 3));
|
|
521
594
|
}
|
|
595
|
+
/** A group with a leading zero: "2.00.079.01" is a version, not an address. */
|
|
596
|
+
const IP_LEADING_ZERO = /(?:^|\.)0\d/;
|
|
597
|
+
const CIDR_PREFIX_AFTER = /^\/(\d{1,2})(?!\d)/;
|
|
598
|
+
/** RFC 5737's documentation ranges: reserved, never routed, used in examples. */
|
|
599
|
+
const DOCUMENTATION_NETS = ["192.0.2.", "198.51.100.", "203.0.113."];
|
|
600
|
+
/**
|
|
601
|
+
* An IPv4 shape that names no host (rules-engine#126): a network address
|
|
602
|
+
* ("10.40.0.0/16"), a documentation address, or a version with leading zeros.
|
|
603
|
+
* An interface address with its prefix ("192.168.1.23/24") stays.
|
|
604
|
+
*/
|
|
605
|
+
function suppressIpNotAHost(text, match) {
|
|
606
|
+
if (match.patternDef.entityType !== EntityType.IP_ADDRESS)
|
|
607
|
+
return false;
|
|
608
|
+
const ip = match.text;
|
|
609
|
+
if (IP_LEADING_ZERO.test(ip) || DOCUMENTATION_NETS.some((n) => ip.startsWith(n)))
|
|
610
|
+
return true;
|
|
611
|
+
// Part of a longer dotted run: "urn:oid:1.2.250.1.71.4.2.1" is an OID.
|
|
612
|
+
if (/\d\.$/.test(text.slice(Math.max(0, match.start - 2), match.start))
|
|
613
|
+
|| /^\.\d/.test(text.slice(match.end, match.end + 2)))
|
|
614
|
+
return true;
|
|
615
|
+
const prefix = CIDR_PREFIX_AFTER.exec(text.slice(match.end, match.end + 4));
|
|
616
|
+
if (prefix !== null && Number(prefix[1]) < 32) {
|
|
617
|
+
const value = ip.split(".").reduce((acc, o) => acc * 256 + Number(o), 0);
|
|
618
|
+
return value % 2 ** (32 - Number(prefix[1])) === 0;
|
|
619
|
+
}
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* The specimen of a form: "das Muster „C01X00T47“". Only "Muster": "z. B." and
|
|
624
|
+
* "e.g." introduce real values in prose as often as samples (rules-engine#126).
|
|
625
|
+
*/
|
|
626
|
+
const EXAMPLE_BEFORE = /(?<![A-Za-z])muster\s*[:,]?\s*[„"«']?$/i;
|
|
627
|
+
/** A dummy value in placeholder glyphs ("XX0000000", "999 999 9999") or MAC
|
|
628
|
+
* groups of one repeated letter. Not "111222333", the standard test BSN. */
|
|
629
|
+
const DUMMY_VALUE = /^(?=(?:[\s:\-./]*[X09]){5})(?:[X\s:\-./]*[X][X0\s:\-./]*|[X9\s:\-./]*X[X9\s:\-./]*|[0\s:\-./]+|[9\s:\-./]+)$|^(?:([a-fA-F])\1[:\-]){2,}([a-fA-F])\2$/;
|
|
630
|
+
/** Algorithm names in a key or signature line: "ED25519", "SHA256". */
|
|
631
|
+
const ALGORITHM = /^(?:ED25519|ED448|SHA-?(?:1|224|256|384|512)|MD5|AES-?\d{3}|RSA-?\d{3,4}|[HRE]S(?:256|384|512))$/i;
|
|
632
|
+
/** A date exactly, which no national number is written as. */
|
|
633
|
+
const EXACT_DOTTED_DATE = /^(?:0[1-9]|[12]\d|3[01])\.(?:0[1-9]|1[0-2])\.(?:19|20)\d\d$/;
|
|
634
|
+
/** The identifier types an example marker or a dummy value rules out. */
|
|
635
|
+
const EXAMPLE_TYPES = new Set([
|
|
636
|
+
EntityType.NATIONAL_ID, EntityType.PASSPORT, EntityType.LICENSE_PLATE,
|
|
637
|
+
EntityType.HEALTH_INSURANCE, EntityType.MAC_ADDRESS,
|
|
638
|
+
EntityType.DRIVERS_LICENSE, EntityType.SSN, EntityType.TAX_ID,
|
|
639
|
+
]);
|
|
640
|
+
/** A placeholder in the shape of a value, for any type (#126). */
|
|
641
|
+
function suppressDummyValue(_text, match) {
|
|
642
|
+
return DUMMY_VALUE.test(match.text.trim());
|
|
643
|
+
}
|
|
644
|
+
/** An identifier given as a form's specimen, not anyone's (#126). */
|
|
645
|
+
function suppressExampleValue(text, match) {
|
|
646
|
+
if (!EXAMPLE_TYPES.has(match.patternDef.entityType))
|
|
647
|
+
return false;
|
|
648
|
+
return EXAMPLE_BEFORE.test(text.slice(Math.max(0, match.start - 24), match.start));
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Shapes that fit an identifier pattern and are something else (#126): a
|
|
652
|
+
* passport without a digit, an algorithm or a law citation read as a plate,
|
|
653
|
+
* a date read as a dotted national number.
|
|
654
|
+
*/
|
|
655
|
+
function suppressNotAnIdentifier(text, match) {
|
|
656
|
+
const type = match.patternDef.entityType;
|
|
657
|
+
const value = match.text.trim();
|
|
658
|
+
if (type === EntityType.PASSPORT)
|
|
659
|
+
return !/\d/.test(value);
|
|
660
|
+
if (type === EntityType.LICENSE_PLATE) {
|
|
661
|
+
return ALGORITHM.test(value) || /§\s*$/.test(text.slice(Math.max(0, match.start - 4), match.start));
|
|
662
|
+
}
|
|
663
|
+
if (type === EntityType.NATIONAL_ID)
|
|
664
|
+
return EXACT_DOTTED_DATE.test(value);
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
522
667
|
function suppressPhoneServiceNumber(_text, match) {
|
|
523
668
|
if (match.patternDef.entityType !== EntityType.PHONE)
|
|
524
669
|
return false;
|
|
@@ -630,7 +775,11 @@ function suppressDePlateUnknownDistrict(text, match) {
|
|
|
630
775
|
const parts = match.text.trim().split(/[\s\-]+/);
|
|
631
776
|
if (parts.length === 0 || !parts[0])
|
|
632
777
|
return false;
|
|
633
|
-
|
|
778
|
+
// A German plate number never starts with 0: without a plate cue,
|
|
779
|
+
// "GR-NB-0412" is an asset tag and "HR-IT-03" a form (rules-engine#126).
|
|
780
|
+
const digits = /\d+/.exec(match.text);
|
|
781
|
+
const leadingZero = digits !== null && digits[0].startsWith("0");
|
|
782
|
+
if (DE_DISTRICT_CODES.has(parts[0].toUpperCase()) && !leadingZero)
|
|
634
783
|
return false;
|
|
635
784
|
const [before, after] = getContext(text, match.start, match.end);
|
|
636
785
|
if (PLATE_CUE_NEAR.test(before + after))
|
|
@@ -1071,15 +1220,22 @@ function suppressRequiresContext(text, match) {
|
|
|
1071
1220
|
return false;
|
|
1072
1221
|
// A value filling an entire field of a delimited row is introduced by its
|
|
1073
1222
|
// column, not by a word, so that is context — but only for narrow shapes.
|
|
1223
|
+
// A date's column decides when the table names it: under "début" or
|
|
1224
|
+
// "last_day" a date is not a birth date (rules-engine#126).
|
|
1074
1225
|
if (DELIMITED_FIELD_TYPES.has(match.patternDef.entityType) &&
|
|
1075
|
-
fillsADelimitedField(text, match.start, match.end)
|
|
1226
|
+
fillsADelimitedField(text, match.start, match.end) &&
|
|
1227
|
+
!(DATE_LABEL_TYPES.has(match.patternDef.entityType) && columnHeader(text, match.start) !== null))
|
|
1076
1228
|
return false;
|
|
1077
1229
|
if (DATE_LABEL_TYPES.has(match.patternDef.entityType)) {
|
|
1078
1230
|
return !labelsThisDate(text, match.start, match.end, match.patternDef.contextKeywords, match.patternDef.entityType);
|
|
1079
1231
|
}
|
|
1080
1232
|
const [before, after] = getContext(text, match.start, match.end);
|
|
1081
1233
|
const context = (before + " " + after).toLowerCase();
|
|
1082
|
-
|
|
1234
|
+
if (match.patternDef.contextKeywords.some(kw => context.includes(kw.toLowerCase())))
|
|
1235
|
+
return false;
|
|
1236
|
+
// The column names it: a passport under "Passport" rows below the header (#130).
|
|
1237
|
+
const header = columnHeader(text, match.start);
|
|
1238
|
+
return header === null || !match.patternDef.contextKeywords.some(kw => keywordAt(header, kw, false) >= 0);
|
|
1083
1239
|
}
|
|
1084
1240
|
/** Date types decided by a label: DOB and DATE_OF_DEATH share one date shape. */
|
|
1085
1241
|
const DATE_LABEL_TYPES = new Set([EntityType.DOB, EntityType.DATE_OF_DEATH]);
|
|
@@ -1115,10 +1271,16 @@ function nothingButAListBetween(gap) {
|
|
|
1115
1271
|
*/
|
|
1116
1272
|
function keywordAt(haystack, keyword, last) {
|
|
1117
1273
|
const kw = keyword.toLowerCase();
|
|
1274
|
+
const isLetter = (c) => c !== "" && c.toLowerCase() !== c.toUpperCase();
|
|
1118
1275
|
if (keyword !== keyword.toUpperCase() || keyword === kw) {
|
|
1119
|
-
|
|
1276
|
+
// It must start a word, though it may run on into a longer one: "née le"
|
|
1277
|
+
// in "signée le" and "born" in "Newborn" are other words (rules-engine#126).
|
|
1278
|
+
let i = last ? haystack.lastIndexOf(kw) : haystack.indexOf(kw);
|
|
1279
|
+
while (i > 0 && isLetter(haystack[i - 1])) {
|
|
1280
|
+
i = last ? (i === 0 ? -1 : haystack.lastIndexOf(kw, i - 1)) : haystack.indexOf(kw, i + 1);
|
|
1281
|
+
}
|
|
1282
|
+
return i;
|
|
1120
1283
|
}
|
|
1121
|
-
const isLetter = (c) => c !== "" && c.toLowerCase() !== c.toUpperCase();
|
|
1122
1284
|
const hits = [];
|
|
1123
1285
|
for (let i = haystack.indexOf(kw); i !== -1; i = haystack.indexOf(kw, i + 1)) {
|
|
1124
1286
|
let next = i + kw.length;
|
|
@@ -1180,29 +1342,44 @@ function columnHeader(text, start) {
|
|
|
1180
1342
|
const lineStart = text.lastIndexOf("\n", start - 1) + 1;
|
|
1181
1343
|
const lineEndAt = text.indexOf("\n", start);
|
|
1182
1344
|
const line = text.slice(lineStart, lineEndAt >= 0 ? lineEndAt : text.length);
|
|
1183
|
-
// The three lines above, by lastIndexOf: splitting the whole prefix was
|
|
1184
|
-
// quadratic in the document, once per date candidate.
|
|
1185
|
-
let aboveStart = lineStart - 1;
|
|
1186
|
-
for (let k = 0; k < 3 && aboveStart >= 0; k++) {
|
|
1187
|
-
aboveStart = aboveStart === 0 ? -1 : text.lastIndexOf("\n", aboveStart - 1);
|
|
1188
|
-
}
|
|
1189
|
-
const above = lineStart ? text.slice(aboveStart + 1, Math.max(0, lineStart - 1)).split("\n") : [];
|
|
1190
1345
|
for (const sep of COLUMN_SEPARATORS) {
|
|
1191
1346
|
if (!line.includes(sep))
|
|
1192
1347
|
continue;
|
|
1193
1348
|
const cells = line.split(sep);
|
|
1194
1349
|
const column = text.slice(lineStart, start).split(sep).length - 1;
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1350
|
+
// Lines above, one lastIndexOf at a time: one of the three lines above, or
|
|
1351
|
+
// further up through rows of the same table (rules-engine#126).
|
|
1352
|
+
let end = lineStart - 1;
|
|
1353
|
+
let seen = 0;
|
|
1354
|
+
while (end >= 0 && seen < HEADER_REACH) {
|
|
1355
|
+
const begin = end === 0 ? 0 : text.lastIndexOf("\n", end - 1) + 1;
|
|
1356
|
+
const header = text.slice(begin, end);
|
|
1357
|
+
seen++;
|
|
1358
|
+
// A rule line ("|---|---|", "----+----") is neither header nor row (#130).
|
|
1359
|
+
if (RULE_LINE.test(header)) {
|
|
1360
|
+
end = begin - 1;
|
|
1198
1361
|
continue;
|
|
1362
|
+
}
|
|
1199
1363
|
const heads = header.split(sep);
|
|
1200
|
-
|
|
1364
|
+
const same = heads.length === cells.length;
|
|
1365
|
+
// A "label : value" field with a comma in its value is not a header
|
|
1366
|
+
// (#111); a commented header ("# Spalten: Name;von;bis") is (#126).
|
|
1367
|
+
// Nor is a data row without a date: a header has no cell with four digits or more (#130).
|
|
1368
|
+
if (same && (!header.includes(":") || header.trimStart().startsWith("#")) && !ANY_DATE.test(header)
|
|
1369
|
+
&& !heads.some((h) => (h.match(/\d/g) ?? []).length >= 4)) {
|
|
1201
1370
|
return heads[column].toLowerCase();
|
|
1371
|
+
}
|
|
1372
|
+
if (seen >= 3 && !same)
|
|
1373
|
+
break; // past the table this value sits in
|
|
1374
|
+
end = begin - 1;
|
|
1202
1375
|
}
|
|
1203
1376
|
}
|
|
1204
1377
|
return null;
|
|
1205
1378
|
}
|
|
1379
|
+
/** How many lines up a table's header is looked for. */
|
|
1380
|
+
const HEADER_REACH = 200;
|
|
1381
|
+
/** A table's rule line: only dashes, equals signs, plus signs, colons, pipes. */
|
|
1382
|
+
const RULE_LINE = /^[\s\-=+:|]*-[\s\-=+:|]*$/;
|
|
1206
1383
|
/** A dotted MAC shape glued to more of a reference is part of it: the tail of
|
|
1207
1384
|
* "WSO-II.6151.4471.2025" (rules-engine#105). Dotted form only. */
|
|
1208
1385
|
function suppressMacInReference(text, match) {
|
|
@@ -1237,12 +1414,18 @@ const TYPE_SUPPRESSORS = {
|
|
|
1237
1414
|
[EntityType.CHAMBER_OF_COMMERCE]: [suppressReference],
|
|
1238
1415
|
[EntityType.MAC_ADDRESS]: [suppressMacInReference],
|
|
1239
1416
|
[EntityType.CREDIT_CARD]: [suppressCardInsideDigitRun],
|
|
1417
|
+
[EntityType.IP_ADDRESS]: [suppressIpNotAHost],
|
|
1240
1418
|
};
|
|
1419
|
+
for (const type of EXAMPLE_TYPES) {
|
|
1420
|
+
TYPE_SUPPRESSORS[type] = [...(TYPE_SUPPRESSORS[type] ?? []), suppressExampleValue, suppressNotAnIdentifier];
|
|
1421
|
+
}
|
|
1241
1422
|
export function shouldSuppress(text, match, scratch) {
|
|
1242
1423
|
if (suppressSequential(text, match))
|
|
1243
1424
|
return true;
|
|
1244
1425
|
if (suppressRedactionPlaceholder(text, match))
|
|
1245
1426
|
return true;
|
|
1427
|
+
if (suppressDummyValue(text, match))
|
|
1428
|
+
return true;
|
|
1246
1429
|
const typeSups = TYPE_SUPPRESSORS[match.patternDef.entityType];
|
|
1247
1430
|
if (typeSups) {
|
|
1248
1431
|
for (const s of typeSups) {
|
package/dist/esm/sdk.d.ts
CHANGED
|
@@ -104,6 +104,15 @@ export declare class TokenMapper {
|
|
|
104
104
|
* restored, since leaving a token behind is the worse failure.
|
|
105
105
|
*/
|
|
106
106
|
export declare function restore(text: string, tokens: Record<string, string>): string;
|
|
107
|
+
/**
|
|
108
|
+
* The entity types a caller asked to leave in the text, by canonical name.
|
|
109
|
+
*
|
|
110
|
+
* Names are matched case-insensitively and legacy names resolve (`IBAN` is
|
|
111
|
+
* `BANK_ACCOUNT`, `CREDENTIAL` is `SECRET`). A custom pattern's name counts
|
|
112
|
+
* once it is registered. Anything else throws: a typo in a type meant to be
|
|
113
|
+
* switched off would otherwise switch nothing off (rules-engine#125).
|
|
114
|
+
*/
|
|
115
|
+
export declare function normalizeExcludeTypes(values: unknown, customNames?: ReadonlySet<string>, param?: string): Set<string>;
|
|
107
116
|
export interface RedactOptions {
|
|
108
117
|
/**
|
|
109
118
|
* Scope. Detections attributed elsewhere are flagged `outOfScope`, never
|
|
@@ -144,6 +153,15 @@ export interface RedactOptions {
|
|
|
144
153
|
* EMAIL and URL only, and covers subdomains. Merged with the instance list.
|
|
145
154
|
*/
|
|
146
155
|
allowlistDomains?: string[] | null;
|
|
156
|
+
/**
|
|
157
|
+
* Entity types to leave in the text: neither in `detections` nor masked, and
|
|
158
|
+
* each such detection reported in `RedactResult.exempted` (`ruleKind:
|
|
159
|
+
* "type"`). Names are case-insensitive and legacy names resolve; unknown
|
|
160
|
+
* names throw. Merged with the instance's list. Default: nothing excluded.
|
|
161
|
+
* In `mode: "cloud"` an excluded rules type is still masked in the text sent
|
|
162
|
+
* to the service — this decides what comes back, not what may leave.
|
|
163
|
+
*/
|
|
164
|
+
excludeTypes?: string[] | null;
|
|
147
165
|
detectDates?: boolean;
|
|
148
166
|
cache?: boolean;
|
|
149
167
|
/**
|
|
@@ -161,16 +179,22 @@ export declare class EuRedact {
|
|
|
161
179
|
private maxInputLength;
|
|
162
180
|
private allowlist;
|
|
163
181
|
private allowlistDomains;
|
|
182
|
+
private customNames;
|
|
183
|
+
private excludeTypes;
|
|
164
184
|
/**
|
|
165
185
|
* @param options.maxInputLength Longest document `redact` accepts, in characters.
|
|
166
186
|
* @param options.allowlist Values never to redact, for every call on this
|
|
167
187
|
* instance — an organisation's own name, its own addresses. Merged with
|
|
168
188
|
* the per-call `allowlist`. Matched whole, case-insensitively.
|
|
189
|
+
* @param options.excludeTypes Entity types never reported or masked, for
|
|
190
|
+
* every call on this instance, e.g. `["JOB_TITLE", "FINANCIAL_AMOUNT"]`.
|
|
191
|
+
* Merged with the per-call value. Unknown names throw.
|
|
169
192
|
*/
|
|
170
193
|
constructor(options?: {
|
|
171
194
|
maxInputLength?: number;
|
|
172
195
|
allowlist?: string[] | null;
|
|
173
196
|
allowlistDomains?: string[] | null;
|
|
197
|
+
excludeTypes?: string[] | null;
|
|
174
198
|
});
|
|
175
199
|
addCustomPattern(name: string, pattern: string): void;
|
|
176
200
|
/** Clear the result cache and referential integrity mappings.
|
|
@@ -178,6 +202,8 @@ export declare class EuRedact {
|
|
|
178
202
|
clear(): void;
|
|
179
203
|
/** The instance allowlist merged with a call's, in comparison form. */
|
|
180
204
|
private allowedFor;
|
|
205
|
+
/** The instance's excluded types merged with a call's. */
|
|
206
|
+
private excludedFor;
|
|
181
207
|
/** The instance domain list merged with a call's. */
|
|
182
208
|
private domainsFor;
|
|
183
209
|
/** The label function for one call, given its output options. */
|