block/buzz · error · anyhow::Error

invalid role: {role_str}

Error message

invalid role: {role_str}

What it means

handle_put_user reads the optional `role` tag on a kind:9000 PUT_USER event and parses it into MemberRole. A present but unrecognized role string fails with 'invalid role: {role_str}'. Omitting the tag is valid: existing members keep their current role; new members default to Member (preventing silent demotion).

Source

Thrown at crates/buzz-relay/src/handlers/side_effects.rs:1306

    info!(pubkey = %hex::encode(&pubkey_bytes), "kind:0 profile synced to users table");
    Ok(())
}

async fn handle_put_user(
    tenant: &TenantContext,
    event: &Event,
    state: &Arc<AppState>,
) -> anyhow::Result<()> {
    let channel_id =
        extract_h_tag_channel(event).ok_or_else(|| anyhow::anyhow!("missing h tag"))?;
    let target_pubkey = extract_p_tag(event).ok_or_else(|| anyhow::anyhow!("missing p tag"))?;
    // No role tag = no role change: preserve an existing member's current role and
    // fall back to Member only for a new member. Unconditionally defaulting to
    // Member let a bare PUT_USER silently demote an existing owner/admin.
    let role: MemberRole = match extract_tag_value(event, "role") {
        Some(role_str) => role_str
            .parse()
            .map_err(|_| anyhow::anyhow!("invalid role: {role_str}"))?,
        None => state
            .db
            .get_members_for_event_write(tenant.community(), channel_id)
            .await?
            .iter()
            .find(|m| m.pubkey == target_pubkey)
            .and_then(|m| m.role.parse().ok())
            .unwrap_or(MemberRole::Member),
    };

    let actor_bytes = event.pubkey.to_bytes().to_vec();

    state
        .db
        .add_member(
            tenant.community(),
            channel_id,
            &target_pubkey,

View on GitHub (pinned to dad5a33865)

Solutions

  1. Use an exact MemberRole wire string in the `role` tag (match the enum's FromStr in buzz-db channel roles)
  2. Omit the `role` tag entirely if no role change is intended — existing role is preserved
  3. Add client-side validation against the allowed role names before publishing
  4. Check the buzz-db MemberRole enum for the current canonical spellings

Example fix

// before
.tag(Tag::custom("role", ["Admin"])) // capitalization mismatch
// after
.tag(Tag::custom("role", ["admin"])) // exact MemberRole parseable value
Defensive patterns

Strategy: validation

Validate before calling

const ALLOWED_ROLES = ['member', 'admin', 'owner']; // match MemberRole::FromStr
const role = event.tags.find(t => t[0] === 'role')?.[1];
if (role !== undefined && !ALLOWED_ROLES.includes(role)) {
  throw new Error(`invalid role: ${role}`);
}

Type guard

function isValidRole(r: string | undefined): r is 'member' | 'admin' | 'owner' {
  return r === undefined || ['member', 'admin', 'owner'].includes(r);
}

Try / catch

match handle_put_user(...).await {
    Err(e) if e.to_string().starts_with("invalid role:") => {
        // surface allowed role values back to the client
    }
    other => other?,
}

Prevention

When it happens

Trigger: Publishing a kind:9000 event whose `role` tag value is not one of MemberRole's parseable variants (e.g. 'Owner' vs 'owner', 'admin', 'moderator', typos, or capitalized forms).

Common situations: Client sends 'administrator' or 'mod' instead of the exact enum string; a UI passes a display label instead of the wire value; case mismatch after a client refactor.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of block/buzz@dad5a33865 (2026-09-05). Data as JSON: /api/errors/9b1e94268bd6b4e5. Report an issue: GitHub.