haematite-cli 0.21.1

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
//! `haematite door --config <absolute door.json>`: Studio's door in the
//! foreground (ACCESS-002 R2).
//!
//! The configuration is read and checked whole, the app's credential is read
//! from the file it names, and Lys's discovery document is read, before the
//! door listens; any of them failing stops the door by name, and nothing is
//! served without a sign-in helper.

use std::io::Read;
use std::net::TcpListener;
use std::path::PathBuf;
use std::sync::Arc;

use super::config::Config;
use super::listen::serve;
use super::lys::LysSignIn;
use super::signin::Door;

const CONFIG_BYTES: u64 = 1_048_576;
const USAGE: &str = "usage: haematite door --config <absolute door.json>";

/// Run the door until its listener stops accepting.
///
/// # Errors
///
/// The usage, or the named reason the door could not start.
pub fn run(mut args: impl Iterator<Item = String>) -> Result<(), String> {
    if args.next().as_deref() != Some("--config") {
        return Err(USAGE.to_owned());
    }
    let path = PathBuf::from(args.next().ok_or(USAGE)?);
    if !path.is_absolute() || args.next().is_some() {
        return Err(USAGE.to_owned());
    }
    let file = std::fs::File::open(&path)
        .map_err(|error| format!("the door configuration could not be opened: {error}"))?;
    let mut bytes = Vec::new();
    file.take(CONFIG_BYTES + 1)
        .read_to_end(&mut bytes)
        .map_err(|error| format!("the door configuration could not be read: {error}"))?;
    if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > CONFIG_BYTES {
        return Err("the door configuration exceeds its byte limit".to_owned());
    }
    let config = Config::parse(&bytes)?;
    let issuer = LysSignIn::connect(&config.lys)?;
    let door = Arc::new(Door::new(issuer));
    let assistant = super::assistant_socket::Listener::start(&door, &config)
        .map_err(|error| format!("{}: {}", error.code, error.message))?;
    let site = Arc::new(config.site());
    let limits = config.limits();
    let listener = TcpListener::bind(config.listen)
        .map_err(|error| format!("the door could not listen on {}: {error}", config.listen))?;
    eprintln!(
        "haematite door: listening on {} for {}",
        config.listen, config.origin
    );
    serve(&listener, &door, &site, &limits);
    drop(assistant);
    Ok(())
}