{"record":{"id":"3157ed73318f04cf","repo":"block/buzz","slug":"must-be-event-author","errorCode":null,"errorMessage":"must be event author","messagePattern":"must be event author","errorType":"validation","errorClass":"IngestError::Rejected","httpStatus":null,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":253,"sourceCode":"        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 {\n        let target_event = state\n            .db\n            .get_event_by_id_including_deleted(tenant.community(), &target_id)\n            .await?\n            .ok_or_else(|| anyhow::anyhow!(\"target event not found\"))?;\n\n        let target_author =\n            effective_message_author(&target_event.event, &state.relay_keypair.public_key());\n        if target_author != actor_bytes\n            && !state\n                .db\n                .is_agent_owner(tenant.community(), &target_author, &actor_bytes)\n                .await?","sourceCodeStart":235,"sourceCodeEnd":271,"githubUrl":"https://github.com/block/buzz/blob/f956e6fe06a76e50cbd8fba1a162482e752e7f1a/crates/buzz-relay/src/handlers/side_effects.rs#L235-L271","documentation":"For an 'a'-tag deletion: the coordinate's publisher pubkey is not the deletion's effective author, and is_agent_owner() reports the signer does not own that agent pubkey in this community. Buzz extends NIP-09 so a human may delete their own agent's addressable events (NIP-OA ownership), but a third party deleting someone else's coordinate is refused. Note effective_message_author() unwraps relay-signed attribution (actor/p tags) before comparing.","triggerScenarios":"Deleting an addressable event whose coordinate names another user's pubkey; signing the deletion with a fresh key instead of the key that published the coordinate; a human deleting an agent's event without the NIP-OA agent-owner registration linking them in this community.","commonSituations":"Rotated keys after republishing under a new pubkey; agent events published under the agent key but the owner relationship was never registered via the agent-owner path; deleting across communities — ownership rows are tenant-scoped.","solutions":["Sign the deletion with the same key that published the addressable event (the coordinate's pubkey)","If the coordinate belongs to your agent, ensure the agent-owner relationship is registered in this community (is_agent_owner must match), then sign with the owner key","If you are a channel admin deleting someone else's content, use kind 9005 (channel admin deletion) instead of kind 5","Check the coordinate string is not stale — republished events may live under a different pubkey than an earlier version"],"exampleFix":"// before: owner key deletes coordinate published by unrelated key\n// a = \"30078:<other-pubkey>:general\" signed by <my-key> → must be event author\n\n// after: sign with the coordinate's own publisher key\nlet keys = Keys::from(...); // the key whose hex == parts[1]\nEventBuilder::new(Kind::EventDeletion, \"\", [Tag::custom(TagKind::Custom(\"a\"), vec![a])])\n    .to_event(&keys)","handlingStrategy":"validation","validationCode":"// Before publishing an a-tag deletion, confirm the coordinate's pubkey is yours\n// (or an agent you own in this community)\nlet parts: Vec<&str> = a_value.splitn(3, ':').collect();\nlet coord_pk = hex::decode(parts[1])?;\nlet mine = coord_pk == signing_keys.public_key().to_bytes().to_vec();\nlet my_agent = client.is_agent_owner(community, coord_pk, signing_keys.public_key()).await?;\nanyhow::ensure!(mine || my_agent, \"cannot delete a coordinate you do not own\");","typeGuard":"fn owns_coordinate(signer_pk: &[u8], coord: &str) -> bool {\n    coord\n        .splitn(3, ':')\n        .nth(1)\n        .and_then(|p| hex::decode(p).ok())\n        .map(|p| p == signer_pk)\n        .unwrap_or(false)\n}","tryCatchPattern":"match validate_standard_deletion_event(&tenant, &event, &state).await {\n    Err(e) if e.to_string().contains(\"must be event author\") && a_tag_path => {\n        // pick the key that published the coordinate (or register agent ownership) — do not retry blindly\n        prompt_switch_identity(&event);\n    }\n    other => other,\n}","preventionTips":["Track which key published each addressable event and select that key when building deletions","Register NIP-OA agent ownership at agent provisioning time so the owner can always clean up","Moderators deleting others' content must use kind 9005, never kind 5 — encode this in tooling"],"tags":["nostr","nip-09","deletion","authorization","ownership"],"backgroundTag":"nip09-deletion-not-authorized","analyzedSha":"f956e6fe06a76e50cbd8fba1a162482e752e7f1a","analyzedAt":"2026-08-16T22:11:40.750Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}