1use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::fmt;
18use zeroize::Zeroize;
19
20use crate::errors::SecretsError;
21
22pub const OAUTH_AT_REST_PEPPER_MIN_LENGTH: usize = 32;
23
24#[derive(Clone, Serialize, Deserialize)]
25pub struct Secrets {
26 pub oauth_at_rest_pepper: String,
27
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub manifest_signing_secret_seed: Option<String>,
30
31 pub database_url: String,
32
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub database_write_url: Option<String>,
35
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub external_database_url: Option<String>,
38
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub internal_database_url: Option<String>,
41
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub gemini: Option<String>,
44
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub anthropic: Option<String>,
47
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub openai: Option<String>,
50
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub github: Option<String>,
53
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub moonshot: Option<String>,
56
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub qwen: Option<String>,
59
60 #[serde(default, flatten)]
61 pub custom: HashMap<String, String>,
62}
63
64impl fmt::Debug for Secrets {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 f.debug_struct("Secrets")
67 .field("ai_providers", &self.has_ai_provider())
68 .field("custom_keys", &self.custom.keys().collect::<Vec<_>>())
69 .finish_non_exhaustive()
70 }
71}
72
73impl Drop for Secrets {
74 fn drop(&mut self) {
75 self.oauth_at_rest_pepper.zeroize();
76 self.manifest_signing_secret_seed.zeroize();
77 self.database_url.zeroize();
78 self.database_write_url.zeroize();
79 self.external_database_url.zeroize();
80 self.internal_database_url.zeroize();
81 self.gemini.zeroize();
82 self.anthropic.zeroize();
83 self.openai.zeroize();
84 self.github.zeroize();
85 self.moonshot.zeroize();
86 self.qwen.zeroize();
87 for value in self.custom.values_mut() {
88 value.zeroize();
89 }
90 }
91}
92
93impl Secrets {
94 pub fn parse(content: &str) -> Result<Self, SecretsError> {
95 let mut value: serde_json::Value =
96 serde_json::from_str(content).map_err(|source| SecretsError::Parse {
97 context: "Failed to parse secrets JSON",
98 source,
99 })?;
100 if let Some(obj) = value.as_object_mut() {
101 obj.retain(|_, v| !v.is_null());
102 }
103 let secrets: Self =
104 serde_json::from_value(value).map_err(|source| SecretsError::Parse {
105 context: "Failed to deserialize secrets after null stripping",
106 source,
107 })?;
108 secrets.validate()?;
109 Ok(secrets)
110 }
111
112 pub fn validate(&self) -> Result<(), SecretsError> {
113 if self.oauth_at_rest_pepper.len() < OAUTH_AT_REST_PEPPER_MIN_LENGTH {
114 return Err(SecretsError::Invalid(format!(
115 "oauth_at_rest_pepper must be at least {} characters (got {})",
116 OAUTH_AT_REST_PEPPER_MIN_LENGTH,
117 self.oauth_at_rest_pepper.len()
118 )));
119 }
120 Ok(())
121 }
122
123 pub fn effective_database_url(&self, external_db_access: bool) -> &str {
124 if external_db_access {
125 if let Some(url) = &self.external_database_url {
126 return url;
127 }
128 }
129 &self.database_url
130 }
131
132 pub const fn has_ai_provider(&self) -> bool {
133 self.gemini.is_some()
134 || self.anthropic.is_some()
135 || self.openai.is_some()
136 || self.moonshot.is_some()
137 || self.qwen.is_some()
138 }
139
140 pub fn get(&self, key: &str) -> Option<&String> {
141 match key {
142 "oauth_at_rest_pepper" | "OAUTH_AT_REST_PEPPER" => Some(&self.oauth_at_rest_pepper),
143 "database_url" | "DATABASE_URL" => Some(&self.database_url),
144 "database_write_url" | "DATABASE_WRITE_URL" => self.database_write_url.as_ref(),
145 "external_database_url" | "EXTERNAL_DATABASE_URL" => {
146 self.external_database_url.as_ref()
147 },
148 "internal_database_url" | "INTERNAL_DATABASE_URL" => {
149 self.internal_database_url.as_ref()
150 },
151 "gemini" | "GEMINI_API_KEY" => self.gemini.as_ref(),
152 "anthropic" | "ANTHROPIC_API_KEY" => self.anthropic.as_ref(),
153 "openai" | "OPENAI_API_KEY" => self.openai.as_ref(),
154 "github" | "GITHUB_TOKEN" => self.github.as_ref(),
155 "moonshot" | "MOONSHOT_API_KEY" | "kimi" | "KIMI_API_KEY" => self.moonshot.as_ref(),
156 "qwen" | "QWEN_API_KEY" | "dashscope" | "DASHSCOPE_API_KEY" => self.qwen.as_ref(),
157 other => self.custom.get(other).or_else(|| {
158 let alternate = if other.chars().any(char::is_uppercase) {
159 other.to_lowercase()
160 } else {
161 other.to_uppercase()
162 };
163 self.custom.get(&alternate)
164 }),
165 }
166 }
167
168 pub fn log_configured_providers(&self) {
169 let configured: Vec<&str> = [
170 self.gemini.as_ref().map(|_| "gemini"),
171 self.anthropic.as_ref().map(|_| "anthropic"),
172 self.openai.as_ref().map(|_| "openai"),
173 self.github.as_ref().map(|_| "github"),
174 self.moonshot.as_ref().map(|_| "moonshot"),
175 self.qwen.as_ref().map(|_| "qwen"),
176 ]
177 .into_iter()
178 .flatten()
179 .collect();
180
181 tracing::info!(providers = ?configured, "Configured API providers");
182 }
183
184 pub fn to_subprocess_env(&self) -> Vec<(String, String)> {
185 let mut pairs: Vec<(String, String)> = Vec::new();
186
187 pairs.push((
188 "OAUTH_AT_REST_PEPPER".to_owned(),
189 self.oauth_at_rest_pepper.clone(),
190 ));
191 pairs.push(("DATABASE_URL".to_owned(), self.database_url.clone()));
192
193 let optionals: &[(&str, &Option<String>)] = &[
194 (
195 "MANIFEST_SIGNING_SECRET_SEED",
196 &self.manifest_signing_secret_seed,
197 ),
198 ("DATABASE_WRITE_URL", &self.database_write_url),
199 ("EXTERNAL_DATABASE_URL", &self.external_database_url),
200 ("INTERNAL_DATABASE_URL", &self.internal_database_url),
201 ("GEMINI_API_KEY", &self.gemini),
202 ("ANTHROPIC_API_KEY", &self.anthropic),
203 ("OPENAI_API_KEY", &self.openai),
204 ("GITHUB_TOKEN", &self.github),
205 ("MOONSHOT_API_KEY", &self.moonshot),
206 ("QWEN_API_KEY", &self.qwen),
207 ];
208 for (name, value) in optionals {
209 if let Some(v) = value
210 && !v.is_empty()
211 {
212 pairs.push(((*name).to_owned(), v.clone()));
213 }
214 }
215
216 if !self.custom.is_empty() {
217 let mut names: Vec<String> = Vec::with_capacity(self.custom.len());
218 for (key, value) in &self.custom {
219 let upper = key.to_uppercase();
220 names.push(upper.clone());
221 pairs.push((upper.clone(), value.clone()));
222 if upper != *key {
223 pairs.push((key.clone(), value.clone()));
224 }
225 }
226 pairs.push(("SYSTEMPROMPT_CUSTOM_SECRETS".to_owned(), names.join(",")));
227 }
228
229 pairs
230 }
231}