@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.
- package/CHANGELOG.md +148 -2
- package/README.md +252 -19
- package/dist/doc.d.ts +68 -1
- package/dist/doc.js +447 -62
- package/dist/dom/docx.d.ts +135 -7
- package/dist/dom/docx.js +752 -125
- package/dist/dom/drawingml.d.ts +59 -0
- package/dist/dom/drawingml.js +176 -0
- package/dist/dom/errors.d.ts +35 -0
- package/dist/dom/errors.js +24 -0
- package/dist/dom/index.d.ts +3 -0
- package/dist/dom/index.js +6 -0
- package/dist/dom/office.d.ts +56 -0
- package/dist/dom/office.js +76 -23
- package/dist/dom/opc.d.ts +135 -7
- package/dist/dom/opc.js +407 -14
- package/dist/dom/package-policy.d.ts +137 -0
- package/dist/dom/package-policy.js +355 -0
- package/dist/dom/xlsx.d.ts +64 -6
- package/dist/dom/xlsx.js +130 -20
- package/dist/engine.d.ts +18 -0
- package/dist/engine.js +37 -13
- package/dist/env.d.ts +21 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.js +10 -5
- package/dist/model-loader.d.ts +61 -6
- package/dist/model-loader.js +122 -17
- package/dist/pipeline.d.ts +87 -8
- package/dist/pipeline.js +249 -50
- package/dist/providers/bardsai.d.ts +8 -1
- package/dist/providers/bardsai.js +22 -2
- package/dist/providers/gliner-base.d.ts +8 -1
- package/dist/providers/gliner-base.js +22 -2
- package/dist/providers/gliner.d.ts +8 -1
- package/dist/providers/gliner.js +22 -2
- package/dist/regex/loader.d.ts +2 -0
- package/dist/regex/loader.js +2 -0
- package/dist/regex/rules.data.js +15 -14
- package/dist/regex/types.d.ts +6 -0
- package/dist/session.d.ts +101 -3
- package/dist/session.js +397 -43
- package/dist/surrogates.js +57 -45
- package/dist/worker-protocol.d.ts +13 -1
- package/dist/worker-protocol.js +164 -12
- package/package.json +1 -1
- package/rules/at.json +1 -1
- package/rules/be.json +1 -1
- package/rules/ch.json +1 -1
- package/rules/de.json +1 -1
- package/rules/dk.json +1 -1
- package/rules/es.json +1 -1
- package/rules/fi.json +1 -1
- package/rules/fr.json +1 -1
- package/rules/it.json +1 -1
- package/rules/no.json +1 -1
- package/rules/pl.json +1 -1
- package/rules/pt.json +1 -1
- package/rules/schema.json +4 -0
- package/rules/se.json +1 -1
- package/rules/universal.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,152 @@ All notable changes to `@doccloak/core` are documented here. The format is
|
|
|
4
4
|
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
|
|
5
5
|
project follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.12.1] - 2026-09-24
|
|
8
|
+
|
|
9
|
+
First run of the real-file corpus job (LibreOffice-generated docx, doc and
|
|
10
|
+
xlsx) on CI after 0.12.0 was published. One reader/writer gap and three test
|
|
11
|
+
expectations came out of it; no published API changed.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **xlsx: identifiers stored as numbers (T176, minimal).** A PESEL, phone or
|
|
16
|
+
account number typed into a cell is a numeric `<v>`. Numbers of seven or
|
|
17
|
+
more digits are now part of the extracted text, and a replaced numeric cell
|
|
18
|
+
is written as an inline string cell without its formula, so Excel opens
|
|
19
|
+
the workbook without repair. Shorter numbers (amounts, counts, date
|
|
20
|
+
serials) are left alone.
|
|
21
|
+
|
|
22
|
+
### Tests
|
|
23
|
+
|
|
24
|
+
- Corpus assertions distinguish consented parts (OLE ObjectPool and Macros
|
|
25
|
+
storages in .doc reported by `inspectDoc`; embedded parts in docx/xlsx
|
|
26
|
+
reported by the readers) from the rest of the package, and account for
|
|
27
|
+
occurrences inside tracked deletions that `acceptTrackedChanges` drops.
|
|
28
|
+
- A .doc whose PII sits only in frames or text boxes re-parses clean but
|
|
29
|
+
placeholder-free (the legacy writer overwrites those stories in place);
|
|
30
|
+
the suite checks for surviving needles instead of a placeholder count.
|
|
31
|
+
- Timing tests guard linearity with budgets that tolerate shared CI runners.
|
|
32
|
+
- Part-name inventory refreshed from the LibreOffice corpus.
|
|
33
|
+
|
|
34
|
+
## [0.12.0] - 2026-09-24
|
|
35
|
+
|
|
36
|
+
Security remediation release (audit `SECURITY_REPORT-2026-09.md`, plan
|
|
37
|
+
`documentation/security-remediation-plan-2026-09-v2.md`). Every change is
|
|
38
|
+
fail-closed: what Core cannot redact is refused or reported, never copied in
|
|
39
|
+
silence.
|
|
40
|
+
|
|
41
|
+
### Breaking for hosts
|
|
42
|
+
|
|
43
|
+
- **`CoreEnv.buildTokenizer(tokenizerJson, tokenizerConfig)` (T185):**
|
|
44
|
+
providers now download `tokenizer.json` and `tokenizer_config.json` from
|
|
45
|
+
their pinned commit through `fetchModelBlob` (SHA-256 and size verified,
|
|
46
|
+
cached in `doccloak-models`, resumable) and hand the parsed JSON to the
|
|
47
|
+
host, which constructs the tokenizer (`new PreTrainedTokenizer(json,
|
|
48
|
+
config)` on the web). `CoreEnv.loadTokenizer(hfModelId)` still works in
|
|
49
|
+
0.12.0 as a deprecated fallback (one console warning) and is removed in
|
|
50
|
+
0.13.0. The `AutoTokenizer.from_pretrained` probe to `resolve/main` on
|
|
51
|
+
every start is gone, so a fully cached model loads with zero requests.
|
|
52
|
+
- **`redactOfficeFile` throws `StaleAnalysisError` (T179)** when an entity
|
|
53
|
+
no longer matches the fresh extraction, instead of silently dropping it
|
|
54
|
+
and exporting the value. Pass `onMismatch: 'drop'` for the old behaviour.
|
|
55
|
+
- **Files with unredactable parts are refused by the writers (T177)** unless
|
|
56
|
+
`allowUnredactable: true` is passed (`writeAnonymizedDocx*`,
|
|
57
|
+
`writeAnonymizedXlsx*`, `redactOfficeFile`). `readDocx` / `readXlsx`
|
|
58
|
+
report them in `extraction.unredactable` (`part`, `kind`, `label`) so a
|
|
59
|
+
host can ask the user first; when allowed, `result.warnings` names every
|
|
60
|
+
part copied verbatim.
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- **Package-wide value scrub, "layer zero" (T172):** after the offset
|
|
65
|
+
replacements, every known session value (entries, variants, name tokens)
|
|
66
|
+
is replaced in every text node and attribute of every XML part of the
|
|
67
|
+
package (`charts`, `diagrams`, `customXml`, `pivotCache`, `docProps`,
|
|
68
|
+
rels and more), with a safety rule that never touches identifiers,
|
|
69
|
+
geometry or layout attributes. `writeAnonymizedDocxWithReport` /
|
|
70
|
+
`writeAnonymizedXlsxWithReport` return `{ blob, warnings }`.
|
|
71
|
+
- **New text parts and units (T174, T175):** charts (`c:v` caches, titles),
|
|
72
|
+
SmartArt (`dgm:t`), DrawingML text (`a:t`), `customXml/item*.xml` leaf
|
|
73
|
+
text, the whole `word/glossary/**` set, `numbering.xml` level text,
|
|
74
|
+
`webSettings.xml` frame titles, `w:delInstrText`, OMML `m:t`; attribute
|
|
75
|
+
units for field instruction arguments, content-control aliases, drawing
|
|
76
|
+
alt text and bookmark names (renamed bookmarks keep `REF`, `PAGEREF`,
|
|
77
|
+
`NOTEREF`, `HYPERLINK \l` and anchors valid). Tabs, breaks and
|
|
78
|
+
non-breaking hyphens are emitted as separators, text boxes are separated
|
|
79
|
+
from their host paragraph, `mc:Fallback` twins are redacted too, and a
|
|
80
|
+
replacement never crosses a paragraph break.
|
|
81
|
+
- **Part policy (T177):** `classifyPart` tables for docx and xlsx (text /
|
|
82
|
+
structural / unredactable / unknown), `describeUnredactableParts`,
|
|
83
|
+
200 MB unpacked-size guard (`UnsupportedDocumentError` code
|
|
84
|
+
`'too-large'`).
|
|
85
|
+
- **`UnsupportedDocumentError`** with codes `unrecognized-namespace`,
|
|
86
|
+
`invalid-package`, `unredactable-parts`, `too-large`, `fast-saved`,
|
|
87
|
+
`encrypted`; `extraction.empty`, `extraction.warnings`.
|
|
88
|
+
- **Engine (T185):** `createEngine(env, factories, { autoLoad: false })` makes
|
|
89
|
+
`detect()` throw `ModelNotLoadedError` until the host calls `preload()`
|
|
90
|
+
(hosts implement consent above Core); `serveEngine` validates every
|
|
91
|
+
message and answers unknown or malformed ones with an `error` frame;
|
|
92
|
+
verification markers carry `sha256:size` and are re-checked against the
|
|
93
|
+
cached blob size.
|
|
94
|
+
- **Session (T182, T183):** `registerDocumentValues`, `lastAmbiguous()` /
|
|
95
|
+
`getAmbiguousValues()`; surrogates are never a value (or a name token)
|
|
96
|
+
that occurs in the document; single-pass exact restore with script-aware
|
|
97
|
+
word boundaries; literal `[TYPE_N]` tokens in the input never collide
|
|
98
|
+
with generated ones.
|
|
99
|
+
- **Tests:** `assertNoTrace` helper scanning every part and the raw archive
|
|
100
|
+
in UTF-8, latin1 and UTF-16LE, 33 synthetic Office / .doc fixtures, a
|
|
101
|
+
real-file corpus generated by LibreOffice in CI with an independent
|
|
102
|
+
extraction check, a 30-document end-to-end session suite, adversarial
|
|
103
|
+
regex inputs.
|
|
104
|
+
|
|
105
|
+
### Changed
|
|
106
|
+
|
|
107
|
+
- Regex rules are enabled by default (`regexEnabled` defaults to `true` when neither the host nor a saved setting says otherwise). Structured identifiers no longer depend on the model alone in a fresh profile (T203).
|
|
108
|
+
- ML spans are trimmed of surrounding punctuation before merging: a whitespace-split provider such as BardS.ai returned `Anna Nowak,` and the redacted text lost the comma (T204).
|
|
109
|
+
|
|
110
|
+
- **Strict OOXML is supported (T172):** transitional and
|
|
111
|
+
`purl.oclc.org` namespaces are both read; a main part without a
|
|
112
|
+
recognised body is refused (`unrecognized-namespace`) instead of being
|
|
113
|
+
exported untouched.
|
|
114
|
+
- **Regex detection runs per line (T180)** (rules with `multiline: true`
|
|
115
|
+
run on the whole text), which removes the multi-minute stalls on long
|
|
116
|
+
prose; postal/city rules end on a letter and use a Unicode look-ahead
|
|
117
|
+
instead of `\b` (T181).
|
|
118
|
+
- **False-positive filter (T181):** CJK / Thai values of 2 characters and
|
|
119
|
+
Latin PERSON values of 2 characters (`Li`, `Ng`, `J.`) are kept; a short
|
|
120
|
+
stoplist drops titles (`Mr`, `Dr`, `Jr`...). Propagation uses Unicode
|
|
121
|
+
boundaries, exact offsets after `İ`, exact case for terms of 2
|
|
122
|
+
characters, and skips lowercase hits of common-word names (`bill`,
|
|
123
|
+
`mark`, `will`...).
|
|
124
|
+
- **Restore (T182):** `renameLabel` rejects empty and colliding labels;
|
|
125
|
+
`importEntries` / `deserialize` validate every entry and cap at 10 000;
|
|
126
|
+
`personTokens` is linear. Surrogate pools no longer contain common
|
|
127
|
+
English words or names of 3 letters or fewer.
|
|
128
|
+
- **Metadata (T179):** `contentStatus`, `identifier`, `created`,
|
|
129
|
+
`modified`, `lastPrinted`, `revision`, `version`, document statistics,
|
|
130
|
+
`TitlesOfParts` / `HeadingPairs` are blanked, zeroed or normalised;
|
|
131
|
+
comment and revision dates are set to 2000-01-01; `w:rsid*` attributes
|
|
132
|
+
are removed; ZIP entries are dated 1980-01-01 with no comments.
|
|
133
|
+
- **Always scrubbed without detection (T174):** hyperlink tooltips,
|
|
134
|
+
`mailto:` relationship targets, `w:docVars`, content-control tags without
|
|
135
|
+
data binding.
|
|
136
|
+
- **Legacy .doc hardening (T178, security report 2026-09 H5, H6, M5, L2):**
|
|
137
|
+
`readDocText` and `writeAnonymizedDoc` now throw `UnsupportedDocumentError`
|
|
138
|
+
(`code: 'fast-saved'`) for incrementally saved files (`fComplex`, or
|
|
139
|
+
`cQuickSaves > 0` on Word 97 files) and (`code: 'encrypted'`) for
|
|
140
|
+
`fEncrypted` / `fObfuscated` files instead of returning partial or
|
|
141
|
+
ciphertext output. `writeAnonymizedDoc` takes an optional
|
|
142
|
+
`valueReplacements` list (same shape as `writeAnonymizedDocx`) and destroys
|
|
143
|
+
every remaining byte occurrence of each original value and its 4+ character
|
|
144
|
+
tokens, any case, in `WordDocument` (UTF-16LE), `Table` (UTF-16LE and
|
|
145
|
+
cp1252) and `Data` (UTF-16LE), length-preserving. `SttbfRMark`,
|
|
146
|
+
`GrpXstAtnOwners`, `SttbfBkmk`, `SttbSavedBy` and `AutosaveSource` are
|
|
147
|
+
scrubbed alongside `SttbfAssoc`. New `inspectDoc(buffer)` reports the
|
|
148
|
+
refusal flags and the presence of `Data`, `ObjectPool` and `Macros`
|
|
149
|
+
streams (the latter two are still copied through; see README "Known
|
|
150
|
+
limits: legacy .doc"). `UnsupportedDocumentError` is also exported from
|
|
151
|
+
the main entry.
|
|
152
|
+
|
|
7
153
|
## [0.11.0] - 2026-09-23
|
|
8
154
|
|
|
9
155
|
### Changed
|
|
@@ -98,13 +244,13 @@ project follows [Semantic Versioning](https://semver.org/).
|
|
|
98
244
|
PEM private-key blocks, JWTs, connection strings with embedded
|
|
99
245
|
credentials, `secret=`-style assignments and high-entropy tokens, and
|
|
100
246
|
a GB driving-licence rule. Credentials never receive realistic
|
|
101
|
-
stand-ins
|
|
247
|
+
stand-ins, even in surrogate mode they always fall back to typed
|
|
102
248
|
placeholders, since a same-shape fake key still reads as a live
|
|
103
249
|
credential.
|
|
104
250
|
|
|
105
251
|
- **Tolerant restore-token matching (T098):** new side-effect-free
|
|
106
252
|
`@doccloak/core/restore-tokens` export that restores placeholders LLM
|
|
107
|
-
replies have mangled
|
|
253
|
+
replies have mangled, case changes (`[person_1]`), separator swaps
|
|
108
254
|
(`[PERSON 1]`), markdown wrapping (`**[PERSON_1]**`), dropped brackets
|
|
109
255
|
and stray edge punctuation. Restoration only happens on an unambiguous
|
|
110
256
|
canonical hit; anything ambiguous is left exactly as the model wrote
|
package/README.md
CHANGED
|
@@ -38,14 +38,19 @@ the engine downloads them from Hugging Face and stores them in the
|
|
|
38
38
|
`CoreEnv.modelCache` you provide (the browser Cache Storage in the reference
|
|
39
39
|
hosts):
|
|
40
40
|
|
|
41
|
-
| Provider | Model | Download |
|
|
41
|
+
| Provider id | Model | Download |
|
|
42
42
|
| --- | --- | --- |
|
|
43
|
-
| `gliner` | GLiNER PII
|
|
44
|
-
| `
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`
|
|
43
|
+
| `gliner` | GLiNER PII Small (multi-language, custom labels) | ~83 MB |
|
|
44
|
+
| `gliner-base` | GLiNER PII Base (often better for English and dates, custom labels) | ~197 MB |
|
|
45
|
+
| `bardsai` | BardS.ai EU PII (24+ EU languages, 35 PII types; labeled "Legacy" in the web app) | ~279 MB |
|
|
46
|
+
|
|
47
|
+
`pickDefaultProvider(hardware)` chooses `gliner` on mobile devices or when
|
|
48
|
+
`deviceMemoryGB` is 4 or less, and `bardsai` otherwise. Download progress is
|
|
49
|
+
surfaced via `engine.onDownloadProgress(cb)`. Every model and both of its
|
|
50
|
+
tokenizer files are fetched from an immutable `resolve/<commit>` URL and
|
|
51
|
+
verified against a pinned SHA-256 and byte size; the pins are listed in
|
|
52
|
+
[documentation/model-provenance.md](../documentation/model-provenance.md)
|
|
53
|
+
of the DocCloak workspace.
|
|
49
54
|
|
|
50
55
|
ONNX Runtime also needs its WASM binaries at runtime; point
|
|
51
56
|
`CoreEnv.wasm.paths` at wherever your host serves the `onnxruntime-web`
|
|
@@ -57,23 +62,198 @@ ONNX Runtime also needs its WASM binaries at runtime; point
|
|
|
57
62
|
`DocCloakEngine`: `detect(text, signal?, onProgress?)`, `preload()`,
|
|
58
63
|
`switchProvider(id)`, `getSettings()` / `updateSettings(patch)`,
|
|
59
64
|
`onDownloadProgress` / `onDetectionProgress`, `release()`. Settings persist
|
|
60
|
-
through `env.kv`.
|
|
65
|
+
through `env.kv`. `options.autoLoad` (default `true`) lets `detect()`
|
|
66
|
+
download and initialise the model on its own; with `autoLoad: false` the
|
|
67
|
+
engine never downloads implicitly: `detect()` before a completed
|
|
68
|
+
`preload()` (or `switchProvider()`) rejects with `ModelNotLoadedError`
|
|
69
|
+
(a preload still in flight is awaited). Use `false` when model downloads
|
|
70
|
+
are gated behind user consent and call `preload()` from the consent flow.
|
|
61
71
|
- **Detection pipeline** - `detectEntities`, `detectWithRegex`,
|
|
62
72
|
`resolveOverlaps`, `filterFalsePositives`, `propagateEntities` for hosts
|
|
63
73
|
that compose their own flow; `ALL_REGEX_RULES` and `REGEX_REGIONS` describe
|
|
64
74
|
the built-in regex rule set (universal + per-country rules).
|
|
65
75
|
- **Session state** - `AnonymizationSession` maps detected entities to stable
|
|
66
|
-
placeholders (labeled or
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
placeholders (labeled, blanked or surrogate) and back. In surrogate mode
|
|
77
|
+
`registerDocumentValues(valuesOrEntities)` tells the session which values
|
|
78
|
+
occur in the document so no surrogate ever equals a real value from it
|
|
79
|
+
(`anonymizeText` does this automatically); `getAmbiguousValues()` lists the
|
|
80
|
+
values whose person-variant match was a tie and therefore got a new
|
|
81
|
+
placeholder instead of being merged with an existing person.
|
|
82
|
+
- **Documents** (`/dom`) - `readDocx` / `writeAnonymizedDocx` /
|
|
83
|
+
`writeAnonymizedDocxWithReport`, `readXlsx` / `writeAnonymizedXlsx`,
|
|
84
|
+
`analyzeOfficeFile` / `redactOfficeFile`, legacy `.doc` support on the main
|
|
85
|
+
entry (`readDocText`, `writeAnonymizedDoc`, `inspectDoc`), OCR helpers
|
|
69
86
|
(`loadImageToCanvas`, `recognizeCanvas`, `renderRedactedImage`) built on
|
|
70
|
-
tesseract.js.
|
|
87
|
+
tesseract.js. See "Document API: refusals, warnings and consent" below for
|
|
88
|
+
the error and result shapes.
|
|
71
89
|
- **Worker protocol** (`/worker-protocol`) - `serveEngine(engine, port)` on
|
|
72
90
|
the side that owns the engine, `connectEngine(port)` on the UI side.
|
|
73
91
|
`PortLike` is satisfied by thin wrappers over `Worker`, `MessagePort` or
|
|
74
92
|
`chrome.runtime.Port`.
|
|
75
93
|
- **Model loader** - `fetchModelBlob` / `evictModelFromCache` implement
|
|
76
|
-
cache-first model downloads with progress
|
|
94
|
+
cache-first model downloads with progress, resume, retry and SHA-256
|
|
95
|
+
verification (`ModelIntegrityError` on a mismatch; the entry is evicted).
|
|
96
|
+
Verified cache entries carry a marker `${sha256}:${size}` bound to the blob
|
|
97
|
+
size, so a cache hit whose size changed is re-hashed instead of trusted.
|
|
98
|
+
Each provider also pins its two tokenizer files (`GLINER_TOKENIZER_FILES`,
|
|
99
|
+
`GLINER_BASE_TOKENIZER_FILES`, `BARDSAI_TOKENIZER_FILES`: immutable
|
|
100
|
+
`resolve/<commit>` URL, sha256, size); `loadPinnedTokenizer` fetches them
|
|
101
|
+
through the same path and hands the parsed JSON to `env.buildTokenizer`.
|
|
102
|
+
|
|
103
|
+
### Document API: refusals, warnings and consent
|
|
104
|
+
|
|
105
|
+
The readers and writers fail closed: a file the engine cannot redact
|
|
106
|
+
completely is refused with a typed error, or reported so the host can ask the
|
|
107
|
+
user, never exported under a false "redacted" label.
|
|
108
|
+
|
|
109
|
+
**`UnsupportedDocumentError`** (main entry and `/dom`) carries a stable
|
|
110
|
+
`code` and a `details: string[]` list for diagnostics. Map `code` to your own
|
|
111
|
+
localized message with a one-line remedy:
|
|
112
|
+
|
|
113
|
+
| `code` | Thrown by | Meaning | Remedy to show |
|
|
114
|
+
| --- | --- | --- | --- |
|
|
115
|
+
| `'unrecognized-namespace'` | `readDocx`, `readXlsx` | The main part uses an XML namespace the reader does not know (neither the transitional nor the strict OOXML set), so text could be missed | Open in Word/Excel and save again as .docx/.xlsx |
|
|
116
|
+
| `'invalid-package'` | `readDocx`, `readXlsx` | The zip or a required XML part cannot be parsed | Same |
|
|
117
|
+
| `'too-large'` | `readDocx`, `readXlsx` | The unpacked size of the package exceeds 200 MB (zip-bomb and memory guard) | Split the document |
|
|
118
|
+
| `'unredactable-parts'` | `writeAnonymizedDocx*`, `writeAnonymizedXlsx*`, `redactOfficeFile` | The package contains parts whose content cannot be redacted and the host did not pass `allowUnredactable: true` | Ask the user (see below) |
|
|
119
|
+
| `'fast-saved'` | `readDocText`, `writeAnonymizedDoc` | Legacy .doc saved incrementally: deleted text lives outside the piece table where no detector sees it | Open in Word, Save As (or save as .docx) |
|
|
120
|
+
| `'encrypted'` | `readDocText`, `writeAnonymizedDoc` | Legacy .doc is encrypted or XOR-obfuscated | Remove the password |
|
|
121
|
+
|
|
122
|
+
**Extraction results.** `readDocx` and `readXlsx` return, next to
|
|
123
|
+
`plainText` and the offset mappings:
|
|
124
|
+
|
|
125
|
+
- `unredactable: UnredactablePart[]` - parts the writer cannot redact, each
|
|
126
|
+
with `part` (zip path), `kind` (`'embedded-object' | 'macros' |
|
|
127
|
+
'html-chunk' | 'external-data' | 'printer-settings' | 'unknown'`) and a
|
|
128
|
+
human `label` ("embedded Excel sheet"). The reader never throws for these;
|
|
129
|
+
the writer refuses with `'unredactable-parts'` unless the host passes
|
|
130
|
+
`{ allowUnredactable: true }`, in which case those parts are copied
|
|
131
|
+
verbatim and one warning per part is returned. Only pass it after the user
|
|
132
|
+
has been told which parts stay unchanged (informed consent); a host that
|
|
133
|
+
does not ask keeps the fail-closed default. Layer zero still scrubs every
|
|
134
|
+
known value from the XML parts of the package either way.
|
|
135
|
+
- `warnings: string[]` - parts that could not be parsed and were therefore
|
|
136
|
+
skipped by the layer-zero scrub, and similar non-fatal findings (a sheet
|
|
137
|
+
name containing an entity, for example). Show them to the user.
|
|
138
|
+
- `empty: boolean` - the document contains no text units at all; show
|
|
139
|
+
"nothing to redact" instead of exporting.
|
|
140
|
+
|
|
141
|
+
**Write results.** `writeAnonymizedDocxWithReport` /
|
|
142
|
+
`writeAnonymizedXlsxWithReport` return `{ blob, warnings }`; the plain
|
|
143
|
+
`writeAnonymizedDocx` / `writeAnonymizedXlsx` return the `Blob` only.
|
|
144
|
+
|
|
145
|
+
**One-call path.** `analyzeOfficeFile(file, detect)` reads the package,
|
|
146
|
+
runs your detector and exposes `unredactable[]` and `warnings[]` next to the
|
|
147
|
+
entities. `redactOfficeFile(file, analysis, session, options)` applies the
|
|
148
|
+
session's replacements and writes the file:
|
|
149
|
+
|
|
150
|
+
- `options.onMismatch` (`'throw' | 'drop'`, default `'throw'`) decides what
|
|
151
|
+
happens when an entity of the analysis no longer matches the text at its
|
|
152
|
+
offsets (the document was re-read or edited in between): `'throw'` rejects
|
|
153
|
+
with `StaleAnalysisError`, which lists the stale entities, so a stale
|
|
154
|
+
analysis never yields a leaking "success"; `'drop'` skips those entities
|
|
155
|
+
and reports them in `result.warnings`. `isStaleAnalysisError(err)` works
|
|
156
|
+
across realms.
|
|
157
|
+
- `options.allowUnredactable` (default `false`) is required to export a file
|
|
158
|
+
with embedded objects, macros, HTML chunks, external data or printer
|
|
159
|
+
settings; `result.warnings` then names each part copied verbatim.
|
|
160
|
+
|
|
161
|
+
All of these are exported from `@doccloak/core/dom`.
|
|
162
|
+
|
|
163
|
+
**Legacy .doc.** `inspectDoc(buffer)` returns `{ encrypted, fastSaved,
|
|
164
|
+
streams: { data, objectPool, macros } }` without throwing, so a host can show
|
|
165
|
+
the same consent card for a .doc whose `ObjectPool` (embedded objects) or
|
|
166
|
+
`Macros` (VBA) storage will be copied unchanged.
|
|
167
|
+
|
|
168
|
+
## What is removed or normalised in Office packages
|
|
169
|
+
|
|
170
|
+
Independently of what was detected, every exported .docx / .xlsx has:
|
|
171
|
+
|
|
172
|
+
- **Removed:** `cp:revision`, `cp:version`; every `w:rsid*` attribute in the
|
|
173
|
+
content parts and `w:rsids` in `word/settings.xml`; the thumbnail
|
|
174
|
+
(`docProps/thumbnail.*` with its relationship and content-type override);
|
|
175
|
+
the attached template, mail-merge settings and document variables
|
|
176
|
+
(`w:docVars`) in `word/settings.xml`; hyperlink tooltips; content-control
|
|
177
|
+
tags without a data binding; `c:externalData` links in charts (the chart
|
|
178
|
+
keeps its cached data).
|
|
179
|
+
- **Blanked:** `creator`, `lastModifiedBy`, `title`, `subject`,
|
|
180
|
+
`description`, `keywords`, `category`, `contentStatus`, `identifier`
|
|
181
|
+
(`docProps/core.xml`); `Company`, `Manager`, `HyperlinkBase`, `Template`
|
|
182
|
+
and every heading / sheet / defined-name title in `TitlesOfParts` and
|
|
183
|
+
`HeadingPairs` (`docProps/app.xml`); custom property values (names kept,
|
|
184
|
+
`docProps/custom.xml`); the comment-author registry (`word/people.xml`)
|
|
185
|
+
and `w:author` / `w:initials` on comments and tracked changes.
|
|
186
|
+
- **Normalised:** `dcterms:created`, `dcterms:modified` and `cp:lastPrinted`
|
|
187
|
+
to `2000-01-01T00:00:00Z`; `w:date` on comments, insertions, deletions and
|
|
188
|
+
property-change records to `2000-01-01T00:00:00Z` (the only visible
|
|
189
|
+
effect: comment and revision dates in the downloaded file); page, word,
|
|
190
|
+
character, line and paragraph counts and `TotalTime` to 0; every ZIP entry
|
|
191
|
+
dated `1980-01-01` with entry and archive comments removed, so the output
|
|
192
|
+
no longer reveals which parts were touched; `mailto:` relationship targets
|
|
193
|
+
replaced by `mailto:redacted@example.invalid`.
|
|
194
|
+
- **Layer zero:** every value of the session (with its variants and name
|
|
195
|
+
tokens of 4+ characters) is scrubbed from the text nodes and attribute
|
|
196
|
+
values of every XML, `.rels` and `.vml` part of the package (charts,
|
|
197
|
+
diagrams, customXml, glossary, docProps, pivot caches), never touching
|
|
198
|
+
element names, `[Content_Types].xml`, relationship `Type`s, ids or numeric
|
|
199
|
+
layout attributes.
|
|
200
|
+
|
|
201
|
+
## Known limits
|
|
202
|
+
|
|
203
|
+
**Office packages (.docx, .xlsx)**
|
|
204
|
+
|
|
205
|
+
- Images inside documents (`media/*`) are copied unchanged; text in pictures
|
|
206
|
+
is not OCR-scanned. Hosts can run the OCR helpers on extracted images.
|
|
207
|
+
- Embedded objects (`embeddings/*`), VBA projects, `altChunk` HTML fragments,
|
|
208
|
+
external data connections, external links and printer settings cannot be
|
|
209
|
+
redacted. They are reported in `extraction.unredactable` and exported
|
|
210
|
+
verbatim only with `allowUnredactable: true` (see above). Removing them is
|
|
211
|
+
planned for 0.13.0 (T199), gated on the real-file corpus (T196).
|
|
212
|
+
- Spreadsheets: sheet names with an entity are reported in `warnings`, not
|
|
213
|
+
renamed; numeric and date cells, formula literals, defined names and data
|
|
214
|
+
validation texts are not extracted yet (T176).
|
|
215
|
+
- A bookmark rename keeps the identifier valid (`PERSON_1`, never
|
|
216
|
+
`[PERSON_1]`); field instructions are replaced only inside their quoted
|
|
217
|
+
arguments and bookmark tokens.
|
|
218
|
+
|
|
219
|
+
**Legacy .doc**
|
|
220
|
+
|
|
221
|
+
`writeAnonymizedDoc(buffer, replacements, valueReplacements?)` rewrites the
|
|
222
|
+
piece table for main-text replacements, overwrites subdocument ranges in
|
|
223
|
+
place, space-fills the OLE property sets and the Table string tables
|
|
224
|
+
(`SttbfAssoc`, `SttbfRMark`, `GrpXstAtnOwners`, `SttbfBkmk`, `SttbSavedBy`,
|
|
225
|
+
`AutosaveSource`) and then destroys every remaining byte occurrence of each
|
|
226
|
+
original value (the value, its whitespace tokens of 4+ characters, any case)
|
|
227
|
+
without moving a single structure: `WordDocument` and `Data` are scanned as
|
|
228
|
+
UTF-16LE, `Table` as UTF-16LE and cp1252. What it does not do:
|
|
229
|
+
|
|
230
|
+
- **Fast-saved files are refused** (`code: 'fast-saved'`; `fComplex`, or
|
|
231
|
+
`cQuickSaves > 0` on Word 97 files). Deleted text of an incremental save
|
|
232
|
+
lives outside the piece table, so detection never sees it. Ask the user to
|
|
233
|
+
open the file in Word and Save As (or save as .docx).
|
|
234
|
+
- **Encrypted or XOR-obfuscated files are refused** (`code: 'encrypted'`).
|
|
235
|
+
- **cp1252 remnants in `Data` and `WordDocument` are not searched.** Hyperlink
|
|
236
|
+
field data and OLE previews store text as UTF-16; a cp1252 search would hit
|
|
237
|
+
bytes inside embedded pictures and corrupt them.
|
|
238
|
+
- **`ObjectPool/*` (embedded OLE objects), `Macros/*` (VBA) and `\x01CompObj`
|
|
239
|
+
are copied through unchanged.** Call `inspectDoc(buffer)` and warn when
|
|
240
|
+
`streams.objectPool` or `streams.macros` is `true`.
|
|
241
|
+
- A token that also occurs inside a replacement (for example `Person` in
|
|
242
|
+
`[PERSON_1]`) is not scrubbed on its own so placeholders survive; the full
|
|
243
|
+
original value always is.
|
|
244
|
+
|
|
245
|
+
**Detection and restore**
|
|
246
|
+
|
|
247
|
+
- No model catches every name; hosts should keep a review step and let users
|
|
248
|
+
add manual entities or dictionary terms.
|
|
249
|
+
- Regex rules run per line since 0.12.0 (except rules flagged `multiline`),
|
|
250
|
+
so a pattern split across a line break (postal code on one line, city on
|
|
251
|
+
the next) is not matched as one entity.
|
|
252
|
+
- Surrogate mode preserves structure by design: name token count, gender and
|
|
253
|
+
locale; the phone country prefix; the date format and a constant per-session
|
|
254
|
+
day shift (so one known date reveals the shift for all dates); the IBAN
|
|
255
|
+
country and letter positions; the email shape. Surrogate restore matches
|
|
256
|
+
by value and is best-effort; placeholder mode is exact.
|
|
77
257
|
|
|
78
258
|
## Host integration: CoreEnv
|
|
79
259
|
|
|
@@ -84,10 +264,12 @@ engine talks to the outside world:
|
|
|
84
264
|
```ts
|
|
85
265
|
interface CoreEnv {
|
|
86
266
|
kv: KVStore; // async string KV for settings persistence
|
|
87
|
-
modelCache: BlobCache; // blob cache for downloaded model files
|
|
267
|
+
modelCache: BlobCache; // blob cache for downloaded model + tokenizer files
|
|
88
268
|
fetch: typeof fetch; // injectable for tests/proxies
|
|
89
269
|
wasm: { paths: string; numThreads?: number }; // onnxruntime-web asset location
|
|
90
|
-
|
|
270
|
+
buildTokenizer?(tokenizerJson: unknown, tokenizerConfig: unknown): unknown | Promise<unknown>;
|
|
271
|
+
/** @deprecated since 0.12.0, removed in 0.13.0; used only when buildTokenizer is absent */
|
|
272
|
+
loadTokenizer?(hfModelId: string): Promise<unknown>;
|
|
91
273
|
hardware?: HardwareHints; // deviceMemoryGB / isMobile, feeds provider default
|
|
92
274
|
persistStorage?: () => Promise<boolean>; // e.g. navigator.storage.persist()
|
|
93
275
|
}
|
|
@@ -96,11 +278,62 @@ interface CoreEnv {
|
|
|
96
278
|
`memoryKV()` and `memoryBlobCache()` provide in-memory implementations for
|
|
97
279
|
tests and short-lived Node processes.
|
|
98
280
|
|
|
281
|
+
### buildTokenizer
|
|
282
|
+
|
|
283
|
+
Core downloads the tokenizer files itself. Every provider pins
|
|
284
|
+
`tokenizer.json` and `tokenizer_config.json` at the same immutable commit as
|
|
285
|
+
its ONNX model, with a SHA-256 and byte size, and fetches them through
|
|
286
|
+
`fetchModelBlob` (resume, retry, `modelCache`, verification marker,
|
|
287
|
+
`ModelIntegrityError`). The host only turns the two verified, parsed objects
|
|
288
|
+
into a tokenizer instance:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import { PreTrainedTokenizer } from '@huggingface/transformers';
|
|
292
|
+
|
|
293
|
+
buildTokenizer: (tokenizerJson, tokenizerConfig) =>
|
|
294
|
+
new PreTrainedTokenizer(tokenizerJson as never, tokenizerConfig as never),
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Pick the class from `tokenizerConfig.tokenizer_class` if your library needs
|
|
298
|
+
a specific subclass (`PreTrainedTokenizerFast` for GLiNER PII Small,
|
|
299
|
+
`DebertaV2Tokenizer` for GLiNER PII Base, `XLMRobertaTokenizer` for BardS.ai);
|
|
300
|
+
`PreTrainedTokenizer` handles all three in `@huggingface/transformers`.
|
|
301
|
+
With `buildTokenizer` the host never contacts the network for tokenizers, a
|
|
302
|
+
warm `modelCache` works offline, and the only URLs fetched on a cold start
|
|
303
|
+
are the three pinned `resolve/<commit>/...` files of the active provider.
|
|
304
|
+
|
|
305
|
+
`loadTokenizer(hfModelId)` (0.11 and earlier) is still honoured when
|
|
306
|
+
`buildTokenizer` is absent, with a one-time deprecation warning. It lets the
|
|
307
|
+
host library probe the mutable `resolve/main` branch and keep an unverified
|
|
308
|
+
second cache, so migrate before 0.13.0, where `buildTokenizer` becomes
|
|
309
|
+
required and `loadTokenizer` is removed.
|
|
310
|
+
|
|
311
|
+
### autoLoad and ModelNotLoadedError
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
import { createEngine, ModelNotLoadedError } from '@doccloak/core';
|
|
315
|
+
|
|
316
|
+
const engine = createEngine(env, undefined, { autoLoad: false });
|
|
317
|
+
try {
|
|
318
|
+
await engine.detect(text);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
if (err instanceof ModelNotLoadedError) {
|
|
321
|
+
// ask the user for consent, then:
|
|
322
|
+
await engine.preload();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
With `autoLoad: false` no code path of the engine downloads a model unless
|
|
328
|
+
the host calls `preload()` or `switchProvider()`; `ModelNotLoadedError`
|
|
329
|
+
carries the `providerId` that needs loading. The default (`autoLoad: true`)
|
|
330
|
+
keeps the 0.11 behaviour where `detect()` loads on demand.
|
|
331
|
+
|
|
99
332
|
### Web app quickstart
|
|
100
333
|
|
|
101
334
|
```ts
|
|
102
335
|
import { createEngine, type CoreEnv } from '@doccloak/core';
|
|
103
|
-
import {
|
|
336
|
+
import { PreTrainedTokenizer } from '@huggingface/transformers';
|
|
104
337
|
|
|
105
338
|
const env: CoreEnv = {
|
|
106
339
|
kv: {
|
|
@@ -118,7 +351,7 @@ const env: CoreEnv = {
|
|
|
118
351
|
},
|
|
119
352
|
fetch: (...args) => fetch(...args),
|
|
120
353
|
wasm: { paths: '/', numThreads: 1 }, // serve onnxruntime-web *.wasm under /
|
|
121
|
-
|
|
354
|
+
buildTokenizer: (json, config) => new PreTrainedTokenizer(json as never, config as never),
|
|
122
355
|
hardware: { isMobile: /Mobile/i.test(navigator.userAgent) },
|
|
123
356
|
};
|
|
124
357
|
|
|
@@ -190,14 +423,14 @@ minimal setup:
|
|
|
190
423
|
|
|
191
424
|
```ts
|
|
192
425
|
import { createEngine, memoryKV, memoryBlobCache } from '@doccloak/core';
|
|
193
|
-
import {
|
|
426
|
+
import { PreTrainedTokenizer } from '@huggingface/transformers';
|
|
194
427
|
|
|
195
428
|
const engine = createEngine({
|
|
196
429
|
kv: memoryKV(),
|
|
197
430
|
modelCache: memoryBlobCache(),
|
|
198
431
|
fetch: (...args) => fetch(...args),
|
|
199
432
|
wasm: { paths: 'node_modules/onnxruntime-web/dist/', numThreads: 1 },
|
|
200
|
-
|
|
433
|
+
buildTokenizer: (json, config) => new PreTrainedTokenizer(json as never, config as never),
|
|
201
434
|
});
|
|
202
435
|
await engine.ready;
|
|
203
436
|
console.log(await engine.detect('Contact jane.doe@example.com or +48 601 123 456.'));
|
package/dist/doc.d.ts
CHANGED
|
@@ -1,8 +1,53 @@
|
|
|
1
|
+
import { type ValueReplacement } from './docx.js';
|
|
2
|
+
/** Result of inspectDoc. */
|
|
3
|
+
export interface DocInspection {
|
|
4
|
+
/** fEncrypted or fObfuscated is set: readDocText / writeAnonymizedDoc refuse the file. */
|
|
5
|
+
encrypted: boolean;
|
|
6
|
+
/** fComplex is set (or Word 97 cQuickSaves > 0): readDocText / writeAnonymizedDoc refuse the file. */
|
|
7
|
+
fastSaved: boolean;
|
|
8
|
+
/** Streams that are copied through the export without being redacted. */
|
|
9
|
+
streams: {
|
|
10
|
+
/** Data stream (field data such as hyperlinks, pictures, OLE previews). Scanned and scrubbed in UTF-16LE only. */
|
|
11
|
+
data: boolean;
|
|
12
|
+
/** ObjectPool storage (embedded OLE documents). Not scrubbed. */
|
|
13
|
+
objectPool: boolean;
|
|
14
|
+
/** Macros storage (VBA project). Not scrubbed. */
|
|
15
|
+
macros: boolean;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Cheap look at a legacy .doc without parsing the piece table: which refusal
|
|
20
|
+
* flags are set and which non-text streams the file carries. Hosts use it to
|
|
21
|
+
* show a warning next to the export (ObjectPool / Macros are copied through
|
|
22
|
+
* unchanged, see writeAnonymizedDoc) or to explain a refusal up front.
|
|
23
|
+
* Throws only when the file is not a .doc at all.
|
|
24
|
+
*/
|
|
25
|
+
export declare function inspectDoc(buffer: ArrayBuffer | Uint8Array): DocInspection;
|
|
26
|
+
/**
|
|
27
|
+
* Write a redacted copy of a legacy .doc.
|
|
28
|
+
*
|
|
29
|
+
* `replacements` are offsets into the text returned by readDocText. Main-text
|
|
30
|
+
* ranges are swapped for the replacement string through a rewritten piece
|
|
31
|
+
* table; ranges in subdocuments (footnotes, headers, comments) are overwritten
|
|
32
|
+
* in place. `valueReplacements` (same shape as writeAnonymizedDocx) add
|
|
33
|
+
* original values that were not located by offset, e.g. session entries.
|
|
34
|
+
*
|
|
35
|
+
* Every original value (from both sources) is then destroyed wherever else it
|
|
36
|
+
* still occurs as bytes, without moving any structure (see scrubStreamBytes).
|
|
37
|
+
*
|
|
38
|
+
* Refuses (UnsupportedDocumentError) fast-saved and encrypted files, see
|
|
39
|
+
* assertSupportedFib.
|
|
40
|
+
*
|
|
41
|
+
* Copied through untouched: ObjectPool (embedded OLE objects), Macros (VBA)
|
|
42
|
+
* and \x01CompObj. TODO(T196, T199): delete them with CFB.utils.cfb_del once
|
|
43
|
+
* the open-in-Word / LibreOffice experiment proves the result still opens.
|
|
44
|
+
* Until then hosts should call inspectDoc and warn when they are present.
|
|
45
|
+
*/
|
|
1
46
|
export declare function writeAnonymizedDoc(buffer: ArrayBuffer, replacements: Array<{
|
|
2
47
|
start: number;
|
|
3
48
|
end: number;
|
|
4
49
|
replacement: string;
|
|
5
|
-
}
|
|
50
|
+
}>, valueReplacements?: ValueReplacement[]): Promise<Blob>;
|
|
6
51
|
/**
|
|
7
52
|
* Extract plain text from a legacy .doc (OLE2/Compound File Binary) file.
|
|
8
53
|
* Parses the FIB and piece table to correctly extract text from both
|
|
@@ -11,3 +56,25 @@ export declare function writeAnonymizedDoc(buffer: ArrayBuffer, replacements: Ar
|
|
|
11
56
|
* in those regions is visible to detection.
|
|
12
57
|
*/
|
|
13
58
|
export declare function readDocText(buffer: ArrayBuffer): string;
|
|
59
|
+
/**
|
|
60
|
+
* Derive the byte-scrub needles from the original values: each value itself,
|
|
61
|
+
* each whitespace-separated token and each token stripped of surrounding
|
|
62
|
+
* punctuation, keeping those of at least MIN_SCRUB_NEEDLE characters.
|
|
63
|
+
* Matching is case-insensitive (see scrubStreamBytes), which covers every
|
|
64
|
+
* case variant (as written, lower, UPPER, Capitalised) in one pass.
|
|
65
|
+
*
|
|
66
|
+
* A needle that occurs (case-insensitively) inside one of the `replacements`
|
|
67
|
+
* is dropped: a token such as "Person" from "Leaky Person" would otherwise
|
|
68
|
+
* blank the "[PERSON_1]" placeholder, and a surrogate that keeps a word of
|
|
69
|
+
* the original ("Smith & Co" -> "Nowak & Co") would lose it. The full
|
|
70
|
+
* original value is never dropped.
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildScrubNeedles(originals: string[], replacements?: string[]): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Overwrite every case-insensitive occurrence of `needle` in `bytes` with
|
|
75
|
+
* spaces of the same encoding. UTF-16LE: units are read at every byte
|
|
76
|
+
* offset (strings in the Table stream are not always 2-byte aligned) and
|
|
77
|
+
* replaced by 0x20 0x00; cp1252: bytes replaced by 0x20. Returns the number
|
|
78
|
+
* of hits. Stream length never changes.
|
|
79
|
+
*/
|
|
80
|
+
export declare function scrubStreamBytes(bytes: Uint8Array, needle: string, encoding: 'utf16le' | 'cp1252'): number;
|