Cross-site scripting (XSS) is still one of the top risks on the OWASP Top 10. Every time you render user-generated content—comments, rich text, profile bios—you're opening a door. The standard fix is sanitization: strip or escape dangerous HTML before it hits the DOM. There are two different jobs hiding behind that one word. One is preserving safe HTML while removing the dangerous parts — what DOMPurify and sanitize-html do, and the harder problem. The other is emitting untrusted content as plain text, where nothing needs to survive. I built Purifai for the second job, in a footprint small enough for edge runtimes with no DOM available.

Purifai is a zero-dependency strip-to-text sanitizer: it removes markup rather than allow-listing safe tags. That leaves no retained markup for a parser to mutate on re-parse, and it places Purifai in a different category from DOMPurify, which exists to keep safe formatting. It's TypeScript-native, needs no DOM, and runs in Node, browsers, edge runtimes, and workers. The full package build is about 4.5 KB gzipped; a tree-shaken sanitize import is 3.5 KB minified / 1.6 KB gzipped.

<MediaContainer src="/projects/purifai/hero.webp" alt="Abstract tangled input passing through a filter into clean output" />

Quick Start

Install it:

npm install purifai
# or
pnpm add purifai

Use it:

import { Purifai } from 'purifai';

// Simple sanitization
const clean = Purifai.sanitize('<script>alert("xss")</script>Hello World');
console.log(clean); // "Hello World"

// Bound work for application-sized inputs
const safe = Purifai.sanitize(userInput, {
  maxLength: 10000
});

That's it. No DOM and no runtime dependencies. The returned value is plain text for normal framework text interpolation.

Security and Content Fidelity

The benchmark inserts each sanitizer's output into a real DOM (jsdom), serializes and re-parses it, then measures executable output, exact benign-text fidelity, retained markup, and raw-container body removal separately:

LibraryCategoryNo executable output*Exact textMarkup keptRaw body removedMedian ops/sec†
Purifai.sanitizestrip-to-text100%100%0% (by design)100%526,709
Purifai.escapeencode-as-text100%40%0%0%596,421
striptagsstrip-to-text100%100%0%20%633,697
DOMPurify (jsdom)preserve-safe-html100%100%100%60%1,686
sanitize-htmlpreserve-safe-html100%100%100%60%88,492
xsspreserve-safe-html100%100%100%0%335,448
rehype-sanitizepreserve-safe-html100%100%100%40%24,522
escape-htmlencode-as-text100%40%0%0%2,238,804
validator.escapeencode-as-text100%40%0%0%735,565
entities.escapeUTF8encode-as-text100%40%0%0%1,208,824
html-entitiesencode-as-text100%40%0%0%869,440
he.escapeencode-as-text100%40%0%0%952,948

84 attack vectors (OWASP, PortSwigger, cure53 corpora) · 15 benign documents.

* This is an observed corpus result, not a security guarantee. † Throughput is the median of seven warmed-up samples captured on 2026-08-02 with Node 26.3.0 on Apple Silicon. It varies by hardware and is only comparable within one category. In this snapshot striptags is smaller and faster; Purifai's measured differentiation is exact benign text together with complete raw-container body removal and bounded malformed-input scaling. Run pnpm benchmark to reproduce the current result.

Where Purifai Fits

DOMPurify and sanitize-html preserve safe markup, which is the right choice for rich text. Purifai is designed for places where the output should be text: labels, previews, notifications, search indexes, logs, and API pipelines. Removing markup also means it can run without a browser DOM or a server-side DOM shim.

The choice is about output requirements. Keep a preserve-HTML sanitizer when formatting must survive; use Purifai when plain text is the intended result.

How Purifai Approaches the Problem

Purifai uses a bounded forward scanner without relying on a runtime DOM. It:

  1. Decodes syntax-relevant encodings — Numeric entities plus encoded angle brackets and JavaScript-style character escapes are normalized without turning prose such as 100%20off into different text.
  2. Consumes markup once — The scanner advances through the input and avoids the repeated suffix scans that made the previous regular-expression pipeline quadratic.
  3. Drops raw containers as regions — Script, style, iframe, SVG, template, and related container bodies do not leak into the resulting text.
  4. Separates output contextssanitize, escape, escapeAttribute, and escapeUrl have distinct contracts instead of treating one transformation as safe everywhere.

Key Features

