@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
package/dist/dom/xlsx.js CHANGED
@@ -15,18 +15,28 @@
15
15
  * - worksheet header/footer definitions
16
16
  * - legacy cell comments (xl/comments*.xml) and threaded comments
17
17
  *
18
- * Deliberately out of scope in phase 1 (documented for T-later): drawing/chart
19
- * text parts (xl/drawings, xl/charts), data connection definitions
20
- * (xl/connections.xml) and defined-name formulas. Metadata scrubbing follows
21
- * the docx module's policy via the shared OPC helpers.
18
+ * - drawing text boxes and chart titles / string caches (xl/drawings,
19
+ * xl/charts) through the DrawingML units (T175)
20
+ *
21
+ * Deliberately out of scope (T176): sheet names, defined-name formulas, table
22
+ * column names, pivot caches; layer zero scrubs known values there. Data
23
+ * connections and external links are reported as unredactable (T177).
24
+ * Metadata scrubbing follows the docx module's policy via the shared OPC
25
+ * helpers.
22
26
  */
23
27
  import JSZip from 'jszip';
24
- import { applyTextReplacements } from "./docx.js";
25
- import { readXmlPart, writeXmlPart, scrubDocPropsParts, removePackageThumbnail, scrubExternalRelTargets, replaceSensitiveValues, } from "./opc.js";
28
+ import { applyTextReplacements, assertUnredactableAllowed, unredactableWarnings } from "./docx.js";
29
+ import { drawingUnits } from "./drawingml.js";
30
+ import { XLSX_DRAWING_TEXT_PARTS, MAX_UNPACKED_BYTES, assertUnpackedSize, collectUnredactableParts, } from "./package-policy.js";
31
+ import { OOXML_NS, isElementIn, elementsByLocalName, foreignNamespaces, readXmlPart, writeXmlPart, scrubDocPropsParts, normalizeZipEntries, removePackageThumbnail, scrubExternalRelTargets, replaceSensitiveValues, scrubPackageValues, } from "./opc.js";
26
32
  import { getFileExtension } from "./docx.js";
