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.
Files changed (57) hide show
  1. package/NOTICE +11 -0
  2. package/README.md +455 -44
  3. package/dist/cjs/cache.js +2 -8
  4. package/dist/cjs/cloud/account.js +180 -0
  5. package/dist/cjs/cloud/batches.js +469 -0
  6. package/dist/cjs/cloud/client.js +124 -14
  7. package/dist/cjs/cloud/config.js +28 -2
  8. package/dist/cjs/cloud/errors.js +39 -2
  9. package/dist/cjs/cloud/index.js +16 -1
  10. package/dist/cjs/index.js +9 -1
  11. package/dist/cjs/platform.browser.js +98 -0
  12. package/dist/cjs/platform.js +140 -0
  13. package/dist/cjs/rules/bicGleif.js +57 -0
  14. package/dist/cjs/rules/bicRegistry.js +37 -11
  15. package/dist/cjs/rules/countries/index.js +83 -13
  16. package/dist/cjs/rules/cues.js +29 -9
  17. package/dist/cjs/rules/engine.js +14 -6
  18. package/dist/cjs/rules/suppressors.js +479 -26
  19. package/dist/cjs/rules/validators.js +50 -0
  20. package/dist/cjs/sdk.js +108 -14
  21. package/dist/cjs/types.js +4 -1
  22. package/dist/esm/cache.js +2 -8
  23. package/dist/esm/cloud/account.d.ts +155 -0
  24. package/dist/esm/cloud/account.js +175 -0
  25. package/dist/esm/cloud/batches.d.ts +166 -0
  26. package/dist/esm/cloud/batches.js +464 -0
  27. package/dist/esm/cloud/client.d.ts +83 -1
  28. package/dist/esm/cloud/client.js +119 -22
  29. package/dist/esm/cloud/config.d.ts +6 -0
  30. package/dist/esm/cloud/config.js +27 -2
  31. package/dist/esm/cloud/errors.d.ts +26 -1
  32. package/dist/esm/cloud/errors.js +35 -1
  33. package/dist/esm/cloud/index.d.ts +4 -2
  34. package/dist/esm/cloud/index.js +4 -2
  35. package/dist/esm/index.d.ts +2 -2
  36. package/dist/esm/index.js +1 -1
  37. package/dist/esm/platform.browser.d.ts +27 -0
  38. package/dist/esm/platform.browser.js +92 -0
  39. package/dist/esm/platform.d.ts +20 -0
  40. package/dist/esm/platform.js +101 -0
  41. package/dist/esm/rules/bicGleif.d.ts +4 -0
  42. package/dist/esm/rules/bicGleif.js +53 -0
  43. package/dist/esm/rules/bicRegistry.d.ts +25 -11
  44. package/dist/esm/rules/bicRegistry.js +36 -11
  45. package/dist/esm/rules/countries/index.js +83 -13
  46. package/dist/esm/rules/cues.d.ts +1 -1
  47. package/dist/esm/rules/cues.js +29 -9
  48. package/dist/esm/rules/engine.js +14 -6
  49. package/dist/esm/rules/suppressors.d.ts +1 -0
  50. package/dist/esm/rules/suppressors.js +478 -25
  51. package/dist/esm/rules/validators.d.ts +14 -0
  52. package/dist/esm/rules/validators.js +47 -0
  53. package/dist/esm/sdk.d.ts +77 -4
  54. package/dist/esm/sdk.js +108 -17
  55. package/dist/esm/types.d.ts +64 -6
  56. package/dist/esm/types.js +4 -1
  57. 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
- const node_crypto_1 = require("node:crypto");
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 never spells
181
- * a word; no 0/1/I/O, so it survives being read back by a person; no
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 = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
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, node_crypto_1.randomInt)(exports.TOKEN_ALPHABET.length)];
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 says what the
563
- // caller wants back, not what may leave; both are applied below, after the
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, { country: countries[0] });
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] = applyAllowlist(text, [...local.detections, ...ontoOriginal(remote.detections, masked, text, labels, CloudError)], allowed, domains);
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
- return this.redactRules(text, options, allowed, domains);
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] = applyAllowlist(text, detections, allowed, domains);
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
- EntityType["CREDENTIAL"] = "CREDENTIAL";
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 { createHash } from "node:crypto";
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 createHash("sha256")
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 {};