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 get_app(&self, app_id: &str) -> Result<JsonValue, ClientError> {
96 let resp = self.authed(self.client.get(self.api(&format!("/apps/{app_id}")))).send().await?;
97 check_response(resp).await?.json().await.map_err(Into::into)
98 }
99
100 pub async fn query_records(&self, app_id: &str, entity: &str, body: &JsonValue) -> Result<JsonValue, ClientError> {
101 let resp = self
102 .authed(self.client.post(self.api(&format!("/apps/{app_id}/collections/{entity}/query"))))
103 .json(body)
104 .send()
105 .await?;
106 check_response(resp).await?.json().await.map_err(Into::into)
107 }
108
109 pub async fn list_records(&self, app_id: &str, entity: &str) -> Result<Vec<JsonValue>, ClientError> {
110 let resp = self.authed(self.client.get(self.api(&format!("/apps/{app_id}/collections/{entity}")))).send().await?;
111 check_response(resp).await?.json().await.map_err(Into::into)
112 }
113
114 pub async fn create_record(&self, app_id: &str, entity: &str, data: &JsonValue) -> Result<JsonValue, ClientError> {
115 let resp = self
116 .authed(self.client.post(self.api(&format!("/apps/{app_id}/collections/{entity}"))))
117 .json(data)
118 .send()
119 .await?;
120 check_response(resp).await?.json().await.map_err(Into::into)
121 }
122
123 pub async fn bulk_create_records(&self, app_id: &str, entity: &str, data: &[JsonValue]) -> Result<Vec<JsonValue>, ClientError> {
124 let resp = self
125 .authed(self.client.post(self.api(&format!("/apps/{app_id}/collections/{entity}/bulk"))))
126 .json(&data)
127 .send()
128 .await?;
129 check_response(resp).await?.json().await.map_err(Into::into)
130 }
131
132 pub async fn get_record(&self, app_id: &str, entity: &str, id: &str) -> Result<JsonValue, ClientError> {
133 let resp = self
134 .authed(self.client.get(self.api(&format!("/apps/{app_id}/collections/{entity}/{id}"))))
135 .send()
136 .await?;
137 check_response(resp).await?.json().await.map_err(Into::into)
138 }
139
140 pub async fn update_record(
141 &self,
142 app_id: &str,
143 entity: &str,
144 id: &str,
145 data: &JsonValue,
146 ) -> Result<JsonValue, ClientError> {
147 let resp = self
148 .authed(self.client.patch(self.api(&format!("/apps/{app_id}/collections/{entity}/{id}"))))
149 .json(data)
150 .send()
151 .await?;
152 check_response(resp).await?.json().await.map_err(Into::into)
153 }
154
155 pub async fn delete_record(&self, app_id: &str, entity: &str, id: &str) -> Result<(), ClientError> {
156 let resp = self
157 .authed(self.client.delete(self.api(&format!("/apps/{app_id}/collections/{entity}/{id}"))))
158 .send()
159 .await?;
160 check_response(resp).await?;
161 Ok(())
162 }
163
164 pub async fn verify_schema(&self, manifest: &AppManifest) -> Result<SchemaVerification, ClientError> {
165 let resp = self.authed(self.client.post(self.api("/apps/schema/verify"))).json(manifest).send().await?;
166 check_response(resp).await?.json().await.map_err(Into::into)
167 }
168
169 pub async fn deploy_app(&self, app_id: &str, archive: Vec<u8>) -> Result<String, ClientError> {
170 self.upload_archive(&format!("/apps/{app_id}/deploy"), archive).await
171 }
172
173 pub async fn deploy_frontend(&self, app_id: &str, archive: Vec<u8>) -> Result<String, ClientError> {
174 self.upload_archive(&format!("/apps/{app_id}/frontend"), archive).await
175 }
176
177 async fn upload_archive(&self, path: &str, archive: Vec<u8>) -> Result<String, ClientError> {
178 let part = reqwest::multipart::Part::bytes(archive)
179 .mime_str("application/gzip")
180 .map_err(ClientError::Http)?;
181 let form = reqwest::multipart::Form::new().part("archive", part);
182 let resp = self.authed(self.client.post(self.api(path))).multipart(form).send().await?;
183 extract_message(resp).await
184 }
185
186 pub async fn start_worker(&self, app_id: &str) -> Result<String, ClientError> {
187 self.worker_action(app_id, "start").await
188 }
189
190 pub async fn stop_worker(&self, app_id: &str) -> Result<String, ClientError> {
191 self.worker_action(app_id, "stop").await
192 }
193
194 pub async fn worker_status(&self, app_id: &str) -> Result<String, ClientError> {
195 let resp = self.authed(self.client.get(self.api(&format!("/apps/{app_id}/worker/status")))).send().await?;
196 let body: JsonValue = check_response(resp).await?.json().await?;
197 Ok(body["status"].as_str().unwrap_or("unknown").to_string())
198 }
199
200 pub async fn list_integrations(&self) -> Result<Vec<JsonValue>, ClientError> {
201 let resp = self.authed(self.client.get(self.api("/integrations"))).send().await?;
202 check_response(resp).await?.json().await.map_err(Into::into)
203 }
204
205 pub async fn get_forge_config(&self) -> Result<JsonValue, ClientError> {
206 let resp = self.authed(self.client.get(self.api("/config/ai/forge"))).send().await?;
207 check_response(resp).await?.json().await.map_err(Into::into)
208 }
209
210 pub async fn get_platform_env(&self) -> Result<HashMap<String, String>, ClientError> {
211 let resp = self.authed(self.client.get(self.api("/platform/secrets/env"))).send().await?;
212 let body: HashMap<String, String> = check_response(resp).await?.json().await?;
213 Ok(body)
214 }
215
216 pub async fn list_platform_secrets(&self) -> Result<Vec<String>, ClientError> {
217 let resp = self.authed(self.client.get(self.api("/platform/secrets"))).send().await?;
218 check_response(resp).await?.json().await.map_err(Into::into)
219 }
220
221 pub async fn set_platform_secret(&self, key: &str, value: &str) -> Result<(), ClientError> {
222 let body = serde_json::json!({ "key": key, "value": value });
223 let resp = self.authed(self.client.post(self.api("/platform/secrets"))).json(&body).send().await?;
224 check_response(resp).await?;
225 Ok(())
226 }
227
228 pub async fn delete_platform_secret(&self, key: &str) -> Result<(), ClientError> {
229 let resp = self.authed(self.client.delete(self.api(&format!("/platform/secrets/{key}")))).send().await?;
230 check_response(resp).await?;
231 Ok(())
232 }
233
234 async fn worker_action(&self, app_id: &str, action: &str) -> Result<String, ClientError> {
235 let resp = self
236 .authed(self.client.post(self.api(&format!("/apps/{app_id}/worker/{action}"))))
237 .send()
238 .await?;
239 extract_message(resp).await
240 }
241}
242
243async fn extract_message(resp: reqwest::Response) -> Result<String, ClientError> {
244 let body: JsonValue = check_response(resp).await?.json().await?;
245 Ok(body["message"].as_str().unwrap_or("ok").to_string())
246}
247
248async fn check_response(resp: reqwest::Response) -> Result<reqwest::Response, ClientError> {
249 if resp.status().is_success() {
250 return Ok(resp);
251 }
252 let status = resp.status().as_u16();
253 let body: JsonValue = resp.json().await.unwrap_or_default();
254 let message = body["error"].as_str().unwrap_or("unknown error").to_string();
255 Err(ClientError::Api { status, message })
256}