diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-23 15:41:32 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-23 15:41:32 +0000 |
| commit | c54ce061d6d278cce8362d5af085808ca60c239b (patch) | |
| tree | ec967d6195d9f7ec4f061449596611afe3a0950f /src/git/authorization.rs | |
| parent | e0ad39a489b3398f8208713bf728db0cb11475b0 (diff) | |
| parent | 113928aa84894ea8f65c247d9987527e792b32a9 (diff) | |
feat: announcement purgatory
Extends purgatory to hold repository announcements until git data arrives,
preventing empty repositories from being served to clients.
When an announcement is received, a bare repo is created immediately and the
announcement is held in purgatory. It is only promoted and served once a git
push confirms real content exists. If no push arrives before expiry, the bare
repo is deleted and the announcement is silently discarded.
Key behaviours:
- Soft expiry: announcements are hidden from clients but kept alive while git
pushes are in progress, reviving on successful push
- Expiry is extended when a matching state event or git push is observed
- NIP-09 deletion events remove announcements from purgatory
- Purgatory state (announcements, state events, PR events, expired set) is
persisted to disk on graceful shutdown and restored on startup, with elapsed
downtime subtracted from expiry deadlines
- Purgatory announcements drive StateOnly sync in the sync system so state
events are fetched from listed relays before promotion
- SyncLevel added to RepoSyncIndex to distinguish purgatory repos (StateOnly)
from promoted repos (Full L2+L3 sync)
Diffstat (limited to 'src/git/authorization.rs')
| -rw-r--r-- | src/git/authorization.rs | 59 |
1 files changed, 57 insertions, 2 deletions
diff --git a/src/git/authorization.rs b/src/git/authorization.rs index 27107db..df780bb 100644 --- a/src/git/authorization.rs +++ b/src/git/authorization.rs | |||
| @@ -287,6 +287,39 @@ pub async fn fetch_repository_data( | |||
| 287 | }) | 287 | }) |
| 288 | } | 288 | } |
| 289 | 289 | ||
| 290 | /// Fetch repository data including announcements from purgatory | ||
| 291 | /// | ||
| 292 | /// This combines database announcements with purgatory announcements, | ||
| 293 | /// which is needed for authorization when the announcement hasn't been | ||
| 294 | /// promoted yet (no git data has arrived). | ||
| 295 | pub async fn fetch_repository_data_with_purgatory( | ||
| 296 | database: &SharedDatabase, | ||
| 297 | purgatory: &crate::purgatory::Purgatory, | ||
| 298 | identifier: &str, | ||
| 299 | ) -> Result<RepositoryData> { | ||
| 300 | // First, fetch from database | ||
| 301 | let mut repo_data = fetch_repository_data(database, identifier).await?; | ||
| 302 | |||
| 303 | // Then, add announcements from purgatory | ||
| 304 | let purgatory_announcements = purgatory.get_announcements_by_identifier(identifier); | ||
| 305 | let purgatory_count = purgatory_announcements.len(); | ||
| 306 | |||
| 307 | for entry in purgatory_announcements { | ||
| 308 | if let Ok(announcement) = RepositoryAnnouncement::from_event(entry.event) { | ||
| 309 | repo_data.announcements.push(announcement); | ||
| 310 | } | ||
| 311 | } | ||
| 312 | |||
| 313 | debug!( | ||
| 314 | "Fetched repository data with purgatory: {} announcements ({} from purgatory), {} states", | ||
| 315 | repo_data.announcements.len(), | ||
| 316 | purgatory_count, | ||
| 317 | repo_data.states.len() | ||
| 318 | ); | ||
| 319 | |||
| 320 | Ok(repo_data) | ||
| 321 | } | ||
| 322 | |||
| 290 | pub fn pubkey_authorised_for_repo_owners( | 323 | pub fn pubkey_authorised_for_repo_owners( |
| 291 | pubkey: &PublicKey, | 324 | pubkey: &PublicKey, |
| 292 | db_repo_data: &RepositoryData, | 325 | db_repo_data: &RepositoryData, |
| @@ -539,8 +572,9 @@ pub async fn get_state_authorization_for_specific_owner_repo( | |||
| 539 | use crate::git::list_refs; | 572 | use crate::git::list_refs; |
| 540 | use crate::purgatory::RefUpdate; | 573 | use crate::purgatory::RefUpdate; |
| 541 | 574 | ||
| 542 | // Fetch announcements only - we don't need database states | 575 | // Fetch announcements from database AND purgatory - needed for authorization |
| 543 | let repo_data = fetch_repository_data(database, identifier).await?; | 576 | // when the announcement hasn't been promoted yet (no git data has arrived) |
| 577 | let repo_data = fetch_repository_data_with_purgatory(database, purgatory, identifier).await?; | ||
| 544 | 578 | ||
| 545 | if repo_data.announcements.is_empty() { | 579 | if repo_data.announcements.is_empty() { |
| 546 | return Ok(AuthorizationResult::denied( | 580 | return Ok(AuthorizationResult::denied( |
| @@ -649,6 +683,27 @@ pub async fn get_state_authorization_for_specific_owner_repo( | |||
| 649 | .unwrap_or_else(|_| latest_authorized.pubkey.to_hex()) | 683 | .unwrap_or_else(|_| latest_authorized.pubkey.to_hex()) |
| 650 | ); | 684 | ); |
| 651 | 685 | ||
| 686 | // Extend purgatory announcement expiry for the owner. | ||
| 687 | // | ||
| 688 | // Per design doc decision #4: git auth extending a state event's expiry | ||
| 689 | // also extends the announcement's expiry. The repo is actively receiving | ||
| 690 | // git data, so the announcement should not expire prematurely. | ||
| 691 | // This also revives soft-expired announcements (recreates bare repo). | ||
| 692 | if let Ok(owner_pk) = PublicKey::parse(owner_pubkey) { | ||
| 693 | if purgatory.has_purgatory_announcement(&owner_pk, identifier) { | ||
| 694 | purgatory.extend_announcement_expiry( | ||
| 695 | &owner_pk, | ||
| 696 | identifier, | ||
| 697 | std::time::Duration::from_secs(1800), | ||
| 698 | ); | ||
| 699 | debug!( | ||
| 700 | identifier = %identifier, | ||
| 701 | owner = %owner_pubkey, | ||
| 702 | "Extended purgatory announcement expiry due to git push authorization" | ||
| 703 | ); | ||
| 704 | } | ||
| 705 | } | ||
| 706 | |||
| 652 | return Ok(AuthorizationResult { | 707 | return Ok(AuthorizationResult { |
| 653 | authorized: true, | 708 | authorized: true, |
| 654 | reason: "Authorized by state event in purgatory".to_string(), | 709 | reason: "Authorized by state event in purgatory".to_string(), |