opencode_provider_manager/auth/
parser.rs1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::Path;
9
10use super::error::{AuthError, Result};
11
12pub type AuthEntries = HashMap<String, AuthEntry>;
14
15#[derive(Clone, Serialize, Deserialize)]
17pub struct AuthEntry {
18 #[serde(rename = "type")]
20 pub auth_type: String,
21
22 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub key: Option<String>,
25
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub token: Option<String>,
29
30 #[serde(flatten)]
32 pub extra: HashMap<String, serde_json::Value>,
33}
34
35impl std::fmt::Debug for AuthEntry {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("AuthEntry")
40 .field("auth_type", &self.auth_type)
41 .field("key", &self.key.as_ref().map(|_| "***"))
42 .field("token", &self.token.as_ref().map(|_| "***"))
43 .field("extra", &self.extra)
44 .finish()
45 }
46}
47
48pub fn parse_auth_file(path: &Path) -> Result<AuthEntries> {
50 if !path.exists() {
51 return Err(AuthError::FileNotFound {
52 path: path.display().to_string(),
53 });
54 }
55
56 let content = std::fs::read_to_string(path)?;
57 parse_auth_json(&content)
58}
59
60pub fn parse_auth_json(content: &str) -> Result<AuthEntries> {
63 if let Ok(entries) = serde_json::from_str::<AuthEntries>(content) {
65 return Ok(entries);
66 }
67
68 let value = jsonc_parser::parse_to_value(content, &Default::default())
70 .map_err(|e| AuthError::InvalidFormat(format!("JSONC parse error: {e:?}")))?;
71
72 match value {
73 Some(v) => {
74 let serde_value = json_value_to_serde(&v);
75 serde_json::from_value::<AuthEntries>(serde_value)
76 .map_err(|e| AuthError::InvalidFormat(format!("{e}")))
77 }
78 None => Err(AuthError::InvalidFormat("Empty auth document".to_string())),
79 }
80}
81
82fn json_value_to_serde(value: &jsonc_parser::JsonValue<'_>) -> serde_json::Value {
84 match value {
85 jsonc_parser::JsonValue::Object(obj) => {
86 let mut map = serde_json::Map::new();
87 for (key, val) in obj.clone().into_iter() {
88 map.insert(key, json_value_to_serde(&val));
89 }
90 serde_json::Value::Object(map)
91 }
92 jsonc_parser::JsonValue::Array(arr) => {
93 let values: Vec<serde_json::Value> =
94 arr.iter().map(|v| json_value_to_serde(v)).collect();
95 serde_json::Value::Array(values)
96 }
97 jsonc_parser::JsonValue::Boolean(b) => serde_json::Value::Bool(*b),
98 jsonc_parser::JsonValue::Number(n) => {
99 if let Ok(i) = n.parse::<i64>() {
100 serde_json::Value::Number(i.into())
101 } else if let Ok(f) = n.parse::<f64>() {
102 serde_json::Value::Number(
103 serde_json::Number::from_f64(f).unwrap_or(serde_json::Number::from(0)),
104 )
105 } else {
106 serde_json::Value::Number(0.into())
107 }
108 }
109 jsonc_parser::JsonValue::String(s) => serde_json::Value::String(s.to_string()),
110 jsonc_parser::JsonValue::Null => serde_json::Value::Null,
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn test_parse_auth_json_api_key() {
120 let json = r#"{
121 "anthropic": {
122 "type": "api",
123 "key": "sk-ant-api03-xxxxx"
124 }
125 }"#;
126
127 let entries = parse_auth_json(json).unwrap();
128 assert!(entries.contains_key("anthropic"));
129 let entry = &entries["anthropic"];
130 assert_eq!(entry.auth_type, "api");
131 assert_eq!(entry.key.as_deref(), Some("sk-ant-api03-xxxxx"));
132 }
133
134 #[test]
135 fn test_parse_auth_json_oauth() {
136 let json = r#"{
137 "github-copilot": {
138 "type": "oauth",
139 "token": "gho_xxxxx"
140 }
141 }"#;
142
143 let entries = parse_auth_json(json).unwrap();
144 assert!(entries.contains_key("github-copilot"));
145 let entry = &entries["github-copilot"];
146 assert_eq!(entry.auth_type, "oauth");
147 assert_eq!(entry.token.as_deref(), Some("gho_xxxxx"));
148 }
149
150 #[test]
151 fn test_parse_auth_json_multiple_providers() {
152 let json = r#"{
153 "anthropic": {
154 "type": "api",
155 "key": "sk-ant-xxx"
156 },
157 "openai": {
158 "type": "api",
159 "key": "sk-xxx"
160 }
161 }"#;
162
163 let entries = parse_auth_json(json).unwrap();
164 assert_eq!(entries.len(), 2);
165 assert!(entries.contains_key("anthropic"));
166 assert!(entries.contains_key("openai"));
167 }
168
169 #[test]
170 fn test_parse_auth_json_empty() {
171 let json = "{}";
172 let entries = parse_auth_json(json).unwrap();
173 assert!(entries.is_empty());
174 }
175
176 #[test]
177 fn test_parse_auth_json_with_comments() {
178 let jsonc = r#"{
179 // This is my Anthropic key
180 "anthropic": {
181 "type": "api",
182 "key": "sk-ant-xxx"
183 }
184 }"#;
185
186 let entries = parse_auth_json(jsonc).unwrap();
187 assert!(entries.contains_key("anthropic"));
188 assert_eq!(entries["anthropic"].auth_type, "api");
189 }
190}