conclavelib/server.rs
1//! The central server (`conclave serve`): the axum WSS endpoint and control plane.
2//!
3//! A single self-contained binary that owns: the `axum` WSS endpoint and control RPCs; the
4//! embedded store (see [`crate::store`]); in-memory presence with heartbeat reaping and the
5//! fan-out router; and admin authorization against the config `users` allowlist plus each
6//! channel's `created_by` (DESIGN.md §7).
7//!
8//! Durable state is config only — no message history. Presence, subscriptions, permission levels,
9//! and the admin allowlist are deliberately *not* in the DB (DESIGN.md §15).
10//!
11//! The subsystem is split by responsibility: `hub` is the transport-agnostic core (store +
12//! in-memory presence, subscriptions, and the fan-out router); `session` is the per-connection
13//! handshake + frame loop; `wss` is the axum WebSocket adapter and the [`serve`] entrypoint. This
14//! module owns the [`AclError`] authorization boundary type and re-exports the public surface.
15
16mod hub;
17mod session;
18mod wss;
19
20#[cfg(test)]
21mod integration;
22
23pub use wss::{ServerConfig, serve};
24
25use crate::protocol::ProtocolError;
26
27/// Errors at the access-control / authorization boundary (DESIGN.md §7, §16), matched on by the
28/// server and surfaced to the caller as a wire error frame.
29#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
30pub enum AclError {
31 /// The issuing user is not an admin for the attempted action.
32 #[error("not authorized: admin role required")]
33 NotAdmin,
34 /// The user is not a member of the channel.
35 #[error("not a member of channel `{0}`")]
36 NotMember(String),
37 /// The channel is private and the user has no ACL entry or valid invite.
38 #[error("channel `{0}` is private")]
39 ChannelPrivate(String),
40 /// The named channel does not exist.
41 #[error("channel `{0}` not found")]
42 ChannelNotFound(String),
43}
44
45impl From<AclError> for ProtocolError {
46 fn from(err: AclError) -> Self {
47 let message = err.to_string();
48 match err {
49 AclError::ChannelNotFound(_) => Self::NotFound(message),
50 _ => Self::Unauthorized(message),
51 }
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use pretty_assertions::assert_eq;
59
60 #[test]
61 fn acl_errors_map_onto_wire_protocol_errors() {
62 assert!(matches!(ProtocolError::from(AclError::NotAdmin), ProtocolError::Unauthorized(_)));
63 assert!(matches!(ProtocolError::from(AclError::ChannelNotFound("ops".to_owned())), ProtocolError::NotFound(_)));
64 }
65
66 #[test]
67 fn acl_error_messages_are_descriptive() {
68 assert_eq!(AclError::NotMember("ops".to_owned()).to_string(), "not a member of channel `ops`");
69 }
70}