mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 06:59:42 +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:
+17
-8
@@ -148,7 +148,13 @@ import { getLatestPlanUsage, setLatestCodexPlanUsage } from './plan-usage-latest
|
||||
import { telemetrySignature } from '../usage-telemetry.js';
|
||||
import { readCodexPlanUsage, resolveCodexBinaryPath } from '../utils/codex-cli-resolver.js';
|
||||
import type { ScheduledRun } from './ports/index.js';
|
||||
import { registerAuthMiddleware, registerSecurityHeaders, registerHostGuard } from './middleware/auth.js';
|
||||
import {
|
||||
registerAuthMiddleware,
|
||||
registerSecurityHeaders,
|
||||
registerHostGuard,
|
||||
isLostWebviewRootFrame,
|
||||
sendLostWebviewFramePage,
|
||||
} from './middleware/auth.js';
|
||||
import { isMultiUserMode } from '../config/multiuser.js';
|
||||
import { bootstrapInitialAdmin, hasUsers, resolveClaudeModeForUsername } from '../user-store.js';
|
||||
import { installRouteErrorHandler } from './route-error-handler.js';
|
||||
@@ -181,7 +187,7 @@ import {
|
||||
registerTabLayoutRoutes,
|
||||
tryWebviewRefererFallback,
|
||||
} from './routes/index.js';
|
||||
import { isLostWebviewFrameNavigation, lostWebviewFramePage, LOST_FRAME_PAGE_CSP } from './webview-proxy.js';
|
||||
import { isLostWebviewFrameNavigation } from './webview-proxy.js';
|
||||
import { CronService } from '../cron/cron-service.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
@@ -805,7 +811,14 @@ export class WebServer extends EventEmitter {
|
||||
|
||||
// Security headers + CORS
|
||||
registerSecurityHeaders(this.app, this.https, this.basePath);
|
||||
this.app.get('/', async (_req, reply) => {
|
||||
this.app.get('/', async (req, reply) => {
|
||||
// A web-tab frame that reloaded on its dashboard's landing page. The proxy's
|
||||
// runtime shim maps `/webview/<cap>/` to exactly `/`, so that reload asks for
|
||||
// Codeman's own root as an iframe navigation, and it used to get the app
|
||||
// shell rendered inside the web tab. Only the credential-free form is taken
|
||||
// (nothing in Codeman frames its root; the sandboxed frame has no cookie and
|
||||
// no Authorization); under a password the auth hook has answered it already.
|
||||
if (isLostWebviewRootFrame(req)) return sendLostWebviewFramePage(reply);
|
||||
return reply
|
||||
.header('Cache-Control', 'no-cache')
|
||||
.type('text/html; charset=utf-8')
|
||||
@@ -981,11 +994,7 @@ export class WebServer extends EventEmitter {
|
||||
// that navigated itself off its proxy prefix: the runtime shim masks the
|
||||
// prefix so the page's router sees its own path, and a reload of that page
|
||||
// lands here. The unauthenticated form is answered in the auth middleware.
|
||||
if (!req.url.startsWith('/api') && isLostWebviewFrameNavigation(req)) {
|
||||
reply.header('content-security-policy', LOST_FRAME_PAGE_CSP);
|
||||
reply.header('cache-control', 'no-store');
|
||||
return reply.type('text/html; charset=utf-8').send(lostWebviewFramePage());
|
||||
}
|
||||
if (!req.url.startsWith('/api') && isLostWebviewFrameNavigation(req)) return sendLostWebviewFramePage(reply);
|
||||
if (req.url.startsWith('/api')) {
|
||||
return reply.code(404).send(createErrorResponse(ApiErrorCode.NOT_FOUND, notFound));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user