Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
target/

# AI agent configs — internal dev tooling, not part of the shipped product
CLAUDE.md
AGENTS.md
.claude/
.caliber/
.cursor/
CALIBER_LEARNINGS.md
61 changes: 0 additions & 61 deletions CLAUDE.md

This file was deleted.

4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/cachekit/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,18 @@ pub trait LockableBackend: Backend {

// ── Feature-gated backend modules ─────────────────────────────────────────────

/// HTTP backend for the cachekit.io SaaS API.
#[cfg(feature = "cachekitio")]
pub mod cachekitio;
#[cfg(feature = "cachekitio")]
mod cachekitio_lock;
#[cfg(feature = "cachekitio")]
mod cachekitio_ttl;

/// Redis backend via the [`fred`](https://crates.io/crates/fred) client.
#[cfg(feature = "redis")]
pub mod redis;

/// Cloudflare Workers backend using `worker::Fetch`.
#[cfg(feature = "workers")]
pub mod workers;
9 changes: 8 additions & 1 deletion crates/cachekit/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use thiserror::Error;

/// Top-level error type for all CacheKit operations.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CachekitError {
/// An error originating from the cache backend.
#[error("backend error: {0}")]
Expand All @@ -21,7 +22,12 @@ pub enum CachekitError {

/// The payload exceeds the maximum allowed size.
#[error("payload too large: {size} bytes (limit: {limit} bytes)")]
PayloadTooLarge { size: usize, limit: usize },
PayloadTooLarge {
/// Actual payload size in bytes.
size: usize,
/// Maximum allowed size in bytes.
limit: usize,
},

/// The cache key is invalid (empty, too long, or contains illegal bytes).
#[error("invalid cache key: {0}")]
Expand All @@ -32,6 +38,7 @@ pub enum CachekitError {

/// Classifies backend errors to determine retry behaviour.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BackendErrorKind {
/// Temporary failure — safe to retry (network blip, pool exhaustion).
Transient,
Expand Down
2 changes: 2 additions & 0 deletions crates/cachekit/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use crate::error::CachekitError;

type Blake2b256 = Blake2b<U32>;

/// Generate a deterministic cache key by Blake2b-256 hashing the function name
/// and serialized arguments, optionally prefixed with a namespace.
pub fn generate_cache_key(
namespace: &str,
function_name: &str,
Expand Down
7 changes: 7 additions & 0 deletions crates/cachekit/src/l1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ impl Expiry<String, L1Entry> for L1Expiry {
}
}

/// In-process LRU cache with per-entry TTL, backed by [`moka`].
///
/// Used as the L1 layer in the dual-layer cache architecture.
pub struct L1Cache {
store: Cache<String, L1Entry>,
}

impl L1Cache {
/// Create a new L1 cache with the given maximum entry capacity.
pub fn new(capacity: usize) -> Self {
Self {
store: Cache::builder()
Expand All @@ -35,10 +39,12 @@ impl L1Cache {
}
}

/// Retrieve cached bytes by key, or `None` if absent or expired.
pub fn get(&self, key: &str) -> Option<Vec<u8>> {
self.store.get(key).map(|entry| entry.data.clone())
}

/// Insert or overwrite an entry with the given TTL.
pub fn set(&self, key: &str, value: &[u8], ttl: Duration) {
self.store.insert(
key.to_string(),
Expand All @@ -49,6 +55,7 @@ impl L1Cache {
);
}

/// Remove an entry by key.
pub fn delete(&self, key: &str) {
self.store.invalidate(key);
}
Expand Down
12 changes: 12 additions & 0 deletions crates/cachekit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

// Production code lints — these only fire in src/, not tests/
#![warn(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
#![warn(missing_docs)]

// Mutually exclusive feature guards
#[cfg(all(feature = "workers", feature = "redis"))]
Expand All @@ -15,19 +16,30 @@ compile_error!(
#[cfg(all(feature = "workers", feature = "l1"))]
compile_error!("features `workers` and `l1` are mutually exclusive — moka requires std threads unavailable in wasm32");

/// Pluggable cache backend trait and implementations (CachekitIO, Redis, Workers).
pub mod backend;
/// High-level cache client with dual-layer (L1/L2) support.
pub mod client;
/// Configuration types and environment variable parsing.
pub mod config;
/// Error types for cache operations and backend communication.
pub mod error;
/// Cache key generation using Blake2b hashing.
pub mod key;
/// L1 cache hit-rate metrics for CachekitIO request headers.
pub mod metrics;
/// Serialization and deserialization of cached values via MessagePack.
pub mod serializer;
/// SDK session tracking (session ID and start timestamp).
pub mod session;
/// SSRF-safe URL validation for CachekitIO endpoints.
pub mod url_validator;

/// Client-side AES-256-GCM encryption with HKDF key derivation.
#[cfg(feature = "encryption")]
pub mod encryption;

/// In-process L1 cache backed by [`moka`] with per-entry TTL.
#[cfg(feature = "l1")]
pub mod l1;

Expand Down
7 changes: 7 additions & 0 deletions crates/cachekit/src/metrics.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
use std::sync::Arc;

/// Snapshot of L1/L2 cache hit statistics.
pub struct L1Stats {
/// Number of requests served from the L1 in-process cache.
pub l1_hits: u64,
/// Number of requests served from the L2 backend.
pub l2_hits: u64,
/// Number of cache misses.
pub misses: u64,
/// Whether the L1 cache is currently enabled.
pub l1_enabled: bool,
}

/// Thread-safe closure that produces an optional [`L1Stats`] snapshot.
pub type MetricsProvider = Arc<dyn Fn() -> Option<L1Stats> + Send + Sync>;

/// Build `X-CacheKit-*` HTTP headers from the current L1 stats provider.
pub fn metrics_headers(provider: Option<&MetricsProvider>) -> Vec<(&'static str, String)> {
let disabled = vec![("X-CacheKit-L1-Status", "disabled".to_string())];

Expand Down
2 changes: 2 additions & 0 deletions crates/cachekit/src/url_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use crate::error::CachekitError;

const ALLOWED_HOSTS: &[&str] = &["api.cachekit.io", "api.staging.cachekit.io"];

/// Validate that a CachekitIO API URL uses HTTPS, is not a private IP (SSRF
/// protection), and matches the allow-list unless `allow_custom_host` is set.
pub fn validate_cachekitio_url(
url_str: &str,
allow_custom_host: bool,
Expand Down
11 changes: 7 additions & 4 deletions deny.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
[advisories]
vulnerability = "deny"
unmaintained = "warn"
# cargo-deny 0.19+: vulnerability/unmaintained/unsound/notice all deny by default.
# Only yanked needs explicit config — warn instead of hard-fail so renovate can fix.
yanked = "warn"
notice = "warn"
unmaintained = "workspace"

[licenses]
allow = [
"MIT",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
"CDLA-Permissive-2.0",
"ISC",
"MPL-2.0",
"OpenSSL",
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
"OpenSSL",
]
confidence-threshold = 0.8

Expand Down