euredact 0.5.1 → 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 +658 -45
  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 +131 -18
  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 +138 -48
  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 +418 -23
  19. package/dist/cjs/rules/validators.js +103 -0
  20. package/dist/cjs/sdk.js +150 -44
  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 +90 -5
  28. package/dist/esm/cloud/client.js +126 -26
  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 +138 -48
  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 +417 -22
  51. package/dist/esm/rules/validators.d.ts +14 -0
  52. package/dist/esm/rules/validators.js +100 -0
  53. package/dist/esm/sdk.d.ts +63 -19
  54. package/dist/esm/sdk.js +148 -44
  55. package/dist/esm/types.d.ts +58 -2
  56. package/dist/esm/types.js +4 -1
  57. package/package.json +12 -4
@@ -2,11 +2,15 @@
2
2
  /**
3
3
  * [CLOUD EXTENSION] The cloud client.
4
4
  *
5
- * Speaks to the euRedact inference service: a deterministic rules engine
6
- * followed by a fine-tuned model asked only *what did the rules miss?* The
7
- * service returns both the redacted document and located spans, so this
5
+ * Speaks to the euRedact inference service: a fine-tuned model asked only
6
+ * *what did the rules miss?* The service returns located spans, so this
8
7
  * module's job is transport and translation, not detection.
9
8
  *
9
+ * It sends the text it is given, as given. `redactAsync(text, { mode: "cloud" })`
10
+ * is what runs the rules engine first and hands this client only the masked
11
+ * text; call the client directly and whatever you pass is what leaves the
12
+ * machine. The spans that come back index the text that was sent.
13
+ *
10
14
  * Three things it hides from the caller:
11
15
  *
12
16
  * - **Whether the document finished inside the service's sync window.** A short
@@ -22,12 +26,27 @@
22
26
  * own `fetch`.
23
27
  */
24
28
  Object.defineProperty(exports, "__esModule", { value: true });
25
- 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;
26
43
  const config_js_1 = require("./config.js");
27
44
  const errors_js_1 = require("./errors.js");
28
45
  const types_js_1 = require("../types.js");
29
- 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]);
30
48
  const MAX_BACKOFF_MS = 30_000;
49
+ /** @internal */
31
50
  function requireFetch(supplied) {
32
51
  const impl = supplied ?? globalThis.fetch;
33
52
  if (typeof impl !== "function") {
@@ -36,9 +55,11 @@ function requireFetch(supplied) {
36
55
  }
37
56
  return impl;
38
57
  }
58
+ /** @internal */
39
59
  function sleep(ms) {
40
60
  return new Promise(resolve => setTimeout(resolve, ms));
41
61
  }
62
+ /** @internal */
42
63
  function retryAfterMs(headers) {
43
64
  const raw = headers?.get("Retry-After");
44
65
  if (!raw)
@@ -51,11 +72,13 @@ function retryAfterMs(headers) {
51
72
  * Full jitter. A synchronised retry storm from many clients is how a
52
73
  * recovering service is knocked back over.
53
74
  */
75
+ /** @internal */
54
76
  function backoffMs(attempt, retryAfter) {
55
77
  if (retryAfter !== null)
56
78
  return Math.min(retryAfter, MAX_BACKOFF_MS);
57
79
  return Math.min(MAX_BACKOFF_MS, Math.random() * 2 ** attempt * 1000);
58
80
  }
81
+ /** @internal */
59
82
  function uuid() {
60
83
  const c = globalThis.crypto;
61
84
  if (c && typeof c.randomUUID === "function")
@@ -83,6 +106,7 @@ function toEntityType(raw) {
83
106
  * Returns the identity when the text has none, which is the common case and
84
107
  * costs one regex test.
85
108
  */
109
+ /** @internal Shared with cloud/batches.ts. */
86
110
  function codePointOffsets(text) {
87
111
  if (!/[\uD800-\uDBFF]/.test(text))
88
112
  return cp => cp;
@@ -95,6 +119,97 @@ function codePointOffsets(text) {
95
119
  units.push(i);
96
120
  return cp => units[Math.min(cp, units.length - 1)];
97
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. */
98
213
  function toResult(payload, text) {
99
214
  const offset = codePointOffsets(text);
100
215
  const detections = (payload.entities ?? []).map(span => ({
@@ -104,7 +219,9 @@ function toResult(payload, text) {
104
219
  text: span.text,
105
220
  source: span.source === "model" ? types_js_1.DetectionSource.CLOUD : types_js_1.DetectionSource.RULES,
106
221
  country: null,
107
- 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"),
108
225
  }));
109
226
  detections.sort((a, b) => a.start - b.start || b.end - a.end);
110
227
  return {
@@ -117,6 +234,7 @@ function toResult(payload, text) {
117
234
  detectionMode: "declared",
118
235
  tokens: {},
119
236
  exempted: [],
237
+ cloud: toCloudInfo(payload),
120
238
  };
121
239
  }
122
240
  class CloudClient {
@@ -125,6 +243,8 @@ class CloudClient {
125
243
  const resolved = config ?? (0, config_js_1.getConfig)();
126
244
  if (resolved === null)
127
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);
128
248
  this.config = resolved;
129
249
  }
130
250
  headers(idempotencyKey) {
@@ -138,15 +258,7 @@ class CloudClient {
138
258
  };
139
259
  }
140
260
  raiseFor(status, payload) {
141
- const message = payload.error ?? `HTTP ${status}`;
142
- const detail = payload.detail ?? {};
143
- if (status === 401)
144
- throw new errors_js_1.CloudError(`authentication failed: ${message}`, status);
145
- if (status === 413)
146
- throw new errors_js_1.TooLargeError(message, status, detail);
147
- if (status === 429)
148
- throw new errors_js_1.QuotaExceededError(message, status, detail);
149
- throw new errors_js_1.CloudError(message, status, detail);
261
+ throw cloudErrorFor(status, payload);
150
262
  }
151
263
  async redact(text, options) {
152
264
  const doFetch = requireFetch(options.fetchImpl);
@@ -156,7 +268,6 @@ class CloudClient {
156
268
  country: options.country.toUpperCase(),
157
269
  language: options.language ?? "",
158
270
  priority: options.priority ?? "interactive",
159
- rules_only: options.rulesOnly ?? false,
160
271
  });
161
272
  let attempt = 0;
162
273
  for (;;) {
@@ -186,7 +297,8 @@ class CloudClient {
186
297
  }
187
298
  if (response.status === 200)
188
299
  return toResult(payload, text);
189
- 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))) {
190
302
  this.raiseFor(response.status, payload);
191
303
  }
192
304
  }
@@ -209,7 +321,7 @@ class CloudClient {
209
321
  if (response.status === 200 && payload.redacted_text !== undefined) {
210
322
  return payload;
211
323
  }
212
- if (response.status !== 200 && !RETRY_STATUS.has(response.status)) {
324
+ if (response.status !== 200 && !exports.RETRY_STATUS.has(response.status)) {
213
325
  this.raiseFor(response.status, payload);
214
326
  }
215
327
  if (Date.now() >= deadline) {
@@ -232,6 +344,7 @@ class CloudClient {
232
344
  }
233
345
  }
234
346
  exports.CloudClient = CloudClient;
347
+ /** @internal */
235
348
  async function readJson(response) {
236
349
  try {
237
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
+ }