Skip to main content

nostr_bbs_mesh/
lib.rs

1//! Federation mesh kit for nostr-bbs deployments.
2//!
3//! Implements [ADR-073] (mesh federation): per-peer connection state, NIP-42
4//! AUTH session management, and kind-30033 federated-broadcast event emission.
5//!
6//! This crate provides the *substrate* — abstract traits + state machines —
7//! that concrete worker implementations (e.g. `nostr-bbs-relay-worker` mesh
8//! mode) plug into. The reference Cloudflare Worker implementation lives
9//! alongside the relay-worker; alternative deployment targets (libp2p, HTTP/3,
10//! Tailscale) implement [`MeshTransport`] themselves.
11//!
12//! # Status
13//!
14//! Sprint v9-v11: scaffold only. The mesh feature is gated by
15//! `[mesh] mode = "federated"` in the operator config (default `"standalone"`),
16//! and the relay-worker's runtime continues to short-circuit when in
17//! standalone mode. Full implementation lands in Sprint v12+ per the PRD-012
18//! Phase X3 plan.
19//!
20//! # Architecture sketch
21//!
22//! ```text
23//!     [PeerRelay A]                         [Local Relay]
24//!         │                                       │
25//!         │ wss://A/.well-known/nostr.json#mesh   │
26//!         │◀──────────────────────────────────────│
27//!         │                                       │
28//!         │   ["AUTH", <NIP-42 challenge>]        │
29//!         │──────────────────────────────────────▶│
30//!         │   ["AUTH", <signed challenge>]        │
31//!         │◀──────────────────────────────────────│
32//!         │   ["EVENT", <kind-30033 mesh anchor>] │
33//!         │──────────────────────────────────────▶│
34//!         │                                       │
35//! ```
36//!
37//! [ADR-073]: https://github.com/DreamLab-AI/nostr-rust-forum/blob/main/docs/adr/ADR-073.md
38
39#![warn(missing_docs)]
40
41use async_trait::async_trait;
42use serde::{Deserialize, Serialize};
43use thiserror::Error;
44
45/// Per-peer mesh session state.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct PeerSession {
48    /// Peer relay URL (`wss://...`).
49    pub url: String,
50    /// Peer pubkey (hex) — the relay's own NIP-42 identity.
51    pub peer_pubkey: String,
52    /// Authenticated state: `false` until NIP-42 AUTH round-trip completes.
53    pub authenticated: bool,
54    /// Unix timestamp of last successful interaction.
55    pub last_seen: u64,
56}
57
58/// Errors raised by mesh transports.
59#[derive(Debug, Error)]
60pub enum MeshError {
61    /// WebSocket / network error.
62    #[error("transport: {0}")]
63    Transport(String),
64    /// NIP-42 AUTH handshake failed.
65    #[error("AUTH failed: {0}")]
66    Auth(String),
67    /// Peer not yet authenticated for this operation.
68    #[error("peer not authenticated")]
69    NotAuthenticated,
70    /// Serialization error.
71    #[error("serialization: {0}")]
72    Serde(String),
73}
74
75/// Abstract transport for connecting to a peer relay.
76///
77/// Cloudflare Workers provide a WebSocket impl; libp2p and other targets
78/// provide their own. The mesh state machine on top is transport-agnostic.
79#[async_trait(?Send)]
80pub trait MeshTransport {
81    /// Connect to a peer relay.
82    async fn connect(&self, url: &str) -> Result<PeerSession, MeshError>;
83
84    /// Send a NIP-42 AUTH response with the local relay's signed challenge.
85    async fn authenticate(
86        &self,
87        session: &mut PeerSession,
88        signed_challenge: &str,
89    ) -> Result<(), MeshError>;
90
91    /// Broadcast a kind-30033 federated-broadcast event to a peer relay.
92    async fn broadcast_kind30033(
93        &self,
94        session: &PeerSession,
95        event_json: &str,
96    ) -> Result<(), MeshError>;
97}
98
99/// Build a kind-30033 mesh anchor event payload (signing + serialization
100/// happens upstream via `nostr-bbs-core`).
101///
102/// The `d` tag identifies the source relay (canonical hostname); event
103/// content carries a JSON array of mirrored event-IDs in this batch.
104pub fn mesh_anchor_tags(source_relay: &str) -> Vec<Vec<String>> {
105    vec![vec!["d".to_string(), source_relay.to_string()]]
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn mesh_anchor_emits_d_tag() {
114        let tags = mesh_anchor_tags("wss://example.com");
115        assert_eq!(tags.len(), 1);
116        assert_eq!(tags[0][0], "d");
117        assert_eq!(tags[0][1], "wss://example.com");
118    }
119}