Skip to main content

seher/opencode_go/
auth.rs

1use serde::Deserialize;
2use std::path::{Path, PathBuf};
3use thiserror::Error;
4
5#[derive(Debug, Error)]
6pub enum OpencodeGoAuthError {
7    #[error("could not determine home directory for OpenCode auth.json")]
8    HomeDirNotFound,
9
10    #[error("OpenCode auth file not found: {0}")]
11    AuthFileNotFound(String),
12
13    #[error("failed to read OpenCode auth file: {0}")]
14    Io(#[from] std::io::Error),
15
16    #[error("failed to parse OpenCode auth file: {0}")]
17    Parse(#[from] serde_json::Error),
18
19    #[error("opencode-go credentials not found in auth.json")]
20    MissingProvider,
21
22    #[error("opencode-go auth entry does not contain an API key")]
23    MissingKey,
24}
25
26#[derive(Debug, Deserialize)]
27struct AuthFile {
28    #[serde(rename = "opencode-go")]
29    opencode_go: Option<AuthEntry>,
30}
31
32#[derive(Debug, Deserialize)]
33struct AuthEntry {
34    key: Option<String>,
35}
36
37pub struct OpencodeGoAuth;
38
39impl OpencodeGoAuth {
40    /// # Errors
41    ///
42    /// Returns an error if the current user's home directory cannot be
43    /// resolved.
44    pub fn default_path() -> Result<PathBuf, OpencodeGoAuthError> {
45        let home = dirs::home_dir().ok_or(OpencodeGoAuthError::HomeDirNotFound)?;
46        Ok(home.join(".local/share/opencode/auth.json"))
47    }
48
49    /// # Errors
50    ///
51    /// Returns an error when the auth file is missing, unreadable, malformed,
52    /// or does not contain an `opencode-go` API key.
53    pub fn read_api_key() -> Result<String, OpencodeGoAuthError> {
54        let path = Self::default_path()?;
55        Self::read_api_key_from(&path)
56    }
57
58    /// # Errors
59    ///
60    /// Returns an error when the auth file is missing, unreadable, malformed,
61    /// or does not contain an `opencode-go` API key.
62    pub fn read_api_key_from(path: &Path) -> Result<String, OpencodeGoAuthError> {
63        if !path.exists() {
64            return Err(OpencodeGoAuthError::AuthFileNotFound(
65                path.display().to_string(),
66            ));
67        }
68
69        let content = std::fs::read_to_string(path)?;
70        let auth: AuthFile = serde_json::from_str(&content)?;
71        let entry = auth
72            .opencode_go
73            .ok_or(OpencodeGoAuthError::MissingProvider)?;
74        entry.key.ok_or(OpencodeGoAuthError::MissingKey)
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    type TestResult = Result<(), Box<dyn std::error::Error>>;
83
84    #[test]
85    fn reads_opencode_go_api_key() -> TestResult {
86        let tmp = tempfile::NamedTempFile::new()?;
87        std::fs::write(
88            tmp.path(),
89            r#"{
90                "opencode-go": {"type": "api", "key": "sk-test"},
91                "openai": {"type": "oauth", "access": "tok"}
92            }"#,
93        )?;
94
95        let key = OpencodeGoAuth::read_api_key_from(tmp.path())?;
96        assert_eq!(key, "sk-test");
97        Ok(())
98    }
99
100    #[test]
101    fn rejects_auth_file_without_opencode_go_entry() -> TestResult {
102        let tmp = tempfile::NamedTempFile::new()?;
103        std::fs::write(tmp.path(), r#"{"opencode": {"type": "api", "key": "sk"}}"#)?;
104
105        let err = OpencodeGoAuth::read_api_key_from(tmp.path())
106            .err()
107            .ok_or("expected missing provider error")?;
108        assert!(matches!(err, OpencodeGoAuthError::MissingProvider));
109        Ok(())
110    }
111
112    #[test]
113    fn rejects_auth_file_without_key() -> TestResult {
114        let tmp = tempfile::NamedTempFile::new()?;
115        std::fs::write(tmp.path(), r#"{"opencode-go": {"type": "api"}}"#)?;
116
117        let err = OpencodeGoAuth::read_api_key_from(tmp.path())
118            .err()
119            .ok_or("expected missing key error")?;
120        assert!(matches!(err, OpencodeGoAuthError::MissingKey));
121        Ok(())
122    }
123}