Skip to main content

ma_core/
endpoint.rs

1//! Endpoint trait.
2//!
3//! [`MaEndpoint`] defines the shared interface for all ma transport endpoints.
4//! The crate currently provides an internal iroh-backed transport implementation.
5
6use async_trait::async_trait;
7
8#[cfg(feature = "iroh")]
9use crate::error::Error;
10use crate::error::Result;
11use crate::inbox::Inbox;
12#[cfg(feature = "iroh")]
13use crate::ipfs::DidDocumentResolver;
14#[cfg(feature = "iroh")]
15use crate::transport::resolve_endpoint_for_protocol;
16#[cfg(feature = "iroh")]
17use crate::Document;
18use crate::Message;
19#[cfg(feature = "iroh")]
20use crate::Outbox;
21
22/// Default inbox capacity for services.
23pub const DEFAULT_INBOX_CAPACITY: usize = 256;
24
25/// Shared interface for ma transport endpoints.
26///
27/// Each implementation provides inbox/outbox
28/// messaging and advertises its registered services for DID documents.
29#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
30#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
31pub trait MaEndpoint: Send + Sync {
32    /// The endpoint's public identifier (hex string).
33    fn id(&self) -> String;
34
35    /// Register a service protocol and return an [`Inbox`] for receiving messages.
36    ///
37    /// Implementations should ensure the service is reachable for inbound delivery
38    /// once it has been registered, so callers do not need a second explicit
39    /// "listen" step in the common case.
40    fn service(&mut self, protocol: &str) -> Inbox<Message>;
41
42    /// Return service strings for all registered protocols.
43    ///
44    /// Each entry is suitable for inclusion in a DID document's `ma.services` array.
45    fn services(&self) -> Vec<String>;
46
47    /// Return service strings as a JSON array value.
48    fn services_json(&self) -> serde_json::Value {
49        serde_json::Value::Array(
50            self.services()
51                .into_iter()
52                .map(serde_json::Value::String)
53                .collect(),
54        )
55    }
56
57    /// Build a [`crate::MaExtension`] pre-populated with this endpoint's
58    /// service strings.
59    ///
60    /// Use this as the starting point when constructing the `ma:` field for a
61    /// DID document. Chain additional builder methods on the returned value
62    /// before passing it to [`crate::config::SecretBundle::build_document`] or
63    /// [`crate::Document::set_ma_extension`]:
64    ///
65    /// ```ignore
66    /// let ma = endpoint.ma_extension().kind("world");
67    /// let document = bundle.build_document(ma)?;
68    /// ```
69    fn ma_extension(&self) -> crate::doc::MaExtension {
70        crate::doc::MaExtension::new().services(self.services())
71    }
72
73    /// Send an explicitly unencrypted broadcast to a transport endpoint.
74    ///
75    /// Point-to-point messages must use [`Self::outbox`] so the recipient DID
76    /// document is available for envelope encryption.
77    async fn send_broadcast_to(
78        &self,
79        target: &str,
80        protocol: &str,
81        message: &Message,
82    ) -> Result<()>;
83
84    /// Gracefully shut down the endpoint, closing all cached connections.
85    async fn close(&mut self);
86
87    /// Open a transport-agnostic outbox to a remote DID and protocol.
88    ///
89    /// Resolves the DID document, checks `ma.services` for the requested
90    /// protocol, and delegates the actual transport connection to
91    /// [`Self::connect_outbox`]. Override this only for non-standard resolution.
92    #[cfg(feature = "iroh")]
93    async fn outbox(
94        &self,
95        resolver: &dyn DidDocumentResolver,
96        did: &str,
97        protocol: &str,
98    ) -> Result<Outbox> {
99        let doc = resolver.resolve(did).await?;
100
101        let services = doc
102            .ma
103            .as_ref()
104            .and_then(|ma| ma.get("services").ok().flatten())
105            .and_then(|services| serde_json::to_value(services).ok());
106
107        let endpoint_id =
108            resolve_endpoint_for_protocol(services.as_ref(), protocol).ok_or_else(|| {
109                Error::NoInboxTransport(format!("{did} has no service for {protocol}"))
110            })?;
111
112        self.connect_outbox(&doc, &endpoint_id, did, protocol).await
113    }
114
115    /// Open a transport-level outbox given a pre-resolved document and endpoint ID.
116    ///
117    /// Implementors use `doc` for transport-specific routing hints (e.g. relay URLs)
118    /// and `endpoint_id` as the peer address on their transport layer.
119    #[cfg(feature = "iroh")]
120    async fn connect_outbox(
121        &self,
122        doc: &Document,
123        endpoint_id: &str,
124        did: &str,
125        protocol: &str,
126    ) -> Result<Outbox>;
127}