@doccloak/core 0.11.0 → 0.12.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +148 -2
  2. package/README.md +252 -19
  3. package/dist/doc.d.ts +68 -1
  4. package/dist/doc.js +447 -62
  5. package/dist/dom/docx.d.ts +135 -7
  6. package/dist/dom/docx.js +752 -125
  7. package/dist/dom/drawingml.d.ts +59 -0
  8. package/dist/dom/drawingml.js +176 -0
  9. package/dist/dom/errors.d.ts +35 -0
  10. package/dist/dom/errors.js +24 -0
  11. package/dist/dom/index.d.ts +3 -0
  12. package/dist/dom/index.js +6 -0
  13. package/dist/dom/office.d.ts +56 -0
  14. package/dist/dom/office.js +76 -23
  15. package/dist/dom/opc.d.ts +135 -7
  16. package/dist/dom/opc.js +407 -14
  17. package/dist/dom/package-policy.d.ts +137 -0
  18. package/dist/dom/package-policy.js +355 -0
  19. package/dist/dom/xlsx.d.ts +64 -6
  20. package/dist/dom/xlsx.js +130 -20
  21. package/dist/engine.d.ts +18 -0
  22. package/dist/engine.js +37 -13
  23. package/dist/env.d.ts +21 -1
  24. package/dist/index.d.ts +8 -6
  25. package/dist/index.js +10 -5
  26. package/dist/model-loader.d.ts +61 -6
  27. package/dist/model-loader.js +122 -17
  28. package/dist/pipeline.d.ts +87 -8
  29. package/dist/pipeline.js +249 -50
  30. package/dist/providers/bardsai.d.ts +8 -1
  31. package/dist/providers/bardsai.js +22 -2
  32. package/dist/providers/gliner-base.d.ts +8 -1
  33. package/dist/providers/gliner-base.js +22 -2
  34. package/dist/providers/gliner.d.ts +8 -1
  35. package/dist/providers/gliner.js +22 -2
  36. package/dist/regex/loader.d.ts +2 -0
  37. package/dist/regex/loader.js +2 -0
  38. package/dist/regex/rules.data.js +15 -14
  39. package/dist/regex/types.d.ts +6 -0
  40. package/dist/session.d.ts +101 -3
  41. package/dist/session.js +397 -43
  42. package/dist/surrogates.js +57 -45
  43. package/dist/worker-protocol.d.ts +13 -1
  44. package/dist/worker-protocol.js +164 -12
  45. package/package.json +1 -1
  46. package/rules/at.json +1 -1
  47. package/rules/be.json +1 -1
  48. package/rules/ch.json +1 -1
  49. package/rules/de.json +1 -1
  50. package/rules/dk.json +1 -1
  51. package/rules/es.json +1 -1
  52. package/rules/fi.json +1 -1
  53. package/rules/fr.json +1 -1
  54. package/rules/it.json +1 -1
  55. package/rules/no.json +1 -1
  56. package/rules/pl.json +1 -1
  57. package/rules/pt.json +1 -1
  58. package/rules/schema.json +4 -0
  59. package/rules/se.json +1 -1
  60. package/rules/universal.json +2 -1
