"""Edge hardening: run throttling (slowapi), security headers (secure). Body caps come from Starlette's RequestBodyLimitMiddleware (wired in main.py). The run throttle is settings-driven so tests can shrink it. State is in-process memory: correct for the single-replica SQLite deployment; revisit with Redis if the API ever scales horizontally. """ from __future__ import annotations from slowapi import Limiter from slowapi.util import get_remote_address from starlette.datastructures import MutableHeaders from starlette.types import ASGIApp, Message, Receive, Scope, Send from app.settings import settings # POST .../run is the expensive endpoint (LFX execution, LLM cost): # per client IP and minute, limit read live from settings so tests can # shrink it without re-creating the app. limiter = Limiter( key_func=get_remote_address, storage_uri="memory://", default_limits=[], ) def run_limit() -> str: return f"{settings.run_throttle_per_minute}/minute" def reset_throttle() -> None: """Drop all counters (tests).""" try: limiter._storage.reset() # type: ignore[attr-defined] except AttributeError: pass class SecurityHeadersMiddleware: """Baseline response headers (TLS/HSTS live on the proxy). Built with the ``secure`` package; values stay byte-identical to the previous hand-rolled version (see test_middleware). """ def __init__(self, app: ASGIApp) -> None: import secure as secure_lib self.app = app self.secure_headers = secure_lib.Secure( xcto=secure_lib.XContentTypeOptions().nosniff(), xfo=secure_lib.XFrameOptions().sameorigin(), referrer=secure_lib.ReferrerPolicy().strict_origin_when_cross_origin(), csp=secure_lib.ContentSecurityPolicy().frame_ancestors("'self'"), ) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] != "http": await self.app(scope, receive, send) return async def secured(message: Message) -> None: if message["type"] == "http.response.start": headers = MutableHeaders(raw=message.setdefault("headers", [])) for key, value in self.secure_headers.headers.items(): headers[key] = value await send(message) await self.app(scope, receive, secured)