{"record":{"id":"8a083fa7bbc3bb1c","repo":"block/buzz","slug":"missing-e-or-a-tag-for-target","errorCode":null,"errorMessage":"missing e or a tag for target","messagePattern":"missing e or a tag for target","errorType":"validation","errorClass":"IngestError::Rejected","httpStatus":null,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":240,"sourceCode":"/// Buzz accepts standard deletions for self-authored events, plus the owning\n/// human deleting their agent's events (mirrors `validate_edit_ownership`).\n/// Channel admin deletions continue to use kind 9005.\npub async fn validate_standard_deletion_event(\n    tenant: &TenantContext,\n    event: &Event,\n    state: &Arc<AppState>,\n) -> anyhow::Result<()> {\n    let actor_bytes = effective_message_author(event, &state.relay_keypair.public_key());\n    let target_ids = extract_target_event_ids(event);\n\n    if !has_e_tag(event) {\n        // a-tag deletion: verify author owns the addressable event\n        let a_tag = event\n            .tags\n            .iter()\n            .find(|t| t.kind().to_string() == \"a\")\n            .and_then(|t| t.content().map(|s| s.to_string()))\n            .ok_or_else(|| anyhow::anyhow!(\"missing e or a tag for target\"))?;\n        let parts: Vec<&str> = a_tag.splitn(3, ':').collect();\n        if parts.len() < 2 {\n            return Err(anyhow::anyhow!(\"invalid a-tag format\"));\n        }\n        let target_pubkey_bytes =\n            hex::decode(parts[1]).map_err(|_| anyhow::anyhow!(\"invalid pubkey in a-tag\"))?;\n        if target_pubkey_bytes != actor_bytes\n            && !state\n                .db\n                .is_agent_owner(tenant.community(), &target_pubkey_bytes, &actor_bytes)\n                .await?\n        {\n            return Err(anyhow::anyhow!(\"must be event author\"));\n        }\n        return Ok(());\n    }\n\n    for target_id in target_ids {","sourceCodeStart":222,"sourceCodeEnd":258,"githubUrl":"https://github.com/block/buzz/blob/f956e6fe06a76e50cbd8fba1a162482e752e7f1a/crates/buzz-relay/src/handlers/side_effects.rs#L222-L258","documentation":"validate_standard_deletion_event() rejects a NIP-09 kind:5 deletion event that names no target: has_e_tag() found no 'e' tag at all, and no 'a' tag with content could be located. Buzz treats e/a as the two target classes for deletions — e tags point at concrete event ids, a tags at addressable (kind 30000-39999) coordinates. A deletion with neither is ambiguous and is refused before storage.","triggerScenarios":"Publishing kind:5 with an empty or missing tags list; building the deletion with only an author/metadata tag (e.g. just a 'reason' tag or a malformed tag array); a client bug that serializes tags under the wrong key so 'e'/'a' never reach the relay.","commonSituations":"Hand-rolled event JSON where the tags array is omitted or misspelled; SDK version change that renamed the tags field; test fixtures copied from a text post instead of a deletion template.","solutions":["Add at least one [\"e\", \"<64-hex event id>\"] tag naming the event(s) to delete","For addressable events, use [\"a\", \"<kind>:<pubkey-hex>:<d-identifier>\"] instead","Log event.tags before publish and assert the array contains an e or a entry"],"exampleFix":"// before\nEventBuilder::new(Kind::EventDeletion, \"\", []).to_event(&keys) // no targets\n\n// after\nEventBuilder::new(\n    Kind::EventDeletion,\n    \"\",\n    [Tag::event(EventId::from_hex(\"<target-id>\")?)],\n).to_event(&keys)","handlingStrategy":"validation","validationCode":"// Client-side pre-publish check mirroring has_e_tag / a-tag lookup\nfn has_deletion_target(tags: &[Tag]) -> bool {\n    tags.iter().any(|t| t.kind().to_string() == \"e\")\n        || tags.iter().any(|t| t.kind().to_string() == \"a\" && t.content().is_some())\n}\nassert!(has_deletion_target(&event.tags), \"deletion needs an e or a target tag\");","typeGuard":"fn isDeletionTargeted(tags: string[][]): boolean {\n  return tags.some(t => t[0] === \"e\") || tags.some(t => t[0] === \"a\" && !!t[1]);\n}","tryCatchPattern":"match validate_standard_deletion_event(&tenant, &event, &state).await {\n    Ok(()) => store_and_apply(),\n    Err(e) if e.to_string().contains(\"missing e or a tag\") => reject_with(\"deletion must name an e or a target\", &event.id),\n    Err(e) => Err(e),\n}","preventionTips":["Always build kind:5 events through an SDK builder that requires target tags","Add a unit assertion on tags contents before signing any deletion","Treat e and a as the only valid target classes; do not invent custom target tag kinds"],"tags":["nostr","nip-09","deletion","event-tags","validation"],"backgroundTag":"nip09-deletion-missing-target","analyzedSha":"f956e6fe06a76e50cbd8fba1a162482e752e7f1a","analyzedAt":"2026-08-16T22:11:40.750Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}