27
- const SS_NS = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
28
- function isSpreadsheetEl(el, localName) {
29
- return el.namespaceURI === SS_NS && el.localName === localName;
33
+ import { UnsupportedDocumentError } from "./errors.js";
34
+ /** True for a SpreadsheetML element (transitional or strict namespace, T172). */
35
+ export function isSpreadsheetEl(el, localName) {
36
+ return isElementIn(el, OOXML_NS.ss, localName);
37
+ }
38
+ function isSpreadsheetNs(el) {
39
+ return el.namespaceURI !== null && OOXML_NS.ss.has(el.namespaceURI);
30
40
  }
31
41
  /** Collect descendant <t> elements (spreadsheet namespace) in document order. */
32
42
  function collectTextLeaves(root) {
@@ -41,12 +51,51 @@ function collectTextLeaves(root) {
41
51
  /** String units in xl/sharedStrings.xml: one per <si>. */
42
52
  function sharedStringUnits(xmlDoc) {
43
53
  const units = [];
44
- const sis = xmlDoc.getElementsByTagNameNS(SS_NS, 'si');
45
- for (let i = 0; i < sis.length; i++) {
46
- units.push(collectTextLeaves(sis[i]));
54
+ for (const si of elementsByLocalName(xmlDoc, OOXML_NS.ss, 'si')) {
55
+ units.push(collectTextLeaves(si));
47
56
  }
48
57
  return units;
49
58
  }
59
+ /** A cell number that can be an identifier: an integer of seven or more digits. */
60
+ const NUMERIC_IDENTIFIER = /^[+-]?\d{7,}$/;
61
+ /** What SpreadsheetML accepts as a numeric <v>. */
62
+ const NUMERIC_VALUE = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
63
+ /**
64
+ * After replacement, a numeric cell whose <v> no longer holds a number
65
+ * (it now carries a placeholder) would make Excel repair the workbook. Turn
66
+ * it into an inline string cell and drop its formula, which would otherwise
67
+ * recompute the original number (T176).
68
+ */
69
+ function normalizeReplacedNumericCells(xmlDoc) {
70
+ for (const cell of elementsByLocalName(xmlDoc, OOXML_NS.ss, 'c')) {
71
+ const type = cell.getAttribute('t');
72
+ if (type !== null && type !== 'n')
73
+ continue;
74
+ let v = null;
75
+ let f = null;
76
+ for (let c = 0; c < cell.children.length; c++) {
77
+ const child = cell.children[c];
78
+ if (isSpreadsheetEl(child, 'v'))
79
+ v = child;
80
+ else if (isSpreadsheetEl(child, 'f'))
81
+ f = child;
82
+ }
83
+ if (!v)
84
+ continue;
85
+ const text = v.textContent ?? '';
86
+ if (text.length === 0 || NUMERIC_VALUE.test(text))
87
+ continue;
88
+ cell.setAttribute('t', 'inlineStr');
89
+ if (f)
90
+ cell.removeChild(f);
91
+ const is = xmlDoc.createElementNS(cell.namespaceURI, 'is');
92
+ const t = xmlDoc.createElementNS(cell.namespaceURI, 't');
93
+ t.setAttribute('xml:space', 'preserve');
94
+ t.textContent = text;
95
+ is.appendChild(t);
96
+ cell.replaceChild(is, v);
97
+ }
98
+ }
50
99
  const HEADER_FOOTER_NAMES = new Set([
51
100
  'oddHeader', 'oddFooter', 'evenHeader', 'evenFooter', 'firstHeader', 'firstFooter',
52
101
  ]);
@@ -59,7 +108,7 @@ function worksheetUnits(xmlDoc) {
59
108
  const all = xmlDoc.getElementsByTagName('*');
60
109
  for (let i = 0; i < all.length; i++) {
61
110
  const el = all[i];
62
- if (el.namespaceURI !== SS_NS)
111
+ if (!isSpreadsheetNs(el))
63
112
  continue;
64
113
  if (el.localName === 'c') {
65
114
  const type = el.getAttribute('t');
@@ -76,6 +125,19 @@ function worksheetUnits(xmlDoc) {
76
125
  units.push([child]);
77
126
  }
78
127
  }
128
+ else if (type === null || type === 'n') {
129
+ // Numeric cells (T176): a PESEL, phone or account number typed into
130
+ // a cell is stored as a number. Values of at least seven digits are
131
+ // extracted so the detectors see them; a replaced cell is turned
132
+ // into an inline string on write (normalizeReplacedNumericCells).
133
+ // Shorter numbers (amounts, counts, dates as serials) are not
134
+ // identifiers and stay out of the text.
135
+ for (let c = 0; c < el.children.length; c++) {
136
+ const child = el.children[c];
137
+ if (isSpreadsheetEl(child, 'v') && NUMERIC_IDENTIFIER.test(child.textContent ?? ''))
138
+ units.push([child]);
139
+ }
140
+ }
79
141
  }
80
142
  else if (HEADER_FOOTER_NAMES.has(el.localName)) {
81
143
  units.push([el]);
@@ -92,9 +154,8 @@ function commentUnits(xmlDoc) {
92
154
  const units = [];
93
155
  const root = xmlDoc.documentElement;
94
156
  if (root.localName === 'comments') {
95
- const texts = xmlDoc.getElementsByTagNameNS(SS_NS, 'text');
96
- for (let i = 0; i < texts.length; i++) {
97
- const leaves = collectTextLeaves(texts[i]);
157
+ for (const text of elementsByLocalName(xmlDoc, OOXML_NS.ss, 'text')) {
158
+ const leaves = collectTextLeaves(text);
98
159
  if (leaves.length > 0)
99
160
  units.push(leaves);
100
161
  }
@@ -115,16 +176,19 @@ function commentUnits(xmlDoc) {
115
176
  /**
116
177
  * Read a .xlsx file and extract its text content with position mapping.
117
178
  */
118
- export async function readXlsx(file) {
179
+ export async function readXlsx(file, options = {}) {
119
180
  const arrayBuffer = await file.arrayBuffer();
120
181
  const zip = await JSZip.loadAsync(arrayBuffer);
182
+ assertUnpackedSize(zip, options.maxUnpackedBytes ?? MAX_UNPACKED_BYTES);
121
183
  if (!zip.file('xl/workbook.xml')) {
122
184
  throw new Error('Invalid .xlsx file: missing xl/workbook.xml');
123
185
  }
124
186
  // Deterministic part order so load-time and export-time extractions produce
125
- // identical offsets: sharedStrings, then worksheets, then comment parts.
187
+ // identical offsets: sharedStrings, then worksheets, then comment parts,
188
+ // then drawing text boxes and charts (T175: a:p paragraphs, string caches).
126
189
  const worksheetPaths = [];
127
190
  const commentPaths = [];
191
+ const drawingPaths = [];
128
192
  zip.forEach((relativePath) => {
129
193
  if (/^xl\/worksheets\/[^/]+\.xml$/.test(relativePath)) {
130
194
  worksheetPaths.push(relativePath);
@@ -133,20 +197,37 @@ export async function readXlsx(file) {
133
197
  /^xl\/threadedComments\/[^/]+\.xml$/.test(relativePath)) {
134
198
  commentPaths.push(relativePath);
135
199
  }
200
+ else if (XLSX_DRAWING_TEXT_PARTS.test(relativePath)) {
201
+ drawingPaths.push(relativePath);
202
+ }
136
203
  });
137
204
  worksheetPaths.sort();
138
205
  commentPaths.sort();
206
+ drawingPaths.sort();
139
207
  const partPlans = [
140
208
  { path: 'xl/sharedStrings.xml', unitsOf: sharedStringUnits },
141
209
  ...worksheetPaths.map((path) => ({ path, unitsOf: worksheetUnits })),
142
210
  ...commentPaths.map((path) => ({ path, unitsOf: commentUnits })),
211
+ ...drawingPaths.map((path) => ({ path, unitsOf: drawingUnits })),
143
212
  ];
144
213
  let flatText = '';
145
214
  const contentParts = [];
215
+ // Fail closed (H1): worksheets whose sheetData is not SpreadsheetML in a
216
+ // namespace we read (transitional or strict) while foreign elements exist
217
+ // are refused rather than copied through.
218
+ let sheetDataFound = false;
219
+ const foreign = new Set();
146
220
  for (const plan of partPlans) {
147
221
  const xmlDoc = await readXmlPart(zip, plan.path);
148
222
  if (!xmlDoc)
149
223
  continue;
224
+ if (plan.unitsOf === worksheetUnits) {
225
+ if (elementsByLocalName(xmlDoc, OOXML_NS.ss, 'sheetData').length > 0)
226
+ sheetDataFound = true;
227
+ else
228
+ for (const ns of foreignNamespaces(xmlDoc))
229
+ foreign.add(ns);
230
+ }
150
231
  const textNodes = [];
151
232
  for (const unit of plan.unitsOf(xmlDoc)) {
152
233
  const nodes = [];
@@ -172,11 +253,20 @@ export async function readXlsx(file) {
172
253
  }
173
254
  contentParts.push({ path: plan.path, xmlDoc, textNodes });
174
255
  }
256
+ if (worksheetPaths.length > 0 && !sheetDataFound && foreign.size > 0) {
257
+ throw new UnsupportedDocumentError('unrecognized-namespace', `Unsupported .xlsx: no SpreadsheetML sheetData in any worksheet, found elements in ${[...foreign].join(', ')}`, [...foreign]);
258
+ }
259
+ // Package policy (T177): parts the writer would copy verbatim are
260
+ // reported; the writer refuses them until the host passes allowUnredactable.
261
+ const unredactable = await collectUnredactableParts(zip, 'xlsx');
175
262
  return {
176
263
  plainText: flatText,
177
264
  zip,
178
265
  contentParts,
179
266
  textNodes: contentParts.flatMap((part) => part.textNodes),
267
+ empty: flatText.trim().length === 0,
268
+ unredactable,
269
+ warnings: [],
180
270
  };
181
271
  }
182
272
  /**
@@ -259,17 +349,37 @@ async function sanitizeXlsxMetadata(zip, valueReplacements) {
259
349
  * Apply text replacements to the xlsx XML, preserving formatting, then scrub
260
350
  * metadata. Returns a new .xlsx file as a Blob; the input file is untouched.
261
351
  */
262
- export async function writeAnonymizedXlsx(extraction, replacements, valueReplacements = []) {
352
+ export async function writeAnonymizedXlsx(extraction, replacements, valueReplacements = [], options = {}) {
353
+ const result = await writeAnonymizedXlsxWithReport(extraction, replacements, valueReplacements, options);
354
+ return result.blob;
355
+ }
356
+ /**
357
+ * Same as writeAnonymizedXlsx, with the write report. After the content
358
+ * parts and metadata, layer zero (T172) scrubs every known value from every
359
+ * other XML/rels/vml part: pivot cache records, drawings, charts, tables,
360
+ * defined names in the workbook, docProps.
361
+ */
362
+ export async function writeAnonymizedXlsxWithReport(extraction, replacements, valueReplacements = [], options = {}) {
363
+ assertUnredactableAllowed(extraction.unredactable, options.allowUnredactable);
263
364
  applyTextReplacements(extraction.textNodes, replacements);
365
+ const contentPaths = new Set();
264
366
  for (const part of extraction.contentParts) {
367
+ if (/^xl\/worksheets\//.test(part.path))
368
+ normalizeReplacedNumericCells(part.xmlDoc);
265
369
  sanitizeXlsxPart(part.xmlDoc, valueReplacements);
266
370
  writeXmlPart(extraction.zip, part.path, part.xmlDoc);
371
+ contentPaths.add(part.path);
267
372
  }
268
373
  await sanitizeXlsxMetadata(extraction.zip, valueReplacements);
269
- return extraction.zip.generateAsync({
374
+ const scrub = await scrubPackageValues(extraction.zip, valueReplacements, { skip: contentPaths });
375
+ // Container normalisation (T179): fixed entry dates, no comments.
376
+ normalizeZipEntries(extraction.zip);
377
+ const blob = await extraction.zip.generateAsync({
270
378
  type: 'blob',
271
379
  mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
380
+ comment: '',
272
381
  });
382
+ return { blob, warnings: [...scrub.warnings, ...unredactableWarnings(extraction.unredactable)] };
273
383
  }
274
384
  /**
275
385
  * Check if a file is a supported Excel workbook (.xlsx only; the legacy
package/dist/engine.d.ts CHANGED
@@ -28,6 +28,24 @@ export type ProviderFactory = (env: CoreEnv) => DetectionProvider;
28
28
  export interface EngineOptions {
29
29
  /** Override provider construction (tests inject deterministic stubs). */
30
30
  providers?: Partial<Record<ProviderId, ProviderFactory>>;
31
+ /**
32
+ * Whether detect() may download and initialise the model on its own
33
+ * (T185, security report S9). Default true (previous behaviour). With
34
+ * false the engine never touches the network implicitly: detect() before
35
+ * a completed preload() (or switchProvider()) rejects with
36
+ * ModelNotLoadedError, while a preload that is still in flight is awaited.
37
+ * Hosts that gate model downloads behind user consent should set this to
38
+ * false and call preload() from the consent flow.
39
+ */
40
+ autoLoad?: boolean;
41
+ }
42
+ /**
43
+ * detect() was called while the active provider's model is not loaded and
44
+ * createEngine was given `autoLoad: false`. Call engine.preload() first.
45
+ */
46
+ export declare class ModelNotLoadedError extends Error {
47
+ providerId: ProviderId;
48
+ constructor(providerId: ProviderId);
31
49
  }
32
50
  /**
33
51
  * env.kv keys, byte-identical to the localStorage keys the web app used
package/dist/engine.js CHANGED
@@ -36,6 +36,17 @@ export const PROVIDERS = [
36
36
  description: 'Best multilingual accuracy, 24+ EU languages, 35 PII types (~279 MB)',
37
37
  },
38
38
  ];
39
+ /**
40
+ * detect() was called while the active provider's model is not loaded and
41
+ * createEngine was given `autoLoad: false`. Call engine.preload() first.
42
+ */
43
+ export class ModelNotLoadedError extends Error {
44
+ constructor(providerId) {
45
+ super(`Model for provider "${providerId}" is not loaded; call engine.preload() before detect() (createEngine autoLoad is off)`);
46
+ this.name = 'ModelNotLoadedError';
47
+ this.providerId = providerId;
48
+ }
49
+ }
39
50
  // ── Settings ───────────────────────────────────────────────
40
51
  /**
41
52
  * env.kv keys, byte-identical to the localStorage keys the web app used
@@ -95,10 +106,13 @@ export function createEngine(env, initial, options) {
95
106
  bardsai: (e) => new BardsaiProvider(e),
96
107
  ...options?.providers,
97
108
  };
109
+ const autoLoad = options?.autoLoad ?? true;
98
110
  const settings = {
99
111
  providerId: initial?.providerId ?? pickDefaultProvider(env.hardware),
100
112
  threshold: 0,
101
- regexEnabled: initial?.regexEnabled ?? false,
113
+ // Regex rules are on unless the host or a saved setting turns them off
114
+ // (T203): structured identifiers must not depend on the model alone.
115
+ regexEnabled: initial?.regexEnabled ?? true,
102
116
  regexRegion: initial?.regexRegion ?? 'all',
103
117
  customLabels: [...(initial?.customLabels ?? [])],
104
118
  };
@@ -175,14 +189,16 @@ export function createEngine(env, initial, options) {
175
189
  await p.setCustomLabels(settings.customLabels);
176
190
  }
177
191
  }
178
- async function preload() {
179
- await ready;
192
+ function preload() {
193
+ // inflightLoad is assigned synchronously so a detect() issued right
194
+ // after preload() (without awaiting it) sees the load in flight.
180
195
  if (inflightLoad)
181
196
  return inflightLoad;
182
- const p = ensureProvider();
183
- if (p.isLoaded())
184
- return;
185
- inflightLoad = (async () => {
197
+ const run = (async () => {
198
+ await ready;
199
+ const p = ensureProvider();
200
+ if (p.isLoaded())
201
+ return;
186
202
  // A previous attempt may have failed: release() clears the cached load
187
203
  // error so load() can retry (model bytes are served from the blob
188
204
  // cache when available). Mirrors the old worker init path.
@@ -194,12 +210,11 @@ export function createEngine(env, initial, options) {
194
210
  if (supportsCustomLabels(p))
195
211
  settings.customLabels = p.getCustomLabels();
196
212
  })();
197
- try {
198
- await inflightLoad;
199
- }
200
- finally {
201
- inflightLoad = null;
202
- }
213
+ inflightLoad = run;
214
+ return run.finally(() => {
215
+ if (inflightLoad === run)
216
+ inflightLoad = null;
217
+ });
203
218
  }
204
219
  async function switchProvider(id) {
205
220
  await ready;
@@ -254,6 +269,15 @@ export function createEngine(env, initial, options) {
254
269
  if (!text.trim())
255
270
  return [];
256
271
  const p = ensureProvider();
272
+ if (!autoLoad && !p.isLoaded()) {
273
+ // Explicit-preload mode: never download from detect(). A preload that
274
+ // is already running is awaited; otherwise the host must call it.
275
+ if (!inflightLoad)
276
+ throw new ModelNotLoadedError(settings.providerId);
277
+ await inflightLoad;
278
+ if (!p.isLoaded())
279
+ throw new ModelNotLoadedError(settings.providerId);
280
+ }
257
281
  const mlResults = await p.detect(text, (progress) => {
258
282
  onProgress?.(progress);
259
283
  for (const cb of detectionListeners)
package/dist/env.d.ts CHANGED
@@ -31,7 +31,27 @@ export interface CoreEnv {
31
31
  paths: string;
32
32
  numThreads?: number;
33
33
  };
34
- loadTokenizer(hfModelId: string): Promise<unknown>;
34
+ /**
35
+ * Build a tokenizer from the parsed contents of the provider's pinned
36
+ * `tokenizer.json` and `tokenizer_config.json` (T185). Core fetches both
37
+ * files itself through fetchModelBlob (immutable resolve/<commit> URL,
38
+ * SHA-256 + size check, blob cache, resume and retry) and only hands the
39
+ * verified, parsed objects here; the host never contacts the network for
40
+ * tokenizers. With @huggingface/transformers this is
41
+ * `new PreTrainedTokenizer(tokenizerJson, tokenizerConfig)` (or the class
42
+ * named by `tokenizerConfig.tokenizer_class`). May return a promise.
43
+ *
44
+ * Optional in 0.12.0 only so hosts still on loadTokenizer keep compiling;
45
+ * required from 0.13.0.
46
+ */
47
+ buildTokenizer?(tokenizerJson: unknown, tokenizerConfig: unknown): unknown | Promise<unknown>;
48
+ /**
49
+ * @deprecated since 0.12.0, removed in 0.13.0. Used only when
50
+ * buildTokenizer is absent. Loading by Hugging Face model id lets the
51
+ * host library probe mutable `resolve/main` and keep an unverified
52
+ * second cache (security report S1/S3); implement buildTokenizer.
53
+ */
54
+ loadTokenizer?(hfModelId: string): Promise<unknown>;
35
55
  hardware?: HardwareHints;
36
56
  persistStorage?: () => Promise<boolean>;
37
57
  }
package/dist/index.d.ts CHANGED
@@ -4,21 +4,23 @@
4
4
  export type { KVStore, BlobCache, HardwareHints, CoreEnv, MemoryBlobCacheOptions, } from './env.js';
5
5
  export { memoryKV, memoryBlobCache } from './env.js';
6
6
  export declare const CORE_PACKAGE_NAME = "@doccloak/core";
7
- export { fetchModelBlob, evictModelFromCache, retryAsync, ModelIntegrityError, verificationMarkerKey } from './model-loader.js';
8
- export type { ModelLoaderEnv, DownloadProgress, FetchModelOptions, ModelVerification } from './model-loader.js';
7
+ export { fetchModelBlob, evictModelFromCache, retryAsync, ModelIntegrityError, verificationMarkerKey, verificationMarkerValue, parseVerificationMarker, fetchPinnedJson, loadPinnedTokenizer, } from './model-loader.js';
8
+ export type { ModelLoaderEnv, DownloadProgress, FetchModelOptions, ModelVerification, TokenizerFileSpec, TokenizerFiles, PinnedTokenizerOptions, } from './model-loader.js';
9
9
  export { ALL_REGEX_RULES, REGEX_REGIONS } from './regex/index.js';
10
10
  export type { RegexRule, PiiDomain, RegionCode, RegexRegionId } from './regex/index.js';
11
11
  export * from './types.js';
12
12
  export * from './session.js';
13
13
  export * from './doc.js';
14
+ export { UnsupportedDocumentError, isUnsupportedDocumentError } from './dom/errors.js';
15
+ export type { UnsupportedDocumentCode } from './dom/errors.js';
14
16
  export { generateSurrogate, generateUniqueSurrogate, generateSessionSalt, sessionDayOffset, FAKE_EMAIL_DOMAINS, } from './surrogates.js';
15
17
  export type { SurrogateContext } from './surrogates.js';
16
- export { GlinerProvider, GLINER_MODEL_URL, GLINER_MODEL_REVISION, GLINER_MODEL_SHA256 } from './providers/gliner.js';
17
- export { GlinerBaseProvider, GLINER_BASE_MODEL_URL, GLINER_BASE_MODEL_REVISION, GLINER_BASE_MODEL_SHA256, GLINER_BASE_MAX_WIDTH, buildSpanIndices, decodeSpanLogits, } from './providers/gliner-base.js';
18
+ export { GlinerProvider, GLINER_MODEL_URL, GLINER_MODEL_REVISION, GLINER_MODEL_SHA256, GLINER_TOKENIZER_FILES, } from './providers/gliner.js';
19
+ export { GlinerBaseProvider, GLINER_BASE_MODEL_URL, GLINER_BASE_MODEL_REVISION, GLINER_BASE_MODEL_SHA256, GLINER_BASE_TOKENIZER_FILES, GLINER_BASE_MAX_WIDTH, buildSpanIndices, decodeSpanLogits, } from './providers/gliner-base.js';
18
20
  export type { SpanIndices } from './providers/gliner-base.js';
19
- export { BardsaiProvider, BARDSAI_MODEL_URL, BARDSAI_MODEL_REVISION, BARDSAI_MODEL_SHA256 } from './providers/bardsai.js';
21
+ export { BardsaiProvider, BARDSAI_MODEL_URL, BARDSAI_MODEL_REVISION, BARDSAI_MODEL_SHA256, BARDSAI_TOKENIZER_FILES, } from './providers/bardsai.js';
20
22
  export { detectWithRegex, resolveOverlaps, filterFalsePositives, propagateEntities, detectEntities, } from './pipeline.js';
21
- export { createEngine, PROVIDERS, pickDefaultProvider, defaultThresholdFor, clampThreshold, ENGINE_SETTINGS_KEYS, } from './engine.js';
23
+ export { createEngine, ModelNotLoadedError, PROVIDERS, pickDefaultProvider, defaultThresholdFor, clampThreshold, ENGINE_SETTINGS_KEYS, } from './engine.js';
22
24
  export type { ProviderId, ProviderEntry, ProviderFactory, EngineOptions, EngineSettings, DocCloakEngine, } from './engine.js';
23
25
  export { serveEngine, connectEngine } from './worker-protocol.js';
24
26
  export type { PortLike, EngineClient, EngineRequest, EngineResponse, } from './worker-protocol.js';
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  export { memoryKV, memoryBlobCache } from "./env.js";
5
5
  export const CORE_PACKAGE_NAME = '@doccloak/core';
6
6
  // T006: model loader
7
- export { fetchModelBlob, evictModelFromCache, retryAsync, ModelIntegrityError, verificationMarkerKey } from "./model-loader.js";
7
+ export { fetchModelBlob, evictModelFromCache, retryAsync, ModelIntegrityError, verificationMarkerKey, verificationMarkerValue, parseVerificationMarker, fetchPinnedJson, loadPinnedTokenizer, } from "./model-loader.js";
8
8
  // end T006
9
9
  // T004: regex rules (REGEX_REGIONS moved here from the web engine in T009)
10
10
  export { ALL_REGEX_RULES, REGEX_REGIONS } from "./regex/index.js";
@@ -14,22 +14,27 @@ export * from "./types.js";
14
14
  export * from "./session.js";
15
15
  export * from "./doc.js";
16
16
  // end T003
17
+ // T178: readDocText / writeAnonymizedDoc refuse fast-saved and encrypted .doc
18
+ // files with this error (also exported from '@doccloak/core/dom'); re-exported
19
+ // here so hosts using only the main entry can switch on `code`.
20
+ export { UnsupportedDocumentError, isUnsupportedDocumentError } from "./dom/errors.js";
21
+ // end T178
17
22
  // T043: surrogate generator (realistic, shape-preserving replacements)
18
23
  export { generateSurrogate, generateUniqueSurrogate, generateSessionSalt, sessionDayOffset, FAKE_EMAIL_DOMAINS, } from "./surrogates.js";
19
24
  // end T043
20
25
  // T007: gliner provider
21
- export { GlinerProvider, GLINER_MODEL_URL, GLINER_MODEL_REVISION, GLINER_MODEL_SHA256 } from "./providers/gliner.js";
26
+ export { GlinerProvider, GLINER_MODEL_URL, GLINER_MODEL_REVISION, GLINER_MODEL_SHA256, GLINER_TOKENIZER_FILES, } from "./providers/gliner.js";
22
27
  // end T007
23
28
  // T122: gliner base provider (markerV0 span mode)
24
- export { GlinerBaseProvider, GLINER_BASE_MODEL_URL, GLINER_BASE_MODEL_REVISION, GLINER_BASE_MODEL_SHA256, GLINER_BASE_MAX_WIDTH, buildSpanIndices, decodeSpanLogits, } from "./providers/gliner-base.js";
29
+ export { GlinerBaseProvider, GLINER_BASE_MODEL_URL, GLINER_BASE_MODEL_REVISION, GLINER_BASE_MODEL_SHA256, GLINER_BASE_TOKENIZER_FILES, GLINER_BASE_MAX_WIDTH, buildSpanIndices, decodeSpanLogits, } from "./providers/gliner-base.js";
25
30
  // end T122
26
31
  // T008: bardsai provider
27
- export { BardsaiProvider, BARDSAI_MODEL_URL, BARDSAI_MODEL_REVISION, BARDSAI_MODEL_SHA256 } from "./providers/bardsai.js";
32
+ export { BardsaiProvider, BARDSAI_MODEL_URL, BARDSAI_MODEL_REVISION, BARDSAI_MODEL_SHA256, BARDSAI_TOKENIZER_FILES, } from "./providers/bardsai.js";
28
33
  // end T008
29
34
  // T005: pipeline
30
35
  export { detectWithRegex, resolveOverlaps, filterFalsePositives, propagateEntities, detectEntities, } from "./pipeline.js";
31
36
  // end T005
32
37
  // T009: engine assembly + worker protocol
33
- export { createEngine, PROVIDERS, pickDefaultProvider, defaultThresholdFor, clampThreshold, ENGINE_SETTINGS_KEYS, } from "./engine.js";
38
+ export { createEngine, ModelNotLoadedError, PROVIDERS, pickDefaultProvider, defaultThresholdFor, clampThreshold, ENGINE_SETTINGS_KEYS, } from "./engine.js";
34
39
  export { serveEngine, connectEngine } from "./worker-protocol.js";
35
40
  // end T009
@@ -17,13 +17,18 @@
17
17
  * - Verifies downloads against a pinned SHA-256 when the caller provides one
18
18
  * (supply-chain hardening, T116): mismatches evict the cache entry and
19
19
  * reject with ModelIntegrityError; verified cache entries carry a marker
20
- * so they are not re-hashed on every startup.
20
+ * so they are not re-hashed on every startup. The marker records the
21
+ * verified byte size (T185): a cache hit whose blob size no longer matches
22
+ * the marker is re-hashed instead of trusted.
23
+ * - Fetches the pinned tokenizer files (tokenizer.json, tokenizer_config.json)
24
+ * through the same path (loadPinnedTokenizer, T185) so tokenizers get the
25
+ * same resume, retry, cache and integrity treatment as the model.
21
26
  *
22
27
  * Environment-agnostic: all host capabilities (blob cache, fetch, persistent
23
28
  * storage request) are injected via ModelLoaderEnv. This module never touches
24
29
  * caches, navigator or global fetch directly.
25
30
  */
26
- import type { BlobCache } from './env.js';
31
+ import type { BlobCache, CoreEnv } from './env.js';
27
32
  export type DownloadProgress = (downloaded: number, total: number) => void;
28
33
  /**
29
34
  * Host capabilities the loader needs. Hosts pass the relevant slice of their
@@ -64,6 +69,19 @@ export interface ModelVerification {
64
69
  * written for the previous pin.
65
70
  */
66
71
  export declare function verificationMarkerKey(url: string, sha256: string): string;
72
+ /**
73
+ * Value stored in a verification marker: `${sha256}:${size}`, binding the
74
+ * marker to the byte size of the blob that was actually hashed (T185, S4).
75
+ * A cache hit is only trusted without re-hashing when the cached blob's
76
+ * size equals the recorded one. Markers written by 0.11.0 hold the single
77
+ * byte '1' (size unknown): they are upgraded by hashing once and rewriting.
78
+ */
79
+ export declare function verificationMarkerValue(sha256: string, size: number): string;
80
+ /**
81
+ * Size recorded in a marker value for the given hash, or null when the
82
+ * marker is the legacy 0.11.0 '1' or otherwise unreadable (size unknown).
83
+ */
84
+ export declare function parseVerificationMarker(value: string, sha256: string): number | null;
67
85
  export interface FetchModelOptions {
68
86
  maxAttempts?: number;
69
87
  retryBaseDelayMs?: number;
@@ -76,6 +94,13 @@ export interface FetchModelOptions {
76
94
  * marked verified or evicted and re-downloaded.
77
95
  */
78
96
  sha256?: string;
97
+ /**
98
+ * Pinned byte size of the file. With sha256 it is cross-checked against
99
+ * every download (a size mismatch is an integrity failure like a hash
100
+ * mismatch). It also serves as the progress total when the server sends
101
+ * no content-length. Without sha256 it is only a progress hint.
102
+ */
103
+ size?: number;
79
104
  /** Fired once the served blob is known to match the pinned sha256. */
80
105
  onVerified?: (verification: ModelVerification) => void;
81
106
  }
@@ -90,9 +115,39 @@ export declare function fetchModelBlob(env: ModelLoaderEnv, url: string, onProgr
90
115
  */
91
116
  export declare function evictModelFromCache(env: ModelLoaderEnv, url: string, sha256?: string): Promise<void>;
92
117
  /**
93
- * Retry an async operation with exponential backoff. Used for the smaller
94
- * companion downloads (tokenizer files) that go through libraries without
95
- * their own retry handling - one dropped request on a flaky connection
96
- * should not abort the whole model load.
118
+ * Retry an async operation with exponential backoff. Kept for the
119
+ * deprecated CoreEnv.loadTokenizer path (a host library without its own
120
+ * retry handling) and for hosts composing their own downloads; the pinned
121
+ * tokenizer files now go through fetchModelBlob via loadPinnedTokenizer.
97
122
  */
98
123
  export declare function retryAsync<T>(operation: () => Promise<T>, label: string, options?: FetchModelOptions): Promise<T>;
124
+ /** One pinned companion file: immutable resolve/<commit> URL, SHA-256 and byte size. */
125
+ export interface TokenizerFileSpec {
126
+ url: string;
127
+ /** Lowercase hex SHA-256 of the file at the pinned commit. */
128
+ sha256: string;
129
+ /** Byte size of the file at the pinned commit. */
130
+ size: number;
131
+ }
132
+ /** The two files a provider pins for its tokenizer: tokenizer.json, then tokenizer_config.json. */
133
+ export type TokenizerFiles = readonly [tokenizerJson: TokenizerFileSpec, tokenizerConfig: TokenizerFileSpec];
134
+ export type PinnedTokenizerOptions = Pick<FetchModelOptions, 'maxAttempts' | 'retryBaseDelayMs' | 'onVerified'>;
135
+ /**
136
+ * Download (or serve from cache) one pinned JSON file through fetchModelBlob
137
+ * with its sha256 and size, then parse it. Integrity failures surface as
138
+ * ModelIntegrityError with the entry evicted, like the model itself.
139
+ */
140
+ export declare function fetchPinnedJson(env: ModelLoaderEnv, file: TokenizerFileSpec, options?: PinnedTokenizerOptions): Promise<unknown>;
141
+ /**
142
+ * Resolve a provider's tokenizer through the host env.
143
+ *
144
+ * Preferred path: fetch both pinned files via fetchPinnedJson (resume,
145
+ * retry, blob cache, verification marker, ModelIntegrityError) and hand the
146
+ * parsed objects to CoreEnv.buildTokenizer. No library probes mutable
147
+ * resolve/main, no second cache bucket, and a warm cache works offline.
148
+ *
149
+ * Fallback (deprecated, removed in 0.13.0): hosts that only implement
150
+ * CoreEnv.loadTokenizer(hfModelId) are still served through it with the
151
+ * previous retry wrapper.
152
+ */
153
+ export declare function loadPinnedTokenizer(env: CoreEnv, files: TokenizerFiles, legacyHfModelId: string, options?: PinnedTokenizerOptions): Promise<unknown>;