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

pii-mask

v0.1.0

Published

A fast, config-driven Node.js/TypeScript masking adapter that masks PII in structured JSON before it is sent to an LLM.

Readme

pii-mask

A fast, config-driven Node.js/TypeScript adapter that masks PII in structured JSON before it is sent to an LLM, and restores the real values in the LLM's response.

Raw JSON → mask() → Masked JSON → LLM → Masked Response → unmask() → Final Response

What this is — and isn't

pii-mask is a data-minimization masking adapter. It replaces PII values in JSON with reversible tokens before the JSON leaves your process, so an LLM (or any third party) never sees the raw values.

  • It is not a KVKK/GDPR (or any other) compliance guarantee. Using this library does not by itself make a system compliant with any privacy law — that depends on your whole system, your legal basis for processing, your retention policy, and more. Treat it as one control among several.
  • It is not a PDF/document parser. Input is structured JSON only. If your PII lives in PDFs, scanned images, or free-form documents, extract the JSON first.
  • Detectors are deterministic (regex + checksum) — there is no ML/NLP model involved, and free-text detection is limited (see Detectors below).

Quickstart

npm install pii-mask

API

import { createMasker } from "pii-mask";
import { FileVaultStorage } from "pii-mask/storage";

const masker = createMasker({
  storage: new FileVaultStorage({ path: "./vault.enc", secret: process.env.MASKING_SECRET }),
});

const maskResult = await masker.mask(inputJson, {
  tenantId: "insurance-company-a",
  mode: "reversible", // or "irreversible"
  config,
});

const llmResult = await callLLM(maskResult.maskedJson);

const finalResult = await masker.unmask({
  sessionId: maskResult.sessionId,
  data: llmResult,
});

maskResult is { sessionId, maskedJson, report: { maskedFields, warnings } }. finalResult is { unmaskedJson, warnings }.

CLI

The same flow from the command line, using the files in examples/:

MASKING_SECRET=some-long-random-secret npx pii-mask mask \
  examples/insurance-claim.json \
  --config examples/mask-config.json \
  --mode reversible \
  --storage file --vault ./vault.enc \
  --out ./claim.masked.json

# claim.masked.json now holds only tokens — send it to the LLM.
# The command also prints { sessionId, report } (counts + warnings) to stdout,
# never the raw input.

# ... call your LLM, save its JSON response as llm-response.json ...

MASKING_SECRET=some-long-random-secret npx pii-mask unmask \
  llm-response.json \
  --session-id <the sessionId from the mask step> \
  --storage file --vault ./vault.enc \
  --out ./final-response.json

Full CLI surface:

pii-mask mask <input.json> --config <mask-config.json> \
  --mode reversible|irreversible --storage memory|file \
  [--vault <path>] [--tenant <id>] [--ttl <seconds>] --out <output.json>

pii-mask unmask <llm-response.json> --session-id <id> \
  --storage file --vault <path> [--tenant <id>] --out <output.json>

--storage memory only makes sense with --mode irreversible: each CLI invocation is a separate process, so an in-memory session cannot be read back by a later unmask call. Reversible masking always needs --storage file (or the equivalent FileVaultStorage in code). The secret always comes from the MASKING_SECRET environment variable — it is never passed as a flag or logged.

Config reference

{
  "fields": {
    "customer.name": "NAME",
    "customer.tckn": "TCKN",
    "customer.phone": "PHONE",
    "vehicle.plate": "PLATE",
    "payment.iban": "IBAN",
    "claim.policyNo": "POLICY_NO",
    "documents[].invoiceNo": "INVOICE_NO"
  },
  "scanTextFields": {
    "claim.description": ["NAME", "TCKN", "IBAN", "PHONE", "PLATE"],
    "notes[]": ["NAME", "PHONE", "ADDRESS"]
  }
}
  • fields — direct masking. The dotted path is looked up and, if present, the whole value is replaced with a token. No regex is used, so this works for any value shape (a number, a UUID, an address — anything).
  • scanTextFields — free-text scanning. The value at the path is scanned with only the detectors listed for that path, and matches are replaced in-place; the surrounding text is preserved byte-for-byte.
  • a.b[].c[] after a segment means "this segment is an array; apply the rest of the path to each element." notes[] (an array of strings) is scanned element-by-element.
  • A value repeated across the document (e.g. the same TCKN in a direct field and inside claim.description) always gets the same token, so the LLM sees one entity per real-world value.

Token format

{{PII:<TYPE>:<ID>}}
e.g. {{PII:NAME:01J4Z7Q9K3D8}}  {{PII:TCKN:01J4Z7Q9N6VA}}

<ID> is 12 random, non-sequential characters (Crockford Base32, from crypto.randomBytes) — never a counter, never derived from the value.

Modes

  • reversible — mappings are persisted (encrypted) so unmask() can restore the real values later, scoped to a session.
  • irreversible — nothing is persisted; the session cannot be unmasked. Useful when you only need one-way redaction.

Storage adapters

  • InMemoryStorage — process-local, non-persistent. Fine for tests, demos, and irreversible mode; a reversible session in memory cannot survive a process restart or be read from another process.
  • FileVaultStorage — a single AES-256-GCM–encrypted file on disk (vault.enc). Key material is derived from MASKING_SECRET via scrypt; writes are atomic (write-temp-then-rename). See SECURITY.md for its single-process caveat.

Roadmap (not implemented yet, but the StorageAdapter interface is designed to support them without changing the public API): Postgres, Redis, MongoDB, AWS KMS, GCP KMS, Azure Key Vault, HashiCorp Vault, and streaming JSON input (current input is whole-document JSON only).

Detectors

Built-in, deterministic (regex + checksum — no ML/NLP), used by scanTextFields:

| Type | What it matches | Checksum-validated | |---------|-------------------------------------------|---------------------| | TCKN | Turkish national ID (11 digits) | Yes (official algorithm) | | VKN | Turkish tax ID (10 digits) | Yes | | IBAN | Turkish IBAN (TR..) | Yes (mod-97) | | PHONE | Turkish phone numbers (+90/0090/0 prefixes)| Format only | | PLATE | Turkish vehicle plates | Format only |

A candidate match that fails its checksum is never masked — this avoids false positives from arbitrary 10/11-digit numbers.

Limitation: NAME and ADDRESS have no built-in text detector in the MVP. There is no reliable regex for free-text name/address detection. Direct field masking (fields) still fully covers NAME/ADDRESS when they live at a known JSON path — the gap is only for names/addresses embedded inside free text. scanTextFields entries listing NAME or ADDRESS are skipped with a one-time warning (detector not available: NAME (direct field masking still applies)) and reported in report.warnings. To close this gap, plug in a custom detector via MaskOptions.detectors (e.g. backed by an NER model) — no library code needs to change.

Security

See SECURITY.md for the full list of guarantees, the threat model, MASKING_SECRET handling, and responsible disclosure. In short: raw values are never logged; persisted values are always AES-256-GCM encrypted; hashes are always HMAC-SHA256(secret, value); sessions are TTL- and tenant-scoped and fail closed.

License

MIT — see LICENSE.