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:
@@ -0,0 +1,51 @@
|
||||
import type { MiddlewareHandler } from 'hono';
|
||||
import * as errors from '../custom-errors';
|
||||
import { resolveAuthToken } from '../auth-token';
|
||||
import { isSuperAdmin } from '../super-admin';
|
||||
import { isApiRequestAllowed } from '../capabilities/authorize';
|
||||
import { isExemptApiPath } from '../capabilities/totality';
|
||||
|
||||
// The global authorization gate: a valid NON-owner token may reach only what its ROLE has been granted.
|
||||
//
|
||||
// Mounted in hono.ts ahead of every router, and it re-verifies the token itself rather than trusting an
|
||||
// earlier middleware — so it covers routes that never mount `userMiddleware`, which is most of the reason
|
||||
// it exists. See auth-token.ts: the caller resolved here must be the same caller `userMiddleware` would
|
||||
// resolve, or the two doors disagree about who someone is.
|
||||
//
|
||||
// A missing or invalid token passes straight through. Signin needs that, and `userMiddleware` rejects bad
|
||||
// tokens on the protected routes. Only a VALID non-owner token is constrained here.
|
||||
//
|
||||
// ── What this file used to be ──
|
||||
//
|
||||
// This was `originScopeMiddleware`, the second half of `origin-validation.ts`, which also enforced
|
||||
// per-origin path scoping: an app shipping a custom-scheme origin (`officer://<hex>` via
|
||||
// OFFICER_<APP>_ORIGIN) could reach only `/api/auth` plus its own feature.
|
||||
//
|
||||
// All of that is gone as of 2026-08-13, along with ALLOW_ANY_ORIGIN and ALLOW_ANY_ORIGIN_MUSIC. Origin
|
||||
// was never authentication here — the custom-scheme origin is chosen by the client, forgeable outside a
|
||||
// browser, and extractable from a shipped app binary — and the flag that disabled it defaulted to ON, so
|
||||
// on every real install none of it ran. It was documented as defence in depth that was switched off.
|
||||
//
|
||||
// This half was deliberately NOT under that flag, because it is account-based rather than origin-based.
|
||||
// Its old comment called it "the airtight half", and separating the two is why the name changed: nothing
|
||||
// in here looks at an Origin header any more.
|
||||
export const capabilityGateMiddleware: MiddlewareHandler = async function (ctx, next) {
|
||||
const path = ctx.req.path;
|
||||
|
||||
const authorization = ctx.req.header('authorization');
|
||||
const token = authorization ? authorization.split(' ')[1] : ctx.req.query('token') || undefined;
|
||||
const payload = token ? await resolveAuthToken(token) : null;
|
||||
const isOwner = payload ? await isSuperAdmin(payload) : false;
|
||||
|
||||
// Exempt paths (signin, the public pages, the Bitwarden door) are skipped because they are served above
|
||||
// the account gate — the same list the boot check uses, deliberately.
|
||||
//
|
||||
// Non-/api paths are not ours: the DAV sync door authenticates with its own app password and carries no
|
||||
// platform account, so there is nothing here to resolve.
|
||||
if (payload && !isOwner && path.startsWith('/api') && !isExemptApiPath(path)) {
|
||||
const { allowed, reason } = await isApiRequestAllowed(payload.id, ctx.req.method, path);
|
||||
if (!allowed) throw errors.FORBIDDEN(reason ?? 'Not permitted for this account');
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
Reference in New Issue
Block a user