"""Resolve Jira credentials for an identity without leaking them. Credentials come from the existing ``SecretStore`` interface so tests can inject a fake and production uses the OS keyring. The resolver never caches credentials beyond the call scope and never includes them in error messages, logs, or MCP payloads. Key naming convention: - Per-project: ``jira:`` - Global fallback: ``jira:default`` The email is stored alongside the token under the same key as a JSON pair ``{"email": "...", "api_token": "..."}`` so one secret-store entry carries both values atomically. """ from __future__ import annotations import json from dataclasses import dataclass from typing import Optional from ...infrastructure.secrets.secret_store import SecretStore from ...mcp_servers.project_context.foundation import IdentityContext, ProviderError @dataclass(frozen=True) class JiraCredentials: """Immutable credential pair resolved for one call.""" email: str api_token: str def _secret_key(cowork_project_id: str) -> str: return f"jira:{cowork_project_id}" _GLOBAL_KEY = "jira:default" class JiraCredentialResolver: """Resolve ``(email, api_token)`` from the secret store for one identity. Raises ``UNAVAILABLE`` when no credentials are configured — never returns empty strings that would cause a silent 401 at the HTTP layer. """ def __init__(self, store: SecretStore) -> None: self._store = store def resolve(self, identity: IdentityContext) -> JiraCredentials: """Look up credentials by project-specific key, then global fallback. Raises: ProviderError: When neither key exists or the stored value is malformed. """ raw = self._store.get(_secret_key(identity.project)) if not raw: raw = self._store.get(_GLOBAL_KEY) if not raw: raise ProviderError( "UNAVAILABLE", "Jira credentials are not configured for this project.", retryable=False, ) try: parsed = json.loads(raw) except (json.JSONDecodeError, TypeError): raise ProviderError( "UNAVAILABLE", "Stored Jira credentials are malformed; re-enter them in Connectors.", retryable=False, ) if not isinstance(parsed, dict): raise ProviderError( "UNAVAILABLE", "Stored Jira credentials are malformed; re-enter them in Connectors.", retryable=False, ) email = str(parsed.get("email", "")).strip() api_token = str(parsed.get("api_token", "")).strip() if not email or not api_token: raise ProviderError( "UNAVAILABLE", "Stored Jira credentials are incomplete; re-enter them in Connectors.", retryable=False, ) return JiraCredentials(email=email, api_token=api_token) __all__ = ["JiraCredentialResolver", "JiraCredentials"]