use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use nostr_relay_builder::builder::WritePolicyResult; /// State Policy - State event validation + ref alignment /// /// Handles validation of NIP-34 repository state events (kind 30618) /// and aligns git refs with authorized state according to GRASP-01. use nostr_relay_builder::prelude::Event; use super::PolicyContext; use crate::git::authorization::fetch_repository_data; use crate::git; use crate::nostr::events::{validate_state, RepositoryAnnouncement, RepositoryState}; /// Result of state policy evaluation #[derive(Debug)] pub enum StateResult { /// Accept: Event passes validation Accept, /// Reject: Event fails validation with reason Reject(String), } /// Policy for validating repository state events and aligning refs #[derive(Clone)] pub struct StatePolicy { ctx: PolicyContext, } impl StatePolicy { pub fn new(ctx: PolicyContext) -> Self { Self { ctx } } /// Validate a repository state event pub fn validate(&self, event: &Event) -> StateResult { match validate_state(event) { Ok(_) => StateResult::Accept, Err(e) => StateResult::Reject(e.to_string()), } } /// Process a state event: validate and align owner repositories /// /// Returns the true if git data already availale or false if added to purgatory pub async fn process_state_event(&self, event: &Event) -> Result { // Parse state to get HEAD and branch info let state = RepositoryState::from_event(event.clone()).context("Failed to parse state event")?; // Duplicate check in purgatory if self .ctx .purgatory .find_state(&state.identifier) .iter() .any(|e| e.event.id.eq(&event.id)) { tracing::debug!( "processed state event duplicate (already in purgatory): {}", event.id, ); return Ok(WritePolicyResult::Reject { status: true, message: "duplicate: in purgatory".into(), }); } // Get all repositories and state events from db with identifier let db_repo_data = fetch_repository_data(&self.ctx.database, &state.identifier).await?; // Duplicate check in db if db_repo_data.states.iter().any(|e| e.event.id.eq(&event.id)) { tracing::debug!("processed state event duplicate (in db): {}", event.id); return Ok(WritePolicyResult::Reject { status: true, message: "duplicate".into(), }); } // Check if git data is available if let Some(repo_with_git_data) = find_repo_with_git_data(&db_repo_data.announcements, &state, &self.ctx.git_data_path) { tracing::debug!( "processing state event as git data already available: {}", event.id, ); // Use unified processing function let result = crate::git::process::process_state_with_git_data( &state, &repo_with_git_data, &db_repo_data, &self.ctx.git_data_path, ); tracing::info!( identifier = %state.identifier, event_id = %event.id, repos_synced = result.repos_synced, refs_created = result.refs_created, refs_updated = result.refs_updated, refs_deleted = result.refs_deleted, "Processed state event with git data already available" ); if !result.errors.is_empty() { for error in &result.errors { tracing::warn!( identifier = %state.identifier, event_id = %event.id, error = %error, "Error processing state event" ); } } // Event will be saved and broadcast by relay builder Ok(WritePolicyResult::Accept) } else { // If no git data - add to purgatory // (add_state automatically enqueues for background sync) self.ctx .purgatory .add_state(event.clone(), state.identifier.clone(), event.pubkey); tracing::info!( "state event added to purgatory: eventid: {}, identifier: {}", state.event.id, state.identifier, ); Ok(WritePolicyResult::Reject { status: true, message: "purgatory: won't be served until git data arrives".into(), }) } } } fn find_repo_with_git_data( announcements: &[RepositoryAnnouncement], state: &RepositoryState, git_data_path: &Path, ) -> Option { for announcement in announcements { let repo_path = git_data_path.join(announcement.repo_path().clone()); if state.branches.iter().all(|branch_state| { if branch_state.commit.starts_with("ref: ") { true // ignore symlinks } else { git::oid_exists(&repo_path, &branch_state.commit) } }) && state .tags .iter() .all(|tag_state| git::oid_exists(&repo_path, &tag_state.commit)) { return Some(repo_path); } } None }