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;
51use serde::{Deserialize, Serialize};
52use std::collections::HashMap;
53use thiserror::Error;
54
55#[derive(Debug, Error)]
57pub enum SecretError {
58 #[error("Secret '{name}' not found from source '{secret_source}'")]
60 NotFound {
61 name: String,
63 secret_source: String,
65 },
66
67 #[error("Secret '{name}' is too short ({len} chars, minimum 4) for cache key inclusion")]
69 TooShort {
70 name: String,
72 len: usize,
74 },
75
76 #[error("CUENV_SECRET_SALT required when secrets have cache_key: true")]
78 MissingSalt,
79
80 #[error("Failed to resolve secret '{name}': {message}")]
82 ResolutionFailed {
83 name: String,
85 message: String,
87 },
88
89 #[error("Unsupported secret resolver: {resolver}")]
91 UnsupportedResolver {
92 resolver: String,
94 },
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
99pub struct SecretSpec {
100 pub source: String,
102
103 #[serde(default)]
105 pub cache_key: bool,
106}
107
108impl SecretSpec {
109 #[must_use]
111 pub fn new(source: impl Into<String>) -> Self {
112 Self {
113 source: source.into(),
114 cache_key: false,
115 }
116 }
117
118 #[must_use]
120 pub fn with_cache_key(source: impl Into<String>) -> Self {
121 Self {
122 source: source.into(),
123 cache_key: true,
124 }
125 }
126}
127
128#[async_trait]
137pub trait SecretResolver: Send + Sync {
138 async fn resolve(&self, name: &str, spec: &SecretSpec) -> Result<String, SecretError>;
142
143 fn provider_name(&self) -> &'static str;
148
149 async fn resolve_secure(
153 &self,
154 name: &str,
155 spec: &SecretSpec,
156 ) -> Result<SecureSecret, SecretError> {
157 let value = self.resolve(name, spec).await?;
158 Ok(SecureSecret::new(value))
159 }
160
161 async fn resolve_batch(
175 &self,
176 secrets: &HashMap<String, SecretSpec>,
177 ) -> Result<HashMap<String, SecureSecret>, SecretError> {
178 use futures::future::try_join_all;
179
180 let futures: Vec<_> = secrets
181 .iter()
182 .map(|(name, spec)| {
183 let name = name.clone();
184 let spec = spec.clone();
185 async move {
186 let value = self.resolve_secure(&name, &spec).await?;
187 Ok::<_, SecretError>((name, value))
188 }
189 })
190 .collect();
191
192 let results = try_join_all(futures).await?;
193 Ok(results.into_iter().collect())
194 }
195
196 fn supports_native_batch(&self) -> bool {
201 false
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_secret_error_not_found() {
211 let err = SecretError::NotFound {
212 name: "API_KEY".to_string(),
213 secret_source: "env:API_KEY".to_string(),
214 };
215 let msg = err.to_string();
216 assert!(msg.contains("API_KEY"));
217 assert!(msg.contains("env:API_KEY"));
218 }
219
220 #[test]
221 fn test_secret_error_too_short() {
222 let err = SecretError::TooShort {
223 name: "SHORT_SECRET".to_string(),
224 len: 2,
225 };
226 let msg = err.to_string();
227 assert!(msg.contains("SHORT_SECRET"));
228 assert!(msg.contains("2 chars"));
229 assert!(msg.contains("minimum 4"));
230 }
231
232 #[test]
233 fn test_secret_error_missing_salt() {
234 let err = SecretError::MissingSalt;
235 let msg = err.to_string();
236 assert!(msg.contains("CUENV_SECRET_SALT"));
237 assert!(msg.contains("cache_key: true"));
238 }
239
240 #[test]
241 fn test_secret_error_resolution_failed() {
242 let err = SecretError::ResolutionFailed {
243 name: "DATABASE_URL".to_string(),
244 message: "connection timeout".to_string(),
245 };
246 let msg = err.to_string();
247 assert!(msg.contains("DATABASE_URL"));
248 assert!(msg.contains("connection timeout"));
249 }
250
251 #[test]
252 fn test_secret_error_unsupported_resolver() {
253 let err = SecretError::UnsupportedResolver {
254 resolver: "unknown".to_string(),
255 };
256 let msg = err.to_string();
257 assert!(msg.contains("unknown"));
258 }
259
260 #[test]
261 fn test_secret_error_debug() {
262 let err = SecretError::MissingSalt;
263 let debug = format!("{err:?}");
264 assert!(debug.contains("MissingSalt"));
265 }
266
267 #[test]
268 fn test_secret_spec_new() {
269 let spec = SecretSpec::new("env:API_KEY");
270 assert_eq!(spec.source, "env:API_KEY");
271 assert!(!spec.cache_key);
272 }
273
274 #[test]
275 fn test_secret_spec_with_cache_key() {
276 let spec = SecretSpec::with_cache_key("env:CACHE_AFFECTING_SECRET");
277 assert_eq!(spec.source, "env:CACHE_AFFECTING_SECRET");
278 assert!(spec.cache_key);
279 }
280
281 #[test]
282 fn test_secret_spec_new_with_string() {
283 let spec = SecretSpec::new(String::from("vault://path/to/secret"));
284 assert_eq!(spec.source, "vault://path/to/secret");
285 }
286
287 #[test]
288 fn test_secret_spec_equality() {
289 let spec1 = SecretSpec::new("source1");
290 let spec2 = SecretSpec::new("source1");
291 let spec3 = SecretSpec::new("source2");
292 let spec4 = SecretSpec::with_cache_key("source1");
293
294 assert_eq!(spec1, spec2);
295 assert_ne!(spec1, spec3);
296 assert_ne!(spec1, spec4); }
298
299 #[test]
300 fn test_secret_spec_clone() {
301 let spec = SecretSpec::with_cache_key("important");
302 let cloned = spec.clone();
303 assert_eq!(spec, cloned);
304 }
305
306 #[test]
307 fn test_secret_spec_debug() {
308 let spec = SecretSpec::new("test-source");
309 let debug = format!("{spec:?}");
310 assert!(debug.contains("SecretSpec"));
311 assert!(debug.contains("test-source"));
312 }
313
314 #[test]
315 fn test_secret_spec_serialization() {
316 let spec = SecretSpec::with_cache_key("op://vault/item/field");
317 let json = serde_json::to_string(&spec).unwrap();
318 assert!(json.contains("op://vault/item/field"));
319 assert!(json.contains("cache_key"));
320
321 let parsed: SecretSpec = serde_json::from_str(&json).unwrap();
322 assert_eq!(spec, parsed);
323 }
324
325 #[test]
326 fn test_secret_spec_deserialization_default_cache_key() {
327 let json = r#"{"source": "test"}"#;
328 let spec: SecretSpec = serde_json::from_str(json).unwrap();
329 assert_eq!(spec.source, "test");
330 assert!(!spec.cache_key); }
332}