Skip to main content

openrtc/
signaling.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use futures::stream::BoxStream;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct DeviceCapabilities {
9    pub can_host: bool,
10    pub can_sync: bool,
11    pub read_only: bool,
12}
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct Device {
17    pub app_tag: Option<String>,
18    pub device_id: String,
19    pub user_id: Option<String>,
20    pub device_name: String,
21    pub platform_type: Option<String>,
22    pub capabilities: Option<DeviceCapabilities>,
23    pub session_id: Option<String>,
24    pub node_id: Option<String>,
25    pub tag: Option<String>,
26    pub kind: Option<String>,
27    pub metadata: Option<String>,
28    pub online: bool,
29    pub ticket: Option<String>,
30    pub last_seen_at: Option<serde_json::Value>,
31    pub expires_at: Option<serde_json::Value>,
32    pub created_at: Option<serde_json::Value>,
33    pub updated_at: Option<serde_json::Value>,
34    /// Device IDs that this peer has explicitly excluded from auto-connect.
35    /// Set when the peer manually disconnects from a device; cleared on manual reconnect.
36    /// All other peers check this before dialing in.
37    #[serde(default)]
38    pub excluded_peers: Vec<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct E2eeInfo {
44    pub session_key_id: String,
45    pub cipher_text: String,
46    pub iv: String,
47    pub salt: String,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct SignalingSession {
53    pub connection_id: String,
54    pub initiator: String,
55    pub target: String,
56    pub initiator_device_id: String,
57    pub target_device_id: String,
58    pub connection_type: Option<String>,
59    pub offer: Option<serde_json::Value>,
60    pub offer_e2ee: Option<E2eeInfo>,
61    pub answer: Option<serde_json::Value>,
62    pub answer_e2ee: Option<E2eeInfo>,
63    #[serde(default)]
64    pub ice_candidates: Vec<serde_json::Value>,
65    pub initiator_node_id: Option<String>,
66    pub target_node_id: Option<String>,
67    pub initiator_endpoint_addr: Option<String>,
68    pub target_endpoint_addr: Option<String>,
69    pub intent: Option<String>,
70    pub app_tag: Option<String>,
71    pub created_at: Option<i64>,
72    pub expires_at: Option<i64>,
73    pub state: String,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(tag = "type", rename_all = "camelCase")]
78pub enum SessionEvent {
79    Added {
80        session: SignalingSession,
81    },
82    Modified {
83        session: SignalingSession,
84    },
85    Removed {
86        #[serde(rename = "sessionId")]
87        session_id: String,
88    },
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(tag = "type", rename_all = "camelCase")]
93pub enum DeviceEvent {
94    Added {
95        device: Device,
96    },
97    Modified {
98        device: Device,
99    },
100    Removed {
101        #[serde(rename = "deviceId")]
102        device_id: String,
103    },
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct SignalingEnvelope {
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub app_tag: Option<String>,
111    pub sender_id: String,
112    pub target_id: String,
113    pub payload: String,
114    pub state: Option<String>,
115    pub reply_payload: Option<String>,
116    pub timestamp: i64,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub sender_user_id: Option<String>,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub target_user_id: Option<String>,
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub expires_at: Option<i64>,
123}
124
125#[cfg(target_arch = "wasm32")]
126pub trait SendSyncBound {}
127#[cfg(target_arch = "wasm32")]
128impl<T> SendSyncBound for T {}
129
130#[cfg(not(target_arch = "wasm32"))]
131pub trait SendSyncBound: Send + Sync {}
132#[cfg(not(target_arch = "wasm32"))]
133impl<T: Send + Sync> SendSyncBound for T {}
134
135#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
136#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
137pub trait SignalingBackend: SendSyncBound {
138    async fn update_presence(
139        &self,
140        user_id: &str,
141        local_node_id: &str,
142        ticket_str: &str,
143        is_online: bool,
144        name: &str,
145        ttl_ms: u64,
146        metadata: Option<&str>,
147    ) -> Result<()>;
148
149    async fn set_offline(&self, user_id: &str, local_node_id: &str) -> Result<()>;
150
151    /// Publish hot liveness/ticket state for a device through the configured
152    /// provider-neutral coordination implementation.
153    async fn update_live_presence(
154        &self,
155        _user_id: &str,
156        _local_node_id: &str,
157        _ticket_str: &str,
158        _name: &str,
159        _metadata: Option<&str>,
160    ) -> Result<()> {
161        #[cfg(target_arch = "wasm32")]
162        return Ok(());
163
164        #[cfg(not(target_arch = "wasm32"))]
165        anyhow::bail!("native signaling backend does not implement managed live presence")
166    }
167
168    /// Mark the managed live-presence plane offline.
169    async fn set_live_presence_offline(&self, _user_id: &str, _local_node_id: &str) -> Result<()> {
170        #[cfg(target_arch = "wasm32")]
171        return Ok(());
172
173        #[cfg(not(target_arch = "wasm32"))]
174        anyhow::bail!("native signaling backend does not implement managed offline presence")
175    }
176
177    async fn update_device(
178        &self,
179        user_id: &str,
180        device_id: &str,
181        device_name: Option<&str>,
182        capabilities: Option<DeviceCapabilities>,
183        metadata: Option<&str>,
184    ) -> Result<()>;
185
186    async fn delete_device(&self, user_id: &str, device_id: &str) -> Result<()>;
187
188    /// Update the `excludedPeers` list on the local device's presence document.
189    /// Called on disconnect (add `remote_device_id`) and reconnect (remove it).
190    async fn set_excluded_peers(
191        &self,
192        user_id: &str,
193        local_node_id: &str,
194        excluded_peers: &[String],
195    ) -> Result<()>;
196
197    async fn search_devices(
198        &self,
199        user_id: &str,
200        exclude_node_id: Option<&str>,
201    ) -> Result<Vec<Device>>;
202
203    async fn list_devices(
204        &self,
205        user_id: &str,
206        exclude_node_id: Option<&str>,
207    ) -> Result<Vec<Device>>;
208
209    async fn send_message(
210        &self,
211        sender_id: &str,
212        target_id: &str,
213        payload: &str,
214        state: Option<&str>,
215        reply_payload: Option<&str>,
216    ) -> Result<String>;
217
218    async fn subscribe_devices(
219        &self,
220        user_id: &str,
221    ) -> Result<BoxStream<'static, Result<Vec<DeviceEvent>>>>;
222
223    async fn create_session(&self, session: SignalingSession) -> Result<()>;
224
225    async fn update_session(&self, session_id: &str, update_data: serde_json::Value) -> Result<()>;
226
227    async fn subscribe_sessions(
228        &self,
229        local_device_id: &str,
230    ) -> Result<BoxStream<'static, Result<Vec<SessionEvent>>>>;
231}
232
233/// Fail-closed default for runtimes whose host has not supplied a managed
234/// coordination provider.
235///
236/// Shipping OpenRTC builds no longer create a Firestore or RTDB backend from a
237/// project id. Browser and Tauri products use the Cloudflare gateway adapter;
238/// standalone Rust hosts must inject a provider or submit externally obtained
239/// desired peers to the Rust lifecycle owner.
240#[derive(Debug, Default)]
241pub struct GatewayRequiredSignalingBackend;
242
243impl GatewayRequiredSignalingBackend {
244    fn unavailable<T>() -> Result<T> {
245        anyhow::bail!(
246            "managed coordination gateway is required; direct Firebase coordination has been removed"
247        )
248    }
249}
250
251#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
252#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
253impl SignalingBackend for GatewayRequiredSignalingBackend {
254    async fn update_presence(
255        &self,
256        _user_id: &str,
257        _local_node_id: &str,
258        _ticket_str: &str,
259        _is_online: bool,
260        _name: &str,
261        _ttl_ms: u64,
262        _metadata: Option<&str>,
263    ) -> Result<()> {
264        Self::unavailable()
265    }
266
267    async fn set_offline(&self, _user_id: &str, _local_node_id: &str) -> Result<()> {
268        Self::unavailable()
269    }
270
271    async fn update_live_presence(
272        &self,
273        _user_id: &str,
274        _local_node_id: &str,
275        _ticket_str: &str,
276        _name: &str,
277        _metadata: Option<&str>,
278    ) -> Result<()> {
279        Self::unavailable()
280    }
281
282    async fn set_live_presence_offline(&self, _user_id: &str, _local_node_id: &str) -> Result<()> {
283        Self::unavailable()
284    }
285
286    async fn update_device(
287        &self,
288        _user_id: &str,
289        _device_id: &str,
290        _device_name: Option<&str>,
291        _capabilities: Option<DeviceCapabilities>,
292        _metadata: Option<&str>,
293    ) -> Result<()> {
294        Self::unavailable()
295    }
296
297    async fn delete_device(&self, _user_id: &str, _device_id: &str) -> Result<()> {
298        Self::unavailable()
299    }
300
301    async fn set_excluded_peers(
302        &self,
303        _user_id: &str,
304        _local_node_id: &str,
305        _excluded_peers: &[String],
306    ) -> Result<()> {
307        Self::unavailable()
308    }
309
310    async fn search_devices(
311        &self,
312        _user_id: &str,
313        _exclude_node_id: Option<&str>,
314    ) -> Result<Vec<Device>> {
315        Self::unavailable()
316    }
317
318    async fn list_devices(
319        &self,
320        _user_id: &str,
321        _exclude_node_id: Option<&str>,
322    ) -> Result<Vec<Device>> {
323        Self::unavailable()
324    }
325
326    async fn send_message(
327        &self,
328        _sender_id: &str,
329        _target_id: &str,
330        _payload: &str,
331        _state: Option<&str>,
332        _reply_payload: Option<&str>,
333    ) -> Result<String> {
334        Self::unavailable()
335    }
336
337    async fn subscribe_devices(
338        &self,
339        _user_id: &str,
340    ) -> Result<BoxStream<'static, Result<Vec<DeviceEvent>>>> {
341        Self::unavailable()
342    }
343
344    async fn create_session(&self, _session: SignalingSession) -> Result<()> {
345        Self::unavailable()
346    }
347
348    async fn update_session(
349        &self,
350        _session_id: &str,
351        _update_data: serde_json::Value,
352    ) -> Result<()> {
353        Self::unavailable()
354    }
355
356    async fn subscribe_sessions(
357        &self,
358        _local_device_id: &str,
359    ) -> Result<BoxStream<'static, Result<Vec<SessionEvent>>>> {
360        Self::unavailable()
361    }
362}