the dev proxy was mounted behind the auth middleware

"Unauthorized" in the pane, and that word is the whole diagnosis: this router's own
refusal says "Not authorised for the dev proxy. Open it from the Preview panel." The
text on screen came from `userMiddleware`, which means the request was rejected
before the router ever saw it.

I had mounted `/dev-proxy` in the PROTECTED table. Everything there gets
`userMiddleware`, which requires an Authorization header — and an iframe navigation
is a navigation, not a fetch, so it has none. The document 401'd every time. The
cookie work in the last commit was correct and could never have helped: nothing was
reaching the code that reads the cookie.

Moved to the unprotected mounts, beside `/auth` and `/sidecar`, with the reason
written where the list is. It is not open — the router verifies the OWNER's token on
`/session` and requires the cookie it issues on everything else.

── Why I did not catch this myself ──

My tests mounted `devProxyRouter` on a bare Hono app. That proves the router and
nothing else, and the bug was entirely in what sits in front of it. Retested through
`buildHonoApp()` — the real assembly, every middleware — where `/session` returns 200
with a cookie, the frame returns 200 carrying the dev server's own HTML and injected
script, and an uncredentialled request now gets THIS router's message rather than the
middleware's, which is the observable difference that proves the path.

A test that constructs its own subset of the system will keep passing while the
system is broken. Worth remembering the next time a router is proven in isolation.

tsgo clean, frontend builds, 889 pass / 7 fail unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 13:37:59 +00:00
co-authored by Claude Opus 5
parent fccabfcea3
commit d9bffe5471
+9 -2
View File
@@ -139,8 +139,6 @@ const PROTECTED_MOUNTS: [prefix: string, router: ReturnType<typeof createRouter>
// ['/gitea', giteaRouter], // plugin — switched off 2026-08-13
['/app-store', appStoreRouter],
['/plugins', pluginsRouter],
// Loopback dev servers, framed by the Preview panel. See its router for why it is not a proxy host.
['/dev-proxy', devProxyRouter],
// ['/caldav', caldavRouter], // the JSON door for Officer's own calendar/contacts UI — plugin, switched off 2026-08-13
// ['/dav', davRouter], // app-password management (the sync door is /dav, top-level) — plugin, switched off
// ['/notify', notifyRouter], // plugin — switched off 2026-08-13
@@ -178,6 +176,7 @@ export const UNPROTECTED_API_PREFIXES: string[] = [
// '/vault', // switched off with the Vaultwarden mounts — see above
'/sidecar',
'/agent-handoff',
'/dev-proxy',
];
/**
@@ -234,6 +233,14 @@ export function buildHonoApp(plugins: MountedPlugin[] = []): Hono<{ Variables: H
// peer on that panel's own dashboard. See servers/api/agent-handoff/router.ts.
app.route('/api/agent-handoff', agentHandoffRouter);
// Loopback dev servers, framed by the Preview panel.
//
// UNPROTECTED because an iframe navigation carries no Authorization header — `userMiddleware` would 401
// the document before the router saw it, which is exactly what happened: the pane showed the
// middleware's "Unauthorized", not this router's own message. It is not open: the router verifies the
// OWNER's token on `/session` and requires the cookie that issues on everything else.
app.route('/api/dev-proxy', devProxyRouter);
// CalDAV/CardDAV for phones and desktop clients — mounted TOP-LEVEL for the same reason the vault is:
// DAVx5, iOS and Thunderbird authenticate with HTTP Basic on every request and have nowhere to put a
// platform JWT, so userMiddleware would 401 them. The credential is a scoped DAV app password; see