npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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,

Readme

anonyma

A modern, zero-dependency TypeScript library for PII detection and data anonymization — built for the AI era.

npm version CI Coverage License: MIT TypeScript


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, sox and ferpa presets
  • 🌍 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 TransformStream wrappers (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 RegExp patterns, fully replace per-category detectors, or define detectors for your own categories
  • 🚫 Allowlist support — skip known-safe values by exact string or RegExp pattern
  • 🔢 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, aadhaarFormat via "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 anonyma

For runtime validation and AI schema features, also install the optional peer dependency:

npm install zod

Quick 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 seed

Reversible 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"); // true

Custom 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; // true

Regulation 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 strings

toLanguageModelMiddleware() 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.