feat: proxy-bridge directive + touch drag recovery (prereview --external)#134
Conversation
…ternal - lvt-fx:proxy-bridge: relays the proxied page's nav (-> setProxyURL) and scroll (-> pin-layer transform) from the injected beacon; locates a region by posting a focus message into the iframe (navigate + scroll). Re-applies the pin-layer transform on every render (morphdom-wipe recovery). - region-select gains a data-surface="page" branch (regionRectFromBox): converts a drawn box to document fractions via beacon scroll/doc metrics. - attachBoxDragSelect: on touch pointers, finalize the region on pointercancel / lostpointercapture instead of discarding (iOS long-drag recovery); mouse/pen unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ReviewOverall the approach is clean and the touch-recovery logic is correct. Two security issues and one test nit to address. Security1. Origin filtering silently falls through when iframe has no src (medium) In let expectedOrigin = "";
try {
if (iframe && iframe.src) expectedOrigin = new URL(iframe.src).origin;
} catch { expectedOrigin = ""; }
...
if (expectedOrigin && e.origin !== expectedOrigin) return; // skipped when emptyIf the iframe's Fix: fail closed — if the origin can't be resolved, log a warning and skip attaching the listener. if (!expectedOrigin) {
console.warn('lvt-fx:proxy-bridge: could not resolve iframe origin, skipping attach');
return;
}2. win.postMessage({ __prereviewFocus: true, url, y }, expectedOrigin || "*");Same empty-origin condition — focus messages (annotation URL + y-fraction) get broadcast to all origins. The data is not highly sensitive, but it contradicts the stated "read-only, origin-validated" contract. Fixing the early-return above eliminates this path too. Test nit
The rest looks good
|
Address PR review: if `new URL(iframe.src).origin` can't resolve (missing/blank src), reject every inbound postMessage instead of falling through, and never broadcast the focus message to "*" — only to the resolved proxied origin. Warn once on an unresolvable origin (should never happen — the server renders an absolute src). +test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Picks up livetemplate/client#134's PR-review fix: the proxy-bridge directive now fails closed when the iframe origin can't be resolved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the origin findings — the proxy-bridge now fails closed: an unresolvable iframe origin rejects all inbound messages and never broadcasts focus (no On the test nit: |
|
Review Touch drag recovery looks correct. The three new tests (cancel-keeps, lostcapture-keeps, tiny-cancel-still-dropped) cover the important cases. One edge case: if lostpointercapture fires before any pointermove, lastMove holds the pointerdown event (set at pointerdown), so finalize(lastMove, true) is called with a zero-area box. The area threshold correctly rejects it, but an explicit test or comment on this path would make the intent clear. Proxy bridge: architecture is sound and origin validation is properly fail-closed. Two issues worth addressing:
Nothing blocking. The security model, cleanup lifecycle, and test coverage are solid. |
PR review follow-ups (non-blocking): warn + ignore a second lvt-fx:proxy-bridge (the module-level pageBridgeMetrics is shared, so a second would collide), and add an explicit test that a lostpointercapture before any pointermove yields a zero-area box the threshold rejects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks — addressed both non-blocking points: added a single-bridge guard (warn + ignore a second |
ReviewOverall this is solid work — the iOS touch-drag recovery is well-reasoned and the proxy bridge is carefully designed (fails closed on unresolvable origin, explicit dedup on nav events, singleton guard for shared metrics). Tests cover the important edge cases. A few findings: Bug (minor):
Observation: This means
No security concerns — origin validation is strict ( |
…rame PR review nits (non-blocking): clear pointerType alongside lastMove in the drag finalize/reset (symmetry; avoids a future unguarded read seeing stale state), and document that regionRectFromBox's box fractions are overlay-relative, not viewport-absolute. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Good, well-structured PR. A few things worth considering: Touch recovery ( finalize(pointerType === "touch" ? m : null, pointerType === "touch" && m !== null);The
Origin check in if (!expectedOrigin || e.origin !== expectedOrigin) return;Correct behavior, but
Tests: Comprehensive and cover the key edge cases (zero-area cancel, tiny cancel still dropped, dedup, foreign-origin rejection, morphdom-wipe recovery). The Overall this is solid — the security posture (origin validation, fail-closed on unresolved origin, singleton guard with a clear diagnostic warning) is good. The touch recovery rationale is well-documented. |
Final PR-review nits (all non-blocking): drop the redundant `m !== null` from onLostCapture (lastMove is non-null during a drag; finalize tolerates null), note that clearing pageBridgeMetrics on cleanup is intentional, and reject a non-positive box in the exported regionRectFromBox. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Folded in the final non-blocking nits: simplified the |
|
Review: feat: proxy-bridge directive + touch drag recovery Overall solid. Security posture is good and the logic holds up. Touch recovery (
Proxy bridge security
One subtle note — Minor
LGTM. |
* feat: --external mode — annotate live local sites through a proxy `prereview --external http://localhost:8080 --out ./review` reverse-proxies a running local site on a second origin, frames it, and overlays a region- annotation UI: drag a box on any page to leave a comment, anchored to URL + region (kind=region) and persisted to comments.csv. - Dual-port boot: UI + a separate-origin reverse proxy so the app's root-relative URLs / websockets forward with zero rewriting; strips X-Frame-Options/CSP and injects a tiny nav/scroll beacon into HTML. - Anchoring: URL + document-fraction rectangle (new CSV `url` column, 13th); frozen like image-area comments (no re-anchoring). - Live re-pin: saved annotations render over the iframe and follow scroll via a beacon-driven pin-layer transform (cross-origin-clean). - Collapsible annotations sidebar (collapsed by default); tap an annotation to locate it (navigate + scroll + highlight); click a pin to edit; lvt-ignore on the iframe so in-app navigation isn't reset on render. - `--out <dir>` generalized to all modes (store root; required for --external). - Docs: cli.md / README / skill docs updated; skill CSV schema fixed to 13 cols. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: re-vendor client bundle (proxy-bridge fail-closed origin fix) Picks up livetemplate/client#134's PR-review fix: the proxy-bridge directive now fails closed when the iframe origin can't be resolved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: re-vendor client bundle (proxy-bridge single-bridge guard) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: re-vendor client bundle (drag pointerType reset + comment) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: re-vendor client bundle (final proxy-bridge review polish) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Client-side support for prereview's
--externallive-site annotation mode (livetemplate/prereview#36). Adds a cross-origin proxy bridge directive and makes the shared drag spine touch-resilient.Changes
lvt-fx:proxy-bridge(new directive) — on the preview stage wrapping a cross-origin proxied iframe. It relays the page's beacon:nav→ dispatchessetProxyURLso the server swaps the visible annotation set;scroll→ sizes the[data-pin-layer]to the document and translates it by-scroll(live re-pin, no server hop). Re-applied on every render so morphdom can't wipe the imperative styles.region-selectdata-surface="page"—regionRectFromBoxconverts a drawn box (overlay-rect fractions) to document fractions using the beacon's scroll/doc metrics (a live cross-origin page has no source-line markers, so it's a rect, not a line range).attachBoxDragSelecttouch recovery — iOS cancels long/fast touch-drags (re-reads them as a swipe); forpointerType==='touch'we now finalize the region onpointercancel/lostpointercaptureinstead of discarding it (mouse/pen unchanged; the area threshold still rejects stray tiny boxes).Tests
New jest: proxy-bridge (nav dispatch, dedup, pin-layer transform + morphdom-wipe recovery, origin reject, focus relay on token change),
regionRectFromBoxconversion + clamping, and touch cancel/lost-capture recovery + tiny-cancel still dropped. Full suite green (732 passing).🤖 Generated with Claude Code