@@ -263,7 +263,7 @@ export const RULES_DATA = [
263
263
  {
264
264
  "id": "regex:universal:postal_city",
265
265
  "entityType": "ADDRESS",
266
- "pattern": "\\b\\d{2,5}[-\\s]?\\d{2,4}\\s+[\\p{L}][\\p{L}]+\\b",
266
+ "pattern": "\\b\\d{2,5}[-\\s]?\\d{2,4}\\s+[\\p{L}][\\p{L}]+(?![\\p{L}\\p{N}])",
267
267
  "flags": "gu",
268
268
  "confidence": 0.65,
269
269
  "domains": [
@@ -370,6 +370,7 @@ export const RULES_DATA = [
370
370
  "entityType": "SECRET",
371
371
  "pattern": "-----BEGIN (?:[A-Z]+ )*PRIVATE KEY(?: BLOCK)?-----[\\s\\S]*?-----END (?:[A-Z]+ )*PRIVATE KEY(?: BLOCK)?-----",
372
372
  "flags": "g",
373
+ "multiline": true,
373
374
  "confidence": 0.99,
374
375
  "domains": [
375
376
  "technical"
@@ -776,7 +777,7 @@ export const RULES_DATA = [
776
777
  {
777
778
  "id": "regex:pl:postal",
778
779
  "entityType": "ADDRESS",
779
- "pattern": "\\b\\d{2}-\\d{3}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
780
+ "pattern": "\\b\\d{2}-\\d{3}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
780
781
  "flags": "gu",
781
782
  "confidence": 0.8,
782
783
  "domains": [
@@ -967,7 +968,7 @@ export const RULES_DATA = [
967
968
  {
968
969
  "id": "regex:de:postal",
969
970
  "entityType": "ADDRESS",
970
- "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
971
+ "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
971
972
  "flags": "gu",
972
973
  "confidence": 0.8,
973
974
  "domains": [
@@ -1161,7 +1162,7 @@ export const RULES_DATA = [
1161
1162
  {
1162
1163
  "id": "regex:fr:postal",
1163
1164
  "entityType": "ADDRESS",
1164
- "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s'-]+\\b",
1165
+ "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s'-]*[\\p{L}](?![\\p{L}\\p{N}])",
1165
1166
  "flags": "gu",
1166
1167
  "confidence": 0.8,
1167
1168
  "domains": [
@@ -1320,7 +1321,7 @@ export const RULES_DATA = [
1320
1321
  {
1321
1322
  "id": "regex:es:postal",
1322
1323
  "entityType": "ADDRESS",
1323
- "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s'-]+\\b",
1324
+ "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s'-]*[\\p{L}](?![\\p{L}\\p{N}])",
1324
1325
  "flags": "gu",
1325
1326
  "confidence": 0.75,
1326
1327
  "domains": [
@@ -1448,7 +1449,7 @@ export const RULES_DATA = [
1448
1449
  {
1449
1450
  "id": "regex:pt:postal",
1450
1451
  "entityType": "ADDRESS",
1451
- "pattern": "\\b\\d{4}-\\d{3}\\s+[\\p{L}][\\p{L}\\s'-]+\\b",
1452
+ "pattern": "\\b\\d{4}-\\d{3}\\s+[\\p{L}][\\p{L}\\s'-]*[\\p{L}](?![\\p{L}\\p{N}])",
1452
1453
  "flags": "gu",
1453
1454
  "confidence": 0.85,
1454
1455
  "domains": [
@@ -1597,7 +1598,7 @@ export const RULES_DATA = [
1597
1598
  {
1598
1599
  "id": "regex:se:postal",
1599
1600
  "entityType": "ADDRESS",
1600
- "pattern": "\\b\\d{3}\\s?\\d{2}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
1601
+ "pattern": "\\b\\d{3}\\s?\\d{2}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
1601
1602
  "flags": "gu",
1602
1603
  "confidence": 0.8,
1603
1604
  "domains": [
@@ -1729,7 +1730,7 @@ export const RULES_DATA = [
1729
1730
  {
1730
1731
  "id": "regex:no:postal",
1731
1732
  "entityType": "ADDRESS",
1732
- "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
1733
+ "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
1733
1734
  "flags": "gu",
1734
1735
  "confidence": 0.8,
1735
1736
  "domains": [
@@ -1875,7 +1876,7 @@ export const RULES_DATA = [
1875
1876
  {
1876
1877
  "id": "regex:it:postal",
1877
1878
  "entityType": "ADDRESS",
1878
- "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
1879
+ "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
1879
1880
  "flags": "gu",
1880
1881
  "confidence": 0.8,
1881
1882
  "domains": [
@@ -2043,7 +2044,7 @@ export const RULES_DATA = [
2043
2044
  {
2044
2045
  "id": "regex:be:postal",
2045
2046
  "entityType": "ADDRESS",
2046
- "pattern": "\\b[1-9]\\d{3}\\s+[\\p{L}][\\p{L}\\s'-]+\\b",
2047
+ "pattern": "\\b[1-9]\\d{3}\\s+[\\p{L}][\\p{L}\\s'-]*[\\p{L}](?![\\p{L}\\p{N}])",
2047
2048
  "flags": "gu",
2048
2049
  "confidence": 0.75,
2049
2050
  "domains": [
@@ -2113,7 +2114,7 @@ export const RULES_DATA = [
2113
2114
  {
2114
2115
  "id": "regex:at:postal",
2115
2116
  "entityType": "ADDRESS",
2116
- "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
2117
+ "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
2117
2118
  "flags": "gu",
2118
2119
  "confidence": 0.8,
2119
2120
  "domains": [
@@ -2179,7 +2180,7 @@ export const RULES_DATA = [
2179
2180
  {
2180
2181
  "id": "regex:ch:postal-city",
2181
2182
  "entityType": "ADDRESS",
2182
- "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
2183
+ "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
2183
2184
  "flags": "gu",
2184
2185
  "confidence": 0.8,
2185
2186
  "domains": [
@@ -2292,7 +2293,7 @@ export const RULES_DATA = [
2292
2293
  {
2293
2294
  "id": "regex:dk:postal-city",
2294
2295
  "entityType": "ADDRESS",
2295
- "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
2296
+ "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
2296
2297
  "flags": "gu",
2297
2298
  "confidence": 0.8,
2298
2299
  "domains": [
@@ -2335,7 +2336,7 @@ export const RULES_DATA = [
2335
2336
  {
2336
2337
  "id": "regex:fi:postal-city",
2337
2338
  "entityType": "ADDRESS",
2338
- "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
2339
+ "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
2339
2340
  "flags": "gu",
2340
2341
  "confidence": 0.8,
2341
2342
  "domains": [
@@ -24,4 +24,10 @@ export interface RegexRule {
24
24
  falsePositiveNotes?: string;
25
25
  /** Optional validation function for post-match filtering (e.g., Luhn check, checksum) */
26
26
  validate?: (match: string) => boolean;
27
+ /**
28
+ * True when the rule must see the whole document (a match spans line
29
+ * breaks, e.g. PEM key blocks). Default false: detectWithRegex runs the
30
+ * rule on each line separately (T180, bounds ReDoS cost to line length).
31
+ */
32
+ multiline?: boolean;
27
33
  }
package/dist/session.d.ts CHANGED
@@ -11,6 +11,10 @@ export interface SessionOptions {
11
11
  */
12
12
  salt?: string;
13
13
  }
14
+ /** Input accepted by registerDocumentValues: plain values or anything carrying a `value`. */
15
+ export type DocumentValueInput = string | {
16
+ value: string;
17
+ };
14
18
  export declare class AnonymizationSession {
15
19
  private forwardMap;
16
20
  private reverseMap;
@@ -21,6 +25,34 @@ export declare class AnonymizationSession {
21
25
  * the base re-derives its variants and deserialized maps re-link.
22
26
  */
23
27
  private variantOf;
28
+ /**
29
+ * Compiled exact-restore alternation (R4), rebuilt lazily; null after
30
+ * every reverse-map mutation (see invalidateRestore).
31
+ */
32
+ private restoreRe;
33
+ /**
34
+ * Every value known to occur in the session's documents (R3), lowercased:
35
+ * whole entity values plus their person tokens (and hyphen parts). Fed by
36
+ * anonymizeText, anonymize, importEntries and registerDocumentValues;
37
+ * cumulative until clear(). A surrogate is never one of these. No
38
+ * capitalised-word heuristic: it would starve the pools in German and
39
+ * Polish prose (plan v2 section 9).
40
+ */
41
+ private documentValues;
42
+ /**
43
+ * Letter-only document tokens of MIN_NAME_TOKEN_LENGTH or more, bucketed
44
+ * by their first MIN_NAME_TOKEN_LENGTH characters, for the containment
45
+ * rule in isDocumentValue: linear in the candidate's length instead of
46
+ * a scan over every document token.
47
+ */
48
+ private documentNameIndex;
49
+ /**
50
+ * PERSON values that matched two or more mapped people equally well and
51
+ * therefore got a fresh identity instead of a guessed one (R5).
52
+ */
53
+ private ambiguousValues;
54
+ /** Whether the most recent anonymize() call refused an ambiguous unification. */
55
+ private lastAmbiguousFlag;
24
56
  private mode;
25
57
  /** Surrogate-mode salt (T043); constant for the session's lifetime. */
26
58
  private salt;
@@ -45,12 +77,67 @@ export declare class AnonymizationSession {
45
77
  */
46
78
  private nextSurrogate;
47
79
  /**
48
- * The already-mapped PERSON value that best matches a new variant
49
- * (T057): most shared tokens wins; ties keep the first-mapped value.
50
- * Deterministic; returns null when nothing matches.
80
+ * Whether a PERSON surrogate candidate carries a document name (R3):
81
+ * any of its tokens is a document value, or contains one of at least
82
+ * MIN_NAME_TOKEN_LENGTH letters ("Johnson" with "John" in the document,
83
+ * "Nowakowski" with "Nowak"). Other types are covered by the exact
84
+ * whole-value check alone: their tokens ("+48", "ul.", "GmbH") recur in
85
+ * every surrogate of the same shape and would starve the pools.
86
+ */
87
+ private containsDocumentName;
88
+ /** Record one document value: whole (lowercased), its tokens and hyphen parts. */
89
+ private addDocumentValue;
90
+ /**
91
+ * Declare values that occur in the session's documents (R3) so no later
92
+ * surrogate equals or, for people, contains one of them. anonymizeText
93
+ * registers its entities itself; hosts that call anonymize() value by
94
+ * value should register the whole entity list first, otherwise a person
95
+ * mapped early can still receive the name of a person mapped later.
96
+ * Accepts plain strings or objects with a `value` (DetectedEntity,
97
+ * ReplacementEntry-like `{ value }` shapes). Cumulative until clear().
98
+ */
99
+ registerDocumentValues(input: ReadonlyArray<DocumentValueInput>): void;
100
+ /**
101
+ * Whether the most recent anonymize() call met an ambiguous PERSON value
102
+ * (R5): a bare name that fits two or more mapped people equally well.
103
+ * Such a value is NOT unified; it gets its own number or surrogate. For
104
+ * anonymizeText, which maps many values, use getAmbiguousValues().
105
+ */
106
+ lastAmbiguous(): boolean;
107
+ /**
108
+ * Every PERSON value this session refused to unify because it matched
109
+ * two or more people equally well (R5), in mapping order. Cumulative
110
+ * until clear(); hosts can flag them for the user to resolve.
111
+ */
112
+ getAmbiguousValues(): string[];
113
+ /**
114
+ * One reference name per person group (R5): the fullest mapped original
115
+ * of each group (most tokens; the first mapped on ties). Variants never
116
+ * act as match targets on their own, so "Anna" cannot pull "Anna
117
+ * Kowalska" into Anna Nowak's group, while a group whose fuller form
118
+ * arrived later ("John" then "John Smith") still matches "Smith".
119
+ */
120
+ private personReferences;
121
+ /**
122
+ * The mapped person that a new PERSON value is a variant of (T057):
123
+ * only group references are considered (see personReferences); most
124
+ * shared tokens wins. A tie between two or more people is AMBIGUOUS
125
+ * (R5): no guess is made, the value gets a fresh identity and the
126
+ * session records it (lastAmbiguous / getAmbiguousValues).
127
+ * Deterministic; match is null when nothing (or too much) matches.
51
128
  */
52
129
  private findPersonVariant;
53
130
  anonymize(original: string, entityType: EntityType): string;
131
+ /** Drop the cached restore regex; called on every reverse-map change. */
132
+ private invalidateRestore;
133
+ /**
134
+ * The exact-restore regex (R4): ONE alternation of every reverse-map
135
+ * key, longest first so a longer key always wins at a given position
136
+ * ("[PERSON_11]" before "[PERSON_1]", "Adam Nowak" before "Adam"), each
137
+ * key escaped and guarded per restoreKeyPattern. Compiled once per map
138
+ * state and reused across replies. Null when nothing is restorable.
139
+ */
140
+ private restoreRegex;
54
141
  /**
55
142
  * The group a mapped value belongs to (T171): its base token and the
56
143
  * canonical original that token restores to.
@@ -86,6 +173,13 @@ export declare class AnonymizationSession {
86
173
  * forms no key owns.
87
174
  */
88
175
  private deanonymizeMangledTokens;
176
+ /**
177
+ * R9: placeholders written LITERALLY in the user's text ("[PERSON_1]" in
178
+ * a template or a previously anonymized document) raise the per-type
179
+ * counter above them, so a freshly issued placeholder never restores to
180
+ * a value the text never carried under that token.
181
+ */
182
+ private reserveLiteralPlaceholders;
89
183
  anonymizeText(text: string, entities: DetectedEntity[]): string;
90
184
  getEntries(): ReplacementEntry[];
91
185
  getForward(original: string): string | undefined;
@@ -156,6 +250,10 @@ export declare class AnonymizationSession {
156
250
  * forward map but excluded from the reverse map, preserving their
157
251
  * irreversibility. Entries merge into whatever the session already
158
252
  * holds; call clear() first for a fresh seeded session.
253
+ *
254
+ * R11: the input is validated BEFORE anything is applied (an array of
255
+ * at most 10 000 well-formed entries, see validateImportEntry); a
256
+ * violation throws and leaves the session untouched.
159
257
  */
160
258
  importEntries(entries: ReplacementEntry[]): void;
161
259
  /**