Skip to main content

rootcx_client/
lib.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use rootcx_types::{AppManifest, InstalledApp, OsStatus, SchemaVerification};
5use serde_json::Value as JsonValue;
6
7#[cfg(feature = "tauri")]
8pub mod oidc;
9
10#[derive(Debug, thiserror::Error)]
11pub enum ClientError {
12    #[error("HTTP request failed: {0}")]
13    Http(#[from] reqwest::Error),
14
15    #[error("API error ({status}): {message}")]
16    Api { status: u16, message: String },
17}
18
19#[derive(Clone)]
20pub struct RuntimeClient {
21    base_url: String,
22    client: reqwest::Client,
23    token: Arc<std::sync::RwLock<Option<String>>>,
24}
25
26impl RuntimeClient {
27    pub fn new(base_url: &str) -> Self {
28        Self {
29            base_url: base_url.trim_end_matches('/').to_string(),
30            client: reqwest::Client::new(),
31            token: Arc::new(std::sync::RwLock::new(None)),
32        }
33    }
34
35    fn api(&self, path: &str) -> String {
36        format!("{}/api/v1{path}", self.base_url)
37    }
38
39    fn authed(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
40        if let Some(ref t) = *self.token.read().unwrap() { req.bearer_auth(t) } else { req }
41    }
42
43    pub fn set_token(&self, token: Option<String>) {
44        *self.token.write().unwrap() = token;
45    }
46
47    pub fn base_url(&self) -> &str {
48        &self.base_url
49    }
50
51    pub fn token(&self) -> Option<String> {
52        self.token.read().unwrap().clone()
53    }
54
55    pub async fn is_available(&self) -> bool {
56        self.client.get(format!("{}/health", self.base_url)).send().await.is_ok()
57    }
58
59    pub async fn status(&self) -> Result<OsStatus, ClientError> {
60        let resp = self.authed(self.client.get(self.api("/status"))).send().await?;
61        check_response(resp).await?.json().await.map_err(Into::into)
62    }
63
64    pub async fn me(&self) -> Result<JsonValue, ClientError> {
65        let resp = self.authed(self.client.get(self.api("/auth/me"))).send().await?;
66        check_response(resp).await?.json().await.map_err(Into::into)
67    }
68
69    pub async fn list_all_agents(&self) -> Result<Vec<JsonValue>, ClientError> {
70        let resp = self.authed(self.client.get(self.api("/agents"))).send().await?;
71        check_response(resp).await?.json().await.map_err(Into::into)
72    }
73
74    pub async fn list_agent_sessions(&self, app_id: &str) -> Result<Vec<JsonValue>, ClientError> {
75        let resp = self.authed(self.client.get(self.api(&format!("/apps/{app_id}/agent/sessions")))).send().await?;
76        check_response(resp).await?.json().await.map_err(Into::into)
77    }
78
79    pub async fn install_app(&self, manifest: &AppManifest) -> Result<String, ClientError> {
80        let resp = self.authed(self.client.post(self.api("/apps"))).json(manifest).send().await?;
81        extract_message(resp).await
82    }
83
84    pub async fn list_apps(&self) -> Result<Vec<InstalledApp>, ClientError> {
85        let resp = self.authed(self.client.get(self.api("/apps"))).send().await?;
86        check_response(resp).await?.json().await.map_err(Into::into)
87    }
88
89    pub async fn uninstall_app(&self, app_id: &str) -> Result<(), ClientError> {
90        let resp = self.authed(self.client.delete(self.api(&format!("/apps/{app_id}")))).send().await?;
91        check_response(resp).await?;
92        Ok(())
93    }
94
95    pub async fn list_records(&self, app_id: &str, entity: &str) -> Result<Vec<JsonValue>, ClientError> {
96        let resp = self.authed(self.client.get(self.api(&format!("/apps/{app_id}/collections/{entity}")))).send().await?;
97        check_response(resp).await?.json().await.map_err(Into::into)
98    }
99
100    pub async fn create_record(&self, app_id: &str, entity: &str, data: &JsonValue) -> Result<JsonValue, ClientError> {
101        let resp = self
102            .authed(self.client.post(self.api(&format!("/apps/{app_id}/collections/{entity}"))))
103            .json(data)
104            .send()
105            .await?;
106        check_response(resp).await?.json().await.map_err(Into::into)
107    }
108
109    pub async fn bulk_create_records(&self, app_id: &str, entity: &str, data: &[JsonValue]) -> Result<Vec<JsonValue>, ClientError> {
110        let resp = self
111            .authed(self.client.post(self.api(&format!("/apps/{app_id}/collections/{entity}/bulk"))))
112            .json(&data)
113            .send()
114            .await?;
115        check_response(resp).await?.json().await.map_err(Into::into)
116    }
117
118    pub async fn get_record(&self, app_id: &str, entity: &str, id: &str) -> Result<JsonValue, ClientError> {
119        let resp = self
120            .authed(self.client.get(self.api(&format!("/apps/{app_id}/collections/{entity}/{id}"))))
121            .send()
122            .await?;
123        check_response(resp).await?.json().await.map_err(Into::into)
124    }
125
126    pub async fn update_record(
127        &self,
128        app_id: &str,
129        entity: &str,
130        id: &str,
131        data: &JsonValue,
132    ) -> Result<JsonValue, ClientError> {
133        let resp = self
134            .authed(self.client.patch(self.api(&format!("/apps/{app_id}/collections/{entity}/{id}"))))
135            .json(data)
136            .send()
137            .await?;
138        check_response(resp).await?.json().await.map_err(Into::into)
139    }
140
141    pub async fn delete_record(&self, app_id: &str, entity: &str, id: &str) -> Result<(), ClientError> {
142        let resp = self
143            .authed(self.client.delete(self.api(&format!("/apps/{app_id}/collections/{entity}/{id}"))))
144            .send()
145            .await?;
146        check_response(resp).await?;
147        Ok(())
148    }
149
150    pub async fn verify_schema(&self, manifest: &AppManifest) -> Result<SchemaVerification, ClientError> {
151        let resp = self.authed(self.client.post(self.api("/apps/schema/verify"))).json(manifest).send().await?;
152        check_response(resp).await?.json().await.map_err(Into::into)
153    }
154
155    pub async fn deploy_app(&self, app_id: &str, archive: Vec<u8>) -> Result<String, ClientError> {
156        self.upload_archive(&format!("/apps/{app_id}/deploy"), archive).await
157    }
158
159    pub async fn deploy_frontend(&self, app_id: &str, archive: Vec<u8>) -> Result<String, ClientError> {
160        self.upload_archive(&format!("/apps/{app_id}/frontend"), archive).await
161    }
162
163    async fn upload_archive(&self, path: &str, archive: Vec<u8>) -> Result<String, ClientError> {
164        let part = reqwest::multipart::Part::bytes(archive)
165            .mime_str("application/gzip")
166            .map_err(ClientError::Http)?;
167        let form = reqwest::multipart::Form::new().part("archive", part);
168        let resp = self.authed(self.client.post(self.api(path))).multipart(form).send().await?;
169        extract_message(resp).await
170    }
171
172    pub async fn start_worker(&self, app_id: &str) -> Result<String, ClientError> {
173        self.worker_action(app_id, "start").await
174    }
175
176    pub async fn stop_worker(&self, app_id: &str) -> Result<String, ClientError> {
177        self.worker_action(app_id, "stop").await
178    }
179
180    pub async fn worker_status(&self, app_id: &str) -> Result<String, ClientError> {
181        let resp = self.authed(self.client.get(self.api(&format!("/apps/{app_id}/worker/status")))).send().await?;
182        let body: JsonValue = check_response(resp).await?.json().await?;
183        Ok(body["status"].as_str().unwrap_or("unknown").to_string())
184    }
185
186    pub async fn list_integrations(&self) -> Result<Vec<JsonValue>, ClientError> {
187        let resp = self.authed(self.client.get(self.api("/integrations"))).send().await?;
188        check_response(resp).await?.json().await.map_err(Into::into)
189    }
190
191    pub async fn get_forge_config(&self) -> Result<JsonValue, ClientError> {
192        let resp = self.authed(self.client.get(self.api("/config/ai/forge"))).send().await?;
193        check_response(resp).await?.json().await.map_err(Into::into)
194    }
195
196    pub async fn get_platform_env(&self) -> Result<HashMap<String, String>, ClientError> {
197        let resp = self.authed(self.client.get(self.api("/platform/secrets/env"))).send().await?;
198        let body: HashMap<String, String> = check_response(resp).await?.json().await?;
199        Ok(body)
200    }
201
202    pub async fn list_platform_secrets(&self) -> Result<Vec<String>, ClientError> {
203        let resp = self.authed(self.client.get(self.api("/platform/secrets"))).send().await?;
204        check_response(resp).await?.json().await.map_err(Into::into)
205    }
206
207    pub async fn set_platform_secret(&self, key: &str, value: &str) -> Result<(), ClientError> {
208        let body = serde_json::json!({ "key": key, "value": value });
209        let resp = self.authed(self.client.post(self.api("/platform/secrets"))).json(&body).send().await?;
210        check_response(resp).await?;
211        Ok(())
212    }
213
214    pub async fn delete_platform_secret(&self, key: &str) -> Result<(), ClientError> {
215        let resp = self.authed(self.client.delete(self.api(&format!("/platform/secrets/{key}")))).send().await?;
216        check_response(resp).await?;
217        Ok(())
218    }
219
220    async fn worker_action(&self, app_id: &str, action: &str) -> Result<String, ClientError> {
221        let resp = self
222            .authed(self.client.post(self.api(&format!("/apps/{app_id}/worker/{action}"))))
223            .send()
224            .await?;
225        extract_message(resp).await
226    }
227}
228
229async fn extract_message(resp: reqwest::Response) -> Result<String, ClientError> {
230    let body: JsonValue = check_response(resp).await?.json().await?;
231    Ok(body["message"].as_str().unwrap_or("ok").to_string())
232}
233
234async fn check_response(resp: reqwest::Response) -> Result<reqwest::Response, ClientError> {
235    if resp.status().is_success() {
236        return Ok(resp);
237    }
238    let status = resp.status().as_u16();
239    let body: JsonValue = resp.json().await.unwrap_or_default();
240    let message = body["error"].as_str().unwrap_or("unknown error").to_string();
241    Err(ClientError::Api { status, message })
242}