did_hosting_server/
config.rs1use crate::error::AppError;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5pub use did_hosting_common::server::config::{
7 AuthConfig, FeaturesConfig, HostingConfig, LogConfig, LogFormat, SecretsConfig, ServerConfig,
8 StoreConfig, TransportSelection, VtaConfig,
9};
10
11#[derive(Debug, Clone, Deserialize, Serialize)]
12pub struct AppConfig {
13 #[serde(default)]
14 pub features: FeaturesConfig,
15 pub server_did: Option<String>,
16 pub mediator_did: Option<String>,
17 pub public_url: Option<String>,
18 #[serde(default)]
19 pub server: ServerConfig,
20 #[serde(default)]
21 pub log: LogConfig,
22 #[serde(default)]
23 pub store: StoreConfig,
24 #[serde(default)]
25 pub auth: AuthConfig,
26 #[serde(default)]
32 pub hosting: HostingConfig,
33 #[serde(default)]
34 pub secrets: SecretsConfig,
35 #[serde(default)]
36 pub limits: LimitsConfig,
37 #[serde(default)]
38 pub stats: StatsConfig,
39 #[serde(default)]
40 pub watchers: Vec<WatcherEndpoint>,
41 pub control_url: Option<String>,
43 pub control_did: Option<String>,
45 #[serde(default)]
46 pub vta: VtaConfig,
47 #[serde(default)]
51 pub identity: did_hosting_common::server::config::IdentityConfig,
52 #[serde(skip)]
53 pub config_path: PathBuf,
54}
55
56#[derive(Clone, Deserialize, Serialize)]
57pub struct WatcherEndpoint {
58 pub url: String,
59 pub token: Option<String>,
60}
61
62impl std::fmt::Debug for WatcherEndpoint {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("WatcherEndpoint")
68 .field("url", &self.url)
69 .field("token", &self.token.as_ref().map(|_| "<redacted>"))
70 .finish()
71 }
72}
73
74#[derive(Debug, Clone, Deserialize, Serialize)]
75pub struct LimitsConfig {
76 #[serde(default = "default_upload_body_limit")]
78 pub upload_body_limit: usize,
79 #[serde(default = "default_max_total_size")]
81 pub default_max_total_size: u64,
82 #[serde(default = "default_max_did_count")]
84 pub default_max_did_count: u64,
85}
86
87fn default_upload_body_limit() -> usize {
88 102_400
89}
90
91fn default_max_total_size() -> u64 {
92 1_048_576
93}
94
95fn default_max_did_count() -> u64 {
96 20
97}
98
99impl Default for LimitsConfig {
100 fn default() -> Self {
101 Self {
102 upload_body_limit: default_upload_body_limit(),
103 default_max_total_size: default_max_total_size(),
104 default_max_did_count: default_max_did_count(),
105 }
106 }
107}
108
109#[derive(Debug, Clone, Deserialize, Serialize)]
111pub struct StatsConfig {
112 #[serde(default = "default_stats_flush_interval")]
114 pub flush_interval_secs: u64,
115 #[serde(default = "default_stats_sync_interval")]
118 pub sync_interval_secs: u64,
119}
120
121fn default_stats_flush_interval() -> u64 {
122 5
123}
124
125fn default_stats_sync_interval() -> u64 {
126 1
127}
128
129impl Default for StatsConfig {
130 fn default() -> Self {
131 Self {
132 flush_interval_secs: default_stats_flush_interval(),
133 sync_interval_secs: default_stats_sync_interval(),
134 }
135 }
136}
137
138impl AppConfig {
139 pub fn public_base_url(&self) -> String {
141 self.public_url
142 .clone()
143 .unwrap_or_else(|| format!("http://{}:{}", self.server.host, self.server.port))
144 }
145
146 pub fn load(config_path: Option<PathBuf>) -> Result<Self, AppError> {
147 let path = config_path
148 .or_else(|| {
149 std::env::var("DID_HOSTING_CONFIG_PATH")
150 .ok()
151 .map(PathBuf::from)
152 })
153 .unwrap_or_else(|| PathBuf::from("config.toml"));
154
155 if !path.exists() {
156 return Err(AppError::Config(format!(
157 "configuration file not found: {}",
158 path.display()
159 )));
160 }
161
162 let contents = std::fs::read_to_string(&path).map_err(AppError::Io)?;
163 let mut config = toml::from_str::<AppConfig>(&contents)
164 .map_err(|e| AppError::Config(format!("failed to parse {}: {e}", path.display())))?;
165
166 config.config_path = path.clone();
167
168 did_hosting_common::server::config::apply_env_overrides(
170 "WEBVH",
171 &mut config.features,
172 &mut config.server,
173 &mut config.log,
174 &mut config.store,
175 &mut config.auth,
176 &mut config.secrets,
177 )?;
178
179 macro_rules! env_opt {
181 ($var:expr, $field:expr) => {
182 if let Ok(v) = std::env::var($var) {
183 $field = Some(v);
184 }
185 };
186 }
187 macro_rules! env_parse {
188 ($var:expr, $field:expr) => {
189 if let Ok(v) = std::env::var($var) {
190 $field = v
191 .parse()
192 .map_err(|e| AppError::Config(format!("invalid {}: {e}", $var)))?;
193 }
194 };
195 }
196
197 env_opt!("DID_HOSTING_SERVER_DID", config.server_did);
198 env_opt!("DID_HOSTING_MEDIATOR_DID", config.mediator_did);
199 env_opt!("DID_HOSTING_PUBLIC_URL", config.public_url);
200 env_opt!("DID_HOSTING_CONTROL_URL", config.control_url);
201 env_opt!("DID_HOSTING_CONTROL_DID", config.control_did);
202
203 env_opt!("DID_HOSTING_VTA_URL", config.vta.url);
205 env_opt!("DID_HOSTING_VTA_DID", config.vta.did);
206 env_opt!("DID_HOSTING_VTA_CONTEXT_ID", config.vta.context_id);
207
208 env_parse!(
210 "DID_HOSTING_LIMITS_UPLOAD_BODY_LIMIT",
211 config.limits.upload_body_limit
212 );
213 env_parse!(
214 "DID_HOSTING_LIMITS_DEFAULT_MAX_TOTAL_SIZE",
215 config.limits.default_max_total_size
216 );
217 env_parse!(
218 "DID_HOSTING_LIMITS_DEFAULT_MAX_DID_COUNT",
219 config.limits.default_max_did_count
220 );
221
222 env_parse!(
224 "DID_HOSTING_STATS_FLUSH_INTERVAL_SECS",
225 config.stats.flush_interval_secs
226 );
227 env_parse!(
228 "DID_HOSTING_STATS_SYNC_INTERVAL_SECS",
229 config.stats.sync_interval_secs
230 );
231
232 config.auth.validate()?;
234 if let Some(ref did) = config.server_did
235 && !did.starts_with("did:")
236 {
237 return Err(AppError::Config(format!(
238 "server_did must start with 'did:': {did}"
239 )));
240 }
241 if let Some(ref url) = config.public_url
242 && !url.starts_with("http://")
243 && !url.starts_with("https://")
244 {
245 return Err(AppError::Config(format!(
246 "public_url must start with http:// or https://: {url}"
247 )));
248 }
249
250 if let Some(ref mut url) = config.public_url {
252 let trimmed = url.trim_end_matches('/').to_string();
253 *url = trimmed;
254 }
255 if let Some(ref mut url) = config.control_url {
256 let trimmed = url.trim_end_matches('/').to_string();
257 *url = trimmed;
258 }
259
260 Ok(config)
261 }
262}