1use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
2use serde_json::Value;
3
4use crate::config::{CairnConfig, Credentials};
5use crate::errors::CairnError;
6
7#[allow(dead_code)]
9#[derive(serde::Deserialize, Debug)]
10pub struct AgentClaims {
11 pub sub: String,
12 pub account_id: String,
13 pub did: String,
14 pub chain: String,
15}
16
17pub struct BackpacClient {
19 inner: reqwest::Client,
20 base_url: String,
21 jwt: Option<String>,
22}
23
24impl BackpacClient {
25 pub fn new(jwt_override: Option<&str>, api_url: Option<&str>) -> Self {
27 let config = CairnConfig::load();
28
29 let base_url = api_url
30 .map(|s| s.to_string())
31 .or(config.api_url.clone())
32 .or_else(|| std::env::var("BACKPAC_API_URL").ok())
33 .unwrap_or_else(|| "https://api.backpac.xyz".to_string());
34
35 let jwt = jwt_override
36 .map(|s| s.to_string())
37 .or_else(|| std::env::var("BACKPAC_JWT").ok())
38 .or_else(|| Credentials::load().map(|c| c.jwt));
39
40 let inner = reqwest::Client::builder()
41 .timeout(std::time::Duration::from_secs(30))
42 .build()
43 .expect("Failed to create HTTP client");
44
45 Self { inner, base_url, jwt }
46 }
47
48 pub fn claims(&self) -> Option<Value> {
50 let jwt = self.jwt.as_ref()?;
51 let parts: Vec<&str> = jwt.split('.').collect();
52 if parts.len() != 3 {
53 return None;
54 }
55
56 let payload_b64 = parts[1];
57 let decoded = base64::Engine::decode(
58 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
59 payload_b64,
60 ).ok()?;
61
62 serde_json::from_slice(&decoded).ok()
63 }
64
65 fn auth_headers(&self) -> Result<HeaderMap, CairnError> {
67 let mut headers = HeaderMap::new();
68 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
69
70 if let Some(ref jwt) = self.jwt {
71 let val = format!("Bearer {}", jwt);
72 headers.insert(
73 AUTHORIZATION,
74 HeaderValue::from_str(&val).map_err(|e| CairnError::Auth(e.to_string()))?,
75 );
76 }
77 Ok(headers)
78 }
79
80 pub async fn get(&self, path: &str) -> Result<Value, CairnError> {
82 let url = format!("{}{}", self.base_url, path);
83 let resp = self
84 .inner
85 .get(&url)
86 .headers(self.auth_headers()?)
87 .send()
88 .await?;
89
90 self.handle_response(resp).await
91 }
92
93 pub fn stream(&self, path: &str) -> Result<reqwest_eventsource::EventSource, CairnError> {
95 let url = format!("{}{}", self.base_url, path);
96 let req = self.inner.get(&url).headers(self.auth_headers()?);
97 reqwest_eventsource::EventSource::new(req).map_err(|e| CairnError::General(e.to_string()))
98 }
99
100 pub async fn post(&self, path: &str, body: &Value) -> Result<Value, CairnError> {
102 let url = format!("{}{}", self.base_url, path);
103 let resp = self
104 .inner
105 .post(&url)
106 .headers(self.auth_headers()?)
107 .json(body)
108 .send()
109 .await?;
110
111 self.handle_response(resp).await
112 }
113
114 pub async fn put(&self, path: &str, body: &Value) -> Result<Value, CairnError> {
116 let url = format!("{}{}", self.base_url, path);
117 let resp = self
118 .inner
119 .put(&url)
120 .headers(self.auth_headers()?)
121 .json(body)
122 .send()
123 .await?;
124
125 self.handle_response(resp).await
126 }
127
128 pub async fn rpc_post(
130 &self,
131 body: &Value,
132 poi_id: Option<&str>,
133 confidence: Option<f64>,
134 hostname: Option<&str>,
135 ) -> Result<Value, CairnError> {
136 let url = format!("{}/", self.base_url);
137 let mut headers = self.auth_headers()?;
138
139 if let Some(h) = hostname {
140 headers.insert(
141 "x-backpac-hostname",
142 HeaderValue::from_str(h).map_err(|e| CairnError::InvalidInput(e.to_string()))?,
143 );
144 }
145
146 if let Some(poi) = poi_id {
147 headers.insert(
148 "X-Backpac-Poi-Id",
149 HeaderValue::from_str(poi).map_err(|e| CairnError::InvalidInput(e.to_string()))?,
150 );
151 }
152 if let Some(conf) = confidence {
153 headers.insert(
154 "X-Backpac-Confidence",
155 HeaderValue::from_str(&conf.to_string())
156 .map_err(|e| CairnError::InvalidInput(e.to_string()))?,
157 );
158 }
159
160 let resp = self.inner.post(&url).headers(headers).json(body).send().await?;
161 self.handle_response(resp).await
162 }
163
164 async fn handle_response(&self, resp: reqwest::Response) -> Result<Value, CairnError> {
166 let status = resp.status();
167
168 let mut l402_challenge = String::new();
170 if let Some(header) = resp.headers().get("WWW-Authenticate") {
171 if let Ok(val) = header.to_str() {
172 l402_challenge = val.to_string();
173 }
174 } else if let Some(header) = resp.headers().get("L402") {
175 if let Ok(val) = header.to_str() {
176 l402_challenge = val.to_string();
177 }
178 }
179
180 if status.is_success() {
181 let body: Value = resp.json().await?;
182 return Ok(body);
183 }
184
185 let body_text = resp.text().await.unwrap_or_default();
187 let error_msg = serde_json::from_str::<Value>(&body_text)
188 .ok()
189 .and_then(|v| v.get("error").and_then(|e| e.as_str()).map(String::from))
190 .unwrap_or_else(|| body_text.clone());
191
192 match status.as_u16() {
193 400 => Err(CairnError::InvalidInput(error_msg)),
194 401 => {
195 if error_msg.contains("expired") {
196 Err(CairnError::TokenExpired)
197 } else {
198 Err(CairnError::Auth(error_msg))
199 }
200 }
201 402 => {
202 if !l402_challenge.is_empty() {
203 let is_auto_pay = std::env::var("CAIRN_AUTO_PAY").map(|v| v == "1").unwrap_or(false);
205
206 if is_auto_pay {
207 return Err(CairnError::InsufficientFunds(
210 format!("Credits exhausted. Auto-payment attempted for L402 challenge but signing logic is pending implementation. Challenge: {}", l402_challenge),
211 ));
212 } else {
213 return Err(CairnError::InsufficientFunds(
214 format!("Credits exhausted. Payment required: {}\nHint: Set CAIRN_AUTO_PAY=1 to enable wallet auto-payment.", l402_challenge),
215 ));
216 }
217 }
218
219 Err(CairnError::InsufficientFunds(
220 "Credits exhausted. x402 auto-payment not available (missing L402 headers).".to_string(),
221 ))
222 }
223 403 => Err(CairnError::Forbidden(error_msg)),
224 404 => Err(CairnError::NotFound(error_msg)),
225 409 => Err(CairnError::Conflict(error_msg)),
226 410 => {
227 if error_msg.contains("expired") {
228 Err(CairnError::IntentExpired(error_msg))
229 } else {
230 Err(CairnError::IntentAborted(error_msg))
231 }
232 }
233 _ => Err(CairnError::General(format!("HTTP {}: {}", status, error_msg))),
234 }
235 }
236}