typescript-auth-env-driven-keys
Root cause: two consumers of the API key disagreed. The requireApiKey middleware read the env-driven app.locals.config.auth.apiKeys (populated from API_KEYS), but the POST /login route hardcoded ['demo-key-123']. In a deployment with API_KEYS=prod-key, middleware accepted prod-key while login rejected it → the app's own key 401'd at login → lockout.
Fix: make login read the same single source of truth, app.locals.config.auth.apiKeys, which already falls back to the demo key when the env var is unset/empty.
src/config.ts — env-driven config with demo-key fallback:
export const DEMO_API_KEY = 'demo-key-123';
export function buildConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
const raw = (env.API_KEYS ?? '').trim();
const apiKeys = raw
? raw.split(',').map((k) => k.trim()).filter(Boolean)
: [DEMO_API_KEY]; // fallback when env unset/empty
return { auth: { apiKeys } };
}
src/app.ts — the fix (before → after):
// BEFORE (bug): hardcoded demo keys ignored app.locals.config
const DEMO_KEYS = ['demo-key-123'];
if (DEMO_KEYS.includes(apiKey)) { ... }
// AFTER (fix): same source of truth as the middleware
const apiKeys = (): string[] => app.locals.config.auth.apiKeys;
app.post('/login', (req, res) => {
const { apiKey } = req.body ?? {};
if (typeof apiKey === 'string' && apiKeys().includes(apiKey)) {
return res.json({ token: 'jwt.' + Buffer.from(apiKey).toString('base64') });
}
return res.status(401).json({ error: 'invalid_credentials' });
});
app.locals.config = config ?? buildConfig(process.env) wires the deployment env; the middleware is unchanged.
Verified in `~/typescript-auth-env-driven-keys` with strict `tsc --noEmit` (clean) and 5 passing vitest tests.
**Red/green proof of the actual bug:**
```
BUGGY login with API_KEYS=prod-key-1 -> HTTP 401 (invalid_credentials) [LOCKOUT]
FIXED login with API_KEYS=prod-key-1 -> HTTP 200 (token issued)
```
**All three required states tested** (`test/login.test.ts`):
1. **Env key ok** — `API_KEYS=prod-key-1,prod-key-2` → login with `prod-key-2` → `200`, token issued.
2. **Demo rejected when env set** — `API_KEYS=prod-key-1` → login with `demo-key-123` → `401 invalid_credentials`.
3. **Fallback when empty** — `API_KEYS=''` → login with `demo-key-123` → `200`, token issued.
**Edge cases also covered:**
- **No lockout on protected routes:** same env key that logs in also passes `x-api-key` middleware (`GET /me` → `200`) — the scenario that was broken.
- **Real wiring, not injected config:** `createApp()` with `process.env.API_KEYS` stubbed to `env-set-key` → env key `200`, demo key `401` (proves `buildConfig(process.env)` path).
- Whitespace/empty segments in comma-separated keys are trimmed/filtered.
Test run:
```
✓ test/login.test.ts (5 tests)
Test Files 1 passed (1)
Tests 5 passed (5)
```{"model": "deepseek-v4-flash", "problem_class": "typescript-auth-env-driven-keys", "result": "passed", "tests": 5}