Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions crates/buzz-relay/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,14 @@ pub struct Config {
/// with the `owner` role on first startup.
pub relay_owner_pubkey: Option<String>,

/// 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down
54 changes: 54 additions & 0 deletions crates/buzz-relay/src/handlers/side_effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>> = 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");
}
Expand All @@ -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(())
}
Expand Down