liminal-server 0.28.3

Standalone server for the liminal messaging bus
Documentation
//! ACCESS-015: construction on an exchange with no `[auth]`.
//!
//! An open exchange proves nothing about who a seat is and records what it
//! declared, `open:<name>`. That holder already creates, deploys, holds, wakes
//! and retires services (ACCESS-012); here it also acts on sites, and its own
//! act stands where a person's approval stands on a gated exchange. The act
//! goes through the same apply and commit as an act Lys answers outright, and
//! the record names the holder as approver and answerable. Nothing reaches
//! Lys: the effects that place and delegate there stand empty, and a builder
//! is an open seat whose station is in the registry.

use std::sync::Arc;

use super::effects::SiteEffects;
use super::{BuilderGrants, SiteAct, SiteChange, SiteError};
use crate::agents::open_session::{OPEN_GRANT, OPEN_KIND};
use crate::auth_lys::session::LysSession;
use crate::server::connection::declared_holder::{ANONYMOUS, PREFIX};
use crate::services::lifecycle::Lifecycle;

/// The refusal an anonymous seat meets on every construction act.
pub const NEEDS_A_NAME: &str = "construction_needs_a_name: an anonymous seat acts for no one; \
                                liminal seat init --open <server> --name <name>";

/// Whether `session` may build: a Lys agent, or an open seat with a name.
#[must_use]
pub fn builds(session: &dyn LysSession) -> bool {
    match session.holder_kind() {
        Some("agent") => true,
        Some(OPEN_KIND) => session.holder() != ANONYMOUS,
        _ => false,
    }
}

/// The holder as a site's records name the one answerable for it: the person
/// id for a Lys person, `open:<name>` for a named open seat.
#[must_use]
pub fn answerable(session: &dyn LysSession) -> Option<String> {
    match session.holder_kind() {
        Some("person") => Some(session.holder().to_owned()),
        Some(OPEN_KIND) if session.holder() != ANONYMOUS => {
            Some(format!("{PREFIX}{}", session.holder()))
        }
        _ => None,
    }
}

/// The approval owner of an open exchange: every act is the declared
/// holder's own and is applied at once.
#[derive(Debug)]
pub struct DeclaredActs;

impl crate::server::connection::services::SiteDraftHolder for DeclaredActs {
    fn hold(
        &self,
        lifecycle: &Lifecycle,
        session: &dyn LysSession,
        operation: &str,
        target: &str,
        change: SiteChange,
        at_ms: u64,
    ) -> Result<serde_json::Value, SiteError> {
        if session.holder_kind() != Some(OPEN_KIND) {
            return Err(SiteError::Invalid("construction holder kind"));
        }
        let holder = answerable(session).ok_or_else(|| SiteError::Refused(NEEDS_A_NAME.into()))?;
        let act = SiteAct {
            target: target.into(),
            holder: holder.clone(),
            responsible: holder,
            change,
        };
        lifecycle.apply_declared(&act, operation, at_ms)
    }
}

/// The effects of an open exchange: no placement, no grant, no revocation
/// reaches Lys; a builder is an open seat with a station in the registry.
pub struct OpenSiteEffects {
    agents: Arc<crate::agents::runtime::Agents>,
}

impl OpenSiteEffects {
    /// Effects over the open exchange's agent registry.
    #[must_use]
    pub const fn new(agents: Arc<crate::agents::runtime::Agents>) -> Self {
        Self { agents }
    }
}

impl SiteEffects for OpenSiteEffects {
    fn agent_owner(&self, agent: &str) -> Result<String, SiteError> {
        let record = self.agents.registry().record(agent).map_err(|_| {
            SiteError::Refused(format!(
                "builder {agent} has no station on this exchange; its seat listens to its \
                 inbox once to place it"
            ))
        })?;
        if !record.active() {
            return Err(SiteError::Refused(format!("agent {agent} is retired")));
        }
        Ok(record.owner)
    }

    fn bind(
        &self,
        _draft: &str,
        _site: &str,
        agent: &str,
        _at_ms: u64,
    ) -> Result<BuilderGrants, SiteError> {
        self.agent_owner(agent)?;
        Ok(BuilderGrants {
            plan: OPEN_GRANT.into(),
            serve: OPEN_GRANT.into(),
        })
    }

    fn revoke(&self, _draft: &str, _label: &str, _grant: &str) -> Result<(), SiteError> {
        Ok(())
    }

    fn open(&self) -> bool {
        true
    }
}

#[cfg(test)]
#[path = "declared_tests.rs"]
mod tests;