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