a
a
` | + | the read-only test value at `RichEditorField.test.ts:159` | `` | + | form/input name=nodeType + img onerror | empty string | + | img name=nodeType | empty string | + | button name=nodeType + iframe | empty string | + | `` | `` | + | `` | `t
` | + | off-origin form + password input + button Go | `Go` | + | iframe + meta refresh + `javascript:` link + style | `x` | + | `ad
` | `ad
` | + +- **happy-dom 20.11.6:** DOMPurify does NOT sanitize. Inside a vitest happy-dom environment it reports `isSupported` true, but it visits only top-level nodes: + - the `a
`; + - `one
` returns `one`; + - the clobbering vectors come back nearly intact. + + This is why Task 1 adds a canary guard, and why exact display assertions live only in the browser suite. +- **Vite build:** building DOMPurify with the admin's Vite adds only the literal `http://www.w3.org` to the JS, which the dist-origin rule already allows. + +**Browser for the test suite** +- Playwright browsers already on disk: `~/.cache/ms-playwright/chromium-1223` and `chromium_headless_shell-1223`, installed by playwright-core 1.60.0. Pinning `playwright` 1.60.0 needs no browser download. +- If Playwright still reports a missing executable, run `npx --prefix admin playwright install chromium` once and note it in the SUMMARY. + +**CSP probe (headless Chromium 1223)** +- Under the full proposed CSP, `frame-src 'none'` blocks an `t
`. + - 'drops an off-origin form, iframes, meta refresh and disallowed links': the vectors are an off-origin form action with a password input, an iframe, a meta refresh and a javascript: href. The output keeps no such element, and every loses a javascript:, ftp: or sms: href. An href on a is dropped, while ``, `` and `` keep theirs.
+ - 'parses into an inert document, so nothing loads before the value is bound': displayHtml runs on input with images pointing at a unique probe URL. A live control img must produce a resource-timing entry, which proves the observation works; the probe URL must produce none.
+ - The policy check helper parses an output with `inertFragment` from lossy.ts and requires two things. Every element's lower-cased localName must be in EDITOR_TAGS. The only attribute anywhere must be href on an a element, with isAllowedLink true.
+ - `describe('richeditor policy helpers')` 'unwraps unknown elements and drops scripts for display' moves here verbatim from RichEditorField.test.ts:186-189, with the exact ` a
ad
` with the config returns exactly `ad
`. displayHtml returns '' when it is false, because DOMPurify returns its input unchanged where it cannot run. +6. Update the header comment to cite T-10-16, T-17-10 and the decision note. html.ts must not contain any of the four raw-HTML tokens the hygiene rule at `scripts/check-phase10.sh:368` greps for, not even inside a comment: the rule counts comment lines. +7. Re-run `npm --prefix admin run test:browser` until it is GREEN. + +**E. Happy-dom suite (`admin/tests/form/RichEditorField.test.ts`).** + +1. In 'renders read-only values through the display policy, never as stored' (lines 154-168), keep the name and make the assertions environment-independent: + - the box contains no script and no element with an onclick attribute; + - the box HTML has no `javascript:`; + - every remaining a href passes isAllowedLink; + - `window.__re` stays undefined. + + Drop the `find('p').attributes` and exact href-list assertions; the browser suite owns them. +2. Delete 'unwraps unknown elements and drops scripts for display' (lines 186-189); it now lives in the browser suite. +3. Add a short comment saying happy-dom cannot run DOMPurify, so displayHtml binds nothing there (fail closed), and that exact display assertions live in `tests/browser/richeditor-display.test.ts`. +4. Run the full `npm --prefix admin test` and fix any other happy-dom test that asserted sanitized read-only richeditor content (planning grep found none outside this file; RepeaterField's read-only test checks structure only). + +**F. Docs.** + +1. In `README.md`, in the admin scripts block at lines 150-157, add `npm --prefix admin run test:browser # Vitest browser mode in Playwright Chromium (needs the chromium build; npx --prefix admin playwright install chromium once)`. +2. In `CLAUDE.md`, change the commands line `npm --prefix admin run test | typecheck | build | dev` to include `test:browser`. +3. Run the docs checker. + +**G. Commit.** Commit the code by path (package.json, package-lock.json, both vitest configs, html.ts, both test files, README.md, CLAUDE.md) as `fix(admin): sanitize the richeditor read-only display with DOMPurify (T-10-16)`. +