{"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":"IngestError::Rejected","httpStatus":400,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":711,"sourceCode":"                // Not a member and channel is private — fall through to owner/admin/owner-of-agent check.\n            }\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                    {","sourceCodeStart":693,"sourceCodeEnd":729,"githubUrl":"https://github.com/block/buzz/blob/f956e6fe06a76e50cbd8fba1a162482e752e7f1a/crates/buzz-relay/src/handlers/side_effects.rs#L693-L729","documentation":"Thrown by kind:9005 (DELETE_EVENT) when the actor is not the message author, not a channel owner/admin, and not the owning human of the agent that authored the target. The author check uses effective_message_author (for relay-signed REST messages the real author is the p tag, not the relay key), and even true authors fall through to this check when they are no longer members of a private channel.","triggerScenarios":"Publishing 9005 for someone else's message without owner/admin role; using key A to delete a message authored by key B (e.g. agent key vs human key); the original author trying to delete after being removed from a private channel; agent-owner mapping not covering the key you publish with.","commonSituations":"Humans deleting bot messages with their own key when the bot was registered under a different owner; moderation tools running with a member key; deleting a message you authored from a device with a different keypair; stale agent-owner rows after re-pairing a bot.","solutions":["Delete your own messages with the same keypair that authored them (for relay-signed REST messages, the key in the message's p tag).","For others' messages, publish with an owner/admin key or ask one to perform the deletion.","If the message was authored by your agent, ensure is_agent_owner(community, author, your_key) holds — re-register the agent under your key if the mapping is stale.","If you authored it but were removed from a private channel, you cannot self-delete anymore — an owner/admin must do it."],"exampleFix":"# before — human key deletes an agent-authored message they don't own\nBUZZ_PRIVATE_KEY=$HUMAN_KEY buzz messages delete --channel $CH --event $ID\n# → \"must be event author or channel owner/admin\"\n\n# after — publish as the author key (the agent's), or as an admin\nBUZZ_PRIVATE_KEY=$AGENT_KEY buzz messages delete --channel $CH --event $ID   # author path\nBUZZ_PRIVATE_KEY=$OWNER_KEY buzz messages delete --channel $CH --event $ID  # owner/admin path","handlingStrategy":"validation","validationCode":"// Verify authorship (or role) before publishing kind:9005\nconst me = myPubkeys; // includes human key + owned agent keys\nconst target = await buzz.messagesGet(eventId);\nconst author = effectiveAuthor(target); // p tag when relay-signed, else pubkey\nconst members = await buzz.channelsMembersList(channelId);\nconst myRole = members.find((m) => m.pubkey === myPubkey)?.role;\n\nif (!me.has(author) && myRole !== \"owner\" && myRole !== \"admin\") {\n  throw new Error(\"Only the author (or an owner/admin) can delete this message\");\n}","typeGuard":"function canDeleteMessage(\n  authorPubkey: string,\n  myKeys: Set<string>,\n  myRole: \"owner\" | \"admin\" | \"member\" | undefined,\n  iOwnAuthorAgent: boolean,\n): boolean {\n  if (myKeys.has(authorPubkey)) return true;\n  if (myRole === \"owner\" || myRole === \"admin\") return true;\n  return iOwnAuthorAgent;\n}","tryCatchPattern":"try {\n  await sdk.deleteEvent(channelId, eventId);\n} catch (e) {\n  if (String(e).includes(\"must be event author or channel owner/admin\")) {\n    ui.toast(\"You can only delete your own messages here — ask a channel admin otherwise\");\n  } else throw e;\n}","preventionTips":["Publish deletes with the same keypair that authored the message (agent key for bot messages).","For relay-signed REST messages, remember the real author is the p tag inside the event, not the relay key.","Authors removed from private channels lose self-delete rights — design moderation flows accordingly.","Keep agent registration (agent-owner mapping) current so the owner-of-agent fallback works."],"tags":["nostr","authorization","insufficient-permissions","kind-9005","relay"],"backgroundTag":"insufficient-permissions","analyzedSha":"f956e6fe06a76e50cbd8fba1a162482e752e7f1a","analyzedAt":"2026-08-16T22:11:40.750Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}