euredact 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/NOTICE +11 -0
  2. package/README.md +403 -39
  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 +63 -10
  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 +292 -22
  19. package/dist/cjs/rules/validators.js +50 -0
  20. package/dist/cjs/sdk.js +18 -7
  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 +63 -10
  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 +291 -21
  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 +51 -4
  54. package/dist/esm/sdk.js +18 -9
  55. package/dist/esm/types.d.ts +58 -2
  56. package/dist/esm/types.js +4 -1
  57. package/package.json +12 -4
@@ -26,12 +26,27 @@
26
26
  * own `fetch`.
27
27
  */
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.CloudClient = void 0;
29
+ exports.CloudClient = exports.RETRY_STATUS = void 0;
30
+ exports.requireFetch = requireFetch;
31
+ exports.sleep = sleep;
32
+ exports.retryAfterMs = retryAfterMs;
33
+ exports.backoffMs = backoffMs;
34
+ exports.uuid = uuid;
35
+ exports.codePointOffsets = codePointOffsets;
36
+ exports.isInteger = isInteger;
37
+ exports.toUsage = toUsage;
38
+ exports.toCloudInfo = toCloudInfo;
39
+ exports.isQuota = isQuota;
40
+ exports.cloudErrorFor = cloudErrorFor;
41
+ exports.toResult = toResult;
42
+ exports.readJson = readJson;
30
43
  const config_js_1 = require("./config.js");
31
44
  const errors_js_1 = require("./errors.js");
32
45
  const types_js_1 = require("../types.js");
33
- const RETRY_STATUS = new Set([429, 500, 502, 503, 504]);
46
+ /** @internal Shared with cloud/account.ts. */
47
+ exports.RETRY_STATUS = new Set([429, 500, 502, 503, 504]);
34
48
  const MAX_BACKOFF_MS = 30_000;
