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}