dor iframe: transparent proxy for the iframe surface#142
Conversation
Implements the "Transparent Proxy (Instrumented Iframe)" section of docs/specs/dor-iframe.md. Instead of pointing the <iframe> at the target, Dormouse stands up a loopback proxy that serves the bytes — which lets it inject a shim, see the upstream result, and resolve limitations #1–#4. Proxy host (vscode-ext/src/iframe-proxy-host.ts): a loopback-only HTTP proxy modeled on the agent-browser stream relay, but it parses/rewrites responses and passes WebSocket upgrades through. Loopback http upstreams are fully instrumented (strip X-Frame-Options, drop the page CSP, inject the fixed leader shim); a remote that refuses framing or an unreachable server is diagnosed and served as a Dormouse error page; https/link-local targets are refused. Each grant gets its own ephemeral server bound to one upstream, so it is inherently not an open forwarder and root-relative sub-resources proxy transparently with no body rewriting — and no token in the URL, which would land in location.pathname and break client routers. Wiring: createIframeProxyUrl on PlatformAdapter + IframeProxyResult, the VSCodeAdapter impl, the iframe:createProxyUrl/iframe:proxyUrl messages and router handler, and the webview frame-src CSP narrowed to the loopback proxy origin. Keyboard side-channel + focus model: the injected shim forwards only the reserved leader chord (dual-tap ⌘/⇧) via postMessage; use-wall-keyboard validates the origin against live proxy grants and re-enters the same dispatch (#1). use-window-focused and Wall's blur handler read document.hasFocus() so a focused iframe no longer reads as backgrounded (#2). IframePanel registers a focus handle (focusSession can focus the frame, #3) and adopts frame-focus as entering the pane (clicks into a cross-origin frame don't bubble a mousedown). The proxy's served error pages replace the blind stall hint (#4). The frame is sandboxed without allow-top-navigation for anti-framebust. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A cross-origin iframe is an out-of-process frame, so Chromium maps pointer events to it relative to its nearest compositing/containing ancestor. Dockview's root (.dv-dockview) sets `contain: layout`, which made that far-away root the frame's coordinate origin — clicks landed offset by the pane's distance from it (~340px for a split pane). Give the iframe's immediate container its own identity compositing layer (transform: translateZ(0)), co-located with the frame, so it becomes the nearest reference and the offset collapses to ~0. translateZ(0) is identity, so getBoundingClientRect (overlay measurement) is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Moves "The Transparent Proxy" out of Future Work into the body as the implemented substrate, with pointers to what shipped. Documents the two deliberate deviations from the original sketch — per-grant dedicated loopback server (not a shared port) and no token in the URL (a path token breaks client-side routers) — and the new cross-origin-frame cursor-offset fix. Marks limitations #2/#3/#4 resolved and reframes #1 as mitigated. Adds Known v1 gaps (https deferred, absolute-origin sub-resources bypass, buffered streaming SSR, no teardown-on-kill hook) and turns the Open Decisions into the decisions actually made. Path 1 / Path 2 / render backends remain future work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both the `end` and `error` stream handlers decoded the buffered body with the same expression; hoist it to a single `complete` callback. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the ternary-in-dependency-array for the proxy-origin registration effect with a named derived value used by both the effect body and its dependency array. Same behavior, reads as a plain value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploying mouseterm with
|
| Latest commit: |
2d0ee43
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://d40838a8.mouseterm.pages.dev |
| Branch Preview URL: | https://iframe-shim.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
Solid, carefully-reasoned implementation — the focus model, the faithful shim port of handle-dual-tap, and the per-grant single-upstream isolation all hang together well. One latent edge case worth a look (inline).
|
@dormouse-bot if we hit truncation, I definitely do not want to serve the truncated HTML. I want to serve a page that says "Error, exceeded HTML_BODY_LIMIT of XXX". Push a commit to make it that way. |
When an upstream HTML response exceeds HTML_BODY_LIMIT, collectBody previously destroyed the stream and handed back the partial bytes, which were then instrumented and served as a truncated document. Serve a clear "exceeded HTML_BODY_LIMIT of N MB" error page instead, so we never ship half a page (where the shim may have landed past the cutoff). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Done in
Extension bundles clean via esbuild. |
stream.destroy() (no error arg) emits 'close'/'aborted' but neither 'end' nor 'error', so the listeners that call complete() never fired on the truncation path — the over-limit branch in collectBody's callback was unreachable and the downstream response hung with nothing served. Call complete() explicitly after destroy(), guarded by a 'settled' flag so the normal end/error path can't double-serve. Co-Authored-By: Claude <noreply@anthropic.com>
main landed the original iframe surface (#142 squash) plus two follow-ups that taught the monolithic vscode-ext/src/iframe-proxy-host.ts to serve an error page when collectBody truncated an over-limit HTML body. This branch refactored that file into a thin wrapper — the proxy now lives in lib/src/host/iframe-proxy.ts (shared with the Tauri sidecar) and streams the HTML instead of buffering it. With streaming there is no full-body buffer and thus no truncation failure mode, so main's truncation handling is superseded; the conflict is resolved by keeping the thin wrapper. Merged tree is otherwise identical to this branch's tip. Verified: lib tsc + 620 tests, VS Code bundle, standalone tsc, sidecar build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dor iframe: transparent proxy for the iframe surfaceImplements the "Transparent Proxy (Instrumented Iframe)" section of
docs/specs/dor-iframe.md.dor iframe <url>previously pointed a raw<iframe>at the target — a separate browsing context Dormouse couldn't observe or control, which made it unusable for real interaction (focus left Dormouse, blocked pages showed a blank pane, no error signals). This fronts the target with a host-owned loopback proxy so Dormouse serves the bytes, and from that one capability the surface gains a keyboard side-channel, an accurate focus model, and real error pages. It's now usable for loopback dev servers.How it works
Instead of framing the target, the panel asks the host for a proxy URL (
createIframeProxyUrl) and frames that. The proxy (vscode-ext/src/iframe-proxy-host.ts) fetches the upstream and serves it back on loopback, with policy by target:httpX-Frame-Options, drop page CSP, inject the shimhttp, frameablehttp, refuses framingdor ab open <url>httpsdor abhintpostMessage; every other keystroke flows to the tool. The Wall validatesevent.originagainst live proxy grants and re-enters the same dispatch (exitTerminalMode).document.hasFocus()(stays true while a descendant frame holds focus) instead of going inactive; the surface registers a focus handle sofocusSessioncan focus it, and adopts "frame took focus" as entering the pane (clicking a cross-origin frame doesn't bubblemousedown).sandboxwithoutallow-top-navigation; SSRF guard refusing link-local/metadata ranges.frame-ancestorsper-directive and treats only a standalone*as permissive — a scoped source likehttps://*.example.comis correctly classed as refusing rather than slipping through to a blank frame.contain: layoutroot made Chromium map pointer events to the wrong origin (clicks landed offset). Fixed by giving the iframe's container its own identity compositing layer (transform: translateZ(0)).frame-src http: https:toframe-src http://127.0.0.1:* http://localhost:*.Design notes (deliberate deviations from the original spec sketch)
location.pathnameand broke client-side routers (a React-Router/Remix dev server matched no route and rendered its own 404). The dedicated server is the isolation boundary instead.The spec (
docs/specs/dor-iframe.md) is updated to match: the proxy moves out of "Future Work," limitations #2–#4 are marked resolved, and the open decisions become the decisions actually made. Path 1 (swappable render backend) and Path 2 (plugin system) remain future work.Host coverage
Implemented for the VS Code host. Other hosts degrade gracefully —
createIframeProxyUrlis optional; where it's absent the panel falls back to a raw, uninstrumented<iframe>.Testing
tsc -bclean; full lib suite green (592 tests); extension bundles clean.Known v1 gaps
httpsupstreams deferred.ws://localhost:5173HMR) bypass the proxy — harmless for loopback, but uninstrumented.hasRestrictiveFrameAncestorsis not unit-tested (vscode-ext has no test harness); covered when the helpers move to the sharedlib/module.🤖 Generated with Claude Code