//! Federation state sync and remote deployment. //! //! Requests prefer FIPS (direct ULA dial, ~LAN latency) and fall back to //! Tor on any network failure. See `crate::fips::dial::PeerRequest` for //! the fallback mechanics. use anyhow::{Context, Result}; use std::path::Path; use super::storage::update_node_state; use super::types::{AppStatus, FederatedNode, NodeStateSnapshot, TrustLevel}; use crate::fips::dial::PeerRequest; /// Sync state with a single federated peer. Tries FIPS first; falls back /// to Tor on any transport-level failure. pub async fn sync_with_peer( data_dir: &Path, peer: &FederatedNode, local_did: &str, sign_fn: impl FnOnce(&[u8]) -> String, ) -> Result { let timestamp = chrono::Utc::now().to_rfc3339(); let signature = sign_fn(timestamp.as_bytes()); let body = serde_json::json!({ "method": "federation.get-state", "params": {} }); let (resp, transport) = PeerRequest::new(peer.fips_npub.as_deref(), &peer.onion, "/rpc/v1") .header("X-Federation-DID", local_did) .header("X-Federation-Sig", signature) .header("X-Federation-Timestamp", timestamp) .timeout(std::time::Duration::from_secs(30)) .send_json(&body) .await .context("Failed to reach federated peer")?; if !resp.status().is_success() { anyhow::bail!("Peer returned {} (via {})", resp.status(), transport); } let result: serde_json::Value = resp.json().await.context("Invalid response from peer")?; let state_val = result .get("result") .ok_or_else(|| anyhow::anyhow!("No result in peer response"))?; let state: NodeStateSnapshot = serde_json::from_value(state_val.clone()).context("Failed to parse peer state")?; update_node_state(data_dir, &peer.did, state.clone()).await?; Ok(state) } /// Build the local node's state snapshot for sharing with peers. pub fn build_local_state( apps: Vec, cpu: f64, mem_used: u64, mem_total: u64, disk_used: u64, disk_total: u64, uptime: u64, tor_active: bool, server_name: Option, nostr_npub: Option, ) -> NodeStateSnapshot { NodeStateSnapshot { timestamp: chrono::Utc::now().to_rfc3339(), node_name: server_name, apps, cpu_usage_percent: Some(cpu), mem_used_bytes: Some(mem_used), mem_total_bytes: Some(mem_total), disk_used_bytes: Some(disk_used), disk_total_bytes: Some(disk_total), uptime_secs: Some(uptime), tor_active: Some(tor_active), nostr_npub, } } /// Deploy an app to a remote federated peer over Tor. /// Only works if the peer is trusted and the app exists in our marketplace. pub async fn deploy_to_peer( peer: &FederatedNode, app_id: &str, version: &str, marketplace_url: &str, local_did: &str, sign_fn: impl FnOnce(&[u8]) -> String, ) -> Result { if peer.trust_level != TrustLevel::Trusted { anyhow::bail!( "Can only deploy to trusted peers (current: {})", peer.trust_level ); } let timestamp = chrono::Utc::now().to_rfc3339(); let signature = sign_fn(timestamp.as_bytes()); let body = serde_json::json!({ "method": "package.install", "params": { "id": app_id, "version": version, "marketplace-url": marketplace_url, } }); let (resp, transport) = PeerRequest::new(peer.fips_npub.as_deref(), &peer.onion, "/rpc/v1") .header("X-Federation-DID", local_did) .header("X-Federation-Sig", signature) .header("X-Federation-Timestamp", timestamp) .timeout(std::time::Duration::from_secs(120)) .send_json(&body) .await .context("Failed to reach federated peer for deploy")?; if !resp.status().is_success() { anyhow::bail!("Remote node returned HTTP {} (via {})", resp.status(), transport); } let result: serde_json::Value = resp.json().await.context("Invalid response from peer")?; if let Some(err) = result.get("error") { if !err.is_null() { let msg = err .get("message") .and_then(|m| m.as_str()) .unwrap_or("Unknown remote error"); anyhow::bail!("Remote node refused deploy: {}", msg); } } Ok(serde_json::json!({ "deployed": true, "app_id": app_id, "peer_did": peer.did, "peer_onion": peer.onion, })) } #[cfg(test)] mod tests { use super::*; #[test] fn test_build_local_state() { let state = build_local_state( vec![AppStatus { id: "lnd".to_string(), status: "running".to_string(), version: Some("0.18".to_string()), }], 25.5, 2_000_000_000, 8_000_000_000, 100_000_000_000, 500_000_000_000, 3600, true, Some("Test Node".to_string()), None, ); assert_eq!(state.apps.len(), 1); assert_eq!(state.cpu_usage_percent, Some(25.5)); assert_eq!(state.tor_active, Some(true)); assert_eq!(state.node_name, Some("Test Node".to_string())); } }