1use crate::error::{Error, Result};
4use crate::types::*;
5use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE, USER_AGENT};
6use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9use tokio::sync::RwLock;
10
11pub const DEFAULT_BASE_URL: &str = "https://api.keyenv.dev";
13
14pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
16
17pub const VERSION: &str = env!("CARGO_PKG_VERSION");
19
20const API_PREFIX: &str = "/api/v1";
22
23#[derive(Debug, Clone)]
25pub struct KeyEnvBuilder {
26 token: Option<String>,
27 base_url: String,
28 timeout: Duration,
29 cache_ttl: Duration,
30}
31
32impl Default for KeyEnvBuilder {
33 fn default() -> Self {
34 Self {
35 token: None,
36 base_url: DEFAULT_BASE_URL.to_string(),
37 timeout: DEFAULT_TIMEOUT,
38 cache_ttl: Duration::ZERO,
39 }
40 }
41}
42
43impl KeyEnvBuilder {
44 pub fn new() -> Self {
46 Self::default()
47 }
48
49 pub fn token(mut self, token: impl Into<String>) -> Self {
51 self.token = Some(token.into());
52 self
53 }
54
55 pub fn base_url(mut self, url: impl Into<String>) -> Self {
57 let u = url.into();
58 self.base_url = u
59 .trim_end_matches('/')
60 .trim_end_matches("/api/v1")
61 .to_string();
62 self
63 }
64
65 pub fn timeout(mut self, timeout: Duration) -> Self {
67 self.timeout = timeout;
68 self
69 }
70
71 pub fn cache_ttl(mut self, ttl: Duration) -> Self {
73 self.cache_ttl = ttl;
74 self
75 }
76
77 pub fn build(self) -> Result<KeyEnv> {
79 let token = self
80 .token
81 .ok_or_else(|| Error::config("token is required"))?;
82
83 let mut headers = HeaderMap::new();
84 headers.insert(
85 AUTHORIZATION,
86 HeaderValue::from_str(&format!("Bearer {}", token))
87 .map_err(|_| Error::config("invalid token"))?,
88 );
89 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
90 headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
91 headers.insert(
92 USER_AGENT,
93 HeaderValue::from_str(&format!("keyenv-rust/{}", VERSION))
94 .unwrap_or_else(|_| HeaderValue::from_static("keyenv-rust")),
95 );
96
97 let client = reqwest::Client::builder()
98 .default_headers(headers)
99 .timeout(self.timeout)
100 .build()?;
101
102 Ok(KeyEnv {
103 client,
104 base_url: self.base_url,
105 cache_ttl: self.cache_ttl,
106 cache: Arc::new(RwLock::new(HashMap::new())),
107 })
108 }
109}
110
111struct CacheEntry {
113 data: String, expires_at: Instant,
115}
116
117#[derive(Clone)]
119pub struct KeyEnv {
120 client: reqwest::Client,
121 base_url: String,
122 cache_ttl: Duration,
123 cache: Arc<RwLock<HashMap<String, CacheEntry>>>,
124}
125
126impl KeyEnv {
127 pub fn builder() -> KeyEnvBuilder {
129 KeyEnvBuilder::new()
130 }
131
132 async fn get_cached<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
134 if self.cache_ttl.is_zero() {
135 return None;
136 }
137
138 let cache = self.cache.read().await;
139 if let Some(entry) = cache.get(key) {
140 if Instant::now() < entry.expires_at {
141 return serde_json::from_str(&entry.data).ok();
142 }
143 }
144 drop(cache);
146
147 let mut cache = self.cache.write().await;
149 if let Some(entry) = cache.get(key) {
150 if Instant::now() >= entry.expires_at {
151 cache.remove(key);
152 }
153 }
154 None
155 }
156
157 async fn set_cached<T: serde::Serialize>(&self, key: &str, data: &T) {
159 if self.cache_ttl.is_zero() {
160 return;
161 }
162
163 if let Ok(json) = serde_json::to_string(data) {
164 let mut cache = self.cache.write().await;
165 let now = Instant::now();
166
167 cache.retain(|_, entry| now < entry.expires_at);
169
170 cache.insert(
171 key.to_string(),
172 CacheEntry {
173 data: json,
174 expires_at: now + self.cache_ttl,
175 },
176 );
177 }
178 }
179
180 pub async fn clear_cache(&self, project_id: Option<&str>, environment: Option<&str>) {
182 let mut cache = self.cache.write().await;
183
184 match (project_id, environment) {
185 (Some(pid), Some(env)) => {
186 let prefix = format!("secrets:{}:{}", pid, env);
187 cache.retain(|k, _| !k.starts_with(&prefix));
188 }
189 (Some(pid), None) => {
190 let prefix = format!("secrets:{}:", pid);
191 cache.retain(|k, _| !k.starts_with(&prefix));
192 }
193 _ => {
194 cache.clear();
195 }
196 }
197 }
198
199 pub async fn clear_all_cache(&self) {
201 let mut cache = self.cache.write().await;
202 cache.clear();
203 }
204
205 async fn get(&self, path: &str) -> Result<String> {
207 let url = format!("{}{}{}", self.base_url, API_PREFIX, path);
208 let response = self.client.get(&url).send().await?;
209 self.handle_response(response).await
210 }
211
212 async fn post<T: serde::Serialize>(&self, path: &str, body: &T) -> Result<String> {
214 let url = format!("{}{}{}", self.base_url, API_PREFIX, path);
215 let response = self.client.post(&url).json(body).send().await?;
216 self.handle_response(response).await
217 }
218
219 async fn put<T: serde::Serialize>(&self, path: &str, body: &T) -> Result<String> {
221 let url = format!("{}{}{}", self.base_url, API_PREFIX, path);
222 let response = self.client.put(&url).json(body).send().await?;
223 self.handle_response(response).await
224 }
225
226 async fn delete(&self, path: &str) -> Result<String> {
228 let url = format!("{}{}{}", self.base_url, API_PREFIX, path);
229 let response = self.client.delete(&url).send().await?;
230 self.handle_response(response).await
231 }
232
233 async fn handle_response(&self, response: reqwest::Response) -> Result<String> {
235 let status = response.status();
236 let body = response.text().await?;
237
238 if status.is_success() {
239 return Ok(body);
240 }
241
242 if let Ok(error_resp) = serde_json::from_str::<ApiErrorResponse>(&body) {
244 let message = error_resp.error.or(error_resp.message).unwrap_or_else(|| {
245 status
246 .canonical_reason()
247 .unwrap_or("Unknown error")
248 .to_string()
249 });
250
251 return Err(match error_resp.code {
252 Some(code) => Error::api_with_code(status.as_u16(), message, code),
253 None => Error::api(status.as_u16(), message),
254 });
255 }
256
257 Err(Error::api(
258 status.as_u16(),
259 status.canonical_reason().unwrap_or("Unknown error"),
260 ))
261 }
262
263 pub async fn get_current_user(&self) -> Result<CurrentUserResponse> {
265 let body = self.get("/users/me").await?;
266 let envelope: DataResponse<CurrentUserResponse> = serde_json::from_str(&body)?;
267 Ok(envelope.data)
268 }
269
270 pub async fn validate_token(&self) -> Result<CurrentUserResponse> {
272 self.get_current_user().await
273 }
274
275 pub async fn list_projects(&self) -> Result<Vec<Project>> {
277 let body = self.get("/projects").await?;
278 let resp: ProjectsResponse = serde_json::from_str(&body)?;
279 Ok(resp.projects)
280 }
281
282 pub async fn get_project(&self, project_id: &str) -> Result<Project> {
284 let body = self.get(&format!("/projects/{}", project_id)).await?;
285 let envelope: DataResponse<Project> = serde_json::from_str(&body)?;
286 Ok(envelope.data)
287 }
288
289 pub async fn create_project(&self, team_id: &str, name: &str) -> Result<Project> {
291 let body = serde_json::json!({
292 "team_id": team_id,
293 "name": name,
294 });
295 let resp = self.post("/projects", &body).await?;
296 let envelope: DataResponse<Project> = serde_json::from_str(&resp)?;
297 Ok(envelope.data)
298 }
299
300 pub async fn delete_project(&self, project_id: &str) -> Result<()> {
302 self.delete(&format!("/projects/{}", project_id)).await?;
303 Ok(())
304 }
305
306 pub async fn list_environments(&self, project_id: &str) -> Result<Vec<Environment>> {
308 let body = self
309 .get(&format!("/projects/{}/environments", project_id))
310 .await?;
311 let resp: EnvironmentsResponse = serde_json::from_str(&body)?;
312 Ok(resp.environments)
313 }
314
315 pub async fn create_environment(
317 &self,
318 project_id: &str,
319 name: &str,
320 inherits_from: Option<&str>,
321 ) -> Result<Environment> {
322 let mut body = serde_json::json!({
323 "name": name,
324 });
325 if let Some(inherits) = inherits_from {
326 body["inherits_from"] = serde_json::Value::String(inherits.to_string());
327 }
328 let path = format!("/projects/{}/environments", project_id);
329 let resp = self.post(&path, &body).await?;
330 let envelope: DataResponse<Environment> = serde_json::from_str(&resp)?;
331 Ok(envelope.data)
332 }
333
334 pub async fn delete_environment(&self, project_id: &str, environment: &str) -> Result<()> {
336 self.delete(&format!(
337 "/projects/{}/environments/{}",
338 project_id, environment
339 ))
340 .await?;
341 Ok(())
342 }
343
344 pub async fn list_secrets(
346 &self,
347 project_id: &str,
348 environment: &str,
349 ) -> Result<Vec<SecretWithInheritance>> {
350 let body = self
351 .get(&format!(
352 "/projects/{}/environments/{}/secrets",
353 project_id, environment
354 ))
355 .await?;
356 let resp: SecretsResponse = serde_json::from_str(&body)?;
357 Ok(resp.secrets)
358 }
359
360 pub async fn export_secrets(
362 &self,
363 project_id: &str,
364 environment: &str,
365 ) -> Result<Vec<SecretWithValueAndInheritance>> {
366 let cache_key = format!("secrets:{}:{}:export", project_id, environment);
367
368 if let Some(cached) = self.get_cached(&cache_key).await {
370 return Ok(cached);
371 }
372
373 let body = self
374 .get(&format!(
375 "/projects/{}/environments/{}/secrets/export",
376 project_id, environment
377 ))
378 .await?;
379 let resp: SecretsExportResponse = serde_json::from_str(&body)?;
380
381 self.set_cached(&cache_key, &resp.secrets).await;
383
384 Ok(resp.secrets)
385 }
386
387 pub async fn export_secrets_as_map(
389 &self,
390 project_id: &str,
391 environment: &str,
392 ) -> Result<HashMap<String, String>> {
393 let secrets = self.export_secrets(project_id, environment).await?;
394 Ok(secrets.into_iter().map(|s| (s.key, s.value)).collect())
395 }
396
397 pub async fn get_secret(
399 &self,
400 project_id: &str,
401 environment: &str,
402 key: &str,
403 ) -> Result<SecretWithValueAndInheritance> {
404 let body = self
405 .get(&format!(
406 "/projects/{}/environments/{}/secrets/{}",
407 project_id, environment, key
408 ))
409 .await?;
410 let resp: SecretResponse = serde_json::from_str(&body)?;
411 Ok(resp.secret)
412 }
413
414 pub async fn set_secret(
416 &self,
417 project_id: &str,
418 environment: &str,
419 key: &str,
420 value: &str,
421 ) -> Result<()> {
422 self.set_secret_with_description(project_id, environment, key, value, None)
423 .await
424 }
425
426 pub async fn set_secret_with_description(
428 &self,
429 project_id: &str,
430 environment: &str,
431 key: &str,
432 value: &str,
433 description: Option<&str>,
434 ) -> Result<()> {
435 let path = format!(
436 "/projects/{}/environments/{}/secrets/{}",
437 project_id, environment, key
438 );
439
440 let mut body = serde_json::json!({
441 "value": value,
442 });
443 if let Some(desc) = description {
444 body["description"] = serde_json::Value::String(desc.to_string());
445 }
446
447 match self.put(&path, &body).await {
449 Ok(_) => {}
450 Err(Error::Api { status: 404, .. }) => {
451 let create_path = format!(
453 "/projects/{}/environments/{}/secrets",
454 project_id, environment
455 );
456 let mut create_body = serde_json::json!({
457 "key": key,
458 "value": value,
459 });
460 if let Some(desc) = description {
461 create_body["description"] = serde_json::Value::String(desc.to_string());
462 }
463 self.post(&create_path, &create_body).await?;
464 }
465 Err(e) => return Err(e),
466 }
467
468 self.clear_cache(Some(project_id), Some(environment)).await;
470
471 Ok(())
472 }
473
474 pub async fn delete_secret(
476 &self,
477 project_id: &str,
478 environment: &str,
479 key: &str,
480 ) -> Result<()> {
481 self.delete(&format!(
482 "/projects/{}/environments/{}/secrets/{}",
483 project_id, environment, key
484 ))
485 .await?;
486
487 self.clear_cache(Some(project_id), Some(environment)).await;
489
490 Ok(())
491 }
492
493 pub async fn bulk_import(
495 &self,
496 project_id: &str,
497 environment: &str,
498 secrets: Vec<SecretInput>,
499 options: BulkImportOptions,
500 ) -> Result<BulkImportResult> {
501 let path = format!(
502 "/projects/{}/environments/{}/secrets/bulk",
503 project_id, environment
504 );
505
506 let body = serde_json::json!({
507 "secrets": secrets,
508 "overwrite": options.overwrite,
509 });
510
511 let resp_body = self.post(&path, &body).await?;
512 let envelope: DataResponse<BulkImportResult> = serde_json::from_str(&resp_body)?;
513 let result = envelope.data;
514
515 self.clear_cache(Some(project_id), Some(environment)).await;
517
518 Ok(result)
519 }
520
521 pub async fn load_env(&self, project_id: &str, environment: &str) -> Result<usize> {
524 let secrets = self.export_secrets(project_id, environment).await?;
525
526 for secret in &secrets {
527 std::env::set_var(&secret.key, &secret.value);
528 }
529
530 Ok(secrets.len())
531 }
532
533 pub async fn generate_env_file(&self, project_id: &str, environment: &str) -> Result<String> {
535 let secrets = self.export_secrets(project_id, environment).await?;
536 let mut content = String::new();
537
538 for secret in secrets {
539 let value = &secret.value;
540 let needs_quotes = value.contains(' ')
541 || value.contains('\t')
542 || value.contains('\n')
543 || value.contains('"')
544 || value.contains('\'')
545 || value.contains('\\')
546 || value.contains('$');
547
548 if needs_quotes {
549 let escaped = value
550 .replace('\\', "\\\\")
551 .replace('"', "\\\"")
552 .replace('\n', "\\n")
553 .replace('$', "\\$");
554 content.push_str(&format!("{}=\"{}\"\n", secret.key, escaped));
555 } else {
556 content.push_str(&format!("{}={}\n", secret.key, value));
557 }
558 }
559
560 Ok(content)
561 }
562
563 pub async fn list_permissions(
565 &self,
566 project_id: &str,
567 environment: &str,
568 ) -> Result<Vec<Permission>> {
569 let body = self
570 .get(&format!(
571 "/projects/{}/environments/{}/permissions",
572 project_id, environment
573 ))
574 .await?;
575 let resp: PermissionsResponse = serde_json::from_str(&body)?;
576 Ok(resp.permissions)
577 }
578
579 pub async fn set_permission(
581 &self,
582 project_id: &str,
583 environment: &str,
584 user_id: &str,
585 role: &str,
586 ) -> Result<()> {
587 let path = format!(
588 "/projects/{}/environments/{}/permissions/{}",
589 project_id, environment, user_id
590 );
591 let body = serde_json::json!({ "role": role });
592 self.put(&path, &body).await?;
593 Ok(())
594 }
595
596 pub async fn delete_permission(
598 &self,
599 project_id: &str,
600 environment: &str,
601 user_id: &str,
602 ) -> Result<()> {
603 self.delete(&format!(
604 "/projects/{}/environments/{}/permissions/{}",
605 project_id, environment, user_id
606 ))
607 .await?;
608 Ok(())
609 }
610
611 pub async fn bulk_set_permissions(
613 &self,
614 project_id: &str,
615 environment: &str,
616 permissions: Vec<PermissionInput>,
617 ) -> Result<()> {
618 let path = format!(
619 "/projects/{}/environments/{}/permissions",
620 project_id, environment
621 );
622 let body = serde_json::json!({ "permissions": permissions });
623 self.put(&path, &body).await?;
624 Ok(())
625 }
626
627 pub async fn get_my_permissions(&self, project_id: &str) -> Result<MyPermissionsResponse> {
629 let body = self
630 .get(&format!("/projects/{}/my-permissions", project_id))
631 .await?;
632 Ok(serde_json::from_str(&body)?)
633 }
634
635 pub async fn get_project_defaults(&self, project_id: &str) -> Result<Vec<DefaultPermission>> {
637 let body = self
638 .get(&format!("/projects/{}/permissions/defaults", project_id))
639 .await?;
640 let resp: DefaultsResponse = serde_json::from_str(&body)?;
641 Ok(resp.defaults)
642 }
643
644 pub async fn set_project_defaults(
646 &self,
647 project_id: &str,
648 defaults: Vec<DefaultPermission>,
649 ) -> Result<()> {
650 let path = format!("/projects/{}/permissions/defaults", project_id);
651 let body = serde_json::json!({ "defaults": defaults });
652 self.put(&path, &body).await?;
653 Ok(())
654 }
655
656 pub async fn get_secret_history(
658 &self,
659 project_id: &str,
660 environment: &str,
661 key: &str,
662 ) -> Result<Vec<SecretHistory>> {
663 let body = self
664 .get(&format!(
665 "/projects/{}/environments/{}/secrets/{}/history",
666 project_id, environment, key
667 ))
668 .await?;
669 let resp: HistoryResponse = serde_json::from_str(&body)?;
670 Ok(resp.history)
671 }
672}