1mod batch;
27mod fingerprint;
28mod registry;
29mod resolved;
30pub mod resolvers;
31mod salt;
32mod types;
33
34pub use batch::{BatchConfig, BatchResolver, resolve_batch};
35pub use fingerprint::compute_secret_fingerprint;
36pub use registry::SecretRegistry;
37pub use resolved::ResolvedSecrets;
38pub use salt::SaltConfig;
39pub use types::{BatchSecrets, SecureSecret};
40
41pub use resolvers::{EnvSecretResolver, ExecSecretResolver};
43
44use async_trait::async_trait;
48use serde::{Deserialize, Serialize};
49use std::collections::HashMap;
50use thiserror::Error;
51
52#[derive(Debug, Error)]
54pub enum SecretError {
55 #[error("Secret '{name}' not found from source '{secret_source}'")]
57 NotFound {
58 name: String,
60 secret_source: String,
62 },
63
64 #[error("Secret '{name}' is too short ({len} chars, minimum 4) for cache key inclusion")]
66 TooShort {
67 name: String,
69 len: usize,
71 },
72
73 #[error("CUENV_SECRET_SALT required when secrets have cache_key: true")]
75 MissingSalt,
76
77 #[error("Failed to resolve secret '{name}': {message}")]
79 ResolutionFailed {
80 name: String,
82 message: String,
84 },
85
86 #[error("Unsupported secret resolver: {resolver}")]
88 UnsupportedResolver {
89 resolver: String,
91 },
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96pub struct SecretSpec {
97 pub source: String,
99
100 #[serde(default)]
102 pub cache_key: bool,
103}
104
105impl SecretSpec {
106 #[must_use]
108 pub fn new(source: impl Into<String>) -> Self {
109 Self {
110 source: source.into(),
111 cache_key: false,
112 }
113 }
114
115 #[must_use]
117 pub fn with_cache_key(source: impl Into<String>) -> Self {
118 Self {
119 source: source.into(),
120 cache_key: true,
121 }
122 }
123}
124
125#[async_trait]
134pub trait SecretResolver: Send + Sync {
135 async fn resolve(&self, name: &str, spec: &SecretSpec) -> Result<String, SecretError>;
139
140 fn provider_name(&self) -> &'static str;
145
146 async fn resolve_secure(
150 &self,
151 name: &str,
152 spec: &SecretSpec,
153 ) -> Result<SecureSecret, SecretError> {
154 let value = self.resolve(name, spec).await?;
155 Ok(SecureSecret::new(value))
156 }
157
158 async fn resolve_batch(
172 &self,
173 secrets: &HashMap<String, SecretSpec>,
174 ) -> Result<HashMap<String, SecureSecret>, SecretError> {
175 use futures::future::try_join_all;
176
177 let futures: Vec<_> = secrets
178 .iter()
179 .map(|(name, spec)| {
180 let name = name.clone();
181 let spec = spec.clone();
182 async move {
183 let value = self.resolve_secure(&name, &spec).await?;
184 Ok::<_, SecretError>((name, value))
185 }
186 })
187 .collect();
188
189 let results = try_join_all(futures).await?;
190 Ok(results.into_iter().collect())
191 }
192
193 fn supports_native_batch(&self) -> bool {
198 false
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_secret_error_not_found() {
208 let err = SecretError::NotFound {
209 name: "API_KEY".to_string(),
210 secret_source: "env:API_KEY".to_string(),
211 };
212 let msg = err.to_string();
213 assert!(msg.contains("API_KEY"));
214 assert!(msg.contains("env:API_KEY"));
215 }
216
217 #[test]
218 fn test_secret_error_too_short() {
219 let err = SecretError::TooShort {
220 name: "SHORT_SECRET".to_string(),
221 len: 2,
222 };
223 let msg = err.to_string();
224 assert!(msg.contains("SHORT_SECRET"));
225 assert!(msg.contains("2 chars"));
226 assert!(msg.contains("minimum 4"));
227 }
228
229 #[test]
230 fn test_secret_error_missing_salt() {
231 let err = SecretError::MissingSalt;
232 let msg = err.to_string();
233 assert!(msg.contains("CUENV_SECRET_SALT"));
234 assert!(msg.contains("cache_key: true"));
235 }
236
237 #[test]
238 fn test_secret_error_resolution_failed() {
239 let err = SecretError::ResolutionFailed {
240 name: "DATABASE_URL".to_string(),
241 message: "connection timeout".to_string(),
242 };
243 let msg = err.to_string();
244 assert!(msg.contains("DATABASE_URL"));
245 assert!(msg.contains("connection timeout"));
246 }
247
248 #[test]
249 fn test_secret_error_unsupported_resolver() {
250 let err = SecretError::UnsupportedResolver {
251 resolver: "unknown".to_string(),
252 };
253 let msg = err.to_string();
254 assert!(msg.contains("unknown"));
255 }
256
257 #[test]
258 fn test_secret_error_debug() {
259 let err = SecretError::MissingSalt;
260 let debug = format!("{err:?}");
261 assert!(debug.contains("MissingSalt"));
262 }
263
264 #[test]
265 fn test_secret_spec_new() {
266 let spec = SecretSpec::new("env:API_KEY");
267 assert_eq!(spec.source, "env:API_KEY");
268 assert!(!spec.cache_key);
269 }
270
271 #[test]
272 fn test_secret_spec_with_cache_key() {
273 let spec = SecretSpec::with_cache_key("env:CACHE_AFFECTING_SECRET");
274 assert_eq!(spec.source, "env:CACHE_AFFECTING_SECRET");
275 assert!(spec.cache_key);
276 }
277
278 #[test]
279 fn test_secret_spec_new_with_string() {
280 let spec = SecretSpec::new(String::from("vault://path/to/secret"));
281 assert_eq!(spec.source, "vault://path/to/secret");
282 }
283
284 #[test]
285 fn test_secret_spec_equality() {
286 let spec1 = SecretSpec::new("source1");
287 let spec2 = SecretSpec::new("source1");
288 let spec3 = SecretSpec::new("source2");
289 let spec4 = SecretSpec::with_cache_key("source1");
290
291 assert_eq!(spec1, spec2);
292 assert_ne!(spec1, spec3);
293 assert_ne!(spec1, spec4); }
295
296 #[test]
297 fn test_secret_spec_clone() {
298 let spec = SecretSpec::with_cache_key("important");
299 let cloned = spec.clone();
300 assert_eq!(spec, cloned);
301 }
302
303 #[test]
304 fn test_secret_spec_debug() {
305 let spec = SecretSpec::new("test-source");
306 let debug = format!("{spec:?}");
307 assert!(debug.contains("SecretSpec"));
308 assert!(debug.contains("test-source"));
309 }
310
311 #[test]
312 fn test_secret_spec_serialization() {
313 let spec = SecretSpec::with_cache_key("op://vault/item/field");
314 let json = serde_json::to_string(&spec).unwrap();
315 assert!(json.contains("op://vault/item/field"));
316 assert!(json.contains("cache_key"));
317
318 let parsed: SecretSpec = serde_json::from_str(&json).unwrap();
319 assert_eq!(spec, parsed);
320 }
321
322 #[test]
323 fn test_secret_spec_deserialization_default_cache_key() {
324 let json = r#"{"source": "test"}"#;
325 let spec: SecretSpec = serde_json::from_str(json).unwrap();
326 assert_eq!(spec.source, "test");
327 assert!(!spec.cache_key); }
329}