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/esm/sdk.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ResultCache } from "./cache.js";
|
|
|
3
3
|
import { normalize, mapOffsetToOriginal } from "./normalizer.js";
|
|
4
4
|
import { RuleEngine } from "./rules/engine.js";
|
|
5
5
|
import { weightsToRanking } from "./rules/evidence.js";
|
|
6
|
-
import { EntityType } from "./types.js";
|
|
6
|
+
import { EntityType, canonicalType } from "./types.js";
|
|
7
7
|
const DATE_TYPES = new Set([EntityType.DOB, EntityType.DATE_OF_DEATH]);
|
|
8
8
|
/**
|
|
9
9
|
* Splice a label over every detection and return the masked text.
|
|
@@ -353,6 +353,70 @@ function applyAllowlist(text, detections, allowed, domains) {
|
|
|
353
353
|
}
|
|
354
354
|
return [kept, exempted];
|
|
355
355
|
}
|
|
356
|
+
const KNOWN_TYPES = new Set(Object.values(EntityType));
|
|
357
|
+
/**
|
|
358
|
+
* The entity types a caller asked to leave in the text, by canonical name.
|
|
359
|
+
*
|
|
360
|
+
* Names are matched case-insensitively and legacy names resolve (`IBAN` is
|
|
361
|
+
* `BANK_ACCOUNT`, `CREDENTIAL` is `SECRET`). A custom pattern's name counts
|
|
362
|
+
* once it is registered. Anything else throws: a typo in a type meant to be
|
|
363
|
+
* switched off would otherwise switch nothing off (rules-engine#125).
|
|
364
|
+
*/
|
|
365
|
+
export function normalizeExcludeTypes(values, customNames = new Set(), param = "excludeTypes") {
|
|
366
|
+
const out = new Set();
|
|
367
|
+
if (values === null || values === undefined)
|
|
368
|
+
return out;
|
|
369
|
+
checkAllowlistArg(values, param);
|
|
370
|
+
for (const v of values) {
|
|
371
|
+
if (typeof v !== "string") {
|
|
372
|
+
throw new TypeError(`${param} entries must be type names, not ${typeof v}`);
|
|
373
|
+
}
|
|
374
|
+
if (customNames.has(v)) {
|
|
375
|
+
out.add(v);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const name = canonicalType(v);
|
|
379
|
+
if (!KNOWN_TYPES.has(name)) {
|
|
380
|
+
throw new Error(`unknown entity type ${JSON.stringify(v)} in ${param}. Known types: ` +
|
|
381
|
+
[...KNOWN_TYPES].sort().join(", ") +
|
|
382
|
+
(customNames.size ? `; custom patterns: ${[...customNames].sort().join(", ")}` : ""));
|
|
383
|
+
}
|
|
384
|
+
out.add(name);
|
|
385
|
+
}
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Split off the detections of the types the caller excluded. Applied to each
|
|
390
|
+
* span's final type, after ranking: dropping an excluded type's candidates
|
|
391
|
+
* earlier would let the next-best type claim the same span.
|
|
392
|
+
*/
|
|
393
|
+
function applyTypeExclusion(text, detections, excluded) {
|
|
394
|
+
if (excluded.size === 0)
|
|
395
|
+
return [detections, []];
|
|
396
|
+
const kept = [];
|
|
397
|
+
const exempted = [];
|
|
398
|
+
for (const d of detections) {
|
|
399
|
+
const name = String(d.entityType);
|
|
400
|
+
if (excluded.has(name)) {
|
|
401
|
+
exempted.push({
|
|
402
|
+
entityType: d.entityType, start: d.start, end: d.end,
|
|
403
|
+
text: text.slice(d.start, d.end), rule: name, ruleKind: "type",
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
else
|
|
407
|
+
kept.push(d);
|
|
408
|
+
}
|
|
409
|
+
return [kept, exempted];
|
|
410
|
+
}
|
|
411
|
+
/** Allowlist first, then excluded types; every exemption in text order. */
|
|
412
|
+
function exempt(text, detections, allowed, domains, excluded) {
|
|
413
|
+
const [afterValues, byValue] = applyAllowlist(text, detections, allowed, domains);
|
|
414
|
+
const [kept, byType] = applyTypeExclusion(text, afterValues, excluded);
|
|
415
|
+
const exempted = [...byValue, ...byType];
|
|
416
|
+
if (byValue.length && byType.length)
|
|
417
|
+
exempted.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
418
|
+
return [kept, exempted];
|
|
419
|
+
}
|
|
356
420
|
/** Two label schemes for the same spans cannot both apply. */
|
|
357
421
|
function checkLabelOptions(options) {
|
|
358
422
|
if (options.tokenize && options.referentialIntegrity) {
|
|
@@ -433,19 +497,26 @@ export class EuRedact {
|
|
|
433
497
|
maxInputLength;
|
|
434
498
|
allowlist;
|
|
435
499
|
allowlistDomains;
|
|
500
|
+
customNames = new Set();
|
|
501
|
+
excludeTypes;
|
|
436
502
|
/**
|
|
437
503
|
* @param options.maxInputLength Longest document `redact` accepts, in characters.
|
|
438
504
|
* @param options.allowlist Values never to redact, for every call on this
|
|
439
505
|
* instance — an organisation's own name, its own addresses. Merged with
|
|
440
506
|
* the per-call `allowlist`. Matched whole, case-insensitively.
|
|
507
|
+
* @param options.excludeTypes Entity types never reported or masked, for
|
|
508
|
+
* every call on this instance, e.g. `["JOB_TITLE", "FINANCIAL_AMOUNT"]`.
|
|
509
|
+
* Merged with the per-call value. Unknown names throw.
|
|
441
510
|
*/
|
|
442
511
|
constructor(options) {
|
|
443
512
|
this.maxInputLength = options?.maxInputLength ?? DEFAULT_MAX_INPUT_LENGTH;
|
|
444
513
|
this.allowlist = normalizeAllowlist(options?.allowlist);
|
|
445
514
|
this.allowlistDomains = normalizeDomains(options?.allowlistDomains);
|
|
515
|
+
this.excludeTypes = normalizeExcludeTypes(options?.excludeTypes);
|
|
446
516
|
}
|
|
447
517
|
addCustomPattern(name, pattern) {
|
|
448
518
|
this.engine.addCustomPattern(name, pattern);
|
|
519
|
+
this.customNames.add(name);
|
|
449
520
|
this.cache.clear();
|
|
450
521
|
}
|
|
451
522
|
/** Clear the result cache and referential integrity mappings.
|
|
@@ -461,6 +532,13 @@ export class EuRedact {
|
|
|
461
532
|
allowed.set(k, v);
|
|
462
533
|
return allowed;
|
|
463
534
|
}
|
|
535
|
+
/** The instance's excluded types merged with a call's. */
|
|
536
|
+
excludedFor(options) {
|
|
537
|
+
const excluded = new Set(this.excludeTypes);
|
|
538
|
+
for (const t of normalizeExcludeTypes(options.excludeTypes, this.customNames))
|
|
539
|
+
excluded.add(t);
|
|
540
|
+
return excluded;
|
|
541
|
+
}
|
|
464
542
|
/** The instance domain list merged with a call's. */
|
|
465
543
|
domainsFor(options) {
|
|
466
544
|
const domains = new Map(this.allowlistDomains);
|
|
@@ -517,14 +595,16 @@ export class EuRedact {
|
|
|
517
595
|
}
|
|
518
596
|
const allowed = this.allowedFor(options);
|
|
519
597
|
const domains = this.domainsFor(options);
|
|
598
|
+
const excluded = this.excludedFor(options);
|
|
520
599
|
const client = new CloudClient();
|
|
521
600
|
// Dates are always on, whatever detectDates says: that is the rules output
|
|
522
601
|
// the model was trained against, and a date of birth the rules can place
|
|
523
602
|
// has no reason to travel. It is the one ignored option that is safe to
|
|
524
603
|
// ignore — it can only cause MORE to be detected, never less.
|
|
525
604
|
//
|
|
526
|
-
// No allowlist and no tokens on this pass. An exemption
|
|
527
|
-
// caller wants back, not what may leave
|
|
605
|
+
// No allowlist, no excluded types and no tokens on this pass. An exemption
|
|
606
|
+
// says what the caller wants back, not what may leave: an excluded EMAIL is
|
|
607
|
+
// still masked in the request. All three are applied below, after the
|
|
528
608
|
// response, to the merged spans.
|
|
529
609
|
const local = this.redactRules(text, { countries, detectDates: true, cache: options.cache }, new Map(), new Map());
|
|
530
610
|
const [masked, labels] = maskForCloud(text, local.detections);
|
|
@@ -534,7 +614,7 @@ export class EuRedact {
|
|
|
534
614
|
country: countries[0], idempotencyKey: options.idempotencyKey,
|
|
535
615
|
});
|
|
536
616
|
// A new array throughout: `local` may be the cached rules result.
|
|
537
|
-
const [detections, exempted] =
|
|
617
|
+
const [detections, exempted] = exempt(text, [...local.detections, ...ontoOriginal(remote.detections, masked, text, labels, CloudError)], allowed, domains, excluded);
|
|
538
618
|
detections.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
539
619
|
const tokenMapper = options.tokenize ? new TokenMapper(text, detections) : null;
|
|
540
620
|
return {
|
|
@@ -553,6 +633,7 @@ export class EuRedact {
|
|
|
553
633
|
checkLabelOptions(options);
|
|
554
634
|
const allowed = this.allowedFor(options);
|
|
555
635
|
const domains = this.domainsFor(options);
|
|
636
|
+
const excluded = this.excludedFor(options);
|
|
556
637
|
const requestedMode = options.mode ?? "rules";
|
|
557
638
|
if (requestedMode === "cloud") {
|
|
558
639
|
// Cannot be served synchronously: the cloud tier is a network call, and
|
|
@@ -569,10 +650,10 @@ export class EuRedact {
|
|
|
569
650
|
if (options.idempotencyKey !== undefined) {
|
|
570
651
|
throw new Error('idempotencyKey applies to mode: "cloud" only: a rules-only redaction makes no request to deduplicate');
|
|
571
652
|
}
|
|
572
|
-
return this.redactRules(text, options, allowed, domains);
|
|
653
|
+
return this.redactRules(text, options, allowed, domains, excluded);
|
|
573
654
|
}
|
|
574
655
|
/** The local pipeline: normalise, detect, exempt, mask. */
|
|
575
|
-
redactRules(text, options, allowed, domains) {
|
|
656
|
+
redactRules(text, options, allowed, domains, excluded = new Set()) {
|
|
576
657
|
if (text.length > this.maxInputLength) {
|
|
577
658
|
throw new Error(`Input text length (${text.length.toLocaleString()} chars) exceeds the maximum ` +
|
|
578
659
|
`(${this.maxInputLength.toLocaleString()} chars). Split the input or ` +
|
|
@@ -595,7 +676,8 @@ export class EuRedact {
|
|
|
595
676
|
// JSON rather than a joined string: an entry may itself contain the
|
|
596
677
|
// separator, and two different lists must never share a key.
|
|
597
678
|
const allowKey = (allowed.size ? JSON.stringify([...allowed.keys()].sort()) : "")
|
|
598
|
-
+ (domains.size ? "|dom=" + JSON.stringify([...domains.keys()].sort()) : "")
|
|
679
|
+
+ (domains.size ? "|dom=" + JSON.stringify([...domains.keys()].sort()) : "")
|
|
680
|
+
+ (excluded.size ? "|excl=" + JSON.stringify([...excluded].sort()) : "");
|
|
599
681
|
const cacheMode = `rules|dates=${detectDates}|hint=${hintKey}|ri=${referentialIntegrity}|tok=${tokenize}|allow=${allowKey}`;
|
|
600
682
|
let cacheKey;
|
|
601
683
|
if (cache) {
|
|
@@ -619,7 +701,7 @@ export class EuRedact {
|
|
|
619
701
|
detections = detections.filter(d => !DATE_TYPES.has(d.entityType));
|
|
620
702
|
}
|
|
621
703
|
let exempted;
|
|
622
|
-
[detections, exempted] =
|
|
704
|
+
[detections, exempted] = exempt(text, detections, allowed, domains, excluded);
|
|
623
705
|
detections.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
624
706
|
const tokenMapper = tokenize ? new TokenMapper(text, detections) : null;
|
|
625
707
|
const redacted = applyReplacements(text, detections, this.labelFor(referentialIntegrity, tokenMapper));
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -141,7 +141,7 @@ export interface Detection {
|
|
|
141
141
|
outOfScope?: boolean;
|
|
142
142
|
}
|
|
143
143
|
/**
|
|
144
|
-
* A detection the allowlist kept out of the redacted output.
|
|
144
|
+
* A detection the allowlist or `excludeTypes` kept out of the redacted output.
|
|
145
145
|
*
|
|
146
146
|
* An exemption is a deliberate decision to leave a direct identifier in a
|
|
147
147
|
* document that otherwise claims to be redacted, so it is reported rather than
|
|
@@ -153,10 +153,12 @@ export interface Exemption {
|
|
|
153
153
|
start: number;
|
|
154
154
|
end: number;
|
|
155
155
|
text: string;
|
|
156
|
-
/** The allowlist entry that matched, as the caller wrote it
|
|
156
|
+
/** The allowlist entry that matched, as the caller wrote it; for an
|
|
157
|
+
* excluded type, the type's canonical name. */
|
|
157
158
|
rule: string;
|
|
158
|
-
/** `"value"` for the literal allowlist, `"domain"` for allowlistDomains
|
|
159
|
-
|
|
159
|
+
/** `"value"` for the literal allowlist, `"domain"` for allowlistDomains,
|
|
160
|
+
* `"type"` for excludeTypes (rules-engine#125). */
|
|
161
|
+
ruleKind: "value" | "domain" | "type";
|
|
160
162
|
}
|
|
161
163
|
/**
|
|
162
164
|
* One reason a cloud request cost what it did. `code` is one of the service's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "euredact",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "European PII redaction SDK -- fast, zero-dependency, rule-based detection and redaction of personal data across 31 European countries",
|
|
5
5
|
"homepage": "https://euredact.dev",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
60
60
|
"postbuild:cjs": "echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
|
|
61
61
|
"build": "rm -rf dist && npm run build:esm && npm run build:cjs",
|
|
62
|
-
"test": "npm run test:spec && npm run test:inference && npm run test:cloud && npm run test:cloud-api && npm run test:tokenize && npm run test:allowlist && npm run test:eval-scoring && npm run test:platform && npm run test:batches && npm run test:conformance",
|
|
62
|
+
"test": "npm run test:spec && npm run test:inference && npm run test:cloud && npm run test:cloud-api && npm run test:tokenize && npm run test:allowlist && npm run test:exclude-types && npm run test:eval-scoring && npm run test:platform && npm run test:batches && npm run test:conformance",
|
|
63
63
|
"benchmark": "node --import tsx src/__tests__/benchmark.ts",
|
|
64
64
|
"prepublishOnly": "npm run build",
|
|
65
65
|
"test:spec": "node --import tsx src/__tests__/spec.ts",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"test:cloud-api": "node --import tsx src/__tests__/cloudApi.ts",
|
|
73
73
|
"test:tokenize": "node --import tsx src/__tests__/tokenize.ts",
|
|
74
74
|
"test:allowlist": "node --import tsx src/__tests__/allowlist.ts",
|
|
75
|
+
"test:exclude-types": "node --import tsx src/__tests__/excludeTypes.ts",
|
|
75
76
|
"test:eval-scoring": "node --import tsx src/__tests__/evalScoring.ts",
|
|
76
77
|
"test:platform": "node --import tsx src/__tests__/platform.ts",
|
|
77
78
|
"test:batches": "node --import tsx src/__tests__/batches.ts"
|