@link-foundation/sensitive-data-sanitizer 0.18.0 → 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 +6 -0
- package/README.md +8 -0
- package/docs/API.md +205 -33
- package/docs/COVERAGE.md +84 -8
- package/docs/RULES.md +403 -0
- package/examples/custom-log-policy.mjs +23 -0
- package/package.json +17 -2
- package/src/compiled-known.js +55 -0
- package/src/detection.js +9 -0
- package/src/document-catalog.js +246 -0
- package/src/document-entities.js +8 -2
- package/src/document-experience.js +9 -4
- package/src/document-extras.js +2 -2
- package/src/document-vocabulary.js +80 -68
- package/src/engines.js +81 -34
- package/src/index.d.ts +52 -4
- package/src/index.js +5 -0
- package/src/jsonl-stream.js +2 -5
- package/src/ner.d.ts +5 -0
- package/src/ner.js +96 -0
- package/src/personal-variants.js +29 -8
- package/src/policy.js +107 -0
- package/src/record-worker.js +6 -0
- package/src/rule-catalog.js +669 -0
- package/src/sanitizer.js +99 -19
- package/src/stream-worker.js +5 -0
- package/src/stream.js +11 -6
- package/src/strict.js +66 -0
- package/src/structured.js +111 -47
- package/src/sync-detectors.js +38 -0
- package/src/sync-sanitizer.js +16 -0
- package/src/transforms.js +85 -7
- package/src/vocabulary.js +217 -0
- package/src/worker-options.js +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 0.18.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -20,6 +20,14 @@ 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
|
|
|
25
33
|
For stored prose that can tolerate extra redaction, opt into structural proper
|
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
|
|
14
|
-
| `sanitize(text, options)` | Synchronous native
|
|
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).
|
|
@@ -156,38 +158,45 @@ derived model answer. Input, output and map paths must be separate. Maps are
|
|
|
156
158
|
saved separately before stdout publication. `--profile documents` enables the
|
|
157
159
|
new document recognizers; `documentEntities: false` turns them off explicitly.
|
|
158
160
|
|
|
159
|
-
| Option
|
|
160
|
-
|
|
|
161
|
-
| `documentEntities`
|
|
162
|
-
| `properNames`
|
|
163
|
-
| `scripts`
|
|
164
|
-
| `profile`
|
|
165
|
-
| `knownOnly`
|
|
166
|
-
| `knownSecrets`
|
|
167
|
-
| `knownPersonal`
|
|
168
|
-
| `publicEntities`
|
|
169
|
-
| `findings`
|
|
170
|
-
| `detectors`
|
|
171
|
-
| `secretlint`
|
|
172
|
-
| `decode`
|
|
173
|
-
| `paranoid`
|
|
174
|
-
| `maxInputLength`
|
|
175
|
-
| `maxFindings`
|
|
176
|
-
| `minConfidence`
|
|
177
|
-
| `threshold`
|
|
178
|
-
| `debug`
|
|
179
|
-
| `identityMask`
|
|
180
|
-
| `fakeIdentity`
|
|
181
|
-
| `fakeValues`
|
|
182
|
-
| `structured`
|
|
183
|
-
| `sensitiveFields`
|
|
184
|
-
| `
|
|
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 |
|
|
185
194
|
|
|
186
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.
|
|
187
196
|
|
|
188
197
|
## Required detectors
|
|
189
198
|
|
|
190
|
-
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.
|
|
191
200
|
|
|
192
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.
|
|
193
202
|
|
|
@@ -486,10 +495,10 @@ In structured JSON/JSONL, `sensitiveFields: string[]` ignores case and removes
|
|
|
486
495
|
underscores and hyphens at any depth, like built-in document fields. For example,
|
|
487
496
|
`fooBar` also matches `foo_bar`, `FooBar` and `foo-bar`. The complete property value becomes
|
|
488
497
|
`"[REDACTED]"`, including empty strings, numbers, booleans, null, arrays and
|
|
489
|
-
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
|
|
490
499
|
priority over `structuralFields` and transformations, preserves property names
|
|
491
500
|
and surrounding whitespace, and works in both `sanitize` and `createSanitizer`.
|
|
492
|
-
It
|
|
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
|
|
493
502
|
`credentialsOnly` still suppresses personal findings.
|
|
494
503
|
|
|
495
504
|
```js
|
|
@@ -508,8 +517,7 @@ sanitize('Lyutik работал', {
|
|
|
508
517
|
|
|
509
518
|
`knownPersonal` is the synchronous custom-value list, including names without
|
|
510
519
|
context. For computed synchronous detection, callers can supply validated
|
|
511
|
-
`findings` for the exact input. `detectors`
|
|
512
|
-
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.
|
|
513
521
|
|
|
514
522
|
## Structural prose redaction
|
|
515
523
|
|
|
@@ -565,3 +573,167 @@ CLI equivalents: `--proper-names capitalized --scripts Cyrillic,Greek`.
|
|
|
565
573
|
Configuration files accept the same `properNames` and `scripts` API options;
|
|
566
574
|
use a configuration file for `scripts: []`. See the runnable
|
|
567
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.
|
package/docs/COVERAGE.md
CHANGED
|
@@ -243,16 +243,17 @@ The `documents` profile additionally recognizes:
|
|
|
243
243
|
- Company/employer/workplace labels, capitalized company/holding/bank names,
|
|
244
244
|
employment prose and year-range employer lines; residence/metro/district
|
|
245
245
|
labels and contextual six-digit postal codes.
|
|
246
|
-
- Russian street-plus-house/flat structures
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
inspect and review standalone public addresses before publication.
|
|
246
|
+
- Russian street-plus-house/flat structures, including inline addresses added
|
|
247
|
+
by #117. `Музей находится на ул. Ленина, д. 5, кв. 12` now removes the full
|
|
248
|
+
street/house/apartment span. Public addresses have the same shape as personal
|
|
249
|
+
addresses and may be flagged; inspect them before publication.
|
|
251
250
|
|
|
252
251
|
Explicit limits accepted for the ambiguous examples in #82/#83:
|
|
253
|
-
`Спасибо, Анна!`, `Анна, добрый
|
|
252
|
+
`Спасибо, Анна!`, `Анна, добрый день!` and `Менеджер Анна`
|
|
254
253
|
remain unchanged without known values or additional context. Bare given names
|
|
255
254
|
and unmarked employer/title fragments cannot reliably establish identity.
|
|
255
|
+
The reversed employer cue `Лютик, менеджер` now redacts `Лютик`, as requested
|
|
256
|
+
by #117. English `Worked at Romashka` now redacts the employer under #115.
|
|
256
257
|
`зарплата 150 000 руб.` and `Salary: 150000` also remain unchanged: salary is
|
|
257
258
|
outside the native document rules. Supply `knownPersonal` or an additional
|
|
258
259
|
recognizer when these fields must be removed. Initial/name rules do not cover
|
|
@@ -386,8 +387,7 @@ Company prose recognizes `компании «…»`, `опыт в`, `Роль в
|
|
|
386
387
|
capitalized lists after those cues, `опыт (Company Name)`, and names followed
|
|
387
388
|
by `(2 года)`/`(3 года)`-style durations. Arbitrary uncued company words remain
|
|
388
389
|
ambiguous; synchronous callers can supply `knownPersonal` lists or precomputed
|
|
389
|
-
`findings`.
|
|
390
|
-
only. No model or network service is required for these native rules.
|
|
390
|
+
`findings`. Array-returning custom `detectors` also run in synchronous methods; Promise-returning callbacks require `createSanitizer`. No model or network service is required for these native rules.
|
|
391
391
|
|
|
392
392
|
`контакт:` and `пишите,` introduce domestic mobile numbers. A complete line
|
|
393
393
|
consisting of an 11-digit `79…` or `89…` mobile shape is also sensitive. Arbitrary
|
|
@@ -448,3 +448,79 @@ classifies maximal tokens in fixed passes, avoiding repeated suffix searches on
|
|
|
448
448
|
long unspaced input. This is not a latency guarantee for the whole pipeline.
|
|
449
449
|
[The requirement inventory](case-studies/issue-106/README.md) records alternatives,
|
|
450
450
|
research, regression evidence and reproducible bounded performance measurements.
|
|
451
|
+
|
|
452
|
+
## Stored-log policy and expanded document catalog
|
|
453
|
+
|
|
454
|
+
The documents profile now covers the exact positive/control rows of issues
|
|
455
|
+
115 and 117: spaced/obfuscated contacts, Discord/MAX handles, additional
|
|
456
|
+
personal-link routes, birthday/age cues, military/VIN identifiers, employer and
|
|
457
|
+
school layouts, patronymics/initials/mixed scripts, complete street/house spans,
|
|
458
|
+
regions, citizenship and plates. Regression tests compare complete text output
|
|
459
|
+
and JSON/JSONL output in both engines. `ОГРНИП` without a value, operational logs,
|
|
460
|
+
work durations, weekdays and technologies remain unchanged. Bare `Сергей`,
|
|
461
|
+
`Смирнова`, `Смирновой`, `СПб`, `Питер` and `Вчера Анна написала` deliberately remain
|
|
462
|
+
ambiguous without explicit known values, the optional recognizer or aggressive
|
|
463
|
+
proper-name mode. Romashka Inc keeps its existing legal-form treatment.
|
|
464
|
+
|
|
465
|
+
`strict: true` independently opts into personal attributes (gender, family,
|
|
466
|
+
ethnicity, religion, health, criminal/political status) and bare dates, ages,
|
|
467
|
+
passport/mobile/postal shapes. It is intended for stored personal prose and can
|
|
468
|
+
remove ordinary uses of these words. It preserves ISO timestamps, work durations,
|
|
469
|
+
unit-bearing request timings and `Дети: нет`. In JSON/JSONL, numeric primitives
|
|
470
|
+
stay numeric, and `structuralFields` suppresses strict findings in declared
|
|
471
|
+
string metadata. Credentials/known values still take priority. Strict is off by
|
|
472
|
+
default and does not claim to identify every sensitive attribute or validate
|
|
473
|
+
that every date/number belongs to a real person.
|
|
474
|
+
|
|
475
|
+
## Optional offline recognition measurements
|
|
476
|
+
|
|
477
|
+
The `/ner` entry uses the separately installed optional
|
|
478
|
+
[NLP.js](https://github.com/axa-group/nlp.js) regex extractor, finite multilingual
|
|
479
|
+
name/place dictionaries and bounded contextual rules. It has synchronous
|
|
480
|
+
inference after `await createNerDetector(...)`. There is no analysis-time network,
|
|
481
|
+
external service, Python process or model download. The core import graph never
|
|
482
|
+
imports `/ner` or NLP.js, so the optional component has no runtime or loading cost
|
|
483
|
+
when disabled. This is symbolic recognition, not general statistical NER; unknown
|
|
484
|
+
cue-free surnames, schools, employers and locations can be missed.
|
|
485
|
+
|
|
486
|
+
[Test set](../experiments/issue-118/ner-corpus.json): 32 synthetic sentences,
|
|
487
|
+
including all six requested positives, eight benign controls and eight challenge
|
|
488
|
+
misses. Exact type and UTF-16 start/end must all match. **All** rows, including
|
|
489
|
+
challenge rows, contribute to these scores. Each language/type has two true
|
|
490
|
+
positives, zero false positives and one false negative:
|
|
491
|
+
|
|
492
|
+
| Language | Type | Precision | Recall | TP / FP / FN |
|
|
493
|
+
| -------- | ------------ | --------- | ------ | ------------ |
|
|
494
|
+
| ru | PERSON | 100% | 66.7% | 2 / 0 / 1 |
|
|
495
|
+
| ru | ORGANIZATION | 100% | 66.7% | 2 / 0 / 1 |
|
|
496
|
+
| ru | EDUCATION | 100% | 66.7% | 2 / 0 / 1 |
|
|
497
|
+
| ru | LOCATION | 100% | 66.7% | 2 / 0 / 1 |
|
|
498
|
+
| en | PERSON | 100% | 66.7% | 2 / 0 / 1 |
|
|
499
|
+
| en | ORGANIZATION | 100% | 66.7% | 2 / 0 / 1 |
|
|
500
|
+
| en | EDUCATION | 100% | 66.7% | 2 / 0 / 1 |
|
|
501
|
+
| en | LOCATION | 100% | 66.7% | 2 / 0 / 1 |
|
|
502
|
+
|
|
503
|
+
These small authored regression scores do not estimate accuracy on arbitrary
|
|
504
|
+
user documents. The test includes explicit misses such as `Zephyr Quill`,
|
|
505
|
+
`Asterion expanded rapidly`, `ETHZ` and `Reykjavik` so dictionary limits remain
|
|
506
|
+
visible. Acceptance rows also test both sanitizers and unchanged controls.
|
|
507
|
+
|
|
508
|
+
On Node v26.13.0, Linux x64, five bounded groups, the median warm detector cost
|
|
509
|
+
was **0.0434 ms per 1 KiB** of ASCII employer prose; module loading plus factory
|
|
510
|
+
initialization took **48.6 ms** once. Cost excludes the sanitizer's other
|
|
511
|
+
recognizers and varies with text and hardware. No latency guarantee is implied.
|
|
512
|
+
|
|
513
|
+
## Compiled known-value measurements
|
|
514
|
+
|
|
515
|
+
On the same host and 1 KiB operational record, median complete `sanitize` cost
|
|
516
|
+
was **2.80 ms** without a list, **3.30 ms** with 500 compiled values and **8.56 ms**
|
|
517
|
+
with a plain array. A direct 500-value matching pass cost **0.73 ms**. Validation
|
|
518
|
+
cost was **0.00416 ms** without a list and **0.00356 ms** compiled (the difference
|
|
519
|
+
is measurement noise), below the allowed 10% of the no-list call, excluding
|
|
520
|
+
matching. Compilation skips per-record entry validation, normalization and regex
|
|
521
|
+
construction; matching itself still scales with list length and pipeline passes.
|
|
522
|
+
|
|
523
|
+
Reproduce with `node --max-old-space-size=384 experiments/issue-118/measure.mjs`.
|
|
524
|
+
The [raw results](case-studies/issue-118/measurements.json) include environment,
|
|
525
|
+
exact-span counts and measurement method. This fixed experiment uses no large
|
|
526
|
+
corpus, unbounded input, parallel builds or external analysis service.
|