from __future__ import annotations import base64 import hashlib import json from typing import Any from app.social.domain.errors import SocialProviderUnavailableError class TokenCipher: """Narrow encryption boundary for non-Vault local development. Imports cryptography lazily so provider discovery and existing media APIs still start when social token storage is unused. Production should use Supabase Vault references instead of this encrypted database fallback. """ def __init__(self, key: str | None) -> None: self._key = key def _fernet(self): if not self._key: raise SocialProviderUnavailableError( "SOCIAL_OAUTH_ENCRYPTION_KEY is required when Supabase Vault is disabled." ) try: from cryptography.fernet import Fernet except ImportError as exc: raise SocialProviderUnavailableError("The cryptography package is required for token storage.") from exc digest = hashlib.sha256(self._key.encode("utf-8")).digest() return Fernet(base64.urlsafe_b64encode(digest)) def encrypt(self, value: dict[str, Any]) -> str: return self._fernet().encrypt(json.dumps(value, separators=(",", ":")).encode()).decode() def decrypt(self, value: str) -> dict[str, Any]: result = json.loads(self._fernet().decrypt(value.encode()).decode()) return result if isinstance(result, dict) else {}