euredact 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +73 -8
- package/dist/cjs/rules/cues.js +21 -2
- 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 +73 -8
- package/dist/esm/rules/cues.d.ts +13 -1
- package/dist/esm/rules/cues.js +21 -2
- 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");
|
|
@@ -31,6 +31,41 @@ function p(entityType, pattern, validator = null, description = "", contextKeywo
|
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
// Secret context keywords (consolidated, reused across SECRET patterns)
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Words that name a travel document, in the official languages of the 31
|
|
36
|
+
* countries this engine covers, plus English.
|
|
37
|
+
*
|
|
38
|
+
* Shared by every passport pattern. Before this existed each country kept its
|
|
39
|
+
* own list, so a passport was only recognised when its shape and its label
|
|
40
|
+
* came from the same country: `Reisepass: CA1234567` was missed because the
|
|
41
|
+
* German pattern rejects that alphabet while the Dutch pattern, whose shape
|
|
42
|
+
* fits, had never heard of "Reisepass" (issue rules-engine#23).
|
|
43
|
+
*
|
|
44
|
+
* The bare Scandinavian "pass", Finnish "passi" and Latvian "pase" are
|
|
45
|
+
* omitted deliberately: context matching is substring, not word-boundary, so
|
|
46
|
+
* they fire inside "password", "passenger", "passive" and "phase". Only the
|
|
47
|
+
* compound forms are listed. Mirrors PASSPORT_CONTEXT in Python.
|
|
48
|
+
*/
|
|
49
|
+
const PASSPORT_CONTEXT = [
|
|
50
|
+
"passport", "passport no", "passport number", "travel document",
|
|
51
|
+
"reisepass", "passnummer", "reisepassnummer", "reisepass-nr", "pass nr",
|
|
52
|
+
"reisedokument",
|
|
53
|
+
"paspoort", "paspoortnummer", "reisdocument", "identiteitsbewijs",
|
|
54
|
+
"passeport", "numéro de passeport", "document de voyage",
|
|
55
|
+
"passaporto", "numero di passaporto", "documento di viaggio",
|
|
56
|
+
"pasaporte", "número de pasaporte", "documento de viaje",
|
|
57
|
+
"passaporte", "número do passaporte",
|
|
58
|
+
"passnr", "passets nummer", "passin numero", "passinumero",
|
|
59
|
+
"passi number", "passinumber",
|
|
60
|
+
"vegabréf", "vegabréfsnúmer",
|
|
61
|
+
"paso numeris", "pases numurs",
|
|
62
|
+
"paszport", "numer paszportu", "cestovní pas", "číslo pasu",
|
|
63
|
+
"cestovný pas", "potni list", "številka potnega lista",
|
|
64
|
+
"putovnica", "broj putovnice",
|
|
65
|
+
"útlevél", "útlevélszám", "pașaport", "numărul pașaportului",
|
|
66
|
+
"паспорт", "номер на паспорт", "διαβατήριο",
|
|
67
|
+
"αριθμός διαβατηρίου", "passaport",
|
|
68
|
+
];
|
|
34
69
|
const SECRET_CONTEXT = [
|
|
35
70
|
// English
|
|
36
71
|
"key", "token", "secret", "password", "credential",
|
|
@@ -99,7 +134,14 @@ const SHARED = {
|
|
|
99
134
|
// is Unicode-aware) matched local parts like "petras_ž@..." while this one
|
|
100
135
|
// did not. \p{L}\p{N} with the `u` flag restores parity; the leading guard
|
|
101
136
|
// is a Unicode-safe word boundary.
|
|
102
|
-
|
|
137
|
+
// The apostrophe is allowed only between word characters: it belongs in a
|
|
138
|
+
// local part, but putting it in the class would let the match absorb a
|
|
139
|
+
// quote from the surrounding text, while leaving it out made
|
|
140
|
+
// `johno'neill@outlook.ie` mask as `johno'[EMAIL]` (issue rules-engine#10).
|
|
141
|
+
// The second lookbehind refuses to start a match after `<word-char>'`,
|
|
142
|
+
// which is a match already in progress; without it every apostrophe was a
|
|
143
|
+
// fresh start offset and the scan went quadratic. Mirrors Python.
|
|
144
|
+
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
145
|
// IBAN (any country) — structure + mod-97 only. Country-specific IBAN
|
|
104
146
|
// patterns still exist and win on identical spans; this one guarantees an
|
|
105
147
|
// IBAN is never missed just because its country was not requested.
|
|
@@ -109,7 +151,22 @@ const SHARED = {
|
|
|
109
151
|
// "+43 664 8213 907" was invisible to the Austrian pattern, which expects
|
|
110
152
|
// an unbroken subscriber number. A leading "+" is self-identifying, so
|
|
111
153
|
// this is deliberately not country-gated.
|
|
112
|
-
|
|
154
|
+
// Parentheses are matched as a pair inside one group: `\(?...\)?` let the
|
|
155
|
+
// closing parenthesis of the surrounding text join the number, so
|
|
156
|
+
// "Call (+32 475 12 34 56) today." also masked the ")" (issue
|
|
157
|
+
// rules-engine#3). Mirrors the Python pattern character for character.
|
|
158
|
+
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"),
|
|
159
|
+
// Passport (any country). The per-country patterns cover BE, DE, FR and NL
|
|
160
|
+
// only; the other 26 had none. Same argument as the E.164 phone rule: one
|
|
161
|
+
// country-independent shape, with the label carrying the precision.
|
|
162
|
+
// Shapes from the project canon (prompts/pii_definitions.md, PASSPORT):
|
|
163
|
+
// EU is 1-2 letters + 7 digits, UK is 9 digits optionally GBR-prefixed.
|
|
164
|
+
// The canon's position is that the EU converged on one shape, so national
|
|
165
|
+
// regexes would contradict it as well as being unverifiable. An earlier
|
|
166
|
+
// draft required a leading letter and missed all-numeric passports, the
|
|
167
|
+
// UK's own format among them; requiring exactly nine digits for the
|
|
168
|
+
// numeric form rejects eight-digit dates on shape (issue rules-engine#23).
|
|
169
|
+
p(types_js_1.EntityType.PASSPORT, String.raw `\b(?:GBR\d{9}|[A-Z]{1,2}\d{7}|\d{9})\b`, null, "Passport number — any country, label-gated", PASSPORT_CONTEXT, true),
|
|
113
170
|
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
171
|
// Space-separated form: "RZBA AT WW", "NICA BE BB". Written as a separate
|
|
115
172
|
// pattern requiring a space between *every* group, rather than making the
|
|
@@ -120,7 +177,15 @@ const SHARED = {
|
|
|
120
177
|
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
178
|
p(types_js_1.EntityType.VIN, String.raw `\b[A-HJ-NPR-Z0-9]{17}\b`, "vin"),
|
|
122
179
|
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
|
-
|
|
180
|
+
// Branch order is load-bearing: alternation is leftmost-first, not
|
|
181
|
+
// longest-match, so the form that consumes most is tried first, and the
|
|
182
|
+
// IPv4-embedded branches lead because every group of a dotted quad is
|
|
183
|
+
// also valid hex. `\b` is the wrong anchor around ":" (a non-word
|
|
184
|
+
// character), which is why `::1` and `2001:db8::` were not detected at
|
|
185
|
+
// all. A bare "::" is deliberately not matched. Mirrors the Python
|
|
186
|
+
// pattern character for character -- see the comment there
|
|
187
|
+
// (issue rules-engine#5).
|
|
188
|
+
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
189
|
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
190
|
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
191
|
p(types_js_1.EntityType.IMEI, String.raw `\b\d{15}\b`, "imei"),
|
|
@@ -195,7 +260,7 @@ const NL = {
|
|
|
195
260
|
p(types_js_1.EntityType.PHONE, String.raw `\b06[\s\-]?\d{2}[\s\-]?\d{2}[\s\-]?\d{2}[\s\-]?\d{2}\b`),
|
|
196
261
|
p(types_js_1.EntityType.PHONE, String.raw `\b0[1-9]\d{1,2}[\s\-]?\d{2,3}[\s\-]?\d{2}[\s\-]?\d{2}\b`),
|
|
197
262
|
p(types_js_1.EntityType.PHONE, String.raw `\+31\s?[1-9]\d{0,2}[\s\-]?\d{3,4}[\s\-]?\d{3,4}\b`),
|
|
198
|
-
p(types_js_1.EntityType.PASSPORT, String.raw `\b[A-Z][A-Z0-9]{8}\b`, null, "",
|
|
263
|
+
p(types_js_1.EntityType.PASSPORT, String.raw `\b[A-Z][A-Z0-9]{8}\b`, null, "", PASSPORT_CONTEXT, true),
|
|
199
264
|
p(types_js_1.EntityType.LICENSE_PLATE, String.raw `\b(?:[A-Z]{2}[\-\s]?\d{3}[\-\s]?[A-Z]|\d[\-\s]?[A-Z]{3}[\-\s]?\d{2}|\d{2}[\-\s]?[A-Z]{3}[\-\s]?\d|[A-Z]{2}[\-\s]?\d{2}[\-\s]?[A-Z]{2}|\d{2}[\-\s]?[A-Z]{2}[\-\s]?\d{2}|\d[\-\s]?[A-Z]{2}[\-\s]?\d{3})\b`),
|
|
200
265
|
p(types_js_1.EntityType.POSTAL_CODE, String.raw `\b[1-9]\d{3}\s?[A-Z]{2}\b`),
|
|
201
266
|
],
|
|
@@ -210,7 +275,7 @@ const BE = {
|
|
|
210
275
|
p(types_js_1.EntityType.CHAMBER_OF_COMMERCE, String.raw `\b0\d{3}\.?\d{3}\.?\d{3}\b`, null, "", ["KBO", "BCE", "ondernemingsnummer", "numéro d'entreprise", "enterprise number", "bedrijfsnummer", "Kruispuntbank", "Banque-Carrefour", "Banque Carrefour", "Ondernemingen", "Entreprises", "onder nummer"], true),
|
|
211
276
|
p(types_js_1.EntityType.PHONE, String.raw `\b0[1-9]\d{0,2}[/\s.\-]?\d{2,3}[.\s\-]?\d{2,3}[.\s\-]?\d{2,3}\b`),
|
|
212
277
|
p(types_js_1.EntityType.PHONE, String.raw `\+32\s?\d{1,3}[\s.\-]?\d{2,3}[\s.\-]?\d{2}[\s.\-]?\d{2}`),
|
|
213
|
-
p(types_js_1.EntityType.PASSPORT, String.raw `\b[A-Z]{2}\d{6}\b`, null, "",
|
|
278
|
+
p(types_js_1.EntityType.PASSPORT, String.raw `\b[A-Z]{2}\d{6}\b`, null, "", PASSPORT_CONTEXT, true),
|
|
214
279
|
p(types_js_1.EntityType.DRIVERS_LICENSE, String.raw `\b\d{10}\b`, null, "", ["rijbewijs", "permis de conduire", "driving licence", "driving license", "rijbewijsnummer"], true),
|
|
215
280
|
p(types_js_1.EntityType.LICENSE_PLATE, String.raw `\b[12][\-\s]?[A-Z]{3}[\-\s]?\d{3}\b`),
|
|
216
281
|
p(types_js_1.EntityType.POSTAL_CODE, String.raw `\b[1-9]\d{3}\b`, null, "", ["postcode", "code postal", "postnummer", "postal code", "zip", "B-", "adres", "adresse", "wonende", "woonplaats", "rue", "straat", "laan", "avenue", "boulevard", "plein", "steenweg", "chaussée", "domicilié", "gedomicilieerd", "Levering:", "siège"], true),
|
|
@@ -224,12 +289,12 @@ const DE = {
|
|
|
224
289
|
patterns: [
|
|
225
290
|
p(types_js_1.EntityType.TAX_ID, String.raw `\b[1-9]\d{10}\b`, "german_tax_id"),
|
|
226
291
|
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-
|
|
292
|
+
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
293
|
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
294
|
p(types_js_1.EntityType.VAT, String.raw `\bDE\s?\d{9}\b`, "vat_de"),
|
|
230
295
|
p(types_js_1.EntityType.PHONE, String.raw `\b0[1-9]\d{1,4}[\s/\-]?\d{3,8}\b`),
|
|
231
296
|
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-
|
|
297
|
+
p(types_js_1.EntityType.PASSPORT, String.raw `\b[CFGHJK][0-9CFGHJKLMNPRTVWXYZ]{8}\d?\b`, null, "", PASSPORT_CONTEXT, true),
|
|
233
298
|
// The separator after the city code is mandatory. When it was optional, a
|
|
234
299
|
// contiguous letter run split across both groups and the hyphen was
|
|
235
300
|
// consumed as the *second* separator, so any "LETTERS-DIGITS" token
|
|
@@ -290,7 +355,7 @@ const FR = {
|
|
|
290
355
|
p(types_js_1.EntityType.VAT, String.raw `\bFR\s?[0-9A-HJ-NP-Z]{2}\s?\d{9}\b`, "vat_fr"),
|
|
291
356
|
p(types_js_1.EntityType.PHONE, String.raw `\b0[1-9][\s.\-]?\d{2}[\s.\-]?\d{2}[\s.\-]?\d{2}[\s.\-]?\d{2}\b`),
|
|
292
357
|
p(types_js_1.EntityType.PHONE, String.raw `\+33\s?[1-9][\s.\-]?\d{2}[\s.\-]?\d{2}[\s.\-]?\d{2}[\s.\-]?\d{2}`),
|
|
293
|
-
p(types_js_1.EntityType.PASSPORT, String.raw `\b\d{2}[A-Z]{2}\d{5}\b`, null, "",
|
|
358
|
+
p(types_js_1.EntityType.PASSPORT, String.raw `\b\d{2}[A-Z]{2}\d{5}\b`, null, "", PASSPORT_CONTEXT, true),
|
|
294
359
|
p(types_js_1.EntityType.LICENSE_PLATE, String.raw `\b[A-Z]{2}[\-\s]?\d{3}[\-\s]?[A-Z]{2}\b`),
|
|
295
360
|
p(types_js_1.EntityType.POSTAL_CODE, String.raw `\b(?:0[1-9]|[1-8]\d|9[0-5]|97[1-6])\d{3}\b`, null, "", ["code postal", "CP", "postal code", "postcode", "adresse", "domicilié", "résidant", "rue", "avenue", "boulevard", "place", "chemin", "allée", "impasse", "ville"], true),
|
|
296
361
|
p(types_js_1.EntityType.CHAMBER_OF_COMMERCE, String.raw `\b\d{3}\s?\d{3}\s?\d{3}\b`, null, "", ["SIREN", "siren", "RCS", "entreprise", "immatricul", "numéro d'entreprise", "registre du commerce"], true),
|
package/dist/cjs/rules/cues.js
CHANGED
|
@@ -69,8 +69,20 @@ const types_js_1 = require("../types.js");
|
|
|
69
69
|
* How far back a cue may sit. Short on purpose: a cue is only evidence about
|
|
70
70
|
* the value it introduces, which is the lesson of the postal-code year defect,
|
|
71
71
|
* where a cue 150 characters away licensed every number in the window.
|
|
72
|
+
*
|
|
73
|
+
* This bounds how long a *label* may be, not how far it may sit from the
|
|
74
|
+
* value: every pattern is anchored with `$`, so the label still has to reach
|
|
75
|
+
* the span through a run-on or one qualifier word.
|
|
76
|
+
*
|
|
77
|
+
* 32 was too short for German tax labels, the longest the engine carries.
|
|
78
|
+
* "Steuerliche Identifikationsnummer " is 34 characters and
|
|
79
|
+
* "Steueridentifikationsnummer lautet: " is 36, so the label's own start sat
|
|
80
|
+
* outside the window and `(?<![A-Za-z0-9_])` had nothing to anchor against,
|
|
81
|
+
* leaving a critical-tier identifier in the clear (rules-engine#26). 44
|
|
82
|
+
* admits the longest with a qualifier: "Steuerliche Identifikationsnummer
|
|
83
|
+
* lautet: " is 42.
|
|
72
84
|
*/
|
|
73
|
-
exports.CUE_WINDOW =
|
|
85
|
+
exports.CUE_WINDOW = 44;
|
|
74
86
|
/**
|
|
75
87
|
* (entity type, label pattern). Ordered: the first match wins, so the narrower
|
|
76
88
|
* label comes first where two could read the same text.
|
|
@@ -93,8 +105,15 @@ exports.CUES = [
|
|
|
93
105
|
/(?<![A-Za-z0-9_])(?:ssn|social\s*security|sozialversicherungsnummer|svnr|sofi)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
94
106
|
// Tax number. Greece's ΑΦΜ is issued by the tax authority; the identity-card
|
|
95
107
|
// number is the ΑΔΤ, above.
|
|
108
|
+
//
|
|
109
|
+
// The German long forms are spelled out rather than left to the run-on after
|
|
110
|
+
// `steuer-?id`. A cue may use its run-on *or* one qualifier word, never both,
|
|
111
|
+
// and "Steuer-IdNr lautet:" needs both: the run-on for "Nr" and the qualifier
|
|
112
|
+
// for "lautet". Naming the whole label leaves the qualifier free.
|
|
113
|
+
// "Steuerliche Identifikationsnummer" matched nothing at all — it is two
|
|
114
|
+
// words, and `steuer-?id` cannot reach across "liche " (rules-engine#26).
|
|
96
115
|
[types_js_1.EntityType.TAX_ID,
|
|
97
|
-
/(?<![A-Za-z0-9_])(?:αφμ|α\.φ\.μ\.|steuer-?id|steuernummer|st\.?-?nr|stnr|tin|finanzamt\s+ist|tax\s*(?:id|no|number)|daňové\s*číslo|numer\s*podatkowy)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
116
|
+
/(?<![A-Za-z0-9_])(?:αφμ|α\.φ\.μ\.|steuerliche\s*identifikations-?nummer|steuer-?identifikations-?nummer|steuer-?id-?nr\.?|steuer-?id|steuernummer|st\.?-?nr|stnr|tin|finanzamt\s+ist|tax\s*(?:id|no|number)|daňové\s*číslo|numer\s*podatkowy)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|
|
98
117
|
// Health insurance: the insured person.
|
|
99
118
|
[types_js_1.EntityType.HEALTH_INSURANCE,
|
|
100
119
|
/(?<![A-Za-z0-9_])(?:versichertennummer|krankenversicherung|kvnr|kv-?nr|mutualité|ziekenfonds|aansluitingsnummer|medical\s*card(?:\s*(?:no|number))?)(?:(?:[A-Za-z0-9_]|[^\x00-\x7F])*|\s+[^\s:.\-\d,;()\/]{2,20})\s*\)?\s*[:.\-]*\s*$/i],
|