From d1209c635b63e5e0d1e13d0cd3fa104dfc59a1f2 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Tue, 31 Mar 2026 11:40:22 -0700 Subject: [PATCH] Fix zooid sync --- backend/.env.template | 1 + backend/README.md | 1 + backend/spec/infra.md | 2 +- backend/src/infra.rs | 42 +++++++++++++++++------- backend/src/repo.rs | 8 ++++- frontend/src/components/ActivityFeed.tsx | 1 + 6 files changed, 42 insertions(+), 13 deletions(-) diff --git a/backend/.env.template b/backend/.env.template index 0eb559d..352c701 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -20,6 +20,7 @@ ROBOT_MESSAGING_RELAYS=auth.nostr1.com,relay.keychat.io,relay.ditto.pub # Zooid ZOOID_API_URL=http://127.0.0.1:3334 +ZOOID_API_SECRET= RELAY_DOMAIN=spaces.coracle.social LIVEKIT_URL= LIVEKIT_API_KEY= diff --git a/backend/README.md b/backend/README.md index 6ecbdf3..0989c15 100644 --- a/backend/README.md +++ b/backend/README.md @@ -38,6 +38,7 @@ Environment variables: | `ADMINS` | Comma-separated admin pubkeys (hex) | _optional_ | | `ALLOW_ORIGINS` | Comma-separated CORS origins. If empty, CORS is permissive. | _optional_ | | `ZOOID_API_URL` | Zooid API base URL used by infra worker | _required for infra sync_ | +| `ZOOID_API_SECRET` | Nostr secret key used for authentication of requests to the zooid API | _required_ | | `RELAY_DOMAIN` | Base domain appended to relay subdomains | empty | | `LIVEKIT_URL` | LiveKit URL sent to zooid when relay livekit is enabled | _optional_ | | `LIVEKIT_API_KEY` | LiveKit API key sent to zooid | _optional_ | diff --git a/backend/spec/infra.md b/backend/spec/infra.md index dd30dbe..7fb30c2 100644 --- a/backend/spec/infra.md +++ b/backend/spec/infra.md @@ -28,6 +28,6 @@ Iterates over `repo.list_activity` since last run and does the following: ## `async fn sync_relay(&self, relay: &Relay, is_new: bool)` -- If `is_new`, sends `POST /relay` to create the relay in zooid. +- 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. diff --git a/backend/src/infra.rs b/backend/src/infra.rs index 381ccd2..766a4f1 100644 --- a/backend/src/infra.rs +++ b/backend/src/infra.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use nostr_sdk::prelude::*; use tokio::sync::Mutex; use crate::repo::Repo; @@ -10,6 +11,7 @@ pub struct Infra { livekit_url: String, livekit_api_key: String, livekit_api_secret: String, + api_secret: String, repo: Repo, last_activity_at: std::sync::Arc>, } @@ -21,12 +23,14 @@ impl Infra { let livekit_url = std::env::var("LIVEKIT_URL").unwrap_or_default(); let livekit_api_key = std::env::var("LIVEKIT_API_KEY").unwrap_or_default(); let livekit_api_secret = std::env::var("LIVEKIT_API_SECRET").unwrap_or_default(); + let api_secret = std::env::var("ZOOID_API_SECRET").unwrap_or_default(); Self { api_url, relay_domain, livekit_url, livekit_api_key, livekit_api_secret, + api_secret, repo, last_activity_at: std::sync::Arc::new(Mutex::new(0)), } @@ -67,7 +71,10 @@ impl Infra { let is_new = relay.synced == 0; match self.sync_relay(&relay, is_new).await { - Ok(()) => self.repo.mark_relay_synced(&relay.id).await?, + Ok(()) => { + tracing::info!(relay = %relay.id, "relay sync succeeded"); + self.repo.mark_relay_synced(&relay.id).await? + } Err(e) => { tracing::warn!(relay = %relay.id, error = %e, "relay sync failed"); self.repo.fail_relay_sync(&relay, e.to_string()).await?; @@ -81,6 +88,13 @@ impl Infra { Ok(()) } + async fn nip98_auth(&self, url: &str, method: HttpMethod) -> Result { + let keys = Keys::parse(&self.api_secret)?; + let server_url = Url::parse(url)?; + let auth = HttpData::new(server_url, method).to_authorization(&keys).await?; + Ok(auth) + } + async fn sync_relay(&self, relay: &crate::models::Relay, is_new: bool) -> Result<()> { let client = reqwest::Client::new(); let base = self.api_url.trim_end_matches('/'); @@ -91,12 +105,12 @@ impl Infra { format!("{}.{}", relay.subdomain, self.relay_domain) }; - let secret = uuid::Uuid::new_v4().to_string(); + let secret = Keys::generate().secret_key().to_secret_hex(); let livekit = if relay.livekit_enabled == 1 { serde_json::json!({ "enabled": true, - "url": self.livekit_url, + "server_url": self.livekit_url, "api_key": self.livekit_api_key, "api_secret": self.livekit_api_secret, }) @@ -113,6 +127,7 @@ impl Infra { "name": relay.info_name, "icon": relay.info_icon, "description": relay.info_description, + "pubkey": relay.tenant, }, "policy": { "public_join": relay.policy_public_join == 1, @@ -123,21 +138,26 @@ impl Infra { "blossom": { "enabled": relay.blossom_enabled == 1 }, "livekit": livekit, "push": { "enabled": relay.push_enabled == 1 }, - "roles": [ - { "name": "admin", "permissions": ["read", "write", "admin"] }, - { "name": "member", "permissions": ["read", "write"] }, - { "name": "guest", "permissions": ["read"] }, - ], + "roles": { + "admin": { "can_manage": true, "can_invite": true }, + "member": { "can_invite": true }, + }, }); let response = if is_new { - client.post(format!("{}/relay", base)).json(&body).send().await? + 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? } else { - client.put(format!("{}/relay/{}", base, relay.id)).json(&body).send().await? + 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? }; if !response.status().is_success() { - anyhow::bail!("zooid sync returned {}", response.status()) + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + anyhow::bail!("zooid sync returned {}: {}", status, body) } Ok(()) } diff --git a/backend/src/repo.rs b/backend/src/repo.rs index f7e1415..60fe8de 100644 --- a/backend/src/repo.rs +++ b/backend/src/repo.rs @@ -313,10 +313,16 @@ impl Repo { } pub async fn mark_relay_synced(&self, relay_id: &str) -> Result<()> { + let mut tx = self.pool.begin().await?; + sqlx::query("UPDATE relay SET synced = 1, status = 'active', sync_error = '' WHERE id = ?") .bind(relay_id) - .execute(&self.pool) + .execute(&mut *tx) .await?; + + Self::insert_activity(&mut tx, "mark_relay_synced", "relay", relay_id).await?; + + tx.commit().await?; Ok(()) } diff --git a/frontend/src/components/ActivityFeed.tsx b/frontend/src/components/ActivityFeed.tsx index e7485bc..914ec29 100644 --- a/frontend/src/components/ActivityFeed.tsx +++ b/frontend/src/components/ActivityFeed.tsx @@ -14,6 +14,7 @@ const ACTIVITY_LABELS: Record = { mark_invoice_attempted: "Invoice payment attempted", mark_invoice_sent: "Invoice sent", mark_invoice_closed: "Invoice closed", + mark_relay_synced: "Relay synchronized", } function formatDate(ts: number) {