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