liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
use super::{
    HEALTH_PATH, HealthWithLinks, METRICS_PATH, OperatorCredentialReissueOutcome,
    OperatorCredentialReissueRefusal, OperatorCredentialReissueRequest, QUIESCE_PATH, READY_PATH,
    RECLAIM_PATH, REISSUE_PATH, ReadyCondition, ReadyWithStream, ServedState, ServerError,
    StatusCode, UNLOADABLE_PATH, empty_response, health_check, json_response, metrics_route,
    parse_request_line, readiness_check, response,
};

pub(super) fn response_for_request(
    request: &[u8],
    served: &ServedState,
) -> Result<Vec<u8>, ServerError> {
    if let Some(answer) = super::admin::route(request, served)? {
        return Ok(answer);
    }
    let Ok(request) = std::str::from_utf8(request) else {
        return Ok(empty_response(StatusCode::BadRequest));
    };
    let Some((method, path)) = parse_request_line(request) else {
        return Ok(empty_response(StatusCode::BadRequest));
    };

    match (method, path) {
        ("GET", HEALTH_PATH) => {
            let mut status = health_check();
            let lys_root = served
                .lys_root
                .get()
                .map(crate::auth_lys::root_presence_start::RootSlot::health);
            status.ephemeral_sweep = served.ephemeral_sweep.get().copied().map(Into::into);
            json_response(
                StatusCode::Ok,
                &HealthWithLinks {
                    started_at: served.started_at,
                    grant_stream: served.grant_stream.get().map(|state| state.health()),
                    lys_root: lys_root.map(|(answer, _)| answer),
                    lys_root_cause: lys_root.and_then(|(_, cause)| cause),
                    health: status,
                    links: served.link_bridges.snapshot(),
                    listen_addresses: served.listen_addresses.get().cloned().unwrap_or_default(),
                },
            )
        }
        ("GET", READY_PATH) => {
            let status = readiness_check(&served.readiness.snapshot());
            let grant_stream = served.grant_stream.get().map(|state| state.health());
            let stream_ready = grant_stream
                .as_ref()
                .is_none_or(crate::auth_membership::grant_stream::StreamHealth::is_ready);
            let mut unmet_conditions: Vec<_> = status
                .unmet_conditions
                .into_iter()
                .map(ReadyCondition::Existing)
                .collect();
            if !stream_ready {
                unmet_conditions.push(ReadyCondition::Stream("grant_stream_ready"));
            }
            let ready = status.ready && stream_ready;
            let status_code = if ready {
                StatusCode::Ok
            } else {
                StatusCode::ServiceUnavailable
            };
            json_response(
                status_code,
                &ReadyWithStream {
                    ready,
                    unmet_conditions,
                    grant_stream,
                },
            )
        }
        ("GET", METRICS_PATH) => Ok(response(
            StatusCode::Ok,
            Some(metrics_route::CONTENT_TYPE),
            metrics_route::render_body().as_bytes(),
        )),
        // The refused-load surface. Reading it is a snapshot of a record
        // another part of the server already maintains — this route computes
        // nothing, schedules nothing, and touches no conversation.
        ("GET", UNLOADABLE_PATH) => json_response(StatusCode::Ok, &served.unloadable.status()),
        // DUR-021 R4: read from the store on each request; readiness never
        // reads it, so a server with a pass running is ready.
        ("GET", RECLAIM_PATH) => served.reclaim.status().map_or_else(
            || Ok(empty_response(StatusCode::ServiceUnavailable)),
            |status| json_response(StatusCode::Ok, &status),
        ),
        ("POST", QUIESCE_PATH) => served.reclaim.quiesce().map_or_else(
            || Ok(empty_response(StatusCode::ServiceUnavailable)),
            |answer| json_response(StatusCode::Ok, &answer),
        ),
        (
            _,
            HEALTH_PATH | READY_PATH | METRICS_PATH | UNLOADABLE_PATH | RECLAIM_PATH | QUIESCE_PATH,
        ) => Ok(empty_response(StatusCode::MethodNotAllowed)),
        // R18 amendment A7. Matched on the path with its query string SPLIT
        // OFF, and only here: the four routes above keep matching the whole
        // request target exactly as they always have, so a query string on
        // `/health` is still a 404 and no existing route's answer moves.
        _ if path.split('?').next() == Some(REISSUE_PATH) => {
            credential_reissue_response(method, path, served)
        }
        _ => Ok(empty_response(StatusCode::NotFound)),
    }
}

