"""Login rate limiting (TASK-03-03, D-041). slowapi Limiter with an in-memory backend (single-instance โ€” D-041). 5 login attempts per minute per client IP. On exceed โ†’ 429 + Retry-After. R-AUTH-03 (in-memory counter lost on restart) is an accepted pilot risk (RESEARCH-v0.4 ยง2.5) โ€” a restart at most resets the counter, which slightly widens the brute-force window but does not enable it (argon2id + 5/min is still the binding control). A hand-rolled counter is the documented fallback if slowapi is ever removed. """ from __future__ import annotations from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address, storage_uri="memory://") def reset_login_rate_limit() -> None: """Clear the in-memory rate-limit counters (test helper + restart-safe).""" try: limiter.reset() except Exception: pass def rate_limit_login(): """Decorator factory: 5 login attempts per minute per IP (D-041).""" return limiter.limit("5/minute") __all__ = ["limiter", "rate_limit_login", "reset_login_rate_limit"]