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}