use serde::{Deserialize, Serialize};
use std::error::Error;
use std::path::Path;
pub struct ApiClient {
base_url: String,
api_key: Option<String>,
client: reqwest::Client,
}
#[derive(Serialize)]
pub struct PushTraceRequest {
pub repo_name: String,
pub commit_sha: String,
pub branch: Option<String>,
pub author: String,
pub model: Option<String>,
pub tool: Option<String>,
pub session_id: Option<String>,
pub total_tokens: Option<i64>,
pub input_tokens: Option<i64>,
pub output_tokens: Option<i64>,
pub estimated_cost_usd: Option<f64>,
pub api_calls: Option<i32>,
pub session_data: Option<serde_json::Value>,
pub transcript: Option<serde_json::Value>,
pub diff_data: Option<serde_json::Value>,
pub model_usage: Option<serde_json::Value>,
pub duration_ms: Option<i64>,
pub started_at: Option<String>,
pub ended_at: Option<String>,
pub user_messages: Option<i32>,
pub assistant_messages: Option<i32>,
pub tool_calls: Option<serde_json::Value>,
pub total_tool_calls: Option<i32>,
pub cache_read_tokens: Option<i64>,
pub cache_write_tokens: Option<i64>,
pub compactions: Option<i32>,
pub compaction_tokens_saved: Option<i64>,
}
#[derive(Deserialize)]
pub struct PushTraceResponse {
pub commit_id: uuid::Uuid,
}
#[derive(Serialize)]
pub struct RegisterRepoRequest {
pub repo_name: String,
pub github_url: Option<String>,
}
#[derive(Deserialize)]
pub struct RegisterRepoResponse {
pub repo_id: uuid::Uuid,
}
#[derive(Deserialize)]
pub struct DeviceAuthResponse {
pub token: String,
}
#[derive(Deserialize)]
pub struct DeviceStatusResponse {
pub status: String,
pub token: Option<String>,
pub email: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CheckPoliciesRequest {
pub sessions: Vec<SessionCheckData>,
}
#[derive(Debug, Serialize)]
pub struct SessionCheckData {
pub session_id: String,
pub tool_calls: Option<serde_json::Value>,
pub files_modified: Option<Vec<String>>,
pub total_tool_calls: Option<i32>,
}
#[derive(Debug, Deserialize)]
pub struct CheckPoliciesResponse {
pub passed: bool,
pub results: Vec<CheckResultItem>,
pub blocked: bool,
}
#[derive(Debug, Deserialize)]
pub struct CheckResultItem {
pub rule_name: String,
pub result: String,
pub action: String,
pub severity: String,
pub details: String,
}
#[derive(Debug, Deserialize)]
pub struct RepoListItem {
pub id: uuid::Uuid,
pub name: String,
}
#[derive(Debug, Serialize)]
pub struct CiVerifyRequest {
pub commits: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct CiVerifyResponse {
pub status: String,
pub total_commits: usize,
pub registered_commits: usize,
pub sealed_commits: usize,
pub policy_passed_commits: usize,
pub results: Vec<CommitVerifyResult>,
}
#[derive(Debug, Deserialize)]
pub struct CommitVerifyResult {
pub commit_sha: String,
pub status: String,
pub registered: bool,
pub sealed: bool,
pub signature_valid: bool,
pub chain_valid: bool,
pub policy_results: Vec<CiPolicyResult>,
}
#[derive(Debug, Deserialize)]
pub struct CiPolicyResult {
pub rule_name: String,
pub result: String,
pub action: String,
pub severity: String,
pub details: String,
}
impl ApiClient {
pub fn new(base_url: &str, api_key: Option<&str>) -> Self {
Self {
base_url: base_url.trim_end_matches('/').to_string(),
api_key: api_key.map(String::from),
client: reqwest::Client::new(),
}
}
pub async fn push_trace(
&self,
org_slug: &str,
req: PushTraceRequest,
) -> Result<PushTraceResponse, Box<dyn Error>> {
let mut builder = self
.client
.post(format!("{}/api/v1/orgs/{}/traces", self.base_url, org_slug));
if let Some(key) = &self.api_key {
builder = builder.header("Authorization", format!("Bearer {key}"));
}
let resp = builder.json(&req).send().await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("Server returned {status}: {body}").into());
}
Ok(resp.json().await?)
}
pub async fn register_repo(
&self,
org_slug: &str,
req: RegisterRepoRequest,
) -> Result<RegisterRepoResponse, Box<dyn Error>> {
let mut builder = self
.client
.post(format!("{}/api/v1/orgs/{}/repos", self.base_url, org_slug));
if let Some(key) = &self.api_key {
builder = builder.header("Authorization", format!("Bearer {key}"));
}
let resp = builder.json(&req).send().await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("Server returned {status}: {body}").into());
}
Ok(resp.json().await?)
}
pub async fn device_start(&self) -> Result<DeviceAuthResponse, Box<dyn Error>> {
let resp = self
.client
.post(format!("{}/api/v1/auth/device", self.base_url))
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("Server returned {status}: {body}").into());
}
Ok(resp.json().await?)
}
pub async fn device_status(&self, token: &str) -> Result<DeviceStatusResponse, Box<dyn Error>> {
let resp = self
.client
.get(format!(
"{}/api/v1/auth/device/{token}/status",
self.base_url
))
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("Server returned {status}: {body}").into());
}
Ok(resp.json().await?)
}
pub async fn logout(&self) -> Result<(), Box<dyn Error>> {
let mut builder = self
.client
.post(format!("{}/api/v1/auth/logout", self.base_url));
if let Some(key) = &self.api_key {
builder = builder.header("Authorization", format!("Bearer {key}"));
}
let resp = builder.send().await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("Server returned {status}: {body}").into());
}
Ok(())
}
pub async fn list_repos(&self, org_slug: &str) -> Result<Vec<RepoListItem>, Box<dyn Error>> {
let mut builder = self
.client
.get(format!("{}/api/v1/orgs/{}/repos", self.base_url, org_slug));
if let Some(key) = &self.api_key {
builder = builder.header("Authorization", format!("Bearer {key}"));
}
let resp = builder.send().await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("Failed to list repos ({status}): {body}").into());
}
let repos: Vec<RepoListItem> = resp.json().await?;
Ok(repos)
}
pub async fn verify_commits(
&self,
org_slug: &str,
repo_id: &uuid::Uuid,
req: CiVerifyRequest,
) -> Result<CiVerifyResponse, Box<dyn Error>> {
let mut builder = self.client.post(format!(
"{}/api/v1/orgs/{}/repos/{}/ci/verify",
self.base_url, org_slug, repo_id
));
if let Some(key) = &self.api_key {
builder = builder.header("Authorization", format!("Bearer {key}"));
}
let resp = builder.json(&req).send().await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("CI verify failed ({status}): {body}").into());
}
Ok(resp.json().await?)
}
pub async fn push_commit(
&self,
org_slug: &str,
repo_id: &str,
req: &tracevault_core::streaming::CommitPushRequest,
) -> Result<tracevault_core::streaming::CommitPushResponse, Box<dyn Error>> {
let mut builder = self.client.post(format!(
"{}/api/v1/orgs/{}/repos/{}/commits",
self.base_url, org_slug, repo_id
));
if let Some(key) = &self.api_key {
builder = builder.header("Authorization", format!("Bearer {key}"));
}
let resp = builder.json(req).send().await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("Commit push failed ({status}): {body}").into());
}
Ok(resp.json().await?)
}
pub async fn stream_event(
&self,
org_slug: &str,
repo_id: &str,
req: &tracevault_core::streaming::StreamEventRequest,
) -> Result<tracevault_core::streaming::StreamEventResponse, Box<dyn Error>> {
let mut builder = self.client.post(format!(
"{}/api/v1/orgs/{}/repos/{}/stream",
self.base_url, org_slug, repo_id
));
if let Some(key) = &self.api_key {
builder = builder.header("Authorization", format!("Bearer {key}"));
}
let resp = builder.json(req).send().await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("Stream failed ({status}): {body}").into());
}
Ok(resp.json().await?)
}
pub async fn check_policies(
&self,
org_slug: &str,
repo_id: &uuid::Uuid,
req: CheckPoliciesRequest,
) -> Result<CheckPoliciesResponse, Box<dyn Error>> {
let mut builder = self.client.post(format!(
"{}/api/v1/orgs/{}/repos/{}/policies/check",
self.base_url, org_slug, repo_id
));
if let Some(key) = &self.api_key {
builder = builder.header("Authorization", format!("Bearer {key}"));
}
let resp = builder.json(&req).send().await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("Policy check failed ({status}): {body}").into());
}
let result: CheckPoliciesResponse = resp.json().await?;
Ok(result)
}
}
pub fn resolve_credentials(project_root: &Path) -> (Option<String>, Option<String>) {
use crate::credentials::Credentials;
let env_key = std::env::var("TRACEVAULT_API_KEY").ok();
let creds = Credentials::load();
let config_path = crate::config::TracevaultConfig::config_path(project_root);
let config_content = std::fs::read_to_string(&config_path).unwrap_or_default();
let config_server_url = config_content
.lines()
.find(|l| l.starts_with("server_url"))
.and_then(|l| l.split('=').nth(1))
.map(|s| s.trim().trim_matches('"').to_string());
let config_api_key = config_content
.lines()
.find(|l| l.starts_with("api_key"))
.and_then(|l| l.split('=').nth(1))
.map(|s| s.trim().trim_matches('"').to_string());
let server_url = std::env::var("TRACEVAULT_SERVER_URL")
.ok()
.or_else(|| creds.as_ref().map(|c| c.server_url.clone()))
.or(config_server_url);
let token = env_key
.or_else(|| creds.map(|c| c.token))
.or(config_api_key);
(server_url, token)
}