1use serde::{Deserialize, Serialize};
2use std::error::Error;
3use std::path::Path;
4
5pub struct ApiClient {
6 base_url: String,
7 api_key: Option<String>,
8 client: reqwest::Client,
9}
10
11#[derive(Serialize)]
12pub struct PushTraceRequest {
13 pub repo_name: String,
14 pub commit_sha: String,
15 pub branch: Option<String>,
16 pub author: String,
17 pub model: Option<String>,
18 pub tool: Option<String>,
19 pub session_id: Option<String>,
20 pub total_tokens: Option<i64>,
21 pub input_tokens: Option<i64>,
22 pub output_tokens: Option<i64>,
23 pub estimated_cost_usd: Option<f64>,
24 pub api_calls: Option<i32>,
25 pub session_data: Option<serde_json::Value>,
26 pub attribution: Option<serde_json::Value>,
27 pub transcript: Option<serde_json::Value>,
28 pub diff_data: Option<serde_json::Value>,
29 pub model_usage: Option<serde_json::Value>,
30 pub duration_ms: Option<i64>,
31 pub started_at: Option<String>,
32 pub ended_at: Option<String>,
33 pub user_messages: Option<i32>,
34 pub assistant_messages: Option<i32>,
35 pub tool_calls: Option<serde_json::Value>,
36 pub total_tool_calls: Option<i32>,
37 pub cache_read_tokens: Option<i64>,
38 pub cache_write_tokens: Option<i64>,
39 pub compactions: Option<i32>,
40 pub compaction_tokens_saved: Option<i64>,
41}
42
43#[derive(Deserialize)]
44pub struct PushTraceResponse {
45 pub commit_id: uuid::Uuid,
46}
47
48#[derive(Serialize)]
49pub struct RegisterRepoRequest {
50 pub repo_name: String,
51 pub github_url: Option<String>,
52}
53
54#[derive(Deserialize)]
55pub struct RegisterRepoResponse {
56 pub repo_id: uuid::Uuid,
57}
58
59#[derive(Deserialize)]
60pub struct DeviceAuthResponse {
61 pub token: String,
62}
63
64#[derive(Deserialize)]
65pub struct DeviceStatusResponse {
66 pub status: String,
67 pub token: Option<String>,
68 pub email: Option<String>,
69}
70
71#[derive(Debug, Serialize)]
72pub struct CheckPoliciesRequest {
73 pub sessions: Vec<SessionCheckData>,
74}
75
76#[derive(Debug, Serialize)]
77pub struct SessionCheckData {
78 pub session_id: String,
79 pub tool_calls: Option<serde_json::Value>,
80 pub files_modified: Option<Vec<String>>,
81 pub total_tool_calls: Option<i32>,
82}
83
84#[derive(Debug, Deserialize)]
85pub struct CheckPoliciesResponse {
86 pub passed: bool,
87 pub results: Vec<CheckResultItem>,
88 pub blocked: bool,
89}
90
91#[derive(Debug, Deserialize)]
92pub struct CheckResultItem {
93 pub rule_name: String,
94 pub result: String,
95 pub action: String,
96 pub severity: String,
97 pub details: String,
98}
99
100#[derive(Debug, Deserialize)]
101pub struct RepoListItem {
102 pub id: uuid::Uuid,
103 pub name: String,
104}
105
106#[derive(Debug, Serialize)]
107pub struct CiVerifyRequest {
108 pub commits: Vec<String>,
109}
110
111#[derive(Debug, Deserialize)]
112pub struct CiVerifyResponse {
113 pub status: String,
114 pub total_commits: usize,
115 pub registered_commits: usize,
116 pub sealed_commits: usize,
117 pub policy_passed_commits: usize,
118 pub results: Vec<CommitVerifyResult>,
119}
120
121#[derive(Debug, Deserialize)]
122pub struct CommitVerifyResult {
123 pub commit_sha: String,
124 pub status: String,
125 pub registered: bool,
126 pub sealed: bool,
127 pub signature_valid: bool,
128 pub chain_valid: bool,
129 pub policy_results: Vec<CiPolicyResult>,
130}
131
132#[derive(Debug, Deserialize)]
133pub struct CiPolicyResult {
134 pub rule_name: String,
135 pub result: String,
136 pub action: String,
137 pub severity: String,
138 pub details: String,
139}
140
141impl ApiClient {
142 pub fn new(base_url: &str, api_key: Option<&str>) -> Self {
143 Self {
144 base_url: base_url.trim_end_matches('/').to_string(),
145 api_key: api_key.map(String::from),
146 client: reqwest::Client::new(),
147 }
148 }
149
150 pub async fn push_trace(
151 &self,
152 org_slug: &str,
153 req: PushTraceRequest,
154 ) -> Result<PushTraceResponse, Box<dyn Error>> {
155 let mut builder = self
156 .client
157 .post(format!("{}/api/v1/orgs/{}/traces", self.base_url, org_slug));
158
159 if let Some(key) = &self.api_key {
160 builder = builder.header("Authorization", format!("Bearer {key}"));
161 }
162
163 let resp = builder.json(&req).send().await?;
164
165 if !resp.status().is_success() {
166 let status = resp.status();
167 let body = resp.text().await.unwrap_or_default();
168 return Err(format!("Server returned {status}: {body}").into());
169 }
170
171 Ok(resp.json().await?)
172 }
173
174 pub async fn register_repo(
175 &self,
176 org_slug: &str,
177 req: RegisterRepoRequest,
178 ) -> Result<RegisterRepoResponse, Box<dyn Error>> {
179 let mut builder = self
180 .client
181 .post(format!("{}/api/v1/orgs/{}/repos", self.base_url, org_slug));
182
183 if let Some(key) = &self.api_key {
184 builder = builder.header("Authorization", format!("Bearer {key}"));
185 }
186
187 let resp = builder.json(&req).send().await?;
188
189 if !resp.status().is_success() {
190 let status = resp.status();
191 let body = resp.text().await.unwrap_or_default();
192 return Err(format!("Server returned {status}: {body}").into());
193 }
194
195 Ok(resp.json().await?)
196 }
197
198 pub async fn device_start(&self) -> Result<DeviceAuthResponse, Box<dyn Error>> {
199 let resp = self
200 .client
201 .post(format!("{}/api/v1/auth/device", self.base_url))
202 .send()
203 .await?;
204
205 if !resp.status().is_success() {
206 let status = resp.status();
207 let body = resp.text().await.unwrap_or_default();
208 return Err(format!("Server returned {status}: {body}").into());
209 }
210
211 Ok(resp.json().await?)
212 }
213
214 pub async fn device_status(&self, token: &str) -> Result<DeviceStatusResponse, Box<dyn Error>> {
215 let resp = self
216 .client
217 .get(format!(
218 "{}/api/v1/auth/device/{token}/status",
219 self.base_url
220 ))
221 .send()
222 .await?;
223
224 if !resp.status().is_success() {
225 let status = resp.status();
226 let body = resp.text().await.unwrap_or_default();
227 return Err(format!("Server returned {status}: {body}").into());
228 }
229
230 Ok(resp.json().await?)
231 }
232
233 pub async fn logout(&self) -> Result<(), Box<dyn Error>> {
234 let mut builder = self
235 .client
236 .post(format!("{}/api/v1/auth/logout", self.base_url));
237 if let Some(key) = &self.api_key {
238 builder = builder.header("Authorization", format!("Bearer {key}"));
239 }
240 let resp = builder.send().await?;
241 if !resp.status().is_success() {
242 let status = resp.status();
243 let body = resp.text().await.unwrap_or_default();
244 return Err(format!("Server returned {status}: {body}").into());
245 }
246 Ok(())
247 }
248
249 pub async fn list_repos(&self, org_slug: &str) -> Result<Vec<RepoListItem>, Box<dyn Error>> {
250 let mut builder = self
251 .client
252 .get(format!("{}/api/v1/orgs/{}/repos", self.base_url, org_slug));
253 if let Some(key) = &self.api_key {
254 builder = builder.header("Authorization", format!("Bearer {key}"));
255 }
256
257 let resp = builder.send().await?;
258
259 if !resp.status().is_success() {
260 let status = resp.status();
261 let body = resp.text().await.unwrap_or_default();
262 return Err(format!("Failed to list repos ({status}): {body}").into());
263 }
264
265 let repos: Vec<RepoListItem> = resp.json().await?;
266 Ok(repos)
267 }
268
269 pub async fn verify_commits(
270 &self,
271 org_slug: &str,
272 repo_id: &uuid::Uuid,
273 req: CiVerifyRequest,
274 ) -> Result<CiVerifyResponse, Box<dyn Error>> {
275 let mut builder = self.client.post(format!(
276 "{}/api/v1/orgs/{}/repos/{}/ci/verify",
277 self.base_url, org_slug, repo_id
278 ));
279 if let Some(key) = &self.api_key {
280 builder = builder.header("Authorization", format!("Bearer {key}"));
281 }
282
283 let resp = builder.json(&req).send().await?;
284
285 if !resp.status().is_success() {
286 let status = resp.status();
287 let body = resp.text().await.unwrap_or_default();
288 return Err(format!("CI verify failed ({status}): {body}").into());
289 }
290
291 Ok(resp.json().await?)
292 }
293
294 pub async fn push_commit(
295 &self,
296 org_slug: &str,
297 repo_id: &str,
298 req: &tracevault_core::streaming::CommitPushRequest,
299 ) -> Result<tracevault_core::streaming::CommitPushResponse, Box<dyn Error>> {
300 let mut builder = self.client.post(format!(
301 "{}/api/v1/orgs/{}/repos/{}/commits",
302 self.base_url, org_slug, repo_id
303 ));
304 if let Some(key) = &self.api_key {
305 builder = builder.header("Authorization", format!("Bearer {key}"));
306 }
307 let resp = builder.json(req).send().await?;
308 if !resp.status().is_success() {
309 let status = resp.status();
310 let body = resp.text().await.unwrap_or_default();
311 return Err(format!("Commit push failed ({status}): {body}").into());
312 }
313 Ok(resp.json().await?)
314 }
315
316 pub async fn stream_event(
317 &self,
318 org_slug: &str,
319 repo_id: &str,
320 req: &tracevault_core::streaming::StreamEventRequest,
321 ) -> Result<tracevault_core::streaming::StreamEventResponse, Box<dyn Error>> {
322 let mut builder = self.client.post(format!(
323 "{}/api/v1/orgs/{}/repos/{}/stream",
324 self.base_url, org_slug, repo_id
325 ));
326 if let Some(key) = &self.api_key {
327 builder = builder.header("Authorization", format!("Bearer {key}"));
328 }
329 let resp = builder.json(req).send().await?;
330 if !resp.status().is_success() {
331 let status = resp.status();
332 let body = resp.text().await.unwrap_or_default();
333 return Err(format!("Stream failed ({status}): {body}").into());
334 }
335 Ok(resp.json().await?)
336 }
337
338 pub async fn check_policies(
339 &self,
340 org_slug: &str,
341 repo_id: &uuid::Uuid,
342 req: CheckPoliciesRequest,
343 ) -> Result<CheckPoliciesResponse, Box<dyn Error>> {
344 let mut builder = self.client.post(format!(
345 "{}/api/v1/orgs/{}/repos/{}/policies/check",
346 self.base_url, org_slug, repo_id
347 ));
348 if let Some(key) = &self.api_key {
349 builder = builder.header("Authorization", format!("Bearer {key}"));
350 }
351
352 let resp = builder.json(&req).send().await?;
353
354 if !resp.status().is_success() {
355 let status = resp.status();
356 let body = resp.text().await.unwrap_or_default();
357 return Err(format!("Policy check failed ({status}): {body}").into());
358 }
359
360 let result: CheckPoliciesResponse = resp.json().await?;
361 Ok(result)
362 }
363}
364
365pub fn resolve_credentials(project_root: &Path) -> (Option<String>, Option<String>) {
369 use crate::credentials::Credentials;
370
371 let env_key = std::env::var("TRACEVAULT_API_KEY").ok();
373
374 let creds = Credentials::load();
376
377 let config_path = crate::config::TracevaultConfig::config_path(project_root);
379 let config_content = std::fs::read_to_string(&config_path).unwrap_or_default();
380
381 let config_server_url = config_content
382 .lines()
383 .find(|l| l.starts_with("server_url"))
384 .and_then(|l| l.split('=').nth(1))
385 .map(|s| s.trim().trim_matches('"').to_string());
386
387 let config_api_key = config_content
388 .lines()
389 .find(|l| l.starts_with("api_key"))
390 .and_then(|l| l.split('=').nth(1))
391 .map(|s| s.trim().trim_matches('"').to_string());
392
393 let server_url = std::env::var("TRACEVAULT_SERVER_URL")
395 .ok()
396 .or_else(|| creds.as_ref().map(|c| c.server_url.clone()))
397 .or(config_server_url);
398
399 let token = env_key
401 .or_else(|| creds.map(|c| c.token))
402 .or(config_api_key);
403
404 (server_url, token)
405}