use super::RpcHandler; use crate::{backup, identity, nostr_discovery}; use crate::container::docker_packages; use anyhow::Result; use nostr_sdk::ToBech32; impl RpcHandler { pub(super) async fn handle_node_did(&self) -> Result { let (data, _) = self.state_manager.get_snapshot().await; let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?; let identity_dir = self.config.data_dir.join("identity"); let nostr_pubkey = nostr_discovery::get_nostr_pubkey(&identity_dir).await.ok(); let nostr_npub = nostr_pubkey.as_ref().and_then(|hex| { nostr_sdk::PublicKey::from_hex(hex) .ok() .and_then(|pk| pk.to_bech32().ok()) }); Ok(serde_json::json!({ "did": did, "pubkey": data.server_info.pubkey, "nostr_pubkey": nostr_pubkey, "nostr_npub": nostr_npub, })) } /// Sign a challenge to prove control of the node DID (proof-of-control for onboarding). pub(super) async fn handle_node_sign_challenge( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let challenge = params .get("challenge") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing challenge string"))?; let identity_dir = self.config.data_dir.join("identity"); let identity = identity::NodeIdentity::load_or_create(&identity_dir).await?; let signature = identity.sign(challenge.as_bytes()); Ok(serde_json::json!({ "signature": signature })) } /// Create an encrypted backup of the node identity (for onboarding). pub(super) async fn handle_node_create_backup( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let passphrase = params .get("passphrase") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing passphrase"))?; let (data, _) = self.state_manager.get_snapshot().await; let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?; let identity_dir = self.config.data_dir.join("identity"); let backup = backup::create_encrypted_backup( &identity_dir, passphrase, &did, &data.server_info.pubkey, ) .await?; Ok(backup) } pub(super) async fn handle_node_tor_address(&self) -> Result { let tor_address = docker_packages::read_tor_address("archipelago"); Ok(serde_json::json!({ "tor_address": tor_address })) } pub(super) async fn handle_node_nostr_publish(&self) -> Result { if !self.config.nostr_discovery_enabled || self.config.nostr_relays.is_empty() { anyhow::bail!( "Nostr discovery disabled. Set ARCHIPELAGO_NOSTR_DISCOVERY_ENABLED=true and ARCHIPELAGO_NOSTR_RELAYS=wss://... to enable." ); } let (data, _) = self.state_manager.get_snapshot().await; let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?; let node_address = data .server_info .node_address .as_deref() .unwrap_or("archipelago://unknown"); let identity_dir = self.config.data_dir.join("identity"); let output = nostr_discovery::publish_node_identity( &identity_dir, &did, node_address, &data.server_info.version, &self.config.nostr_relays, self.config.nostr_tor_proxy.as_deref(), ) .await?; Ok(serde_json::json!({ "event_id": output.id().to_hex(), "success": output.success.len(), "failed": output.failed.len(), })) } pub(super) async fn handle_node_nostr_pubkey(&self) -> Result { let identity_dir = self.config.data_dir.join("identity"); let pubkey_hex = nostr_discovery::get_nostr_pubkey(&identity_dir).await?; let npub = nostr_sdk::PublicKey::from_hex(&pubkey_hex) .ok() .and_then(|pk| pk.to_bech32().ok()); Ok(serde_json::json!({ "nostr_pubkey": pubkey_hex, "nostr_npub": npub, })) } /// Sign a Nostr event with the node's Nostr key. /// Accepts full event object, computes NIP-01 hash, returns signed event. pub(super) async fn handle_node_nostr_sign( &self, params: Option, ) -> Result { let params = params.unwrap_or_default(); let identity_dir = self.config.data_dir.join("identity"); let pubkey_hex = nostr_discovery::get_nostr_pubkey(&identity_dir).await?; let event = params.get("event") .ok_or_else(|| anyhow::anyhow!("Missing 'event' parameter"))?; let kind = event.get("kind").and_then(|v| v.as_u64()).unwrap_or(1); let content = event.get("content").and_then(|v| v.as_str()).unwrap_or(""); let created_at = event.get("created_at").and_then(|v| v.as_u64()) .unwrap_or_else(|| std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs()); let tags = event.get("tags").cloned().unwrap_or_else(|| serde_json::json!([])); // NIP-01 serialization: [0, pubkey, created_at, kind, tags, content] let serialized = serde_json::json!([0, pubkey_hex, created_at, kind, tags, content]); let serialized_str = serde_json::to_string(&serialized)?; use sha2::{Sha256, Digest}; let hash = Sha256::digest(serialized_str.as_bytes()); let event_hash_hex = hex::encode(hash); let signature = nostr_discovery::nostr_sign_hash(&identity_dir, &event_hash_hex).await?; Ok(serde_json::json!({ "id": event_hash_hex, "pubkey": pubkey_hex, "created_at": created_at, "kind": kind, "tags": tags, "content": content, "sig": signature, })) } pub(super) async fn handle_node_nostr_verify_revoked(&self) -> Result { let identity_dir = self.config.data_dir.join("identity"); let status = nostr_discovery::verify_revocation( &identity_dir, self.config.nostr_tor_proxy.as_deref(), ) .await?; Ok(serde_json::json!({ "revoked": status.revoked, "nostr_pubkey": status.nostr_pubkey, "latest_content": status.latest_content, "error": status.error, })) } }