fix(web): harden the app-origin paths and correct three docs

Copilot review round 1 on #2100.

- POST /api/app-document: `null` and `[1,2]` are valid JSON, so the
  destructure that followed threw OUTSIDE the parse try/catch and turned a
  malformed body into a 500 instead of the documented 400.
- app-origin controller: the error handler cleared `server` but not
  `origin`, and `publish` gates on `origin` — so an error after a successful
  listen kept minting URLs on a dead port and the browser never took the
  srcdoc fallback. Clear both, and drop the now-unfetchable documents.
- sandbox proxy: drop a stale `srcdoc` before navigating to a dedicated
  origin (srcdoc wins over src), and restore the opaque-origin posture —
  strip `allow-same-origin`, reset `innerOrigin` — before assigning `srcdoc`
  on the fallback path. Not reachable today (each render remounts the
  iframe), but the cost of being wrong about that later is an isolation
  boundary.
- publishAppDocument tests: replace two `as unknown as Response` casts with
  real `Response`s.
- docs: `frame-ancestors` admits the proxy AND the Inspector page (every
  ancestor is checked); the shared helper falls back to loopback, not
  `'none'`; the showcase origin is `127.0.0.1`, the default bind address.
- tests: null/array body, post-listen error, and both factories receiving a
  working `publishAppDocument`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-23 23:48:30 -04:00 • committed by GitHub
commit fa441da85f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -79,9 +79,11 @@ export function sandboxFrameAncestors(allowedOrigins?: string[]): string {
* {@link LOOPBACK_FRAME_ANCESTORS} when nothing valid remains.
*
* Shared with the app-origin server (#2056), which restricts a published app
* document to being framed by the sandbox proxy. Same grammar, same failure
* mode — a directive whose only source is invalid degrades to `'none'` and
* blocks the frame — so the two derive it here rather than each rolling one.
* document to being framed by the sandbox proxy and the Inspector page above
* it. Same grammar, same fallback — an all-invalid list yields the loopback
* family rather than an empty source list, since a `frame-ancestors` with no
* sources means `'none'` and would block the frame outright — so the two
* derive it here rather than each rolling one.
*/
export function frameAncestorsDirective(origins?: string[]): string {
const valid = (origins ?? []).filter((o) => CSP_HOST_SOURCE.test(o));