mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 22:49:41 +02:00
fix(webview): recover a proxied dashboard that reloads on its landing page
The runtime shim masks `/webview/<cap>/` off a proxied page's URL so its router boots on the path it expects, and the landing page masks to exactly `/`. A `location.reload()` there (a Vite dev server on a config change or a failed HMR update, the likeliest case in the feature's own motivating scenario) therefore asks for Codeman's root as an iframe navigation. `serveLostWebviewFrame()` returned early for `/`, so on a passwordless install the frame received Codeman's own app shell and rendered it inside the web tab, and with a password it got a 401 in the frame. Either way no `codeman:webview-lost` message was posted, and because the document loaded fine the load handler cleared the failed-frame panel, so the Reload / Open in new tab affordances never appeared. Before masking the frame's URL was the prefixed one, so a reload worked; this was a regression. `/` is the one lost-frame path a registered route also serves, so the route table cannot tell that reload from a real navigation. Credentials can: nothing in Codeman frames its own root, and a sandboxed frame is opaque-origin with no cookie and no Authorization header. `carriesAuthCredentials()` (pure, in webview-proxy.ts) makes that test, and `/` is now admitted by the auth hook only when it fails; a framed `/` that does carry credentials still gets the shell. Without a password no auth hook runs at all, so the index route applies the same test itself (`isLostWebviewRootFrame`) before rendering the shell, and the three places that emitted the recovery page share `sendLostWebviewFramePage()`. Tests: the password form in webview-auth-exemption (recovery page for a credential-free framed `/`, shell with valid Basic auth, 401 with a stale cookie or a top-level navigation), the passwordless form against a real WebServer in webview-lost-root-frame (port 3198), and the credential predicate in webview-proxy. All three fail without the fix. Verified against a live isolated instance as well: a framed `GET /` with no credentials answers the 470-byte recovery page, a top-level `GET /` and a framed one carrying a cookie answer the shell. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -734,7 +734,9 @@ export const LOST_FRAME_PAGE_CSP = `default-src 'none'; script-src 'sha256-${LOS
|
||||
* no capability anywhere on it: no prefix in the path, no cookie in an
|
||||
* opaque-origin frame, and a Referer that names the masked page. Such a request
|
||||
* is recognisable by shape alone: a top-level navigation of an `<iframe>`
|
||||
* (`Sec-Fetch-Dest`), asking for HTML, for a path Codeman does not serve.
|
||||
* (`Sec-Fetch-Dest`), asking for HTML, for a path Codeman does not serve. The
|
||||
* one served path that still qualifies is `/` itself, which the callers admit
|
||||
* only when the request carries no credentials (see carriesAuthCredentials).
|
||||
*
|
||||
* The answer is `lostWebviewFramePage()`, a static page whose only content is a
|
||||
* `postMessage` to the parent naming the path; the Codeman tab that owns the
|
||||
@@ -754,6 +756,31 @@ export function isLostWebviewFrameNavigation(req: {
|
||||
return typeof accept === 'string' && accept.includes('text/html');
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a request carries something Codeman's auth would recognise: the
|
||||
* session cookie, or an `Authorization` header (Basic auth, which a browser
|
||||
* re-sends on every request to the realm once it has been accepted).
|
||||
*
|
||||
* `/` is the one lost-frame path a registered route also serves (the app shell),
|
||||
* so the route table cannot tell a landing-page reload of a proxied dashboard
|
||||
* (the runtime shim maps `/webview/<cap>/` to exactly `/`) from a genuine
|
||||
* navigation. Credentials can: nothing in Codeman frames its own root, and a
|
||||
* sandboxed web-tab frame is opaque-origin and carries neither, so an `<iframe>`
|
||||
* navigation of `/` with NEITHER credential can only be that frame. A framed
|
||||
* `/` that does carry credentials is left to the shell.
|
||||
*/
|
||||
export function carriesAuthCredentials(
|
||||
headers: Record<string, string | string[] | undefined>,
|
||||
sessionCookieName: string
|
||||
): boolean {
|
||||
const authorization = headers.authorization;
|
||||
if (Array.isArray(authorization) ? authorization.length > 0 : (authorization ?? '').trim() !== '') return true;
|
||||
const cookie = headers.cookie;
|
||||
const cookies = Array.isArray(cookie) ? cookie.join('; ') : cookie;
|
||||
if (typeof cookies !== 'string' || cookies === '') return false;
|
||||
return cookies.split(';').some((part) => part.trim().startsWith(`${sessionCookieName}=`));
|
||||
}
|
||||
|
||||
/** The static page that hands a lost frame back to its owning tab. */
|
||||
export function lostWebviewFramePage(): string {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user