Skip to main content

cachekit/
config.rs

1use std::time::Duration;
2
3use zeroize::Zeroizing;
4
5use crate::error::CachekitError;
6
7// ── CachekitConfig ────────────────────────────────────────────────────────────
8
9/// Runtime configuration for a [`crate::client::CacheKit`] instance.
10pub struct CachekitConfig {
11    /// API key for cachekit.io authentication.
12    pub api_key: Option<Zeroizing<String>>,
13    /// Base URL of the cachekit.io API.
14    pub api_url: String,
15    /// Master key used for zero-knowledge encryption (AES-256-GCM).
16    pub master_key: Option<Zeroizing<Vec<u8>>>,
17    /// Default TTL for cache entries when none is specified at call site.
18    pub default_ttl: Duration,
19    /// Optional namespace prefix applied to all cache keys.
20    pub namespace: Option<String>,
21    /// Maximum number of entries in the L1 in-process cache.
22    pub l1_capacity: usize,
23    /// Maximum allowed payload size in bytes. Larger payloads are rejected.
24    pub max_payload_bytes: usize,
25}
26
27impl std::fmt::Debug for CachekitConfig {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        let api_key_repr = if self.api_key.is_some() {
30            "[REDACTED]"
31        } else {
32            "None"
33        };
34        let master_key_repr = if self.master_key.is_some() {
35            "[REDACTED]"
36        } else {
37            "None"
38        };
39
40        f.debug_struct("CachekitConfig")
41            .field("api_key", &api_key_repr)
42            .field("api_url", &self.api_url)
43            .field("master_key", &master_key_repr)
44            .field("default_ttl", &self.default_ttl)
45            .field("namespace", &self.namespace)
46            .field("l1_capacity", &self.l1_capacity)
47            .field("max_payload_bytes", &self.max_payload_bytes)
48            .finish()
49    }
50}
51
52impl Default for CachekitConfig {
53    fn default() -> Self {
54        Self {
55            api_key: None,
56            api_url: "https://api.cachekit.io".to_owned(),
57            master_key: None,
58            default_ttl: Duration::from_secs(300),
59            namespace: None,
60            l1_capacity: 1000,
61            max_payload_bytes: 5 * 1024 * 1024, // 5 MiB
62        }
63    }
64}
65
66impl CachekitConfig {
67    /// Build configuration from environment variables.
68    ///
69    /// | Variable | Description |
70    /// |---|---|
71    /// | `CACHEKIT_API_KEY` | API key for cachekit.io |
72    /// | `CACHEKIT_API_URL` | Override API base URL (must be HTTPS) |
73    /// | `CACHEKIT_MASTER_KEY` | Hex-encoded master key (min 32 bytes) |
74    /// | `CACHEKIT_DEFAULT_TTL` | Default TTL in seconds (min 1) |
75    pub fn from_env() -> Result<Self, CachekitError> {
76        let mut config = Self::default();
77
78        // API key
79        if let Ok(val) = std::env::var("CACHEKIT_API_KEY") {
80            config.api_key = Some(Zeroizing::new(val));
81        }
82
83        // API URL — must be HTTPS
84        if let Ok(val) = std::env::var("CACHEKIT_API_URL") {
85            validate_https(&val)?;
86            config.api_url = val;
87        }
88
89        // Master key — hex-decode and validate length >= 32 bytes
90        if let Ok(val) = std::env::var("CACHEKIT_MASTER_KEY") {
91            let bytes = hex::decode(&val).map_err(|e| {
92                CachekitError::Config(format!("CACHEKIT_MASTER_KEY is not valid hex: {e}"))
93            })?;
94            if bytes.len() < 32 {
95                return Err(CachekitError::Config(format!(
96                    "CACHEKIT_MASTER_KEY must be at least 32 bytes ({} hex chars); got {} bytes",
97                    64,
98                    bytes.len()
99                )));
100            }
101            config.master_key = Some(Zeroizing::new(bytes));
102        }
103
104        // Default TTL — minimum 1 second
105        if let Ok(val) = std::env::var("CACHEKIT_DEFAULT_TTL") {
106            let secs: u64 = val.parse().map_err(|e| {
107                CachekitError::Config(format!("CACHEKIT_DEFAULT_TTL must be an integer: {e}"))
108            })?;
109            if secs < 1 {
110                return Err(CachekitError::Config(
111                    "CACHEKIT_DEFAULT_TTL must be at least 1 second".to_owned(),
112                ));
113            }
114            config.default_ttl = Duration::from_secs(secs);
115        }
116
117        Ok(config)
118    }
119}
120
121// ── CachekitConfigBuilder ─────────────────────────────────────────────────────
122
123/// Fluent builder for [`CachekitConfig`].
124#[derive(Default)]
125#[must_use]
126pub struct CachekitConfigBuilder {
127    inner: CachekitConfig,
128}
129
130impl CachekitConfigBuilder {
131    /// Create a new builder with defaults.
132    pub fn new() -> Self {
133        Self {
134            inner: CachekitConfig::default(),
135        }
136    }
137
138    /// Set the API key.
139    pub fn api_key(mut self, key: impl Into<String>) -> Self {
140        self.inner.api_key = Some(Zeroizing::new(key.into()));
141        self
142    }
143
144    /// Set the API base URL. Must use HTTPS.
145    pub fn api_url(mut self, url: impl Into<String>) -> Result<Self, CachekitError> {
146        let url = url.into();
147        validate_https(&url)?;
148        self.inner.api_url = url;
149        Ok(self)
150    }
151
152    /// Set the master key from a hex string. Must decode to at least 32 bytes.
153    pub fn master_key(mut self, hex_key: &str) -> Result<Self, CachekitError> {
154        let bytes = hex::decode(hex_key)
155            .map_err(|e| CachekitError::Config(format!("master_key is not valid hex: {e}")))?;
156        if bytes.len() < 32 {
157            return Err(CachekitError::Config(format!(
158                "master_key must be at least 32 bytes; got {}",
159                bytes.len()
160            )));
161        }
162        self.inner.master_key = Some(Zeroizing::new(bytes));
163        Ok(self)
164    }
165
166    /// Set the default TTL. Must be at least 1 second.
167    pub fn default_ttl(mut self, ttl: Duration) -> Result<Self, CachekitError> {
168        if ttl < Duration::from_secs(1) {
169            return Err(CachekitError::Config(
170                "default_ttl must be at least 1 second".to_owned(),
171            ));
172        }
173        self.inner.default_ttl = ttl;
174        Ok(self)
175    }
176
177    /// Set the namespace prefix.
178    pub fn namespace(mut self, ns: impl Into<String>) -> Self {
179        self.inner.namespace = Some(ns.into());
180        self
181    }
182
183    /// Set the L1 cache capacity (max entries).
184    pub fn l1_capacity(mut self, capacity: usize) -> Self {
185        self.inner.l1_capacity = capacity;
186        self
187    }
188
189    /// Finalise and return the [`CachekitConfig`].
190    pub fn build(self) -> CachekitConfig {
191        self.inner
192    }
193}
194
195// ── Helpers ───────────────────────────────────────────────────────────────────
196
197fn validate_https(url: &str) -> Result<(), CachekitError> {
198    if !url.starts_with("https://") {
199        return Err(CachekitError::Config(format!(
200            "API URL must use HTTPS; got: {url}"
201        )));
202    }
203    Ok(())
204}