From 675ea4ed119efcf4aeee35fc4a9cc9aa36e37469 Mon Sep 17 00:00:00 2001 From: wilson Date: Mon, 3 Aug 2026 16:27:39 +0800 Subject: [PATCH] feat(relay): auto-add relay owner to newly created channels Add BUZZ_AUTO_ADD_OWNER_TO_CHANNELS (default off). When enabled, the relay owner (RELAY_OWNER_PUBKEY) is automatically enrolled as an admin member of every channel they did not create, so the owner's channel list stays in sync with agent- and member-created channels without a manual join. The enrollment runs in handle_create_group before NIP-29 discovery emission, so the kind:39002 members roster already includes the owner, and emits a member-added notification so the owner's client subscribes in real time. Idempotent and non-fatal: the channel and creator membership are already committed if the owner add fails. Signed-off-by: wilson --- crates/buzz-relay/src/config.rs | 38 +++++++++++++ .../buzz-relay/src/handlers/side_effects.rs | 54 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/crates/buzz-relay/src/config.rs b/crates/buzz-relay/src/config.rs index 037c6b1dd3..953daedcbf 100644 --- a/crates/buzz-relay/src/config.rs +++ b/crates/buzz-relay/src/config.rs @@ -183,6 +183,14 @@ pub struct Config { /// with the `owner` role on first startup. pub relay_owner_pubkey: Option, + /// When true (`BUZZ_AUTO_ADD_OWNER_TO_CHANNELS`), the relay owner + /// (`RELAY_OWNER_PUBKEY`) is automatically added as an `admin` member of + /// every newly created channel they did not create themselves. This keeps + /// the owner's channel list in sync with agent- or member-created channels + /// without a manual join. Default off; requires `relay_owner_pubkey` to be + /// set to have any effect. + pub auto_add_owner_to_channels: bool, + /// Canonical HTTP origin of the deployment-global operator API. /// /// Every operator NIP-98 `u` tag is verified against this origin, independent @@ -908,6 +916,7 @@ impl Config { let privacy_markdown = read_policy_markdown("BUZZ_PRIVACY_POLICY_MARKDOWN")?; let age_attestation_required = parse_optional_bool("BUZZ_AGE_ATTESTATION_REQUIRED")?; let audit_enabled = parse_bool("BUZZ_AUDIT_ENABLED", true)?; + let auto_add_owner_to_channels = parse_bool("BUZZ_AUTO_ADD_OWNER_TO_CHANNELS", false)?; let join_policy = if terms_markdown.is_none() && privacy_markdown.is_none() && !age_attestation_required @@ -1016,6 +1025,7 @@ impl Config { mesh, mesh_demo_echo, relay_owner_pubkey, + auto_add_owner_to_channels, relay_operator_api_origin, relay_operator_pubkeys, allow_nip_oa_auth, @@ -1187,6 +1197,34 @@ mod tests { )); } + #[test] + fn auto_add_owner_to_channels_defaults_off_and_opts_in() { + let _guard = ENV_MUTEX.lock().unwrap(); + let previous = std::env::var_os("BUZZ_AUTO_ADD_OWNER_TO_CHANNELS"); + + std::env::remove_var("BUZZ_AUTO_ADD_OWNER_TO_CHANNELS"); + let default_off = Config::from_env() + .expect("default config") + .auto_add_owner_to_channels; + + std::env::set_var("BUZZ_AUTO_ADD_OWNER_TO_CHANNELS", "true"); + let opted_in = Config::from_env() + .expect("opt-in config") + .auto_add_owner_to_channels; + + if let Some(value) = previous { + std::env::set_var("BUZZ_AUTO_ADD_OWNER_TO_CHANNELS", value); + } else { + std::env::remove_var("BUZZ_AUTO_ADD_OWNER_TO_CHANNELS"); + } + + assert!(!default_off, "auto-add owner must default off"); + assert!( + opted_in, + "BUZZ_AUTO_ADD_OWNER_TO_CHANNELS=true must enable auto-add" + ); + } + #[cfg(unix)] #[test] fn s3_addressing_style_env_rejects_non_unicode_values() { diff --git a/crates/buzz-relay/src/handlers/side_effects.rs b/crates/buzz-relay/src/handlers/side_effects.rs index 98f8a9aa84..a200f1228d 100644 --- a/crates/buzz-relay/src/handlers/side_effects.rs +++ b/crates/buzz-relay/src/handlers/side_effects.rs @@ -1855,6 +1855,43 @@ async fn handle_create_group( ) .await?; + // Auto-enroll the relay owner into channels they did not create, so the + // owner's channel list stays in sync with agent- and member-created channels + // without a manual join. Gated by BUZZ_AUTO_ADD_OWNER_TO_CHANNELS; a no-op + // unless RELAY_OWNER_PUBKEY is configured. Runs before discovery emission so + // the NIP-29 kind:39002 roster already includes the owner. Non-fatal: the + // channel and creator membership are already committed. + let mut auto_added_owner: Option> = None; + if state.config.auto_add_owner_to_channels { + if let Some(owner_bytes) = state + .config + .relay_owner_pubkey + .as_deref() + .and_then(|hex_pk| hex::decode(hex_pk).ok()) + .filter(|bytes| *bytes != actor_bytes) + { + match state + .db + .add_member( + tenant.community(), + channel.id, + &owner_bytes, + buzz_db::channel::MemberRole::Admin, + Some(&actor_bytes), + ) + .await + { + Ok(_) => { + state.invalidate_membership(tenant, channel.id, &owner_bytes); + auto_added_owner = Some(owner_bytes); + } + Err(e) => { + warn!(channel = %channel.id, error = %e, "auto-add relay owner to channel failed"); + } + } + } + } + if let Err(e) = emit_group_discovery_events(tenant, state, channel.id).await { warn!(channel = %channel.id, error = %e, "NIP-29 group discovery emission failed"); } @@ -1872,6 +1909,23 @@ async fn handle_create_group( warn!(channel = %channel.id, error = %e, "membership notification emission failed"); } + // Notify the auto-added owner so their client subscribes to the new channel + // in real time (mirrors the creator notification above). + if let Some(owner_bytes) = auto_added_owner { + if let Err(e) = emit_membership_notification( + tenant, + state, + channel.id, + &actor_bytes, + &owner_bytes, + KIND_MEMBER_ADDED_NOTIFICATION, + ) + .await + { + warn!(channel = %channel.id, error = %e, "owner membership notification emission failed"); + } + } + info!(channel_id = %channel.id, name = %name, "NIP-29 CREATE_GROUP processed"); Ok(()) }