Skip to content
Merged
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
1 change: 0 additions & 1 deletion crates/sprout-audit/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ impl AuditService {
.catch_unwind()
.await;

// Always release the lock before returning the connection to the pool.
let _ = sqlx::query("DO RELEASE_LOCK(?)")
.bind(AUDIT_LOCK_NAME)
.execute(&mut *conn)
Expand Down
1 change: 0 additions & 1 deletion crates/sprout-auth/src/nip42.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ fn normalize_relay_url(raw: &str) -> String {
let _ = parsed.set_host(Some("127.0.0.1"));
}
}
// Remove trailing slash from the path component.
let path = parsed.path().trim_end_matches('/').to_string();
parsed.set_path(&path);
parsed.to_string()
Expand Down
2 changes: 0 additions & 2 deletions crates/sprout-auth/src/okta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,6 @@ impl JwksCache {
ttl_secs: u64,
client: &reqwest::Client,
) -> Result<CachedJwks, AuthError> {
// Fast path: read lock, return if fresh.
{
let guard = self.inner.read().await;
if let Some(cached) = guard.as_ref() {
Expand Down Expand Up @@ -244,7 +243,6 @@ impl JwksCache {
fetched_at: Instant::now(),
};

// Re-acquire write lock to store the result.
// Final re-check: another thread may have stored a fresh entry while
// we were fetching. If so, discard our result and return theirs.
let mut guard = self.inner.write().await;
Expand Down
4 changes: 0 additions & 4 deletions crates/sprout-core/src/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,18 @@ mod tests {
let past = Timestamp::from(now_ts.as_u64() - 3600);
let future = Timestamp::from(now_ts.as_u64() + 3600);

// kind
assert!(filters_match(&[Filter::new().kind(Kind::TextNote)], &ev));
assert!(!filters_match(
&[Filter::new().kind(Kind::ContactList)],
&ev
));

// author
assert!(filters_match(&[Filter::new().author(pubkey)], &ev));
assert!(!filters_match(
&[Filter::new().author(Keys::generate().public_key())],
&ev
));

// compound AND
assert!(filters_match(
&[Filter::new().kind(Kind::TextNote).author(pubkey)],
&ev
Expand All @@ -115,7 +112,6 @@ mod tests {
&ev
));

// since / until
assert!(filters_match(&[Filter::new().since(past)], &ev));
assert!(!filters_match(&[Filter::new().since(future)], &ev));
assert!(filters_match(&[Filter::new().until(future)], &ev));
Expand Down
7 changes: 0 additions & 7 deletions crates/sprout-db/src/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,6 @@ pub async fn create_channel(
let id = Uuid::new_v4();
let id_bytes = id.as_bytes().as_slice().to_vec();

// Use a transaction so the INSERT + SELECT are atomic. Without this, a concurrent
// reader could see the channel between the insert and the fetch, or the channel
// could be modified before we read it back.
let mut tx = pool.begin().await?;

sqlx::query(
Expand Down Expand Up @@ -329,9 +326,6 @@ pub async fn add_member(

let channel_id_bytes = channel_id.as_bytes().as_slice().to_vec();

// Begin transaction: all role checks and the INSERT run atomically.
// This prevents a TOCTOU race where the inviter is removed between the
// role check and the INSERT.
let mut tx = pool.begin().await?;

let channel = get_channel_tx(&mut tx, channel_id).await?;
Expand Down Expand Up @@ -514,7 +508,6 @@ pub async fn get_members(pool: &MySqlPool, channel_id: Uuid) -> Result<Vec<Membe
///
/// Includes channels where the pubkey is an active member AND all open channels.
/// Open channels must be included in REQ filter resolution.
/// Returns IDs of all channels accessible to the given pubkey.
pub async fn get_accessible_channel_ids(pool: &MySqlPool, pubkey: &[u8]) -> Result<Vec<Uuid>> {
let rows = sqlx::query(
r#"
Expand Down
15 changes: 0 additions & 15 deletions crates/sprout-db/src/feed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,10 @@ pub async fn query_mentions(
.push_bind(serde_json::json!([["p", pubkey_hex]]).to_string())
.push(", '$')");

// Kinds: stream messages, stream replies, forum posts, forum comments
qb.push(format!(
" AND kind IN ({KIND_STREAM_MESSAGE}, {KIND_STREAM_MESSAGE_V2}, {KIND_FORUM_POST}, {KIND_FORUM_COMMENT})"
));

// Channel access filter
if !accessible_channel_ids.is_empty() {
qb.push(" AND channel_id IN (");
let mut sep = qb.separated(", ");
Expand Down Expand Up @@ -130,16 +128,12 @@ pub async fn query_needs_action(
" AND kind IN ({KIND_WORKFLOW_APPROVAL_REQUESTED}, {KIND_STREAM_REMINDER})"
));

// Tag filter: must be tagged to this user.
// Wrap in outer array so MySQL checks for exact sub-array membership — see
// query_mentions for a full explanation of the JSON_CONTAINS semantics.
qb.push(" AND JSON_CONTAINS(tags, ")
.push_bind(serde_json::json!([["p", pubkey_hex]]).to_string())
.push(", '$')");

// Access control: only return events from channels the user can access.
// Identical pattern to query_mentions — prevents leaking events from
// channels the user has been removed from.
if !accessible_channel_ids.is_empty() {
qb.push(" AND channel_id IN (");
let mut sep = qb.separated(", ");
Expand Down Expand Up @@ -183,8 +177,6 @@ pub async fn query_activity(
FROM events WHERE 1=1",
);

// Stream messages, forum posts, agent job events.
// KIND_JOB_REQUEST = agent job requested, KIND_JOB_PROGRESS = in-flight progress update, KIND_JOB_RESULT = completed result.
qb.push(format!(
" AND kind IN ({KIND_STREAM_MESSAGE}, {KIND_STREAM_MESSAGE_V2}, {KIND_FORUM_POST}, {KIND_JOB_REQUEST}, {KIND_JOB_PROGRESS}, {KIND_JOB_RESULT})"
));
Expand Down Expand Up @@ -233,7 +225,6 @@ mod tests {

#[test]
fn pubkey_hex_encoding_32_byte_key() {
// Simulate a full 32-byte Nostr pubkey.
let pubkey_bytes: Vec<u8> = (0u8..32).collect();
let hex = hex::encode(&pubkey_bytes);
assert_eq!(hex.len(), 64);
Expand Down Expand Up @@ -308,8 +299,6 @@ mod tests {
use sprout_core::kind::{
KIND_FORUM_COMMENT, KIND_FORUM_POST, KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_V2,
};
// query_mentions filters for: KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_V2,
// KIND_FORUM_POST, KIND_FORUM_COMMENT
let mention_kinds: &[u32] = &[
KIND_STREAM_MESSAGE,
KIND_STREAM_MESSAGE_V2,
Expand Down Expand Up @@ -338,7 +327,6 @@ mod tests {
#[test]
fn needs_action_query_includes_approval_and_reminder_kinds() {
use sprout_core::kind::{KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED};
// query_needs_action filters for: KIND_WORKFLOW_APPROVAL_REQUESTED, KIND_STREAM_REMINDER
let needs_action_kinds: &[u32] = &[KIND_WORKFLOW_APPROVAL_REQUESTED, KIND_STREAM_REMINDER];

assert!(
Expand All @@ -357,8 +345,6 @@ mod tests {
KIND_FORUM_POST, KIND_JOB_PROGRESS, KIND_JOB_REQUEST, KIND_JOB_RESULT,
KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_V2,
};
// query_activity filters for: KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_V2,
// KIND_FORUM_POST, KIND_JOB_REQUEST, KIND_JOB_PROGRESS, KIND_JOB_RESULT
let activity_kinds: &[u32] = &[
KIND_STREAM_MESSAGE,
KIND_STREAM_MESSAGE_V2,
Expand Down Expand Up @@ -423,7 +409,6 @@ mod tests {
KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_V2, KIND_STREAM_REMINDER,
KIND_WORKFLOW_APPROVAL_REQUESTED,
};
// The two queries serve different purposes — their kind sets should not overlap.
let needs_action_kinds: &[u32] = &[KIND_WORKFLOW_APPROVAL_REQUESTED, KIND_STREAM_REMINDER];
let activity_kinds: &[u32] = &[
KIND_STREAM_MESSAGE,
Expand Down
5 changes: 0 additions & 5 deletions crates/sprout-db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,6 @@ mod tests {
assert_eq!(channel.description, Some("desc".to_string()));
assert!(db.is_member(channel.id, &owner).await.unwrap());

// Add member via owner invite
db.add_member(
channel.id,
&member,
Expand All @@ -818,7 +817,6 @@ mod tests {
let members = db.get_members(channel.id).await.expect("get members");
assert_eq!(members.len(), 2);

// Owner removes member
db.remove_member(channel.id, &member, &owner)
.await
.expect("remove");
Expand Down Expand Up @@ -919,17 +917,14 @@ mod tests {
.await
.expect("add rando");

// Rando cannot remove member
let result = db.remove_member(channel.id, &member, &rando).await;
assert!(matches!(result, Err(DbError::AccessDenied(_))));

// Owner can remove member
db.remove_member(channel.id, &member, &owner)
.await
.expect("owner removes");
assert!(!db.is_member(channel.id, &member).await.unwrap());

// Member can remove themselves
db.remove_member(channel.id, &rando, &rando)
.await
.expect("self-remove");
Expand Down
3 changes: 0 additions & 3 deletions crates/sprout-db/src/workflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -940,14 +940,12 @@ mod tests {
let mut cloned = record.clone();
cloned.name = "Cloned".to_owned();

// Original is unchanged.
assert_eq!(record.name, "Original");
assert_eq!(cloned.name, "Cloned");
}

#[test]
fn workflow_record_status_variants() {
// Verify all WorkflowStatus variants can be stored in the struct.
let now = Utc::now();
for status in &[
WorkflowStatus::Active,
Expand Down Expand Up @@ -1093,7 +1091,6 @@ mod tests {
created_at: now,
};

// Trace is a JSON array with 2 entries.
assert!(record.execution_trace.is_array());
assert_eq!(record.execution_trace.as_array().unwrap().len(), 2);
}
Expand Down
1 change: 0 additions & 1 deletion crates/sprout-huddle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ mod tests {
HuddleService::create_room_name(id),
"sprout-550e8400-e29b-41d4-a716-446655440000"
);
// Deterministic
assert_eq!(
HuddleService::create_room_name(id),
HuddleService::create_room_name(id)
Expand Down
7 changes: 0 additions & 7 deletions crates/sprout-huddle/src/webhook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,12 @@ mod tests {

#[test]
fn test_webhook_event_variants() {
// room_started
let ev = signed_parse(r#"{"event":"room_started","room":{"name":"r1"}}"#).unwrap();
assert_eq!(ev, WebhookEvent::RoomStarted { room: "r1".into() });

// room_finished
let ev = signed_parse(r#"{"event":"room_finished","room":{"name":"r1"}}"#).unwrap();
assert_eq!(ev, WebhookEvent::RoomFinished { room: "r1".into() });

// participant_joined
let ev = signed_parse(
r#"{"event":"participant_joined","room":{"name":"r1"},"participant":{"identity":"alice"}}"#,
)
Expand All @@ -207,7 +204,6 @@ mod tests {
}
);

// participant_left
let ev = signed_parse(
r#"{"event":"participant_left","room":{"name":"r1"},"participant":{"identity":"alice"}}"#,
)
Expand All @@ -220,7 +216,6 @@ mod tests {
}
);

// track_published — audio (default)
let ev = signed_parse(
r#"{"event":"track_published","room":{"name":"r1"},"participant":{"identity":"alice"},"track":{"type":"audio"}}"#,
)
Expand All @@ -234,7 +229,6 @@ mod tests {
}
);

// track_published — video
let ev = signed_parse(
r#"{"event":"track_published","room":{"name":"r1"},"participant":{"identity":"alice"},"track":{"type":"video"}}"#,
)
Expand All @@ -248,7 +242,6 @@ mod tests {
}
);

// track_published — screen_share
let ev = signed_parse(
r#"{"event":"track_published","room":{"name":"r1"},"participant":{"identity":"alice"},"track":{"type":"screen_share"}}"#,
)
Expand Down
3 changes: 0 additions & 3 deletions crates/sprout-mcp/src/relay_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,13 +435,11 @@ impl RelayClient {
let new_inner =
Self::connect_with_retry(&self.relay_url, &self.keys, self.api_token.as_deref()).await;

// Swap the inner connection.
{
let mut inner = self.inner.lock().await;
*inner = new_inner;
}

// Resubscribe to all active subscriptions.
let subs = self.active_subscriptions.lock().await.clone();
if !subs.is_empty() {
tracing::info!("resubscribing to {} active subscription(s)", subs.len());
Expand Down Expand Up @@ -879,7 +877,6 @@ mod tests {
let text = "not json at all";
let result = parse_relay_message(text);
assert!(result.is_err());
// Should be a JSON parse error.
assert!(matches!(result.unwrap_err(), RelayClientError::Json(_)));
}

Expand Down
2 changes: 0 additions & 2 deletions crates/sprout-mcp/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,10 @@ impl SproutMcpServer {
description = "Send a message to a Sprout channel"
)]
pub async fn send_message(&self, Parameters(p): Parameters<SendMessageParams>) -> String {
// Validate channel_id is a well-formed UUID at the tool boundary.
if let Err(e) = validate_uuid(&p.channel_id) {
return format!("Error: {e}");
}

// Guard against excessively large message content.
if p.content.len() > MAX_CONTENT_BYTES {
return format!(
"Error: content exceeds maximum size of {} bytes (got {})",
Expand Down
2 changes: 0 additions & 2 deletions crates/sprout-proxy/src/invite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ mod tests {

#[test]
fn test_invite_token_validation() {
// Valid token
let token = InviteToken::new("tok-valid", vec![], future(3600), 5);
assert!(token.validate(Utc::now()).is_ok());
assert!(token.is_valid(Utc::now()));
Expand Down Expand Up @@ -108,7 +107,6 @@ mod tests {
// Still valid (uses < max_uses)
assert!(token.is_valid(Utc::now()));
token.consume();
// Now exhausted
assert!(!token.is_valid(Utc::now()));
}

Expand Down
6 changes: 0 additions & 6 deletions crates/sprout-relay/src/api/agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ pub async fn agents_handler(
.await
.map_err(|e| internal_error(&format!("db error: {e}")))?;

// Collect pubkeys for bulk presence lookup.
let mut pubkeys_for_presence: Vec<nostr::PublicKey> = Vec::new();
let mut bot_pubkey_hexes: Vec<String> = Vec::new();

Expand All @@ -64,7 +63,6 @@ pub async fn agents_handler(
Default::default()
});

// Fetch user records for name resolution.
let user_records = state
.db
.get_users_bulk(&bots.iter().map(|b| b.pubkey.clone()).collect::<Vec<_>>())
Expand All @@ -85,7 +83,6 @@ pub async fn agents_handler(
let mut result = Vec::with_capacity(bots.len());

for (bot, hex) in bots.iter().zip(bot_pubkey_hexes.iter()) {
// Resolve display name: users table → bot record → test mapping → fallback.
let name = user_name_map
.get(hex.as_str())
.cloned()
Expand All @@ -95,15 +92,13 @@ pub async fn agents_handler(
format!("agent-{}", &hex[..end])
});

// Parse channel names from comma-separated string, filtered to requester's access.
let channels: Vec<&str> = bot
.channel_names
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty() && accessible_names.contains(*s))
.collect();

// Parse capabilities from JSON value.
let capabilities: Vec<String> = bot
.capabilities
.as_ref()
Expand All @@ -115,7 +110,6 @@ pub async fn agents_handler(
})
.unwrap_or_default();

// Presence status.
let status = presence_map
.get(hex.as_str())
.map(|s| s.as_str())
Expand Down
1 change: 0 additions & 1 deletion crates/sprout-relay/src/api/channels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ async fn resolve_dm_participants(

let member_pubkeys: Vec<Vec<u8>> = members.iter().map(|m| m.pubkey.clone()).collect();

// Bulk-fetch user records for name resolution.
let user_records = state
.db
.get_users_bulk(&member_pubkeys)
Expand Down
Loading