mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
fix(announcements): runtime deletion on service delist replacements
This commit is contained in:
@@ -627,6 +627,8 @@ impl Nip34WritePolicy {
|
||||
// Parse announcement to get repository details
|
||||
match RepositoryAnnouncement::from_event(event.clone()) {
|
||||
Ok(announcement) => {
|
||||
self.maybe_apply_runtime_delist_deletion(event).await;
|
||||
|
||||
// Try to create bare repository if it doesn't exist
|
||||
if let Err(e) = self
|
||||
.announcement_policy
|
||||
@@ -732,6 +734,8 @@ impl Nip34WritePolicy {
|
||||
// Parse announcement to get details for logging
|
||||
match RepositoryAnnouncement::from_event(event.clone()) {
|
||||
Ok(announcement) => {
|
||||
self.maybe_apply_runtime_delist_deletion(event).await;
|
||||
|
||||
// Phase 5 recovery trigger: any accepted announcement route
|
||||
// (including maintainer acceptance) can restore eligible
|
||||
// holding/archive state for this owner+identifier.
|
||||
@@ -776,6 +780,7 @@ impl Nip34WritePolicy {
|
||||
}
|
||||
}
|
||||
AnnouncementResult::Reject(reason) => {
|
||||
self.maybe_apply_runtime_delist_deletion(event).await;
|
||||
tracing::warn!(
|
||||
"Rejected repository announcement {}: {}",
|
||||
event_id_str,
|
||||
@@ -786,6 +791,102 @@ impl Nip34WritePolicy {
|
||||
}
|
||||
}
|
||||
|
||||
/// Runtime de-list parity path for already-served repositories.
|
||||
///
|
||||
/// When a newer replacement announcement for the same owner+identifier no
|
||||
/// longer lists this relay's service, trigger the normal cascade
|
||||
/// delete->holding/archive flow for the currently served announcement.
|
||||
///
|
||||
/// This mirrors startup whitelist/blacklist parity behavior, but runs at
|
||||
/// write time so service removals are enforced immediately.
|
||||
async fn maybe_apply_runtime_delist_deletion(&self, incoming: &Event) {
|
||||
let incoming_announcement = match RepositoryAnnouncement::from_event(incoming.clone()) {
|
||||
Ok(announcement) => announcement,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// This path is only for announcements that REMOVE this relay listing.
|
||||
if incoming_announcement.lists_service(&self.ctx.domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::GitRepoAnnouncement)
|
||||
.author(incoming.pubkey)
|
||||
.custom_tag(
|
||||
SingleLetterTag::lowercase(Alphabet::D),
|
||||
incoming_announcement.identifier.clone(),
|
||||
);
|
||||
|
||||
let current = match self.ctx.database.query(filter).await {
|
||||
Ok(events) => events.into_iter().max_by_key(|event| event.created_at),
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
owner = %incoming.pubkey.to_hex(),
|
||||
identifier = %incoming_announcement.identifier,
|
||||
error = %e,
|
||||
"Runtime de-list parity: failed querying existing announcement"
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let Some(current) = current else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Not newer than what we already serve -> no runtime parity action.
|
||||
if incoming.created_at <= current.created_at {
|
||||
return;
|
||||
}
|
||||
|
||||
let current_announcement = match RepositoryAnnouncement::from_event(current.clone()) {
|
||||
Ok(announcement) => announcement,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
event_id = %current.id.to_hex(),
|
||||
owner = %incoming.pubkey.to_hex(),
|
||||
identifier = %incoming_announcement.identifier,
|
||||
error = %e,
|
||||
"Runtime de-list parity: failed parsing current announcement"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Safety guard: only run cascade when the currently served announcement
|
||||
// actually lists this relay.
|
||||
if !current_announcement.lists_service(&self.ctx.domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
match self
|
||||
.deletion_policy
|
||||
.apply_whitelist_deletion_for_announcement(¤t)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
tracing::info!(
|
||||
owner = %incoming.pubkey.to_hex(),
|
||||
identifier = %incoming_announcement.identifier,
|
||||
replaced_announcement = %current.id.to_hex(),
|
||||
replacement = %incoming.id.to_hex(),
|
||||
"Runtime de-list parity: deleted previously served repository"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
owner = %incoming.pubkey.to_hex(),
|
||||
identifier = %incoming_announcement.identifier,
|
||||
replaced_announcement = %current.id.to_hex(),
|
||||
replacement = %incoming.id.to_hex(),
|
||||
error = %e,
|
||||
"Runtime de-list parity: deletion flow failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle repository state event
|
||||
///
|
||||
/// # Arguments
|
||||
|
||||
@@ -76,6 +76,22 @@ impl AnnouncementPolicy {
|
||||
);
|
||||
}
|
||||
|
||||
// Owner de-list replacement for an already served repo:
|
||||
// do NOT allow maintainer-exception acceptance to bypass
|
||||
// de-list enforcement. Let the caller treat this as a
|
||||
// reject path so runtime deletion parity can run.
|
||||
let lists_service = announcement.lists_service(&self.config.domain);
|
||||
if !lists_service {
|
||||
match self
|
||||
.has_db_announcement(&event.pubkey, &announcement.identifier)
|
||||
.await
|
||||
{
|
||||
Ok(true) => return AnnouncementResult::Reject(reason),
|
||||
Ok(false) => {}
|
||||
Err(_) => return AnnouncementResult::Reject(reason),
|
||||
}
|
||||
}
|
||||
|
||||
match self
|
||||
.is_maintainer_in_any_announcement(
|
||||
&announcement.identifier,
|
||||
|
||||
@@ -1002,6 +1002,90 @@ async fn reannouncement_restores_events_and_git_archive_happy_path() {
|
||||
relay.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replacement_announcement_delisting_this_relay_triggers_runtime_deletion_flow() {
|
||||
let relay = TestRelay::start_with_lmdb().await;
|
||||
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
|
||||
.await
|
||||
.expect("create client");
|
||||
|
||||
let (announcement, repo_id, state_event) =
|
||||
publish_served_repo_with_state_event(&client, "runtime-delist").await;
|
||||
let issue = client
|
||||
.create_issue(&announcement, "runtime delist issue", "body", vec![])
|
||||
.expect("build issue");
|
||||
client.send_event(issue.clone()).await.expect("send issue");
|
||||
tokio::time::sleep(Duration::from_millis(600)).await;
|
||||
|
||||
assert!(client
|
||||
.is_event_on_relay(issue.id)
|
||||
.await
|
||||
.expect("query issue before delist"));
|
||||
|
||||
let replacement_delist = EventBuilder::new(Kind::GitRepoAnnouncement, "delist relay")
|
||||
.tags(vec![
|
||||
Tag::identifier(repo_id.clone()),
|
||||
Tag::custom("name", vec![repo_id.clone()]),
|
||||
Tag::custom(
|
||||
"clone",
|
||||
vec![format!(
|
||||
"https://example.com/{}/{}.git",
|
||||
client.public_key().to_hex(),
|
||||
repo_id
|
||||
)],
|
||||
),
|
||||
Tag::custom("relays", vec!["wss://example.com".to_string()]),
|
||||
])
|
||||
.custom_created_at(Timestamp::from_secs(announcement.created_at.as_secs() + 30))
|
||||
.finalize(client.keys())
|
||||
.expect("build replacement delist announcement");
|
||||
|
||||
let rejection = client.send_event(replacement_delist.clone()).await;
|
||||
assert!(
|
||||
rejection.is_err(),
|
||||
"replacement delist announcement should be rejected"
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(900)).await;
|
||||
|
||||
assert!(
|
||||
!client
|
||||
.is_event_on_relay(announcement.id)
|
||||
.await
|
||||
.expect("query original announcement after delist"),
|
||||
"runtime de-list should delete the previously served announcement"
|
||||
);
|
||||
assert!(
|
||||
!client
|
||||
.is_event_on_relay(state_event.id)
|
||||
.await
|
||||
.expect("query state after runtime delist"),
|
||||
"runtime de-list should cascade-delete repository state"
|
||||
);
|
||||
assert!(
|
||||
!client
|
||||
.is_event_on_relay(issue.id)
|
||||
.await
|
||||
.expect("query issue after runtime delist"),
|
||||
"runtime de-list should cascade-delete dependent events"
|
||||
);
|
||||
|
||||
let store = open_holding(&relay).await;
|
||||
assert!(
|
||||
!store.metadata_for_event(&announcement.id).await.is_empty(),
|
||||
"runtime de-list should archive announcement into holding metadata"
|
||||
);
|
||||
assert!(
|
||||
!store.metadata_for_event(&state_event.id).await.is_empty(),
|
||||
"runtime de-list should archive state into holding metadata"
|
||||
);
|
||||
assert!(
|
||||
!store.metadata_for_event(&issue.id).await.is_empty(),
|
||||
"runtime de-list should archive dependent events into holding metadata"
|
||||
);
|
||||
|
||||
relay.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn out_of_retention_reannouncement_behaves_as_fresh_new_repo() {
|
||||
let relay = TestRelay::start_with_lmdb().await;
|
||||
|
||||
Reference in New Issue
Block a user