purifai 2.0.3 → 3.0.1
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 +234 -376
- package/benchmark/results/v3.json +2767 -0
- package/dist/index.cjs +14 -475
- package/dist/index.d.cts +2 -189
- package/dist/index.d.ts +2 -189
- package/dist/index.js +14 -443
- package/dist/src/api.d.ts +7 -0
- package/dist/src/config.d.ts +18 -0
- package/dist/src/contracts.d.ts +40 -0
- package/dist/src/entities.d.ts +15 -0
- package/dist/src/formatter.d.ts +36 -0
- package/dist/src/generated/entities.d.ts +3 -0
- package/dist/src/policy.d.ts +15 -0
- package/dist/src/scanner.d.ts +71 -0
- package/dist/src/session.d.ts +27 -0
- package/docs/benchmarks/v3.md +92 -0
- package/docs/migration-v3.md +95 -0
- package/package.json +42 -42
package/README.md
CHANGED
|
@@ -1,447 +1,305 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[](https://www.npmjs.com/package/purifai)
|
|
7
|
+
[](https://github.com/moji2002/purifai/actions/workflows/ci.yml)
|
|
8
|
+
[](docs/benchmarks/v3.md)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
|
|
11
|
+
**Readable text from hostile HTML—without a DOM.**
|
|
12
|
+
|
|
13
|
+
Purifai is a fixed-policy HTML-to-text converter for servers, browsers, and
|
|
14
|
+
edge runtimes. It keeps useful document structure, drops non-reader bodies, and
|
|
15
|
+
enforces input, output, nesting, and retained-token limits while scanning.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { toText } from 'purifai';
|
|
19
|
+
|
|
20
|
+
const text = toText(
|
|
21
|
+
'<script>alert(1)</script><h2>Release</h2><ul><li>Fast</li></ul>',
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
console.log(text);
|
|
25
|
+
// Release
|
|
26
|
+
//
|
|
27
|
+
// - Fast
|
|
100
28
|
```
|
|
101
29
|
|
|
102
|
-
|
|
30
|
+
A flat tag remover can leak `alert(1)` from the script body and collapse the
|
|
31
|
+
remaining text. Purifai drops that body and formats the reader content.
|
|
103
32
|
|
|
104
|
-
|
|
33
|
+
## Choose Purifai when
|
|
105
34
|
|
|
106
|
-
|
|
107
|
-
|
|
35
|
+
- HTML may be large, malformed, or hostile.
|
|
36
|
+
- You want readable plain text—not preserved markup or a browser DOM.
|
|
37
|
+
- Conversion must have deterministic resource limits.
|
|
38
|
+
- The same implementation must run in Node, Bun, Deno, Workers, and browsers.
|
|
39
|
+
- Streaming should produce the same result regardless of chunk boundaries.
|
|
108
40
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
console.log(clean); // "Hello World"
|
|
41
|
+
If you need selector-driven formatting, complex table layout, or allow-listed
|
|
42
|
+
safe HTML, jump to [Which tool should you choose?](#which-tool-should-you-choose).
|
|
112
43
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
npm install purifai
|
|
117
48
|
```
|
|
118
49
|
|
|
119
|
-
|
|
50
|
+
Purifai v3 requires Node.js 22 or newer when used in Node. It ships ESM and
|
|
51
|
+
CommonJS exports and has zero runtime dependencies.
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
120
54
|
|
|
121
|
-
```
|
|
122
|
-
import {
|
|
55
|
+
```ts
|
|
56
|
+
import { toText } from 'purifai';
|
|
123
57
|
|
|
124
|
-
const
|
|
58
|
+
const text = toText('<h1>Guide</h1><p>Start <strong>here</strong>.</p>', {
|
|
59
|
+
layout: 'readable',
|
|
60
|
+
links: 'label',
|
|
61
|
+
images: 'alt',
|
|
62
|
+
});
|
|
125
63
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
console.log(result.processingTime); // 0.023 (ms)
|
|
64
|
+
// Guide
|
|
65
|
+
//
|
|
66
|
+
// Start here.
|
|
130
67
|
```
|
|
131
68
|
|
|
132
|
-
|
|
69
|
+
`toText` returns a JavaScript string. It does not return safe HTML.
|
|
133
70
|
|
|
134
|
-
|
|
135
|
-
import { sanitizeBatch } from 'purifai';
|
|
71
|
+
## Safe output
|
|
136
72
|
|
|
137
|
-
|
|
138
|
-
'<script>alert(1)</script>Hello',
|
|
139
|
-
'<img src=x onerror=alert(1)>World',
|
|
140
|
-
'Safe content'
|
|
141
|
-
];
|
|
73
|
+
Prefer a text sink:
|
|
142
74
|
|
|
143
|
-
|
|
144
|
-
|
|
75
|
+
```ts
|
|
76
|
+
element.textContent = toText(untrustedHtml);
|
|
145
77
|
```
|
|
146
78
|
|
|
147
|
-
|
|
79
|
+
If the only available sink is an HTML text node, escape the text explicitly:
|
|
148
80
|
|
|
149
|
-
```
|
|
150
|
-
import {
|
|
81
|
+
```ts
|
|
82
|
+
import { escapeHtmlText, toText } from 'purifai';
|
|
151
83
|
|
|
152
|
-
|
|
153
|
-
// Optional telemetry only. Do not use this advisory signal as an
|
|
154
|
-
// authorization, authentication, or request-blocking decision.
|
|
155
|
-
console.warn('Potentially dangerous markup observed');
|
|
156
|
-
}
|
|
84
|
+
element.innerHTML = escapeHtmlText(toText(untrustedHtml));
|
|
157
85
|
```
|
|
158
86
|
|
|
159
|
-
|
|
87
|
+
`escapeHtmlText` is only for an HTML text context. It does not make a value safe
|
|
88
|
+
for an attribute, URL, JavaScript, CSS, or template source. A displayed URL is
|
|
89
|
+
also still text; moving it into `href` requires a separate URL-policy decision.
|
|
160
90
|
|
|
161
|
-
|
|
162
|
-
**plain text**, escaping is the better tool — it is lossless, and no guessing is
|
|
163
|
-
involved. These follow OWASP's context-specific output encoding guidance.
|
|
91
|
+
## Why Purifai
|
|
164
92
|
|
|
165
|
-
|
|
166
|
-
|
|
93
|
+
Most HTML-to-text tools optimize for either minimal tag removal or broad
|
|
94
|
+
formatting control. Purifai targets a narrower intersection:
|
|
167
95
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
96
|
+
| Requirement | Purifai behavior |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| Reader-friendly output | Preserves headings, paragraphs, lists, quotes, code, simple tables, links, and image alternatives |
|
|
99
|
+
| Non-reader content | Drops bodies such as `script`, `style`, `template`, `iframe`, `svg`, and `math` |
|
|
100
|
+
| Hostile-input bounds | Enforces input, output, depth, and aggregate retained-token limits during scanning |
|
|
101
|
+
| Streaming | Uses a native Web `TransformStream` with chunk-invariant output |
|
|
102
|
+
| Portability | Uses no DOM, document tree, Node built-in, or runtime dependency |
|
|
103
|
+
| Predictability | Fixed policy, validated options, explicit overflow behavior, and frozen reports |
|
|
171
104
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
105
|
+
That fixed scope is the reason to choose Purifai. It deliberately does not
|
|
106
|
+
preserve markup, reconstruct CSS layout, expose custom formatters, or classify a
|
|
107
|
+
user's intent.
|
|
175
108
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
109
|
+
## Streaming
|
|
110
|
+
|
|
111
|
+
`createTextTransform` converts incrementally using the same state machine as
|
|
112
|
+
`toText`. Joining its output produces exactly the same text for every possible
|
|
113
|
+
input chunking.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { createTextTransform } from 'purifai';
|
|
117
|
+
|
|
118
|
+
const response = await fetch('https://example.test/article');
|
|
119
|
+
if (response.body === null) throw new Error('Response has no body');
|
|
181
120
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
interface PurifaiOptions {
|
|
190
|
-
/** Maximum input length (default: 1MB) */
|
|
191
|
-
maxLength?: number;
|
|
192
|
-
|
|
193
|
-
/** Subset of built-in safe protocols: http, https, mailto */
|
|
194
|
-
allowedProtocols?: string[];
|
|
195
|
-
|
|
196
|
-
/** @deprecated Retained for compatibility; strip-to-text is always used. */
|
|
197
|
-
aggressiveMode?: boolean;
|
|
121
|
+
const transform = createTextTransform({ links: 'label-and-url' });
|
|
122
|
+
const readable = response.body
|
|
123
|
+
.pipeThrough(new TextDecoderStream())
|
|
124
|
+
.pipeThrough(transform);
|
|
125
|
+
|
|
126
|
+
for await (const chunk of readable) {
|
|
127
|
+
consumeText(chunk);
|
|
198
128
|
}
|
|
129
|
+
|
|
130
|
+
const report = await transform.result;
|
|
199
131
|
```
|
|
200
132
|
|
|
201
|
-
|
|
202
|
-
|
|
133
|
+
Stream conversion always throws when a limit is breached. Some output may
|
|
134
|
+
already have been enqueued when `readable` and `transform.result` reject, so
|
|
135
|
+
discard partial output unless your application explicitly accepts it.
|
|
203
136
|
|
|
204
|
-
##
|
|
137
|
+
## Bounded conversion
|
|
205
138
|
|
|
206
|
-
|
|
207
|
-
|
|
139
|
+
`toText` throws a `PurifaiLimitError` when any configured limit is exceeded.
|
|
140
|
+
Use `convert` only when a bounded prefix is an acceptable result:
|
|
208
141
|
|
|
209
|
-
```
|
|
210
|
-
import
|
|
211
|
-
import assert from 'node:assert/strict';
|
|
212
|
-
import { sanitize, escape, escapeUrl } from 'purifai';
|
|
142
|
+
```ts
|
|
143
|
+
import { convert } from 'purifai';
|
|
213
144
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
assert.equal(escapeUrl('javascript:alert(1)'), '');
|
|
145
|
+
const result = convert(largeHtml, {
|
|
146
|
+
limits: { input: 1_000_000, output: 20_000, depth: 64, token: 65_536 },
|
|
147
|
+
overflow: 'truncate',
|
|
218
148
|
});
|
|
149
|
+
|
|
150
|
+
result.text;
|
|
151
|
+
result.truncatedBy; // 'input', 'output', 'depth', 'token', or null
|
|
152
|
+
result.scanComplete; // false after truncation
|
|
153
|
+
result.consumedInputCodeUnits;
|
|
154
|
+
result.outputCodeUnits;
|
|
155
|
+
result.droppedContainers; // e.g. { script: 2, style: 1 }
|
|
219
156
|
```
|
|
220
157
|
|
|
221
|
-
|
|
158
|
+
Truncation is explicit and deterministic, and never emits half of a UTF-16
|
|
159
|
+
surrogate pair. `toText` and `createTextTransform` never truncate silently.
|
|
222
160
|
|
|
223
|
-
|
|
224
|
-
node --test purifai.test.mjs
|
|
225
|
-
```
|
|
161
|
+
## Options
|
|
226
162
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
repository-owned copy runs with `pnpm test:example`.
|
|
230
|
-
|
|
231
|
-
## 🧪 Testing Methodology
|
|
232
|
-
|
|
233
|
-
Our comprehensive test suite evaluates sanitizers against:
|
|
234
|
-
|
|
235
|
-
- **84 attack vectors** from OWASP, PortSwigger, cure53, and regression research
|
|
236
|
-
- **Advanced polyglot attacks** that combine multiple bypass techniques
|
|
237
|
-
- **Encoding variations** (Unicode, HTML entities, URL encoding)
|
|
238
|
-
- **Context-breaking attacks** for different HTML contexts
|
|
239
|
-
- **Modern browser vectors** including HTML5 and SVG attacks
|
|
240
|
-
- **Template injection** patterns from popular frameworks
|
|
241
|
-
|
|
242
|
-
### Test Categories:
|
|
243
|
-
1. **Basic XSS** - Standard script injection attempts
|
|
244
|
-
2. **Event Handlers** - Various HTML event attributes
|
|
245
|
-
3. **Protocol Variations** - javascript:, vbscript:, data: URIs
|
|
246
|
-
4. **CSS Expressions** - Style-based code execution
|
|
247
|
-
5. **Template Injection** - Framework-specific patterns
|
|
248
|
-
6. **Polyglot Attacks** - Multi-context bypass attempts
|
|
249
|
-
7. **Encoding Bypasses** - Obfuscation techniques
|
|
250
|
-
8. **Modern Vectors** - HTML5, SVG, and browser-specific attacks
|
|
251
|
-
|
|
252
|
-
## 📊 Detailed Comparison
|
|
253
|
-
|
|
254
|
-
### Security Comparison by Attack Type
|
|
255
|
-
|
|
256
|
-
Superseded by the two-axis benchmark above. The per-category percentages that
|
|
257
|
-
used to sit here came from a scoring rule that counted "output is empty" as a
|
|
258
|
-
win, so it rewarded deletion rather than safety and marked correct competitor
|
|
259
|
-
behaviour as failure. Run `pnpm test:fair` for numbers that survive scrutiny.
|
|
260
|
-
|
|
261
|
-
### Bundle Size Comparison
|
|
262
|
-
|
|
263
|
-
| Library | Category | Target | Minified | Gzip | Direct runtime deps |
|
|
264
|
-
|---------|----------|--------|----------|------|---------------------|
|
|
265
|
-
| **Purifai** | strip-text | browser | **3.5 KB** | **1.6 KB** | **0** |
|
|
266
|
-
| striptags | strip-text | browser | 2.1 KB | 1.1 KB | 0 |
|
|
267
|
-
| DOMPurify | preserve-html | browser | 28.0 KB | 10.6 KB | 0 |
|
|
268
|
-
| sanitize-html | preserve-html | Node | 192.2 KB | 70.4 KB | 7 |
|
|
269
|
-
| xss | preserve-html | browser | 18.4 KB | 6.2 KB | 2 |
|
|
270
|
-
| rehype-sanitize | preserve-html | browser | 244.5 KB | 70.7 KB | 2 |
|
|
271
|
-
| escape-html | escape-html | browser | 1.2 KB | 0.7 KB | 0 |
|
|
272
|
-
| validator.escape | escape-html | browser | 0.4 KB | 0.2 KB | 0 |
|
|
273
|
-
| entities.escapeUTF8 | escape-html | browser | 0.7 KB | 0.4 KB | 0 |
|
|
274
|
-
| html-entities | escape-html | browser | 34.8 KB | 13.1 KB | 0 |
|
|
275
|
-
| he.escape | escape-html | browser | 85.7 KB | 30.2 KB | 0 |
|
|
276
|
-
|
|
277
|
-
Measured by `pnpm test:size` with esbuild 0.27.7: smallest supported ESM
|
|
278
|
-
import, bundled and minified for ES2020, then gzipped. The lockfile pins the
|
|
279
|
-
exact library versions. DOMPurify is measured against the native browser API;
|
|
280
|
-
sanitize-html is a Node bundle; the rehype row includes the parser, sanitizer,
|
|
281
|
-
and serializer pipeline. Direct dependency counts come from each named package's
|
|
282
|
-
manifest. Compare sizes within a category and target—the tools do different jobs.
|
|
283
|
-
|
|
284
|
-
## 🌟 Use Cases
|
|
285
|
-
|
|
286
|
-
### Web Applications
|
|
287
|
-
```typescript
|
|
288
|
-
// Sanitize user-generated content
|
|
289
|
-
app.post('/comments', (req, res) => {
|
|
290
|
-
const safeComment = Purifai.sanitize(req.body.comment);
|
|
291
|
-
// Store safeComment in database
|
|
292
|
-
});
|
|
293
|
-
```
|
|
163
|
+
Unknown keys and invalid values throw `TypeError`; Purifai does not guess around
|
|
164
|
+
configuration mistakes.
|
|
294
165
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
166
|
+
| Option | Type | Default | Meaning |
|
|
167
|
+
| --- | --- | --- | --- |
|
|
168
|
+
| `layout` | `'readable' \| 'compact'` | `'readable'` | Structural boundaries, or normalized single-space text |
|
|
169
|
+
| `links` | `'label' \| 'label-and-url' \| 'drop'` | `'label'` | Keep the label, append an accepted display URL, or drop the link body |
|
|
170
|
+
| `images` | `'alt' \| 'drop'` | `'alt'` | Emit decoded non-empty `alt` text, or omit images |
|
|
171
|
+
| `baseUrl` | `string \| URL` | none | Resolve relative display URLs against a credential-free HTTP(S) base |
|
|
172
|
+
| `limits.input` | non-negative safe integer | `1_000_000` | Maximum input UTF-16 code units consumed |
|
|
173
|
+
| `limits.output` | non-negative safe integer | `250_000` | Maximum output UTF-16 code units emitted |
|
|
174
|
+
| `limits.depth` | non-negative safe integer | `64` | Maximum live structural nesting |
|
|
175
|
+
| `limits.token` | non-negative safe integer | `65_536` | Maximum aggregate retained token and attribute code units |
|
|
176
|
+
| `overflow` | `'throw' \| 'truncate'` | `'throw'` | `convert` only; other APIs always throw |
|
|
304
177
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// Clean messages before broadcasting
|
|
308
|
-
socket.on('message', (data) => {
|
|
309
|
-
const result = analyze(data.message);
|
|
310
|
-
// hadThreats/threatLevel are advisory telemetry, not an auth gate.
|
|
311
|
-
broadcast(result.content);
|
|
312
|
-
});
|
|
313
|
-
```
|
|
178
|
+
All four limits are enforced before unbounded caller-controlled state can
|
|
179
|
+
accumulate. Values measure JavaScript UTF-16 code units, not encoded bytes.
|
|
314
180
|
|
|
315
|
-
|
|
181
|
+
### Display URL policy
|
|
316
182
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
183
|
+
`label-and-url` emits destinations as display text, never as active links. It
|
|
184
|
+
accepts absolute `http:`, `https:`, and `mailto:` URLs. Relative URLs require a
|
|
185
|
+
validated HTTP(S) `baseUrl`. Credentials, controls, ambiguous schemes,
|
|
186
|
+
protocol-relative inputs, leading backslashes, unsupported schemes, and invalid
|
|
187
|
+
URLs are omitted while their visible label remains.
|
|
322
188
|
|
|
323
|
-
|
|
324
|
-
import { sanitize } from 'purifai';
|
|
325
|
-
const clean = sanitize(dirty);
|
|
326
|
-
// Only migrate when dropping every tag is intended. Otherwise keep DOMPurify.
|
|
327
|
-
```
|
|
189
|
+
## Extraction policy
|
|
328
190
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
// After
|
|
336
|
-
import { sanitize } from 'purifai';
|
|
337
|
-
const clean = sanitize(dirty);
|
|
338
|
-
// Purifai removes all tags. If the migration needs to KEEP safe HTML,
|
|
339
|
-
// stay on sanitize-html - Purifai targets the strip-to-text case.
|
|
340
|
-
```
|
|
191
|
+
Purifai removes source and non-reader bodies including `script`, `style`,
|
|
192
|
+
`template`, `iframe`, `noscript`, `noembed`, `noframes`, `svg`, and `math`. It
|
|
193
|
+
preserves selected fallback and form text, decodes the complete pinned WHATWG
|
|
194
|
+
character-reference set, preserves literal `xmp`, and treats `plaintext` as text
|
|
195
|
+
through end of input.
|
|
341
196
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const clean = xss(dirty);
|
|
197
|
+
This is a bounded extraction grammar, not browser tree construction. It does not
|
|
198
|
+
recreate CSS layout, browser `innerText`, complex `rowspan`/`colspan` tables,
|
|
199
|
+
SVG/MathML semantics, selector rules, custom formatters, or browser-equivalent
|
|
200
|
+
malformed-markup recovery.
|
|
347
201
|
|
|
348
|
-
|
|
349
|
-
import { sanitize } from 'purifai';
|
|
350
|
-
const clean = sanitize(dirty);
|
|
351
|
-
// Only migrate when dropping every tag is intended. Otherwise keep xss.
|
|
352
|
-
```
|
|
202
|
+
## Benchmarks
|
|
353
203
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
import validator from 'validator';
|
|
358
|
-
const clean = validator.escape(dirty);
|
|
204
|
+
The checked category benchmark pins `striptags@3.2.0` and
|
|
205
|
+
`html-to-text@10.0.0`. It measures reviewed readability and body-removal
|
|
206
|
+
fixtures, isolated warm median and p95 latency, and fresh-process peak RSS.
|
|
359
207
|
|
|
360
|
-
|
|
361
|
-
import { escape } from 'purifai';
|
|
362
|
-
const clean = escape(dirty);
|
|
363
|
-
```
|
|
208
|
+
On the recorded Apple M1 / Node 24 run, Purifai passed all 11 category gates:
|
|
364
209
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
import sanitize from 'node-sanitize';
|
|
369
|
-
const clean = sanitize(dirty);
|
|
210
|
+
- 8/8 readability fixtures and all 5 non-reader-body fixtures;
|
|
211
|
+
- lower hostile-input p95 than `html-to-text` on four hostile corpora; and
|
|
212
|
+
- lower streaming peak RSS than `html-to-text` on all five memory corpora.
|
|
370
213
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const clean = sanitize(dirty);
|
|
374
|
-
```
|
|
214
|
+
`striptags` remains faster on some flat-strip cases. That is not Purifai's
|
|
215
|
+
claim. Results are machine-, runtime-, and corpus-specific.
|
|
375
216
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
- **Forward-only scanning** with bounded adversarial scaling checks
|
|
380
|
-
- **Fail-closed raw-text removal** for unclosed scriptable containers
|
|
381
|
-
- **Context-specific encoders** instead of one output reused everywhere
|
|
382
|
-
- **No executable output observed** across the current 84-vector corpus
|
|
383
|
-
|
|
384
|
-
### Encoded Attack Detection
|
|
385
|
-
```typescript
|
|
386
|
-
// Markup variants are decoded before the strip-to-text scan:
|
|
387
|
-
'<script>alert(1)</script>' // Direct
|
|
388
|
-
'<script>alert(1)</script>' // HTML entities
|
|
389
|
-
'%3Cscript%3Ealert(1)%3C/script%3E' // URL encoded
|
|
390
|
-
'\\u003cscript\\u003ealert(1)\\u003c/script\\u003e' // Unicode
|
|
391
|
-
```
|
|
217
|
+
See the [complete methodology, raw results, and tables](docs/benchmarks/v3.md).
|
|
218
|
+
Reproduce measurements with `pnpm run bench`; check the recorded release gates
|
|
219
|
+
with `pnpm run bench:check`.
|
|
392
220
|
|
|
393
|
-
##
|
|
221
|
+
## Size, portability, and release proof
|
|
394
222
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
223
|
+
The complete minified ESM runtime—including all 2,231 pinned WHATWG entity
|
|
224
|
+
names—is 23,689 bytes with deterministic `gzip -9`. The release gate also checks
|
|
225
|
+
packed exports, zero runtime dependencies, cold import time, and retained import
|
|
226
|
+
heap.
|
|
399
227
|
|
|
400
|
-
|
|
228
|
+
The same packed artifact is tested in:
|
|
401
229
|
|
|
402
|
-
|
|
230
|
+
| Runtime | Release coverage |
|
|
231
|
+
| --- | --- |
|
|
232
|
+
| Node.js | 22, 24, and 26; ESM and CommonJS |
|
|
233
|
+
| Bun | ESM and CommonJS |
|
|
234
|
+
| Deno | ESM |
|
|
235
|
+
| Cloudflare Workers | Real `workerd`, without Node compatibility |
|
|
236
|
+
| Browsers | Chromium, Firefox, and WebKit |
|
|
403
237
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
cd purifai
|
|
408
|
-
pnpm install
|
|
409
|
-
pnpm build
|
|
410
|
-
pnpm test
|
|
411
|
-
```
|
|
238
|
+
Release qualification also includes 10,000 seeded malformed-input cases,
|
|
239
|
+
adversarial scaling checks, safe-sink tests with a positive control, package
|
|
240
|
+
smoke tests, and npm OIDC provenance bound to the tagged GitHub source commit.
|
|
412
241
|
|
|
413
|
-
|
|
414
|
-
```bash
|
|
415
|
-
pnpm benchmark
|
|
416
|
-
```
|
|
242
|
+
## API reference
|
|
417
243
|
|
|
418
|
-
|
|
419
|
-
then measures Purifai's throughput, critical-attack checks, and bundle size. The
|
|
420
|
-
competitor set and exact versions are pinned in `package.json` and
|
|
421
|
-
`pnpm-lock.yaml` so results are reproducible.
|
|
244
|
+
### `toText(html, options?) → string`
|
|
422
245
|
|
|
423
|
-
|
|
246
|
+
Converts one HTML string into readable text. Throws `TypeError` for invalid
|
|
247
|
+
input or options and `PurifaiLimitError` for a breached limit.
|
|
424
248
|
|
|
425
|
-
|
|
249
|
+
### `convert(html, options?) → ConversionResult`
|
|
426
250
|
|
|
427
|
-
|
|
251
|
+
Returns text plus a frozen report containing completion, truncation, consumed
|
|
252
|
+
input, output length, and dropped-container counts. It is the only API that can
|
|
253
|
+
return a deliberately truncated prefix.
|
|
428
254
|
|
|
429
|
-
|
|
430
|
-
- Inspiration from existing sanitization libraries
|
|
431
|
-
- Comprehensive testing methodologies from security experts
|
|
255
|
+
### `createTextTransform(options?) → TextTransform`
|
|
432
256
|
|
|
433
|
-
|
|
257
|
+
Returns a native `TransformStream<string, string>` with a `result` promise for
|
|
258
|
+
the frozen conversion report. Limit failures reject both the stream and the
|
|
259
|
+
promise with the same error object.
|
|
434
260
|
|
|
435
|
-
|
|
436
|
-
- [PortSwigger XSS Labs](https://portswigger.net/web-security/cross-site-scripting)
|
|
437
|
-
- [MDN Web Security](https://developer.mozilla.org/en-US/docs/Web/Security)
|
|
261
|
+
### `escapeHtmlText(text) → string`
|
|
438
262
|
|
|
439
|
-
|
|
263
|
+
Losslessly encodes `&`, `<`, `>`, `"`, and `'` for an HTML text-node context.
|
|
440
264
|
|
|
441
|
-
|
|
265
|
+
### `PurifaiLimitError`
|
|
442
266
|
|
|
443
|
-
|
|
444
|
-
|
|
267
|
+
Extends `RangeError` and exposes `kind`, `limit`, and `observed`.
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { PurifaiLimitError, toText } from 'purifai';
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
toText(html, { limits: { input: 10_000 } });
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (error instanceof PurifaiLimitError) {
|
|
276
|
+
console.error(error.kind, error.limit, error.observed);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
445
279
|
```
|
|
446
280
|
|
|
447
|
-
|
|
281
|
+
## Which tool should you choose?
|
|
282
|
+
|
|
283
|
+
| Need | Choice |
|
|
284
|
+
| --- | --- |
|
|
285
|
+
| Fixed-policy readable text, hostile-input bounds, and portable Web streaming | Choose Purifai |
|
|
286
|
+
| Selectors, custom formatters, advanced tables, wrapping, and broad formatting control | Choose `html-to-text` |
|
|
287
|
+
| The smallest flat tag-removal operation | Choose stable `striptags` |
|
|
288
|
+
| Preserve an allow-listed safe HTML fragment | Choose DOMPurify or `sanitize-html` |
|
|
289
|
+
|
|
290
|
+
These categories are not interchangeable. DOMPurify and `sanitize-html` are
|
|
291
|
+
the correct category when safe markup must survive.
|
|
292
|
+
|
|
293
|
+
## Migration and development
|
|
294
|
+
|
|
295
|
+
V3 is a clean break. See the [v3 migration guide](docs/migration-v3.md) for every
|
|
296
|
+
removed export and option.
|
|
297
|
+
|
|
298
|
+
- [Runnable examples](examples)
|
|
299
|
+
- [Contributor guide](CONTRIBUTING.md)
|
|
300
|
+
- [Project notes](https://worksonmy.dev/projects/purifai)
|
|
301
|
+
- [Issues](https://github.com/moji2002/purifai/issues)
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
[MIT](LICENSE)
|