Sifa professional network API (Fastify, AT Protocol, Jetstream) sifa.id/

fix(oauth): crash on startup when OAuth keys are missing (#92)

* fix(oauth): crash on startup when OAuth keys are missing

Instead of silently setting oauthClient to null and serving 503 on all
auth endpoints, throw a clear error during server initialization when
JWKS/private key files are missing or Valkey is unavailable. This
prevents deploying an API that looks healthy but cannot authenticate
users.

Test mode (NODE_ENV=test) still skips OAuth initialization.

Closes #92

* ci(deploy): add post-deploy health gate with OAuth verification

After deploying, poll /api/health/ready for up to 30s and verify the
OAuth endpoint returns a non-503 status. Fails the workflow if the API
doesn't become healthy or OAuth keys are missing.

* style: fix prettier formatting in metadata.ts

authored by

Guido X Jansen and committed by
GitHub
a871aa19 002ff4ca

+46 -7
+29 -2
.github/workflows/deploy.yml
··· 35 35 key: ${{ secrets.VPS_SSH_KEY }} 36 36 script: | 37 37 cd /opt/sifa 38 - docker compose pull 39 - docker compose up -d --force-recreate --remove-orphans 38 + docker compose pull sifa-api 39 + docker compose up -d --force-recreate --no-deps sifa-api 40 + docker image prune -af 41 + 42 + - name: Verify deployment health 43 + uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1 44 + with: 45 + host: ${{ secrets.VPS_HOST }} 46 + username: ${{ secrets.VPS_USER }} 47 + key: ${{ secrets.VPS_SSH_KEY }} 48 + script: | 49 + for i in $(seq 1 10); do 50 + status=$(curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:3100/api/health/ready 2>/dev/null || echo "000") 51 + if [ "$status" = "200" ]; then 52 + echo "API healthy after $((i * 3))s" 53 + # Verify OAuth is functional (should return 400 not 503) 54 + oauth_status=$(curl -sf -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:3100/oauth/login -H 'Content-Type: application/json' -d '{"handle":"test.invalid"}' 2>/dev/null || echo "000") 55 + if [ "$oauth_status" = "503" ]; then 56 + echo "FATAL: OAuth not configured — keys may be missing" 57 + exit 1 58 + fi 59 + echo "OAuth endpoint responding (status: $oauth_status)" 60 + exit 0 61 + fi 62 + echo "Waiting for API... (attempt $i, status: $status)" 63 + sleep 3 64 + done 65 + echo "FATAL: API did not become healthy within 30s" 66 + exit 1
+5 -3
src/oauth/metadata.ts
··· 3 3 import type { Env } from '../config.js'; 4 4 5 5 export function registerOAuthMetadata(app: FastifyInstance, config: Env) { 6 - if (!existsSync(config.OAUTH_JWKS_PATH)) { 7 - app.log.warn( 8 - `JWKS file not found at ${config.OAUTH_JWKS_PATH}, skipping OAuth metadata registration`, 6 + if (config.NODE_ENV !== 'test' && !existsSync(config.OAUTH_JWKS_PATH)) { 7 + throw new Error( 8 + `JWKS file not found at ${config.OAUTH_JWKS_PATH} — OAuth metadata cannot be registered`, 9 9 ); 10 + } 11 + if (!existsSync(config.OAUTH_JWKS_PATH)) { 10 12 return; 11 13 } 12 14
+12 -2
src/server.ts
··· 95 95 96 96 app.get('/api/health', async () => ({ status: 'ok' })); 97 97 98 - // Create OAuth client conditionally (skip in test mode or when JWKS file doesn't exist) 98 + // Create OAuth client (required in non-test mode) 99 99 let oauthClient = null; 100 - if (config.NODE_ENV !== 'test' && valkey && existsSync(config.OAUTH_JWKS_PATH)) { 100 + if (config.NODE_ENV !== 'test') { 101 + if (!valkey) { 102 + throw new Error('Valkey connection required for OAuth — check VALKEY_URL'); 103 + } 104 + if (!existsSync(config.OAUTH_JWKS_PATH)) { 105 + const privateKeyPath = config.OAUTH_JWKS_PATH.replace('jwks', 'private-key'); 106 + throw new Error( 107 + `OAuth keys missing — expected ${config.OAUTH_JWKS_PATH} and ${privateKeyPath}. ` + 108 + 'Generate with: node -e "..." (see sifa-deploy README)', 109 + ); 110 + } 101 111 oauthClient = await createOAuthClient(config, db, valkey); 102 112 } 103 113