49
+ /** @internal */
35
50
  function requireFetch(supplied) {
36
51
  const impl = supplied ?? globalThis.fetch;
37
52
  if (typeof impl !== "function") {
@@ -40,9 +55,11 @@ function requireFetch(supplied) {
40
55
  }
41
56
  return impl;
42
57
  }
58
+ /** @internal */
43
59
  function sleep(ms) {
44
60
  return new Promise(resolve => setTimeout(resolve, ms));
45
61
  }
62
+ /** @internal */
46
63
  function retryAfterMs(headers) {
47
64
  const raw = headers?.get("Retry-After");
48
65
  if (!raw)
@@ -55,11 +72,13 @@ function retryAfterMs(headers) {
55
72
  * Full jitter. A synchronised retry storm from many clients is how a
56
73
  * recovering service is knocked back over.
57
74
  */
75
+ /** @internal */
58
76
  function backoffMs(attempt, retryAfter) {
59
77
  if (retryAfter !== null)
60
78
  return Math.min(retryAfter, MAX_BACKOFF_MS);
61
79
  return Math.min(MAX_BACKOFF_MS, Math.random() * 2 ** attempt * 1000);
62
80
  }
81
+ /** @internal */
63
82
  function uuid() {
64
83
  const c = globalThis.crypto;
65
84
  if (c && typeof c.randomUUID === "function")
@@ -87,6 +106,7 @@ function toEntityType(raw) {
87
106
  * Returns the identity when the text has none, which is the common case and
88
107
  * costs one regex test.
89
108
  */
109
+ /** @internal Shared with cloud/batches.ts. */
90
110
  function codePointOffsets(text) {
91
111
  if (!/[\uD800-\uDBFF]/.test(text))
92
112
  return cp => cp;
@@ -99,6 +119,97 @@ function codePointOffsets(text) {
99
119
  units.push(i);
100
120
  return cp => units[Math.min(cp, units.length - 1)];
101
121
  }
122
+ /** @internal */
123
+ function isInteger(value) {
124
+ return typeof value === "number" && Number.isInteger(value);
125
+ }
126
+ /**
127
+ * The `usage` block, or undefined when absent or malformed. Shared rules with
128
+ * the Python SDK (`conformance/cloud_usage.json`): `tokens` and `credits` are
129
+ * integers, `billing_rate` a number, `factors` a list whose entries carry a
130
+ * string `code`. Anything else there drops the whole block rather than
131
+ * reporting a cost nobody can trust; unknown keys are ignored.
132
+ * @internal Exported for the conformance test.
133
+ */
134
+ function toUsage(raw) {
135
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
136
+ return undefined;
137
+ const u = raw;
138
+ if (!isInteger(u.tokens) || !isInteger(u.credits) || typeof u.billing_rate !== "number"
139
+ || !Array.isArray(u.factors))
140
+ return undefined;
141
+ const factors = [];
142
+ for (const f of u.factors) {
143
+ if (f === null || typeof f !== "object" || Array.isArray(f))
144
+ return undefined;
145
+ const r = f;
146
+ if (typeof r.code !== "string")
147
+ return undefined;
148
+ const factor = { code: r.code, detail: typeof r.detail === "string" ? r.detail : "" };
149
+ if (Array.isArray(r.types))
150
+ factor.types = r.types.filter((t) => typeof t === "string");
151
+ factors.push(factor);
152
+ }
153
+ return { tokens: u.tokens, billingRate: u.billing_rate, credits: u.credits, factors };
154
+ }
155
+ function str(value) {
156
+ return typeof value === "string" ? value : null;
157
+ }
158
+ /**
159
+ * The service's account of a request beside its spans. Shared rules with the
160
+ * Python SDK (`conformance/cloud_result.json`): a field of the wrong type reads
161
+ * as absent, and an unlocated entry without string `text` is skipped.
162
+ * @internal
163
+ */
164
+ function toCloudInfo(payload) {
165
+ const unlocated = [];
166
+ for (const item of Array.isArray(payload.unlocated) ? payload.unlocated : []) {
167
+ if (item && typeof item === "object" && typeof item.text === "string") {
168
+ const t = item.type;
169
+ unlocated.push({
170
+ text: item.text,
171
+ entityType: toEntityType(typeof t === "string" ? t : "OTHER"),
172
+ });
173
+ }
174
+ }
175
+ const usage = toUsage(payload.usage);
176
+ return {
177
+ jobId: str(payload.job_id),
178
+ modelVersion: str(payload.model_version),
179
+ ...(usage ? { usage } : {}),
180
+ unlocated,
181
+ };
182
+ }
183
+ /**
184
+ * A 429 from the gateway's daily quota rather than the edge's rate limit: the
185
+ * gateway answers in JSON with `detail.used` and `detail.limit`; nginx in HTML.
186
+ * @internal
187
+ */
188
+ function isQuota(payload) {
189
+ const detail = payload.detail;
190
+ return !!detail && typeof detail === "object" && "limit" in detail && "used" in detail;
191
+ }
192
+ /** The error for a non-success answer. @internal Shared with batches and account. */
193
+ function cloudErrorFor(status, payload) {
194
+ const message = typeof payload.error === "string" ? payload.error : `HTTP ${status}`;
195
+ const detail = (payload.detail && typeof payload.detail === "object"
196
+ ? payload.detail : {});
197
+ if (status === 401)
198
+ return new errors_js_1.CloudError(`authentication failed: ${message}`, status);
199
+ if (status === 404)
200
+ return new errors_js_1.NotFoundError(message, status, detail);
201
+ if (status === 410)
202
+ return new errors_js_1.ResultExpiredError(message, status, detail);
203
+ if (status === 413)
204
+ return new errors_js_1.TooLargeError(message, status, detail);
205
+ if (status === 429) {
206
+ return isQuota(payload)
207
+ ? new errors_js_1.QuotaExceededError(message, status, detail)
208
+ : new errors_js_1.RateLimitedError("rate limited by the service (HTTP 429); try again shortly", status, detail);
209
+ }
210
+ return new errors_js_1.CloudError(message, status, detail);
211
+ }
212
+ /** @internal Shared with cloud/batches.ts. */
102
213
  function toResult(payload, text) {
103
214
  const offset = codePointOffsets(text);
104
215
  const detections = (payload.entities ?? []).map(span => ({
@@ -108,7 +219,9 @@ function toResult(payload, text) {
108
219
  text: span.text,
109
220
  source: span.source === "model" ? types_js_1.DetectionSource.CLOUD : types_js_1.DetectionSource.RULES,
110
221
  country: null,
111
- confidence: span.confidence ?? "high",
222
+ // A model span carries no evidence of its own: the service's confidence
223
+ // when it sends one, never an invented "high" (rules-engine#108).
224
+ confidence: span.source === "model" ? (span.confidence ?? null) : (span.confidence ?? "high"),
112
225
  }));
113
226
  detections.sort((a, b) => a.start - b.start || b.end - a.end);
114
227
  return {
@@ -121,6 +234,7 @@ function toResult(payload, text) {
121
234
  detectionMode: "declared",
122
235
  tokens: {},
123
236
  exempted: [],
237
+ cloud: toCloudInfo(payload),
124
238
  };
125
239
  }
126
240
  class CloudClient {
@@ -129,6 +243,8 @@ class CloudClient {
129
243
  const resolved = config ?? (0, config_js_1.getConfig)();
130
244
  if (resolved === null)
131
245
  throw new errors_js_1.NotConfiguredError();
246
+ // A config built by hand bypasses configure(), so check it here too.
247
+ (0, config_js_1.requireSecureBaseUrl)(resolved.baseUrl);
132
248
  this.config = resolved;
133
249
  }
134
250
  headers(idempotencyKey) {
@@ -142,15 +258,7 @@ class CloudClient {
142
258
  };
143
259
  }
144
260
  raiseFor(status, payload) {
145
- const message = payload.error ?? `HTTP ${status}`;
146
- const detail = payload.detail ?? {};
147
- if (status === 401)
148
- throw new errors_js_1.CloudError(`authentication failed: ${message}`, status);
149
- if (status === 413)
150
- throw new errors_js_1.TooLargeError(message, status, detail);
151
- if (status === 429)
152
- throw new errors_js_1.QuotaExceededError(message, status, detail);
153
- throw new errors_js_1.CloudError(message, status, detail);
261
+ throw cloudErrorFor(status, payload);
154
262
  }
155
263
  async redact(text, options) {
156
264
  const doFetch = requireFetch(options.fetchImpl);
@@ -189,7 +297,8 @@ class CloudClient {
189
297
  }
190
298
  if (response.status === 200)
191
299
  return toResult(payload, text);
192
- if (!RETRY_STATUS.has(response.status) || attempt >= this.config.maxRetries) {
300
+ if (!exports.RETRY_STATUS.has(response.status) || attempt >= this.config.maxRetries
301
+ || (response.status === 429 && isQuota(payload))) {
193
302
  this.raiseFor(response.status, payload);
194
303
  }
195
304
  }
@@ -212,7 +321,7 @@ class CloudClient {
212
321
  if (response.status === 200 && payload.redacted_text !== undefined) {
213
322
  return payload;
214
323
  }
215
- if (response.status !== 200 && !RETRY_STATUS.has(response.status)) {
324
+ if (response.status !== 200 && !exports.RETRY_STATUS.has(response.status)) {
216
325
  this.raiseFor(response.status, payload);
217
326
  }
218
327
  if (Date.now() >= deadline) {
@@ -235,6 +344,7 @@ class CloudClient {
235
344
  }
236
345
  }
237
346
  exports.CloudClient = CloudClient;
347
+ /** @internal */
238
348
  async function readJson(response) {
239
349
  try {
240
350
  const data = await response.json();
@@ -2,6 +2,7 @@
2
2
  /** [CLOUD EXTENSION] Cloud tier configuration. */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.DEFAULT_MAX_RETRIES = exports.DEFAULT_POLL_TIMEOUT_MS = exports.DEFAULT_TIMEOUT_MS = exports.DEFAULT_BASE_URL = void 0;
5
+ exports.requireSecureBaseUrl = requireSecureBaseUrl;
5
6
  exports.configure = configure;
6
7
  exports.getConfig = getConfig;
7
8
  exports.reset = reset;
@@ -19,6 +20,29 @@ exports.DEFAULT_TIMEOUT_MS = 90_000;
19
20
  */
20
21
  exports.DEFAULT_POLL_TIMEOUT_MS = 900_000;
21
22
  exports.DEFAULT_MAX_RETRIES = 3;
23
+ /** Hosts a plain-HTTP base URL may name: a gateway or TLS proxy on this machine. */
24
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
25
+ /**
26
+ * Refuse a cloud address that would not use TLS. The API key and the text sent
27
+ * must travel encrypted; plain `http` is allowed only to a loopback host, for a
28
+ * local gateway or a TLS-terminating proxy on the same machine (rules-engine#86).
29
+ */
30
+ function requireSecureBaseUrl(baseUrl) {
31
+ // Parsed by hand rather than with `URL`: a runtime without that global would
32
+ // otherwise refuse every address, https included, with a misleading message.
33
+ const m = /^([a-z][a-z0-9+.-]*):\/\/(?:[^@/?#]*@)?(\[[^\]]*\]|[^:/?#]*)/i.exec(baseUrl);
34
+ if (m !== null) {
35
+ const scheme = m[1].toLowerCase();
36
+ const host = m[2].toLowerCase();
37
+ if (scheme === "https" && host)
38
+ return;
39
+ if (scheme === "http" && LOOPBACK_HOSTS.has(host))
40
+ return;
41
+ }
42
+ throw new Error(`refusing cloud base URL ${JSON.stringify(baseUrl)}: it must start with https:// ` +
43
+ "(plain http:// is allowed only to localhost, 127.0.0.1 or ::1), because the " +
44
+ "API key and document text would otherwise travel unencrypted");
45
+ }
22
46
  let config = null;
23
47
  /** Read an env var where there is an environment to read. Browsers have none. */
24
48
  function env(name) {
@@ -36,10 +60,12 @@ function configure(options = {}) {
36
60
  if (!apiKey) {
37
61
  throw new Error("no API key: pass configure({ apiKey }) or set EUREDACT_API_KEY");
38
62
  }
63
+ const baseUrl = (options.baseUrl ?? env("EUREDACT_BASE_URL") ?? exports.DEFAULT_BASE_URL)
64
+ .replace(/\/+$/, "");
65
+ requireSecureBaseUrl(baseUrl);
39
66
  config = {
40
67
  apiKey,
41
- baseUrl: (options.baseUrl ?? env("EUREDACT_BASE_URL") ?? exports.DEFAULT_BASE_URL)
42
- .replace(/\/+$/, ""),
68
+ baseUrl,
43
69
  timeoutMs: options.timeoutMs ?? exports.DEFAULT_TIMEOUT_MS,
44
70
  pollTimeoutMs: options.pollTimeoutMs ?? exports.DEFAULT_POLL_TIMEOUT_MS,
45
71
  maxRetries: options.maxRetries ?? exports.DEFAULT_MAX_RETRIES,
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  /** [CLOUD EXTENSION] Errors the cloud tier can raise. */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.TooLargeError = exports.QuotaExceededError = exports.NotConfiguredError = exports.CloudError = void 0;
4
+ exports.TooLargeError = exports.ResultExpiredError = exports.NotFoundError = exports.RateLimitedError = exports.QuotaExceededError = exports.NotConfiguredError = exports.CloudError = void 0;
5
5
  class CloudError extends Error {
6
6
  status;
7
7
  detail;
@@ -31,7 +31,11 @@ class NotConfiguredError extends CloudError {
31
31
  }
32
32
  }
33
33
  exports.NotConfiguredError = NotConfiguredError;
34
- /** 429 after retries were exhausted. */
34
+ /**
35
+ * 429: the account's daily document quota is used up. Thrown at once, not
36
+ * after retries: the gateway's quota answer (JSON with `detail.used` and
37
+ * `detail.limit`) will not change before the day does (rules-engine#89).
38
+ */
35
39
  class QuotaExceededError extends CloudError {
36
40
  constructor(message, status, detail) {
37
41
  super(message, status, detail);
@@ -39,6 +43,39 @@ class QuotaExceededError extends CloudError {
39
43
  }
40
44
  }
41
45
  exports.QuotaExceededError = QuotaExceededError;
46
+ /**
47
+ * 429 from the edge's rate limit, still there after every retry. It clears
48
+ * within seconds, so it is retried with backoff first. A subclass of
49
+ * `QuotaExceededError`, which is what every 429 threw before the two were told apart.
50
+ */
51
+ class RateLimitedError extends QuotaExceededError {
52
+ constructor(message, status, detail) {
53
+ super(message, status, detail);
54
+ this.name = "RateLimitedError";
55
+ }
56
+ }
57
+ exports.RateLimitedError = RateLimitedError;
58
+ /** 404: no such job, batch or key -- or another account's, which the service
59
+ * deliberately does not distinguish. */
60
+ class NotFoundError extends CloudError {
61
+ constructor(message, status, detail) {
62
+ super(message, status, detail);
63
+ this.name = "NotFoundError";
64
+ }
65
+ }
66
+ exports.NotFoundError = NotFoundError;
67
+ /**
68
+ * 410: the job succeeded, but its result is no longer retained -- swept by the
69
+ * account's retention period, or discarded on delivery under no-retention. An
70
+ * empty result here would read like a document with no personal data in it.
71
+ */
72
+ class ResultExpiredError extends CloudError {
73
+ constructor(message, status, detail) {
74
+ super(message, status, detail);
75
+ this.name = "ResultExpiredError";
76
+ }
77
+ }
78
+ exports.ResultExpiredError = ResultExpiredError;
42
79
  /**
43
80
  * 413. The document is over the service's input cap.
44
81
  *
@@ -16,16 +16,31 @@
16
16
  * ```
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.TooLargeError = exports.QuotaExceededError = exports.NotConfiguredError = exports.CloudError = exports.DEFAULT_BASE_URL = exports.reset = exports.getConfig = exports.configure = exports.CloudClient = void 0;
19
+ exports.TooLargeError = exports.ResultExpiredError = exports.RateLimitedError = exports.QuotaExceededError = exports.NotFoundError = exports.NotConfiguredError = exports.CloudError = exports.DEFAULT_BASE_URL = exports.requireSecureBaseUrl = exports.reset = exports.getConfig = exports.configure = exports.MAX_DOCUMENT_TOKENS = exports.MAX_BODY_BYTES = exports.CUSTOM_ID = exports.MAX_CUSTOM_ID = exports.MAX_DOCUMENTS = exports.BatchError = exports.Batches = exports.Jobs = exports.Account = exports.CloudClient = void 0;
20
20
  var client_js_1 = require("./client.js");
21
21
  Object.defineProperty(exports, "CloudClient", { enumerable: true, get: function () { return client_js_1.CloudClient; } });
22
+ var account_js_1 = require("./account.js");
23
+ Object.defineProperty(exports, "Account", { enumerable: true, get: function () { return account_js_1.Account; } });
24
+ Object.defineProperty(exports, "Jobs", { enumerable: true, get: function () { return account_js_1.Jobs; } });
25
+ var batches_js_1 = require("./batches.js");
26
+ Object.defineProperty(exports, "Batches", { enumerable: true, get: function () { return batches_js_1.Batches; } });
27
+ Object.defineProperty(exports, "BatchError", { enumerable: true, get: function () { return batches_js_1.BatchError; } });
28
+ Object.defineProperty(exports, "MAX_DOCUMENTS", { enumerable: true, get: function () { return batches_js_1.MAX_DOCUMENTS; } });
29
+ Object.defineProperty(exports, "MAX_CUSTOM_ID", { enumerable: true, get: function () { return batches_js_1.MAX_CUSTOM_ID; } });
30
+ Object.defineProperty(exports, "CUSTOM_ID", { enumerable: true, get: function () { return batches_js_1.CUSTOM_ID; } });
31
+ Object.defineProperty(exports, "MAX_BODY_BYTES", { enumerable: true, get: function () { return batches_js_1.MAX_BODY_BYTES; } });
32
+ Object.defineProperty(exports, "MAX_DOCUMENT_TOKENS", { enumerable: true, get: function () { return batches_js_1.MAX_DOCUMENT_TOKENS; } });
22
33
  var config_js_1 = require("./config.js");
23
34
  Object.defineProperty(exports, "configure", { enumerable: true, get: function () { return config_js_1.configure; } });
24
35
  Object.defineProperty(exports, "getConfig", { enumerable: true, get: function () { return config_js_1.getConfig; } });
25
36
  Object.defineProperty(exports, "reset", { enumerable: true, get: function () { return config_js_1.reset; } });
37
+ Object.defineProperty(exports, "requireSecureBaseUrl", { enumerable: true, get: function () { return config_js_1.requireSecureBaseUrl; } });
26
38
  Object.defineProperty(exports, "DEFAULT_BASE_URL", { enumerable: true, get: function () { return config_js_1.DEFAULT_BASE_URL; } });
27
39
  var errors_js_1 = require("./errors.js");
28
40
  Object.defineProperty(exports, "CloudError", { enumerable: true, get: function () { return errors_js_1.CloudError; } });
29
41
  Object.defineProperty(exports, "NotConfiguredError", { enumerable: true, get: function () { return errors_js_1.NotConfiguredError; } });
42
+ Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return errors_js_1.NotFoundError; } });
30
43
  Object.defineProperty(exports, "QuotaExceededError", { enumerable: true, get: function () { return errors_js_1.QuotaExceededError; } });
44
+ Object.defineProperty(exports, "RateLimitedError", { enumerable: true, get: function () { return errors_js_1.RateLimitedError; } });
45
+ Object.defineProperty(exports, "ResultExpiredError", { enumerable: true, get: function () { return errors_js_1.ResultExpiredError; } });
31
46
  Object.defineProperty(exports, "TooLargeError", { enumerable: true, get: function () { return errors_js_1.TooLargeError; } });
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TooLargeError = exports.QuotaExceededError = exports.NotConfiguredError = exports.CloudError = exports.CloudClient = exports.getConfig = exports.configure = exports.SEED_BIC6_PREFIXES = exports.getBicRegistry = exports.setBicRegistry = exports.COUNTRY_CONFIGS = exports.restore = exports.EuRedact = exports.DocumentContext = exports.canonicalType = exports.LEGACY_TYPE_ALIASES = exports.DetectionSource = exports.EntityType = void 0;
3
+ exports.TooLargeError = exports.ResultExpiredError = exports.RateLimitedError = exports.QuotaExceededError = exports.NotFoundError = exports.NotConfiguredError = exports.Jobs = exports.CUSTOM_ID = exports.BatchError = exports.Batches = exports.CloudError = exports.CloudClient = exports.Account = exports.getConfig = exports.configure = exports.SEED_BIC6_PREFIXES = exports.getBicRegistry = exports.setBicRegistry = exports.COUNTRY_CONFIGS = exports.restore = exports.EuRedact = exports.DocumentContext = exports.canonicalType = exports.LEGACY_TYPE_ALIASES = exports.DetectionSource = exports.EntityType = void 0;
4
4
  exports.availableCountries = availableCountries;
5
5
  exports.redact = redact;
6
6
  exports.redactAsync = redactAsync;
@@ -26,10 +26,18 @@ Object.defineProperty(exports, "SEED_BIC6_PREFIXES", { enumerable: true, get: fu
26
26
  var index_js_2 = require("./cloud/index.js");
27
27
  Object.defineProperty(exports, "configure", { enumerable: true, get: function () { return index_js_2.configure; } });
28
28
  Object.defineProperty(exports, "getConfig", { enumerable: true, get: function () { return index_js_2.getConfig; } });
29
+ Object.defineProperty(exports, "Account", { enumerable: true, get: function () { return index_js_2.Account; } });
29
30
  Object.defineProperty(exports, "CloudClient", { enumerable: true, get: function () { return index_js_2.CloudClient; } });
30
31
  Object.defineProperty(exports, "CloudError", { enumerable: true, get: function () { return index_js_2.CloudError; } });
32
+ Object.defineProperty(exports, "Batches", { enumerable: true, get: function () { return index_js_2.Batches; } });
33
+ Object.defineProperty(exports, "BatchError", { enumerable: true, get: function () { return index_js_2.BatchError; } });
34
+ Object.defineProperty(exports, "CUSTOM_ID", { enumerable: true, get: function () { return index_js_2.CUSTOM_ID; } });
35
+ Object.defineProperty(exports, "Jobs", { enumerable: true, get: function () { return index_js_2.Jobs; } });
31
36
  Object.defineProperty(exports, "NotConfiguredError", { enumerable: true, get: function () { return index_js_2.NotConfiguredError; } });
37
+ Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return index_js_2.NotFoundError; } });
32
38
  Object.defineProperty(exports, "QuotaExceededError", { enumerable: true, get: function () { return index_js_2.QuotaExceededError; } });
39
+ Object.defineProperty(exports, "RateLimitedError", { enumerable: true, get: function () { return index_js_2.RateLimitedError; } });
40
+ Object.defineProperty(exports, "ResultExpiredError", { enumerable: true, get: function () { return index_js_2.ResultExpiredError; } });
33
41
  Object.defineProperty(exports, "TooLargeError", { enumerable: true, get: function () { return index_js_2.TooLargeError; } });
34
42
  const sdk_js_2 = require("./sdk.js");
35
43
  const index_js_3 = require("./rules/countries/index.js");
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ /**
3
+ * `platform.ts` for browsers: no `node:` imports, so the package bundles for a
4
+ * page. Selected by the `browser` field in package.json (rules-engine#56).
5
+ *
6
+ * The cache key is computed synchronously and must stay collision-resistant
7
+ * (see `ResultCache.key`), and Web Crypto's `subtle.digest` is asynchronous, so
8
+ * SHA-256 is implemented here. A test checks it against `node:crypto` byte for
9
+ * byte, astral characters and lone surrogates included.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.sha256Hex = sha256Hex;
13
+ exports.randomIndex = randomIndex;
14
+ exports.defaultBatchStore = defaultBatchStore;
15
+ exports.gzip = gzip;
16
+ const K = new Uint32Array([
17
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
18
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
19
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
20
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
21
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
22
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
23
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
24
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
25
+ ]);
26
+ /** Hex SHA-256 of the UTF-8 encoding of `parts`, joined with nothing between. */
27
+ function sha256Hex(parts) {
28
+ // TextEncoder replaces a lone surrogate with U+FFFD, as Node's hash.update does.
29
+ const bytes = new TextEncoder().encode(parts.join(""));
30
+ const bitLength = bytes.length * 8;
31
+ const padded = new Uint8Array(((bytes.length + 9 + 63) >> 6) << 6);
32
+ padded.set(bytes);
33
+ padded[bytes.length] = 0x80;
34
+ const view = new DataView(padded.buffer);
35
+ view.setUint32(padded.length - 8, Math.floor(bitLength / 0x100000000));
36
+ view.setUint32(padded.length - 4, bitLength >>> 0);
37
+ const h = new Uint32Array([
38
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
39
+ ]);
40
+ const w = new Uint32Array(64);
41
+ for (let offset = 0; offset < padded.length; offset += 64) {
42
+ for (let i = 0; i < 16; i++)
43
+ w[i] = view.getUint32(offset + i * 4);
44
+ for (let i = 16; i < 64; i++) {
45
+ const a = w[i - 15], b = w[i - 2];
46
+ const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
47
+ const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
48
+ w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
49
+ }
50
+ let [a, b, c, d, e, f, g, hh] = h;
51
+ for (let i = 0; i < 64; i++) {
52
+ const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
53
+ const ch = (e & f) ^ (~e & g);
54
+ const t1 = (hh + S1 + ch + K[i] + w[i]) >>> 0;
55
+ const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
56
+ const maj = (a & b) ^ (a & c) ^ (b & c);
57
+ const t2 = (S0 + maj) >>> 0;
58
+ hh = g;
59
+ g = f;
60
+ f = e;
61
+ e = (d + t1) >>> 0;
62
+ d = c;
63
+ c = b;
64
+ b = a;
65
+ a = (t1 + t2) >>> 0;
66
+ }
67
+ h[0] = (h[0] + a) >>> 0;
68
+ h[1] = (h[1] + b) >>> 0;
69
+ h[2] = (h[2] + c) >>> 0;
70
+ h[3] = (h[3] + d) >>> 0;
71
+ h[4] = (h[4] + e) >>> 0;
72
+ h[5] = (h[5] + f) >>> 0;
73
+ h[6] = (h[6] + g) >>> 0;
74
+ h[7] = (h[7] + hh) >>> 0;
75
+ }
76
+ return Array.from(h, x => x.toString(16).padStart(8, "0")).join("");
77
+ }
78
+ /**
79
+ * A uniformly random integer in `[0, n)`, from `crypto.getRandomValues`.
80
+ * Rejection sampling: a plain modulo would favour the low residues.
81
+ */
82
+ function randomIndex(n) {
83
+ const limit = Math.floor(0x100000000 / n) * n;
84
+ const buf = new Uint32Array(1);
85
+ for (;;) {
86
+ globalThis.crypto.getRandomValues(buf);
87
+ if (buf[0] < limit)
88
+ return buf[0] % n;
89
+ }
90
+ }
91
+ /** A page has no filesystem: the caller passes `store` to `Batches`. */
92
+ function defaultBatchStore(_directory) {
93
+ return null;
94
+ }
95
+ /** No synchronous gzip in a page; the batch is uploaded uncompressed. */
96
+ function gzip(_data) {
97
+ return null;
98
+ }
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.sha256Hex = sha256Hex;
37
+ exports.randomIndex = randomIndex;
38
+ exports.defaultBatchStore = defaultBatchStore;
39
+ exports.gzip = gzip;
40
+ /**
41
+ * The two things the SDK needs from a crypto library: a SHA-256 digest for the
42
+ * result-cache key and a uniform random index for token suffixes.
43
+ *
44
+ * This is the Node implementation, on `node:crypto`. A browser bundle gets
45
+ * `platform.browser.ts` instead, through the `browser` field in package.json,
46
+ * because `node:crypto` does not exist there and importing it from `cache.ts`
47
+ * and `sdk.ts` made the package unbundleable for a page (rules-engine#56).
48
+ * Every other module imports these two functions from here and nothing from
49
+ * `node:`; a test holds the source tree to that.
50
+ */
51
+ const node_crypto_1 = require("node:crypto");
52
+ const fs = __importStar(require("node:fs"));
53
+ const os = __importStar(require("node:os"));
54
+ const path = __importStar(require("node:path"));
55
+ const node_zlib_1 = require("node:zlib");
56
+ /** Hex SHA-256 of the UTF-8 encoding of `parts`, joined with nothing between. */
57
+ function sha256Hex(parts) {
58
+ const hash = (0, node_crypto_1.createHash)("sha256");
59
+ for (const part of parts)
60
+ hash.update(part);
61
+ return hash.digest("hex");
62
+ }
63
+ /** A uniformly random integer in `[0, n)`. */
64
+ function randomIndex(n) {
65
+ return (0, node_crypto_1.randomInt)(n);
66
+ }
67
+ /**
68
+ * `<dir>/<batchId>.json`, directory `0700`, files `0600`, the default local
69
+ * batch store. Writes go to a temporary file in the same directory, created
70
+ * `0600` before any byte is written, then renamed over the old one.
71
+ */
72
+ function defaultBatchStore(directory) {
73
+ const dir = directory ?? path.join(os.homedir(), ".euredact", "batches");
74
+ const fileFor = (batchId) => {
75
+ if (!batchId || /[/\\]/.test(batchId) || batchId === "." || batchId === "..") {
76
+ throw new Error(`not a batch id: ${JSON.stringify(batchId)}`);
77
+ }
78
+ return path.join(dir, `${batchId}.json`);
79
+ };
80
+ return {
81
+ async read(batchId) {
82
+ try {
83
+ return new Uint8Array(fs.readFileSync(fileFor(batchId)));
84
+ }
85
+ catch (err) {
86
+ if (err.code === "ENOENT")
87
+ return null;
88
+ throw err;
89
+ }
90
+ },
91
+ async write(batchId, data) {
92
+ const target = fileFor(batchId);
93
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
94
+ try {
95
+ fs.chmodSync(dir, 0o700);
96
+ }
97
+ catch { /* not ours to change */ }
98
+ const tmp = path.join(dir, `.${batchId}.${process.pid}.${Date.now()}.tmp`);
99
+ const fd = fs.openSync(tmp, "wx", 0o600);
100
+ try {
101
+ fs.writeSync(fd, data);
102
+ fs.fsyncSync(fd);
103
+ }
104
+ finally {
105
+ fs.closeSync(fd);
106
+ }
107
+ try {
108
+ fs.renameSync(tmp, target);
109
+ }
110
+ catch (err) {
111
+ try {
112
+ fs.unlinkSync(tmp);
113
+ }
114
+ catch { /* already gone */ }
115
+ throw err;
116
+ }
117
+ },
118
+ async delete(batchId) {
119
+ try {
120
+ fs.unlinkSync(fileFor(batchId));
121
+ }
122
+ catch (err) {
123
+ if (err.code !== "ENOENT")
124
+ throw err;
125
+ }
126
+ },
127
+ async list() {
128
+ if (!fs.existsSync(dir))
129
+ return [];
130
+ return fs.readdirSync(dir)
131
+ .filter(name => name.endsWith(".json") && !name.startsWith("."))
132
+ .map(name => name.slice(0, -".json".length))
133
+ .sort();
134
+ },
135
+ };
136
+ }
137
+ /** Gzip, for the batch upload. */
138
+ function gzip(data) {
139
+ return new Uint8Array((0, node_zlib_1.gzipSync)(data));
140
+ }