@link-foundation/sensitive-data-sanitizer 0.17.1 → 0.19.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 54c8095: Add synchronous custom detectors, rule/type controls, configurable document vocabulary, structured field selectors, verified callback transforms and reusable compiled known values. Expand document coverage and add opt-in strict stored-log rules plus a separately loaded offline Russian/English recognizer.
8
+
9
+ ## 0.18.0
10
+
11
+ ### Minor Changes
12
+
13
+ - f0016ea: Add opt-in structural proper-name redaction with per-string activation and
14
+ configurable Unicode scripts. Broaden dated document employers, group and school
15
+ cues, comma-separated mobiles and short share links; preserve operational paths
16
+ and technology controls. Normalize caller-sensitive fields like built-in names.
17
+
3
18
  ## 0.17.1
4
19
 
5
20
  ### Patch Changes
package/README.md CHANGED
@@ -20,8 +20,33 @@ two-pass publication:
20
20
  sensitive-data-sanitizer redact session.jsonl --stream --jsonl --two-pass --output safe.jsonl
21
21
  ```
22
22
 
23
+ For synchronous log hooks, `createSyncSanitizer` supports custom detectors and
24
+ verified callback transformations. Rule/type controls, caller vocabulary,
25
+ structured field selectors and `compileKnown` configure reusable policies; see
26
+ [API examples](docs/API.md#rule-type-and-field-policy) and the
27
+ [runnable log policy](examples/custom-log-policy.mjs). The optional `/ner` entry
28
+ adds offline Russian/English symbolic recognition with
29
+ [published coverage and cost](docs/COVERAGE.md#optional-offline-recognition-measurements).
30
+
23
31
  ## Quick Start
24
32
 
33
+ For stored prose that can tolerate extra redaction, opt into structural proper
34
+ names independently for each string value:
35
+
36
+ ```js
37
+ sanitize('Работал в Ромашке менеджером по продажам.', {
38
+ profile: 'documents',
39
+ properNames: 'capitalized',
40
+ }).text;
41
+ // [REDACTED] в [REDACTED] менеджером по продажам.
42
+ ```
43
+
44
+ The rule defaults to Cyrillic or structural data triggers, keeps JSON keys and
45
+ numbers, and supports caller-selected Unicode `scripts`. It is off by default.
46
+ See [activation rules and limits](docs/API.md#structural-prose-redaction) and the
47
+ [prose-log example](examples/prose-logs.mjs). CLI:
48
+ `redact input.jsonl --jsonl --proper-names capitalized --profile documents`.
49
+
25
50
  ### Readable placeholders and document profiles
26
51
 
27
52
  Use the opt-in `documents` profile for CVs and similar prose. It adds bounded
@@ -60,6 +60,8 @@ history rewrite SOURCE --output DIRECTORY [--apply]
60
60
  --jsonl Sanitize JSONL records; combine with --stream for sessions
61
61
  --identity-mask First/last 2 identity mask (publication profile default)
62
62
  --profile NAME publication (redact default) or documents (CV/entity rules)
63
+ --proper-names MODE Structural prose rule: capitalized (off by default)
64
+ --scripts LIST Comma-separated Unicode Script names (default Cyrillic)
63
65
  --placeholder Typed numbered placeholders; requires --map or --no-map
64
66
  --map PATH Private JSON/.lino restore map; reuse it across files/runs
65
67
  --no-map Discard placeholder mapping after full anonymization
@@ -122,6 +124,8 @@ function parseArgs(argv) {
122
124
  ['--worker-timeout-ms', 'workerTimeoutMs'],
123
125
  ['--filter-repo', 'filterRepo'],
124
126
  ['--profile', 'profile'],
127
+ ['--proper-names', 'properNames'],
128
+ ['--scripts', 'scripts'],
125
129
  ['--map', 'map'],
126
130
  ['--map-format', 'mapFormat'],
127
131
  ['--placeholder-format', 'placeholderFormat'],
@@ -341,6 +345,10 @@ async function loadOptions(config) {
341
345
 
342
346
  async function featureOptions(config, options) {
343
347
  await localCredentials(config, options);
348
+ options.properNames = config.properNames ?? options.properNames;
349
+ if (config.scripts !== undefined) {
350
+ options.scripts = config.scripts.split(',');
351
+ }
344
352
  if (config.knownOnly) {
345
353
  options.knownOnly = true;
346
354
  }
package/docs/API.md CHANGED
@@ -10,9 +10,11 @@ Import the ESM root entry; complete declarations are in `src/index.d.ts`. Inputs
10
10
  | `engine.inspect(text)` | Detect, validate, deduplicate, apply public policy, and return metadata |
11
11
  | `engine.sanitize(text)` | Detect, union overlapping spans, mask completely, verify every engine, return `{text, findings, redactions}` |
12
12
  | `engine.sanitizeJsonl(text)` | Batched structured JSONL sanitization using the same options and required engines |
13
- | `inspect(text, options)` | Synchronous native detection and caller findings |
14
- | `sanitize(text, options)` | Synchronous native redaction and native residual verification |
13
+ | `inspect(text, options)` | Synchronous native + array-returning detectors and caller findings |
14
+ | `sanitize(text, options)` | Synchronous native + array-returning detectors and residual verification |
15
15
  | `redact(text, findings, options)` | Apply validated ranges without automatic detection/verification |
16
+ | `createSyncSanitizer(options)` | Reusable synchronous `inspect` / `sanitize` with the same policies |
17
+ | `compileKnown(...lists)` | Copy, freeze, merge and precompile caller-known personal values |
16
18
  | `auditGitHistory(repository, options)` | Read-only audit of local reachable text objects |
17
19
 
18
20
  `redactions` counts merged replacement spans. `findings` can contain several detector hits for one replacement. Findings have `{start, end, type, category, rule, line, column}`; no text, quote, match, entropy score, provider details, or raw diagnostic is copied. Debugging is off by default; `debug(event)` receives value-free counts, rule timings, residual rounds and registry event metadata (see `DebugEvent` in the TypeScript declarations).
@@ -140,7 +142,9 @@ and a shared map; such conflicting occurrence metadata is refused. Choose
140
142
  separate maps for those documents if exact independent restoration is needed.
141
143
 
142
144
  JSON/JSONL keys and values participate equally in placeholder mode, including
143
- ordinary identifier keys and declared structural fields. Parseability and
145
+ ordinary identifier keys and declared structural fields. With `properNames`,
146
+ JSON keys remain unchanged by automatic personal detection in all transformation
147
+ modes. Explicit known values and credentials retain priority. Parseability and
144
148
  duplicate-key checks remain enforced. Plain redact/fake policies retain their
145
149
  existing metadata/key behavior. Use `structured` for valid structured input.
146
150
  The normal encoding limits and credential redaction policy continue to apply.
@@ -154,36 +158,45 @@ derived model answer. Input, output and map paths must be separate. Maps are
154
158
  saved separately before stdout publication. `--profile documents` enables the
155
159
  new document recognizers; `documentEntities: false` turns them off explicitly.
156
160
 
157
- | Option | Default / meaning |
158
- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
159
- | `documentEntities` | Off; bounded organization, education and location rules; enabled by the documents profile |
160
- | `profile` | Off; `'publication'` sets a 0.3 threshold and identity masking; `'documents'` enables document entities |
161
- | `knownOnly` | `false`; disable all automatic detection (including credentials, Secretlint and custom detectors); use only explicit known values and reused map identities |
162
- | `knownSecrets` | `[]`; exact literal credential values, regardless of length or format |
163
- | `knownPersonal` | `[]`; `{type, value}` values with whole-word Unicode case-insensitive matching and bounded Russian forms; list parts to match them alone |
164
- | `publicEntities` | `[]`; exact PERSON/ORGANIZATION/EDUCATION/LOCATION/EMAIL exception with HTTPS evidence and review date |
165
- | `findings` | `[]`; validated detections for the original input, supplied by the caller |
166
- | `detectors` | `[]`; additional required callbacks in the asynchronous engine |
167
- | `secretlint` | `true` in the asynchronous engine |
168
- | `decode` | `true`; bounded base64/base64url, hex and percent decoding |
169
- | `paranoid` | `false`; entropy-based supplemental credential heuristic |
170
- | `maxInputLength` | 33,554,432 UTF-16 units |
171
- | `maxFindings` | 100,000 detector hits before deduplication |
172
- | `minConfidence` | 0.5; personal findings below this score remain visible to inspect but are not replaced |
173
- | `threshold` | Alias for minConfidence; both options must agree when supplied |
174
- | `debug` | Optional trusted callback for value-free counts, timing and event metadata |
175
- | `identityMask` | `false`; identity types keep 2+2 Unicode characters with fixed `***`, or redact below five |
176
- | `fakeIdentity` | Off; `'specimen-and-synthetic'` retains explicitly fake identity values |
177
- | `fakeValues` | `[]`; exact caller-reviewed identity values to retain, regardless of fakeIdentity |
178
- | `structured` | Off; `'json'` or `'jsonl'` validates and sanitizes string keys/values separately |
179
- | `sensitiveFields` | `[]`; exact JSON/JSONL property names whose complete values become a redaction string; supports synchronous calls |
180
- | `structuralFields` | `[]`; declared property names suppress weak name/ID/entropy heuristics in their string values |
161
+ | Option | Default / meaning |
162
+ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
163
+ | `documentEntities` | Off; bounded organization, education and location rules; enabled by the documents profile |
164
+ | `properNames` | Off; `'capitalized'` enables structural name redaction in activated prose strings |
165
+ | `scripts` | `['Cyrillic']`; Unicode Script aliases that activate the prose rule; `[]` disables only script activation |
166
+ | `profile` | Off; `'publication'` sets a 0.3 threshold and identity masking; `'documents'` enables document entities |
167
+ | `knownOnly` | `false`; disable all automatic detection (including credentials, Secretlint and custom detectors); use explicit findings, known values and reused map identities |
168
+ | `knownSecrets` | `[]`; exact literal credential values, regardless of length or format |
169
+ | `knownPersonal` | `[]`; `{type, value}` values with whole-word Unicode case-insensitive matching and bounded Russian forms; list parts to match them alone |
170
+ | `publicEntities` | `[]`; exact PERSON/ORGANIZATION/EDUCATION/LOCATION/EMAIL exception with HTTPS evidence and review date |
171
+ | `findings` | `[]`; validated detections for the original input, supplied by the caller |
172
+ | `detectors` | `[]`; required callbacks in both engines; sync methods require arrays |
173
+ | `secretlint` | `true` in the asynchronous engine |
174
+ | `decode` | `true`; bounded base64/base64url, hex and percent decoding |
175
+ | `paranoid` | `false`; entropy-based supplemental credential heuristic |
176
+ | `maxInputLength` | 33,554,432 UTF-16 units |
177
+ | `maxFindings` | 100,000 detector hits before deduplication |
178
+ | `minConfidence` | 0.5; personal findings below this score remain visible to inspect but are not replaced |
179
+ | `threshold` | Alias for minConfidence; both options must agree when supplied |
180
+ | `debug` | Optional trusted callback for value-free counts, timing and event metadata |
181
+ | `identityMask` | `false`; identity types keep 2+2 Unicode characters with fixed `***`, or redact below five |
182
+ | `fakeIdentity` | Off; `'specimen-and-synthetic'` retains explicitly fake identity values |
183
+ | `fakeValues` | `[]`; exact caller-reviewed identity values to retain, regardless of fakeIdentity |
184
+ | `structured` | Off; `'json'` or `'jsonl'` validates and sanitizes string keys/values separately |
185
+ | `sensitiveFields` | `[]`; JSON/JSONL names normalized for case and `_`/`-`; complete values become a redaction string; supports synchronous calls |
186
+ | `exactFields` | `false`; sensitive/allowed selectors use exact case/punctuation when true |
187
+ | `allowedFields` | Off; redact unlisted JSON/JSONL string leaves, scan listed leaves normally |
188
+ | `rules` / `types` | Per-rule / per-type booleans for automatic built-in findings |
189
+ | `fields` | `{location: 'scan'}` skips whole-field inference and still scans content |
190
+ | `allowCredentialDisable` | `false`; explicit permission required to suppress credential rules/types |
191
+ | `vocabulary` | Optional additive document cue lists; see below |
192
+ | `strict` | `false`; aggressive stored-log personal attributes and bare values |
193
+ | `structuralFields` | `[]`; declared property names suppress weak name/ID/entropy heuristics in their string values |
181
194
 
182
195
  Unknown options and invalid limits are errors. `knownSecretsFromEnv(env, names?)` discovers credential-named variables (excluding auth configuration names) when names are omitted, or uses an explicit selection, deduplicating nonempty values. It never invokes shell commands or reads auth files. `personalVariants(entries)` generates numeric and ISO/day-first date spellings; name case variants are already handled by detection. Neither helper invents transliterations.
183
196
 
184
197
  ## Required detectors
185
198
 
186
- A detector supplies `detect(text)`, returning an array of `{start, end, type, category, rule}` or a Promise of that array. Every range must refer to the exact input passed to that invocation. `category` is `credential` or `personal`; types are uppercase identifiers, rules are bounded lowercase identifiers. Throw if analysis is incomplete. The framework invokes engines on original input, normalized input when changed, bounded decoded views, and finally the sanitized output. Decoded matches redact the entire original encoded run. Malformed findings and engine errors block publication with generic error codes.
199
+ A detector supplies `detect(text)`, returning an array of `{start, end, type, category, rule}` or, in `createSanitizer`, a Promise of that array. Sync `inspect`, `sanitize` and `createSyncSanitizer` require an array; returning a thenable throws `ERR_CONFIG` (its rejection is observed). Every range must refer to the exact input passed to that invocation. `category` is `credential` or `personal`; types are uppercase identifiers, rules are bounded lowercase identifiers. Throw if analysis is incomplete. The framework invokes engines on original input, normalized input when changed, bounded decoded views, and finally the sanitized output. Structured callbacks receive each decoded string separately, including escaped Unicode and nested serialized strings, with mapped original offsets. Decoded matches redact the entire original encoded run. Malformed findings and engine errors block publication with generic error codes.
187
200
 
188
201
  Callbacks, policies, binaries, model packages, and their rule identifiers are trusted code/configuration. A malicious adapter can omit findings, leak input through its own logging/network calls, or never resolve. Local subprocess helpers use 30-second termination and 10 MiB output caps; arbitrary callback time budgets belong to the embedding application. Pin binary/model versions in deployments. Do not convert a failed scan into an empty result.
189
202
 
@@ -478,13 +491,14 @@ Authorization headers (including Proxy-Authorization) redact the scheme and one
478
491
 
479
492
  ## Caller-declared sensitive fields
480
493
 
481
- In structured JSON/JSONL, `sensitiveFields: string[]` matches exact,
482
- case-sensitive property names at any depth. The complete property value becomes
494
+ In structured JSON/JSONL, `sensitiveFields: string[]` ignores case and removes
495
+ underscores and hyphens at any depth, like built-in document fields. For example,
496
+ `fooBar` also matches `foo_bar`, `FooBar` and `foo-bar`. The complete property value becomes
483
497
  `"[REDACTED]"`, including empty strings, numbers, booleans, null, arrays and
484
- objects. This deliberate schema policy can change value types. It takes
498
+ objects. `exactFields: true` preserves exact case and punctuation in selectors. A single name matches at any depth; dotted paths such as `a.b` match from the root, and `items.*.name` matches array elements (or one object segment). This deliberate schema policy can change value types. It takes
485
499
  priority over `structuralFields` and transformations, preserves property names
486
500
  and surrounding whitespace, and works in both `sanitize` and `createSanitizer`.
487
- It has no effect without `structured`. The personal-data switch
501
+ `allowedFields: ['msg', 'level']` instead replaces every unlisted **string leaf**, including empty strings, while retaining keys, numeric/boolean/null primitives and containers. Listed strings still undergo normal detection. It supports the same paths, wildcard and exact matching; `sensitiveFields` takes priority on overlap. These selectors have no effect without `structured`. The personal-data switch
488
502
  `credentialsOnly` still suppresses personal findings.
489
503
 
490
504
  ```js
