euredact 0.3.9 → 0.4.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.
package/dist/cjs/sdk.js CHANGED
@@ -1,4 +1,37 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.EuRedact = void 0;
4
37
  const cache_js_1 = require("./cache.js");
@@ -48,6 +81,40 @@ class ReferentialMapper {
48
81
  }
49
82
  }
50
83
  const DEFAULT_MAX_INPUT_LENGTH = 10_485_760; // ~10 MB of text
84
+ /**
85
+ * Send a document to the cloud tier.
86
+ *
87
+ * Options the service cannot honour throw rather than being ignored. Silently
88
+ * dropping one would mean returning a result that does not match what was
89
+ * asked for — which, for anything that changes which spans come back, is
90
+ * under-redaction wearing a plausible face.
91
+ */
92
+ async function redactViaCloud(text, options) {
93
+ const { CloudClient } = await Promise.resolve().then(() => __importStar(require("./cloud/client.js")));
94
+ const countries = options.countries ?? null;
95
+ if (!countries || countries.length !== 1) {
96
+ throw new Error('cloud mode needs exactly one country, e.g. { countries: ["BE"] }. The ' +
97
+ "model is trained and evaluated per country, so a multi-country request " +
98
+ "has no defined behaviour.");
99
+ }
100
+ if (options.countryHint) {
101
+ throw new Error("countryHint is not supported in cloud mode");
102
+ }
103
+ if (options.context || options.chunkOffset) {
104
+ throw new Error("context/chunkOffset are not supported in cloud mode: the model has never " +
105
+ "seen a chunk boundary, so the service rejects oversized input rather " +
106
+ "than splitting it");
107
+ }
108
+ if (options.referentialIntegrity) {
109
+ throw new Error("referentialIntegrity is not supported in cloud mode");
110
+ }
111
+ // detectDates is deliberately NOT forwarded. The service always runs its
112
+ // rules engine with dates on, because that is what the model was trained
113
+ // against; the caller's value cannot change that. It is the one ignored
114
+ // option that is safe to ignore — it can only cause MORE to be detected,
115
+ // never less, so it cannot produce under-redaction.
116
+ return new CloudClient().redact(text, { country: countries[0] });
117
+ }
51
118
  /**
52
119
  * Reject a bare string where a list of country codes is expected.
53
120
  *
@@ -95,6 +162,19 @@ class EuRedact {
95
162
  redact(text, options = {}) {
96
163
  checkCountryArg(options.countries, "countries");
97
164
  checkCountryArg(options.countryHint, "countryHint");
165
+ const requestedMode = options.mode ?? "rules";
166
+ if (requestedMode === "cloud") {
167
+ // Cannot be served synchronously: the cloud tier is a network call, and
168
+ // returning rules-only output instead would be the silent under-redaction
169
+ // this guard exists to prevent. Point at the async entry point rather
170
+ // than inventing a Promise-or-value union return type.
171
+ throw new Error('mode: "cloud" is asynchronous — use redactAsync(text, { mode: "cloud" }) ' +
172
+ "(or EuRedact#redactAsync). redact() is synchronous and can only serve " +
173
+ 'mode: "rules".');
174
+ }
175
+ if (requestedMode !== "rules") {
176
+ throw new Error(`unknown mode ${JSON.stringify(requestedMode)}: expected "rules" or "cloud"`);
177
+ }
98
178
  if (text.length > this.maxInputLength) {
99
179
  throw new Error(`Input text length (${text.length.toLocaleString()} chars) exceeds the maximum ` +
100
180
  `(${this.maxInputLength.toLocaleString()} chars). Split the input or ` +
@@ -178,6 +258,18 @@ class EuRedact {
178
258
  }
179
259
  return result;
180
260
  }
261
+ /**
262
+ * Redact, awaiting the cloud tier when `mode: "cloud"` is asked for.
263
+ *
264
+ * `mode: "rules"` resolves immediately with exactly what `redact()` returns,
265
+ * so callers that may or may not use the cloud tier can hold one code path.
266
+ */
267
+ async redactAsync(text, options = {}) {
268
+ const mode = options.mode ?? "rules";
269
+ if (mode !== "cloud")
270
+ return this.redact(text, options);
271
+ return redactViaCloud(text, options);
272
+ }
181
273
  redactBatch(texts, options = {}) {
182
274
  // Generation is country-blind, so every country's patterns are loaded
183
275
  // regardless of what the caller declared.
package/dist/cjs/types.js CHANGED
@@ -1,9 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DetectionSource = exports.LEGACY_TYPE_ALIASES = exports.EntityType = void 0;
4
+ exports.canonicalType = canonicalType;
4
5
  var EntityType;
5
6
  (function (EntityType) {
6
- EntityType["NAME"] = "NAME";
7
+ /**
8
+ * Canonical name for a person's name. `NAME` is kept below as a legacy alias
9
+ * with the same value, exactly as `IBAN` aliases `BANK_ACCOUNT`, so
10
+ * `EntityType.NAME === EntityType.PERSON_NAME` and existing code keeps
11
+ * working — but the emitted value, and therefore the `[PERSON_NAME]`
12
+ * placeholder written into redacted text, is canonical.
13
+ *
14
+ * Renamed rather than translated at the cloud boundary: two names for one
15
+ * type is how a whole category goes missing when someone filters on the
16
+ * spelling they happened to know. Nothing could have depended on the old
17
+ * value — the type is cloud-only and the cloud tier was stubbed, so it was
18
+ * never emitted.
19
+ */
20
+ EntityType["PERSON_NAME"] = "PERSON_NAME";
21
+ /** @deprecated legacy alias of {@link EntityType.PERSON_NAME} */
22
+ EntityType["NAME"] = "PERSON_NAME";
7
23
  EntityType["ADDRESS"] = "ADDRESS";
8
24
  /**
9
25
  * Canonical name for a bank account identifier. `IBAN` is kept below as a
@@ -51,6 +67,20 @@ var EntityType;
51
67
  EntityType["UUID"] = "UUID";
52
68
  EntityType["SOCIAL_HANDLE"] = "SOCIAL_HANDLE";
53
69
  EntityType["SECRET"] = "SECRET";
70
+ // ── [CLOUD EXTENSION] ────────────────────────────────────────────────────
71
+ // Types only the cloud tier can detect. The rule engine never emits these —
72
+ // there is no shape to match on — which is exactly why they exist: the
73
+ // model's job is what the rules cannot catch. Named to match the detection
74
+ // canon the service is trained and evaluated against.
75
+ EntityType["ORGANISATION_NAME"] = "ORGANISATION_NAME";
76
+ EntityType["JOB_TITLE"] = "JOB_TITLE";
77
+ EntityType["MEDICAL_CONDITION"] = "MEDICAL_CONDITION";
78
+ EntityType["SENSITIVE_ATTRIBUTE"] = "SENSITIVE_ATTRIBUTE";
79
+ EntityType["BIOMETRIC_REF"] = "BIOMETRIC_REF";
80
+ EntityType["FINANCIAL_AMOUNT"] = "FINANCIAL_AMOUNT";
81
+ EntityType["QUASI_IDENTIFIER"] = "QUASI_IDENTIFIER";
82
+ EntityType["CREDENTIAL"] = "CREDENTIAL";
83
+ EntityType["URL"] = "URL";
54
84
  EntityType["OTHER"] = "OTHER";
55
85
  })(EntityType || (exports.EntityType = EntityType = {}));
56
86
  /**
@@ -60,7 +90,21 @@ var EntityType;
60
90
  */
61
91
  exports.LEGACY_TYPE_ALIASES = {
62
92
  IBAN: "BANK_ACCOUNT",
93
+ NAME: "PERSON_NAME",
94
+ STREET_ADDRESS: "ADDRESS",
95
+ NATIONALITY_ETHNICITY: "SENSITIVE_ATTRIBUTE",
63
96
  };
97
+ /**
98
+ * Resolve a possibly-legacy type name to its canonical form.
99
+ *
100
+ * Mirrors `EntityType._missing_` in the Python SDK. An unrecognised name is
101
+ * returned unchanged rather than dropped: a client one release behind the
102
+ * service must not silently lose a whole category of PII.
103
+ */
104
+ function canonicalType(name) {
105
+ const upper = String(name ?? "").trim().toUpperCase();
106
+ return exports.LEGACY_TYPE_ALIASES[upper] ?? upper;
107
+ }
64
108
  var DetectionSource;
65
109
  (function (DetectionSource) {
66
110
  DetectionSource["RULES"] = "rules";
@@ -0,0 +1,42 @@
1
+ /**
2
+ * [CLOUD EXTENSION] The cloud client.
3
+ *
4
+ * Speaks to the euRedact inference service: a deterministic rules engine
5
+ * followed by a fine-tuned model asked only *what did the rules miss?* The
6
+ * service returns both the redacted document and located spans, so this
7
+ * module's job is transport and translation, not detection.
8
+ *
9
+ * Three things it hides from the caller:
10
+ *
11
+ * - **Whether the document finished inside the service's sync window.** A short
12
+ * document comes back on the original request; a long one, or one arriving
13
+ * while a GPU node is cold, returns a job handle and is polled. Callers
14
+ * should never write that branch, so `redact()` resolves either way.
15
+ * - **Retries.** Every request carries an `Idempotency-Key`, so a retry after a
16
+ * timeout cannot produce a second job or a second usage row.
17
+ * - **`Retry-After`.** When the service says how long to wait, that is obeyed
18
+ * rather than second-guessed with a backoff curve.
19
+ *
20
+ * Zero dependencies, like the rest of the package: this uses the platform's
21
+ * own `fetch`.
22
+ */
23
+ import { type CloudConfig } from "./config.js";
24
+ import { type RedactResult } from "../types.js";
25
+ export interface CloudRedactOptions {
26
+ country: string;
27
+ language?: string;
28
+ priority?: "interactive" | "batch";
29
+ rulesOnly?: boolean;
30
+ idempotencyKey?: string;
31
+ /** Injectable for tests. Defaults to the platform `fetch`. */
32
+ fetchImpl?: typeof fetch;
33
+ }
34
+ export declare class CloudClient {
35
+ readonly config: CloudConfig;
36
+ constructor(config?: CloudConfig | null);
37
+ private headers;
38
+ private raiseFor;
39
+ redact(text: string, options: CloudRedactOptions): Promise<RedactResult>;
40
+ private poll;
41
+ private withTimeout;
42
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * [CLOUD EXTENSION] The cloud client.
3
+ *
4
+ * Speaks to the euRedact inference service: a deterministic rules engine
5
+ * followed by a fine-tuned model asked only *what did the rules miss?* The
6
+ * service returns both the redacted document and located spans, so this
7
+ * module's job is transport and translation, not detection.
8
+ *
9
+ * Three things it hides from the caller:
10
+ *
11
+ * - **Whether the document finished inside the service's sync window.** A short
12
+ * document comes back on the original request; a long one, or one arriving
13
+ * while a GPU node is cold, returns a job handle and is polled. Callers
14
+ * should never write that branch, so `redact()` resolves either way.
15
+ * - **Retries.** Every request carries an `Idempotency-Key`, so a retry after a
16
+ * timeout cannot produce a second job or a second usage row.
17
+ * - **`Retry-After`.** When the service says how long to wait, that is obeyed
18
+ * rather than second-guessed with a backoff curve.
19
+ *
20
+ * Zero dependencies, like the rest of the package: this uses the platform's
21
+ * own `fetch`.
22
+ */
23
+ import { getConfig, } from "./config.js";
24
+ import { CloudError, NotConfiguredError, QuotaExceededError, TooLargeError, } from "./errors.js";
25
+ import { canonicalType, DetectionSource, EntityType, } from "../types.js";
26
+ const RETRY_STATUS = new Set([429, 500, 502, 503, 504]);
27
+ const MAX_BACKOFF_MS = 30_000;
28
+ function requireFetch(supplied) {
29
+ const impl = supplied ?? globalThis.fetch;
30
+ if (typeof impl !== "function") {
31
+ throw new NotConfiguredError("the cloud tier needs a global fetch. Node 18+ provides one; on Node 16 " +
32
+ "supply one via the fetchImpl option or install a polyfill.");
33
+ }
34
+ return impl;
35
+ }
36
+ function sleep(ms) {
37
+ return new Promise(resolve => setTimeout(resolve, ms));
38
+ }
39
+ function retryAfterMs(headers) {
40
+ const raw = headers?.get("Retry-After");
41
+ if (!raw)
42
+ return null;
43
+ const seconds = Number(raw);
44
+ // The HTTP-date form is legal too; fall back to backoff rather than parse it.
45
+ return Number.isFinite(seconds) ? Math.max(0, seconds * 1000) : null;
46
+ }
47
+ /**
48
+ * Full jitter. A synchronised retry storm from many clients is how a
49
+ * recovering service is knocked back over.
50
+ */
51
+ function backoffMs(attempt, retryAfter) {
52
+ if (retryAfter !== null)
53
+ return Math.min(retryAfter, MAX_BACKOFF_MS);
54
+ return Math.min(MAX_BACKOFF_MS, Math.random() * 2 ** attempt * 1000);
55
+ }
56
+ function uuid() {
57
+ const c = globalThis.crypto;
58
+ if (c && typeof c.randomUUID === "function")
59
+ return c.randomUUID();
60
+ return `idem-${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`;
61
+ }
62
+ /**
63
+ * Canon name -> EntityType, tolerating one this build has not heard of.
64
+ *
65
+ * An unknown type is passed through as a plain string rather than dropped:
66
+ * `Detection.entityType` is `EntityType | string` precisely so a client one
67
+ * release behind the service does not silently lose a whole category of PII.
68
+ */
69
+ function toEntityType(raw) {
70
+ const canonical = canonicalType(raw);
71
+ const known = Object.values(EntityType).includes(canonical);
72
+ return known ? canonical : canonical;
73
+ }
74
+ function toResult(payload, text) {
75
+ const detections = (payload.entities ?? []).map(span => ({
76
+ entityType: toEntityType(span.type),
77
+ start: span.start,
78
+ end: span.end,
79
+ text: span.text,
80
+ source: span.source === "model" ? DetectionSource.CLOUD : DetectionSource.RULES,
81
+ country: null,
82
+ confidence: span.confidence ?? "high",
83
+ }));
84
+ detections.sort((a, b) => a.start - b.start || b.end - a.end);
85
+ return {
86
+ redactedText: payload.redacted_text ?? text,
87
+ detections,
88
+ source: "cloud",
89
+ degraded: false,
90
+ inferredCountries: [],
91
+ evidence: [],
92
+ detectionMode: "declared",
93
+ };
94
+ }
95
+ export class CloudClient {
96
+ config;
97
+ constructor(config) {
98
+ const resolved = config ?? getConfig();
99
+ if (resolved === null)
100
+ throw new NotConfiguredError();
101
+ this.config = resolved;
102
+ }
103
+ headers(idempotencyKey) {
104
+ return {
105
+ Authorization: `Bearer ${this.config.apiKey}`,
106
+ "Content-Type": "application/json",
107
+ // A retry after a timeout must not create a second job or a second usage
108
+ // row. The key is per logical document, not per attempt.
109
+ "Idempotency-Key": idempotencyKey,
110
+ ...this.config.headers,
111
+ };
112
+ }
113
+ raiseFor(status, payload) {
114
+ const message = payload.error ?? `HTTP ${status}`;
115
+ const detail = payload.detail ?? {};
116
+ if (status === 401)
117
+ throw new CloudError(`authentication failed: ${message}`, status);
118
+ if (status === 413)
119
+ throw new TooLargeError(message, status, detail);
120
+ if (status === 429)
121
+ throw new QuotaExceededError(message, status, detail);
122
+ throw new CloudError(message, status, detail);
123
+ }
124
+ async redact(text, options) {
125
+ const doFetch = requireFetch(options.fetchImpl);
126
+ const key = options.idempotencyKey ?? uuid();
127
+ const body = JSON.stringify({
128
+ text,
129
+ country: options.country.toUpperCase(),
130
+ language: options.language ?? "",
131
+ priority: options.priority ?? "interactive",
132
+ rules_only: options.rulesOnly ?? false,
133
+ });
134
+ let attempt = 0;
135
+ for (;;) {
136
+ let response;
137
+ let payload = {};
138
+ try {
139
+ response = await this.withTimeout(doFetch, `${this.config.baseUrl}/v1/redact`, {
140
+ method: "POST",
141
+ headers: this.headers(key),
142
+ body,
143
+ });
144
+ payload = await readJson(response);
145
+ }
146
+ catch (e) {
147
+ if (attempt >= this.config.maxRetries) {
148
+ throw new CloudError(`cloud request failed: ${e instanceof Error ? e.message : String(e)}`);
149
+ }
150
+ }
151
+ if (response) {
152
+ if (response.status === 202) {
153
+ // Outlived the sync window. Location is where the answer will appear;
154
+ // polling it is this client's job, not the caller's.
155
+ const location = response.headers.get("Location") ??
156
+ payload.location ??
157
+ `/v1/jobs/${payload.job_id ?? ""}`;
158
+ return toResult(await this.poll(doFetch, location, key), text);
159
+ }
160
+ if (response.status === 200)
161
+ return toResult(payload, text);
162
+ if (!RETRY_STATUS.has(response.status) || attempt >= this.config.maxRetries) {
163
+ this.raiseFor(response.status, payload);
164
+ }
165
+ }
166
+ await sleep(backoffMs(attempt, retryAfterMs(response?.headers)));
167
+ attempt++;
168
+ }
169
+ }
170
+ async poll(doFetch, location, key) {
171
+ const url = location.startsWith("http")
172
+ ? location
173
+ : `${this.config.baseUrl}${location}`;
174
+ const deadline = Date.now() + this.config.pollTimeoutMs;
175
+ let delay = 500;
176
+ for (;;) {
177
+ const response = await this.withTimeout(doFetch, url, {
178
+ method: "GET",
179
+ headers: this.headers(key),
180
+ });
181
+ const payload = await readJson(response);
182
+ if (response.status === 200 && payload.redacted_text !== undefined) {
183
+ return payload;
184
+ }
185
+ if (response.status !== 200 && !RETRY_STATUS.has(response.status)) {
186
+ this.raiseFor(response.status, payload);
187
+ }
188
+ if (Date.now() >= deadline) {
189
+ throw new CloudError(`document did not complete within ${Math.round(this.config.pollTimeoutMs / 1000)}s (${url})`);
190
+ }
191
+ const after = retryAfterMs(response.headers);
192
+ await sleep(after ?? delay);
193
+ delay = Math.min(delay * 1.5, 5000);
194
+ }
195
+ }
196
+ async withTimeout(doFetch, url, init) {
197
+ const controller = new AbortController();
198
+ const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
199
+ try {
200
+ return await doFetch(url, { ...init, signal: controller.signal });
201
+ }
202
+ finally {
203
+ clearTimeout(timer);
204
+ }
205
+ }
206
+ }
207
+ async function readJson(response) {
208
+ try {
209
+ const data = await response.json();
210
+ return data && typeof data === "object" ? data : {};
211
+ }
212
+ catch {
213
+ return {};
214
+ }
215
+ }
@@ -0,0 +1,43 @@
1
+ /** [CLOUD EXTENSION] Cloud tier configuration. */
2
+ export declare const DEFAULT_BASE_URL = "https://api.euredact.dev";
3
+ /**
4
+ * How long to wait for a single document. The service holds a connection open
5
+ * for its sync window and then hands back a job handle, so this has to exceed
6
+ * that window or the client abandons requests the service is about to answer.
7
+ */
8
+ export declare const DEFAULT_TIMEOUT_MS = 90000;
9
+ /**
10
+ * Total wall-clock budget for a document that outlives the sync window and has
11
+ * to be polled. Cold-starting a GPU node and loading an ~18 GB checkpoint takes
12
+ * minutes, and an async job absorbs that invisibly — but not forever.
13
+ */
14
+ export declare const DEFAULT_POLL_TIMEOUT_MS = 900000;
15
+ export declare const DEFAULT_MAX_RETRIES = 3;
16
+ export interface CloudConfig {
17
+ apiKey: string;
18
+ baseUrl: string;
19
+ timeoutMs: number;
20
+ pollTimeoutMs: number;
21
+ maxRetries: number;
22
+ /** Extra headers, for a proxy or a self-hosted deployment. */
23
+ headers: Record<string, string>;
24
+ }
25
+ export interface ConfigureOptions {
26
+ apiKey?: string;
27
+ baseUrl?: string;
28
+ timeoutMs?: number;
29
+ pollTimeoutMs?: number;
30
+ maxRetries?: number;
31
+ headers?: Record<string, string>;
32
+ }
33
+ /**
34
+ * Configure the cloud tier.
35
+ *
36
+ * Falls back to `EUREDACT_API_KEY` and `EUREDACT_BASE_URL` so a key never has
37
+ * to be written into source to get started.
38
+ */
39
+ export declare function configure(options?: ConfigureOptions): CloudConfig;
40
+ /** The active configuration, or null. Never throws. */
41
+ export declare function getConfig(): CloudConfig | null;
42
+ /** Forget the configuration. Mainly for tests. */
43
+ export declare function reset(): void;
@@ -0,0 +1,51 @@
1
+ /** [CLOUD EXTENSION] Cloud tier configuration. */
2
+ export const DEFAULT_BASE_URL = "https://api.euredact.dev";
3
+ /**
4
+ * How long to wait for a single document. The service holds a connection open
5
+ * for its sync window and then hands back a job handle, so this has to exceed
6
+ * that window or the client abandons requests the service is about to answer.
7
+ */
8
+ export const DEFAULT_TIMEOUT_MS = 90_000;
9
+ /**
10
+ * Total wall-clock budget for a document that outlives the sync window and has
11
+ * to be polled. Cold-starting a GPU node and loading an ~18 GB checkpoint takes
12
+ * minutes, and an async job absorbs that invisibly — but not forever.
13
+ */
14
+ export const DEFAULT_POLL_TIMEOUT_MS = 900_000;
15
+ export const DEFAULT_MAX_RETRIES = 3;
16
+ let config = null;
17
+ /** Read an env var where there is an environment to read. Browsers have none. */
18
+ function env(name) {
19
+ const proc = globalThis.process;
20
+ return proc?.env?.[name];
21
+ }
22
+ /**
23
+ * Configure the cloud tier.
24
+ *
25
+ * Falls back to `EUREDACT_API_KEY` and `EUREDACT_BASE_URL` so a key never has
26
+ * to be written into source to get started.
27
+ */
28
+ export function configure(options = {}) {
29
+ const apiKey = options.apiKey ?? env("EUREDACT_API_KEY") ?? "";
30
+ if (!apiKey) {
31
+ throw new Error("no API key: pass configure({ apiKey }) or set EUREDACT_API_KEY");
32
+ }
33
+ config = {
34
+ apiKey,
35
+ baseUrl: (options.baseUrl ?? env("EUREDACT_BASE_URL") ?? DEFAULT_BASE_URL)
36
+ .replace(/\/+$/, ""),
37
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
38
+ pollTimeoutMs: options.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
39
+ maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
40
+ headers: { ...(options.headers ?? {}) },
41
+ };
42
+ return config;
43
+ }
44
+ /** The active configuration, or null. Never throws. */
45
+ export function getConfig() {
46
+ return config;
47
+ }
48
+ /** Forget the configuration. Mainly for tests. */
49
+ export function reset() {
50
+ config = null;
51
+ }
@@ -0,0 +1,32 @@
1
+ /** [CLOUD EXTENSION] Errors the cloud tier can raise. */
2
+ export declare class CloudError extends Error {
3
+ readonly status?: number;
4
+ readonly detail: Record<string, unknown>;
5
+ constructor(message: string, status?: number, detail?: Record<string, unknown>);
6
+ }
7
+ /**
8
+ * Raised when the cloud tier is used without configuration.
9
+ *
10
+ * Deliberately an error rather than a quiet fallback to rules-only output.
11
+ * `mode: "cloud"` that silently returns rules-only results is the worst failure
12
+ * this library can have: the caller believes names, employers and diagnoses
13
+ * were checked, sees a plausible redacted document, and ships it with the PII
14
+ * still in it.
15
+ */
16
+ export declare class NotConfiguredError extends CloudError {
17
+ constructor(message?: string);
18
+ }
19
+ /** 429 after retries were exhausted. */
20
+ export declare class QuotaExceededError extends CloudError {
21
+ constructor(message: string, status?: number, detail?: Record<string, unknown>);
22
+ }
23
+ /**
24
+ * 413. The document is over the service's input cap.
25
+ *
26
+ * Permanent and not retryable. The model has never seen a chunk boundary, so
27
+ * the service refuses oversized input rather than splitting it and changing the
28
+ * accuracy story silently.
29
+ */
30
+ export declare class TooLargeError extends CloudError {
31
+ constructor(message: string, status?: number, detail?: Record<string, unknown>);
32
+ }
@@ -0,0 +1,48 @@
1
+ /** [CLOUD EXTENSION] Errors the cloud tier can raise. */
2
+ export class CloudError extends Error {
3
+ status;
4
+ detail;
5
+ constructor(message, status, detail) {
6
+ super(message);
7
+ this.name = "CloudError";
8
+ this.status = status;
9
+ this.detail = detail ?? {};
10
+ }
11
+ }
12
+ /**
13
+ * Raised when the cloud tier is used without configuration.
14
+ *
15
+ * Deliberately an error rather than a quiet fallback to rules-only output.
16
+ * `mode: "cloud"` that silently returns rules-only results is the worst failure
17
+ * this library can have: the caller believes names, employers and diagnoses
18
+ * were checked, sees a plausible redacted document, and ships it with the PII
19
+ * still in it.
20
+ */
21
+ export class NotConfiguredError extends CloudError {
22
+ constructor(message) {
23
+ super(message ??
24
+ "Cloud tier not configured. Call euredact.configure({ apiKey }) first, " +
25
+ "or set EUREDACT_API_KEY.");
26
+ this.name = "NotConfiguredError";
27
+ }
28
+ }
29
+ /** 429 after retries were exhausted. */
30
+ export class QuotaExceededError extends CloudError {
31
+ constructor(message, status, detail) {
32
+ super(message, status, detail);
33
+ this.name = "QuotaExceededError";
34
+ }
35
+ }
36
+ /**
37
+ * 413. The document is over the service's input cap.
38
+ *
39
+ * Permanent and not retryable. The model has never seen a chunk boundary, so
40
+ * the service refuses oversized input rather than splitting it and changing the
41
+ * accuracy story silently.
42
+ */
43
+ export class TooLargeError extends CloudError {
44
+ constructor(message, status, detail) {
45
+ super(message, status, detail);
46
+ this.name = "TooLargeError";
47
+ }
48
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * [CLOUD EXTENSION] Cloud tier — rules engine plus a fine-tuned model.
3
+ *
4
+ * The rule engine catches what has a shape: IBANs, national IDs, phone
5
+ * numbers, anything with a checksum. It cannot catch what does not — a
6
+ * person's name, an employer, a diagnosis, a job title. Those are what the
7
+ * cloud tier adds, and the `EntityType` members marked `[CLOUD EXTENSION]` are
8
+ * exactly that gap.
9
+ *
10
+ * ```ts
11
+ * import { configure, redactAsync } from "euredact";
12
+ *
13
+ * configure({ apiKey: "erk_..." });
14
+ * const result = await redactAsync(text, { countries: ["BE"], mode: "cloud" });
15
+ * ```
16
+ */
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";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * [CLOUD EXTENSION] Cloud tier — rules engine plus a fine-tuned model.
3
+ *
4
+ * The rule engine catches what has a shape: IBANs, national IDs, phone
5
+ * numbers, anything with a checksum. It cannot catch what does not — a
6
+ * person's name, an employer, a diagnosis, a job title. Those are what the
7
+ * cloud tier adds, and the `EntityType` members marked `[CLOUD EXTENSION]` are
8
+ * exactly that gap.
9
+ *
10
+ * ```ts
11
+ * import { configure, redactAsync } from "euredact";
12
+ *
13
+ * configure({ apiKey: "erk_..." });
14
+ * const result = await redactAsync(text, { countries: ["BE"], mode: "cloud" });
15
+ * ```
16
+ */
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";