1use std::net::SocketAddr;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Result, anyhow};
7use auths_utils::path::expand_tilde;
8use clap::{Parser, Subcommand};
9
10use auths_infra_http::HttpAsyncWitnessClient;
11use auths_sdk::ports::IdentityStorage;
12use auths_sdk::storage::RegistryIdentityStorage;
13use auths_sdk::witness::AsyncWitnessProvider;
14use auths_sdk::witness::{
15 EquivocationDetection, IndependencePolicy, WitnessConfig, WitnessRef, honesty_ceiling,
16};
17use auths_sdk::witness::{
18 WitnessServerConfig, WitnessServerState, generate_and_persist_witness_signer,
19 load_witness_signer, run_server, witness_signer_from_seed_hex,
20};
21
22#[derive(Parser, Debug, Clone)]
24pub struct WitnessCommand {
25 #[command(subcommand)]
26 pub subcommand: WitnessSubcommand,
27}
28
29#[derive(Subcommand, Debug, Clone)]
31pub enum WitnessSubcommand {
32 #[command(visible_alias = "serve")]
34 Start {
35 #[clap(long, default_value = "127.0.0.1:3333")]
37 bind: SocketAddr,
38
39 #[clap(long, default_value = "witness.db")]
41 db_path: PathBuf,
42
43 #[clap(long, visible_alias = "id")]
48 identity: Option<PathBuf>,
49
50 #[clap(long)]
53 generate: bool,
54
55 #[clap(long, default_value = "p256")]
57 curve: String,
58 },
59
60 Add {
62 #[clap(long)]
64 url: String,
65 },
66
67 Remove {
69 #[clap(long)]
71 url: String,
72 },
73
74 List,
76}
77
78fn parse_curve_arg(curve: &str) -> Result<auths_crypto::CurveType> {
80 match curve {
81 "p256" | "P256" => Ok(auths_crypto::CurveType::P256),
82 "ed25519" | "Ed25519" => Ok(auths_crypto::CurveType::Ed25519),
83 other => Err(anyhow!(
84 "unknown --curve '{other}'; expected 'p256' or 'ed25519'"
85 )),
86 }
87}
88
89fn build_witness_config(
96 db_path: PathBuf,
97 identity: Option<PathBuf>,
98 generate: bool,
99 curve: auths_crypto::CurveType,
100) -> Result<WitnessServerConfig> {
101 #[allow(clippy::disallowed_methods)]
102 let env_seed = std::env::var("AUTHS_WITNESS_SEED").ok();
105
106 if let Some(seed_hex) = env_seed {
107 let signer = witness_signer_from_seed_hex(curve, &seed_hex)
108 .map_err(|e| anyhow!("invalid AUTHS_WITNESS_SEED: {e}"))?;
109 return WitnessServerConfig::from_signer(db_path, signer)
110 .map_err(|e| anyhow!("witness config from injected seed: {e}"));
111 }
112
113 if let Some(identity_path) = identity {
114 let path =
115 expand_tilde(&identity_path).map_err(|e| anyhow!("invalid --identity path: {e}"))?;
116 let signer = if path.exists() {
117 load_witness_signer(&path).map_err(|e| anyhow!("{e}"))?
118 } else if generate {
119 let signer =
120 generate_and_persist_witness_signer(&path, curve).map_err(|e| anyhow!("{e}"))?;
121 println!("Generated new witness identity at {}", path.display());
122 signer
123 } else {
124 return Err(anyhow!(
125 "no witness identity at {}; pass --generate to create one \
126 (refusing to mint an ephemeral key for a --identity path)",
127 path.display()
128 ));
129 };
130 return WitnessServerConfig::from_signer(db_path, signer)
131 .map_err(|e| anyhow!("witness config: {e}"));
132 }
133
134 eprintln!(
135 "warning: starting with an EPHEMERAL witness identity (new AID each launch, \
136 not pinnable); pass --identity <path> --generate for a stable identity"
137 );
138 WitnessServerConfig::with_generated_keypair(db_path, curve)
139 .map_err(|e| anyhow!("Failed to generate witness keypair: {e}"))
140}
141
142pub fn handle_witness(cmd: WitnessCommand, repo_opt: Option<PathBuf>) -> Result<()> {
144 match cmd.subcommand {
145 WitnessSubcommand::Start {
146 bind,
147 db_path,
148 identity,
149 generate,
150 curve,
151 } => {
152 let curve = parse_curve_arg(&curve)?;
153 let cfg = build_witness_config(db_path, identity, generate, curve)?;
154 let rt = tokio::runtime::Runtime::new()?;
155 rt.block_on(async {
156 let state = WitnessServerState::new(cfg)
157 .map_err(|e| anyhow::anyhow!("Failed to create witness state: {}", e))?;
158
159 println!(
160 "Witness server started at {} (identity: {})",
161 bind,
162 state.witness_did()
163 );
164
165 run_server(state, bind)
166 .await
167 .map_err(|e| anyhow::anyhow!("Server error: {}", e))?;
168
169 Ok(())
170 })
171 }
172
173 WitnessSubcommand::Add { url } => {
174 let repo_path = resolve_repo_path(repo_opt)?;
175 let parsed_url: url::Url = url
176 .parse()
177 .map_err(|e| anyhow!("Invalid witness URL '{}': {}", url, e))?;
178 let mut config = load_witness_config(&repo_path)?;
179 let rt = tokio::runtime::Runtime::new()?;
184 let aid = rt
185 .block_on(async {
186 let client = HttpAsyncWitnessClient::new(
187 parsed_url.to_string(),
188 config.threshold.max(1),
189 );
190 client.witness_aid().await
191 })
192 .map_err(|e| {
193 anyhow!(
194 "Could not resolve witness identity from {}/health: {}",
195 parsed_url,
196 e
197 )
198 })?;
199 if !config.pin(WitnessRef {
200 url: parsed_url.clone(),
201 aid: aid.clone(),
202 operator_info: None,
206 }) {
207 println!("Witness already configured (aid {}): {}", aid.as_str(), url);
208 return Ok(());
209 }
210 if config.threshold == 0 {
211 config.threshold = 1;
212 }
213 save_witness_config(&repo_path, &config)?;
214 println!("Added witness: {} (aid {})", url, aid.as_str());
215 println!(
216 " Witnesses: {}, required: {}",
217 config.witnesses.len(),
218 config.threshold
219 );
220 Ok(())
221 }
222
223 WitnessSubcommand::Remove { url } => {
224 let repo_path = resolve_repo_path(repo_opt)?;
225 let parsed_url: url::Url = url
226 .parse()
227 .map_err(|e| anyhow!("Invalid witness URL '{}': {}", url, e))?;
228 let mut config = load_witness_config(&repo_path)?;
229 if !config.remove_url(&parsed_url) {
230 println!("Witness not found: {}", url);
231 return Ok(());
232 }
233 if config.threshold > config.witnesses.len() {
235 config.threshold = config.witnesses.len();
236 }
237 save_witness_config(&repo_path, &config)?;
238 println!("Removed witness: {}", url);
239 println!(
240 " Remaining witnesses: {}, required: {}",
241 config.witnesses.len(),
242 config.threshold
243 );
244 Ok(())
245 }
246
247 WitnessSubcommand::List => {
248 let repo_path = resolve_repo_path(repo_opt)?;
249 let config = load_witness_config(&repo_path)?;
250 if config.witnesses.is_empty() {
251 println!("No witnesses configured.");
252 return Ok(());
253 }
254 println!("Configured witnesses:");
255 for (i, w) in config.witnesses.iter().enumerate() {
256 println!(" {}. {} (aid {})", i + 1, w.url, w.aid.as_str());
257 }
258 println!(
259 "\nRequired: {}/{} (policy: {:?})",
260 config.threshold,
261 config.witnesses.len(),
262 config.policy
263 );
264
265 let independence = config.roster_independence(&IndependencePolicy::default());
268 let ceiling = honesty_ceiling(&independence, EquivocationDetection::Sampled);
269 let status = if ceiling.policy_met { "MET" } else { "FAILING" };
270 println!("\nIndependence: {status} — {}", ceiling.label);
271 if !ceiling.shortfalls.is_empty() {
272 println!(" shortfall: {}", ceiling.shortfalls.join(", "));
273 }
274 Ok(())
275 }
276 }
277}
278
279fn resolve_repo_path(repo_opt: Option<PathBuf>) -> Result<PathBuf> {
283 if let Some(path) = repo_opt {
284 return Ok(expand_tilde(&path)?);
285 }
286 let home = dirs::home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
287 Ok(home.join(".auths"))
288}
289
290fn load_witness_config(repo_path: &Path) -> Result<WitnessConfig> {
292 let storage = RegistryIdentityStorage::new(repo_path.to_path_buf());
293 let identity = storage.load_identity()?;
294
295 if let Some(ref metadata) = identity.metadata
296 && let Some(wc) = metadata.get("witness_config")
297 {
298 let config: WitnessConfig = serde_json::from_value(wc.clone())?;
299 return Ok(config);
300 }
301 Ok(WitnessConfig::default())
302}
303
304fn save_witness_config(repo_path: &Path, config: &WitnessConfig) -> Result<()> {
306 let storage = RegistryIdentityStorage::new(repo_path.to_path_buf());
307 let mut identity = storage.load_identity()?;
308
309 let metadata = identity
310 .metadata
311 .get_or_insert_with(|| serde_json::json!({}));
312 if let Some(obj) = metadata.as_object_mut() {
313 obj.insert("witness_config".to_string(), serde_json::to_value(config)?);
314 }
315
316 storage.create_identity(identity.controller_did.as_str(), identity.metadata)?;
317 Ok(())
318}
319
320impl crate::commands::executable::ExecutableCommand for WitnessCommand {
321 fn execute(&self, ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
322 handle_witness(self.clone(), ctx.repo_path.clone())
323 }
324}