Skip to content

fix(config): refuse to start with the development auth secret, and add CI - #7

Open
VampiricCyborg wants to merge 3 commits into
mainfrom
fix/auth-secret-guard-ci
Open

VampiricCyborg wants to merge 3 commits into
mainfrom
fix/auth-secret-guard-ci

Conversation

@VampiricCyborg

Copy link
Copy Markdown
Owner

Two unrelated fixes that pair usefully: CI's very first run is against the commit that makes CI need AUTH_SECRET, so the first PR this workflow gates is also the one that validates it end to end.

1. The auth secret guard

auth_secret is the only input to the session cookie's HMAC, and it fell back to "dev-only-change-me" — a constant committed to this public repository. Any deployment that never set AUTH_SECRET was signing sessions with a key anyone could read, which is enough to mint a cookie for any user id and read that user's documents. AUTH_SECRET was also missing from .env.example, so nothing ever prompted anyone to set it.

A @model_validator(mode="after") on Settings now rejects an empty or still-default secret whenever debug is off. It fires at Settings() construction — which app/main.py performs at import — so a misconfigured process dies before create_app() wires a single route. Debug builds stay exempt, so local development needs no setup.

Verified both paths:

DEBUG=false AUTH_SECRET=ci-only-secret  ->  app imported OK
DEBUG=false (no AUTH_SECRET)            ->  ValidationError: AUTH_SECRET is unset or still
                                            the development default...

Five new cases in tests/test_security.py cover the default, empty and whitespace secrets, a real secret, and debug builds staying exempt.

Important

Railway production does not currently have AUTH_SECRET set (confirmed against the Railway API, not railway.json), and it deploys from main. Set the variable in Railway before merging this PR. Merging first means the guard does exactly what it is built to do and takes the backend down on deploy.

Whatever value is set invalidates every session cookie currently outstanding, so everyone signed in is logged out once. That is the intended outcome given the old key was public — but it is worth a heads-up rather than a surprise.

2. CI

Two jobs, neither needing a live service: pytest -q for the backend, and tsc --noEmit / npm run lint / npm run build for the frontend. The backend suite mocks the database and the embedding service, and the tests use ASGITransport, so the app's lifespan never loads the ONNX model.

The backend job sets a throwaway AUTH_SECRET because of the guard above.

npm run lint was already failing on react-hooks/set-state-in-effect in useDemoLoop.ts, so CI would have been red on its first run. useTypewriter now rewinds during render instead of in an effect, which also removes a frame where a changed line rendered at the previous line's length.

Checks run locally

  • pytest -q — 84 passed (was 79)
  • npx tsc --noEmit — clean
  • npx eslint . — 0 errors (1 warning in services/mock.ts, which is being deleted separately)
  • npm run build — 15 routes

🤖 Generated with Claude Code

VampiricCyborg and others added 3 commits September 20, 2026 13:28
auth_secret is the only input to the session cookie's HMAC, and it fell back
to "dev-only-change-me" -- a constant committed to a public repository. Any
deployment that never set AUTH_SECRET was signing sessions with a key anyone
could read, so anyone could mint a cookie for any user id.

Nothing forced the override, and AUTH_SECRET was absent from .env.example, so
there was not even a prompt to set it. A model validator on Settings now
rejects an empty or still-default secret whenever debug is off. It runs at
Settings() construction, which app.main performs at import, so a misconfigured
process dies before create_app() wires a single route rather than serving
forgeable sessions. Debug builds stay exempt so local development needs no
setup.

Also documents AUTH_SECRET in .env.example alongside the three other AUTH_*
variables, which were equally undocumented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
useTypewriter reset its character count inside the effect that starts the
interval, so a changed line rendered once at the previous line's length before
the effect snapped it back to zero. Moving the reset into render -- the
adjusting-state-on-a-changed-input pattern -- removes that frame, and seeding
the initial count from `active` removes the same flicker on mount.

This was also the one error failing `npm run lint`, which the next commit turns
into a required check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repository had no automated checks at all. Two jobs, neither needing a
live service: pytest for the backend, and typecheck, lint and build for the
frontend.

The backend job sets AUTH_SECRET because `from app.main import app` builds
Settings at import time and a non-debug Settings now refuses the development
default -- so this workflow's first run also exercises the guard added two
commits ago.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
docu-query Ready Ready Preview Sep 20, 2026 7:59am UTC

This branch was successfully deployed

1 active deployment
Preview — 4f3f889f Deployed Sep 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant