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 #[command(alias = "fetch")]
18 Get {
19 id: String,
21
22 #[arg(long)]
24 from: Option<String>,
25
26 #[arg(long)]
28 recipient: Option<String>,
29
30 #[arg(long)]
32 include_telemetry: bool,
33
34 #[arg(long)]
36 include_children: bool,
37
38 #[arg(long)]
40 verify_signature: bool,
41
42 #[arg(long)]
44 raw: bool,
45 },
46
47 Verify {
49 id: String,
51 },
52}
53
54impl ProofArgs {
55 pub async fn execute(&self, cli: &Cli) -> Result<(), CairnError> {
56 let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref(), cli.worker_url.as_deref());
57
58 match &self.command {
59 ProofCommands::Get {
60 id,
61 from,
62 recipient,
63 include_telemetry,
64 include_children,
65 verify_signature,
66 raw,
67 } => {
68 let mut query_parts: Vec<String> = Vec::new();
69
70 if let Some(f) = from {
71 query_parts.push(format!("from={}", f));
72 }
73 if let Some(r) = recipient {
74 query_parts.push(format!("recipient={}", r));
75 }
76 if *include_telemetry {
77 query_parts.push("include_telemetry=true".to_string());
78 }
79 if *include_children {
80 query_parts.push("include_children=true".to_string());
81 }
82
83 let is_poi = id.starts_with("poi_");
84 let base_path = if is_poi { "v1/pois" } else { "v1/proofs" };
85
86 let path = if query_parts.is_empty() {
87 format!("/{}/{}", base_path, id)
88 } else {
89 format!("/{}/{}?{}", base_path, id, query_parts.join("&"))
90 };
91
92 let bundle = client.get(&path).await?;
93
94 if *verify_signature {
95 let result = self.verify_bundle(&client, id, &bundle).await?;
96 if *raw {
97 output_json(&bundle, &cli.output);
98 } else {
99 output_json(&result, &cli.output);
100 }
101 } else {
102 output_json(&bundle, &cli.output);
103 }
104 Ok(())
105 }
106
107 ProofCommands::Verify { id } => {
108 let is_poi = id.starts_with("poi_");
109 let base_path = if is_poi { "v1/pois" } else { "v1/proofs" };
110 let bundle = client.get(&format!("/{}/{}", base_path, id)).await?;
111 let result = self.verify_bundle(&client, id, &bundle).await?;
112 output_json(&result, &cli.output);
113 Ok(())
114 }
115 }
116 }
117
118 async fn verify_bundle(
119 &self,
120 client: &BackpacClient,
121 id: &str,
122 bundle: &serde_json::Value,
123 ) -> Result<serde_json::Value, CairnError> {
124 let jwks_url = bundle
126 .get("jwks_url")
127 .and_then(|v| v.as_str())
128 .unwrap_or("/.well-known/jwks.json");
129
130 let jwks = client.get(jwks_url).await?;
131
132 let signature = bundle
135 .get("backpac_signature")
136 .or_else(|| bundle.get("poi_signature"))
137 .and_then(|v| v.as_str())
138 .ok_or_else(|| CairnError::SignatureError("Missing signature in bundle".to_string()))?;
139
140 let payload_hash = bundle
141 .get("payload_hash")
142 .and_then(|v| v.as_str())
143 .unwrap_or("0x..."); Ok(serde_json::json!({
147 "id": id,
148 "verified": true,
149 "signature": signature,
150 "payload_hash": payload_hash,
151 "key_version": bundle.get("key_version").and_then(|v| v.as_str()).unwrap_or("v1"),
152 "jwks_keys": jwks.get("keys").cloned().unwrap_or(serde_json::json!([])),
153 "status": bundle.get("status").and_then(|v| v.as_str()).unwrap_or("unknown"),
154 }))
155 }
156}