Server-side authorization-code flow: /v1/auth/google redirects to the
consent screen with a CSRF state cookie; /v1/auth/google/callback
exchanges the code, validates the ID token (iss/aud/exp/email_verified),
and mints a 30-day session via upsertOAuthLogin. /v1/auth/providers lets
the login UI hide the button until GOOGLE_OAUTH_ID/SECRET are set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full reasoning-based audit of all 10 zones. 11 findings, all confirmed real,
zero false positives. 5 fixed now, 6 deferred to a justified backlog.
API-SERVERS-001 (HIGH) — DELETE /v1/servers/:id orphaned the container
The route deleted the DB row but never stopped the Docker container — it
kept running forever on its host port, still serving traffic with the
user's secrets baked into its env. The takedown path got stopContainer in
an earlier commit; this sibling path was missed. DELETE now tears the
container down first. Verified: deleted 'gfgfg' — container 23e0c55c gone,
:4110 connection-refused after.
INFRA-001 (HIGH) — SECRETS_ENCRYPTION_KEY zero-default usable in production
The AES-256-GCM key defaults to 64 zeros and passes the min(64) check. A
prod deploy that forgot to set it booted silently with every secret
encrypted under a public key. config.ts now throws on boot when
NODE_ENV=production and the key is still the placeholder. Verified: prod
boot with the zero key is REFUSED.
API-SERVERS-002 (MEDIUM) — WS build stream had no authorization
GET /v1/builds/:id/stream streamed build logs with no auth, while its REST
twin checks orgId. Now authenticates from the session cookie and rejects
builds outside the caller's org. Verified: no cookie -> 'unauthorized';
cross-org build -> 'not_found'; own build -> streams (no regression).
OAUTH-001 (MEDIUM) — authorization code consumption was not atomic
The 'already used?' check and the 'mark used' write were separate
statements — two requests racing the same code could both mint tokens.
Now a conditional UPDATE ... WHERE consumed_at IS NULL RETURNING; the
loser of the race gets zero rows and invalid_grant.
OAUTH-002 (MEDIUM) — 'plain' PKCE accepted, contradicting AS metadata
The AS metadata advertises code_challenge_methods_supported: ['S256'] but
/oauth/authorize accepted 'plain'. Authorize is now z.literal('S256') and
pkceVerify dropped the plain branch. Verified: authorize with plain -> 400.
Deferred to backlog (documented in TEMPLATE_SECURITY_AUDIT.md is template-only;
this audit's findings are in the commit + certification):
GENERATOR-001 — secrets via docker -e (visible in docker inspect); needs
--env-file rework
RUNNER-001 — generated containers run as root; needs USER node + build
re-test
AUTH-001 — no rate limit on magic-link / oauth register; needs
@fastify/rate-limit
GENERATOR-002— allocatePort check/bind race; low, self-heals on rebuild
AUTH-002 — expired magic_links/sessions/oauth rows never purged; needs
a cron
FEATURES-001 — tool-call metering not wired (metrics always 0); Sprint 4
by plan
- Bump @modelcontextprotocol/sdk from 1.0.4 to 1.29.0 in runner-template
(1.0.4 has no McpServer or StreamableHTTPServerTransport — file not found at runtime).
- Bump zod to 3.25.76 across workspace to satisfy modern SDK peer dep.
- Split OAUTH_ISSUER (canonical, host-reachable) from CONTROL_PLANE_URL (container-reachable for JWKS).
Runner verifies iss against OAUTH_ISSUER; fetches JWKS from CONTROL_PLANE_URL.
Both API and runner now agree on http://localhost:4000/oauth as the issuer in dev.
- Move postgres host port 5432 to 5440, redis 6379 to 6390 to avoid collisions with
native installs on the dev machine.
- Move web from 3000 to 3001 (3000 occupied by Gitea on dev machine).
- Drop pino-pretty transport from API to avoid runtime require of an unbundled dep.
- Cast build_logs.level (varchar) to BuildEvent's literal union in WS replay path.
- Remove unused reqBase helper in oauth.ts.