@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +5 -0
  4. package/README.md +230 -0
  5. package/dist/doc.d.ts +13 -0
  6. package/dist/doc.js +683 -0
  7. package/dist/docx.d.ts +31 -0
  8. package/dist/docx.js +26 -0
  9. package/dist/dom/docx.d.ts +90 -0
  10. package/dist/dom/docx.js +472 -0
  11. package/dist/dom/index.d.ts +11 -0
  12. package/dist/dom/index.js +11 -0
  13. package/dist/dom/ocr.d.ts +112 -0
  14. package/dist/dom/ocr.js +224 -0
  15. package/dist/engine.d.ts +81 -0
  16. package/dist/engine.js +274 -0
  17. package/dist/env.d.ts +49 -0
  18. package/dist/env.js +53 -0
  19. package/dist/index.d.ts +20 -0
  20. package/dist/index.js +29 -0
  21. package/dist/model-loader.d.ts +55 -0
  22. package/dist/model-loader.js +253 -0
  23. package/dist/pipeline.d.ts +19 -0
  24. package/dist/pipeline.js +128 -0
  25. package/dist/providers/bardsai.d.ts +38 -0
  26. package/dist/providers/bardsai.js +393 -0
  27. package/dist/providers/gliner.d.ts +52 -0
  28. package/dist/providers/gliner.js +360 -0
  29. package/dist/regex/index.d.ts +17 -0
  30. package/dist/regex/index.js +18 -0
  31. package/dist/regex/loader.d.ts +44 -0
  32. package/dist/regex/loader.js +44 -0
  33. package/dist/regex/rules.data.d.ts +2 -0
  34. package/dist/regex/rules.data.js +2671 -0
  35. package/dist/regex/types.d.ts +27 -0
  36. package/dist/regex/types.js +1 -0
  37. package/dist/regex/validators.d.ts +17 -0
  38. package/dist/regex/validators.js +492 -0
  39. package/dist/session.d.ts +59 -0
  40. package/dist/session.js +153 -0
  41. package/dist/types.d.ts +42 -0
  42. package/dist/types.js +28 -0
  43. package/dist/worker-protocol.d.ts +119 -0
  44. package/dist/worker-protocol.js +285 -0
  45. package/package.json +75 -0
  46. package/rules/at.json +70 -0
  47. package/rules/be.json +70 -0
  48. package/rules/ch.json +66 -0
  49. package/rules/cn.json +155 -0
  50. package/rules/de.json +192 -0
  51. package/rules/dk.json +43 -0
  52. package/rules/es.json +158 -0
  53. package/rules/fi.json +43 -0
  54. package/rules/fr.json +178 -0
  55. package/rules/gb.json +194 -0
  56. package/rules/ie.json +48 -0
  57. package/rules/it.json +116 -0
  58. package/rules/jp.json +136 -0
  59. package/rules/nl.json +99 -0
  60. package/rules/no.json +132 -0
  61. package/rules/pl.json +210 -0
  62. package/rules/pt.json +113 -0
  63. package/rules/schema.json +91 -0
  64. package/rules/se.json +149 -0
  65. package/rules/universal.json +279 -0
  66. 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
+ }