haematite-cli 0.21.7

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use super::tests::{ISSUER, NOW, role, signer};
use super::{AUDIENCE, Gate, Unreachable};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use ed25519_dalek::Signer;
use lys_pass::{Error, KeySet};
use serde_json::json;
use std::sync::{
    Arc,
    atomic::{AtomicUsize, Ordering},
};
type TestResult = Result<(), Box<dyn std::error::Error>>;

fn keys(kid: &str) -> Result<KeySet, Error> {
    let x = URL_SAFE_NO_PAD.encode(signer().verifying_key().as_bytes());
    KeySet::from_json(
        &json!({"keys":[{"kty":"OKP","crv":"Ed25519","kid":kid,"x":x,"alg":"EdDSA","use":"sig"}]})
            .to_string(),
    )
}
fn token(kid: &str) -> String {
    let header = URL_SAFE_NO_PAD.encode(json!({"alg":"EdDSA","typ":"JWT","kid":kid}).to_string());
    let claims = URL_SAFE_NO_PAD.encode(json!({
        "iss": ISSUER, "aud":AUDIENCE, "sub":"reader", "iat":NOW - 10, "exp":NOW + 600,
        "holder":{"id":"reader","kind":"person","responsible":null}, "rights":role(&["read"],"grant")
    }).to_string());
    let signature = URL_SAFE_NO_PAD.encode(
        signer()
            .sign(format!("{header}.{claims}").as_bytes())
            .to_bytes(),
    );
    format!("{header}.{claims}.{signature}")
}
#[test]
fn rotated_issuer_keys_are_refetched_once_for_concurrent_passes() -> TestResult {
    let asks = Arc::new(AtomicUsize::new(0));
    let count = Arc::clone(&asks);
    let gate = Gate::fetching(
        move || {
            let ask = count.fetch_add(1, Ordering::SeqCst);
            keys(if ask == 0 { "old" } else { "rotated" })
        },
        ISSUER,
        "wards",
        Arc::new(Unreachable),
    )?;
    gate.keys.wait();
    let new = token("rotated");
    std::thread::scope(|scope| -> TestResult {
        let workers: Vec<_> = (0..8)
            .map(|_| scope.spawn(|| gate.present(&new, NOW)))
            .collect();
        for worker in workers {
            let answer = worker.join().map_err(|_| "verification panicked")?;
            assert!(answer.is_ok(), "rotated issuer refused a pass: {answer:?}");
        }
        Ok(())
    })?;
    assert_eq!(
        asks.load(Ordering::SeqCst),
        2,
        "rotation was not single-flight"
    );
    for _ in 0..5 {
        let refusal = gate
            .present(&token("unpublished"), NOW)
            .err()
            .ok_or("unpublished key accepted")?;
        assert_eq!(refusal.code, "unpublished_key");
    }
    assert_eq!(
        asks.load(Ordering::SeqCst),
        2,
        "unknown ids bypassed the refresh interval"
    );
    Ok(())
}

#[test]
fn hello_remains_available_while_issuer_keys_are_pending() -> TestResult {
    use crate::serve::protocol::{HelloAgreement, Operation};
    use std::sync::{Mutex, mpsc};
    let (release, released) = mpsc::channel();
    let released = Mutex::new(released);
    let gate = Gate::fetching(
        move || {
            released
                .lock()
                .map_err(|_| Error::CannotAsk("fixture poisoned"))?
                .recv()
                .map_err(|_| Error::CannotAsk("fixture closed"))?;
            keys("old")
        },
        ISSUER,
        "wards",
        Arc::new(Unreachable),
    )?;
    let result = gate.authorize(None, &Operation::Hello(HelloAgreement::client()), None, NOW);
    release.send(())?;
    gate.keys.wait();
    assert!(
        result.is_ok(),
        "hello was gated on pending issuer keys: {result:?}"
    );
    Ok(())
}