"""API 키 guard — [WebServerConfig].api_keys 가 설정된 경우에만 /v1 라우터 전체를 보호한다. 개발(local/dev)은 키를 비워 **개방 모드**로 쓰고, prod toml 에서만 키를 채운다(협의 결정 2026-07-13). '키의 존재'가 토글이다. - 키는 복수 허용 — 무중단 키 교체(새 키 추가 → 호출자 전환 → 옛 키 제거). - 비교는 secrets.compare_digest(상수시간) — 타이밍 공격 방지. - /healthz·/readyz 는 라우터 밖이라 guard 대상이 아니다(LB/오케스트레이터 프로브). - prod 는 여기에 더해 lps-api 포트 비공개(내부 네트워크만)를 권장 — docs/operations.md. """ import secrets from fastapi import Header, HTTPException from config.server_configs import web_server_config def configured_keys() -> set[str]: """유효 API 키 집합. 매 호출 config 를 읽는다 — 테스트에서 monkeypatch 로 on/off 전환 가능.""" return {k.strip() for k in web_server_config.api_keys if k.strip()} async def require_api_key(x_api_key: str | None = Header(None, alias="X-API-Key")): """/v1 공통 의존성. 키 미설정=개방 모드(무검증), 설정 시 X-API-Key 불일치는 401.""" keys = configured_keys() if not keys: return if not x_api_key or not any(secrets.compare_digest(x_api_key, k) for k in keys): raise HTTPException(status_code=401, detail="invalid or missing X-API-Key")