seher/opencode_go/
auth.rs1use 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 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 pub fn read_api_key() -> Result<String, OpencodeGoAuthError> {
54 let path = Self::default_path()?;
55 Self::read_api_key_from(&path)
56 }
57
58 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}