euredact 0.3.9 → 0.5.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/README.md +313 -25
- package/dist/cjs/cache.js +5 -0
- package/dist/cjs/cloud/client.js +243 -0
- package/dist/cjs/cloud/config.js +57 -0
- package/dist/cjs/cloud/errors.js +55 -0
- package/dist/cjs/cloud/index.js +31 -0
- package/dist/cjs/index.js +23 -3
- package/dist/cjs/rules/countries/index.js +24 -5
- package/dist/cjs/sdk.js +442 -29
- package/dist/cjs/types.js +45 -1
- package/dist/esm/cache.d.ts +2 -0
- package/dist/esm/cache.js +5 -1
- package/dist/esm/cloud/client.d.ts +42 -0
- package/dist/esm/cloud/client.js +239 -0
- package/dist/esm/cloud/config.d.ts +43 -0
- package/dist/esm/cloud/config.js +51 -0
- package/dist/esm/cloud/errors.d.ts +32 -0
- package/dist/esm/cloud/errors.js +48 -0
- package/dist/esm/cloud/index.d.ts +19 -0
- package/dist/esm/cloud/index.js +19 -0
- package/dist/esm/index.d.ts +11 -3
- package/dist/esm/index.js +12 -2
- package/dist/esm/rules/countries/index.js +24 -5
- package/dist/esm/sdk.d.ts +128 -1
- package/dist/esm/sdk.js +405 -28
- package/dist/esm/types.d.ts +63 -1
- package/dist/esm/types.js +44 -1
- package/package.json +7 -3
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* [CLOUD EXTENSION] The cloud client.
|
|
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
|
|
8
|
+
* module's job is transport and translation, not detection.
|
|
9
|
+
*
|
|
10
|
+
* Three things it hides from the caller:
|
|
11
|
+
*
|
|
12
|
+
* - **Whether the document finished inside the service's sync window.** A short
|
|
13
|
+
* document comes back on the original request; a long one, or one arriving
|
|
14
|
+
* while a GPU node is cold, returns a job handle and is polled. Callers
|
|
15
|
+
* should never write that branch, so `redact()` resolves either way.
|
|
16
|
+
* - **Retries.** Every request carries an `Idempotency-Key`, so a retry after a
|
|
17
|
+
* timeout cannot produce a second job or a second usage row.
|
|
18
|
+
* - **`Retry-After`.** When the service says how long to wait, that is obeyed
|
|
19
|
+
* rather than second-guessed with a backoff curve.
|
|
20
|
+
*
|
|
21
|
+
* Zero dependencies, like the rest of the package: this uses the platform's
|
|
22
|
+
* own `fetch`.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.CloudClient = void 0;
|
|
26
|
+
const config_js_1 = require("./config.js");
|
|
27
|
+
const errors_js_1 = require("./errors.js");
|
|
28
|
+
const types_js_1 = require("../types.js");
|
|
29
|
+
const RETRY_STATUS = new Set([429, 500, 502, 503, 504]);
|
|
30
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
31
|
+
function requireFetch(supplied) {
|
|
32
|
+
const impl = supplied ?? globalThis.fetch;
|
|
33
|
+
if (typeof impl !== "function") {
|
|
34
|
+
throw new errors_js_1.NotConfiguredError("the cloud tier needs a global fetch. Node 18+ provides one; on Node 16 " +
|
|
35
|
+
"supply one via the fetchImpl option or install a polyfill.");
|
|
36
|
+
}
|
|
37
|
+
return impl;
|
|
38
|
+
}
|
|
39
|
+
function sleep(ms) {
|
|
40
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
41
|
+
}
|
|
42
|
+
function retryAfterMs(headers) {
|
|
43
|
+
const raw = headers?.get("Retry-After");
|
|
44
|
+
if (!raw)
|
|
45
|
+
return null;
|
|
46
|
+
const seconds = Number(raw);
|
|
47
|
+
// The HTTP-date form is legal too; fall back to backoff rather than parse it.
|
|
48
|
+
return Number.isFinite(seconds) ? Math.max(0, seconds * 1000) : null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Full jitter. A synchronised retry storm from many clients is how a
|
|
52
|
+
* recovering service is knocked back over.
|
|
53
|
+
*/
|
|
54
|
+
function backoffMs(attempt, retryAfter) {
|
|
55
|
+
if (retryAfter !== null)
|
|
56
|
+
return Math.min(retryAfter, MAX_BACKOFF_MS);
|
|
57
|
+
return Math.min(MAX_BACKOFF_MS, Math.random() * 2 ** attempt * 1000);
|
|
58
|
+
}
|
|
59
|
+
function uuid() {
|
|
60
|
+
const c = globalThis.crypto;
|
|
61
|
+
if (c && typeof c.randomUUID === "function")
|
|
62
|
+
return c.randomUUID();
|
|
63
|
+
return `idem-${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Canon name -> EntityType, tolerating one this build has not heard of.
|
|
67
|
+
*
|
|
68
|
+
* An unknown type is passed through as a plain string rather than dropped:
|
|
69
|
+
* `Detection.entityType` is `EntityType | string` precisely so a client one
|
|
70
|
+
* release behind the service does not silently lose a whole category of PII.
|
|
71
|
+
*/
|
|
72
|
+
function toEntityType(raw) {
|
|
73
|
+
const canonical = (0, types_js_1.canonicalType)(raw);
|
|
74
|
+
const known = Object.values(types_js_1.EntityType).includes(canonical);
|
|
75
|
+
return known ? canonical : canonical;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Map the service's offsets, which count code points, onto this SDK's, which
|
|
79
|
+
* count UTF-16 units.
|
|
80
|
+
*
|
|
81
|
+
* The two agree until the first character outside the BMP — an emoji, say —
|
|
82
|
+
* after which every service offset is short by one unit per such character.
|
|
83
|
+
* Returns the identity when the text has none, which is the common case and
|
|
84
|
+
* costs one regex test.
|
|
85
|
+
*/
|
|
86
|
+
function codePointOffsets(text) {
|
|
87
|
+
if (!/[\uD800-\uDBFF]/.test(text))
|
|
88
|
+
return cp => cp;
|
|
89
|
+
const units = [];
|
|
90
|
+
let i = 0;
|
|
91
|
+
for (const ch of text) {
|
|
92
|
+
units.push(i);
|
|
93
|
+
i += ch.length;
|
|
94
|
+
}
|
|
95
|
+
units.push(i);
|
|
96
|
+
return cp => units[Math.min(cp, units.length - 1)];
|
|
97
|
+
}
|
|
98
|
+
function toResult(payload, text) {
|
|
99
|
+
const offset = codePointOffsets(text);
|
|
100
|
+
const detections = (payload.entities ?? []).map(span => ({
|
|
101
|
+
entityType: toEntityType(span.type),
|
|
102
|
+
start: offset(span.start),
|
|
103
|
+
end: offset(span.end),
|
|
104
|
+
text: span.text,
|
|
105
|
+
source: span.source === "model" ? types_js_1.DetectionSource.CLOUD : types_js_1.DetectionSource.RULES,
|
|
106
|
+
country: null,
|
|
107
|
+
confidence: span.confidence ?? "high",
|
|
108
|
+
}));
|
|
109
|
+
detections.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
110
|
+
return {
|
|
111
|
+
redactedText: payload.redacted_text ?? text,
|
|
112
|
+
detections,
|
|
113
|
+
source: "cloud",
|
|
114
|
+
degraded: false,
|
|
115
|
+
inferredCountries: [],
|
|
116
|
+
evidence: [],
|
|
117
|
+
detectionMode: "declared",
|
|
118
|
+
tokens: {},
|
|
119
|
+
exempted: [],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
class CloudClient {
|
|
123
|
+
config;
|
|
124
|
+
constructor(config) {
|
|
125
|
+
const resolved = config ?? (0, config_js_1.getConfig)();
|
|
126
|
+
if (resolved === null)
|
|
127
|
+
throw new errors_js_1.NotConfiguredError();
|
|
128
|
+
this.config = resolved;
|
|
129
|
+
}
|
|
130
|
+
headers(idempotencyKey) {
|
|
131
|
+
return {
|
|
132
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
133
|
+
"Content-Type": "application/json",
|
|
134
|
+
// A retry after a timeout must not create a second job or a second usage
|
|
135
|
+
// row. The key is per logical document, not per attempt.
|
|
136
|
+
"Idempotency-Key": idempotencyKey,
|
|
137
|
+
...this.config.headers,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
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);
|
|
150
|
+
}
|
|
151
|
+
async redact(text, options) {
|
|
152
|
+
const doFetch = requireFetch(options.fetchImpl);
|
|
153
|
+
const key = options.idempotencyKey ?? uuid();
|
|
154
|
+
const body = JSON.stringify({
|
|
155
|
+
text,
|
|
156
|
+
country: options.country.toUpperCase(),
|
|
157
|
+
language: options.language ?? "",
|
|
158
|
+
priority: options.priority ?? "interactive",
|
|
159
|
+
rules_only: options.rulesOnly ?? false,
|
|
160
|
+
});
|
|
161
|
+
let attempt = 0;
|
|
162
|
+
for (;;) {
|
|
163
|
+
let response;
|
|
164
|
+
let payload = {};
|
|
165
|
+
try {
|
|
166
|
+
response = await this.withTimeout(doFetch, `${this.config.baseUrl}/v1/redact`, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: this.headers(key),
|
|
169
|
+
body,
|
|
170
|
+
});
|
|
171
|
+
payload = await readJson(response);
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
if (attempt >= this.config.maxRetries) {
|
|
175
|
+
throw new errors_js_1.CloudError(`cloud request failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (response) {
|
|
179
|
+
if (response.status === 202) {
|
|
180
|
+
// Outlived the sync window. Location is where the answer will appear;
|
|
181
|
+
// polling it is this client's job, not the caller's.
|
|
182
|
+
const location = response.headers.get("Location") ??
|
|
183
|
+
payload.location ??
|
|
184
|
+
`/v1/jobs/${payload.job_id ?? ""}`;
|
|
185
|
+
return toResult(await this.poll(doFetch, location, key), text);
|
|
186
|
+
}
|
|
187
|
+
if (response.status === 200)
|
|
188
|
+
return toResult(payload, text);
|
|
189
|
+
if (!RETRY_STATUS.has(response.status) || attempt >= this.config.maxRetries) {
|
|
190
|
+
this.raiseFor(response.status, payload);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
await sleep(backoffMs(attempt, retryAfterMs(response?.headers)));
|
|
194
|
+
attempt++;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async poll(doFetch, location, key) {
|
|
198
|
+
const url = location.startsWith("http")
|
|
199
|
+
? location
|
|
200
|
+
: `${this.config.baseUrl}${location}`;
|
|
201
|
+
const deadline = Date.now() + this.config.pollTimeoutMs;
|
|
202
|
+
let delay = 500;
|
|
203
|
+
for (;;) {
|
|
204
|
+
const response = await this.withTimeout(doFetch, url, {
|
|
205
|
+
method: "GET",
|
|
206
|
+
headers: this.headers(key),
|
|
207
|
+
});
|
|
208
|
+
const payload = await readJson(response);
|
|
209
|
+
if (response.status === 200 && payload.redacted_text !== undefined) {
|
|
210
|
+
return payload;
|
|
211
|
+
}
|
|
212
|
+
if (response.status !== 200 && !RETRY_STATUS.has(response.status)) {
|
|
213
|
+
this.raiseFor(response.status, payload);
|
|
214
|
+
}
|
|
215
|
+
if (Date.now() >= deadline) {
|
|
216
|
+
throw new errors_js_1.CloudError(`document did not complete within ${Math.round(this.config.pollTimeoutMs / 1000)}s (${url})`);
|
|
217
|
+
}
|
|
218
|
+
const after = retryAfterMs(response.headers);
|
|
219
|
+
await sleep(after ?? delay);
|
|
220
|
+
delay = Math.min(delay * 1.5, 5000);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async withTimeout(doFetch, url, init) {
|
|
224
|
+
const controller = new AbortController();
|
|
225
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
226
|
+
try {
|
|
227
|
+
return await doFetch(url, { ...init, signal: controller.signal });
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
exports.CloudClient = CloudClient;
|
|
235
|
+
async function readJson(response) {
|
|
236
|
+
try {
|
|
237
|
+
const data = await response.json();
|
|
238
|
+
return data && typeof data === "object" ? data : {};
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return {};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** [CLOUD EXTENSION] Cloud tier configuration. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.DEFAULT_MAX_RETRIES = exports.DEFAULT_POLL_TIMEOUT_MS = exports.DEFAULT_TIMEOUT_MS = exports.DEFAULT_BASE_URL = void 0;
|
|
5
|
+
exports.configure = configure;
|
|
6
|
+
exports.getConfig = getConfig;
|
|
7
|
+
exports.reset = reset;
|
|
8
|
+
exports.DEFAULT_BASE_URL = "https://api.euredact.dev";
|
|
9
|
+
/**
|
|
10
|
+
* How long to wait for a single document. The service holds a connection open
|
|
11
|
+
* for its sync window and then hands back a job handle, so this has to exceed
|
|
12
|
+
* that window or the client abandons requests the service is about to answer.
|
|
13
|
+
*/
|
|
14
|
+
exports.DEFAULT_TIMEOUT_MS = 90_000;
|
|
15
|
+
/**
|
|
16
|
+
* Total wall-clock budget for a document that outlives the sync window and has
|
|
17
|
+
* to be polled. Cold-starting a GPU node and loading an ~18 GB checkpoint takes
|
|
18
|
+
* minutes, and an async job absorbs that invisibly — but not forever.
|
|
19
|
+
*/
|
|
20
|
+
exports.DEFAULT_POLL_TIMEOUT_MS = 900_000;
|
|
21
|
+
exports.DEFAULT_MAX_RETRIES = 3;
|
|
22
|
+
let config = null;
|
|
23
|
+
/** Read an env var where there is an environment to read. Browsers have none. */
|
|
24
|
+
function env(name) {
|
|
25
|
+
const proc = globalThis.process;
|
|
26
|
+
return proc?.env?.[name];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Configure the cloud tier.
|
|
30
|
+
*
|
|
31
|
+
* Falls back to `EUREDACT_API_KEY` and `EUREDACT_BASE_URL` so a key never has
|
|
32
|
+
* to be written into source to get started.
|
|
33
|
+
*/
|
|
34
|
+
function configure(options = {}) {
|
|
35
|
+
const apiKey = options.apiKey ?? env("EUREDACT_API_KEY") ?? "";
|
|
36
|
+
if (!apiKey) {
|
|
37
|
+
throw new Error("no API key: pass configure({ apiKey }) or set EUREDACT_API_KEY");
|
|
38
|
+
}
|
|
39
|
+
config = {
|
|
40
|
+
apiKey,
|
|
41
|
+
baseUrl: (options.baseUrl ?? env("EUREDACT_BASE_URL") ?? exports.DEFAULT_BASE_URL)
|
|
42
|
+
.replace(/\/+$/, ""),
|
|
43
|
+
timeoutMs: options.timeoutMs ?? exports.DEFAULT_TIMEOUT_MS,
|
|
44
|
+
pollTimeoutMs: options.pollTimeoutMs ?? exports.DEFAULT_POLL_TIMEOUT_MS,
|
|
45
|
+
maxRetries: options.maxRetries ?? exports.DEFAULT_MAX_RETRIES,
|
|
46
|
+
headers: { ...(options.headers ?? {}) },
|
|
47
|
+
};
|
|
48
|
+
return config;
|
|
49
|
+
}
|
|
50
|
+
/** The active configuration, or null. Never throws. */
|
|
51
|
+
function getConfig() {
|
|
52
|
+
return config;
|
|
53
|
+
}
|
|
54
|
+
/** Forget the configuration. Mainly for tests. */
|
|
55
|
+
function reset() {
|
|
56
|
+
config = null;
|
|
57
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** [CLOUD EXTENSION] Errors the cloud tier can raise. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.TooLargeError = exports.QuotaExceededError = exports.NotConfiguredError = exports.CloudError = void 0;
|
|
5
|
+
class CloudError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
detail;
|
|
8
|
+
constructor(message, status, detail) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "CloudError";
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.detail = detail ?? {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.CloudError = CloudError;
|
|
16
|
+
/**
|
|
17
|
+
* Raised when the cloud tier is used without configuration.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately an error rather than a quiet fallback to rules-only output.
|
|
20
|
+
* `mode: "cloud"` that silently returns rules-only results is the worst failure
|
|
21
|
+
* this library can have: the caller believes names, employers and diagnoses
|
|
22
|
+
* were checked, sees a plausible redacted document, and ships it with the PII
|
|
23
|
+
* still in it.
|
|
24
|
+
*/
|
|
25
|
+
class NotConfiguredError extends CloudError {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message ??
|
|
28
|
+
"Cloud tier not configured. Call euredact.configure({ apiKey }) first, " +
|
|
29
|
+
"or set EUREDACT_API_KEY.");
|
|
30
|
+
this.name = "NotConfiguredError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.NotConfiguredError = NotConfiguredError;
|
|
34
|
+
/** 429 after retries were exhausted. */
|
|
35
|
+
class QuotaExceededError extends CloudError {
|
|
36
|
+
constructor(message, status, detail) {
|
|
37
|
+
super(message, status, detail);
|
|
38
|
+
this.name = "QuotaExceededError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.QuotaExceededError = QuotaExceededError;
|
|
42
|
+
/**
|
|
43
|
+
* 413. The document is over the service's input cap.
|
|
44
|
+
*
|
|
45
|
+
* Permanent and not retryable. The model has never seen a chunk boundary, so
|
|
46
|
+
* the service refuses oversized input rather than splitting it and changing the
|
|
47
|
+
* accuracy story silently.
|
|
48
|
+
*/
|
|
49
|
+
class TooLargeError extends CloudError {
|
|
50
|
+
constructor(message, status, detail) {
|
|
51
|
+
super(message, status, detail);
|
|
52
|
+
this.name = "TooLargeError";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.TooLargeError = TooLargeError;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* [CLOUD EXTENSION] Cloud tier — rules engine plus a fine-tuned model.
|
|
4
|
+
*
|
|
5
|
+
* The rule engine catches what has a shape: IBANs, national IDs, phone
|
|
6
|
+
* numbers, anything with a checksum. It cannot catch what does not — a
|
|
7
|
+
* person's name, an employer, a diagnosis, a job title. Those are what the
|
|
8
|
+
* cloud tier adds, and the `EntityType` members marked `[CLOUD EXTENSION]` are
|
|
9
|
+
* exactly that gap.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { configure, redactAsync } from "euredact";
|
|
13
|
+
*
|
|
14
|
+
* configure({ apiKey: "erk_..." });
|
|
15
|
+
* const result = await redactAsync(text, { countries: ["BE"], mode: "cloud" });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
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;
|
|
20
|
+
var client_js_1 = require("./client.js");
|
|
21
|
+
Object.defineProperty(exports, "CloudClient", { enumerable: true, get: function () { return client_js_1.CloudClient; } });
|
|
22
|
+
var config_js_1 = require("./config.js");
|
|
23
|
+
Object.defineProperty(exports, "configure", { enumerable: true, get: function () { return config_js_1.configure; } });
|
|
24
|
+
Object.defineProperty(exports, "getConfig", { enumerable: true, get: function () { return config_js_1.getConfig; } });
|
|
25
|
+
Object.defineProperty(exports, "reset", { enumerable: true, get: function () { return config_js_1.reset; } });
|
|
26
|
+
Object.defineProperty(exports, "DEFAULT_BASE_URL", { enumerable: true, get: function () { return config_js_1.DEFAULT_BASE_URL; } });
|
|
27
|
+
var errors_js_1 = require("./errors.js");
|
|
28
|
+
Object.defineProperty(exports, "CloudError", { enumerable: true, get: function () { return errors_js_1.CloudError; } });
|
|
29
|
+
Object.defineProperty(exports, "NotConfiguredError", { enumerable: true, get: function () { return errors_js_1.NotConfiguredError; } });
|
|
30
|
+
Object.defineProperty(exports, "QuotaExceededError", { enumerable: true, get: function () { return errors_js_1.QuotaExceededError; } });
|
|
31
|
+
Object.defineProperty(exports, "TooLargeError", { enumerable: true, get: function () { return errors_js_1.TooLargeError; } });
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SEED_BIC6_PREFIXES = exports.getBicRegistry = exports.setBicRegistry = exports.COUNTRY_CONFIGS = exports.EuRedact = exports.DocumentContext = exports.LEGACY_TYPE_ALIASES = exports.DetectionSource = exports.EntityType = void 0;
|
|
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;
|
|
4
4
|
exports.availableCountries = availableCountries;
|
|
5
5
|
exports.redact = redact;
|
|
6
|
+
exports.redactAsync = redactAsync;
|
|
6
7
|
exports.addCustomPattern = addCustomPattern;
|
|
7
8
|
exports.clear = clear;
|
|
8
9
|
exports.redactBatch = redactBatch;
|
|
@@ -10,18 +11,28 @@ var types_js_1 = require("./types.js");
|
|
|
10
11
|
Object.defineProperty(exports, "EntityType", { enumerable: true, get: function () { return types_js_1.EntityType; } });
|
|
11
12
|
Object.defineProperty(exports, "DetectionSource", { enumerable: true, get: function () { return types_js_1.DetectionSource; } });
|
|
12
13
|
Object.defineProperty(exports, "LEGACY_TYPE_ALIASES", { enumerable: true, get: function () { return types_js_1.LEGACY_TYPE_ALIASES; } });
|
|
14
|
+
Object.defineProperty(exports, "canonicalType", { enumerable: true, get: function () { return types_js_1.canonicalType; } });
|
|
13
15
|
var context_js_1 = require("./rules/context.js");
|
|
14
16
|
Object.defineProperty(exports, "DocumentContext", { enumerable: true, get: function () { return context_js_1.DocumentContext; } });
|
|
15
17
|
var sdk_js_1 = require("./sdk.js");
|
|
16
18
|
Object.defineProperty(exports, "EuRedact", { enumerable: true, get: function () { return sdk_js_1.EuRedact; } });
|
|
19
|
+
Object.defineProperty(exports, "restore", { enumerable: true, get: function () { return sdk_js_1.restore; } });
|
|
17
20
|
var index_js_1 = require("./rules/countries/index.js");
|
|
18
21
|
Object.defineProperty(exports, "COUNTRY_CONFIGS", { enumerable: true, get: function () { return index_js_1.COUNTRY_CONFIGS; } });
|
|
19
22
|
var bicRegistry_js_1 = require("./rules/bicRegistry.js");
|
|
20
23
|
Object.defineProperty(exports, "setBicRegistry", { enumerable: true, get: function () { return bicRegistry_js_1.setBicRegistry; } });
|
|
21
24
|
Object.defineProperty(exports, "getBicRegistry", { enumerable: true, get: function () { return bicRegistry_js_1.getBicRegistry; } });
|
|
22
25
|
Object.defineProperty(exports, "SEED_BIC6_PREFIXES", { enumerable: true, get: function () { return bicRegistry_js_1.SEED_BIC6_PREFIXES; } });
|
|
26
|
+
var index_js_2 = require("./cloud/index.js");
|
|
27
|
+
Object.defineProperty(exports, "configure", { enumerable: true, get: function () { return index_js_2.configure; } });
|
|
28
|
+
Object.defineProperty(exports, "getConfig", { enumerable: true, get: function () { return index_js_2.getConfig; } });
|
|
29
|
+
Object.defineProperty(exports, "CloudClient", { enumerable: true, get: function () { return index_js_2.CloudClient; } });
|
|
30
|
+
Object.defineProperty(exports, "CloudError", { enumerable: true, get: function () { return index_js_2.CloudError; } });
|
|
31
|
+
Object.defineProperty(exports, "NotConfiguredError", { enumerable: true, get: function () { return index_js_2.NotConfiguredError; } });
|
|
32
|
+
Object.defineProperty(exports, "QuotaExceededError", { enumerable: true, get: function () { return index_js_2.QuotaExceededError; } });
|
|
33
|
+
Object.defineProperty(exports, "TooLargeError", { enumerable: true, get: function () { return index_js_2.TooLargeError; } });
|
|
23
34
|
const sdk_js_2 = require("./sdk.js");
|
|
24
|
-
const
|
|
35
|
+
const index_js_3 = require("./rules/countries/index.js");
|
|
25
36
|
let _instance = null;
|
|
26
37
|
function getInstance() {
|
|
27
38
|
if (_instance === null)
|
|
@@ -29,11 +40,20 @@ function getInstance() {
|
|
|
29
40
|
return _instance;
|
|
30
41
|
}
|
|
31
42
|
function availableCountries() {
|
|
32
|
-
return Object.keys(
|
|
43
|
+
return Object.keys(index_js_3.COUNTRY_CONFIGS).filter(c => c !== "SHARED").sort();
|
|
33
44
|
}
|
|
34
45
|
function redact(text, options) {
|
|
35
46
|
return getInstance().redact(text, options);
|
|
36
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Redact, awaiting the cloud tier when `mode: "cloud"` is asked for.
|
|
50
|
+
*
|
|
51
|
+
* `mode: "rules"` resolves immediately with exactly what `redact()` returns,
|
|
52
|
+
* so a caller that may or may not use the cloud tier can hold one code path.
|
|
53
|
+
*/
|
|
54
|
+
function redactAsync(text, options) {
|
|
55
|
+
return getInstance().redactAsync(text, options);
|
|
56
|
+
}
|
|
37
57
|
function addCustomPattern(name, pattern) {
|
|
38
58
|
getInstance().addCustomPattern(name, pattern);
|
|
39
59
|
}
|
|
@@ -99,7 +99,14 @@ const SHARED = {
|
|
|
99
99
|
// is Unicode-aware) matched local parts like "petras_ž@..." while this one
|
|
100
100
|
// did not. \p{L}\p{N} with the `u` flag restores parity; the leading guard
|
|
101
101
|
// is a Unicode-safe word boundary.
|
|
102
|
-
|
|
102
|
+
// The apostrophe is allowed only between word characters: it belongs in a
|
|
103
|
+
// local part, but putting it in the class would let the match absorb a
|
|
104
|
+
// quote from the surrounding text, while leaving it out made
|
|
105
|
+
// `johno'neill@outlook.ie` mask as `johno'[EMAIL]` (issue rules-engine#10).
|
|
106
|
+
// The second lookbehind refuses to start a match after `<word-char>'`,
|
|
107
|
+
// which is a match already in progress; without it every apostrophe was a
|
|
108
|
+
// fresh start offset and the scan went quadratic. Mirrors Python.
|
|
109
|
+
p(types_js_1.EntityType.EMAIL, String.raw `(?<![\p{L}\p{N}._%+\-])(?<![\p{L}\p{N}._%+\-]')[\p{L}\p{N}._%+\-]+(?:'[\p{L}\p{N}._%+\-]+)*@(?:[\p{L}\p{N}\-]+\.)+\p{L}{2,}\b`),
|
|
103
110
|
// IBAN (any country) — structure + mod-97 only. Country-specific IBAN
|
|
104
111
|
// patterns still exist and win on identical spans; this one guarantees an
|
|
105
112
|
// IBAN is never missed just because its country was not requested.
|
|
@@ -109,7 +116,11 @@ const SHARED = {
|
|
|
109
116
|
// "+43 664 8213 907" was invisible to the Austrian pattern, which expects
|
|
110
117
|
// an unbroken subscriber number. A leading "+" is self-identifying, so
|
|
111
118
|
// this is deliberately not country-gated.
|
|
112
|
-
|
|
119
|
+
// Parentheses are matched as a pair inside one group: `\(?...\)?` let the
|
|
120
|
+
// closing parenthesis of the surrounding text join the number, so
|
|
121
|
+
// "Call (+32 475 12 34 56) today." also masked the ")" (issue
|
|
122
|
+
// rules-engine#3). Mirrors the Python pattern character for character.
|
|
123
|
+
p(types_js_1.EntityType.PHONE, String.raw `\+\d{1,3}(?:[\s\-/]?(?:\(\d{1,4}\)|\d{1,4})){2,7}`, "e164", "International phone number (E.164) — any country"),
|
|
113
124
|
p(types_js_1.EntityType.BIC, String.raw `\b[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?\b`, "bic"),
|
|
114
125
|
// Space-separated form: "RZBA AT WW", "NICA BE BB". Written as a separate
|
|
115
126
|
// pattern requiring a space between *every* group, rather than making the
|
|
@@ -120,7 +131,15 @@ const SHARED = {
|
|
|
120
131
|
p(types_js_1.EntityType.CREDIT_CARD, String.raw `\b(?:4[0-9]{3}[\s\-]?[0-9]{4}[\s\-]?[0-9]{4}[\s\-]?[0-9]{4}|5[1-5][0-9]{2}[\s\-]?[0-9]{4}[\s\-]?[0-9]{4}[\s\-]?[0-9]{4}|3[47][0-9]{2}[\s\-]?[0-9]{6}[\s\-]?[0-9]{5})\b`, "luhn"),
|
|
121
132
|
p(types_js_1.EntityType.VIN, String.raw `\b[A-HJ-NPR-Z0-9]{17}\b`, "vin"),
|
|
122
133
|
p(types_js_1.EntityType.IP_ADDRESS, String.raw `\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b`),
|
|
123
|
-
|
|
134
|
+
// Branch order is load-bearing: alternation is leftmost-first, not
|
|
135
|
+
// longest-match, so the form that consumes most is tried first, and the
|
|
136
|
+
// IPv4-embedded branches lead because every group of a dotted quad is
|
|
137
|
+
// also valid hex. `\b` is the wrong anchor around ":" (a non-word
|
|
138
|
+
// character), which is why `::1` and `2001:db8::` were not detected at
|
|
139
|
+
// all. A bare "::" is deliberately not matched. Mirrors the Python
|
|
140
|
+
// pattern character for character -- see the comment there
|
|
141
|
+
// (issue rules-engine#5).
|
|
142
|
+
p(types_js_1.EntityType.IPV6_ADDRESS, String.raw `(?<![0-9A-Za-z_:.])(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])|(?:[0-9A-Fa-f]{1,4}:){1,5}:(?:[0-9A-Fa-f]{1,4}:)*(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])|::(?:[0-9A-Fa-f]{1,4}:)*(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])|(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}(?::[0-9A-Fa-f]{1,4}){1,7}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|:(?::[0-9A-Fa-f]{1,4}){1,7})(?:%[0-9A-Za-z][0-9A-Za-z._-]*)?(?![0-9A-Za-z_:])`),
|
|
124
143
|
p(types_js_1.EntityType.MAC_ADDRESS, String.raw `\b(?:[0-9a-fA-F]{2}[:\-]){5}[0-9a-fA-F]{2}\b`, null, "colon/dash"),
|
|
125
144
|
p(types_js_1.EntityType.MAC_ADDRESS, String.raw `\b[0-9a-fA-F]{4}\.[0-9a-fA-F]{4}\.[0-9a-fA-F]{4}\b`, null, "Cisco"),
|
|
126
145
|
p(types_js_1.EntityType.IMEI, String.raw `\b\d{15}\b`, "imei"),
|
|
@@ -224,12 +243,12 @@ const DE = {
|
|
|
224
243
|
patterns: [
|
|
225
244
|
p(types_js_1.EntityType.TAX_ID, String.raw `\b[1-9]\d{10}\b`, "german_tax_id"),
|
|
226
245
|
p(types_js_1.EntityType.TAX_ID, String.raw `\b[1-9]\d[\s.]?\d{3}[\s.]?\d{3}[\s.]?\d{3}\b`, "german_tax_id"),
|
|
227
|
-
p(types_js_1.EntityType.NATIONAL_ID, String.raw `\b[CFGHJKLMNPRTVWXYZ][0-
|
|
246
|
+
p(types_js_1.EntityType.NATIONAL_ID, String.raw `\b[CFGHJKLMNPRTVWXYZ][0-9CFGHJKLMNPRTVWXYZ]{8}\d?\b`, null, "", ["Personalausweis", "Personalausweisnummer", "Ausweis", "Ausweisnummer", "Ausweis-Nr", "identity card", "Identitätskarte", "Perso", "PA-Nummer"], true),
|
|
228
247
|
p(types_js_1.EntityType.IBAN, String.raw `\bDE\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{2}\b`, "iban"),
|
|
229
248
|
p(types_js_1.EntityType.VAT, String.raw `\bDE\s?\d{9}\b`, "vat_de"),
|
|
230
249
|
p(types_js_1.EntityType.PHONE, String.raw `\b0[1-9]\d{1,4}[\s/\-]?\d{3,8}\b`),
|
|
231
250
|
p(types_js_1.EntityType.PHONE, String.raw `\+49\s?\d{2,5}[\s/\-]?\d{3,8}\b`),
|
|
232
|
-
p(types_js_1.EntityType.PASSPORT, String.raw `\b[CFGHJK][0-
|
|
251
|
+
p(types_js_1.EntityType.PASSPORT, String.raw `\b[CFGHJK][0-9CFGHJKLMNPRTVWXYZ]{8}\d?\b`, null, "", ["Reisepass", "passport", "Passnummer", "Reisepassnummer", "Reisepass Nummer", "Reisepass-Nr", "Pass Nr"], true),
|
|
233
252
|
// The separator after the city code is mandatory. When it was optional, a
|
|
234
253
|
// contiguous letter run split across both groups and the hyphen was
|
|
235
254
|
// consumed as the *second* separator, so any "LETTERS-DIGITS" token
|