Skip to main content

browsertap_shared/
protocol.rs

1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4// ─── Browser → Daemon messages ───────────────────────────────────────────────
5
6/// Messages sent from browser runtime to daemon via WebSocket.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(tag = "kind", rename_all = "camelCase")]
9pub enum BrowserMessage {
10    /// Browser registers a new session with the daemon.
11    Register {
12        token: String,
13        session_id: Uuid,
14        url: String,
15        title: String,
16        user_agent: String,
17        top_origin: String,
18    },
19    /// Periodic heartbeat to keep the session alive.
20    Heartbeat { session_id: Uuid },
21    /// Result of a command executed in the browser.
22    CommandResult {
23        session_id: Uuid,
24        command_id: String,
25        result: CommandResult,
26    },
27    /// Batch of console events captured from the page.
28    Console {
29        session_id: Uuid,
30        events: Vec<ConsoleEvent>,
31    },
32    /// Network events captured from the page.
33    Network {
34        session_id: Uuid,
35        events: Vec<NetworkEvent>,
36    },
37}
38
39// ─── Daemon → Browser messages ───────────────────────────────────────────────
40
41/// Messages sent from daemon to browser runtime via WebSocket.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(tag = "kind", rename_all = "camelCase")]
44pub enum DaemonMessage {
45    /// Session metadata after successful registration.
46    Metadata { session_id: Uuid, codename: String },
47    /// Command to execute in the browser context.
48    Command {
49        session_id: Uuid,
50        command: BrowserCommand,
51    },
52    /// Disconnect notification.
53    Disconnect { reason: String },
54    /// Error response.
55    Error { message: String },
56}
57
58// ─── Commands (CLI → Daemon → Browser) ───────────────────────────────────────
59
60/// Commands that can be sent to a browser session.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(tag = "type", rename_all = "camelCase")]
63pub enum BrowserCommand {
64    /// Execute JavaScript in the page context.
65    RunScript {
66        id: String,
67        code: String,
68        #[serde(default)]
69        capture_console: bool,
70    },
71    /// Take a screenshot of the page or a specific element.
72    Screenshot {
73        id: String,
74        #[serde(default)]
75        selector: Option<String>,
76        #[serde(default = "default_quality")]
77        quality: f32,
78        #[serde(default)]
79        hooks: Vec<ScreenshotHook>,
80    },
81    /// Click an element by CSS selector.
82    Click { id: String, selector: String },
83    /// Navigate to a URL.
84    Navigate { id: String, url: String },
85    /// Discover interactive selectors on the page.
86    DiscoverSelectors { id: String },
87}
88
89fn default_quality() -> f32 {
90    0.85
91}
92
93impl BrowserCommand {
94    pub fn id(&self) -> &str {
95        match self {
96            BrowserCommand::RunScript { id, .. }
97            | BrowserCommand::Screenshot { id, .. }
98            | BrowserCommand::Click { id, .. }
99            | BrowserCommand::Navigate { id, .. }
100            | BrowserCommand::DiscoverSelectors { id, .. } => id,
101        }
102    }
103}
104
105/// Pre-screenshot hooks to prepare the page.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(tag = "type", rename_all = "camelCase")]
108pub enum ScreenshotHook {
109    ScrollIntoView { selector: String },
110    WaitForSelector { selector: String, timeout_ms: u64 },
111    WaitForIdle { timeout_ms: u64 },
112    Wait { ms: u64 },
113    Script { code: String },
114}
115
116// ─── Command results ─────────────────────────────────────────────────────────
117
118/// Result of a command execution.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct CommandResult {
121    pub ok: bool,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub data: Option<serde_json::Value>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub error: Option<String>,
126    pub duration_ms: u64,
127}
128
129/// Screenshot-specific result data.
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct ScreenshotData {
132    pub mime_type: String,
133    pub base64: String,
134    pub width: u32,
135    pub height: u32,
136    pub renderer: String,
137}
138
139// ─── Telemetry events ────────────────────────────────────────────────────────
140
141/// Console event captured from the browser.
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct ConsoleEvent {
144    pub id: String,
145    pub timestamp: i64,
146    pub level: ConsoleLevel,
147    pub args: Vec<serde_json::Value>,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "lowercase")]
152pub enum ConsoleLevel {
153    Log,
154    Info,
155    Warn,
156    Error,
157    Debug,
158}
159
160/// Network event captured from the browser.
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct NetworkEvent {
163    pub id: String,
164    pub timestamp: i64,
165    pub method: String,
166    pub url: String,
167    pub status: Option<u16>,
168    pub duration_ms: Option<u64>,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub error: Option<String>,
171}
172
173// ─── REST API types ──────────────────────────────────────────────────────────
174
175/// POST /api/handshake - request body.
176#[derive(Debug, Serialize, Deserialize)]
177pub struct HandshakeRequest {
178    pub app_label: String,
179}
180
181/// POST /api/handshake - response body.
182#[derive(Debug, Serialize, Deserialize)]
183pub struct HandshakeResponse {
184    pub session_id: Uuid,
185    pub session_token: String,
186    pub socket_url: String,
187    pub expires_at: i64,
188}
189
190/// GET /api/sessions - single session info.
191#[derive(Debug, Serialize, Deserialize)]
192pub struct SessionInfo {
193    pub session_id: Uuid,
194    pub codename: String,
195    pub url: String,
196    pub title: String,
197    pub user_agent: String,
198    pub socket_state: SocketState,
199    pub connected_at: i64,
200    pub last_heartbeat: i64,
201    pub console_buffer_size: usize,
202    pub network_buffer_size: usize,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
206#[serde(rename_all = "lowercase")]
207pub enum SocketState {
208    Open,
209    Closed,
210}
211
212/// POST /api/sessions/{id}/command - request body.
213#[derive(Debug, Serialize, Deserialize)]
214pub struct CommandRequest {
215    pub command: BrowserCommand,
216    #[serde(default = "default_timeout")]
217    pub timeout_ms: u64,
218}
219
220fn default_timeout() -> u64 {
221    30_000
222}
223
224/// POST /api/sessions/{id}/command - response body.
225#[derive(Debug, Serialize, Deserialize)]
226pub struct CommandResponse {
227    pub result: CommandResult,
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn browser_message_serialization() {
236        let msg = BrowserMessage::Register {
237            token: "test-token".into(),
238            session_id: Uuid::new_v4(),
239            url: "http://localhost:3000".into(),
240            title: "Test Page".into(),
241            user_agent: "Mozilla/5.0".into(),
242            top_origin: "http://localhost:3000".into(),
243        };
244
245        let json = serde_json::to_string(&msg).unwrap();
246        assert!(json.contains("\"kind\":\"register\""));
247
248        let decoded: BrowserMessage = serde_json::from_str(&json).unwrap();
249        assert!(matches!(decoded, BrowserMessage::Register { .. }));
250    }
251
252    #[test]
253    fn command_serialization() {
254        let cmd = BrowserCommand::RunScript {
255            id: "cmd-1".into(),
256            code: "document.title".into(),
257            capture_console: true,
258        };
259
260        let json = serde_json::to_string(&cmd).unwrap();
261        assert!(json.contains("\"type\":\"runScript\""));
262    }
263}