Skip to main content

dakera_client/
keys.rs

1//! API Key management for the Dakera client.
2//!
3//! Provides methods for creating, listing, rotating, and managing API keys.
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::Result;
8use crate::DakeraClient;
9
10// ============================================================================
11// Key Types
12// ============================================================================
13
14/// Request to create a new API key
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct CreateKeyRequest {
17    /// Human-readable name for this key
18    pub name: String,
19    /// Scope/permission level (read, write, admin, super_admin)
20    pub scope: String,
21    /// Optional: restrict to specific namespaces
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub namespaces: Option<Vec<String>>,
24    /// Optional: key expires in N days
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub expires_in_days: Option<u64>,
27}
28
29/// Response after creating an API key
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CreateKeyResponse {
32    /// The API key ID (for management)
33    pub key_id: String,
34    /// The full API key (shown only once!)
35    pub key: String,
36    /// Key name
37    pub name: String,
38    /// Key scope
39    pub scope: String,
40    /// Namespaces this key can access
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub namespaces: Option<Vec<String>>,
43    /// When the key was created (Unix timestamp)
44    pub created_at: u64,
45    /// When the key expires (Unix timestamp), if set
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub expires_at: Option<u64>,
48    /// Warning message to save the key
49    pub warning: String,
50}
51
52/// API key info (without sensitive data)
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct KeyInfo {
55    pub key_id: String,
56    pub name: String,
57    pub scope: String,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub namespaces: Option<Vec<String>>,
60    pub created_at: u64,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub expires_at: Option<u64>,
63    pub active: bool,
64}
65
66/// List keys response
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ListKeysResponse {
69    pub keys: Vec<KeyInfo>,
70    pub total: usize,
71}
72
73/// Generic success response
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct KeySuccessResponse {
76    pub success: bool,
77    pub message: String,
78}
79
80/// Rotate key response
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct RotateKeyResponse {
83    /// The new API key (shown only once!)
84    pub new_key: String,
85    /// The key ID (unchanged)
86    pub key_id: String,
87    /// Warning message
88    pub warning: String,
89}
90
91/// API key usage statistics
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ApiKeyUsageResponse {
94    pub key_id: String,
95    pub total_requests: u64,
96    pub successful_requests: u64,
97    pub failed_requests: u64,
98    pub rate_limited_requests: u64,
99    pub bytes_transferred: u64,
100    pub avg_latency_ms: f64,
101    #[serde(default)]
102    pub by_endpoint: Vec<EndpointUsageInfo>,
103    #[serde(default)]
104    pub by_namespace: Vec<NamespaceUsageInfo>,
105}
106
107/// Usage statistics per endpoint
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct EndpointUsageInfo {
110    pub endpoint: String,
111    pub requests: u64,
112    pub avg_latency_ms: f64,
113}
114
115/// Usage statistics per namespace
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct NamespaceUsageInfo {
118    pub namespace: String,
119    pub requests: u64,
120    pub vectors_accessed: u64,
121}
122
123// ============================================================================
124// Key Client Methods
125// ============================================================================
126
127impl DakeraClient {
128    /// Create a new API key
129    pub async fn create_key(&self, request: CreateKeyRequest) -> Result<CreateKeyResponse> {
130        let url = format!("{}/admin/keys", self.base_url);
131        let response = self.client.post(&url).json(&request).send().await?;
132        self.handle_response(response).await
133    }
134
135    /// List all API keys
136    pub async fn list_keys(&self) -> Result<ListKeysResponse> {
137        let url = format!("{}/admin/keys", self.base_url);
138        let response = self.client.get(&url).send().await?;
139        self.handle_response(response).await
140    }
141
142    /// Get a specific API key by ID
143    pub async fn get_key(&self, key_id: &str) -> Result<KeyInfo> {
144        let url = format!("{}/admin/keys/{}", self.base_url, key_id);
145        let response = self.client.get(&url).send().await?;
146        self.handle_response(response).await
147    }
148
149    /// Delete (revoke) an API key
150    pub async fn delete_key(&self, key_id: &str) -> Result<KeySuccessResponse> {
151        let url = format!("{}/admin/keys/{}", self.base_url, key_id);
152        let response = self.client.delete(&url).send().await?;
153        self.handle_response(response).await
154    }
155
156    /// Deactivate an API key (soft delete)
157    pub async fn deactivate_key(&self, key_id: &str) -> Result<KeySuccessResponse> {
158        let url = format!("{}/admin/keys/{}/deactivate", self.base_url, key_id);
159        let response = self.client.post(&url).send().await?;
160        self.handle_response(response).await
161    }
162
163    /// Rotate an API key (creates new key, deactivates old)
164    pub async fn rotate_key(&self, key_id: &str) -> Result<RotateKeyResponse> {
165        let url = format!("{}/admin/keys/{}/rotate", self.base_url, key_id);
166        let response = self.client.post(&url).send().await?;
167        self.handle_response(response).await
168    }
169
170    /// Get API key usage statistics
171    pub async fn key_usage(&self, key_id: &str) -> Result<ApiKeyUsageResponse> {
172        let url = format!("{}/admin/keys/{}/usage", self.base_url, key_id);
173        let response = self.client.get(&url).send().await?;
174        self.handle_response(response).await
175    }
176
177    // ========================================================================
178    // Namespace-Scoped API Keys — SEC-1
179    // ========================================================================
180
181    /// Create a namespace-scoped API key (SEC-1).
182    ///
183    /// The `key` field in the response is shown **only once** — store it securely.
184    pub async fn create_namespace_key(
185        &self,
186        namespace: &str,
187        request: CreateNamespaceKeyRequest,
188    ) -> Result<CreateNamespaceKeyResponse> {
189        let url = format!("{}/v1/namespaces/{}/keys", self.base_url, namespace);
190        let response = self.client.post(&url).json(&request).send().await?;
191        self.handle_response(response).await
192    }
193
194    /// List all API keys scoped to a namespace (SEC-1).
195    pub async fn list_namespace_keys(&self, namespace: &str) -> Result<ListNamespaceKeysResponse> {
196        let url = format!("{}/v1/namespaces/{}/keys", self.base_url, namespace);
197        let response = self.client.get(&url).send().await?;
198        self.handle_response(response).await
199    }
200
201    /// Revoke a namespace-scoped API key (SEC-1).
202    pub async fn delete_namespace_key(
203        &self,
204        namespace: &str,
205        key_id: &str,
206    ) -> Result<KeySuccessResponse> {
207        let url = format!(
208            "{}/v1/namespaces/{}/keys/{}",
209            self.base_url, namespace, key_id
210        );
211        let response = self.client.delete(&url).send().await?;
212        self.handle_response(response).await
213    }
214
215    /// Get usage statistics for a namespace-scoped API key (SEC-1).
216    pub async fn namespace_key_usage(
217        &self,
218        namespace: &str,
219        key_id: &str,
220    ) -> Result<NamespaceKeyUsageResponse> {
221        let url = format!(
222            "{}/v1/namespaces/{}/keys/{}/usage",
223            self.base_url, namespace, key_id
224        );
225        let response = self.client.get(&url).send().await?;
226        self.handle_response(response).await
227    }
228
229    /// Alias for [`namespace_key_usage`](Self::namespace_key_usage) matching Python/JS naming.
230    pub async fn get_namespace_key_usage(
231        &self,
232        namespace: &str,
233        key_id: &str,
234    ) -> Result<NamespaceKeyUsageResponse> {
235        self.namespace_key_usage(namespace, key_id).await
236    }
237}
238
239// ============================================================================
240// Namespace Key Types (SEC-1)
241// ============================================================================
242
243/// Request body for `POST /v1/namespaces/:namespace/keys` (SEC-1).
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct CreateNamespaceKeyRequest {
246    /// Human-readable label for this key.
247    pub name: String,
248    /// Optional: key expires in N days from now.
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub expires_in_days: Option<u64>,
251}
252
253/// Response from `POST /v1/namespaces/:namespace/keys` (SEC-1).
254///
255/// The `key` field contains the raw API key and is **shown only once**.
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct CreateNamespaceKeyResponse {
258    pub key_id: String,
259    /// The raw API key — store it securely, cannot be retrieved again.
260    pub key: String,
261    pub name: String,
262    pub namespace: String,
263    pub created_at: u64,
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub expires_at: Option<u64>,
266    pub warning: String,
267}
268
269/// Namespace-scoped API key metadata — no secret included (SEC-1).
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct NamespaceKeyInfo {
272    pub key_id: String,
273    pub name: String,
274    pub namespace: String,
275    pub created_at: u64,
276    pub active: bool,
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub expires_at: Option<u64>,
279}
280
281/// Response from `GET /v1/namespaces/:namespace/keys` (SEC-1).
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct ListNamespaceKeysResponse {
284    pub namespace: String,
285    pub keys: Vec<NamespaceKeyInfo>,
286    pub total: usize,
287}
288
289/// Response from `GET /v1/namespaces/:namespace/keys/:key_id/usage` (SEC-1).
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct NamespaceKeyUsageResponse {
292    pub key_id: String,
293    pub namespace: String,
294    pub total_requests: u64,
295    pub successful_requests: u64,
296    pub failed_requests: u64,
297    pub bytes_transferred: u64,
298    pub avg_latency_ms: f64,
299}
300
301// ============================================================================
302// Tests
303// ============================================================================
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn test_create_namespace_key_request_serializes_without_expiry() {
311        let req = CreateNamespaceKeyRequest {
312            name: "ci-runner".to_string(),
313            expires_in_days: None,
314        };
315        let json = serde_json::to_string(&req).unwrap();
316        assert!(json.contains("\"name\":\"ci-runner\""));
317        assert!(!json.contains("expires_in_days"));
318    }
319
320    #[test]
321    fn test_create_namespace_key_request_serializes_with_expiry() {
322        let req = CreateNamespaceKeyRequest {
323            name: "ci-runner".to_string(),
324            expires_in_days: Some(30),
325        };
326        let json = serde_json::to_string(&req).unwrap();
327        assert!(json.contains("\"expires_in_days\":30"));
328    }
329
330    #[test]
331    fn test_namespace_key_info_deserializes() {
332        let json = r#"{
333            "key_id": "key-abc",
334            "name": "ci-runner",
335            "namespace": "prod-ns",
336            "created_at": 1774000000,
337            "active": true
338        }"#;
339        let info: NamespaceKeyInfo = serde_json::from_str(json).unwrap();
340        assert_eq!(info.key_id, "key-abc");
341        assert_eq!(info.namespace, "prod-ns");
342        assert!(info.active);
343        assert!(info.expires_at.is_none());
344    }
345
346    #[test]
347    fn test_namespace_key_usage_response_deserializes() {
348        let json = r#"{
349            "key_id": "key-abc",
350            "namespace": "prod-ns",
351            "total_requests": 1000,
352            "successful_requests": 980,
353            "failed_requests": 20,
354            "bytes_transferred": 512000,
355            "avg_latency_ms": 12.4
356        }"#;
357        let usage: NamespaceKeyUsageResponse = serde_json::from_str(json).unwrap();
358        assert_eq!(usage.total_requests, 1000);
359        assert!((usage.avg_latency_ms - 12.4).abs() < 0.001);
360    }
361
362    #[test]
363    fn test_list_namespace_keys_response_deserializes() {
364        let json = r#"{
365            "namespace": "prod-ns",
366            "keys": [],
367            "total": 0
368        }"#;
369        let resp: ListNamespaceKeysResponse = serde_json::from_str(json).unwrap();
370        assert_eq!(resp.namespace, "prod-ns");
371        assert_eq!(resp.total, 0);
372        assert!(resp.keys.is_empty());
373    }
374}