Skip to main content

oxicode_ai/
secret.rs

1//! Wrapper type to prevent accidental exposure of sensitive data (e.g., API keys).
2//!
3//! `Secret<T>` masks values in `Debug` and `Display` implementations.
4//! The actual value is only accessible via [`expose()`](Secret::expose).
5
6use std::fmt;
7
8/// A wrapper for sensitive data such as API keys.
9///
10/// # Examples
11/// ```ignore
12/// let key = Secret::new("sk-ant-1234567890abcdef".to_string());
13/// println!("{key:?}");   // Secret([REDACTED])
14/// println!("{key}");     // sk-an...cdef
15/// let header = format!("Bearer {}", key.expose());
16/// ```
17#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct Secret<T> {
19    inner: T,
20}
21
22impl<T> Secret<T> {
23    /// Wraps a sensitive value.
24    pub fn new(value: T) -> Self {
25        Self { inner: value }
26    }
27
28    /// Access the underlying value. Use only in essential logic such as HTTP header generation.
29    pub fn expose(&self) -> &T {
30        &self.inner
31    }
32
33    /// Extract the value by transferring ownership.
34    pub fn expose_owned(self) -> T {
35        self.inner
36    }
37
38    /// Transform the inner value.
39    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Secret<U> {
40        Secret::new(f(self.inner))
41    }
42}
43
44impl<T> fmt::Debug for Secret<T> {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.write_str("Secret([REDACTED])")
47    }
48}
49
50impl fmt::Display for Secret<String> {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        let s = &self.inner;
53        if s.len() > 8 {
54            write!(f, "{}...{}", &s[..4], &s[s.len() - 4..])
55        } else {
56            f.write_str("[REDACTED]")
57        }
58    }
59}
60
61impl serde::Serialize for Secret<String> {
62    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
63        // Don't serialize the actual secret value
64        s.serialize_str("[REDACTED]")
65    }
66}
67
68impl<'de> serde::Deserialize<'de> for Secret<String> {
69    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
70        Ok(Self::new(String::deserialize(d)?))
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn debug_masks_value() {
80        let s = Secret::new("sk-ant-12345678".to_string());
81        assert_eq!(format!("{s:?}"), "Secret([REDACTED])");
82    }
83
84    #[test]
85    fn display_short_value_redacted() {
86        let s = Secret::new("short".to_string());
87        assert_eq!(format!("{s}"), "[REDACTED]");
88    }
89
90    #[test]
91    fn display_long_value_partial() {
92        let s = Secret::new("sk-ant-12345678".to_string());
93        assert_eq!(format!("{s}"), "sk-a...5678");
94    }
95
96    #[test]
97    fn expose_returns_inner() {
98        let s = Secret::new("secret-key".to_string());
99        assert_eq!(s.expose(), "secret-key");
100    }
101
102    #[test]
103    fn expose_owned_consumes() {
104        let s = Secret::new("my-key".to_string());
105        let inner = s.expose_owned();
106        assert_eq!(inner, "my-key");
107    }
108
109    #[test]
110    fn map_transforms() {
111        let s = Secret::new("key".to_string());
112        let mapped = s.map(|v| v.len());
113        assert_eq!(*mapped.expose(), 3);
114    }
115
116    #[test]
117    fn serde_roundtrip() {
118        let s = Secret::new("sk-test-123".to_string());
119        let json = serde_json::to_string(&s).unwrap();
120        // Serialize should mask the value
121        assert!(json.contains("[REDACTED]"));
122        assert!(!json.contains("sk-test-123"));
123        // Deserialize still works (from non-redacted JSON)
124        let input_json = r#""sk-test-123""#;
125        let back: Secret<String> = serde_json::from_str(input_json).unwrap();
126        assert_eq!(back.expose(), "sk-test-123");
127    }
128}