systemprompt_cloud/api_client/
methods.rs1use std::time::{Duration, Instant};
17
18use serde::de::DeserializeOwned;
19use serde::{Deserialize, Serialize};
20
21use super::CloudApiClient;
22use crate::error::{CloudError, CloudResult};
23
24const TOKEN_REFRESH_MARGIN: Duration = Duration::from_secs(30);
25const RFC8693_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:token-exchange";
26const RFC8693_TOKEN_TYPE: &str = "urn:ietf:params:oauth:token-type:access_token";
27const PUBLIC_CLIENT_ID: &str = "sp_web";
28
29#[derive(Debug, Deserialize)]
30struct TokenExchangeResponse {
31 access_token: String,
32 #[serde(default)]
33 expires_in: Option<u64>,
34}
35
36impl CloudApiClient {
37 pub(super) async fn get<T: DeserializeOwned>(&self, path: &str) -> CloudResult<T> {
38 let url = format!("{}{}", self.api_url, path);
39 let response = self
40 .client
41 .get(&url)
42 .header("Authorization", format!("Bearer {}", self.token))
43 .send()
44 .await?;
45 self.handle_response(response).await
46 }
47
48 pub async fn tenant_session_token(&self) -> CloudResult<String> {
49 self.tenant_access_token().await
50 }
51
52 async fn tenant_access_token(&self) -> CloudResult<String> {
53 {
54 let cached = self.tenant_token_cache.lock().await;
55 if let Some((token, expires_at)) = cached.as_ref()
56 && *expires_at > Instant::now() + TOKEN_REFRESH_MARGIN
57 {
58 return Ok(token.clone());
59 }
60 }
61 self.exchange_token().await
62 }
63
64 async fn exchange_token(&self) -> CloudResult<String> {
65 let url = format!("{}/api/v1/core/oauth/token", self.api_url);
66 let response = self
67 .client
68 .post(&url)
69 .form(&[
70 ("grant_type", RFC8693_GRANT_TYPE),
71 ("subject_token", self.token.as_str()),
72 ("subject_token_type", RFC8693_TOKEN_TYPE),
73 ("resource", self.api_url.as_str()),
74 ("client_id", PUBLIC_CLIENT_ID),
75 ])
76 .send()
77 .await?;
78
79 let status = response.status();
80 if status == reqwest::StatusCode::UNAUTHORIZED {
81 return Err(CloudError::Unauthorized);
82 }
83 if !status.is_success() {
84 let body = response
85 .text()
86 .await
87 .unwrap_or_else(|e| format!("<unreadable body: {e}>"));
88 return Err(CloudError::HttpStatus {
89 status: status.as_u16(),
90 body: body.chars().take(500).collect(),
91 });
92 }
93
94 let parsed: TokenExchangeResponse = response.json().await?;
95 let lifetime = parsed
96 .expires_in
97 .map_or_else(|| Duration::from_secs(300), Duration::from_secs);
98 let expires_at = Instant::now() + lifetime;
99
100 let access_token = parsed.access_token;
101 {
102 let mut cached = self.tenant_token_cache.lock().await;
103 *cached = Some((access_token.clone(), expires_at));
104 }
105 Ok(access_token)
106 }
107
108 async fn invalidate_tenant_token(&self) {
109 let mut cached = self.tenant_token_cache.lock().await;
110 *cached = None;
111 }
112
113 pub(super) async fn tenant_get<T: DeserializeOwned>(&self, path: &str) -> CloudResult<T> {
114 let url = format!("{}{}", self.api_url, path);
115 let send = || async {
116 let bearer = self.tenant_access_token().await?;
117 self.client
118 .get(&url)
119 .header("Authorization", format!("Bearer {bearer}"))
120 .send()
121 .await
122 .map_err(CloudError::from)
123 };
124 let response = send().await?;
125 if response.status() == reqwest::StatusCode::UNAUTHORIZED {
126 self.invalidate_tenant_token().await;
127 let retry = send().await?;
128 return self.handle_response(retry).await;
129 }
130 self.handle_response(response).await
131 }
132
133 pub(super) async fn tenant_post<T: DeserializeOwned, B: Serialize + Sync>(
134 &self,
135 path: &str,
136 body: &B,
137 ) -> CloudResult<T> {
138 let url = format!("{}{}", self.api_url, path);
139 let send = || async {
140 let bearer = self.tenant_access_token().await?;
141 self.client
142 .post(&url)
143 .header("Authorization", format!("Bearer {bearer}"))
144 .json(body)
145 .send()
146 .await
147 .map_err(CloudError::from)
148 };
149 let response = send().await?;
150 if response.status() == reqwest::StatusCode::UNAUTHORIZED {
151 self.invalidate_tenant_token().await;
152 let retry = send().await?;
153 return self.handle_response(retry).await;
154 }
155 self.handle_response(response).await
156 }
157
158 pub(super) async fn tenant_put_no_content<B: Serialize + Sync>(
159 &self,
160 path: &str,
161 body: &B,
162 ) -> CloudResult<()> {
163 let url = format!("{}{}", self.api_url, path);
164 let send = || async {
165 let bearer = self.tenant_access_token().await?;
166 self.client
167 .put(&url)
168 .header("Authorization", format!("Bearer {bearer}"))
169 .json(body)
170 .send()
171 .await
172 .map_err(CloudError::from)
173 };
174 let response = send().await?;
175 if response.status() == reqwest::StatusCode::UNAUTHORIZED {
176 self.invalidate_tenant_token().await;
177 let retry = send().await?;
178 return self.handle_no_content_response(retry).await;
179 }
180 self.handle_no_content_response(response).await
181 }
182
183 pub(super) async fn tenant_delete(&self, path: &str) -> CloudResult<()> {
184 let url = format!("{}{}", self.api_url, path);
185 let send = || async {
186 let bearer = self.tenant_access_token().await?;
187 self.client
188 .delete(&url)
189 .header("Authorization", format!("Bearer {bearer}"))
190 .send()
191 .await
192 .map_err(CloudError::from)
193 };
194 let response = send().await?;
195 if response.status() == reqwest::StatusCode::UNAUTHORIZED {
196 self.invalidate_tenant_token().await;
197 let retry = send().await?;
198 return self.handle_no_content_response(retry).await;
199 }
200 self.handle_no_content_response(response).await
201 }
202
203 pub(super) async fn tenant_post_empty<T: DeserializeOwned>(
204 &self,
205 path: &str,
206 ) -> CloudResult<T> {
207 let url = format!("{}{}", self.api_url, path);
208 let send = || async {
209 let bearer = self.tenant_access_token().await?;
210 self.client
211 .post(&url)
212 .header("Authorization", format!("Bearer {bearer}"))
213 .send()
214 .await
215 .map_err(CloudError::from)
216 };
217 let response = send().await?;
218 if response.status() == reqwest::StatusCode::UNAUTHORIZED {
219 self.invalidate_tenant_token().await;
220 let retry = send().await?;
221 return self.handle_response(retry).await;
222 }
223 self.handle_response(response).await
224 }
225}