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://` via // OFFICER__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(); };