Skip to main content

nodedb_types/sync/wire/
session.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Session lifecycle messages: handshake, token refresh, keepalive.
4
5use std::collections::HashMap;
6
7use serde::{Deserialize, Serialize};
8
9/// Handshake message (client → server, 0x01).
10#[derive(
11    Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
12)]
13pub struct HandshakeMsg {
14    /// JWT bearer token for authentication.
15    pub jwt_token: String,
16    /// Client's vector clock: `{ collection: { doc_id: lamport_ts } }`.
17    pub vector_clock: HashMap<String, HashMap<String, u64>>,
18    /// Shape IDs the client is subscribed to.
19    pub subscribed_shapes: Vec<String>,
20    /// Client version string.
21    pub client_version: String,
22    /// Lite instance identity (UUID v7). Default empty for non-Lite peers.
23    #[serde(default)]
24    pub lite_id: String,
25    /// Monotonic epoch counter (incremented on every open). Default 0 for non-Lite peers.
26    #[serde(default)]
27    pub epoch: u64,
28    /// Wire format version. Server rejects connections with incompatible versions.
29    /// Missing field deserializes to 0 and is rejected by the server explicitly.
30    #[serde(default)]
31    pub wire_version: u16,
32}
33
34/// Handshake acknowledgment (server → client, 0x02).
35#[derive(
36    Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
37)]
38pub struct HandshakeAckMsg {
39    /// Whether the handshake succeeded.
40    pub success: bool,
41    /// Session ID assigned by the server.
42    pub session_id: String,
43    /// Server's vector clock (for initial sync).
44    pub server_clock: HashMap<String, u64>,
45    /// Error message (if !success).
46    pub error: Option<String>,
47    /// Fork detection: if true, client must regenerate LiteId and reconnect.
48    #[serde(default)]
49    pub fork_detected: bool,
50    /// Server's wire format version (for client-side compatibility check).
51    #[serde(default)]
52    pub server_wire_version: u16,
53    /// Server-assigned producer ID for this session. 0 if not yet assigned.
54    #[serde(default)]
55    pub producer_id: u64,
56    /// Server's current accepted epoch for this producer. 0 if not yet tracked.
57    #[serde(default)]
58    pub accepted_epoch: u64,
59    /// Stable per-user key for signing queued CRDT deltas. Issued only after
60    /// successful authentication over the protected sync transport. All zeros
61    /// means signing is unavailable for this session.
62    #[serde(default)]
63    pub delta_signing_key: [u8; 32],
64}
65
66/// Token refresh request (client → server, 0x60).
67///
68/// Sent by Lite before the current JWT expires. The client provides
69/// a fresh token obtained from the application's auth layer.
70/// Origin validates the new token and either upgrades the session
71/// or disconnects if the token is invalid.
72#[derive(
73    Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
74)]
75pub struct TokenRefreshMsg {
76    /// New JWT bearer token.
77    pub new_token: String,
78}
79
80/// Token refresh acknowledgment (server → client, 0x61).
81#[derive(
82    Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
83)]
84pub struct TokenRefreshAckMsg {
85    /// Whether the token refresh succeeded.
86    pub success: bool,
87    /// Error message (if !success).
88    pub error: Option<String>,
89    /// Seconds until this new token expires (so Lite can schedule next refresh).
90    #[serde(default)]
91    pub expires_in_secs: u64,
92}
93
94/// Ping/Pong keepalive (0xFF).
95#[derive(
96    Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
97)]
98pub struct PingPongMsg {
99    /// Timestamp (epoch milliseconds) for RTT measurement.
100    pub timestamp_ms: u64,
101    /// Whether this is a pong (response to ping).
102    pub is_pong: bool,
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::sync::wire::{SyncFrame, SyncMessageType};
109
110    #[test]
111    fn handshake_serialization() {
112        let msg = HandshakeMsg {
113            jwt_token: "test.jwt.token".into(),
114            vector_clock: HashMap::new(),
115            subscribed_shapes: vec!["shape1".into()],
116            client_version: "0.1.0".into(),
117            lite_id: String::new(),
118            epoch: 0,
119            wire_version: 1,
120        };
121        let frame = SyncFrame::new_msgpack(SyncMessageType::Handshake, &msg).unwrap();
122        let bytes = frame.to_bytes();
123        assert!(bytes.len() > SyncFrame::HEADER_SIZE);
124        assert_eq!(bytes[0], SyncFrame::FORMAT_VERSION);
125        assert_eq!(bytes[1], 0x01);
126    }
127}