use crate::store::ApiKeyStore;
use crate::store::stateless::StatelessStore;
use crate::{
ApiKey, ApiKeyStartText, AuthError, AuthResult, ConsumeApiKeyResult, CreateApiKey, UpdateApiKey,
};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
#[async_trait]
impl ApiKeyStore for StatelessStore {
async fn create_api_key(&self, input: CreateApiKey) -> AuthResult<ApiKey> {
let now = Utc::now().to_rfc3339();
let start = input.start.map(|start| match start.storage_text() {
ApiKeyStartText::Utf8(text) => Ok(text),
ApiKeyStartText::Wtf8(_) => Err(AuthError::internal(
"Unpaired UTF-16 API-key starting characters cannot be represented by the native String schema",
)),
}).transpose()?;
let row = ApiKey {
id: uuid::Uuid::new_v4().to_string(),
name: input.name,
start,
prefix: input.prefix,
key_hash: input.key_hash,
reference_id: input.reference_id,
config_id: input.config_id,
refill_interval: input.refill_interval,
refill_amount: input.refill_amount,
last_refill_at: None,
enabled: input.enabled,
rate_limit_enabled: input.rate_limit_enabled,
rate_limit_time_window: input.rate_limit_time_window,
rate_limit_max: input.rate_limit_max,
request_count: Some(0.0),
remaining: input.remaining,
last_request: None,
expires_at: validated_date(input.expires_at)?,
created_at: now.clone(),
updated_at: now,
permissions: input.permissions,
metadata: input.metadata,
};
_ = self.lock()?.api_keys.insert(row.id.clone(), row.clone());
Ok(row)
}
async fn get_api_key_by_id(&self, id: &str) -> AuthResult<Option<ApiKey>> {
Ok(self.lock()?.api_keys.get(id).cloned())
}
async fn get_api_key_by_hash(&self, hash: &str) -> AuthResult<Option<ApiKey>> {
Ok(self
.lock()?
.api_keys
.values()
.find(|row| row.key_hash == hash)
.cloned())
}
async fn list_api_keys_by_reference(&self, reference_id: &str) -> AuthResult<Vec<ApiKey>> {
Ok(self
.lock()?
.api_keys
.values()
.filter(|row| row.reference_id == reference_id)
.cloned()
.collect())
}
async fn update_api_key(&self, id: &str, mut update: UpdateApiKey) -> AuthResult<ApiKey> {
update.expires_at = update.expires_at.map(validated_date).transpose()?;
update.last_request = update.last_request.map(validated_date).transpose()?;
update.last_refill_at = update.last_refill_at.map(validated_date).transpose()?;
let mut state = self.lock()?;
let row = state.api_keys.get_mut(id).ok_or_else(missing_key)?;
macro_rules! optional {
($($field:ident),*) => { $(if let Some(value) = update.$field { row.$field = Some(value); })* };
}
optional!(
name,
remaining,
rate_limit_time_window,
rate_limit_max,
refill_interval,
refill_amount,
permissions,
metadata,
request_count
);
macro_rules! direct {
($($field:ident),*) => { $(if let Some(value) = update.$field { row.$field = value; })* };
}
direct!(
enabled,
rate_limit_enabled,
expires_at,
last_request,
last_refill_at
);
row.updated_at = Utc::now().to_rfc3339();
Ok(row.clone())
}
async fn delete_api_key(&self, id: &str) -> AuthResult<()> {
_ = self.lock()?.api_keys.shift_remove(id);
Ok(())
}
async fn delete_expired_api_keys(&self) -> AuthResult<usize> {
let mut state = self.lock()?;
let now = Utc::now();
let before = state.api_keys.len();
state.api_keys.retain(|_, row| {
row.expires_at
.as_deref()
.is_none_or(|value| stored_date(value).is_ok_and(|date| date >= now))
});
Ok(before - state.api_keys.len())
}
async fn consume_api_key_usage(
&self,
id: &str,
global_rate_limit_enabled: bool,
) -> AuthResult<ConsumeApiKeyResult> {
let mut state = self.lock()?;
let stored = state.api_keys.get_mut(id).ok_or_else(missing_key)?;
let mut row = stored.clone();
let observed = row.clone();
if observed.remaining.is_some() && !consume_remaining(&mut row, &observed, Utc::now())? {
return Ok(ConsumeApiKeyResult::UsageExhausted);
}
loop {
let observed = row.clone();
match consume_rate(&mut row, &observed, global_rate_limit_enabled, Utc::now())? {
RateClaim::Denied(delay) => {
*stored = row;
return Ok(ConsumeApiKeyResult::RateLimited {
try_again_in: delay,
});
}
RateClaim::Written => break,
RateClaim::Retry => {}
}
}
row.updated_at = Utc::now().to_rfc3339();
*stored = row.clone();
Ok(ConsumeApiKeyResult::Allowed(Box::new(row)))
}
async fn consume_api_key_usage_from_snapshot(
&self,
observed: &ApiKey,
global_rate_limit_enabled: bool,
) -> AuthResult<ConsumeApiKeyResult> {
let mut row = if observed.remaining.is_some() {
let mut state = self.lock()?;
let Some(current) = state.api_keys.get_mut(&observed.id) else {
return Ok(ConsumeApiKeyResult::UsageExhausted);
};
if !consume_remaining(current, observed, Utc::now())? {
return Ok(ConsumeApiKeyResult::UsageExhausted);
}
current.clone()
} else {
observed.clone()
};
loop {
let mut state = self.lock()?;
let Some(current) = state.api_keys.get_mut(&row.id) else {
return Err(invalid_key());
};
match consume_rate(current, &row, global_rate_limit_enabled, Utc::now())? {
RateClaim::Denied(delay) => {
return Ok(ConsumeApiKeyResult::RateLimited {
try_again_in: delay,
});
}
RateClaim::Written => break,
RateClaim::Retry => row = current.clone(),
}
}
let mut state = self.lock()?;
let current = state.api_keys.get_mut(&row.id).ok_or_else(invalid_key)?;
current.updated_at = Utc::now().to_rfc3339();
Ok(ConsumeApiKeyResult::Allowed(Box::new(current.clone())))
}
}
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "Match JavaScript millisecond arithmetic"
)]
fn consume_remaining(
current: &mut ApiKey,
observed: &ApiKey,
now: DateTime<Utc>,
) -> AuthResult<bool> {
if let (Some(interval), Some(amount)) = (observed.refill_interval, observed.refill_amount)
&& interval != 0.0
&& amount != 0.0
&& (now.timestamp_millis()
- stored_date(
observed
.last_refill_at
.as_deref()
.unwrap_or(&observed.created_at),
)?
.timestamp_millis()) as f64
> interval
&& same_date(
current.last_refill_at.as_deref(),
observed.last_refill_at.as_deref(),
)?
{
current.remaining = Some(amount - 1.0);
current.last_refill_at = Some(now.to_rfc3339());
return Ok(true);
}
if let Some(remaining) = current.remaining
&& remaining > 0.0
{
current.remaining = Some(remaining - 1.0);
return Ok(true);
}
Ok(false)
}
enum RateClaim {
Written,
Retry,
Denied(f64),
}
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "Match JavaScript millisecond arithmetic and Date TimeClip"
)]
fn consume_rate(
current: &mut ApiKey,
observed: &ApiKey,
enabled: bool,
now: DateTime<Utc>,
) -> AuthResult<RateClaim> {
if !enabled || !observed.rate_limit_enabled {
current.last_request = Some(now.to_rfc3339());
return Ok(RateClaim::Written);
}
let (Some(window), Some(max)) = (observed.rate_limit_time_window, observed.rate_limit_max)
else {
return Ok(RateClaim::Written);
};
let previous = observed
.last_request
.as_deref()
.map(stored_date)
.transpose()?;
let current_previous = current
.last_request
.as_deref()
.map(stored_date)
.transpose()?;
let elapsed = previous.map(|last| (now.timestamp_millis() - last.timestamp_millis()) as f64);
let window_start = (now.timestamp_millis() as f64 - window).trunc();
if !window_start.is_finite() || window_start.abs() > 8_640_000_000_000_000.0 {
return Err(AuthError::internal(
"Invalid API key rate-limit window date",
));
}
let guard = current_previous.map(|date| date.timestamp_millis() as f64);
match elapsed {
None if current_previous.is_none() => current.request_count = Some(1.0),
Some(elapsed) if elapsed > window && guard.is_some_and(|last| last <= window_start) => {
current.request_count = Some(1.0);
}
Some(elapsed) if elapsed <= window => {
if observed.request_count.unwrap_or(0.0) >= max {
return Ok(RateClaim::Denied((window - elapsed).ceil()));
}
if !guard.is_some_and(|last| last > window_start)
|| current.request_count.unwrap_or(0.0) >= max
{
return Ok(RateClaim::Retry);
}
current.request_count = Some(current.request_count.unwrap_or(0.0) + 1.0);
}
_ => return Ok(RateClaim::Retry),
}
current.last_request = Some(now.to_rfc3339());
Ok(RateClaim::Written)
}
fn same_date(left: Option<&str>, right: Option<&str>) -> AuthResult<bool> {
match (left, right) {
(None, None) => Ok(true),
(Some(left), Some(right)) => {
Ok(stored_date(left)?.timestamp_millis() == stored_date(right)?.timestamp_millis())
}
_ => Ok(false),
}
}
fn validated_date(value: Option<String>) -> AuthResult<Option<String>> {
value
.map(|value| stored_date(&value).map(|_| value))
.transpose()
}
fn stored_date(value: &str) -> AuthResult<DateTime<Utc>> {
DateTime::parse_from_rfc3339(value)
.map(|date| date.with_timezone(&Utc))
.map_err(|error| AuthError::internal(format!("Invalid stored API key date: {error}")))
}
fn missing_key() -> AuthError {
AuthError::not_found("API Key not found")
}
const fn invalid_key() -> AuthError {
AuthError::Upstream {
status: 401,
code: "INVALID_API_KEY",
message: "Invalid API key.",
}
}