lc/cli/
keys.rs

1use anyhow::Result;
2use colored::Colorize;
3use std::io::{self, Write};
4
5use crate::cli::KeyCommands;
6use crate::config;
7
8/// Handle key command operations
9pub async fn handle(command: KeyCommands) -> Result<()> {
10    match command {
11        KeyCommands::Add { name } => add_key(name).await,
12        KeyCommands::Get { name } => get_key(name).await,
13        KeyCommands::List => list_keys().await,
14        KeyCommands::Remove { name } => remove_key(name).await,
15    }
16}
17
18async fn add_key(name: String) -> Result<()> {
19    let mut config = config::Config::load()?;
20
21    if !config.has_provider(&name) {
22        anyhow::bail!(
23            "Provider '{}' not found. Add it first with 'lc providers add'",
24            name
25        );
26    }
27
28    // Detect Google SA JWT providers and prompt for Service Account JSON
29    let provider_cfg = config.get_provider(&name)?;
30    let is_google_sa = provider_cfg.auth_type.as_deref() == Some("google_sa_jwt")
31        || provider_cfg.endpoint.contains("aiplatform.googleapis.com");
32
33    if is_google_sa {
34        println!("Detected Google Vertex AI provider. Please provide the Service Account JSON.");
35        println!("Options:");
36        println!("  1. Paste the base64 version directly (ex: cat sa.json | base64)");
37        println!("  2. Provide the path to the JSON file (ex: /path/to/sa.json)");
38        print!("Base64 Service Account JSON or file path for {}: ", name);
39        io::stdout().flush()?;
40
41        // Use regular stdin reading instead of rpassword for large inputs
42        let mut input = String::new();
43        io::stdin().read_line(&mut input)?;
44        let input = input.trim();
45
46        let sa_json = if input.starts_with('/') || input.ends_with(".json") {
47            // Treat as file path
48            match std::fs::read_to_string(input) {
49                Ok(file_content) => file_content,
50                Err(e) => {
51                    anyhow::bail!("Failed to read service account file '{}': {}", input, e)
52                }
53            }
54        } else {
55            // Treat as base64 input - clean whitespace and newlines
56            let sa_json_b64 = input
57                .trim()
58                .replace("\n", "")
59                .replace("\r", "")
60                .replace(" ", "");
61
62            // Decode base64
63            use base64::{engine::general_purpose, Engine as _};
64            match general_purpose::STANDARD.decode(&sa_json_b64) {
65                Ok(decoded_bytes) => match String::from_utf8(decoded_bytes) {
66                    Ok(json_str) => json_str,
67                    Err(_) => anyhow::bail!("Invalid UTF-8 in decoded base64 data"),
68                },
69                Err(_) => anyhow::bail!("Invalid base64 format"),
70            }
71        };
72
73        // Minimal validation
74        let parsed: serde_json::Value =
75            serde_json::from_str(&sa_json).map_err(|e| anyhow::anyhow!("Invalid JSON: {}", e))?;
76        let sa_type = parsed.get("type").and_then(|v| v.as_str()).unwrap_or("");
77        let client_email = parsed
78            .get("client_email")
79            .and_then(|v| v.as_str())
80            .unwrap_or("");
81        let private_key = parsed
82            .get("private_key")
83            .and_then(|v| v.as_str())
84            .unwrap_or("");
85
86        if sa_type != "service_account" {
87            anyhow::bail!("Service Account JSON must have \"type\": \"service_account\"");
88        }
89        if client_email.is_empty() {
90            anyhow::bail!("Service Account JSON missing 'client_email'");
91        }
92        if private_key.is_empty() {
93            anyhow::bail!("Service Account JSON missing 'private_key'");
94        }
95
96        // Store full JSON string in api_key field (used by JWT mint flow)
97        config.set_api_key(name.clone(), sa_json)?;
98        config.save()?;
99        println!(
100            "{} Service Account stored for provider '{}'",
101            "✓".green(),
102            name
103        );
104    } else {
105        print!("Enter API key for {}: ", name);
106        io::stdout().flush()?;
107        let key = rpassword::read_password()?;
108
109        config.set_api_key(name.clone(), key)?;
110        config.save()?;
111        println!("{} API key set for provider '{}'", "✓".green(), name);
112    }
113
114    Ok(())
115}
116
117async fn get_key(name: String) -> Result<()> {
118    let config = config::Config::load()?;
119
120    if !config.has_provider(&name) {
121        anyhow::bail!("Provider '{}' not found", name);
122    }
123
124    // Use centralized keys instead of provider config
125    let keys = crate::keys::KeysConfig::load()?;
126    if let Some(auth) = keys.get_auth(&name) {
127        match auth {
128            crate::keys::ProviderAuth::ApiKey(key) => println!("{}", key),
129            crate::keys::ProviderAuth::ServiceAccount(sa_json) => println!("{}", sa_json),
130            crate::keys::ProviderAuth::Token(token) => println!("{}", token),
131            crate::keys::ProviderAuth::OAuthToken(oauth) => println!("{}", oauth),
132            crate::keys::ProviderAuth::Headers(headers) => {
133                for (k, v) in headers {
134                    println!("{}={}", k, v);
135                }
136            }
137        }
138    } else {
139        anyhow::bail!("No API key configured for provider '{}'", name);
140    }
141
142    Ok(())
143}
144
145async fn list_keys() -> Result<()> {
146    let config = config::Config::load()?;
147    if config.providers.is_empty() {
148        println!("No providers configured.");
149        return Ok(());
150    }
151
152    println!("\n{}", "API Key Status:".bold().blue());
153
154    // Load keys from centralized keys.toml
155    let keys = crate::keys::KeysConfig::load().unwrap_or_else(|_| crate::keys::KeysConfig::new());
156
157    for (name, _provider_config) in &config.providers {
158        // Check if provider has authentication in centralized keys
159        let has_auth = keys.has_auth(name);
160        let status = if has_auth {
161            "✓ Configured".green()
162        } else {
163            "✗ Missing".red()
164        };
165        println!("  {} {} - {}", "•".blue(), name.bold(), status);
166    }
167
168    Ok(())
169}
170
171async fn remove_key(name: String) -> Result<()> {
172    let mut config = config::Config::load()?;
173
174    if !config.has_provider(&name) {
175        anyhow::bail!("Provider '{}' not found", name);
176    }
177
178    if let Some(provider_config) = config.providers.get_mut(&name) {
179        provider_config.api_key = None;
180    }
181    config.save()?;
182    println!("{} API key removed for provider '{}'", "✓".green(), name);
183
184    Ok(())
185}