liminal-server 0.20.1

Standalone server for the liminal messaging bus
Documentation
//! The live Lys question over HTTP (ACCESS-002 R1, ACCESS-003 R1).
//!
//! `lys-pass`'s batch route is asynchronous; the connection's decision path
//! is not. Each ask runs its future to completion on a scoped thread against
//! the server's tokio runtime, so a connection process never blocks the
//! runtime's own workers and never nests a runtime. Deliberate asks are rare
//! by definition (opening a link, deploying a service, an administrator's
//! act), so a thread per ask is the honest cost; nothing is cached, and no
//! failure falls back to the hot seam. A held answer is recorded as a draft
//! with the holder's own pass before it is reported, so a held open is a
//! draft Lys retains, never a promise liminal invented.

use super::GrantMode;
use super::lys_pass_adapter::{LiveAnswer, LiveLysAsk};
use lys_pass::deliberate::CheckAnswer;
use lys_pass::drafts::{DraftRequest, request_digest};
use lys_pass::{Client, Error, Mode, Target};

/// `lys-pass`'s client behind the [`LiveLysAsk`] seam.
pub struct HttpLysAsk {
    client: Client,
    handle: tokio::runtime::Handle,
}

impl std::fmt::Debug for HttpLysAsk {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.debug_struct("HttpLysAsk").finish_non_exhaustive()
    }
}

impl HttpLysAsk {
    /// Asks `issuer` through a fresh HTTP client driven by `handle`'s runtime.
    ///
    /// # Errors
    /// The issuer is not an HTTP origin.
    pub fn new(issuer: &str, handle: tokio::runtime::Handle) -> Result<Self, Error> {
        let issuer = issuer
            .parse()
            .map_err(|_| Error::Invalid("issuer is not a URL"))?;
        Ok(Self {
            client: Client::new(reqwest::Client::default(), issuer)?,
            handle,
        })
    }

    /// Runs one ask to completion on its own thread against the runtime.
    fn block<T, F>(&self, future: F) -> Result<T, Error>
    where
        T: Send,
        F: std::future::Future<Output = Result<T, Error>> + Send,
    {
        let handle = &self.handle;
        std::thread::scope(|scope| scope.spawn(move || handle.block_on(future)).join())
            .unwrap_or_else(|_| Err(Error::CannotAsk("the live ask did not complete")))
    }

    /// Records the held act as a draft with the holder's own pass, returning
    /// the draft Lys retained.
    fn draft(
        &self,
        pass: &str,
        holder: &str,
        target: &Target,
        grant: &str,
        judgment: &CheckAnswer,
    ) -> Result<String, Error> {
        let words = format!(
            "{holder} asks {} on {}:{}",
            target.action, target.kind, target.id
        );
        let draft = DraftRequest {
            operation: format!(
                "liminal:{holder}:{}:{}:{}",
                target.kind, target.id, target.action
            ),
            grant: grant.to_owned(),
            target: Target::new(&target.kind, &target.id, &target.action)?,
            request_digest: request_digest(&words),
            words,
        };
        self.block(self.client.create_draft(pass, &draft, judgment))
            .map(|created| created.draft)
    }
}

impl LiveLysAsk for HttpLysAsk {
    fn ask(
        &self,
        pass: &str,
        holder: &str,
        target: &Target,
        _now: u64,
    ) -> Result<LiveAnswer, Error> {
        let answer =
            self.block(
                self.client
                    .check_batch(pass, holder, std::slice::from_ref(target), None),
            )?;
        let Some(result) = answer.results.into_iter().next() else {
            return Err(Error::CannotAsk("the live answer carried no result"));
        };
        match (
            result.allowed,
            result.mode.as_ref(),
            result.grant.as_deref(),
            result.refusal.as_deref(),
        ) {
            (true, Some(Mode::Outright), Some(grant), None) => Ok(LiveAnswer::Outright {
                grant: grant.to_owned(),
            }),
            (false, Some(Mode::ByDraft), Some(grant), None) => Ok(LiveAnswer::Held {
                mode: GrantMode::ByDraft,
                grant: grant.to_owned(),
                draft: self.draft(pass, holder, target, grant, &result)?,
            }),
            (false, Some(Mode::ByTwo), Some(grant), None) => Ok(LiveAnswer::Held {
                mode: GrantMode::ByTwo,
                grant: grant.to_owned(),
                draft: self.draft(pass, holder, target, grant, &result)?,
            }),
            (false, None, None, Some(refusal)) => Ok(LiveAnswer::Refused {
                refusal: refusal.to_owned(),
            }),
            _ => Err(Error::CannotAsk("inconsistent live permission answer")),
        }
    }
}