1use crate::constants::DEFAULT_REGISTRY_LIST;
4use crate::error::{Result, VoltError};
5use serde::{Deserialize, Serialize};
6use std::path::Path;
7
8#[derive(Debug, Clone, Serialize, Deserialize, Default)]
10pub struct VoltConfig {
11 pub id: String,
13
14 #[serde(default)]
16 pub address: Option<String>,
17
18 #[serde(default)]
20 pub ca_pem: Option<String>,
21
22 #[serde(default)]
24 pub public_key: Option<String>,
25
26 #[serde(default)]
28 pub relay: Option<RelayConfig>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, Default)]
33pub struct RelayConfig {
34 pub address: String,
36
37 #[serde(default)]
39 pub ca_pem: Option<String>,
40
41 #[serde(default)]
43 pub cloud: bool,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct VoltClientConfig {
49 pub client_name: String,
51
52 pub volt: VoltConfig,
54
55 #[serde(default)]
57 pub credential: Option<CredentialCache>,
58
59 #[serde(default = "default_true")]
61 pub auto_reconnect: bool,
62
63 #[serde(default = "default_ping_interval")]
65 pub ping_interval: u64,
66
67 #[serde(default = "default_reconnect_interval")]
69 pub reconnect_interval: u64,
70
71 #[serde(default = "default_timeout_interval")]
73 pub timeout_interval: u64,
74
75 #[serde(default)]
77 pub bind_request_ttl: Option<u64>,
78
79 #[serde(skip)]
81 pub passphrase: Option<String>,
82}
83
84fn default_true() -> bool {
85 true
86}
87fn default_ping_interval() -> u64 {
88 10000
89}
90fn default_reconnect_interval() -> u64 {
91 5000
92}
93fn default_timeout_interval() -> u64 {
94 60000
95}
96
97impl Default for VoltClientConfig {
98 fn default() -> Self {
99 Self {
100 client_name: String::new(),
101 volt: VoltConfig::default(),
102 credential: None,
103 auto_reconnect: true,
104 ping_interval: 10000,
105 reconnect_interval: 5000,
106 timeout_interval: 60000,
107 bind_request_ttl: None,
108 passphrase: None,
109 }
110 }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, Default)]
115pub struct CredentialCache {
116 #[serde(default)]
118 pub key: Option<String>,
119
120 #[serde(default)]
122 pub cert: Option<String>,
123
124 #[serde(default)]
126 pub ca: Option<String>,
127
128 #[serde(default)]
130 pub session_id: Option<String>,
131
132 #[serde(default)]
134 pub identity_did: Option<String>,
135
136 #[serde(default)]
138 pub challenge_code: Option<String>,
139
140 #[serde(default)]
142 pub bind_ip: Option<String>,
143
144 #[serde(default)]
146 pub vc: Vec<serde_json::Value>,
147}
148
149#[derive(Debug, Clone, Default)]
151pub struct InitialiseOptions {
152 pub did_registry_list: Option<Vec<String>>,
154
155 pub extra_config: Option<serde_json::Value>,
157
158 pub own_did: bool,
160
161 pub no_did: bool,
163
164 pub exchange_token: Option<String>,
166
167 pub wait_for_auth: bool,
169}
170
171impl InitialiseOptions {
172 pub fn new() -> Self {
173 Self {
174 wait_for_auth: true,
175 ..Default::default()
176 }
177 }
178
179 pub fn with_did_registries(mut self, registries: Vec<String>) -> Self {
180 self.did_registry_list = Some(registries);
181 self
182 }
183
184 pub fn with_exchange_token(mut self, token: String) -> Self {
185 self.exchange_token = Some(token);
186 self
187 }
188
189 pub fn with_no_did(mut self) -> Self {
190 self.no_did = true;
191 self
192 }
193
194 pub fn with_own_did(mut self) -> Self {
195 self.own_did = true;
196 self
197 }
198
199 pub fn without_waiting(mut self) -> Self {
200 self.wait_for_auth = false;
201 self
202 }
203}
204
205impl VoltClientConfig {
206 pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
208 let contents = tokio::fs::read_to_string(path).await?;
209 let config: Self = serde_json::from_str(&contents)?;
210 Ok(config)
211 }
212
213 pub fn from_str(s: &str) -> Result<Self> {
215 let config: Self = serde_json::from_str(s)?;
216 Ok(config)
217 }
218
219 pub fn from_value(value: serde_json::Value) -> Result<Self> {
221 let config: Self = serde_json::from_value(value)?;
222 Ok(config)
223 }
224
225 pub async fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
227 let contents = serde_json::to_string_pretty(self)?;
228 tokio::fs::write(path, contents).await?;
229 Ok(())
230 }
231
232 pub fn validate(&self) -> Result<()> {
234 if self.client_name.is_empty() {
235 return Err(VoltError::missing_config("client_name"));
236 }
237
238 if self.volt.id.is_empty() {
239 return Err(VoltError::missing_config("volt.id"));
240 }
241
242 if self.volt.relay.is_none() {
244 if self.volt.address.is_none() {
245 return Err(VoltError::missing_config("volt.address"));
246 }
247 if self.volt.ca_pem.is_none() {
248 return Err(VoltError::missing_config("volt.ca_pem"));
249 }
250 }
251
252 Ok(())
253 }
254
255 pub fn is_relayed(&self) -> bool {
257 self.volt.relay.is_some() && !self.volt.id.is_empty()
258 }
259}
260
261pub async fn resolve_volt_config(
263 volt_config: VoltConfig,
264 registry_list: Option<&[String]>,
265) -> Result<VoltConfig> {
266 if volt_config.address.is_some() && volt_config.ca_pem.is_some() {
268 return Ok(volt_config);
269 }
270
271 if volt_config.id.starts_with("did:volt:") {
273 let registries = registry_list.map(|r| r.to_vec()).unwrap_or_else(|| {
274 DEFAULT_REGISTRY_LIST
275 .iter()
276 .map(|s| s.to_string())
277 .collect()
278 });
279
280 for registry in registries {
281 match resolve_did(&volt_config.id, ®istry).await {
282 Ok(resolved) => return Ok(resolved),
283 Err(e) => {
284 tracing::debug!("Failed to resolve DID from {}: {}", registry, e);
285 continue;
286 }
287 }
288 }
289
290 return Err(VoltError::DidResolutionError(format!(
291 "Could not resolve DID: {}",
292 volt_config.id
293 )));
294 }
295
296 Ok(volt_config)
297}
298
299async fn resolve_did(did: &str, registry_url: &str) -> Result<VoltConfig> {
301 let url = get_did_resolution_url(did, registry_url)?;
302
303 let client = reqwest::Client::new();
304 let response = client.get(&url).send().await?;
305
306 if !response.status().is_success() {
307 return Err(VoltError::DidResolutionError(format!(
308 "DID resolution returned status: {}",
309 response.status()
310 )));
311 }
312
313 let did_document: serde_json::Value = response.json().await?;
314
315 let config = extract_volt_config_from_did(&did_document)?;
318
319 Ok(config)
320}
321
322fn get_did_resolution_url(did: &str, registry_url: &str) -> Result<String> {
323 let did_lower = did.to_lowercase();
324 let did_parts: Vec<&str> = did_lower.split(':').collect();
325
326 if did_parts.len() < 3 {
327 return Err(VoltError::InvalidArgument("Invalid DID format".into()));
328 }
329
330 if did_parts[0] != "did" || did_parts[1] != "volt" {
331 return Err(VoltError::InvalidArgument("Not a Volt DID".into()));
332 }
333
334 let lookup_url = if registry_url.starts_with("http") {
335 registry_url.to_string()
336 } else {
337 format!("https://{}", registry_url)
338 };
339
340 Ok(format!("{}/did/{}", lookup_url, did))
341}
342
343fn extract_volt_config_from_did(did_document: &serde_json::Value) -> Result<VoltConfig> {
344 let services = did_document
346 .get("service")
347 .and_then(|s| s.as_array())
348 .ok_or_else(|| VoltError::DidResolutionError("No services in DID document".into()))?;
349
350 let volt_service = services
352 .iter()
353 .find(|s| s.get("type").and_then(|t| t.as_str()) == Some("volt-discovery"))
354 .ok_or_else(|| VoltError::DidResolutionError("No volt-discovery service found".into()))?;
355
356 let id = did_document
357 .get("id")
358 .and_then(|id| id.as_str())
359 .ok_or_else(|| VoltError::DidResolutionError("No id in DID document".into()))?
360 .to_string();
361
362 let endpoint = volt_service
363 .get("serviceEndpoint")
364 .ok_or_else(|| VoltError::DidResolutionError("No serviceEndpoint".into()))?;
365
366 let address = endpoint
367 .get("address")
368 .and_then(|a| a.as_str())
369 .map(|s| s.to_string());
370
371 let ca_pem = endpoint
372 .get("ca_pem")
373 .and_then(|c| c.as_str())
374 .map(|s| s.to_string());
375
376 let public_key = endpoint
377 .get("public_key")
378 .and_then(|p| p.as_str())
379 .map(|s| s.to_string());
380
381 Ok(VoltConfig {
382 id,
383 address,
384 ca_pem,
385 public_key,
386 relay: None,
387 })
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_config_validation() {
396 let mut config = VoltClientConfig::default();
397 assert!(config.validate().is_err());
398
399 config.client_name = "test-client".to_string();
400 assert!(config.validate().is_err());
401
402 config.volt.id = "did:volt:test".to_string();
403 config.volt.address = Some("localhost:8080".to_string());
404 config.volt.ca_pem = Some("-----BEGIN CERTIFICATE-----".to_string());
405 assert!(config.validate().is_ok());
406 }
407
408 #[test]
409 fn test_is_relayed() {
410 let mut config = VoltClientConfig::default();
411 config.volt.id = "did:volt:test".to_string();
412 assert!(!config.is_relayed());
413
414 config.volt.relay = Some(RelayConfig {
415 address: "relay.example.com:443".to_string(),
416 ca_pem: None,
417 cloud: false,
418 });
419 assert!(config.is_relayed());
420 }
421}