Skip to main content

suture_protocol/
lib.rs

1//! Suture Protocol — wire format for client-server communication.
2//!
3//! Defines the request/response types used by the Suture Hub for
4//! push, pull, authentication, and repository management operations.
5//! All types are serializable via `serde` for JSON transport.
6
7use serde::{Deserialize, Serialize};
8
9pub const PROTOCOL_VERSION: u32 = 1;
10
11#[derive(Clone, Debug, Serialize, Deserialize)]
12pub struct HandshakeRequest {
13    pub client_version: u32,
14    pub client_name: String,
15}
16
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct HandshakeResponse {
19    pub server_version: u32,
20    pub server_name: String,
21    pub compatible: bool,
22}
23
24#[derive(Clone, Debug, Serialize, Deserialize)]
25pub enum AuthMethod {
26    None,
27    Signature {
28        public_key: String,
29        signature: String,
30    },
31    Token(String),
32}
33
34#[derive(Clone, Debug, Serialize, Deserialize)]
35pub struct AuthRequest {
36    pub method: AuthMethod,
37    pub timestamp: u64,
38}
39
40#[derive(Clone, Debug, Serialize, Deserialize)]
41pub struct HashProto {
42    pub value: String,
43}
44
45#[derive(Clone, Debug, Serialize, Deserialize)]
46pub struct PatchProto {
47    pub id: HashProto,
48    pub operation_type: String,
49    pub touch_set: Vec<String>,
50    pub target_path: Option<String>,
51    pub payload: String,
52    pub parent_ids: Vec<HashProto>,
53    pub author: String,
54    pub message: String,
55    pub timestamp: u64,
56}
57
58#[derive(Clone, Debug, Serialize, Deserialize)]
59pub struct BranchProto {
60    pub name: String,
61    pub target_id: HashProto,
62}
63
64#[derive(Clone, Debug, Serialize, Deserialize)]
65pub struct BlobRef {
66    pub hash: HashProto,
67    pub data: String,
68}
69
70#[derive(Debug, Serialize, Deserialize)]
71pub struct PushRequest {
72    pub repo_id: String,
73    pub patches: Vec<PatchProto>,
74    pub branches: Vec<BranchProto>,
75    pub blobs: Vec<BlobRef>,
76    /// Optional Ed25519 signature (64 bytes, base64-encoded).
77    /// Required when the hub has authorized keys configured.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub signature: Option<Vec<u8>>,
80    /// Client's known state of branches at time of push.
81    /// Used for fast-forward validation on the hub.
82    /// Optional for backward compatibility.
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub known_branches: Option<Vec<BranchProto>>,
85    /// If true, skip fast-forward validation on push.
86    #[serde(default)]
87    pub force: bool,
88}
89
90#[derive(Debug, Serialize, Deserialize)]
91pub struct PushResponse {
92    pub success: bool,
93    pub error: Option<String>,
94    pub existing_patches: Vec<HashProto>,
95}
96
97#[derive(Debug, Serialize, Deserialize)]
98pub struct PullRequest {
99    pub repo_id: String,
100    pub known_branches: Vec<BranchProto>,
101    /// Limit the number of patches returned from each branch tip.
102    /// None = full history, Some(n) = last n patches per branch.
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub max_depth: Option<u32>,
105}
106
107#[derive(Debug, Serialize, Deserialize)]
108pub struct PullResponse {
109    pub success: bool,
110    pub error: Option<String>,
111    pub patches: Vec<PatchProto>,
112    pub branches: Vec<BranchProto>,
113    pub blobs: Vec<BlobRef>,
114}
115
116#[derive(Debug, Serialize, Deserialize)]
117pub struct ListReposResponse {
118    pub repo_ids: Vec<String>,
119}
120
121#[derive(Debug, Serialize, Deserialize)]
122pub struct RepoInfoResponse {
123    pub repo_id: String,
124    pub patch_count: u64,
125    pub branches: Vec<BranchProto>,
126    pub success: bool,
127    pub error: Option<String>,
128}
129
130pub fn hash_to_hex(h: &HashProto) -> String {
131    h.value.clone()
132}
133
134pub fn hex_to_hash(hex: &str) -> HashProto {
135    HashProto {
136        value: hex.to_string(),
137    }
138}
139
140/// Build canonical bytes for push request signing.
141/// Format: repo_id \0 patch_count \0 (each patch: id \0 op \0 author \0 msg \0 timestamp \0) ... branch_count \0 (each: name \0 target \0) ...
142pub fn canonical_push_bytes(req: &PushRequest) -> Vec<u8> {
143    let mut buf = Vec::new();
144
145    buf.extend_from_slice(req.repo_id.as_bytes());
146    buf.push(0);
147
148    buf.extend_from_slice(&(req.patches.len() as u64).to_le_bytes());
149    for patch in &req.patches {
150        buf.extend_from_slice(patch.id.value.as_bytes());
151        buf.push(0);
152        buf.extend_from_slice(patch.operation_type.as_bytes());
153        buf.push(0);
154        buf.extend_from_slice(patch.author.as_bytes());
155        buf.push(0);
156        buf.extend_from_slice(patch.message.as_bytes());
157        buf.push(0);
158        buf.extend_from_slice(&patch.timestamp.to_le_bytes());
159        buf.push(0);
160    }
161
162    buf.extend_from_slice(&(req.branches.len() as u64).to_le_bytes());
163    for branch in &req.branches {
164        buf.extend_from_slice(branch.name.as_bytes());
165        buf.push(0);
166        buf.extend_from_slice(branch.target_id.value.as_bytes());
167        buf.push(0);
168    }
169
170    buf
171}