Functional Weave
Code in Rust

auth.login-throttle@1.0.0

impl/rust.rs

3,088 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

/// Allowed or locked, by replaying the failures in time order: max_attempts
/// failures within window_seconds of the latest lock the account for
/// lockout_seconds from that failure, and the count starts again.
///
/// # Panics
/// Panics on a policy number below 1.
pub fn login_throttle(failures: &[i64], now: i64, policy: &ThrottlePolicy) -> ThrottleDecision {
    if policy.max_attempts < 1 {
        panic!("maxAttempts must be a whole number of at least 1");
    }
    if policy.window_seconds < 1 {
        panic!("windowSeconds must be a whole number of at least 1");
    }
    if policy.lockout_seconds < 1 {
        panic!("lockoutSeconds must be a whole number of at least 1");
    }
    let mut times: Vec<i64> = failures.iter().copied().filter(|&t| t <= now).collect();
    times.sort_unstable();
    let mut locked_until: Option<i64> = None;
    let mut streak: Vec<i64> = Vec::new();
    for t in times {
        if let Some(until) = locked_until {
            if t < until {
                continue;
            }
        }
        streak.retain(|&s| s > t - policy.window_seconds);
        streak.push(t);
        if streak.len() as i64 >= policy.max_attempts {
            locked_until = Some(t + policy.lockout_seconds);
            streak.clear();
        }
    }
    if let Some(until) = locked_until {
        if now < until {
            return ThrottleDecision {
                allowed: false,
                retry_after_seconds: until - now,
                remaining_attempts: 0,
                locked_until: Some(until),
            };
        }
    }
    let live = streak.iter().filter(|&&s| s > now - policy.window_seconds).count() as i64;
    ThrottleDecision {
        allowed: true,
        retry_after_seconds: 0,
        remaining_attempts: policy.max_attempts - live,
        locked_until: None,
    }
}

pub fn throttle_decision_to_value(decision: &ThrottleDecision) -> Value {
    Value::obj(vec![
        ("allowed", Value::Bool(decision.allowed)),
        ("retryAfterSeconds", Value::Int(decision.retry_after_seconds)),
        ("remainingAttempts", Value::Int(decision.remaining_attempts)),
        ("lockedUntil", decision.locked_until.map(Value::Int).unwrap_or(Value::Null)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let failures: Vec<i64> = args[0]
        .as_arr()
        .iter()
        .map(|v| match v {
            Value::Int(i) => *i,
            _ => panic!("failures must be whole Unix seconds"),
        })
        .collect();
    let now = match &args[1] {
        Value::Int(i) => *i,
        _ => panic!("now must be a whole number of Unix seconds"),
    };
    let field = |key: &str| match args[2].get(key) {
        Value::Int(i) => *i,
        _ => panic!("{} must be a whole number of at least 1", key),
    };
    let policy = ThrottlePolicy {
        max_attempts: field("maxAttempts"),
        window_seconds: field("windowSeconds"),
        lockout_seconds: field("lockoutSeconds"),
    };
    throttle_decision_to_value(&login_throttle(&failures, now, &policy))
}