upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/nostr/policy/pr_event.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-04 15:42:00 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-04 15:42:00 +0000
commit819866330c7e2f535a155d1d7efaf2e12dc15dc2 (patch)
treed84c8361811544aad9cad089c0358b9028c8fb80 /src/nostr/policy/pr_event.rs
parentfd0c87c787d0626b3546fa571541c9c809711821 (diff)
refactor: split Nip34WritePolicy into focused sub-policies
Split the ~900 line Nip34WritePolicy into focused sub-policies for improved testability and maintainability: - AnnouncementPolicy - Repository announcement validation - StatePolicy - State event validation + ref alignment - PrEventPolicy - PR/PR Update validation - RelatedEventPolicy - Forward/backward reference checking The main Nip34WritePolicy now delegates to these sub-policies via a shared PolicyContext that provides domain, database, and git_data_path. Also updates: - README.md: Accurate project structure reflecting actual implementation - docs/learnings: Marks this technical debt item as complete
Diffstat (limited to 'src/nostr/policy/pr_event.rs')
-rw-r--r--src/nostr/policy/pr_event.rs198
1 files changed, 198 insertions, 0 deletions
diff --git a/src/nostr/policy/pr_event.rs b/src/nostr/policy/pr_event.rs
new file mode 100644
index 0000000..fee9a2a
--- /dev/null
+++ b/src/nostr/policy/pr_event.rs
@@ -0,0 +1,198 @@
1/// PR Event Policy - PR/PR Update validation
2///
3/// Handles validation of NIP-34 PR events (kind 1618) and PR Update events (kind 1619)
4/// according to GRASP-01 specification.
5use nostr_relay_builder::prelude::{Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag};
6
7use super::PolicyContext;
8use crate::git;
9use crate::nostr::events::{RepositoryAnnouncement, KIND_REPOSITORY_ANNOUNCEMENT};
10
11/// Policy for validating PR and PR Update events
12#[derive(Clone)]
13pub struct PrEventPolicy {
14 ctx: PolicyContext,
15}
16
17impl PrEventPolicy {
18 pub fn new(ctx: PolicyContext) -> Self {
19 Self { ctx }
20 }
21
22 /// Validate refs/nostr/<event-id> ref against a PR or PR Update event's `c` tag
23 ///
24 /// When a PR event (kind 1618) or PR Update event (kind 1619) is received,
25 /// this checks if a corresponding refs/nostr/<event-id> ref exists in the
26 /// repository and validates that it points to the correct commit (from the
27 /// `c` tag). If the ref exists but points to a different commit, the ref is
28 /// deleted.
29 ///
30 /// PR and PR Update events can have multiple `a` tags to update multiple
31 /// repositories simultaneously.
32 ///
33 /// This is part of GRASP-01 compliance: ensuring refs/nostr refs are consistent
34 /// with their corresponding events.
35 ///
36 /// # Returns
37 /// Ok(Some(n)) if n refs were deleted, Ok(None) if no action taken, Err on failure
38 pub async fn validate_nostr_ref(&self, event: &Event) -> Result<Option<usize>, String> {
39 let event_id = event.id.to_hex();
40
41 // Extract the `c` tag (commit hash) from the PR event
42 let expected_commit = event.tags.iter().find_map(|tag| {
43 let tag_vec = tag.clone().to_vec();
44 if tag_vec.len() >= 2 && tag_vec[0] == "c" {
45 Some(tag_vec[1].clone())
46 } else {
47 None
48 }
49 });
50
51 let expected_commit = match expected_commit {
52 Some(c) => c,
53 None => {
54 tracing::debug!(
55 "PR event {} has no 'c' tag, skipping ref validation",
56 event_id
57 );
58 return Ok(None);
59 }
60 };
61
62 // Extract ALL `a` tags (repository references) from the PR event
63 // PR events can reference multiple repositories
64 // Format: 30617:<pubkey>:<identifier>
65 let repo_refs: Vec<String> = event
66 .tags
67 .iter()
68 .filter_map(|tag| {
69 let tag_vec = tag.clone().to_vec();
70 if tag_vec.len() >= 2 && tag_vec[0] == "a" && tag_vec[1].starts_with("30617:") {
71 Some(tag_vec[1].clone())
72 } else {
73 None
74 }
75 })
76 .collect();
77
78 if repo_refs.is_empty() {
79 tracing::debug!(
80 "PR event {} has no repo 'a' tags, skipping ref validation",
81 event_id
82 );
83 return Ok(None);
84 }
85
86 let mut deleted_count = 0;
87
88 // Process each repository reference
89 for repo_ref in repo_refs {
90 // Parse the repo reference: 30617:<pubkey>:<identifier>
91 let parts: Vec<&str> = repo_ref.split(':').collect();
92 if parts.len() < 3 {
93 tracing::debug!(
94 "PR event {} has invalid 'a' tag format: {}",
95 event_id,
96 repo_ref
97 );
98 continue;
99 }
100
101 let repo_pubkey = match PublicKey::from_hex(parts[1]) {
102 Ok(pk) => pk,
103 Err(_) => {
104 tracing::debug!(
105 "PR event {} has invalid pubkey in 'a' tag: {}",
106 event_id,
107 parts[1]
108 );
109 continue;
110 }
111 };
112 let identifier = parts[2];
113
114 // Look up repository announcement to get the npub for path
115 let filter = Filter::new()
116 .kind(Kind::from(KIND_REPOSITORY_ANNOUNCEMENT))
117 .author(repo_pubkey)
118 .custom_tag(
119 SingleLetterTag::lowercase(Alphabet::D),
120 identifier.to_string(),
121 );
122
123 let announcements: Vec<Event> = match self.ctx.database.query(filter).await {
124 Ok(events) => events.into_iter().collect(),
125 Err(e) => {
126 tracing::warn!(
127 "Failed to query for repository announcement for PR {}: {}",
128 event_id,
129 e
130 );
131 continue;
132 }
133 };
134
135 if announcements.is_empty() {
136 tracing::debug!(
137 "No repository announcement found for PR event {} (repo {}:{})",
138 event_id,
139 repo_pubkey.to_hex(),
140 identifier
141 );
142 continue;
143 }
144
145 // Process each matching announcement (there could be multiple)
146 for announcement_event in announcements {
147 let announcement = match RepositoryAnnouncement::from_event(announcement_event) {
148 Ok(a) => a,
149 Err(e) => {
150 tracing::warn!(
151 "Failed to parse announcement for PR {} validation: {}",
152 event_id,
153 e
154 );
155 continue;
156 }
157 };
158
159 // Build repository path
160 let repo_path = self.ctx.git_data_path.join(announcement.repo_path());
161
162 // Validate the ref
163 match git::validate_nostr_ref(&repo_path, &event_id, &expected_commit) {
164 Ok(true) => {
165 tracing::info!(
166 "Deleted mismatched refs/nostr/{} in {} (expected commit {})",
167 event_id,
168 repo_path.display(),
169 expected_commit
170 );
171 deleted_count += 1;
172 }
173 Ok(false) => {
174 tracing::debug!(
175 "refs/nostr/{} in {} is valid or doesn't exist",
176 event_id,
177 repo_path.display()
178 );
179 }
180 Err(e) => {
181 tracing::warn!(
182 "Failed to validate refs/nostr/{} in {}: {}",
183 event_id,
184 repo_path.display(),
185 e
186 );
187 }
188 }
189 }
190 }
191
192 if deleted_count > 0 {
193 Ok(Some(deleted_count))
194 } else {
195 Ok(None)
196 }
197 }
198} \ No newline at end of file