use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use serde_json::json;
use std::time::{Duration, Instant};
#[derive(Debug, thiserror::Error)]
pub enum FabricError {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("API error ({code}): {message}")]
Api { code: String, message: String },
#[error("{0}")]
Other(String),
}
pub type Result<T> = std::result::Result<T, FabricError>;
pub struct FabricClient {
client: reqwest::Client,
base_url: String,
organization_id: String,
}
impl FabricClient {
pub fn new(base_url: &str, api_key: &str) -> Result<Self> {
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {api_key}"))
.map_err(|e| FabricError::Other(e.to_string()))?,
);
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let client = reqwest::Client::builder()
.default_headers(headers)
.build()?;
Ok(Self {
client,
base_url: base_url.trim_end_matches('/').to_string(),
organization_id: String::new(),
})
}
pub fn with_principal(base_url: &str, principal_id: &str) -> Result<Self> {
let mut headers = HeaderMap::new();
headers.insert(
"X-Principal-Id",
HeaderValue::from_str(principal_id).map_err(|e| FabricError::Other(e.to_string()))?,
);
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let client = reqwest::Client::builder()
.default_headers(headers)
.build()?;
Ok(Self {
client,
base_url: base_url.trim_end_matches('/').to_string(),
organization_id: String::new(),
})
}
pub fn set_organization_id(&mut self, org_id: &str) {
self.organization_id = org_id.to_string();
}
async fn request<T: serde::de::DeserializeOwned>(
&self,
method: reqwest::Method,
path: &str,
body: Option<serde_json::Value>,
) -> Result<T> {
let url = format!("{}{path}", self.base_url);
let mut req = self.client.request(method, &url);
if let Some(b) = body {
req = req.json(&b);
}
let resp = req.send().await?;
let status = resp.status();
let json: serde_json::Value = resp.json().await?;
if let Some(err) = json.get("error") {
return Err(FabricError::Api {
code: status.as_u16().to_string(),
message: err
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| err.to_string()),
});
}
if let Some(data) = json.get("data") {
serde_json::from_value(data.clone())
.map_err(|e| FabricError::Other(format!("Failed to deserialize data: {e}")))
} else {
serde_json::from_value(json)
.map_err(|e| FabricError::Other(format!("Failed to deserialize response: {e}")))
}
}
async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
self.request(reqwest::Method::GET, path, None).await
}
async fn post<T: serde::de::DeserializeOwned>(
&self,
path: &str,
body: serde_json::Value,
) -> Result<T> {
self.request(reqwest::Method::POST, path, Some(body)).await
}
async fn post_empty(&self, path: &str) -> Result<()> {
let url = format!("{}{path}", self.base_url);
let resp = self.client.post(&url).send().await?;
let status = resp.status();
let json: serde_json::Value = resp.json().await?;
if let Some(err) = json.get("error") {
return Err(FabricError::Api {
code: status.as_u16().to_string(),
message: err
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| err.to_string()),
});
}
Ok(())
}
async fn delete_req(&self, path: &str) -> Result<()> {
let url = format!("{}{path}", self.base_url);
let resp = self.client.delete(&url).send().await?;
let status = resp.status();
let json: serde_json::Value = resp.json().await?;
if let Some(err) = json.get("error") {
return Err(FabricError::Api {
code: status.as_u16().to_string(),
message: err
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| err.to_string()),
});
}
Ok(())
}
pub async fn health_check(&self) -> Result<serde_json::Value> {
self.get("/health").await
}
pub async fn system_status(&self) -> Result<serde_json::Value> {
self.get("/api/v1/system/status").await
}
pub async fn get_me(&self) -> Result<serde_json::Value> {
self.get("/api/v1/me").await
}
pub async fn get_my_organizations(&self) -> Result<Vec<serde_json::Value>> {
self.get("/api/v1/me/organizations").await
}
pub async fn get_my_teams(&self) -> Result<Vec<serde_json::Value>> {
self.get("/api/v1/me/teams").await
}
pub async fn get_my_permissions(&self) -> Result<Vec<serde_json::Value>> {
self.get("/api/v1/me/permissions").await
}
pub async fn create_organization(&self, slug: &str, name: &str) -> Result<serde_json::Value> {
self.post(
"/api/v1/organizations",
json!({ "slug": slug, "name": name }),
)
.await
}
pub async fn list_organizations(&self) -> Result<Vec<serde_json::Value>> {
self.get("/api/v1/organizations").await
}
pub async fn get_organization(&self, org_id: &str) -> Result<serde_json::Value> {
self.get(&format!("/api/v1/organizations/{org_id}")).await
}
pub async fn list_org_teams(&self, org_id: &str) -> Result<Vec<serde_json::Value>> {
self.get(&format!("/api/v1/organizations/{org_id}/teams"))
.await
}
pub async fn list_org_members(&self, org_id: &str) -> Result<Vec<serde_json::Value>> {
self.get(&format!("/api/v1/organizations/{org_id}/members"))
.await
}
pub async fn create_team(
&self,
org_id: &str,
slug: &str,
name: &str,
) -> Result<serde_json::Value> {
self.post(
&format!("/api/v1/organizations/{org_id}/teams"),
json!({ "slug": slug, "name": name }),
)
.await
}
pub async fn get_team(&self, team_id: &str) -> Result<serde_json::Value> {
self.get(&format!("/api/v1/teams/{team_id}")).await
}
pub async fn create_invitation(
&self,
org_id: &str,
email: &str,
role: &str,
) -> Result<serde_json::Value> {
self.post(
&format!("/api/v1/organizations/{org_id}/invitations"),
json!({ "email": email, "role": role }),
)
.await
}
pub async fn accept_invitation(&self, invitation_id: &str) -> Result<()> {
self.post_empty(&format!("/api/v1/invitations/{invitation_id}/accept"))
.await
}
pub async fn revoke_invitation(&self, invitation_id: &str) -> Result<()> {
self.delete_req(&format!("/api/v1/invitations/{invitation_id}"))
.await
}
pub async fn check_permission(&self, action: &str, resource: Option<&str>) -> Result<bool> {
let mut body = json!({ "action": action });
if let Some(r) = resource {
body["resource"] = serde_json::Value::String(r.to_string());
}
let resp: serde_json::Value = self.post("/api/v1/authz/check", body).await?;
Ok(resp
.get("allowed")
.and_then(|v| v.as_bool())
.unwrap_or(false))
}
pub async fn check_permissions(
&self,
checks: Vec<serde_json::Value>,
) -> Result<Vec<serde_json::Value>> {
self.post("/api/v1/authz/check-batch", json!({ "checks": checks }))
.await
}
pub async fn create_api_key(
&self,
name: &str,
org_id: &str,
scopes: Option<Vec<&str>>,
) -> Result<serde_json::Value> {
let mut body = json!({ "name": name, "organization_id": org_id });
if let Some(s) = scopes {
body["scopes"] = serde_json::Value::from(s);
}
self.post("/api/v1/api-keys", body).await
}
pub async fn list_api_keys(&self) -> Result<Vec<serde_json::Value>> {
self.get("/api/v1/api-keys").await
}
pub async fn get_api_key(&self, key_id: &str) -> Result<serde_json::Value> {
self.get(&format!("/api/v1/api-keys/{key_id}")).await
}
pub async fn delete_api_key(&self, key_id: &str) -> Result<()> {
self.delete_req(&format!("/api/v1/api-keys/{key_id}")).await
}
pub async fn disable_api_key(&self, key_id: &str) -> Result<()> {
self.post_empty(&format!("/api/v1/api-keys/{key_id}/disable"))
.await
}
pub async fn rotate_api_key(&self, key_id: &str) -> Result<serde_json::Value> {
self.post(&format!("/api/v1/api-keys/{key_id}/rotate"), json!({}))
.await
}
pub async fn upsert_workflow(&self, name: &str, body: serde_json::Value) -> Result<String> {
let mut payload = body;
payload["name"] = serde_json::Value::String(name.to_string());
let resp: serde_json::Value = self.post("/api/v1/workflows", payload).await?;
resp.get("id")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| FabricError::Other("Missing workflow id in response".to_string()))
}
pub async fn list_workflows(&self) -> Result<Vec<serde_json::Value>> {
self.get("/api/v1/workflows").await
}
pub async fn run_workflow(
&self,
workflow_id: &str,
context: serde_json::Value,
) -> Result<String> {
let resp: serde_json::Value = self
.post(
&format!("/api/v1/workflows/{workflow_id}/runs"),
json!({ "context": context }),
)
.await?;
let run_id = resp
.get("id")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| FabricError::Other("Missing run id in response".to_string()))?;
self.start_run(&run_id).await?;
Ok(run_id)
}
pub async fn get_run(&self, run_id: &str) -> Result<serde_json::Value> {
self.get(&format!("/api/v1/runs/{run_id}")).await
}
pub async fn start_run(&self, run_id: &str) -> Result<()> {
self.post_empty(&format!("/api/v1/runs/{run_id}/start"))
.await
}
pub async fn cancel_run(&self, run_id: &str) -> Result<()> {
self.post_empty(&format!("/api/v1/runs/{run_id}/cancel"))
.await
}
pub async fn wait_for_run(&self, run_id: &str) -> Result<serde_json::Value> {
let timeout = Duration::from_secs(300);
let poll_interval = Duration::from_secs(2);
let start = Instant::now();
loop {
let run = self.get_run(run_id).await?;
if let Some("completed" | "failed" | "cancelled") =
run.get("status").and_then(|v| v.as_str())
{
return Ok(run);
}
if start.elapsed() >= timeout {
return Err(FabricError::Other(format!(
"Timed out waiting for run {run_id} after {timeout:?}"
)));
}
tokio::time::sleep(poll_interval).await;
}
}
pub async fn list_runs(&self, workflow_id: &str) -> Result<Vec<serde_json::Value>> {
self.get(&format!("/api/v1/workflows/{workflow_id}/runs"))
.await
}
pub async fn run_log(&self, run_id: &str) -> Result<serde_json::Value> {
self.get(&format!("/api/v1/runs/{run_id}/log")).await
}
pub async fn face_swap(
&self,
source_url: &str,
target_url: Option<&str>,
persona_gallery_id: Option<&str>,
) -> Result<serde_json::Value> {
let mut context = json!({ "source_url": source_url });
if let Some(url) = target_url {
context["target_url"] = serde_json::Value::String(url.to_string());
}
if let Some(gid) = persona_gallery_id {
context["persona_gallery_id"] = serde_json::Value::String(gid.to_string());
}
if !self.organization_id.is_empty() {
context["organization_id"] = serde_json::Value::String(self.organization_id.clone());
}
let run_id = self.run_workflow("video/face-swap", context).await?;
self.wait_for_run(&run_id).await
}
pub async fn motion_transfer(
&self,
driving_video_url: &str,
source_image_url: Option<&str>,
persona_gallery_id: Option<&str>,
motion_model: Option<&str>,
) -> Result<serde_json::Value> {
let mut context = json!({ "driving_video_url": driving_video_url });
if let Some(url) = source_image_url {
context["source_image_url"] = serde_json::Value::String(url.to_string());
}
if let Some(gid) = persona_gallery_id {
context["persona_gallery_id"] = serde_json::Value::String(gid.to_string());
}
if let Some(model) = motion_model {
context["motion_model"] = serde_json::Value::String(model.to_string());
}
if !self.organization_id.is_empty() {
context["organization_id"] = serde_json::Value::String(self.organization_id.clone());
}
let run_id = self.run_workflow("video/motion-transfer", context).await?;
self.wait_for_run(&run_id).await
}
pub async fn create_job(&self, body: serde_json::Value) -> Result<serde_json::Value> {
self.post("/api/v1/jobs", body).await
}
pub async fn get_job(&self, job_id: &str) -> Result<serde_json::Value> {
self.get(&format!("/api/v1/jobs/{job_id}")).await
}
pub async fn list_jobs(&self) -> Result<Vec<serde_json::Value>> {
self.get("/api/v1/jobs").await
}
pub async fn get_job_usage(&self, job_id: &str) -> Result<serde_json::Value> {
self.get(&format!("/api/v1/jobs/{job_id}/usage")).await
}
pub async fn list_providers(&self) -> Result<Vec<serde_json::Value>> {
self.get("/api/v1/providers").await
}
pub async fn execute_provider(&self, body: serde_json::Value) -> Result<serde_json::Value> {
self.post("/api/v1/providers/execute", body).await
}
pub async fn estimate_cost(&self, body: serde_json::Value) -> Result<serde_json::Value> {
self.post("/api/v1/providers/estimate", body).await
}
pub async fn list_nodes(&self) -> Result<Vec<serde_json::Value>> {
self.get("/api/v1/nodes").await
}
pub async fn get_node(&self, node_id: &str) -> Result<serde_json::Value> {
self.get(&format!("/api/v1/nodes/{node_id}")).await
}
pub async fn get_org_usage(&self, org_id: &str) -> Result<serde_json::Value> {
self.get(&format!("/api/v1/organizations/{org_id}/usage"))
.await
}
pub async fn get_org_usage_records(&self, org_id: &str) -> Result<Vec<serde_json::Value>> {
self.get(&format!("/api/v1/organizations/{org_id}/usage/records"))
.await
}
pub async fn get_org_usage_daily(&self, org_id: &str) -> Result<Vec<serde_json::Value>> {
self.get(&format!("/api/v1/organizations/{org_id}/usage/daily"))
.await
}
pub async fn get_org_audit_logs(&self, org_id: &str) -> Result<Vec<serde_json::Value>> {
self.get(&format!("/api/v1/organizations/{org_id}/audit-logs"))
.await
}
pub async fn get_audit_logs(&self) -> Result<Vec<serde_json::Value>> {
self.get("/api/v1/audit-logs").await
}
pub async fn create_webhook(
&self,
org_id: &str,
url: &str,
events: Vec<&str>,
secret: Option<&str>,
) -> Result<serde_json::Value> {
let mut body = json!({
"url": url,
"events": events,
});
if let Some(s) = secret {
body["secret"] = serde_json::Value::String(s.to_string());
}
self.post(&format!("/api/v1/organizations/{org_id}/webhooks"), body)
.await
}
pub async fn list_webhooks(&self, org_id: &str) -> Result<Vec<serde_json::Value>> {
self.get(&format!("/api/v1/organizations/{org_id}/webhooks"))
.await
}
pub async fn get_webhook(&self, webhook_id: &str) -> Result<serde_json::Value> {
self.get(&format!("/api/v1/webhooks/{webhook_id}")).await
}
pub async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
self.delete_req(&format!("/api/v1/webhooks/{webhook_id}"))
.await
}
pub async fn set_secret(&self, name: &str, value: &str) -> Result<()> {
let _: serde_json::Value = self
.post("/api/v1/secrets", json!({ "name": name, "value": value }))
.await?;
Ok(())
}
pub async fn list_secrets(&self) -> Result<Vec<String>> {
self.get("/api/v1/secrets").await
}
pub async fn delete_secret(&self, name: &str) -> Result<()> {
self.delete_req(&format!("/api/v1/secrets/{name}")).await
}
pub async fn create_schedule(
&self,
workflow_id: &str,
cron: &str,
context: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let mut body = json!({ "cron": cron });
if let Some(ctx) = context {
body["context"] = ctx;
}
self.post(&format!("/api/v1/workflows/{workflow_id}/schedules"), body)
.await
}
pub async fn list_schedules(&self, workflow_id: &str) -> Result<Vec<serde_json::Value>> {
self.get(&format!("/api/v1/workflows/{workflow_id}/schedules"))
.await
}
pub async fn delete_schedule(&self, schedule_id: &str) -> Result<()> {
self.delete_req(&format!("/api/v1/schedules/{schedule_id}"))
.await
}
}