Skip to main content

volt_client_grpc/
config.rs

1//! Configuration types for the Volt client.
2
3use crate::constants::DEFAULT_REGISTRY_LIST;
4use crate::error::{Result, VoltError};
5use serde::{Deserialize, Serialize};
6use std::path::Path;
7
8/// Configuration for a Volt server
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
10pub struct VoltConfig {
11    /// Unique identifier for the Volt (typically a DID)
12    pub id: String,
13
14    /// Address of the Volt server (host:port)
15    #[serde(default)]
16    pub address: Option<String>,
17
18    /// CA certificate in PEM format
19    #[serde(default)]
20    pub ca_pem: Option<String>,
21
22    /// Public key of the Volt in PEM format
23    #[serde(default)]
24    pub public_key: Option<String>,
25
26    /// Relay configuration for connecting via a relay
27    #[serde(default)]
28    pub relay: Option<RelayConfig>,
29}
30
31/// Configuration for connecting via a relay
32#[derive(Debug, Clone, Serialize, Deserialize, Default)]
33pub struct RelayConfig {
34    /// Address of the relay server
35    pub address: String,
36
37    /// CA certificate for the relay in PEM format
38    #[serde(default)]
39    pub ca_pem: Option<String>,
40
41    /// Whether this is a cloud relay
42    #[serde(default)]
43    pub cloud: bool,
44}
45
46/// Full client configuration
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct VoltClientConfig {
49    /// Name of this client
50    pub client_name: String,
51
52    /// Volt server configuration
53    pub volt: VoltConfig,
54
55    /// Cached credentials
56    #[serde(default)]
57    pub credential: Option<CredentialCache>,
58
59    /// Auto-reconnect on disconnection
60    #[serde(default = "default_true")]
61    pub auto_reconnect: bool,
62
63    /// Ping interval in milliseconds
64    #[serde(default = "default_ping_interval")]
65    pub ping_interval: u64,
66
67    /// Reconnect interval in milliseconds
68    #[serde(default = "default_reconnect_interval")]
69    pub reconnect_interval: u64,
70
71    /// Timeout interval in milliseconds
72    #[serde(default = "default_timeout_interval")]
73    pub timeout_interval: u64,
74
75    /// TTL for bind requests
76    #[serde(default)]
77    pub bind_request_ttl: Option<u64>,
78
79    /// Passphrase for encrypted keys
80    #[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/// Cached credential information
114#[derive(Debug, Clone, Serialize, Deserialize, Default)]
115pub struct CredentialCache {
116    /// Private key in PEM format
117    #[serde(default)]
118    pub key: Option<String>,
119
120    /// Certificate issued by the Volt
121    #[serde(default)]
122    pub cert: Option<String>,
123
124    /// CA certificate chain
125    #[serde(default)]
126    pub ca: Option<String>,
127
128    /// Session ID assigned by the Volt
129    #[serde(default)]
130    pub session_id: Option<String>,
131
132    /// Identity DID
133    #[serde(default)]
134    pub identity_did: Option<String>,
135
136    /// Challenge code for authentication
137    #[serde(default)]
138    pub challenge_code: Option<String>,
139
140    /// IP address to bind to
141    #[serde(default)]
142    pub bind_ip: Option<String>,
143
144    /// Verifiable credentials
145    #[serde(default)]
146    pub vc: Vec<serde_json::Value>,
147}
148
149/// Options for initializing the Volt client
150#[derive(Debug, Clone, Default)]
151pub struct InitialiseOptions {
152    /// List of DID registry URLs for resolving DIDs
153    pub did_registry_list: Option<Vec<String>>,
154
155    /// Additional configuration to merge
156    pub extra_config: Option<serde_json::Value>,
157
158    /// Whether the Volt should manage the DID
159    pub own_did: bool,
160
161    /// Whether to create a key-based session instead of DID-based
162    pub no_did: bool,
163
164    /// Optional token to exchange for an authenticated session
165    pub exchange_token: Option<String>,
166
167    /// Whether to block and poll on 'prompt' decisions
168    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    /// Load configuration from a JSON file
207    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    /// Load configuration from a JSON string
214    pub fn from_str(s: &str) -> Result<Self> {
215        let config: Self = serde_json::from_str(s)?;
216        Ok(config)
217    }
218
219    /// Create configuration from a JSON object
220    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    /// Save configuration to a file
226    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    /// Validate the configuration
233    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 not relayed, we need address and CA
243        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    /// Check if this connection is through a relay
256    pub fn is_relayed(&self) -> bool {
257        self.volt.relay.is_some() && !self.volt.id.is_empty()
258    }
259}
260
261/// Resolve a Volt configuration from a DID
262pub async fn resolve_volt_config(
263    volt_config: VoltConfig,
264    registry_list: Option<&[String]>,
265) -> Result<VoltConfig> {
266    // If we already have an address, no need to resolve
267    if volt_config.address.is_some() && volt_config.ca_pem.is_some() {
268        return Ok(volt_config);
269    }
270
271    // If the ID looks like a DID, try to resolve it
272    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, &registry).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
299/// Resolve a DID to get Volt configuration
300async 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    // Extract Volt configuration from DID document
316    // This depends on the structure of your DID documents
317    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    // Extract service endpoints from DID document
345    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    // Find volt-discovery service
351    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}