/// Answers one `OperatorCredentialReissue` call (R18 amendment A7, §0.18).
///
/// # Why the inputs ride the query string
///
/// The three inputs are fixed by §0.18 (`conversation_id`, `participant_id`,
/// `expected_current_generation`) and none of them is a secret — the SECRET
/// travels only in the response. The request shape is a build decision, and a
/// query string is chosen because this endpoint reads each request with ONE
/// bounded read and no message framing: a body would be present only when the
/// client happened to put it in the same segment, so parsing one would make
/// the route's answer depend on TCP segmentation. Teaching the shared request
/// reader to frame bodies would change the serving discipline of every route
/// here, which is not this lane's to do.
pub(super) fn credential_reissue_response(
    method: &str,
    path: &str,
    served: &ServedState,
) -> Result<Vec<u8>, ServerError> {
    if method != "POST" {
        return Ok(empty_response(StatusCode::MethodNotAllowed));
    }
    let query = path.split_once('?').map_or("", |(_, query)| query);
    let Some(request) = parse_reissue_query(query) else {
        return Ok(empty_response(StatusCode::BadRequest));
    };
    match served.reissue.reissue(request) {
        // No participant is configured on this node. Distinct from every
        // identity answer, and reported as such rather than as a lookup miss.
        Ok(None) => Ok(empty_response(StatusCode::ServiceUnavailable)),
        Ok(Some(OperatorCredentialReissueOutcome::Issued(issued))) => {
            json_response(StatusCode::Ok, &issued)
        }
        Ok(Some(OperatorCredentialReissueOutcome::Refused(refusal))) => {
            json_response(reissue_refusal_status(&refusal), &refusal)
        }
        Err(error) => {
            // The operation could not be DECIDED. The operator is told, and
            // the text is the refusal's own — never a bare close.
            tracing::error!(%error, "operator credential re-issue could not be decided");
            json_response(
                StatusCode::ServiceUnavailable,
                &serde_json::json!({ "error": error.message }),
            )
        }
    }
}

/// The status each typed refusal is served with.
///
/// A lookup miss is a 404 and every guard refusal is a 409: the identity
/// resolved, and the operation was refused by a live fact about it. The
/// discriminator an operator branches on is the body's `refusal` field, never
/// the status — the status is the HTTP-shaped summary of it.
pub(super) const fn reissue_refusal_status(
    refusal: &OperatorCredentialReissueRefusal,
) -> StatusCode {
    match refusal {
        OperatorCredentialReissueRefusal::ConversationUnknown { .. }
        | OperatorCredentialReissueRefusal::ParticipantUnknown { .. } => StatusCode::NotFound,
        OperatorCredentialReissueRefusal::Retired { .. }
        | OperatorCredentialReissueRefusal::LiveBinding { .. }
        | OperatorCredentialReissueRefusal::DetachReplayOpen { .. }
        | OperatorCredentialReissueRefusal::LiveReceipt { .. }
        | OperatorCredentialReissueRefusal::GenerationMismatch { .. } => StatusCode::Conflict,
    }
}

/// Parses the three §0.18 inputs, or answers `None` for anything else.
///
/// Every field is mandatory and every value is a plain decimal `u64`. An
/// unknown parameter, a repeat, or a missing one is refused rather than
/// defaulted: an operator who mistypes `expected_current_generation` must not
/// have a zero silently substituted for it.
pub(super) fn parse_reissue_query(query: &str) -> Option<OperatorCredentialReissueRequest> {
    let mut conversation_id = None;
    let mut participant_id = None;
    let mut expected_current_generation = None;
    for pair in query.split('&') {
        let (name, value) = pair.split_once('=')?;
        let value = value.parse::<u64>().ok()?;
        let slot = match name {
            "conversation_id" => &mut conversation_id,
            "participant_id" => &mut participant_id,
            "expected_current_generation" => &mut expected_current_generation,
            _ => return None,
        };
        if slot.replace(value).is_some() {
            return None;
        }
    }
    Some(OperatorCredentialReissueRequest {
        conversation_id: conversation_id?,
        participant_id: participant_id?,
        expected_current_generation: expected_current_generation?,
    })
}