openrtc 1.0.11

OpenRTC: a Rust-first P2P runtime for device discovery, signaling, and iroh/QUIC networking.
Documentation
use anyhow::Result;
use async_trait::async_trait;
use futures::stream::BoxStream;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeviceCapabilities {
    pub can_host: bool,
    pub can_sync: bool,
    pub read_only: bool,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Device {
    pub app_tag: Option<String>,
    pub device_id: String,
    pub user_id: Option<String>,
    pub device_name: String,
    pub platform_type: Option<String>,
    pub capabilities: Option<DeviceCapabilities>,
    pub session_id: Option<String>,
    pub node_id: Option<String>,
    pub tag: Option<String>,
    pub kind: Option<String>,
    pub metadata: Option<String>,
    pub online: bool,
    pub ticket: Option<String>,
    pub last_seen_at: Option<serde_json::Value>,
    pub expires_at: Option<serde_json::Value>,
    pub created_at: Option<serde_json::Value>,
    pub updated_at: Option<serde_json::Value>,
    /// Device IDs that this peer has explicitly excluded from auto-connect.
    /// Set when the peer manually disconnects from a device; cleared on manual reconnect.
    /// All other peers check this before dialing in.
    #[serde(default)]
    pub excluded_peers: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct E2eeInfo {
    pub session_key_id: String,
    pub cipher_text: String,
    pub iv: String,
    pub salt: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignalingSession {
    pub connection_id: String,
    pub initiator: String,
    pub target: String,
    pub initiator_device_id: String,
    pub target_device_id: String,
    pub connection_type: Option<String>,
    pub offer: Option<serde_json::Value>,
    pub offer_e2ee: Option<E2eeInfo>,
    pub answer: Option<serde_json::Value>,
    pub answer_e2ee: Option<E2eeInfo>,
    #[serde(default)]
    pub ice_candidates: Vec<serde_json::Value>,
    pub initiator_node_id: Option<String>,
    pub target_node_id: Option<String>,
    pub initiator_endpoint_addr: Option<String>,
    pub target_endpoint_addr: Option<String>,
    pub intent: Option<String>,
    pub app_tag: Option<String>,
    pub created_at: Option<i64>,
    pub expires_at: Option<i64>,
    pub state: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum SessionEvent {
    Added {
        session: SignalingSession,
    },
    Modified {
        session: SignalingSession,
    },
    Removed {
        #[serde(rename = "sessionId")]
        session_id: String,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum DeviceEvent {
    Added {
        device: Device,
    },
    Modified {
        device: Device,
    },
    Removed {
        #[serde(rename = "deviceId")]
        device_id: String,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignalingEnvelope {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub app_tag: Option<String>,
    pub sender_id: String,
    pub target_id: String,
    pub payload: String,
    pub state: Option<String>,
    pub reply_payload: Option<String>,
    pub timestamp: i64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sender_user_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target_user_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expires_at: Option<i64>,
}

#[cfg(target_arch = "wasm32")]
pub trait SendSyncBound {}
#[cfg(target_arch = "wasm32")]
impl<T> SendSyncBound for T {}

#[cfg(not(target_arch = "wasm32"))]
pub trait SendSyncBound: Send + Sync {}
#[cfg(not(target_arch = "wasm32"))]
impl<T: Send + Sync> SendSyncBound for T {}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait SignalingBackend: SendSyncBound {
    async fn update_presence(
        &self,
        user_id: &str,
        local_node_id: &str,
        ticket_str: &str,
        is_online: bool,
        name: &str,
        ttl_ms: u64,
        metadata: Option<&str>,
    ) -> Result<()>;

    async fn set_offline(&self, user_id: &str, local_node_id: &str) -> Result<()>;

    /// Publish hot liveness/ticket state for a device through the configured
    /// provider-neutral coordination implementation.
    async fn update_live_presence(
        &self,
        _user_id: &str,
        _local_node_id: &str,
        _ticket_str: &str,
        _name: &str,
        _metadata: Option<&str>,
    ) -> Result<()> {
        #[cfg(target_arch = "wasm32")]
        return Ok(());

        #[cfg(not(target_arch = "wasm32"))]
        anyhow::bail!("native signaling backend does not implement managed live presence")
    }

    /// Mark the managed live-presence plane offline.
    async fn set_live_presence_offline(&self, _user_id: &str, _local_node_id: &str) -> Result<()> {
        #[cfg(target_arch = "wasm32")]
        return Ok(());

        #[cfg(not(target_arch = "wasm32"))]
        anyhow::bail!("native signaling backend does not implement managed offline presence")
    }

    async fn update_device(
        &self,
        user_id: &str,
        device_id: &str,
        device_name: Option<&str>,
        capabilities: Option<DeviceCapabilities>,
        metadata: Option<&str>,
    ) -> Result<()>;

    async fn delete_device(&self, user_id: &str, device_id: &str) -> Result<()>;

    /// Update the `excludedPeers` list on the local device's presence document.
    /// Called on disconnect (add `remote_device_id`) and reconnect (remove it).
    async fn set_excluded_peers(
        &self,
        user_id: &str,
        local_node_id: &str,
        excluded_peers: &[String],
    ) -> Result<()>;

    async fn search_devices(
        &self,
        user_id: &str,
        exclude_node_id: Option<&str>,
    ) -> Result<Vec<Device>>;

    async fn list_devices(
        &self,
        user_id: &str,
        exclude_node_id: Option<&str>,
    ) -> Result<Vec<Device>>;

    async fn send_message(
        &self,
        sender_id: &str,
        target_id: &str,
        payload: &str,
        state: Option<&str>,
        reply_payload: Option<&str>,
    ) -> Result<String>;

    async fn subscribe_devices(
        &self,
        user_id: &str,
    ) -> Result<BoxStream<'static, Result<Vec<DeviceEvent>>>>;

    async fn create_session(&self, session: SignalingSession) -> Result<()>;

    async fn update_session(&self, session_id: &str, update_data: serde_json::Value) -> Result<()>;

    async fn subscribe_sessions(
        &self,
        local_device_id: &str,
    ) -> Result<BoxStream<'static, Result<Vec<SessionEvent>>>>;
}

/// Fail-closed default for runtimes whose host has not supplied a managed
/// coordination provider.
///
/// Shipping OpenRTC builds no longer create a Firestore or RTDB backend from a
/// project id. Browser and Tauri products use the Cloudflare gateway adapter;
/// standalone Rust hosts must inject a provider or submit externally obtained
/// desired peers to the Rust lifecycle owner.
#[derive(Debug, Default)]
pub struct GatewayRequiredSignalingBackend;

impl GatewayRequiredSignalingBackend {
    fn unavailable<T>() -> Result<T> {
        anyhow::bail!(
            "managed coordination gateway is required; direct Firebase coordination has been removed"
        )
    }
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl SignalingBackend for GatewayRequiredSignalingBackend {
    async fn update_presence(
        &self,
        _user_id: &str,
        _local_node_id: &str,
        _ticket_str: &str,
        _is_online: bool,
        _name: &str,
        _ttl_ms: u64,
        _metadata: Option<&str>,
    ) -> Result<()> {
        Self::unavailable()
    }

    async fn set_offline(&self, _user_id: &str, _local_node_id: &str) -> Result<()> {
        Self::unavailable()
    }

    async fn update_live_presence(
        &self,
        _user_id: &str,
        _local_node_id: &str,
        _ticket_str: &str,
        _name: &str,
        _metadata: Option<&str>,
    ) -> Result<()> {
        Self::unavailable()
    }

    async fn set_live_presence_offline(&self, _user_id: &str, _local_node_id: &str) -> Result<()> {
        Self::unavailable()
    }

    async fn update_device(
        &self,
        _user_id: &str,
        _device_id: &str,
        _device_name: Option<&str>,
        _capabilities: Option<DeviceCapabilities>,
        _metadata: Option<&str>,
    ) -> Result<()> {
        Self::unavailable()
    }

    async fn delete_device(&self, _user_id: &str, _device_id: &str) -> Result<()> {
        Self::unavailable()
    }

    async fn set_excluded_peers(
        &self,
        _user_id: &str,
        _local_node_id: &str,
        _excluded_peers: &[String],
    ) -> Result<()> {
        Self::unavailable()
    }

    async fn search_devices(
        &self,
        _user_id: &str,
        _exclude_node_id: Option<&str>,
    ) -> Result<Vec<Device>> {
        Self::unavailable()
    }

    async fn list_devices(
        &self,
        _user_id: &str,
        _exclude_node_id: Option<&str>,
    ) -> Result<Vec<Device>> {
        Self::unavailable()
    }

    async fn send_message(
        &self,
        _sender_id: &str,
        _target_id: &str,
        _payload: &str,
        _state: Option<&str>,
        _reply_payload: Option<&str>,
    ) -> Result<String> {
        Self::unavailable()
    }

    async fn subscribe_devices(
        &self,
        _user_id: &str,
    ) -> Result<BoxStream<'static, Result<Vec<DeviceEvent>>>> {
        Self::unavailable()
    }

    async fn create_session(&self, _session: SignalingSession) -> Result<()> {
        Self::unavailable()
    }

    async fn update_session(
        &self,
        _session_id: &str,
        _update_data: serde_json::Value,
    ) -> Result<()> {
        Self::unavailable()
    }

    async fn subscribe_sessions(
        &self,
        _local_device_id: &str,
    ) -> Result<BoxStream<'static, Result<Vec<SessionEvent>>>> {
        Self::unavailable()
    }
}