liminal-server 0.28.4

Standalone server for the liminal messaging bus
Documentation
//! Signed liminal passes with the running server's agent holder shape.

use lys_core::keys::Ed25519Identity;
use serde_json::{Value, json};
use zeroize::Zeroizing;

/// The issuer and published signing key already trusted by the fixture.
pub struct Issuer<'a> {
    pub url: &'a str,
    pub key_id: &'a str,
    pub identity: &'a Ed25519Identity,
}

/// Claims a pin deliberately varies while retaining the agent holder shape.
pub struct Options<'a> {
    pub holder: &'a str,
    pub expires_at: u64,
    /// The `iat` Lys stamps: the moment it builds the pass.
    pub issued_at: u64,
}

impl<'a> From<&'a str> for Options<'a> {
    fn from(holder: &'a str) -> Self {
        Self {
            holder,
            expires_at: u64::MAX,
            issued_at: 0,
        }
    }
}

/// Mints the agent's own pass without changing the fixture's effective rights.
///
/// # Errors
/// The signed header or claims could not be encoded.
pub fn mint<'a>(
    options: impl Into<Options<'a>>,
    rights: &[Value],
    issuer: &Issuer<'_>,
) -> Result<Zeroizing<String>, Box<dyn std::error::Error>> {
    let Options {
        holder,
        expires_at,
        issued_at,
    } = options.into();
    let claims = json!({
        "iss": issuer.url, "sub": holder, "aud": "liminal", "iat": issued_at, "exp": expires_at,
        "holder": {"id": holder, "kind": "agent", "responsible": "owner"},
        "rights": rights,
    });
    let header = base64url(&serde_json::to_vec(&json!({
        "alg": "EdDSA", "typ": "JWT", "kid": issuer.key_id,
    }))?);
    let signed = format!("{header}.{}", base64url(&serde_json::to_vec(&claims)?));
    Ok(Zeroizing::new(format!(
        "{signed}.{}",
        base64url(&issuer.identity.sign(signed.as_bytes()))
    )))
}

/// Encodes the fixture's published issuer key in the same form as its pass.
#[must_use]
pub fn base64url(bytes: &[u8]) -> String {
    const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
    let mut encoded = String::new();
    for chunk in bytes.chunks(3) {
        let first = chunk[0];
        let second = chunk.get(1).copied().unwrap_or(0);
        let third = chunk.get(2).copied().unwrap_or(0);
        encoded.push(char::from(ALPHABET[usize::from(first >> 2)]));
        encoded.push(char::from(
            ALPHABET[usize::from((first & 3) << 4 | second >> 4)],
        ));
        if chunk.len() > 1 {
            encoded.push(char::from(
                ALPHABET[usize::from((second & 15) << 2 | third >> 6)],
            ));
        }
        if chunk.len() > 2 {
            encoded.push(char::from(ALPHABET[usize::from(third & 63)]));
        }
    }
    encoded
}