auths_cli/commands/
witness.rs1use 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#[derive(Parser, Debug, Clone)]
16pub struct WitnessCommand {
17 #[command(subcommand)]
18 pub subcommand: WitnessSubcommand,
19}
20
21#[derive(Subcommand, Debug, Clone)]
23pub enum WitnessSubcommand {
24 #[command(visible_alias = "serve")]
26 Start {
27 #[clap(long, default_value = "127.0.0.1:3333")]
29 bind: SocketAddr,
30
31 #[clap(long, default_value = "witness.db")]
33 db_path: PathBuf,
34
35 #[clap(long, visible_alias = "witness")]
37 witness_did: Option<String>,
38 },
39
40 Add {
42 #[clap(long)]
44 url: String,
45 },
46
47 Remove {
49 #[clap(long)]
51 url: String,
52 },
53
54 List,
56}
57
58pub 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 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
176fn 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
185fn 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
199fn 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}