1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
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<WritePolicyResult> {
// 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<PathBuf> {
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
}
|