Content
View differences
Updated by Alexander Coles 15 days ago
## Context
Two cross-cutting concerns in the Turbo↔Angular layer are each encoded in several places, with no single owner. Both are good consolidation (deepening) candidates and become easier once `frontend/src/turbo/` has a jsdom test seam (see #DREAM-514 / the testability work package).
## A) CSP nonce enforcement has one owner per render path instead of one owner
`scrubScriptElements` (the pure helper in `frontend/src/turbo/helpers.ts`) is unit-tested, but the security-critical behaviour lives in the wiring: it is hooked separately in three render paths in `frontend/src/turbo/turbo-event-listeners.ts` — `turbo:before-render` and `turbo:before-frame-render` (both `{ capture: true }`) and `turbo:before-stream-render` (which hand-wraps `event.detail.render`) — plus a fourth, separate `X-Turbo-Nonce` request header. A regression in any one hook (wrong phase, or a render path Turbo adds in a future version) silently lets unauthorised scripts through, and no test covers the hooks.
* - Consolidate the three hook points and the header into one `enforceScriptNonce(target = document)` module that owns every render path.
*
- Add a jsdom spec: dispatch a synthetic `turbo:before-stream-render` carrying a bad-nonce `<script>` and assert it is removed — so the security guarantee, not the leaf helper, is the test surface.
## B) Three mechanisms protect custom elements from morph; make it one policy
Three places independently decide what happens to a custom element when idiomorph runs, each with a different rule:
- `frontend/src/turbo/turbo-global-listeners.ts` — `turbo:before-morph-element` blanket `preventDefault()` on any `OPCE-*` element.
- `frontend/src/turbo/action-menu-morph-remount.ts` — `turbo:morph-element` clones the `<action-menu>` host to force `connectedCallback` to re-fire.
- `frontend/src/stimulus/controllers/dynamic/work-packages/dialog/preview.controller.ts` — its own `beforeNodeMorphed` that skips `OPCE-*` unless a scheduling flag flips.
The root fact ("Primer/Angular custom elements do not survive morph cleanly") is re-discovered and re-encoded three ways; a fourth bug will likely become a fourth special case.
- Extract a single custom-element morph policy with a small interface (e.g. `shouldMorph(node)` / `onMorphSkipped(node)`) consumed by all three call sites.
- Confirm during design whether one policy covers all three cases (whole-page vs frame vs single host) or only two — they are subtly different.
## Notes
Relates to #DREAM-514. Part A has a security edge and is the higher-value half. The `turbo_power` allowlist in `setup.ts` (a related "imperative registration" "imperative registration" smell) is being addressed upstream via https://github.com/marcoroth/turbo\_power/pull/333 https://github.com/marcoroth/turbo_power/pull/333 and is intentionally out of scope here.
Two cross-cutting concerns in the Turbo↔Angular layer are each encoded in several places, with no single owner. Both are good consolidation (deepening) candidates and become easier once `frontend/src/turbo/` has a jsdom test seam (see #DREAM-514 / the testability work package).
## A) CSP nonce enforcement has one owner per render path instead of one owner
`scrubScriptElements` (the pure helper in `frontend/src/turbo/helpers.ts`) is unit-tested, but the security-critical behaviour lives in the wiring: it is hooked separately in three render paths in `frontend/src/turbo/turbo-event-listeners.ts` — `turbo:before-render` and `turbo:before-frame-render` (both `{ capture: true }`) and `turbo:before-stream-render` (which hand-wraps `event.detail.render`) — plus a fourth, separate `X-Turbo-Nonce` request header. A regression in any one hook (wrong phase, or a render path Turbo adds in a future version) silently lets unauthorised scripts through, and no test covers the hooks.
*
*
-
Three places independently decide what happens to a custom element when idiomorph runs, each with a different rule:
- `frontend/src/turbo/turbo-global-listeners.ts` — `turbo:before-morph-element` blanket `preventDefault()` on any `OPCE-*` element.
- `frontend/src/turbo/action-menu-morph-remount.ts` — `turbo:morph-element` clones the `<action-menu>` host to force `connectedCallback` to re-fire.
- `frontend/src/stimulus/controllers/dynamic/work-packages/dialog/preview.controller.ts` — its own `beforeNodeMorphed` that skips `OPCE-*` unless a scheduling flag flips.
The root fact ("Primer/Angular custom elements do not survive morph cleanly") is re-discovered and re-encoded three ways; a fourth bug will likely become a fourth special case.
- Extract a single custom-element morph policy with a small interface (e.g. `shouldMorph(node)` / `onMorphSkipped(node)`) consumed by all three call sites.
- Confirm during design whether one policy covers all three cases (whole-page vs frame vs single host) or only two — they are subtly different.
##
Relates to #DREAM-514. Part A has a security edge and is the higher-value half. The `turbo_power` allowlist in `setup.ts` (a related "imperative registration"