Commit 373e86f
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>1 parent b14ba64 commit 373e86f
File tree
13 files changed
+257
-48
lines changed- src
- __tests__
- handlers
- helpers
- middleware
13 files changed
+257
-48
lines changedSome generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
35 | 59 | | |
36 | | - | |
37 | 60 | | |
38 | 61 | | |
39 | | - | |
| 62 | + | |
40 | 63 | | |
41 | 64 | | |
42 | 65 | | |
43 | 66 | | |
44 | 67 | | |
45 | 68 | | |
46 | 69 | | |
47 | | - | |
48 | 70 | | |
49 | 71 | | |
50 | 72 | | |
| |||
53 | 75 | | |
54 | 76 | | |
55 | 77 | | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
56 | 88 | | |
57 | 89 | | |
58 | 90 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | | - | |
| 53 | + | |
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
62 | | - | |
63 | | - | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
64 | 66 | | |
65 | 67 | | |
66 | 68 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
218 | 218 | | |
219 | 219 | | |
220 | 220 | | |
221 | | - | |
| 221 | + | |
| 222 | + | |
222 | 223 | | |
223 | 224 | | |
224 | 225 | | |
| |||
274 | 275 | | |
275 | 276 | | |
276 | 277 | | |
277 | | - | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
278 | 281 | | |
279 | 282 | | |
280 | 283 | | |
| |||
387 | 390 | | |
388 | 391 | | |
389 | 392 | | |
390 | | - | |
| 393 | + | |
391 | 394 | | |
392 | 395 | | |
393 | 396 | | |
| 397 | + | |
394 | 398 | | |
395 | 399 | | |
396 | 400 | | |
397 | 401 | | |
398 | 402 | | |
399 | 403 | | |
400 | 404 | | |
401 | | - | |
| 405 | + | |
402 | 406 | | |
403 | 407 | | |
404 | 408 | | |
405 | 409 | | |
406 | 410 | | |
407 | 411 | | |
408 | 412 | | |
409 | | - | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
410 | 420 | | |
411 | 421 | | |
412 | 422 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
62 | | - | |
| 62 | + | |
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
| 37 | + | |
| 38 | + | |
37 | 39 | | |
38 | 40 | | |
39 | 41 | | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
40 | 81 | | |
41 | 82 | | |
42 | 83 | | |
| |||
48 | 89 | | |
49 | 90 | | |
50 | 91 | | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
51 | 98 | | |
52 | 99 | | |
53 | | - | |
| 100 | + | |
54 | 101 | | |
55 | 102 | | |
56 | 103 | | |
| |||
60 | 107 | | |
61 | 108 | | |
62 | 109 | | |
63 | | - | |
64 | | - | |
65 | | - | |
66 | | - | |
67 | | - | |
68 | | - | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | | - | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
75 | 125 | | |
76 | 126 | | |
77 | 127 | | |
| |||
118 | 168 | | |
119 | 169 | | |
120 | 170 | | |
121 | | - | |
122 | | - | |
123 | | - | |
124 | | - | |
125 | | - | |
126 | | - | |
127 | | - | |
128 | | - | |
129 | | - | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
130 | 179 | | |
131 | 180 | | |
132 | 181 | | |
| |||
256 | 305 | | |
257 | 306 | | |
258 | 307 | | |
| 308 | + | |
259 | 309 | | |
260 | 310 | | |
261 | 311 | | |
| |||
279 | 329 | | |
280 | 330 | | |
281 | 331 | | |
| 332 | + | |
282 | 333 | | |
283 | 334 | | |
284 | 335 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| 10 | + | |
9 | 11 | | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
10 | 17 | | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
11 | 62 | | |
12 | 63 | | |
13 | 64 | | |
| |||
0 commit comments