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/// The server-admin allowlist (DESIGN.md §7): each admin username mapped to the public key
28/// (base64) permitted to claim it, or `None` if unpinned. Pinning stops a fresh-deploy admin
29/// username from being squatted by the first client to register it (PRD-0007 T-002).
30pub type AdminAllowlist = std::collections::HashMap<String, Option<String>>;
31
32/// Errors at the access-control / authorization boundary (DESIGN.md §7, §16), matched on by the
33/// server and surfaced to the caller as a wire error frame.
34#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
35pub enum AclError {
36 /// The issuing user is not an admin for the attempted action.
37 #[error("not authorized: admin role required")]
38 NotAdmin,
39 /// The user is not a member of the channel.
40 #[error("not a member of channel `{0}`")]
41 NotMember(String),
42 /// The channel is private and the user has no ACL entry or valid invite.
43 #[error("channel `{0}` is private")]
44 ChannelPrivate(String),
45 /// The named channel does not exist.
46 #[error("channel `{0}` not found")]
47 ChannelNotFound(String),
48}
49
50impl From<AclError> for ProtocolError {
51 fn from(err: AclError) -> Self {
52 let message = err.to_string();
53 match err {
54 AclError::ChannelNotFound(_) => Self::NotFound(message),
55 _ => Self::Unauthorized(message),
56 }
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use pretty_assertions::assert_eq;
64
65 #[test]
66 fn acl_errors_map_onto_wire_protocol_errors() {
67 assert!(matches!(ProtocolError::from(AclError::NotAdmin), ProtocolError::Unauthorized(_)));
68 assert!(matches!(ProtocolError::from(AclError::ChannelNotFound("ops".to_owned())), ProtocolError::NotFound(_)));
69 }
70
71 #[test]
72 fn acl_error_messages_are_descriptive() {
73 assert_eq!(AclError::NotMember("ops".to_owned()).to_string(), "not a member of channel `ops`");
74 }
75}