Skip to main content

cachekit/
client.rs

1use std::borrow::Cow;
2use std::sync::Arc;
3use std::time::Duration;
4
5use bytes::Bytes;
6
7use serde::{de::DeserializeOwned, Serialize};
8
9use crate::backend::Backend;
10#[cfg(feature = "l1")]
11use crate::backend::{ttl_wire_secs, Freshness};
12use crate::error::CachekitError;
13use crate::metrics::{CacheCounters, L1Stats, ReadOutcome};
14use crate::serializer;
15
16// ── SharedBackend type alias ──────────────────────────────────────────────────
17
18/// Reference-counted pointer to a heap-allocated backend.
19///
20/// On native targets (without `unsync`) we require `Send + Sync` via `Arc`.
21/// On `wasm32` or with the `unsync` feature, `Rc` is used instead — the runtime
22/// is single-threaded so `Send` bounds are unnecessary.
23#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
24pub type SharedBackend = std::sync::Arc<dyn Backend>;
25
26/// Reference-counted pointer to a heap-allocated backend (`?Send` variant).
27#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
28pub type SharedBackend = std::rc::Rc<dyn Backend>;
29
30// ── SharedFlight type alias ──────────────────────────────────────────────────
31
32/// Reference-counted pointer to the single-flight map, so client clones share
33/// fill-dedup state (two clones racing a cold miss must collapse to one fill).
34#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
35pub(crate) type SharedFlight = std::sync::Arc<crate::flight::FlightMap>;
36
37#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
38pub(crate) type SharedFlight = std::rc::Rc<crate::flight::FlightMap>;
39
40/// Separate same-key ordering from single-flight: a refresh holds the flight
41/// lock while computing, then takes this lock only for its version-checked
42/// commit. Direct reads/writes/deletes take the same mutation lock.
43#[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
44type SharedMutations = std::sync::Arc<crate::flight::MutationMap>;
45
46// ── SharedEncryption type alias ──────────────────────────────────────────────
47
48/// Reference-counted pointer to the encryption layer.
49///
50/// On native targets (without `unsync`) `Arc` is used (requires `Sync`).
51/// On `wasm32` or with `unsync`, `Rc` is used — avoids the `!Sync` problem
52/// caused by `Cell<u64>` inside cachekit-core's nonce counter.
53#[cfg(all(
54    feature = "encryption",
55    not(any(target_arch = "wasm32", feature = "unsync"))
56))]
57type SharedEncryption = std::sync::Arc<crate::encryption::EncryptionLayer>;
58
59#[cfg(all(
60    feature = "encryption",
61    any(target_arch = "wasm32", feature = "unsync")
62))]
63type SharedEncryption = std::rc::Rc<crate::encryption::EncryptionLayer>;
64
65// ── Key validation ────────────────────────────────────────────────────────────
66
67const MAX_KEY_BYTES: usize = 1024;
68
69/// Maximum TTL for L1 entries populated from L2 cache hits.
70/// Uses a short ceiling to limit staleness when the original TTL is unknown.
71/// A server `X-CacheKit-Fresh-For` bounds it further, and a stale read or a
72/// zero bound is not backfilled at all — see [`l1_backfill_ttl`].
73///
74/// Reconciliation with stale-while-revalidate: a backfilled entry's SWR
75/// freshness window derives from this capped TTL (window = ratio × entry
76/// TTL), **not** from the write-path TTL — the cap is the staleness bound
77/// for L2-derived data, deliberately kept. SWR removes the cap's expiry
78/// cliff instead: past ~ratio × 30 s the entry is served stale while one
79/// background refresh re-executes the origin. If no newer mutation replaced
80/// the entry, that refresh writes both layers and renews L1 hard expiry with
81/// the caller's full TTL. Without SWR the entry simply hard-expires at the cap
82/// and the next read blocks on L2, as before.
83#[cfg(feature = "l1")]
84const L1_BACKFILL_TTL_SECS: u64 = 30;
85
86/// L1 lifetime for a backfill from an L2 hit, or `None` when the read must not
87/// be backfilled (`spec/saas-api.md` § Remaining Freshness). A stale label or a
88/// zero `Fresh-For` forbids it; a present `Fresh-For` is a hard bound under the
89/// 30 s cap, which SWR cannot outlive because L1 never serves an entry past its TTL.
90/// With no header the cap applies unchanged.
91#[cfg(feature = "l1")]
92fn l1_backfill_ttl(default_ttl: Duration, freshness: Freshness) -> Option<Duration> {
93    let cap = std::cmp::min(default_ttl, Duration::from_secs(L1_BACKFILL_TTL_SECS));
94    let ttl = freshness
95        .fresh_for
96        .map_or(cap, |fresh_for| cap.min(fresh_for));
97    (!freshness.is_stale && !ttl.is_zero()).then_some(ttl)
98}
99
100fn validate_key(key: &str) -> Result<(), CachekitError> {
101    if key.is_empty() {
102        return Err(CachekitError::InvalidKey(
103            "key must not be empty".to_owned(),
104        ));
105    }
106    if key.len() > MAX_KEY_BYTES {
107        return Err(CachekitError::InvalidKey(format!(
108            "key is {} bytes (limit: {MAX_KEY_BYTES})",
109            key.len()
110        )));
111    }
112    if !has_control_byte(key.as_bytes()) {
113        return Ok(());
114    }
115    // Rejection only: find the first offending byte to name it.
116    key.bytes()
117        .find(|&b| is_control_byte(b))
118        .map_or(Ok(()), |b| {
119            Err(CachekitError::InvalidKey(format!(
120                "key contains illegal control character 0x{b:02X}"
121            )))
122        })
123}
124
125/// A byte no key may hold: a control character below 0x20, or DEL.
126const fn is_control_byte(b: u8) -> bool {
127    (b < 0x20) | (b == 0x7F)
128}
129
130/// Whether any byte of `bytes` is a control byte.
131///
132/// Every validated key passes through here, so it reads 16 bytes at a time:
133/// a block's compares are folded with no early exit, which lets the compiler
134/// vectorise them where a byte loop spends a branch per byte. When the length
135/// is not a multiple of 16, the last block overlaps the one before it instead
136/// of leaving a byte-at-a-time tail.
137fn has_control_byte(bytes: &[u8]) -> bool {
138    fn any_control(block: &[u8]) -> bool {
139        block
140            .iter()
141            .fold(false, |found, &b| found | is_control_byte(b))
142    }
143    if bytes.len() < 16 {
144        return any_control(bytes);
145    }
146    let (blocks, _) = bytes.as_chunks::<16>();
147    blocks.iter().any(|block| any_control(block)) || any_control(&bytes[bytes.len() - 16..])
148}
149
150// ── Stale-while-revalidate ───────────────────────────────────────────────────
151//
152// SWR needs an L1 to age entries in and a spawnable (`Send`) runtime for the
153// background refresh — native, non-`unsync` targets with the `l1` feature.
154// Everywhere else the SWR read path degrades to the plain read path and
155// `SwrRead::Stale` is never produced.
156
157/// Outcome of an SWR-aware typed read — see [`CacheKit::interop_get_swr`].
158#[derive(Debug, Clone, PartialEq)]
159pub enum SwrRead<T> {
160    /// Cache hit within the freshness window (or an L2 hit): use directly.
161    Fresh(T),
162    /// L1 hit past the freshness threshold but before hard expiry: use the
163    /// value now, and schedule a background refresh (the `#[cachekit]` macro
164    /// re-executes the function, deduplicated on the [`CacheKit::single_flight`]
165    /// locks without waiting on them). The token
166    /// makes refresh completion conditional: a newer set or delete wins.
167    Stale(T, SwrToken),
168    /// No usable entry: fall through to a normal blocking miss + fill.
169    Miss,
170}
171
172/// Mutation version captured by an SWR stale read.
173///
174/// Pass this back only through the `#[cachekit]`-generated refresh path. It
175/// prevents an older background computation from overwriting a newer write
176/// or resurrecting a deleted entry on this client or any of its clones.
177#[derive(Clone)]
178pub struct SwrToken {
179    #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
180    state: std::sync::Arc<crate::flight::MutationState>,
181    version: u64,
182}
183
184impl std::fmt::Debug for SwrToken {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        f.debug_struct("SwrToken")
187            .field("version", &self.version)
188            .finish_non_exhaustive()
189    }
190}
191
192impl PartialEq for SwrToken {
193    fn eq(&self, other: &Self) -> bool {
194        #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
195        {
196            self.version == other.version && std::sync::Arc::ptr_eq(&self.state, &other.state)
197        }
198        #[cfg(not(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32"))))]
199        {
200            self.version == other.version
201        }
202    }
203}
204
205impl Eq for SwrToken {}
206
207// ── CacheKit ─────────────────────────────────────────────────────────────────
208
209/// Cache client with optional L1 in-process cache layer.
210///
211/// `Clone` is cheap and shares everything: backend, L1 cache, single-flight
212/// state, and encryption layer. Clones exist so `'static` background work
213/// (e.g. the SWR refresh spawned by `#[cachekit]`) can hold the client
214/// without borrowing it.
215///
216/// # Encryption
217///
218/// A client built with encryption configured (by any of the builder's
219/// encryption methods, such as
220/// [`encryption_from_env`](CacheKitBuilder::encryption_from_env), or the
221/// `secure` / `secure_from_env` preset) encrypts every
222/// value it writes and decrypts every value it reads — [`Self::get`],
223/// [`Self::interop_get`], [`Self::interop_get_swr`], [`Self::set`] and
224/// [`Self::set_with_ttl`] — and L1 holds ciphertext. Such a client has no
225/// plaintext value path, and the `SecureCache` handle from `secure_cache()`
226/// reads and writes the same entries. [`Self::delete`] and [`Self::exists`]
227/// carry no value and are the same either way.
228#[derive(Clone)]
229pub struct CacheKit {
230    backend: SharedBackend,
231    default_ttl: Duration,
232    namespace: Option<String>,
233    max_payload_bytes: usize,
234    flight: SharedFlight,
235    /// Read counters behind [`Self::stats`]; the backend holds a
236    /// `MetricsProvider` over the same allocation.
237    counters: Arc<CacheCounters>,
238
239    /// The reliability stack's breaker, when configured — kept so
240    /// [`Self::circuit_state`] can read it through the type-erased backend.
241    #[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
242    breaker: Option<Arc<crate::reliability::CircuitBreaker>>,
243
244    #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
245    mutations: SharedMutations,
246
247    #[cfg(feature = "l1")]
248    pub(crate) l1: Option<crate::l1::L1Cache>,
249
250    #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
251    pub(crate) swr_enabled: bool,
252
253    #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
254    swr_threshold_ratio: f64,
255
256    #[cfg(feature = "encryption")]
257    encryption: Option<SharedEncryption>,
258}
259
260impl CacheKit {
261    /// Create a new builder.
262    pub fn builder() -> CacheKitBuilder {
263        CacheKitBuilder::default()
264    }
265
266    /// Build from environment variables via [`crate::config::CachekitConfig::from_env`].
267    ///
268    /// Creates a [`crate::backend::cachekitio::CachekitIO`] backend from the
269    /// config. Requires the `cachekitio` feature.
270    ///
271    /// `CACHEKIT_MASTER_KEY` does not make the client encrypt: a key in the
272    /// environment is a key source, never a switch (protocol
273    /// `spec/intent-presets.md` § Encryption Activation, rule 2). State the
274    /// intent instead — encrypt with the environment's keys, or don't:
275    ///
276    /// ```no_run
277    /// # fn demo() -> Result<(), cachekit::CachekitError> {
278    /// let encrypted = cachekit::CacheKit::from_env()?
279    ///     .encryption_from_env()?
280    ///     .build()?;
281    /// let plaintext = cachekit::CacheKit::from_env()?.no_encryption().build()?;
282    /// # Ok(()) }
283    /// ```
284    ///
285    /// Releases up to 0.11 encrypted whenever the variable was set. So that
286    /// such a deployment cannot silently start writing plaintext, a builder
287    /// from `from_env()` that saw `CACHEKIT_MASTER_KEY` and states neither
288    /// intent fails at [`CacheKitBuilder::build`] with
289    /// [`CachekitError::Config`]. This check is transitional and goes away
290    /// in a later release; without the variable, `from_env()?.build()?`
291    /// builds a plaintext client.
292    ///
293    /// A set key is validated here: one that is empty, not hex, shorter than
294    /// 32 bytes or not UTF-8 is a [`CachekitError::Config`].
295    ///
296    /// The default TTL is 300 s, and no environment variable changes it. Chain
297    /// [`CacheKitBuilder::default_ttl`] on the returned builder, or pass a TTL
298    /// per call with [`CacheKit::set_with_ttl`].
299    #[cfg(all(feature = "cachekitio", not(target_arch = "wasm32")))]
300    pub fn from_env() -> Result<CacheKitBuilder, CachekitError> {
301        use crate::backend::cachekitio::CachekitIO;
302        use crate::config::CachekitConfig;
303
304        let config = CachekitConfig::from_env()?;
305
306        let api_key_z = config
307            .api_key
308            .ok_or_else(|| CachekitError::Config("CACHEKIT_API_KEY is required".to_owned()))?;
309
310        let backend = CachekitIO::builder()
311            .api_key(api_key_z.as_str())
312            .api_url(config.api_url)
313            .build()
314            .map_err(|e| CachekitError::Config(e.to_string()))?;
315
316        #[cfg(not(feature = "unsync"))]
317        let shared: SharedBackend = std::sync::Arc::new(backend);
318        #[cfg(feature = "unsync")]
319        let shared: SharedBackend = std::rc::Rc::new(backend);
320
321        let mut builder = CacheKitBuilder::default()
322            .backend(shared)
323            .default_ttl(config.default_ttl)
324            .max_payload_bytes(config.max_payload_bytes)
325            .l1_capacity(config.l1_capacity);
326
327        if let Some(ns) = config.namespace {
328            builder = builder.namespace(ns);
329        }
330        builder.env_master_key = config.master_key.is_some();
331
332        Ok(builder)
333    }
334
335    // ── Namespacing ───────────────────────────────────────────────────────────
336
337    /// `key` behind the client's namespace prefix. Without a namespace the
338    /// storage key is `key` itself, borrowed: an L1 hit then allocates
339    /// nothing for its key.
340    fn namespaced_key<'k>(&self, key: &'k str) -> Cow<'k, str> {
341        match &self.namespace {
342            Some(ns) => Cow::Owned(format!("{ns}:{key}")),
343            None => Cow::Borrowed(key),
344        }
345    }
346
347    /// Validate key and return the namespaced version.
348    fn resolve_key<'k>(&self, key: &'k str) -> Result<Cow<'k, str>, CachekitError> {
349        validate_key(key)?;
350        Ok(self.namespaced_key(key))
351    }
352
353    // ── L1 helpers ───────────────────────────────────────────────────────────
354
355    /// Try L1 cache first. Returns Some(bytes) on hit.
356    #[cfg(feature = "l1")]
357    fn l1_get(&self, full_key: &str) -> Option<Bytes> {
358        self.l1.as_ref().and_then(|l1| l1.get_shared(full_key))
359    }
360
361    /// Populate L1 from an L2 hit, for no longer than [`l1_backfill_ttl`] allows.
362    #[cfg(feature = "l1")]
363    fn l1_backfill(&self, full_key: &str, bytes: Bytes, freshness: Freshness) {
364        if let (Some(l1), Some(ttl)) = (&self.l1, l1_backfill_ttl(self.default_ttl, freshness)) {
365            l1.set_shared(full_key, bytes, ttl);
366        }
367    }
368
369    /// Write-through to L1 after a write the backend accepted. The copy lives
370    /// no longer than the TTL the write sent on the wire ([`ttl_wire_secs`]):
371    /// `spec/saas-api.md` forbids serving it past `min(local_ttl, effective_ttl)`,
372    /// and the wire truncates a fractional TTL from 1 s up (1.5 s goes as 1).
373    #[cfg(feature = "l1")]
374    fn l1_set(&self, full_key: &str, bytes: Bytes, ttl: Duration) {
375        if let Some(ref l1) = self.l1 {
376            let wire = Duration::from_secs(ttl_wire_secs(ttl));
377            l1.set_shared(full_key, bytes, ttl.min(wire));
378        }
379    }
380
381    /// Invalidate L1 entry.
382    #[cfg(feature = "l1")]
383    fn l1_delete(&self, full_key: &str) {
384        if let Some(ref l1) = self.l1 {
385            l1.delete(full_key);
386        }
387    }
388
389    #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
390    async fn lock_l1_mutation(&self, full_key: &str) -> Option<crate::flight::MutationGuard> {
391        if self.l1.is_some() {
392            Some(self.mutations.lock(full_key).await)
393        } else {
394            None
395        }
396    }
397
398    #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
399    async fn complete_swr_bytes(
400        &self,
401        key: &str,
402        bytes: Vec<u8>,
403        ttl: Duration,
404        token: SwrToken,
405    ) -> Result<bool, CachekitError> {
406        Self::validate_ttl(ttl)?;
407        self.check_payload_size(bytes.len())?;
408        let full_key = self.resolve_key(key)?;
409        let Some(mutation) = self.lock_l1_mutation(&full_key).await else {
410            return Ok(false);
411        };
412
413        // Check before touching L2. The mutation state lives independently of
414        // the L1 entry, so hard expiry or capacity eviction does not look
415        // like an explicit set/delete and waste a valid origin result.
416        if !mutation.is_current(&token.state, token.version) {
417            return Ok(false);
418        }
419        let l1_bytes = Bytes::copy_from_slice(&bytes);
420        self.backend.set(&full_key, bytes, Some(ttl)).await?;
421        self.l1_set(&full_key, l1_bytes, ttl);
422        mutation.advance();
423        crate::metrics::trace_write(&full_key, ttl);
424        Ok(true)
425    }
426
427    #[cfg(not(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32"))))]
428    async fn complete_swr_bytes(
429        &self,
430        _key: &str,
431        _bytes: Vec<u8>,
432        _ttl: Duration,
433        _token: SwrToken,
434    ) -> Result<bool, CachekitError> {
435        // `SwrRead::Stale` is unreachable on this build, so there is no
436        // versioned entry a caller could legitimately complete.
437        Ok(false)
438    }
439
440    /// Rejects a zero TTL.
441    fn validate_ttl(ttl: Duration) -> Result<(), CachekitError> {
442        if ttl.is_zero() {
443            return Err(CachekitError::Config(
444                "TTL must be greater than zero".to_owned(),
445            ));
446        }
447        Ok(())
448    }
449
450    // ── Public operations ─────────────────────────────────────────────────────
451
452    /// Retrieve and deserialize a value stored under `key`.
453    ///
454    /// Returns `None` if the key does not exist.
455    /// Checks L1 cache before hitting the backend.
456    ///
457    /// Values are MessagePack. A `Vec<u8>` field written by [`Self::set`]
458    /// as a plain `Vec<u8>` decodes back here unchanged, but it is stored as
459    /// an array of integers; see [`Self::set`] for the compact form.
460    ///
461    /// # Errors
462    ///
463    /// On a client with encryption configured, returns
464    /// [`CachekitError::Encryption`] — not a miss — if the stored entry fails
465    /// decryption. The key's L1 copy is dropped first and the backend entry
466    /// is left in place, so the next read reaches the backend: a hit once the
467    /// entry is replaced with ciphertext this client can decrypt, a miss once
468    /// it expires or is deleted. A decrypted entry that does not decode as `T`
469    /// is [`CachekitError::Serialization`] naming only `T`, never quoting the
470    /// value.
471    pub async fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, CachekitError> {
472        #[cfg(feature = "encryption")]
473        if let Some(secure) = self.encrypting() {
474            return secure.get(key).await;
475        }
476        match self.get_bytes(key).await? {
477            Some(bytes) => Ok(Some(serializer::deserialize(&bytes)?)),
478            None => Ok(None),
479        }
480    }
481
482    /// Retrieve and deserialize an interop-mode value stored under `key`.
483    ///
484    /// Identical to [`Self::get`] except the payload is decoded with
485    /// [`crate::interop::deserialize`]. Both consume exactly one MessagePack
486    /// document and reject trailing bytes (interop/v1 spec MUST); this one
487    /// also names a Python-SDK-internal CK frame with a specific diagnostic
488    /// instead of a generic trailing-bytes error.
489    ///
490    /// Use with keys from [`crate::interop::interop_key`] on a client
491    /// **without** a namespace prefix. There is no interop-specific write
492    /// method: [`Self::set`] already writes plain MessagePack (no ByteStorage
493    /// envelope), which is the interop value format. On a client with
494    /// encryption configured the stored bytes are AES-GCM ciphertext of that
495    /// MessagePack, decrypted here before decoding: in interop mode the
496    /// AES-GCM plaintext is the plain MessagePack value bytes, so the AAD
497    /// (v0x03, `format="msgpack"`, `compressed="False"`) verifies cross-SDK
498    /// unchanged.
499    ///
500    /// Unlike this read, [`Self::set`] / [`Self::set_with_ttl`] do not check
501    /// the client namespace: on a client with [`CacheKitBuilder::namespace`]
502    /// set they return `Ok` and store the entry under
503    /// `{namespace}:{interop_key}`, which no interop reader addresses. Interop
504    /// writes need a client without a namespace.
505    ///
506    /// # Errors
507    ///
508    /// Returns [`CachekitError::Config`] if the client was built with
509    /// [`CacheKitBuilder::namespace`]: the prefix
510    /// would rewrite the storage key to `{prefix}:{interop_key}`, which no
511    /// other SDK computes — every cross-SDK entry would silently miss. Interop
512    /// keys carry their own namespace segment; failing loudly here beats a
513    /// 100% miss rate that looks like a cold cache. With encryption
514    /// configured, an entry that fails decryption is
515    /// [`CachekitError::Encryption`], with the key's L1 copy dropped as for
516    /// [`Self::get`].
517    pub async fn interop_get<T: DeserializeOwned>(
518        &self,
519        key: &str,
520    ) -> Result<Option<T>, CachekitError> {
521        #[cfg(feature = "encryption")]
522        if let Some(secure) = self.encrypting() {
523            return secure.interop_get(key).await;
524        }
525        self.reject_namespaced_interop()?;
526        match self.get_bytes(key).await? {
527            Some(bytes) => Ok(Some(crate::interop::deserialize(&bytes)?)),
528            None => Ok(None),
529        }
530    }
531
532    /// Interop keys must reach the backend verbatim; a client namespace prefix
533    /// would silently produce storage keys no other SDK computes.
534    fn reject_namespaced_interop(&self) -> Result<(), CachekitError> {
535        match self.namespace {
536            None => Ok(()),
537            Some(_) => Err(CachekitError::Config(
538                "interop reads require a client without a namespace prefix: .namespace() \
539                 would store interop entries under {prefix}:{interop_key}, \
540                 which other SDKs never compute (interop keys already carry a namespace \
541                 segment) — use a dedicated non-namespaced client for interop entries"
542                    .to_owned(),
543            )),
544        }
545    }
546
547    /// Retrieve and deserialize an interop-mode value with SWR classification.
548    ///
549    /// Identical to [`Self::interop_get`] except an L1 hit is classified
550    /// against the client's stale-while-revalidate freshness window:
551    ///
552    /// - [`SwrRead::Fresh`] — L1 hit within `swr_threshold_ratio` of the
553    ///   entry's TTL (±10% jitter), or any L2 hit. Use directly.
554    /// - [`SwrRead::Stale`] — L1 hit past the threshold but **before hard
555    ///   expiry**: the value is returned without touching the backend or
556    ///   origin, and the caller should schedule exactly one background
557    ///   refresh. The `#[cachekit]` macro dedups it on the cold-miss
558    ///   [`Self::single_flight`] locks without waiting for another worker's
559    ///   fill: a refresh that finds either lock held, or whose lock call
560    ///   fails, stands down and the stale value keeps being served. The
561    ///   accompanying [`SwrToken`] makes completion conditional,
562    ///   so a newer set/delete always wins.
563    /// - [`SwrRead::Miss`] — nothing usable anywhere: normal blocking miss.
564    ///
565    /// A hard-expired L1 entry is a [`SwrRead::Miss`], never `Stale` — L1
566    /// never serves an entry past its TTL, so SWR cannot serve past hard
567    /// expiry.
568    ///
569    /// With SWR disabled ([`CacheKitBuilder::swr_enabled`]`(false)`), without
570    /// the `l1` feature, on wasm32, or under `unsync`, this behaves exactly
571    /// like [`Self::interop_get`]: hits are `Fresh`, `Stale` is never
572    /// produced.
573    ///
574    /// On a client with encryption configured, staleness is judged on the L1
575    /// **ciphertext** entry (freshness metadata never exposes plaintext),
576    /// then the value is decrypted as for [`Self::interop_get`].
577    ///
578    /// # Errors
579    ///
580    /// Same as [`Self::interop_get`] (including the namespaced-client
581    /// rejection).
582    pub async fn interop_get_swr<T: DeserializeOwned>(
583        &self,
584        key: &str,
585    ) -> Result<SwrRead<T>, CachekitError> {
586        #[cfg(feature = "encryption")]
587        if let Some(secure) = self.encrypting() {
588            return secure.interop_get_swr(key).await;
589        }
590        self.reject_namespaced_interop()?;
591        match self.get_bytes_swr(key).await? {
592            SwrRead::Fresh(b) => Ok(SwrRead::Fresh(crate::interop::deserialize(&b)?)),
593            SwrRead::Stale(b, token) => Ok(SwrRead::Stale(crate::interop::deserialize(&b)?, token)),
594            SwrRead::Miss => Ok(SwrRead::Miss),
595        }
596    }
597
598    /// Fetch raw payload bytes with SWR classification and count the read:
599    /// one outcome, by the layer that served it, or one miss.
600    async fn get_bytes_swr(&self, key: &str) -> Result<SwrRead<Bytes>, CachekitError> {
601        let full_key = self.resolve_key(key)?;
602        Ok(match self.read_bytes_swr(&full_key).await? {
603            SwrRead::Fresh((bytes, by)) => {
604                self.counters.record(by, &full_key);
605                SwrRead::Fresh(bytes)
606            }
607            SwrRead::Stale((bytes, by), token) => {
608                self.counters.record(by, &full_key);
609                SwrRead::Stale(bytes, token)
610            }
611            SwrRead::Miss => {
612                self.counters.record(ReadOutcome::Miss, &full_key);
613                SwrRead::Miss
614            }
615        })
616    }
617
618    /// Fetch raw payload bytes for `key` (L1, then L2 with L1 backfill) and
619    /// count the read: one outcome, by the layer that served it, or one miss.
620    async fn get_bytes(&self, key: &str) -> Result<Option<Bytes>, CachekitError> {
621        let full_key = self.resolve_key(key)?;
622        let read = self.read_bytes(&full_key).await?;
623        let by = read.as_ref().map_or(ReadOutcome::Miss, |(_, by)| *by);
624        self.counters.record(by, &full_key);
625        Ok(read.map(|(bytes, _)| bytes))
626    }
627
628    /// The uncounted read behind [`Self::get_bytes_swr`]: an L1 hit is split
629    /// into fresh vs stale against the configured freshness window; on L1
630    /// miss this defers to [`Self::read_bytes`] (L2 + backfill), whose hit is
631    /// always fresh. Each value comes with the layer that served it.
632    async fn read_bytes_swr(
633        &self,
634        full_key: &str,
635    ) -> Result<SwrRead<(Bytes, ReadOutcome)>, CachekitError> {
636        #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
637        if self.swr_enabled {
638            if let Some(ref l1) = self.l1 {
639                match l1.get_with_swr_shared(full_key, self.swr_threshold_ratio) {
640                    crate::l1::SharedSwrRead::Fresh(bytes) => {
641                        self.check_payload_size(bytes.len())?;
642                        return Ok(SwrRead::Fresh((bytes, ReadOutcome::L1Hit)));
643                    }
644                    crate::l1::SharedSwrRead::Stale(_) => {
645                        // Token capture and the stale snapshot must be atomic
646                        // relative to explicit mutations. Re-check after
647                        // taking the per-key guard; a write may have landed
648                        // between the optimistic classification and here.
649                        let mutation = self.mutations.lock(full_key).await;
650                        match l1.get_with_swr_shared(full_key, self.swr_threshold_ratio) {
651                            crate::l1::SharedSwrRead::Fresh(bytes) => {
652                                self.check_payload_size(bytes.len())?;
653                                return Ok(SwrRead::Fresh((bytes, ReadOutcome::L1Hit)));
654                            }
655                            crate::l1::SharedSwrRead::Stale(bytes) => {
656                                self.check_payload_size(bytes.len())?;
657                                let (state, version) = mutation.snapshot();
658                                return Ok(SwrRead::Stale(
659                                    (bytes, ReadOutcome::L1Stale),
660                                    SwrToken { state, version },
661                                ));
662                            }
663                            crate::l1::SharedSwrRead::Miss => {}
664                        }
665                    }
666                    // Absent or hard-expired: fall through to the normal
667                    // read path (the redundant L1 re-check there is a cheap
668                    // in-process miss).
669                    crate::l1::SharedSwrRead::Miss => {}
670                }
671            }
672        }
673
674        Ok(match self.read_bytes(full_key).await? {
675            Some(read) => SwrRead::Fresh(read),
676            None => SwrRead::Miss,
677        })
678    }
679
680    /// The uncounted read behind [`Self::get_bytes`]: L1, then L2 with L1
681    /// backfill, with the layer that served the bytes.
682    async fn read_bytes(
683        &self,
684        full_key: &str,
685    ) -> Result<Option<(Bytes, ReadOutcome)>, CachekitError> {
686        // L1 hit
687        #[cfg(feature = "l1")]
688        if let Some(bytes) = self.l1_get(full_key) {
689            self.check_payload_size(bytes.len())?;
690            return Ok(Some((bytes, ReadOutcome::L1Hit)));
691        }
692
693        // Serialize an L2 read/backfill with same-key writes. Re-check L1
694        // after taking the lock because another operation may have filled it
695        // between the optimistic read above and lock acquisition.
696        #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
697        let _mutation = self.lock_l1_mutation(full_key).await;
698
699        #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
700        if let Some(bytes) = self.l1_get(full_key) {
701            self.check_payload_size(bytes.len())?;
702            return Ok(Some((bytes, ReadOutcome::L1Hit)));
703        }
704
705        // L2 backend
706        let Some((bytes, freshness)) = self.backend.get_with_freshness(full_key).await? else {
707            return Ok(None);
708        };
709
710        self.check_payload_size(bytes.len())?;
711        // Takes over the Vec's allocation: no copy, and L1 shares it below.
712        let bytes = Bytes::from(bytes);
713
714        // Populate L1 on L2 hit, bounded by the cap and the server's freshness
715        #[cfg(feature = "l1")]
716        self.l1_backfill(full_key, bytes.clone(), freshness);
717        #[cfg(not(feature = "l1"))]
718        let _ = freshness;
719
720        Ok(Some((bytes, ReadOutcome::L2Hit)))
721    }
722
723    /// Serialize and store `value` under `key` using the client's default TTL.
724    ///
725    /// Encrypts first on a client with encryption configured — see
726    /// [`Self::set_with_ttl`].
727    ///
728    /// # Binary payloads
729    ///
730    /// Values are serialized as MessagePack through serde. serde treats a
731    /// plain `Vec<u8>` (or `&[u8]`) as a sequence, so it is stored as a
732    /// MessagePack array of integers: up to 2 bytes per byte, and a per-element
733    /// encode and decode. Mark the field with `serde_bytes` to store it as a
734    /// single MessagePack `bin` value instead. `serde_bytes` is a crate you add
735    /// to your own `Cargo.toml`:
736    ///
737    /// ```ignore
738    /// #[derive(serde::Serialize, serde::Deserialize)]
739    /// struct Response {
740    ///     status: u16,
741    ///     #[serde(with = "serde_bytes")]
742    ///     body: Vec<u8>,
743    /// }
744    ///
745    /// cache.set("resp", &Response { status: 200, body }).await?;
746    /// ```
747    ///
748    /// For a bare byte value, store a `serde_bytes::ByteBuf`. Either form
749    /// decodes into either type on [`Self::get`], so switching a field to
750    /// `serde_bytes` still reads entries written before the switch.
751    pub async fn set<T: Serialize>(&self, key: &str, value: &T) -> Result<(), CachekitError> {
752        self.set_with_ttl(key, value, self.default_ttl).await
753    }
754
755    /// Serialize and store `value` under `key` with an explicit `ttl`.
756    ///
757    /// On a client with encryption configured the value is encrypted first:
758    /// the backend and L1 receive only the AES-256-GCM ciphertext, and the
759    /// payload limit applies to the ciphertext.
760    ///
761    /// A positive sub-second `ttl` is sent to the backend as 1 second.
762    ///
763    /// Returns [`CachekitError::Config`] if `ttl` is zero.
764    ///
765    /// The client namespace is not checked here: an interop key written on a
766    /// namespaced client lands under `{namespace}:{interop_key}`, which no
767    /// interop reader addresses — see [`Self::interop_get`].
768    pub async fn set_with_ttl<T: Serialize>(
769        &self,
770        key: &str,
771        value: &T,
772        ttl: Duration,
773    ) -> Result<(), CachekitError> {
774        #[cfg(feature = "encryption")]
775        if let Some(secure) = self.encrypting() {
776            return secure.set_with_ttl(key, value, ttl).await;
777        }
778        Self::validate_ttl(ttl)?;
779
780        let bytes = serializer::serialize(value)?;
781        self.check_payload_size(bytes.len())?;
782
783        let full_key = self.resolve_key(key)?;
784
785        #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
786        let mutation = self.lock_l1_mutation(&full_key).await;
787
788        // Invalidate older refresh tokens before the first backend await, so
789        // cancellation cannot leave an applied/attempted write vulnerable to
790        // a stale background result.
791        #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
792        if let Some(ref mutation) = mutation {
793            mutation.advance();
794        }
795
796        // L1 needs its own copy: the backend consumes the Vec.
797        #[cfg(feature = "l1")]
798        {
799            let l1_bytes = Bytes::copy_from_slice(&bytes);
800            self.backend.set(&full_key, bytes, Some(ttl)).await?;
801            self.l1_set(&full_key, l1_bytes, ttl);
802        }
803        #[cfg(not(feature = "l1"))]
804        {
805            self.backend.set(&full_key, bytes, Some(ttl)).await?;
806        }
807
808        crate::metrics::trace_write(&full_key, ttl);
809        Ok(())
810    }
811
812    /// Commit an SWR refresh only if its stale-read token is still current,
813    /// encrypted when the client has encryption configured. Macro plumbing;
814    /// ordinary writes use [`Self::set_with_ttl`].
815    #[doc(hidden)]
816    pub async fn __complete_swr_refresh<T: Serialize>(
817        &self,
818        key: &str,
819        value: &T,
820        ttl: Duration,
821        token: SwrToken,
822    ) -> Result<bool, CachekitError> {
823        #[cfg(feature = "encryption")]
824        if let Some(secure) = self.encrypting() {
825            return secure.__complete_swr_refresh(key, value, ttl, token).await;
826        }
827        let bytes = serializer::serialize(value)?;
828        self.complete_swr_bytes(key, bytes, ttl, token).await
829    }
830
831    /// Delete `key` and return `true` if it existed.
832    ///
833    /// `CachekitIO` and `WorkersCachekitIO` return `true` on every successful
834    /// delete, whether or not the key existed: the server does not report
835    /// existence on `DELETE`.
836    ///
837    /// Invalidates the L1 entry regardless of the backend result.
838    pub async fn delete(&self, key: &str) -> Result<bool, CachekitError> {
839        let full_key = self.resolve_key(key)?;
840
841        #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
842        let mutation = self.lock_l1_mutation(&full_key).await;
843
844        // Invalidate before L1 changes or backend I/O so task cancellation
845        // cannot let an older refresh resurrect this key.
846        #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
847        if let Some(ref mutation) = mutation {
848            mutation.advance();
849        }
850
851        // Invalidate L1 first so callers never read a stale value even if the
852        // backend delete fails partway through.
853        #[cfg(feature = "l1")]
854        self.l1_delete(&full_key);
855
856        let existed = self.backend.delete(&full_key).await?;
857        crate::metrics::trace_delete(&full_key, existed);
858        Ok(existed)
859    }
860
861    /// Return `true` if `key` exists without fetching the value.
862    ///
863    /// A live L1 entry answers without a backend call. That check does not
864    /// count as a use of the entry for L1 eviction; only reads do.
865    pub async fn exists(&self, key: &str) -> Result<bool, CachekitError> {
866        let full_key = self.resolve_key(key)?;
867
868        // Check L1 first — avoids a network round-trip for warm entries.
869        #[cfg(feature = "l1")]
870        if self.l1.as_ref().is_some_and(|l1| l1.contains(&full_key)) {
871            return Ok(true);
872        }
873
874        Ok(self.backend.exists(&full_key).await?)
875    }
876
877    // ── Observability ─────────────────────────────────────────────────────────
878
879    /// Live hit/miss counters for this client and all of its clones.
880    ///
881    /// Every value read — `get`, `interop_get`, their SWR variants, and the
882    /// [`SecureCache`] equivalents — counts one outcome: a hit by the layer
883    /// that served it, or a miss when nothing was found. A `#[cachekit]` call
884    /// also counts one outcome: a hit by the layer that served its value, or
885    /// one miss when it runs the function. The reads inside that call (its
886    /// first read, single-flight re-polls) count nothing themselves.
887    /// `exists`, and reads that fail at the backend or on an oversized
888    /// payload, are not counted. A plain read whose served entry then fails to
889    /// decode or decrypt counts the layer that served it. The
890    /// same counters feed the cachekit.io backends' `X-CacheKit-*` telemetry
891    /// headers, so for a backend used by this client alone, what you see here
892    /// is what the SaaS sees (a backend shared by several clients reports the
893    /// first client built over it — see [`Backend::attach_metrics`]).
894    ///
895    /// ```no_run
896    /// # fn example(cache: &cachekit::CacheKit) {
897    /// let stats = cache.stats();
898    /// let reads = stats.l1_hits + stats.l2_hits + stats.misses;
899    /// eprintln!("L1 hit rate {:.1}% over {reads} reads", stats.l1_hit_rate() * 100.0);
900    /// # }
901    /// ```
902    pub fn stats(&self) -> L1Stats {
903        self.counters.snapshot()
904    }
905
906    /// Number of entries currently held in L1, or `None` when L1 is disabled
907    /// (or the `l1` feature is off).
908    ///
909    /// Exact at the time of the call: expired entries are swept out first,
910    /// a pass over every entry that makes this a housekeeping call, not a
911    /// free counter read — poll it, don't put it on a hot path.
912    pub fn l1_entry_count(&self) -> Option<u64> {
913        #[cfg(feature = "l1")]
914        let count = self.l1.as_ref().map(crate::l1::L1Cache::entry_count);
915        #[cfg(not(feature = "l1"))]
916        let count = None;
917        count
918    }
919
920    /// Current circuit-breaker state, or `None` when this client has no
921    /// breaker (reliability disabled, or configured with
922    /// `circuit_breaker: None`).
923    ///
924    /// Reads the live state: an open circuit whose `open_timeout` has elapsed
925    /// reports [`CircuitState::HalfOpen`](crate::CircuitState::HalfOpen). With
926    /// the `tracing` feature every transition is also emitted on the
927    /// `cachekit::reliability` target — `warn` when the circuit opens, `info`
928    /// for half-open and closed.
929    #[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
930    pub fn circuit_state(&self) -> Option<crate::reliability::CircuitState> {
931        self.breaker.as_ref().map(|breaker| breaker.state())
932    }
933
934    // ── Single-flight ─────────────────────────────────────────────────────────
935
936    /// Begin a cold-miss single-flight for `key` (see [`crate::flight`]).
937    ///
938    /// Call after a cache miss, before computing the value. Concurrent
939    /// in-process fills of the same key are collapsed to one; with the
940    /// `reliability` feature and a lock-capable backend (CachekitIO, Redis),
941    /// fills are also suppressed across processes via a distributed fill
942    /// lock. The `#[cachekit]` macro does this automatically.
943    ///
944    /// The key is namespaced like every cache operation but not validated —
945    /// this call is infallible; an invalid key simply fails later at the
946    /// actual cache operation.
947    pub async fn single_flight(&self, key: &str) -> crate::flight::SingleFlight {
948        let full_key = self.namespaced_key(key);
949        crate::flight::SingleFlight::acquire(&self.flight, &self.backend, &full_key).await
950    }
951
952    /// Begin the single-flight for an SWR background refresh, or `None` when
953    /// another worker already holds this key's in-process flight or
954    /// distributed fill lock, or the lock call fails. Unlike
955    /// [`Self::single_flight`] it never waits for another worker's fill.
956    /// Macro plumbing for `#[cachekit]` — not public API.
957    #[doc(hidden)]
958    pub async fn __refresh_flight(&self, key: &str) -> Option<crate::flight::SingleFlight> {
959        let full_key = self.namespaced_key(key);
960        crate::flight::SingleFlight::try_lead(&self.flight, &self.backend, &full_key).await
961    }
962
963    /// [`Self::interop_get`] for a `#[cachekit]` call: the value with the
964    /// layer that served it, counted by nobody. The call's own reads (first
965    /// read, single-flight re-polls) count nothing; the call counts its one
966    /// outcome with [`Self::__count`]. Macro plumbing for `#[cachekit]` — not
967    /// public API.
968    #[doc(hidden)]
969    pub async fn __interop_read<T: DeserializeOwned>(
970        &self,
971        key: &str,
972    ) -> Result<Option<(T, ReadOutcome)>, CachekitError> {
973        #[cfg(feature = "encryption")]
974        if let Some(secure) = self.encrypting() {
975            return secure.__interop_read(key).await;
976        }
977        self.reject_namespaced_interop()?;
978        let full_key = self.resolve_key(key)?;
979        match self.read_bytes(&full_key).await? {
980            Some((bytes, by)) => Ok(Some((crate::interop::deserialize(&bytes)?, by))),
981            None => Ok(None),
982        }
983    }
984
985    /// [`Self::interop_get_swr`] for a `#[cachekit]` call, uncounted like
986    /// [`Self::__interop_read`]. Macro plumbing for `#[cachekit]` — not
987    /// public API.
988    #[doc(hidden)]
989    pub async fn __interop_read_swr<T: DeserializeOwned>(
990        &self,
991        key: &str,
992    ) -> Result<SwrRead<(T, ReadOutcome)>, CachekitError> {
993        #[cfg(feature = "encryption")]
994        if let Some(secure) = self.encrypting() {
995            return secure.__interop_read_swr(key).await;
996        }
997        self.reject_namespaced_interop()?;
998        let full_key = self.resolve_key(key)?;
999        Ok(match self.read_bytes_swr(&full_key).await? {
1000            SwrRead::Fresh((bytes, by)) => {
1001                SwrRead::Fresh((crate::interop::deserialize(&bytes)?, by))
1002            }
1003            SwrRead::Stale((bytes, by), token) => {
1004                SwrRead::Stale((crate::interop::deserialize(&bytes)?, by), token)
1005            }
1006            SwrRead::Miss => SwrRead::Miss,
1007        })
1008    }
1009
1010    /// Count a `#[cachekit]` call's one outcome: the layer that served its
1011    /// value, or [`ReadOutcome::Miss`] when it runs the function. Macro
1012    /// plumbing for `#[cachekit]` — not public API.
1013    #[doc(hidden)]
1014    pub fn __count(&self, key: &str, outcome: ReadOutcome) {
1015        self.counters.record(outcome, &self.namespaced_key(key));
1016    }
1017
1018    // ── Secure cache ─────────────────────────────────────────────────────────
1019
1020    /// Return a [`SecureCache`] handle that encrypts all values before storage.
1021    ///
1022    /// L1 stores **ciphertext** (not plaintext) to preserve the zero-knowledge
1023    /// property across all cache layers.
1024    ///
1025    /// The handle encrypts exactly what this client's own value methods
1026    /// already encrypt (see [Encryption](CacheKit#encryption)), and the two
1027    /// read each other's entries. What it adds is the guarantee: code that
1028    /// must never run unencrypted asks for the handle, which fails on a
1029    /// client without encryption, and it carries the
1030    /// [`previous_key_hits`](SecureCache::previous_key_hits) rotation signal.
1031    ///
1032    /// # Errors
1033    /// Returns `CachekitError::Config` if no encryption layer is configured.
1034    /// Configure encryption via [`CacheKitBuilder::encryption`] /
1035    /// [`CacheKitBuilder::encryption_from_env`] /
1036    /// [`CacheKitBuilder::encryption_from_bytes`], or start from the
1037    /// `CacheKit::secure(url, master_key_hex)` /
1038    /// `CacheKit::secure_from_env(url)` preset (`redis` feature).
1039    #[cfg(feature = "encryption")]
1040    pub fn secure_cache(&self) -> Result<SecureCache<'_>, CachekitError> {
1041        self.encrypting().ok_or_else(|| {
1042            CachekitError::Config(
1043                "encryption not configured: use the CacheKit::secure(url, master_key_hex) \
1044                 preset, CacheKit::secure_from_env(url) to read the hex key from \
1045                 CACHEKIT_MASTER_KEY, or call .encryption() on the builder \
1046                 (.encryption_from_env() to read CACHEKIT_MASTER_KEY)"
1047                    .to_owned(),
1048            )
1049        })
1050    }
1051
1052    /// The encrypting handle when encryption is configured. Every value
1053    /// method routes through it first, so a configured key leaves no
1054    /// plaintext path (protocol `spec/intent-presets.md` § Encryption
1055    /// Activation, rule 1).
1056    #[cfg(feature = "encryption")]
1057    fn encrypting(&self) -> Option<SecureCache<'_>> {
1058        self.encryption.as_deref().map(|encryption| SecureCache {
1059            client: self,
1060            encryption,
1061        })
1062    }
1063
1064    // ── Private helpers ───────────────────────────────────────────────────────
1065
1066    fn check_payload_size(&self, size: usize) -> Result<(), CachekitError> {
1067        if size > self.max_payload_bytes {
1068            return Err(CachekitError::PayloadTooLarge {
1069                size,
1070                limit: self.max_payload_bytes,
1071            });
1072        }
1073        Ok(())
1074    }
1075}
1076
1077// ── SecureCache ──────────────────────────────────────────────────────────────
1078
1079/// Encrypted cache handle returned by [`CacheKit::secure_cache()`].
1080///
1081/// All values are serialized, then encrypted with AES-256-GCM before storage.
1082/// L1 stores ciphertext to maintain zero-knowledge guarantees. The client's
1083/// own value methods route through these same methods once encryption is
1084/// configured, so the handle and the client read each other's entries.
1085///
1086/// The handle borrows its [`CacheKit`] (`SecureCache<'a>` holds `&'a CacheKit`),
1087/// so it cannot be stored in the same struct as the client it came from.
1088/// Store the `CacheKit` and call [`CacheKit::secure_cache()`] where you need
1089/// the handle: it only checks that encryption is configured and copies two
1090/// references, so calling it per operation is cheap.
1091#[cfg(feature = "encryption")]
1092pub struct SecureCache<'a> {
1093    client: &'a CacheKit,
1094    encryption: &'a crate::encryption::EncryptionLayer,
1095}
1096
1097#[cfg(feature = "encryption")]
1098impl std::fmt::Debug for SecureCache<'_> {
1099    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1100        f.debug_struct("SecureCache")
1101            .field("tenant_id", &self.encryption.tenant_id())
1102            .finish()
1103    }
1104}
1105
1106#[cfg(feature = "encryption")]
1107impl SecureCache<'_> {
1108    /// Rotation drain signal; see
1109    /// [`EncryptionLayer::previous_key_hits`](crate::EncryptionLayer::previous_key_hits).
1110    pub fn previous_key_hits(&self) -> Vec<u64> {
1111        self.encryption.previous_key_hits()
1112    }
1113
1114    /// Whether AES is hardware-accelerated on this host (informational); see
1115    /// [`EncryptionLayer::hardware_acceleration_enabled`](crate::EncryptionLayer::hardware_acceleration_enabled).
1116    pub fn hardware_acceleration_enabled(&self) -> bool {
1117        self.encryption.hardware_acceleration_enabled()
1118    }
1119
1120    /// Encrypt and store `value` under `key` using the client's default TTL.
1121    pub async fn set<T: Serialize>(&self, key: &str, value: &T) -> Result<(), CachekitError> {
1122        self.set_with_ttl(key, value, self.client.default_ttl).await
1123    }
1124
1125    /// Encrypt and store `value` under `key` with an explicit `ttl`.
1126    ///
1127    /// A positive sub-second `ttl` is sent to the backend as 1 second.
1128    ///
1129    /// Returns [`CachekitError::Config`] if `ttl` is zero.
1130    ///
1131    /// The client namespace is not checked here: an interop key written on a
1132    /// namespaced client lands under `{namespace}:{interop_key}`, which no
1133    /// interop reader addresses — see [`CacheKit::interop_get`].
1134    pub async fn set_with_ttl<T: Serialize>(
1135        &self,
1136        key: &str,
1137        value: &T,
1138        ttl: Duration,
1139    ) -> Result<(), CachekitError> {
1140        CacheKit::validate_ttl(ttl)?;
1141        let (full_key, ciphertext) = self.seal(key, value)?;
1142        // Size-check what is actually persisted (nonce + ciphertext + tag).
1143        // The get paths check the stored ciphertext length, so checking the
1144        // plaintext here would let a value within 28 bytes of the limit write
1145        // successfully and then fail EVERY subsequent read with PayloadTooLarge.
1146        self.client.check_payload_size(ciphertext.len())?;
1147
1148        #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
1149        let mutation = self.client.lock_l1_mutation(&full_key).await;
1150
1151        // Match the plain write path: invalidate older refresh tokens before
1152        // the first backend await, including on cancellation or failure.
1153        #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
1154        if let Some(ref mutation) = mutation {
1155            mutation.advance();
1156        }
1157
1158        // L1 needs its own copy: the backend consumes the Vec.
1159        #[cfg(feature = "l1")]
1160        {
1161            let l1_bytes = Bytes::copy_from_slice(&ciphertext);
1162            self.client
1163                .backend
1164                .set(&full_key, ciphertext, Some(ttl))
1165                .await?;
1166            self.client.l1_set(&full_key, l1_bytes, ttl);
1167        }
1168        #[cfg(not(feature = "l1"))]
1169        {
1170            self.client
1171                .backend
1172                .set(&full_key, ciphertext, Some(ttl))
1173                .await?;
1174        }
1175
1176        crate::metrics::trace_write(&full_key, ttl);
1177        Ok(())
1178    }
1179
1180    /// Commit an encrypted SWR refresh only if its stale-read token is still
1181    /// current. Macro plumbing; ordinary writes use [`Self::set_with_ttl`].
1182    #[doc(hidden)]
1183    pub async fn __complete_swr_refresh<T: Serialize>(
1184        &self,
1185        key: &str,
1186        value: &T,
1187        ttl: Duration,
1188        token: SwrToken,
1189    ) -> Result<bool, CachekitError> {
1190        let (_, ciphertext) = self.seal(key, value)?;
1191        self.client
1192            .complete_swr_bytes(key, ciphertext, ttl, token)
1193            .await
1194    }
1195
1196    /// Serialize and encrypt `value` for `key`, returning the key this client
1197    /// passes to its backend (`.namespace()` prefix included) with the
1198    /// ciphertext. Every write encrypts here, so the AAD binds that key and an
1199    /// entry copied to another namespace fails to decrypt.
1200    fn seal<T: Serialize>(&self, key: &str, value: &T) -> Result<(String, Vec<u8>), CachekitError> {
1201        let full_key = self.client.resolve_key(key)?;
1202        let plaintext = serializer::serialize(value)?;
1203        let ciphertext = self.encryption.encrypt(&plaintext, &full_key)?;
1204        Ok((full_key.into_owned(), ciphertext))
1205    }
1206
1207    /// Retrieve, decrypt, and deserialize a value stored under `key`.
1208    ///
1209    /// Checks L1 (which holds ciphertext) before the backend.
1210    ///
1211    /// # Errors
1212    ///
1213    /// Returns [`CachekitError::Encryption`] if the stored entry fails
1214    /// decryption. The key's L1 copy is dropped first and the backend entry
1215    /// is left in place, so the next read reaches the backend: a hit once the
1216    /// entry is replaced with ciphertext this client can decrypt, a miss once
1217    /// it expires or is deleted. A decrypted entry that does not decode as `T`
1218    /// is [`CachekitError::Serialization`] naming only `T`: the decoder's own
1219    /// message would quote the plaintext.
1220    pub async fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, CachekitError> {
1221        match self.get_plaintext(key).await? {
1222            Some(plaintext) => Ok(Some(redact_decode(serializer::deserialize(&plaintext))?)),
1223            None => Ok(None),
1224        }
1225    }
1226
1227    /// Retrieve, decrypt, and deserialize an interop-mode value stored under `key`.
1228    ///
1229    /// Identical to [`Self::get`] except the decrypted plaintext is decoded
1230    /// with [`crate::interop::deserialize`] — exactly one MessagePack document,
1231    /// trailing bytes rejected (interop/v1 spec MUST), as in [`Self::get`]. In interop mode the
1232    /// AES-GCM plaintext is the plain MessagePack value bytes, so the AAD
1233    /// (v0x03, `format="msgpack"`, `compressed="False"`) verifies cross-SDK
1234    /// unchanged.
1235    ///
1236    /// # Errors
1237    ///
1238    /// Returns [`CachekitError::Config`] on a namespace-prefixed client — see
1239    /// [`CacheKit::interop_get`] — and [`CachekitError::Encryption`] if the
1240    /// stored entry fails decryption, with the key's L1 copy dropped as for
1241    /// [`Self::get`].
1242    pub async fn interop_get<T: DeserializeOwned>(
1243        &self,
1244        key: &str,
1245    ) -> Result<Option<T>, CachekitError> {
1246        self.client.reject_namespaced_interop()?;
1247        match self.get_plaintext(key).await? {
1248            Some(plaintext) => Ok(Some(redact_decode(crate::interop::deserialize(
1249                &plaintext,
1250            ))?)),
1251            None => Ok(None),
1252        }
1253    }
1254
1255    /// Retrieve, decrypt, and deserialize an interop-mode value with SWR
1256    /// classification. The secure twin of [`CacheKit::interop_get_swr`]:
1257    /// staleness is judged on the L1 **ciphertext** entry (zero-knowledge is
1258    /// preserved — freshness metadata never exposes plaintext), then the
1259    /// value is decrypted and decoded per [`Self::interop_get`].
1260    ///
1261    /// # Errors
1262    ///
1263    /// Same as [`Self::interop_get`] — the secure path fails closed on every
1264    /// backend and decryption error.
1265    pub async fn interop_get_swr<T: DeserializeOwned>(
1266        &self,
1267        key: &str,
1268    ) -> Result<SwrRead<T>, CachekitError> {
1269        self.client.reject_namespaced_interop()?;
1270        match self.client.get_bytes_swr(key).await? {
1271            SwrRead::Fresh(ct) => Ok(SwrRead::Fresh(redact_decode(crate::interop::deserialize(
1272                &self.decrypt_or_evict(&ct, key)?,
1273            ))?)),
1274            SwrRead::Stale(ct, token) => Ok(SwrRead::Stale(
1275                redact_decode(crate::interop::deserialize(
1276                    &self.decrypt_or_evict(&ct, key)?,
1277                ))?,
1278                token,
1279            )),
1280            SwrRead::Miss => Ok(SwrRead::Miss),
1281        }
1282    }
1283
1284    /// The secure twin of [`CacheKit::__interop_read`], which hands an
1285    /// encrypting client's reads here.
1286    async fn __interop_read<T: DeserializeOwned>(
1287        &self,
1288        key: &str,
1289    ) -> Result<Option<(T, ReadOutcome)>, CachekitError> {
1290        self.client.reject_namespaced_interop()?;
1291        let full_key = self.client.resolve_key(key)?;
1292        match self.client.read_bytes(&full_key).await? {
1293            Some((ct, by)) => Ok(Some((
1294                redact_decode(crate::interop::deserialize(
1295                    &self.decrypt_or_evict(&ct, key)?,
1296                ))?,
1297                by,
1298            ))),
1299            None => Ok(None),
1300        }
1301    }
1302
1303    /// The secure twin of [`CacheKit::__interop_read_swr`], which hands an
1304    /// encrypting client's reads here.
1305    async fn __interop_read_swr<T: DeserializeOwned>(
1306        &self,
1307        key: &str,
1308    ) -> Result<SwrRead<(T, ReadOutcome)>, CachekitError> {
1309        self.client.reject_namespaced_interop()?;
1310        let full_key = self.client.resolve_key(key)?;
1311        let decode = |ct: &[u8]| -> Result<T, CachekitError> {
1312            redact_decode(crate::interop::deserialize(
1313                &self.decrypt_or_evict(ct, key)?,
1314            ))
1315        };
1316        Ok(match self.client.read_bytes_swr(&full_key).await? {
1317            SwrRead::Fresh((ct, by)) => SwrRead::Fresh((decode(&ct)?, by)),
1318            SwrRead::Stale((ct, by), token) => SwrRead::Stale((decode(&ct)?, by), token),
1319            SwrRead::Miss => SwrRead::Miss,
1320        })
1321    }
1322
1323    /// Fetch ciphertext (L1, then L2 with L1 backfill) and decrypt it.
1324    ///
1325    /// Ciphertext retrieval delegates to [`CacheKit::get_bytes`], which returns
1326    /// the stored bytes untransformed — for a secure cache exactly the AES-GCM
1327    /// ciphertext, so decrypt receives the same bytes the backend holds.
1328    async fn get_plaintext(&self, key: &str) -> Result<Option<Vec<u8>>, CachekitError> {
1329        match self.client.get_bytes(key).await? {
1330            Some(ciphertext) => Ok(Some(self.decrypt_or_evict(&ciphertext, key)?)),
1331            None => Ok(None),
1332        }
1333    }
1334
1335    /// Decrypt ciphertext read for `key` against the key this client passes to
1336    /// its backend (`.namespace()` prefix included); on failure, drop the key's L1 copy before propagating the
1337    /// error.
1338    ///
1339    /// An L2 hit is backfilled into L1 before it is decrypted, so an entry
1340    /// that fails authentication would otherwise keep failing from L1 after
1341    /// the backend entry is fixed or deleted. The backend entry itself is
1342    /// left untouched as evidence.
1343    fn decrypt_or_evict(&self, ciphertext: &[u8], key: &str) -> Result<Vec<u8>, CachekitError> {
1344        let full_key = self.client.resolve_key(key)?;
1345        self.encryption
1346            .decrypt(ciphertext, &full_key)
1347            .inspect_err(|_| {
1348                #[cfg(feature = "l1")]
1349                self.client.l1_delete(&full_key);
1350            })
1351    }
1352
1353    /// Delete an encrypted key. Behaves identically to [`CacheKit::delete`].
1354    pub async fn delete(&self, key: &str) -> Result<bool, CachekitError> {
1355        self.client.delete(key).await
1356    }
1357
1358    /// Check if an encrypted key exists. Behaves identically to [`CacheKit::exists`].
1359    pub async fn exists(&self, key: &str) -> Result<bool, CachekitError> {
1360        self.client.exists(key).await
1361    }
1362}
1363
1364/// Decoding decrypted plaintext keeps the `Serialization` class, which
1365/// `#[cachekit]` reads as a miss, but replaces the decoder's message: serde
1366/// quotes the offending value (`invalid type: string "…", expected struct
1367/// …`), and that value is the plaintext encryption exists to protect
1368/// (CWE-532).
1369#[cfg(feature = "encryption")]
1370fn redact_decode<T>(decoded: Result<T, CachekitError>) -> Result<T, CachekitError> {
1371    decoded.map_err(|e| match e {
1372        CachekitError::Serialization(_) => CachekitError::Serialization(format!(
1373            "decrypted entry does not decode as {}",
1374            std::any::type_name::<T>()
1375        )),
1376        other => other,
1377    })
1378}
1379
1380// ── CacheKitBuilder ───────────────────────────────────────────────────────────
1381
1382/// Fluent builder for [`CacheKit`].
1383#[derive(Default)]
1384#[must_use]
1385pub struct CacheKitBuilder {
1386    backend: Option<SharedBackend>,
1387    default_ttl: Option<Duration>,
1388    namespace: Option<String>,
1389    max_payload_bytes: Option<usize>,
1390
1391    #[cfg(feature = "l1")]
1392    l1_capacity: Option<usize>,
1393
1394    #[cfg(feature = "l1")]
1395    no_l1: bool,
1396
1397    #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
1398    swr_enabled: Option<bool>,
1399
1400    #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
1401    swr_threshold_ratio: Option<f64>,
1402
1403    // pub(crate): the `secure` preset's key-path tests read the layer.
1404    #[cfg(feature = "encryption")]
1405    pub(crate) encryption: Option<SharedEncryption>,
1406
1407    /// [`Self::no_encryption`] was called.
1408    no_encryption: bool,
1409
1410    /// `CacheKit::from_env` saw `CACHEKIT_MASTER_KEY`: [`Self::build`]
1411    /// refuses until an encryption call or [`Self::no_encryption`] states
1412    /// the intent (the transitional gate documented on `from_env`).
1413    env_master_key: bool,
1414
1415    #[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
1416    reliability: Option<crate::reliability::ReliabilityConfig>,
1417
1418    /// Retry deadlines for the backend `CacheKit::io` built; cleared by
1419    /// [`Self::backend`] so a replaced backend never inherits them.
1420    #[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
1421    pub(crate) retry_deadlines: Option<crate::reliability::RetryDeadlines>,
1422}
1423
1424#[cfg(not(feature = "encryption"))]
1425fn encryption_feature_missing(what: &str) -> CachekitError {
1426    CachekitError::Config(format!(
1427        "{what} needs the `encryption` cargo feature, which this build of \
1428         cachekit-rs was compiled without; enable it to encrypt"
1429    ))
1430}
1431
1432impl CacheKitBuilder {
1433    /// Set the storage backend.
1434    pub fn backend(mut self, backend: SharedBackend) -> Self {
1435        self.backend = Some(backend);
1436        #[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
1437        {
1438            self.retry_deadlines = None;
1439        }
1440        self
1441    }
1442
1443    /// Override the default TTL (used when no per-call TTL is specified).
1444    pub fn default_ttl(mut self, ttl: Duration) -> Self {
1445        self.default_ttl = Some(ttl);
1446        self
1447    }
1448
1449    /// Set a namespace prefix. All keys will be stored as `{namespace}:{key}`.
1450    pub fn namespace(mut self, ns: impl Into<String>) -> Self {
1451        self.namespace = Some(ns.into());
1452        self
1453    }
1454
1455    /// Set the maximum accepted payload size in bytes.
1456    pub fn max_payload_bytes(mut self, limit: usize) -> Self {
1457        self.max_payload_bytes = Some(limit);
1458        self
1459    }
1460
1461    /// Set the L1 cache capacity (max entries).
1462    #[cfg(feature = "l1")]
1463    pub fn l1_capacity(mut self, capacity: usize) -> Self {
1464        self.l1_capacity = Some(capacity);
1465        self
1466    }
1467
1468    /// Disable the L1 cache entirely.
1469    #[cfg(feature = "l1")]
1470    pub fn no_l1(mut self) -> Self {
1471        self.no_l1 = true;
1472        self
1473    }
1474
1475    /// Enable or disable L1 stale-while-revalidate (default: **enabled**,
1476    /// matching the Python and TypeScript SDKs).
1477    ///
1478    /// With SWR on, an L1 hit older than `swr_threshold_ratio` of its TTL is
1479    /// still served immediately, and the `#[cachekit]` macro schedules at
1480    /// most one background refresh (deduplicated on the
1481    /// [`CacheKit::single_flight`] locks, in-process and — on lock-capable
1482    /// backends — across processes). A refresh that finds either lock held,
1483    /// or whose lock call fails, stands down instead of waiting, and the
1484    /// stale value keeps being served. A hard-expired entry is never served:
1485    /// it falls through to a normal blocking miss.
1486    ///
1487    /// Native targets only: this knob does not exist on wasm32, under the
1488    /// `unsync` feature, or without `l1` — calling it there is a compile
1489    /// error rather than a silent no-op.
1490    #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
1491    pub fn swr_enabled(mut self, enabled: bool) -> Self {
1492        self.swr_enabled = Some(enabled);
1493        self
1494    }
1495
1496    /// Set the SWR freshness threshold as a fraction of each L1 entry's TTL
1497    /// (default: **0.5**, matching the Python and TypeScript SDKs).
1498    ///
1499    /// An entry is *fresh* until it has lived `ratio × TTL` (±10% jitter,
1500    /// drawn once when the entry is inserted, to de-synchronise refreshes
1501    /// across processes), then *stale* — served immediately with a background
1502    /// refresh — until hard expiry. Mirrors cachekit-py's
1503    /// `swr_threshold_ratio` semantics (elapsed-lifetime fraction). Must be in
1504    /// `(0.0, 1.0]`; validated at [`Self::build`].
1505    #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
1506    pub fn swr_threshold_ratio(mut self, ratio: f64) -> Self {
1507        self.swr_threshold_ratio = Some(ratio);
1508        self
1509    }
1510
1511    // Stubs for when the l1 feature is disabled — still compile cleanly.
1512    /// No-op: the `l1` feature is disabled, so there is no L1 to size.
1513    #[cfg(not(feature = "l1"))]
1514    pub fn l1_capacity(self, _capacity: usize) -> Self {
1515        self
1516    }
1517
1518    /// No-op: the `l1` feature is disabled, so L1 is already off.
1519    #[cfg(not(feature = "l1"))]
1520    pub fn no_l1(self) -> Self {
1521        self
1522    }
1523
1524    /// Wrap the backend in the reliability stack (retry with exponential
1525    /// backoff + jitter, circuit breaker, backpressure) — see
1526    /// [`crate::reliability`].
1527    ///
1528    /// Enabled by default with production settings by the `production`,
1529    /// `secure`, and `io` intent presets; off for `minimal` and for
1530    /// manually-built clients. To opt a preset out, pass
1531    /// [`ReliabilityConfig::disabled()`](crate::reliability::ReliabilityConfig::disabled)
1532    /// — a disabled config applies no wrapping at all.
1533    #[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
1534    pub fn reliability(mut self, config: crate::reliability::ReliabilityConfig) -> Self {
1535        self.reliability = Some(config);
1536        self
1537    }
1538
1539    /// Configure encryption from raw master key bytes and tenant ID.
1540    ///
1541    /// The master key must be exactly 32 raw bytes; anything else is a
1542    /// [`CachekitError::Config`]. Hex-encoded keys go to [`Self::encryption`]
1543    /// (or [`Self::encryption_with_previous`] to rotate) — their ASCII bytes
1544    /// are 64 bytes long and rejected here.
1545    /// Keys are derived per-tenant via HKDF-SHA256.
1546    ///
1547    /// The built client encrypts every value read and write, not only those
1548    /// through the [`secure_cache()`](CacheKit::secure_cache) handle; see
1549    /// [Encryption](CacheKit#encryption). Calling any encryption method
1550    /// again replaces the earlier configuration.
1551    ///
1552    /// Without the `encryption` cargo feature this method, and every other
1553    /// builder encryption method, returns [`CachekitError::Config`]: a build
1554    /// that cannot encrypt never accepts a key.
1555    #[cfg(feature = "encryption")]
1556    pub fn encryption_from_bytes(
1557        mut self,
1558        master_key: &[u8],
1559        tenant_id: &str,
1560    ) -> Result<Self, CachekitError> {
1561        let layer = crate::encryption::EncryptionLayer::new(master_key, tenant_id)?;
1562        self.encryption = Some(SharedEncryption::new(layer));
1563        Ok(self)
1564    }
1565
1566    /// Configure encryption with decrypt-only previous master keys for
1567    /// key rotation.
1568    ///
1569    /// Writes encrypt under `master_key`; reads attempt it first, then each
1570    /// key in `previous_keys` sequentially (attempt order = slice order).
1571    /// At most 3 previous keys; supplying more is a config error, never
1572    /// truncated. See [`crate::encryption::EncryptionLayer::with_previous_keys`].
1573    ///
1574    /// Every key, current and previous, must be exactly 32 raw bytes. For
1575    /// hex keys, use the hex counterpart, [`Self::encryption_with_previous`].
1576    ///
1577    /// Encrypts every value read and write, as for
1578    /// [`Self::encryption_from_bytes`].
1579    #[cfg(feature = "encryption")]
1580    pub fn encryption_from_bytes_with_previous(
1581        mut self,
1582        master_key: &[u8],
1583        previous_keys: &[&[u8]],
1584        tenant_id: &str,
1585    ) -> Result<Self, CachekitError> {
1586        let layer = crate::encryption::EncryptionLayer::with_previous_keys(
1587            master_key,
1588            previous_keys,
1589            tenant_id,
1590        )?;
1591        self.encryption = Some(SharedEncryption::new(layer));
1592        Ok(self)
1593    }
1594
1595    /// Configure encryption from a hex-encoded master key string.
1596    ///
1597    /// Hex-decodes with the same decoder as [`CacheKit::from_env`] and the
1598    /// `secure` preset. The key must decode to at least 32 bytes; use exactly
1599    /// 32 (64 hex chars), the only length every SDK accepts.
1600    ///
1601    /// Encrypts every value read and write, as for
1602    /// [`Self::encryption_from_bytes`].
1603    #[cfg(feature = "encryption")]
1604    pub fn encryption(self, hex_key: &str, tenant_id: &str) -> Result<Self, CachekitError> {
1605        let bytes = crate::config::decode_master_key_hex(hex_key, "master key")?;
1606        self.encryption_from_hex_decoded(&bytes, &[], tenant_id)
1607    }
1608
1609    /// Configure encryption from hex-encoded keys, with decrypt-only previous
1610    /// master keys for key rotation.
1611    ///
1612    /// The hex counterpart of [`Self::encryption_from_bytes_with_previous`].
1613    /// Every key, current and previous, is decoded as for [`Self::encryption`]
1614    /// and must decode to at least 32 bytes, so a key longer than 32 bytes
1615    /// that [`Self::encryption`] accepted can be listed as a previous key
1616    /// under the tenant it was written for (the env variables always derive
1617    /// for tenant `"default"`). Use exactly 32 bytes (64 hex chars) for new
1618    /// keys, the only length every SDK accepts.
1619    ///
1620    /// Writes encrypt under `hex_key`; reads attempt it first, then each key
1621    /// in `previous_hex_keys` sequentially (attempt order = slice order).
1622    /// At most 3 previous keys, and the current key must not be among them;
1623    /// either violation is a [`CachekitError::Config`], never truncated.
1624    ///
1625    /// ```no_run
1626    /// # fn demo(backend: cachekit::client::SharedBackend, k2_hex: &str, k1_hex: &str)
1627    /// #     -> Result<(), cachekit::CachekitError> {
1628    /// let cache = cachekit::CacheKit::builder()
1629    ///     .backend(backend)
1630    ///     .encryption_with_previous(k2_hex, &[k1_hex], "tenant-a")?
1631    ///     .build()?;
1632    /// # Ok(()) }
1633    /// ```
1634    ///
1635    /// Encrypts every value read and write, as for
1636    /// [`Self::encryption_from_bytes`].
1637    #[cfg(feature = "encryption")]
1638    pub fn encryption_with_previous(
1639        self,
1640        hex_key: &str,
1641        previous_hex_keys: &[&str],
1642        tenant_id: &str,
1643    ) -> Result<Self, CachekitError> {
1644        let current = crate::config::decode_master_key_hex(hex_key, "master key")?;
1645        let previous = previous_hex_keys
1646            .iter()
1647            .enumerate()
1648            .map(|(i, key)| {
1649                crate::config::decode_master_key_hex(key, &format!("previous master key {i}"))
1650            })
1651            .collect::<Result<Vec<_>, _>>()?;
1652        let previous: Vec<&[u8]> = previous.iter().map(|key| key.as_slice()).collect();
1653        self.encryption_from_hex_decoded(&current, &previous, tenant_id)
1654    }
1655
1656    /// Configure encryption from the environment: the hex key in
1657    /// `CACHEKIT_MASTER_KEY`, plus any decrypt-only rotation keys in
1658    /// `CACHEKIT_PREVIOUS_MASTER_KEYS`, for tenant `"default"`.
1659    ///
1660    /// The explicit call that turns a `CACHEKIT_MASTER_KEY` into encryption:
1661    /// no constructor, [`CacheKit::from_env`] included, encrypts because the
1662    /// variable is set. Keys are decoded and validated as for
1663    /// [`Self::encryption_with_previous`] and [`CacheKit::from_env`].
1664    ///
1665    /// ```no_run
1666    /// # fn demo() -> Result<(), cachekit::CachekitError> {
1667    /// let cache = cachekit::CacheKit::from_env()?
1668    ///     .encryption_from_env()?
1669    ///     .build()?;
1670    /// # Ok(()) }
1671    /// ```
1672    ///
1673    /// Encrypts every value read and write, as for
1674    /// [`Self::encryption_from_bytes`].
1675    ///
1676    /// # Errors
1677    ///
1678    /// [`CachekitError::Config`] when `CACHEKIT_MASTER_KEY` is unset, empty,
1679    /// not UTF-8, not hex or shorter than 32 bytes, or when
1680    /// `CACHEKIT_PREVIOUS_MASTER_KEYS` is malformed: asked to encrypt, the
1681    /// builder never falls back to plaintext.
1682    #[cfg(feature = "encryption")]
1683    pub fn encryption_from_env(self) -> Result<Self, CachekitError> {
1684        let (master_key, previous) = crate::config::master_keys_from_env(
1685            ".encryption_with_previous(hex_key, previous_hex_keys, \"default\")",
1686        )?;
1687        let previous: Vec<&[u8]> = previous.iter().map(|key| key.as_slice()).collect();
1688        self.encryption_from_hex_decoded(&master_key, &previous, "default")
1689    }
1690
1691    /// Shared tail of the hex paths: keys already validated by
1692    /// `decode_master_key_hex`, so the `>= 32` floor applies, not the
1693    /// raw-bytes exactly-32 rule.
1694    #[cfg(feature = "encryption")]
1695    pub(crate) fn encryption_from_hex_decoded(
1696        mut self,
1697        master_key: &[u8],
1698        previous_keys: &[&[u8]],
1699        tenant_id: &str,
1700    ) -> Result<Self, CachekitError> {
1701        let layer = crate::encryption::EncryptionLayer::from_hex_decoded_keys(
1702            master_key,
1703            previous_keys,
1704            tenant_id,
1705        )?;
1706        self.encryption = Some(SharedEncryption::new(layer));
1707        Ok(self)
1708    }
1709
1710    // Without the `encryption` feature the builder methods still exist, so
1711    // feature-agnostic code compiles, but each one is an error: accepting a
1712    // key and building a plaintext client is the silent downgrade protocol
1713    // `spec/intent-presets.md` § Encryption Activation rule 1 forbids.
1714
1715    /// Always [`CachekitError::Config`]: this build has no `encryption`
1716    /// feature.
1717    #[cfg(not(feature = "encryption"))]
1718    pub fn encryption_from_bytes(
1719        self,
1720        _master_key: &[u8],
1721        _tenant_id: &str,
1722    ) -> Result<Self, CachekitError> {
1723        Err(encryption_feature_missing(".encryption_from_bytes()"))
1724    }
1725
1726    /// Always [`CachekitError::Config`]: this build has no `encryption`
1727    /// feature.
1728    #[cfg(not(feature = "encryption"))]
1729    pub fn encryption_from_bytes_with_previous(
1730        self,
1731        _master_key: &[u8],
1732        _previous_keys: &[&[u8]],
1733        _tenant_id: &str,
1734    ) -> Result<Self, CachekitError> {
1735        Err(encryption_feature_missing(
1736            ".encryption_from_bytes_with_previous()",
1737        ))
1738    }
1739
1740    /// Always [`CachekitError::Config`]: this build has no `encryption`
1741    /// feature.
1742    #[cfg(not(feature = "encryption"))]
1743    pub fn encryption(self, _hex_key: &str, _tenant_id: &str) -> Result<Self, CachekitError> {
1744        Err(encryption_feature_missing(".encryption()"))
1745    }
1746
1747    /// Always [`CachekitError::Config`]: this build has no `encryption`
1748    /// feature.
1749    #[cfg(not(feature = "encryption"))]
1750    pub fn encryption_with_previous(
1751        self,
1752        _hex_key: &str,
1753        _previous_hex_keys: &[&str],
1754        _tenant_id: &str,
1755    ) -> Result<Self, CachekitError> {
1756        Err(encryption_feature_missing(".encryption_with_previous()"))
1757    }
1758
1759    /// Always [`CachekitError::Config`]: this build has no `encryption`
1760    /// feature.
1761    #[cfg(not(feature = "encryption"))]
1762    pub fn encryption_from_env(self) -> Result<Self, CachekitError> {
1763        Err(encryption_feature_missing(".encryption_from_env()"))
1764    }
1765
1766    /// State that this client does not encrypt, even with
1767    /// `CACHEKIT_MASTER_KEY` set: the explicit opt-out of protocol
1768    /// `spec/intent-presets.md` § Encryption Activation, rule 3.
1769    ///
1770    /// No client encrypts without an encryption call, so this changes
1771    /// nothing on its own. It answers the transitional check on a builder
1772    /// from [`CacheKit::from_env`] that saw `CACHEKIT_MASTER_KEY`, and it
1773    /// records the intent at the call site. Combined with any encryption
1774    /// call, or the `secure` preset, [`Self::build`] returns
1775    /// [`CachekitError::Config`]: an opt-out never quietly undoes one.
1776    pub fn no_encryption(mut self) -> Self {
1777        self.no_encryption = true;
1778        self
1779    }
1780
1781    /// Finalise and build the [`CacheKit`] client.
1782    ///
1783    /// Returns an error if no backend was provided, if
1784    /// [`Self::no_encryption`] was combined with an encryption call, or if
1785    /// the builder came from [`CacheKit::from_env`] with
1786    /// `CACHEKIT_MASTER_KEY` set and states no encryption intent.
1787    pub fn build(self) -> Result<CacheKit, CachekitError> {
1788        #[cfg(feature = "encryption")]
1789        let encrypting = self.encryption.is_some();
1790        #[cfg(not(feature = "encryption"))]
1791        let encrypting = false;
1792        if self.no_encryption && encrypting {
1793            return Err(CachekitError::Config(
1794                ".no_encryption() conflicts with an encryption call (or the secure preset) \
1795                 on the same builder: keep one"
1796                    .to_owned(),
1797            ));
1798        }
1799        if self.env_master_key && !encrypting && !self.no_encryption {
1800            #[cfg(feature = "encryption")]
1801            let msg = "CACHEKIT_MASTER_KEY is set, and CacheKit::from_env() no longer encrypts \
1802                       because of it: chain .encryption_from_env() to encrypt with it, or \
1803                       .no_encryption() to build a plaintext client";
1804            #[cfg(not(feature = "encryption"))]
1805            let msg = "CACHEKIT_MASTER_KEY is set, but this build of cachekit-rs has no \
1806                       `encryption` cargo feature and cannot encrypt with it: chain \
1807                       .no_encryption() to build a plaintext client, or enable the feature \
1808                       to encrypt";
1809            return Err(CachekitError::Config(msg.to_owned()));
1810        }
1811
1812        let backend = self.backend.ok_or_else(|| {
1813            CachekitError::Config("a backend must be provided via .backend()".to_owned())
1814        })?;
1815
1816        // Validate namespace if provided
1817        if let Some(ref ns) = self.namespace {
1818            if ns.is_empty() {
1819                return Err(CachekitError::Config("namespace cannot be empty".into()));
1820            }
1821            if ns.len() > 255 {
1822                return Err(CachekitError::Config("namespace exceeds 255 bytes".into()));
1823            }
1824            if !ns.bytes().all(|b| (0x20..=0x7E).contains(&b)) {
1825                return Err(CachekitError::Config(
1826                    "namespace must be ASCII printable".into(),
1827                ));
1828            }
1829        }
1830
1831        #[cfg(feature = "l1")]
1832        let l1 = if self.no_l1 {
1833            None
1834        } else {
1835            let capacity = self.l1_capacity.unwrap_or(1000);
1836            Some(crate::l1::L1Cache::new(capacity))
1837        };
1838
1839        // SWR defaults mirror the sibling SDKs: enabled, threshold ratio 0.5,
1840        // ratio validated in (0.0, 1.0] exactly like py's L1CacheConfig.
1841        #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
1842        let swr_threshold_ratio = {
1843            let ratio = self.swr_threshold_ratio.unwrap_or(0.5);
1844            if !(ratio > 0.0 && ratio <= 1.0) {
1845                return Err(CachekitError::Config(format!(
1846                    "swr_threshold_ratio must be in (0.0, 1.0]; got {ratio}"
1847                )));
1848            }
1849            ratio
1850        };
1851
1852        // Hand the backend a live view of this client's read counters so the
1853        // SaaS telemetry headers report real numbers with no user plumbing.
1854        // Attached to the raw backend, before the reliability decorator, so
1855        // no forwarding is needed; a provider set on the backend's own
1856        // builder wins (see `Backend::attach_metrics`).
1857        #[cfg(feature = "l1")]
1858        let l1_enabled = l1.is_some();
1859        #[cfg(not(feature = "l1"))]
1860        let l1_enabled = false;
1861        let counters = Arc::new(CacheCounters::new(l1_enabled));
1862        backend.attach_metrics(counters.provider());
1863
1864        // Apply the reliability stack last so it decorates the final backend.
1865        // A disabled config is the documented opt-out: skip the (no-op)
1866        // decorator entirely. The layer check lives on ReliabilityConfig
1867        // itself so a future layer can't be missed here (this gate
1868        // shipped that exact bug once already).
1869        #[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
1870        let (backend, breaker) = match self.reliability {
1871            Some(config) if !config.is_disabled() => {
1872                crate::reliability::wrap_reliable(backend, config, self.retry_deadlines)
1873            }
1874            _ => (backend, None),
1875        };
1876
1877        Ok(CacheKit {
1878            backend,
1879            default_ttl: self.default_ttl.unwrap_or(Duration::from_secs(300)),
1880            namespace: self.namespace,
1881            max_payload_bytes: self.max_payload_bytes.unwrap_or(5 * 1024 * 1024),
1882            flight: SharedFlight::default(),
1883            counters,
1884
1885            #[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
1886            breaker,
1887
1888            #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
1889            mutations: SharedMutations::default(),
1890
1891            #[cfg(feature = "l1")]
1892            l1,
1893
1894            #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
1895            swr_enabled: self.swr_enabled.unwrap_or(true),
1896
1897            #[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
1898            swr_threshold_ratio,
1899
1900            #[cfg(feature = "encryption")]
1901            encryption: self.encryption,
1902        })
1903    }
1904}
1905
1906#[cfg(test)]
1907mod validate_key_tests {
1908    use super::{validate_key, MAX_KEY_BYTES};
1909    use crate::error::CachekitError;
1910
1911    /// The byte loop `validate_key` ran before it checked 16 bytes at a time:
1912    /// the behaviour it must keep, error text included.
1913    fn byte_loop(key: &str) -> Result<(), String> {
1914        if key.is_empty() {
1915            return Err("key must not be empty".to_owned());
1916        }
1917        if key.len() > MAX_KEY_BYTES {
1918            return Err(format!(
1919                "key is {} bytes (limit: {MAX_KEY_BYTES})",
1920                key.len()
1921            ));
1922        }
1923        for b in key.bytes() {
1924            if b < 0x20 || b == 0x7F {
1925                return Err(format!("key contains illegal control character 0x{b:02X}"));
1926            }
1927        }
1928        Ok(())
1929    }
1930
1931    fn check(key: &str) {
1932        let got = validate_key(key).map_err(|e| match e {
1933            CachekitError::InvalidKey(msg) => msg,
1934            other => format!("not InvalidKey: {other:?}"),
1935        });
1936        assert_eq!(got, byte_loop(key), "key {key:?}");
1937    }
1938
1939    /// Every ASCII character, plus a 2-, 3- and 4-byte character (whose bytes
1940    /// are all 0x80 or more), at every position of every key length from 0
1941    /// to 48: every block boundary and overlap of the 16-byte check.
1942    #[test]
1943    fn same_verdict_and_message_as_the_byte_loop() {
1944        let fillers: Vec<char> = (0u8..0x80)
1945            .map(char::from)
1946            .chain(['é', '€', '😀'])
1947            .collect();
1948        for len in 0..=48 {
1949            for pos in 0..len {
1950                for &c in &fillers {
1951                    let mut key: String = "k".repeat(pos);
1952                    key.push(c);
1953                    key.push_str(&"k".repeat(len - pos - 1));
1954                    check(&key);
1955                }
1956            }
1957            check(&"k".repeat(len));
1958        }
1959    }
1960
1961    /// With two offending bytes the message names the first, wherever the
1962    /// two fall relative to the blocks.
1963    #[test]
1964    fn names_the_first_offending_byte() {
1965        for len in 2..=48 {
1966            for first in 0..len {
1967                for second in first + 1..len {
1968                    let mut key = vec![b'k'; len];
1969                    key[first] = 0x7F;
1970                    key[second] = 0x01;
1971                    check(&String::from_utf8_lossy(&key));
1972                }
1973            }
1974        }
1975    }
1976
1977    /// Near the length limit, where the last block overlaps the most.
1978    #[test]
1979    fn long_keys_around_the_limit() {
1980        for len in MAX_KEY_BYTES - 40..=MAX_KEY_BYTES + 1 {
1981            for pos in (0..len).filter(|p| *p < 40 || *p % 16 < 2 || *p % 16 > 13 || len - p < 40) {
1982                for c in ['\u{0}', '\u{1F}', ' ', '~', '\u{7F}', 'é'] {
1983                    let mut key: String = "k".repeat(pos);
1984                    key.push(c);
1985                    key.push_str(&"k".repeat(len - pos - 1));
1986                    check(&key);
1987                }
1988            }
1989        }
1990    }
1991}
1992
1993#[cfg(all(test, feature = "l1"))]
1994mod backfill_ttl_tests {
1995    use std::time::Duration;
1996
1997    use super::l1_backfill_ttl;
1998    use crate::backend::Freshness;
1999
2000    const DEFAULT_TTL: Duration = Duration::from_secs(300);
2001
2002    fn fresh_for(secs: u64) -> Freshness {
2003        Freshness {
2004            is_stale: false,
2005            fresh_for: Some(Duration::from_secs(secs)),
2006        }
2007    }
2008
2009    /// (e) No header: today's `min(default_ttl, 30 s)`, unchanged.
2010    #[test]
2011    fn absent_header_keeps_the_30s_cap() {
2012        let absent = Freshness::default();
2013        assert_eq!(
2014            l1_backfill_ttl(DEFAULT_TTL, absent),
2015            Some(Duration::from_secs(30))
2016        );
2017        assert_eq!(
2018            l1_backfill_ttl(Duration::from_secs(10), absent),
2019            Some(Duration::from_secs(10))
2020        );
2021    }
2022
2023    #[test]
2024    fn fresh_for_bounds_but_never_lifts_the_cap() {
2025        assert_eq!(
2026            l1_backfill_ttl(DEFAULT_TTL, fresh_for(5)),
2027            Some(Duration::from_secs(5))
2028        );
2029        assert_eq!(
2030            l1_backfill_ttl(DEFAULT_TTL, fresh_for(3600)),
2031            Some(Duration::from_secs(30))
2032        );
2033    }
2034
2035    #[test]
2036    fn stale_or_zero_forbids_the_backfill() {
2037        assert_eq!(l1_backfill_ttl(DEFAULT_TTL, fresh_for(0)), None);
2038        let stale = |fresh_for| Freshness {
2039            is_stale: true,
2040            fresh_for,
2041        };
2042        assert_eq!(l1_backfill_ttl(DEFAULT_TTL, stale(None)), None);
2043        assert_eq!(
2044            l1_backfill_ttl(DEFAULT_TTL, stale(Some(Duration::from_secs(60)))),
2045            None
2046        );
2047    }
2048}
2049
2050// SWR staleness exists only on native builds with L1 (see tests/swr_tests.rs).
2051#[cfg(all(
2052    test,
2053    feature = "encryption",
2054    feature = "l1",
2055    not(feature = "unsync"),
2056    not(target_arch = "wasm32")
2057))]
2058#[allow(clippy::unwrap_used)] // test-only: a failed setup step should panic loudly
2059mod secure_l1_tests {
2060    use std::time::Duration;
2061
2062    use super::{CacheKit, SharedBackend, SwrRead};
2063    use crate::backend::MemoryBackend;
2064    use crate::error::CachekitError;
2065
2066    /// The Stale arm reads straight from L1, and every public write on an
2067    /// encrypted client stores decryptable ciphertext there, so the
2068    /// undecryptable bytes are planted in L1 directly.
2069    #[tokio::test]
2070    async fn secure_interop_get_swr_evicts_stale_l1_after_decrypt_failure() {
2071        let backend = MemoryBackend::default();
2072        let shared: SharedBackend = std::sync::Arc::new(backend.clone());
2073        let client = CacheKit::builder()
2074            .backend(shared)
2075            .l1_capacity(100)
2076            .swr_threshold_ratio(0.001)
2077            .encryption_from_bytes(&[7u8; 32], "test-tenant")
2078            .unwrap()
2079            .build()
2080            .unwrap();
2081        client.l1_set(
2082            "poisoned-stale",
2083            bytes::Bytes::from_static(b"not ciphertext"),
2084            Duration::from_secs(60),
2085        );
2086        // Real time, not tokio's paused clock: L1 ages entries by std
2087        // Instant. threshold = 0.001 × 60 s = 60 ms (±10%); hard expiry 60 s.
2088        tokio::time::sleep(Duration::from_millis(200)).await;
2089
2090        let secure = client.secure_cache().unwrap();
2091        let err = secure
2092            .interop_get_swr::<String>("poisoned-stale")
2093            .await
2094            .unwrap_err();
2095        assert!(matches!(err, CachekitError::Encryption(_)), "got: {err:?}");
2096        // Served from L1, so the entry had not hard-expired into the L2 path.
2097        let stats = client.stats();
2098        assert_eq!((stats.l1_hits, stats.l2_hits), (1, 0), "{stats:?}");
2099
2100        // Nothing in the backend: a surviving L1 copy would keep failing; an
2101        // evicted one yields a miss.
2102        assert!(backend.store.lock().await.is_empty());
2103        assert!(matches!(
2104            secure.interop_get_swr::<String>("poisoned-stale").await,
2105            Ok(SwrRead::Miss)
2106        ));
2107    }
2108}