{"record":{"id":"9b1e94268bd6b4e5","repo":"block/buzz","slug":"invalid-role-role-str","errorCode":null,"errorMessage":"invalid role: {role_str}","messagePattern":"invalid role: (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":1306,"sourceCode":"    info!(pubkey = %hex::encode(&pubkey_bytes), \"kind:0 profile synced to users table\");\n    Ok(())\n}\n\nasync fn handle_put_user(\n    tenant: &TenantContext,\n    event: &Event,\n    state: &Arc<AppState>,\n) -> anyhow::Result<()> {\n    let channel_id =\n        extract_h_tag_channel(event).ok_or_else(|| anyhow::anyhow!(\"missing h tag\"))?;\n    let target_pubkey = extract_p_tag(event).ok_or_else(|| anyhow::anyhow!(\"missing p tag\"))?;\n    // No role tag = no role change: preserve an existing member's current role and\n    // fall back to Member only for a new member. Unconditionally defaulting to\n    // Member let a bare PUT_USER silently demote an existing owner/admin.\n    let role: MemberRole = match extract_tag_value(event, \"role\") {\n        Some(role_str) => role_str\n            .parse()\n            .map_err(|_| anyhow::anyhow!(\"invalid role: {role_str}\"))?,\n        None => state\n            .db\n            .get_members_for_event_write(tenant.community(), channel_id)\n            .await?\n            .iter()\n            .find(|m| m.pubkey == target_pubkey)\n            .and_then(|m| m.role.parse().ok())\n            .unwrap_or(MemberRole::Member),\n    };\n\n    let actor_bytes = event.pubkey.to_bytes().to_vec();\n\n    state\n        .db\n        .add_member(\n            tenant.community(),\n            channel_id,\n            &target_pubkey,","sourceCodeStart":1288,"sourceCodeEnd":1324,"githubUrl":"https://github.com/block/buzz/blob/dad5a33865fc81a2e55b3b60746632f615ec1e3a/crates/buzz-relay/src/handlers/side_effects.rs#L1288-L1324","documentation":"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).","triggerScenarios":"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).","commonSituations":"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.","solutions":["Use an exact MemberRole wire string in the `role` tag (match the enum's FromStr in buzz-db channel roles)","Omit the `role` tag entirely if no role change is intended — existing role is preserved","Add client-side validation against the allowed role names before publishing","Check the buzz-db MemberRole enum for the current canonical spellings"],"exampleFix":"// before\n.tag(Tag::custom(\"role\", [\"Admin\"])) // capitalization mismatch\n// after\n.tag(Tag::custom(\"role\", [\"admin\"])) // exact MemberRole parseable value","handlingStrategy":"validation","validationCode":"const ALLOWED_ROLES = ['member', 'admin', 'owner']; // match MemberRole::FromStr\nconst role = event.tags.find(t => t[0] === 'role')?.[1];\nif (role !== undefined && !ALLOWED_ROLES.includes(role)) {\n  throw new Error(`invalid role: ${role}`);\n}","typeGuard":"function isValidRole(r: string | undefined): r is 'member' | 'admin' | 'owner' {\n  return r === undefined || ['member', 'admin', 'owner'].includes(r);\n}","tryCatchPattern":"match handle_put_user(...).await {\n    Err(e) if e.to_string().starts_with(\"invalid role:\") => {\n        // surface allowed role values back to the client\n    }\n    other => other?,\n}","preventionTips":["Derive client role literals from the buzz-db MemberRole enum, not UI labels","Omit the role tag when no role change is intended (preserves existing role)","Add a table test enumerating all parseable/unparseable role strings"],"tags":["rust","nostr","roles","validation","admin"],"backgroundTag":"invalid-enum-value","analyzedSha":"dad5a33865fc81a2e55b3b60746632f615ec1e3a","analyzedAt":"2026-09-05T18:13:50.666Z","contentChangedAt":"2026-09-05T18:13:50.666Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}