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
2 changes: 2 additions & 0 deletions contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ hex = "0.4"
chrono = "0.4.43"
base64 = "0.22"
rand = "0.8"
uuid = { version = "1", features = ["v4"] }
async-trait = "0.1"

[dev-dependencies]
httpmock = "0.7"
Expand Down
96 changes: 96 additions & 0 deletions contract/cmmty/per_client_rate_limit/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//! Rate-limit configuration loaded from environment variables.

use std::env;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClientTier {
Known,
Default,
}

/// Configuration for per-client rate limiting.
#[derive(Debug, Clone)]
pub struct RateLimitConfig {
/// Max requests per window for unknown/anonymous clients.
pub default_limit: u64,
/// Max requests per window for known (whitelisted) clients.
pub known_client_limit: u64,
/// Window size in seconds.
pub window_secs: u64,
/// List of known client IDs (from `KNOWN_CLIENT_IDS` env var, comma-separated).
pub known_clients: Vec<String>,
}

impl RateLimitConfig {
/// Load from environment variables with sensible defaults.
///
/// | Variable | Default |
/// |-----------------------|---------|
/// | `RL_DEFAULT_LIMIT` | 60 |
/// | `RL_KNOWN_LIMIT` | 600 |
/// | `RL_WINDOW_SECS` | 60 |
/// | `KNOWN_CLIENT_IDS` | (empty) |
pub fn from_env() -> Self {
let default_limit = env_u64("RL_DEFAULT_LIMIT", 60);
let known_client_limit = env_u64("RL_KNOWN_LIMIT", 600);
let window_secs = env_u64("RL_WINDOW_SECS", 60);
let known_clients = env::var("KNOWN_CLIENT_IDS")
.unwrap_or_default()
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.collect();

Self {
default_limit,
known_client_limit,
window_secs,
known_clients,
}
}

/// Determine the tier for a given client ID.
pub fn tier_for(&self, client_id: &str) -> ClientTier {
if self.known_clients.iter().any(|k| k == client_id) {
ClientTier::Known
} else {
ClientTier::Default
}
}
}

fn env_u64(key: &str, default: u64) -> u64 {
env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn tier_for_known_client() {
let cfg = RateLimitConfig {
default_limit: 10,
known_client_limit: 100,
window_secs: 60,
known_clients: vec!["client-a".to_string()],
};
assert_eq!(cfg.tier_for("client-a"), ClientTier::Known);
assert_eq!(cfg.tier_for("client-b"), ClientTier::Default);
}

#[test]
fn tier_for_empty_known_list() {
let cfg = RateLimitConfig {
default_limit: 10,
known_client_limit: 100,
window_secs: 60,
known_clients: vec![],
};
assert_eq!(cfg.tier_for("anyone"), ClientTier::Default);
}
}
Loading
Loading