1use std::path::PathBuf;
2use std::time::Duration;
3
4use anyhow::{Context, Result, bail};
5use auths_infra_http::HttpRegistryClient;
6use auths_sdk::ports::RegistryClient;
7use auths_transparency::SignedCheckpoint;
8use clap::{Args, Subcommand};
9use serde::Serialize;
10
11use super::executable::ExecutableCommand;
12use crate::config::CliConfig;
13use crate::ux::format::{JsonResponse, is_json_mode};
14
15#[derive(Args, Debug, Clone)]
16#[command(about = "Inspect, verify, and operate the transparency log")]
17pub struct LogCommand {
18 #[command(subcommand)]
19 pub command: LogSubcommand,
20}
21
22#[derive(Subcommand, Debug, Clone)]
23pub enum LogSubcommand {
24 Inspect(InspectArgs),
26 Verify(VerifyArgs),
28 Append(AppendArgs),
30 Prove(ProveArgs),
32}
33
34#[derive(Args, Debug, Clone)]
35pub struct InspectArgs {
36 pub sequence: u128,
38
39 #[clap(long, default_value = "https://public.auths.dev")]
41 pub registry: String,
42}
43
44#[derive(Args, Debug, Clone)]
45pub struct VerifyArgs {
46 #[clap(long, default_value = "https://public.auths.dev")]
48 pub registry: String,
49}
50
51#[derive(Args, Debug, Clone)]
52pub struct AppendArgs {
53 #[clap(long)]
55 pub artifact: String,
56
57 #[clap(long)]
59 pub log_dir: PathBuf,
60
61 #[clap(long, default_value = "auths.local/log")]
63 pub origin: String,
64}
65
66#[derive(Args, Debug, Clone)]
67pub struct ProveArgs {
68 #[clap(long)]
70 pub artifact: String,
71
72 #[clap(long)]
74 pub log_dir: PathBuf,
75
76 #[clap(long, default_value = "auths.local/log")]
78 pub origin: String,
79
80 #[clap(long)]
82 pub out: Option<PathBuf>,
83}
84
85#[derive(Serialize)]
86struct VerifyResult {
87 consistent: bool,
88 cached_size: u64,
89 latest_size: u64,
90 cached_root: String,
91 latest_root: String,
92}
93
94#[derive(Serialize)]
95struct AppendResult {
96 artifact_digest: String,
97 leaf_hash: String,
98 index: u64,
99 size: u64,
100 root: String,
101 origin: String,
102}
103
104impl ExecutableCommand for LogCommand {
105 fn execute(&self, _ctx: &CliConfig) -> Result<()> {
106 let rt = tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
107 rt.block_on(async {
108 match &self.command {
109 LogSubcommand::Inspect(args) => handle_inspect(args).await,
110 LogSubcommand::Verify(args) => handle_verify(args).await,
111 LogSubcommand::Append(args) => handle_append(args).await,
112 LogSubcommand::Prove(args) => handle_prove(args).await,
113 }
114 })
115 }
116}
117
118async fn handle_inspect(args: &InspectArgs) -> Result<()> {
119 let registry_url = args.registry.trim_end_matches('/');
120 let client =
121 HttpRegistryClient::new_with_timeouts(Duration::from_secs(30), Duration::from_secs(60));
122
123 let path = format!("v1/log/entries/{}", args.sequence);
124 let response_bytes = client
125 .fetch_registry_data(registry_url, &path)
126 .await
127 .context("Failed to fetch log entry")?;
128
129 let entry: serde_json::Value =
130 serde_json::from_slice(&response_bytes).context("Failed to parse log entry response")?;
131
132 if is_json_mode() {
133 println!(
134 "{}",
135 serde_json::to_string_pretty(&entry).context("Failed to serialize entry")?
136 );
137 } else {
138 let entry_type = entry
139 .get("content")
140 .and_then(|c| c.get("entry_type"))
141 .and_then(|t| t.as_str())
142 .unwrap_or("unknown");
143 let actor = entry
144 .get("content")
145 .and_then(|c| c.get("actor_did"))
146 .and_then(|a| a.as_str())
147 .unwrap_or("unknown");
148 let timestamp = entry
149 .get("timestamp")
150 .and_then(|t| t.as_str())
151 .unwrap_or("unknown");
152 let sequence: u128 = entry
153 .get("sequence")
154 .and_then(|s| s.as_u64())
155 .map(u128::from)
156 .unwrap_or(args.sequence);
157
158 println!("Log Entry #{sequence}");
159 println!(" Type: {entry_type}");
160 println!(" Actor: {actor}");
161 println!(" Timestamp: {timestamp}");
162
163 if let Some(body) = entry.get("content").and_then(|c| c.get("body")) {
164 println!(
165 " Body: {}",
166 serde_json::to_string_pretty(body).unwrap_or_default()
167 );
168 }
169 }
170
171 Ok(())
172}
173
174#[allow(clippy::disallowed_methods)] async fn handle_verify(args: &VerifyArgs) -> Result<()> {
176 let cache_path = dirs::home_dir()
177 .map(|h| h.join(".auths").join("log_checkpoint.json"))
178 .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
179
180 let cached_checkpoint: SignedCheckpoint = match std::fs::read_to_string(&cache_path) {
181 Ok(json) => serde_json::from_str(&json).context("Failed to parse cached checkpoint")?,
182 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
183 if is_json_mode() {
184 let result = VerifyResult {
185 consistent: false,
186 cached_size: 0,
187 latest_size: 0,
188 cached_root: String::new(),
189 latest_root: String::new(),
190 };
191 JsonResponse::success("log verify", result).print()?;
192 } else {
193 println!("No cached checkpoint found at {}", cache_path.display());
194 println!("Run 'auths artifact verify' with a bundle to establish initial trust.");
195 }
196 return Ok(());
197 }
198 Err(e) => return Err(e).context("Failed to read cached checkpoint"),
199 };
200
201 let registry_url = args.registry.trim_end_matches('/');
202 let client =
203 HttpRegistryClient::new_with_timeouts(Duration::from_secs(30), Duration::from_secs(60));
204
205 let response_bytes = client
206 .fetch_registry_data(registry_url, "v1/log/checkpoint")
207 .await
208 .context("Failed to fetch latest checkpoint from registry")?;
209
210 let latest_checkpoint: SignedCheckpoint =
211 serde_json::from_slice(&response_bytes).context("Failed to parse latest checkpoint")?;
212
213 let report = auths_sdk::workflows::transparency::try_cache_checkpoint(
214 &crate::adapters::config_store::FileConfigStore,
215 &cache_path,
216 &latest_checkpoint,
217 None,
218 );
219
220 match report {
221 Ok(consistency) => {
222 if is_json_mode() {
223 let result = VerifyResult {
224 consistent: consistency.consistent,
225 cached_size: consistency.old_size,
226 latest_size: consistency.new_size,
227 cached_root: hex::encode(cached_checkpoint.checkpoint.root.as_bytes()),
228 latest_root: hex::encode(latest_checkpoint.checkpoint.root.as_bytes()),
229 };
230 JsonResponse::success("log verify", result).print()?;
231 } else {
232 println!("Log Consistency: verified");
233 println!(
234 " Cached: size={}, root={}",
235 consistency.old_size,
236 hex::encode(cached_checkpoint.checkpoint.root.as_bytes())
237 );
238 println!(
239 " Latest: size={}, root={}",
240 consistency.new_size,
241 hex::encode(latest_checkpoint.checkpoint.root.as_bytes())
242 );
243 println!(" Checkpoint updated.");
244 }
245 }
246 Err(e) => {
247 if is_json_mode() {
248 let result = VerifyResult {
249 consistent: false,
250 cached_size: cached_checkpoint.checkpoint.size,
251 latest_size: latest_checkpoint.checkpoint.size,
252 cached_root: hex::encode(cached_checkpoint.checkpoint.root.as_bytes()),
253 latest_root: hex::encode(latest_checkpoint.checkpoint.root.as_bytes()),
254 };
255 let resp: JsonResponse<VerifyResult> = JsonResponse {
256 success: false,
257 command: "log verify".into(),
258 data: Some(result),
259 error: Some(e.to_string()),
260 };
261 println!(
262 "{}",
263 serde_json::to_string(&resp).context("Failed to serialize error response")?
264 );
265 } else {
266 eprintln!("Log Consistency: FAILED");
267 eprintln!(" Error: {e}");
268 eprintln!(" This may indicate a split-view attack.");
269 }
270 bail!("Log consistency verification failed: {e}");
271 }
272 }
273
274 Ok(())
275}
276
277#[allow(clippy::disallowed_methods)] async fn handle_append(args: &AppendArgs) -> Result<()> {
279 let appended = auths_sdk::workflows::transparency::append_artifact_digest(
280 &args.log_dir,
281 &args.origin,
282 &args.artifact,
283 chrono::Utc::now(),
284 )
285 .await
286 .context("Failed to append artifact to transparency log")?;
287
288 let checkpoint = &appended.signed_checkpoint.checkpoint;
289 let result = AppendResult {
290 artifact_digest: appended.artifact_digest,
291 leaf_hash: hex::encode(appended.leaf_hash.as_bytes()),
292 index: appended.index,
293 size: checkpoint.size,
294 root: hex::encode(checkpoint.root.as_bytes()),
295 origin: checkpoint.origin.to_string(),
296 };
297
298 if is_json_mode() {
299 JsonResponse::success("log append", result).print()?;
300 } else {
301 println!("Appended to transparency log");
302 println!(" Artifact: {}", result.artifact_digest);
303 println!(" Leaf: {}", result.leaf_hash);
304 println!(" Index: {}", result.index);
305 println!(" Size: {}", result.size);
306 println!(" Root: {}", result.root);
307 println!(" Origin: {}", result.origin);
308 }
309 Ok(())
310}
311
312async fn handle_prove(args: &ProveArgs) -> Result<()> {
313 let inclusion = auths_sdk::workflows::transparency::prove_artifact_digest(
314 &args.log_dir,
315 &args.origin,
316 &args.artifact,
317 )
318 .await
319 .context("Failed to prove artifact inclusion")?;
320
321 if let Some(out) = &args.out {
322 let json = serde_json::to_string_pretty(&inclusion)
323 .context("Failed to serialize inclusion evidence")?;
324 std::fs::write(out, json).with_context(|| format!("Failed to write {}", out.display()))?;
325 if is_json_mode() {
326 JsonResponse::success("log prove", &inclusion).print()?;
327 } else {
328 println!("Inclusion evidence written");
329 println!(" Artifact: {}", args.artifact);
330 println!(" Index: {}", inclusion.inclusion_proof.index);
331 println!(" Size: {}", inclusion.inclusion_proof.size);
332 println!(" Out: {}", out.display());
333 }
334 } else if is_json_mode() {
335 JsonResponse::success("log prove", &inclusion).print()?;
336 } else {
337 println!(
338 "{}",
339 serde_json::to_string_pretty(&inclusion)
340 .context("Failed to serialize inclusion evidence")?
341 );
342 }
343 Ok(())
344}