{"record":{"id":"e9ba5e28c70d961f","repo":"block/buzz","slug":"must-be-event-author-or-channel-owner-admin","errorCode":null,"errorMessage":"must be event author or channel owner/admin","messagePattern":"must be event author or channel owner/admin","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":651,"sourceCode":"            }\n\n            // Not the author, or author who is no longer a member of a private channel —\n            // must be owner/admin or the owning human of the message's agent-author.\n            let members = state.db.get_members(tenant.community(), channel_id).await?;\n            if actor_is_channel_owner_or_admin(&members, &actor_bytes) {\n                Ok(())\n            } else {\n                // Allow the owning human of the agent that authored the target message,\n                // even when the human is not a channel member.\n                if state\n                    .db\n                    .is_agent_owner(tenant.community(), &author, &actor_bytes)\n                    .await?\n                {\n                    Ok(())\n                } else {\n                    Err(anyhow::anyhow!(\n                        \"must be event author or channel owner/admin\"\n                    ))\n                }\n            }\n        }\n        9008 => {\n            // DELETE_GROUP: owner only, or the owning human of the channel's agent-owner.\n            let members = state.db.get_members(tenant.community(), channel_id).await?;\n            let actor_member = members.iter().find(|m| m.pubkey == actor_bytes);\n            match actor_member {\n                Some(m) if m.role == \"owner\" => Ok(()),\n                _ => {\n                    // Allow the owning human of any active owner-role agent in the\n                    // channel, even when the human is not a channel member —\n                    // diverges from kind:9001 intentionally.\n                    if actor_owns_any_owner_agent(state, tenant.community(), &members, &actor_bytes)\n                        .await?\n                    {\n                        return Ok(());","sourceCodeStart":633,"sourceCodeEnd":669,"githubUrl":"https://github.com/block/buzz/blob/dad5a33865fc81a2e55b3b60746632f615ec1e3a/crates/buzz-relay/src/handlers/side_effects.rs#L633-L669","documentation":"Authorization for a kind:9005 delete of someone else's message: the actor must be a channel owner/admin member, the effective author of the target (the p-tag pubkey for relay-signed REST messages), or the registered owning human of the agent that authored the message (is_agent_owner). Failing all four checks, the relay rejects the delete. The effective-author rule exists because REST-published messages are signed by the relay key, with the real author in a p tag.","triggerScenarios":"A plain member publishing 9005 against another member's message; a human owner deleting their agent's message without the agent-ownership relation being registered; acting with a different key than the one that authored the message; relay-signed messages whose p tag does not match the actor.","commonSituations":"Moderation tooling running with a member-level key; humans expecting implicit ownership of their agents' messages without the DB ownership link; key rotation making the actor no longer the recorded author.","solutions":["Delete your own messages with the same key that authored them (for relay-signed messages, the key matching the p tag).","For others' messages, use a key whose member role is owner or admin in that channel (check relay_members/get_members).","For deleting an agent's message as its owner, ensure the agent-ownership relation exists (is_agent_owner lookup) — register it if the agent was set up outside the normal flow.","If acting for moderation legitimately, have an admin grant the acting key the admin role first."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// before publishing 9005 for someone else's message, check your standing\nconst members = await relay.channelMembers(channelId);\nconst me = members.find((m) => m.pubkey === myPubkey);\nconst canModerate = me && (me.role === 'owner' || me.role === 'admin');\nconst isAuthor = effectiveAuthor(target) === myPubkey;\nif (!canModerate && !isAuthor) throw new Error('not permitted to delete this message');","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Sign each user's deletes with that user's key — never one shared moderation key with member role.","For relay-signed REST messages, ensure the p tag carries the real author so the self-delete path matches.","Register agent ownership at agent-provisioning time so humans can manage their agents' messages."],"tags":["buzz-relay","nostr","delete-event","authorization","permission-denied"],"backgroundTag":"permission-denied","analyzedSha":"dad5a33865fc81a2e55b3b60746632f615ec1e3a","analyzedAt":"2026-08-20T04:38:24.874Z","contentChangedAt":"2026-08-20T04:38:24.874Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}