fix(channels): let owner agents rename channels

Signed-off-by: Sol <49aa1f65411fd096d2e2ec144f1e7aa36fdc76d1b907cfdf7be000c66f9d3b8e@buzz.block.builderlab.xyz>
This commit is contained in:
Sol
2026-09-25 13:33:18 -04:00
parent 9263cda845
commit 75f5cd868c
2 changed files with 66 additions and 15 deletions

View File

@@ -70,6 +70,20 @@ pub fn is_sole_owner(members: &[MemberRecord], pubkey: &[u8]) -> bool {
sole && owners.next().is_none()
}
/// Decide whether an actor may change privileged channel metadata.
///
/// The booleans are resolved by the caller from the channel roster and the
/// immutable agent-owner mapping.
pub fn can_edit_privileged_metadata(
actor_role: Option<&str>,
actor_owns_owner_agent: bool,
actor_is_owned_by_elevated_member: bool,
) -> bool {
matches!(actor_role, Some("owner" | "admin"))
|| actor_owns_owner_agent
|| actor_is_owned_by_elevated_member
}
/// Decide whether `actor` may remove themselves from the channel.
///
/// Shared by kind:9001 self-removal and kind:9022 leave — they enforced
@@ -640,6 +654,11 @@ mod tests {
}
}
#[test]
fn owner_agent_may_edit_privileged_metadata_for_its_human_owner() {
assert!(can_edit_privileged_metadata(None, false, true));
}
/// The wire contract: these strings reach NIP-29 clients verbatim, and the
/// two last-owner phrasings are deliberately distinct.
#[test]

View File

@@ -441,6 +441,27 @@ async fn actor_owns_any_owner_agent(
Ok(false)
}
/// Returns `true` if `actor_bytes` is an agent whose owning human is an active
/// owner member. This grants an owner's managed agent the same metadata
/// authority without requiring the agent itself to be added to the channel.
async fn actor_is_owned_by_channel_owner(
state: &Arc<AppState>,
community_id: buzz_core::CommunityId,
members: &[buzz_db::channel::MemberRecord],
actor_bytes: &[u8],
) -> anyhow::Result<bool> {
for member in members.iter().filter(|member| member.role == "owner") {
if state
.db
.is_agent_owner(community_id, actor_bytes, &member.pubkey)
.await?
{
return Ok(true);
}
}
Ok(false)
}
/// Validate an admin kind event BEFORE storage.
pub async fn validate_admin_event(
tenant: &TenantContext,
@@ -664,27 +685,38 @@ pub async fn validate_admin_event(
});
if has_privileged_tag {
let members = state.db.get_members(tenant.community(), channel_id).await?;
let actor_member = members.iter().find(|m| m.pubkey == actor_bytes);
match actor_member {
Some(m) if m.role == "owner" || m.role == "admin" => Ok(()),
_ => {
// Allow the owning human of any active owner-role agent in the
// channel, even when the human is not a channel member —
// diverges from kind:9001 intentionally.
if actor_owns_any_owner_agent(
let actor_role = members
.iter()
.find(|member| member.pubkey == actor_bytes)
.map(|member| member.role.as_str());
let actor_owns_owner_agent = if matches!(actor_role, Some("owner" | "admin")) {
false
} else {
actor_owns_any_owner_agent(state, tenant.community(), &members, &actor_bytes)
.await?
};
let actor_is_owned_by_channel_owner =
if actor_role.is_some() || actor_owns_owner_agent {
false
} else {
actor_is_owned_by_channel_owner(
state,
tenant.community(),
&members,
&actor_bytes,
)
.await?
{
return Ok(());
}
Err(anyhow::anyhow!(
"actor not authorized for name/about/archived/visibility/ttl changes"
))
}
};
if channel_authz::can_edit_privileged_metadata(
actor_role,
actor_owns_owner_agent,
actor_is_owned_by_channel_owner,
) {
Ok(())
} else {
Err(anyhow::anyhow!(
"actor not authorized for name/about/archived/visibility/ttl changes"
))
}
} else {
// topic/purpose: any member