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

prompt-sanitizer

v1.1.0

Published

Lightweight, tiered, bidirectional PII sanitizer for LLM pipelines

Readme

prompt sanitizer

TypeScript-first PII sanitization for LLM pipelines in Node.js.

  • npm: prompt-sanitizer
  • Node: >= 18
  • Required deps: none in Mode.FAST
  • Optional extras: @huggingface/transformers for NER, @faker-js/faker for realistic synthetic replacements
  • Formats: ESM + CJS + bundled .d.ts
  • Exports: main entrypoint + integration sub-paths

Install

Base install:

npm install prompt-sanitizer

Optional NER for Mode.SMART / Mode.FULL:

npm install prompt-sanitizer @huggingface/transformers

Optional realistic synthetic replacements:

npm install prompt-sanitizer @faker-js/faker

Everything:

npm install prompt-sanitizer @huggingface/transformers @faker-js/faker

Quick start

import { Mode, Sanitizer } from "prompt-sanitizer";
const sanitizer = new Sanitizer({ mode: Mode.FAST });
const result = await sanitizer.sanitize(
  "Contact [email protected] or 555-123-4567",
);
console.log(result.text);
console.log(result.tokens);
console.log(result.entities.length > 0); // has PII?
console.log(result.score); // 0.0 - 1.0

Why this package

  • LLM-native: sanitize before the model, restore after the model
  • Tiered modes: FAST, SMART, FULL
  • Zero required dependencies: safe default install path
  • TypeScript-first: typed public API with ESM and CJS support
  • Framework-ready: Vercel AI, Express, Next.js, LangChain.js, LlamaIndex.TS

Export map

.
./integrations/vercel-ai
./integrations/express
./integrations/nextjs
./integrations/langchain
./integrations/llamaindex

Modes

| Mode | Use case | Extra install | Detection stack | | ------------ | --------------------------------- | --------------------------- | ---------------------------- | | Mode.FAST | fastest path, serverless defaults | none | regex + secrets | | Mode.SMART | stronger unstructured detection | @huggingface/transformers | FAST + NER | | Mode.FULL | NER + audit logging | @huggingface/transformers | SMART + automatic AuditLog |

Mode.FAST

import { Mode, Sanitizer } from "prompt-sanitizer";
const sanitizer = new Sanitizer({ mode: Mode.FAST, onDetect: "redact" });
const result = await sanitizer.sanitize(
  "Card 4111 1111 1111 1111, token sk-test-123456",
);
console.log(result.text);
console.log(result.entities.map((e) => e.entityType));

Mode.SMART

import { Mode, Sanitizer } from "prompt-sanitizer";
const sanitizer = new Sanitizer({ mode: Mode.SMART, nerSilent: true });
const result = await sanitizer.sanitize(
  "Alice from Acme Corp is meeting in Nairobi tomorrow.",
);
console.log(result.entities);

Mode.FULL

import { AuditLog, Mode, Sanitizer } from "prompt-sanitizer";
const audit = new AuditLog();
const sanitizer = new Sanitizer({ mode: Mode.FULL, auditLog: audit });
await sanitizer.sanitize("Email [email protected] and notify Acme Corp.");
console.log(audit.events());
console.log(audit.export({ format: "csv" }));

If @faker-js/faker is installed, replacements can look more realistic. Without it, the package falls back to placeholder tokens such as [EMAIL_1].

NER / Mode.SMART

Default model:

Xenova/bert-base-NER

Notes:

  • first use downloads the model once (~65 MB)
  • the model is cached after download
  • nerSilent: true falls back cleanly if transformers is missing
  • nerSilent: false throws instead of silently falling back

Override the model:

const sanitizer = new Sanitizer({
  mode: Mode.SMART,
  nerModel: "Xenova/bert-base-NER",
  nerSilent: false,
});

Free model memory:

await sanitizer.dispose();

Sessions: anonymize now, deanonymize later

Sessions keep a stable mapping so the same PII becomes the same replacement across a conversation.

import { Sanitizer } from "prompt-sanitizer";
const sanitizer = new Sanitizer();
const session = sanitizer.session("chat-42");
const clean = await session.anonymize(
  "Alice's email is [email protected]. Draft a reply.",
);
const llmReply = `I will contact ${clean} today.`;
const finalReply = session.deanonymize(llmReply);
console.log(clean);
console.log(finalReply);

Useful members: anonymize(), anonymizeWithResult(), deanonymize(), reset(), size, mapping, sessionId.

