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        &crate::adapters::config_store::FileConfigStore,
164        &cache_path,
165        &latest_checkpoint,
166        None,
167    );
168
169    match report {
170        Ok(consistency) => {
171            if is_json_mode() {
172                let result = VerifyResult {
173                    consistent: consistency.consistent,
174                    cached_size: consistency.old_size,
175                    latest_size: consistency.new_size,
176                    cached_root: hex::encode(cached_checkpoint.checkpoint.root.as_bytes()),
177                    latest_root: hex::encode(latest_checkpoint.checkpoint.root.as_bytes()),
178                };
179                JsonResponse::success("log verify", result).print()?;
180            } else {
181                println!("Log Consistency: verified");
182                println!(
183                    "  Cached:  size={}, root={}",
184                    consistency.old_size,
185                    hex::encode(cached_checkpoint.checkpoint.root.as_bytes())
186                );
187                println!(
188                    "  Latest:  size={}, root={}",
189                    consistency.new_size,
190                    hex::encode(latest_checkpoint.checkpoint.root.as_bytes())
191                );
192                println!("  Checkpoint updated.");
193            }
194        }
195        Err(e) => {
196            if is_json_mode() {
197                let result = VerifyResult {
198                    consistent: false,
199                    cached_size: cached_checkpoint.checkpoint.size,
200                    latest_size: latest_checkpoint.checkpoint.size,
201                    cached_root: hex::encode(cached_checkpoint.checkpoint.root.as_bytes()),
202                    latest_root: hex::encode(latest_checkpoint.checkpoint.root.as_bytes()),
203                };
204                let resp: JsonResponse<VerifyResult> = JsonResponse {
205                    success: false,
206                    command: "log verify".into(),
207                    data: Some(result),
208                    error: Some(e.to_string()),
209                };
210                println!(
211                    "{}",
212                    serde_json::to_string(&resp).context("Failed to serialize error response")?
213                );
214            } else {
215                eprintln!("Log Consistency: FAILED");
216                eprintln!("  Error: {e}");
217                eprintln!("  This may indicate a split-view attack.");
218            }
219            bail!("Log consistency verification failed: {e}");
220        }
221    }
222
223    Ok(())
224}