archy/core/archipelago/src/api/rpc/federation.rs
Dorian e74bf9bcfd feat: propagate Tor address rotation to Nostr relays and federation peers
After rotation, spawns background task that publishes updated .onion to
Nostr relays and sends federation.peer-address-changed RPC to all peers
over Tor. Peers update their nodes.json with the new address.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 00:08:16 +00:00

369 lines
13 KiB
Rust

use super::RpcHandler;
use crate::federation::{self, FederatedNode, TrustLevel};
use crate::identity;
use anyhow::Result;
use tracing::info;
impl RpcHandler {
/// federation.invite — Generate an invite code containing our DID + onion for a peer.
pub(super) async fn handle_federation_invite(&self) -> Result<serde_json::Value> {
let (data, _) = self.state_manager.get_snapshot().await;
let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
let onion = data
.server_info
.tor_address
.clone()
.unwrap_or_default();
let pubkey = data.server_info.pubkey.clone();
if onion.is_empty() {
anyhow::bail!("Tor address not available. Tor may not be running.");
}
let code = federation::create_invite(&self.config.data_dir, &did, &onion, &pubkey).await?;
info!(did = %did, "Generated federation invite");
Ok(serde_json::json!({
"code": code,
"did": did,
"onion": onion,
}))
}
/// federation.join — Accept an invite code and establish federation with the remote node.
pub(super) async fn handle_federation_join(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let code = params
.get("code")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'code' parameter"))?;
let (data, _) = self.state_manager.get_snapshot().await;
let local_did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
let local_onion = data.server_info.tor_address.clone().unwrap_or_default();
let local_pubkey = data.server_info.pubkey.clone();
let node = federation::accept_invite(
&self.config.data_dir,
code,
&local_did,
&local_onion,
&local_pubkey,
)
.await?;
info!(peer_did = %node.did, "Joined federation with peer");
Ok(serde_json::json!({
"joined": true,
"node": {
"did": node.did,
"onion": node.onion,
"pubkey": node.pubkey,
"trust_level": node.trust_level.to_string(),
}
}))
}
/// federation.list-nodes — List all federated nodes with their status and last state.
pub(super) async fn handle_federation_list_nodes(&self) -> Result<serde_json::Value> {
let nodes = federation::load_nodes(&self.config.data_dir).await?;
let nodes_json: Vec<serde_json::Value> = nodes
.iter()
.map(|n| {
let mut obj = serde_json::json!({
"did": n.did,
"pubkey": n.pubkey,
"onion": n.onion,
"trust_level": n.trust_level.to_string(),
"added_at": n.added_at,
});
if let Some(name) = &n.name {
obj["name"] = serde_json::json!(name);
}
if let Some(last_seen) = &n.last_seen {
obj["last_seen"] = serde_json::json!(last_seen);
}
if let Some(state) = &n.last_state {
obj["last_state"] = serde_json::to_value(state).unwrap_or_default();
}
obj
})
.collect();
Ok(serde_json::json!({ "nodes": nodes_json }))
}
/// federation.remove-node — Remove a node from the federation by DID.
pub(super) async fn handle_federation_remove_node(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let did = params
.get("did")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'did' parameter"))?;
let nodes = federation::remove_node(&self.config.data_dir, did).await?;
info!(did = %did, "Removed node from federation");
Ok(serde_json::json!({
"removed": true,
"nodes_remaining": nodes.len(),
}))
}
/// federation.set-trust — Change trust level for a federated node.
pub(super) async fn handle_federation_set_trust(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let did = params
.get("did")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'did' parameter"))?;
let trust_str = params
.get("trust_level")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'trust_level' parameter"))?;
let trust = match trust_str {
"trusted" => TrustLevel::Trusted,
"observer" => TrustLevel::Observer,
"untrusted" => TrustLevel::Untrusted,
_ => anyhow::bail!("Invalid trust level: {} (expected trusted/observer/untrusted)", trust_str),
};
federation::set_trust_level(&self.config.data_dir, did, trust).await?;
Ok(serde_json::json!({
"updated": true,
"did": did,
"trust_level": trust.to_string(),
}))
}
/// federation.sync-state — Manually trigger state sync with all federated peers.
pub(super) async fn handle_federation_sync_state(&self) -> Result<serde_json::Value> {
let nodes = federation::load_nodes(&self.config.data_dir).await?;
if nodes.is_empty() {
return Ok(serde_json::json!({
"synced": 0,
"failed": 0,
"results": [],
}));
}
let (data, _) = self.state_manager.get_snapshot().await;
let local_did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
let identity_dir = self.config.data_dir.join("identity");
let node_identity = identity::NodeIdentity::load_or_create(&identity_dir).await?;
let mut synced = 0u32;
let mut failed = 0u32;
let mut results = Vec::new();
for node in &nodes {
if node.trust_level == TrustLevel::Untrusted {
continue;
}
let did_clone = local_did.clone();
match federation::sync_with_peer(
&self.config.data_dir,
node,
&did_clone,
|bytes| node_identity.sign(bytes),
)
.await
{
Ok(state) => {
synced += 1;
results.push(serde_json::json!({
"did": node.did,
"status": "ok",
"apps": state.apps.len(),
}));
}
Err(e) => {
failed += 1;
results.push(serde_json::json!({
"did": node.did,
"status": "error",
"error": e.to_string(),
}));
}
}
}
Ok(serde_json::json!({
"synced": synced,
"failed": failed,
"results": results,
}))
}
/// federation.get-state — Return this node's state snapshot (called by peers during sync).
pub(super) async fn handle_federation_get_state(&self) -> Result<serde_json::Value> {
let (data, _) = self.state_manager.get_snapshot().await;
// Build app statuses from package_data
let apps: Vec<federation::AppStatus> = data
.package_data
.iter()
.map(|(id, pkg)| federation::AppStatus {
id: id.clone(),
status: format!("{:?}", pkg.state).to_lowercase(),
version: Some(pkg.manifest.version.clone()),
})
.collect();
let tor_active = data.server_info.tor_address.is_some();
let state = federation::build_local_state(
apps, 0.0, 0, 0, 0, 0, 0, tor_active,
);
Ok(serde_json::to_value(&state)?)
}
/// federation.peer-joined — Called by a remote peer after they accept our invite.
pub(super) async fn handle_federation_peer_joined(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let did = params
.get("did")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'did'"))?;
let onion = params
.get("onion")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'onion'"))?;
let pubkey = params
.get("pubkey")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'pubkey'"))?;
let nodes = federation::load_nodes(&self.config.data_dir).await?;
if nodes.iter().any(|n| n.did == did) {
return Ok(serde_json::json!({ "accepted": true, "already_known": true }));
}
let node = FederatedNode {
did: did.to_string(),
pubkey: pubkey.to_string(),
onion: onion.to_string(),
name: None,
trust_level: TrustLevel::Trusted,
added_at: chrono::Utc::now().to_rfc3339(),
last_seen: None,
last_state: None,
};
federation::add_node(&self.config.data_dir, node).await?;
info!(peer_did = %did, "Peer joined our federation");
Ok(serde_json::json!({ "accepted": true }))
}
/// federation.deploy-app — Deploy an app to a remote federated node.
pub(super) async fn handle_federation_deploy_app(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let peer_did = params
.get("did")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'did' (target node)"))?;
let app_id = params
.get("app_id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'app_id'"))?;
let version = params
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("latest");
let marketplace_url = params
.get("marketplace_url")
.and_then(|v| v.as_str())
.unwrap_or("");
let nodes = federation::load_nodes(&self.config.data_dir).await?;
let peer = nodes
.iter()
.find(|n| n.did == peer_did)
.ok_or_else(|| anyhow::anyhow!("No federated node with DID {}", peer_did))?;
let (data, _) = self.state_manager.get_snapshot().await;
let local_did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
let identity_dir = self.config.data_dir.join("identity");
let node_identity = identity::NodeIdentity::load_or_create(&identity_dir).await?;
let result = federation::deploy_to_peer(
peer,
app_id,
version,
marketplace_url,
&local_did,
|bytes| node_identity.sign(bytes),
)
.await?;
info!(app = %app_id, peer = %peer_did, "Deployed app to federated peer");
Ok(result)
}
/// federation.peer-address-changed — A peer notifies us that their .onion changed.
pub(super) async fn handle_federation_peer_address_changed(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let did = params
.get("did")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing did"))?;
let new_onion = params
.get("new_onion")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing new_onion"))?;
// Load existing nodes, find the peer by DID, update their onion
let mut nodes = federation::load_nodes(&self.config.data_dir).await?;
let found = nodes.iter_mut().find(|n| n.did == did);
match found {
Some(node) => {
let old = node.onion.clone();
node.onion = new_onion.to_string();
federation::save_nodes(&self.config.data_dir, &nodes).await?;
info!(did = %did, old_onion = %old, new_onion = %new_onion, "Updated federated peer address");
Ok(serde_json::json!({
"updated": true,
"did": did,
"old_onion": old,
"new_onion": new_onion,
}))
}
None => {
info!(did = %did, "Received address change from unknown peer — ignoring");
Ok(serde_json::json!({
"updated": false,
"reason": "Unknown peer DID",
}))
}
}
}
}