From 8816a192c95cf539b65975469a2d61aed46f0414 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 16:11:05 +0530 Subject: feat: initial implementation of grasp-mirror daemon GRASP mirror daemon that discovers repos from watched npubs and mirrors git data + Nostr events across all known GRASP servers for redundancy. Features: - Configurable npub watch list via .env (MIRROR_NPUBS) - TOML config for GRASP server list, index relays, storage paths - NIP-11 verification of GRASP servers on startup - Discovery of repos via kind:30617 announcements on index relays - Git mirroring (bare clone + push --mirror) to missing GRASP servers - Nostr event forwarding to all GRASP server embedded relays - SQLite state tracking for sync status and event dedup - Optional signing key for updating announcements with new clone URLs - CLI subcommands: daemon, status, verify, mirror-once Architecture: config.rs - TOML + .env config loading db.rs - SQLite state tracking health.rs - NIP-11 GRASP server verification discovery.rs - Relay subscription, kind:30617 parsing git_mirror.rs - Bare clone + push to GRASP servers nostr_mirror.rs - Event forwarding to all GRASP relays signing.rs - Optional announcement updates main.rs - CLI entry point, daemon loop --- src/signing.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/signing.rs (limited to 'src/signing.rs') diff --git a/src/signing.rs b/src/signing.rs new file mode 100644 index 0000000..7ec796a --- /dev/null +++ b/src/signing.rs @@ -0,0 +1,76 @@ +use crate::discovery::DiscoveredRepo; +use crate::health::GraspServer; +use anyhow::{Context, Result}; +use nostr_sdk::prelude::*; + +pub struct OptionalSigner { + keys: Option, +} + +impl OptionalSigner { + pub fn none() -> Self { + Self { keys: None } + } + + pub fn from_nsec(nsec: &str) -> Result { + let keys = Keys::parse(nsec).context("failed to parse nsec")?; + Ok(Self { keys: Some(keys) }) + } + + pub fn is_available(&self) -> bool { + self.keys.is_some() + } + + pub async fn build_updated_announcement( + &self, + original: &DiscoveredRepo, + additional_servers: &[GraspServer], + ) -> Result> { + let keys = match &self.keys { + Some(k) => k, + None => { + tracing::debug!("no signer available, skipping announcement update"); + return Ok(None); + } + }; + + if additional_servers.is_empty() { + return Ok(None); + } + + let pk_hex = original.pubkey.to_hex(); + + let mut new_clone_urls: Vec = original.clone_urls.clone(); + for server in additional_servers { + let url = server.clone_url(&pk_hex, &original.identifier); + if !new_clone_urls.contains(&url) { + new_clone_urls.push(url); + } + } + + let mut tags: Vec = vec![ + Tag::custom(TagKind::Custom("d".into()), [&original.identifier]), + ]; + + for url in &new_clone_urls { + tags.push(Tag::custom(TagKind::Custom("clone".into()), [url.as_str()])); + } + for url in &original.relay_urls { + tags.push(Tag::custom( + TagKind::Custom("relays".into()), + [url.as_str()], + )); + } + + let builder = EventBuilder::new(Kind::Custom(30617), "").tags(tags); + let event = builder.sign_with_keys(keys)?; + + tracing::info!( + identifier = %original.identifier, + added = additional_servers.len(), + "built updated announcement with additional clone URLs" + ); + + Ok(Some(event)) + } +} -- cgit v1.2.3