""" Credential manager for secure API key handling. Manages credentials for real API calls with validation and security. """ from typing import Optional, Dict import os from pathlib import Path import json class CredentialManager: """Manages API credentials securely.""" def __init__(self, config_path: Optional[str] = None): """ Initialize credential manager. Args: config_path: Optional path to credentials file """ self.config_path = config_path or os.path.expanduser("~/.token-estimator/credentials.json") self.credentials: Dict[str, Dict] = {} self._load_credentials() def _load_credentials(self): """Load credentials from file if it exists.""" config_file = Path(self.config_path) if config_file.exists(): try: with open(config_file, 'r') as f: self.credentials = json.load(f) except Exception as e: print(f"Warning: Could not load credentials: {e}") self.credentials = {} else: # Try environment variables self._load_from_env() def _load_from_env(self): """Load credentials from environment variables.""" env_mapping = { 'github': 'GITHUB_TOKEN', 'slack': 'SLACK_TOKEN', 'linear': 'LINEAR_API_KEY', 'notion': 'NOTION_TOKEN', 'stripe': 'STRIPE_API_KEY', 'sentry': 'SENTRY_AUTH_TOKEN', } for service, env_var in env_mapping.items(): value = os.getenv(env_var) if value: self.credentials[service] = {'token': value} def save_credentials(self): """Save credentials to file.""" config_file = Path(self.config_path) config_file.parent.mkdir(parents=True, exist_ok=True) with open(config_file, 'w') as f: json.dump(self.credentials, f, indent=2) # Set restrictive permissions os.chmod(config_file, 0o600) def set_credential(self, service: str, credential_type: str, value: str): """ Set a credential for a service. Args: service: Service name (e.g., 'github') credential_type: Type of credential (e.g., 'token', 'api_key') value: The credential value """ if service not in self.credentials: self.credentials[service] = {} self.credentials[service][credential_type] = value def get_credential(self, service: str, credential_type: str = 'token') -> Optional[str]: """ Get a credential for a service. Args: service: Service name credential_type: Type of credential to retrieve Returns: The credential value or None if not found """ if service in self.credentials: return self.credentials[service].get(credential_type) return None def get_all_credentials(self, service: str) -> Optional[Dict]: """ Get all credentials for a service. Args: service: Service name Returns: Dictionary of all credentials for the service """ return self.credentials.get(service) def has_credentials(self, service: str) -> bool: """ Check if credentials exist for a service. Args: service: Service name Returns: True if credentials exist """ return service in self.credentials and len(self.credentials[service]) > 0 def remove_credential(self, service: str): """ Remove all credentials for a service. Args: service: Service name """ if service in self.credentials: del self.credentials[service] def list_services(self) -> list[str]: """ List all services with stored credentials. Returns: List of service names """ return list(self.credentials.keys()) def validate_format(self, service: str, credentials: Dict) -> tuple[bool, Optional[str]]: """ Validate credential format for a service. Args: service: Service name credentials: Credentials dictionary Returns: (is_valid, error_message) """ if service == 'github': token = credentials.get('token', '') valid_prefixes = ['ghp_', 'gho_', 'ghs_', 'github_pat_'] if not any(token.startswith(p) for p in valid_prefixes): return False, "GitHub token should start with ghp_, gho_, ghs_, or github_pat_" elif service == 'slack': token = credentials.get('token', '') if not (token.startswith('xoxb-') or token.startswith('xoxp-')): return False, "Slack token should start with xoxb- (bot) or xoxp- (user)" elif service == 'linear': api_key = credentials.get('api_key', '') if not api_key.startswith('lin_api_'): return False, "Linear API key should start with lin_api_" elif service == 'notion': token = credentials.get('token', '') if not token.startswith('secret_'): return False, "Notion integration token should start with secret_" elif service == 'stripe': api_key = credentials.get('api_key', '') valid_prefixes = ['sk_test_', 'sk_live_', 'rk_test_', 'rk_live_'] if not any(api_key.startswith(p) for p in valid_prefixes): return False, "Stripe API key should start with sk_test_, sk_live_, rk_test_, or rk_live_" elif service == 'sentry': token = credentials.get('token', '') if len(token) != 64: return False, "Sentry auth token should be 64 characters" return True, None def get_masked_credentials(self, service: str) -> Optional[Dict]: """ Get credentials with values masked for display. Args: service: Service name Returns: Dictionary with masked credential values """ creds = self.get_all_credentials(service) if not creds: return None masked = {} for key, value in creds.items(): if len(value) > 8: masked[key] = value[:4] + '****' + value[-4:] else: masked[key] = '****' return masked # Global instance _credential_manager: Optional[CredentialManager] = None def get_credential_manager() -> CredentialManager: """Get the global credential manager instance.""" global _credential_manager if _credential_manager is None: _credential_manager = CredentialManager() return _credential_manager