Skip to main content

sqlite_graphrag/config/
api_keys.rs

1//! API key resolution, fingerprinting and masking.
2//!
3//! Precedence is CLI flag then XDG `config add-key`. Product environment
4//! variables are deliberately NOT read (G-T-XDG-04).
5
6use super::store::load_config;
7use super::ResolvedKey;
8use secrecy::SecretBox;
9
10/// Resolve API key.
11pub fn resolve_api_key(provider: &str, cli_key: Option<&str>) -> Option<ResolvedKey> {
12    // G-T-XDG-04: flag/cli > XDG `config add-key` only. Product env is not read.
13    if let Some(k) = cli_key {
14        if !k.is_empty() {
15            return Some(ResolvedKey {
16                value: SecretBox::new(Box::new(k.to_owned())),
17                source: "cli",
18            });
19        }
20    }
21
22    if let Ok(cfg) = load_config() {
23        if let Some(entry) = cfg.keys.iter().find(|k| k.provider == provider) {
24            return Some(ResolvedKey {
25                value: SecretBox::new(Box::new(entry.value.clone())),
26                source: "config",
27            });
28        }
29    }
30
31    None
32}
33
34/// Compute fingerprint.
35pub fn compute_fingerprint(key: &str) -> String {
36    let hash = blake3::hash(key.as_bytes());
37    hash.to_hex()[..16].to_string()
38}
39
40/// Mask key.
41pub fn mask_key(key: &str) -> String {
42    if key.len() <= 8 {
43        return "****".to_string();
44    }
45    format!("{}...{}", &key[..4], &key[key.len() - 4..])
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use secrecy::ExposeSecret;
52    #[test]
53    fn compute_fingerprint_deterministic() {
54        let fp1 = compute_fingerprint("sk-or-v1-test-key-12345");
55        let fp2 = compute_fingerprint("sk-or-v1-test-key-12345");
56        assert_eq!(fp1, fp2);
57        assert_eq!(fp1.len(), 16);
58    }
59
60    #[test]
61    fn compute_fingerprint_differs_for_different_keys() {
62        let fp1 = compute_fingerprint("key-a");
63        let fp2 = compute_fingerprint("key-b");
64        assert_ne!(fp1, fp2);
65    }
66
67    #[test]
68    fn mask_key_short() {
69        assert_eq!(mask_key("abcd"), "****");
70        assert_eq!(mask_key("12345678"), "****");
71        assert_eq!(mask_key(""), "****");
72    }
73
74    #[test]
75    fn mask_key_normal() {
76        assert_eq!(mask_key("sk-or-v1-abcdef1234"), "sk-o...1234");
77    }
78
79    #[test]
80    fn resolve_api_key_cli_wins() {
81        let resolved = resolve_api_key("openrouter", Some("cli-key-value"));
82        assert!(resolved.is_some());
83        let r = resolved.unwrap();
84        assert_eq!(r.source, "cli");
85        assert_eq!(r.value.expose_secret(), "cli-key-value");
86    }
87
88    #[test]
89    fn resolve_api_key_cli_fallback() {
90        let resolved = resolve_api_key("nonexistent-provider", Some("cli-key"));
91        assert!(resolved.is_some());
92        let r = resolved.unwrap();
93        assert_eq!(r.source, "cli");
94        assert_eq!(r.value.expose_secret(), "cli-key");
95    }
96
97    #[test]
98    fn resolve_api_key_none_when_nothing_available() {
99        let resolved = resolve_api_key("totally-unknown-provider-xyz-no-key", None);
100        // Only returns Some if host XDG config has that provider (unlikely).
101        if let Some(r) = resolved {
102            assert_eq!(r.source, "config");
103        }
104    }
105
106    #[test]
107    fn resolve_api_key_ignores_product_env() {
108        // G-T-XDG-04: even if OPENROUTER_API_KEY is set, it must not be used.
109        unsafe {
110            std::env::set_var("OPENROUTER_API_KEY", "env-must-be-ignored");
111        }
112        let resolved = resolve_api_key("openrouter-env-ignore-test-provider", None);
113        assert!(resolved.is_none(), "product env must not supply API keys");
114        unsafe {
115            std::env::remove_var("OPENROUTER_API_KEY");
116        }
117    }
118}