Skip to main content

synaptic_secrets/
registry.rs

1use std::collections::HashMap;
2use std::sync::{Arc, RwLock};
3
4use synaptic_core::SynapticError;
5
6struct SecretEntry {
7    value: String,
8    mask: String,
9}
10
11/// Registry for managing secrets that should be masked in AI outputs.
12///
13/// Secrets are registered with a name and value, and optionally a custom mask.
14/// The registry can mask occurrences of secret values in text, and inject
15/// secret values into templates using `{{secret:name}}` syntax.
16pub struct SecretRegistry {
17    secrets: Arc<RwLock<HashMap<String, SecretEntry>>>,
18}
19
20impl Default for SecretRegistry {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl SecretRegistry {
27    pub fn new() -> Self {
28        Self {
29            secrets: Arc::new(RwLock::new(HashMap::new())),
30        }
31    }
32
33    /// Register a secret with default mask `[REDACTED:name]`.
34    pub fn register(&self, name: &str, value: &str) {
35        let mask = format!("[REDACTED:{}]", name);
36        self.register_with_mask(name, value, &mask);
37    }
38
39    /// Register a secret with a custom mask string.
40    pub fn register_with_mask(&self, name: &str, value: &str, mask: &str) {
41        let mut secrets = self.secrets.write().unwrap();
42        secrets.insert(
43            name.to_string(),
44            SecretEntry {
45                value: value.to_string(),
46                mask: mask.to_string(),
47            },
48        );
49    }
50
51    /// Replace all secret values in the text with their masks.
52    pub fn mask_output(&self, text: &str) -> String {
53        let secrets = self.secrets.read().unwrap();
54        let mut result = text.to_string();
55        // Sort by value length descending to handle overlapping secrets
56        let mut entries: Vec<_> = secrets.values().collect();
57        entries.sort_by(|a, b| b.value.len().cmp(&a.value.len()));
58        for entry in entries {
59            if !entry.value.is_empty() {
60                result = result.replace(&entry.value, &entry.mask);
61            }
62        }
63        result
64    }
65
66    /// Inject secret values into a template string.
67    ///
68    /// Replaces `{{secret:name}}` patterns with the actual secret value.
69    pub fn inject(&self, template: &str) -> Result<String, SynapticError> {
70        let secrets = self.secrets.read().unwrap();
71        let re = regex::Regex::new(r"\{\{secret:(\w+)\}\}")
72            .map_err(|e| SynapticError::Config(format!("invalid regex: {}", e)))?;
73
74        let mut result = template.to_string();
75        for cap in re.captures_iter(template) {
76            let full_match = &cap[0];
77            let name = &cap[1];
78            match secrets.get(name) {
79                Some(entry) => {
80                    result = result.replace(full_match, &entry.value);
81                }
82                None => {
83                    return Err(SynapticError::Config(format!(
84                        "secret '{}' not found in registry",
85                        name
86                    )));
87                }
88            }
89        }
90        Ok(result)
91    }
92
93    /// Remove a secret from the registry.
94    pub fn remove(&self, name: &str) {
95        let mut secrets = self.secrets.write().unwrap();
96        secrets.remove(name);
97    }
98}