delegated 0.2.2

Minimal fail-closed capability-token evaluation core
Documentation
# Integration guide

## Issuer side

The issuer owns the Ed25519 private key and decides the claims it is willing to assert.

```rust
use chrono::{Duration, Utc};
use delegated::DelegationTokenBuilder;
use ed25519_dalek::SigningKey;

let signing_key = SigningKey::from_bytes(&[7; 32]); // load from secure key custody in production
let now = Utc::now();
let token = DelegationTokenBuilder::new()
    .token_id("cap-01")
    .issuer("https://issuer.example")
    .agent_id("agent:scheduler")
    .delegator_id("user:alice")
    .audience("calendar-api")
    .allowed_action("calendar.create")
    .allowed_resource("calendar:alice")
    .issued_at(now)
    .expires_at(now + Duration::minutes(10))
    .nonce("random-value-at-least-16-bytes")
    .key_id("issuer-2026-01")
    .build_and_sign(&signing_key)?;
# Ok::<(), Box<dyn std::error::Error>>(())
```

Use a cryptographically random nonce and unique token identifier. Keep lifetimes short. The
builder validates structure, but it cannot assess whether identifiers or authority are correct.

## Resource-server side

Pin or otherwise resolve issuer keys from trusted configuration. Never copy a key from request
JSON into the resolver.

```rust
use delegated::{Evaluator, InMemoryTrustState, OperationContext, PinnedIssuerKeys};
use ed25519_dalek::VerifyingKey;

fn authorize(
    request_bytes: &[u8],
    issuer_key: VerifyingKey,
) -> Result<bool, Box<dyn std::error::Error>> {
    let keys = PinnedIssuerKeys::new();
    keys.insert("https://issuer.example", "issuer-2026-01", issuer_key)?;
    let state = InMemoryTrustState::new();

    // Values come from the operation dispatcher and validated target—not request claims.
    let operation = OperationContext::new("calendar-api", "calendar.create")
        .with_resource("calendar:alice");

    let (decision, audit) =
        Evaluator::new(&keys, &state).evaluate(request_bytes, &operation, chrono::Utc::now());
    persist_before_execution(audit)?;
    Ok(decision.allowed)
}

# fn persist_before_execution(_: delegated::AuditEvent) -> std::io::Result<()> { Ok(()) }
```

Reuse durable issuer-key and trust-state implementations across requests. Constructing a fresh
`InMemoryTrustState` per request disables cross-request replay protection.

## Operation mapping rules

- Define a stable, case-sensitive action vocabulary such as `calendar.create`.
- Derive the action from the handler selected by the server.
- Derive the resource after parsing and validating the actual target.
- Use a stable audience identifier for the service/trust domain.
- Provide delegation depth only if trusted infrastructure tracks it.
- Execute exactly the operation represented by `OperationContext` after authorization.

If a capability contains `allowed_resources`, omission of the host resource denies. If it contains
`max_delegation_depth`, omission of host depth denies. An unconstrained capability intentionally
authorizes any resource for an allowed action and audience.

## Durable integrations

Production deployments normally implement:

- `IssuerKeyResolver` over immutable configuration or a verified key registry.
- `TrustStateStore` over a shared transactional/key-value system.
- `AuditSink` over an append-only, access-controlled log pipeline.

Backend errors fail closed. Set dependency timeouts so failure is prompt and observable.

## Redis trust state (`delegated-redis`)

For multi-instance verifiers, use the [`delegated-redis`](https://crates.io/crates/delegated-redis)
crate instead of `InMemoryTrustState`. It implements the core
[`TrustStateStore`](https://docs.rs/delegated/latest/delegated/trait.TrustStateStore.html)
and [`TrustStateAdmin`](https://docs.rs/delegated/latest/delegated/trait.TrustStateAdmin.html)
traits against Redis.

```rust
use delegated::{Evaluator, OperationContext, PinnedIssuerKeys, VecAuditSink};
use delegated_redis::RedisTrustState;
use ed25519_dalek::VerifyingKey;

fn authorize(
    request_bytes: &[u8],
    issuer_key: VerifyingKey,
    redis_url: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
    let keys = PinnedIssuerKeys::new();
    keys.insert("https://issuer.example", "issuer-2026-01", issuer_key)?;

    // Reuse one Redis-backed store per process (or per pool worker).
    let state = RedisTrustState::with_prefix(redis_url, "calendar-api")?;

    let operation = OperationContext::new("calendar-api", "calendar.create")
        .with_resource("calendar:alice");

    let sink = VecAuditSink::new();
    let decision = Evaluator::new(&keys, &state).evaluate_and_audit(
        request_bytes,
        &operation,
        chrono::Utc::now(),
        &sink,
    )?;
    Ok(decision.allowed)
}
```

Operational requirements:

- Point every evaluator at the same Redis logical database.
- Use a unique key prefix per environment or service when sharing a cluster.
- Treat Redis outages as authorization failure; do not fall back to in-memory state.
- Run revocation and deny cleanup according to the
  [delegated-redis operations guide]../crates/delegated-redis/OPERATIONS.md.

A working end-to-end example lives in
[`examples/reference-stack/`](../examples/reference-stack/README.md).