refactor(auth): /api/* boundary + drop sessionGateEnabled flag (#537) #539

Closed
claude-desktop wants to merge 1 commit from refactor/api-prefix-and-session-gate into main
Collaborator

Closes #537.

Why

The session gate had three layers fighting:

  1. sessionGateEnabled defaulting to publicBaseUrl !== null
  2. an auth.session_gate_enabled override
  3. a hardcoded exempt list of every static path that ever needs to load without a cookie

On a fresh deployment with no public_base_url and no override, the gate auto-disabled, the SPA mounted, every /api-style call returned blank or errored silently, and the user saw an empty dashboard with no path to log in. The hardcoded list was also brittle — /favicon.ico was missing and surfaced as a confusing 401 in DevTools.

What

Single auth boundary by path prefix:

  • /api/* → session-cookie required
  • everything else (SPA bundle, /login, /oauth/*, /webhook/*, /health, favicons) → open

No flags, no inference, no list.

Server

  • Mass-prefix every protected route in apps/server/src/main.ts with /api/ (~83 routes)
  • Public routes stay at root: /login, /logout, /oauth/*, /webhook/*, /webhooks/*, /health, /manifest.webmanifest, /icon*.svg, /favicon.ico, /, /dashboard, /app, /app/*
  • session-middleware.ts:
    • isSessionGateExempt(path) reduces to !path.startsWith("/api/") plus a CLAUDE_HOOKS_DISABLE_AUTH=1 test escape hatch
    • makeSessionGateMiddleware() no longer takes { enabled }. Always enforces within /api/*
  • webhook-config.ts: drop sessionGateEnabled field, schema entry, and inference. Deprecated auth.session_gate_enabled in agents.json warns and is otherwise ignored

Web

  • apiUrl(path) helper in lib/api.ts prefixes /api for paths that aren't already there (and aren't login/oauth/app static)
  • jsonFetch routes through apiUrl
  • All raw fetch("/...") and new EventSource(\/...`)callsites inlib/api.ts, lib/board.ts, lib/foreman.ts, components/app-shell.tsxrewritten to/api/...`
  • features/flows/flowsApi.ts: httpFetch wraps apiUrl(path)
  • lib/sse.ts: default URL /events/api/events

Tests

  • apps/server/package.json test + qa scripts set CLAUDE_HOOKS_DISABLE_AUTH=1 so existing suites that don't thread a session cookie keep running
  • session-middleware.test.ts + session-gate.test.ts clear the bypass locally and hit /api/* paths. Both green
  • webhook-config.test.ts: drop the sessionGateEnabled describe block (5 tests)

Known follow-up

~77 server tests still fail. Two buckets, both mechanical:

  1. Path updates — tests that still hit the old root paths (req("GET", "/queue", …) style across main-agents.test.ts, main.test.ts, auth.test.ts, flows-routes.test.ts, etc.). Surgical s//queue//api/queue/g style, but I avoided a mass regex pass because the first attempt over-matched and broke fixtures.
  2. Legacy Authelia auth testsauth.test.ts and the mutating routes — 403 when Remote-User is absent block test the old Remote-User / trust_proxy shape that the proxmox-iac side already removed. They exercise dead code now and should be deleted.

Both are bounded — should be one or two follow-up commits on this branch before merge.

Verification

  • bun run typecheck clean
  • bun test apps/server/src/http/session-middleware.test.ts 15/15 green
  • bun test apps/server/src/http/session-gate.test.ts 21/21 green
  • Service boots after config flip on production deployment (operator confirmed earlier in chat)
  • Full server test suite green — deferred to the cleanup commits described above
  • Web suite green — pre-existing localStorage.clear flake on main, unrelated

Test plan

  • Operator hits claude.jacquin.app/ with no cookie → 302 to /login/app/login renders, no blank-screen failure mode
  • Authenticated request to /api/board returns 200 with the cookie, 401 without
  • /webhook/forgejo still accepts forge POSTs without a session cookie
Closes #537. ## Why The session gate had three layers fighting: 1. `sessionGateEnabled` defaulting to `publicBaseUrl !== null` 2. an `auth.session_gate_enabled` override 3. a hardcoded exempt list of every static path that ever needs to load without a cookie On a fresh deployment with no `public_base_url` and no override, the gate auto-disabled, the SPA mounted, every `/api`-style call returned blank or errored silently, and the user saw an empty dashboard with no path to log in. The hardcoded list was also brittle — `/favicon.ico` was missing and surfaced as a confusing 401 in DevTools. ## What Single auth boundary by path prefix: - `/api/*` → session-cookie required - everything else (SPA bundle, `/login`, `/oauth/*`, `/webhook/*`, `/health`, favicons) → open No flags, no inference, no list. ### Server - Mass-prefix every protected route in `apps/server/src/main.ts` with `/api/` (~83 routes) - Public routes stay at root: `/login`, `/logout`, `/oauth/*`, `/webhook/*`, `/webhooks/*`, `/health`, `/manifest.webmanifest`, `/icon*.svg`, `/favicon.ico`, `/`, `/dashboard`, `/app`, `/app/*` - `session-middleware.ts`: - `isSessionGateExempt(path)` reduces to `!path.startsWith("/api/")` plus a `CLAUDE_HOOKS_DISABLE_AUTH=1` test escape hatch - `makeSessionGateMiddleware()` no longer takes `{ enabled }`. Always enforces within `/api/*` - `webhook-config.ts`: drop `sessionGateEnabled` field, schema entry, and inference. Deprecated `auth.session_gate_enabled` in `agents.json` warns and is otherwise ignored ### Web - `apiUrl(path)` helper in `lib/api.ts` prefixes `/api` for paths that aren't already there (and aren't login/oauth/app static) - `jsonFetch` routes through `apiUrl` - All raw `fetch("/...")` and `new EventSource(\`/...\`)` callsites in `lib/api.ts`, `lib/board.ts`, `lib/foreman.ts`, `components/app-shell.tsx` rewritten to `/api/...` - `features/flows/flowsApi.ts`: `httpFetch` wraps `apiUrl(path)` - `lib/sse.ts`: default URL `/events` → `/api/events` ### Tests - `apps/server/package.json` test + qa scripts set `CLAUDE_HOOKS_DISABLE_AUTH=1` so existing suites that don't thread a session cookie keep running - `session-middleware.test.ts` + `session-gate.test.ts` clear the bypass locally and hit `/api/*` paths. Both green - `webhook-config.test.ts`: drop the `sessionGateEnabled` describe block (5 tests) ## Known follow-up ~77 server tests still fail. Two buckets, both mechanical: 1. **Path updates** — tests that still hit the old root paths (`req("GET", "/queue", …)` style across `main-agents.test.ts`, `main.test.ts`, `auth.test.ts`, `flows-routes.test.ts`, etc.). Surgical s/`/queue`/`/api/queue`/g style, but I avoided a mass regex pass because the first attempt over-matched and broke fixtures. 2. **Legacy Authelia auth tests** — `auth.test.ts` and the `mutating routes — 403 when Remote-User is absent` block test the old `Remote-User` / `trust_proxy` shape that the proxmox-iac side already removed. They exercise dead code now and should be deleted. Both are bounded — should be one or two follow-up commits on this branch before merge. ## Verification - [x] `bun run typecheck` clean - [x] `bun test apps/server/src/http/session-middleware.test.ts` 15/15 green - [x] `bun test apps/server/src/http/session-gate.test.ts` 21/21 green - [x] Service boots after config flip on production deployment (operator confirmed earlier in chat) - [ ] Full server test suite green — deferred to the cleanup commits described above - [ ] Web suite green — pre-existing `localStorage.clear` flake on main, unrelated ## Test plan - [ ] Operator hits `claude.jacquin.app/` with no cookie → 302 to `/login` → `/app/login` renders, no blank-screen failure mode - [ ] Authenticated request to `/api/board` returns 200 with the cookie, 401 without - [ ] `/webhook/forgejo` still accepts forge POSTs without a session cookie
refactor(auth): /api/* boundary + drop sessionGateEnabled flag (#537)
Some checks failed
qa / qa (pull_request) Failing after 7m46s
qa / dockerfile (pull_request) Successful in 13s
d7cfb4a5aa
Closes #537. Single boundary on path prefix replaces the hardcoded
exempt list + the per-deployment activation flag.

## Server

- Mass-prefix every protected route in `apps/server/src/main.ts` with
  `/api/` (~83 routes). Public routes stay at root: `/login`, `/logout`,
  `/oauth/*`, `/webhook/*`, `/webhooks/*`, `/health`,
  `/manifest.webmanifest`, `/icon*.svg`, `/favicon.ico`, `/`,
  `/dashboard`, `/app`, `/app/*`.
- `session-middleware.ts`:
  - `isSessionGateExempt(path)` reduces to `!path.startsWith("/api/")`
    + a `CLAUDE_HOOKS_DISABLE_AUTH=1` test escape hatch.
  - `makeSessionGateMiddleware()` no longer takes `{ enabled }`.
    The gate is unconditional within `/api/*`.
- `webhook-config.ts`: drop the `sessionGateEnabled` field, the
  `auth.session_gate_enabled` schema entry, and the
  inference-from-`publicBaseUrl`. Deprecated key in `agents.json`
  prints a warning and is otherwise ignored.

## Web

- `apiUrl(path)` helper in `apps/web/src/lib/api.ts` prefixes `/api`
  for paths that aren't already there (and aren't login/oauth/app
  static).
- `jsonFetch` routes through `apiUrl`.
- All raw `fetch("/...")` and `new EventSource(\`/...\`)` callsites
  in `lib/api.ts`, `lib/board.ts`, `lib/foreman.ts`, and
  `components/app-shell.tsx` rewritten to `/api/...`.
- `features/flows/flowsApi.ts`: `httpFetch` wraps `apiUrl(path)`.
- `lib/sse.ts`: default URL `/events` → `/api/events`.

## Tests

- Server `package.json`: `test` + `qa` set
  `CLAUDE_HOOKS_DISABLE_AUTH=1` so existing test suites that don't
  thread a session cookie still run.
- `session-middleware.test.ts` + `session-gate.test.ts` clear the
  bypass locally and hit `/api/*` paths. Both green.
- `webhook-config.test.ts`: drop the `sessionGateEnabled` describe
  block (5 tests).
- `main-agents.test.ts` + `main.test.ts`: paths re-pointed to `/api/*`.

## Follow-up

~77 remaining test failures fall into two buckets, both mechanical:
(a) test files still posting to the old root paths, (b) the legacy
Authelia `Remote-User` / `trust_proxy` auth tests (`auth.test.ts`,
`mutating-auth.test.ts`) which the proxmox-iac side has already
removed and now exercise dead code. To be addressed in a follow-up
commit on this branch before merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
Collaborator

Closing — superseded by #538 for the gate fix (#537), which landed cleaner with all server tests green. The /api/* boundary refactor in this PR is orthogonal cleanup; reopening as a separate follow-up issue against today's main rather than carrying the 77 known test failures forward.

Branch refactor/api-prefix-and-session-gate retained for reference until the follow-up issue picks up the route-rename + SPA apiUrl helper work.

Closing — superseded by #538 for the gate fix (#537), which landed cleaner with all server tests green. The `/api/*` boundary refactor in this PR is orthogonal cleanup; reopening as a separate follow-up issue against today's main rather than carrying the 77 known test failures forward. Branch `refactor/api-prefix-and-session-gate` retained for reference until the follow-up issue picks up the route-rename + SPA `apiUrl` helper work.
claude-desktop closed this pull request 2026-04-28 19:18:19 +00:00
Some checks failed
qa / qa (pull_request) Failing after 7m46s
Required
Details
qa / dockerfile (pull_request) Successful in 13s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!539
No description provided.