Skip to main content

thunder/server/
dispatch.rs

1//! The product integration surface (SRV-020..022): one trait, three hooks.
2//!
3//! Command routing, argument extraction and business logic are product-side
4//! (SRV-020); credential validation is product code — Thunder owns the
5//! handshake state machine, never the credential store (SRV-012). Command
6//! name matching is byte-exact pass-through: case policy lives inside the
7//! product's `dispatch` (SRV-022).
8
9use std::future::Future;
10
11use crate::wire::Value;
12
13use crate::server::session::Session;
14
15/// Credentials parsed by Thunder from `HELLO`/`AUTH` payloads (SRV-012).
16///
17/// - `AUTH <api_key>` → [`Credentials::ApiKey`] (single-arg form)
18/// - `AUTH <user> <pass>` → [`Credentials::UserPass`]
19/// - `HELLO {token: …}` → [`Credentials::Token`] (map payload)
20/// - `HELLO {api_key: …}` → [`Credentials::ApiKey`]
21/// - `HELLO {}` / missing map → [`Credentials::None`] — a deployment with
22///   `auth_required = false` accepts it; everyone else rejects it.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum Credentials {
25    /// A bare API key.
26    ApiKey(String),
27    /// Username + password.
28    UserPass(String, String),
29    /// A bearer token from a `MapPayload` HELLO.
30    Token(String),
31    /// No credentials supplied.
32    None,
33}
34
35/// The identity a successful [`Dispatch::authenticate`] resolves to. Stored
36/// on the [`Session`] and fed to [`Dispatch::capabilities`] for the HELLO
37/// reply (SRV-014).
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Principal {
40    /// Product-defined principal name (user, key id, …).
41    pub name: String,
42}
43
44/// Authentication failure from the product hook (SRV-012). Thunder maps it
45/// to the profile's error convention before it reaches the wire (SRV-021).
46#[derive(Debug, thiserror::Error)]
47pub enum AuthError {
48    /// Credentials failed validation. Rendered as the family's
49    /// `WRONGPASS …` string under `Resp3Prefixes`, `[unauthorized] …`
50    /// under `BracketCode`/`Both`.
51    #[error("invalid credentials")]
52    InvalidCredentials,
53    /// Product-specific failure; the message travels verbatim (WIRE-040).
54    #[error("{0}")]
55    Message(String),
56}
57
58/// Product integration is exactly this trait (SRV-020).
59///
60/// Declared with return-position `impl Future + Send` so implementers can
61/// write plain `async fn` (no `async-trait` dependency) while the listener
62/// can still spawn dispatch futures onto the runtime. The listener is
63/// generic over `D: Dispatch`, so object safety is not required.
64pub trait Dispatch: Send + Sync + 'static {
65    /// Run one command. The error `String` travels verbatim on the wire
66    /// (SRV-021, WIRE-040); a returned `Err` never closes the connection
67    /// (SRV-005).
68    fn dispatch(
69        &self,
70        session: &Session,
71        command: &str,
72        args: Vec<Value>,
73    ) -> impl Future<Output = Result<Value, String>> + Send;
74
75    /// Validate credentials parsed from `HELLO`/`AUTH` (SRV-012). Thunder
76    /// flips the session's auth flag on `Ok` — product code never touches
77    /// the state machine.
78    fn authenticate(
79        &self,
80        creds: Credentials,
81    ) -> impl Future<Output = Result<Principal, AuthError>> + Send;
82
83    /// Capability names advertised in `MapPayload` HELLO replies
84    /// (SRV-014). Defaults to none.
85    fn capabilities(&self, principal: &Principal) -> Vec<String> {
86        let _ = principal;
87        vec![]
88    }
89}