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:
2026-08-13 00:15:25 +00:00
co-authored by Claude Opus 5
parent c5adb4aa08
commit f063fc0c08
22 changed files with 117 additions and 370 deletions
+10 -10
View File
@@ -72,7 +72,7 @@ src/
│ └── landing/ # marketing landing page
├── servers/
│ ├── hono.ts # router composition; everything under /api
│ ├── _middlewares/ # auth, body parsing, origin validation, rate limiting
│ ├── _middlewares/ # auth, body parsing, the capability gate, rate limiting
│ ├── api/<feature>/ # one folder per feature, each exporting a router
│ ├── channels/ # send-claude-code / send-opencode — how /chat drives an agent turn
│ ├── queue/ # background job engine
@@ -142,14 +142,14 @@ exceed Postgres's 63-character identifier limit: name it explicitly. See `src/da
## Security Model
- `IS_DEV_BUILD` (`src/servers/build-env.ts`) is true **only** when `PUBLIC_BUILD_ENV` is explicitly
`dev`/`development`. Everything else, including unset, is hardened. Origin validation, rate
limiting and password rules all key off it — they fail closed.
- Allowed origins come from `PUBLIC_URL`. Officer always sits behind an HTTPS reverse proxy, so the
forwarded `Host` must equal `PUBLIC_URL`'s authority exactly.
- **`ALLOW_ANY_ORIGIN` defaults to ON** — origin checking is off unless the var is explicitly `false`.
A deliberate inversion of the usual rule, safe only because the perimeter is the tailnet and a valid
token is still required on every protected route. It is defence in depth that is currently switched
off, not the lock.
`dev`/`development`. Everything else, including unset, is hardened. Rate limiting and password
rules key off it — they fail closed.
- **There is no origin checking.** It was removed on 2026-08-13, along with `ALLOW_ANY_ORIGIN` and
`ALLOW_ANY_ORIGIN_MUSIC`. The flag defaulted to ON, so origin validation ran on no real install —
what came out was documented defence in depth that was already switched off. Origin was never
authentication here anyway: an app's `officer://<hex>` origin is chosen by the client, forgeable
outside a browser, and extractable from a shipped binary. The perimeter is the tailnet, and the lock
is a valid token on every protected route plus the capability gate below.
- JWTs are 30-day, blacklisted on signout, and invalidated by a password change (`passwordChangedAt`).
**The role is deliberately not a claim** — every authorization decision re-reads `users.role` from
Postgres, so a grant or a revoke takes effect on the next request rather than at next sign-in.
@@ -159,7 +159,7 @@ exceed Postgres's 63-character identifier limit: name it explicitly. See `src/da
### Capabilities — read this before mounting a router
Authorization is one system, and it is not in `userMiddleware` (which only answers "is this token
valid"). It is `originScopeMiddleware``capabilities/authorize.ts`, mounted globally in `hono.ts`
valid"). It is `_middlewares/capability-gate.ts``capabilities/authorize.ts`, mounted globally in `hono.ts`
ahead of everything, and it re-verifies the token itself so it covers routes that never mount
`userMiddleware`.