You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: harden security and API semantics (Agent Team findings)
Three AI agents (Codex GPT-5.4, Gemini CLI, Claude subagent) ran end-to-end
scenarios against the live deployment. This commit fixes all findings they
surfaced, ordered by severity.
── P0 security fixes ──
1. Magic Link IP rate limiting (Codex MEDIUM + Portal BUG#4)
Previously only per-email. An attacker could rotate across many victim
emails from one IP and burn the Resend quota. Added per-IP counter (10
per hour, cf-connecting-ip keyed), 1 KB request body cap, and unified
the rate-limit helpers into `incrementRateCounter()` / `readRateCounter()`.
src/handlers/auth.ts
2. Revocation propagation: LRU TTL 60s → 10s (Portal BUG#2)
A revoked key kept working for ~73s because the auth LRU cache was per
isolate with 60s TTL. 10s is a better worst-case guarantee; D1 read
amplification is bounded by TIER_QUOTAS traffic and the hot cache below.
Hard cross-isolate invalidation is Phase D territory.
src/middleware/auth-d1.ts
3. CORS: add DELETE to Allow-Methods (Portal BUG#1)
Portal worked same-origin, but any cross-origin client (CI dashboard,
browser extension) couldn't preflight DELETE /api/keys/:id.
src/config.ts
4. Portal CSP + HSTS (Portal BUG#5, BUG#6)
The landing page had a CSP but /portal/ — the page that literally shows
plaintext API keys in a modal — had none. Added PORTAL_CSP in
helpers/response.ts (allows 'unsafe-inline' for the single-file SPA JS,
Google Fonts for styles, same-origin for API calls). Added HSTS header
(1 year, includeSubDomains) to landing, portal, and loading pages.
src/helpers/response.ts, src/index.ts
5. Cache-Control: no-store, private on Portal API (Codex LOW)
/api/me, /api/keys, /api/usage, /api/auth/* now set Cache-Control so
session-authenticated data is never cached by browsers or intermediaries.
src/handlers/keys.ts, usage.ts, auth.ts
── P1 correctness / semantics ──
6. /api/health now public-only by default (Codex LOW)
Default response is { status, service, uptime_seconds } — no browser
queue depth, paywall rule counts, or metric counters. Full metrics
require /api/health?full=1 with API_TOKEN Bearer. Fallback behavior
on stats-provider errors is preserved for the full path.
src/handlers/health.ts (new handlePublicHealth/handleFullHealth/handleHealthRoute)
7. /api/stream now emits X-RateLimit-* headers (Codex INFO)
Previously rate limit headers only appeared on 429. Now the SSE success
response carries X-RateLimit-Limit/Remaining and X-Request-Cost so
clients can self-throttle without a separate /api/usage call.
src/handlers/stream.ts, src/index.ts
8. POST /api/keys prefix format consistency (Portal UX)
GET returned `mk_abc12345...` but POST returned the raw `abc12345`.
Clients that treat "prefix is prefix" broke on copy-paste. POST now
returns the same `mk_xxx...` format.
src/handlers/keys.ts
9. Too Many Keys: 400 → 409 (Portal UX)
The request is well-formed; the account state prevents creation.
409 Conflict is the correct status for a resource-state refusal.
src/handlers/keys.ts
10. DELETE is now idempotent (Portal BUG#4)
A second DELETE on an already-revoked key returned 400 "Already Revoked".
REST convention is that DELETE is idempotent. Now returns 200 with the
original revocation timestamp.
src/handlers/keys.ts
── Tests ──
- 2 new health endpoint tests (public vs full with auth gate)
- Updated portal-keys.test.ts for prefix format, 409 status, idempotent DELETE
- Updated index-health-fallback.test.ts to use ?full=1 + admin Bearer
- All 603 tests pass (up from 601 due to 2 new health tests)
── Not in this commit ──
- HTTP 000 / SSL resets (Portal BUG#3): unable to reproduce outside Portal
agent's network context, leaving for now
- prompt/alert/confirm in portal JS: UI polish, deferred
- __Host- session cookie prefix: breaks existing sessions, deferred
- Google Fonts SRI: Google Fonts doesn't publish subresource-integrity hashes;
CSP restricts to fonts.googleapis.com which is the best available mitigation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0 commit comments