systemprompt_models/
secrets.rs1use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11use crate::errors::SecretsError;
12
13pub const JWT_SECRET_MIN_LENGTH: usize = 32;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Secrets {
17 pub jwt_secret: String,
18
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub manifest_signing_secret_seed: Option<String>,
21
22 pub database_url: String,
23
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub database_write_url: Option<String>,
26
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub external_database_url: Option<String>,
29
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub internal_database_url: Option<String>,
32
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub sync_token: Option<String>,
35
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub gemini: Option<String>,
38
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub anthropic: Option<String>,
41
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub openai: Option<String>,
44
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub github: Option<String>,
47
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub moonshot: Option<String>,
50
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub qwen: Option<String>,
53
54 #[serde(default, flatten)]
55 pub custom: HashMap<String, String>,
56}
57
58impl Secrets {
59 pub fn parse(content: &str) -> Result<Self, SecretsError> {
60 let mut value: serde_json::Value =
61 serde_json::from_str(content).map_err(|source| SecretsError::Parse {
62 context: "Failed to parse secrets JSON",
63 source,
64 })?;
65 if let Some(obj) = value.as_object_mut() {
66 obj.retain(|_, v| !v.is_null());
67 }
68 let secrets: Self =
69 serde_json::from_value(value).map_err(|source| SecretsError::Parse {
70 context: "Failed to deserialize secrets after null stripping",
71 source,
72 })?;
73 secrets.validate()?;
74 Ok(secrets)
75 }
76
77 pub fn validate(&self) -> Result<(), SecretsError> {
78 if self.jwt_secret.len() < JWT_SECRET_MIN_LENGTH {
79 return Err(SecretsError::Invalid(format!(
80 "jwt_secret must be at least {} characters (got {})",
81 JWT_SECRET_MIN_LENGTH,
82 self.jwt_secret.len()
83 )));
84 }
85 Ok(())
86 }
87
88 pub fn effective_database_url(&self, external_db_access: bool) -> &str {
89 if external_db_access {
90 if let Some(url) = &self.external_database_url {
91 return url;
92 }
93 }
94 &self.database_url
95 }
96
97 pub const fn has_ai_provider(&self) -> bool {
98 self.gemini.is_some()
99 || self.anthropic.is_some()
100 || self.openai.is_some()
101 || self.moonshot.is_some()
102 || self.qwen.is_some()
103 }
104
105 pub fn get(&self, key: &str) -> Option<&String> {
106 match key {
107 "jwt_secret" | "JWT_SECRET" => Some(&self.jwt_secret),
108 "database_url" | "DATABASE_URL" => Some(&self.database_url),
109 "database_write_url" | "DATABASE_WRITE_URL" => self.database_write_url.as_ref(),
110 "external_database_url" | "EXTERNAL_DATABASE_URL" => {
111 self.external_database_url.as_ref()
112 },
113 "internal_database_url" | "INTERNAL_DATABASE_URL" => {
114 self.internal_database_url.as_ref()
115 },
116 "sync_token" | "SYNC_TOKEN" => self.sync_token.as_ref(),
117 "gemini" | "GEMINI_API_KEY" => self.gemini.as_ref(),
118 "anthropic" | "ANTHROPIC_API_KEY" => self.anthropic.as_ref(),
119 "openai" | "OPENAI_API_KEY" => self.openai.as_ref(),
120 "github" | "GITHUB_TOKEN" => self.github.as_ref(),
121 "moonshot" | "MOONSHOT_API_KEY" | "kimi" | "KIMI_API_KEY" => self.moonshot.as_ref(),
122 "qwen" | "QWEN_API_KEY" | "dashscope" | "DASHSCOPE_API_KEY" => self.qwen.as_ref(),
123 other => self.custom.get(other).or_else(|| {
124 let alternate = if other.chars().any(char::is_uppercase) {
125 other.to_lowercase()
126 } else {
127 other.to_uppercase()
128 };
129 self.custom.get(&alternate)
130 }),
131 }
132 }
133
134 pub fn log_configured_providers(&self) {
135 let configured: Vec<&str> = [
136 self.gemini.as_ref().map(|_| "gemini"),
137 self.anthropic.as_ref().map(|_| "anthropic"),
138 self.openai.as_ref().map(|_| "openai"),
139 self.github.as_ref().map(|_| "github"),
140 self.moonshot.as_ref().map(|_| "moonshot"),
141 self.qwen.as_ref().map(|_| "qwen"),
142 ]
143 .into_iter()
144 .flatten()
145 .collect();
146
147 tracing::info!(providers = ?configured, "Configured API providers");
148 }
149
150 pub fn custom_env_vars(&self) -> Vec<(String, &str)> {
151 self.custom
152 .iter()
153 .flat_map(|(key, value)| {
154 let upper_key = key.to_uppercase();
155 let value_str = value.as_str();
156 if upper_key == *key {
157 vec![(key.clone(), value_str)]
158 } else {
159 vec![(key.clone(), value_str), (upper_key, value_str)]
160 }
161 })
162 .collect()
163 }
164
165 pub fn custom_env_var_names(&self) -> Vec<String> {
166 self.custom.keys().map(|key| key.to_uppercase()).collect()
167 }
168}