Skip to main content

auths_cli/commands/
log.rs

1use std::time::Duration;
2
3use anyhow::{Context, Result, bail};
4use auths_infra_http::HttpRegistryClient;
5use auths_sdk::ports::RegistryClient;
6use auths_transparency::SignedCheckpoint;
7use clap::{Args, Subcommand};
8use serde::Serialize;
9
10use super::executable::ExecutableCommand;
11use crate::config::CliConfig;
12use crate::ux::format::{JsonResponse, is_json_mode};
13
14#[derive(Args, Debug, Clone)]
15#[command(about = "Inspect and verify the transparency log")]
16pub struct LogCommand {
17    #[command(subcommand)]
18    pub command: LogSubcommand,
19}
20
21#[derive(Subcommand, Debug, Clone)]
22pub enum LogSubcommand {
23    /// Fetch and display a log entry by sequence number
24    Inspect(InspectArgs),
25    /// Verify log consistency from the cached checkpoint
26    Verify(VerifyArgs),
27}
28
29#[derive(Args, Debug, Clone)]
30pub struct InspectArgs {
31    /// Sequence number of the entry to inspect
32    pub sequence: u128,
33
34    /// Registry URL to fetch from
35    #[clap(long, default_value = "https://public.auths.dev")]
36    pub registry: String,
37}
38
39#[derive(Args, Debug, Clone)]
40pub struct VerifyArgs {
41    /// Registry URL to verify against
42    #[clap(long, default_value = "https://public.auths.dev")]
43    pub registry: String,
44}
45
46#[derive(Serialize)]
47struct VerifyResult {
48    consistent: bool,
49    cached_size: u64,
50    latest_size: u64,
51    cached_root: String,
52    latest_root: String,
53}
54
55impl ExecutableCommand for LogCommand {
56    fn execute(&self, _ctx: &CliConfig) -> Result<()> {
57        let rt = tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
58        rt.block_on(async {
59            match &self.command {
60                LogSubcommand::Inspect(args) => handle_inspect(args).await,
61                LogSubcommand::Verify(args) => handle_verify(args).await,
62            }
63        })
64    }
65}
66
67async fn handle_inspect(args: &InspectArgs) -> Result<()> {
68    let registry_url = args.registry.trim_end_matches('/');
69    let client =
70        HttpRegistryClient::new_with_timeouts(Duration::from_secs(30), Duration::from_secs(60));
71
72    let path = format!("v1/log/entries/{}", args.sequence);
73    let response_bytes = client
74        .fetch_registry_data(registry_url, &path)
75        .await
76        .context("Failed to fetch log entry")?;
77
78    let entry: serde_json::Value =
79        serde_json::from_slice(&response_bytes).context("Failed to parse log entry response")?;
80
81    if is_json_mode() {
82        println!(
83            "{}",
84            serde_json::to_string_pretty(&entry).context("Failed to serialize entry")?
85        );
86    } else {
87        let entry_type = entry
88            .get("content")
89            .and_then(|c| c.get("entry_type"))
90            .and_then(|t| t.as_str())
91            .unwrap_or("unknown");
92        let actor = entry
93            .get("content")
94            .and_then(|c| c.get("actor_did"))
95            .and_then(|a| a.as_str())
96            .unwrap_or("unknown");
97        let timestamp = entry
98            .get("timestamp")
99            .and_then(|t| t.as_str())
100            .unwrap_or("unknown");
101        let sequence: u128 = entry
102            .get("sequence")
103            .and_then(|s| s.as_u64())
104            .map(u128::from)
105            .unwrap_or(args.sequence);
106
107        println!("Log Entry #{sequence}");
108        println!("  Type:      {entry_type}");
109        println!("  Actor:     {actor}");
110        println!("  Timestamp: {timestamp}");
111
112        if let Some(body) = entry.get("content").and_then(|c| c.get("body")) {
113            println!(
114                "  Body:      {}",
115                serde_json::to_string_pretty(body).unwrap_or_default()
116            );
117        }
118    }
119
120    Ok(())
121}
122
123#[allow(clippy::disallowed_methods)] // CLI is the presentation boundary
124async fn handle_verify(args: &VerifyArgs) -> Result<()> {
125    let cache_path = dirs::home_dir()
126        .map(|h| h.join(".auths").join("log_checkpoint.json"))
127        .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
128
129    let cached_checkpoint: SignedCheckpoint = match std::fs::read_to_string(&cache_path) {
130        Ok(json) => serde_json::from_str(&json).context("Failed to parse cached checkpoint")?,
131        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
132            if is_json_mode() {
133                let result = VerifyResult {
134                    consistent: false,
135                    cached_size: 0,
136                    latest_size: 0,
137                    cached_root: String::new(),
138                    latest_root: String::new(),
139                };
140                JsonResponse::success("log verify", result).print()?;
141            } else {
142                println!("No cached checkpoint found at {}", cache_path.display());
143                println!("Run 'auths artifact verify' with a bundle to establish initial trust.");
144            }
145            return Ok(());
146        }
147        Err(e) => return Err(e).context("Failed to read cached checkpoint"),
148    };
149
150    let registry_url = args.registry.trim_end_matches('/');
151    let client =
152        HttpRegistryClient::new_with_timeouts(Duration::from_secs(30), Duration::from_secs(60));
153
154    let response_bytes = client
155        .fetch_registry_data(registry_url, "v1/log/checkpoint")
156        .await
157        .context("Failed to fetch latest checkpoint from registry")?;
158
159    let latest_checkpoint: SignedCheckpoint =
160        serde_json::from_slice(&response_bytes).context("Failed to parse latest checkpoint")?;
161
162    let report = auths_sdk::workflows::transparency::try_cache_checkpoint(
163        &cache_path,
164        &latest_checkpoint,
165        None,
166    );
167
168    match report {
169        Ok(consistency) => {
170            if is_json_mode() {
171                let result = VerifyResult {
172                    consistent: consistency.consistent,
173                    cached_size: consistency.old_size,
174                    latest_size: consistency.new_size,
175                    cached_root: hex::encode(cached_checkpoint.checkpoint.root.as_bytes()),
176                    latest_root: hex::encode(latest_checkpoint.checkpoint.root.as_bytes()),
177                };
178                JsonResponse::success("log verify", result).print()?;
179            } else {
180                println!("Log Consistency: verified");
181                println!(
182                    "  Cached:  size={}, root={}",
183                    consistency.old_size,
184                    hex::encode(cached_checkpoint.checkpoint.root.as_bytes())
185                );
186                println!(
187                    "  Latest:  size={}, root={}",
188                    consistency.new_size,
189                    hex::encode(latest_checkpoint.checkpoint.root.as_bytes())
190                );
191                println!("  Checkpoint updated.");
192            }
193        }
194        Err(e) => {
195            if is_json_mode() {
196                let result = VerifyResult {
197                    consistent: false,
198                    cached_size: cached_checkpoint.checkpoint.size,
199                    latest_size: latest_checkpoint.checkpoint.size,
200                    cached_root: hex::encode(cached_checkpoint.checkpoint.root.as_bytes()),
201                    latest_root: hex::encode(latest_checkpoint.checkpoint.root.as_bytes()),
202                };
203                let resp: JsonResponse<VerifyResult> = JsonResponse {
204                    success: false,
205                    command: "log verify".into(),
206                    data: Some(result),
207                    error: Some(e.to_string()),
208                };
209                println!(
210                    "{}",
211                    serde_json::to_string(&resp).context("Failed to serialize error response")?
212                );
213            } else {
214                eprintln!("Log Consistency: FAILED");
215                eprintln!("  Error: {e}");
216                eprintln!("  This may indicate a split-view attack.");
217            }
218            bail!("Log consistency verification failed: {e}");
219        }
220    }
221
222    Ok(())
223}