From d60fa03de6edae0667a93ac36be4206e76255a2c Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 22:07:55 +0530 Subject: Add NIP-46 remote signing for kind:30618 state events - nip46.rs: full NIP-46 client with session management, NIP-04 encrypted relay-based communication, oneshot response awaiting - db.rs: nip46_sessions table, upsert/get methods - config.rs: Nip46Config with relays + signing_timeout_secs - git_mirror.rs: builds unsigned kind:30618 state event from bare repo refs, signs via NIP-46 before push, publishes to target server relay - http_health.rs: exposes NIP-46 session status in health endpoint - main.rs: wires NIP-46 client into daemon startup, passes to mirror_cycle --- src/git_mirror.rs | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) (limited to 'src/git_mirror.rs') diff --git a/src/git_mirror.rs b/src/git_mirror.rs index 6866de3..c486887 100644 --- a/src/git_mirror.rs +++ b/src/git_mirror.rs @@ -1,9 +1,12 @@ use crate::db::MirrorDb; use crate::discovery::DiscoveredRepo; use crate::health::GraspServer; +use crate::nip46::Nip46Client; use anyhow::{Context, Result}; use git2::RemoteCallbacks; +use nostr_sdk::prelude::*; use std::path::Path; +use std::sync::Arc; pub struct GitMirror { mirror_dir: std::path::PathBuf, @@ -27,6 +30,7 @@ impl GitMirror { db: &MirrorDb, repo: &DiscoveredRepo, target_servers: &[GraspServer], + nip46_client: Option<&Arc>, ) -> Result<()> { if target_servers.is_empty() { tracing::debug!( @@ -43,6 +47,25 @@ impl GitMirror { self.clone_bare(&repo_path, &repo.clone_urls)?; } + let state_event = match self.build_state_event(&repo_path, repo, nip46_client).await { + Ok(Some(event)) => Some(event), + Ok(None) => { + tracing::warn!( + identifier = %repo.identifier, + "could not build state event — push may be rejected by GRASP servers" + ); + None + } + Err(e) => { + tracing::error!( + identifier = %repo.identifier, + error = %e, + "failed to build state event" + ); + None + } + }; + for server in target_servers { let target_url = server.clone_url(&pk_hex, &repo.identifier); @@ -53,6 +76,31 @@ impl GitMirror { "mirroring git data" ); + if let Some(ref event) = state_event { + let relay_url = server.relay_url(); + if let Ok(url) = RelayUrl::parse(&relay_url) { + let urls = vec![url]; + if let Err(e) = nip46_client + .map_or(Ok(()), |_| { + Err(anyhow::anyhow!("need nostr client to send state event")) + }) + { + let _ = e; + } + + let nostr_client = nostr_sdk::Client::default(); + let _ = nostr_client.add_relay(&relay_url).await; + nostr_client.connect().await; + if let Err(e) = nostr_client.send_event_to(urls, event.clone()).await { + tracing::warn!( + server = %server.domain, + error = %e, + "failed to publish state event to server relay" + ); + } + } + } + let repo_id = db.get_all_repos().await.ok().and_then(|repos| { repos .iter() @@ -88,6 +136,62 @@ impl GitMirror { Ok(()) } + async fn build_state_event( + &self, + repo_path: &std::path::PathBuf, + repo: &DiscoveredRepo, + nip46_client: Option<&Arc>, + ) -> Result> { + let nip46 = match nip46_client { + Some(c) => c, + None => return Ok(None), + }; + + let git_repo = git2::Repository::open(repo_path) + .with_context(|| format!("failed to open bare repo at {:?}", repo_path))?; + + let mut tags: Vec = vec![ + Tag::custom(TagKind::Custom("d".into()), [&repo.identifier]), + ]; + + let refs = git_repo.references()?; + for reference in refs { + let reference = reference?; + let name = reference.name().unwrap_or(""); + if name.is_empty() { + continue; + } + if let Some(oid) = reference.target() { + tags.push(Tag::custom( + TagKind::Custom("ref".into()), + [name, &oid.to_string()], + )); + } + } + + let builder = EventBuilder::new(Kind::Custom(30618), "").tags(tags); + let unsigned = builder.build(repo.pubkey); + + match nip46.sign_event(&repo.pubkey, &unsigned).await { + Ok(signed) => { + tracing::info!( + identifier = %repo.identifier, + event_id = %signed.id.to_hex(), + "signed kind:30618 state event via NIP-46" + ); + Ok(Some(signed)) + } + Err(e) => { + tracing::error!( + identifier = %repo.identifier, + error = %e, + "NIP-46 signing failed for state event" + ); + Err(e) + } + } + } + fn clone_bare(&self, repo_path: &Path, clone_urls: &[String]) -> Result<()> { if let Some(parent) = repo_path.parent() { std::fs::create_dir_all(parent) -- cgit v1.2.3