Skip to main content

huddle_protocol/
relay.rs

1//! The relay control protocol: the JSON messages a huddle client and a
2//! `huddle-server` relay exchange over the WebSocket door.
3//!
4//! Extracted here so the client (`huddle-core::network::server`) and the relay
5//! (`huddle-server`) share ONE definition instead of the two hand-kept-in-sync
6//! copies they carried before. [`ClientMsg`] is what the client sends and the
7//! relay receives; [`ServerMsg`] is the reverse. Both derive `Serialize` +
8//! `Deserialize` so each side uses whichever direction it needs.
9//!
10//! Wire-compat note: the field-level `#[serde(default)]` / `skip_serializing_if`
11//! attributes match the relay's historical (authoritative) serialization, so
12//! these unified types are byte-identical to both prior copies — old clients
13//! and relays interoperate unchanged.
14
15use serde::{Deserialize, Serialize};
16
17/// Client → relay. The client serializes these; the relay deserializes them,
18/// tolerating absent optional fields from older clients via `serde(default)`.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(tag = "type", rename_all = "snake_case")]
21pub enum ClientMsg {
22    /// Announce identity and (re)assert room memberships, then drain the
23    /// mailbox. Must be the first message. huddle 1.1.4: it authenticates —
24    /// `pubkey_b64` is the client's Ed25519 pubkey and `signature_b64` is a
25    /// signature over `RELAY_AUTH_DOMAIN || nonce` for the nonce the relay sent
26    /// in the opening `Challenge`. The relay verifies both before registering.
27    Hello {
28        fingerprint: String,
29        #[serde(default)]
30        pubkey_b64: String,
31        #[serde(default)]
32        signature_b64: String,
33        #[serde(default)]
34        rooms: Vec<String>,
35        /// huddle 2.0: capability bit — the client implements at-least-once
36        /// mailbox ACKs (it answers each queued `Message` with `ClientMsg::Ack`
37        /// carrying the row's `mailbox_id`). When `true` the relay tags each
38        /// mailbox delivery with its row id and keeps the row until the matching
39        /// `Ack` arrives; when `false` (pre-2.0 clients — the serde default) the
40        /// relay falls back to classical delete-on-deliver.
41        #[serde(default)]
42        acks: bool,
43    },
44    Subscribe {
45        room: String,
46    },
47    Unsubscribe {
48        room: String,
49    },
50    /// Send an opaque payload to every other member of `room`.
51    Publish {
52        room: String,
53        id: String,
54        payload_b64: String,
55    },
56    /// huddle 1.2: deliver an opaque payload to a SPECIFIC recipient
57    /// fingerprint, independent of room membership — how 1:1 DMs and friend
58    /// requests route reliably. `room` is an opaque tag the recipient's client
59    /// uses to file the message; the relay never interprets it.
60    SendDirect {
61        to: String,
62        room: String,
63        id: String,
64        payload_b64: String,
65    },
66    /// huddle 1.2.1: mint a short-lived connect code bound to this
67    /// authenticated identity. The relay replies with `ConnectToken`.
68    CreateConnectToken,
69    /// huddle 1.2.1: resolve a connect code to its owner's fingerprint+pubkey.
70    /// The relay replies with `ConnectTokenResolved` (fingerprint = None when
71    /// unknown/expired).
72    RedeemConnectToken {
73        token: String,
74    },
75    /// Re-drain the mailbox on demand.
76    Fetch,
77    /// huddle 2.0: acknowledge durable receipt of a relay-delivered mailbox
78    /// message. The relay deletes the row only after this ACK (at-least-once
79    /// delivery). Scoped to the authenticated fingerprint.
80    Ack {
81        mailbox_id: i64,
82    },
83    Ping,
84}
85
86/// Relay → client. The relay serializes these; the client deserializes them.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(tag = "type", rename_all = "snake_case")]
89pub enum ServerMsg {
90    /// huddle 1.1.4: sent immediately on connect. The client signs the nonce to
91    /// prove control of its identity key before it can do anything.
92    Challenge {
93        nonce_b64: String,
94    },
95    /// Sent after a successful `Hello`. Carries the authenticated fingerprint
96    /// (the client already knows its own identity, so it ignores the field).
97    Ready {
98        fingerprint: String,
99    },
100    /// A room message delivered live or from the offline mailbox. huddle 2.0:
101    /// `mailbox_id` is `Some(row_id)` when the message came from the relay's
102    /// on-disk queue AND the recipient advertised ACK support in its `Hello`;
103    /// `None` for live fan-out and pre-2.0 clients. `skip_serializing_if` keeps
104    /// the field off the wire for live/legacy messages so old clients see
105    /// exactly the bytes they expect.
106    Message {
107        room: String,
108        id: String,
109        payload_b64: String,
110        #[serde(default, skip_serializing_if = "Option::is_none")]
111        mailbox_id: Option<i64>,
112        /// huddle 2.0.8 (WS2 foundations #5): the relay-assigned **per-room
113        /// monotonic sequence number** for total-ordered delivery — the
114        /// foundation MLS commit ordering needs. `None` for non-room deliveries
115        /// (`SendDirect`), offline-mailbox replays in this first slice, and
116        /// pre-2.0.8 relays; additive, so older clients ignore it.
117        #[serde(default, skip_serializing_if = "Option::is_none")]
118        seq: Option<i64>,
119    },
120    Sent {
121        id: String,
122        delivered: usize,
123        queued: usize,
124    },
125    /// huddle 1.2.1: a freshly minted connect code + its lifetime (seconds).
126    ConnectToken {
127        token: String,
128        ttl_secs: u64,
129    },
130    /// huddle 1.2.1: result of redeeming a connect code. `fingerprint` /
131    /// `pubkey_b64` are `None` when the code is unknown or expired. The relay
132    /// echoes the `token`; the client ignores it.
133    ConnectTokenResolved {
134        token: String,
135        #[serde(default)]
136        fingerprint: Option<String>,
137        #[serde(default)]
138        pubkey_b64: Option<String>,
139    },
140    Pong,
141    Error {
142        message: String,
143    },
144}