ping-openmls-sdk-core 0.6.3

Platform-agnostic OpenMLS-based messaging engine
Documentation
//! Transport trait — the SDK never opens a socket. Hosts implement this against their backend.

use crate::{
    conversation::ConversationId, device::DeviceId, identity::UserId, message::MessageEnvelope,
    sync::SyncCursor, Result,
};
use std::fmt::Debug;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

// See `storage.rs` — same reason: `JsFuture` is `!Send` on wasm32. The native bound is kept so
// UniFFI's tokio multi-thread runtime can schedule the future across worker threads.
#[cfg(not(target_arch = "wasm32"))]
pub type TransportFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'a>>;
#[cfg(target_arch = "wasm32")]
pub type TransportFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + 'a>>;

/// Server returns events strictly after the given cursor, sorted by (epoch ASC, server_seq ASC).
/// May return at-most-`limit` envelopes; caller pages.
pub trait Transport: Send + Sync + Debug {
    /// Publish a single envelope. Servers may batch; callers should not depend on it.
    fn send<'a>(&'a self, envelope: MessageEnvelope) -> TransportFuture<'a, ()>;

    /// Pull events for one conversation since `cursor`. Empty list = caught up.
    fn fetch_since<'a>(
        &'a self,
        conversation_id: ConversationId,
        cursor: SyncCursor,
        limit: u32,
    ) -> TransportFuture<'a, Vec<MessageEnvelope>>;

    /// Subscribe to live events. Returned subscription's drop cancels.
    fn subscribe<'a>(
        &'a self,
        callback: Arc<dyn Fn(MessageEnvelope) + Send + Sync>,
    ) -> TransportFuture<'a, TransportSubscription>;

    /// Look up published KeyPackages for a user's currently-active devices.
    fn discover_devices<'a>(
        &'a self,
        user_id: UserId,
    ) -> TransportFuture<'a, Vec<DiscoveredDevice>>;

    /// Notify the auth-layer server that `device_id` has been revoked
    /// (`POST /v1/devices/{device_id}/revoke`). Called from
    /// [`crate::MessagingClient::revoke_device`] AFTER each per-conversation
    /// MLS Commit has been sent — i.e. peers learn about the removal via
    /// MLS first, then the auth layer cleans up KeyPackages, refresh
    /// tokens, and any future envelope rejection.
    ///
    /// Defaults to `Ok(())` so existing host transports (web, iOS,
    /// Android) keep building while they wire the new endpoint up. Hosts
    /// SHOULD override to call the backend; the SDK's MLS-side work is
    /// sufficient for in-group hygiene, but auth-layer state (KeyPackage
    /// pool, sessions on `auth.devices`) stays stale until this method
    /// hits the server.
    ///
    /// Idempotent at the server: re-calling on an already-revoked device
    /// is a no-op (the server returns success on the second call).
    fn revoke_device_remote<'a>(&'a self, _device_id: DeviceId) -> TransportFuture<'a, ()> {
        Box::pin(async move { Ok(()) })
    }

    /// Prime the host transport with the recipient device id(s) the NEXT
    /// `Welcome` envelope on `conversation_id` should address. The web
    /// host's MLS POST `/v1/messages` routes Welcomes via a
    /// `?recipient=<device_id[,...]>` query param the BE needs to fan
    /// the inbox row to the joiner — without it the BE rejects (400)
    /// or stores the Welcome with no inbox route. Called by
    /// [`crate::MessagingClient::admit_device_to_chats`] before each
    /// per-chat `Welcome` send so hosts don't have to coordinate the
    /// priming themselves.
    ///
    /// Defaults to `Ok(())` so non-web hosts (iOS/Android FFI bindings)
    /// keep building without implementing this — they can route
    /// Welcomes by other means (e.g. an envelope-side recipient field).
    fn set_next_welcome_recipients<'a>(
        &'a self,
        _conversation_id: ConversationId,
        _recipient_device_ids: Vec<DeviceId>,
    ) -> TransportFuture<'a, ()> {
        Box::pin(async move { Ok(()) })
    }
}

#[derive(Debug, Clone)]
pub struct DiscoveredDevice {
    pub device_id: DeviceId,
    pub key_package: Vec<u8>, // serialized OpenMLS KeyPackage
}

/// Cancels its subscription on drop.
pub struct TransportSubscription {
    pub(crate) cancel: Box<dyn FnOnce() + Send>,
}

impl std::fmt::Debug for TransportSubscription {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("TransportSubscription")
    }
}

impl TransportSubscription {
    pub fn new(cancel: impl FnOnce() + Send + 'static) -> Self {
        Self {
            cancel: Box::new(cancel),
        }
    }
}

impl Drop for TransportSubscription {
    fn drop(&mut self) {
        // Replace with a no-op closure so we can take ownership and call once.
        let cancel = std::mem::replace(&mut self.cancel, Box::new(|| ()));
        cancel();
    }
}