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}