1use std::convert::Infallible;
4use std::str::FromStr;
5use std::time::Duration;
6
7use axum::extract::{Query, State};
8use axum::response::sse::{Event as SseEvent, KeepAlive, Sse};
9use futures_util::stream::{Stream, StreamExt};
10use serde::Deserialize;
11use serde::de::{self, Deserializer};
12use tokio_stream::wrappers::BroadcastStream;
13use uuid::Uuid;
14
15use crate::authorization::AuthorizeExt;
16use crate::error::ApiError;
17use crate::state::AppState;
18use ironflow_auth::extractor::Authenticated;
19use ironflow_engine::notify::Event;
20use ironflow_store::entities::ApiKeyScope;
21
22pub use ironflow_store::entities::EventKind;
23
24fn deserialize_comma_event_kinds<'de, D>(
26 deserializer: D,
27) -> Result<Option<Vec<EventKind>>, D::Error>
28where
29 D: Deserializer<'de>,
30{
31 let opt: Option<String> = Option::deserialize(deserializer)?;
32 match opt {
33 None => Ok(None),
34 Some(raw) => {
35 let kinds: Result<Vec<EventKind>, _> = raw
36 .split(',')
37 .map(|s| s.trim())
38 .filter(|s| !s.is_empty())
39 .map(EventKind::from_str)
40 .collect();
41 kinds.map(Some).map_err(de::Error::custom)
42 }
43 }
44}
45
46#[derive(Debug, Deserialize)]
61pub struct EventsQuery {
62 pub run_id: Option<Uuid>,
64 #[serde(default, deserialize_with = "deserialize_comma_event_kinds")]
66 pub types: Option<Vec<EventKind>>,
67}
68
69pub async fn events(
89 auth: Authenticated,
90 State(state): State<AppState>,
91 Query(query): Query<EventsQuery>,
92) -> Result<Sse<impl Stream<Item = Result<SseEvent, Infallible>>>, ApiError> {
93 auth.require(ApiKeyScope::RunsRead, false)?;
94 let receiver = state.event_sender.subscribe();
95 let sees_provider_account_events = auth.require(ApiKeyScope::Admin, true).is_ok();
98 let type_filter = query.types;
99
100 let stream = BroadcastStream::new(receiver).filter_map(move |result: Result<Event, _>| {
101 let type_filter = type_filter.clone();
102 let run_id_filter = query.run_id;
103 async move {
104 let event = result.ok()?;
105
106 if !sees_provider_account_events
107 && matches!(
108 event,
109 Event::ProviderAccountUpdated(_) | Event::ProviderAccountUsageUpdated(_)
110 )
111 {
112 return None;
113 }
114
115 if let Some(ref rid) = run_id_filter
116 && event.run_id() != Some(*rid)
117 {
118 return None;
119 }
120
121 if let Some(ref kinds) = type_filter {
122 let event_type = event.event_type();
123 if !kinds.iter().any(|k| k.as_str() == event_type) {
124 return None;
125 }
126 }
127
128 let data = serde_json::to_string(&event).ok()?;
129 let sse_event = SseEvent::default().event(event.event_type()).data(data);
130
131 Some(Ok::<_, Infallible>(sse_event))
132 }
133 });
134
135 let sse = Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(30)));
136 Ok(sse)
137}
138
139#[cfg(test)]
140mod tests {
141 use std::collections::HashMap;
142 use std::sync::Arc;
143 use std::time::Duration;
144
145 use axum::Router;
146 use axum::routing::get;
147 use chrono::Utc;
148 use ironflow_auth::extractor::{API_KEY_PREFIX, API_KEY_SUFFIX_LEN};
149 use ironflow_auth::password;
150 use ironflow_core::providers::claude::ClaudeCodeProvider;
151 use ironflow_engine::engine::Engine;
152 use ironflow_engine::notify::{
153 Event, ProviderAccountChange, ProviderAccountUpdatedEvent, RunStatusChangedEvent,
154 UserSignedInEvent,
155 };
156 use ironflow_store::entities::{ApiKeyScope, NewApiKey, NewUser};
157 use ironflow_store::memory::InMemoryStore;
158 use ironflow_store::models::RunStatus;
159 use rust_decimal::Decimal;
160 use tokio::io::AsyncBufReadExt;
161 use tokio::io::BufReader;
162 use tokio::net::TcpListener;
163 use tokio::sync::broadcast;
164 use tokio::time::{sleep, timeout};
165 use uuid::Uuid;
166
167 use super::events;
168 use crate::routes::test_helpers::create_user_auth_header;
169 use crate::state::AppState;
170
171 fn test_state() -> AppState {
172 let store = Arc::new(InMemoryStore::new());
173 let provider = Arc::new(ClaudeCodeProvider::new());
174 let engine = Arc::new(Engine::new(store.clone(), provider));
175 let jwt_config = Arc::new(ironflow_auth::jwt::JwtConfig {
176 secret: "test-secret".to_string(),
177 access_token_ttl_secs: 900,
178 refresh_token_ttl_secs: 604800,
179 cookie_domain: None,
180 cookie_secure: false,
181 });
182 let (event_sender, _) = broadcast::channel::<Event>(16);
183 AppState::new(
184 store,
185 engine,
186 jwt_config,
187 "test-worker-token".to_string(),
188 event_sender,
189 )
190 }
191
192 fn sample_run_event(run_id: Uuid) -> Event {
193 Event::RunStatusChanged(RunStatusChangedEvent {
194 run_id,
195 workflow_name: "deploy".to_string(),
196 from: RunStatus::Running,
197 to: RunStatus::Completed,
198 error: None,
199 cost_usd: Decimal::ZERO,
200 duration_ms: 1000,
201 labels: HashMap::new(),
202 at: Utc::now(),
203 })
204 }
205
206 fn sample_user_event() -> Event {
207 Event::UserSignedIn(UserSignedInEvent {
208 user_id: Uuid::now_v7(),
209 username: "alice".to_string(),
210 at: Utc::now(),
211 })
212 }
213
214 fn sample_provider_account_event() -> Event {
215 Event::ProviderAccountUpdated(ProviderAccountUpdatedEvent {
216 account_id: Uuid::now_v7(),
217 name: "acct".into(),
218 change: ProviderAccountChange::Updated,
219 at: Utc::now(),
220 })
221 }
222
223 async fn api_key_header(
229 state: &AppState,
230 owner_is_admin: bool,
231 scopes: Vec<ApiKeyScope>,
232 ) -> String {
233 let suffix = Uuid::now_v7().simple().to_string();
234 let owner = state
235 .store
236 .create_user(NewUser {
237 email: format!("owner-{suffix}@example.com"),
238 username: format!("owner-{suffix}"),
239 password_hash: "argon2hash".to_string(),
240 is_admin: Some(owner_is_admin),
241 })
242 .await
243 .unwrap();
244 let raw_key = format!("irfl_{}rest-of-secret-key", &suffix[24..]);
245 let prefix = &raw_key[..API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN];
246 state
247 .store
248 .create_api_key(NewApiKey {
249 user_id: owner.id,
250 name: format!("key-{suffix}"),
251 key_hash: password::hash(&raw_key).unwrap(),
252 key_prefix: prefix.to_string(),
253 scopes,
254 expires_at: None,
255 rate_limit_override: None,
256 })
257 .await
258 .unwrap();
259 format!("Bearer {raw_key}")
260 }
261
262 async fn start_sse_server(state: AppState) -> (String, broadcast::Sender<Event>, String) {
264 let sender = state.event_sender.clone();
265 let auth = create_user_auth_header(&state, "testuser", false).await;
266 let app = Router::new()
267 .route("/events", get(events))
268 .with_state(state);
269
270 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
271 let addr = listener.local_addr().unwrap().to_string();
272 tokio::spawn(async move {
273 axum::serve(listener, app).await.unwrap();
274 });
275 (addr, sender, auth)
276 }
277
278 async fn connect_sse(addr: &str, query: &str, auth: &str) -> BufReader<tokio::net::TcpStream> {
280 let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
281 let (reader, mut writer) = stream.into_split();
282
283 use tokio::io::AsyncWriteExt;
284 writer
285 .write_all(
286 format!(
287 "GET /events{query} HTTP/1.1\r\nHost: {addr}\r\nAccept: text/event-stream\r\nAuthorization: {auth}\r\n\r\n"
288 )
289 .as_bytes(),
290 )
291 .await
292 .unwrap();
293
294 BufReader::new(reader.reunite(writer).unwrap())
295 }
296
297 async fn read_until_contains(
300 reader: &mut BufReader<tokio::net::TcpStream>,
301 needle: &str,
302 dur: Duration,
303 ) -> String {
304 let mut accumulated = String::new();
305 let result = timeout(dur, async {
306 loop {
307 let mut line = String::new();
308 let n = reader.read_line(&mut line).await.unwrap();
309 if n == 0 {
310 break;
311 }
312 accumulated.push_str(&line);
313 if accumulated.contains(needle) {
314 break;
315 }
316 }
317 })
318 .await;
319 if result.is_err() {
320 panic!("timeout waiting for '{needle}' in SSE stream. Data so far:\n{accumulated}");
321 }
322 accumulated
323 }
324
325 async fn wait_for_subscriber(sender: &broadcast::Sender<Event>) {
331 let ready = timeout(Duration::from_secs(5), async {
332 while sender.receiver_count() == 0 {
333 sleep(Duration::from_millis(5)).await;
334 }
335 })
336 .await;
337 assert!(ready.is_ok(), "SSE handler did not subscribe in time");
338 }
339
340 #[tokio::test]
341 async fn sse_stream_receives_events() {
342 let state = test_state();
343 let (addr, sender, auth) = start_sse_server(state).await;
344 let mut reader = connect_sse(&addr, "", &auth).await;
345
346 wait_for_subscriber(&sender).await;
347
348 let run_id = Uuid::now_v7();
349 sender.send(sample_run_event(run_id)).unwrap();
350
351 let text =
352 read_until_contains(&mut reader, &run_id.to_string(), Duration::from_secs(5)).await;
353
354 assert!(text.contains("run_status_changed"));
355 assert!(text.contains(&run_id.to_string()));
356 }
357
358 #[tokio::test]
359 async fn sse_filters_by_run_id() {
360 let state = test_state();
361 let (addr, sender, auth) = start_sse_server(state).await;
362
363 let target_run = Uuid::now_v7();
364 let other_run = Uuid::now_v7();
365
366 let mut reader = connect_sse(&addr, &format!("?run_id={target_run}"), &auth).await;
367 wait_for_subscriber(&sender).await;
368
369 sender.send(sample_run_event(other_run)).unwrap();
370 sender.send(sample_run_event(target_run)).unwrap();
371
372 let text =
373 read_until_contains(&mut reader, &target_run.to_string(), Duration::from_secs(5)).await;
374
375 assert!(text.contains(&target_run.to_string()));
376 assert!(!text.contains(&other_run.to_string()));
377 }
378
379 #[tokio::test]
380 async fn sse_filters_by_event_type() {
381 let state = test_state();
382 let (addr, sender, auth) = start_sse_server(state).await;
383
384 let mut reader = connect_sse(&addr, "?types=user_signed_in", &auth).await;
385 wait_for_subscriber(&sender).await;
386
387 let run_id = Uuid::now_v7();
388 sender.send(sample_run_event(run_id)).unwrap();
389 sender.send(sample_user_event()).unwrap();
390
391 let text = read_until_contains(&mut reader, "user_signed_in", Duration::from_secs(5)).await;
392
393 assert!(text.contains("user_signed_in"));
394 assert!(!text.contains("run_status_changed"));
395 }
396
397 #[tokio::test]
398 async fn sse_returns_correct_content_type() {
399 let state = test_state();
400 let (addr, _sender, auth) = start_sse_server(state).await;
401 let mut reader = connect_sse(&addr, "", &auth).await;
402
403 let text =
404 read_until_contains(&mut reader, "text/event-stream", Duration::from_secs(5)).await;
405
406 assert!(text.contains("text/event-stream"));
407 }
408
409 #[tokio::test]
410 async fn sse_rejects_unauthenticated() {
411 let state = test_state();
412 let (addr, _sender, _auth) = start_sse_server(state).await;
413 let stream = tokio::net::TcpStream::connect(&addr).await.unwrap();
415 let (reader, mut writer) = stream.into_split();
416
417 use tokio::io::AsyncWriteExt;
418 writer
419 .write_all(
420 format!(
421 "GET /events HTTP/1.1\r\nHost: {addr}\r\nAccept: text/event-stream\r\n\r\n"
422 )
423 .as_bytes(),
424 )
425 .await
426 .unwrap();
427
428 let mut buf_reader = BufReader::new(reader.reunite(writer).unwrap());
429 let text = read_until_contains(&mut buf_reader, "401", Duration::from_secs(5)).await;
430
431 assert!(text.contains("401"));
432 }
433
434 async fn assert_provider_account_events_hidden(state: AppState, auth: &str) {
440 let (addr, sender, _jwt) = start_sse_server(state).await;
441 let mut reader = connect_sse(&addr, "", auth).await;
442 wait_for_subscriber(&sender).await;
443
444 let run_id = Uuid::now_v7();
445 sender.send(sample_provider_account_event()).unwrap();
446 sender.send(sample_run_event(run_id)).unwrap();
447
448 let text =
449 read_until_contains(&mut reader, &run_id.to_string(), Duration::from_secs(5)).await;
450
451 assert!(!text.contains("provider_account"));
452 }
453
454 #[tokio::test]
455 async fn sse_scope_hides_provider_account_events_from_a_runs_read_key() {
456 let state = test_state();
457 let auth = api_key_header(&state, true, vec![ApiKeyScope::RunsRead]).await;
458 assert_provider_account_events_hidden(state, &auth).await;
459 }
460
461 #[tokio::test]
462 async fn sse_scope_hides_provider_account_events_from_an_admin_scope_key_of_a_member() {
463 let state = test_state();
464 let auth = api_key_header(&state, false, vec![ApiKeyScope::Admin]).await;
465 assert_provider_account_events_hidden(state, &auth).await;
466 }
467
468 #[tokio::test]
469 async fn sse_scope_streams_provider_account_events_to_an_admin_scope_key() {
470 let state = test_state();
471 let auth = api_key_header(&state, true, vec![ApiKeyScope::Admin]).await;
472 let (addr, sender, _jwt) = start_sse_server(state).await;
473 let mut reader = connect_sse(&addr, "", &auth).await;
474 wait_for_subscriber(&sender).await;
475
476 sender.send(sample_provider_account_event()).unwrap();
477
478 let text =
479 read_until_contains(&mut reader, "provider_account", Duration::from_secs(5)).await;
480
481 assert!(text.contains("provider_account"));
482 }
483
484 #[tokio::test]
485 async fn sse_scope_rejects_a_key_without_runs_read() {
486 let state = test_state();
487 let auth = api_key_header(&state, true, vec![ApiKeyScope::StatsRead]).await;
488 let (addr, _sender, _jwt) = start_sse_server(state).await;
489 let mut reader = connect_sse(&addr, "", &auth).await;
490
491 let text = read_until_contains(&mut reader, "\r\n", Duration::from_secs(5)).await;
492
493 assert!(text.starts_with("HTTP/1.1 403"), "status line: {text}");
494 }
495}