Lint / PHP (phpcs PSR-12) (push) Successful in 28s
Lint / JS (eslint) (push) Successful in 12s
Lint / PHP requirements (version + extensions) (push) Successful in 30s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 3m9s
Lint / Deploy (push) Successful in 2s
Four interrelated gaps in the same rate-limiting path: - #80: RATE_LIMIT_DEFAULT/RATE_LIMIT_API were defined in config.php but RateLimitMiddleware never read them (hardcoded class constants instead), and they weren't in .env.example — a deployer editing them saw zero effect with no documented way to actually change the limit. - #81: Bearer traffic was rate-limited purely by a shared IP bucket (the session-based half was a no-op for stateless clients, since a fresh session starts on every request). Two different API keys from the same host/NAT egress IP shared ONE bucket, so a chatty or misbehaving key could 429 a completely unrelated key's traffic. - #82: X-RateLimit-* headers reported the meaningless session counter for Bearer clients instead of whatever bucket actually governed them. - #83: RateLimitMiddleware::check() called session_start() unconditionally, before ApiKeyAuth even runs — continuous session-file churn and an unnecessary Set-Cookie on every stateless API request, using un-hardened cookie defaults since it runs before AuthMiddleware's hardening (which Bearer requests never reach anyway). Fixed as one pass since they're the same code path: config.php now reads RATE_LIMIT_DEFAULT/RATE_LIMIT_API from .env (added there too, documented); the middleware now extracts the raw Bearer token (independent of ApiKeyAuth, so no DB round-trip needed before rate limiting, and it works whether or not the token later turns out valid) and rate-limits it via its own per-token bucket instead of starting a session — the existing IP-based bucket still applies underneath as defense-in-depth against volumetric abuse from one network path, but each distinct key now gets real isolated headroom. getStatus()/addHeaders() report that per-token bucket for Bearer requests instead of the session counter. Verified: a Bearer request creates zero session files (confirmed via real session-directory file count before/after); two different keys from different IPs are fully isolated (one exhausting its own 120/min bucket has zero effect on the other); a config-driven RATE_LIMIT_API override (e.g. 5) is correctly honored for session-based (non-Bearer) traffic; X-RateLimit-* status correctly reflects the per-key bucket for a Bearer request. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lhz7pGMaoTfL5sdYS5XiKv