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 Serve {
26 #[clap(long, default_value = "127.0.0.1:3333")]
28 bind: SocketAddr,
29
30 #[clap(long, default_value = "witness.db")]
32 db_path: PathBuf,
33
34 #[clap(long)]
36 witness_did: Option<String>,
37 },
38
39 Add {
41 #[clap(long)]
43 url: String,
44 },
45
46 Remove {
48 #[clap(long)]
50 url: String,
51 },
52
53 List,
55}
56
57pub 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 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
175fn 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
184fn 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
198fn 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}