Persisting sessions across restarts

By default a session's vault lives only in process memory. Pass a VaultStore to reattach to the same mapping later by sessionId — e.g. after a worker restart or serverless cold start:

import { FileVaultStore } from "prompt-sanitizer";

const store = new FileVaultStore("./vault-data");
const session = await sanitizer.session("chat-42", { store }); // async: loads any existing snapshot first
const clean = await session.anonymize("Alice's email is [email protected]");
await session.persist();

// ...later, possibly in a new process:
const resumed = await sanitizer.session("chat-42", { store });
const finalReply = resumed.deanonymize(llmReply);

InMemoryVaultStore is the zero-dependency, same-process reference store; FileVaultStore persists to disk (Node builtins only). Pass { store, autoPersist: true } to persist automatically after every anonymize() call instead of calling persist() yourself. No store is active unless you pass one.

guard()

Wrap a function so all string arguments are sanitized before invocation.

import { Sanitizer } from "prompt-sanitizer";
const sanitizer = new Sanitizer();
const safeCall = sanitizer.guard(
  async (prompt: string) => `Model saw: ${prompt}`,
  "redact",
);
console.log(await safeCall("Reach me at [email protected]"));

onDetect values: "redact", "warn", "block".

Custom entities

Use addEntity() for domain-specific identifiers.

import { Sanitizer } from "prompt-sanitizer";
const sanitizer = new Sanitizer();
sanitizer.addEntity("EMPLOYEE_ID", /EMP-\d{6}/g, {
  confidence: 0.98,
  validator: (match) => match.startsWith("EMP-"),
});
const result = await sanitizer.sanitize(
  "Reviewer EMP-123456 approved the request.",
);
console.log(result.text);
console.log(result.entities);

Audit logging

AuditLog records detection events without storing raw PII.

import { AuditLog, Mode, Sanitizer } from "prompt-sanitizer";
const audit = new AuditLog();
const sanitizer = new Sanitizer({ mode: Mode.FULL, auditLog: audit });
await sanitizer.sanitize("[email protected] logged in from 203.0.113.10");
console.log(audit.events());
console.log(audit.export({ format: "json" }));
console.log(audit.export({ format: "csv", since: "1h" }));

Recorded fields include timestamp, entity type, confidence, detection layer, redaction method, hashed value fingerprint, and optional session ID.

Streaming with Vercel AI

import {
  wrapGenerate,
  wrapStream,
} from "prompt-sanitizer/integrations/vercel-ai";

Full streaming example:

import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";
import { Mode, Sanitizer } from "prompt-sanitizer";
import { wrapStream } from "prompt-sanitizer/integrations/vercel-ai";
const sanitizer = new Sanitizer({ mode: Mode.FAST });
const safeStreamText = wrapStream(sanitizer, streamText);
const result = await safeStreamText({
  model: openai("gpt-4o-mini"),
  system: "Be concise.",
  prompt: "Email Alice at [email protected] and summarize the request.",
});
let fullText = "";
for await (const chunk of result.fullStream) {
  if (chunk.type === "text-delta") fullText += chunk.textDelta ?? "";
}
console.log(fullText);

The wrapper sanitizes prompt, system, and messages, then deanonymizes streamed text-delta chunks, including tokens split across chunk boundaries.

Express.js

import express from "express";
import { Mode, Sanitizer } from "prompt-sanitizer";
import { createExpressMiddleware } from "prompt-sanitizer/integrations/express";
const app = express();
const sanitizer = new Sanitizer({ mode: Mode.SMART });
app.use(express.json());
app.use(
  createExpressMiddleware(sanitizer, {
    routes: ["/api/chat"],
    fields: ["prompt", "message"],
  }),
);

The middleware sanitizes configured request fields and restores values in JSON or string responses.

Next.js middleware

import { Mode, Sanitizer } from "prompt-sanitizer";
import {
  createNextjsMiddleware,
  matcherConfig,
} from "prompt-sanitizer/integrations/nextjs";
const sanitizer = new Sanitizer({ mode: Mode.FAST });
export default createNextjsMiddleware(sanitizer, {
  routes: ["/api/chat"],
  fields: ["prompt", "message"],
});
export const config = matcherConfig(["/api/chat"]);

This integration is edge-compatible and uses standard Request / Response APIs.

LangChain.js

