{"record":{"id":"64edf34e85c7faac","repo":"block/buzz","slug":"cannot-remove-the-last-owner","errorCode":null,"errorMessage":"cannot remove the last owner","messagePattern":"cannot remove the last owner","errorType":"validation","errorClass":"IngestError::Rejected","httpStatus":null,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":463,"sourceCode":"\n            Ok(())\n        }\n        9001 => {\n            // REMOVE_USER: self-remove allowed unless actor is the last owner; removing others requires owner/admin\n            let target_pubkey =\n                extract_p_tag(event).ok_or_else(|| anyhow::anyhow!(\"missing p tag\"))?;\n            if target_pubkey == actor_bytes {\n                // Self-removal: 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                    _ => {}\n                }\n                Ok(())\n            } else {\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\" || m.role == \"admin\" => Ok(()),\n                    Some(_) => {\n                        if state\n                            .db\n                            .is_agent_owner(tenant.community(), &target_pubkey, &actor_bytes)\n                            .await?\n                        {\n                            Ok(())\n                        } else {","sourceCodeStart":445,"sourceCodeEnd":481,"githubUrl":"https://github.com/block/buzz/blob/f956e6fe06a76e50cbd8fba1a162482e752e7f1a/crates/buzz-relay/src/handlers/side_effects.rs#L445-L481","documentation":"Thrown by a kind:9001 (REMOVE_USER) self-leave when the actor is a channel owner and the member list contains exactly one owner. The relay counts members with role == \"owner\" and blocks the leave at <= 1 so the channel can never be left ownerless. It is a data-integrity guard, not a permissions bug.","triggerScenarios":"The sole owner publishes kind:9001 with p tag equal to their own pubkey. Every owner before you already left or was demoted, leaving owner_count == 1.","commonSituations":"Small teams where one person created the channel and later wants to leave; handover scripts that remove the founder without first promoting a successor; test channels abandoned by everyone except the creator.","solutions":["Promote another member to owner first (admin/owner role change event, kind 9003-style add-admin flow), then re-send your 9001 leave.","If nobody should inherit the channel, delete the group entirely with kind:9008 (owner-only) instead of leaving.","If a successor was already promoted, wait for the membership change to be committed (re-check member roles) before retrying the leave.","For automation, make leave conditional: fetch members, count owners > 1, else promote or delete."],"exampleFix":"// before — sole owner tries to leave directly\nawait sdk.removeUser(channelId, myPubkey); // → \"cannot remove the last owner\"\n\n// after — promote a successor, then leave\nawait sdk.addChannelAdmin(channelId, successorPubkey); // grants owner/admin role\nawait sdk.updateMemberRole(channelId, successorPubkey, \"owner\");\nawait sdk.removeUser(channelId, myPubkey); // now succeeds","handlingStrategy":"validation","validationCode":"// Before self-leave as an owner: ensure another owner exists\nconst members = await buzz.channelsMembersList(channelId);\nconst owners = members.filter((m) => m.role === \"owner\");\nconst me = owners.find((m) => m.pubkey === myPubkey);\nif (me && owners.length <= 1) {\n  throw new Error(\"Promote another owner or delete the channel before leaving\");\n}","typeGuard":"function canOwnerLeave(members: { pubkey: string; role: string }[], myPubkey: string): boolean {\n  const me = members.find((m) => m.pubkey === myPubkey);\n  if (!me || me.role !== \"owner\") return true; // non-owner leaving is fine\n  return members.filter((m) => m.role === \"owner\").length > 1;\n}","tryCatchPattern":"try {\n  await sdk.removeUser(channelId, myPubkey);\n} catch (e) {\n  if (String(e).includes(\"cannot remove the last owner\")) {\n    await sdk.addChannelAdmin(channelId, successorPubkey);\n    await sdk.removeUser(channelId, myPubkey); // retry after promoting\n  } else throw e;\n}","preventionTips":["Onboarding flows should create a second owner early, before anyone leaves.","Automated leave flows: fetch members, count owners, and branch (promote / delete group / leave).","Never fire-and-forget 9001 leaves in scripts without owner-count pre-checks."],"tags":["nostr","channel-ownership","kind-9001","relay","data-integrity"],"backgroundTag":"last-owner-protection","analyzedSha":"f956e6fe06a76e50cbd8fba1a162482e752e7f1a","analyzedAt":"2026-08-16T22:11:40.750Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}