@epilot/anonymization
Advanced tools
+23
-13
@@ -59,19 +59,22 @@ "use strict"; | ||
| for (const [key, value] of Object.entries(entity)) { | ||
| if (key !== '_title' && defaults_1.SYSTEM_KEEP_KEYS.includes(key)) { | ||
| result[key] = value; | ||
| continue; | ||
| } | ||
| const attribute = key === '_title' ? undefined : attributesByName.get(key); | ||
| // _title is computed from attributes and can carry PII (e.g. contact names) | ||
| if (key === '_title') { | ||
| const strategy = (0, exports.getAttributeStrategy)({ slug, attributeName: '_title', overrides }); | ||
| const strategy = (0, exports.getAttributeStrategy)({ slug, attributeName: key, attribute, overrides }); | ||
| if (strategy !== 'keep') { | ||
| result[key] = (0, exports.applyStrategy)({ value, strategy, pseudonymizer }); | ||
| continue; | ||
| } | ||
| if (defaults_1.SYSTEM_KEEP_KEYS.includes(key)) { | ||
| result[key] = value; | ||
| continue; | ||
| } | ||
| const strategy = (0, exports.getAttributeStrategy)({ | ||
| slug, | ||
| attributeName: key, | ||
| attribute: attributesByName.get(key), | ||
| overrides, | ||
| }); | ||
| result[key] = (0, exports.applyStrategy)({ value, strategy, pseudonymizer }); | ||
| // Kept values fall into two cases: | ||
| // - explicitly kept (data_classification 'public', or an override to 'keep'): the org has | ||
| // reviewed the field and opted out — pass through untouched. | ||
| // - kept by default (unclassified custom fields, computed titles, attribute types we have | ||
| // not special-cased): apply the catch-all scrub so a stray email/phone/IBAN embedded in | ||
| // the value cannot leak. Only matched identifier substrings are replaced. | ||
| result[key] = isExplicitlyKept({ slug, key, attribute, overrides }) | ||
| ? value | ||
| : (0, exports.mapStringLeaves)(value, (str) => pseudonymizer.scrubText(str)); | ||
| } | ||
@@ -81,2 +84,9 @@ return result; | ||
| exports.anonymizeEntity = anonymizeEntity; | ||
| /** true when the org deliberately opted a field out of anonymization (never re-scrub it) */ | ||
| const isExplicitlyKept = (params) => { | ||
| const { slug, key, attribute, overrides } = params; | ||
| return (attribute?.data_classification === 'public' || | ||
| overrides?.[`${slug}:${key}`] === 'keep' || | ||
| overrides?.[`*:${key}`] === 'keep'); | ||
| }; | ||
| /** | ||
@@ -83,0 +93,0 @@ * Apply an anonymization strategy to an attribute value, preserving the value's shape |
@@ -25,3 +25,5 @@ "use strict"; | ||
| redact, | ||
| scrubText: redact, | ||
| // still scrub identifiers OUT of free text (replacing only the matched substrings), | ||
| // rather than blanking the whole string — so the catch-all pass keeps non-PII text intact | ||
| scrubText: makeScrubText(redact, redact, redact), | ||
| }; | ||
@@ -50,11 +52,16 @@ } | ||
| redact: () => defaults_1.REDACTED_PLACEHOLDER, | ||
| scrubText: (value) => value | ||
| // skip already-pseudonymized emails so scrubbing stays idempotent | ||
| // (phone pseudonyms are idempotent by format, IBAN pseudonyms don't match IBAN_PATTERN) | ||
| .replace(EMAIL_PATTERN, (match) => (match.endsWith(`@${defaults_1.PSEUDONYM_EMAIL_DOMAIN}`) ? match : email(match))) | ||
| .replace(IBAN_PATTERN, iban) | ||
| .replace(PHONE_PATTERN, phone), | ||
| scrubText: makeScrubText(email, iban, phone), | ||
| }; | ||
| }; | ||
| exports.createPseudonymizer = createPseudonymizer; | ||
| /** | ||
| * Build a text scrubber that replaces email / IBAN / phone substrings embedded in free text, | ||
| * leaving the rest of the string intact. `email`/`iban`/`phone` are the pseudonymizer's own | ||
| * replacers (or the redactor when no secret is configured). Idempotent: pseudonymized emails | ||
| * are skipped, and phone/IBAN pseudonyms do not re-match the patterns. | ||
| */ | ||
| const makeScrubText = (email, iban, phone) => (value) => value | ||
| .replace(EMAIL_PATTERN, (match) => (match.endsWith(`@${defaults_1.PSEUDONYM_EMAIL_DOMAIN}`) ? match : email(match))) | ||
| .replace(IBAN_PATTERN, iban) | ||
| .replace(PHONE_PATTERN, phone); | ||
| const normalize = (value) => value.trim().toLowerCase(); |
+2
-2
| { | ||
| "name": "@epilot/anonymization", | ||
| "version": "0.1.4", | ||
| "description": "Deterministic PII pseudonymization and redaction for epilot entity data and arbitrary JSON — data minimization for AI agents, analytics, logs and integrations", | ||
| "version": "0.2.0", | ||
| "description": "Deterministic PII pseudonymization and redaction for epilot entity data and arbitrary JSON \u2014 data minimization for AI agents, analytics, logs and integrations", | ||
| "license": "MIT", | ||
@@ -6,0 +6,0 @@ "author": "epilot GmbH", |
+8
-5
@@ -150,7 +150,10 @@ <h1 align="center"><img alt="epilot" src="https://raw.githubusercontent.com/epilot-dev/sdk-js/main/logo.png" width="200"><br>@epilot/anonymization</h1> | ||
| destroys utility.) | ||
| 2. **Free text is opt-in by default.** Unclassified free-text fields pass through in | ||
| schema-aware mode (masking them all would destroy utility). Well-known risky fields | ||
| (note/message content, opportunity descriptions, …) are covered by the curated defaults, and | ||
| organizations opt further fields in with `data_classification: 'pii'`. Users can still type | ||
| PII into fields nobody classified. | ||
| 2. **Free text is kept by default, but identifiers in it are scrubbed.** Unclassified free-text | ||
| fields are not fully masked in schema-aware mode (masking them all would destroy utility), | ||
| but a catch-all pass scrubs any email / phone / IBAN embedded in kept string values (custom | ||
| fields, computed titles, attribute types not special-cased). Well-known risky fields | ||
| (note/message content, opportunity descriptions, …) are fully redacted via the curated | ||
| defaults; organizations opt further fields in with `data_classification: 'pii'` or out with | ||
| `'public'` (which also exempts them from the catch-all). Names or other non-pattern PII typed | ||
| into unclassified fields can still slip through. | ||
| 3. **Heuristics are best-effort.** The non-schema mode catches common field names and | ||
@@ -157,0 +160,0 @@ email/phone/IBAN patterns. Novel field names or unusual formats can slip through — use |
63379
2.66%1128
1.53%205
1.49%