Zero dependencies. No DOM, jsdom, or cheerio at runtime. The full package build is about 15.0 KB / 4.5 KB gzip, and a bundled, tree-shaken sanitize import measures 3.5 KB / 1.6 KB gzip. Works in Node and the browser.

Threat analysis. Use analyze() when you need more than sanitization—logging, blocking, or incident response:

import { analyze } from 'purifai';

const result = analyze('<script>alert("hack")</script>User content');
console.log(result.content);     // "User content"
console.log(result.hadThreats);  // true
console.log(result.threatLevel); // "critical"

if (result.hadThreats) {
  console.warn('Potential XSS detected', { level: result.threatLevel });
}
broadcast(result.content); // Plain text; render through normal text interpolation

Batch processing. Sanitize multiple strings at once for APIs or content pipelines:

import { sanitizeBatch } from 'purifai';

const cleanData = sanitizeBatch([
  '<script>alert(1)</script>Hello',
  '<img src=x onerror=alert(1)>World',
  'Safe content'
]);
// ["Hello", "World", "Safe content"]

Danger check. isDangerous() is advisory telemetry for logging and review; it is not an authorization gate.

Choosing Purifai over a Preserve-HTML Sanitizer

Purifai is not a drop-in replacement when your product needs to retain rich-text formatting. Its API is straightforward to adopt when the desired output is plain text.

When Migration Makes Sense

  • Plain-text output — Markup does not need to survive
  • Edge and worker runtimes — No browser DOM or jsdom available
  • Small dependency budget — A compact, zero-dependency package
  • TypeScript projects — Typed APIs without separate declarations

From DOMPurify

Before:

import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirty);

After:

import { sanitize } from 'purifai';
const clean = sanitize(dirty);

If you were using DOMPurify in Node with jsdom, you no longer need jsdom—Purifai doesn't use the DOM.

From sanitize-html

Before:

import sanitizeHtml from 'sanitize-html';
const clean = sanitizeHtml(dirty, {
  allowedTags: ['b', 'i', 'em', 'strong', 'p'],
  allowedAttributes: { a: ['href'] },
});

After:

import { sanitize } from 'purifai';
const clean = sanitize(dirty, {
  maxLength: 50000
});

This migration intentionally removes formatting. If safe tags such as <b> or <a> must survive, keep sanitize-html or another preserve-HTML sanitizer.

Edge Cases

URL protocols. URL handling belongs to escapeUrl, not sanitize. The optional list can narrow the built-in http, https, and mailto set; it cannot enable executable or custom schemes:

import { escapeUrl } from 'purifai';

escapeUrl(candidate, { allowedProtocols: ['https'] });

Max length. Cap input size to avoid DoS:

sanitize(dirty, { maxLength: 10000 });

Batch processing. For many strings (e.g. API request bodies):

import { sanitizeBatch } from 'purifai';
const cleanData = sanitizeBatch(Object.values(request.body));

Testing Strategy

Don't switch cold. Run both sanitizers in parallel during rollout:

  1. Install Purifai alongside your current library.
  2. Add a comparison layer in development or staging:
import { sanitize as purifaiSanitize } from 'purifai';
import DOMPurify from 'dompurify';

function sanitizeWithComparison(input: string) {
  const purifaiResult = purifaiSanitize(input);
  const dompurifyResult = DOMPurify.sanitize(input);
  if (purifaiResult !== dompurifyResult) {
    console.warn('Sanitizer output differs', { input, purifaiResult, dompurifyResult });
  }
  return purifaiResult;
}
  1. Log differences—Purifai may strip more because its default output is plain text.
  2. Run your test suite. Fix any legitimate content that gets over-stripped.
  3. Deploy, then remove the old library and comparison code.

Quick Checklist

  • Install: pnpm add purifai
  • Replace imports: sanitize from purifai
  • Map sanitize's maxLength option and any escapeUrl protocol narrowing
  • Remove jsdom (if you only had it for DOMPurify)
  • Run both sanitizers in parallel in staging
  • Run tests, fix any over-stripping
  • Deploy and remove old dependency

Wrap-up

Purifai offers a compact option for turning untrusted markup into text without a DOM dependency. If your product needs rich text, keep a sanitizer built to preserve safe HTML; if it needs text, Purifai keeps that path small and portable.

Purifai is on npm and open source on GitHub. Run the benchmark yourself with pnpm benchmark; the suite compares security, content fidelity, and throughput across the major Node-compatible sanitizer categories. The README and GitHub issues cover usage and edge cases.