"""Per-request credential extraction from X- headers (FastAPI dependencies). The service is a STATELESS proxy: it stores no secrets. Every credential is supplied per request as an X- header and used only to talk to the upstream API (see AGENTS.md "Secrets v parametrech"). Declaring the headers as FastAPI ``Header`` parameters makes them appear per-operation in Swagger, including the "Try it out" form. Google Analytics (chosen model: token has precedence over service account): * ``X-GA-Access-Token`` - a ready OAuth2 access token; used directly as Bearer. * ``X-GA-Credentials`` - base64-encoded service-account JSON key; the proxy mints a short-lived access token from it. * ``X-GA-Quota-Project`` - optional billing/quota project id. At least one of token / credentials must be present. Sklik: * ``X-Sklik-Token`` - the Sklik API token from account settings. The proxy calls ``client.loginByToken`` to obtain a session. """ from __future__ import annotations import base64 import binascii import json from dataclasses import dataclass from fastapi import Header from .errors import MissingCredentialsError # --- Google Analytics --------------------------------------------------------- @dataclass class GaCredentials: access_token: str | None service_account_info: dict | None quota_project: str | None def get_ga_credentials( x_ga_access_token: str | None = Header( default=None, alias="X-GA-Access-Token", description="Ready OAuth2 access token used directly as a Bearer token. " "Takes precedence over X-GA-Credentials.", ), x_ga_credentials: str | None = Header( default=None, alias="X-GA-Credentials", description="Base64-encoded Google service-account JSON key. The proxy " "mints a short-lived access token from it (scope analytics.readonly). " "Used only if X-GA-Access-Token is absent.", ), x_ga_quota_project: str | None = Header( default=None, alias="X-GA-Quota-Project", description="Optional Google Cloud project id used for quota/billing " "(sets the x-goog-user-project header upstream).", ), ) -> GaCredentials: """Resolve GA credentials from headers. Token wins over service account.""" access_token = (x_ga_access_token or "").strip() or None service_account_info: dict | None = None raw = (x_ga_credentials or "").strip() if raw: try: decoded = base64.b64decode(raw, validate=True) except (binascii.Error, ValueError) as exc: raise MissingCredentialsError( "X-GA-Credentials is not valid base64." ) from exc try: service_account_info = json.loads(decoded) except (json.JSONDecodeError, UnicodeDecodeError) as exc: raise MissingCredentialsError( "X-GA-Credentials does not decode to valid JSON." ) from exc if not isinstance(service_account_info, dict): raise MissingCredentialsError( "X-GA-Credentials JSON must be a service-account object." ) if not access_token and service_account_info is None: raise MissingCredentialsError( "Provide either X-GA-Access-Token or X-GA-Credentials." ) return GaCredentials( access_token=access_token, service_account_info=service_account_info, quota_project=(x_ga_quota_project or "").strip() or None, ) # --- Sklik -------------------------------------------------------------------- def get_sklik_token( x_sklik_token: str | None = Header( default=None, alias="X-Sklik-Token", description="Sklik API token from Sklik account settings. The proxy uses " "it to obtain a session via client.loginByToken.", ), ) -> str: token = (x_sklik_token or "").strip() if not token: raise MissingCredentialsError("X-Sklik-Token header is required.") return token