{"record":{"id":"485987abc4317860","repo":"block/buzz","slug":"only-owner-can-delete-group","errorCode":null,"errorMessage":"only owner can delete group","messagePattern":"only owner can delete group","errorType":"validation","errorClass":"IngestError::Rejected","httpStatus":400,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":732,"sourceCode":"                }\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(());\n                    }\n                    Err(anyhow::anyhow!(\"only owner can delete group\"))\n                }\n            }\n        }\n        9022 => {\n            // LEAVE_REQUEST: must be an active member, and cannot be the last 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                None => {\n                    return Err(anyhow::anyhow!(\"actor is not an active member\"));\n                }\n                Some(m) if m.role == \"owner\" => {\n                    let owner_count = members.iter().filter(|m| m.role == \"owner\").count();\n                    if owner_count <= 1 {\n                        return Err(anyhow::anyhow!(\"cannot remove the last owner\"));\n                    }\n                }\n                _ => {}","sourceCodeStart":714,"sourceCodeEnd":750,"githubUrl":"https://github.com/block/buzz/blob/f956e6fe06a76e50cbd8fba1a162482e752e7f1a/crates/buzz-relay/src/handlers/side_effects.rs#L714-L750","documentation":"Thrown by the relay's side-effect authorization for kind:9008 (DELETE_GROUP) events. The actor's pubkey must appear in the channel's active member list with role == \"owner\", or the actor must be the owning human of an active owner-role agent in that channel (an intentional divergence from kind:9001, per the comment in the source). If neither holds, the delete is rejected with this message.","triggerScenarios":"Publishing a signed kind:9008 event where the signer's membership role is \"member\" or \"admin\" (not \"owner\"), or the signer is not a channel member at all, AND actor_owns_any_owner_agent() finds no active owner-role agent in the channel owned by the actor.","commonSituations":"An admin assumes delete rights (admin suffices for kind:9001 edits, not 9008 deletes); a human tries to delete via their agent after the agent was demoted or deactivated; signing with a different key than the one holding the owner membership; the actor's membership lives under a different community host (tenant scoping hides it).","solutions":["Re-publish the kind:9008 event signed by a pubkey whose channel membership role is owner","If acting through an agent, promote that agent to owner role and confirm it is active, then retry as the agent's owning human","Promote the current actor to owner first (membership/role change from an existing owner), then delete","Verify the signing key and community host match the owner membership row (check the channel's kind:39002 membership events)"],"exampleFix":"// before: signed by an admin key — rejected with \"only owner can delete group\"\nlet ev = EventBuilder::new(Kind::Custom(9008), \"\").tags([tag_h(channel_id)]).to_event(&admin_keys)?;\n\n// after: sign with an owner-role member's key\nlet members = client.get_members(channel_id).await?;\nlet owner = members.iter().find(|m| m.role == \"owner\").ok_or(NoOwner)?;\nlet ev = EventBuilder::new(Kind::Custom(9008), \"\").tags([tag_h(channel_id)]).to_event(&owner_keys)?;","handlingStrategy":"validation","validationCode":"// Before publishing kind:9008, confirm the signer is an active owner of the channel\n// (membership resolves from kind:39002 events whose d-tag is the channel id).\nlet members = client.get_members(channel_id).await?;\nlet is_owner = members.iter().any(|m| m.pubkey == actor_pubkey && m.role == \"owner\");\nif !is_owner {\n    return Err(DeleteGroupError::NotOwner);\n}","typeGuard":"fn is_owner(m: &ChannelMember) -> bool {\n    m.role == \"owner\"\n}","tryCatchPattern":"match publish_delete_group(&owner_keys, channel_id).await {\n    Ok(_) => {}\n    Err(e) if e.to_string().contains(\"only owner can delete group\") => {\n        // terminal authorization failure — do not retry; surface to the user\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Gate delete-group UI actions on the actor's owner role fetched from current membership before offering the action","Keep agent ownership metadata (owning human + agent role) current so the owning-human fallback resolves","Log actor pubkey and channel id on rejection to distinguish wrong-key from wrong-role","Remember admin role is not delete rights — only 9008 owner (or owner-agent's human) may delete"],"tags":["authorization","channels","nostr","kind-9008","side-effects"],"backgroundTag":"permission-denied","analyzedSha":"f956e6fe06a76e50cbd8fba1a162482e752e7f1a","analyzedAt":"2026-08-16T22:11:40.750Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}