Skip to main content

fips_core/endpoint/
builder.rs

1use super::*;
2
3/// Builder for an embedded FIPS endpoint.
4#[derive(Debug, Clone)]
5pub struct FipsEndpointBuilder {
6    config: Config,
7    identity_nsec: Option<String>,
8    discovery_scope: Option<String>,
9    local_rendezvous: bool,
10    local_instance_roles: Vec<crate::discovery::local::LocalInstanceCapability>,
11    disable_system_networking: bool,
12    packet_channel_capacity: usize,
13    #[cfg(test)]
14    test_nostr_discovery: Option<TestNostrDiscovery>,
15    #[cfg(feature = "host-ble-transport")]
16    host_ble: Option<HostBleAttachment>,
17    #[cfg(feature = "host-ble-transport")]
18    host_ble_config: Option<crate::config::BleConfig>,
19}
20
21#[cfg(test)]
22#[derive(Clone)]
23struct TestNostrDiscovery(Arc<crate::discovery::nostr::NostrDiscovery>);
24
25#[cfg(test)]
26impl std::fmt::Debug for TestNostrDiscovery {
27    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        formatter.write_str("TestNostrDiscovery(..)")
29    }
30}
31
32#[cfg(feature = "host-ble-transport")]
33#[derive(Clone)]
34struct HostBleAttachment(Arc<std::sync::Mutex<Option<crate::transport::ble::host::HostBleIo>>>);
35
36#[cfg(feature = "host-ble-transport")]
37impl std::fmt::Debug for HostBleAttachment {
38    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        formatter.write_str("HostBleAttachment(..)")
40    }
41}
42
43const DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY: usize = 4096;
44
45impl Default for FipsEndpointBuilder {
46    fn default() -> Self {
47        Self {
48            config: Config::new(),
49            identity_nsec: None,
50            discovery_scope: None,
51            local_rendezvous: false,
52            local_instance_roles: Vec::new(),
53            disable_system_networking: true,
54            packet_channel_capacity: DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY,
55            #[cfg(test)]
56            test_nostr_discovery: None,
57            #[cfg(feature = "host-ble-transport")]
58            host_ble: None,
59            #[cfg(feature = "host-ble-transport")]
60            host_ble_config: None,
61        }
62    }
63}
64
65impl FipsEndpointBuilder {
66    /// Start from an explicit FIPS config.
67    pub fn config(mut self, config: Config) -> Self {
68        self.config = config;
69        self
70    }
71
72    /// Use an `nsec` or hex secret for the endpoint identity.
73    pub fn identity_nsec(mut self, nsec: impl Into<String>) -> Self {
74        self.identity_nsec = Some(nsec.into());
75        self
76    }
77
78    /// Set an application-level discovery scope.
79    ///
80    /// When the builder owns the default empty connectivity config, this also
81    /// enables scoped Nostr discovery, open same-scope peer discovery, local
82    /// LAN candidates, host-wide loopback rendezvous, and a UDP NAT advert.
83    /// If an explicit transport or Nostr config was supplied, the explicit
84    /// config is left in control and the scope is retained as endpoint
85    /// metadata; call [`Self::local_rendezvous`] to opt that endpoint into
86    /// same-host composition.
87    pub fn discovery_scope(mut self, scope: impl Into<String>) -> Self {
88        self.discovery_scope = Some(scope.into());
89        self
90    }
91
92    /// Enable host-wide authenticated loopback composition.
93    pub fn local_rendezvous(mut self) -> Self {
94        self.local_rendezvous = true;
95        self
96    }
97
98    /// Advertise a portless same-host role, such as `nostr.pubsub/1`.
99    /// Empty, oversized, and excess names are ignored.
100    pub fn local_role(mut self, name: impl Into<String>, priority: i16) -> Self {
101        let name = name.into().trim().to_string();
102        if crate::discovery::local_udp::local_capability_name_is_valid(&name)
103            && self.local_instance_roles.len()
104                < crate::discovery::local_udp::LOCAL_CAPABILITY_MAX_COUNT
105        {
106            self.local_instance_roles.push(
107                crate::discovery::local::LocalInstanceCapability::role(name)
108                    .with_priority(priority),
109            );
110        }
111        self
112    }
113
114    /// Disable FIPS-owned TUN and DNS system integration.
115    pub fn without_system_tun(mut self) -> Self {
116        self.disable_system_networking = true;
117        self
118    }
119
120    /// Set the app packet/data channel capacity.
121    pub fn packet_channel_capacity(mut self, capacity: usize) -> Self {
122        self.packet_channel_capacity = capacity.max(1);
123        self
124    }
125
126    /// Attach one platform-command BLE adapter to this endpoint.
127    ///
128    /// Cloned builders share a single-use attachment; only the first bind can
129    /// consume it. The platform adapter must be pumping commands before bind.
130    #[cfg(feature = "host-ble-transport")]
131    pub fn host_ble(
132        mut self,
133        io: crate::transport::ble::host::HostBleIo,
134        config: crate::config::BleConfig,
135    ) -> Self {
136        self.host_ble = Some(HostBleAttachment(Arc::new(std::sync::Mutex::new(Some(io)))));
137        self.host_ble_config = Some(config);
138        self
139    }
140
141    pub(super) fn prepared_config(&self) -> Config {
142        let mut config = self.config.clone();
143        if let Some(nsec) = &self.identity_nsec {
144            config.node.identity = IdentityConfig {
145                nsec: Some(nsec.clone()),
146                persistent: false,
147            };
148        }
149        if self.disable_system_networking {
150            config.tun.enabled = false;
151            config.dns.enabled = false;
152            config.node.system_files_enabled = false;
153        }
154        #[cfg(feature = "host-ble-transport")]
155        if let Some(ble_config) = &self.host_ble_config {
156            // Explicit host BLE is connectivity in its own right. Install it
157            // before applying scoped defaults so a BLE-only mobile endpoint
158            // does not silently acquire UDP/Nostr and their relay requirements.
159            config.transports.ble = crate::config::TransportInstances::Single(ble_config.clone());
160        }
161        if self.local_rendezvous {
162            config.node.discovery.local.enabled = true;
163        }
164        if let Some(scope) = self.discovery_scope.as_deref() {
165            if config
166                .node
167                .discovery
168                .lan
169                .scope
170                .as_deref()
171                .is_none_or(|scope| scope.trim().is_empty())
172            {
173                config.node.discovery.lan.scope = Some(scope.to_string());
174            }
175            apply_default_scoped_discovery(&mut config, scope);
176        }
177        config
178    }
179
180    /// Bind and start the embedded endpoint.
181    pub async fn bind(self) -> Result<FipsEndpoint, FipsEndpointError> {
182        self.bind_inner(None).await
183    }
184
185    /// Bind with a bounded receiver for direct dataplane endpoint packet runs.
186    pub async fn bind_with_direct_receiver(
187        self,
188    ) -> Result<(FipsEndpoint, FipsEndpointDirectReceiver), FipsEndpointError> {
189        let (sink, receiver) = FipsEndpointDirectReceiver::channel();
190        let endpoint = self.bind_with_direct_sink(sink).await?;
191        Ok((endpoint, receiver))
192    }
193
194    /// Bind and start the endpoint with a direct dataplane endpoint-data sink.
195    ///
196    /// Decrypted dataplane endpoint output is delivered to `sink` synchronously from
197    /// the dataplane output path. Generic endpoint events, including loopback sends
198    /// and non-dataplane delivery, continue to use the regular receive queue.
199    pub async fn bind_with_direct_sink<S>(self, sink: S) -> Result<FipsEndpoint, FipsEndpointError>
200    where
201        S: FipsEndpointDirectSink,
202    {
203        self.bind_inner(Some(EndpointDirectSink::new(sink))).await
204    }
205
206    async fn bind_inner(
207        self,
208        direct_sink: Option<EndpointDirectSink>,
209    ) -> Result<FipsEndpoint, FipsEndpointError> {
210        let config = self.prepared_config();
211        #[cfg(test)]
212        let test_nostr_discovery = self
213            .test_nostr_discovery
214            .as_ref()
215            .map(|value| value.0.clone());
216
217        let mut node = Node::new(config)?;
218        node.set_local_instance_roles(self.local_instance_roles);
219        #[cfg(test)]
220        if let Some(discovery) = test_nostr_discovery {
221            node.set_nostr_discovery_for_test(discovery);
222        }
223        #[cfg(feature = "host-ble-transport")]
224        if let Some(attachment) = &self.host_ble {
225            let io = attachment
226                .0
227                .lock()
228                .unwrap_or_else(|error| error.into_inner())
229                .take()
230                .ok_or(FipsEndpointError::HostBleAdapterConsumed)?;
231            node.set_host_ble_io(io);
232        }
233        let identity = PeerIdentity::from_pubkey_full(node.identity().pubkey_full());
234        let npub = identity.npub();
235        let node_addr = *identity.node_addr();
236        let address = *identity.address();
237        let packet_io = node.attach_external_packet_io(self.packet_channel_capacity)?;
238        let endpoint_data_io = match direct_sink {
239            Some(sink) => {
240                node.attach_endpoint_data_io_with_direct_sink(self.packet_channel_capacity, sink)?
241            }
242            None => node.attach_endpoint_data_io(self.packet_channel_capacity)?,
243        };
244        node.start().await?;
245        let local_capability_directory = node.local_capability_directory();
246
247        let (shutdown_tx, shutdown_rx) = oneshot::channel();
248        let task = spawn_node_task(node, shutdown_rx);
249        let endpoint_control_tx = endpoint_data_io.control_tx;
250        let endpoint_data_batches = endpoint_data_io.data_batch_tx;
251        let inbound_service_tx = endpoint_data_io.service_event_tx;
252
253        Ok(FipsEndpoint {
254            identity,
255            npub,
256            node_addr,
257            address,
258            discovery_scope: self.discovery_scope,
259            local_capability_directory,
260            outbound_packets: packet_io.outbound_tx,
261            delivered_packets: Arc::new(Mutex::new(packet_io.inbound_rx)),
262            endpoint_control_tx,
263            endpoint_data_batches,
264            inbound_endpoint_tx: endpoint_data_io.event_tx,
265            inbound_endpoint_rx: Arc::new(Mutex::new(EndpointReceiveState::new(
266                endpoint_data_io.event_rx,
267            ))),
268            inbound_service_tx,
269            inbound_service_rx: Arc::new(Mutex::new(ServiceReceiveState::new(
270                endpoint_data_io.service_event_rx,
271            ))),
272            registered_services: Arc::new(StdMutex::new(HashMap::new())),
273            service_channel_capacity: self.packet_channel_capacity,
274            shutdown_tx: std::sync::Mutex::new(Some(shutdown_tx)),
275            task: std::sync::Mutex::new(Some(task)),
276        })
277    }
278}
279
280#[cfg(test)]
281impl FipsEndpointBuilder {
282    pub(super) async fn bind_with_nostr_discovery_for_test(
283        mut self,
284        discovery: Arc<crate::discovery::nostr::NostrDiscovery>,
285    ) -> Result<FipsEndpoint, FipsEndpointError> {
286        self.test_nostr_discovery = Some(TestNostrDiscovery(discovery));
287        self.bind().await
288    }
289}