Skip to main content

truffle_core/
node.rs

1//! Node API — the single public entry point for all truffle functionality.
2//!
3//! The [`Node`] struct wires together Layers 3-6 and exposes a clean ~12-method
4//! API that Layer 7 applications consume. Applications should **never** import
5//! from lower layers directly; everything they need is accessible through `Node`.
6//!
7//! # Quick start
8//!
9//! ```ignore
10//! use truffle_core::Node;
11//!
12//! let node = Node::builder()
13//!     .name("my-app")
14//!     .sidecar_path("/usr/local/bin/truffle-sidecar")
15//!     .build()
16//!     .await?;
17//!
18//! // Discover peers (Layer 3 — no transport needed)
19//! let peers = node.peers().await;
20//!
21//! // Send a namespaced message (Layer 6 envelope over Layer 4 WS)
22//! node.send(&peers[0].id, "chat", b"hello!").await?;
23//!
24//! // Subscribe to a namespace
25//! let mut rx = node.subscribe("chat");
26//! let msg = rx.recv().await?;
27//!
28//! // Open a raw TCP stream (Layer 4 direct)
29//! let stream = node.open_tcp(&peers[0].id, 8080).await?;
30//! ```
31
32use std::collections::HashMap;
33use std::net::IpAddr;
34use std::path::{Path, PathBuf};
35use std::sync::Arc;
36// `std::sync::RwLock` is aliased to `StdRwLock` so the namespace_filters
37// lock — which is held only briefly for pure HashMap ops with no `.await`
38// points underneath — can stay sync. Keeping it sync lets
39// `Node::subscribe` remain non-async (required by the NAPI bridge, which
40// is called from Node.js's sync main thread with no tokio runtime in
41// scope) without the `try_write`-panic footgun that the previous
42// `tokio::sync::RwLock` implementation suffered from (see RFC 017 fix K).
43use std::sync::RwLock as StdRwLock;
44
45use tokio::net::TcpStream;
46use tokio::sync::broadcast;
47// The tokio RwLock is only used by the in-file test `MockNetworkProvider`
48// (see `#[cfg(test)] mod tests`); production code now uses the std
49// RwLock alias above for `namespace_filters`.
50#[cfg(test)]
51use tokio::sync::RwLock;
52
53use crate::envelope::codec::{EnvelopeCodec, JsonCodec};
54use crate::envelope::{Envelope, EnvelopeError};
55use crate::file_transfer::{self, FileTransferState};
56use crate::identity::{self, AppId, DeviceId, DeviceName};
57use crate::network::tailscale::{TailscaleConfig, TailscaleProvider};
58use crate::network::{DialOpts, HealthInfo, NetworkProvider, NodeIdentity, PingResult};
59use crate::session::{PeerEvent, PeerRegistry, PeerState};
60use crate::transport::quic::{QuicConnection, QuicListener};
61use crate::transport::websocket::WebSocketTransport;
62use crate::transport::{DatagramSocket, RawListener, WsConfig};
63
64// ---------------------------------------------------------------------------
65// NamespacedMessage — public message type for subscribers
66// ---------------------------------------------------------------------------
67
68/// A message received on a specific namespace.
69///
70/// This is the public type that [`Node::subscribe`] delivers to application
71/// code. It contains the deserialized envelope fields plus the sender's peer ID.
72#[derive(Debug, Clone)]
73pub struct NamespacedMessage {
74    /// Stable node ID of the sender.
75    pub from: String,
76    /// Namespace the message was sent on.
77    pub namespace: String,
78    /// Application-defined message type within the namespace.
79    pub msg_type: String,
80    /// Opaque JSON payload.
81    pub payload: serde_json::Value,
82    /// Millisecond Unix timestamp from the sender, if set.
83    pub timestamp: Option<u64>,
84}
85
86// ---------------------------------------------------------------------------
87// Peer — simplified view for application code
88// ---------------------------------------------------------------------------
89
90/// A peer as seen by application code.
91///
92/// This is a simplified projection of the internal [`PeerState`] that hides
93/// session-layer internals. Applications use this to display peer lists and
94/// resolve peer IDs for `send()` / `open_tcp()`.
95///
96/// RFC 017 introduced `device_id` and `device_name` derived from the hello
97/// envelope — these are populated once the WebSocket link comes up. Before
98/// that, they fall back to the legacy Tailscale ID / hostname pair so
99/// application code has something to display even for not-yet-connected
100/// peers.
101#[derive(Debug, Clone)]
102pub struct Peer {
103    /// Legacy per-peer ID. Kept for back-compat with call sites that
104    /// destructure `peer.id`; new code should prefer [`device_id`](Self::device_id)
105    /// directly. Equal to `device_id` once the hello has landed; equal to
106    /// the Tailscale stable ID beforehand.
107    pub id: String,
108    /// Legacy name (the Layer 3 Tailscale hostname — the *slug*). New code
109    /// should prefer [`device_name`](Self::device_name).
110    pub name: String,
111    /// Stable per-device ULID (RFC 017 §5.4) from the hello envelope.
112    /// Falls back to the Tailscale stable ID until the hello handshake
113    /// completes.
114    pub device_id: String,
115    /// Human-readable device name from the hello envelope (original
116    /// Unicode form, NOT the slug). Falls back to the hostname until the
117    /// hello completes.
118    pub device_name: String,
119    /// Tailscale stable node ID — escape hatch for diagnostics and the
120    /// transport routing key.
121    pub tailscale_id: String,
122    /// Network IP address.
123    pub ip: IpAddr,
124    /// Whether the peer is online (from Layer 3).
125    pub online: bool,
126    /// Whether there is an active WebSocket connection.
127    pub ws_connected: bool,
128    /// Connection type description (e.g., `"direct"` or `"relay:ord"`).
129    pub connection_type: String,
130    /// Operating system, if known. Prefers the hello envelope's value
131    /// and falls back to Layer 3.
132    pub os: Option<String>,
133    /// Last time the peer was seen online (RFC 3339 string).
134    pub last_seen: Option<String>,
135}
136
137impl From<PeerState> for Peer {
138    fn from(s: PeerState) -> Self {
139        let (device_id, device_name, os) = match s.identity.as_ref() {
140            Some(identity) => (
141                identity.device_id.clone(),
142                identity.device_name.clone(),
143                Some(identity.os.clone()),
144            ),
145            None => (s.id.clone(), s.name.clone(), s.os.clone()),
146        };
147        let legacy_id = s
148            .identity
149            .as_ref()
150            .map(|i| i.device_id.clone())
151            .unwrap_or_else(|| s.id.clone());
152        Self {
153            id: legacy_id,
154            name: s.name,
155            device_id,
156            device_name,
157            tailscale_id: s.id,
158            ip: s.ip,
159            online: s.online,
160            ws_connected: s.ws_connected,
161            connection_type: s.connection_type,
162            os,
163            last_seen: s.last_seen,
164        }
165    }
166}
167
168// ---------------------------------------------------------------------------
169// NodeError
170// ---------------------------------------------------------------------------
171
172/// Errors from the Node API.
173#[derive(Debug, thiserror::Error)]
174pub enum NodeError {
175    /// The requested peer is not known.
176    #[error("peer not found: {0}")]
177    PeerNotFound(String),
178
179    /// Failed to establish a connection.
180    #[error("connection failed: {0}")]
181    ConnectionFailed(String),
182
183    /// Failed to send a message.
184    #[error("send failed: {0}")]
185    SendFailed(String),
186
187    /// Envelope encoding/decoding error.
188    #[error("envelope error: {0}")]
189    Envelope(#[from] EnvelopeError),
190
191    /// Session layer error.
192    #[error("session error: {0}")]
193    Session(#[from] crate::session::SessionError),
194
195    /// Network layer error.
196    #[error("network error: {0}")]
197    Network(#[from] crate::network::NetworkError),
198
199    /// Transport layer error.
200    #[error("transport error: {0}")]
201    Transport(#[from] crate::transport::TransportError),
202
203    /// The requested feature is not yet implemented.
204    #[error("not implemented: {0}")]
205    NotImplemented(String),
206
207    /// The port is reserved by truffle's own listeners.
208    #[error("port {0} is reserved by truffle (443 = sidecar TLS, or the session WebSocket port)")]
209    ReservedPort(u16),
210
211    /// The node has been stopped.
212    #[error("node stopped")]
213    Stopped,
214
215    /// Builder configuration error.
216    #[error("build error: {0}")]
217    BuildError(String),
218
219    /// I/O error from the builder (state dir creation, device-id persistence).
220    #[error("io error: {0}")]
221    Io(#[from] std::io::Error),
222}
223
224// ---------------------------------------------------------------------------
225// Node
226// ---------------------------------------------------------------------------
227
228/// The main truffle node — single public entry point for all functionality.
229///
230/// Generic over `N: NetworkProvider` so that tests can inject a mock provider
231/// without Tailscale. In production, use the concrete type
232/// `Node<TailscaleProvider>` (created via [`NodeBuilder`]).
233///
234/// # Lifecycle
235///
236/// 1. Create via [`Node::builder()`] + `.build().await`
237/// 2. Use `peers()`, `send()`, `subscribe()`, `open_tcp()`, etc.
238/// 3. Call `stop()` to shut down
239pub struct Node<N: NetworkProvider + 'static> {
240    /// Layer 3 network provider.
241    pub(crate) network: Arc<N>,
242    /// Layer 5 session / peer registry.
243    session: Arc<PeerRegistry<N>>,
244    /// Layer 6 envelope codec.
245    codec: Arc<dyn EnvelopeCodec>,
246    /// Broadcast sender for all incoming namespaced messages.
247    /// Kept alive to prevent the channel from closing. The router task holds a clone.
248    #[allow(dead_code)]
249    incoming_tx: broadcast::Sender<NamespacedMessage>,
250    /// Per-namespace subscription channels.
251    ///
252    /// Guarded by a `std::sync::RwLock` (not tokio) because the lock is
253    /// held only for quick HashMap operations with no `.await` points
254    /// underneath, and the `subscribe()` API has to be callable from
255    /// synchronous contexts (the NAPI bridge, which runs on Node.js's
256    /// main thread with no tokio runtime in scope).
257    namespace_filters: Arc<StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
258    /// File transfer subsystem state.
259    pub(crate) file_transfer_state: FileTransferState,
260    /// State directory for persistence (e.g., CRDT backends). Empty path
261    /// when constructed via `from_parts` (tests); set by the builder.
262    state_dir: PathBuf,
263    /// The session WebSocket listen port (reserved against raw listeners).
264    /// Defaults to 9417 in `from_parts`; set by the builder.
265    ws_port: u16,
266    /// Reverse proxy subsystem state.
267    pub(crate) proxy_state: crate::proxy::ProxyState,
268    /// Set once [`stop`](Self::stop) has completed teardown. Makes further
269    /// `stop()` calls no-ops and causes the send paths to fail fast with
270    /// [`NodeError::Stopped`].
271    stopped: std::sync::atomic::AtomicBool,
272}
273
274impl<N: NetworkProvider + 'static> Node<N> {
275    /// Create a `Node` from pre-built components (used by builder and tests).
276    ///
277    /// This constructor wires together the layers and spawns the envelope
278    /// router task that reads from the session layer, deserializes envelopes,
279    /// and dispatches to namespace subscribers.
280    pub(crate) fn from_parts(
281        network: Arc<N>,
282        session: Arc<PeerRegistry<N>>,
283        codec: Arc<dyn EnvelopeCodec>,
284    ) -> Self {
285        let (incoming_tx, _) = broadcast::channel(1024);
286        let namespace_filters: Arc<
287            StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>,
288        > = Arc::new(StdRwLock::new(HashMap::new()));
289
290        let node = Self {
291            network,
292            session: session.clone(),
293            codec: codec.clone(),
294            incoming_tx: incoming_tx.clone(),
295            namespace_filters: namespace_filters.clone(),
296            file_transfer_state: FileTransferState::new(),
297            state_dir: PathBuf::new(),
298            ws_port: 9417,
299            proxy_state: crate::proxy::ProxyState::new(),
300            stopped: std::sync::atomic::AtomicBool::new(false),
301        };
302
303        // Spawn the envelope router task.
304        node.spawn_envelope_router(session, codec, incoming_tx, namespace_filters);
305
306        node
307    }
308
309    /// Spawn a background task that reads incoming raw messages from the
310    /// session layer, deserializes them as envelopes, and routes them to
311    /// the global channel and per-namespace subscribers.
312    fn spawn_envelope_router(
313        &self,
314        session: Arc<PeerRegistry<N>>,
315        codec: Arc<dyn EnvelopeCodec>,
316        incoming_tx: broadcast::Sender<NamespacedMessage>,
317        namespace_filters: Arc<StdRwLock<HashMap<String, broadcast::Sender<NamespacedMessage>>>>,
318    ) {
319        let mut rx = session.subscribe();
320
321        tokio::spawn(async move {
322            loop {
323                match rx.recv().await {
324                    Ok(msg) => {
325                        if let Ok(envelope) = codec.decode(&msg.data) {
326                            let namespaced = NamespacedMessage {
327                                from: msg.from,
328                                namespace: envelope.namespace.clone(),
329                                msg_type: envelope.msg_type,
330                                payload: envelope.payload,
331                                timestamp: envelope.timestamp,
332                            };
333
334                            tracing::debug!(
335                                from = %namespaced.from,
336                                namespace = %namespaced.namespace,
337                                msg_type = %namespaced.msg_type,
338                                "envelope router: dispatching message"
339                            );
340
341                            // Send to global channel (best-effort).
342                            let _ = incoming_tx.send(namespaced.clone());
343
344                            // Route to namespace-specific subscriber if present.
345                            // Sync read on the std RwLock: the critical section
346                            // is a HashMap lookup with no `.await` points.
347                            let filters = namespace_filters
348                                .read()
349                                .expect("namespace_filters std RwLock poisoned");
350                            let _has_subscriber = filters.contains_key(&namespaced.namespace);
351                            if let Some(tx) = filters.get(&namespaced.namespace) {
352                                let send_result = tx.send(namespaced);
353                                tracing::debug!(
354                                    namespace = %envelope.namespace,
355                                    subscriber_count = tx.receiver_count(),
356                                    sent = send_result.is_ok(),
357                                    "envelope router: sent to namespace subscriber"
358                                );
359                            } else {
360                                tracing::debug!(
361                                    namespace = %envelope.namespace,
362                                    "envelope router: no subscriber for namespace"
363                                );
364                            }
365                        } else {
366                            tracing::warn!(
367                                from = %msg.from,
368                                data_len = msg.data.len(),
369                                "node: failed to decode envelope from incoming message"
370                            );
371                        }
372                    }
373                    Err(broadcast::error::RecvError::Lagged(n)) => {
374                        tracing::warn!(
375                            missed = n,
376                            "node: envelope router lagged, missed {n} messages"
377                        );
378                        continue;
379                    }
380                    Err(broadcast::error::RecvError::Closed) => {
381                        tracing::debug!("node: session incoming channel closed, router exiting");
382                        break;
383                    }
384                }
385            }
386        });
387    }
388
389    // ── Builder ──────────────────────────────────────────────────────────
390
391    /// Create a new [`NodeBuilder`] for configuring and constructing a node.
392    pub fn builder() -> NodeBuilder {
393        NodeBuilder::default()
394    }
395
396    // ── File Transfer ────────────────────────────────────────────────────
397
398    /// Access the file transfer subsystem.
399    ///
400    /// Returns a [`FileTransfer`](file_transfer::FileTransfer) handle
401    /// that provides methods for sending, receiving, and pulling files.
402    pub fn file_transfer(&self) -> file_transfer::FileTransfer<'_, N> {
403        file_transfer::FileTransfer::new(self)
404    }
405
406    // ── Reverse Proxy ──────────────────────────────────────────────────
407
408    /// Access the reverse proxy subsystem.
409    ///
410    /// Returns a [`Proxy`](crate::proxy::Proxy) handle that provides
411    /// methods for adding, removing, and listing reverse proxies.
412    pub fn proxy(&self) -> crate::proxy::Proxy<'_, N> {
413        crate::proxy::Proxy::new(self)
414    }
415
416    /// Create a synchronized store for device-owned state.
417    ///
418    /// Returns an `Arc<SyncedStore<T>>` that syncs data across the mesh on
419    /// namespace `"ss:{store_id}"`. The caller owns the returned Arc;
420    /// the background sync task also holds one.
421    ///
422    /// Requires `self` to be wrapped in an `Arc` because the sync task
423    /// needs to outlive this call.
424    pub fn synced_store<T>(
425        self: &Arc<Self>,
426        store_id: &str,
427    ) -> Arc<crate::synced_store::SyncedStore<T>>
428    where
429        T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
430    {
431        crate::synced_store::SyncedStore::new(self.clone(), store_id)
432    }
433
434    /// Create a synchronized store with a custom persistence backend.
435    ///
436    /// Same as [`synced_store`](Self::synced_store) but restores persisted
437    /// data on startup and writes through to the backend on every change.
438    pub fn synced_store_with_backend<T>(
439        self: &Arc<Self>,
440        store_id: &str,
441        backend: std::sync::Arc<dyn crate::synced_store::StoreBackend>,
442    ) -> Arc<crate::synced_store::SyncedStore<T>>
443    where
444        T: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
445    {
446        crate::synced_store::SyncedStore::new_with_backend(self.clone(), store_id, backend)
447    }
448
449    // ── CRDT Documents ───────────────────────────────���───────────────────
450
451    /// Create a CRDT document synchronized across the mesh.
452    ///
453    /// Returns an `Arc<CrdtDoc>` that syncs on namespace `"crdt:{doc_id}"`.
454    /// Uses the default [`MemoryCrdtBackend`](crate::crdt_doc::MemoryCrdtBackend)
455    /// (no persistence).
456    pub async fn crdt_doc(
457        self: &Arc<Self>,
458        doc_id: &str,
459    ) -> Result<Arc<crate::crdt_doc::CrdtDoc>, crate::crdt_doc::CrdtDocError> {
460        crate::crdt_doc::CrdtDoc::new(self.clone(), doc_id).await
461    }
462
463    /// Create a CRDT document with a custom persistence backend.
464    ///
465    /// Same as [`crdt_doc`](Self::crdt_doc) but restores persisted data on
466    /// startup and writes through to the backend on every change.
467    pub async fn crdt_doc_with_backend(
468        self: &Arc<Self>,
469        doc_id: &str,
470        backend: Arc<dyn crate::crdt_doc::CrdtBackend>,
471    ) -> Result<Arc<crate::crdt_doc::CrdtDoc>, crate::crdt_doc::CrdtDocError> {
472        crate::crdt_doc::CrdtDoc::new_with_backend(self.clone(), doc_id, backend).await
473    }
474
475    // ── State directory ───────────────────��────────────────────────────��
476
477    /// Set the state directory (called by the builder after construction).
478    pub(crate) fn with_state_dir(mut self, dir: PathBuf) -> Self {
479        self.state_dir = dir;
480        self
481    }
482
483    /// Record the session WebSocket port (called by the builder) so the
484    /// reserved-port guard tracks a customized `ws_port`.
485    pub(crate) fn with_ws_port(mut self, port: u16) -> Self {
486        self.ws_port = port;
487        self
488    }
489
490    /// The state directory for persistence backends.
491    ///
492    /// Returns an empty path for test nodes created via `from_parts`.
493    pub fn state_dir(&self) -> &Path {
494        &self.state_dir
495    }
496
497    // ── Lifecycle ────────────────────────────────────────────────────────
498
499    /// Stop the node and all underlying layers.
500    ///
501    /// Closes every active WebSocket connection (Layer 5) and shuts down the
502    /// network provider (Layer 3 — sidecar + bridge). After `stop()` returns,
503    /// [`send`](Self::send) and [`send_typed`](Self::send_typed) fail with
504    /// [`NodeError::Stopped`], and [`broadcast`](Self::broadcast) /
505    /// [`broadcast_typed`](Self::broadcast_typed) become no-ops.
506    ///
507    /// Idempotent: calling `stop()` more than once is safe — subsequent calls
508    /// return immediately without repeating teardown.
509    ///
510    /// The envelope-router and peer-event background tasks are not aborted
511    /// here; they exit on their own once the `Node` is dropped and their
512    /// broadcast channels close. They sit idle after `stop()`.
513    pub async fn stop(&self) {
514        if self.stopped.swap(true, std::sync::atomic::Ordering::SeqCst) {
515            tracing::debug!("node: stop() called on already-stopped node");
516            return;
517        }
518        tracing::info!("node: stopping");
519        // Layer 5: close all active WebSocket connections.
520        self.session.shutdown().await;
521        // Layer 3: shut down the network provider (sidecar + bridge).
522        if let Err(e) = self.network.stop().await {
523            tracing::warn!(error = %e, "node: network provider stop failed");
524        }
525        tracing::info!("node: stopped");
526    }
527
528    // ── Identity ─────────────────────────────────────────────────────────
529
530    /// Return the local node's identity (stable ID, hostname, name).
531    pub fn local_info(&self) -> NodeIdentity {
532        self.network.local_identity()
533    }
534
535    // ── Discovery (from Layer 3, no transport needed) ────────────────────
536
537    /// Return all known peers.
538    ///
539    /// Includes peers that are online but not yet connected (no active WS).
540    /// This information comes from Layer 3 peer discovery.
541    pub async fn peers(&self) -> Vec<Peer> {
542        let app_id = self.network.local_identity().app_id;
543        self.session
544            .peers()
545            .await
546            .into_iter()
547            .map(|s| {
548                // Before any hello, `device_name` falls back to the full
549                // Layer 3 hostname; strip it to the bare device-name slug so
550                // raw-transport-only peers read like `getLocalInfo()` names
551                // (asymmetry found by the RFC 021 cross-machine smoke test).
552                let bare = s
553                    .identity
554                    .is_none()
555                    .then(|| hostname_slug(&s.name, &app_id).map(str::to_string))
556                    .flatten();
557                let mut peer = Peer::from(s);
558                if let Some(bare) = bare {
559                    peer.device_name = bare;
560                }
561                peer
562            })
563            .collect()
564    }
565
566    /// Subscribe to peer change events (joined, left, connected, etc.).
567    pub fn on_peer_change(&self) -> broadcast::Receiver<PeerEvent> {
568        self.session.on_peer_change()
569    }
570
571    /// Resolve any accepted peer identifier form to the peer's current
572    /// session state.
573    ///
574    /// The single resolution path shared by
575    /// [`resolve_peer_id`](Self::resolve_peer_id), [`ping`](Self::ping),
576    /// and the raw transport methods. Accepts the identifier forms
577    /// documented on `resolve_peer_id`, plus the peer's Tailscale IP.
578    pub(crate) async fn resolve_peer(&self, peer_ref: &str) -> Result<PeerState, NodeError> {
579        let peers = self.session.peers().await;
580
581        // Direct matches first — device_id, device_name, hostname,
582        // tailscale_id, IP — so deterministic inputs always take the
583        // fast path.
584        for p in &peers {
585            if let Some(identity) = p.identity.as_ref() {
586                if identity.device_id == peer_ref || identity.device_name == peer_ref {
587                    return Ok(p.clone());
588                }
589            }
590            if p.id == peer_ref || p.name == peer_ref || p.ip.to_string() == peer_ref {
591                return Ok(p.clone());
592            }
593        }
594
595        // Second pass: bare device name for peers that have not completed a
596        // hello yet (raw-transport-only contact — no WS envelope traffic).
597        // Layer 3 hostnames follow `truffle-{app_id}-{slug(device_name)}`
598        // (RFC 017), so match the slugged input against the hostname's slug
599        // part; slugging the input makes "EC2 Smoke" match "ec2-smoke".
600        let app_id = self.network.local_identity().app_id;
601        let ref_slug = identity::slug(peer_ref, 255);
602        if !ref_slug.is_empty() {
603            for p in &peers {
604                if hostname_slug(&p.name, &app_id) == Some(ref_slug.as_str()) {
605                    return Ok(p.clone());
606                }
607            }
608        }
609
610        // Prefix match on device_id (require at least 4 chars and a single
611        // unambiguous hit). This matches the CLI affordance of typing just
612        // the first few characters of a ULID.
613        if peer_ref.len() >= 4 {
614            let mut hits = peers.iter().filter(|p| {
615                p.identity
616                    .as_ref()
617                    .map(|i| i.device_id.starts_with(peer_ref))
618                    .unwrap_or(false)
619            });
620            if let (Some(hit), None) = (hits.next(), hits.next()) {
621                return Ok(hit.clone());
622            }
623        }
624
625        Err(NodeError::PeerNotFound(peer_ref.to_string()))
626    }
627
628    /// Resolve a peer identifier to the peer's Tailscale IP address.
629    ///
630    /// Accepts the same identifier forms as
631    /// [`resolve_peer_id`](Self::resolve_peer_id). Used by the FFI layers
632    /// to address datagram sends by peer name.
633    pub async fn resolve_peer_ip(&self, peer_ref: &str) -> Result<IpAddr, NodeError> {
634        Ok(self.resolve_peer(peer_ref).await?.ip)
635    }
636
637    /// Resolve a peer identifier to the canonical per-device ULID
638    /// (`device_id`) from the RFC 017 hello envelope.
639    ///
640    /// Accepts any of:
641    /// - the stable `device_id` (full ULID)
642    /// - a unique prefix of the `device_id` (at least 4 characters; must
643    ///   match exactly one known peer)
644    /// - the human-readable `device_name` from the hello
645    /// - the bare device name of a peer that has not helloed yet (matched
646    ///   via the `truffle-{app_id}-{slug}` hostname convention)
647    /// - the Layer 3 Tailscale hostname (the sanitised slug) — legacy
648    /// - the Tailscale stable ID — escape hatch for diagnostics
649    /// - the Tailscale IP address (e.g. `100.x.x.x`)
650    ///
651    /// The returned string is always a `device_id` (or the Tailscale
652    /// stable ID when no hello identity is known yet), so it is safe to
653    /// feed back into `Node::send` or any other method that takes a peer
654    /// identifier.
655    pub async fn resolve_peer_id(&self, peer_id: &str) -> Result<String, NodeError> {
656        let p = self.resolve_peer(peer_id).await?;
657        Ok(p.identity
658            .as_ref()
659            .map(|i| i.device_id.clone())
660            .unwrap_or_else(|| p.id.clone()))
661    }
662
663    // ── Diagnostics ──────────────────────────────────────────────────────
664
665    /// Ping a peer via the network layer.
666    ///
667    /// Resolves the peer ID to an IP address and pings via Layer 3. Accepts
668    /// the same identifier forms as [`resolve_peer_id`](Self::resolve_peer_id).
669    pub async fn ping(&self, peer_id: &str) -> Result<PingResult, NodeError> {
670        let peer = self.resolve_peer(peer_id).await?;
671        let addr = peer.ip.to_string();
672        self.network.ping(&addr).await.map_err(NodeError::Network)
673    }
674
675    /// Return health information from the network layer.
676    pub async fn health(&self) -> HealthInfo {
677        self.network.health().await
678    }
679
680    // ── Messaging (Layer 6 envelope over Layer 4 WS) ─────────────────────
681
682    /// Return [`NodeError::Stopped`] if [`stop`](Self::stop) has already run.
683    ///
684    /// Used by the send paths to fail fast instead of attempting a doomed
685    /// session send after teardown.
686    fn ensure_not_stopped(&self) -> Result<(), NodeError> {
687        if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
688            return Err(NodeError::Stopped);
689        }
690        Ok(())
691    }
692
693    /// Send a namespaced message to a specific peer.
694    ///
695    /// The data is wrapped in a Layer 6 [`Envelope`] with the given namespace
696    /// and a `"message"` type, then serialized and sent via the session layer.
697    /// If no WebSocket connection exists, one is lazily established.
698    ///
699    /// Returns [`NodeError::Stopped`] if [`stop`](Self::stop) has been called.
700    pub async fn send(&self, peer_id: &str, namespace: &str, data: &[u8]) -> Result<(), NodeError> {
701        self.ensure_not_stopped()?;
702        // If the data is valid UTF-8 JSON, parse it into a proper JSON value
703        // so the receiver gets a structured object rather than an array of
704        // byte values.  This is critical for the file transfer protocol and
705        // any other protocol that serializes structs to JSON bytes before
706        // calling send().
707        let payload = std::str::from_utf8(data)
708            .ok()
709            .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
710            .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
711
712        let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
713
714        let encoded = self.codec.encode(&envelope)?;
715        self.session.send(peer_id, &encoded).await?;
716        Ok(())
717    }
718
719    /// Send a namespaced message with an explicit `msg_type` and JSON payload.
720    ///
721    /// Unlike [`send`](Self::send), this method takes a pre-built
722    /// [`serde_json::Value`] payload and a caller-chosen `msg_type` instead
723    /// of raw bytes with a hardcoded `"message"` type. Used by subsystems
724    /// (file transfer, synced store, request/reply) that define their own
725    /// wire protocol message types.
726    pub async fn send_typed(
727        &self,
728        peer_id: &str,
729        namespace: &str,
730        msg_type: &str,
731        payload: &serde_json::Value,
732    ) -> Result<(), NodeError> {
733        self.ensure_not_stopped()?;
734        let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
735        let encoded = self.codec.encode(&envelope)?;
736        self.session.send(peer_id, &encoded).await?;
737        Ok(())
738    }
739
740    /// Broadcast a namespaced message with an explicit `msg_type` and JSON
741    /// payload to all connected peers.
742    pub async fn broadcast_typed(
743        &self,
744        namespace: &str,
745        msg_type: &str,
746        payload: &serde_json::Value,
747    ) {
748        if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
749            tracing::debug!("node: broadcast after stop ignored");
750            return;
751        }
752        let envelope = Envelope::new(namespace, msg_type, payload.clone()).with_timestamp();
753        match self.codec.encode(&envelope) {
754            Ok(encoded) => {
755                self.session.broadcast(&encoded).await;
756            }
757            Err(e) => {
758                tracing::error!("node: failed to encode broadcast envelope: {e}");
759            }
760        }
761    }
762
763    /// Broadcast a namespaced message to all connected peers.
764    ///
765    /// Only peers with active WebSocket connections receive the broadcast.
766    /// No lazy connections are established.
767    pub async fn broadcast(&self, namespace: &str, data: &[u8]) {
768        if self.stopped.load(std::sync::atomic::Ordering::SeqCst) {
769            tracing::debug!("node: broadcast after stop ignored");
770            return;
771        }
772        let payload = std::str::from_utf8(data)
773            .ok()
774            .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
775            .unwrap_or_else(|| serde_json::Value::from(data.to_vec()));
776
777        let envelope = Envelope::new(namespace, "message", payload).with_timestamp();
778
779        match self.codec.encode(&envelope) {
780            Ok(encoded) => {
781                self.session.broadcast(&encoded).await;
782            }
783            Err(e) => {
784                tracing::error!("node: failed to encode broadcast envelope: {e}");
785            }
786        }
787    }
788
789    /// Subscribe to messages in a specific namespace.
790    ///
791    /// Returns a broadcast receiver that yields [`NamespacedMessage`]s
792    /// matching the given namespace. Multiple subscribers to the same
793    /// namespace share the same underlying channel.
794    pub fn subscribe(&self, namespace: &str) -> broadcast::Receiver<NamespacedMessage> {
795        // Fast path: check if subscriber already exists (read lock).
796        {
797            let filters = self
798                .namespace_filters
799                .read()
800                .expect("namespace_filters std RwLock poisoned");
801            if let Some(tx) = filters.get(namespace) {
802                return tx.subscribe();
803            }
804        }
805
806        // Slow path: create a new channel for this namespace (write lock).
807        let mut filters = self
808            .namespace_filters
809            .write()
810            .expect("namespace_filters std RwLock poisoned");
811        // Double-check after acquiring write lock.
812        if let Some(tx) = filters.get(namespace) {
813            return tx.subscribe();
814        }
815        let (tx, rx) = broadcast::channel(256);
816        filters.insert(namespace.to_string(), tx);
817        rx
818    }
819
820    // ── Raw streams (Layer 4 direct) ─────────────────────────────────────
821
822    /// Open a raw TCP stream to a peer on the given port.
823    ///
824    /// Resolves the peer ID to an IP address via the session's peer list,
825    /// then dials via the network layer. Accepts the same identifier
826    /// forms as [`resolve_peer_id`](Self::resolve_peer_id). Returns a
827    /// plain `TcpStream` for byte-oriented I/O.
828    ///
829    /// The stream is raw even on port 443 — the sidecar's legacy auto-TLS
830    /// wrap for 443 dials is disabled on this path.
831    pub async fn open_tcp(&self, peer_id: &str, port: u16) -> Result<TcpStream, NodeError> {
832        let peer = self.resolve_peer(peer_id).await?;
833        let addr = peer.ip.to_string();
834        self.network
835            .dial_tcp_opts(&addr, port, DialOpts { tls: Some(false) })
836            .await
837            .map_err(|e| NodeError::ConnectionFailed(e.to_string()))
838    }
839
840    /// Listen for incoming TCP connections on a port.
841    ///
842    /// Returns a [`RawListener`] that yields raw `TcpStream`s. The caller
843    /// is responsible for accepting connections in a loop. Port 0 binds an
844    /// ephemeral port (advertise the resolved `RawListener::port` in-band,
845    /// like the file transfer subsystem does). Ports 443 and 9417 are
846    /// reserved by truffle's own listeners.
847    pub async fn listen_tcp(&self, port: u16) -> Result<RawListener, NodeError> {
848        use crate::transport::tcp::TcpTransport;
849        use crate::transport::RawTransport;
850
851        ensure_port_unreserved(port, self.ws_port)?;
852        let tcp = TcpTransport::new(self.network.clone());
853        tcp.listen(port).await.map_err(NodeError::Transport)
854    }
855
856    /// Stop listening on a previously opened TCP port.
857    ///
858    /// Dropping the [`RawListener`] alone stops local delivery but leaves
859    /// the tsnet port bound in the sidecar; this releases it.
860    pub async fn unlisten_tcp(&self, port: u16) -> Result<(), NodeError> {
861        self.network
862            .unlisten_tcp(port)
863            .await
864            .map_err(NodeError::Network)
865    }
866
867    /// Open a raw QUIC connection to a peer on the given port.
868    ///
869    /// The connection carries multiple concurrent bidirectional byte
870    /// streams ([`QuicConnection::open_stream`]) with no head-of-line
871    /// blocking between them. App scoping is enforced at the TLS layer via
872    /// ALPN (`truffle-raw.{app_id}`) — peers from a different app fail the
873    /// handshake. Accepts the same identifier forms as
874    /// [`resolve_peer_id`](Self::resolve_peer_id).
875    pub async fn connect_quic(
876        &self,
877        peer_id: &str,
878        port: u16,
879    ) -> Result<QuicConnection, NodeError> {
880        let peer = self.resolve_peer(peer_id).await?;
881        let alpn = crate::transport::quic::raw_alpn(&self.network.local_identity().app_id);
882        crate::transport::quic::connect_raw(&self.network, &peer.ip.to_string(), port, &alpn)
883            .await
884            .map_err(NodeError::Transport)
885    }
886
887    /// Listen for raw QUIC connections on a port.
888    ///
889    /// Returns a [`QuicListener`] that yields [`QuicConnection`]s. Only
890    /// same-app peers can complete the handshake (ALPN scoping). Ports 443
891    /// and 9417 are reserved, and port 0 is not yet supported over the
892    /// tsnet relay (the relay cannot report the actual ephemeral port
893    /// back).
894    pub async fn listen_quic(&self, port: u16) -> Result<QuicListener, NodeError> {
895        ensure_port_unreserved(port, self.ws_port)?;
896        if port == 0 {
897            return Err(NodeError::NotImplemented(
898                "ephemeral (port 0) QUIC listeners are not supported over the tsnet relay yet — choose an explicit port"
899                    .to_string(),
900            ));
901        }
902        let alpn = crate::transport::quic::raw_alpn(&self.network.local_identity().app_id);
903        crate::transport::quic::listen_raw(&self.network, port, &alpn)
904            .await
905            .map_err(NodeError::Transport)
906    }
907
908    /// Bind a UDP datagram socket on a port.
909    ///
910    /// Datagrams are relayed through the network provider (tsnet) with
911    /// boundaries preserved; the transport falls back to a direct host
912    /// socket only when the provider has no UDP support (tests). Returns a
913    /// [`DatagramSocket`] supporting `send_to` / `recv_from` with tailnet
914    /// addresses. IPv4 (`100.x`) peers only; keep payloads ≤ ~1200 bytes
915    /// to stay under the tailnet MTU. Port 0 binds an ephemeral relay
916    /// port — suitable for client-style sockets that send first.
917    pub async fn bind_udp(&self, port: u16) -> Result<DatagramSocket, NodeError> {
918        use crate::transport::udp::{UdpConfig, UdpTransport};
919        use crate::transport::DatagramTransport;
920
921        let udp = UdpTransport::new(self.network.clone(), UdpConfig::default());
922        udp.bind(port).await.map_err(NodeError::Transport)
923    }
924}
925
926/// The bare device-name slug from a Layer 3 hostname, when it follows this
927/// app's `truffle-{app_id}-{slug}` convention (RFC 017). `None` otherwise.
928fn hostname_slug<'a>(hostname: &'a str, app_id: &str) -> Option<&'a str> {
929    hostname
930        .strip_prefix("truffle-")?
931        .strip_prefix(app_id)?
932        .strip_prefix('-')
933}
934
935/// Reject ports reserved by truffle's own listeners: 443 (sidecar TLS) and
936/// the node's configured session WebSocket port (default 9417).
937fn ensure_port_unreserved(port: u16, ws_port: u16) -> Result<(), NodeError> {
938    if port == 443 || port == ws_port {
939        Err(NodeError::ReservedPort(port))
940    } else {
941        Ok(())
942    }
943}
944
945// ---------------------------------------------------------------------------
946// NodeBuilder
947// ---------------------------------------------------------------------------
948
949/// Builder for constructing a [`Node<TailscaleProvider>`].
950///
951/// Configures the Tailscale sidecar, RFC 017 identity, and transport
952/// parameters before wiring all layers together.
953///
954/// # Example
955///
956/// ```ignore
957/// let node = Node::builder()
958///     .app_id("playground")?
959///     .device_name("alice-mbp")
960///     .sidecar_path("/opt/truffle/sidecar")
961///     .ws_port(9417)
962///     .build()
963///     .await?;
964/// ```
965#[derive(Debug, Clone)]
966pub struct NodeBuilder {
967    app_id: Option<AppId>,
968    device_name: Option<DeviceName>,
969    device_id: Option<DeviceId>,
970    sidecar_path: Option<PathBuf>,
971    state_dir: Option<String>,
972    auth_key: Option<String>,
973    ephemeral: bool,
974    ws_port: u16,
975    idle_timeout_secs: Option<u64>,
976}
977
978/// Atomically write a string to `path` by writing to a sibling `.tmp`
979/// file first and then renaming it over the destination.
980///
981/// On POSIX, `rename(2)` is atomic and replaces an existing destination
982/// in-place. On Windows, `std::fs::rename` refuses to overwrite, so we
983/// explicitly remove the destination first — the window between remove
984/// and rename is acceptable here because the caller (device-id.txt
985/// persistence) runs once at startup and is not racing itself.
986fn atomic_write_string(path: &Path, content: &str) -> std::io::Result<()> {
987    let _parent = path.parent().ok_or_else(|| {
988        std::io::Error::new(
989            std::io::ErrorKind::InvalidInput,
990            "path has no parent directory",
991        )
992    })?;
993    let mut tmp = path.to_path_buf();
994    tmp.set_extension("tmp");
995    std::fs::write(&tmp, content)?;
996    #[cfg(unix)]
997    {
998        std::fs::rename(&tmp, path)?;
999    }
1000    #[cfg(windows)]
1001    {
1002        // Windows: std::fs::rename errors if the destination exists, so
1003        // remove the old file first. Best-effort — missing file is fine.
1004        let _ = std::fs::remove_file(path);
1005        std::fs::rename(&tmp, path)?;
1006    }
1007    Ok(())
1008}
1009
1010impl Default for NodeBuilder {
1011    fn default() -> Self {
1012        Self {
1013            app_id: None,
1014            device_name: None,
1015            device_id: None,
1016            sidecar_path: None,
1017            state_dir: None,
1018            auth_key: None,
1019            ephemeral: false,
1020            ws_port: 9417,
1021            idle_timeout_secs: None,
1022        }
1023    }
1024}
1025
1026impl NodeBuilder {
1027    /// Set the application namespace identifier (RFC 017 §5.1).
1028    ///
1029    /// The input is validated against `^[a-z][a-z0-9-]{1,31}$`. Invalid
1030    /// values are rejected with `NodeError::BuildError`.
1031    pub fn app_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1032        let raw: String = s.into();
1033        let app_id = AppId::parse(&raw)
1034            .map_err(|e| NodeError::BuildError(format!("invalid app_id: {e}")))?;
1035        self.app_id = Some(app_id);
1036        Ok(self)
1037    }
1038
1039    /// Set the human-readable device name.
1040    ///
1041    /// Accepts any Unicode input; soft-truncated to 256 graphemes. When
1042    /// unset, the builder falls back to `hostname::get()` at `build()` time.
1043    pub fn device_name(mut self, s: impl Into<String>) -> Self {
1044        self.device_name = Some(DeviceName::parse(s));
1045        self
1046    }
1047
1048    /// Override the auto-generated device ID.
1049    ///
1050    /// Validates that `s` is a well-formed ULID. When provided, the value
1051    /// is persisted to `{state_dir}/device-id.txt` during `build()` so
1052    /// subsequent starts without an explicit `device_id` see it.
1053    pub fn device_id(mut self, s: impl Into<String>) -> Result<Self, NodeError> {
1054        let raw: String = s.into();
1055        let device_id = DeviceId::parse(&raw)
1056            .map_err(|e| NodeError::BuildError(format!("invalid device_id: {e}")))?;
1057        self.device_id = Some(device_id);
1058        Ok(self)
1059    }
1060
1061    /// Set the path to the Go sidecar binary.
1062    pub fn sidecar_path(mut self, path: impl Into<PathBuf>) -> Self {
1063        self.sidecar_path = Some(path.into());
1064        self
1065    }
1066
1067    /// Set the Tailscale state directory.
1068    pub fn state_dir(mut self, dir: &str) -> Self {
1069        self.state_dir = Some(dir.to_string());
1070        self
1071    }
1072
1073    /// Set the Tailscale auth key for headless authentication.
1074    pub fn auth_key(mut self, key: &str) -> Self {
1075        self.auth_key = Some(key.to_string());
1076        self
1077    }
1078
1079    /// Set whether the node is ephemeral (auto-removed from tailnet on shutdown).
1080    pub fn ephemeral(mut self, val: bool) -> Self {
1081        self.ephemeral = val;
1082        self
1083    }
1084
1085    /// Set the WebSocket listen port.
1086    pub fn ws_port(mut self, port: u16) -> Self {
1087        self.ws_port = port;
1088        self
1089    }
1090
1091    /// Set the idle timeout (in seconds) for bridged raw TCP connections.
1092    ///
1093    /// The sidecar reaps quiet bridged connections after this long
1094    /// (default: 600 s). Apps holding long-lived quiet sockets should
1095    /// raise this or send application-level keepalives.
1096    pub fn idle_timeout_secs(mut self, secs: u64) -> Self {
1097        self.idle_timeout_secs = Some(secs);
1098        self
1099    }
1100
1101    /// Resolve RFC 017 identity values and the Tailscale config.
1102    ///
1103    /// Shared between [`build()`](Self::build) and
1104    /// [`build_with_auth_handler()`](Self::build_with_auth_handler). Returns
1105    /// the ready-to-start `TailscaleConfig` along with the parsed identity
1106    /// triple.
1107    fn prepare_config(&self) -> Result<TailscaleConfig, NodeError> {
1108        // 1. sidecar binary is required.
1109        let binary_path = self
1110            .sidecar_path
1111            .clone()
1112            .ok_or_else(|| NodeError::BuildError("sidecar_path is required".into()))?;
1113
1114        // 2. app_id is required.
1115        let app_id = self
1116            .app_id
1117            .clone()
1118            .ok_or_else(|| NodeError::BuildError("app_id is required".into()))?;
1119
1120        // 3. device_name falls back to the OS hostname.
1121        let device_name = match self.device_name.clone() {
1122            Some(name) => name,
1123            None => {
1124                let os_hostname = hostname::get()
1125                    .map_err(|e| {
1126                        NodeError::BuildError(format!(
1127                            "device_name is unset and hostname::get() failed: {e}"
1128                        ))
1129                    })?
1130                    .to_string_lossy()
1131                    .into_owned();
1132                DeviceName::parse(os_hostname)
1133            }
1134        };
1135
1136        // 4. Compose the Tailscale hostname once, here. Downstream code
1137        //    MUST NOT rebuild it — the provider config stores this verbatim.
1138        let tailscale_host = identity::tailscale_hostname(&app_id, &device_name);
1139
1140        // 5. Resolve state_dir. Default:
1141        //    `{dirs::data_dir}/truffle/{app_id}/{slug(device_name)}`.
1142        let state_dir = self.state_dir.clone().unwrap_or_else(|| {
1143            let base = dirs::data_dir().unwrap_or_else(|| {
1144                tracing::warn!(
1145                    "dirs::data_dir() returned None, falling back to std::env::temp_dir()"
1146                );
1147                std::env::temp_dir()
1148            });
1149            base.join("truffle")
1150                .join(app_id.as_str())
1151                .join(identity::slug(device_name.as_str(), 255))
1152                .to_string_lossy()
1153                .into_owned()
1154        });
1155
1156        // 6. Ensure the state directory exists before Tailscale starts.
1157        std::fs::create_dir_all(&state_dir)?;
1158
1159        // 7. Resolve device_id. Priority:
1160        //    a) explicit builder override → validate + persist
1161        //    b) existing `device-id.txt` → read + validate
1162        //    c) generate + persist
1163        let device_id_file = Path::new(&state_dir).join("device-id.txt");
1164        let device_id = match self.device_id.clone() {
1165            Some(id) => {
1166                // Persist the override so later auto-generated calls see it.
1167                atomic_write_string(&device_id_file, id.as_str())?;
1168                id
1169            }
1170            None => {
1171                if device_id_file.exists() {
1172                    let s = std::fs::read_to_string(&device_id_file)?.trim().to_string();
1173                    DeviceId::parse(&s).map_err(|e| {
1174                        NodeError::BuildError(format!(
1175                            "device-id.txt at {device_id_file:?} contains an invalid ULID: {e}"
1176                        ))
1177                    })?
1178                } else {
1179                    let id = DeviceId::generate();
1180                    atomic_write_string(&device_id_file, id.as_str())?;
1181                    id
1182                }
1183            }
1184        };
1185
1186        Ok(TailscaleConfig {
1187            binary_path,
1188            app_id: app_id.as_str().to_string(),
1189            device_id: device_id.as_str().to_string(),
1190            device_name: device_name.as_str().to_string(),
1191            hostname: tailscale_host,
1192            state_dir,
1193            auth_key: self.auth_key.clone(),
1194            ephemeral: if self.ephemeral { Some(true) } else { None },
1195            tags: None,
1196            idle_timeout_secs: self.idle_timeout_secs,
1197        })
1198    }
1199
1200    /// Build and start the node.
1201    ///
1202    /// This creates the TailscaleProvider, starts it, creates the WebSocket
1203    /// transport and PeerRegistry, starts the session, and spawns the
1204    /// envelope router.
1205    ///
1206    /// # Errors
1207    ///
1208    /// Returns [`NodeError::BuildError`] if required configuration is missing,
1209    /// or propagates errors from the network provider startup.
1210    pub async fn build(self) -> Result<Node<TailscaleProvider>, NodeError> {
1211        let ws_port = self.ws_port;
1212        let config = self.prepare_config()?;
1213        let state_dir = PathBuf::from(&config.state_dir);
1214
1215        let mut provider = TailscaleProvider::new(config);
1216        provider.start().await.map_err(NodeError::Network)?;
1217
1218        let network = Arc::new(provider);
1219
1220        // 2. Create WebSocket transport.
1221        let ws_config = WsConfig {
1222            port: ws_port,
1223            ..Default::default()
1224        };
1225        let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1226
1227        // 3. Create PeerRegistry and start session.
1228        let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1229        session.start().await;
1230
1231        // 4. Create the node with the envelope router.
1232        let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1233        let node = Node::from_parts(network, session, codec)
1234            .with_state_dir(state_dir)
1235            .with_ws_port(ws_port);
1236
1237        tracing::info!("node: started successfully");
1238        Ok(node)
1239    }
1240
1241    /// Build and start the node, calling `on_auth` if authentication is needed.
1242    ///
1243    /// This is identical to [`build()`](Self::build) except it subscribes to
1244    /// provider events *before* `provider.start()` blocks, forwarding
1245    /// `AuthRequired` events to the callback while waiting for authentication
1246    /// to complete.
1247    ///
1248    /// # Errors
1249    ///
1250    /// Returns [`NodeError::BuildError`] if required configuration is missing,
1251    /// or propagates errors from the network provider startup.
1252    pub async fn build_with_auth_handler(
1253        self,
1254        on_auth: impl Fn(String) + Send + 'static,
1255    ) -> Result<Node<TailscaleProvider>, NodeError> {
1256        let ws_port = self.ws_port;
1257        let config = self.prepare_config()?;
1258        let state_dir = PathBuf::from(&config.state_dir);
1259
1260        let mut provider = TailscaleProvider::new(config);
1261
1262        // 2. Subscribe to peer events BEFORE start() so we capture auth URLs.
1263        let mut auth_rx = provider.peer_events();
1264
1265        // 3. Spawn a task that forwards AuthRequired events to the callback.
1266        let auth_task = tokio::spawn(async move {
1267            use crate::network::NetworkPeerEvent;
1268            loop {
1269                match auth_rx.recv().await {
1270                    Ok(NetworkPeerEvent::AuthRequired { url }) => {
1271                        on_auth(url);
1272                    }
1273                    Err(broadcast::error::RecvError::Closed) => break,
1274                    Err(broadcast::error::RecvError::Lagged(_)) => continue,
1275                    _ => {} // Ignore other events
1276                }
1277            }
1278        });
1279
1280        // 4. Start the provider (blocks until auth completes).
1281        let start_result = provider.start().await.map_err(NodeError::Network);
1282
1283        // 5. Cancel the auth forwarding task — auth is done.
1284        auth_task.abort();
1285
1286        start_result?;
1287
1288        let network = Arc::new(provider);
1289
1290        // 6. Create WebSocket transport.
1291        let ws_config = WsConfig {
1292            port: ws_port,
1293            ..Default::default()
1294        };
1295        let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config));
1296
1297        // 7. Create PeerRegistry and start session.
1298        let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1299        session.start().await;
1300
1301        // 8. Create the node with the envelope router.
1302        let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1303        let node = Node::from_parts(network, session, codec)
1304            .with_state_dir(state_dir)
1305            .with_ws_port(ws_port);
1306
1307        tracing::info!("node: started successfully (with auth handler)");
1308        Ok(node)
1309    }
1310}
1311
1312// ---------------------------------------------------------------------------
1313// Tests
1314// ---------------------------------------------------------------------------
1315
1316#[cfg(test)]
1317mod tests {
1318    use super::*;
1319    use crate::network::{
1320        HealthInfo, IncomingConnection, NetworkError, NetworkPeer, NetworkPeerEvent,
1321        NetworkTcpListener, NetworkUdpSocket, PeerAddr,
1322    };
1323    use crate::transport::WsConfig;
1324    use serde_json::json;
1325    use std::sync::atomic::{AtomicUsize, Ordering};
1326    use std::time::Duration;
1327    use tokio::sync::{broadcast, mpsc};
1328
1329    // ── Mock NetworkProvider ──────────────────────────────────────────
1330
1331    struct MockNetworkProvider {
1332        identity: NodeIdentity,
1333        local_addr: PeerAddr,
1334        peer_event_tx: broadcast::Sender<NetworkPeerEvent>,
1335        /// Pre-loaded peer list for `peers()`.
1336        mock_peers: Arc<RwLock<Vec<NetworkPeer>>>,
1337        /// Count of `stop()` invocations — lets tests assert the Node
1338        /// actually shut the provider down during teardown.
1339        stop_calls: Arc<AtomicUsize>,
1340    }
1341
1342    impl MockNetworkProvider {
1343        fn new(id: &str) -> Self {
1344            let (peer_event_tx, _) = broadcast::channel(64);
1345            Self {
1346                identity: NodeIdentity {
1347                    app_id: "test".to_string(),
1348                    // RFC 017: align `device_id` with fixture input so
1349                    // tests can reason about a single identifier.
1350                    device_id: id.to_string(),
1351                    device_name: format!("Test Node {id}"),
1352                    tailscale_hostname: format!("truffle-test-{id}"),
1353                    tailscale_id: id.to_string(),
1354                    dns_name: None,
1355                    ip: Some("127.0.0.1".parse().unwrap()),
1356                },
1357                local_addr: PeerAddr {
1358                    ip: Some("127.0.0.1".parse().unwrap()),
1359                    hostname: format!("truffle-test-{id}"),
1360                    dns_name: None,
1361                },
1362                peer_event_tx,
1363                mock_peers: Arc::new(RwLock::new(Vec::new())),
1364                stop_calls: Arc::new(AtomicUsize::new(0)),
1365            }
1366        }
1367
1368        fn event_sender(&self) -> broadcast::Sender<NetworkPeerEvent> {
1369            self.peer_event_tx.clone()
1370        }
1371
1372        /// Number of times `stop()` has been called on this provider.
1373        fn stop_call_count(&self) -> usize {
1374            self.stop_calls.load(Ordering::SeqCst)
1375        }
1376    }
1377
1378    impl NetworkProvider for MockNetworkProvider {
1379        async fn start(&mut self) -> Result<(), NetworkError> {
1380            Ok(())
1381        }
1382
1383        async fn stop(&self) -> Result<(), NetworkError> {
1384            self.stop_calls.fetch_add(1, Ordering::SeqCst);
1385            Ok(())
1386        }
1387
1388        fn local_identity(&self) -> NodeIdentity {
1389            self.identity.clone()
1390        }
1391
1392        fn local_addr(&self) -> PeerAddr {
1393            self.local_addr.clone()
1394        }
1395
1396        fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent> {
1397            self.peer_event_tx.subscribe()
1398        }
1399
1400        async fn peers(&self) -> Vec<NetworkPeer> {
1401            self.mock_peers.read().await.clone()
1402        }
1403
1404        async fn dial_tcp(&self, addr: &str, port: u16) -> Result<TcpStream, NetworkError> {
1405            let target = format!("{addr}:{port}");
1406            TcpStream::connect(&target)
1407                .await
1408                .map_err(|e| NetworkError::DialFailed(format!("mock dial {target}: {e}")))
1409        }
1410
1411        async fn listen_tcp(&self, port: u16) -> Result<NetworkTcpListener, NetworkError> {
1412            let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{port}"))
1413                .await
1414                .map_err(|e| NetworkError::ListenFailed(format!("mock listen :{port}: {e}")))?;
1415
1416            let actual_port = listener.local_addr().unwrap().port();
1417            let (tx, rx) = mpsc::channel::<IncomingConnection>(64);
1418
1419            tokio::spawn(async move {
1420                loop {
1421                    match listener.accept().await {
1422                        Ok((stream, addr)) => {
1423                            let conn = IncomingConnection {
1424                                stream,
1425                                remote_addr: addr.to_string(),
1426                                remote_identity: String::new(),
1427                                port: actual_port,
1428                            };
1429                            if tx.send(conn).await.is_err() {
1430                                break;
1431                            }
1432                        }
1433                        Err(e) => {
1434                            tracing::debug!("mock listener error: {e}");
1435                            break;
1436                        }
1437                    }
1438                }
1439            });
1440
1441            Ok(NetworkTcpListener {
1442                port: actual_port,
1443                incoming: rx,
1444            })
1445        }
1446
1447        async fn unlisten_tcp(&self, _port: u16) -> Result<(), NetworkError> {
1448            Ok(())
1449        }
1450
1451        async fn bind_udp(&self, _port: u16) -> Result<NetworkUdpSocket, NetworkError> {
1452            Err(NetworkError::Internal("mock: UDP not supported".into()))
1453        }
1454
1455        async fn ping(&self, _addr: &str) -> Result<PingResult, NetworkError> {
1456            Ok(PingResult {
1457                latency: Duration::from_millis(1),
1458                connection: "direct".to_string(),
1459                peer_addr: None,
1460            })
1461        }
1462
1463        async fn health(&self) -> HealthInfo {
1464            HealthInfo {
1465                state: "running".to_string(),
1466                healthy: true,
1467                ..Default::default()
1468            }
1469        }
1470    }
1471
1472    // ── Helpers ──────────────────────────────────────────────────────
1473
1474    fn make_loopback_peer(id: &str) -> NetworkPeer {
1475        NetworkPeer {
1476            id: id.to_string(),
1477            hostname: format!("truffle-test-{id}"),
1478            ip: "127.0.0.1".parse().unwrap(),
1479            online: true,
1480            cur_addr: Some("127.0.0.1:41641".to_string()),
1481            relay: None,
1482            os: Some("linux".to_string()),
1483            last_seen: Some("2026-03-25T12:00:00Z".to_string()),
1484            key_expiry: None,
1485            dns_name: None,
1486        }
1487    }
1488
1489    fn ws_config(port: u16) -> WsConfig {
1490        WsConfig {
1491            port,
1492            ping_interval: Duration::from_secs(300),
1493            pong_timeout: Duration::from_secs(300),
1494            ..Default::default()
1495        }
1496    }
1497
1498    async fn random_port() -> u16 {
1499        let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1500        l.local_addr().unwrap().port()
1501    }
1502
1503    /// Create a Node backed by a mock provider for testing.
1504    async fn make_test_node(
1505        id: &str,
1506        ws_port: u16,
1507    ) -> (
1508        Node<MockNetworkProvider>,
1509        broadcast::Sender<NetworkPeerEvent>,
1510        Arc<MockNetworkProvider>,
1511    ) {
1512        let provider = MockNetworkProvider::new(id);
1513        let event_tx = provider.event_sender();
1514        let network = Arc::new(provider);
1515        let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config(ws_port)));
1516        let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
1517        session.start().await;
1518
1519        let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
1520        let node = Node::from_parts(network.clone(), session, codec);
1521
1522        (node, event_tx, network)
1523    }
1524
1525    // ── Tests ────────────────────────────────────────────────────────
1526
1527    #[tokio::test]
1528    async fn test_node_builder_creates_node() {
1529        let ws_port = random_port().await;
1530        let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1531
1532        let identity = node.local_info();
1533        assert_eq!(identity.tailscale_id, "node-1");
1534        assert_eq!(identity.device_id, "node-1");
1535        assert!(identity.tailscale_hostname.contains("node-1"));
1536    }
1537
1538    #[tokio::test]
1539    async fn test_node_peers_from_network() {
1540        let ws_port = random_port().await;
1541        let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1542
1543        // Initially no peers.
1544        let peers = node.peers().await;
1545        assert!(peers.is_empty());
1546
1547        // Inject a peer via Layer 3.
1548        let peer = make_loopback_peer("peer-a");
1549        let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1550        tokio::time::sleep(Duration::from_millis(50)).await;
1551
1552        let peers = node.peers().await;
1553        assert_eq!(peers.len(), 1);
1554        assert_eq!(peers[0].id, "peer-a");
1555        assert!(peers[0].online);
1556        assert!(!peers[0].ws_connected);
1557    }
1558
1559    #[tokio::test]
1560    async fn test_node_send_to_unknown_peer_errors() {
1561        let ws_port = random_port().await;
1562        let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1563
1564        let result = node.send("nonexistent", "test", b"hello").await;
1565        assert!(result.is_err());
1566        let err_str = result.unwrap_err().to_string();
1567        assert!(
1568            err_str.contains("unknown peer") || err_str.contains("not found"),
1569            "expected unknown peer error, got: {err_str}"
1570        );
1571    }
1572
1573    #[tokio::test]
1574    async fn test_node_send_wraps_in_envelope() {
1575        // Test that send() properly creates an envelope.
1576        // We test the codec directly since a full send requires two connected nodes.
1577        let codec = JsonCodec;
1578        let data = b"hello world";
1579        let envelope = Envelope::new("test-ns", "message", serde_json::Value::from(data.to_vec()))
1580            .with_timestamp();
1581
1582        let encoded = codec.encode(&envelope).unwrap();
1583        let decoded = codec.decode(&encoded).unwrap();
1584
1585        assert_eq!(decoded.namespace, "test-ns");
1586        assert_eq!(decoded.msg_type, "message");
1587        assert!(decoded.timestamp.is_some());
1588    }
1589
1590    #[tokio::test]
1591    async fn test_node_subscribe_filters_by_namespace() {
1592        let ws_port = random_port().await;
1593        let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1594
1595        // Create subscribers for two different namespaces.
1596        let _rx_chat = node.subscribe("chat");
1597        let _rx_ft = node.subscribe("ft");
1598
1599        // Subscribing to the same namespace again should work.
1600        let _rx_chat2 = node.subscribe("chat");
1601    }
1602
1603    #[tokio::test]
1604    async fn test_node_broadcast() {
1605        let ws_port = random_port().await;
1606        let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1607
1608        // Broadcast with no connected peers should not panic.
1609        node.broadcast("test", b"hello everyone").await;
1610    }
1611
1612    #[tokio::test]
1613    async fn test_node_open_tcp_resolves_peer() {
1614        let ws_port = random_port().await;
1615        let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1616
1617        // Start a TCP server for the test.
1618        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1619        let tcp_port = listener.local_addr().unwrap().port();
1620
1621        // Inject a loopback peer.
1622        let peer = make_loopback_peer("peer-tcp");
1623        let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1624        tokio::time::sleep(Duration::from_millis(50)).await;
1625
1626        // Accept a connection in the background.
1627        let accept_handle = tokio::spawn(async move {
1628            let (stream, _) = listener.accept().await.unwrap();
1629            stream
1630        });
1631
1632        // open_tcp should resolve peer-tcp to 127.0.0.1 and connect.
1633        let stream = node.open_tcp("peer-tcp", tcp_port).await;
1634        assert!(stream.is_ok(), "open_tcp failed: {:?}", stream.err());
1635
1636        let _ = accept_handle.await;
1637    }
1638
1639    #[tokio::test]
1640    async fn test_node_open_tcp_unknown_peer_errors() {
1641        let ws_port = random_port().await;
1642        let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1643
1644        let result = node.open_tcp("nonexistent", 8080).await;
1645        assert!(result.is_err());
1646        let err_str = result.unwrap_err().to_string();
1647        assert!(
1648            err_str.contains("not found"),
1649            "expected peer not found error, got: {err_str}"
1650        );
1651    }
1652
1653    #[tokio::test]
1654    async fn test_node_ping_resolves_peer() {
1655        let ws_port = random_port().await;
1656        let (node, event_tx, _network) = make_test_node("node-1", ws_port).await;
1657
1658        // No peer yet.
1659        let result = node.ping("peer-ping").await;
1660        assert!(result.is_err());
1661
1662        // Inject peer.
1663        let peer = make_loopback_peer("peer-ping");
1664        let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1665        tokio::time::sleep(Duration::from_millis(50)).await;
1666
1667        // Should succeed (mock returns 1ms latency).
1668        let result = node.ping("peer-ping").await;
1669        assert!(result.is_ok());
1670        assert_eq!(result.unwrap().latency, Duration::from_millis(1));
1671    }
1672
1673    #[tokio::test]
1674    async fn test_node_health() {
1675        let ws_port = random_port().await;
1676        let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1677
1678        let health = node.health().await;
1679        assert!(health.healthy);
1680        assert_eq!(health.state, "running");
1681    }
1682
1683    #[tokio::test]
1684    async fn test_node_connect_quic_unknown_peer_errors() {
1685        let ws_port = random_port().await;
1686        let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1687
1688        let result = node.connect_quic("peer", 4433).await;
1689        assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
1690    }
1691
1692    #[tokio::test]
1693    async fn test_node_listen_tcp() {
1694        let ws_port = random_port().await;
1695        let (node, _event_tx, _network) = make_test_node("node-1", ws_port).await;
1696
1697        // listen_tcp(0) should bind to an ephemeral port.
1698        let listener = node.listen_tcp(0).await;
1699        assert!(listener.is_ok(), "listen_tcp failed: {:?}", listener.err());
1700    }
1701
1702    #[tokio::test]
1703    async fn test_envelope_serialize_deserialize() {
1704        let envelope = Envelope::new("chat", "message", json!({"text": "hello"})).with_timestamp();
1705
1706        let bytes = envelope.serialize().unwrap();
1707        let decoded = Envelope::deserialize(&bytes).unwrap();
1708
1709        assert_eq!(decoded.namespace, "chat");
1710        assert_eq!(decoded.msg_type, "message");
1711        assert_eq!(decoded.payload["text"], "hello");
1712        assert!(decoded.timestamp.is_some());
1713    }
1714
1715    #[tokio::test]
1716    async fn test_envelope_codec_json() {
1717        let codec = JsonCodec;
1718        let envelope = Envelope::new("ft", "offer", json!({"file": "test.bin"}));
1719
1720        let encoded = codec.encode(&envelope).unwrap();
1721        let decoded = codec.decode(&encoded).unwrap();
1722
1723        assert_eq!(decoded.namespace, "ft");
1724        assert_eq!(decoded.payload["file"], "test.bin");
1725    }
1726
1727    #[tokio::test]
1728    async fn test_envelope_unknown_fields_ignored() {
1729        let json_bytes = br#"{
1730            "namespace": "v2",
1731            "msg_type": "new",
1732            "payload": {},
1733            "future_field": "ignored"
1734        }"#;
1735
1736        let codec = JsonCodec;
1737        let decoded = codec.decode(json_bytes).unwrap();
1738        assert_eq!(decoded.namespace, "v2");
1739        assert_eq!(decoded.msg_type, "new");
1740    }
1741
1742    #[tokio::test]
1743    async fn test_node_send_and_receive_roundtrip() {
1744        // Set up two nodes that communicate via loopback WS.
1745        let port_a = random_port().await;
1746        let port_b = random_port().await;
1747
1748        let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
1749        let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
1750
1751        // Inject each node as a peer of the other.
1752        let peer_b = NetworkPeer {
1753            id: "node-b".to_string(),
1754            hostname: "truffle-test-node-b".to_string(),
1755            ip: "127.0.0.1".parse().unwrap(),
1756            online: true,
1757            cur_addr: Some("127.0.0.1:41641".to_string()),
1758            relay: None,
1759            os: None,
1760            last_seen: None,
1761            key_expiry: None,
1762            dns_name: None,
1763        };
1764        let peer_a = NetworkPeer {
1765            id: "node-a".to_string(),
1766            hostname: "truffle-test-node-a".to_string(),
1767            ip: "127.0.0.1".parse().unwrap(),
1768            online: true,
1769            cur_addr: Some("127.0.0.1:41641".to_string()),
1770            relay: None,
1771            os: None,
1772            last_seen: None,
1773            key_expiry: None,
1774            dns_name: None,
1775        };
1776
1777        let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
1778        let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
1779        tokio::time::sleep(Duration::from_millis(100)).await;
1780
1781        // Subscribe to namespace on node_b.
1782        let mut rx = node_b.subscribe("test");
1783
1784        // Send from node_a to node_b. This triggers lazy WS connect.
1785        // Note: this will connect to node_b's WS listener on port_b.
1786        let send_result = node_a.send("node-b", "test", b"hello from a").await;
1787
1788        // The send may fail in loopback mock because the WS port for node-b
1789        // is the listener port, and the mock's dial connects to 127.0.0.1:port_b.
1790        // In a real scenario with Tailscale, this works because each node
1791        // listens on its own Tailscale IP.
1792        //
1793        // For unit tests, we verify the envelope codec roundtrip works.
1794        // Full integration tests require two separate processes.
1795        if send_result.is_ok() {
1796            // If send succeeded, verify the message arrives.
1797            let msg = tokio::time::timeout(Duration::from_secs(2), rx.recv()).await;
1798            if let Ok(Ok(msg)) = msg {
1799                assert_eq!(msg.namespace, "test");
1800            }
1801        }
1802        // If send fails due to loopback WS peer-id mismatch, that's expected
1803        // in unit tests. The important thing is no panics.
1804    }
1805
1806    // ── Lifecycle: stop() teardown ───────────────────────────────────
1807
1808    #[tokio::test]
1809    async fn test_node_stop_shuts_down_provider_and_is_idempotent() {
1810        let ws_port = random_port().await;
1811        let (node, event_tx, network) = make_test_node("node-1", ws_port).await;
1812
1813        let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-1")));
1814        tokio::time::sleep(Duration::from_millis(50)).await;
1815
1816        assert_eq!(network.stop_call_count(), 0);
1817        node.stop().await;
1818        assert_eq!(
1819            network.stop_call_count(),
1820            1,
1821            "stop() must shut down the provider"
1822        );
1823
1824        // Idempotent: second stop must not stop the provider again.
1825        node.stop().await;
1826        assert_eq!(network.stop_call_count(), 1);
1827
1828        // Post-stop sends fail with NodeError::Stopped.
1829        let err = node.send("peer-1", "test", b"hi").await.unwrap_err();
1830        assert!(matches!(err, NodeError::Stopped));
1831    }
1832
1833    #[tokio::test]
1834    async fn test_node_stop_closes_ws_connections() {
1835        // Two nodes on random ports, each discovering the other as a loopback
1836        // peer. Modeled on `test_node_send_and_receive_roundtrip`.
1837        let port_a = random_port().await;
1838        let port_b = random_port().await;
1839
1840        let (node_a, event_tx_a, _net_a) = make_test_node("node-a", port_a).await;
1841        let (node_b, event_tx_b, _net_b) = make_test_node("node-b", port_b).await;
1842
1843        let peer_b = NetworkPeer {
1844            id: "node-b".to_string(),
1845            hostname: "truffle-test-node-b".to_string(),
1846            ip: "127.0.0.1".parse().unwrap(),
1847            online: true,
1848            cur_addr: Some("127.0.0.1:41641".to_string()),
1849            relay: None,
1850            os: None,
1851            last_seen: None,
1852            key_expiry: None,
1853            dns_name: None,
1854        };
1855        let peer_a = NetworkPeer {
1856            id: "node-a".to_string(),
1857            hostname: "truffle-test-node-a".to_string(),
1858            ip: "127.0.0.1".parse().unwrap(),
1859            online: true,
1860            cur_addr: Some("127.0.0.1:41641".to_string()),
1861            relay: None,
1862            os: None,
1863            last_seen: None,
1864            key_expiry: None,
1865            dns_name: None,
1866        };
1867
1868        let _ = event_tx_a.send(NetworkPeerEvent::Joined(peer_b));
1869        let _ = event_tx_b.send(NetworkPeerEvent::Joined(peer_a));
1870        tokio::time::sleep(Duration::from_millis(100)).await;
1871
1872        // node_b's listener exists on port_b; in a real multi-IP tailnet
1873        // node_a would dial it. Under the loopback mock every node shares
1874        // 127.0.0.1, so node_a's dial lands on its own listener instead —
1875        // which is fine for exercising stop()'s connection teardown.
1876        let _rx = node_b.subscribe("test");
1877
1878        // Send from node_a to establish the WS. In loopback the dial lands on
1879        // node_a's own listener, so the hello stamps node_a's identity onto
1880        // the node-b entry — that flips the public `Peer.id` projection to
1881        // node-a, so we key off the stable `tailscale_id` (the Layer 3
1882        // routing key) which stays "node-b".
1883        node_a
1884            .send("node-b", "test", b"hello from a")
1885            .await
1886            .expect("loopback send should establish a WS connection");
1887
1888        let peers = node_a.peers().await;
1889        let b = peers
1890            .iter()
1891            .find(|p| p.tailscale_id == "node-b")
1892            .expect("peer b discovered");
1893        assert!(
1894            b.ws_connected,
1895            "send() should have established a WS connection to node-b"
1896        );
1897
1898        node_a.stop().await;
1899        let peers = node_a.peers().await;
1900        let b = peers
1901            .iter()
1902            .find(|p| p.tailscale_id == "node-b")
1903            .expect("peer b still discovered");
1904        assert!(
1905            !b.ws_connected,
1906            "stop() must close and un-mark WS connections"
1907        );
1908    }
1909
1910    // ── RFC 017 Phase 2: resolve_peer_id ─────────────────────────────
1911
1912    /// Helper: inject a peer entry into the session registry and then
1913    /// stamp a synthetic RFC 017 identity on it so `resolve_peer_id`
1914    /// has something to look up. This skips the real hello exchange.
1915    async fn inject_peer_with_identity(
1916        node: &Node<MockNetworkProvider>,
1917        event_tx: &broadcast::Sender<NetworkPeerEvent>,
1918        tailscale_id: &str,
1919        device_id: &str,
1920        device_name: &str,
1921    ) {
1922        let peer = make_loopback_peer(tailscale_id);
1923        let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
1924        tokio::time::sleep(Duration::from_millis(30)).await;
1925
1926        let identity = crate::session::PeerIdentity {
1927            app_id: "test".into(),
1928            device_id: device_id.into(),
1929            device_name: device_name.into(),
1930            os: "linux".into(),
1931            tailscale_id: tailscale_id.into(),
1932        };
1933        assert!(
1934            node.session
1935                .test_stamp_identity(tailscale_id, identity)
1936                .await,
1937            "peer {tailscale_id} should exist in session registry before stamping identity"
1938        );
1939    }
1940
1941    #[tokio::test]
1942    async fn test_resolve_peer_id_by_device_id() {
1943        let ws_port = random_port().await;
1944        let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1945
1946        inject_peer_with_identity(
1947            &node,
1948            &event_tx,
1949            "tailscale-abc",
1950            "01HZZZZZZZZZZZZZZZZZZZZZZZ",
1951            "Alice MacBook",
1952        )
1953        .await;
1954
1955        let resolved = node
1956            .resolve_peer_id("01HZZZZZZZZZZZZZZZZZZZZZZZ")
1957            .await
1958            .unwrap();
1959        assert_eq!(resolved, "01HZZZZZZZZZZZZZZZZZZZZZZZ");
1960    }
1961
1962    #[tokio::test]
1963    async fn test_resolve_peer_id_by_device_name() {
1964        let ws_port = random_port().await;
1965        let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1966
1967        inject_peer_with_identity(
1968            &node,
1969            &event_tx,
1970            "tailscale-abc",
1971            "01HXYZXYZXYZXYZXYZXYZXYZXY",
1972            "Bob's Mac",
1973        )
1974        .await;
1975
1976        let resolved = node.resolve_peer_id("Bob's Mac").await.unwrap();
1977        assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
1978    }
1979
1980    #[tokio::test]
1981    async fn test_resolve_peer_id_by_device_id_prefix() {
1982        let ws_port = random_port().await;
1983        let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
1984
1985        inject_peer_with_identity(
1986            &node,
1987            &event_tx,
1988            "tailscale-abc",
1989            "01HXYZXYZXYZXYZXYZXYZXYZXY",
1990            "laptop",
1991        )
1992        .await;
1993
1994        // Prefix match — 4 chars is the minimum the implementation
1995        // accepts.
1996        let resolved = node.resolve_peer_id("01HX").await.unwrap();
1997        assert_eq!(resolved, "01HXYZXYZXYZXYZXYZXYZXYZXY");
1998    }
1999
2000    #[tokio::test]
2001    async fn test_resolve_peer_id_by_tailscale_id_legacy() {
2002        // Escape hatch: resolving by the Tailscale stable ID should
2003        // still work and return the device_id (or the tailscale_id as
2004        // fallback when no identity is populated).
2005        let ws_port = random_port().await;
2006        let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2007
2008        inject_peer_with_identity(
2009            &node,
2010            &event_tx,
2011            "tailscale-legacy",
2012            "01HLEGACY0000000000000000X",
2013            "legacy box",
2014        )
2015        .await;
2016
2017        let resolved = node.resolve_peer_id("tailscale-legacy").await.unwrap();
2018        assert_eq!(resolved, "01HLEGACY0000000000000000X");
2019    }
2020
2021    #[tokio::test]
2022    async fn test_resolve_peer_id_unknown() {
2023        let ws_port = random_port().await;
2024        let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2025        let result = node.resolve_peer_id("nope").await;
2026        assert!(matches!(result, Err(NodeError::PeerNotFound(_))));
2027    }
2028
2029    // ── RFC 021: raw transport surface ────────────────────────────────
2030
2031    #[tokio::test]
2032    async fn test_resolve_peer_by_bare_name_before_hello() {
2033        // Raw-transport-only peers have no hello identity; the bare device
2034        // name must still resolve via the hostname slug (RFC 021 smoke-test
2035        // finding: only the full `truffle-{app}-{slug}` hostname matched).
2036        let ws_port = random_port().await;
2037        let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2038
2039        let mut peer = make_loopback_peer("nodeid-9");
2040        peer.hostname = "truffle-test-ec2-smoke".to_string();
2041        let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2042        tokio::time::sleep(Duration::from_millis(50)).await;
2043
2044        // Bare slug, and an unslugged variant that normalizes to it.
2045        assert_eq!(node.resolve_peer("ec2-smoke").await.unwrap().id, "nodeid-9");
2046        assert_eq!(node.resolve_peer("EC2 Smoke").await.unwrap().id, "nodeid-9");
2047        // Unknown bare names still miss.
2048        assert!(node.resolve_peer("other-box").await.is_err());
2049    }
2050
2051    #[tokio::test]
2052    async fn test_peers_device_name_strips_hostname_before_hello() {
2053        let ws_port = random_port().await;
2054        let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2055
2056        let mut peer = make_loopback_peer("nodeid-9");
2057        peer.hostname = "truffle-test-ec2-smoke".to_string();
2058        let _ = event_tx.send(NetworkPeerEvent::Joined(peer));
2059        tokio::time::sleep(Duration::from_millis(50)).await;
2060
2061        let peers = node.peers().await;
2062        assert_eq!(peers.len(), 1);
2063        // device_name reads like getLocalInfo() names, not the raw hostname.
2064        assert_eq!(peers[0].device_name, "ec2-smoke");
2065        assert_eq!(peers[0].name, "truffle-test-ec2-smoke");
2066    }
2067
2068    #[tokio::test]
2069    async fn test_resolve_peer_accepts_ip() {
2070        let ws_port = random_port().await;
2071        let (node, event_tx, _net) = make_test_node("node-1", ws_port).await;
2072
2073        let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-a")));
2074        tokio::time::sleep(Duration::from_millis(50)).await;
2075
2076        let resolved = node.resolve_peer("127.0.0.1").await.unwrap();
2077        assert_eq!(resolved.id, "peer-a");
2078        // resolve_peer_id falls back to the tailscale id when no hello
2079        // identity is populated yet.
2080        assert_eq!(node.resolve_peer_id("127.0.0.1").await.unwrap(), "peer-a");
2081    }
2082
2083    #[tokio::test]
2084    async fn test_listen_tcp_rejects_reserved_ports() {
2085        let ws_port = random_port().await;
2086        let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2087
2088        for port in [443u16, 9417] {
2089            let err = node.listen_tcp(port).await.unwrap_err();
2090            assert!(
2091                matches!(err, NodeError::ReservedPort(p) if p == port),
2092                "expected ReservedPort({port}), got: {err}"
2093            );
2094        }
2095    }
2096
2097    #[tokio::test]
2098    async fn test_listen_quic_rejects_reserved_and_ephemeral_ports() {
2099        let ws_port = random_port().await;
2100        let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2101
2102        let err = node.listen_quic(443).await.unwrap_err();
2103        assert!(matches!(err, NodeError::ReservedPort(443)));
2104
2105        let err = node.listen_quic(0).await.unwrap_err();
2106        assert!(matches!(err, NodeError::NotImplemented(_)));
2107    }
2108
2109    #[tokio::test]
2110    async fn test_bind_udp_falls_back_to_direct_socket() {
2111        // The mock provider has no UDP support, so bind_udp exercises the
2112        // direct-socket fallback and must return a usable socket.
2113        let ws_port = random_port().await;
2114        let (node, _event_tx, _net) = make_test_node("node-1", ws_port).await;
2115
2116        let socket = node.bind_udp(0).await.unwrap();
2117        let addr = socket.local_addr().unwrap();
2118        assert_ne!(addr.port(), 0);
2119    }
2120
2121    #[test]
2122    fn test_raw_alpn_scoping() {
2123        assert_eq!(
2124            crate::transport::quic::raw_alpn("demo"),
2125            b"truffle-raw.demo".to_vec()
2126        );
2127        assert_eq!(
2128            crate::transport::quic::raw_alpn(""),
2129            b"truffle-raw".to_vec()
2130        );
2131    }
2132
2133    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2134    async fn test_connect_quic_roundtrip_via_raw_listener() {
2135        let ws_port = random_port().await;
2136        let (client_node, event_tx, _net) = make_test_node("cli", ws_port).await;
2137
2138        // Raw listener on port 0 (direct-socket fallback path) with the
2139        // ALPN that connect_quic derives from the mock app_id ("test").
2140        let server_network = Arc::new(MockNetworkProvider::new("srv"));
2141        let alpn = crate::transport::quic::raw_alpn("test");
2142        let listener = crate::transport::quic::listen_raw(&server_network, 0, &alpn)
2143            .await
2144            .unwrap();
2145        let port = listener.port();
2146        assert_ne!(port, 0);
2147
2148        let echo_task = tokio::spawn(async move {
2149            let conn = listener.accept().await.expect("no incoming connection");
2150            let mut stream = conn
2151                .accept_stream()
2152                .await
2153                .unwrap()
2154                .expect("connection closed before stream");
2155            let mut received = Vec::new();
2156            while let Some(chunk) = stream.read(1024).await.unwrap() {
2157                received.extend_from_slice(&chunk);
2158            }
2159            stream.write(&received).await.unwrap();
2160            stream.finish();
2161            // Keep the connection alive until the peer has read the echo —
2162            // closing immediately could drop in-flight stream data.
2163            tokio::time::sleep(Duration::from_millis(500)).await;
2164            conn.close();
2165        });
2166
2167        // Register the "server" as a peer at 127.0.0.1 and connect by name.
2168        let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("srv-peer")));
2169        tokio::time::sleep(Duration::from_millis(50)).await;
2170
2171        let conn = client_node.connect_quic("srv-peer", port).await.unwrap();
2172        let mut stream = conn.open_stream().await.unwrap();
2173        stream.write(b"hello quic").await.unwrap();
2174        stream.finish();
2175
2176        let mut echoed = Vec::new();
2177        while let Some(chunk) = stream.read(1024).await.unwrap() {
2178            echoed.extend_from_slice(&chunk);
2179        }
2180        assert_eq!(echoed, b"hello quic");
2181
2182        echo_task.await.unwrap();
2183        conn.close();
2184    }
2185
2186    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2187    async fn test_quic_raw_alpn_mismatch_rejected() {
2188        // Cross-app connections must fail the TLS handshake outright —
2189        // ALPN is the QUIC analog of the WS hello's app_id check.
2190        let server_network = Arc::new(MockNetworkProvider::new("srv"));
2191        let listener = crate::transport::quic::listen_raw(
2192            &server_network,
2193            0,
2194            &crate::transport::quic::raw_alpn("app-a"),
2195        )
2196        .await
2197        .unwrap();
2198        let port = listener.port();
2199
2200        let client_network = Arc::new(MockNetworkProvider::new("cli"));
2201        let result = crate::transport::quic::connect_raw(
2202            &client_network,
2203            "127.0.0.1",
2204            port,
2205            &crate::transport::quic::raw_alpn("app-b"),
2206        )
2207        .await;
2208
2209        assert!(
2210            result.is_err(),
2211            "cross-app QUIC connect must fail (ALPN mismatch)"
2212        );
2213    }
2214}