Skip to main content

keyenv/
client.rs

1//! KeyEnv client implementation.
2
3use 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
11/// Default API base URL.
12pub const DEFAULT_BASE_URL: &str = "https://api.keyenv.dev";
13
14/// Default request timeout.
15pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
16
17/// SDK version.
18pub const VERSION: &str = env!("CARGO_PKG_VERSION");
19
20/// API version prefix for all requests.
21const API_PREFIX: &str = "/api/v1";
22
23/// Builder for creating a KeyEnv client.
24#[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    /// Create a new builder.
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Set the authentication token (required).
50    pub fn token(mut self, token: impl Into<String>) -> Self {
51        self.token = Some(token.into());
52        self
53    }
54
55    /// Set the API base URL (optional).
56    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    /// Set the request timeout (optional, default 30s).
66    pub fn timeout(mut self, timeout: Duration) -> Self {
67        self.timeout = timeout;
68        self
69    }
70
71    /// Set the cache TTL (optional, 0 disables caching).
72    pub fn cache_ttl(mut self, ttl: Duration) -> Self {
73        self.cache_ttl = ttl;
74        self
75    }
76
77    /// Build the KeyEnv client.
78    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
111/// Cache entry with expiration.
112struct CacheEntry {
113    data: String, // JSON serialized
114    expires_at: Instant,
115}
116
117/// KeyEnv API client.
118#[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    /// Create a new builder for the KeyEnv client.
128    pub fn builder() -> KeyEnvBuilder {
129        KeyEnvBuilder::new()
130    }
131
132    /// Get from cache if available and not expired.
133    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 the read lock before acquiring write lock for cleanup
145        drop(cache);
146
147        // Lazy cleanup: remove the expired entry
148        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    /// Store in cache, pruning expired entries.
158    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            // Prune expired entries to prevent memory leaks
168            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    /// Clear cache for a specific project/environment.
181    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    /// Clear all cached data.
200    pub async fn clear_all_cache(&self) {
201        let mut cache = self.cache.write().await;
202        cache.clear();
203    }
204
205    /// Make a GET request.
206    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    /// Make a POST request.
213    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    /// Make a PUT request.
220    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    /// Make a DELETE request.
227    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    /// Handle API response.
234    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        // Try to parse error response
243        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    /// Get current user or service token information.
264    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    /// Validate the token and return user info.
271    pub async fn validate_token(&self) -> Result<CurrentUserResponse> {
272        self.get_current_user().await
273    }
274
275    /// List all accessible projects.
276    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    /// Get a project by ID.
283    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    /// Create a new project.
290    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    /// Delete a project.
301    pub async fn delete_project(&self, project_id: &str) -> Result<()> {
302        self.delete(&format!("/projects/{}", project_id)).await?;
303        Ok(())
304    }
305
306    /// List environments in a project.
307    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    /// Create a new environment in a project.
316    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    /// Delete an environment from a project.
335    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    /// List secrets (without values) in an environment.
345    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    /// Export secrets with values from an environment.
361    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        // Check cache
369        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        // Store in cache
382        self.set_cached(&cache_key, &resp.secrets).await;
383
384        Ok(resp.secrets)
385    }
386
387    /// Export secrets as a HashMap.
388    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    /// Get a single secret by key.
398    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    /// Set (create or update) a secret.
415    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    /// Set a secret with an optional description.
427    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        // Try PUT first (update), then POST (create) if not found
448        match self.put(&path, &body).await {
449            Ok(_) => {}
450            Err(Error::Api { status: 404, .. }) => {
451                // Secret doesn't exist, create it
452                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        // Clear cache for this environment
469        self.clear_cache(Some(project_id), Some(environment)).await;
470
471        Ok(())
472    }
473
474    /// Delete a secret.
475    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        // Clear cache for this environment
488        self.clear_cache(Some(project_id), Some(environment)).await;
489
490        Ok(())
491    }
492
493    /// Bulk import secrets.
494    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        // Clear cache for this environment
516        self.clear_cache(Some(project_id), Some(environment)).await;
517
518        Ok(result)
519    }
520
521    /// Load secrets into environment variables.
522    /// Returns the number of secrets loaded.
523    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    /// Generate .env file content.
534    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    /// List permissions for an environment.
564    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    /// Set a user's permission for an environment.
580    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    /// Delete a user's permission for an environment.
597    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    /// Bulk set permissions.
612    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    /// Get the current user's permissions for a project.
628    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    /// Get default permissions for a project.
636    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    /// Set default permissions for a project.
645    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    /// Get the version history of a secret.
657    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}