Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions crates/buzz-relay/src/handlers/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
45 changes: 44 additions & 1 deletion crates/buzz-relay/src/handlers/side_effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down