import {
  PromptSanitizerRunnable,
  SanitizedChain,
  SanitizedLLM,
} from "prompt-sanitizer/integrations/langchain";

Runnable example:

const sanitizeStep = new PromptSanitizerRunnable(sanitizer);
const cleanPrompt = await sanitizeStep.invoke("My email is [email protected]");

LLM wrapper example:

import { ChatOpenAI } from "@langchain/openai";
import { SanitizedLLM } from "prompt-sanitizer/integrations/langchain";
const llm = new ChatOpenAI({ model: "gpt-4o-mini" });
const safeLLM = new SanitizedLLM(llm, sanitizer);
const reply = await safeLLM.invoke(
  "Contact [email protected] with the summary.",
);
console.log(reply);

LlamaIndex.TS

import {
  PromptSanitizerNodePostprocessor,
  PromptSanitizerQueryTransform,
} from "prompt-sanitizer/integrations/llamaindex";
const transform = new PromptSanitizerQueryTransform(sanitizer);
const cleanQuery = await transform.transform(
  "Find records mentioning [email protected]",
);
const postprocessor = new PromptSanitizerNodePostprocessor(sanitizer, {
  preserveOriginal: true,
});
const nodes = await postprocessor.postprocessNodes([
  {
    node: { text: "Alice ([email protected]) approved the request." },
    score: 0.92,
  },
]);
console.log(nodes[0].node.text);
console.log(nodes[0].node.metadata?.__original_text);

TypeScript API

Sanitizer

new Sanitizer(options?: SanitizerOptions)
interface SanitizerOptions {
  mode?: Mode;
  locale?: string;
  entities?: EntityType[];
  onDetect?: "redact" | "warn" | "block";
  auditLog?: AuditLog | boolean;
  nerModel?: string;
  nerSilent?: boolean;
}

Key members:

  • sanitize(text: string): Promise<SanitizeResult>
  • sanitizeBatch(texts: string[]): Promise<SanitizeResult[]>
  • session(sessionId?: string): Session
  • addEntity(name: string, pattern: RegExp, options?: AddEntityOptions): void
  • guard(fn, onDetect?): wrappedFn
  • audit: AuditLog | null
  • ner: NerEngine | null
  • dispose(): Promise<void>

SanitizeResult

interface SanitizeResult {
  text: string;
  original: string;
  entities: DetectedEntity[];
  tokens: Record<string, string>;
  score: number;
}

Use result.entities.length > 0 as your hasPii check.

DetectedEntity

interface DetectedEntity {
  entityType: EntityType;
  value: string;
  start: number;
  end: number;
  confidence: number;
  layer: string;
  replacement?: string;
}

Session

class Session {
  anonymize(text: string): Promise<string>;
  anonymizeWithResult(text: string): Promise<SanitizeResult>;
  deanonymize(text: string): string;
  reset(): void;
  readonly size: number;
  readonly mapping: Record<string, string>;
  readonly sessionId?: string;
}

Vault

class Vault {
  add(original: string, replacement: string): string;
  getReplacement(original: string): string | undefined;
  getOriginal(replacement: string): string | undefined;
  restore(text: string): string;
  clear(): void;
  readonly size: number;
  snapshot(): Record<string, string>;
}

AuditLog

class AuditLog {
  record(event: AuditEvent): void;
  events(since?: string | Date): AuditEvent[];
  export(options?: ExportOptions): string;
  clear(): void;
  readonly size: number;
}

Mode

enum Mode {
  FAST = "fast",
  SMART = "smart",
  FULL = "full",
}

EntityType

Common built-in values:

EMAIL;
PHONE;
SSN;
CREDIT_CARD;
IBAN;
IP_ADDRESS;
MAC_ADDRESS;
URL;
CRYPTO_ADDRESS;
DATE_OF_BIRTH;
PASSPORT;
DRIVING_LICENSE;
PERSON_NAME;
LOCATION;
ORGANIZATION;
AGE;
GENDER;
NATIONALITY;
RELIGION;
API_KEY;
SECRET_KEY;
PASSWORD;
JWT_TOKEN;
PRIVATE_KEY;
DATABASE_URL;
AWS_KEY;
OAUTH_TOKEN;
CUSTOM;

Notes

  • FAST is the best default for latency-sensitive paths
  • SMART and FULL lazy-load NER on first use
  • await sanitizer.dispose() releases NER model memory
  • missing @faker-js/faker does not break sanitization
  • session-based flows are the safest way to preserve meaning across model calls

License

MIT