mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +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:
@@ -257,7 +257,7 @@ describe('a lost web-tab frame', () => {
|
||||
});
|
||||
|
||||
it('never for a path Codeman actually serves, and never for a plain navigation', async () => {
|
||||
expect((await app.inject({ method: 'GET', url: '/', headers: lostFrame })).statusCode).toBe(401);
|
||||
expect((await app.inject({ method: 'GET', url: '/webviewfoo/bar', headers: lostFrame })).statusCode).toBe(401);
|
||||
expect((await app.inject({ method: 'GET', url: '/api/sessions/abc', headers: lostFrame })).statusCode).toBe(401);
|
||||
expect((await app.inject({ method: 'GET', url: '/about' })).statusCode).toBe(401);
|
||||
expect(
|
||||
@@ -273,4 +273,49 @@ describe('a lost web-tab frame', () => {
|
||||
// A genuinely unauthenticated request afterwards is still a plain 401, not a 429.
|
||||
expect((await app.inject({ method: 'GET', url: '/static/app.js' })).statusCode).toBe(401);
|
||||
});
|
||||
|
||||
/**
|
||||
* The landing page. The shim maps `/webview/<cap>/` to exactly `/`, so a reload
|
||||
* there asks for Codeman's ROOT as an iframe navigation, and `/` is a registered
|
||||
* route (the app shell), which the route-table fence cannot tell from a real
|
||||
* navigation. It used to answer 401 inside the frame (or the shell itself on a
|
||||
* passwordless install), with no recovery message and the failed-frame panel
|
||||
* cleared because the document loaded fine. Credentials are the separator:
|
||||
* nothing in Codeman frames its own root, and the sandboxed frame carries none.
|
||||
*/
|
||||
describe('a reload on the dashboard landing page', () => {
|
||||
it('gets the recovery page when the iframe navigation of / carries no credentials', async () => {
|
||||
for (const url of ['/', '/?tab=2']) {
|
||||
const res = await app.inject({ method: 'GET', url, headers: lostFrame });
|
||||
expect(res.statusCode, url).toBe(200);
|
||||
expect(res.body, url).toContain('codeman:webview-lost');
|
||||
expect(res.headers['content-security-policy']).toContain("default-src 'none'");
|
||||
}
|
||||
});
|
||||
|
||||
it('is the shell, or the usual 401, once a session cookie or Authorization header is present', async () => {
|
||||
const ok = `Basic ${Buffer.from(`admin:${PASSWORD}`).toString('base64')}`;
|
||||
const shell = await app.inject({ method: 'GET', url: '/', headers: { ...lostFrame, authorization: ok } });
|
||||
expect(shell.statusCode).toBe(200);
|
||||
expect(shell.body).toBe('app shell');
|
||||
const wrong = `Basic ${Buffer.from('admin:nope').toString('base64')}`;
|
||||
expect(
|
||||
(await app.inject({ method: 'GET', url: '/', headers: { ...lostFrame, authorization: wrong } })).statusCode
|
||||
).toBe(401);
|
||||
const cookie = 'codeman_session=stale; other=1';
|
||||
expect((await app.inject({ method: 'GET', url: '/', headers: { ...lostFrame, cookie } })).statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('is still a 401 for a top-level navigation of /, and for a frame asking for JSON', async () => {
|
||||
expect((await app.inject({ method: 'GET', url: '/' })).statusCode).toBe(401);
|
||||
expect(
|
||||
(await app.inject({ method: 'GET', url: '/', headers: { ...lostFrame, 'sec-fetch-dest': 'document' } }))
|
||||
.statusCode
|
||||
).toBe(401);
|
||||
expect(
|
||||
(await app.inject({ method: 'GET', url: '/', headers: { ...lostFrame, accept: 'application/json' } }))
|
||||
.statusCode
|
||||
).toBe(401);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user