[
  {
    "file": "architecture-audit-report.md",
    "patch": "Index: architecture-audit-report.md\n===================================================================\n--- architecture-audit-report.md\t\n+++ architecture-audit-report.md\t\n@@ -0,0 +1,118 @@\n+# Full Backend Architecture Review + Production Readiness Audit\n+\n+**Project:** Streaming App (FastAPI backend, 6 DDD modules)\n+**Tool:** OpenCode via Kimi K2.6\n+**Date:** 2026-05-16\n+**Total findings:** 25 (7 Critical, 6 High, 7 Medium, 5 Low)\n+\n+---\n+\n+## Architecture Review\n+\n+### Critical\n+\n+**[CRITICAL] app/modules/streaming/presentation/dependencies.py:108**\n+**Import-time singleton instantiation — `STREAM_RESOLVE_CACHE` built at module import.** `_build_stream_cache()` is called during module import, causing Redis connections (or in-memory state) to be established before the ASGI lifespan startup hook runs. This leads to import failures when Redis is unavailable and makes tests prone to singleton state pollution.\n+\n+**[CRITICAL] app/modules/streaming/infrastructure/account_lookup.py:9-12**\n+**Cross-module infrastructure-to-infrastructure dependency breaks DIP.** `DebridAccountLookup` directly imports `SQLAlchemyDebridAccountRepository` from the debrid module's infrastructure layer. The streaming module should declare its own port (`DebridAccountLookupPort`) and receive an implementation via DI, not reach into another module's SQLAlchemy internals.\n+\n+### High\n+\n+**[HIGH] app/modules/addons/presentation/dependencies.py:17-20**\n+**Addons presentation couples to streaming presentation + metadata infrastructure.** Imports `ensure_stream_resolvers` from `streaming.presentation` and instantiates `TMDbClient` from `metadata.infrastructure` directly. Bypasses managed singletons and creates presentation-layer coupling across module boundaries.\n+\n+**[HIGH] app/modules/addons/infrastructure/addon_provider.py:46-50 / app/modules/addons/presentation/dependencies.py:46**\n+**Addons module leaks unclosed TMDbClient.** `_build_addon_provider()` creates a fresh `TMDbClient()` instance, but `_StreamingAppAddonProvider` does not implement `close()`. Leaks TCP connections on every app restart.\n+\n+### Medium\n+\n+**[MEDIUM] app/modules/addons/application/stremio_stream_usecase.py:14-25**\n+**Application-layer cross-module coupling.** `StremioStreamUseCase` imports `ProviderStreamResolver` and `ResolveSource` from the streaming module. Uses public interfaces but ties modules together.\n+\n+**[MEDIUM] app/modules/streaming/application/cache_policy.py:10-13**\n+**Application layer imports private domain helper.** `from app.modules.streaming.domain.value_objects import _extract_btih_hash` — pulls a private (`_`-prefixed) function across the domain/application boundary.\n+\n+**[MEDIUM] app/modules/streaming/presentation/dependencies.py:42,63,108**\n+**Singleton state holders typed as `Any`.** `_PROVIDER_COOLDOWN`, `_STREAM_CACHE`, and `STREAM_RESOLVE_CACHE` weaken static analysis by discarding their protocol types.\n+\n+### Low\n+\n+**[LOW] app/modules/debrid/infrastructure/provider_clients.py:29**\n+**ABC base class with no abstract methods.** `BaseDebridClient` inherits from `ABC` but all methods have concrete implementations.\n+\n+**[LOW] app/core/resources.py:10-31**\n+**Dead code.** `UserRole`, `ResponseMessages`, `STATUS_MESSAGES` enums defined in core but never consumed.\n+\n+---\n+\n+## Production Readiness Audit\n+\n+### Critical\n+\n+**[CRITICAL] app/modules/user/presentation/routers.py:71**\n+**`/register` has no rate limiting.** Unlike `/login` and `/refresh`, registration accepts anonymous requests without throttling, enabling mass account creation spam and email/username enumeration.\n+\n+**[CRITICAL] app/core/security.py:39,156**\n+**JWT `SECRET_KEY` reused for Fernet API-key encryption.** A single compromise breaks both authentication confidentiality and stored credential confidentiality.\n+\n+**[CRITICAL] app/core/middleware.py:28-32**\n+**Overly permissive CORS with credentials.** `allow_methods=[\"*\"]`, `allow_headers=[\"*\"]`, `allow_credentials=True`. Violates least privilege.\n+\n+**[CRITICAL] app/main.py:84-88**\n+**Missing security headers.** No `X-Content-Type-Options`, `X-Frame-Options`, `Content-Security-Policy`, `Strict-Transport-Security`, or `Referrer-Policy` headers. No request body size limit or global request timeout middleware.\n+\n+### High\n+\n+**[HIGH] app/core/middleware.py:57**\n+**TrustedHostMiddleware only exact-matches \"production\".** Environments like `staging`, `prod`, `production-v2` fallback to `allowed_hosts=[\"*\"]`.\n+\n+**[HIGH] app/core/exception_handler.py:38-44**\n+**Unhandled exceptions not logged to structured logger.** `internal_exception_handler` returns sanitized 500 but never emits to structlog pipeline.\n+\n+**[HIGH] app/core/database.py:29-33**\n+**Database engine lacks explicit pool tuning.** Uses defaults (`pool_size=5`, `max_overflow=10`) with no `pool_recycle`. Risks connection exhaustion under production load.\n+\n+**[HIGH] app/modules/debrid/infrastructure/provider_clients.py:53-76**\n+**Non-idempotent POST/PUT requests retried by default.** `retry_once=True` with no HTTP-method restriction may cause duplicate debrid mutations.\n+\n+### Medium\n+\n+**[MEDIUM] app/core/rate_limit.py:85 / app/core/jti_denylist.py:45 / app/modules/streaming/infrastructure/provider_cooldown.py:34**\n+**In-memory stores not async-safe.** `InMemoryRateLimiter`, `InMemoryJtiDenylist`, `InMemoryProviderCooldown` mutate without locks. Concurrent async tasks can race.\n+\n+**[MEDIUM] app/modules/user/application/use_cases.py:55-106**\n+**Login silently omits refresh token when session repo is unavailable.** Clients expecting refresh-token rotation will break.\n+\n+**[MEDIUM] app/core/redis.py:19-30**\n+**Redis client rebuilt on every call without connection reuse.** Health checks and singleton factories create separate TCP connections.\n+\n+**[MEDIUM] app/core/config.py:53-54**\n+**Default dev SECRET_KEY hardcoded.** `DEFAULT_DEV_SECRET = \"dev-secret-key-change-in-production\"` — while validated in production, remains risky.\n+\n+### Low\n+\n+**[LOW] app/modules/shared/application/use_cases.py:20**\n+**Naive datetime in health check.** `datetime.now().isoformat()` lacks timezone info.\n+\n+**[LOW] app/modules/user/presentation/routers.py:232-252**\n+**Redundant DB round-trip in `get_current_user`.** Decodes JWT then re-fetches user from DB.\n+\n+**[LOW] app/modules/streaming/presentation/routers.py:95**\n+**Rate limiting occurs after input parsing.** Invalid sources trigger 422 before rate limiter.\n+\n+**[LOW] OAuth2PasswordBearer duplicated across 4 modules.** Should be centralized in `app.core.auth`.\n+\n+---\n+\n+## Strengths Noted\n+\n+- Clean DDD layers (domain/ → application/ → infrastructure/ → presentation/) across all 6 modules\n+- Consistent use of Protocol classes for ports/adapters pattern\n+- Proper dependency injection via FastAPI Depends()\n+- Good fail-open patterns for Redis-backed features\n+- JWT with explicit iss/aud/typ/jti claims, refresh rotation, and denylist support\n+- Fernet key versioning with OLD_SECRET_KEYS for rotation\n+- Proxy trust via X-Forwarded-For and TRUSTED_PROXY_IPS\n+- Proper structured logging setup with structlog\n+- Comprehensive error handling with domain-specific exception hierarchy\n",
    "additions": 118,
    "deletions": 0,
    "status": "added"
  }
]