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