remove origin validation
ALLOW_ANY_ORIGIN, ALLOW_ANY_ORIGIN_MUSIC, and everything they gated. The flag defaulted to ON, so none of it ran on a real install — what comes out is documented defence in depth that was already switched off. The file said so itself: "Both flags and their call sites come out once the tailnet is the perimeter." Origin was never authentication here in any case. An app's `officer://<hex>` origin is chosen by the client, forgeable outside a browser, and extractable from a shipped binary. Gone: the two flags, isOriginAllowed, isOriginCheckDisabled, isMusicOriginExempt, originValidationMiddleware, ORIGIN_RULES and the whole OFFICER_<APP>_ORIGIN scheme, PUBLIC_URL's origin/host derivation, and origin-validation.test.ts, which existed only to pin them. CORS now echoes whatever Origin it is given, which is what every install already did. What SURVIVES is the reason this needed care. origin-validation.ts held two unrelated things, and the second was the global authorization gate — a valid non-owner token reaches only what its role grants, deliberately NOT under the flag because it is account-based rather than origin-based. Its own comment called it "the airtight half". Deleting the file wholesale would have deleted authorization. So it moves to _middlewares/capability-gate.ts as capabilityGateMiddleware, with the name matching what it does: nothing in it reads an Origin header any more. hono.ts mounts it in the same position, ahead of every router. origin-middleware.ts stays and is untouched — it extracts the Origin for six auth handlers that log it, and for passkeys. Extraction, not validation. Also updates every claim that rested on the old model: CLAUDE.md's security section and repo map, docs/secret-store.md, docs/mobile-api-keys.md, and five messages in machine-setup's Tailscale section which told the owner to set ALLOW_ANY_ORIGIN=false when declining a tailnet. That advice is now impossible to follow, and the honest version is different: with no tailnet the token is the whole lock, so put a proxy in front and restrict who can reach it. Not typechecked (empty node_modules, frozen installs). Every changed file parses; the setup section was run and writes four variables now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+10
-14
@@ -57,8 +57,7 @@ import { agentStatusRouter } from './api/agent-status/router';
|
||||
import { chatRouter } from './api/chat/chat';
|
||||
import { pipelineJobsRouter } from './api/tasks/pipeline-jobs-routes';
|
||||
import { CustomError } from './custom-errors';
|
||||
import { userMiddleware, bodyParser, isOriginAllowed, originScopeMiddleware } from './_middlewares';
|
||||
import { isMusicOriginExempt } from './_middlewares/origin-validation';
|
||||
import { userMiddleware, bodyParser, capabilityGateMiddleware } from './_middlewares';
|
||||
|
||||
export { Hono };
|
||||
export { createRouter };
|
||||
@@ -66,14 +65,12 @@ export type { HonoVariables };
|
||||
|
||||
export const honoServer = new Hono<{ Variables: HonoVariables }>();
|
||||
|
||||
// Origin checking was removed on 2026-08-13, so CORS echoes back whatever Origin it is given. That is
|
||||
// not a loosening: the check it replaced defaulted to off, so this is what every real install already
|
||||
// did. The perimeter is the tailnet and the lock is a valid token on every protected route, plus the
|
||||
// capability gate below.
|
||||
const corsMiddleware = cors({
|
||||
origin: (origin, c) => {
|
||||
const host = c.req.header('host');
|
||||
// TEMPORARY: see isMusicOriginExempt — echoes any Origin back for /api/music when enabled, so a
|
||||
// browser client is not blocked by CORS after userMiddleware has already let it through.
|
||||
if (isMusicOriginExempt(c.req.path)) return origin ?? '*';
|
||||
return isOriginAllowed(origin, host) ? origin : '';
|
||||
},
|
||||
origin: (origin) => origin ?? '*',
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
||||
allowHeaders: ['Content-Type', 'Authorization'],
|
||||
});
|
||||
@@ -92,17 +89,16 @@ const isDavPath = (path: string) =>
|
||||
|
||||
honoServer.use((ctx, next) => (isDavPath(ctx.req.path) ? next() : corsMiddleware(ctx, next)));
|
||||
|
||||
// Scoped-origin gate: restrict app origins (e.g. the music app) to their allowed path prefixes
|
||||
// (/api/auth + /api/music). No-ops for the main web origin and while OFFICER_MUSIC_ORIGIN is unset.
|
||||
honoServer.use(originScopeMiddleware);
|
||||
// The authorization gate: a valid non-owner token reaches only what its role grants. Ahead of every
|
||||
// router, and it re-verifies the token itself so it covers routes that never mount userMiddleware.
|
||||
honoServer.use(capabilityGateMiddleware);
|
||||
|
||||
honoServer.get('/api', (ctx) => ctx.json({ officerAPI: 'ok' }));
|
||||
honoServer.route('/api/auth', authRouter);
|
||||
honoServer.route('/api/landing-page-data', landingPageDataRouter);
|
||||
honoServer.route('/api/waitlist', waitlistRouter);
|
||||
// Vaultwarden reverse-proxy — mounted TOP-LEVEL (not under protectedRouter): the Bitwarden client
|
||||
// carries its own bearer token, not a platform session JWT, so userMiddleware would 401 it. Origin
|
||||
// gating still applies via originScopeMiddleware above (OFFICER_VAULT_ORIGIN → /api/vault). The
|
||||
// carries its own bearer token, not a platform session JWT, so userMiddleware would 401 it. The
|
||||
// notifications WebSocket is upgraded at the serve level (server.tsx).
|
||||
honoServer.route('/api/vault', vaultRouter);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user