diff --git a/.changeset/fix-webview-route-masking.md b/.changeset/fix-webview-route-masking.md new file mode 100644 index 00000000..6f542d7a --- /dev/null +++ b/.changeset/fix-webview-route-masking.md @@ -0,0 +1,18 @@ +--- +"aicodeman": patch +--- + +fix(webview): let a proxied single-page app route on its own path, and recover a frame that reloads + +A dashboard served through a web tab saw `/webview//` as its `location.pathname`, and +no app has a route for that: a React Router, Vue Router or Vite dev-server page painted its +HTML and CSS and then replaced them with its own "page not found" the moment its script ran. +The proxy's runtime shim now rewrites the history entry to the path the page would see on its +own origin before any page script runs, while every URL the page emits still goes through +the existing rewrite layers (plus `Worker`, `sendBeacon` and `window.open`, which the masked +Referer can no longer rescue). A navigation the page starts itself afterwards — a dev +server's full-reload HMR, a root-absolute `location.href` — lands on Codeman's root with no +capability; it is recognised by shape (an iframe navigation asking for HTML for a path Codeman +does not serve), answered with a static page that tells the owning tab which path was lost, +and the tab remounts the frame inside the prefix at that path. That answer is served before +the credential checks, so it never counts as a failed login. diff --git a/docs/web-tabs.md b/docs/web-tabs.md index 753a607d..7f6be80e 100644 --- a/docs/web-tabs.md +++ b/docs/web-tabs.md @@ -147,6 +147,21 @@ layers cooperate so a dashboard talking to its own backend just works: using its `Referer` to identify the dashboard. This only fires for a request that already missed every Codeman route, and never for one that resolves to a real route, which is what keeps it from being an authentication bypass. +5. The same script **masks the proxy prefix off the page's own URL** before any + of the page's code runs (`history.replaceState` to the path the page would see + on its own origin). A single-page app routes on `location.pathname` at boot, + and `/webview//` is a path no app has a route for: without this, a React + Router / Vue Router / Next dev server painted its HTML and CSS and then replaced + them with its own "page not found" the moment its script ran. The page only + *reads* the masked path; every URL it emits still goes through the layers above. +6. A navigation the page starts **itself** after that — `location.reload()` (a dev + server's full-reload HMR), a root-absolute `location.href = '/login'` — now + targets Codeman's root with no capability anywhere on it. Codeman recognises + that request by shape (a top-level `