euredact 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +11 -0
- package/README.md +403 -39
- package/dist/cjs/cache.js +2 -8
- package/dist/cjs/cloud/account.js +180 -0
- package/dist/cjs/cloud/batches.js +469 -0
- package/dist/cjs/cloud/client.js +124 -14
- package/dist/cjs/cloud/config.js +28 -2
- package/dist/cjs/cloud/errors.js +39 -2
- package/dist/cjs/cloud/index.js +16 -1
- package/dist/cjs/index.js +9 -1
- package/dist/cjs/platform.browser.js +98 -0
- package/dist/cjs/platform.js +140 -0
- package/dist/cjs/rules/bicGleif.js +57 -0
- package/dist/cjs/rules/bicRegistry.js +37 -11
- package/dist/cjs/rules/countries/index.js +63 -10
- package/dist/cjs/rules/cues.js +29 -9
- package/dist/cjs/rules/engine.js +14 -6
- package/dist/cjs/rules/suppressors.js +292 -22
- package/dist/cjs/rules/validators.js +50 -0
- package/dist/cjs/sdk.js +18 -7
- package/dist/cjs/types.js +4 -1
- package/dist/esm/cache.js +2 -8
- package/dist/esm/cloud/account.d.ts +155 -0
- package/dist/esm/cloud/account.js +175 -0
- package/dist/esm/cloud/batches.d.ts +166 -0
- package/dist/esm/cloud/batches.js +464 -0
- package/dist/esm/cloud/client.d.ts +83 -1
- package/dist/esm/cloud/client.js +119 -22
- package/dist/esm/cloud/config.d.ts +6 -0
- package/dist/esm/cloud/config.js +27 -2
- package/dist/esm/cloud/errors.d.ts +26 -1
- package/dist/esm/cloud/errors.js +35 -1
- package/dist/esm/cloud/index.d.ts +4 -2
- package/dist/esm/cloud/index.js +4 -2
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/platform.browser.d.ts +27 -0
- package/dist/esm/platform.browser.js +92 -0
- package/dist/esm/platform.d.ts +20 -0
- package/dist/esm/platform.js +101 -0
- package/dist/esm/rules/bicGleif.d.ts +4 -0
- package/dist/esm/rules/bicGleif.js +53 -0
- package/dist/esm/rules/bicRegistry.d.ts +25 -11
- package/dist/esm/rules/bicRegistry.js +36 -11
- package/dist/esm/rules/countries/index.js +63 -10
- package/dist/esm/rules/cues.d.ts +1 -1
- package/dist/esm/rules/cues.js +29 -9
- package/dist/esm/rules/engine.js +14 -6
- package/dist/esm/rules/suppressors.d.ts +1 -0
- package/dist/esm/rules/suppressors.js +291 -21
- package/dist/esm/rules/validators.d.ts +14 -0
- package/dist/esm/rules/validators.js +47 -0
- package/dist/esm/sdk.d.ts +51 -4
- package/dist/esm/sdk.js +18 -9
- package/dist/esm/types.d.ts +58 -2
- package/dist/esm/types.js +4 -1
- package/package.json +12 -4
package/dist/cjs/types.js
CHANGED
|
@@ -79,7 +79,8 @@ var EntityType;
|
|
|
79
79
|
EntityType["BIOMETRIC_REF"] = "BIOMETRIC_REF";
|
|
80
80
|
EntityType["FINANCIAL_AMOUNT"] = "FINANCIAL_AMOUNT";
|
|
81
81
|
EntityType["QUASI_IDENTIFIER"] = "QUASI_IDENTIFIER";
|
|
82
|
-
|
|
82
|
+
/** Legacy alias of SECRET: every credential, the model's included, is SECRET (rules-engine#111). */
|
|
83
|
+
EntityType["CREDENTIAL"] = "SECRET";
|
|
83
84
|
EntityType["URL"] = "URL";
|
|
84
85
|
EntityType["OTHER"] = "OTHER";
|
|
85
86
|
})(EntityType || (exports.EntityType = EntityType = {}));
|
|
@@ -93,6 +94,8 @@ exports.LEGACY_TYPE_ALIASES = {
|
|
|
93
94
|
NAME: "PERSON_NAME",
|
|
94
95
|
STREET_ADDRESS: "ADDRESS",
|
|
95
96
|
NATIONALITY_ETHNICITY: "SENSITIVE_ATTRIBUTE",
|
|
97
|
+
// The model labels prose credentials CREDENTIAL; the SDK reports every credential as SECRET (rules-engine#111).
|
|
98
|
+
CREDENTIAL: "SECRET",
|
|
96
99
|
};
|
|
97
100
|
/**
|
|
98
101
|
* Resolve a possibly-legacy type name to its canonical form.
|
package/dist/esm/cache.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sha256Hex } from "./platform.js";
|
|
2
2
|
// Default ceiling on retained characters, not entries. A count-only cap of
|
|
3
3
|
// 1024 entries says nothing about memory: 1024 cached 1 MB documents is a
|
|
4
4
|
// gigabyte of PII-bearing text held live. ~16M characters is a few tens of MB.
|
|
@@ -33,13 +33,7 @@ export class ResultCache {
|
|
|
33
33
|
// deployment an attacker who can guess a victim's document could submit a
|
|
34
34
|
// same-length colliding one and be served the victim's cached result,
|
|
35
35
|
// detections and raw matched PII included.
|
|
36
|
-
return
|
|
37
|
-
.update(text)
|
|
38
|
-
.update("|")
|
|
39
|
-
.update(sorted.join("|"))
|
|
40
|
-
.update("|")
|
|
41
|
-
.update(mode)
|
|
42
|
-
.digest("hex");
|
|
36
|
+
return sha256Hex([text, "|", sorted.join("|"), "|", mode]);
|
|
43
37
|
}
|
|
44
38
|
get(key) {
|
|
45
39
|
if (!this.enabled)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [CLOUD EXTENSION] The account and job endpoints an API key may call.
|
|
3
|
+
*
|
|
4
|
+
* Everything a customer's key can do on the service is reachable from the SDK
|
|
5
|
+
* (rules-engine#89): redaction (`CloudClient`, `redactAsync`), batches
|
|
6
|
+
* (`Batches`), a past job (`Jobs`) and the account itself (`Account`). What
|
|
7
|
+
* needs a signed-in person -- logging in, minting keys, changing settings,
|
|
8
|
+
* members, accepting terms -- is not here, because a key cannot do it.
|
|
9
|
+
*
|
|
10
|
+
* Every typed value carries `raw`, the service's JSON, so a field the SDK does
|
|
11
|
+
* not know yet is still reachable.
|
|
12
|
+
*/
|
|
13
|
+
import { type CloudConfig } from "./config.js";
|
|
14
|
+
import type { RedactResult } from "../types.js";
|
|
15
|
+
type Json = Record<string, unknown>;
|
|
16
|
+
/** `GET /v1/account`: plan limits, retention and usage against quota. */
|
|
17
|
+
export interface AccountSummary {
|
|
18
|
+
tenantId: string | null;
|
|
19
|
+
tenantName: string | null;
|
|
20
|
+
active: boolean | null;
|
|
21
|
+
memberSince: string | null;
|
|
22
|
+
role: string | null;
|
|
23
|
+
/** `"ttl"`, or `"none"` when text is discarded on delivery. */
|
|
24
|
+
retentionMode: string | null;
|
|
25
|
+
payloadTtlHours: number | null;
|
|
26
|
+
documentsToday: number | null;
|
|
27
|
+
documentsThisMonth: number | null;
|
|
28
|
+
tokensThisMonth: number | null;
|
|
29
|
+
failures24h: number | null;
|
|
30
|
+
/** `null` for an account without a daily quota. */
|
|
31
|
+
dailyQuota: number | null;
|
|
32
|
+
quotaRemaining: number | null;
|
|
33
|
+
activeKeys: number | null;
|
|
34
|
+
batchesAvailable: boolean | null;
|
|
35
|
+
raw: Json;
|
|
36
|
+
}
|
|
37
|
+
/** `GET /v1/account/credits`: the balance, in credits. */
|
|
38
|
+
export interface Credits {
|
|
39
|
+
balance: number | null;
|
|
40
|
+
granted: number | null;
|
|
41
|
+
spent: number | null;
|
|
42
|
+
unit: string | null;
|
|
43
|
+
raw: Json;
|
|
44
|
+
}
|
|
45
|
+
/** One line of the credit history: movements rolled up per minute, reason and direction, newest first. */
|
|
46
|
+
export interface CreditEntry {
|
|
47
|
+
at: string | null;
|
|
48
|
+
/** Negative for a debit. */
|
|
49
|
+
delta: number | null;
|
|
50
|
+
reason: string | null;
|
|
51
|
+
count: number | null;
|
|
52
|
+
jobId: string | null;
|
|
53
|
+
sourceRef: string | null;
|
|
54
|
+
raw: Json;
|
|
55
|
+
}
|
|
56
|
+
/** One day of `GET /v1/account/usage`, or of one key's series. */
|
|
57
|
+
export interface UsageDay {
|
|
58
|
+
day: string | null;
|
|
59
|
+
documents: number | null;
|
|
60
|
+
tokens: number | null;
|
|
61
|
+
failures: number | null;
|
|
62
|
+
rulesOnly: number | null;
|
|
63
|
+
raw: Json;
|
|
64
|
+
}
|
|
65
|
+
/** One key of `GET /v1/account/usage/by-key`; the rest are rolled into one with `keyId` null. */
|
|
66
|
+
export interface KeyUsage {
|
|
67
|
+
keyId: number | null;
|
|
68
|
+
name: string | null;
|
|
69
|
+
documents: number | null;
|
|
70
|
+
tokens: number | null;
|
|
71
|
+
failures: number | null;
|
|
72
|
+
series: UsageDay[];
|
|
73
|
+
raw: Json;
|
|
74
|
+
}
|
|
75
|
+
/** One key of `GET /v1/account/keys`. Never its secret. */
|
|
76
|
+
export interface ApiKey {
|
|
77
|
+
id: number | null;
|
|
78
|
+
name: string | null;
|
|
79
|
+
createdBy: string | null;
|
|
80
|
+
createdAt: string | null;
|
|
81
|
+
lastUsedAt: string | null;
|
|
82
|
+
revokedAt: string | null;
|
|
83
|
+
active: boolean | null;
|
|
84
|
+
raw: Json;
|
|
85
|
+
}
|
|
86
|
+
/** The answer to `POST /v1/account/keys/{id}/revoke`. */
|
|
87
|
+
export interface KeyRevocation {
|
|
88
|
+
id: number | null;
|
|
89
|
+
keysRemaining: number | null;
|
|
90
|
+
/** True when no active key is left: nothing can call the service until a person mints one. */
|
|
91
|
+
lockedOut: boolean | null;
|
|
92
|
+
/** True when the key revoked is the one that made this call. */
|
|
93
|
+
selfRevoked: boolean | null;
|
|
94
|
+
raw: Json;
|
|
95
|
+
}
|
|
96
|
+
/** `GET /v1/jobs/{id}`: a job's state, and its result once it has one. */
|
|
97
|
+
export interface Job {
|
|
98
|
+
jobId: string;
|
|
99
|
+
/** `queued`, `running` or `succeeded`. A failed or rejected job throws instead. */
|
|
100
|
+
status: string;
|
|
101
|
+
createdAt: string | null;
|
|
102
|
+
/**
|
|
103
|
+
* Set when the job succeeded. Its text and offsets are those of the text the
|
|
104
|
+
* service received -- under the local-first cloud mode, the locally masked
|
|
105
|
+
* text, not the caller's original.
|
|
106
|
+
*/
|
|
107
|
+
result: RedactResult | null;
|
|
108
|
+
raw: Json;
|
|
109
|
+
}
|
|
110
|
+
export interface ApiClientOptions {
|
|
111
|
+
/** Injectable for tests. Defaults to the platform `fetch`. */
|
|
112
|
+
fetchImpl?: typeof fetch;
|
|
113
|
+
}
|
|
114
|
+
/** A JSON client: GETs are retried like the redact call; writes are sent once. */
|
|
115
|
+
declare class ApiClient {
|
|
116
|
+
readonly config: CloudConfig;
|
|
117
|
+
private readonly fetchImpl?;
|
|
118
|
+
constructor(config?: CloudConfig | null, options?: ApiClientOptions);
|
|
119
|
+
protected call(method: "GET" | "POST", path: string, params?: Record<string, number>, retry?: boolean): Promise<Json>;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The account an API key belongs to: summary, credits, usage and keys.
|
|
123
|
+
*
|
|
124
|
+
* These calls pass with an API key while the service accepts account calls
|
|
125
|
+
* without a browser `Origin` (its `EUREDACT_CONSOLE_REQUIRE_ORIGIN=0`); a
|
|
126
|
+
* refusal arrives as a `CloudError` with status 403.
|
|
127
|
+
*/
|
|
128
|
+
export declare class Account extends ApiClient {
|
|
129
|
+
summary(): Promise<AccountSummary>;
|
|
130
|
+
credits(): Promise<Credits>;
|
|
131
|
+
/** Recent credit movements, newest first (1-500). */
|
|
132
|
+
creditHistory(limit?: number): Promise<CreditEntry[]>;
|
|
133
|
+
/** Documents, tokens and failures per day over the last `days`. */
|
|
134
|
+
usage(days?: number): Promise<UsageDay[]>;
|
|
135
|
+
/** The same window, split by the key that submitted the work. */
|
|
136
|
+
usageByKey(days?: number): Promise<KeyUsage[]>;
|
|
137
|
+
keys(): Promise<ApiKey[]>;
|
|
138
|
+
/**
|
|
139
|
+
* Revoke a key, e.g. one that leaked: the one write a key may make. Sent once,
|
|
140
|
+
* never retried. Already revoked throws a `CloudError` with status 409; a key
|
|
141
|
+
* that does not exist, or is another account's, throws `NotFoundError`.
|
|
142
|
+
* Revoking the key this client uses works, and is reported by `selfRevoked`.
|
|
143
|
+
*/
|
|
144
|
+
revokeKey(keyId: number): Promise<KeyRevocation>;
|
|
145
|
+
}
|
|
146
|
+
/** Past jobs, by id. */
|
|
147
|
+
export declare class Jobs extends ApiClient {
|
|
148
|
+
/**
|
|
149
|
+
* A job's state, and its result if it has one. Throws `ResultExpiredError`
|
|
150
|
+
* when the job succeeded but its result is no longer retained, and
|
|
151
|
+
* `NotFoundError` for an unknown job (or another account's).
|
|
152
|
+
*/
|
|
153
|
+
retrieve(jobId: string): Promise<Job>;
|
|
154
|
+
}
|
|
155
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [CLOUD EXTENSION] The account and job endpoints an API key may call.
|
|
3
|
+
*
|
|
4
|
+
* Everything a customer's key can do on the service is reachable from the SDK
|
|
5
|
+
* (rules-engine#89): redaction (`CloudClient`, `redactAsync`), batches
|
|
6
|
+
* (`Batches`), a past job (`Jobs`) and the account itself (`Account`). What
|
|
7
|
+
* needs a signed-in person -- logging in, minting keys, changing settings,
|
|
8
|
+
* members, accepting terms -- is not here, because a key cannot do it.
|
|
9
|
+
*
|
|
10
|
+
* Every typed value carries `raw`, the service's JSON, so a field the SDK does
|
|
11
|
+
* not know yet is still reachable.
|
|
12
|
+
*/
|
|
13
|
+
import { getConfig, requireSecureBaseUrl } from "./config.js";
|
|
14
|
+
import { CloudError, NotConfiguredError } from "./errors.js";
|
|
15
|
+
import { RETRY_STATUS, backoffMs, cloudErrorFor, isInteger, isQuota, readJson, requireFetch, retryAfterMs, sleep, toResult, uuid, } from "./client.js";
|
|
16
|
+
const str = (v) => (typeof v === "string" ? v : null);
|
|
17
|
+
const int = (v) => (isInteger(v) ? v : null);
|
|
18
|
+
const bool = (v) => (typeof v === "boolean" ? v : null);
|
|
19
|
+
const obj = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : {});
|
|
20
|
+
const objects = (v) => (Array.isArray(v) ? v : []).filter((x) => !!x && typeof x === "object" && !Array.isArray(x));
|
|
21
|
+
const usageDay = (r) => ({
|
|
22
|
+
day: str(r.day), documents: int(r.documents), tokens: int(r.tokens),
|
|
23
|
+
failures: int(r.failures), rulesOnly: int(r.rules_only), raw: r,
|
|
24
|
+
});
|
|
25
|
+
/** A JSON client: GETs are retried like the redact call; writes are sent once. */
|
|
26
|
+
class ApiClient {
|
|
27
|
+
config;
|
|
28
|
+
fetchImpl;
|
|
29
|
+
constructor(config, options = {}) {
|
|
30
|
+
const resolved = config ?? getConfig();
|
|
31
|
+
if (resolved === null)
|
|
32
|
+
throw new NotConfiguredError();
|
|
33
|
+
requireSecureBaseUrl(resolved.baseUrl);
|
|
34
|
+
this.config = resolved;
|
|
35
|
+
this.fetchImpl = options.fetchImpl;
|
|
36
|
+
}
|
|
37
|
+
async call(method, path, params = {}, retry = method === "GET") {
|
|
38
|
+
const doFetch = requireFetch(this.fetchImpl);
|
|
39
|
+
const query = Object.entries(params).map(([k, v]) => `${k}=${v}`).join("&");
|
|
40
|
+
const url = `${this.config.baseUrl}${path}${query ? `?${query}` : ""}`;
|
|
41
|
+
const maxRetries = retry ? this.config.maxRetries : 0;
|
|
42
|
+
for (let attempt = 0;; attempt++) {
|
|
43
|
+
let response;
|
|
44
|
+
let payload = {};
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
47
|
+
try {
|
|
48
|
+
response = await doFetch(url, {
|
|
49
|
+
method,
|
|
50
|
+
headers: {
|
|
51
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
52
|
+
"Idempotency-Key": uuid(),
|
|
53
|
+
...this.config.headers,
|
|
54
|
+
},
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
});
|
|
57
|
+
payload = (await readJson(response));
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
if (attempt >= maxRetries) {
|
|
61
|
+
throw new CloudError(`cloud request failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
if (response) {
|
|
68
|
+
if (response.status === 200)
|
|
69
|
+
return payload;
|
|
70
|
+
if (!RETRY_STATUS.has(response.status) || attempt >= maxRetries
|
|
71
|
+
|| (response.status === 429 && isQuota(payload))) {
|
|
72
|
+
throw cloudErrorFor(response.status, payload);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
await sleep(backoffMs(attempt, retryAfterMs(response?.headers)));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The account an API key belongs to: summary, credits, usage and keys.
|
|
81
|
+
*
|
|
82
|
+
* These calls pass with an API key while the service accepts account calls
|
|
83
|
+
* without a browser `Origin` (its `EUREDACT_CONSOLE_REQUIRE_ORIGIN=0`); a
|
|
84
|
+
* refusal arrives as a `CloudError` with status 403.
|
|
85
|
+
*/
|
|
86
|
+
export class Account extends ApiClient {
|
|
87
|
+
async summary() {
|
|
88
|
+
const raw = await this.call("GET", "/v1/account");
|
|
89
|
+
const tenant = obj(raw.tenant), retention = obj(raw.retention), usage = obj(raw.usage);
|
|
90
|
+
return {
|
|
91
|
+
tenantId: str(tenant.id), tenantName: str(tenant.name), active: bool(tenant.active),
|
|
92
|
+
memberSince: str(tenant.member_since), role: str(raw.role),
|
|
93
|
+
retentionMode: str(retention.mode), payloadTtlHours: int(retention.payload_ttl_hours),
|
|
94
|
+
documentsToday: int(usage.documents_today), documentsThisMonth: int(usage.documents_this_month),
|
|
95
|
+
tokensThisMonth: int(usage.tokens_this_month), failures24h: int(usage.failures_24h),
|
|
96
|
+
dailyQuota: int(usage.daily_quota), quotaRemaining: int(usage.quota_remaining),
|
|
97
|
+
activeKeys: int(obj(raw.keys).active), batchesAvailable: bool(obj(raw.batches).available),
|
|
98
|
+
raw,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async credits() {
|
|
102
|
+
const raw = await this.call("GET", "/v1/account/credits");
|
|
103
|
+
return { balance: int(raw.balance), granted: int(raw.granted), spent: int(raw.spent),
|
|
104
|
+
unit: str(raw.unit), raw };
|
|
105
|
+
}
|
|
106
|
+
/** Recent credit movements, newest first (1-500). */
|
|
107
|
+
async creditHistory(limit = 50) {
|
|
108
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 500) {
|
|
109
|
+
throw new RangeError("limit must be between 1 and 500");
|
|
110
|
+
}
|
|
111
|
+
const data = await this.call("GET", "/v1/account/credits/history", { limit });
|
|
112
|
+
return objects(data.entries).map(r => ({
|
|
113
|
+
at: str(r.at), delta: int(r.delta), reason: str(r.reason), count: int(r.count),
|
|
114
|
+
jobId: str(r.job_id), sourceRef: str(r.source_ref), raw: r,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
/** Documents, tokens and failures per day over the last `days`. */
|
|
118
|
+
async usage(days = 30) {
|
|
119
|
+
if (!Number.isInteger(days) || days < 1)
|
|
120
|
+
throw new RangeError("days must be at least 1");
|
|
121
|
+
const data = await this.call("GET", "/v1/account/usage", { days });
|
|
122
|
+
return objects(data.series).map(usageDay);
|
|
123
|
+
}
|
|
124
|
+
/** The same window, split by the key that submitted the work. */
|
|
125
|
+
async usageByKey(days = 30) {
|
|
126
|
+
if (!Number.isInteger(days) || days < 1)
|
|
127
|
+
throw new RangeError("days must be at least 1");
|
|
128
|
+
const data = await this.call("GET", "/v1/account/usage/by-key", { days });
|
|
129
|
+
return objects(data.keys).map(r => ({
|
|
130
|
+
keyId: int(r.key_id), name: str(r.name), documents: int(r.documents),
|
|
131
|
+
tokens: int(r.tokens), failures: int(r.failures), series: objects(r.series).map(usageDay), raw: r,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
async keys() {
|
|
135
|
+
const data = await this.call("GET", "/v1/account/keys");
|
|
136
|
+
return objects(data.keys).map(r => ({
|
|
137
|
+
id: int(r.id), name: str(r.name), createdBy: str(r.created_by), createdAt: str(r.created_at),
|
|
138
|
+
lastUsedAt: str(r.last_used_at), revokedAt: str(r.revoked_at), active: bool(r.active), raw: r,
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Revoke a key, e.g. one that leaked: the one write a key may make. Sent once,
|
|
143
|
+
* never retried. Already revoked throws a `CloudError` with status 409; a key
|
|
144
|
+
* that does not exist, or is another account's, throws `NotFoundError`.
|
|
145
|
+
* Revoking the key this client uses works, and is reported by `selfRevoked`.
|
|
146
|
+
*/
|
|
147
|
+
async revokeKey(keyId) {
|
|
148
|
+
if (!Number.isInteger(keyId))
|
|
149
|
+
throw new RangeError("keyId must be an integer");
|
|
150
|
+
const raw = await this.call("POST", `/v1/account/keys/${keyId}/revoke`);
|
|
151
|
+
return { id: int(raw.id), keysRemaining: int(raw.keys_remaining), lockedOut: bool(raw.locked_out),
|
|
152
|
+
selfRevoked: bool(raw.self_revoked), raw };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Past jobs, by id. */
|
|
156
|
+
export class Jobs extends ApiClient {
|
|
157
|
+
/**
|
|
158
|
+
* A job's state, and its result if it has one. Throws `ResultExpiredError`
|
|
159
|
+
* when the job succeeded but its result is no longer retained, and
|
|
160
|
+
* `NotFoundError` for an unknown job (or another account's).
|
|
161
|
+
*/
|
|
162
|
+
async retrieve(jobId) {
|
|
163
|
+
const raw = await this.call("GET", `/v1/jobs/${encodeURIComponent(jobId)}`);
|
|
164
|
+
const result = typeof raw.redacted_text === "string"
|
|
165
|
+
? toResult(raw, raw.redacted_text)
|
|
166
|
+
: null;
|
|
167
|
+
return {
|
|
168
|
+
jobId: str(raw.job_id) ?? jobId,
|
|
169
|
+
status: str(raw.status) ?? (result ? "succeeded" : ""),
|
|
170
|
+
createdAt: str(raw.created_at),
|
|
171
|
+
result,
|
|
172
|
+
raw,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [CLOUD EXTENSION] Batches: cloud mode, with hours between masking and mapping
|
|
3
|
+
* (rules-engine#84; the gateway side is `docs/BATCHES.md` in euredact-inference).
|
|
4
|
+
*
|
|
5
|
+
* Cloud mode masks a document here, sends only the masked text and maps the
|
|
6
|
+
* service's answer back onto the original, all in one call. A batch returns up
|
|
7
|
+
* to 24 h later, so `create()` writes one **local batch file** per batch: each
|
|
8
|
+
* original, its local detections and the SHA-256 of the masked text that was
|
|
9
|
+
* sent. None of it is uploaded. `results()` maps the service's spans back from
|
|
10
|
+
* that file, checking the hash before placing a span, then wipes the originals
|
|
11
|
+
* and leaves a text-free receipt.
|
|
12
|
+
*
|
|
13
|
+
* The file format is shared with the Python SDK, offsets counted in code points,
|
|
14
|
+
* so a batch created by one can be resolved by the other.
|
|
15
|
+
*
|
|
16
|
+
* In Node the file is `~/.euredact/batches/<batchId>.json` (`0600` in a `0700`
|
|
17
|
+
* directory); `batchDir` moves it. A page has no filesystem, so there `store`
|
|
18
|
+
* is required: any object with async `read`, `write`, `delete` and `list`.
|
|
19
|
+
* `cipher` encrypts the file at rest with a key the caller holds.
|
|
20
|
+
*/
|
|
21
|
+
import type { RedactResult } from "../types.js";
|
|
22
|
+
import { EuRedact } from "../sdk.js";
|
|
23
|
+
import { type CloudConfig } from "./config.js";
|
|
24
|
+
import { CloudError } from "./errors.js";
|
|
25
|
+
/** The gateway's limits (docs/BATCHES.md §2-§3), checked before upload. */
|
|
26
|
+
export declare const MAX_DOCUMENTS = 5000;
|
|
27
|
+
export declare const MAX_CUSTOM_ID = 64;
|
|
28
|
+
/** The gateway's customId rule (euredact-inference gateway/batches.py), whole string. */
|
|
29
|
+
export declare const CUSTOM_ID: RegExp;
|
|
30
|
+
export declare const MAX_BODY_BYTES: number;
|
|
31
|
+
/**
|
|
32
|
+
* Per document, counted by the model's tokenizer on the gateway. The SDK has no
|
|
33
|
+
* copy of it and no character count decides it, so it is **not** checked before
|
|
34
|
+
* upload: a document over it comes back as an error with code `too_long`.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MAX_DOCUMENT_TOKENS = 5000;
|
|
37
|
+
export declare const FILE_FORMAT = "euredact-batch/1";
|
|
38
|
+
export declare class BatchError extends CloudError {
|
|
39
|
+
constructor(message: string);
|
|
40
|
+
}
|
|
41
|
+
/** Where local batch files live. One opaque blob per batch id. */
|
|
42
|
+
export interface BatchStore {
|
|
43
|
+
read(batchId: string): Promise<Uint8Array | null>;
|
|
44
|
+
write(batchId: string, data: Uint8Array): Promise<void>;
|
|
45
|
+
delete(batchId: string): Promise<void>;
|
|
46
|
+
list(): Promise<string[]>;
|
|
47
|
+
}
|
|
48
|
+
/** Encryption at rest, with a key the caller holds. The SDK does not pick a scheme. */
|
|
49
|
+
export interface Cipher {
|
|
50
|
+
encrypt(data: Uint8Array): Uint8Array | Promise<Uint8Array>;
|
|
51
|
+
decrypt(data: Uint8Array): Uint8Array | Promise<Uint8Array>;
|
|
52
|
+
}
|
|
53
|
+
export interface BatchDocument {
|
|
54
|
+
customId: string;
|
|
55
|
+
text: string;
|
|
56
|
+
/** Exactly one, as in cloud mode. */
|
|
57
|
+
countries: string[];
|
|
58
|
+
language?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface Batch {
|
|
61
|
+
id: string;
|
|
62
|
+
status: string;
|
|
63
|
+
createdAt: string | null;
|
|
64
|
+
expiresAt: string | null;
|
|
65
|
+
endedAt: string | null;
|
|
66
|
+
resultsExpireAt: string | null;
|
|
67
|
+
counts: Record<string, number>;
|
|
68
|
+
documents: number | null;
|
|
69
|
+
/** `{ prompt, completion }` tokens so far (rules-engine#89). */
|
|
70
|
+
tokens: Record<string, number>;
|
|
71
|
+
/** Credits debited for the batch so far, at `billingRate`. */
|
|
72
|
+
creditsCharged: number | null;
|
|
73
|
+
/** The batch rate, e.g. `0.75`: batches are billed at their own rate. */
|
|
74
|
+
billingRate: number | null;
|
|
75
|
+
raw: Record<string, unknown>;
|
|
76
|
+
}
|
|
77
|
+
/** One document of a resolved batch: a result, or why there is none. */
|
|
78
|
+
export interface DocumentOutcome {
|
|
79
|
+
customId: string;
|
|
80
|
+
ok: boolean;
|
|
81
|
+
result: RedactResult | null;
|
|
82
|
+
/**
|
|
83
|
+
* `null` on success. Otherwise a code: the gateway's (`too_long`, …),
|
|
84
|
+
* `expired`, `cancelled`, or the SDK's own `local_mismatch` (the local file
|
|
85
|
+
* does not match what was sent) or `missing_local_entry`.
|
|
86
|
+
*/
|
|
87
|
+
error: string | null;
|
|
88
|
+
message: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* `status`: `not_ended` (nothing changed locally), `resolved` (mapped now; the
|
|
92
|
+
* file is a text-free receipt), `already_resolved` (never mapped twice), or
|
|
93
|
+
* `expired` (the gateway no longer holds the results; the entries are wiped).
|
|
94
|
+
*/
|
|
95
|
+
export interface BatchResults {
|
|
96
|
+
batchId: string;
|
|
97
|
+
status: "not_ended" | "resolved" | "already_resolved" | "expired";
|
|
98
|
+
documents: Record<string, DocumentOutcome>;
|
|
99
|
+
batch: Batch | null;
|
|
100
|
+
}
|
|
101
|
+
export interface BatchesOptions {
|
|
102
|
+
config?: CloudConfig | null;
|
|
103
|
+
batchDir?: string;
|
|
104
|
+
store?: BatchStore;
|
|
105
|
+
cipher?: Cipher;
|
|
106
|
+
sdk?: EuRedact;
|
|
107
|
+
/** Injectable for tests. Defaults to the platform `fetch`. */
|
|
108
|
+
fetchImpl?: typeof fetch;
|
|
109
|
+
}
|
|
110
|
+
export declare class Batches {
|
|
111
|
+
readonly config: CloudConfig;
|
|
112
|
+
readonly store: BatchStore;
|
|
113
|
+
private readonly cipher;
|
|
114
|
+
private readonly fetchImpl;
|
|
115
|
+
private sdk;
|
|
116
|
+
constructor(options?: BatchesOptions);
|
|
117
|
+
private engine;
|
|
118
|
+
private request;
|
|
119
|
+
private json;
|
|
120
|
+
private raiseFor;
|
|
121
|
+
private load;
|
|
122
|
+
private save;
|
|
123
|
+
private static batch;
|
|
124
|
+
/**
|
|
125
|
+
* Mask every document here, upload only the masked text, keep the rest in
|
|
126
|
+
* the local batch file. The document count, `customId`s, countries and body
|
|
127
|
+
* size are checked before anything is uploaded or written. The 5,000-token
|
|
128
|
+
* limit per document is **not**: it is counted by the model's tokenizer on
|
|
129
|
+
* the gateway, which the SDK does not have, so a document over it comes back
|
|
130
|
+
* from `results()` as an error with code `too_long` that says so.
|
|
131
|
+
*/
|
|
132
|
+
create(documents: BatchDocument[], options?: {
|
|
133
|
+
idempotencyKey?: string;
|
|
134
|
+
}): Promise<Batch>;
|
|
135
|
+
/** The gateway's view: status, counts and timestamps. */
|
|
136
|
+
retrieve(batchId: string): Promise<Batch>;
|
|
137
|
+
/**
|
|
138
|
+
* This account's batches, newest first (1-100, the gateway's bound). The
|
|
139
|
+
* gateway's view only: `pending()` lists the ones with a local file here.
|
|
140
|
+
*/
|
|
141
|
+
list(limit?: number): Promise<Batch[]>;
|
|
142
|
+
/** Cancel queued documents; the local file stays pending until the finished
|
|
143
|
+
* ones are mapped by `results()`. */
|
|
144
|
+
cancel(batchId: string): Promise<Batch>;
|
|
145
|
+
/** Batch ids whose local file still waits to be mapped. Reads files only. */
|
|
146
|
+
pending(): Promise<string[]>;
|
|
147
|
+
/** Delete the local file, whatever its status. */
|
|
148
|
+
purge(batchId: string): Promise<void>;
|
|
149
|
+
/**
|
|
150
|
+
* Delete receipts and expire pending files past `results_expire_at`. Runs at
|
|
151
|
+
* the start of the calls that look in the store, so nothing has to be
|
|
152
|
+
* scheduled. A pending file past that time can never be mapped -- the gateway
|
|
153
|
+
* has swept the results -- so its originals are wiped.
|
|
154
|
+
*/
|
|
155
|
+
sweep(at?: Date): Promise<void>;
|
|
156
|
+
private wipe;
|
|
157
|
+
/**
|
|
158
|
+
* Map an ended batch back onto the originals in its local file. Each
|
|
159
|
+
* document's masked text is rebuilt from the file and must hash to both the
|
|
160
|
+
* stored SHA-256 and the gateway's `masked_sha256` before a span is placed.
|
|
161
|
+
* Anything that cannot be mapped is a per-document error, never the masked
|
|
162
|
+
* text passed off as a result.
|
|
163
|
+
*/
|
|
164
|
+
results(batchId: string): Promise<BatchResults>;
|
|
165
|
+
private outcome;
|
|
166
|
+
}
|