{"_id":"@bartolli/vaglio","_rev":"3-fb1a2a984e516d7d4ed29eb301757f74","name":"@bartolli/vaglio","dist-tags":{"latest":"0.1.0","rc":"0.1.0-rc.3"},"versions":{"0.1.0-rc.0":{"name":"@bartolli/vaglio","version":"0.1.0-rc.0","keywords":["llm","prompt-injection","sanitize","unicode","nfkc","homoglyph","redaction","trust-boundary","ai-security"],"author":{"name":"Angel Bartolli","email":"bartolli@gmail.com"},"license":"MIT","_id":"@bartolli/vaglio@0.1.0-rc.0","maintainers":[{"name":"bartolli","email":"bartolli@gmail.com"}],"homepage":"https://github.com/bartolli/vaglio#readme","bugs":{"url":"https://github.com/bartolli/vaglio/issues"},"dist":{"shasum":"1505bc0a95a2c4ecdac6b1f739ca189055e23dbd","tarball":"https://registry.npmjs.org/@bartolli/vaglio/-/vaglio-0.1.0-rc.0.tgz","fileCount":56,"integrity":"sha512-2nGGEH+Dkde75KSSu78LQxvA5yyInyyycc+zMb/A0zYqJQljCzt+9au8cvxxdK8GoPfgPvF9Fn/8Gn7wB8PmtQ==","signatures":[{"sig":"MEUCIQDtgQnDOY9s7Io/fp3nWB/G+0OLhzsKiqF8VSz90S7u8gIgd00QWMHbJjwY5X8NiYRtqwII7Qr+jvH8xmqvVC/4ov8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":179831},"main":"./dist/index.js","pnpm":{"onlyBuiltDependencies":[]},"type":"module","types":"./dist/index.d.ts","engines":{"node":">=22.0.0","pnpm":">=10.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./tags":{"types":"./dist/subpath-tags.d.ts","import":"./dist/subpath-tags.js"},"./unicode":{"types":"./dist/subpath-unicode.d.ts","import":"./dist/subpath-unicode.js"},"./credentials":{"types":"./dist/subpath-credentials.d.ts","import":"./dist/subpath-credentials.js"},"./package.json":"./package.json"},"gitHead":"85f284e44947bbc16be560f7d2ea384fe56d4e48","scripts":{"attw":"attw --pack . --profile esm-only","lint":"biome check .","size":"size-limit","test":"vitest run","build":"tsc -p tsconfig.build.json","clean":"rm -rf dist","format":"biome format --write .","prepack":"pnpm run clean && pnpm run build","publint":"publint","lint:fix":"biome check --write .","typecheck":"tsc --noEmit -p tsconfig.json","test:smoke":"vitest run tests/integration/ollama-smoke.test.ts","test:watch":"vitest","test:exports":"vitest run --config vitest.exports.config.ts","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"bartolli","email":"bartolli@gmail.com"},"repository":{"url":"git+https://github.com/bartolli/vaglio.git","type":"git"},"_npmVersion":"10.9.3","description":"Sanitize text crossing the LLM trust boundary: Unicode prompt-injection defense, NFKC normalization, credential redaction, reasoning-tag stripping. Stream-aware, zero deps, ESM-first.","directories":{},"sideEffects":false,"_nodeVersion":"22.20.0","_hasShrinkwrap":false,"packageManager":"pnpm@10.33.0","devDependencies":{"vite":"^8.0.10","ollama":"^0.6.3","vitest":"^4.1.5","publint":"^0.3.18","fast-check":"^4.7.0","size-limit":"^12.1.0","typescript":"^6.0.3","@types/node":"^22.19.17","@biomejs/biome":"^2.4.13","@vitest/coverage-v8":"^4.1.5","@arethetypeswrong/cli":"^0.18.2","@size-limit/preset-small-lib":"^12.1.0"},"_npmOperationalInternal":{"tmp":"tmp/vaglio_0.1.0-rc.0_1777751218739_0.7166148264604038","host":"s3://npm-registry-packages-npm-production"}},"0.1.0-rc.3":{"name":"@bartolli/vaglio","version":"0.1.0-rc.3","keywords":["llm","prompt-injection","sanitize","unicode","nfkc","homoglyph","redaction","trust-boundary","ai-security"],"author":{"name":"Angel Bartolli","email":"bartolli@gmail.com"},"license":"MIT","_id":"@bartolli/vaglio@0.1.0-rc.3","maintainers":[{"name":"bartolli","email":"bartolli@gmail.com"}],"homepage":"https://github.com/bartolli/vaglio#readme","bugs":{"url":"https://github.com/bartolli/vaglio/issues"},"dist":{"shasum":"02b3eac3f042a80401c5801be6feeb8d57220018","tarball":"https://registry.npmjs.org/@bartolli/vaglio/-/vaglio-0.1.0-rc.3.tgz","fileCount":56,"integrity":"sha512-xO99OVgeJrraKagsypgEz5OIPj9M1Fyye8j3xC4Cdh6eBiLMkf1ocXJdzMdzBbKqy36OHA7e9C06jngYorQA0w==","signatures":[{"sig":"MEQCIEo648Vn0IIQ9zhKDWuNvjIMISpREn5d79vg9dfCbPpnAiB0mUW/THkiFO8LYEhY9O8QsvYdXAQvNt1mu0e0GecvjQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bartolli%2fvaglio@0.1.0-rc.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":180638},"main":"./dist/index.js","pnpm":{"onlyBuiltDependencies":[]},"type":"module","types":"./dist/index.d.ts","engines":{"node":">=22.0.0","pnpm":">=10.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./tags":{"types":"./dist/subpath-tags.d.ts","import":"./dist/subpath-tags.js"},"./unicode":{"types":"./dist/subpath-unicode.d.ts","import":"./dist/subpath-unicode.js"},"./credentials":{"types":"./dist/subpath-credentials.d.ts","import":"./dist/subpath-credentials.js"},"./package.json":"./package.json"},"gitHead":"a08038b9b4eba05858ac95eb8dec271b0e3de9a5","scripts":{"attw":"attw --pack . --profile esm-only","lint":"biome check .","size":"size-limit","test":"vitest run","build":"tsc -p tsconfig.build.json","clean":"rm -rf dist","format":"biome format --write .","prepack":"pnpm run clean && pnpm run build","publint":"publint","lint:fix":"biome check --write .","typecheck":"tsc --noEmit -p tsconfig.json","test:smoke":"vitest run tests/integration/ollama-smoke.test.ts","test:watch":"vitest","test:exports":"vitest run --config vitest.exports.config.ts","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:3272e7e1-a5f5-4d2e-9cfc-3b48f58c9af9"}},"repository":{"url":"git+https://github.com/bartolli/vaglio.git","type":"git"},"_npmVersion":"11.11.0","description":"Sanitize text crossing the LLM trust boundary: Unicode prompt-injection defense, NFKC normalization, credential redaction, reasoning-tag stripping. Stream-aware, zero deps, ESM-first.","directories":{},"sideEffects":false,"_nodeVersion":"24.14.1","_hasShrinkwrap":false,"packageManager":"pnpm@10.33.0","readmeFilename":"README.md","devDependencies":{"vite":"^8.0.10","ollama":"^0.6.3","vitest":"^4.1.5","publint":"^0.3.18","fast-check":"^4.7.0","size-limit":"^12.1.0","typescript":"^6.0.3","@types/node":"^22.19.17","@biomejs/biome":"^2.4.13","@vitest/coverage-v8":"^4.1.5","@arethetypeswrong/cli":"^0.18.2","@size-limit/preset-small-lib":"^12.1.0"},"_npmOperationalInternal":{"tmp":"tmp/vaglio_0.1.0-rc.3_1777753633830_0.1685256156924675","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@bartolli/vaglio","version":"0.1.0","description":"Sanitize text crossing the LLM trust boundary: Unicode prompt-injection defense, NFKC normalization, credential redaction, reasoning-tag stripping. Stream-aware, zero deps, ESM-first.","license":"MIT","author":{"name":"Angel Bartolli","email":"bartolli@gmail.com"},"homepage":"https://github.com/bartolli/vaglio#readme","repository":{"type":"git","url":"git+https://github.com/bartolli/vaglio.git"},"bugs":{"url":"https://github.com/bartolli/vaglio/issues"},"keywords":["llm","prompt-injection","sanitize","unicode","nfkc","homoglyph","redaction","trust-boundary","ai-security"],"type":"module","engines":{"node":">=22.0.0","pnpm":">=10.0.0"},"packageManager":"pnpm@10.33.0","sideEffects":false,"main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./unicode":{"types":"./dist/subpath-unicode.d.ts","import":"./dist/subpath-unicode.js"},"./credentials":{"types":"./dist/subpath-credentials.d.ts","import":"./dist/subpath-credentials.js"},"./tags":{"types":"./dist/subpath-tags.d.ts","import":"./dist/subpath-tags.js"},"./package.json":"./package.json"},"scripts":{"build":"tsc -p tsconfig.build.json","clean":"rm -rf dist","typecheck":"tsc --noEmit -p tsconfig.json","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","test:smoke":"vitest run tests/integration/ollama-smoke.test.ts","test:exports":"vitest run --config vitest.exports.config.ts","lint":"biome check .","lint:fix":"biome check --write .","format":"biome format --write .","publint":"publint","attw":"attw --pack . --profile esm-only","size":"size-limit","prepack":"pnpm run clean && pnpm run build"},"devDependencies":{"@arethetypeswrong/cli":"^0.18.2","@biomejs/biome":"^2.4.13","@size-limit/preset-small-lib":"^12.1.0","@types/node":"^22.19.17","@vitest/coverage-v8":"^4.1.5","fast-check":"^4.7.0","ollama":"^0.6.3","publint":"^0.3.18","size-limit":"^12.1.0","typescript":"^6.0.3","vite":"^8.0.10","vitest":"^4.1.5"},"pnpm":{"onlyBuiltDependencies":[]},"gitHead":"62c6277f1fe7307b9ffefca2ef09249a6638c88a","_id":"@bartolli/vaglio@0.1.0","_nodeVersion":"24.14.1","_npmVersion":"11.11.0","dist":{"integrity":"sha512-uxHkUrxyvY+g3AVbm5Zvzj1Jdys26eyb6exIj26usq74MlBX5KZvBiXaiJZ4+TAk2MQ0HfeDG3vUsGa7FYLMyA==","shasum":"c7c00e0b468a38551c1d43c721b2e9b5e6ce9f56","tarball":"https://registry.npmjs.org/@bartolli/vaglio/-/vaglio-0.1.0.tgz","fileCount":56,"unpackedSize":180855,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bartolli%2fvaglio@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBFtzFbVCg2VLl7OyFBVh34i/5naPf21eXxdv7O1VCn5AiB+Pod4+QGuOJS7+XudeX7xUSLxXibDCp77o0a/Tq/L+A=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:3272e7e1-a5f5-4d2e-9cfc-3b48f58c9af9"}},"directories":{},"maintainers":[{"name":"bartolli","email":"bartolli@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/vaglio_0.1.0_1777754119810_0.1742583749126443"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-02T19:46:58.670Z","modified":"2026-05-02T20:35:20.256Z","0.1.0-rc.0":"2026-05-02T19:46:58.874Z","0.1.0-rc.3":"2026-05-02T20:27:13.971Z","0.1.0":"2026-05-02T20:35:19.953Z"},"bugs":{"url":"https://github.com/bartolli/vaglio/issues"},"author":{"name":"Angel Bartolli","email":"bartolli@gmail.com"},"license":"MIT","homepage":"https://github.com/bartolli/vaglio#readme","keywords":["llm","prompt-injection","sanitize","unicode","nfkc","homoglyph","redaction","trust-boundary","ai-security"],"repository":{"type":"git","url":"git+https://github.com/bartolli/vaglio.git"},"description":"Sanitize text crossing the LLM trust boundary: Unicode prompt-injection defense, NFKC normalization, credential redaction, reasoning-tag stripping. Stream-aware, zero deps, ESM-first.","maintainers":[{"name":"bartolli","email":"bartolli@gmail.com"}],"readme":"# vaglio\n\nSanitize text crossing the LLM trust boundary.\n\nText-domain filter between extracted content, tool output, RAG chunks,\npeer-model output, and the model. Zero runtime dependencies, ESM-only,\nstream-aware, deterministic.\n\n> Pre-1.0. The v0.1 surface is spec-locked. Breaking changes possible at\n> minor-version boundaries until 1.0.\n\n## Install\n\n```sh\npnpm add @bartolli/vaglio\n```\n\nRequires Node ≥ 22 LTS. ESM-only.\n\n## Quick start\n\n```ts\nimport { sanitize } from '@bartolli/vaglio';\n\nconst safe = sanitize(untrustedText);\nawait llm.send(safe);\n```\n\nPipeline order: NFKC → strip Unicode invisibles → strip reasoning-tag\nblocks → redact credentials. All four stages run against\n[`DEFAULT_POLICY`](#default-policy).\n\n## Threat coverage\n\nDeterministic. No ML, no entropy heuristics.\n\n- **Unicode invisibles.** Tags block (U+E0001–U+E007F), zero-width\n  (ZWSP, ZWNJ, BOM, word joiner), bidi overrides (U+202A–U+202E,\n  U+2066–U+2069), Mongolian Free Variation Selectors, interlinear\n  annotations, supplementary PUA, supplementary variation selectors,\n  soft hyphen / CGJ / Hangul fillers, invisible math operators, orphaned\n  UTF-16 surrogates.\n- **Homoglyph forging via NFKC.** Mathematical-alphanumeric\n  (𝐬𝐲𝐬𝐭𝐞𝐦 → system) and fullwidth (＜system＞ → \\<system\\>) collapse to\n  ASCII. Cross-script (Greek↔Latin, Cyrillic↔Latin) preserved by spec.\n- **Zalgo.** Combining-mark cap per base character (default 4).\n- **ANSI escapes.** `ESC[…]` sequences — invisible in terminals,\n  tokenized verbatim by LLMs.\n- **C0/C1 controls.** U+0000–U+001F and U+007F–U+009F stripped except\n  `\\t`, `\\n`, `\\r`. NBSP (U+00A0) preserved (printable, not control).\n- **Reasoning-tag leakage.** `<internal>…</internal>` blocks; tag-name\n  set is configurable.\n- **Credentials.** Anthropic, AWS (AKIA/ASIA), Bearer JWT, Slack, GitHub\n  PAT, Stripe restricted, PEM private key (RSA / EC / Ed25519 /\n  generic), long hex (≥ 64 chars). Default placeholder `<credential>`.\n\n## Where it plugs in\n\n```\nURL  → fetch → HTML→markdown extractor          → markdown ┐\nFile → document extractor (PDF, DOCX, …)        → text     │\nHTML → HTML→text converter                      → text     │\nRAG  → retrieve → chunk                         → text     ├→ sanitize() → LLM\nTool → API call → serialize                     → string   │\nLLM  → output (router / planner / loop step)    → text     ┘\n```\n\nThe last row is the inter-model trust boundary. The receiving model\ntreats peer-model output as untrusted — same threat surface as user\ninput.\n\n## Recipes\n\n### Sanitize a string\n\n```ts\nimport { sanitize } from '@bartolli/vaglio';\nconst safe = sanitize(text);\n```\n\n### Telemetry via callback\n\nNon-`Detailed` variants build no findings array unless `onFinding` is\nprovided. Silent operation is the default cost.\n\n```ts\nimport { sanitize } from '@bartolli/vaglio';\n\nsanitize(text, {\n  onFinding: (f) => metrics.emit(f.kind, f.ruleId, f.severity),\n});\n```\n\n### Detail variant\n\n```ts\nimport { sanitizeDetailed } from '@bartolli/vaglio';\n\nconst result = sanitizeDetailed(input);\nif (result.text === input) return input; // identity-check fast path (contract)\naudit.write(result.findings);\nreturn result.text;\n```\n\n### Web Streams\n\nCross-chunk credentials redact via an internal sliding-window buffer\n(`Policy.bufferLimit`, auto-derived from the longest active pattern + 64).\n\n```ts\nimport { createSanitizeStream } from '@bartolli/vaglio';\n\nawait response.body!\n  .pipeThrough(new TextDecoderStream())\n  .pipeThrough(createSanitizeStream({ onFinding: emitMetric }))\n  .pipeTo(modelInputSink);\n```\n\n### Async iterable\n\n```ts\nimport { sanitizeIterable } from '@bartolli/vaglio';\n\nasync function* turns() {\n  for await (const turn of agentLoop) yield turn.content;\n}\n\nfor await (const safe of sanitizeIterable(turns(), { onFinding: emitMetric })) {\n  yield safe;\n}\n```\n\n### Custom credential pattern\n\n```ts\nimport { policy, sanitize } from '@bartolli/vaglio';\n\nconst myPolicy = policy()\n  .addCredentialPattern(/sot-session-[a-z0-9]{32}/i, {\n    ruleId: 'sot-session',\n    placeholder: '<session>',\n    severity: 'high',\n  })\n  .build();\n\nconst safe = sanitize(text, { policy: myPolicy });\n```\n\nThe builder is immutable: each method returns a new instance; `build()`\nreturns a frozen `Policy`. Reuse a base to derive variants:\n\n```ts\nconst base     = policy().addReasoningTag('plan');\nconst stricter = base.disableUnicodeCategory('soft-hyphen-fillers').build();\nconst lenient  = base.build();\n```\n\n### Per-hop policy in a model-to-model chain\n\n```ts\nimport { policy, sanitize } from '@bartolli/vaglio';\n\nconst planner = policy().addReasoningTag('scratchpad').addReasoningTag('plan').build();\nconst router  = policy().build();\n\nconst safeRouterOut = sanitize(routerOutput, { policy: router });\nawait mainModel.generate({ context: safeRouterOut });\n\nconst safePlannerOut = sanitize(plannerOutput, { policy: planner });\nawait worker.run(safePlannerOut);\n```\n\n### Low-latency streaming\n\nThe default `bufferLimit` is **4160** characters — driven by the PEM\nprivate-key pattern (`maxMatchLength: 4096` + 64). At ~4 chars/token,\ndownstream sees ~1k tokens of holdback. Drop the PEM pattern for\ntoken-stream agents that don't ingest PEM blocks:\n\n```ts\nimport { policy, createSanitizeStream } from '@bartolli/vaglio';\n\nconst lowLatency = policy().removeCredentialPattern('pem-private-key').build();\n// bufferLimit = 320 (~80 tokens)\n\nconst stream = createSanitizeStream({ policy: lowLatency });\n```\n\n### Tool result and RAG\n\n```ts\nimport { sanitize } from '@bartolli/vaglio';\n\nconst apiResponse    = await externalTool(args);\nconst safeToolResult = sanitize(JSON.stringify(apiResponse));\n\nconst chunks  = await retriever.search(query);\nconst context = chunks.map((c) => sanitize(c.text)).join('\\n\\n');\n```\n\n## Findings\n\n```ts\ntype Finding =\n  | { kind: 'unicode-strip';     ruleId: string; ruleVersion: number; action: PolicyAction; offset: number; length: number; charClass: string; count: number; severity: Severity; }\n  | { kind: 'credential';        ruleId: string; ruleVersion: number; action: PolicyAction; offset: number; length: number; placeholder: string; severity: Severity; }\n  | { kind: 'stream-diagnostic'; ruleId: string; ruleVersion: number; severity: Severity; message: string; };\n\ntype PolicyAction = 'stripped' | 'redacted' | 'replaced';\ntype Severity     = 'low' | 'medium' | 'high' | 'critical';\n```\n\nIn v0.1, `unicode-strip` and reasoning-tag findings emit\n`action: 'stripped'`; credential findings emit `action: 'redacted'`.\n`'replaced'` is reserved.\n\nFindings carry no raw snippets — emitting partial credentials or PII\ninto telemetry is a secondary leakage anti-pattern.\n\n`ruleVersion` increments per CHANGELOG when a builtin's pattern,\nseverity, or `charClass` changes. Forensic logs survive pattern updates.\n\nContinuous identical infractions (Zalgo run, repeated Tags-block\ncodepoints) aggregate into a single finding via `count` and `length`.\n\n## Default policy\n\n`DEFAULT_POLICY`:\n\n| Slot                 | Default                                 |\n| -------------------- | --------------------------------------- |\n| Unicode categories   | all 13 enabled (see below)              |\n| Combining-mark cap   | 4 per base character                    |\n| NFKC                 | enabled                                 |\n| Reasoning tags       | `internal`                              |\n| Credential patterns  | 8 builtins                              |\n| Placeholder          | `<credential>`                          |\n| `bufferLimit`        | 4160 (PEM-driven)                       |\n\nCategories: `tags-block`, `zero-width`, `bidi-override`, `mongolian-fvs`,\n`interlinear-annotations`, `object-replacement`, `supplementary-pua`,\n`supplementary-variation-selectors`, `soft-hyphen-fillers`,\n`math-invisibles`, `orphaned-surrogates`, `ansi-escapes`,\n`c0-c1-controls`.\n\nEvery slot is tunable via the builder.\n\n## Streaming contract\n\n- Identical semantics across batch (`sanitize`, `redact`), Web Streams\n  (`createSanitizeStream`, `createRedactStream`), and async iterables\n  (`sanitizeIterable`, `redactIterable`).\n- Sliding-window buffer holds back the trailing `bufferLimit` characters\n  per push so a chunk-straddling credential lands in the next leading\n  region.\n- Overflow: when the held tail can't shrink without losing a match,\n  oldest bytes commit and a `stream-diagnostic` finding fires\n  (`ruleId: 'buffer-overflow-warning'`). Redaction trumps fidelity.\n- Cancel: `TransformStream.cancel(reason)` or async-iter `return()`\n  releases the buffer and discards partial-credential state. Subsequent\n  operations throw `VaglioStreamCanceledError`. A final\n  `stream-diagnostic` finding (`ruleId: 'stream-canceled'`) emits via\n  `onFinding` if subscribed.\n- `flush()` is idempotent. Double-flush is a no-op. `push()` after\n  `flush()` throws.\n- Errors are fail-fast: catastrophic regex backtracking or corrupted\n  state tears down the stream. Consumers must not retry.\n- Identity preservation does not extend to streaming — use the batch\n  surface for reference-equality fast paths.\n\n## Roadmap\n\nPlanned for v0.2 and beyond:\n\n- **Cross-script homoglyph folding** (Greek↔Latin, Cyrillic↔Latin) as an\n  opt-in `Policy` flag. Off by default — folding across scripts would\n  destroy legitimate non-Latin text.\n- **ReDoS static analysis** on user-supplied patterns at `Policy.build()`\n  time. v0.1 builtins are pre-validated at library-build time; user\n  patterns are accepted unchecked.\n- **Detail-variant streams.** Streaming exposes `onFinding` only in v0.1;\n  a `createSanitizeStreamDetailed` shape is under consideration.\n- **Pluggable Unicode rules** — `addUnicodeRule({ ruleId, range, action })`\n  for codepoint-range stripping outside the closed `UnicodeCategory` set.\n  Workaround in v0.1: `addStripPattern` with a regex.\n- **Control-token forging detection** — model-specific message-role\n  markers (`<|im_start|>`, `Human:` / `Assistant:`, etc.). Requires\n  per-model presets.\n- **External-content tagging** — auto-wrap RAG output in\n  `<untrusted-data>…</untrusted-data>`.\n\n## Out of scope\n\nThese belong in other layers, not in vaglio:\n\n- **HTML parsing.** Vaglio operates on text and markdown only. Run an\n  HTML sanitizer upstream.\n- **Content extraction.** Vaglio sanitizes already-extracted text, not\n  raw documents. Run an extractor upstream.\n- **Binary / EXIF stripping.** Different domain (image / file).\n- **Channel-specific output formatting.** App policy — chat-platform\n  rendering, emoji allowlists, length caps, casing normalization.\n- **Non-deterministic detection.** Repetition / entropy heuristics,\n  ML-based detection, indirect prompt-injection (IPIA) defenses,\n  token-boundary disruption mitigation. Vaglio is deterministic by\n  contract — these belong in a separate detection layer.\n\n## Compatibility\n\n| Runtime                                            | v0.1                              |\n| -------------------------------------------------- | --------------------------------- |\n| Node ≥ 22 LTS                                      | CI matrix on 22 + 24              |\n| Browsers                                           | supported via standard Web APIs (not in CI) |\n| Edge runtimes                                      | supported via standard surfaces (not in CI) |\n| Alternative JS runtimes                            | deferred from CI                  |\n\n## License\n\n[MIT](./LICENSE)\n","readmeFilename":"README.md"}