Skip to main content

logicaffeine_cli/project/
credentials.rs

1//! Phase 39: Credential Management
2//!
3//! Stores and retrieves API tokens for the package registry.
4//!
5//! Credentials are stored in a TOML file at `~/.config/logos/credentials.toml`
6//! with restrictive permissions (0600 on Unix) to protect sensitive tokens.
7//!
8//! # Token Resolution Order
9//!
10//! When retrieving a token via [`get_token`], the following order is used:
11//! 1. `LOGOS_TOKEN` environment variable (highest priority)
12//! 2. Credentials file entry for the registry URL
13//!
14//! # Security
15//!
16//! - Tokens are stored in plaintext (like cargo, npm, etc.)
17//! - File permissions are set to owner-only on Unix systems
18//! - The `LOGOS_CREDENTIALS_PATH` env var can override the default location
19
20use std::collections::HashMap;
21use std::fs;
22use std::path::PathBuf;
23
24/// Persistent storage for registry authentication tokens.
25///
26/// Tokens are stored per-registry URL, allowing authentication with
27/// multiple registries simultaneously.
28///
29/// # File Format
30///
31/// ```toml
32/// [registries]
33/// "https://registry.logicaffeine.com" = "tok_xxxxx"
34/// ```
35#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
36pub struct Credentials {
37    /// Map of registry URL to authentication token.
38    #[serde(default)]
39    pub registries: HashMap<String, String>,
40}
41
42impl Credentials {
43    /// Load credentials from the default location
44    pub fn load() -> Result<Self, CredentialsError> {
45        let path = credentials_path().ok_or(CredentialsError::NoConfigDir)?;
46
47        if !path.exists() {
48            return Ok(Self::default());
49        }
50
51        let content = fs::read_to_string(&path)
52            .map_err(|e| CredentialsError::Io(e.to_string()))?;
53
54        toml::from_str(&content)
55            .map_err(|e| CredentialsError::Parse(e.to_string()))
56    }
57
58    /// Save credentials to the default location
59    pub fn save(&self) -> Result<(), CredentialsError> {
60        let path = credentials_path().ok_or(CredentialsError::NoConfigDir)?;
61
62        // Create parent directory if needed
63        if let Some(parent) = path.parent() {
64            fs::create_dir_all(parent)
65                .map_err(|e| CredentialsError::Io(e.to_string()))?;
66        }
67
68        let content = toml::to_string_pretty(self)
69            .map_err(|e| CredentialsError::Serialize(e.to_string()))?;
70
71        fs::write(&path, content)
72            .map_err(|e| CredentialsError::Io(e.to_string()))?;
73
74        // Set restrictive permissions on Unix
75        #[cfg(unix)]
76        {
77            use std::os::unix::fs::PermissionsExt;
78            let perms = std::fs::Permissions::from_mode(0o600);
79            fs::set_permissions(&path, perms)
80                .map_err(|e| CredentialsError::Io(e.to_string()))?;
81        }
82
83        Ok(())
84    }
85
86    /// Get token for a registry
87    pub fn get_token(&self, registry_url: &str) -> Option<&str> {
88        self.registries.get(registry_url).map(|s| s.as_str())
89    }
90
91    /// Set token for a registry
92    pub fn set_token(&mut self, registry_url: &str, token: &str) {
93        self.registries.insert(registry_url.to_string(), token.to_string());
94    }
95
96    /// Remove token for a registry
97    pub fn remove_token(&mut self, registry_url: &str) {
98        self.registries.remove(registry_url);
99    }
100}
101
102/// Get the token for a registry, checking env var first then credentials file
103pub fn get_token(registry_url: &str) -> Option<String> {
104    // Check LOGOS_TOKEN env var first
105    if let Ok(token) = std::env::var("LOGOS_TOKEN") {
106        if !token.is_empty() {
107            return Some(token);
108        }
109    }
110
111    // Fall back to credentials file
112    Credentials::load()
113        .ok()
114        .and_then(|c| c.get_token(registry_url).map(String::from))
115}
116
117/// Get the path to the credentials file
118pub fn credentials_path() -> Option<PathBuf> {
119    // Check LOGOS_CREDENTIALS_PATH env var first
120    if let Ok(path) = std::env::var("LOGOS_CREDENTIALS_PATH") {
121        return Some(PathBuf::from(path));
122    }
123
124    // Use standard config directory
125    dirs::config_dir().map(|p| p.join("logos").join("credentials.toml"))
126}
127
128/// Errors that can occur when loading or saving credentials.
129#[derive(Debug)]
130pub enum CredentialsError {
131    /// Could not determine the config directory (e.g., `$HOME` not set).
132    NoConfigDir,
133    /// File system operation failed.
134    Io(String),
135    /// Failed to parse the credentials TOML file.
136    Parse(String),
137    /// Failed to serialize credentials to TOML.
138    Serialize(String),
139}
140
141impl std::fmt::Display for CredentialsError {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        match self {
144            Self::NoConfigDir => write!(f, "Could not determine config directory"),
145            Self::Io(e) => write!(f, "I/O error: {}", e),
146            Self::Parse(e) => write!(f, "Failed to parse credentials: {}", e),
147            Self::Serialize(e) => write!(f, "Failed to serialize credentials: {}", e),
148        }
149    }
150}
151
152impl std::error::Error for CredentialsError {}