liminal-cli 0.3.1

The liminal command: serve the network, maintain its stores, or serve its discovery MCP over stdio
Documentation
//! A run proof remains private and asks its issuer once at the close boundary.

use crate::config::{Config, Credential};
use std::sync::Mutex;
use zeroize::Zeroizing;

pub(super) struct Revoke {
    held: Mutex<Option<Request>>,
}

struct Request {
    issuer: String,
    agent: String,
    pass: Zeroizing<String>,
    response_bytes: usize,
    timeout_ms: u64,
}

impl Revoke {
    pub fn new(config: &Config) -> Result<Option<Self>, String> {
        let Credential::Lys(lys) = &config.credential else {
            return Ok(None);
        };
        if !lys.key_file.as_os_str().is_empty() {
            return Ok(None);
        }
        let agent = config
            .agent
            .as_deref()
            .ok_or("the run has no agent to revoke")?;
        let environment = std::env::var("LYS_AGENT")
            .map_err(|_| "LYS_AGENT is unavailable at the revoke boundary")?;
        if environment != agent {
            return Err("the revoke agent differs from LYS_AGENT".to_owned());
        }
        if !agent
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
        {
            return Err("revoke agent must be one plain path segment".to_owned());
        }
        let issuer = origin(&lys.issuer)?;
        let pass = Zeroizing::new(
            std::env::var("LYS_AGENT_PASS")
                .map_err(|_| "LYS_AGENT_PASS is unavailable at the revoke boundary")?,
        );
        if pass.is_empty()
            || pass.len() > config.listen.run_pass_bytes
            || !pass.is_ascii()
            || pass.bytes().any(|byte| byte.is_ascii_control())
        {
            return Err(format!(
                "LYS_AGENT_PASS must be a nonempty header of at most {} bytes",
                config.listen.run_pass_bytes
            ));
        }
        Ok(Some(Self {
            held: Mutex::new(Some(Request {
                issuer: issuer.to_owned(),
                agent: agent.to_owned(),
                pass,
                response_bytes: config.listen.issuer_response_bytes,
                timeout_ms: config.limits.reply_timeout_ms,
            })),
        }))
    }

    pub fn send(&self) -> Result<(), String> {
        let held = self
            .held
            .lock()
            .map_err(|_| "the revoke lock is poisoned")?
            .take();
        held.map_or(Ok(()), |held| {
            crate::agent::exit::revoke_run_pass(
                &held.issuer,
                &held.agent,
                held.pass.to_string(),
                held.response_bytes,
                held.timeout_ms,
            )
            .map_err(|error| error.to_string())
        })
    }
}

fn origin(issuer: &str) -> Result<&str, String> {
    let issuer = issuer.trim_end_matches('/');
    let uri: ureq::http::Uri = issuer.parse().map_err(|_| "lys.issuer is not an origin")?;
    let authority = uri.authority().ok_or("lys.issuer has no authority")?;
    if authority.as_str().contains('@')
        || uri.query().is_some()
        || !matches!(uri.path(), "" | "/")
        || issuer.contains('#')
    {
        return Err(
            "lys.issuer must be an origin without credentials, path, query or fragment".to_owned(),
        );
    }
    if uri.scheme_str() != Some("https")
        && !(uri.scheme_str() == Some("http")
            && ["localhost", "127.0.0.1", "[::1]"].contains(&authority.host()))
    {
        return Err("lys.issuer must be https or loopback http".to_owned());
    }
    Ok(issuer)
}