@@ -503,5 +517,223 @@ sanitize('Lyutik работал', {
503
517
 
504
518
  `knownPersonal` is the synchronous custom-value list, including names without
505
519
  context. For computed synchronous detection, callers can supply validated
506
- `findings` for the exact input. `detectors` remains an asynchronous-engine
507
- option; `sanitize` does not invoke these callbacks.
520
+ `findings` for the exact input. `detectors` is supported in both engines; synchronous methods reject Promise-returning callbacks.
521
+
522
+ ## Structural prose redaction
523
+
524
+ `properNames: 'capitalized'` is an optional, deliberately aggressive native rule
525
+ for stored prose. It works with any profile; the examples use `documents` for
526
+ its other document rules. It is off by default.
527
+
528
+ ```js
529
+ sanitize('Работал в Ромашке менеджером по продажам.', {
530
+ profile: 'documents',
531
+ properNames: 'capitalized',
532
+ }).text;
533
+ // [REDACTED] в [REDACTED] менеджером по продажам.
534
+ ```
535
+
536
+ Each string activates independently if it contains a letter from `scripts`, a
537
+ parenthesized document date range, a comma followed by an 11-digit `79…`/`89…`
538
+ mobile shape, or an HTTP(S)/bare-domain link with a `/d/<id>` path. `scripts`
539
+ defaults to `['Cyrillic']`, suitable for English operational logs; choose Unicode
540
+ Script aliases such as `['Greek', 'Cyrillic']` for your workload. Script names
541
+ are case-sensitive runtime Unicode property aliases, not language codes.
542
+ `scripts: []` disables script activation while retaining the other triggers.
543
+
544
+ Inside activated values, the rule replaces words containing an uppercase letter
545
+ anywhere (`iPhone`, `A.B`, `Санкт-Петербург`), typographic quoted spans (including
546
+ lowercase `«ромашка лютик»`), links including `example.org/path`, and bare domestic
547
+ mobile shapes. Labels immediately before a colon (also `Label :`), lowercase
548
+ words, numbers, date ranges and path tokens stay. A path can still match existing
549
+ personal-path or credential rules. Numeric primitives and JSON keys stay; string
550
+ escapes and surrounding source formatting are retained. JSON/JSONL activation
551
+ also applies independently within serialized JSON strings, to the existing
552
+ bounded decoding depth. Findings do not seed document-wide name propagation.
553
+ Repeated redaction is stable. Reusing a placeholder map for the same original
554
+ produces stable tokens and exact restoration; the existing placeholder-namespace
555
+ collision checks still reject input that already contains typed placeholders.
556
+
557
+ This is a structural heuristic, so capitalized ordinary words also disappear.
558
+ Lowercase organizations and `Ромашка: менеджер` stay. English prose without a
559
+ trigger (`Worked at Romashka for three years.`), isolated neutral-key values
560
+ `{"note":"Romashka"}` / `{"note":"1991-03-12"}`, and a personal website root
561
+ without a trigger stay under this rule. The existing detectors and explicit
562
+ known/sensitive-field policies still run: an unactivated value is byte-for-byte
563
+ unchanged by this additional rule, not exempt from credential detection.
564
+ `credentialsOnly` and `knownOnly` suppress automatic prose detection.
565
+
566
+ The new scanner uses a fixed number of forward passes over maximal tokens,
567
+ including long tokens without spaces. This does not impose a linear-time bound
568
+ on optional external engines or every preexisting detection rule. Debugging is
569
+ off by default; `debug` receives value-free `proper-name-prose` and
570
+ `document-experience` hit counts when those scans run.
571
+
572
+ CLI equivalents: `--proper-names capitalized --scripts Cyrillic,Greek`.
573
+ Configuration files accept the same `properNames` and `scripts` API options;
574
+ use a configuration file for `scripts: []`. See the runnable
575
+ [prose-log example](../examples/prose-logs.mjs).
576
+
577
+ ## Rule, type and field policy
578
+
579
+ `builtInRules` is a frozen list of accepted stable `Finding.rule` ids; the complete
580
+ [rule reference](RULES.md) is generated from the bundled catalog. Unknown ids
581
+ throw `ERR_CONFIG`. For example:
582
+
583
+ ```js
584
+ sanitize('Работала в компании Ромашка', {
585
+ rules: { 'organization-context': false },
586
+ });
587
+ sanitize('{"location":"apps/api"}', {
588
+ structured: 'json',
589
+ fields: { location: 'scan' },
590
+ });
591
+ ```
592
+
593
+ `types: { ORGANIZATION: false }` suppresses automatic built-in organization
594
+ findings. Independently matching rules can overlap: disabling one rule does not
595
+ suppress another rule's evidence. Explicit `findings` and custom detectors are
596
+ caller policy and remain active. `fields` skips built-in whole-field inference,
597
+ then scans the value normally, so a credential or email inside `location` still
598
+ gets replaced. Field names use normal case/underscore/hyphen normalization.
599
+
600
+ Credential types and rules require `allowCredentialDisable: true` before accepting
601
+ `false`. Credential findings cannot be suppressed through personal type controls
602
+ without that permission. This includes mixed encoded rules and Secretlint. The
603
+ permission intentionally reduces verification coverage; `true` enables an
604
+ already active recognizer and does not load optional detectors.
605
+
606
+ `documentEntities: false` turns off the additional document recognizers. The older
607
+ `organization-context`, `organization-cue`, personal-name/contact and multilingual
608
+ label recognizers still run. Thus `Работала в компании Ромашка` still redacts
609
+ `Ромашка`; use its rule or type control for that policy. `strict` is independent
610
+ of `documentEntities`. Defaults retain the existing policy, with the additional
611
+ catalog formats documented in COVERAGE.
612
+
613
+ ## Caller document vocabulary
614
+
615
+ ```js
616
+ const vocabulary = {
617
+ roles: ['маркетолог'],
618
+ organizationCues: ['артель'],
619
+ educationCues: ['альма-матер'],
620
+ personalLabels: ['личный контакт'],
621
+ personalFields: ['contactPerson'],
622
+ };
623
+ sanitize('работаю в артели Лютик', { profile: 'documents', vocabulary }).text;
624
+ // работаю в артели [REDACTED]
625
+ ```
626
+
627
+ Arrays extend the corresponding list. `{values: [...], replace: true}` replaces
628
+ one list, including an empty list. Other lists retain defaults. Replacement
629
+ changes that list's cue-based recognizers; independent legal forms, dated work
630
+ history, full names, identifiers and known values still count as evidence.
631
+ `defaultVocabulary` exposes frozen default cue lists; `documentVocabulary` exposes
632
+ the frozen multilingual document rows. Neither is modified by an operation.
633
+ Cues ignore case and support bounded Russian noun/adjective forms; personal fields
634
+ use normalized field names. Capitalized proper values are required after prose
635
+ cues. Inputs allow at most 256 nonempty strings of 128 code units per list, with
636
+ no line breaks/NUL. Unknown lists and malformed replacement records fail with
637
+ `ERR_CONFIG`. Compilation uses a bounded 64-configuration cache.
638
+
639
+ ## Callback transformations and stages
640
+
641
+ ```js
642
+ const sync = createSyncSanitizer({
643
+ transformations: { PERSON: (finding) => `Person-${finding.start}` },
644
+ });
645
+ sync.sanitize('Анна Смирнова').text; // Person-0
646
+ ```
647
+
648
+ `transformation(finding)` applies globally; `transformations[type](finding)` takes
649
+ precedence for that type. Each callback receives a frozen finding plus `value`,
650
+ the matched surface (decoded JSON string content when applicable), and must
651
+ return a string. Callbacks receive private data: keep it out of diagnostics.
652
+ JSON replacements are escaped, including partial string spans. Overlapping spans
653
+ are unioned and receive one callback for the selected type. Schema-level
654
+ `JSON_VALUE` denials always produce a complete redaction string.
655
+
656
+ Sync methods reject thenables with `ERR_CONFIG`; the asynchronous engine awaits
657
+ callbacks. Throws block publication. Returning the original, its normalized or
658
+ bounded encoded representation fails with `ERR_RESIDUAL`. The emitted text also
659
+ passes all configured residual detectors, so returning a new email/secret blocks.
660
+ This is bounded heuristic verification, not proof against arbitrary encodings.
661
+ Unlike built-in partial transforms, a callback deliberately controls credential
662
+ replacement too; the same checks apply. `preserveEncoding` recursively transforms
663
+ decoded content then re-encodes it.
664
+
665
+ The stages are detection → validated finding union/policy → transformation →
666
+ residual verification. `redact` exposes only the transformation stage and does not
667
+ perform residual scanning. To replace automatic detection while keeping the
668
+ sanitizer's verification step:
669
+
670
+ ```js
671
+ const input = 'private customer name';
672
+ const findings = myDetector.detect(input); // exact UTF-16 offsets
673
+ const result = sanitize(input, {
674
+ knownOnly: true,
675
+ findings,
676
+ knownSecrets: ['explicit-private-token'],
677
+ });
678
+ ```
679
+
680
+ `knownOnly` disables automatic detectors during both detection and verification;
681
+ only configured known values and findings remain. For custom detector verification,
682
+ pass it in `detectors` with automatic detection enabled, and tune built-in rules
683
+ as needed. `createSyncSanitizer(options)` implements the exported `SyncSanitizer`
684
+ interface (`inspect` / `sanitize`); Promise-returning detectors/transforms need
685
+ `createSanitizer` instead.
686
+
687
+ ## Compiled caller-known values
688
+
689
+ ```js
690
+ const known = compileKnown(
691
+ [{ type: 'ORGANIZATION', value: 'Ромашка' }],
692
+ [{ type: 'URL', value: 'https://example.org/anna' }]
693
+ );
694
+ const merged = compileKnown(known, [{ type: 'LOCATION', value: 'Тверь' }]);
695
+ sanitize('Живу в Твери', { knownPersonal: merged });
696
+ ```
697
+
698
+ Compilation copies only `{type, value, properCase?}`, deduplicates and freezes the
699
+ entries/array, and precompiles normalized projections and matching expressions.
700
+ There is no process-wide registry of caller values; weak metadata is released
701
+ with the compiled list. Mutating the input cannot change it. Compiled arrays
702
+ work with the same APIs as plain arrays, with identical output.
703
+
704
+ | Type | Matching behavior |
705
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
706
+ | `PERSON` | Unicode whole-word, case-insensitive; Russian cases, ё/е, reversed two-part names, supported full-name orders/initials and contiguous parts; list a part separately to match it alone |
707
+ | `ORGANIZATION` | Russian word forms, legal-form/quote-free core for names of at least three characters |
708
+ | `EDUCATION`, `LOCATION` | Whole-word case-insensitive with bounded Russian noun/adjective forms (e.g. МГУ / мгу, Тверь / Твери) |
709
+ | `URL` | Optional HTTP(S) scheme; query, fragment and trailing slash are consumed with the known path |
710
+ | `DATE`, `DATE_OF_BIRTH`, `PHONE` | Literal spellings by default; use `personalVariants(entries)` before compilation for ISO/day-first date and numeric phone variants |
711
+ | Other uppercase type ids | Same whole-word case-insensitive and bounded Russian word forms; type labels the finding/transform and does not activate a new parser |
712
+
713
+ No helper invents transliterations. Values longer than 256 code units use literal
714
+ matching instead of word-form expansion. `properCase: true` rejects a lower-case
715
+ first character (used by reused map identities). Explicit private values remain
716
+ private despite public exceptions. The 500-entry measurement and reproduction
717
+ script are linked from COVERAGE; matching remains linear in list size.
718
+
719
+ ## Optional offline recognition
720
+
721
+ Install `@nlpjs/ner@4.27.0` alongside this package, then load the separate entry:
722
+
723
+ ```js
724
+ import { createNerDetector } from '@link-foundation/sensitive-data-sanitizer/ner';
725
+ const detector = await createNerDetector({ languages: ['ru', 'en'] });
726
+ const sync = createSyncSanitizer({ detectors: [detector] });
727
+ sync.sanitize('Worked at Romashka for three years.');
728
+ ```
729
+
730
+ The factory loads the optional peer dependency once. Detection is synchronous,
731
+ in process and offline, with no model download or subprocess. This recognizer
732
+ uses NLP.js regex extraction with finite dictionaries and context rules; it is
733
+ not a neural model. It supports PERSON, ORGANIZATION, EDUCATION and LOCATION in
734
+ Russian/English. Unknown cue-free names can remain; see the published exact-span
735
+ precision/recall, challenge misses and per-KiB cost in COVERAGE. `languages` selects
736
+ one or both languages. Each invocation is limited to 1 Mi UTF-16 units; larger
737
+ inputs throw `ERR_LIMIT`. `debug` is off by default and emits counts only. Core
738
+ imports never load the optional entry or NLP.js; leaving it off adds no optional
739
+ runtime/module cost.