@stll/anonymize 2.8.2 → 2.9.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.
@@ -0,0 +1,429 @@
1
+ import { a as DetectionSource, n as DETECTION_SOURCES, p as OperatorType } from "./constants2.mjs";
2
+ //#region src/types.d.ts
3
+ /**
4
+ * Fields shared by every entity span in the source text.
5
+ */
6
+ type EntityBase = {
7
+ start: number;
8
+ end: number;
9
+ label: string;
10
+ text: string;
11
+ score: number;
12
+ sourceDetail?: "custom-deny-list" | "custom-regex" | "gazetteer-extension";
13
+ };
14
+ /**
15
+ * A PII entity span found by a primary detection layer
16
+ * (regex, NER, legal forms, deny list, ...).
17
+ */
18
+ type DetectedEntity = EntityBase & {
19
+ source: Exclude<DetectionSource, typeof DETECTION_SOURCES.COREFERENCE>;
20
+ };
21
+ /**
22
+ * An alias mention of a previously detected entity: a
23
+ * defined term ("the Seller") or a propagated bare
24
+ * mention ("Acme" after "Acme Corp.").
25
+ *
26
+ * `corefSourceText` is required by construction, so an
27
+ * alias cannot exist without the link back to its source
28
+ * entity. Placeholder numbering reads it to give the
29
+ * alias the same placeholder as the source. The link
30
+ * travels with the entity instead of living in a
31
+ * side-channel map that a producer could forget to
32
+ * write — or that a later pass could clear.
33
+ */
34
+ type CorefAliasEntity = EntityBase & {
35
+ source: typeof DETECTION_SOURCES.COREFERENCE;
36
+ /** Full text of the source entity this alias refers to. */
37
+ corefSourceText: string;
38
+ };
39
+ /**
40
+ * A detected PII entity span in the source text.
41
+ * Every detection layer produces these.
42
+ */
43
+ type Entity = DetectedEntity | CorefAliasEntity;
44
+ /**
45
+ * Entity after human review. Extends the base Entity
46
+ * with a review decision.
47
+ */
48
+ type ReviewDecision = "confirmed" | "rejected" | "relabeled";
49
+ type ReviewedEntity = Entity & {
50
+ decision?: ReviewDecision;
51
+ originalLabel?: string;
52
+ };
53
+ /**
54
+ * A single entry in the workspace-scoped gazetteer
55
+ * (deny list). Persisted in IndexedDB.
56
+ */
57
+ type GazetteerEntry = {
58
+ id: string;
59
+ canonical: string;
60
+ label: string;
61
+ variants: string[];
62
+ workspaceId: string;
63
+ createdAt: number;
64
+ source: "manual" | "confirmed-from-model";
65
+ };
66
+ /** Extraction strategy — closed discriminated union. */
67
+ type TriggerStrategy = {
68
+ type: "to-next-comma";
69
+ /**
70
+ * Optional list of lowercase keywords that terminate
71
+ * the value scan, in addition to commas/newlines. Useful
72
+ * for triggers like court names that may continue past
73
+ * a missing comma into adjacent clause text ("Městským
74
+ * soudem v Praze dne 1. 1. 2020"); listing `"dne"` here
75
+ * stops the scan at the date boundary. Matched on a
76
+ * word-boundary, case-insensitive.
77
+ */
78
+ stopWords?: string[];
79
+ /**
80
+ * Hard cap on the captured span length, in characters,
81
+ * regardless of where the next comma / stop char sits.
82
+ * Use for triggers that label short formulaic phrases
83
+ * ("State of Delaware") and must not absorb the rest
84
+ * of a long forum-selection clause when the comma is
85
+ * sentences away. Falls back to the default 100-char
86
+ * fallback when omitted.
87
+ */
88
+ maxLength?: number;
89
+ } | {
90
+ type: "to-end-of-line";
91
+ } | {
92
+ type: "n-words";
93
+ count: number;
94
+ } | {
95
+ type: "company-id-value";
96
+ } | {
97
+ type: "address";
98
+ maxChars?: number;
99
+ } | {
100
+ /**
101
+ * Extract the first regex match in the value text.
102
+ * Useful for shape-bounded values that follow a
103
+ * label on the same line as other fields, where
104
+ * `to-end-of-line` would over-capture. The pattern
105
+ * is anchored to the start of the (already
106
+ * leading-whitespace-stripped) value, so use
107
+ * `(?:.*?)` prefix only when intentional.
108
+ */
109
+ type: "match-pattern";
110
+ pattern: string;
111
+ flags?: string;
112
+ };
113
+ /** Validation rules — closed discriminated union. */
114
+ type TriggerValidation = {
115
+ type: "starts-uppercase";
116
+ } | {
117
+ type: "min-length";
118
+ min: number;
119
+ } | {
120
+ type: "max-length";
121
+ max: number;
122
+ } | {
123
+ type: "no-digits";
124
+ } | {
125
+ type: "has-digits";
126
+ } | {
127
+ type: "matches-pattern";
128
+ pattern: string;
129
+ flags?: string;
130
+ } |
131
+ /**
132
+ * Run a named stdnum validator (checksum + length)
133
+ * against the captured value. Keeps the trigger
134
+ * path symmetrical with the formatted-regex
135
+ * detectors so e.g. `CPF nº 00000000000` does not
136
+ * survive as a tax-ID entity.
137
+ */
138
+ {
139
+ type: "valid-id";
140
+ validator: ValidIdValidator;
141
+ };
142
+ /** Built-in stdnum validators that can be referenced
143
+ * by `valid-id` validations. */
144
+ type ValidIdValidator = "br.cpf" | "br.cnpj" | "us.rtn";
145
+ /** Auto-generated trigger variants — closed set. */
146
+ type TriggerExtension = "add-colon" | "add-trailing-space" | "add-colon-space" | "normalize-spaces";
147
+ /** V2 trigger config entry (JSON shape). */
148
+ type TriggerGroupConfig = {
149
+ id?: string;
150
+ triggers: string[];
151
+ label: string;
152
+ strategy: TriggerStrategy;
153
+ extensions?: TriggerExtension[];
154
+ validations?: TriggerValidation[];
155
+ /** When true, include the trigger text in the
156
+ * entity span (e.g., court names). */
157
+ includeTrigger?: boolean;
158
+ };
159
+ /** Compiled validation with pre-built regex. */
160
+ type CompiledValidation = {
161
+ type: "starts-uppercase";
162
+ re: RegExp;
163
+ } | {
164
+ type: "min-length";
165
+ min: number;
166
+ } | {
167
+ type: "max-length";
168
+ max: number;
169
+ } | {
170
+ type: "no-digits";
171
+ re: RegExp;
172
+ } | {
173
+ type: "has-digits";
174
+ re: RegExp;
175
+ } | {
176
+ type: "matches-pattern";
177
+ re: RegExp;
178
+ } | {
179
+ type: "valid-id";
180
+ validator: ValidIdValidator;
181
+ check: (value: string) => boolean;
182
+ };
183
+ /**
184
+ * Runtime rule — one per trigger string after
185
+ * expansion. Fed to the Aho-Corasick automaton.
186
+ */
187
+ type TriggerRule = {
188
+ trigger: string;
189
+ label: string;
190
+ strategy: TriggerStrategy;
191
+ validations: CompiledValidation[];
192
+ includeTrigger: boolean;
193
+ };
194
+ /** Per-label operator selection. Key is the entity label. */
195
+ type MaskDirection = "start" | "end";
196
+ type MaskOperatorConfig = {
197
+ type: "mask";
198
+ maskingCharacter: string;
199
+ charactersToMask: number;
200
+ direction: MaskDirection;
201
+ };
202
+ type OperatorSelection = Exclude<OperatorType, "mask"> | MaskOperatorConfig;
203
+ type OperatorConfig = {
204
+ /** Operator per label. Missing labels default to "replace". */
205
+ operators: Record<string, OperatorSelection>;
206
+ /** Custom replacement string for the redact operator. */
207
+ redactString: string;
208
+ };
209
+ /** Whether an operator produces a reversible redaction entry. */
210
+ type OperatorReversibility = "reversible" | "irreversible" | "preserving";
211
+ type AnonymisationOperator = {
212
+ type: OperatorType;
213
+ reversibility: OperatorReversibility;
214
+ /**
215
+ * Apply the operator to a single entity occurrence.
216
+ * Returns the replacement string to embed in the document.
217
+ */
218
+ apply: (text: string, label: string, placeholder: string, redactString: string, selection: OperatorSelection) => string;
219
+ };
220
+ /**
221
+ * Redacted document output with stable entity mapping.
222
+ */
223
+ type RedactionResult = {
224
+ redactedText: string;
225
+ /**
226
+ * Maps placeholder to original text. Only populated for
227
+ * reversible operators (replace). Empty for redact, keep, and mask.
228
+ */
229
+ redactionMap: Map<string, string>;
230
+ /** Maps placeholder to the operator that produced it. */
231
+ operatorMap: Map<string, OperatorType>;
232
+ entityCount: number;
233
+ };
234
+ /**
235
+ * Configuration for the detection pipeline.
236
+ */
237
+ type DenyListCategory = "Names" | "Places" | "Addresses" | "Courts" | "Financial" | "Government" | "Healthcare" | "Education" | "Political" | "Organizations" | "International";
238
+ /**
239
+ * Metadata for a single dictionary entry in the
240
+ * deny-list system. Mirrors the shape from
241
+ * the anonymize-data package so consumers can pass
242
+ * pre-loaded data without a runtime dependency.
243
+ */
244
+ type DictionaryMeta = {
245
+ label: string;
246
+ category: DenyListCategory;
247
+ country: string | null;
248
+ };
249
+ /**
250
+ * Caller-supplied exact terms for deny-list matching.
251
+ * These entries are merged with the published deny-list
252
+ * dictionaries when `enableDenyList` is enabled.
253
+ */
254
+ type CustomDenyListEntry = {
255
+ value: string;
256
+ label: string;
257
+ variants?: readonly string[];
258
+ };
259
+ /**
260
+ * Caller-supplied regex detector. The pattern is passed
261
+ * to the native Rust regex engine, so use its supported
262
+ * regex syntax. Inline flags such as `(?i)` are accepted
263
+ * when supported by that engine.
264
+ */
265
+ type CustomRegexPattern = {
266
+ pattern: string;
267
+ label: string;
268
+ score?: number;
269
+ preparedArtifactPolicy?: "include" | "omit";
270
+ };
271
+ /**
272
+ * Pre-loaded dictionary data for dependency injection.
273
+ * Consumers that want name/city/deny-list detection
274
+ * load dictionaries themselves (e.g. from the
275
+ * anonymize-data package) and pass them here; the
276
+ * anonymize package has zero cross-package imports.
277
+ *
278
+ * All fields are optional. When a field is absent,
279
+ * the corresponding detection path is skipped (same
280
+ * behavior as when no dictionaries are available).
281
+ */
282
+ type Dictionaries = {
283
+ /**
284
+ * First names per language code (e.g., "cs", "de").
285
+ */
286
+ firstNames?: Readonly<Record<string, readonly string[]>>;
287
+ /**
288
+ * Surnames per language code.
289
+ */
290
+ surnames?: Readonly<Record<string, readonly string[]>>;
291
+ /**
292
+ * Non-Western name tokens per locale code
293
+ * (e.g., "in", "ar", "ja-latn", "ko", "zh-latn",
294
+ * "th", "vi", "fil", "id"). Merged with bundled
295
+ * names-nw-*.json data at init time.
296
+ */
297
+ nonWesternNames?: Readonly<Record<string, readonly string[]>>;
298
+ /**
299
+ * Pre-loaded deny-list dictionaries keyed by
300
+ * dictionary ID (e.g., "courts/CZ", "banks/DE").
301
+ * Each value is the array of terms for that
302
+ * dictionary.
303
+ */
304
+ denyList?: Readonly<Record<string, readonly string[]>>;
305
+ /**
306
+ * Metadata per dictionary ID. Required when
307
+ * `denyList` is provided so the pipeline knows
308
+ * labels, categories, and country filters.
309
+ */
310
+ denyListMeta?: Readonly<Record<string, DictionaryMeta>>;
311
+ /**
312
+ * Pre-loaded city names, already merged across
313
+ * all desired countries.
314
+ *
315
+ * Prefer `citiesByCountry` when callers also pass
316
+ * `denyListCountries` / `denyListRegions`; merged
317
+ * city arrays cannot be scoped after injection.
318
+ */
319
+ cities?: readonly string[];
320
+ /**
321
+ * Pre-loaded city names keyed by ISO 3166-1 alpha-2
322
+ * country code. When provided, the deny-list builder
323
+ * applies `denyListCountries` / `denyListRegions`
324
+ * before adding city patterns to the search automaton.
325
+ */
326
+ citiesByCountry?: Readonly<Record<string, readonly string[]>>;
327
+ };
328
+ /**
329
+ * Street-address detection without a known-city anchor.
330
+ */
331
+ type StandaloneStreetDetection = "off" | "houseNumberAnchored";
332
+ type PipelineConfig = {
333
+ threshold: number;
334
+ enableTriggerPhrases: boolean;
335
+ enableRegex: boolean;
336
+ /**
337
+ * Expected content language codes. When present, these
338
+ * derive default dictionary scopes for name corpus and
339
+ * deny-list matching unless the lower-level scope fields
340
+ * below are set explicitly.
341
+ */
342
+ languages?: string[];
343
+ /**
344
+ * Convenience form for single-language documents. Ignored
345
+ * when `languages` is also provided.
346
+ */
347
+ language?: string;
348
+ /**
349
+ * Enables legal-form organization detection.
350
+ * Required for typed callers; legacy untyped
351
+ * callers that omit this field are treated as
352
+ * enabled at runtime for backward compatibility.
353
+ */
354
+ enableLegalForms: boolean;
355
+ /**
356
+ * Enables first-name/surname/title corpus matching.
357
+ * When deny-list mode is enabled, this also controls
358
+ * whether name-corpus entries are injected into the
359
+ * deny-list search automaton.
360
+ */
361
+ enableNameCorpus: boolean;
362
+ /**
363
+ * Optional language scope for first-name/surname
364
+ * dictionaries, using the keys present in
365
+ * `dictionaries.firstNames` / `dictionaries.surnames`
366
+ * (for example `["en", "de"]`). When omitted, all
367
+ * injected name languages are used for backward
368
+ * compatibility.
369
+ */
370
+ nameCorpusLanguages?: string[];
371
+ enableDenyList: boolean;
372
+ denyListCountries?: string[];
373
+ denyListRegions?: string[];
374
+ denyListExcludeCategories?: string[];
375
+ /**
376
+ * Caller-owned exact terms to match through the
377
+ * deny-list layer. Requires `enableDenyList: true`.
378
+ */
379
+ customDenyList?: readonly CustomDenyListEntry[];
380
+ /**
381
+ * Caller-owned regex detectors. Requires
382
+ * `enableRegex: true`.
383
+ */
384
+ customRegexes?: readonly CustomRegexPattern[];
385
+ enableGazetteer: boolean;
386
+ /**
387
+ * Detect country names (ISO 3166-1 names, curated
388
+ * aliases, alpha-3 codes). Defaults to true. Names
389
+ * span all manifest languages plus widely-used
390
+ * additions (Dutch, Russian, Chinese, Arabic, etc.).
391
+ */
392
+ enableCountries?: boolean;
393
+ enableConfidenceBoost: boolean;
394
+ enableCoreference: boolean;
395
+ enableZoneClassification?: boolean;
396
+ enableHotwordRules?: boolean;
397
+ /**
398
+ * Detect a street address that carries no known-city
399
+ * anchor. Defaults to `"off"`.
400
+ *
401
+ * `"houseNumberAnchored"` accepts a street-type word
402
+ * with a house number directly beside it, in either
403
+ * order ("14 Rue de la Paix", "Hauptstraße 5",
404
+ * "123 Main Street"). A bare street name with no
405
+ * number never fires.
406
+ *
407
+ * A street-type word plus a nearby number is a much
408
+ * weaker signal than a city-anchored address and does
409
+ * fire on contract prose ("District Court 2019"), so
410
+ * this stays opt-in per workspace.
411
+ */
412
+ standaloneStreetDetection?: StandaloneStreetDetection;
413
+ /**
414
+ * Requested output labels. An empty array means
415
+ * "do not filter by label" for deterministic detectors.
416
+ */
417
+ labels: string[];
418
+ workspaceId: string;
419
+ /**
420
+ * Pre-loaded dictionary data for name, deny-list,
421
+ * and city detection. When omitted, dictionary-based
422
+ * detection paths are skipped. Consumers load from
423
+ * the anonymize-data package and pass the data here.
424
+ */
425
+ dictionaries?: Dictionaries;
426
+ };
427
+ //#endregion
428
+ export { TriggerRule as _, Dictionaries as a, GazetteerEntry as c, PipelineConfig as d, RedactionResult as f, TriggerGroupConfig as g, TriggerExtension as h, DenyListCategory as i, OperatorConfig as l, ReviewedEntity as m, CustomDenyListEntry as n, DictionaryMeta as o, ReviewDecision as p, CustomRegexPattern as r, Entity as s, AnonymisationOperator as t, OperatorSelection as u, TriggerStrategy as v, TriggerValidation as y };
429
+ //# sourceMappingURL=types.d.mts.map
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/anonymize",
3
- "version": "2.8.2",
3
+ "version": "2.9.0",
4
4
  "description": "Deterministic PII detection and anonymization with regex, deny lists, and coreference resolution",
