Skip to main content

auths_cli/commands/
witness.rs

1//! Witness server and client management commands.
2
3use std::net::SocketAddr;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Result, anyhow};
7use clap::{Parser, Subcommand};
8
9use auths_core::witness::{WitnessServerConfig, WitnessServerState, run_server};
10use auths_id::storage::identity::IdentityStorage;
11use auths_id::witness_config::WitnessConfig;
12use auths_storage::git::RegistryIdentityStorage;
13
14/// Manage the KERI witness server.
15#[derive(Parser, Debug, Clone)]
16pub struct WitnessCommand {
17    #[command(subcommand)]
18    pub subcommand: WitnessSubcommand,
19}
20
21/// Witness subcommands.
22#[derive(Subcommand, Debug, Clone)]
23pub enum WitnessSubcommand {
24    /// Start the witness HTTP server.
25    Serve {
26        /// Address to bind to (e.g., "127.0.0.1:3333").
27        #[clap(long, default_value = "127.0.0.1:3333")]
28        bind: SocketAddr,
29
30        /// Path to the SQLite database for witness storage.
31        #[clap(long, default_value = "witness.db")]
32        db_path: PathBuf,
33
34        /// Witness DID (auto-generated if not provided).
35        #[clap(long)]
36        witness_did: Option<String>,
37    },
38
39    /// Add a witness URL to the identity configuration.
40    Add {
41        /// Witness server URL (e.g., "http://127.0.0.1:3333").
42        #[clap(long)]
43        url: String,
44    },
45
46    /// Remove a witness URL from the identity configuration.
47    Remove {
48        /// Witness server URL to remove.
49        #[clap(long)]
50        url: String,
51    },
52
53    /// List configured witnesses for the current identity.
54    List,
55}
56
57/// Handle witness commands.
58pub fn handle_witness(cmd: WitnessCommand, repo_opt: Option<PathBuf>) -> Result<()> {
59    match cmd.subcommand {
60        WitnessSubcommand::Serve {
61            bind,
62            db_path,
63            witness_did,
64        } => {
65            let rt = tokio::runtime::Runtime::new()?;
66            rt.block_on(async {
67                let state = {
68                    let (seed, pubkey) =
69                        auths_core::crypto::provider_bridge::generate_ed25519_keypair_sync()
70                            .map_err(|e| anyhow::anyhow!("Failed to generate keypair: {}", e))?;
71
72                    let witness_did = if let Some(did) = witness_did {
73                        did
74                    } else {
75                        format!("did:key:z6Mk{}", hex::encode(&pubkey[..16]))
76                    };
77
78                    WitnessServerState::new(WitnessServerConfig {
79                        witness_did,
80                        keypair_seed: seed,
81                        keypair_pubkey: pubkey,
82                        db_path,
83                        tls_cert_path: None,
84                        tls_key_path: None,
85                    })
86                    .map_err(|e| anyhow::anyhow!("Failed to create witness state: {}", e))?
87                };
88
89                println!(
90                    "Witness server starting on {} (DID: {})",
91                    bind,
92                    state.witness_did()
93                );
94
95                run_server(state, bind)
96                    .await
97                    .map_err(|e| anyhow::anyhow!("Server error: {}", e))?;
98
99                Ok(())
100            })
101        }
102
103        WitnessSubcommand::Add { url } => {
104            let repo_path = resolve_repo_path(repo_opt)?;
105            let parsed_url: url::Url = url
106                .parse()
107                .map_err(|e| anyhow!("Invalid witness URL '{}': {}", url, e))?;
108            let mut config = load_witness_config(&repo_path)?;
109            if config.witness_urls.contains(&parsed_url) {
110                println!("Witness already configured: {}", url);
111                return Ok(());
112            }
113            config.witness_urls.push(parsed_url);
114            if config.threshold == 0 {
115                config.threshold = 1;
116            }
117            save_witness_config(&repo_path, &config)?;
118            println!("Added witness: {}", url);
119            println!(
120                "  Total witnesses: {}, threshold: {}",
121                config.witness_urls.len(),
122                config.threshold
123            );
124            Ok(())
125        }
126
127        WitnessSubcommand::Remove { url } => {
128            let repo_path = resolve_repo_path(repo_opt)?;
129            let parsed_url: url::Url = url
130                .parse()
131                .map_err(|e| anyhow!("Invalid witness URL '{}': {}", url, e))?;
132            let mut config = load_witness_config(&repo_path)?;
133            let before = config.witness_urls.len();
134            config.witness_urls.retain(|u| u != &parsed_url);
135            if config.witness_urls.len() == before {
136                println!("Witness not found: {}", url);
137                return Ok(());
138            }
139            // Adjust threshold if needed
140            if config.threshold > config.witness_urls.len() {
141                config.threshold = config.witness_urls.len();
142            }
143            save_witness_config(&repo_path, &config)?;
144            println!("Removed witness: {}", url);
145            println!(
146                "  Remaining witnesses: {}, threshold: {}",
147                config.witness_urls.len(),
148                config.threshold
149            );
150            Ok(())
151        }
152
153        WitnessSubcommand::List => {
154            let repo_path = resolve_repo_path(repo_opt)?;
155            let config = load_witness_config(&repo_path)?;
156            if config.witness_urls.is_empty() {
157                println!("No witnesses configured.");
158                return Ok(());
159            }
160            println!("Configured witnesses:");
161            for (i, url) in config.witness_urls.iter().enumerate() {
162                println!("  {}. {}", i + 1, url);
163            }
164            println!(
165                "\nThreshold: {}/{} (policy: {:?})",
166                config.threshold,
167                config.witness_urls.len(),
168                config.policy
169            );
170            Ok(())
171        }
172    }
173}
174
175/// Resolve the identity repo path (defaults to ~/.auths).
176fn resolve_repo_path(repo_opt: Option<PathBuf>) -> Result<PathBuf> {
177    if let Some(path) = repo_opt {
178        return Ok(path);
179    }
180    let home = dirs::home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
181    Ok(home.join(".auths"))
182}
183
184/// Load witness config from identity metadata.
185fn load_witness_config(repo_path: &Path) -> Result<WitnessConfig> {
186    let storage = RegistryIdentityStorage::new(repo_path.to_path_buf());
187    let identity = storage.load_identity()?;
188
189    if let Some(ref metadata) = identity.metadata
190        && let Some(wc) = metadata.get("witness_config")
191    {
192        let config: WitnessConfig = serde_json::from_value(wc.clone())?;
193        return Ok(config);
194    }
195    Ok(WitnessConfig::default())
196}
197
198/// Save witness config into identity metadata.
199fn save_witness_config(repo_path: &Path, config: &WitnessConfig) -> Result<()> {
200    let storage = RegistryIdentityStorage::new(repo_path.to_path_buf());
201    let mut identity = storage.load_identity()?;
202
203    let metadata = identity
204        .metadata
205        .get_or_insert_with(|| serde_json::json!({}));
206    if let Some(obj) = metadata.as_object_mut() {
207        obj.insert("witness_config".to_string(), serde_json::to_value(config)?);
208    }
209
210    storage.create_identity(identity.controller_did.as_str(), identity.metadata)?;
211    Ok(())
212}
213
214impl crate::commands::executable::ExecutableCommand for WitnessCommand {
215    fn execute(&self, ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
216        handle_witness(self.clone(), ctx.repo_path.clone())
217    }
218}