block/buzz · error · IngestError::Rejected
invalid role: {s}
Error message
invalid role: {s} What it means
A kind 9000 PUT_USER carried a 'role' tag whose value failed MemberRole::from_str. Buzz's channel role vocabulary is exactly five canonical lowercase strings — owner, admin, member, guest, bot — matching the DB enum and Nostr tags. Any other spelling (including case variants like 'Admin', abbreviations like 'mod', or plural forms) is rejected before the membership change is stored.
Source
Thrown at crates/buzz-relay/src/handlers/side_effects.rs:348
&& event.tags.iter().any(|t| {
let parts = t.as_slice();
parts.len() >= 2 && parts[0] == "archived" && parts[1] == "false"
});
if channel.archived_at.is_some() && !is_unarchive_request {
return Err(anyhow::anyhow!("channel is archived"));
}
match kind {
9000 => {
// An absent role tag means "no role change requested": for an existing
// member that preserves the role they already hold, and only defaults
// to Member for a genuinely new member. Defaulting unconditionally to
// Member made a bare self-targeted PUT_USER silently demote an owner.
let role_str = extract_tag_value(event, "role");
let requested_role = match role_str {
Some(ref s) => match s.parse::<buzz_db::channel::MemberRole>() {
Ok(r) => Some(r),
Err(_) => return Err(anyhow::anyhow!("invalid role: {s}")),
},
None => None,
};
let members = state.db.get_members(tenant.community(), channel_id).await?;
let actor_role: Option<buzz_db::channel::MemberRole> = members
.iter()
.find(|m| m.pubkey == actor_bytes)
.and_then(|m| m.role.parse().ok());
let target_pubkey =
extract_p_tag(event).ok_or_else(|| anyhow::anyhow!("missing p tag"))?;
// PUT_USER: open channels allow any authenticated user; private channels
// require the actor to be an existing active member. Any active member may
// add an ordinary member, guest, or bot, but only owners/admins may grant
// an elevated role.
if channel.visibility == "private" {
if actor_role.is_none() {View on GitHub (pinned to f956e6fe06)
Solutions
- Use one of the five canonical values: owner, admin, member, guest, bot (lowercase)
- Validate the role string against the allowed set before publishing the 9000 event
- Map UI labels to wire values centrally (e.g. 'Moderator' → 'admin') instead of sending display strings
Example fix
// before
Tag::custom(TagKind::Custom("role"), vec!["moderator"]) // → invalid role: moderator
// after
const ROLES: [&str; 5] = ["owner", "admin", "member", "guest", "bot"];
assert!(ROLES.contains(&role.as_str()), "unknown role {role}");
Tag::custom(TagKind::Custom("role"), vec![role]) Defensive patterns
Strategy: type-guard
Validate before calling
// Validate the role tag against the exact wire vocabulary before publish
const ROLES: [&str; 5] = ["owner", "admin", "member", "guest", "bot"];
if let Some(role) = role_tag_value {
anyhow::ensure!(ROLES.contains(&role.as_str()), "invalid role {role}");
} Type guard
const MEMBER_ROLES = ["owner", "admin", "member", "guest", "bot"] as const; export type MemberRole = typeof MEMBER_ROLES[number]; const isMemberRole = (v: string): v is MemberRole => (MEMBER_ROLES as readonly string[]).includes(v);
Try / catch
match validate_admin_event(&tenant, 9000, &event, &state).await {
Err(e) if e.to_string().starts_with("invalid role") => {
// surface the allowed list to the user; never retry with a synonym
reject("role must be one of owner|admin|member|guest|bot", &event.id)
}
other => other,
} Prevention
- Define the five role strings as a shared constant/type used by both the UI dropdown and the event builder
- Map display labels ('Moderator') to wire values ('admin') in one place
- Reject unknown roles at parse time in your client so bad events never reach the relay
When it happens
Trigger: role tag value "moderator", "mod", "Member" (capitalized), "admin " (trailing space), or a numeric role like "3"; role taken from a UI dropdown whose labels differ from the wire values; templating that uppercases tag values.
Common situations: Design docs introduce a 'moderator' tier that the enum does not have (v1 hierarchy is Owner > Admin > Member > Guest with Bot separate); locale/casing transformations in the client; role strings sourced from a different product's vocabulary.
Related errors
- missing or invalid h tag
- missing p tag
- only owners/admins may grant elevated roles
- only owners/admins may change an active member's role
- moderator access required
AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16).
Data as JSON: /api/errors/e1f51f8a1c021c30.
Report an issue: GitHub.