{"record":{"id":"5266242998b86b62","repo":"block/buzz","slug":"missing-p-tag","errorCode":null,"errorMessage":"missing p tag","messagePattern":"missing p tag","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":367,"sourceCode":"            // member that preserves the role they already hold, and only defaults\n            // to Member for a genuinely new member. Defaulting unconditionally to\n            // Member made a bare self-targeted PUT_USER silently demote an owner.\n            let role_str = extract_tag_value(event, \"role\");\n            let requested_role = match role_str {\n                Some(ref s) => match s.parse::<buzz_db::channel::MemberRole>() {\n                    Ok(r) => Some(r),\n                    Err(_) => return Err(anyhow::anyhow!(\"invalid role: {s}\")),\n                },\n                None => None,\n            };\n\n            let members = state.db.get_members(tenant.community(), channel_id).await?;\n            let actor_role: Option<buzz_db::channel::MemberRole> = members\n                .iter()\n                .find(|m| m.pubkey == actor_bytes)\n                .and_then(|m| m.role.parse().ok());\n            let target_pubkey =\n                extract_p_tag(event).ok_or_else(|| anyhow::anyhow!(\"missing p tag\"))?;\n\n            // Authorization policy — visibility gate, elevated-grant gate,\n            // active-member role-change gate, and last-owner demotion — lives in\n            // `channel_authz`, which is pure and table-tested. The database reads\n            // it depends on stay here.\n            match channel_authz::decide_put_user(\n                &channel.visibility,\n                actor_role,\n                requested_role,\n                &members,\n                &target_pubkey,\n                &actor_bytes,\n            )? {\n                // Self-add: always allowed regardless of policy.\n                PutUserDecision::Allow => Ok(()),\n                // Third-party add: check channel_add_policy on the target.\n                PutUserDecision::CheckAddPolicy => {\n                    if let Some((policy, owner)) = state","sourceCodeStart":349,"sourceCodeEnd":385,"githubUrl":"https://github.com/block/buzz/blob/dad5a33865fc81a2e55b3b60746632f615ec1e3a/crates/buzz-relay/src/handlers/side_effects.rs#L349-L385","documentation":"For kind 9000 (PUT_USER) admin events, validate_admin_event extracts the target member from the event's `p` tag via extract_p_tag. If the tag is absent it fails with 'missing p tag'. The p tag identifies which member the role/membership mutation applies to.","triggerScenarios":"A kind:9000 channel PUT_USER event published without a `p` tag (or with an empty/malformed one that extract_p_tag rejects).","commonSituations":"A bot or script constructs the admin event manually and forgets the p tag; a client bug after a UI change drops the target member tag; hand-crafted events during interop testing.","solutions":["Add a `p` tag containing the target member's 32-byte hex pubkey to the kind:9000 event","Validate event shape client-side before publishing (required tags per kind)","Check the client library's PUT_USER helper is actually attaching the tag"],"exampleFix":"// before\nlet ev = EventBuilder::new(9000, \"\").tag(Tag::custom(\"h\", [channel_id]));\n// after\nlet ev = EventBuilder::new(9000, \"\")\n    .tag(Tag::custom(\"h\", [channel_id]))\n    .tag(Tag::public_key(target_pubkey)); // p tag: mutation target","handlingStrategy":"validation","validationCode":"// validate before publishing kind:9000\nconst p = event.tags.find(t => t[0] === 'p');\nif (!p || !/^[0-9a-f]{64}$/.test(p[1])) {\n  throw new Error('kind:9000 requires a p tag with a 32-byte hex pubkey');\n}","typeGuard":"function hasPTag(tags: string[][]): tags is [string[], ...string[][]] & { p: string } {\n  return tags.some(t => t[0] === 'p' && /^[0-9a-f]{64}$/.test(t[1] ?? ''));\n}","tryCatchPattern":"match publish(put_user_event).await {\n    Err(e) if e.to_string().contains(\"missing p tag\") => fix_and_republish_with_p_tag(),\n    other => other?,\n}","preventionTips":["Encode required-tags-per-kind in the client event builder so they cannot be omitted","Add unit tests asserting PUT_USER events always carry a p tag","Validate tag shape (hex pubkey) before signing"],"tags":["rust","nostr","admin","tags","validation"],"backgroundTag":"missing-required-tag","analyzedSha":"dad5a33865fc81a2e55b3b60746632f615ec1e3a","analyzedAt":"2026-09-05T18:13:50.666Z","contentChangedAt":"2026-09-05T18:13:50.666Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}