1use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
12use serde::{Deserialize, Serialize};
13use serde_json::json;
14use std::time::Duration;
15
16#[cfg(not(target_arch = "wasm32"))]
17use std::time::Instant;
18#[cfg(target_arch = "wasm32")]
19use web_time::Instant;
20
21pub mod sse;
22pub use sse::SseEvent;
23
24async fn sleep(d: Duration) {
27 #[cfg(not(target_arch = "wasm32"))]
28 {
29 tokio::time::sleep(d).await;
30 }
31 #[cfg(target_arch = "wasm32")]
32 {
33 gloo_timers::future::TimeoutFuture::new(d.as_millis() as u32).await;
34 }
35}
36
37#[derive(Debug, thiserror::Error)]
38pub enum FabricError {
39 #[error("HTTP error: {0}")]
40 Http(#[from] reqwest::Error),
41 #[error("API error ({code}): {message}")]
42 Api { code: String, message: String },
43 #[error("{0}")]
44 Other(String),
45}
46
47pub type Result<T> = std::result::Result<T, FabricError>;
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct RunArtifact {
60 pub id: String,
61 pub run_id: String,
62 pub asset_id: Option<String>,
63 pub task_id: String,
64 pub filename: String,
65 pub content_type: Option<String>,
66 pub produced_by: Option<String>,
67 pub consumed_from: Option<Vec<String>>,
68 pub metadata: Option<serde_json::Value>,
69 pub artifact_type: String,
70 #[serde(default)]
72 pub variant_index: i16,
73 pub created_at: Option<String>,
74 pub download_url: Option<String>,
75 pub download_url_expires_at: Option<String>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
83#[serde(rename_all = "lowercase")]
84pub enum VariantKind {
85 Video,
86 Carousel,
87 Image,
88 Text,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct VariantOutput {
94 pub variant_index: i16,
95 #[serde(default)]
99 pub workflow_name: Option<String>,
100 #[serde(default)]
104 pub kind: Option<VariantKind>,
105 pub output: Option<serde_json::Value>,
106 #[serde(default)]
107 pub artifacts: Vec<RunArtifact>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct BundleEntry {
113 pub workflow: String,
115 pub input: serde_json::Value,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
126#[serde(rename_all = "kebab-case")]
127pub enum RegenerateDirection {
128 Punchier,
129 Deeper,
130 Contrarian,
131 Visual,
132 DataFirst,
133 Surprise,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
138#[serde(rename_all = "snake_case")]
139pub enum RegenerateKeepFlag {
140 Platform,
141 Format,
142 CoreTopic,
143 ToneOfVoice,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct Regenerate {
154 pub direction: RegenerateDirection,
155 #[serde(default, skip_serializing_if = "Vec::is_empty")]
156 pub keep: Vec<RegenerateKeepFlag>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub extra_instructions: Option<String>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub parent_run_id: Option<String>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub parent_variant_index: Option<u16>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct RunOutput {
171 pub run_id: String,
172 pub status: String,
173 pub error: Option<String>,
174 #[serde(default)]
175 pub nodes: Vec<serde_json::Value>,
176 #[serde(default = "default_variants")]
177 pub variants: i16,
178 #[serde(default)]
179 pub outputs: Vec<VariantOutput>,
180}
181
182fn default_variants() -> i16 {
183 1
184}
185
186impl RunOutput {
187 pub fn first_output(&self) -> Option<&serde_json::Value> {
189 self.outputs.first().and_then(|v| v.output.as_ref())
190 }
191
192 pub fn all_artifacts(&self) -> impl Iterator<Item = &RunArtifact> {
194 self.outputs.iter().flat_map(|v| v.artifacts.iter())
195 }
196}
197
198pub struct FabricClient {
207 client: reqwest::Client,
208 base_url: String,
209 organization_id: String,
210 team_id: Option<String>,
211 user_id: Option<String>,
212}
213
214impl FabricClient {
215 pub fn new(base_url: &str, api_key: &str) -> Result<Self> {
217 let mut headers = HeaderMap::new();
218 headers.insert(
219 AUTHORIZATION,
220 HeaderValue::from_str(&format!("Bearer {api_key}"))
221 .map_err(|e| FabricError::Other(e.to_string()))?,
222 );
223 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
224
225 let client = reqwest::Client::builder()
226 .default_headers(headers)
227 .build()?;
228
229 Ok(Self {
230 client,
231 base_url: base_url.trim_end_matches('/').to_string(),
232 organization_id: String::new(),
233 team_id: None,
234 user_id: None,
235 })
236 }
237
238 pub fn with_principal(base_url: &str, principal_id: &str) -> Result<Self> {
240 let mut headers = HeaderMap::new();
241 headers.insert(
242 "X-Principal-Id",
243 HeaderValue::from_str(principal_id).map_err(|e| FabricError::Other(e.to_string()))?,
244 );
245 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
246
247 let client = reqwest::Client::builder()
248 .default_headers(headers)
249 .build()?;
250
251 Ok(Self {
252 client,
253 base_url: base_url.trim_end_matches('/').to_string(),
254 organization_id: String::new(),
255 team_id: None,
256 user_id: None,
257 })
258 }
259
260 pub fn set_organization_id(&mut self, org_id: &str) {
262 self.organization_id = org_id.to_string();
263 }
264
265 pub fn set_team_id(&mut self, team_id: Option<&str>) {
267 self.team_id = team_id.map(str::to_string);
268 }
269
270 pub fn set_user_id(&mut self, user_id: Option<&str>) {
273 self.user_id = user_id.map(str::to_string);
274 }
275
276 fn resolve_org_id(&self, org_id: Option<&str>) -> Result<String> {
278 org_id
279 .map(str::to_string)
280 .or_else(|| {
281 if self.organization_id.is_empty() {
282 None
283 } else {
284 Some(self.organization_id.clone())
285 }
286 })
287 .ok_or_else(|| {
288 FabricError::Other(
289 "Organization ID required — pass it or set it on the client".to_string(),
290 )
291 })
292 }
293
294 async fn request<T: serde::de::DeserializeOwned>(
297 &self,
298 method: reqwest::Method,
299 path: &str,
300 body: Option<serde_json::Value>,
301 ) -> Result<T> {
302 let url = format!("{}{path}", self.base_url);
303 let mut req = self.client.request(method, &url);
304 if let Some(b) = body {
305 req = req.json(&b);
306 }
307 let resp = req.send().await?;
308 let status = resp.status();
309 let json: serde_json::Value = resp.json().await?;
310
311 if let Some(err) = json.get("error") {
312 return Err(FabricError::Api {
313 code: status.as_u16().to_string(),
314 message: err
315 .as_str()
316 .map(|s| s.to_string())
317 .unwrap_or_else(|| err.to_string()),
318 });
319 }
320
321 if let Some(data) = json.get("data") {
322 serde_json::from_value(data.clone())
323 .map_err(|e| FabricError::Other(format!("Failed to deserialize data: {e}")))
324 } else {
325 serde_json::from_value(json)
326 .map_err(|e| FabricError::Other(format!("Failed to deserialize response: {e}")))
327 }
328 }
329
330 async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
331 self.request(reqwest::Method::GET, path, None).await
332 }
333
334 async fn post<T: serde::de::DeserializeOwned>(
335 &self,
336 path: &str,
337 body: serde_json::Value,
338 ) -> Result<T> {
339 self.request(reqwest::Method::POST, path, Some(body)).await
340 }
341
342 async fn post_empty(&self, path: &str) -> Result<()> {
343 let url = format!("{}{path}", self.base_url);
344 let resp = self.client.post(&url).send().await?;
345 let status = resp.status();
346 let json: serde_json::Value = resp.json().await?;
347
348 if let Some(err) = json.get("error") {
349 return Err(FabricError::Api {
350 code: status.as_u16().to_string(),
351 message: err
352 .as_str()
353 .map(|s| s.to_string())
354 .unwrap_or_else(|| err.to_string()),
355 });
356 }
357 Ok(())
358 }
359
360 async fn delete_req(&self, path: &str) -> Result<()> {
361 let url = format!("{}{path}", self.base_url);
362 let resp = self.client.delete(&url).send().await?;
363 let status = resp.status();
364 let json: serde_json::Value = resp.json().await?;
365
366 if let Some(err) = json.get("error") {
367 return Err(FabricError::Api {
368 code: status.as_u16().to_string(),
369 message: err
370 .as_str()
371 .map(|s| s.to_string())
372 .unwrap_or_else(|| err.to_string()),
373 });
374 }
375 Ok(())
376 }
377
378 pub async fn health_check(&self) -> Result<serde_json::Value> {
381 self.get("/health").await
382 }
383
384 pub async fn system_status(&self) -> Result<serde_json::Value> {
385 self.get("/v1/system/status").await
386 }
387
388 pub async fn get_me(&self) -> Result<serde_json::Value> {
391 self.get("/v1/me").await
392 }
393
394 pub async fn get_my_organizations(&self) -> Result<Vec<serde_json::Value>> {
395 self.get("/v1/me/organizations").await
396 }
397
398 pub async fn get_my_teams(&self) -> Result<Vec<serde_json::Value>> {
399 self.get("/v1/me/teams").await
400 }
401
402 pub async fn get_my_permissions(&self) -> Result<Vec<serde_json::Value>> {
403 self.get("/v1/me/permissions").await
404 }
405
406 pub async fn create_organization(&self, slug: &str, name: &str) -> Result<serde_json::Value> {
409 self.post("/v1/organizations", json!({ "slug": slug, "name": name }))
410 .await
411 }
412
413 pub async fn list_organizations(&self) -> Result<Vec<serde_json::Value>> {
414 self.get("/v1/organizations").await
415 }
416
417 pub async fn get_organization(&self, org_id: Option<&str>) -> Result<serde_json::Value> {
419 let id = self.resolve_org_id(org_id)?;
420 self.get(&format!("/v1/organizations/{id}")).await
421 }
422
423 pub async fn list_org_teams(&self, org_id: Option<&str>) -> Result<Vec<serde_json::Value>> {
425 let id = self.resolve_org_id(org_id)?;
426 self.get(&format!("/v1/organizations/{id}/teams")).await
427 }
428
429 pub async fn list_org_members(&self, org_id: Option<&str>) -> Result<Vec<serde_json::Value>> {
431 let id = self.resolve_org_id(org_id)?;
432 self.get(&format!("/v1/organizations/{id}/members")).await
433 }
434
435 pub async fn create_team(
439 &self,
440 org_id: Option<&str>,
441 slug: &str,
442 name: &str,
443 ) -> Result<serde_json::Value> {
444 let id = self.resolve_org_id(org_id)?;
445 self.post(
446 &format!("/v1/organizations/{id}/teams"),
447 json!({ "slug": slug, "name": name }),
448 )
449 .await
450 }
451
452 pub async fn get_team(&self, team_id: &str) -> Result<serde_json::Value> {
453 self.get(&format!("/v1/teams/{team_id}")).await
454 }
455
456 pub async fn create_invitation(
460 &self,
461 org_id: Option<&str>,
462 email: &str,
463 role: &str,
464 ) -> Result<serde_json::Value> {
465 let id = self.resolve_org_id(org_id)?;
466 self.post(
467 &format!("/v1/organizations/{id}/invitations"),
468 json!({ "email": email, "role": role }),
469 )
470 .await
471 }
472
473 pub async fn accept_invitation(&self, invitation_id: &str) -> Result<()> {
474 self.post_empty(&format!("/v1/invitations/{invitation_id}/accept"))
475 .await
476 }
477
478 pub async fn revoke_invitation(&self, invitation_id: &str) -> Result<()> {
480 self.post_empty(&format!("/v1/invitations/{invitation_id}/revoke"))
481 .await
482 }
483
484 pub async fn check_permission(&self, action: &str, resource: Option<&str>) -> Result<bool> {
487 let mut body = json!({ "action": action });
488 if let Some(r) = resource {
489 body["resource"] = serde_json::Value::String(r.to_string());
490 }
491 let resp: serde_json::Value = self.post("/v1/authz/check", body).await?;
492 Ok(resp
493 .get("allowed")
494 .and_then(|v| v.as_bool())
495 .unwrap_or(false))
496 }
497
498 pub async fn check_permissions(
499 &self,
500 checks: Vec<serde_json::Value>,
501 ) -> Result<Vec<serde_json::Value>> {
502 self.post("/v1/authz/check-batch", json!({ "checks": checks }))
503 .await
504 }
505
506 pub async fn create_api_key(
510 &self,
511 name: &str,
512 org_id: Option<&str>,
513 scopes: Option<Vec<&str>>,
514 ) -> Result<serde_json::Value> {
515 let id = self.resolve_org_id(org_id)?;
516 let mut body = json!({ "name": name, "organization_id": id });
517 if let Some(s) = scopes {
518 body["scopes"] = serde_json::Value::from(s);
519 }
520 self.post("/v1/api-keys", body).await
521 }
522
523 pub async fn list_api_keys(&self) -> Result<Vec<serde_json::Value>> {
524 self.get("/v1/api-keys").await
525 }
526
527 pub async fn get_api_key(&self, key_id: &str) -> Result<serde_json::Value> {
528 self.get(&format!("/v1/api-keys/{key_id}")).await
529 }
530
531 pub async fn delete_api_key(&self, key_id: &str) -> Result<()> {
532 self.delete_req(&format!("/v1/api-keys/{key_id}")).await
533 }
534
535 pub async fn disable_api_key(&self, key_id: &str) -> Result<()> {
536 self.post_empty(&format!("/v1/api-keys/{key_id}/disable"))
537 .await
538 }
539
540 pub async fn rotate_api_key(&self, key_id: &str) -> Result<serde_json::Value> {
541 self.post(&format!("/v1/api-keys/{key_id}/rotate"), json!({}))
542 .await
543 }
544
545 pub async fn upsert_workflow(&self, name: &str, body: serde_json::Value) -> Result<String> {
553 let mut payload = body;
554 payload["name"] = serde_json::Value::String(name.to_string());
555 let resp: serde_json::Value = self.post("/v1/workflow-registry", payload).await?;
556 resp.get("id")
557 .and_then(|v| v.as_str())
558 .map(|s| s.to_string())
559 .ok_or_else(|| FabricError::Other("Missing workflow id in response".to_string()))
560 }
561
562 pub async fn list_workflows(&self) -> Result<Vec<serde_json::Value>> {
572 let mut params = vec![("limit", "500".to_string())];
573 if !self.organization_id.is_empty() {
574 params.push(("organization_id", self.organization_id.clone()));
575 if let Some(team) = &self.team_id {
576 params.push(("team_id", team.clone()));
577 }
578 }
579 let qs = params
580 .iter()
581 .map(|(k, v)| format!("{k}={v}"))
582 .collect::<Vec<_>>()
583 .join("&");
584 self.get(&format!("/v1/workflow-registry?{qs}")).await
585 }
586
587 pub async fn run_workflow(
592 &self,
593 workflow_name: &str,
594 input: serde_json::Value,
595 ) -> Result<String> {
596 self.run_workflow_opts(workflow_name, input, false).await
597 }
598
599 pub async fn run_workflow_validated(
604 &self,
605 workflow_name: &str,
606 input: serde_json::Value,
607 ) -> Result<String> {
608 self.run_workflow_opts(workflow_name, input, true).await
609 }
610
611 pub async fn run_workflow_with_variants(
620 &self,
621 workflow_name: &str,
622 input: serde_json::Value,
623 variants: u16,
624 ) -> Result<String> {
625 let mut input = input;
626 if let Some(obj) = input.as_object_mut() {
627 obj.entry("variants").or_insert_with(|| json!(variants));
628 }
629 self.run_workflow_opts(workflow_name, input, false).await
630 }
631
632 pub async fn run_regenerate(
640 &self,
641 workflow_name: &str,
642 input: serde_json::Value,
643 regenerate: Regenerate,
644 variants: Option<u16>,
645 ) -> Result<String> {
646 let mut input = input;
647 if let Some(obj) = input.as_object_mut() {
648 if let Some(v) = variants {
649 obj.entry("variants").or_insert_with(|| json!(v));
650 }
651 obj.insert(
652 "regenerate".to_string(),
653 serde_json::to_value(®enerate)
654 .map_err(|e| FabricError::Other(format!("serialize regenerate: {e}")))?,
655 );
656 }
657 self.run_workflow_opts(workflow_name, input, false).await
658 }
659
660 pub async fn run_bundle(&self, bundle: Vec<BundleEntry>) -> Result<String> {
673 let input = json!({ "bundle": bundle });
674 self.run_workflow_opts("bundle/ad-hoc", input, false).await
675 }
676
677 async fn run_workflow_opts(
678 &self,
679 workflow_name: &str,
680 input: serde_json::Value,
681 validate: bool,
682 ) -> Result<String> {
683 let mut path = format!("/v1/workflows/run?name={}", urlencode(workflow_name));
684 if !self.organization_id.is_empty() {
685 path.push_str(&format!("&organization_id={}", self.organization_id));
686 }
687 if let Some(team) = &self.team_id {
688 path.push_str(&format!("&team_id={team}"));
689 }
690 if validate {
691 path.push_str("&validate=true");
692 }
693 let resp: serde_json::Value = self.post(&path, json!({ "input": input })).await?;
694 resp.get("id")
695 .and_then(|v| v.as_str())
696 .map(|s| s.to_string())
697 .ok_or_else(|| FabricError::Other("Missing run id in response".to_string()))
698 }
699
700 pub async fn get_run(&self, run_id: &str) -> Result<serde_json::Value> {
701 self.get(&format!("/v1/workflows/runs/{run_id}")).await
702 }
703
704 pub async fn get_run_output(
722 &self,
723 run_id: &str,
724 expires_in_secs: Option<u32>,
725 ) -> Result<RunOutput> {
726 let path = match expires_in_secs {
727 Some(ttl) => format!("/v1/workflows/runs/{run_id}/output?expires_in={ttl}"),
728 None => format!("/v1/workflows/runs/{run_id}/output"),
729 };
730 self.get(&path).await
731 }
732
733 pub async fn get_workflow_schemas(&self, name: &str) -> Result<serde_json::Value> {
745 let mut path = format!("/v1/workflow-schemas/{}", urlencode(name));
746 if !self.organization_id.is_empty() {
747 path.push_str(&format!("?organization_id={}", self.organization_id));
748 }
749 self.get(&path).await
750 }
751
752 pub async fn cancel_run(&self, run_id: &str) -> Result<()> {
753 self.post_empty(&format!("/v1/workflows/runs/{run_id}/cancel"))
754 .await
755 }
756
757 pub async fn pause_run(&self, run_id: &str) -> Result<()> {
758 self.post_empty(&format!("/v1/workflows/runs/{run_id}/pause"))
759 .await
760 }
761
762 pub async fn resume_run(&self, run_id: &str) -> Result<()> {
763 self.post_empty(&format!("/v1/workflows/runs/{run_id}/resume"))
764 .await
765 }
766
767 pub async fn wait_for_run(&self, run_id: &str) -> Result<serde_json::Value> {
768 let timeout = Duration::from_secs(300);
769 let poll_interval = Duration::from_secs(2);
770 let start = Instant::now();
771
772 loop {
773 let run = self.get_run(run_id).await?;
774 if let Some("completed" | "failed" | "cancelled") =
775 run.get("status").and_then(|v| v.as_str())
776 {
777 return Ok(run);
778 }
779 if start.elapsed() >= timeout {
780 return Err(FabricError::Other(format!(
781 "Timed out waiting for run {run_id} after {timeout:?}"
782 )));
783 }
784 sleep(poll_interval).await;
785 }
786 }
787
788 pub async fn run_workflow_and_get_output(
797 &self,
798 workflow_name: &str,
799 input: serde_json::Value,
800 expires_in_secs: Option<u32>,
801 ) -> Result<RunOutput> {
802 let run_id = self.run_workflow(workflow_name, input).await?;
803 self.wait_for_run(&run_id).await?;
804 self.get_run_output(&run_id, expires_in_secs).await
805 }
806
807 pub async fn run_bundle_and_get_output(
810 &self,
811 bundle: Vec<BundleEntry>,
812 expires_in_secs: Option<u32>,
813 ) -> Result<RunOutput> {
814 let run_id = self.run_bundle(bundle).await?;
815 self.wait_for_run(&run_id).await?;
816 self.get_run_output(&run_id, expires_in_secs).await
817 }
818
819 pub async fn download_artifact(&self, download_url: &str) -> Result<Vec<u8>> {
825 let resp = self
826 .client
827 .get(download_url)
828 .send()
829 .await
830 .map_err(|e| FabricError::Other(format!("download request failed: {e}")))?;
831 if !resp.status().is_success() {
832 return Err(FabricError::Api {
833 code: resp.status().as_u16().to_string(),
834 message: format!("Failed to download artifact: {}", resp.status()),
835 });
836 }
837 resp.bytes()
838 .await
839 .map(|b| b.to_vec())
840 .map_err(|e| FabricError::Other(format!("download body read failed: {e}")))
841 }
842
843 pub async fn list_runs(
855 &self,
856 organization_id: Option<&str>,
857 team_id: Option<&str>,
858 created_by: Option<&str>,
859 limit: Option<i64>,
860 offset: Option<i64>,
861 ) -> Result<Vec<serde_json::Value>> {
862 let org = self.resolve_org_id(organization_id)?;
863
864 let team = team_id.map(str::to_string).or_else(|| self.team_id.clone());
865 let user = created_by
866 .map(str::to_string)
867 .or_else(|| self.user_id.clone());
868
869 let mut path = format!("/v1/workflows/runs?organization_id={}", urlencode(&org));
870 if let Some(t) = team.as_deref() {
871 path.push_str(&format!("&team_id={}", urlencode(t)));
872 }
873 if let Some(u) = user.as_deref() {
874 path.push_str(&format!("&created_by={}", urlencode(u)));
875 }
876 if let Some(l) = limit {
877 path.push_str(&format!("&limit={l}"));
878 }
879 if let Some(o) = offset {
880 path.push_str(&format!("&offset={o}"));
881 }
882 self.get(&path).await
883 }
884
885 pub async fn list_waiting_runs(
887 &self,
888 organization_id: Option<&str>,
889 team_id: Option<&str>,
890 created_by: Option<&str>,
891 ) -> Result<Vec<serde_json::Value>> {
892 let org = self.resolve_org_id(organization_id)?;
893
894 let team = team_id.map(str::to_string).or_else(|| self.team_id.clone());
895 let user = created_by
896 .map(str::to_string)
897 .or_else(|| self.user_id.clone());
898
899 let mut path = format!(
900 "/v1/workflows/runs/waiting?organization_id={}",
901 urlencode(&org)
902 );
903 if let Some(t) = team.as_deref() {
904 path.push_str(&format!("&team_id={}", urlencode(t)));
905 }
906 if let Some(u) = user.as_deref() {
907 path.push_str(&format!("&created_by={}", urlencode(u)));
908 }
909 self.get(&path).await
910 }
911
912 pub async fn cancel_run_with_reason(&self, run_id: &str, reason: Option<&str>) -> Result<()> {
914 let body = match reason {
915 Some(r) => json!({ "reason": r }),
916 None => json!({}),
917 };
918 let _: serde_json::Value = self
919 .post(&format!("/v1/workflows/runs/{run_id}/cancel"), body)
920 .await?;
921 Ok(())
922 }
923
924 pub async fn face_swap(
932 &self,
933 source_url: &str,
934 target_url: Option<&str>,
935 persona_gallery_id: Option<&str>,
936 ) -> Result<serde_json::Value> {
937 let mut input = json!({ "source_url": source_url });
938 if let Some(url) = target_url {
939 input["target_url"] = serde_json::Value::String(url.to_string());
940 }
941 if let Some(gid) = persona_gallery_id {
942 input["persona_gallery_id"] = serde_json::Value::String(gid.to_string());
943 }
944 let run_id = self.run_workflow("video/face-swap", input).await?;
945 self.wait_for_run(&run_id).await
946 }
947
948 pub async fn motion_transfer(
953 &self,
954 driving_video_url: &str,
955 source_image_url: Option<&str>,
956 persona_gallery_id: Option<&str>,
957 motion_model: Option<&str>,
958 ) -> Result<serde_json::Value> {
959 let mut input = json!({ "driving_video_url": driving_video_url });
960 if let Some(url) = source_image_url {
961 input["source_image_url"] = serde_json::Value::String(url.to_string());
962 }
963 if let Some(gid) = persona_gallery_id {
964 input["persona_gallery_id"] = serde_json::Value::String(gid.to_string());
965 }
966 if let Some(model) = motion_model {
967 input["motion_model"] = serde_json::Value::String(model.to_string());
968 }
969 let run_id = self.run_workflow("video/motion-transfer", input).await?;
970 self.wait_for_run(&run_id).await
971 }
972
973 pub async fn list_providers(&self) -> Result<Vec<serde_json::Value>> {
976 self.get("/v1/providers").await
977 }
978
979 pub async fn execute_provider(&self, body: serde_json::Value) -> Result<serde_json::Value> {
980 self.post("/v1/providers/execute", body).await
981 }
982
983 pub async fn estimate_cost(&self, body: serde_json::Value) -> Result<serde_json::Value> {
984 self.post("/v1/providers/estimate", body).await
985 }
986
987 pub async fn get_org_usage(&self, org_id: Option<&str>) -> Result<serde_json::Value> {
991 let id = self.resolve_org_id(org_id)?;
992 self.get(&format!("/v1/organizations/{id}/usage")).await
993 }
994
995 pub async fn get_org_usage_daily(
997 &self,
998 org_id: Option<&str>,
999 ) -> Result<Vec<serde_json::Value>> {
1000 let id = self.resolve_org_id(org_id)?;
1001 self.get(&format!("/v1/organizations/{id}/usage/daily"))
1002 .await
1003 }
1004
1005 pub async fn get_org_audit_logs(&self, org_id: Option<&str>) -> Result<Vec<serde_json::Value>> {
1007 let id = self.resolve_org_id(org_id)?;
1008 self.get(&format!("/v1/organizations/{id}/audit-logs"))
1009 .await
1010 }
1011
1012 pub async fn get_audit_logs(&self) -> Result<Vec<serde_json::Value>> {
1013 self.get("/v1/audit-logs").await
1014 }
1015
1016 pub async fn create_webhook(
1023 &self,
1024 org_id: Option<&str>,
1025 url: &str,
1026 events: Vec<&str>,
1027 ) -> Result<serde_json::Value> {
1028 let id = self.resolve_org_id(org_id)?;
1029 let body = json!({
1030 "url": url,
1031 "event_filter": events,
1032 });
1033 self.post(&format!("/v1/organizations/{id}/webhooks"), body)
1034 .await
1035 }
1036
1037 pub async fn list_webhooks(&self, org_id: Option<&str>) -> Result<Vec<serde_json::Value>> {
1039 let id = self.resolve_org_id(org_id)?;
1040 self.get(&format!("/v1/organizations/{id}/webhooks")).await
1041 }
1042
1043 pub async fn get_webhook(&self, webhook_id: &str) -> Result<serde_json::Value> {
1044 self.get(&format!("/v1/webhooks/{webhook_id}")).await
1045 }
1046
1047 pub async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
1048 self.delete_req(&format!("/v1/webhooks/{webhook_id}")).await
1049 }
1050
1051 pub async fn set_secret(&self, org_id: Option<&str>, name: &str, value: &str) -> Result<()> {
1055 let id = self.resolve_org_id(org_id)?;
1056 let _: serde_json::Value = self
1057 .post(
1058 &format!("/v1/organizations/{id}/secrets"),
1059 json!({ "name": name, "value": value }),
1060 )
1061 .await?;
1062 Ok(())
1063 }
1064
1065 pub async fn list_secrets(&self, org_id: Option<&str>) -> Result<Vec<String>> {
1067 let id = self.resolve_org_id(org_id)?;
1068 self.get(&format!("/v1/organizations/{id}/secrets")).await
1069 }
1070
1071 pub async fn delete_secret(&self, org_id: Option<&str>, name: &str) -> Result<()> {
1073 let id = self.resolve_org_id(org_id)?;
1074 self.delete_req(&format!("/v1/organizations/{id}/secrets/{name}"))
1075 .await
1076 }
1077
1078 pub async fn create_schedule(
1085 &self,
1086 workflow_definition_id: &str,
1087 cron: &str,
1088 input_context: Option<serde_json::Value>,
1089 ) -> Result<serde_json::Value> {
1090 let mut body = json!({ "cron_expression": cron });
1091 if let Some(ctx) = input_context {
1092 body["input_context"] = ctx;
1093 }
1094 self.post(
1095 &format!("/v1/workflow-definitions/{workflow_definition_id}/schedules"),
1096 body,
1097 )
1098 .await
1099 }
1100
1101 pub async fn list_schedules(
1102 &self,
1103 workflow_definition_id: &str,
1104 ) -> Result<Vec<serde_json::Value>> {
1105 self.get(&format!(
1106 "/v1/workflow-definitions/{workflow_definition_id}/schedules"
1107 ))
1108 .await
1109 }
1110
1111 pub async fn delete_schedule(&self, schedule_id: &str) -> Result<()> {
1112 self.delete_req(&format!("/v1/schedules/{schedule_id}"))
1113 .await
1114 }
1115}
1116
1117fn urlencode(s: &str) -> String {
1124 let mut out = String::with_capacity(s.len());
1125 for b in s.bytes() {
1126 match b {
1127 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1128 out.push(b as char);
1129 }
1130 _ => {
1131 out.push('%');
1132 out.push_str(&format!("{b:02X}"));
1133 }
1134 }
1135 }
1136 out
1137}