euredact 0.4.0 → 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 +230 -16
- package/dist/cjs/cache.js +5 -0
- package/dist/cjs/cloud/client.js +26 -2
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/rules/countries/index.js +24 -5
- package/dist/cjs/sdk.js +385 -64
- package/dist/esm/cache.d.ts +2 -0
- package/dist/esm/cache.js +5 -1
- package/dist/esm/cloud/client.js +26 -2
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/rules/countries/index.js +24 -5
- package/dist/esm/sdk.d.ts +121 -1
- package/dist/esm/sdk.js +381 -63
- package/dist/esm/types.d.ts +30 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -42,6 +42,11 @@ console.log(result.detections);
|
|
|
42
42
|
OpenAI, Slack, JWT, SendGrid, plus Shannon entropy-based detection for generic
|
|
43
43
|
high-entropy tokens near context keywords
|
|
44
44
|
- **Custom patterns:** register your own regex patterns for domain-specific PII
|
|
45
|
+
- **Reversible tokenization:** `tokenize: true` swaps values for `EMAIL_K7Q2`-style
|
|
46
|
+
tokens and `restore()` puts them back — for prompts that go to an LLM and
|
|
47
|
+
come back
|
|
48
|
+
- **Allowlist:** values that are never redacted, such as your own organisation's
|
|
49
|
+
name and addresses, per call or per instance
|
|
45
50
|
- **Checksum validation:** IBAN mod-97, Luhn (credit cards/IMEI), and 30+
|
|
46
51
|
country-specific validators
|
|
47
52
|
- **Priority-aware deduplication:** validated (checksum, corroborated by the
|
|
@@ -66,6 +71,26 @@ console.log(result.detections);
|
|
|
66
71
|
|
|
67
72
|
## API Reference
|
|
68
73
|
|
|
74
|
+
### Which option do I need?
|
|
75
|
+
|
|
76
|
+
| I want to… | Use |
|
|
77
|
+
|---|---|
|
|
78
|
+
| redact a document with no further setup | `redact(text)` — all 31 countries, `[ENTITY_TYPE]` output |
|
|
79
|
+
| restrict scope to the countries I operate in | `{ countries: ["NL", "BE"] }` |
|
|
80
|
+
| keep detection wide but resolve ambiguity | `{ countryHint: ["DE"] }` |
|
|
81
|
+
| send a prompt to an LLM and restore the reply | `{ tokenize: true }`, then [`restore()`](#restoretext-tokens) |
|
|
82
|
+
| keep relationships visible across a whole session | `{ referentialIntegrity: true }` |
|
|
83
|
+
| never redact my own company name or addresses | `{ allowlist: [...] }`, or `new EuRedact({ allowlist })` |
|
|
84
|
+
| never redact anything at my own domain | `{ allowlistDomains: ["acme.be"] }` |
|
|
85
|
+
| audit what the allowlist kept in the document | `result.exempted` |
|
|
86
|
+
| catch person names, employers, job titles, diagnoses | `redactAsync(text, { mode: "cloud" })` — see [Cloud tier](#cloud-tier) |
|
|
87
|
+
| include dates of birth | `{ detectDates: true }` |
|
|
88
|
+
| redact a document too large for one call | `context` + `chunkOffset` — see [Chunked documents](#chunked-documents) |
|
|
89
|
+
| detect an identifier the engine does not know | [`addCustomPattern()`](#addcustompatternname-pattern) |
|
|
90
|
+
| isolate tenants from each other | one `new EuRedact()` each |
|
|
91
|
+
| free PII held in memory | `clear()` |
|
|
92
|
+
|
|
93
|
+
|
|
69
94
|
### Module-Level Functions
|
|
70
95
|
|
|
71
96
|
#### `redact(text, options?)`
|
|
@@ -78,25 +103,68 @@ Main entry point. Detects and redacts PII in the given text.
|
|
|
78
103
|
|
|
79
104
|
```ts
|
|
80
105
|
interface RedactOptions {
|
|
81
|
-
countries?: string[] | null;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
106
|
+
countries?: string[] | null; // scope (see below); null = all 31
|
|
107
|
+
countryHint?: string[] | null; // a prior only; does not narrow scope
|
|
108
|
+
context?: DocumentContext | null; // share evidence across chunks
|
|
109
|
+
chunkOffset?: number; // where this chunk starts
|
|
110
|
+
mode?: string; // "rules" (default) | "cloud"
|
|
111
|
+
referentialIntegrity?: boolean; // consistent labels, EMAIL_1
|
|
112
|
+
tokenize?: boolean; // reversible tokens, EMAIL_K7Q2
|
|
113
|
+
allowlist?: string[] | null; // values never redacted
|
|
114
|
+
allowlistDomains?: string[] | null; // domains never redacted (EMAIL/URL)
|
|
115
|
+
detectDates?: boolean; // include DOB / date of death
|
|
116
|
+
cache?: boolean; // reuse results for identical input
|
|
91
117
|
}
|
|
92
118
|
```
|
|
93
119
|
|
|
120
|
+
**What is looked for**
|
|
121
|
+
|
|
94
122
|
| Parameter | Default | Description |
|
|
95
123
|
|---|---|---|
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
124
|
+
| `text` | — | Input text to scan. Longer than `maxInputLength` throws. |
|
|
125
|
+
| `countries` | `null` | ISO 3166-1 alpha-2 codes that define **scope**. `null` loads all 31. This never gates what is looked for: a detection attributed elsewhere is flagged `outOfScope`, never dropped. A bare string throws `TypeError`. See [Country codes](#country-codes). |
|
|
126
|
+
| `countryHint` | `null` | A **prior only**. Resolves an ambiguous value without narrowing scope. See [Country Hints](#country-hints). |
|
|
127
|
+
| `detectDates` | `false` | Include `DOB` and `DATE_OF_DEATH`. Off by default: a bare date without keyword or structural context is deferred to the cloud tier. |
|
|
128
|
+
|
|
129
|
+
**How the output looks** — pick at most one of the first three.
|
|
130
|
+
|
|
131
|
+
| Parameter | Default | Description |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| *(none)* | — | Default: each span becomes `[ENTITY_TYPE]`. |
|
|
134
|
+
| `referentialIntegrity` | `false` | Consistent label per distinct value (`EMAIL_1`), persisting **on the instance** across calls. See [Referential Integrity](#referential-integrity). |
|
|
135
|
+
| `tokenize` | `false` | Reversible token per value (`EMAIL_K7Q2`), with the mapping in `result.tokens` for [`restore()`](#restoretext-tokens). Unique to the **call**. Cannot be combined with `referentialIntegrity`. See [Reversible tokenization](#reversible-tokenization). |
|
|
136
|
+
| `allowlist` | `null` | Values never to redact, whole-span and case-insensitive, merged with the instance's list. Structured identifiers (IBAN, phone, VAT, …) also match across spacing and hyphenation. Applies to cloud-tier types too. See [Allowlist](#allowlist). |
|
|
137
|
+
| `allowlistDomains` | `null` | Domains whose addresses are never redacted, e.g. `["acme.be"]`. Applies to `EMAIL` and `URL` only, and covers subdomains. See [Allowlist](#allowlist). |
|
|
138
|
+
|
|
139
|
+
**Long documents and tiers**
|
|
140
|
+
|
|
141
|
+
| Parameter | Default | Description |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `mode` | `"rules"` | `"rules"` runs locally and synchronously. `"cloud"` must go through [`redactAsync()`](#redactasynctext-options) — `redact()` is synchronous and a network call cannot be. See [Cloud tier](#cloud-tier). |
|
|
144
|
+
| `context` | `null` | Share country evidence across the chunks of one document. Pass the same `DocumentContext` to every chunk. Disables the cache. See [Chunked documents](#chunked-documents). |
|
|
145
|
+
| `chunkOffset` | `0` | Where this chunk starts in the whole document. Only rebases spans recorded in `context`; returned detections stay relative to `text`. |
|
|
146
|
+
| `cache` | `true` | Reuse the result for an identical input and configuration. |
|
|
147
|
+
|
|
148
|
+
TypeScript has no `coref` option; the Python SDK accepts one as a reserved
|
|
149
|
+
no-op.
|
|
150
|
+
|
|
151
|
+
#### `redactAsync(text, options?)`
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
function redactAsync(text: string, options?: RedactOptions): Promise<RedactResult>;
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The asynchronous entry point, and the only way to reach the [cloud
|
|
158
|
+
tier](#cloud-tier). With `mode: "rules"` (the default) it resolves immediately
|
|
159
|
+
with exactly what `redact()` returns, so a caller that may or may not use the
|
|
160
|
+
cloud tier can keep one code path:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const result = await redactAsync(text, { countries: ["BE"], mode: "cloud" });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Takes the same `RedactOptions` as `redact()`. Options the service cannot honour
|
|
167
|
+
throw rather than being ignored — see [Cloud tier](#cloud-tier).
|
|
100
168
|
|
|
101
169
|
#### `redactBatch(texts, options?)`
|
|
102
170
|
|
|
@@ -116,6 +184,16 @@ function addCustomPattern(name: string, pattern: string): void;
|
|
|
116
184
|
Register a custom regex pattern. Matches are reported with `name` as the entity
|
|
117
185
|
type. See [Custom Patterns](#custom-patterns) below.
|
|
118
186
|
|
|
187
|
+
#### `restore(text, tokens)`
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
function restore(text: string, tokens: Record<string, string>): string;
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Put the original values back into text that derives from a `tokenize: true`
|
|
194
|
+
result — typically an LLM's reply to the tokenized prompt. `tokens` is
|
|
195
|
+
`result.tokens`. See [Reversible tokenization](#reversible-tokenization).
|
|
196
|
+
|
|
119
197
|
#### `availableCountries()`
|
|
120
198
|
|
|
121
199
|
```ts
|
|
@@ -142,8 +220,25 @@ console.log(result.redactedText);
|
|
|
142
220
|
// "See [CASE_REF] for details"
|
|
143
221
|
```
|
|
144
222
|
|
|
145
|
-
The `EuRedact` class exposes
|
|
146
|
-
`addCustomPattern()`.
|
|
223
|
+
The `EuRedact` class exposes `redact()`, `redactAsync()`, `redactBatch()`,
|
|
224
|
+
`clear()` and `addCustomPattern()`.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
new EuRedact({
|
|
228
|
+
maxInputLength?: number, // default 10_485_760 (~10 MB)
|
|
229
|
+
allowlist?: string[] | null,
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
| Parameter | Default | Description |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| `maxInputLength` | `10_485_760` | Longest document `redact()` accepts, in characters. Above it, it throws — split the input or raise the ceiling. |
|
|
236
|
+
| `allowlist` | `null` | Values never to redact, for every call on this instance. Merged with the per-call `allowlist`. See [Allowlist](#allowlist). |
|
|
237
|
+
| `allowlistDomains` | `null` | Domains never to redact, for every call on this instance. |
|
|
238
|
+
|
|
239
|
+
The result cache, referential-integrity labels and custom patterns are all per
|
|
240
|
+
instance, which is what makes one instance per tenant the right default.
|
|
241
|
+
`clear()` releases the cache and the label mapping.
|
|
147
242
|
|
|
148
243
|
### Return Types
|
|
149
244
|
|
|
@@ -155,6 +250,8 @@ interface RedactResult {
|
|
|
155
250
|
detections: Detection[]; // All PII spans found
|
|
156
251
|
source: string; // Detection backend ("rules")
|
|
157
252
|
degraded: boolean; // True if the engine fell back to a simpler mode
|
|
253
|
+
tokens: Record<string, string>; // token -> original value; only with tokenize: true
|
|
254
|
+
exempted: Exemption[]; // spans the allowlist kept, with the rule that matched
|
|
158
255
|
}
|
|
159
256
|
```
|
|
160
257
|
|
|
@@ -211,6 +308,7 @@ interface RedactResult {
|
|
|
211
308
|
evidence: CountryEvidence[]; // every signal, with the span behind it
|
|
212
309
|
detectionMode: string; // "declared" if countries was passed,
|
|
213
310
|
// "inferred" otherwise
|
|
311
|
+
tokens: Record<string, string>; // token -> original value; only with tokenize: true
|
|
214
312
|
}
|
|
215
313
|
```
|
|
216
314
|
|
|
@@ -314,6 +412,8 @@ seen a chunk boundary.
|
|
|
314
412
|
|
|
315
413
|
Options the service cannot honour reject rather than being ignored: multiple
|
|
316
414
|
`countries`, `countryHint`, `context`/`chunkOffset` and `referentialIntegrity`.
|
|
415
|
+
`tokenize`, `allowlist` and `allowlistDomains` are honoured: the SDK applies them to the spans the
|
|
416
|
+
service returns and rebuilds the text from those.
|
|
317
417
|
|
|
318
418
|
The package stays **zero-dependency** — the client uses the platform's own
|
|
319
419
|
`fetch`. Node 18+ provides one; on Node 16 the rules engine is unaffected and a
|
|
@@ -617,6 +717,120 @@ console.log(result.redactedText);
|
|
|
617
717
|
// "BSN NATIONAL_ID_1 en later weer NATIONAL_ID_1"
|
|
618
718
|
```
|
|
619
719
|
|
|
720
|
+
## Reversible tokenization
|
|
721
|
+
|
|
722
|
+
`tokenize: true` is for text that has to come back. Each value is replaced by a
|
|
723
|
+
token that names its type and nothing else, and the result carries the mapping
|
|
724
|
+
that turns tokens back into values:
|
|
725
|
+
|
|
726
|
+
```ts
|
|
727
|
+
import { redact, restore } from "euredact";
|
|
728
|
+
|
|
729
|
+
const prompt = "Write an email to Joren at joren.janssens@euredact.be or call +32 475 12 34 56 about the invoice.";
|
|
730
|
+
const result = redact(prompt, { countries: ["BE"], tokenize: true });
|
|
731
|
+
console.log(result.redactedText);
|
|
732
|
+
// "Write an email to Joren at EMAIL_K7Q2 or call PHONE_P4RT about the invoice."
|
|
733
|
+
console.log(result.tokens);
|
|
734
|
+
// { EMAIL_K7Q2: "joren.janssens@euredact.be", PHONE_P4RT: "+32 475 12 34 56" }
|
|
735
|
+
|
|
736
|
+
const reply = await callYourLlm(result.redactedText); // sees tokens, never the values
|
|
737
|
+
console.log(restore(reply, result.tokens));
|
|
738
|
+
// the reply, with the real address and number back in it
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
The suffixes are random; yours will differ. With the [cloud tier](#cloud-tier)
|
|
742
|
+
the name is tokenized too (`PERSON_NAME_W3NB`), since person names have no
|
|
743
|
+
shape for the rules tier to match on.
|
|
744
|
+
|
|
745
|
+
Within one call the same value gets the same token, so a prompt that names
|
|
746
|
+
someone twice still reads as one person. Across calls it gets a different
|
|
747
|
+
token: nothing is retained on the instance, `result.tokens` is the only copy,
|
|
748
|
+
and two tokenized documents never reveal that they share a value. That is the
|
|
749
|
+
opposite retention model from `referentialIntegrity`, which is why the two
|
|
750
|
+
cannot be combined. `redactBatch` tokenizes each text on its own.
|
|
751
|
+
|
|
752
|
+
A token is `TYPE_` plus four characters from `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`
|
|
753
|
+
(no vowels, no `0`/`1`/`I`/`O`). Tokens are kept clear of any token-shaped
|
|
754
|
+
string already in the document, so an LLM's reply to a tokenized prompt can
|
|
755
|
+
itself be redacted without `restore()` putting the wrong value back.
|
|
756
|
+
`restore()` replaces every occurrence, including a token an LLM glued to other
|
|
757
|
+
characters (`EMAIL_P4RTs`) — leaving a token behind is the worse failure.
|
|
758
|
+
|
|
759
|
+
Works in cloud mode via `redactAsync`: the SDK rebuilds the text from the spans
|
|
760
|
+
the service returns, which is exactly what the service built its own output
|
|
761
|
+
from.
|
|
762
|
+
|
|
763
|
+
## Allowlist
|
|
764
|
+
|
|
765
|
+
Values a caller declares are not PII to them — their own organisation's name,
|
|
766
|
+
their own addresses. Set it per call, on the instance, or both; the two merge:
|
|
767
|
+
|
|
768
|
+
```ts
|
|
769
|
+
import { EuRedact } from "euredact";
|
|
770
|
+
|
|
771
|
+
const sdk = new EuRedact({ allowlist: ["ACME NV", "info@acme.be"] });
|
|
772
|
+
|
|
773
|
+
const text = "ACME NV: mail info@acme.be or jan@acme.be about IBAN NL91 ABNA 0417 1643 00.";
|
|
774
|
+
console.log(sdk.redact(text, { countries: ["NL"] }).redactedText);
|
|
775
|
+
// "ACME NV: mail info@acme.be or [EMAIL] about IBAN [BANK_ACCOUNT]."
|
|
776
|
+
|
|
777
|
+
console.log(sdk.redact(text, { countries: ["NL"], allowlist: ["jan@acme.be"] }).redactedText);
|
|
778
|
+
// "ACME NV: mail info@acme.be or jan@acme.be about IBAN [BANK_ACCOUNT]."
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
Matching is whole-span and case-insensitive. A bare string
|
|
782
|
+
(`allowlist: "ACME NV"`) throws `TypeError` rather than being iterated into
|
|
783
|
+
single letters that exempt nothing. Works in cloud mode via `redactAsync` —
|
|
784
|
+
including on types only the model finds, such as `ORGANISATION_NAME` — and
|
|
785
|
+
together with `tokenize`.
|
|
786
|
+
|
|
787
|
+
### Spacing and punctuation
|
|
788
|
+
|
|
789
|
+
For **structured identifiers** separators are presentational, so an allowlisted
|
|
790
|
+
value matches however the document writes it:
|
|
791
|
+
|
|
792
|
+
```ts
|
|
793
|
+
const sdk = new EuRedact({ allowlist: ["NL91ABNA0417164300"] });
|
|
794
|
+
sdk.redact("Pay to NL91 ABNA 0417 1643 00.", { countries: ["NL"] }).redactedText;
|
|
795
|
+
// 'Pay to NL91 ABNA 0417 1643 00.' — exempt, despite the spacing
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
Applies to `BANK_ACCOUNT`, `BIC`, `CREDIT_CARD`, `PHONE`, `VAT`, `NATIONAL_ID`,
|
|
799
|
+
`SSN`, `TAX_ID`, `PASSPORT`, `DRIVERS_LICENSE`, `RESIDENCE_PERMIT`,
|
|
800
|
+
`HEALTH_INSURANCE`, `CHAMBER_OF_COMMERCE`, `IMEI` and `VIN`. It widens the
|
|
801
|
+
*spelling*, never the *scope*: a different account is still redacted. Free-text
|
|
802
|
+
types stay literal, because `jan.devries@acme.be` and `jandevries@acme.be` are
|
|
803
|
+
different mailboxes at most providers.
|
|
804
|
+
|
|
805
|
+
### Exempting a whole domain
|
|
806
|
+
|
|
807
|
+
```ts
|
|
808
|
+
const sdk = new EuRedact({ allowlistDomains: ["acme.be"] });
|
|
809
|
+
sdk.redact("Mail jan@acme.be or piet@acme.be", { countries: ["NL"] }).redactedText;
|
|
810
|
+
// 'Mail jan@acme.be or piet@acme.be'
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
`EMAIL` and `URL` only, covering subdomains (`mail.acme.be`). The match is on a
|
|
814
|
+
label boundary, so `acme.be` does **not** exempt `evilacme.be`. Entries may be
|
|
815
|
+
written `acme.be`, `@acme.be` or `.acme.be`.
|
|
816
|
+
|
|
817
|
+
There are deliberately **no wildcards**. The allowlist is the only option that
|
|
818
|
+
turns redaction *off*, so an over-broad entry fails toward under-redaction and
|
|
819
|
+
does so silently.
|
|
820
|
+
|
|
821
|
+
### What was exempted
|
|
822
|
+
|
|
823
|
+
```ts
|
|
824
|
+
const r = redact("Mail jan@acme.be", { countries: ["NL"], allowlistDomains: ["acme.be"] });
|
|
825
|
+
for (const e of r.exempted) console.log(e.entityType, e.text, e.rule, e.ruleKind);
|
|
826
|
+
// EMAIL jan@acme.be acme.be domain
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
`Exemption` carries `entityType`, `start`, `end`, `text`, the `rule` that
|
|
830
|
+
matched as you wrote it, and `ruleKind` (`"value"` or `"domain"`). An exempted
|
|
831
|
+
span is absent from `detections`, so `exempted` is the only record that it was
|
|
832
|
+
found at all.
|
|
833
|
+
|
|
620
834
|
## Architecture
|
|
621
835
|
|
|
622
836
|
```
|
package/dist/cjs/cache.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ResultCache = void 0;
|
|
4
|
+
exports.resultChars = resultChars;
|
|
4
5
|
const node_crypto_1 = require("node:crypto");
|
|
5
6
|
// Default ceiling on retained characters, not entries. A count-only cap of
|
|
6
7
|
// 1024 entries says nothing about memory: 1024 cached 1 MB documents is a
|
|
@@ -11,6 +12,10 @@ function resultChars(result) {
|
|
|
11
12
|
let chars = result.redactedText.length;
|
|
12
13
|
for (const d of result.detections)
|
|
13
14
|
chars += d.text.length;
|
|
15
|
+
for (const [token, value] of Object.entries(result.tokens))
|
|
16
|
+
chars += token.length + value.length;
|
|
17
|
+
for (const e of result.exempted)
|
|
18
|
+
chars += e.text.length + e.rule.length;
|
|
14
19
|
return chars;
|
|
15
20
|
}
|
|
16
21
|
class ResultCache {
|
package/dist/cjs/cloud/client.js
CHANGED
|
@@ -74,11 +74,33 @@ function toEntityType(raw) {
|
|
|
74
74
|
const known = Object.values(types_js_1.EntityType).includes(canonical);
|
|
75
75
|
return known ? canonical : canonical;
|
|
76
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
|
+
}
|
|
77
98
|
function toResult(payload, text) {
|
|
99
|
+
const offset = codePointOffsets(text);
|
|
78
100
|
const detections = (payload.entities ?? []).map(span => ({
|
|
79
101
|
entityType: toEntityType(span.type),
|
|
80
|
-
start: span.start,
|
|
81
|
-
end: span.end,
|
|
102
|
+
start: offset(span.start),
|
|
103
|
+
end: offset(span.end),
|
|
82
104
|
text: span.text,
|
|
83
105
|
source: span.source === "model" ? types_js_1.DetectionSource.CLOUD : types_js_1.DetectionSource.RULES,
|
|
84
106
|
country: null,
|
|
@@ -93,6 +115,8 @@ function toResult(payload, text) {
|
|
|
93
115
|
inferredCountries: [],
|
|
94
116
|
evidence: [],
|
|
95
117
|
detectionMode: "declared",
|
|
118
|
+
tokens: {},
|
|
119
|
+
exempted: [],
|
|
96
120
|
};
|
|
97
121
|
}
|
|
98
122
|
class CloudClient {
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TooLargeError = exports.QuotaExceededError = exports.NotConfiguredError = exports.CloudError = exports.CloudClient = exports.getConfig = exports.configure = exports.SEED_BIC6_PREFIXES = exports.getBicRegistry = exports.setBicRegistry = exports.COUNTRY_CONFIGS = exports.EuRedact = exports.DocumentContext = exports.canonicalType = 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
6
|
exports.redactAsync = redactAsync;
|
|
@@ -16,6 +16,7 @@ var context_js_1 = require("./rules/context.js");
|
|
|
16
16
|
Object.defineProperty(exports, "DocumentContext", { enumerable: true, get: function () { return context_js_1.DocumentContext; } });
|
|
17
17
|
var sdk_js_1 = require("./sdk.js");
|
|
18
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; } });
|
|
19
20
|
var index_js_1 = require("./rules/countries/index.js");
|
|
20
21
|
Object.defineProperty(exports, "COUNTRY_CONFIGS", { enumerable: true, get: function () { return index_js_1.COUNTRY_CONFIGS; } });
|
|
21
22
|
var bicRegistry_js_1 = require("./rules/bicRegistry.js");
|
|
@@ -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
|