From 74873af0550e7a23e36a686785bb5170d29e7b81 Mon Sep 17 00:00:00 2001 From: iroiro147 Date: Sat, 1 Aug 2026 17:01:29 +0530 Subject: [PATCH] relay: write audit entry for NIP-09 event deletion Wire AuditAction::EventDeleted into all three NIP-09 deletion paths so operators gain trace evidence for deletions that currently flow silently: * handle_delete_event_side_effect (NIP-29 kind-9005 moderation e-tag): audit after the soft-delete succeeds. Actor is the kind-9005 signer. * handle_standard_deletion_event (NIP-09 kind:5 e-tag loop): audit after each successful soft_delete_event_and_update_thread. Actor is the kind:5 signer. Detail carries the deleted event's kind and channel_id so the entry remains queryable post-tombstone. * handle_a_tag_deletion (NIP-09 a-tag coordinate): audit after a match in soft_delete_by_coordinate. Object id is the full coordinate :: which is stable across a replace and resolvable by the operator to the deleted row; actor is the kind:5 signer. Introduce enqueue_event_deleted_audit next to enqueue_event_created_audit, mirroring its bounded-channel / propagate-backpressure / no-op-when-None contract. The helper is best-effort (any failure-path is absorbed by the send error branch, never panics the deletion flow). deletion_path is recorded in detail ("moderation_e_tag" | "e_tag" | "a_tag") so post-hoc queries can distinguish moderation deletions from user self-deletions from coordinate deletions. Fixes #4034 Signed-off-by: Sarthak Singh --- crates/buzz-relay/src/handlers/event.rs | 41 ++++++++++++++++++ .../buzz-relay/src/handlers/side_effects.rs | 43 ++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/crates/buzz-relay/src/handlers/event.rs b/crates/buzz-relay/src/handlers/event.rs index a9cdffcdec..e5ae41a3fe 100644 --- a/crates/buzz-relay/src/handlers/event.rs +++ b/crates/buzz-relay/src/handlers/event.rs @@ -600,6 +600,47 @@ async fn enqueue_event_created_audit( } } +/// Enqueue an [`AuditAction::EventDeleted`] entry for a NIP-09 deletion. +/// +/// Mirrors [`enqueue_event_created_audit`] (same bounded channel, same +/// propensity to propagate backpressure rather than silently drop entries, +/// same no-op when `audit_tx` is `None`). The `actor_pubkey_hex` here is the +/// pubkey that *signed* the NIP-09 kind:5 event — i.e. the human/user asking +/// for the delete — not the relay key and not the (now-tombstoned) target +/// event's author. `deletion_path` distinguishes which NIP-09 path fired so +/// the audit log reflects operator intent: `"e_tag"` for the standard +/// `["e", ]` soft-delete, `"a_tag"` for the addressable-coordinate +/// variant. +pub(super) async fn enqueue_event_deleted_audit( + tenant: &TenantContext, + state: &Arc, + actor_pubkey_hex: &str, + target_event_id_hex: &str, + channel_id: Option, + event_kind_u32: Option, + deletion_path: &'static str, +) { + let Some(audit_tx) = &state.audit_tx else { + return; + }; + let detail = serde_json::json!({ + "event_kind": event_kind_u32, + "channel_id": channel_id.map(|c| c.to_string()), + "deletion_path": deletion_path, + }); + let audit_entry = buzz_audit::NewAuditEntry { + community_id: tenant.community(), + action: buzz_audit::AuditAction::EventDeleted, + actor_pubkey: hex::decode(actor_pubkey_hex).ok(), + object_id: Some(target_event_id_hex.to_owned()), + detail, + }; + if let Err(e) = audit_tx.send(audit_entry).await { + error!(target_event_id = %target_event_id_hex, "Audit channel closed — entry lost: {e}"); + metrics::counter!("buzz_audit_send_errors_total").increment(1); + } +} + /// Handle an EVENT message from a WebSocket connection. /// /// Extracts auth from the WS connection, dispatches ephemeral events locally, diff --git a/crates/buzz-relay/src/handlers/side_effects.rs b/crates/buzz-relay/src/handlers/side_effects.rs index 660a55fef3..a8a1bd1ab7 100644 --- a/crates/buzz-relay/src/handlers/side_effects.rs +++ b/crates/buzz-relay/src/handlers/side_effects.rs @@ -16,7 +16,7 @@ use buzz_core::kind::{ use buzz_core::StoredEvent; use buzz_db::channel::{MemberRecord, MemberRole}; -use super::event::dispatch_persistent_event; +use super::event::{dispatch_persistent_event, enqueue_event_deleted_audit}; use crate::protocol::RelayMessage; use crate::state::AppState; use buzz_core::tenant::TenantContext; @@ -1731,6 +1731,19 @@ async fn handle_delete_event_side_effect( return Ok(()); // No-op: skip system message to avoid false audit records. } + // Best-effort domain audit; failure must never break the deletion flow. + // The actor is the NIP-29 kind-9005 moderation signer (`event.pubkey`). + enqueue_event_deleted_audit( + tenant, + state, + &hex::encode(event.pubkey.to_bytes()), + &hex::encode(&target_id), + Some(channel_id), + None, // kind of the deleted event is not known to this handler + "moderation_e_tag", + ) + .await; + // Thread counters were decremented in the same transaction — push a fresh // relay-signed 39005 so live badge counts also count *down*. if let Some(root_id) = root_id { @@ -2191,6 +2204,20 @@ async fn handle_a_tag_deletion( d_tag = d_tag, "NIP-09 a-tag deletion: soft-deleted addressable event by coordinate" ); + + // Best-effort domain audit. Actor is the NIP-09 signer. Object id + // is the coordinate (::) — stable across a + // replace and resolvable by the operator to the deleted row. + enqueue_event_deleted_audit( + tenant, + state, + &hex::encode(event.pubkey.to_bytes()), + &format!("{kind_i32}:{pubkey_hex}:{d_tag}"), + None, + Some(k), + "a_tag", + ) + .await; } else { tracing::debug!( kind = k, @@ -2262,6 +2289,20 @@ async fn handle_standard_deletion_event( continue; } + // Best-effort domain audit; failure must never break the deletion flow. + // The actor is the NIP-09 kind:5 signer (`event.pubkey`). The target + // event's own kind/channel travel in the detail for post-hoc queryability. + enqueue_event_deleted_audit( + tenant, + state, + &hex::encode(event.pubkey.to_bytes()), + &hex::encode(&target_id), + target_event.channel_id, + Some(u32::from(target_event.event.kind.as_u16())), + "e_tag", + ) + .await; + // Thread counters were decremented in the same transaction — push a // fresh relay-signed 39005 so live badge counts also count *down*. if let (Some(root_id), Some(channel_id)) = (root_id, target_event.channel_id) {