Test-only auth endpoint — engineering runbook
Implements §1 of qa_proposal.md: a way for E2E tests to start authenticated in <50 ms instead of driving Kick OAuth / email-PIN in CI.
⚠️ Dangerous by design. This endpoint mints a real session cookie for a fixture account. It ships with three independent guards and must never be active in production.
Contract
POST /api/test/session
Headers:
X-Test-Secret: <TEST_SESSION_SECRET>
Body:
{ "as": "streamer" | "operator", "fixture": "e2e-operator-fresh" }
200 Set-Cookie: session=… (identical to a real PIN login)
{ "userId": <number>, "role": "streamer" | "operator" }
400 malformed body
401 bad / missing X-Test-Secret
403 endpoint disabled, misconfigured, or fixture-role mismatch
404 unknown fixture, or fixture not provisioned on this environmentThe minted cookie is produced by the same issueSession / sessionCookieHeader the real login uses, so verifySession accepts it unchanged.
The three guards (all three, not just one)
- Build-time exclusion. The handler lives in
api/src/test_session.ts. The production Docker build physically removesdist/test_session.js(Dockerfile:ARG INCLUDE_TEST_AUTH=false→rm).server.tsimports it lazily, so a stripped build still boots and the route just 404s. Env misconfiguration alone cannot expose it. - Runtime env gate.
server.tsonly wires the route whenTEST_AUTH_ENABLED === "true", and the handler refuses everything unless aTEST_SESSION_SECRETis configured. - Per-request gate. Constant-time
X-Test-Secretcheck, a fixed fixture whitelist (no arbitrary emails), and a role-match check (a fixture only mints the role it is declared for). Every accept/reject is logged at WARN with the caller IP for alerting:[test-auth] test session minted for fixture '…' (operator #N) from <ip>.
Fixtures
Whitelisted in test_session.ts (FIXTURES), mirroring §2 of the proposal. Each maps to a fixed non-routable email ‹fixture›@e2e.verifluence.test and a role. The endpoint resolves that email to a real row (streamers.email / operators.contact_email) and 404s if the account isn't provisioned.
Dependency: the §2 fixture accounts must be seeded on staging before any fixture returns 200. Until then the endpoint correctly returns
404 … not provisioned.
Deployment wiring (required to activate on staging)
The CI pipeline builds one API image that Flux promotes to both stage and prod, so the build-time exclusion needs a stage-specific image that opts the module back in. Two follow-up steps, in two repos:
1. CI (this repo, .github/workflows/deploy-api.yml) — ✅ done
A Build and push — webapi (stage, test-auth enabled) step builds the webapi target with --build-arg INCLUDE_TEST_AUTH=true on main and pushes ghcr.io/verifluence/api-stage:{tag,latest}. The default api:* images stay stripped → prod binary never contains the route.
2. Operations repo (Flux)
- Point the staging API Deployment at
ghcr.io/verifluence/api-stage(image-automation marker). - Set on staging only:
TEST_AUTH_ENABLED=trueTEST_SESSION_SECRET=<rotating secret>(Sealed Secret; share with CI/QA out of band)
- Production must NOT set
TEST_AUTH_ENABLEDorTEST_SESSION_SECRET, and continues to track the strippedghcr.io/verifluence/apiimage.
Alerting
Page on any [test-auth] … WARN line originating from a prod cluster, and on any … minted … line whose caller IP is outside the CI / QA egress range.
Local / dev usage
# build with the module retained, then run with the gates on
docker build --build-arg INCLUDE_TEST_AUTH=true -t vf-api-test ./api
TEST_AUTH_ENABLED=true TEST_SESSION_SECRET=dev-secret SESSION_SECRET=… node dist/server.js
curl -X POST http://localhost:3000/api/test/session \
-H 'X-Test-Secret: dev-secret' -H 'Content-Type: application/json' \
-d '{"as":"operator","fixture":"e2e-operator-fresh"}' -iTests
api/test/tc-test-session.test.ts (TC-TESTAUTH-01…06): disabled→403, bad/missing secret→401, unknown fixture→404, role mismatch→403, unprovisioned→404, and the happy path (200 + a Set-Cookie that verifySession accepts).