5
5
  "keywords": [
6
6
  "anonymization",
@@ -104,24 +104,18 @@
104
104
  "smoke:wasm-browser": "node scripts/smoke-wasm-browser.mjs",
105
105
  "format": "oxfmt ."
106
106
  },
107
- "peerDependencies": {
107
+ "dependencies": {
108
108
  "@stll/anonymize-data": "^0.0.10"
109
109
  },
110
- "peerDependenciesMeta": {
111
- "@stll/anonymize-data": {
112
- "optional": true
113
- }
114
- },
115
110
  "optionalDependencies": {
116
- "@stll/anonymize-darwin-arm64": "2.8.2",
117
- "@stll/anonymize-darwin-x64": "2.8.2",
118
- "@stll/anonymize-linux-arm64-gnu": "2.8.2",
119
- "@stll/anonymize-linux-x64-gnu": "2.8.2",
120
- "@stll/anonymize-win32-x64-msvc": "2.8.2"
111
+ "@stll/anonymize-darwin-arm64": "2.9.0",
112
+ "@stll/anonymize-darwin-x64": "2.9.0",
113
+ "@stll/anonymize-linux-arm64-gnu": "2.9.0",
114
+ "@stll/anonymize-linux-x64-gnu": "2.9.0",
115
+ "@stll/anonymize-win32-x64-msvc": "2.9.0"
121
116
  },
122
117
  "devDependencies": {
123
118
  "@napi-rs/cli": "^3.8.6",
124
- "@stll/anonymize-data": "workspace:*",
125
119
  "bun-types": "1.4.0",
126
120
  "fast-check": "^4.9.0",
127
121
  "fflate": "^0.8.3",
@@ -6,7 +6,12 @@ import { pathToFileURL } from "node:url";
6
6
  import {
7
7
  DEFAULT_NATIVE_PIPELINE_CONFIG,
8
8
  prepareNativePipelinePackage,
9
+ SUPPORTED_LANGUAGES,
9
10
  } from "../dist/index.mjs";
11
+ import {
12
+ applyPipelineLanguageScope,
13
+ defaultDictionaryBundleOptions,
14
+ } from "../dist/build-native-package.mjs";
10
15
  import { loadNativeAnonymizeBinding } from "../dist/native-node.mjs";
11
16
 
12
17
  const args = parseArgs(process.argv.slice(2));
@@ -100,20 +105,20 @@ function requiredValue(values, index, option) {
100
105
 
101
106
  async function loadPackageInput(options) {
102
107
  const input = await loadBasePackageInput(options);
108
+ const scopedConfig = applyPipelineLanguageScope(
109
+ applyCliLanguageScope(input.config, options),
110
+ );
103
111
  const withDictionaries =
104
- !options.defaultDictionaries || input.config.dictionaries !== undefined
105
- ? input
112
+ !options.defaultDictionaries || scopedConfig.dictionaries !== undefined
113
+ ? { ...input, config: scopedConfig }
106
114
  : {
107
115
  ...input,
108
116
  config: {
109
- ...input.config,
110
- dictionaries: await loadDefaultDictionaries(),
117
+ ...scopedConfig,
118
+ dictionaries: await loadDefaultDictionaries(scopedConfig),
111
119
  },
112
120
  };
113
- return {
114
- ...withDictionaries,
115
- config: applyCliLanguageScope(withDictionaries.config, options),
116
- };
121
+ return withDictionaries;
117
122
  }
118
123
 
119
124
  async function loadBasePackageInput(options) {
@@ -171,6 +176,11 @@ function normalizeLanguageOption(value, option) {
171
176
  if (language.length === 0) {
172
177
  throw new Error(`${option} requires a non-empty language code`);
173
178
  }
179
+ if (!SUPPORTED_LANGUAGES.includes(language)) {
180
+ throw new Error(
181
+ `Unsupported pipeline language ${JSON.stringify(value)}; expected one of: ${SUPPORTED_LANGUAGES.join(", ")}`,
182
+ );
183
+ }
174
184
  return language;
175
185
  }
176
186
 
@@ -185,7 +195,7 @@ function normalizeLanguageList(value) {
185
195
  return languages;
186
196
  }
187
197
 
188
- async function loadDefaultDictionaries() {
198
+ async function loadDefaultDictionaries(pipelineConfig) {
189
199
  let loaded;
190
200
  try {
191
201
  // The bundle loads city dictionaries, so it ships in the data package's
@@ -203,7 +213,9 @@ async function loadDefaultDictionaries() {
203
213
  "@stll/anonymize-data/cities does not export loadDictionaryBundle",
204
214
  );
205
215
  }
206
- return loaded.loadDictionaryBundle();
216
+ return loaded.loadDictionaryBundle(
217
+ defaultDictionaryBundleOptions(pipelineConfig),
218
+ );
207
219
  }
208
220
 
209
221
  function formatError(error) {