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