"""Credential store abstraction. Wraps the ``keyring`` library so callers do not depend on it directly. Phase B note: the same interface is used on both Windows (Credential Manager) and Linux (Secret Service / file-based vault); see ``plans/implementation-plan-A-B.md`` step B3. """ from __future__ import annotations from dataclasses import dataclass from typing import Protocol @dataclass(frozen=True) class Credential: """Username + secret pair retrieved from a credential store.""" username: str password: str class CredentialStore(Protocol): """Read-only credential lookup.""" def get(self, target: str) -> Credential: """Return the credential associated with ``target``. Raises :class:`KeyError` if the target is unknown. """ ... class KeyringCredentialStore: """Default :class:`CredentialStore` backed by the ``keyring`` library.""" def __init__(self, service: str = "ci") -> None: self._service = service def get(self, target: str) -> Credential: try: import keyring except ImportError as exc: # pragma: no cover - dep declared raise RuntimeError( "keyring is not installed; run `pip install keyring`." ) from exc # 1. Native lookup — works on Windows Credential Manager and backends # that implement get_credential(service, username=None). cred = keyring.get_credential(target, None) if cred is not None: return Credential(username=cred.username, password=cred.password) # 2. Two-entry scheme for file backends (PlaintextKeyring, etc.) where # get_credential(target, None) is a no-op. The username is stored # separately under service="{target}:meta", key="username". username = keyring.get_password(f"{target}:meta", "username") if username is not None: password = keyring.get_password(target, username) if password is not None: return Credential(username=username, password=password) # 3. Legacy fallback: password stored under service=self._service. password = keyring.get_password(self._service, target) if password is not None: return Credential(username=target, password=password) raise KeyError(f"Credential '{target}' not found in keyring.") __all__ = ["Credential", "CredentialStore", "KeyringCredentialStore"]