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
41 changes: 41 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,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", <id>]` soft-delete, `"a_tag"` for the addressable-coordinate
/// variant.
pub(super) async fn enqueue_event_deleted_audit(
tenant: &TenantContext,
state: &Arc<AppState>,
actor_pubkey_hex: &str,
target_event_id_hex: &str,
channel_id: Option<uuid::Uuid>,
event_kind_u32: Option<u32>,
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,
Expand Down
43 changes: 42 additions & 1 deletion crates/buzz-relay/src/handlers/side_effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 (<kind>:<pubkey>:<d_tag>) — 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,
Expand Down Expand Up @@ -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) {
Expand Down