block/buzz · error · IngestError::Rejected
actor not authorized
Error message
actor not authorized
What it means
PUT_USER on a private channel requires the actor to already be an active member, and get_members() (which filters removed_at IS NULL) contains no row for the actor's pubkey. Open channels let any authenticated user add members; private channels require membership before you can invite. A soft-removed member also counts as absent — their stored role is history, not live authority.
Source
Thrown at crates/buzz-relay/src/handlers/side_effects.rs:367
},
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() {
return Err(anyhow::anyhow!("actor not authorized"));
}
if requested_role.is_some_and(|role| role.is_elevated())
&& !actor_role.is_some_and(|role| role.is_elevated())
{
return Err(anyhow::anyhow!(
"only owners/admins may grant elevated roles"
));
}
}
// Changing an ACTIVE existing member's role is privileged in both
// directions, on every visibility. `get_members` filters
// `removed_at IS NULL`, so a soft-removed row is deliberately not an
// "existing member" here: its stored role is history, not live
// authority, and reactivation is governed by the elevated-granter
// check above rather than by the role the row remembers.
//View on GitHub (pinned to f956e6fe06)
Solutions
- Have an existing active member (or the channel owner/admin) perform the add
- If you were removed, get re-added by a member first — a removed row is deliberately not live authority
- Verify your signing pubkey matches the pubkey in channel_members (get_members output) before retrying
- For open channels, no membership is needed — confirm the channel's visibility is actually private before assuming a bug
Example fix
// before: non-member adds a user to a private channel client.publish(put_user(private_channel, target)).await?; // actor not authorized // after: route the add through an active member/owner let member_client = login(channel_owner_key).await?; member_client.publish(put_user(private_channel, target)).await?;
Defensive patterns
Strategy: validation
Validate before calling
// For private channels, verify active membership before publishing 9000
if channel.visibility == "private" {
let members = client.get_members(community, channel_id).await?;
let active = members.iter().any(|m| m.pubkey == actor_bytes /* removed rows already filtered */);
anyhow::ensure!(active, "join the private channel (or get re-added) before adding others");
} Type guard
const isActiveMember = (members: {pubkey: string}[], actor: string): boolean =>
members.some(m => m.pubkey === actor); // get_members filters removed_at IS NULL Try / catch
match validate_admin_event(&tenant, 9000, &event, &state).await {
Err(e) if e.to_string().contains("actor not authorized") => {
// Escalate to an existing member; retrying the same signer is futile
request_member_invite(channel_id, target_pubkey).await
}
other => other,
} Prevention
- Pre-check channel visibility and actor membership in the invite UI before showing an add button
- Community-level roles do not bypass private-channel membership at this seam — grant channel membership explicitly
- Soft-removed members lose add rights immediately; re-invite them before they can invite others
When it happens
Trigger: A non-member (or never-invited user) publishes 9000 against a private channel; an actor who was soft-removed from the private channel tries to re-invite themselves or others; the actor's pubkey has no membership row because they only ever had community-level (relay_members) status, not channel membership.
Common situations: Community admins assume their relay-wide role grants private-channel add rights — it does not at this validator seam; users removed from a private channel reusing an old invite flow; signing key rotated so the membership row's pubkey no longer matches.
Related errors
- only owners/admins may grant elevated roles
- only owners/admins may change an active member's role
- missing or invalid h tag
- channel not found
- channel is archived
AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16).
Data as JSON: /api/errors/21d5b4f07d970304.
Report an issue: GitHub.