from __future__ import annotations from dataclasses import dataclass from typing import Mapping, Protocol @dataclass(frozen=True) class Actor: """Authenticated caller context. In the MVP this is commonly produced from dev headers. Production deployments can map JWT/OIDC/session claims into the same shape. """ subject: str display_name: str | None = None roles: tuple[str, ...] = () class AuthenticationBackend(Protocol): def authenticate(self, headers: Mapping[str, str]) -> Actor | None: """Return an Actor for the request or None when unauthenticated.""" class DevHeaderAuthBackend: """Development authentication using headers. Headers: - X-Fan-User: stable external subject - X-Fan-Display-Name: optional display name - X-Fan-Roles: optional comma-separated roles """ def authenticate(self, headers: Mapping[str, str]) -> Actor | None: subject = headers.get("x-fan-user") or headers.get("X-Fan-User") if not subject: return None display_name = headers.get("x-fan-display-name") or headers.get("X-Fan-Display-Name") roles_header = headers.get("x-fan-roles") or headers.get("X-Fan-Roles") or "" roles = tuple(role.strip() for role in roles_header.split(",") if role.strip()) return Actor(subject=subject, display_name=display_name, roles=roles) @dataclass(frozen=True) class ModerationDecision: allowed: bool reason: str = "" class ModerationBackend(Protocol): def check_text(self, *, text: str, context: dict[str, object] | None = None) -> ModerationDecision: """Return whether user-provided text is acceptable.""" class AllowAllModerationBackend: """Default placeholder moderation backend. This does not pretend to moderate content; it provides a concrete seam for plugging in human review queues, ML classifiers, or third-party trust/safety tools while keeping the API code stable. """ def check_text(self, *, text: str, context: dict[str, object] | None = None) -> ModerationDecision: return ModerationDecision(allowed=True)