block/buzz · error · IngestError::Rejected

an admin cannot ban or time out a community owner or fellow

Error message

an admin cannot ban or time out a community owner or fellow admin

What it means

Moderation authorization in the relay: an actor whose relay_members role is 'admin' holds community-wide capabilities, but the Ban and Timeout actions are explicitly capped — they may never target a member whose role is 'owner' or 'admin'. Only the community owner may action an admin (and nobody but the process owner path actions the owner). The guard keys on the TARGET's role row only: a target with no relay_members row (e.g. a drive-by spammer who left) is still bannable. Unban/Untimeout are intentionally NOT guarded at this seam — lifting another admin's restriction is benign, audited, and owner-reversible — and the command handler separately rejects banned actors on every transport.

Source

Thrown at crates/buzz-relay/src/handlers/moderation_authz.rs:167

    channel_role: Option<&str>,
    action: ModerationAction,
) -> anyhow::Result<ModerationAuthority> {
    match actor_role {
        // Owner holds every capability, community-wide, with no guard rail.
        Some("owner") => Ok(ModerationAuthority::CommunityOwner),
        // Admin holds every capability, but cannot ban/timeout the owner or a
        // fellow admin — only the owner may action an admin. The guard trips only
        // on a target *role* of owner/admin: a target with no `relay_members` row
        // (a drive-by spammer who already left) is bannable. Unban/Untimeout lift
        // a restriction and are intentionally unguarded at this role seam. The
        // command handler separately rejects a banned actor on every transport,
        // so the reachable case is an unrestricted admin lifting another admin's
        // restriction; that remains benign, audited, and owner-reversible.
        Some("admin") => {
            if matches!(action, ModerationAction::Ban | ModerationAction::Timeout)
                && matches!(target_role, Some("owner") | Some("admin"))
            {
                anyhow::bail!("an admin cannot ban or time out a community owner or fellow admin");
            }
            Ok(ModerationAuthority::CommunityAdmin)
        }
        // Not a community owner/admin: channel owner/admin keep channel-local
        // authority for DeleteMessage/Kick only.
        _ => match (action, channel_role) {
            (
                ModerationAction::DeleteMessage | ModerationAction::Kick,
                Some("owner") | Some("admin"),
            ) => Ok(ModerationAuthority::ChannelRole),
            _ => anyhow::bail!("moderator access required"),
        },
    }
}

#[cfg(test)]
mod tests {
    use super::*;

View on GitHub (pinned to f956e6fe06)

Solutions

  1. Route the action through the community owner: the owner's key signs the ban/timeout against the admin target — that path is permitted.
  2. If the target admin must be stopped urgently and the owner is unavailable, use channel-scope tools an admin still holds (e.g. delete-message) or demote via owner-approved governance; do not attempt direct bans.
  3. For anti-spam bots, exclude owner/admin pubkeys from automatic ban lists (fetch the kind:39001 admin list first) so the guard is never hit.
  4. If you intended UNban or un-timeout of a fellow admin, that is intentionally allowed — re-issue as Unban/Untimeout, which is unguarded at this seam.

Example fix

// before (admin key signs ban targeting another admin)
const ev = await createModerationEvent(9000, { p: adminB_pubkey, reason: 'spam' }); // -> 403 guard

// after (community owner's key signs the same action)
await impersonateOwner(); // or have the owner run the moderation
const ev = await createModerationEvent(9000, { p: adminB_pubkey, reason: 'spam' });
Defensive patterns

Strategy: type-guard

Validate before calling

// before issuing ban/timeout, fetch roles and veto elevated targets
const admins = await fetchAdminList(relayUrl, channelId); // kind:39001 p-tags + owner
function canModerate(actorRole: string, targetPubkey: string, action: 'ban'|'timeout'): boolean {
  if (actorRole !== 'admin' && actorRole !== 'owner') return false;
  if (actorRole === 'admin' && admins.has(targetPubkey)) return false; // guard mirrors relay
  return true;
}

Type guard

function isProtectedTarget(actorRole: string, targetRole: string | null, action: string): boolean {
  const elevated = targetRole === 'owner' || targetRole === 'admin';
  return actorRole === 'admin' && elevated && (action === 'ban' || action === 'timeout');
}

Try / catch

// relay returns an authz error for kind:900x — surface it as a role issue, not a transport failure
try { await publishModeration(action, targetPubkey); }
catch (e) {
  if (String(e).includes('cannot ban or time out'))
    throw new Error(`Refused: ${targetPubkey} is owner/admin; ask the community owner to act`);
  throw e;
}

Prevention

When it happens

Trigger: A community admin sends a kind:900x ban/timeout moderation event (via WebSocket NIP-29 or the equivalent HTTP path) whose p-tag targets the community owner's pubkey or another admin's pubkey. The authz resolution sees actor role 'admin' + target role 'owner'/'admin' + action Ban|Timeout and bails before any state change.

Common situations: Two admins in a heated channel, one tries to timeout the other; automation/bot accounts with admin role applying anti-spam bans that sweep an admin's pubkey; attempts to ban the owner during a dispute. Legitimate path: have the OWNER issue the ban/timeout against the admin.

Understand the failure class

Related errors


AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16). Data as JSON: /api/errors/2cc747300acfd301. Report an issue: GitHub.