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}