From 606cbd62acb0f0cfdd1657d6efa807605fb85dec Mon Sep 17 00:00:00 2001
From: iroiro147 <sarthak.singh@juspay.in>
Date: Sat, 1 Aug 2026 06:52:16 +0530
Subject: [PATCH] fix(relay): wire EventDeleted audit into all event deletion
 paths (#4034)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

AuditAction::EventDeleted was defined but had no production call site —
neither NIP-09 soft-delete (e-tag or a-tag) nor NIP-29 kind:9005 admin
delete ever wrote an audit log entry. The relay could not produce an
audit trail for any event deletion.

Add enqueue_event_deleted_audit alongside the existing
enqueue_event_created_audit in event.rs, following the same bounded-
channel, backpressure-propagating pattern. Wire it into all three
deletion paths in side_effects.rs:

  1. handle_delete_event_side_effect (kind:9005 NIP-29 admin delete)
  2. handle_standard_deletion_event (NIP-09 e-tag soft-delete)
  3. handle_a_tag_deletion (NIP-09 a-tag coordinate soft-delete)

Each call records the actor (deletion requester), the target event ID
or coordinate, and a detail JSON with the delete_kind and event_kind
so auditors can distinguish admin deletes from user self-deletes and
coordinate-based deletions.

Closes #4034.

Signed-off-by: Claude <noreply@anthropic.com>
Signed-off-by: iroiro147 <sarthak.singh@juspay.in>
---
 crates/buzz-relay/src/handlers/event.rs       | 31 +++++++++++++
 .../buzz-relay/src/handlers/side_effects.rs   | 45 ++++++++++++++++++-
 2 files changed, 75 insertions(+), 1 deletion(-)

diff --git a/crates/buzz-relay/src/handlers/event.rs b/crates/buzz-relay/src/handlers/event.rs
index a9cdffcdec..5821113393 100644
--- a/crates/buzz-relay/src/handlers/event.rs
+++ b/crates/buzz-relay/src/handlers/event.rs
@@ -600,6 +600,37 @@ async fn enqueue_event_created_audit(
     }
 }
 
+/// Write an `EventDeleted` audit entry after a successful soft-delete.
+///
+/// Follows the same pattern as [`enqueue_event_created_audit`]: bounded channel,
+/// backpressure-propagating send, fire-and-log on error. Called from every path
+/// that removes an event from the live set (NIP-09 e-tag, NIP-09 a-tag, NIP-29
+/// kind:9005 admin delete).
+///
+/// `pub(super)` so `side_effects.rs` sibling handlers can call it.
+pub(super) async fn enqueue_event_deleted_audit(
+    tenant: &TenantContext,
+    state: &Arc<AppState>,
+    actor_pubkey_hex: &str,
+    event_id_hex: &str,
+    detail: serde_json::Value,
+) {
+    let Some(audit_tx) = &state.audit_tx else {
+        return;
+    };
+    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(event_id_hex.to_owned()),
+        detail,
+    };
+    if let Err(e) = audit_tx.send(audit_entry).await {
+        error!(event_id = %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..cc4607f30c 100644
--- a/crates/buzz-relay/src/handlers/side_effects.rs
+++ b/crates/buzz-relay/src/handlers/side_effects.rs
@@ -1731,13 +1731,27 @@ async fn handle_delete_event_side_effect(
         return Ok(()); // No-op: skip system message to avoid false audit records.
     }
 
+    // Audit the deletion — mirrors enqueue_event_created_audit on the create
+    // path. The actor is the admin who sent the kind:9005 request.
+    let actor_hex = hex::encode(event.pubkey.to_bytes());
+    super::event::enqueue_event_deleted_audit(
+        tenant,
+        state,
+        &actor_hex,
+        &hex::encode(&target_id),
+        serde_json::json!({
+            "delete_kind": "nip29_admin",
+            "channel_id": channel_id,
+        }),
+    )
+    .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 {
         emit_live_thread_summary(tenant, state, channel_id, root_id);
     }
 
-    let actor_hex = hex::encode(event.pubkey.to_bytes());
     let mut tombstone = serde_json::json!({
         "type": "message_deleted",
         "actor": actor_hex,
@@ -2191,6 +2205,20 @@ async fn handle_a_tag_deletion(
                     d_tag = d_tag,
                     "NIP-09 a-tag deletion: soft-deleted addressable event by coordinate"
                 );
+                // Audit the deletion.
+                let a_tag_actor_hex = hex::encode(&actor_bytes);
+                super::event::enqueue_event_deleted_audit(
+                    tenant,
+                    state,
+                    &a_tag_actor_hex,
+                    &a_value,
+                    serde_json::json!({
+                        "delete_kind": "nip09_a_tag",
+                        "event_kind": k,
+                        "coordinate": a_value,
+                    }),
+                )
+                .await;
             } else {
                 tracing::debug!(
                     kind = k,
@@ -2262,6 +2290,21 @@ async fn handle_standard_deletion_event(
             continue;
         }
 
+        // Audit the deletion.
+        let nip09_actor_hex = hex::encode(event.pubkey.to_bytes());
+        super::event::enqueue_event_deleted_audit(
+            tenant,
+            state,
+            &nip09_actor_hex,
+            &hex::encode(&target_id),
+            serde_json::json!({
+                "delete_kind": "nip09_e_tag",
+                "event_kind": u32::from(target_event.event.kind.as_u16()),
+                "channel_id": target_event.channel_id,
+            }),
+        )
+        .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) {
