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