anonyma
v1.1.1
Published
TypeScript-first PII detection & anonymization — 27 built-in detectors (email, SSN, IBAN, and more), 8 strategies (mask, redact, hash, AES-256 encrypt, tokenize, pseudonymize, generalize, synthesize), 8 compliance presets (GDPR, LGPD, PIPEDA, HIPAA, CCPA,
Maintainers
Keywords
Readme
anonyma
A modern, zero-dependency TypeScript library for PII detection and data anonymization — built for the AI era.
Features
- 🔍 Detect 27 PII categories — email, phone, SSN, credit card, IBAN, passport, address, VIN, API keys, crypto wallets, and more
- 🛡️ 8 anonymization strategies — mask, redact, pseudonymize, hash (SHA-256), generalize, tokenize, encrypt (AES-GCM), synthesize
- 🔁 Reversible tokenization —
tokenize()/detokenize()for round-trip fidelity - 🤖 LLM pipeline helpers —
sanitizeForLLM()/restoreFromLLM()for safe prompt injection with reversible tokens - 📋 Compliance presets — built-in
gdpr,lgpd,pipeda,hipaa,ccpa,pci-dss,soxandferpapresets - 🌍 International identifiers — national IDs, tax IDs, passports, licence plates and company registrations of many countries, detected without configuration
- ⚡ Batch processing —
anonymizeBatch(),anonymizeBatchAsync(),tokenizeBatch(),detectBatch() - 🌊 Streaming support — WHATWG
TransformStreamwrappers (createAnonymizeStream(),createTokenizeStream()) - 🎯 Field-level record anonymization — dot-notation paths for nested objects
- 🌳 Deep object anonymization —
anonymizeObject()recursively cleans entire JSON trees - ✅ PII presence check —
hasPII()with early-exit for fast gating - 🔎 Aggressive mode — expanded, permissive patterns for obfuscated PII
- 🧩 Custom patterns & detectors — inject ad-hoc
RegExppatterns, fully replace per-category detectors, or define detectors for your own categories - 🚫 Allowlist support — skip known-safe values by exact string or
RegExppattern - 🔢 Confidence threshold — filter out low-confidence matches
- 🔒 Encryption — reversible AES-GCM
encrypt()/decrypt()strategy (Web Crypto API) - 🎭 Synthesis — format-preserving synthetic data replacement (deterministic, seeded)
- ✔️ Checksum validators —
luhn,verhoeff,nhsMod11,cpfChecksum,vinChecksum,deaChecksum,ibanMod97,ninoValid,aadhaarFormatvia"anonyma/validators" - ⚡ Zero runtime dependencies — Zod is an optional peer dependency
- 🌲 Tree-shakeable — import only what you use
- 🤖 AI-ready — OpenAI/MCP tool definitions and Zod schemas included
- 🧵 Span engine — compiled pipelines that leave no detected character in the output, validating detectors for email, SSN, IBAN, IP addresses and payment cards, and streams that catch values split across chunks (
"anonyma/engine") - 🔑 Keyed tokenization — session, keyed and sealed tokenizers, a versioned key ring with rotation, and erasure by deleting a record or shredding a key (
"anonyma/vault") - 🧾 Audit trail without personal data — hash-chained records of what was replaced, and chain verification (
"anonyma/audit") - ⚖️ Regulation profiles — GDPR, LGPD, PIPEDA, CCPA/CPRA, HIPAA and PCI DSS mapped to their provisions, with policy documents checked against them (
"anonyma/compliance") - 💬 LLM guard — chat messages and JSON sanitized with reversible tokens, restored in complete and streamed model output (
"anonyma/ai") - 🔌 MCP server and HTTP middleware — a dependency-free Model Context Protocol server, and payload scrubbing for Express, Hono and Fetch handlers (
"anonyma/mcp","anonyma/middleware") - 📦 Dual ESM + CJS — works everywhere Node.js ≥ 18 runs
- 🔒 Strict TypeScript — no
any, full declaration files
Installation
npm install anonyma
# or
pnpm add anonyma
# or
yarn add anonymaFor runtime validation and AI schema features, also install the optional peer dependency:
npm install zodQuick Start
import {
anonymize,
anonymizeAsync,
detect,
hasPII,
anonymizeObject,
anonymizeRecord,
createAnonymizer,
tokenize,
tokenizeAsync,
detokenize,
sanitizeForLLM,
restoreFromLLM,
anonymizeBatch,
anonymizeBatchAsync,
} from "anonyma";
// ── Detect PII ─────────────────────────────────────────────────────────────
const matches = detect("Contact [email protected] or call 555-867-5309.");
// [
// { category: "email", value: "[email protected]", start: 8, end: 25, confidence: 0.99 },
// { category: "phone", value: "555-867-5309", start: 34, end: 46, confidence: 0.9 },
// ]
// ── Fast PII presence check (stops at first match) ─────────────────────────
if (hasPII(commentText)) {
return { error: "Comment contains personal information." };
}
// ── Anonymize free text (default: redact all PII) ──────────────────────────
const { text } = anonymize("Contact [email protected] or call 555-867-5309.");
// "Contact [REDACTED] or call [REDACTED]."
// ── Async anonymization (required for hash/encrypt strategies) ─────────────
const { text: hashed } = await anonymizeAsync("[email protected]", {
defaultStrategy: { strategy: "hash", pepper: "my-pepper" },
});
// "e2a233272b2a9f2b"
// ── Consistent token mapping ───────────────────────────────────────────────
anonymize("From [email protected] ([email protected])", { consistentTokens: true }).text;
// "From EMAIL_1 (EMAIL_1)"
// ── Global replacement ─────────────────────────────────────────────────────
anonymize("[email protected] — 555-867-5309", { globalReplacement: "***" }).text;
// "*** — ***"
// ── Aggressive mode — catch obfuscated PII ────────────────────────────────
anonymize("user [at] example [dot] com", { aggressive: true }).text;
// "[REDACTED]"
// ── Custom pattern ─────────────────────────────────────────────────────────
anonymize("Order ACME-001234 confirmed.", {
customPatterns: [{ pattern: /\bACME-\d{6}\b/g, label: "[ORDER_ID]" }],
rules: [],
}).text;
// "Order [ORDER_ID] confirmed."
// ── Deep object anonymization ──────────────────────────────────────────────
anonymizeObject({
user: { email: "[email protected]", phone: "555-867-5309" },
notes: ["Call later", "IP: 192.168.1.1"],
});
// {
// user: { email: "[REDACTED]", phone: "[REDACTED]" },
// notes: ["Call later", "IP: [REDACTED]"],
// }
// ── Custom strategy per category ───────────────────────────────────────────
const { text: masked } = anonymize("[email protected] and 192.168.1.1", {
rules: [
{ category: "email", strategy: { strategy: "mask", keepLeading: 1, keepTrailing: 3 } },
{ category: "ipv4", strategy: { strategy: "redact", label: "[IP REMOVED]" } },
],
});
// "a*************com and [IP REMOVED]"
// ── Format-preserving mask ─────────────────────────────────────────────────
anonymize("123-45-6789", {
rules: [{ category: "ssn", strategy: { strategy: "mask", preserveFormat: true } }],
}).text;
// "000-00-0000"
// ── Enable only specific categories ───────────────────────────────────────
anonymize("[email protected] — call 555-867-5309", {
enabledCategories: { email: true },
}).text;
// "[REDACTED] — call 555-867-5309"
// ── Allowlist — skip known-safe values ────────────────────────────────────
anonymize("[email protected] or [email protected]", {
allowlist: ["[email protected]"],
}).text;
// "[email protected] or [REDACTED]"
// ── Confidence threshold ──────────────────────────────────────────────────
anonymize("maybe a name here", { confidenceThreshold: 0.8 }).text;
// Low-confidence matches are left untouched
// ── Compliance presets ─────────────────────────────────────────────────────
anonymize(medicalNote, { preset: "hipaa" }).text;
anonymize(userData, { preset: "gdpr" }).text;
// ── International identifiers ──────────────────────────────────────────────
anonymize("NHS: 943 476 5919").text;
// "NHS: [REDACTED]"
// ── Anonymize object fields ─────────────────────────────────────────────────
anonymizeRecord(
{ name: "Alice", email: "[email protected]", age: "27" },
{
email: { strategy: { strategy: "redact" } },
age: { strategy: { strategy: "generalize" } }, // 27 → "20-29"
},
);
// { name: "Alice", email: "[REDACTED]", age: "20-29" }
// ── Reusable anonymizer ─────────────────────────────────────────────────────
const anonymizer = createAnonymizer({
categories: ["email", "phone"],
defaultStrategy: { strategy: "pseudonymize", seed: "my-secret" },
});
anonymizer.anonymize("[email protected]").text;
// "id_a908a815c8327f0b"
anonymizer.hasPII("no pii here"); // false
// ── Reversible tokenization ────────────────────────────────────────────────
const { text: tokenized, mapping } = tokenize("[email protected] called 555-867-5309");
// text: "[EMAIL_0001] called [PHONE_0001]"
const { text: restored } = detokenize(tokenized, mapping);
// "[email protected] called 555-867-5309"
// ── LLM pipeline — sanitize then restore ──────────────────────────────────
const { text: sanitized, mapping: llmMapping } = sanitizeForLLM(
"Send invoice to [email protected]",
);
// "Send invoice to [EMAIL_0001]"
const llmResponse = await callLLM(sanitized);
const final = restoreFromLLM(llmResponse, llmMapping);
// Tokens in the LLM's response are swapped back to the original values
// ── Batch processing ───────────────────────────────────────────────────────
const results = anonymizeBatch(["[email protected]", "192.0.2.1", "123-45-6789"]);
for (const r of results) {
if (r.ok) console.log(r.value.text);
else console.error(`Item ${r.index} failed:`, r.error.message);
}
// ── AES-GCM encryption (reversible) ───────────────────────────────────────
import { encrypt, decrypt } from "anonyma";
const ciphertext = await encrypt("[email protected]", { passphrase: "s3cr3t" });
// "base64:<iv>:<ciphertext>"
const original = await decrypt(ciphertext, { passphrase: "s3cr3t" });
// "[email protected]"
// ── Synthetic data replacement ────────────────────────────────────────────
import { synthesize } from "anonyma";
synthesize("[email protected]", "email", { seed: "project-y" });
// "[email protected]" (deterministic, structurally valid)Strategies
| Strategy | Description | Async | Deterministic | Reversible |
| -------------- | ------------------------------------------------------------- | :---: | :-----------: | :--------: |
| redact | Replace with [REDACTED] label (customizable) | ❌ | ✅ | ❌ |
| mask | Replace inner chars with *; optional format-preserving mode | ❌ | ✅ | ❌ |
| pseudonymize | Replace with a hex pseudonym (seeded or random) | ❌ | ✅† | ❌ |
| hash | SHA-256 one-way hash with optional pepper (Web Crypto) | ✅ | ✅ | ❌ |
| generalize | Replace numbers with a bucket range (e.g. 27 → 20-29) | ❌ | ✅ | ❌ |
| tokenize | Replace with a reversible placeholder token | ❌ | ✅ | ✅ |
| encrypt | AES-256-GCM encryption via passphrase or raw key (Web Crypto) | ✅ | ❌‡ | ✅ |
| synthesize | Format-preserving synthetic replacement (seeded, no real PII) | ❌ | ✅† | ❌ |
† Deterministic when seed is provided. ‡ Random IV per encryption; decryptable with the same key.
The strategy functions (mask(), hash(), encrypt(), synthesize(), …) implement every
strategy. As rules of anonymize(), anonymizeAsync() and the helpers built on them
(anonymizeObject(), anonymizeRecord(), createAnonymizer(), the batch and stream functions),
redact, mask, pseudonymize and generalize apply everywhere, and hash applies in the
asynchronous functions; the synchronous ones replace a hashed value with a seeded pseudonym
(hsh_…). Values whose rule is tokenize, encrypt or synthesize are redacted, with a
warning. Use tokenize() or sanitizeForLLM() for reversible tokens, and a pipeline from
"anonyma/engine" to apply any of the eight strategies to text
(Span engine).
Detected PII Categories
Personal Information
| Category | Examples | Validator / Notes |
| ----------------- | --------------------------------------- | ---------------------------------- |
| email | [email protected] | RFC 5321 regex |
| phone | +1 (555) 867-5309, 415.555.2671 | Multi-format regex |
| ssn | 123-45-6789 | Regex + exclusions |
| name | Dear Alice Smith, Patient: John Doe | Heuristic (greeting/title context) |
| date-of-birth | 1990-04-15, April 15, 1990 | Multi-format regex |
| address | 123 Main St, Springfield, IL 62701 | Pattern + keyword heuristic |
| passport | A12345678, P1234567 | Country-specific patterns |
| drivers-license | D123-4567-8901, F123-456-78-910-1 | Multi-state/country patterns |
| national-id | Aadhaar, NHS number, NINO, CPF, etc. | Country-specific patterns |
Financial
| Category | Examples | Validator / Notes |
| ---------------- | ----------------------------------- | ---------------------- |
| credit-card | 4111 1111 1111 1111 | Luhn algorithm |
| iban | GB82 WEST 1234 5698 7654 32 | MOD-97 (ISO 13616) |
| bank-account | Routing + account number pairs | Pattern regex |
| cryptocurrency | BTC, ETH, XRP wallet addresses | Format regex per chain |
| tax-id | EIN 12-3456789, VAT GB123456789 | Multi-country patterns |
Healthcare
| Category | Examples | Validator / Notes |
| ------------------ | --------------------------------- | ---------------------- |
| medical-record | MRN: 1234567, MR#00456789 | Keyword + pattern |
| health-insurance | Subscriber ID, Group/Member # | Pattern regex |
| prescription | Rx# 1234567, DEA numbers | Pattern + DEA checksum |
Digital Identity
| Category | Examples | Validator / Notes |
| -------------- | ------------------------------------------ | ---------------------------- |
| ipv4 | 192.168.1.1, 10.0.0.0/8 | Octet-range regex |
| ipv6 | 2001:0db8::8a2e:0370:7334 | Regex |
| url | https://example.com/path?q=1 | http/https (scheme-required) |
| api-key | Bearer tokens, AWS keys, GitHub PATs, etc. | Pattern regex per provider |
| social-media | @username, profile URLs, handles | Pattern regex |
Vehicles & Transportation
| Category | Examples | Validator / Notes |
| ----------------- | ------------------------------------ | ------------------------- |
| vin | 1HGCM82633A004352 | VIN checksum (pos 9) |
| license-plate | ABC-1234, AB12CDE | Multi-country patterns |
| tracking-number | FedEx, UPS, USPS, DHL tracking codes | Pattern regex per carrier |
Government & Legal
| Category | Examples | Validator / Notes |
| ---------------------- | -------------------------------- | ---------------------- |
| case-number | 2023-CV-001234, CR-2022-5678 | Pattern regex |
| company-registration | EIN, CRN, SIREN, ABN, etc. | Multi-country patterns |
Compliance Presets
Built-in presets pre-configure which categories are detected and which default strategy is applied.
| Preset | Categories Covered | Default Strategy |
| --------- | -------------------------------------------------------------------------- | -------------------------------------------------- |
| gdpr | Personal, financial, health and online identifiers (EU GDPR) | pseudonymize |
| lgpd | Personal data, including the CPF (Brazil, LGPD) | redact |
| pipeda | Personal information, including the Social Insurance Number (Canada) | redact |
| hipaa | The HIPAA Safe Harbor identifiers that can be detected in text | redact |
| ccpa | Consumer identifiers, financial and health data, online activity, API keys | redact |
| pci-dss | Card and bank account numbers, cardholder name and contact details | redact; card and account numbers keep the last 4 |
| sox | Financial and corporate officer identifiers for audit trails | redact |
| ferpa | Student PII — name, contact details, date of birth, SSN, national ID | redact |
A preset selects what the library can detect. Some data that a regulation names has no detector —
for HIPAA, ZIP codes, dates other than birth dates, device identifiers and photographs; for PCI DSS,
card verification codes. REGULATIONS[id].gaps from "anonyma/compliance" lists them, with the
provision that names each one.
import { anonymize, getPreset, PRESET_REGISTRY } from "anonyma";
// Apply a preset
anonymize(text, { preset: "hipaa" });
// Inspect a preset's configuration
const hipaa = getPreset("hipaa");
console.log(hipaa.categories); // ["name", "address", "date-of-birth", "phone", ...]To add a category to a preset, compile a pipeline from "anonyma/engine" with the preset's
categories and yours — anonymize() uses the preset's categories when preset is set:
import { compilePipeline } from "anonyma/engine";
const gdprWithKeys = compilePipeline({
preset: "gdpr",
categories: [...getPreset("gdpr").categories, "api-key"],
});
gdprWithKeys.transform("Key AKIAIOSFODNN7EXAMPLE, mail [email protected]").text;
// "Key id_…, mail id_…" — random pseudonyms, since the preset sets no seedReversible Tokenization
tokenize() replaces PII with opaque placeholder tokens and returns a mapping for lossless restoration.
import { tokenize, detokenize } from "anonyma";
const { text, mapping, tokens } = tokenize("[email protected] called 555-867-5309", {
format: "bracket", // "[EMAIL_0001]", "[PHONE_0001]" (default)
// format: "angle", // "<EMAIL_1>", "<PHONE_1>" (LLM-friendly)
});
// text: "[EMAIL_0001] called [PHONE_0001]"
const { text: restored } = detokenize(text, mapping);
// "[email protected] called 555-867-5309"Identical values share a token. TokenizeOptions also declares format: "custom" with
tokenTemplate, deterministic: false and locales, which tokenize() does not apply.
LLM Pipeline Helpers
Sanitize user input before sending it to a language model, then restore PII from the model's response.
import { sanitizeForLLM, restoreFromLLM } from "anonyma";
// 1. Replace PII with bracket tokens before the LLM sees it
const { text: prompt, mapping } = sanitizeForLLM("Send invoice to [email protected]");
// prompt: "Send invoice to [EMAIL_0001]"
// 2. Call your LLM
const response = await callLLM(prompt);
// response: "I have sent the invoice to [EMAIL_0001]."
// 3. Restore original values in the LLM's response
const { text: final, unresolved } = restoreFromLLM(response, mapping);
// final: "I have sent the invoice to [email protected]."
// unresolved: [] (any tokens the LLM dropped are reported here)Batch Processing
Process arrays of strings efficiently. Each item is independent — failures don't abort the batch.
import { anonymizeBatch, anonymizeBatchAsync, tokenizeBatch, detectBatch } from "anonyma";
// Sync batch
const results = anonymizeBatch(["[email protected]", "192.0.2.1", "bad\x00input"]);
for (const r of results) {
if (r.ok) console.log(r.value.text);
else console.error(`Item ${r.index}:`, r.error.message);
}
// Async batch (supports hash/encrypt strategies, runs in parallel)
const asyncResults = await anonymizeBatchAsync(texts, {
defaultStrategy: { strategy: "hash", pepper: "secret" },
concurrency: 10,
});
// Batch tokenization
const tokenResults = tokenizeBatch(texts, { format: "bracket" });
// Batch detection only
const detections = detectBatch(texts);Streaming
Process data through WHATWG TransformStream pipelines (Node ≥ 18 / browsers with Streams API).
import {
createAnonymizeStream,
createAnonymizeStreamAsync,
createTokenizeStream,
} from "anonyma/stream";
// Anonymize line by line
const readable = ReadableStream.from(lines);
const anonymized = readable.pipeThrough(
createAnonymizeStream({ defaultStrategy: { strategy: "mask" } }),
);
for await (const { text } of anonymized) {
process.stdout.write(text + "\n");
}
// Async stream (for hash/encrypt strategies)
const asyncStream = createAnonymizeStreamAsync({ preset: "hipaa" });
// Tokenize stream
const tokenStream = createTokenizeStream({ format: "bracket" });Encryption Strategy
Reversible AES-256-GCM encryption using the Web Crypto API. Requires Node.js ≥ 18.
import { encrypt, decrypt } from "anonyma";
// Encrypt with a passphrase (key derived via PBKDF2)
const ciphertext = await encrypt("[email protected]", { passphrase: "s3cr3t" });
// "base64:<iv>:<ciphertext>"
// Or supply raw key bytes (16 or 32 bytes for AES-128/256)
const ct = await encrypt("[email protected]", { keyBytes: myKeyBytes, encoding: "hex" });
// Decrypt
const original = await decrypt(ciphertext, { passphrase: "s3cr3t" });
// "[email protected]"
// Encrypt what a pipeline detects; the key is passed apart from the specification
import { compilePipeline } from "anonyma/engine";
const pipeline = compilePipeline(
{ rules: { email: { strategy: "encrypt" } } },
{ encryption: { passphrase: "s3cr3t" } },
);
const { text } = await pipeline.transformAsync("Mail [email protected]");
// "Mail base64:<iv>:<ciphertext>"Synthesis Strategy
Replace PII with structurally valid, format-preserving synthetic data. Deterministic when a seed is provided.
import { synthesize } from "anonyma";
import { compilePipeline } from "anonyma/engine";
synthesize("[email protected]", "email", { seed: "project-y" });
// "[email protected]"
compilePipeline({ defaultStrategy: { strategy: "synthesize", seed: "project-z" } }).transform(
"Call 555-867-5309 or email [email protected]",
).text;
// "Call +1-297-841-6744 or email [email protected]"Validators
Standalone checksum and format validators are exported from "anonyma/validators". They are used internally by detectors but can also be used directly.
import {
luhn, // Luhn (ISO/IEC 7812) — credit cards
verhoeff, // Verhoeff — Indian Aadhaar
nhsMod11, // NHS mod-11 — UK NHS numbers
cpfChecksum, // CPF — Brazilian tax IDs
vinChecksum, // VIN position-9 check digit
deaChecksum, // DEA number check — US prescriptions
ibanMod97, // MOD-97 — IBAN (ISO 13616)
ninoValid, // NINO format — UK National Insurance
aadhaarFormat, // Aadhaar format — Indian national ID
} from "anonyma/validators";
luhn("4111111111111111"); // true
ibanMod97("GB82WEST12345698765432"); // true
nhsMod11("943-476-5919"); // trueCustom Detectors
Replace the detector of a built-in category with customDetectors, or catch values of your own
format with customPatterns:
import { anonymize } from "anonyma";
anonymize("Employee EMP-001234 called.", {
customPatterns: [{ pattern: /\bEMP-\d{6}\b/g, label: "[EMPLOYEE_ID]" }],
}).text;
// "Employee [EMPLOYEE_ID] called."The span engine takes detectors for categories of your own, each with its own strategy:
import {
constant,
createPipeline,
defineRegexDetector,
emailDetector,
redactWith,
} from "anonyma/engine";
const employeeId = defineRegexDetector({
category: "employee-id",
pattern: /\bEMP-\d{6}\b/g,
confidence: 0.95,
requires: ["EMP-"], // skip texts that cannot contain a hit
});
const pipeline = createPipeline({
detectors: [emailDetector, employeeId],
replace: { fallback: redactWith(), byCategory: { "employee-id": constant("[EMPLOYEE]") } },
});
pipeline.transform("EMP-001234 wrote from [email protected]").text;
// "[EMPLOYEE] wrote from [REDACTED]"AnonymaPlugin and the plugins option of createAnonymizer() are declared in the types, but
createAnonymizer() does not apply plugins.
AI Integration
OpenAI / Anthropic Function Calling
import {
ANONYMIZE_TOOL_DEFINITION,
DETECT_TOOL_DEFINITION,
HAS_PII_TOOL_DEFINITION,
ANONYMIZE_OBJECT_TOOL_DEFINITION,
} from "anonyma/schemas";
// Pass directly to the tools array:
const response = await openai.chat.completions.create({
model: "gpt-4o",
tools: [
{ type: "function", function: ANONYMIZE_TOOL_DEFINITION },
{ type: "function", function: HAS_PII_TOOL_DEFINITION },
{ type: "function", function: ANONYMIZE_OBJECT_TOOL_DEFINITION },
],
messages: [{ role: "user", content: "Anonymize: [email protected]" }],
});MCP Tool Definition
import { ANONYMA_MANIFEST } from "anonyma/schemas";
// Use the capability manifest to let an AI agent discover anonyma's tools:
const manifest = ANONYMA_MANIFEST;
console.log(manifest.capabilities.anonymize.strategies);Runtime Validation with Zod
import { AnonymizeOptionsSchema } from "anonyma/schemas";
import { anonymize } from "anonyma";
// Validate untrusted input (e.g. from an HTTP request):
const opts = AnonymizeOptionsSchema.parse(req.body.options);
const result = anonymize(req.body.text as string, opts);The schema covers rules, defaultStrategy, customPatterns, enabledCategories,
globalReplacement, consistentTokens, aggressive and includeMatches. parse() removes the
other options, such as preset, allowlist, confidenceThreshold and locales. To accept a
whole pipeline from untrusted input, check a policy document with parsePolicy() from
"anonyma/compliance" instead.
Subpath Imports
| Import path | Contents |
| ---------------------- | ------------------------------------------------------------------------------------------ |
| "anonyma" | Core API — no Zod dependency |
| "anonyma/detectors" | All detectors + DETECTOR_REGISTRY + AGGRESSIVE_DETECTOR_REGISTRY |
| "anonyma/schemas" | Zod schemas, JSON schemas, AI/MCP tool definitions (requires zod) |
| "anonyma/validators" | Standalone checksum validators (Luhn, MOD-97, NHS, VIN, etc.) |
| "anonyma/stream" | WHATWG TransformStream wrappers (Node ≥ 18 / browsers) |
| "anonyma/crypto" | Low-level Web Crypto utilities |
| "anonyma/engine" | Span engine: createPipeline, compilePipeline, detectors, replacers, chunk-safe streams |
| "anonyma/vault" | Session, keyed and sealed tokenizers; key ring with rotation; token vault |
| "anonyma/audit" | Hash-chained audit logger without personal data; chain verification |
| "anonyma/compliance" | Regulation profiles with citations, policy parser, erasure planning |
| "anonyma/ai" | Structure-preserving JSON and chat-message redaction; stream restoration |
| "anonyma/mcp" | MCP tool and resource declarations; dependency-free MCP server |
| "anonyma/middleware" | HTTP payload scrubber; adapters in /express and /hono |
Span Engine, Tokenization, Audit and AI Modules
The subpaths anonyma/engine, anonyma/vault, anonyma/audit, anonyma/compliance,
anonyma/ai, anonyma/mcp and anonyma/middleware were added in 1.1.0. They are a second,
span-based API next to the functions above, which keep their 1.0 behaviour, and they follow
semantic versioning like the rest of the package.
Span engine ("anonyma/engine")
A pipeline is compiled once and reused. Detectors report offsets, overlapping detections are resolved so that no detected character is left in the output, and the output is assembled in one pass. Email addresses, US SSNs, IBANs, IP addresses and card numbers are found by validating detectors (checksums, issuer ranges, allocation rules) that reject look-alikes such as version strings; the other categories use the 1.0 detectors.
import { compilePipeline, createPipelineStream } from "anonyma/engine";
const pipeline = compilePipeline({ preset: "hipaa" });
const { text, spans } = pipeline.transform("Patient [email protected], SSN 123-45-6789");
// text: "Patient [REDACTED], SSN [REDACTED]"
// spans: offsets, categories and detectors, never the detected text
// Text that arrives in arbitrary chunks, with values split between them:
const scrubbed = source.pipeThrough(createPipelineStream(pipeline));compilePipeline() takes plain data — a preset, categories, strategies, patterns — so a pipeline
can live in a configuration file; keys and seeds are passed next to it, never inside it.
createPipeline() builds one from detectors and replacers imported one by one, so that bundlers
keep only those.
Keyed tokenization ("anonyma/vault")
| Tokenizer | Reversible | Same value, same token | Token |
| ---------------------------------------- | ---------------------- | ----------------------------------- | ------------------------- |
| createSessionTokenizer() | within the session | within the session | [EMAIL_0001] |
| createKeyedTokenizer() with a vault | through the vault | across calls and processes, per key | [EMAIL_k1_7ZK3M9QW2ABC] |
| createKeyedTokenizer() without a vault | no | across calls and processes, per key | [EMAIL_k1_7ZK3M9QW2ABC] |
| createSealedTokenizer() | with the key, no vault | across calls and processes, per key | [EMAIL_k1.5wq…] |
import { compilePipeline } from "anonyma/engine";
import {
createKeyedTokenizer,
createKeyRing,
createMemoryVault,
createSessionTokenizer,
generateKeyMaterial,
restoreTokens,
} from "anonyma/vault";
// One conversation: tokens that only this session can restore
const session = createSessionTokenizer();
const tokenizing = compilePipeline(
{ defaultStrategy: { strategy: "tokenize" } },
{ tokenization: session },
);
tokenizing.transform("Email [email protected]").text; // "Email [EMAIL_0001]"
session.restore("Sent to [EMAIL_0001].").text; // "Sent to [email protected]."
// Stable tokens across processes, restorable through a vault
const keyring = await createKeyRing({
namespace: "crm",
keys: [{ id: "k1", material: await generateKeyMaterial() }],
});
const keyed = createKeyedTokenizer({ keyring, vault: createMemoryVault() });
const crm = compilePipeline({ defaultStrategy: { strategy: "tokenize" } }, { tokenization: keyed });
const { text } = await crm.transformAsync("Mail [email protected]"); // "Mail [EMAIL_k1_Y5DMH9ED2DTF]"
(await restoreTokens(text, keyed)).text; // "Mail [email protected]"Keys are versioned and derived per purpose with HKDF. keyring.rotate() adds a version,
rewrapVault() re-seals the vault under it without changing any token, and shredKey() destroys a
version so that nothing sealed under it can be recovered, not even from a backup of the vault.
forgetSubject() deletes the records of one data subject. createMemoryVault() is the reference
implementation of the TokenVault interface; implement it over your own database.
Audit trail ("anonyma/audit")
A record says what was replaced — fields, categories, detectors, rules, counts — and never what the data was. Each record's hash covers its predecessor, so removing, reordering or editing a record breaks the chain; with a key, the hash is an HMAC. When a sink fails, the logger stops instead of leaving a gap.
import { createAuditLogger, memorySink, summarizeSpans, verifyAuditChain } from "anonyma/audit";
const sink = memorySink();
const audit = createAuditLogger({ sinks: [sink] });
await audit.record({
operation: "anonymize",
policy: { id: "hipaa" },
fields: summarizeSpans(spans, { rule: "redact" }),
});
(await verifyAuditChain(sink.records())).ok; // trueRegulation profiles and policies ("anonyma/compliance")
REGULATIONS maps GDPR, LGPD, PIPEDA, CCPA/CPRA, HIPAA and PCI DSS to the categories they bring
into scope, the provision behind each one, the protection a replacement must give, and the data
they name that no detector covers. A policy document extends regulations and is checked against
them:
import { compilePipeline } from "anonyma/engine";
import { parsePolicy, policyToSpec } from "anonyma/compliance";
const policy = parsePolicy({ version: 1, id: "clinic", extends: ["hipaa", "gdpr"] });
const clinic = compilePipeline(policyToSpec(policy));
parsePolicy({
version: 1,
id: "clinic",
extends: ["hipaa"],
rules: { email: { strategy: "mask", keepLeading: 3 } },
});
// throws PolicyError: /rules/email "mask" is not sufficient for "email" under HIPAA …
// (45 CFR § 164.514(b)(2)(i)(F))The profiles describe technical measures. They are not legal advice, and they do not make a system compliant on their own.
LLM guard and JSON ("anonyma/ai")
import { createLlmGuard, sanitizeJson } from "anonyma/ai";
const exchange = createLlmGuard().begin();
const { messages } = exchange.sanitizeMessages([
{ role: "user", content: "Email [email protected]" },
]);
// messages[0].content: "Email [EMAIL_0001]"; the mapping never leaves the process
const reply = await callLLM(messages); // "Sent to [EMAIL_0001]."
exchange.restoreText(reply).text; // "Sent to [email protected]."
// Streamed output: stream.pipeThrough(exchange.restoreStream()), tokens split across chunks too
sanitizeJson({ user: { email: "[email protected]" }, id: 7 }, { pipeline }).value;
// { user: { email: "[REDACTED]" }, id: 7 } — same shape, sanitized stringstoLanguageModelMiddleware() adapts a guard to the middleware of the Vercel AI SDK
(wrapLanguageModel()). It is typed structurally and tested against stand-ins for the SDK, so check
it against the SDK version you use.
MCP server ("anonyma/mcp")
A Model Context Protocol server without dependencies. Its tools are anonyma_detect,
anonyma_anonymize, anonyma_tokenize and anonyma_check_policy, plus anonyma_detokenize when
the operator enables it (allowDetokenize). The values behind tokens stay in server memory, and
anonyma_detect reports positions, not the detected text.
// anonyma-mcp.mjs — register with: claude mcp add anonyma -- node anonyma-mcp.mjs
import { createMcpServer, serveStdio } from "anonyma/mcp";
await serveStdio(createMcpServer(), {
input: process.stdin,
write: (line) => void process.stdout.write(line),
});HTTP middleware ("anonyma/middleware")
import express from "express";
import { anonymaExpress } from "anonyma/middleware/express";
const app = express();
app.use(express.json());
// Scrubs responses and, with `request: true`, the parsed request bodies
app.use("/support", anonymaExpress({ spec: { preset: "gdpr" }, request: true }));anonymaHono() from "anonyma/middleware/hono" does the same for Hono, on every runtime Hono
supports. createScrubber() from "anonyma/middleware" scrubs JSON values, text and Fetch API
responses directly. Neither adapter depends on its framework.
Errors
The library's errors are AnonymaErrors with a machine-readable code. The classes are exported
from "anonyma", including those thrown by the subpaths: ValidationError,
UnsupportedStrategyError, UnknownCategoryError, CryptoNotAvailableError, EncryptionError,
PresetNotFoundError, AllowlistMatchError, BatchProcessingError, AsyncStrategyError,
KeyManagementError, TokenVaultError, PolicyError and AuditIntegrityError.
import { AnonymaError, PolicyError } from "anonyma";
import { parsePolicy } from "anonyma/compliance";
try {
parsePolicy(document);
} catch (error) {
if (error instanceof PolicyError) {
for (const issue of error.issues) console.error(issue.path, issue.message);
} else if (error instanceof AnonymaError) {
console.error(error.code, error.message);
}
}API Documentation
Full API reference: docs/api.md
Requirements
- Node.js ≥ 18 (for
hash,encrypt, and streaming features — Web Crypto +TransformStream) - TypeScript ≥ 5.0 (optional)
- Zod ≥ 3.23 (optional peer-dependency for
"anonyma/schemas")
Contributing
See CONTRIBUTING.md.
License
MIT — see LICENSE.
