From 364b8fd26cca444d9f85c4182839dc17004e4faf Mon Sep 17 00:00:00 2001 From: userAdityaa Date: Sat, 18 Apr 2026 14:15:28 +0545 Subject: [PATCH] fix: relay secret rotation on infra sync updates --- backend/spec/infra.md | 5 +- backend/spec/models.md | 3 +- backend/src/infra.rs | 116 ++++++++++++++++++++++++----------------- 3 files changed, 72 insertions(+), 52 deletions(-) diff --git a/backend/spec/infra.md b/backend/spec/infra.md index b4beff8..4c4b45a 100644 --- a/backend/spec/infra.md +++ b/backend/spec/infra.md @@ -30,5 +30,6 @@ Members: ## `async fn sync_relay(&self, relay: &Relay, is_new: bool)` - If `is_new`, sends `POST /relay/:id` to create the relay in zooid. -- Otherwise, sends `PUT /relay/:id` to update it. -- Passes full relay configuration in the body including host, schema, secret, inactive flag, info, policy, groups, management, blossom, livekit, push, and roles. +- Otherwise, sends `PATCH /relay/:id` to update it. +- Includes `secret` only for relay creation (`POST`) so updates do not rotate relay identity. +- Passes relay configuration in the body including host, schema, inactive flag, info, policy, groups, management, blossom, livekit, push, and roles. diff --git a/backend/spec/models.md b/backend/spec/models.md index b0c937d..0dd429f 100644 --- a/backend/spec/models.md +++ b/backend/spec/models.md @@ -85,9 +85,8 @@ A relay is a nostr relay owned by a `tenant` and hosted by the attached zooid in Some attributes persisted to zooid via API have special handling: -- The relay's `secret` is generated once and persisted to the zooid configuration but isn't stored in the database. +- The relay's `secret` is generated once on relay creation, persisted to the zooid configuration, and isn't stored in the database. Relay updates do not resend `secret`. - The relay's `host` is calculated based on `subdomain` + `RELAY_DOMAIN` - The value of `inactive` is calculated based on `status` - The relay's `livekit_*` configuration is inferred based on environment variables and `livekit_enabled`. - The relay's `roles` are hard-coded for now. - diff --git a/backend/src/infra.rs b/backend/src/infra.rs index fea69c0..6f09271 100644 --- a/backend/src/infra.rs +++ b/backend/src/infra.rs @@ -2,7 +2,7 @@ use anyhow::Result; use nostr_sdk::prelude::*; use crate::command::Command; -use crate::models::{Activity, RELAY_STATUS_DELINQUENT, RELAY_STATUS_INACTIVE}; +use crate::models::{Activity, Relay, RELAY_STATUS_DELINQUENT, RELAY_STATUS_INACTIVE}; use crate::query::Query; #[derive(Clone)] @@ -70,7 +70,7 @@ impl Infra { Ok(()) } - async fn sync_and_report(&self, relay: &crate::models::Relay, is_new: bool) { + async fn sync_and_report(&self, relay: &Relay, is_new: bool) { match self.sync_relay(relay, is_new).await { Ok(()) => { tracing::info!(relay = %relay.id, "relay sync succeeded"); @@ -96,7 +96,7 @@ impl Infra { Ok(auth) } - async fn sync_relay(&self, relay: &crate::models::Relay, is_new: bool) -> Result<()> { + async fn sync_relay(&self, relay: &Relay, is_new: bool) -> Result<()> { let client = reqwest::Client::new(); let base = self.api_url.trim_end_matches('/'); @@ -106,8 +106,6 @@ impl Infra { format!("{}.{}", relay.subdomain, self.relay_domain) }; - let secret = Keys::generate().secret_key().to_secret_hex(); - let livekit = if relay.livekit_enabled == 1 { serde_json::json!({ "enabled": true, @@ -119,53 +117,28 @@ impl Infra { serde_json::json!({ "enabled": false }) }; - let body = serde_json::json!({ - "host": host, - "schema": relay.schema, - "secret": secret, - "inactive": relay.status == RELAY_STATUS_INACTIVE - || relay.status == RELAY_STATUS_DELINQUENT, - "info": { - "name": relay.info_name, - "icon": relay.info_icon, - "description": relay.info_description, - "pubkey": relay.tenant, - }, - "policy": { - "public_join": relay.policy_public_join == 1, - "strip_signatures": relay.policy_strip_signatures == 1, - }, - "groups": { "enabled": relay.groups_enabled == 1 }, - "management": { "enabled": relay.management_enabled == 1 }, - "blossom": { "enabled": relay.blossom_enabled == 1 }, - "livekit": livekit, - "push": { "enabled": relay.push_enabled == 1 }, - "roles": { - "admin": { "can_manage": true, "can_invite": true }, - "member": { "can_invite": true }, - }, - }); + let body = relay_sync_body( + relay, + host, + livekit, + is_new.then(|| Keys::generate().secret_key().to_secret_hex()), + ); - let response = if is_new { - let url = format!("{}/relay/{}", base, relay.id); - let auth = self.nip98_auth(&url, HttpMethod::POST).await?; - client - .post(&url) - .header("Authorization", auth) - .json(&body) - .send() - .await? + let url = format!("{}/relay/{}", base, relay.id); + let auth = self.nip98_auth(&url, zooid_sync_http_method(is_new)).await?; + + let request = if is_new { + client.post(&url) } else { - let url = format!("{}/relay/{}", base, relay.id); - let auth = self.nip98_auth(&url, HttpMethod::PUT).await?; - client - .put(&url) - .header("Authorization", auth) - .json(&body) - .send() - .await? + client.patch(&url) }; + let response = request + .header("Authorization", auth) + .json(&body) + .send() + .await?; + if !response.status().is_success() { let status = response.status(); let body = response.text().await.unwrap_or_default(); @@ -175,6 +148,53 @@ impl Infra { } } +fn zooid_sync_http_method(is_new: bool) -> HttpMethod { + if is_new { + HttpMethod::POST + } else { + HttpMethod::PATCH + } +} + +fn relay_sync_body( + relay: &Relay, + host: String, + livekit: serde_json::Value, + secret: Option, +) -> serde_json::Value { + let mut body = serde_json::json!({ + "host": host, + "schema": relay.schema, + "inactive": relay.status == RELAY_STATUS_INACTIVE + || relay.status == RELAY_STATUS_DELINQUENT, + "info": { + "name": relay.info_name, + "icon": relay.info_icon, + "description": relay.info_description, + "pubkey": relay.tenant, + }, + "policy": { + "public_join": relay.policy_public_join == 1, + "strip_signatures": relay.policy_strip_signatures == 1, + }, + "groups": { "enabled": relay.groups_enabled == 1 }, + "management": { "enabled": relay.management_enabled == 1 }, + "blossom": { "enabled": relay.blossom_enabled == 1 }, + "livekit": livekit, + "push": { "enabled": relay.push_enabled == 1 }, + "roles": { + "admin": { "can_manage": true, "can_invite": true }, + "member": { "can_invite": true }, + }, + }); + + if let (Some(secret), Some(body_obj)) = (secret, body.as_object_mut()) { + body_obj.insert("secret".to_string(), serde_json::Value::String(secret)); + } + + body +} + fn should_sync_relay_activity(activity_type: &str) -> bool { matches!( activity_type, -- 2.52.0