euredact 0.6.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/NOTICE +11 -0
- package/README.md +455 -44
- package/dist/cjs/cache.js +2 -8
- package/dist/cjs/cloud/account.js +180 -0
- package/dist/cjs/cloud/batches.js +469 -0
- package/dist/cjs/cloud/client.js +124 -14
- package/dist/cjs/cloud/config.js +28 -2
- package/dist/cjs/cloud/errors.js +39 -2
- package/dist/cjs/cloud/index.js +16 -1
- package/dist/cjs/index.js +9 -1
- package/dist/cjs/platform.browser.js +98 -0
- package/dist/cjs/platform.js +140 -0
- package/dist/cjs/rules/bicGleif.js +57 -0
- package/dist/cjs/rules/bicRegistry.js +37 -11
- package/dist/cjs/rules/countries/index.js +83 -13
- package/dist/cjs/rules/cues.js +29 -9
- package/dist/cjs/rules/engine.js +14 -6
- package/dist/cjs/rules/suppressors.js +479 -26
- package/dist/cjs/rules/validators.js +50 -0
- package/dist/cjs/sdk.js +108 -14
- package/dist/cjs/types.js +4 -1
- package/dist/esm/cache.js +2 -8
- package/dist/esm/cloud/account.d.ts +155 -0
- package/dist/esm/cloud/account.js +175 -0
- package/dist/esm/cloud/batches.d.ts +166 -0
- package/dist/esm/cloud/batches.js +464 -0
- package/dist/esm/cloud/client.d.ts +83 -1
- package/dist/esm/cloud/client.js +119 -22
- package/dist/esm/cloud/config.d.ts +6 -0
- package/dist/esm/cloud/config.js +27 -2
- package/dist/esm/cloud/errors.d.ts +26 -1
- package/dist/esm/cloud/errors.js +35 -1
- package/dist/esm/cloud/index.d.ts +4 -2
- package/dist/esm/cloud/index.js +4 -2
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/platform.browser.d.ts +27 -0
- package/dist/esm/platform.browser.js +92 -0
- package/dist/esm/platform.d.ts +20 -0
- package/dist/esm/platform.js +101 -0
- package/dist/esm/rules/bicGleif.d.ts +4 -0
- package/dist/esm/rules/bicGleif.js +53 -0
- package/dist/esm/rules/bicRegistry.d.ts +25 -11
- package/dist/esm/rules/bicRegistry.js +36 -11
- package/dist/esm/rules/countries/index.js +83 -13
- package/dist/esm/rules/cues.d.ts +1 -1
- package/dist/esm/rules/cues.js +29 -9
- package/dist/esm/rules/engine.js +14 -6
- package/dist/esm/rules/suppressors.d.ts +1 -0
- package/dist/esm/rules/suppressors.js +478 -25
- package/dist/esm/rules/validators.d.ts +14 -0
- package/dist/esm/rules/validators.js +47 -0
- package/dist/esm/sdk.d.ts +77 -4
- package/dist/esm/sdk.js +108 -17
- package/dist/esm/types.d.ts +64 -6
- package/dist/esm/types.js +4 -1
- package/package.json +13 -4
|
@@ -32,7 +32,10 @@ exports.validateSpanishDni = validateSpanishDni;
|
|
|
32
32
|
exports.validateSpanishNie = validateSpanishNie;
|
|
33
33
|
exports.validatePortugueseNif = validatePortugueseNif;
|
|
34
34
|
exports.validatePolishPesel = validatePolishPesel;
|
|
35
|
+
exports.validatePolishNrb = validatePolishNrb;
|
|
35
36
|
exports.validatePolishNip = validatePolishNip;
|
|
37
|
+
exports.validatePolishIdCard = validatePolishIdCard;
|
|
38
|
+
exports.validatePolishRegon = validatePolishRegon;
|
|
36
39
|
exports.validateCzechBirthNumber = validateCzechBirthNumber;
|
|
37
40
|
exports.validateRomanianCnp = validateRomanianCnp;
|
|
38
41
|
exports.validateHungarianTaj = validateHungarianTaj;
|
|
@@ -538,6 +541,13 @@ function validatePolishPesel(candidate) {
|
|
|
538
541
|
total += d[i] * weights[i];
|
|
539
542
|
return d[10] === (10 - (total % 10)) % 10;
|
|
540
543
|
}
|
|
544
|
+
/** Polish domestic account number (NRB): the PL IBAN without its "PL", so the IBAN's mod-97 decides it (rules-engine#93). */
|
|
545
|
+
function validatePolishNrb(candidate) {
|
|
546
|
+
const c = candidate.replace(/[\s\-]/g, "");
|
|
547
|
+
if (!/^\d{26}$/.test(c))
|
|
548
|
+
return false;
|
|
549
|
+
return validateIban("PL" + c);
|
|
550
|
+
}
|
|
541
551
|
function validatePolishNip(candidate) {
|
|
542
552
|
const c = candidate.replace(/[\s\-]/g, "");
|
|
543
553
|
if (c.length !== 10 || !/^\d{10}$/.test(c))
|
|
@@ -549,6 +559,43 @@ function validatePolishNip(candidate) {
|
|
|
549
559
|
total += d[i] * weights[i];
|
|
550
560
|
return total % 11 === d[9];
|
|
551
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* Polish identity card (dowód osobisty): 3 letters + 6 digits, the first digit
|
|
564
|
+
* a check digit. Letters A=10 … Z=35, weights 7,3,1,9,7,3,1,7,3: the weighted
|
|
565
|
+
* sum of all nine is 0 mod 10 exactly when the check digit is right (rules-engine#75).
|
|
566
|
+
*/
|
|
567
|
+
function validatePolishIdCard(candidate) {
|
|
568
|
+
const c = candidate.replace(/\s/g, "").toUpperCase();
|
|
569
|
+
if (!/^[A-Z]{3}\d{6}$/.test(c))
|
|
570
|
+
return false;
|
|
571
|
+
const weights = [7, 3, 1, 9, 7, 3, 1, 7, 3];
|
|
572
|
+
let total = 0;
|
|
573
|
+
for (let i = 0; i < 9; i++) {
|
|
574
|
+
const ch = c[i];
|
|
575
|
+
total += (/[A-Z]/.test(ch) ? ch.charCodeAt(0) - 55 : Number(ch)) * weights[i];
|
|
576
|
+
}
|
|
577
|
+
return total % 10 === 0;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Polish REGON: 9 digits, or 14 for a local unit, each with a mod-11 check (a
|
|
581
|
+
* remainder of 10 counts as 0). A 14-digit REGON begins with its parent's valid
|
|
582
|
+
* 9-digit REGON (rules-engine#76).
|
|
583
|
+
*/
|
|
584
|
+
function validatePolishRegon(candidate) {
|
|
585
|
+
const c = candidate.replace(/[\s\-]/g, "");
|
|
586
|
+
if (!/^\d+$/.test(c) || (c.length !== 9 && c.length !== 14))
|
|
587
|
+
return false;
|
|
588
|
+
const check = (digits, weights) => {
|
|
589
|
+
let total = 0;
|
|
590
|
+
for (let i = 0; i < weights.length; i++)
|
|
591
|
+
total += Number(digits[i]) * weights[i];
|
|
592
|
+
const rest = total % 11;
|
|
593
|
+
return (rest === 10 ? 0 : rest) === Number(digits[weights.length]);
|
|
594
|
+
};
|
|
595
|
+
if (!check(c.slice(0, 9), [8, 9, 2, 3, 4, 5, 6, 7]))
|
|
596
|
+
return false;
|
|
597
|
+
return c.length === 9 || check(c, [2, 4, 8, 5, 0, 9, 7, 3, 6, 1, 2, 4, 8]);
|
|
598
|
+
}
|
|
552
599
|
// Days per month for the rodné číslo date check. February is 29 because the
|
|
553
600
|
// century is not recoverable from a two-digit year, so a leap year cannot be
|
|
554
601
|
// ruled out -- being permissive by one day is the right direction here.
|
|
@@ -799,6 +846,9 @@ exports.VALIDATORS = {
|
|
|
799
846
|
portuguese_nif: validatePortugueseNif,
|
|
800
847
|
polish_pesel: validatePolishPesel,
|
|
801
848
|
polish_nip: validatePolishNip,
|
|
849
|
+
polish_nrb: validatePolishNrb,
|
|
850
|
+
polish_id_card: validatePolishIdCard,
|
|
851
|
+
polish_regon: validatePolishRegon,
|
|
802
852
|
czech_birth_number: validateCzechBirthNumber,
|
|
803
853
|
romanian_cnp: validateRomanianCnp,
|
|
804
854
|
hungarian_taj: validateHungarianTaj,
|
package/dist/cjs/sdk.js
CHANGED
|
@@ -35,8 +35,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.EuRedact = exports.TokenMapper = exports.TOKEN_SUFFIX_LENGTH = exports.TOKEN_ALPHABET = void 0;
|
|
37
37
|
exports.applyReplacements = applyReplacements;
|
|
38
|
+
exports.maskForCloud = maskForCloud;
|
|
39
|
+
exports.ontoOriginal = ontoOriginal;
|
|
38
40
|
exports.restore = restore;
|
|
39
|
-
|
|
41
|
+
exports.normalizeExcludeTypes = normalizeExcludeTypes;
|
|
42
|
+
const platform_js_1 = require("./platform.js");
|
|
40
43
|
const cache_js_1 = require("./cache.js");
|
|
41
44
|
const normalizer_js_1 = require("./normalizer.js");
|
|
42
45
|
const engine_js_1 = require("./rules/engine.js");
|
|
@@ -98,6 +101,7 @@ function applyReplacements(text, detections, labelFor) {
|
|
|
98
101
|
* do not list it". A token or a numbered label would be read as ordinary text,
|
|
99
102
|
* so the format on the wire is a model contract and not an output option.
|
|
100
103
|
*/
|
|
104
|
+
/** @internal Shared with cloud/batches.ts. */
|
|
101
105
|
function maskForCloud(text, detections) {
|
|
102
106
|
const parts = [];
|
|
103
107
|
const labels = [];
|
|
@@ -148,6 +152,7 @@ function labelBefore(labels, offset, strict) {
|
|
|
148
152
|
* a span cannot be placed, and masking where it points would cover the wrong
|
|
149
153
|
* characters and leave the right ones in the clear.
|
|
150
154
|
*/
|
|
155
|
+
/** @internal Shared with cloud/batches.ts. */
|
|
151
156
|
function ontoOriginal(spans, masked, text, labels, Unplaceable) {
|
|
152
157
|
const placed = [];
|
|
153
158
|
for (const span of spans) {
|
|
@@ -177,11 +182,12 @@ function ontoOriginal(spans, masked, text, labels, Unplaceable) {
|
|
|
177
182
|
return placed;
|
|
178
183
|
}
|
|
179
184
|
/**
|
|
180
|
-
* Characters a token suffix is drawn from. No vowels, so a suffix
|
|
181
|
-
* a word; no 0/1/I/O, so it survives being read back by a person;
|
|
182
|
-
* underscore, so the type prefix stays unambiguous.
|
|
185
|
+
* Characters a token suffix is drawn from. No vowels (and no Y), so a suffix
|
|
186
|
+
* never spells a word; no 0/1/I/O, so it survives being read back by a person;
|
|
187
|
+
* no underscore, so the type prefix stays unambiguous. A, E, U and Y were in it
|
|
188
|
+
* until rules-engine#55, against this comment.
|
|
183
189
|
*/
|
|
184
|
-
exports.TOKEN_ALPHABET = "
|
|
190
|
+
exports.TOKEN_ALPHABET = "BCDFGHJKLMNPQRSTVWXZ23456789";
|
|
185
191
|
exports.TOKEN_SUFFIX_LENGTH = 4;
|
|
186
192
|
const TOKEN_MAX_DRAWS = 100;
|
|
187
193
|
function escapeRegExp(s) {
|
|
@@ -229,7 +235,7 @@ class TokenMapper {
|
|
|
229
235
|
}
|
|
230
236
|
let suffix = "";
|
|
231
237
|
for (let i = 0; i < exports.TOKEN_SUFFIX_LENGTH; i++) {
|
|
232
|
-
suffix += exports.TOKEN_ALPHABET[(0,
|
|
238
|
+
suffix += exports.TOKEN_ALPHABET[(0, platform_js_1.randomIndex)(exports.TOKEN_ALPHABET.length)];
|
|
233
239
|
}
|
|
234
240
|
token = prefix + suffix;
|
|
235
241
|
} while (this.taken.has(token));
|
|
@@ -389,6 +395,70 @@ function applyAllowlist(text, detections, allowed, domains) {
|
|
|
389
395
|
}
|
|
390
396
|
return [kept, exempted];
|
|
391
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
|
+
}
|
|
392
462
|
/** Two label schemes for the same spans cannot both apply. */
|
|
393
463
|
function checkLabelOptions(options) {
|
|
394
464
|
if (options.tokenize && options.referentialIntegrity) {
|
|
@@ -469,19 +539,26 @@ class EuRedact {
|
|
|
469
539
|
maxInputLength;
|
|
470
540
|
allowlist;
|
|
471
541
|
allowlistDomains;
|
|
542
|
+
customNames = new Set();
|
|
543
|
+
excludeTypes;
|
|
472
544
|
/**
|
|
473
545
|
* @param options.maxInputLength Longest document `redact` accepts, in characters.
|
|
474
546
|
* @param options.allowlist Values never to redact, for every call on this
|
|
475
547
|
* instance — an organisation's own name, its own addresses. Merged with
|
|
476
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.
|
|
477
552
|
*/
|
|
478
553
|
constructor(options) {
|
|
479
554
|
this.maxInputLength = options?.maxInputLength ?? DEFAULT_MAX_INPUT_LENGTH;
|
|
480
555
|
this.allowlist = normalizeAllowlist(options?.allowlist);
|
|
481
556
|
this.allowlistDomains = normalizeDomains(options?.allowlistDomains);
|
|
557
|
+
this.excludeTypes = normalizeExcludeTypes(options?.excludeTypes);
|
|
482
558
|
}
|
|
483
559
|
addCustomPattern(name, pattern) {
|
|
484
560
|
this.engine.addCustomPattern(name, pattern);
|
|
561
|
+
this.customNames.add(name);
|
|
485
562
|
this.cache.clear();
|
|
486
563
|
}
|
|
487
564
|
/** Clear the result cache and referential integrity mappings.
|
|
@@ -497,6 +574,13 @@ class EuRedact {
|
|
|
497
574
|
allowed.set(k, v);
|
|
498
575
|
return allowed;
|
|
499
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
|
+
}
|
|
500
584
|
/** The instance domain list merged with a call's. */
|
|
501
585
|
domainsFor(options) {
|
|
502
586
|
const domains = new Map(this.allowlistDomains);
|
|
@@ -553,22 +637,26 @@ class EuRedact {
|
|
|
553
637
|
}
|
|
554
638
|
const allowed = this.allowedFor(options);
|
|
555
639
|
const domains = this.domainsFor(options);
|
|
640
|
+
const excluded = this.excludedFor(options);
|
|
556
641
|
const client = new CloudClient();
|
|
557
642
|
// Dates are always on, whatever detectDates says: that is the rules output
|
|
558
643
|
// the model was trained against, and a date of birth the rules can place
|
|
559
644
|
// has no reason to travel. It is the one ignored option that is safe to
|
|
560
645
|
// ignore — it can only cause MORE to be detected, never less.
|
|
561
646
|
//
|
|
562
|
-
// No allowlist and no tokens on this pass. An exemption
|
|
563
|
-
// 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
|
|
564
650
|
// response, to the merged spans.
|
|
565
651
|
const local = this.redactRules(text, { countries, detectDates: true, cache: options.cache }, new Map(), new Map());
|
|
566
652
|
const [masked, labels] = maskForCloud(text, local.detections);
|
|
567
653
|
// The client converts the service's code-point offsets to UTF-16 units of
|
|
568
654
|
// the text it sent, so everything from here on is in one unit.
|
|
569
|
-
const remote = await client.redact(masked, {
|
|
655
|
+
const remote = await client.redact(masked, {
|
|
656
|
+
country: countries[0], idempotencyKey: options.idempotencyKey,
|
|
657
|
+
});
|
|
570
658
|
// A new array throughout: `local` may be the cached rules result.
|
|
571
|
-
const [detections, exempted] =
|
|
659
|
+
const [detections, exempted] = exempt(text, [...local.detections, ...ontoOriginal(remote.detections, masked, text, labels, CloudError)], allowed, domains, excluded);
|
|
572
660
|
detections.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
573
661
|
const tokenMapper = options.tokenize ? new TokenMapper(text, detections) : null;
|
|
574
662
|
return {
|
|
@@ -578,6 +666,7 @@ class EuRedact {
|
|
|
578
666
|
source: "cloud",
|
|
579
667
|
tokens: tokenMapper ? tokenMapper.tokens : {},
|
|
580
668
|
exempted,
|
|
669
|
+
...(remote.cloud ? { cloud: remote.cloud } : {}),
|
|
581
670
|
};
|
|
582
671
|
}
|
|
583
672
|
redact(text, options = {}) {
|
|
@@ -586,6 +675,7 @@ class EuRedact {
|
|
|
586
675
|
checkLabelOptions(options);
|
|
587
676
|
const allowed = this.allowedFor(options);
|
|
588
677
|
const domains = this.domainsFor(options);
|
|
678
|
+
const excluded = this.excludedFor(options);
|
|
589
679
|
const requestedMode = options.mode ?? "rules";
|
|
590
680
|
if (requestedMode === "cloud") {
|
|
591
681
|
// Cannot be served synchronously: the cloud tier is a network call, and
|
|
@@ -599,10 +689,13 @@ class EuRedact {
|
|
|
599
689
|
if (requestedMode !== "rules") {
|
|
600
690
|
throw new Error(`unknown mode ${JSON.stringify(requestedMode)}: expected "rules" or "cloud"`);
|
|
601
691
|
}
|
|
602
|
-
|
|
692
|
+
if (options.idempotencyKey !== undefined) {
|
|
693
|
+
throw new Error('idempotencyKey applies to mode: "cloud" only: a rules-only redaction makes no request to deduplicate');
|
|
694
|
+
}
|
|
695
|
+
return this.redactRules(text, options, allowed, domains, excluded);
|
|
603
696
|
}
|
|
604
697
|
/** The local pipeline: normalise, detect, exempt, mask. */
|
|
605
|
-
redactRules(text, options, allowed, domains) {
|
|
698
|
+
redactRules(text, options, allowed, domains, excluded = new Set()) {
|
|
606
699
|
if (text.length > this.maxInputLength) {
|
|
607
700
|
throw new Error(`Input text length (${text.length.toLocaleString()} chars) exceeds the maximum ` +
|
|
608
701
|
`(${this.maxInputLength.toLocaleString()} chars). Split the input or ` +
|
|
@@ -625,7 +718,8 @@ class EuRedact {
|
|
|
625
718
|
// JSON rather than a joined string: an entry may itself contain the
|
|
626
719
|
// separator, and two different lists must never share a key.
|
|
627
720
|
const allowKey = (allowed.size ? JSON.stringify([...allowed.keys()].sort()) : "")
|
|
628
|
-
+ (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()) : "");
|
|
629
723
|
const cacheMode = `rules|dates=${detectDates}|hint=${hintKey}|ri=${referentialIntegrity}|tok=${tokenize}|allow=${allowKey}`;
|
|
630
724
|
let cacheKey;
|
|
631
725
|
if (cache) {
|
|
@@ -649,7 +743,7 @@ class EuRedact {
|
|
|
649
743
|
detections = detections.filter(d => !DATE_TYPES.has(d.entityType));
|
|
650
744
|
}
|
|
651
745
|
let exempted;
|
|
652
|
-
[detections, exempted] =
|
|
746
|
+
[detections, exempted] = exempt(text, detections, allowed, domains, excluded);
|
|
653
747
|
detections.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
654
748
|
const tokenMapper = tokenize ? new TokenMapper(text, detections) : null;
|
|
655
749
|
const redacted = applyReplacements(text, detections, this.labelFor(referentialIntegrity, tokenMapper));
|
package/dist/cjs/types.js
CHANGED
|
@@ -79,7 +79,8 @@ var EntityType;
|
|
|
79
79
|
EntityType["BIOMETRIC_REF"] = "BIOMETRIC_REF";
|
|
80
80
|
EntityType["FINANCIAL_AMOUNT"] = "FINANCIAL_AMOUNT";
|
|
81
81
|
EntityType["QUASI_IDENTIFIER"] = "QUASI_IDENTIFIER";
|
|
82
|
-
|
|
82
|
+
/** Legacy alias of SECRET: every credential, the model's included, is SECRET (rules-engine#111). */
|
|
83
|
+
EntityType["CREDENTIAL"] = "SECRET";
|
|
83
84
|
EntityType["URL"] = "URL";
|
|
84
85
|
EntityType["OTHER"] = "OTHER";
|
|
85
86
|
})(EntityType || (exports.EntityType = EntityType = {}));
|
|
@@ -93,6 +94,8 @@ exports.LEGACY_TYPE_ALIASES = {
|
|
|
93
94
|
NAME: "PERSON_NAME",
|
|
94
95
|
STREET_ADDRESS: "ADDRESS",
|
|
95
96
|
NATIONALITY_ETHNICITY: "SENSITIVE_ATTRIBUTE",
|
|
97
|
+
// The model labels prose credentials CREDENTIAL; the SDK reports every credential as SECRET (rules-engine#111).
|
|
98
|
+
CREDENTIAL: "SECRET",
|
|
96
99
|
};
|
|
97
100
|
/**
|
|
98
101
|
* Resolve a possibly-legacy type name to its canonical form.
|
package/dist/esm/cache.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sha256Hex } from "./platform.js";
|
|
2
2
|
// Default ceiling on retained characters, not entries. A count-only cap of
|
|
3
3
|
// 1024 entries says nothing about memory: 1024 cached 1 MB documents is a
|
|
4
4
|
// gigabyte of PII-bearing text held live. ~16M characters is a few tens of MB.
|
|
@@ -33,13 +33,7 @@ export class ResultCache {
|
|
|
33
33
|
// deployment an attacker who can guess a victim's document could submit a
|
|
34
34
|
// same-length colliding one and be served the victim's cached result,
|
|
35
35
|
// detections and raw matched PII included.
|
|
36
|
-
return
|
|
37
|
-
.update(text)
|
|
38
|
-
.update("|")
|
|
39
|
-
.update(sorted.join("|"))
|
|
40
|
-
.update("|")
|
|
41
|
-
.update(mode)
|
|
42
|
-
.digest("hex");
|
|
36
|
+
return sha256Hex([text, "|", sorted.join("|"), "|", mode]);
|
|
43
37
|
}
|
|
44
38
|
get(key) {
|
|
45
39
|
if (!this.enabled)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [CLOUD EXTENSION] The account and job endpoints an API key may call.
|
|
3
|
+
*
|
|
4
|
+
* Everything a customer's key can do on the service is reachable from the SDK
|
|
5
|
+
* (rules-engine#89): redaction (`CloudClient`, `redactAsync`), batches
|
|
6
|
+
* (`Batches`), a past job (`Jobs`) and the account itself (`Account`). What
|
|
7
|
+
* needs a signed-in person -- logging in, minting keys, changing settings,
|
|
8
|
+
* members, accepting terms -- is not here, because a key cannot do it.
|
|
9
|
+
*
|
|
10
|
+
* Every typed value carries `raw`, the service's JSON, so a field the SDK does
|
|
11
|
+
* not know yet is still reachable.
|
|
12
|
+
*/
|
|
13
|
+
import { type CloudConfig } from "./config.js";
|
|
14
|
+
import type { RedactResult } from "../types.js";
|
|
15
|
+
type Json = Record<string, unknown>;
|
|
16
|
+
/** `GET /v1/account`: plan limits, retention and usage against quota. */
|
|
17
|
+
export interface AccountSummary {
|
|
18
|
+
tenantId: string | null;
|
|
19
|
+
tenantName: string | null;
|
|
20
|
+
active: boolean | null;
|
|
21
|
+
memberSince: string | null;
|
|
22
|
+
role: string | null;
|
|
23
|
+
/** `"ttl"`, or `"none"` when text is discarded on delivery. */
|
|
24
|
+
retentionMode: string | null;
|
|
25
|
+
payloadTtlHours: number | null;
|
|
26
|
+
documentsToday: number | null;
|
|
27
|
+
documentsThisMonth: number | null;
|
|
28
|
+
tokensThisMonth: number | null;
|
|
29
|
+
failures24h: number | null;
|
|
30
|
+
/** `null` for an account without a daily quota. */
|
|
31
|
+
dailyQuota: number | null;
|
|
32
|
+
quotaRemaining: number | null;
|
|
33
|
+
activeKeys: number | null;
|
|
34
|
+
batchesAvailable: boolean | null;
|
|
35
|
+
raw: Json;
|
|
36
|
+
}
|
|
37
|
+
/** `GET /v1/account/credits`: the balance, in credits. */
|
|
38
|
+
export interface Credits {
|
|
39
|
+
balance: number | null;
|
|
40
|
+
granted: number | null;
|
|
41
|
+
spent: number | null;
|
|
42
|
+
unit: string | null;
|
|
43
|
+
raw: Json;
|
|
44
|
+
}
|
|
45
|
+
/** One line of the credit history: movements rolled up per minute, reason and direction, newest first. */
|
|
46
|
+
export interface CreditEntry {
|
|
47
|
+
at: string | null;
|
|
48
|
+
/** Negative for a debit. */
|
|
49
|
+
delta: number | null;
|
|
50
|
+
reason: string | null;
|
|
51
|
+
count: number | null;
|
|
52
|
+
jobId: string | null;
|
|
53
|
+
sourceRef: string | null;
|
|
54
|
+
raw: Json;
|
|
55
|
+
}
|
|
56
|
+
/** One day of `GET /v1/account/usage`, or of one key's series. */
|
|
57
|
+
export interface UsageDay {
|
|
58
|
+
day: string | null;
|
|
59
|
+
documents: number | null;
|
|
60
|
+
tokens: number | null;
|
|
61
|
+
failures: number | null;
|
|
62
|
+
rulesOnly: number | null;
|
|
63
|
+
raw: Json;
|
|
64
|
+
}
|
|
65
|
+
/** One key of `GET /v1/account/usage/by-key`; the rest are rolled into one with `keyId` null. */
|
|
66
|
+
export interface KeyUsage {
|
|
67
|
+
keyId: number | null;
|
|
68
|
+
name: string | null;
|
|
69
|
+
documents: number | null;
|
|
70
|
+
tokens: number | null;
|
|
71
|
+
failures: number | null;
|
|
72
|
+
series: UsageDay[];
|
|
73
|
+
raw: Json;
|
|
74
|
+
}
|
|
75
|
+
/** One key of `GET /v1/account/keys`. Never its secret. */
|
|
76
|
+
export interface ApiKey {
|
|
77
|
+
id: number | null;
|
|
78
|
+
name: string | null;
|
|
79
|
+
createdBy: string | null;
|
|
80
|
+
createdAt: string | null;
|
|
81
|
+
lastUsedAt: string | null;
|
|
82
|
+
revokedAt: string | null;
|
|
83
|
+
active: boolean | null;
|
|
84
|
+
raw: Json;
|
|
85
|
+
}
|
|
86
|
+
/** The answer to `POST /v1/account/keys/{id}/revoke`. */
|
|
87
|
+
export interface KeyRevocation {
|
|
88
|
+
id: number | null;
|
|
89
|
+
keysRemaining: number | null;
|
|
90
|
+
/** True when no active key is left: nothing can call the service until a person mints one. */
|
|
91
|
+
lockedOut: boolean | null;
|
|
92
|
+
/** True when the key revoked is the one that made this call. */
|
|
93
|
+
selfRevoked: boolean | null;
|
|
94
|
+
raw: Json;
|
|
95
|
+
}
|
|
96
|
+
/** `GET /v1/jobs/{id}`: a job's state, and its result once it has one. */
|
|
97
|
+
export interface Job {
|
|
98
|
+
jobId: string;
|
|
99
|
+
/** `queued`, `running` or `succeeded`. A failed or rejected job throws instead. */
|
|
100
|
+
status: string;
|
|
101
|
+
createdAt: string | null;
|
|
102
|
+
/**
|
|
103
|
+
* Set when the job succeeded. Its text and offsets are those of the text the
|
|
104
|
+
* service received -- under the local-first cloud mode, the locally masked
|
|
105
|
+
* text, not the caller's original.
|
|
106
|
+
*/
|
|
107
|
+
result: RedactResult | null;
|
|
108
|
+
raw: Json;
|
|
109
|
+
}
|
|
110
|
+
export interface ApiClientOptions {
|
|
111
|
+
/** Injectable for tests. Defaults to the platform `fetch`. */
|
|
112
|
+
fetchImpl?: typeof fetch;
|
|
113
|
+
}
|
|
114
|
+
/** A JSON client: GETs are retried like the redact call; writes are sent once. */
|
|
115
|
+
declare class ApiClient {
|
|
116
|
+
readonly config: CloudConfig;
|
|
117
|
+
private readonly fetchImpl?;
|
|
118
|
+
constructor(config?: CloudConfig | null, options?: ApiClientOptions);
|
|
119
|
+
protected call(method: "GET" | "POST", path: string, params?: Record<string, number>, retry?: boolean): Promise<Json>;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The account an API key belongs to: summary, credits, usage and keys.
|
|
123
|
+
*
|
|
124
|
+
* These calls pass with an API key while the service accepts account calls
|
|
125
|
+
* without a browser `Origin` (its `EUREDACT_CONSOLE_REQUIRE_ORIGIN=0`); a
|
|
126
|
+
* refusal arrives as a `CloudError` with status 403.
|
|
127
|
+
*/
|
|
128
|
+
export declare class Account extends ApiClient {
|
|
129
|
+
summary(): Promise<AccountSummary>;
|
|
130
|
+
credits(): Promise<Credits>;
|
|
131
|
+
/** Recent credit movements, newest first (1-500). */
|
|
132
|
+
creditHistory(limit?: number): Promise<CreditEntry[]>;
|
|
133
|
+
/** Documents, tokens and failures per day over the last `days`. */
|
|
134
|
+
usage(days?: number): Promise<UsageDay[]>;
|
|
135
|
+
/** The same window, split by the key that submitted the work. */
|
|
136
|
+
usageByKey(days?: number): Promise<KeyUsage[]>;
|
|
137
|
+
keys(): Promise<ApiKey[]>;
|
|
138
|
+
/**
|
|
139
|
+
* Revoke a key, e.g. one that leaked: the one write a key may make. Sent once,
|
|
140
|
+
* never retried. Already revoked throws a `CloudError` with status 409; a key
|
|
141
|
+
* that does not exist, or is another account's, throws `NotFoundError`.
|
|
142
|
+
* Revoking the key this client uses works, and is reported by `selfRevoked`.
|
|
143
|
+
*/
|
|
144
|
+
revokeKey(keyId: number): Promise<KeyRevocation>;
|
|
145
|
+
}
|
|
146
|
+
/** Past jobs, by id. */
|
|
147
|
+
export declare class Jobs extends ApiClient {
|
|
148
|
+
/**
|
|
149
|
+
* A job's state, and its result if it has one. Throws `ResultExpiredError`
|
|
150
|
+
* when the job succeeded but its result is no longer retained, and
|
|
151
|
+
* `NotFoundError` for an unknown job (or another account's).
|
|
152
|
+
*/
|
|
153
|
+
retrieve(jobId: string): Promise<Job>;
|
|
154
|
+
}
|
|
155
|
+
export {};
|