upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-30 13:20:55 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-30 13:25:30 +0000
commit08ab20509b9c730d3db98dd6e9deb5e2b548979e (patch)
treec23f9be98f0647e639a64ac6936a24a3e4cf361b
parent70d0197e85ae4ef85202781f6d2dc9e76bd508b3 (diff)
purgatory: improve git authorization integetration
-rw-r--r--src/git/authorization.rs177
-rw-r--r--src/git/handlers.rs268
-rw-r--r--src/http/mod.rs2
-rw-r--r--src/main.rs4
4 files changed, 230 insertions, 221 deletions
diff --git a/src/git/authorization.rs b/src/git/authorization.rs
index fbeaf9e..9bcbdf8 100644
--- a/src/git/authorization.rs
+++ b/src/git/authorization.rs
@@ -28,9 +28,11 @@
28//! - HEAD updates triggered by state events in builder.rs (event policy) 28//! - HEAD updates triggered by state events in builder.rs (event policy)
29 29
30use anyhow::{anyhow, Result}; 30use anyhow::{anyhow, Result};
31use hyper::body::Bytes;
31use nostr_relay_builder::prelude::*; 32use nostr_relay_builder::prelude::*;
32use nostr_sdk::{EventId, ToBech32}; 33use nostr_sdk::{EventId, ToBech32};
33use std::collections::{HashMap, HashSet}; 34use std::collections::{HashMap, HashSet};
35use std::sync::Arc;
34use tracing::{debug, info, warn}; 36use tracing::{debug, info, warn};
35 37
36use crate::nostr::builder::SharedDatabase; 38use crate::nostr::builder::SharedDatabase;
@@ -38,6 +40,181 @@ use crate::nostr::events::{
38 RepositoryAnnouncement, RepositoryState, KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT, 40 RepositoryAnnouncement, RepositoryState, KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT,
39 KIND_REPOSITORY_STATE, 41 KIND_REPOSITORY_STATE,
40}; 42};
43use crate::purgatory::Purgatory;
44
45/// Perform GRASP authorization for a push operation
46///
47/// This function queries the database directly (not via WebSocket):
48/// 1. Parses the pushed refs from the git pack protocol
49/// 2. Separates refs/nostr/ refs from normal refs
50/// 3. For normal refs: validates against state events in purgatory
51/// 4. For refs/nostr/ refs: validates event ID format and collects PR/PR-update events from purgatory
52/// 5. Returns all authorizing events (state + PR/PR-update) in the result
53pub async fn authorize_push(
54 database: &SharedDatabase,
55 identifier: &str,
56 owner_pubkey: &str,
57 request_body: &Bytes,
58 purgatory: &Arc<Purgatory>,
59 repo_path: &std::path::Path,
60) -> anyhow::Result<AuthorizationResult> {
61 debug!(
62 "Authorizing push for {} owned by {} via database query",
63 identifier, owner_pubkey
64 );
65
66 // Parse refs from the push request
67 let pushed_refs = parse_pushed_refs(request_body);
68 debug!("Parsed {} refs from push request", pushed_refs.len());
69 for (old_oid, new_oid, ref_name) in &pushed_refs {
70 debug!(" {} {} -> {}", ref_name, old_oid, new_oid);
71 }
72
73 // Separate refs/nostr/ refs from state refs
74 let (nostr_refs, state_refs): (Vec<_>, Vec<_>) = pushed_refs
75 .iter()
76 .partition(|(_, _, ref_name)| ref_name.starts_with("refs/nostr/"));
77
78 // Collect all purgatory events that authorize this push
79 let mut purgatory_events = Vec::new();
80
81 // Handle refs/nostr/ refs - validate and collect PR/PR-update events from purgatory
82 if !nostr_refs.is_empty() {
83 debug!(
84 "Found {} refs/nostr/ refs - validating and collecting from purgatory",
85 nostr_refs.len()
86 );
87
88 for (_, new_oid, ref_name) in &nostr_refs {
89 // Extract event ID from ref name
90 if let Some(event_id_hex) = ref_name.strip_prefix("refs/nostr/") {
91 // Validate event ID format
92 if EventId::parse(event_id_hex).is_err() {
93 warn!("Invalid event ID format in ref: {}", ref_name);
94 return Ok(AuthorizationResult::denied(format!(
95 "Invalid event ID format in ref: {}",
96 ref_name
97 )));
98 }
99
100 // Check purgatory for PR event
101 if let Some(entry) = purgatory.find_pr(event_id_hex) {
102 if let Some(event) = entry.event {
103 // Verify commit matches
104 if entry.commit == *new_oid {
105 debug!(
106 "Found matching PR event {} in purgatory for ref {}",
107 event_id_hex, ref_name
108 );
109 purgatory_events.push(event);
110 } else {
111 warn!(
112 "PR event {} in purgatory has commit mismatch: expected {}, got {}",
113 event_id_hex, entry.commit, new_oid
114 );
115 return Ok(AuthorizationResult::denied(format!(
116 "PR event {} commit mismatch: expected {}, got {}",
117 event_id_hex, entry.commit, new_oid
118 )));
119 }
120 } else {
121 // Placeholder exists - allow push (git-data-first scenario)
122 debug!(
123 "Found placeholder already for PR event {} in purgatory - as we dont have the event and therefore dont know the required commit_id we allow overwriting with a different commit_id",
124 event_id_hex
125 );
126 }
127 } else {
128 // No entry in purgatory - check database for existing event
129 let nostr_refs_owned = vec![(String::new(), new_oid.clone(), ref_name.clone())];
130 if let Err(e) = validate_nostr_ref_pushes(database, &nostr_refs_owned).await {
131 warn!("refs/nostr/ validation failed: {}", e);
132 return Ok(AuthorizationResult::denied(format!(
133 "refs/nostr/ validation failed: {}",
134 e
135 )));
136 }
137 debug!(
138 "No purgatory entry for {} - validated against database",
139 event_id_hex
140 );
141 }
142 }
143 }
144 }
145
146 // Handle normal refs - validate against state events
147 if !state_refs.is_empty() {
148 debug!(
149 "Found {} non-refs/nostr/ refs - checking state authorization",
150 state_refs.len()
151 );
152
153 let auth_result = get_state_authorization_for_specific_owner_repo(
154 database,
155 identifier,
156 owner_pubkey,
157 purgatory,
158 &pushed_refs, //it would be better to accept state_refs but thats in different format
159 repo_path,
160 )
161 .await?;
162
163 if !auth_result.authorized {
164 return Ok(auth_result);
165 }
166
167 // Collect state events from purgatory
168 purgatory_events.extend(auth_result.purgatory_events);
169
170 // Validate refs against state
171 let other_refs_owned: Vec<(String, String, String)> = state_refs
172 .into_iter()
173 .map(|(a, b, c)| (a.clone(), b.clone(), c.clone()))
174 .collect();
175
176 if let Some(ref state) = auth_result.state {
177 debug!(
178 "Validating against state with {} branches",
179 state.branches.len()
180 );
181
182 if other_refs_owned.is_empty() && !state.branches.is_empty() {
183 warn!("No refs parsed from push request but state event has branches - rejecting");
184 return Ok(AuthorizationResult::denied(
185 "Failed to parse refs from push request - cannot validate against state",
186 ));
187 }
188
189 if let Err(e) = validate_push_refs(state, &other_refs_owned) {
190 warn!("Ref validation failed: {}", e);
191 return Ok(AuthorizationResult::denied(format!(
192 "Ref validation failed: {}",
193 e
194 )));
195 }
196 debug!("Ref validation passed");
197 }
198
199 // Return result with purgatory events
200 return Ok(AuthorizationResult {
201 authorized: true,
202 reason: auth_result.reason,
203 state: auth_result.state,
204 maintainers: auth_result.maintainers,
205 purgatory_events,
206 });
207 }
208
209 // Only refs/nostr/ refs - return success with collected events
210 Ok(AuthorizationResult {
211 authorized: true,
212 reason: "Push to refs/nostr/ validated".to_string(),
213 state: None,
214 maintainers: vec![],
215 purgatory_events,
216 })
217}
41 218
42/// Repository data fetched from the database 219/// Repository data fetched from the database
43/// 220///
diff --git a/src/git/handlers.rs b/src/git/handlers.rs
index df6f0e9..2930852 100644
--- a/src/git/handlers.rs
+++ b/src/git/handlers.rs
@@ -4,22 +4,20 @@
4 4
5use http_body_util::Full; 5use http_body_util::Full;
6use hyper::{body::Bytes, Response, StatusCode}; 6use hyper::{body::Bytes, Response, StatusCode};
7use nostr_relay_builder::LocalRelay;
7use nostr_sdk::prelude::*; 8use nostr_sdk::prelude::*;
8use std::path::PathBuf; 9use std::path::PathBuf;
9use std::sync::Arc; 10use std::sync::Arc;
10use tokio::io::{AsyncReadExt, AsyncWriteExt}; 11use tokio::io::{AsyncReadExt, AsyncWriteExt};
11use tracing::{debug, error, info, warn}; 12use tracing::{debug, error, info, warn};
12 13
13use super::authorization::{
14 get_state_authorization_for_specific_owner_repo, parse_pushed_refs, validate_nostr_ref_pushes,
15 validate_push_refs, AuthorizationResult,
16};
17use super::protocol::{GitService, PktLine}; 14use super::protocol::{GitService, PktLine};
18use super::subprocess::GitSubprocess; 15use super::subprocess::GitSubprocess;
19use super::try_set_head_if_available; 16use super::try_set_head_if_available;
20 17
18use crate::git::authorization::authorize_push;
21use crate::nostr::builder::SharedDatabase; 19use crate::nostr::builder::SharedDatabase;
22use crate::nostr::events::{RepositoryState, KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_STATE}; 20use crate::nostr::events::{KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_STATE};
23use crate::purgatory::Purgatory; 21use crate::purgatory::Purgatory;
24 22
25/// Handle GET /info/refs?service=git-{upload,receive}-pack 23/// Handle GET /info/refs?service=git-{upload,receive}-pack
@@ -186,6 +184,7 @@ pub async fn handle_receive_pack(
186 repo_path: PathBuf, 184 repo_path: PathBuf,
187 request_body: Bytes, 185 request_body: Bytes,
188 database: SharedDatabase, 186 database: SharedDatabase,
187 relay: LocalRelay,
189 identifier: &str, 188 identifier: &str,
190 owner_pubkey: &str, 189 owner_pubkey: &str,
191 purgatory: Arc<Purgatory>, 190 purgatory: Arc<Purgatory>,
@@ -196,17 +195,14 @@ pub async fn handle_receive_pack(
196 return Err(GitError::RepositoryNotFound); 195 return Err(GitError::RepositoryNotFound);
197 } 196 }
198 197
199 // Keep track of state and events for processing after push
200 let mut authorized_state: Option<RepositoryState> = None;
201 let mut authorized_events: Vec<Event> = Vec::new();
202
203 // GRASP Authorization Check 198 // GRASP Authorization Check
204 info!( 199 info!(
205 "Authorizing push for {} owned by {} via database query", 200 "Authorizing push for {} owned by {} via database query",
206 identifier, owner_pubkey 201 identifier, owner_pubkey
207 ); 202 );
208 203
209 match authorize_push( 204 // check push is authorised
205 let auth_result = match authorize_push(
210 &database, 206 &database,
211 identifier, 207 identifier,
212 owner_pubkey, 208 owner_pubkey,
@@ -222,21 +218,19 @@ pub async fn handle_receive_pack(
222 return Err(GitError::Unauthorized); 218 return Err(GitError::Unauthorized);
223 } 219 }
224 info!( 220 info!(
225 "Push authorized for {} - {} maintainers, {} purgatory events", 221 "Push authorized for {} - {} maintainers, {} purgatory events: {}",
226 identifier, 222 identifier,
227 auth_result.maintainers.len(), 223 auth_result.maintainers.len(),
228 auth_result.purgatory_events.len() 224 auth_result.purgatory_events.len(),
225 auth_result.reason
229 ); 226 );
230 // Save the state for HEAD setting after push 227 auth_result
231 authorized_state = auth_result.state.clone();
232 // Save the purgatory events for database saving after push
233 authorized_events = auth_result.purgatory_events;
234 } 228 }
235 Err(e) => { 229 Err(e) => {
236 warn!("Authorization check failed for {}: {}", identifier, e); 230 warn!("Authorization check failed for {}: {}", identifier, e);
237 return Err(GitError::Unauthorized); 231 return Err(GitError::Unauthorized);
238 } 232 }
239 } 233 };
240 234
241 // Spawn git receive-pack 235 // Spawn git receive-pack
242 let mut git = GitSubprocess::spawn(GitService::ReceivePack, &repo_path, false) 236 let mut git = GitSubprocess::spawn(GitService::ReceivePack, &repo_path, false)
@@ -283,7 +277,7 @@ pub async fn handle_receive_pack(
283 // GRASP-01: Set HEAD after git data is received 277 // GRASP-01: Set HEAD after git data is received
284 // "MUST set repository HEAD per repository state announcement 278 // "MUST set repository HEAD per repository state announcement
285 // as soon as the git data related to that branch has been received." 279 // as soon as the git data related to that branch has been received."
286 if let Some(ref state) = authorized_state { 280 if let Some(ref state) = auth_result.state {
287 if let Some(head_ref) = &state.head { 281 if let Some(head_ref) = &state.head {
288 if let Some(branch_name) = state.get_head_branch() { 282 if let Some(branch_name) = state.get_head_branch() {
289 if let Some(commit) = state.get_branch_commit(branch_name) { 283 if let Some(commit) = state.get_branch_commit(branch_name) {
@@ -308,41 +302,53 @@ pub async fn handle_receive_pack(
308 302
309 // Save all events from purgatory that authorized this push and remove them from purgatory 303 // Save all events from purgatory that authorized this push and remove them from purgatory
310 // This includes state events, PR events, and PR-update events 304 // This includes state events, PR events, and PR-update events
311 if !authorized_events.is_empty() { 305 info!(
312 info!( 306 "Saving {} purgatory event(s) to database after successful push",
313 "Saving {} purgatory event(s) to database after successful push", 307 auth_result.purgatory_events.len()
314 authorized_events.len() 308 );
315 );
316 309
317 for event in &authorized_events { 310 for event in &auth_result.purgatory_events {
318 match database.save_event(event).await { 311 match database.save_event(event).await {
319 Ok(_) => { 312 Ok(_) => {
320 info!("Saved purgatory event {} to database", event.id); 313 // Remove from purgatory based on event kind
321 // TODO let broadcast_success = local_relay.notify_event(event.clone()); 314 if event.kind == Kind::from(KIND_REPOSITORY_STATE) {
322 warn!("TODO Here we need to broadcast on open websockets for live listeners. eventid; {}", event.id); 315 info!("Saved purgatory state event {} to database", event.id);
323 // Remove from purgatory based on event kind 316 purgatory.remove_state_event(identifier, &event.id);
324 if event.kind == Kind::from(KIND_REPOSITORY_STATE) { 317 info!("Removed saved state event {} from purgatory", event.id);
325 purgatory.remove_state_event(identifier, &event.id); 318 } else if event.kind == Kind::from(KIND_PR)
326 info!("Removed state event {} from purgatory", event.id); 319 || event.kind == Kind::from(KIND_PR_UPDATE)
327 } else if event.kind == Kind::from(KIND_PR) 320 {
328 || event.kind == Kind::from(KIND_PR_UPDATE) 321 info!("Saved purgatory PR event {} to database", event.id);
329 { 322 // Extract event ID from the event itself (it's the event.id)
330 // Extract event ID from the event itself (it's the event.id) 323 let event_id_hex = event.id.to_hex();
331 let event_id_hex = event.id.to_hex(); 324 purgatory.remove_pr(&event_id_hex);
332 purgatory.remove_pr(&event_id_hex); 325 info!("Removed saved PR event {} from purgatory", event.id);
333 info!("Removed PR event {} from purgatory", event.id);
334 }
335 } 326 }
336 Err(e) => { 327 // Broadcast to WebSocket subscribers
328 if relay.notify_event(event.clone()) {
329 info!(
330 "Broadcast purgatory event {} to websocket listeners",
331 event.id
332 );
333 } else {
337 warn!( 334 warn!(
338 "Failed to save purgatory event {} to database: {}", 335 "Failed to broadcast purgatory event {} to websocket listeners",
339 event.id, e 336 event.id
340 ); 337 );
341 } 338 }
342 } 339 }
340 Err(e) => {
341 warn!(
342 "Failed to save purgatory event {} to database: {}",
343 event.id, e
344 );
345 }
343 } 346 }
344 } 347 }
345 348
349 // TODO figure out what atomic pushes look like in GRASP (we cant accepted differnte state events changing different branches at the same time)
350 // TODO sync git data to other repos that these events authorise.
351
346 Ok(Response::builder() 352 Ok(Response::builder()
347 .status(StatusCode::OK) 353 .status(StatusCode::OK)
348 .header( 354 .header(
@@ -354,180 +360,6 @@ pub async fn handle_receive_pack(
354 .unwrap()) 360 .unwrap())
355} 361}
356 362
357/// Perform GRASP authorization for a push operation
358///
359/// This function queries the database directly (not via WebSocket):
360/// 1. Parses the pushed refs from the git pack protocol
361/// 2. Separates refs/nostr/ refs from normal refs
362/// 3. For normal refs: validates against state events in purgatory
363/// 4. For refs/nostr/ refs: validates event ID format and collects PR/PR-update events from purgatory
364/// 5. Returns all authorizing events (state + PR/PR-update) in the result
365async fn authorize_push(
366 database: &SharedDatabase,
367 identifier: &str,
368 owner_pubkey: &str,
369 request_body: &Bytes,
370 purgatory: &Arc<Purgatory>,
371 repo_path: &std::path::Path,
372) -> anyhow::Result<AuthorizationResult> {
373 debug!(
374 "Authorizing push for {} owned by {} via database query",
375 identifier, owner_pubkey
376 );
377
378 // Parse refs from the push request
379 let pushed_refs = parse_pushed_refs(request_body);
380 debug!("Parsed {} refs from push request", pushed_refs.len());
381 for (old_oid, new_oid, ref_name) in &pushed_refs {
382 debug!(" {} {} -> {}", ref_name, old_oid, new_oid);
383 }
384
385 // Separate refs/nostr/ refs from state refs
386 let (nostr_refs, state_refs): (Vec<_>, Vec<_>) = pushed_refs
387 .iter()
388 .partition(|(_, _, ref_name)| ref_name.starts_with("refs/nostr/"));
389
390 // Collect all purgatory events that authorize this push
391 let mut purgatory_events = Vec::new();
392
393 // Handle refs/nostr/ refs - validate and collect PR/PR-update events from purgatory
394 if !nostr_refs.is_empty() {
395 debug!(
396 "Found {} refs/nostr/ refs - validating and collecting from purgatory",
397 nostr_refs.len()
398 );
399
400 for (_, new_oid, ref_name) in &nostr_refs {
401 // Extract event ID from ref name
402 if let Some(event_id_hex) = ref_name.strip_prefix("refs/nostr/") {
403 // Validate event ID format
404 if EventId::parse(event_id_hex).is_err() {
405 warn!("Invalid event ID format in ref: {}", ref_name);
406 return Ok(AuthorizationResult::denied(format!(
407 "Invalid event ID format in ref: {}",
408 ref_name
409 )));
410 }
411
412 // Check purgatory for PR event
413 if let Some(entry) = purgatory.find_pr(event_id_hex) {
414 if let Some(event) = entry.event {
415 // Verify commit matches
416 if entry.commit == *new_oid {
417 debug!(
418 "Found matching PR event {} in purgatory for ref {}",
419 event_id_hex, ref_name
420 );
421 purgatory_events.push(event);
422 } else {
423 warn!(
424 "PR event {} in purgatory has commit mismatch: expected {}, got {}",
425 event_id_hex, entry.commit, new_oid
426 );
427 return Ok(AuthorizationResult::denied(format!(
428 "PR event {} commit mismatch: expected {}, got {}",
429 event_id_hex, entry.commit, new_oid
430 )));
431 }
432 } else {
433 // Placeholder exists - allow push (git-data-first scenario)
434 debug!(
435 "Found placeholder already for PR event {} in purgatory - as we dont have the event and therefore dont know the required commit_id we allow overwriting with a different commit_id",
436 event_id_hex
437 );
438 }
439 } else {
440 // No entry in purgatory - check database for existing event
441 let nostr_refs_owned = vec![(String::new(), new_oid.clone(), ref_name.clone())];
442 if let Err(e) = validate_nostr_ref_pushes(database, &nostr_refs_owned).await {
443 warn!("refs/nostr/ validation failed: {}", e);
444 return Ok(AuthorizationResult::denied(format!(
445 "refs/nostr/ validation failed: {}",
446 e
447 )));
448 }
449 debug!(
450 "No purgatory entry for {} - validated against database",
451 event_id_hex
452 );
453 }
454 }
455 }
456 }
457
458 // Handle normal refs - validate against state events
459 if !state_refs.is_empty() {
460 debug!(
461 "Found {} non-refs/nostr/ refs - checking state authorization",
462 state_refs.len()
463 );
464
465 let auth_result = get_state_authorization_for_specific_owner_repo(
466 database,
467 identifier,
468 owner_pubkey,
469 purgatory,
470 &pushed_refs, //it would be better to accept state_refs but thats in different format
471 repo_path,
472 )
473 .await?;
474
475 if !auth_result.authorized {
476 return Ok(auth_result);
477 }
478
479 // Collect state events from purgatory
480 purgatory_events.extend(auth_result.purgatory_events);
481
482 // Validate refs against state
483 let other_refs_owned: Vec<(String, String, String)> = state_refs
484 .into_iter()
485 .map(|(a, b, c)| (a.clone(), b.clone(), c.clone()))
486 .collect();
487
488 if let Some(ref state) = auth_result.state {
489 debug!(
490 "Validating against state with {} branches",
491 state.branches.len()
492 );
493
494 if other_refs_owned.is_empty() && !state.branches.is_empty() {
495 warn!("No refs parsed from push request but state event has branches - rejecting");
496 return Ok(AuthorizationResult::denied(
497 "Failed to parse refs from push request - cannot validate against state",
498 ));
499 }
500
501 if let Err(e) = validate_push_refs(state, &other_refs_owned) {
502 warn!("Ref validation failed: {}", e);
503 return Ok(AuthorizationResult::denied(format!(
504 "Ref validation failed: {}",
505 e
506 )));
507 }
508 debug!("Ref validation passed");
509 }
510
511 // Return result with purgatory events
512 return Ok(AuthorizationResult {
513 authorized: true,
514 reason: auth_result.reason,
515 state: auth_result.state,
516 maintainers: auth_result.maintainers,
517 purgatory_events,
518 });
519 }
520
521 // Only refs/nostr/ refs - return success with collected events
522 Ok(AuthorizationResult {
523 authorized: true,
524 reason: "Push to refs/nostr/ validated".to_string(),
525 state: None,
526 maintainers: vec![],
527 purgatory_events,
528 })
529}
530
531/// Errors that can occur in Git handlers 363/// Errors that can occur in Git handlers
532#[derive(Debug)] 364#[derive(Debug)]
533pub enum GitError { 365pub enum GitError {
diff --git a/src/http/mod.rs b/src/http/mod.rs
index d62cc4a..10563da 100644
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -163,6 +163,7 @@ impl Service<Request<Incoming>> for HttpService {
163 163
164 let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); 164 let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier);
165 let metrics_clone = self.metrics.clone(); 165 let metrics_clone = self.metrics.clone();
166 let relay = self.relay.clone();
166 167
167 return Box::pin(async move { 168 return Box::pin(async move {
168 // Collect request body once before the match statement 169 // Collect request body once before the match statement
@@ -232,6 +233,7 @@ impl Service<Request<Incoming>> for HttpService {
232 repo_path, 233 repo_path,
233 body_bytes.clone(), 234 body_bytes.clone(),
234 database.clone(), 235 database.clone(),
236 relay.clone(),
235 &identifier, 237 &identifier,
236 &owner_pubkey_hex, 238 &owner_pubkey_hex,
237 purgatory.clone(), 239 purgatory.clone(),
diff --git a/src/main.rs b/src/main.rs
index e39c1ab..d382462 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -119,9 +119,7 @@ async fn main() -> Result<()> {
119 metrics, 119 metrics,
120 purgatory, 120 purgatory,
121 ) => { 121 ) => {
122 if let Err(e) = result { 122 result?
123 return Err(e);
124 }
125 } 123 }
126 _ = signal::ctrl_c() => { 124 _ = signal::ctrl_c() => {
127 info!("Received shutdown signal, cleaning up..."); 125 info!("Received shutdown signal, cleaning up...");