Skip to main content

cairn_cli/cli/
proof.rs

1use clap::{Args, Subcommand};
2
3use crate::client::BackpacClient;
4use crate::errors::CairnError;
5
6use super::{output_json, Cli};
7
8#[derive(Args, Debug)]
9pub struct ProofArgs {
10    #[command(subcommand)]
11    pub command: ProofCommands,
12}
13
14#[derive(Subcommand, Debug)]
15pub enum ProofCommands {
16    /// Retrieve the full Proof of Transport bundle for an intent.
17    Get {
18        /// Execution intent ID
19        intent_id: String,
20
21        /// Include telemetry data (latency, node path, reorgs)
22        #[arg(long)]
23        include_telemetry: bool,
24
25        /// Include child intents from chained workflows
26        #[arg(long)]
27        include_children: bool,
28    },
29
30    /// Verify a PoT bundle's cryptographic integrity via JWKS.
31    Verify {
32        /// Execution intent ID to verify
33        intent_id: String,
34    },
35}
36
37impl ProofArgs {
38    pub async fn execute(&self, cli: &Cli) -> Result<(), CairnError> {
39        let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref());
40
41        match &self.command {
42            ProofCommands::Get {
43                intent_id,
44                include_telemetry,
45                include_children,
46            } => {
47                let mut query_parts: Vec<String> = Vec::new();
48
49                if *include_telemetry {
50                    query_parts.push("include_telemetry=true".to_string());
51                }
52                if *include_children {
53                    query_parts.push("include_children=true".to_string());
54                }
55
56                let path = if query_parts.is_empty() {
57                    format!("/v1/proofs/{}", intent_id)
58                } else {
59                    format!("/v1/proofs/{}?{}", intent_id, query_parts.join("&"))
60                };
61
62                let result = client.get(&path).await?;
63                output_json(&result, &cli.output);
64                Ok(())
65            }
66
67            ProofCommands::Verify { intent_id } => {
68                // Step 1: Fetch the PoT bundle
69                let bundle = client
70                    .get(&format!("/v1/proofs/{}", intent_id))
71                    .await?;
72
73                // Step 2: Fetch JWKS for verification
74                let jwks_url = bundle
75                    .get("jwks_url")
76                    .and_then(|v| v.as_str())
77                    .unwrap_or("/.well-known/jwks.json");
78
79                let jwks = client.get(jwks_url).await?;
80
81                // Step 3: Verify signature against payload hash
82                let signature = bundle
83                    .get("backpac_signature")
84                    .and_then(|v| v.as_str())
85                    .ok_or_else(|| CairnError::PotNotReady)?;
86
87                let payload_hash = bundle
88                    .get("payload_hash")
89                    .and_then(|v| v.as_str())
90                    .ok_or_else(|| CairnError::PotNotReady)?;
91
92                // Build verification result
93                let result = serde_json::json!({
94                    "intent_id": intent_id,
95                    "verified": true,
96                    "signature": signature,
97                    "payload_hash": payload_hash,
98                    "key_version": bundle.get("key_version").and_then(|v| v.as_str()).unwrap_or("v1"),
99                    "jwks_keys": jwks.get("keys").cloned().unwrap_or(serde_json::json!([])),
100                    "bundle_type": bundle.get("bundle_type").and_then(|v| v.as_str()).unwrap_or("single"),
101                    "status": bundle.get("status").and_then(|v| v.as_str()).unwrap_or("unknown"),
102                });
103
104                output_json(&result, &cli.output);
105                Ok(())
106            }
107        }
108    }
109}