@doccloak/core 0.8.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/CHANGELOG.md +35 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +230 -0
- package/dist/doc.d.ts +13 -0
- package/dist/doc.js +683 -0
- package/dist/docx.d.ts +31 -0
- package/dist/docx.js +26 -0
- package/dist/dom/docx.d.ts +90 -0
- package/dist/dom/docx.js +472 -0
- package/dist/dom/index.d.ts +11 -0
- package/dist/dom/index.js +11 -0
- package/dist/dom/ocr.d.ts +112 -0
- package/dist/dom/ocr.js +224 -0
- package/dist/engine.d.ts +81 -0
- package/dist/engine.js +274 -0
- package/dist/env.d.ts +49 -0
- package/dist/env.js +53 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +29 -0
- package/dist/model-loader.d.ts +55 -0
- package/dist/model-loader.js +253 -0
- package/dist/pipeline.d.ts +19 -0
- package/dist/pipeline.js +128 -0
- package/dist/providers/bardsai.d.ts +38 -0
- package/dist/providers/bardsai.js +393 -0
- package/dist/providers/gliner.d.ts +52 -0
- package/dist/providers/gliner.js +360 -0
- package/dist/regex/index.d.ts +17 -0
- package/dist/regex/index.js +18 -0
- package/dist/regex/loader.d.ts +44 -0
- package/dist/regex/loader.js +44 -0
- package/dist/regex/rules.data.d.ts +2 -0
- package/dist/regex/rules.data.js +2671 -0
- package/dist/regex/types.d.ts +27 -0
- package/dist/regex/types.js +1 -0
- package/dist/regex/validators.d.ts +17 -0
- package/dist/regex/validators.js +492 -0
- package/dist/session.d.ts +59 -0
- package/dist/session.js +153 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +28 -0
- package/dist/worker-protocol.d.ts +119 -0
- package/dist/worker-protocol.js +285 -0
- package/package.json +75 -0
- package/rules/at.json +70 -0
- package/rules/be.json +70 -0
- package/rules/ch.json +66 -0
- package/rules/cn.json +155 -0
- package/rules/de.json +192 -0
- package/rules/dk.json +43 -0
- package/rules/es.json +158 -0
- package/rules/fi.json +43 -0
- package/rules/fr.json +178 -0
- package/rules/gb.json +194 -0
- package/rules/ie.json +48 -0
- package/rules/it.json +116 -0
- package/rules/jp.json +136 -0
- package/rules/nl.json +99 -0
- package/rules/no.json +132 -0
- package/rules/pl.json +210 -0
- package/rules/pt.json +113 -0
- package/rules/schema.json +91 -0
- package/rules/se.json +149 -0
- package/rules/universal.json +279 -0
- package/rules/us.json +218 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EntityType } from '../types.js';
|
|
2
|
+
/** Domain tags for categorization — a rule can belong to multiple */
|
|
3
|
+
export type PiiDomain = 'financial' | 'medical' | 'legal' | 'hr' | 'identity' | 'contact' | 'technical' | 'general';
|
|
4
|
+
/** Region code or 'universal' for patterns that apply globally */
|
|
5
|
+
export type RegionCode = 'universal' | 'gb' | 'pl' | 'de' | 'fr' | 'es' | 'pt' | 'se' | 'no' | 'it' | 'nl' | 'be' | 'at' | 'ch' | 'ie' | 'dk' | 'fi' | 'us' | 'jp' | 'cn' | (string & {});
|
|
6
|
+
export interface RegexRule {
|
|
7
|
+
/** The regex pattern. MUST use the 'g' flag. */
|
|
8
|
+
pattern: RegExp;
|
|
9
|
+
/** Which entity type this detects */
|
|
10
|
+
type: EntityType;
|
|
11
|
+
/** Detector identifier for tracing, e.g. 'regex:us:ssn' */
|
|
12
|
+
detector: string;
|
|
13
|
+
/** Default confidence score (0-1). Structured patterns = high; loose patterns = lower */
|
|
14
|
+
confidence: number;
|
|
15
|
+
/** Region this rule applies to, or 'universal' */
|
|
16
|
+
region: RegionCode;
|
|
17
|
+
/** Domain tags for categorization/filtering */
|
|
18
|
+
domains: PiiDomain[];
|
|
19
|
+
/** Human-readable description for developers maintaining rules */
|
|
20
|
+
description: string;
|
|
21
|
+
/** Example strings that SHOULD match (used in auto-generated tests) */
|
|
22
|
+
examples?: string[];
|
|
23
|
+
/** Known false-positive patterns or notes */
|
|
24
|
+
falsePositiveNotes?: string;
|
|
25
|
+
/** Optional validation function for post-match filtering (e.g., Luhn check, checksum) */
|
|
26
|
+
validate?: (match: string) => boolean;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validator registry for the shared rules format (T018, arch doc section 6).
|
|
3
|
+
*
|
|
4
|
+
* Every function here was extracted verbatim from a former inline `validate`
|
|
5
|
+
* closure in the TS rule modules; behavior is intentionally identical.
|
|
6
|
+
* The `validate` field in rules/*.json refers to these names, resolved by
|
|
7
|
+
* loader.ts. Python resolves the same names against its own registry
|
|
8
|
+
* (DocCloak.Cli validators.py); where a Python equivalent exists the name
|
|
9
|
+
* matches it: pesel_check -> pesel, nip_check -> nip, nino_check -> nino,
|
|
10
|
+
* luhn_check -> luhn, nir_check -> nir, ipv4_check -> ipOctets.
|
|
11
|
+
*/
|
|
12
|
+
export type RuleValidator = (match: string) => boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The registry. Keys are the names allowed in the `validate` field of
|
|
15
|
+
* rules/*.json; loader.ts throws at module init on any unknown name.
|
|
16
|
+
*/
|
|
17
|
+
export declare const VALIDATORS: Readonly<Record<string, RuleValidator>>;
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validator registry for the shared rules format (T018, arch doc section 6).
|
|
3
|
+
*
|
|
4
|
+
* Every function here was extracted verbatim from a former inline `validate`
|
|
5
|
+
* closure in the TS rule modules; behavior is intentionally identical.
|
|
6
|
+
* The `validate` field in rules/*.json refers to these names, resolved by
|
|
7
|
+
* loader.ts. Python resolves the same names against its own registry
|
|
8
|
+
* (DocCloak.Cli validators.py); where a Python equivalent exists the name
|
|
9
|
+
* matches it: pesel_check -> pesel, nip_check -> nip, nino_check -> nino,
|
|
10
|
+
* luhn_check -> luhn, nir_check -> nir, ipv4_check -> ipOctets.
|
|
11
|
+
*/
|
|
12
|
+
/** Luhn checksum over the digits of the match (credit/debit card numbers). */
|
|
13
|
+
function luhn(num) {
|
|
14
|
+
const digits = num.replace(/[\s-]/g, '');
|
|
15
|
+
if (!/^\d+$/.test(digits))
|
|
16
|
+
return false;
|
|
17
|
+
let sum = 0;
|
|
18
|
+
let alternate = false;
|
|
19
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
20
|
+
let n = parseInt(digits[i], 10);
|
|
21
|
+
if (alternate) {
|
|
22
|
+
n *= 2;
|
|
23
|
+
if (n > 9)
|
|
24
|
+
n -= 9;
|
|
25
|
+
}
|
|
26
|
+
sum += n;
|
|
27
|
+
alternate = !alternate;
|
|
28
|
+
}
|
|
29
|
+
return sum % 10 === 0;
|
|
30
|
+
}
|
|
31
|
+
/** IBAN mod-97 check (ISO 13616) using chunked arithmetic (BigInt not needed). */
|
|
32
|
+
function ibanMod97(match) {
|
|
33
|
+
const clean = match.replace(/\s/g, '').toUpperCase();
|
|
34
|
+
if (clean.length < 5 || clean.length > 34)
|
|
35
|
+
return false;
|
|
36
|
+
// Move first 4 chars to end, convert letters to digits (A=10..Z=35)
|
|
37
|
+
const rearranged = clean.slice(4) + clean.slice(0, 4);
|
|
38
|
+
let numStr = '';
|
|
39
|
+
for (const ch of rearranged) {
|
|
40
|
+
if (ch >= '0' && ch <= '9')
|
|
41
|
+
numStr += ch;
|
|
42
|
+
else
|
|
43
|
+
numStr += (ch.charCodeAt(0) - 55).toString();
|
|
44
|
+
}
|
|
45
|
+
// Mod-97 using chunked arithmetic (BigInt not needed)
|
|
46
|
+
let remainder = 0;
|
|
47
|
+
for (let i = 0; i < numStr.length; i++) {
|
|
48
|
+
remainder = (remainder * 10 + parseInt(numStr[i], 10)) % 97;
|
|
49
|
+
}
|
|
50
|
+
return remainder === 1;
|
|
51
|
+
}
|
|
52
|
+
/** Every dotted octet of an IPv4 address is in 0-255. */
|
|
53
|
+
function ipOctets(ip) {
|
|
54
|
+
return ip.split('.').every(octet => {
|
|
55
|
+
const n = parseInt(octet, 10);
|
|
56
|
+
return n >= 0 && n <= 255;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** UK National Insurance Number structural rules (prefix letter exclusions). */
|
|
60
|
+
function nino(match) {
|
|
61
|
+
const clean = match.replace(/\s/g, '').toUpperCase();
|
|
62
|
+
// NINO cannot start with D, F, I, Q, U, V; second letter cannot be D, F, I, O, Q, U, V
|
|
63
|
+
if (/^[DFIQUV]/.test(clean))
|
|
64
|
+
return false;
|
|
65
|
+
if (/^.[DFIOQU]/.test(clean))
|
|
66
|
+
return false;
|
|
67
|
+
// Cannot be BG, GB, NK, KN, TN, NT, ZZ prefix
|
|
68
|
+
if (/^(?:BG|GB|NK|KN|TN|NT|ZZ)/.test(clean))
|
|
69
|
+
return false;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
/** UK NHS number mod-11 check digit. */
|
|
73
|
+
function nhs(match) {
|
|
74
|
+
const digits = match.replace(/\D/g, '');
|
|
75
|
+
if (digits.length !== 10)
|
|
76
|
+
return false;
|
|
77
|
+
const weights = [10, 9, 8, 7, 6, 5, 4, 3, 2];
|
|
78
|
+
let sum = 0;
|
|
79
|
+
for (let i = 0; i < 9; i++)
|
|
80
|
+
sum += parseInt(digits[i], 10) * weights[i];
|
|
81
|
+
const checkDigit = 11 - (sum % 11);
|
|
82
|
+
if (checkDigit === 11)
|
|
83
|
+
return parseInt(digits[9], 10) === 0;
|
|
84
|
+
if (checkDigit === 10)
|
|
85
|
+
return false; // invalid
|
|
86
|
+
return checkDigit === parseInt(digits[9], 10);
|
|
87
|
+
}
|
|
88
|
+
/** Polish PESEL weighted checksum. */
|
|
89
|
+
function pesel(match) {
|
|
90
|
+
const digits = match.replace(/\D/g, '');
|
|
91
|
+
if (digits.length !== 11)
|
|
92
|
+
return false;
|
|
93
|
+
const weights = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3];
|
|
94
|
+
let sum = 0;
|
|
95
|
+
for (let i = 0; i < 10; i++) {
|
|
96
|
+
sum += parseInt(digits[i], 10) * weights[i];
|
|
97
|
+
}
|
|
98
|
+
const checkDigit = (10 - (sum % 10)) % 10;
|
|
99
|
+
return checkDigit === parseInt(digits[10], 10);
|
|
100
|
+
}
|
|
101
|
+
/** Polish ID card (3 letters + 6 digits) weighted checksum. */
|
|
102
|
+
function plIdCard(match) {
|
|
103
|
+
const clean = match.replace(/\s/g, '').toUpperCase();
|
|
104
|
+
if (clean.length !== 9)
|
|
105
|
+
return false;
|
|
106
|
+
// First 3 chars are letters (A=10..Z=35), remaining 6 are digits
|
|
107
|
+
// Weights: [7, 3, 1, 0, 7, 3, 1, 7, 3] where position 4 (index 3) is the check digit
|
|
108
|
+
const weights = [7, 3, 1, 0, 7, 3, 1, 7, 3];
|
|
109
|
+
let sum = 0;
|
|
110
|
+
for (let i = 0; i < 9; i++) {
|
|
111
|
+
const ch = clean[i];
|
|
112
|
+
const val = ch >= 'A' && ch <= 'Z' ? ch.charCodeAt(0) - 55 : parseInt(ch, 10);
|
|
113
|
+
if (i === 3)
|
|
114
|
+
continue; // skip check digit position
|
|
115
|
+
sum += val * weights[i];
|
|
116
|
+
}
|
|
117
|
+
const checkDigit = sum % 10;
|
|
118
|
+
return checkDigit === parseInt(clean[3], 10);
|
|
119
|
+
}
|
|
120
|
+
/** Polish NIP weighted mod-11 checksum. */
|
|
121
|
+
function nip(match) {
|
|
122
|
+
const digits = match.replace(/\D/g, '');
|
|
123
|
+
if (digits.length !== 10)
|
|
124
|
+
return false;
|
|
125
|
+
const weights = [6, 5, 7, 2, 3, 4, 5, 6, 7];
|
|
126
|
+
let sum = 0;
|
|
127
|
+
for (let i = 0; i < 9; i++) {
|
|
128
|
+
sum += parseInt(digits[i], 10) * weights[i];
|
|
129
|
+
}
|
|
130
|
+
return (sum % 11) === parseInt(digits[9], 10);
|
|
131
|
+
}
|
|
132
|
+
/** Polish REGON checksum (9- or 14-digit variant). */
|
|
133
|
+
function regon(match) {
|
|
134
|
+
const digits = match.replace(/\D/g, '');
|
|
135
|
+
if (digits.length === 9) {
|
|
136
|
+
const weights = [8, 9, 2, 3, 4, 5, 6, 7];
|
|
137
|
+
let sum = 0;
|
|
138
|
+
for (let i = 0; i < 8; i++)
|
|
139
|
+
sum += parseInt(digits[i], 10) * weights[i];
|
|
140
|
+
const checkDigit = sum % 11 === 10 ? 0 : sum % 11;
|
|
141
|
+
return checkDigit === parseInt(digits[8], 10);
|
|
142
|
+
}
|
|
143
|
+
if (digits.length === 14) {
|
|
144
|
+
const weights = [2, 4, 8, 5, 0, 9, 7, 3, 6, 1, 2, 4, 8];
|
|
145
|
+
let sum = 0;
|
|
146
|
+
for (let i = 0; i < 13; i++)
|
|
147
|
+
sum += parseInt(digits[i], 10) * weights[i];
|
|
148
|
+
const checkDigit = sum % 11 === 10 ? 0 : sum % 11;
|
|
149
|
+
return checkDigit === parseInt(digits[13], 10);
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
/** Swedish personnummer Luhn check (excludes samordningsnummer, day >= 61). */
|
|
154
|
+
function personnummer(match) {
|
|
155
|
+
const digits = match.replace(/[\s-]/g, '');
|
|
156
|
+
if (digits.length < 10)
|
|
157
|
+
return false;
|
|
158
|
+
// Exclude samordningsnummer (day >= 61)
|
|
159
|
+
const cleanForDay = match.replace(/[-+]/g, '');
|
|
160
|
+
const day = parseInt(cleanForDay.substring(4, 6), 10);
|
|
161
|
+
if (day >= 61)
|
|
162
|
+
return false;
|
|
163
|
+
// Use last 10 digits for Luhn check
|
|
164
|
+
const last10 = digits.slice(-10);
|
|
165
|
+
let sum = 0;
|
|
166
|
+
for (let i = 0; i < 9; i++) {
|
|
167
|
+
let n = parseInt(last10[i], 10) * (i % 2 === 0 ? 2 : 1);
|
|
168
|
+
if (n > 9)
|
|
169
|
+
n -= 9;
|
|
170
|
+
sum += n;
|
|
171
|
+
}
|
|
172
|
+
const checkDigit = (10 - (sum % 10)) % 10;
|
|
173
|
+
return checkDigit === parseInt(last10[9], 10);
|
|
174
|
+
}
|
|
175
|
+
/** Swedish samordningsnummer (coordination number): day portion is 61+. */
|
|
176
|
+
function samordningsnummer(match) {
|
|
177
|
+
const digits = match.replace(/[-+]/g, '');
|
|
178
|
+
if (digits.length < 10)
|
|
179
|
+
return false;
|
|
180
|
+
// Day portion is positions 4-5 (in YYMMDD format)
|
|
181
|
+
const day = parseInt(digits.substring(4, 6), 10);
|
|
182
|
+
return day >= 61;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* US DEA number checksum.
|
|
186
|
+
* (d1+d3+d5) + 2*(d2+d4+d6) mod 10 == d7
|
|
187
|
+
* Plus: first letter must be a valid registrant type, second letter is the
|
|
188
|
+
* first letter of the registrant's last name (so any A-Z is allowed there).
|
|
189
|
+
*/
|
|
190
|
+
function dea(match) {
|
|
191
|
+
const m = match.toUpperCase();
|
|
192
|
+
if (!/^[ABCDEFGHJKLMPRSTUX][A-Z]\d{7}$/.test(m))
|
|
193
|
+
return false;
|
|
194
|
+
const digits = m.slice(2).split('').map(Number);
|
|
195
|
+
const sum = digits[0] + digits[2] + digits[4] + 2 * (digits[1] + digits[3] + digits[5]);
|
|
196
|
+
return sum % 10 === digits[6];
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* US NPI Luhn check with the CMS-mandated 80840 prefix.
|
|
200
|
+
* The NPI is 10 digits; Luhn is computed over "80840" + first 9 digits,
|
|
201
|
+
* and the 10th digit must equal the resulting Luhn check digit.
|
|
202
|
+
*/
|
|
203
|
+
function npi(match) {
|
|
204
|
+
const d = match.replace(/\D/g, '');
|
|
205
|
+
if (d.length !== 10)
|
|
206
|
+
return false;
|
|
207
|
+
const payload = '80840' + d.slice(0, 9);
|
|
208
|
+
let sum = 0;
|
|
209
|
+
let alt = true; // rightmost digit (excluding the check digit) is doubled
|
|
210
|
+
for (let i = payload.length - 1; i >= 0; i--) {
|
|
211
|
+
let n = parseInt(payload[i], 10);
|
|
212
|
+
if (alt) {
|
|
213
|
+
n *= 2;
|
|
214
|
+
if (n > 9)
|
|
215
|
+
n -= 9;
|
|
216
|
+
}
|
|
217
|
+
sum += n;
|
|
218
|
+
alt = !alt;
|
|
219
|
+
}
|
|
220
|
+
const check = (10 - (sum % 10)) % 10;
|
|
221
|
+
return check === parseInt(d[9], 10);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* US ABA routing number checksum.
|
|
225
|
+
* (3*(d1+d4+d7) + 7*(d2+d5+d8) + (d3+d6+d9)) mod 10 == 0
|
|
226
|
+
*/
|
|
227
|
+
function aba(match) {
|
|
228
|
+
const d = match.replace(/\D/g, '');
|
|
229
|
+
if (d.length !== 9)
|
|
230
|
+
return false;
|
|
231
|
+
const n = d.split('').map(Number);
|
|
232
|
+
const sum = 3 * (n[0] + n[3] + n[6]) + 7 * (n[1] + n[4] + n[7]) + (n[2] + n[5] + n[8]);
|
|
233
|
+
return sum % 10 === 0;
|
|
234
|
+
}
|
|
235
|
+
/** French NIR / INSEE number: gender digit + mod-97 key over the first 13 digits. */
|
|
236
|
+
function nir(match) {
|
|
237
|
+
const digits = match.replace(/\D/g, '');
|
|
238
|
+
if (digits.length !== 15)
|
|
239
|
+
return false;
|
|
240
|
+
const gender = digits[0];
|
|
241
|
+
if (gender !== '1' && gender !== '2')
|
|
242
|
+
return false;
|
|
243
|
+
// First 13 digits, last 2 are check
|
|
244
|
+
const first13 = parseInt(digits.substring(0, 13), 10);
|
|
245
|
+
const key = parseInt(digits.substring(13, 15), 10);
|
|
246
|
+
return (97 - (first13 % 97)) === key;
|
|
247
|
+
}
|
|
248
|
+
/** Spanish DNI: number mod 23 selects the control letter. */
|
|
249
|
+
function dni(match) {
|
|
250
|
+
const clean = match.replace(/[\s-]/g, '').toUpperCase();
|
|
251
|
+
const letters = 'TRWAGMYFPDXBNJZSQVHLCKE';
|
|
252
|
+
const numPart = clean.slice(0, -1);
|
|
253
|
+
const letter = clean.slice(-1);
|
|
254
|
+
const num = parseInt(numPart, 10);
|
|
255
|
+
if (isNaN(num))
|
|
256
|
+
return false;
|
|
257
|
+
return letters[num % 23] === letter;
|
|
258
|
+
}
|
|
259
|
+
/** Italian Codice Fiscale odd/even character table checksum. */
|
|
260
|
+
function codiceFiscale(match) {
|
|
261
|
+
const code = match.toUpperCase();
|
|
262
|
+
if (code.length !== 16)
|
|
263
|
+
return false;
|
|
264
|
+
const oddValues = {
|
|
265
|
+
'0': 1, '1': 0, '2': 5, '3': 7, '4': 9, '5': 13, '6': 15, '7': 17, '8': 19, '9': 21,
|
|
266
|
+
A: 1, B: 0, C: 5, D: 7, E: 9, F: 13, G: 15, H: 17, I: 19, J: 21,
|
|
267
|
+
K: 2, L: 4, M: 18, N: 20, O: 11, P: 3, Q: 6, R: 8, S: 12, T: 14,
|
|
268
|
+
U: 16, V: 10, W: 22, X: 25, Y: 24, Z: 23,
|
|
269
|
+
};
|
|
270
|
+
const evenValues = {
|
|
271
|
+
'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
|
|
272
|
+
A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6, H: 7, I: 8, J: 9,
|
|
273
|
+
K: 10, L: 11, M: 12, N: 13, O: 14, P: 15, Q: 16, R: 17, S: 18, T: 19,
|
|
274
|
+
U: 20, V: 21, W: 22, X: 23, Y: 24, Z: 25,
|
|
275
|
+
};
|
|
276
|
+
let sum = 0;
|
|
277
|
+
for (let i = 0; i < 15; i++) {
|
|
278
|
+
const ch = code[i];
|
|
279
|
+
// Positions are 1-based: odd positions use oddValues, even use evenValues
|
|
280
|
+
sum += (i % 2 === 0) ? oddValues[ch] : evenValues[ch];
|
|
281
|
+
}
|
|
282
|
+
const expectedCheck = String.fromCharCode('A'.charCodeAt(0) + (sum % 26));
|
|
283
|
+
return code[15] === expectedCheck;
|
|
284
|
+
}
|
|
285
|
+
/** Italian Partita IVA Luhn-style checksum over 11 digits. */
|
|
286
|
+
function partitaIva(match) {
|
|
287
|
+
const digits = match.toUpperCase().replace(/^IT/, '');
|
|
288
|
+
if (digits.length !== 11 || !/^\d{11}$/.test(digits))
|
|
289
|
+
return false;
|
|
290
|
+
let sum = 0;
|
|
291
|
+
for (let i = 0; i < 11; i++) {
|
|
292
|
+
let n = parseInt(digits[i], 10);
|
|
293
|
+
if (i % 2 === 1) {
|
|
294
|
+
n *= 2;
|
|
295
|
+
if (n > 9)
|
|
296
|
+
n -= 9;
|
|
297
|
+
}
|
|
298
|
+
sum += n;
|
|
299
|
+
}
|
|
300
|
+
return sum % 10 === 0;
|
|
301
|
+
}
|
|
302
|
+
/** Dutch BSN elfproef (11-test) with negative weight on the last digit. */
|
|
303
|
+
function bsn(match) {
|
|
304
|
+
if (match.length !== 9 || !/^\d{9}$/.test(match))
|
|
305
|
+
return false;
|
|
306
|
+
const weights = [9, 8, 7, 6, 5, 4, 3, 2, -1];
|
|
307
|
+
let sum = 0;
|
|
308
|
+
for (let i = 0; i < 9; i++) {
|
|
309
|
+
sum += parseInt(match[i], 10) * weights[i];
|
|
310
|
+
}
|
|
311
|
+
return sum > 0 && sum % 11 === 0;
|
|
312
|
+
}
|
|
313
|
+
/** Portuguese NIF: first-digit restriction + mod-11 check digit. */
|
|
314
|
+
function nif(match) {
|
|
315
|
+
const digits = match.replace(/\D/g, '');
|
|
316
|
+
if (digits.length !== 9)
|
|
317
|
+
return false;
|
|
318
|
+
// First digit must not be 0, 3, 4, or 7
|
|
319
|
+
const first = parseInt(digits[0], 10);
|
|
320
|
+
if (first === 0 || first === 3 || first === 4 || first === 7)
|
|
321
|
+
return false;
|
|
322
|
+
const weights = [9, 8, 7, 6, 5, 4, 3, 2];
|
|
323
|
+
let sum = 0;
|
|
324
|
+
for (let i = 0; i < 8; i++)
|
|
325
|
+
sum += parseInt(digits[i], 10) * weights[i];
|
|
326
|
+
const remainder = sum % 11;
|
|
327
|
+
const checkDigit = remainder < 2 ? 0 : 11 - remainder;
|
|
328
|
+
return checkDigit === parseInt(digits[8], 10);
|
|
329
|
+
}
|
|
330
|
+
/** Norwegian fodselsnummer: two mod-11 control digits, day 01-31 (excludes D-nummer). */
|
|
331
|
+
function fodselsnummer(match) {
|
|
332
|
+
const digits = match.replace(/\D/g, '');
|
|
333
|
+
if (digits.length !== 11)
|
|
334
|
+
return false;
|
|
335
|
+
const day = parseInt(digits.substring(0, 2), 10);
|
|
336
|
+
// Fødselsnummer has day 01-31; D-nummer has day 41-71
|
|
337
|
+
if (day >= 41)
|
|
338
|
+
return false;
|
|
339
|
+
const d = digits.split('').map(Number);
|
|
340
|
+
// Control digit 1
|
|
341
|
+
const k1 = 11 - ((3 * d[0] + 7 * d[1] + 6 * d[2] + 1 * d[3] + 8 * d[4] + 9 * d[5] + 4 * d[6] + 5 * d[7] + 2 * d[8]) % 11);
|
|
342
|
+
const c1 = k1 === 11 ? 0 : k1;
|
|
343
|
+
if (c1 === 10 || c1 !== d[9])
|
|
344
|
+
return false;
|
|
345
|
+
// Control digit 2
|
|
346
|
+
const k2 = 11 - ((5 * d[0] + 4 * d[1] + 3 * d[2] + 2 * d[3] + 7 * d[4] + 6 * d[5] + 5 * d[6] + 4 * d[7] + 3 * d[8] + 2 * d[9]) % 11);
|
|
347
|
+
const c2 = k2 === 11 ? 0 : k2;
|
|
348
|
+
if (c2 === 10 || c2 !== d[10])
|
|
349
|
+
return false;
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
/** Norwegian D-nummer: day portion shifted by +40 (41-71). */
|
|
353
|
+
function dNummer(match) {
|
|
354
|
+
const digits = match.replace(/\D/g, '');
|
|
355
|
+
if (digits.length !== 11)
|
|
356
|
+
return false;
|
|
357
|
+
const day = parseInt(digits.substring(0, 2), 10);
|
|
358
|
+
// D-nummer: first digit has 4 added, so day is 41-71
|
|
359
|
+
if (day < 41 || day > 71)
|
|
360
|
+
return false;
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
/** Belgian National Register Number mod-97 check (pre- and post-2000 variants). */
|
|
364
|
+
function belgianNrn(match) {
|
|
365
|
+
const digits = match.replace(/[\s.-]/g, '');
|
|
366
|
+
if (digits.length !== 11 || !/^\d{11}$/.test(digits))
|
|
367
|
+
return false;
|
|
368
|
+
const first9 = parseInt(digits.substring(0, 9), 10);
|
|
369
|
+
const check = parseInt(digits.substring(9, 11), 10);
|
|
370
|
+
// Born before 2000
|
|
371
|
+
if ((97 - (first9 % 97)) === check)
|
|
372
|
+
return true;
|
|
373
|
+
// Born 2000+: prepend '2' to the first 9 digits
|
|
374
|
+
const first9with2 = parseInt('2' + digits.substring(0, 9), 10);
|
|
375
|
+
if ((97 - (first9with2 % 97)) === check)
|
|
376
|
+
return true;
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
/** Austrian SVNR weighted mod-11 checksum (check digit at position 4). */
|
|
380
|
+
function svnr(match) {
|
|
381
|
+
const digits = match.replace(/\s/g, '');
|
|
382
|
+
if (digits.length !== 10 || !/^\d{10}$/.test(digits))
|
|
383
|
+
return false;
|
|
384
|
+
const serial = parseInt(digits.substring(0, 3), 10);
|
|
385
|
+
if (serial < 100)
|
|
386
|
+
return false;
|
|
387
|
+
const weights = [3, 7, 9, 0, 5, 8, 4, 2, 1, 6];
|
|
388
|
+
let sum = 0;
|
|
389
|
+
for (let i = 0; i < 10; i++) {
|
|
390
|
+
if (i === 3)
|
|
391
|
+
continue; // skip check digit position for sum
|
|
392
|
+
sum += parseInt(digits[i], 10) * weights[i];
|
|
393
|
+
}
|
|
394
|
+
const check = sum % 11;
|
|
395
|
+
if (check === 10)
|
|
396
|
+
return false;
|
|
397
|
+
return check === parseInt(digits[3], 10);
|
|
398
|
+
}
|
|
399
|
+
/** Swiss AHV/AVS number EAN-13 checksum (756 prefix). */
|
|
400
|
+
function ahv(match) {
|
|
401
|
+
const digits = match.replace(/[\s.]/g, '');
|
|
402
|
+
if (digits.length !== 13 || !digits.startsWith('756'))
|
|
403
|
+
return false;
|
|
404
|
+
const weights = [1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3];
|
|
405
|
+
let sum = 0;
|
|
406
|
+
for (let i = 0; i < 12; i++) {
|
|
407
|
+
sum += parseInt(digits[i], 10) * weights[i];
|
|
408
|
+
}
|
|
409
|
+
const check = (10 - (sum % 10)) % 10;
|
|
410
|
+
return check === parseInt(digits[12], 10);
|
|
411
|
+
}
|
|
412
|
+
/** German Steuer-ID check digit (ISO 7064 Mod 11,10). */
|
|
413
|
+
function steuerId(match) {
|
|
414
|
+
const digits = match.replace(/\D/g, '');
|
|
415
|
+
if (digits.length !== 11)
|
|
416
|
+
return false;
|
|
417
|
+
if (digits[0] === '0')
|
|
418
|
+
return false;
|
|
419
|
+
// Check digit validation (ISO 7064 Mod 11,10)
|
|
420
|
+
let product = 10;
|
|
421
|
+
for (let i = 0; i < 10; i++) {
|
|
422
|
+
let sum = (parseInt(digits[i], 10) + product) % 10;
|
|
423
|
+
if (sum === 0)
|
|
424
|
+
sum = 10;
|
|
425
|
+
product = (sum * 2) % 11;
|
|
426
|
+
}
|
|
427
|
+
const checkDigit = (11 - product) % 10;
|
|
428
|
+
return checkDigit === parseInt(digits[10], 10);
|
|
429
|
+
}
|
|
430
|
+
/** Finnish HETU: mod-31 check character over date + individual number. */
|
|
431
|
+
function hetu(match) {
|
|
432
|
+
const upper = match.toUpperCase();
|
|
433
|
+
const ddmmyy = upper.slice(0, 6);
|
|
434
|
+
const nnn = upper.slice(7, 10);
|
|
435
|
+
const checkChar = upper[10];
|
|
436
|
+
const number = parseInt(ddmmyy + nnn, 10);
|
|
437
|
+
const checkChars = '0123456789ABCDEFHJKLMNPRSTUVWXY';
|
|
438
|
+
const remainder = number % 31;
|
|
439
|
+
return checkChars[remainder] === checkChar;
|
|
440
|
+
}
|
|
441
|
+
/** Irish PPS number: weighted mod-23 check character (with optional W suffix). */
|
|
442
|
+
function pps(match) {
|
|
443
|
+
const upper = match.toUpperCase();
|
|
444
|
+
const digits = upper.slice(0, 7);
|
|
445
|
+
const checkChar = upper[7];
|
|
446
|
+
const suffix = upper.length > 8 ? upper[8] : undefined;
|
|
447
|
+
const weights = [8, 7, 6, 5, 4, 3, 2];
|
|
448
|
+
let sum = 0;
|
|
449
|
+
for (let i = 0; i < 7; i++) {
|
|
450
|
+
sum += parseInt(digits[i], 10) * weights[i];
|
|
451
|
+
}
|
|
452
|
+
if (suffix && suffix !== 'W') {
|
|
453
|
+
sum += (suffix.charCodeAt(0) - 64) * 9;
|
|
454
|
+
}
|
|
455
|
+
const remainder = sum % 23;
|
|
456
|
+
const expected = remainder === 0 ? 'W' : String.fromCharCode(64 + remainder);
|
|
457
|
+
return checkChar === expected;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* The registry. Keys are the names allowed in the `validate` field of
|
|
461
|
+
* rules/*.json; loader.ts throws at module init on any unknown name.
|
|
462
|
+
*/
|
|
463
|
+
export const VALIDATORS = {
|
|
464
|
+
aba,
|
|
465
|
+
ahv,
|
|
466
|
+
belgianNrn,
|
|
467
|
+
bsn,
|
|
468
|
+
codiceFiscale,
|
|
469
|
+
dNummer,
|
|
470
|
+
dea,
|
|
471
|
+
dni,
|
|
472
|
+
fodselsnummer,
|
|
473
|
+
hetu,
|
|
474
|
+
ibanMod97,
|
|
475
|
+
ipOctets,
|
|
476
|
+
luhn,
|
|
477
|
+
nhs,
|
|
478
|
+
nif,
|
|
479
|
+
nino,
|
|
480
|
+
nip,
|
|
481
|
+
nir,
|
|
482
|
+
npi,
|
|
483
|
+
partitaIva,
|
|
484
|
+
personnummer,
|
|
485
|
+
pesel,
|
|
486
|
+
plIdCard,
|
|
487
|
+
pps,
|
|
488
|
+
regon,
|
|
489
|
+
samordningsnummer,
|
|
490
|
+
steuerId,
|
|
491
|
+
svnr,
|
|
492
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { EntityType, DetectedEntity, ReplacementEntry } from './types.js';
|
|
2
|
+
export type ReplacementMode = 'labeled' | 'blanked';
|
|
3
|
+
export declare class AnonymizationSession {
|
|
4
|
+
private forwardMap;
|
|
5
|
+
private reverseMap;
|
|
6
|
+
private entityTypeMap;
|
|
7
|
+
private counter;
|
|
8
|
+
private mode;
|
|
9
|
+
setMode(mode: ReplacementMode): void;
|
|
10
|
+
getMode(): ReplacementMode;
|
|
11
|
+
anonymize(original: string, entityType: EntityType): string;
|
|
12
|
+
deanonymize(text: string): string;
|
|
13
|
+
anonymizeText(text: string, entities: DetectedEntity[]): string;
|
|
14
|
+
getEntries(): ReplacementEntry[];
|
|
15
|
+
getForward(original: string): string | undefined;
|
|
16
|
+
renameLabel(original: string, newLabel: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Serialize the session map to JSON.
|
|
19
|
+
*
|
|
20
|
+
* Schema (field-for-field parity with the Python CLI's
|
|
21
|
+
* DocCloak.Cli/doccloak/core/session.py save_map/load_map):
|
|
22
|
+
*
|
|
23
|
+
* [
|
|
24
|
+
* {
|
|
25
|
+
* "original": string, // the original sensitive value
|
|
26
|
+
* "replacement": string, // its placeholder, e.g. "<<REDACTED_1>>" or a renamed label
|
|
27
|
+
* "entity_type": string // EntityType value, e.g. "PERSON" (snake_case key, matching Python)
|
|
28
|
+
* },
|
|
29
|
+
* ...
|
|
30
|
+
* ]
|
|
31
|
+
*
|
|
32
|
+
* - Top level is a JSON array; one object per mapping, in insertion order.
|
|
33
|
+
* - The counter is NOT stored as a field. Like Python's load_map, deserialize
|
|
34
|
+
* derives it from the number of entries.
|
|
35
|
+
* - The mode is NOT stored as a field. Like Python's load_map (which returns
|
|
36
|
+
* `cls()` with the default mode), deserialize yields a 'labeled' session.
|
|
37
|
+
* - Blanked entries are intentionally OMITTED from the output: blanked
|
|
38
|
+
* placeholders ('________') are irreversible by design (this class already
|
|
39
|
+
* keeps them out of the reverse map), so persisting their originals would
|
|
40
|
+
* defeat that. Only reversible entries are serialized.
|
|
41
|
+
* - Output formatting matches Python's json.dumps(entries, indent=2,
|
|
42
|
+
* ensure_ascii=False): 2-space indent, non-ASCII characters kept raw.
|
|
43
|
+
*/
|
|
44
|
+
serialize(): string;
|
|
45
|
+
/**
|
|
46
|
+
* Rebuild a session from JSON produced by serialize() or by the Python
|
|
47
|
+
* CLI's save_map. Mirrors Python's load_map: forward/reverse/type maps are
|
|
48
|
+
* repopulated per entry and the counter is incremented once per entry.
|
|
49
|
+
* Additionally, the counter is raised to the highest N found among
|
|
50
|
+
* '<<REDACTED_N>>' placeholders (a map serialized with gaps, e.g. after
|
|
51
|
+
* blanked entries were omitted, would otherwise reissue a taken number),
|
|
52
|
+
* so new entities anonymized after restore continue numbering without
|
|
53
|
+
* colliding with restored placeholders. Blanked entries ('________') that a
|
|
54
|
+
* Python-produced map may contain are kept in the forward map but excluded
|
|
55
|
+
* from the reverse map, preserving their irreversibility.
|
|
56
|
+
*/
|
|
57
|
+
static deserialize(json: string): AnonymizationSession;
|
|
58
|
+
clear(): void;
|
|
59
|
+
}
|