@pii-mask/core
v0.1.0
Published
The masking engine at the heart of the `@pii-mask` toolkit. Ships 22 built-in PII detectors, six masking modes, a detector registry, and both structured-data and freeform-text masking — all with zero required runtime dependencies beyond `libphonenumber-js
Downloads
79
Readme
@pii-mask/core
The masking engine at the heart of the @pii-mask toolkit. Ships 22 built-in PII detectors, six masking modes, a detector registry, and both structured-data and freeform-text masking — all with zero required runtime dependencies beyond libphonenumber-js.
Installation
pnpm add @pii-mask/coreFor substitute mode (fake data generation), also install the optional peer dependency:
pnpm add @faker-js/fakerQuick Start
import { createMasker } from '@pii-mask/core';
const masker = createMasker(); // defaults to 'mask' mode
const { result, detections } = masker.maskString('[email protected]');
// result: "jo***[email protected]"
// detections: ["email"]API Reference
createMasker(options?)
Factory function that returns a masker instance. Call it once, reuse the returned object.
import { createMasker } from '@pii-mask/core';
const masker = createMasker({
mode: 'redact', // 'mask' | 'redact' | 'pseudonymize' | 'anonymize' | 'tokenize' | 'substitute'
only: ['email', 'ssn-us'], // run only these detector IDs or category names
disable: ['phone'], // disable specific detector IDs
extend: [myCustomDetector], // add custom detectors to the registry
keyNameOnly: false, // skip value-level regex; rely on key-name heuristics only
});masker.maskString(input, key?)
Mask a single string value. First attempts atomic detection (is the whole string PII?), then falls back to freeform text scanning using detector pattern regexes.
// Atomic value
masker.maskString('[email protected]');
// → { result: '[REDACTED]', detections: ['email'], tokenMap: {} }
// Freeform text with inline PII
masker.maskString('Contact [email protected] or call 555-0123');
// → { result: 'Contact [REDACTED] or call [REDACTED]', detections: ['email', 'phone'], tokenMap: {} }Parameters:
input— The string to maskkey(optional) — Object key or column header, used by key-name heuristic detectors
Returns: MaskResult
result— The masked stringtokenMap— Intokenizemode, mapstoken → originalfor restorationdetections— Array of detector IDs that fired
masker.maskObject(input)
Recursively walk a plain object, masking all string values.
const { result, detections } = masker.maskObject({
user: {
email: '[email protected]',
ssn: '123-45-6789',
age: 30,
},
});
const parsed = JSON.parse(result);
// parsed.user.email → '[REDACTED]'
// parsed.user.ssn → '[REDACTED]'
// parsed.user.age → 30 (non-string, untouched)Parameters:
input— ARecord<string, unknown>to walk
Returns: MaskResult (result is a JSON string)
masker.maskArray(input)
Recursively walk an array of objects, masking all string values.
const { result } = masker.maskArray([
{ email: '[email protected]' },
{ email: '[email protected]' },
]);
const parsed = JSON.parse(result);
// parsed[0].email → '[REDACTED]'
// parsed[1].email → '[REDACTED]'masker.restore(masked, tokenMap)
Reverse tokenization. Only works when masking was done in tokenize mode.
const masker = createMasker({ mode: 'tokenize' });
const { result, tokenMap } = masker.maskString('[email protected]');
// result: '<<PII_a1b2c3d4>>'
const original = masker.restore(result, tokenMap);
// original: '[email protected]'Masking Modes
mask (default)
Partial obscure that preserves the type shape.
const masker = createMasker({ mode: 'mask' });
masker.maskString('[email protected]'); // → 'jo***[email protected]'
masker.maskString('4111111111111111'); // → '****-****-****-1111'
masker.maskString('123-45-6789'); // → '***-**-6789'redact
Full replacement. Every PII value becomes [REDACTED].
const masker = createMasker({ mode: 'redact' });
masker.maskString('[email protected]'); // → '[REDACTED]'pseudonymize / anonymize
Replaces PII with consistent labels per session. The same value always maps to the same label within a single maskObject/maskArray call.
const masker = createMasker({ mode: 'pseudonymize' });
masker.maskObject({
primary: '[email protected]',
backup: '[email protected]',
confirm: '[email protected]',
});
// primary → 'EMAIL_1', backup → 'EMAIL_2', confirm → 'EMAIL_1'tokenize
Generates cryptographically random tokens. Reversible via the returned token map.
const masker = createMasker({ mode: 'tokenize' });
const { result, tokenMap } = masker.maskString('[email protected]');
// result: '<<PII_a1b2c3d4>>'
masker.restore(result, tokenMap);
// → '[email protected]'Tokens are generated using node:crypto randomBytes — never Math.random().
substitute
Generates plausible fake values using @faker-js/faker. Requires @faker-js/faker as a peer dependency.
const masker = createMasker({ mode: 'substitute' });
masker.maskString('[email protected]'); // → '[email protected]' (random)
masker.maskString('123-45-6789'); // → '987-65-4321' (random)Filtering Detectors
Run only specific detectors
// By detector ID
const masker = createMasker({ only: ['email', 'ssn-us'] });
// By category — runs all detectors in that category
const masker = createMasker({ only: ['financial'] });Disable specific detectors
const masker = createMasker({ disable: ['phone', 'dob'] });Custom Detectors
Register additional detectors via the extend option:
import { createMasker, PIICategory, MaskMode } from '@pii-mask/core';
import type { PIIDetector } from '@pii-mask/core';
const myDetector: PIIDetector = {
id: 'custom-id',
label: 'My Custom PII',
category: PIICategory.GOV_ID,
regions: ['XX'],
detect(value, key) {
return /^\d{10}$/.test(value);
},
mask(value, mode, ctx) {
if (mode === MaskMode.REDACT) return '[REDACTED]';
if (mode === MaskMode.TOKENIZE) return getOrCreateToken(value, ctx);
if (mode === MaskMode.PSEUDONYMIZE || mode === MaskMode.ANONYMIZE) {
return getOrCreateLabel('CUSTOM', value, ctx);
}
if (mode === MaskMode.SUBSTITUTE) {
return ctx.faker?.string.numeric(10) ?? '0000000000';
}
return `******${value.slice(-4)}`;
},
// Optional: enables freeform text scanning (must have 'g' flag)
pattern: /\b\d{10}\b/g,
};
const masker = createMasker({ extend: [myDetector] });pattern property
When a detector has a pattern regex (with the g flag), maskString() can find and replace occurrences within freeform text. Without pattern, the detector only works on atomic field values (whole-string matches via detect()).
The detect() function is still called as a secondary validation gate on each pattern match, so detectors with checksums (like credit cards, CPF, Aadhaar) still validate correctly.
Registry
The singleton registry holds all registered detectors. Detectors self-register on import.
import { registry } from '@pii-mask/core';
// List all registered detectors
const all = registry.list();
// → [{ id: 'email', ... }, { id: 'phone', ... }, ...]
// Resolve with filtering
const filtered = registry.resolve({ only: ['email'], disable: [] });Override behaviour
// Accidental double-registration — warns, then replaces
registry.register(myDetector);
registry.register(myDetector); // ⚠ console.warn
// Intentional override — silent
registry.register(myDetector, { override: true }); // ✓ silentBuilt-in Detectors
| ID | Category | Regions | Description |
|----|----------|---------|-------------|
| email | contact | — | Email addresses |
| phone | contact | — | Phone numbers (libphonenumber-js) |
| ssn-us | gov-id | US | Social Security Numbers |
| nin-ng | gov-id | NG | Nigerian NIN (11 digits) |
| bvn-ng | gov-id | NG | Nigerian BVN (11 digits, key-gated) |
| aadhaar-in | gov-id | IN | Aadhaar numbers (Verhoeff-validated) |
| pan-in | gov-id | IN | PAN cards (AAAAA0000A) |
| said-za | gov-id | ZA | South African ID (Luhn-validated) |
| nin-uk | gov-id | GB | National Insurance Numbers |
| nik-id | gov-id | ID | Indonesian NIK (16 digits) |
| cpf-br | gov-id | BR | Brazilian CPF (checksum-validated) |
| credit-card | financial | — | Credit/debit cards (Luhn-validated) |
| iban | financial | — | IBANs (ISO 13616) |
| ip-address | network | — | IPv4 addresses |
| ipv6 | network | — | IPv6 addresses |
| dob | biometric | — | Dates of birth |
| person-name | identity | — | Person names (key-name heuristic) |
| address | identity | — | Physical addresses (key-name heuristic) |
| secret-key | secret | — | API keys, passwords (key-name heuristic) |
| jwt | secret | — | JSON Web Tokens |
| bcrypt-hash | secret | — | bcrypt hashes |
| hex-secret | secret | — | Long hex strings (32+ chars) |
Secret-category detectors always return
[REDACTED]regardless of masking mode.
Types
import type {
PIIDetector,
MaskContext,
MaskResult,
MaskOptions,
} from '@pii-mask/core';
import { PIICategory, MaskMode } from '@pii-mask/core';
// Categories: 'gov-id' | 'financial' | 'contact' | 'secret' | 'biometric' | 'network' | 'identity'
// Modes: 'mask' | 'redact' | 'pseudonymize' | 'anonymize' | 'tokenize' | 'substitute'Lexicon Data
Regional name lexicons for identity detectors:
import '@pii-mask/core/lexicon/africa';
import '@pii-mask/core/lexicon/south-asia';
import '@pii-mask/core/lexicon/east-asia';License
MIT
