from __future__ import annotations from collections.abc import Mapping from fastapi import Request from qonto_assistant.config import Settings from qonto_assistant.contracts import ActorClaims from qonto_assistant.key_cape_auth import KeyCapeAuthError, KeyCapeTokenVerifier def actor_claims_from_headers( headers: Mapping[str, str], settings: Settings, *, key_cape_verifier: KeyCapeTokenVerifier | None = None, ) -> ActorClaims: """Shared REST/MCP claims parsing so both transports enforce identical actor identity. When a `KeyCapeTokenVerifier` is configured (QONTO-WP-0004-T03), a verified `Authorization: Bearer ` takes precedence over the self-asserted `X-Actor-*` header convention -- closing the gap `docs/mcp-integration.md` called out explicitly. If the verifier is marked `required`, a missing or invalid bearer token is rejected outright rather than silently falling back to self-asserted headers. """ authorization = headers.get("authorization", "") if authorization.lower().startswith("bearer ") and key_cape_verifier is not None: token = authorization.split(" ", 1)[1].strip() return key_cape_verifier.verify(token) if key_cape_verifier is not None and key_cape_verifier.required: raise KeyCapeAuthError("bearer_token_required") actor_id = headers.get("x-actor-id", "anonymous") tenant_id = headers.get("x-tenant-id", settings.default_tenant_id) lane = headers.get("x-actor-lane", settings.default_actor_lane) raw_scopes = headers.get("x-actor-scopes", "") scopes = frozenset(scope.strip() for scope in raw_scopes.split(",") if scope.strip()) return ActorClaims(actor_id=actor_id, tenant_id=tenant_id, lane=lane, scopes=scopes) def actor_claims_from_request( request: Request, settings: Settings, *, key_cape_verifier: KeyCapeTokenVerifier | None = None, ) -> ActorClaims: return actor_claims_from_headers(request.headers, settings, key_cape_verifier=key_cape_verifier)