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
//! The door's server-side state (ACCESS-002 R2).
//!
//! Sign-ins under way are keyed by their state, and signed-in sessions by an
//! opaque random id. The browser holds only that id; the pass and its
//! refresh token never leave the door.

use std::collections::BTreeMap;
use std::fs::File;
use std::io::Read;
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::Instant;

use crate::serve::storage::Failure;

/// What a sign-in was started with, and a signed-in person's pass, as
/// lys-pass's sign-in helper answers them.
pub use lys_pass::signin::{SignedIn, Started};

pub(super) struct Pending {
    pub(super) started: Started,
    pub(super) at: u64,
}

pub(super) struct Live {
    pub(super) until: u64,
    pub(super) state: Arc<Mutex<Session>>,
    pub(super) valid: Arc<std::sync::atomic::AtomicBool>,
}

impl Drop for Live {
    fn drop(&mut self) {
        self.valid
            .store(false, std::sync::atomic::Ordering::Release);
    }
}

pub(super) struct Session {
    pub(super) signed_in: SignedIn,
    /// When Lys last answered for this session, in Unix seconds.
    pub(super) answered: u64,
    /// The name of the last refresh Lys refused, while the pass still held.
    pub(super) refused: Option<String>,
    pub(super) attempted: Option<u64>,
    /// Requests already queued when a refresh finishes share that result.
    pub(super) refresh_finished: Option<Instant>,
}

#[derive(Default)]
pub(super) struct Sessions {
    pub(super) pending: Mutex<BTreeMap<String, Pending>>,
    pub(super) live: Mutex<BTreeMap<String, Live>>,
}

pub(super) fn lock<T>(mutex: &Mutex<T>) -> Result<MutexGuard<'_, T>, Failure> {
    mutex
        .lock()
        .map_err(|_| Failure::new("door_poisoned", "the door's sessions were poisoned"))
}

/// A new opaque session id: 32 random bytes as 64 lowercase hex digits.
///
/// # Errors
///
/// `door_random_unavailable` when the system's random source cannot be read.
pub(super) fn session_id() -> Result<String, Failure> {
    let mut bytes = [0_u8; 32];
    File::open("/dev/urandom")
        .and_then(|mut source| source.read_exact(&mut bytes))
        .map_err(|error| Failure::new("door_random_unavailable", error.to_string()))?;
    let mut id = String::with_capacity(64);
    for digit in bytes.iter().flat_map(|byte| [byte >> 4, byte & 0x0f]) {
        id.extend(char::from_digit(u32::from(digit), 16));
    }
    Ok(id)
}