# DOX — packages/client/src/lib/security

Trusted Types policy set. One row per source file.

| File | Purpose |
|------|---------|
| `trusted-types.ts` | Named policies `pi-sanitized-html` (`(html, profile)` → DOMPurify with `shared/sanitize-profiles`; `eml` also resolves own blob: cid refs + constant wrapper), `pi-worker-url` (same-origin `/sw.js` / `/assets/<f>.m?js`; DEV adds Vite worker paths) + one mode-aware `default` (mode from `<meta name="pi-trusted-types">`, missing → report: pass-through + count; enforce: tag-free passes, `<svg` → mermaid profile, else HTML+SVG+MathML, `createScript` → null, script URLs → allowlist). Exports `installTrustedTypes`, `readTrustedTypesMode`, `trustedSanitizedHtml`, `trustedEmlSrcDoc`, `trustedWorkerUrl`; violation reporter `recordViolation`, `flushViolations` (≤ 50 keys + `overflow`, no samples, never throws), `startViolationReporter` (30 s + pagehide). Sanitizes with or without browser TT support. Report mode adds shadow evaluation: each `default` call classified into `SHADOW_OUTCOMES` (6 keys: would-block-script, would-block-script-url, would-alter-html, would-throw-html, not-evaluated, shadow-error) = what enforce WOULD do; output + `default`-hit counts unchanged, never throws, `inShadow` re-entry guard. HTML: separate `DOMPurify(window)` instance + private parse-only policy `pi-shadow-parse` (never exported), shared `enforceProfileFor`, two-pass test (pass 2 FORCE_BODY for leading head content), exact-string LRU memo (512 / 4 Mi units), per-30 s-window budget (200 / 2 Mi units / 50 ms) + skip set. Transport: `shadowTotals` aggregate (`shadow` report field, clamp 1,000,000, kept across failures except 400 → `shadow-error`), totals-only reports, byte-bounded priority `takeReport` (≤ 7,168 B; violations → default hits → shadow detail), sources cut to 512. Test seams `__shadowTotalsForTests`, `__shadowPurifyForTests`. Allowlisted raw-sink module. See change: harden-client-sinks, add-trusted-types-shadow-enforce. |
