use crate::core::secret_provider::ProviderChain;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct EphemeralParam {
pub key: String,
pub provider_key: String,
}
#[derive(Debug, Clone)]
pub struct ResolvedEphemeral {
pub key: String,
pub value: String,
pub hash: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct EphemeralRecord {
pub key: String,
pub hash: String,
}
pub fn resolve_ephemerals(
params: &[EphemeralParam],
chain: &ProviderChain,
) -> Result<Vec<ResolvedEphemeral>, String> {
let mut results = Vec::with_capacity(params.len());
for param in params {
let secret = chain
.resolve(¶m.provider_key)
.map_err(|e| format!("ephemeral '{}': {e}", param.key))?
.ok_or_else(|| format!("ephemeral '{}': no provider resolved key", param.key))?;
let hash = blake3_hash(&secret.value);
results.push(ResolvedEphemeral {
key: param.key.clone(),
value: secret.value,
hash,
});
}
Ok(results)
}
pub fn to_records(resolved: &[ResolvedEphemeral]) -> Vec<EphemeralRecord> {
resolved
.iter()
.map(|r| EphemeralRecord {
key: r.key.clone(),
hash: r.hash.clone(),
})
.collect()
}
pub fn check_drift(current: &[ResolvedEphemeral], stored: &[EphemeralRecord]) -> Vec<DriftResult> {
let stored_map: HashMap<&str, &str> = stored
.iter()
.map(|r| (r.key.as_str(), r.hash.as_str()))
.collect();
current
.iter()
.map(|c| {
let status = match stored_map.get(c.key.as_str()) {
Some(stored_hash) if *stored_hash == c.hash => DriftStatus::Unchanged,
Some(_) => DriftStatus::Changed,
None => DriftStatus::New,
};
DriftResult {
key: c.key.clone(),
status,
}
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DriftResult {
pub key: String,
pub status: DriftStatus,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DriftStatus {
Unchanged,
Changed,
New,
}
pub fn substitute_ephemerals(template: &str, resolved: &[ResolvedEphemeral]) -> String {
let mut result = template.to_string();
for r in resolved {
let pattern = format!("{{{{ephemeral.{}}}}}", r.key);
result = result.replace(&pattern, &r.value);
}
result
}
fn blake3_hash(value: &str) -> String {
let hash = blake3::hash(value.as_bytes());
hash.to_hex().to_string()
}
pub fn blake3_keyed_hash(key: &[u8; 32], value: &str) -> String {
let hash = blake3::keyed_hash(key, value.as_bytes());
hash.to_hex().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::secret_provider::{EnvProvider, ProviderChain};
#[test]
fn blake3_hash_deterministic() {
let h1 = blake3_hash("secret-value");
let h2 = blake3_hash("secret-value");
assert_eq!(h1, h2);
assert_eq!(h1.len(), 64); }
#[test]
fn blake3_hash_different_inputs() {
let h1 = blake3_hash("secret-a");
let h2 = blake3_hash("secret-b");
assert_ne!(h1, h2);
}
#[test]
fn keyed_hash_works() {
let key = [0u8; 32];
let h1 = blake3_keyed_hash(&key, "data");
let h2 = blake3_keyed_hash(&key, "data");
assert_eq!(h1, h2);
assert_eq!(h1.len(), 64);
}
#[test]
fn keyed_hash_different_keys() {
let k1 = [0u8; 32];
let k2 = [1u8; 32];
let h1 = blake3_keyed_hash(&k1, "data");
let h2 = blake3_keyed_hash(&k2, "data");
assert_ne!(h1, h2);
}
#[test]
fn to_records_strips_plaintext() {
let resolved = vec![ResolvedEphemeral {
key: "db_pass".into(),
value: "s3cret".into(),
hash: blake3_hash("s3cret"),
}];
let records = to_records(&resolved);
assert_eq!(records.len(), 1);
assert_eq!(records[0].key, "db_pass");
assert_eq!(records[0].hash, blake3_hash("s3cret"));
}
#[test]
fn check_drift_unchanged() {
let hash = blake3_hash("val");
let current = vec![ResolvedEphemeral {
key: "k".into(),
value: "val".into(),
hash: hash.clone(),
}];
let stored = vec![EphemeralRecord {
key: "k".into(),
hash,
}];
let results = check_drift(¤t, &stored);
assert_eq!(results[0].status, DriftStatus::Unchanged);
}
#[test]
fn check_drift_changed() {
let current = vec![ResolvedEphemeral {
key: "k".into(),
value: "new-val".into(),
hash: blake3_hash("new-val"),
}];
let stored = vec![EphemeralRecord {
key: "k".into(),
hash: blake3_hash("old-val"),
}];
let results = check_drift(¤t, &stored);
assert_eq!(results[0].status, DriftStatus::Changed);
}
#[test]
fn check_drift_new_key() {
let current = vec![ResolvedEphemeral {
key: "new-key".into(),
value: "val".into(),
hash: blake3_hash("val"),
}];
let stored: Vec<EphemeralRecord> = vec![];
let results = check_drift(¤t, &stored);
assert_eq!(results[0].status, DriftStatus::New);
}
#[test]
fn substitute_ephemerals_replaces() {
let resolved = vec![
ResolvedEphemeral {
key: "db_pass".into(),
value: "s3cret".into(),
hash: String::new(),
},
ResolvedEphemeral {
key: "api_key".into(),
value: "abc123".into(),
hash: String::new(),
},
];
let template = "postgres://user:{{ephemeral.db_pass}}@host/db?key={{ephemeral.api_key}}";
let result = substitute_ephemerals(template, &resolved);
assert_eq!(result, "postgres://user:s3cret@host/db?key=abc123");
}
#[test]
fn substitute_no_match_unchanged() {
let resolved = vec![];
let template = "no {{ephemeral.x}} substitution";
let result = substitute_ephemerals(template, &resolved);
assert_eq!(result, "no {{ephemeral.x}} substitution");
}
#[test]
fn resolve_with_env_provider() {
let chain = ProviderChain::new().with(Box::new(EnvProvider));
let params = vec![EphemeralParam {
key: "path".into(),
provider_key: "PATH".into(),
}];
let resolved = resolve_ephemerals(¶ms, &chain).unwrap();
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].key, "path");
assert!(!resolved[0].value.is_empty());
assert_eq!(resolved[0].hash.len(), 64);
}
#[test]
fn resolve_missing_key_errors() {
let chain = ProviderChain::new();
let params = vec![EphemeralParam {
key: "missing".into(),
provider_key: "NONEXISTENT_KEY_12345".into(),
}];
let result = resolve_ephemerals(¶ms, &chain);
assert!(result.is_err());
assert!(result.unwrap_err().contains("no provider resolved"));
}
#[test]
fn ephemeral_record_serde() {
let record = EphemeralRecord {
key: "db_pass".into(),
hash: blake3_hash("test"),
};
let json = serde_json::to_string(&record).unwrap();
let parsed: EphemeralRecord = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.key, "db_pass");
assert_eq!(parsed.hash, record.hash);
}
}