Context
Astro middleware deliberately skips auth for /_actions/ and /api/ (SELF_AUTH_PREFIXES in src/middleware.ts:147), so every action handler must check context.locals.user itself. Most do — but two whole action files don't, and one API route doesn't.
Audited against commit 3531916.
What's exposed
src/actions/providers.ts — all 7 actions, no auth check in any handler (handlers receive only input, never read context.locals.user):
connect (sets provider API keys via client.auth.set)
disconnect (src/actions/providers.ts:105 — removes provider credentials)
startOauth / finishOauth (drive provider OAuth flows)
list, getAvailableModelsForUser (cached, leak provider/model config)
src/actions/update.ts — all 3 actions, no auth check:
checkForUpdate (src/actions/update.ts:232)
pull (src/actions/update.ts:308 — docker-pulls a new image)
restart (src/actions/update.ts:344 — restarts the app container)
src/pages/api/system/health.ts — GET handler has no requireAuth call (every other API route has one). Leaks queue/project counts and infrastructure health. Minor, but fix in the same pass.
Impact: anyone who can reach the port (e.g. anyone on the same tailnet, or the internet if exposed) can disconnect your AI providers, hijack OAuth flows, or trigger an app update/restart without logging in.
What to do
- Add an auth guard to every handler in
providers.ts and update.ts. Follow the existing repo patterns — either the ensureAuthenticated(context) helper pattern from src/actions/mcps.ts:10-22, or the inline context.locals.user check from src/actions/chat.ts:53-56. Consider extracting one shared requireUser(context) helper into src/server/auth/ and reusing it, since TODO.md's multi-user plans will need exactly that.
- Add
requireAuth(cookies) (see src/server/auth/requireAuth.ts, used by e.g. src/pages/api/settings/base-url.ts) to src/pages/api/system/health.ts.
- Add a regression test that enumerates all action files / API routes and asserts each handler checks auth (or at minimum, unit tests hitting the fixed handlers unauthenticated and expecting
UNAUTHORIZED).
Out of scope
src/actions/auth.ts and src/actions/setup.ts are intentionally public (login/first-run setup) — do not touch.
- The OpenCode permissive permission config and docker.sock mount are by design.
- Multi-user roles/permissions (separate TODO.md effort).
Acceptance criteria
- Calling any
providers.* or update.* action without a session cookie returns UNAUTHORIZED, and GET /api/system/health returns 401.
- All flows still work when logged in (connect/disconnect a provider, check for updates from settings UI).
pnpm format passes.
Context
Astro middleware deliberately skips auth for
/_actions/and/api/(SELF_AUTH_PREFIXESinsrc/middleware.ts:147), so every action handler must checkcontext.locals.useritself. Most do — but two whole action files don't, and one API route doesn't.Audited against commit
3531916.What's exposed
src/actions/providers.ts— all 7 actions, no auth check in any handler (handlers receive onlyinput, never readcontext.locals.user):connect(sets provider API keys viaclient.auth.set)disconnect(src/actions/providers.ts:105— removes provider credentials)startOauth/finishOauth(drive provider OAuth flows)list,getAvailableModelsForUser(cached, leak provider/model config)src/actions/update.ts— all 3 actions, no auth check:checkForUpdate(src/actions/update.ts:232)pull(src/actions/update.ts:308— docker-pulls a new image)restart(src/actions/update.ts:344— restarts the app container)src/pages/api/system/health.ts—GEThandler has norequireAuthcall (every other API route has one). Leaks queue/project counts and infrastructure health. Minor, but fix in the same pass.Impact: anyone who can reach the port (e.g. anyone on the same tailnet, or the internet if exposed) can disconnect your AI providers, hijack OAuth flows, or trigger an app update/restart without logging in.
What to do
providers.tsandupdate.ts. Follow the existing repo patterns — either theensureAuthenticated(context)helper pattern fromsrc/actions/mcps.ts:10-22, or the inlinecontext.locals.usercheck fromsrc/actions/chat.ts:53-56. Consider extracting one sharedrequireUser(context)helper intosrc/server/auth/and reusing it, since TODO.md's multi-user plans will need exactly that.requireAuth(cookies)(seesrc/server/auth/requireAuth.ts, used by e.g.src/pages/api/settings/base-url.ts) tosrc/pages/api/system/health.ts.UNAUTHORIZED).Out of scope
src/actions/auth.tsandsrc/actions/setup.tsare intentionally public (login/first-run setup) — do not touch.Acceptance criteria
providers.*orupdate.*action without a session cookie returnsUNAUTHORIZED, andGET /api/system/healthreturns 401.pnpm formatpasses.