"""Small, transport-facing safeguards for social provider data. Provider access tokens are credentials, not application data. This module is intentionally used at every boundary that can serialize provider-controlled metadata (REST, MCP, SDK, n8n, frontend, and audit records). """ from __future__ import annotations import re from typing import Any _SENSITIVE_KEY_PARTS = frozenset( { "access_token", "refresh_token", "id_token", "token", "secret", "authorization", "cookie", "password", "api_key", "credential", } ) _BEARER = re.compile(r"(?i)\bbearer\s+[A-Za-z0-9._~+/=-]+") _ASSIGNED_SECRET = re.compile( r"(?i)(access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|" r"authorization|api[_-]?key|password|secret|credential)" r"([\"']?\s*[:=]\s*[\"']?)([^\"'\s,&}]+)" ) def public_provider_data(value: Any) -> Any: """Recursively omit credential-like fields from externally visible data. This is defense in depth. TokenService remains the only supported credential reader, but provider responses and future metadata additions must never be able to bypass that contract accidentally. """ if isinstance(value, dict): return { str(key): public_provider_data(item) for key, item in value.items() if not _is_sensitive_key(str(key)) } if isinstance(value, list): return [public_provider_data(item) for item in value] if isinstance(value, tuple): return [public_provider_data(item) for item in value] if isinstance(value, str): return redact_sensitive_text(value) return value def _is_sensitive_key(key: str) -> bool: normalized = key.strip().lower().replace("-", "_") return any(part in normalized for part in _SENSITIVE_KEY_PARTS) def redact_sensitive_text(value: str) -> str: """Remove recognizable credentials embedded in provider-controlled text.""" redacted = _BEARER.sub("Bearer [REDACTED]", value) return _ASSIGNED_SECRET.sub( lambda match: f"{match.group(1)}{match.group(2)}[REDACTED]", redacted, )