purifai 2.0.2 β†’ 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,432 +1,268 @@
1
- # πŸ›‘οΈ Purifai
2
-
3
- [![npm version](https://badge.fury.io/js/purifai.svg)](https://badge.fury.io/js/purifai)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
- [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
6
- [![Zero Dependencies](https://img.shields.io/badge/dependencies-zero-green.svg)](https://www.npmjs.com/package/purifai)
7
-
8
- > **Zero-dependency strip-to-text sanitizer with contextual output encoding**
9
-
10
- Purifai is a lightweight, zero-dependency sanitizer for the **strip-to-text** case:
11
- untrusted content that must be displayed as text, never as markup. It removes all
12
- HTML rather than allow-listing safe tags, which makes it immune to mutation XSS by
13
- construction β€” nothing survives for a parser to mutate on re-parse.
14
-
15
- **Reach for Purifai when** you want untrusted input rendered as plain text, with no
16
- dependencies, no DOM, and a ~4.1 KB gzipped footprint that runs in Node, browsers,
17
- and edge runtimes alike.
18
-
19
- **Reach for DOMPurify or sanitize-html when** you need to *keep* safe formatting
20
- such as `<b>` and `<a href>`. That is a harder problem and they solve it well.
21
-
22
- ## πŸ† Benchmark Results
23
-
24
- Produced by `pnpm benchmark`. Each tool's output is inserted into a real DOM
25
- (jsdom), then serialized and re-parsed β€” the round trip where mutation XSS lives.
26
- A vector counts as blocked only if *neither* parse yields a script node, an `on*`
27
- handler, or a dangerous-protocol URL. The suite covers the major Node-compatible
28
- sanitizers, tag strippers, and HTML encoders with materially different APIs.
29
-
30
- | Library | Category | Security | Text kept | Markup kept | Ops/sec* |
31
- |---------|----------|----------|-----------|-------------|----------|
32
- | **Purifai** | strip-text | **100%** | **100%** | 0% *(by design)* | **341,705** |
33
- | striptags | strip-text | 100% | 100% | 0% | 674,214 |
34
- | DOMPurify | preserve-html | 100% | 100% | 100% | 1,809 |
35
- | sanitize-html | preserve-html | 100% | 100% | 100% | 85,340 |
36
- | xss | preserve-html | 100% | 100% | 100% | 240,911 |
37
- | rehype-sanitize | preserve-html | 100% | 100% | 100% | 23,738 |
38
- | escape-html | escape-html | 100% | 60% | 0% | 2,374,124 |
39
- | validator.escape | escape-html | 100% | 60% | 0% | 713,563 |
40
- | entities.escapeUTF8 | escape-html | 100% | 60% | 0% | 1,148,270 |
41
- | html-entities | escape-html | 100% | 60% | 0% | 890,538 |
42
- | he.escape | escape-html | 100% | 60% | 0% | 1,054,481 |
43
- | `() => ''` *(calibration)* | β€” | 100% | 0% | 0% | β€” |
44
- | `v => v` *(calibration)* | β€” | 38.1% | 100% | 100% | β€” |
45
-
46
- 84 attack vectors (OWASP, PortSwigger, cure53 corpora) Β· 15 benign documents.
47
-
48
- \* Throughput is a single-process development snapshot captured on 2026-08-02
49
- with Node 26.3.0 on Apple Silicon. It varies by hardware and runtime and should
50
- only be compared within the same category. The command above is the source of
51
- truth. HTML encoders safely display the original markup as text; unlike Purifai
52
- and striptags, they do not turn markup-bearing input into clean reader text.
53
-
54
- > πŸ“Š **Read this honestly.** On security there is no headroom left β€” every
55
- > maintained sanitizer above blocks everything thrown at it. The calibration rows
56
- > are the whole point: a function returning `''` also scores 100% security, which
57
- > is why a security number means nothing without a fidelity axis beside it.
58
- > Purifai's 0% markup retention is its design, not a defect β€” but it does mean
59
- > Purifai is **not** "more secure than DOMPurify". It is smaller, dependency-free,
60
- > and needs no DOM.
61
-
62
- ## πŸš€ Why Purifai?
63
-
64
- ### βœ… Security
65
- - Blocks every vector in the 84-payload corpus, verified by real DOM re-parse
66
- - Handles **Unicode**, **HTML entity**, and **URL encoding** bypasses
67
- - Prevents **template injection** and **CSS expression** attacks
68
- - Immune to **mutation XSS** by construction: it emits no markup to mutate
69
- - Contextual encoders for HTML body, attribute, and URL contexts
70
-
71
- ### ⚑ High Performance
72
- - **Fast processing** - optimized algorithms for high-throughput applications
73
- - **Zero dependencies** - minimal bundle size and attack surface
74
- - **TypeScript native** with full type definitions
75
- - **Node.js and Browser** compatible
76
-
77
- ### 🎯 Developer Friendly
78
- - Simple API with intelligent defaults
79
- - Detailed threat analysis and reporting
80
- - Batch processing support
81
- - Comprehensive documentation
82
-
83
- ## πŸ”₯ Attack Vectors Blocked
84
-
85
- Purifai blocks **all 84 vectors** in the corpus (64 classic + 20 modern mXSS/namespace), including:
86
-
87
- ### Critical Polyglot Attacks
88
- ```javascript
89
- // βœ… Purifai reduces each of these to empty output:
90
-
91
- // Universal XSS Polyglot
92
- jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()///>\\x3e
93
-
94
- // Ultimate XSS Polyglot
95
- javascript:/*--></title></style></textarea></script></xmp><svg/onload='+/"/+/onmouseover=1/+/[*/[]/+alert(1)//'>'
96
-
97
- // Namespace Confusion Attack
98
- <form><math><mtext></form><form><mglyph><style></math><img src onerror=alert(1)>
1
+ > Stream untrusted HTML into clean, readable text with bounded working memory.
2
+ > No DOM. No document tree. No runtime dependencies.
3
+
4
+ # Purifai
5
+
6
+ [npm](https://www.npmjs.com/package/purifai) Β·
7
+ [Project notes](https://worksonmy.dev/projects/purifai) Β·
8
+ [Runnable examples](https://github.com/moji2002/purifai/tree/main/examples) Β·
9
+ [Issues](https://github.com/moji2002/purifai/issues)
10
+
11
+ Purifai is a fixed-policy HTML-to-readable-text converter for servers, browsers,
12
+ and edge runtimes. It incrementally removes non-reader bodies, decodes the full
13
+ WHATWG character-reference set, and preserves useful structure such as headings,
14
+ paragraphs, lists, links, image alternatives, code, and simple tables.
15
+
16
+ ```ts
17
+ import { toText } from 'purifai';
18
+
19
+ const text = toText(
20
+ '<script>alert(1)</script><h2>Release</h2><ul><li>Fast</li></ul>',
21
+ );
22
+ // Release
23
+ //
24
+ // - Fast
99
25
  ```
100
26
 
101
- ### Standard XSS Vectors
102
- - Script injection: `<script>alert("xss")</script>`
103
- - Event handlers: `<img src=x onerror=alert(1)>`
104
- - Protocol injection: `javascript:alert(1)`
105
- - CSS expressions: `<div style="expression(alert(1))">`
106
- - Template injection: `{{constructor.constructor("alert(1)")()}}`
107
- - Encoding bypasses: `&#60;script&#62;alert(1)&#60;/script&#62;`
27
+ A flat tag remover can leak `alert(1)` from the script body and collapse the
28
+ remaining text. Purifai drops that body and formats the reader content.
108
29
 
109
- ## πŸ› οΈ Installation
30
+ The output is a JavaScript string, not safe HTML. Use one of these supported
31
+ sinks:
110
32
 
111
- ```bash
112
- npm install purifai
113
- # or
114
- yarn add purifai
115
- # or
116
- pnpm add purifai
33
+ ```ts
34
+ import { escapeHtmlText, toText } from 'purifai';
35
+
36
+ element.textContent = toText(untrustedHtml);
37
+ element.innerHTML = escapeHtmlText(toText(untrustedHtml));
117
38
  ```
118
39
 
119
- ## πŸ“– Usage
40
+ Prefer `textContent`. `escapeHtmlText` exists for an HTML text context only; it
41
+ does not make a value safe for an attribute, URL, JavaScript, CSS, or template
42
+ source.
120
43
 
121
- ### Basic Usage
44
+ ## Why it exists
122
45
 
123
- ```typescript
124
- import { Purifai } from 'purifai';
46
+ Purifai targets one narrow intersection:
125
47
 
126
- // Simple sanitization
127
- const clean = Purifai.sanitize('<script>alert("xss")</script>Hello World');
128
- console.log(clean); // "Hello World"
48
+ - readable extraction instead of flat deletion;
49
+ - deterministic input, output, nesting, and retained-token limits;
50
+ - chunk-invariant Web `TransformStream` conversion;
51
+ - no DOM, tree, Node built-in, or runtime dependency; and
52
+ - one side-effect-free artifact across server, browser, and edge runtimes.
129
53
 
130
- // With options
131
- const safe = Purifai.sanitize(userInput, {
132
- maxLength: 10000,
133
- aggressiveMode: true
134
- });
135
- ```
54
+ It does not preserve markup and does not classify a user's intent. If either is
55
+ your requirement, use a tool designed for that different job.
136
56
 
137
- ### Advanced Analysis
57
+ ## Install
138
58
 
139
- ```typescript
140
- import { analyze } from 'purifai';
59
+ ```sh
60
+ npm install purifai
61
+ ```
141
62
 
142
- const result = analyze('<script>alert("hack")</script>User content');
63
+ Purifai v3 requires Node.js 22 or newer when used in Node.
143
64
 
144
- console.log(result.content); // "User content"
145
- console.log(result.hadThreats); // true
146
- console.log(result.threatLevel); // "critical"
147
- console.log(result.processingTime); // 0.023 (ms)
148
- ```
65
+ ## API
149
66
 
150
- ### Batch Processing
67
+ ### `toText(html, options?)`
151
68
 
152
- ```typescript
153
- import { sanitizeBatch } from 'purifai';
69
+ Converts one string and returns readable text. A breached limit throws a
70
+ `PurifaiLimitError`.
154
71
 
155
- const userInputs = [
156
- '<script>alert(1)</script>Hello',
157
- '<img src=x onerror=alert(1)>World',
158
- 'Safe content'
159
- ];
72
+ ```ts
73
+ import { toText } from 'purifai';
160
74
 
161
- const cleanData = sanitizeBatch(userInputs);
162
- // Result: ["Hello", "World", "Safe content"]
75
+ const text = toText('<h1>Guide</h1><p>Start here.</p>', {
76
+ layout: 'readable',
77
+ links: 'label-and-url',
78
+ images: 'alt',
79
+ baseUrl: 'https://docs.example/',
80
+ limits: { input: 1_000_000, output: 250_000, depth: 64, token: 65_536 },
81
+ });
163
82
  ```
164
83
 
165
- ### Threat Detection
84
+ ### `convert(html, options?)`
166
85
 
167
- ```typescript
168
- import { isDangerous } from 'purifai';
86
+ Returns the text plus a frozen conversion report. It is the only entry point
87
+ that can deliberately return a bounded prefix instead of throwing.
169
88
 
170
- if (isDangerous(userInput)) {
171
- // Log security incident
172
- console.warn('Potential XSS attack detected');
173
- }
89
+ ```ts
90
+ import { convert } from 'purifai';
91
+
92
+ const result = convert(largeHtml, {
93
+ limits: { output: 20_000 },
94
+ overflow: 'truncate',
95
+ });
96
+
97
+ result.text;
98
+ result.truncatedBy; // 'output' or null
99
+ result.scanComplete; // false after truncation
100
+ result.consumedInputCodeUnits;
101
+ result.outputCodeUnits;
102
+ result.droppedContainers; // e.g. { script: 2, style: 1 }
174
103
  ```
175
104
 
176
- ### Contextual Escaping
105
+ Truncation is explicit, deterministic, and never emits half of a UTF-16
106
+ surrogate pair. When multiple limits meet at the same point, the first observed
107
+ limit is reported.
177
108
 
178
- `sanitize()` treats its input as HTML and removes markup. When the input is
179
- **plain text**, escaping is the better tool β€” it is lossless, and no guessing is
180
- involved. These follow OWASP's context-specific output encoding guidance.
109
+ ### `createTextTransform(options?)`
181
110
 
182
- ```typescript
183
- import { escape, escapeAttribute, escapeUrl } from 'purifai';
111
+ Returns a native `TransformStream<string, string>` with a `result` promise. The
112
+ stream uses the same state machine and produces exactly the same joined text as
113
+ `toText`, regardless of chunk boundaries.
184
114
 
185
- // HTML body context β€” lossless, nothing is removed
186
- escape('if (a<b && c>d) return;');
187
- // "if (a&lt;b &amp;&amp; c&gt;d) return;"
115
+ ```ts
116
+ import { createTextTransform } from 'purifai';
188
117
 
189
- // Attribute context β€” safe even unquoted, since a bare space or
190
- // backtick could otherwise close the value and start onerror=
191
- `<div title="${escapeAttribute(userInput)}">`;
118
+ const response = await fetch('https://example.test/article');
119
+ if (response.body === null) throw new Error('Response has no body');
192
120
 
193
- // URL context β€” returns '' when the protocol is not allow-listed
194
- escapeUrl('https://example.com'); // escaped, safe for href
195
- escapeUrl('javascript:alert(1)'); // ""
196
- escapeUrl('java\tscript:alert(1)'); // "" (whitespace-split protocols too)
197
- ```
121
+ const transform = createTextTransform({ links: 'label-and-url' });
122
+ const readable = response.body
123
+ .pipeThrough(new TextDecoderStream())
124
+ .pipeThrough(transform);
125
+ const reader = readable.getReader();
198
126
 
199
- Why both exist: `a<b && c>d` is a valid HTML start tag per the parsing spec, so
200
- `sanitize()` correctly drops it while `escape()` preserves it verbatim. Only the
201
- caller knows whether a string is markup or text β€” so the API asks rather than
202
- guesses.
203
-
204
- ## βš™οΈ Configuration Options
205
-
206
- ```typescript
207
- interface PurifaiOptions {
208
- /** Maximum input length (default: 1MB) */
209
- maxLength?: number;
210
-
211
- /** Custom allowed protocols (default: ['http', 'https', 'mailto']) */
212
- allowedProtocols?: string[];
213
-
214
- /** Enable aggressive mode for maximum security (default: true) */
215
- aggressiveMode?: boolean;
127
+ for (;;) {
128
+ const { done, value } = await reader.read();
129
+ if (done) break;
130
+ consumeText(value);
216
131
  }
217
- ```
218
132
 
219
- ## πŸ§ͺ Testing Methodology
220
-
221
- Our comprehensive test suite evaluates sanitizers against:
222
-
223
- - **64 sophisticated attack vectors** from OWASP, PortSwigger, and security research
224
- - **Advanced polyglot attacks** that combine multiple bypass techniques
225
- - **Encoding variations** (Unicode, HTML entities, URL encoding)
226
- - **Context-breaking attacks** for different HTML contexts
227
- - **Modern browser vectors** including HTML5 and SVG attacks
228
- - **Template injection** patterns from popular frameworks
229
-
230
- ### Test Categories:
231
- 1. **Basic XSS** - Standard script injection attempts
232
- 2. **Event Handlers** - Various HTML event attributes
233
- 3. **Protocol Variations** - javascript:, vbscript:, data: URIs
234
- 4. **CSS Expressions** - Style-based code execution
235
- 5. **Template Injection** - Framework-specific patterns
236
- 6. **Polyglot Attacks** - Multi-context bypass attempts
237
- 7. **Encoding Bypasses** - Obfuscation techniques
238
- 8. **Modern Vectors** - HTML5, SVG, and browser-specific attacks
239
-
240
- ## πŸ“Š Detailed Comparison
241
-
242
- ### Security Comparison by Attack Type
243
-
244
- Superseded by the two-axis benchmark above. The per-category percentages that
245
- used to sit here came from a scoring rule that counted "output is empty" as a
246
- win, so it rewarded deletion rather than safety and marked correct competitor
247
- behaviour as failure. Run `pnpm test:fair` for numbers that survive scrutiny.
248
-
249
- ### Bundle Size Comparison
250
-
251
- | Library | Bundle Size | Dependencies | TypeScript |
252
- |---------|-------------|--------------|-------------|
253
- | **Purifai** | **13.6KB / 4.1KB gzip** | **0** | **βœ… Native** |
254
- | validator.js | ~15KB | 0 | βœ… Available |
255
- | xss | ~25KB | 3 | ❌ None |
256
- | node-sanitize | ~32KB | 5 | ❌ None |
257
- | DOMPurify | ~45KB | 0 | βœ… Available |
258
- | sanitize-html | ~200KB+ | 15+ | βœ… Available |
259
-
260
- ## 🌟 Use Cases
261
-
262
- ### Web Applications
263
- ```typescript
264
- // Sanitize user-generated content
265
- app.post('/comments', (req, res) => {
266
- const safeComment = Purifai.sanitize(req.body.comment);
267
- // Store safeComment in database
268
- });
133
+ const report = await transform.result;
269
134
  ```
270
135
 
271
- ### Content Management Systems
272
- ```typescript
273
- // Reduce rich text editor content to safe plain text.
274
- // Note: Purifai strips ALL markup. If you need to KEEP <b>/<a href>,
275
- // use DOMPurify or sanitize-html instead - that is a different job.
276
- const cleanText = Purifai.sanitize(editorContent, {
277
- maxLength: 50000
278
- });
279
- ```
136
+ Stream conversion always throws on a breached limit. Output may already have
137
+ been enqueued when `readable` and `transform.result` reject, so discard partial
138
+ output unless your application has deliberately defined it as useful. Purifai
139
+ does not buffer the whole result to make an error transactional.
280
140
 
281
- ### API Gateways
282
- ```typescript
283
- // Sanitize all incoming string data
284
- const sanitizedPayload = sanitizeBatch(Object.values(request.body));
285
- ```
141
+ ### `escapeHtmlText(text)`
286
142
 
287
- ### Real-time Chat
288
- ```typescript
289
- // Clean messages before broadcasting
290
- socket.on('message', (data) => {
291
- const result = analyze(data.message);
292
- if (result.threatLevel === 'critical') {
293
- // Block and log the attempt
294
- return;
295
- }
296
- broadcast(result.content);
297
- });
298
- ```
143
+ Encodes `&`, `<`, `>`, `"`, and `'` for an HTML text node. It is lossless and is
144
+ for plain textβ€”including `toText` outputβ€”when the only available sink is
145
+ `innerHTML`.
299
146
 
300
- ## 🚦 Migration Guide
147
+ ### `PurifaiLimitError`
301
148
 
302
- ### From DOMPurify
303
- ```typescript
304
- // Before
305
- import DOMPurify from 'dompurify';
306
- const clean = DOMPurify.sanitize(dirty);
149
+ Extends `RangeError` and exposes `kind`, `limit`, and `observed`.
307
150
 
308
- // After
309
- import { sanitize } from 'purifai';
310
- const clean = sanitize(dirty);
311
- ```
151
+ ```ts
152
+ import { PurifaiLimitError, toText } from 'purifai';
312
153
 
313
- ### From sanitize-html
314
- ```typescript
315
- // Before
316
- import sanitizeHtml from 'sanitize-html';
317
- const clean = sanitizeHtml(dirty, options);
318
-
319
- // After
320
- import { sanitize } from 'purifai';
321
- const clean = sanitize(dirty);
322
- // Purifai removes all tags. If the migration needs to KEEP safe HTML,
323
- // stay on sanitize-html - Purifai targets the strip-to-text case.
154
+ try {
155
+ toText(html, { limits: { input: 10_000 } });
156
+ } catch (error) {
157
+ if (error instanceof PurifaiLimitError) {
158
+ console.error(error.kind, error.limit, error.observed);
159
+ }
160
+ }
324
161
  ```
325
162
 
326
- ### From xss
327
- ```typescript
328
- // Before
329
- import xss from 'xss';
330
- const clean = xss(dirty);
331
-
332
- // After
333
- import { sanitize } from 'purifai';
334
- const clean = sanitize(dirty);
335
- ```
163
+ ## Options and defaults
164
+
165
+ Unknown keys and invalid values throw `TypeError`; Purifai does not silently
166
+ guess around configuration mistakes.
336
167
 
337
- ### From validator.js
338
- ```typescript
339
- // Before
340
- import validator from 'validator';
341
- const clean = validator.escape(dirty);
168
+ | Option | Type | Default | Meaning |
169
+ | --- | --- | --- | --- |
170
+ | `layout` | `'readable' \| 'compact'` | `'readable'` | Structural newlines/lists/tables, or normalized single-space text |
171
+ | `links` | `'label' \| 'label-and-url' \| 'drop'` | `'label'` | Keep label, append an accepted display URL, or drop the link body |
172
+ | `images` | `'alt' \| 'drop'` | `'alt'` | Emit decoded non-empty `alt` text, or omit images |
173
+ | `baseUrl` | `string \| URL` | none | Resolve relative display URLs against a credential-free HTTP(S) base |
174
+ | `limits.input` | non-negative safe integer | `1_000_000` | Maximum input UTF-16 code units consumed |
175
+ | `limits.output` | non-negative safe integer | `250_000` | Maximum output UTF-16 code units emitted |
176
+ | `limits.depth` | non-negative safe integer | `64` | Maximum live structural nesting |
177
+ | `limits.token` | non-negative safe integer | `65_536` | Maximum aggregate retained token/attribute code units |
178
+ | `overflow` | `'throw' \| 'truncate'` | `'throw'` | `convert` only; other APIs always throw |
342
179
 
343
- // After
344
- import { sanitize } from 'purifai';
345
- const clean = sanitize(dirty);
346
- ```
180
+ All four limits are enforced while scanning, before unbounded caller-controlled
181
+ state can accumulate. Values measure JavaScript UTF-16 code units, not encoded
182
+ bytes.
347
183
 
348
- ### From node-sanitize
349
- ```typescript
350
- // Before
351
- import sanitize from 'node-sanitize';
352
- const clean = sanitize(dirty);
184
+ ## Link policy
353
185
 
354
- // After
355
- import { sanitize } from 'purifai';
356
- const clean = sanitize(dirty);
357
- ```
186
+ `label-and-url` emits a destination as display text, never as an active link. It
187
+ accepts absolute `http:`, `https:`, and `mailto:` URLs. Relative URLs require a
188
+ validated HTTP(S) `baseUrl`. Control characters, whitespace-split schemes,
189
+ protocol-relative inputs, leading backslashes, credentials, unsupported schemes,
190
+ and invalid URLs are omitted while their visible label remains.
358
191
 
359
- ## πŸ” Security Features
360
-
361
- ### Advanced Protection Techniques
362
- - **Multi-layer sanitization** with fallback mechanisms
363
- - **Context-aware parsing** to prevent bypass attempts
364
- - **Aggressive mode** for maximum security applications
365
- - **No executable output** across the current 84-vector benchmark corpus
366
-
367
- ### Encoded Attack Detection
368
- ```typescript
369
- // All these variants are detected and blocked:
370
- '<script>alert(1)</script>' // Direct
371
- '&#60;script&#62;alert(1)&#60;/script&#62;' // HTML entities
372
- '%3Cscript%3Ealert(1)%3C/script%3E' // URL encoded
373
- '\\u003cscript\\u003ealert(1)\\u003c/script\\u003e' // Unicode
374
- ```
192
+ The returned URL string is still only text. Do not move it into `href` without a
193
+ separate URL-policy decision at that sink.
375
194
 
376
- ## πŸ“ˆ Performance Optimization
195
+ ## Extraction fidelity
377
196
 
378
- Purifai is optimized for:
379
- - **High-throughput** applications with efficient processing
380
- - **Low memory** footprint with optimized regex patterns
381
- - **Fast startup** with zero dependencies
382
- - **Minimal CPU** usage through intelligent algorithms
383
- - **Scalable** performance across different input sizes and complexity
197
+ Purifai intentionally removes source and non-reader bodies including `script`,
198
+ `style`, `template`, `iframe`, `noscript`, `noembed`, `noframes`, `svg`, and
199
+ `math`. It preserves selected fallback/form text, decodes `textarea`, preserves
200
+ literal `xmp`, and treats `plaintext` as text through end of input.
384
201
 
385
- ## 🀝 Contributing
202
+ This is a bounded extraction grammar, not browser tree construction. It does not
203
+ recreate CSS layout, browser `innerText`, complex `rowspan`/`colspan` tables,
204
+ SVG/MathML semantics, selector rules, custom formatters, or browser-equivalent
205
+ malformed-markup recovery. Simple rows and cells are represented with tabs and
206
+ line boundaries.
386
207
 
387
- We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
208
+ ## Which tool should you choose?
388
209
 
389
- ### Development Setup
390
- ```bash
391
- git clone https://github.com/moji2002/purifai.git
392
- cd purifai
393
- pnpm install
394
- pnpm build
395
- pnpm test
396
- ```
210
+ | Need | Choice |
211
+ | --- | --- |
212
+ | Fixed-policy readable text, hostile-input bounds, and portable Web streaming | Choose Purifai |
213
+ | Selectors, custom formatters, advanced tables, wrapping, and broader formatting control | Choose `html-to-text` |
214
+ | The smallest flat tag-removal operation | Choose stable `striptags` |
215
+ | Preserve an allow-listed safe HTML fragment | Choose DOMPurify or `sanitize-html` |
397
216
 
398
- ### Running Benchmarks
399
- ```bash
400
- pnpm benchmark
401
- ```
217
+ These tools are not interchangeable. In particular, DOMPurify and
218
+ `sanitize-html` are the right category when safe markup must survive.
402
219
 
403
- This builds the published artifact, runs the two-axis competitor benchmark, and
404
- then measures Purifai's throughput, critical-attack checks, and bundle size. The
405
- competitor set and exact versions are pinned in `package.json` and
406
- `pnpm-lock.yaml` so results are reproducible.
220
+ ## Benchmarks
407
221
 
408
- ## πŸ“„ License
222
+ The checked category benchmark pins `striptags@3.2.0` and
223
+ `html-to-text@10.0.0`. It measures exact readability/body-removal fixtures,
224
+ isolated warm median and p95 one-shot latency, and fresh-process peak RSS. The
225
+ throughput path gives every package the same materialized string; the memory path
226
+ lets Purifai consume lazy 16,384-code-unit chunks because streaming ingestion is
227
+ the product claim.
409
228
 
410
- MIT License - see [LICENSE](LICENSE) file for details.
229
+ On the recorded Apple M1 / Node 24 run, Purifai passed all 11 category gates: all
230
+ readability and body-removal fixtures, lower hostile-input p95 than
231
+ `html-to-text` on four hostile corpora, and lower streaming peak RSS on all five
232
+ memory corpora. `striptags` remains faster on some flat-strip cases, which is not
233
+ Purifai's claim.
411
234
 
412
- ## πŸ™ Acknowledgments
235
+ See the [complete methodology, raw-result link, and tables](docs/benchmarks/v3.md).
236
+ Reproduce it with `pnpm run bench`; re-check the saved gates with
237
+ `pnpm run bench:check`.
413
238
 
414
- - Security research from OWASP, PortSwigger, and the security community
415
- - Inspiration from existing sanitization libraries
416
- - Comprehensive testing methodologies from security experts
239
+ ## Size and runtime matrix
417
240
 
418
- ## πŸ“š Related Resources
241
+ The complete minified ESM runtimeβ€”including the full 2,231-name WHATWG entity
242
+ dataβ€”is gated at 25 KiB using deterministic `gzip -9`. The recorded artifact is
243
+ 23,689 bytes. `pnpm run test:size` also checks the packed exports, zero runtime
244
+ dependencies, cold import time, and retained import heap.
419
245
 
420
- - [OWASP XSS Prevention](https://owasp.org/www-community/xss-filter-evasion-cheatsheet)
421
- - [PortSwigger XSS Labs](https://portswigger.net/web-security/cross-site-scripting)
422
- - [MDN Web Security](https://developer.mozilla.org/en-US/docs/Web/Security)
246
+ The release matrix exercises the same packed ESM artifact:
423
247
 
424
- ---
248
+ | Runtime | Required release coverage |
249
+ | --- | --- |
250
+ | Node.js | 22, 24, and 26; ESM and CommonJS |
251
+ | Bun | ESM and CommonJS consumers |
252
+ | Deno | ESM consumer |
253
+ | Cloudflare Workers | real `workerd`, without Node compatibility |
254
+ | Browsers | Chromium, Firefox, and WebKit |
425
255
 
426
- **⚑ Need untrusted markup reduced to plain text? Install Purifai.**
256
+ Browser qualification also reparses escaped output in a real DOM with a working
257
+ positive control. This validates the documented sinks; it is not a universal
258
+ claim about every output context.
427
259
 
428
- ```bash
429
- npm install purifai
430
- ```
260
+ ## Migration and development
431
261
 
432
- *Purifai β€” strip untrusted markup, keep the text.*
262
+ V3 is a clean break. See the [v3 migration guide](docs/migration-v3.md) for every
263
+ removed export and option. Contributor setup and the full verification commands
264
+ are in [CONTRIBUTING.md](CONTRIBUTING.md).
265
+
266
+ ## License
267
+
268
+ MIT