runbeam_sdk/runbeam_api/
types.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4/// Runbeam API error type
5///
6/// Represents all possible errors that can occur when interacting with
7/// the Runbeam Cloud API or performing related operations.
8#[derive(Debug)]
9pub enum RunbeamError {
10    /// JWT validation failed
11    JwtValidation(String),
12    /// API request failed (network, HTTP, or response parsing error)
13    Api(ApiError),
14    /// Token storage operation failed
15    Storage(crate::storage::StorageError),
16    /// Configuration error
17    Config(String),
18    /// TOML validation failed
19    Validation(crate::validation::ValidationError),
20}
21
22impl fmt::Display for RunbeamError {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            RunbeamError::JwtValidation(msg) => write!(f, "JWT validation failed: {}", msg),
26            RunbeamError::Api(err) => write!(f, "API error: {}", err),
27            RunbeamError::Storage(err) => write!(f, "Storage error: {}", err),
28            RunbeamError::Config(msg) => write!(f, "Configuration error: {}", msg),
29            RunbeamError::Validation(err) => write!(f, "Validation error: {}", err),
30        }
31    }
32}
33
34impl std::error::Error for RunbeamError {}
35
36impl From<ApiError> for RunbeamError {
37    fn from(err: ApiError) -> Self {
38        RunbeamError::Api(err)
39    }
40}
41
42impl From<crate::storage::StorageError> for RunbeamError {
43    fn from(err: crate::storage::StorageError) -> Self {
44        RunbeamError::Storage(err)
45    }
46}
47
48impl From<jsonwebtoken::errors::Error> for RunbeamError {
49    fn from(err: jsonwebtoken::errors::Error) -> Self {
50        RunbeamError::JwtValidation(err.to_string())
51    }
52}
53
54impl From<crate::validation::ValidationError> for RunbeamError {
55    fn from(err: crate::validation::ValidationError) -> Self {
56        RunbeamError::Validation(err)
57    }
58}
59
60/// API-specific errors
61#[derive(Debug)]
62pub enum ApiError {
63    /// Network error (connection, timeout, etc.)
64    Network(String),
65    /// HTTP error with status code
66    Http { status: u16, message: String },
67    /// Failed to parse response
68    Parse(String),
69    /// Request building failed
70    Request(String),
71}
72
73impl fmt::Display for ApiError {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            ApiError::Network(msg) => write!(f, "Network error: {}", msg),
77            ApiError::Http { status, message } => {
78                write!(f, "HTTP {} error: {}", status, message)
79            }
80            ApiError::Parse(msg) => write!(f, "Parse error: {}", msg),
81            ApiError::Request(msg) => write!(f, "Request error: {}", msg),
82        }
83    }
84}
85
86impl std::error::Error for ApiError {}
87
88impl From<reqwest::Error> for ApiError {
89    fn from(err: reqwest::Error) -> Self {
90        if err.is_timeout() {
91            ApiError::Network("Request timeout".to_string())
92        } else if err.is_connect() {
93            ApiError::Network(format!("Connection failed: {}", err))
94        } else if let Some(status) = err.status() {
95            ApiError::Http {
96                status: status.as_u16(),
97                message: err.to_string(),
98            }
99        } else {
100            ApiError::Network(err.to_string())
101        }
102    }
103}
104
105/// User information from JWT claims
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct UserInfo {
108    pub id: String,
109    pub email: String,
110    pub name: String,
111}
112
113/// User authentication token (JWT)
114///
115/// This token is used for authenticating user actions with the Runbeam Cloud API.
116/// It has a shorter lifespan than machine tokens and is typically issued after
117/// a user successfully logs in via the browser-based OAuth flow.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct UserToken {
120    /// JWT token for API authentication
121    pub token: String,
122    /// Token expiration timestamp (seconds since Unix epoch)
123    #[serde(default)]
124    pub expires_at: Option<i64>,
125    /// User information from JWT claims
126    #[serde(default)]
127    pub user: Option<UserInfo>,
128}
129
130impl UserToken {
131    /// Create a new user token
132    pub fn new(token: String, expires_at: Option<i64>, user: Option<UserInfo>) -> Self {
133        Self {
134            token,
135            expires_at,
136            user,
137        }
138    }
139}
140
141/// Team information from JWT claims
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct TeamInfo {
144    pub id: String,
145    pub name: String,
146}
147
148/// Gateway information returned from authorize endpoint
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct GatewayInfo {
151    pub id: String,
152    pub code: String,
153    pub name: String,
154    #[serde(default)]
155    pub authorized_by: Option<AuthorizedBy>,
156}
157
158/// User who authorized the gateway
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct AuthorizedBy {
161    pub id: String,
162    pub name: String,
163    pub email: String,
164}
165
166/// Response from Runbeam Cloud authorize endpoint
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct AuthorizeResponse {
169    pub machine_token: String,
170    pub expires_in: f64,
171    pub expires_at: String,
172    pub gateway: GatewayInfo,
173    #[serde(default)]
174    pub abilities: Vec<String>,
175}
176
177/// Request payload for storing/updating Harmony configuration
178///
179/// This is used by the `harmony.update` endpoint to send TOML configuration
180/// from Harmony instances back to Runbeam Cloud for storage as database models.
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct StoreConfigRequest {
183    /// Type of configuration being stored ("gateway", "pipeline", or "transform")
184    #[serde(rename = "type")]
185    pub config_type: String,
186    /// Optional ID for updating existing resources (omitted for creates)
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub id: Option<String>,
189    /// TOML configuration content
190    pub config: String,
191}
192
193/// Response from storing/updating Harmony configuration
194///
195/// The API returns UpdateSuccessResource format.
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct StoreConfigResponse {
198    /// Success flag
199    pub success: bool,
200    /// Success message
201    pub message: String,
202    /// Response data with model and change info
203    pub data: StoreConfigModel,
204}
205
206/// Model information from store config response
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct StoreConfigModel {
209    /// Model ID (ULID)
210    pub id: String,
211    /// Model type ("gateway", "pipeline", "transform")
212    #[serde(rename = "type")]
213    pub model_type: String,
214    /// Action taken ("created", "updated")
215    pub action: String,
216}
217
218/// Mesh information returned from Runbeam Cloud API
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct MeshInfo {
221    /// Unique mesh identifier (ULID)
222    pub id: String,
223    /// Human-readable mesh name
224    pub name: String,
225    /// Protocol type for mesh communication (http, http3)
226    #[serde(rename = "type")]
227    pub mesh_type: String,
228    /// Mesh provider (local, runbeam)
229    pub provider: String,
230    /// Authentication type for mesh members (currently only "jwt")
231    #[serde(default = "default_auth_type")]
232    pub auth_type: String,
233    /// JWT secret for HS256 symmetric key authentication (local provider)
234    #[serde(default, skip_serializing_if = "Option::is_none")]
235    pub jwt_secret: Option<String>,
236    /// Path to RSA private key (PEM) for RS256 JWT signing (local provider)
237    #[serde(default, skip_serializing_if = "Option::is_none")]
238    pub jwt_private_key_path: Option<String>,
239    /// Path to RSA public key (PEM) for RS256 JWT verification (local provider)
240    #[serde(default, skip_serializing_if = "Option::is_none")]
241    pub jwt_public_key_path: Option<String>,
242    /// List of ingress definition names
243    #[serde(default)]
244    pub ingress: Vec<String>,
245    /// List of egress definition names
246    #[serde(default)]
247    pub egress: Vec<String>,
248    /// Whether the mesh is enabled
249    #[serde(default = "default_true")]
250    pub enabled: bool,
251    /// Optional description
252    #[serde(default)]
253    pub description: Option<String>,
254}
255
256/// Mesh ingress information - allows other mesh members to send requests
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct MeshIngressInfo {
259    /// Unique ingress identifier (ULID)
260    pub id: String,
261    /// Human-readable ingress name
262    pub name: String,
263    /// Protocol type for incoming mesh requests (http, http3)
264    #[serde(rename = "type")]
265    pub ingress_type: String,
266    /// Pipeline name that owns this ingress (required)
267    pub pipeline: String,
268    /// Mode: 'default' allows all requests, 'mesh' requires valid mesh authentication
269    #[serde(default = "default_mode")]
270    pub mode: String,
271    /// Optional endpoint override. If omitted, the first endpoint in the pipeline is used.
272    #[serde(default, skip_serializing_if = "Option::is_none")]
273    pub endpoint: Option<String>,
274    /// List of URLs that map to this ingress
275    #[serde(default)]
276    pub urls: Vec<String>,
277    /// Whether the ingress is enabled
278    #[serde(default = "default_true")]
279    pub enabled: bool,
280    /// Optional description
281    #[serde(default)]
282    pub description: Option<String>,
283}
284
285/// Mesh egress information - allows sending requests to other mesh members
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct MeshEgressInfo {
288    /// Unique egress identifier (ULID)
289    pub id: String,
290    /// Human-readable egress name
291    pub name: String,
292    /// Protocol type for outgoing mesh requests (http, http3)
293    #[serde(rename = "type")]
294    pub egress_type: String,
295    /// Pipeline name that owns this egress (required)
296    pub pipeline: String,
297    /// Mode: 'default' allows all destinations, 'mesh' requires destination to match a mesh ingress
298    #[serde(default = "default_mode")]
299    pub mode: String,
300    /// Optional backend override. If omitted, the first backend in the pipeline is used.
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub backend: Option<String>,
303    /// Whether the egress is enabled
304    #[serde(default = "default_true")]
305    pub enabled: bool,
306    /// Optional description
307    #[serde(default)]
308    pub description: Option<String>,
309}
310
311fn default_true() -> bool {
312    true
313}
314
315fn default_mode() -> String {
316    "default".to_string()
317}
318
319fn default_auth_type() -> String {
320    "jwt".to_string()
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_store_config_request_with_id() {
329        let request = StoreConfigRequest {
330            config_type: "gateway".to_string(),
331            id: Some("01k8ek6h9aahhnrv3benret1nn".to_string()),
332            config: "[proxy]\nid = \"test\"\n".to_string(),
333        };
334
335        let json = serde_json::to_string(&request).unwrap();
336        assert!(json.contains("\"type\":\"gateway\""));
337        assert!(json.contains("\"id\":\"01k8ek6h9aahhnrv3benret1nn\""));
338        assert!(json.contains("\"config\":"));
339        assert!(json.contains("[proxy]"));
340
341        // Test deserialization
342        let deserialized: StoreConfigRequest = serde_json::from_str(&json).unwrap();
343        assert_eq!(deserialized.config_type, "gateway");
344        assert_eq!(
345            deserialized.id,
346            Some("01k8ek6h9aahhnrv3benret1nn".to_string())
347        );
348    }
349
350    #[test]
351    fn test_store_config_request_without_id() {
352        let request = StoreConfigRequest {
353            config_type: "pipeline".to_string(),
354            id: None,
355            config: "[pipeline]\nname = \"test\"\n".to_string(),
356        };
357
358        let json = serde_json::to_string(&request).unwrap();
359        assert!(json.contains("\"type\":\"pipeline\""));
360        assert!(json.contains("\"config\":"));
361        // Should not contain the id field when None
362        assert!(!json.contains("\"id\""));
363
364        // Test deserialization
365        let deserialized: StoreConfigRequest = serde_json::from_str(&json).unwrap();
366        assert_eq!(deserialized.config_type, "pipeline");
367        assert_eq!(deserialized.id, None);
368    }
369
370    #[test]
371    fn test_store_config_request_type_field_rename() {
372        // Test that the "type" field is correctly serialized despite the field being named config_type
373        let json = r#"{"type":"transform","config":"[transform]\nname = \"test\"\n"}"#;
374        let request: StoreConfigRequest = serde_json::from_str(json).unwrap();
375        assert_eq!(request.config_type, "transform");
376        assert_eq!(request.id, None);
377    }
378
379    #[test]
380    fn test_store_config_response() {
381        let json = r#"{
382            "success": true,
383            "message": "Gateway configuration updated successfully",
384            "data": {
385                "id": "01k9npa4tatmwddk66xxpcr2r0",
386                "type": "gateway",
387                "action": "updated"
388            }
389        }"#;
390
391        let response: StoreConfigResponse = serde_json::from_str(json).unwrap();
392        assert_eq!(response.success, true);
393        assert!(response.message.contains("updated successfully"));
394        assert_eq!(response.data.id, "01k9npa4tatmwddk66xxpcr2r0");
395        assert_eq!(response.data.model_type, "gateway");
396        assert_eq!(response.data.action, "updated");
397    }
398}