delegated 0.2.2

Minimal fail-closed capability-token evaluation core
Documentation
use chrono::{Duration, TimeZone, Utc};
use delegated::{DelegationToken, RequestEnvelope};
use delegated::{
    KIND_DELEGATION_TOKEN, KIND_REQUEST_ENVELOPE, SPEC_VERSION_CURRENT, validate_request,
    validate_token,
};

fn base_token() -> DelegationToken {
    let now = Utc.with_ymd_and_hms(2026, 7, 1, 12, 0, 0).single().unwrap();
    DelegationToken {
        spec_version: SPEC_VERSION_CURRENT.to_owned(),
        kind: KIND_DELEGATION_TOKEN.to_owned(),
        token_id: "token-1".into(),
        issuer: "https://issuer.example".into(),
        agent_id: "agent:scheduler".into(),
        delegator_id: "user:alice".into(),
        audience: vec!["calendar-api".into()],
        allowed_actions: vec!["calendar.create".into()],
        allowed_resources: Some(vec!["calendar:alice".into()]),
        max_delegation_depth: Some(0),
        issued_at: now - Duration::minutes(1),
        expires_at: now + Duration::minutes(10),
        nonce: "nonce-000000000001".into(),
        key_id: "issuer-1".into(),
        signature_alg: "Ed25519".into(),
        signature: "sig".into(),
    }
}

fn base_envelope(token: DelegationToken) -> RequestEnvelope {
    RequestEnvelope {
        spec_version: SPEC_VERSION_CURRENT.to_owned(),
        kind: KIND_REQUEST_ENVELOPE.to_owned(),
        request_id: Some("request-1".into()),
        delegation_token: token,
    }
}

#[test]
fn validate_token_accepts_a_minimal_valid_token() {
    assert!(validate_token(&base_token()).is_ok());
}

#[test]
fn validate_request_accepts_a_minimal_valid_envelope() {
    assert!(validate_request(&base_envelope(base_token())).is_ok());
}

#[test]
fn validate_token_rejects_wrong_spec_version() {
    let mut token = base_token();
    token.spec_version = "0.1".into();
    let err = validate_token(&token).unwrap_err();
    assert_eq!(err.stage, "normalize_request");
    assert!(err.reason.contains("spec_version"));
}

#[test]
fn validate_token_rejects_wrong_kind() {
    let mut token = base_token();
    token.kind = "WrongKind".into();
    assert!(validate_token(&token).is_err());
}

#[test]
fn validate_token_rejects_empty_required_fields() {
    let mut token = base_token();
    token.token_id.clear();
    assert!(validate_token(&token).is_err());
}

#[test]
fn validate_token_rejects_whitespace_padding() {
    let mut token = base_token();
    token.issuer = " https://issuer.example".into();
    assert!(validate_token(&token).is_err());
}

#[test]
fn validate_token_rejects_short_nonce() {
    let mut token = base_token();
    token.nonce = "short".into();
    let err = validate_token(&token).unwrap_err();
    assert!(err.reason.contains("nonce"));
}

#[test]
fn validate_token_rejects_empty_audience_or_actions() {
    let mut token = base_token();
    token.audience.clear();
    assert!(validate_token(&token).is_err());

    let mut token = base_token();
    token.allowed_actions.clear();
    assert!(validate_token(&token).is_err());
}

#[test]
fn validate_token_rejects_duplicate_list_entries() {
    let mut token = base_token();
    token.audience = vec!["calendar-api".into(), "calendar-api".into()];
    let err = validate_token(&token).unwrap_err();
    assert!(err.reason.contains("duplicates"));
}

#[test]
fn validate_token_rejects_too_many_list_entries() {
    let mut token = base_token();
    token.audience = (0..65).map(|index| format!("audience-{index}")).collect();
    assert!(validate_token(&token).is_err());
}

#[test]
fn validate_token_rejects_oversized_identifiers() {
    let mut token = base_token();
    token.agent_id = "a".repeat(513);
    assert!(validate_token(&token).is_err());
}

#[test]
fn validate_token_rejects_inverted_lifetime() {
    let mut token = base_token();
    std::mem::swap(&mut token.issued_at, &mut token.expires_at);
    let err = validate_token(&token).unwrap_err();
    assert!(err.reason.contains("issued_at"));
}

#[test]
fn validate_token_rejects_wrong_signature_alg() {
    let mut token = base_token();
    token.signature_alg = "RS256".into();
    assert!(validate_token(&token).is_err());
}

#[test]
fn validate_request_rejects_bad_request_id() {
    let mut envelope = base_envelope(base_token());
    envelope.request_id = Some(" ".repeat(10));
    assert!(validate_request(&envelope).is_err());
}