11//! Challenge Events API
22//!
33//! Allows challenges to broadcast custom events to validators via WebSocket.
4+ //! Secured with shared secret to prevent unauthorized broadcasts.
45
56use crate :: models:: { ChallengeCustomEvent , WsEvent } ;
67use crate :: state:: AppState ;
7- use axum:: { extract:: State , http:: StatusCode , Json } ;
8+ use axum:: {
9+ extract:: State ,
10+ http:: { HeaderMap , StatusCode } ,
11+ Json ,
12+ } ;
813use serde:: { Deserialize , Serialize } ;
914use std:: sync:: Arc ;
10- use tracing:: info;
15+ use tracing:: { info, warn} ;
16+
17+ /// Shared secret for challenge event broadcasts (set via BROADCAST_SECRET env var)
18+ /// Challenges must include this in X-Broadcast-Secret header
19+ fn get_broadcast_secret ( ) -> Option < String > {
20+ std:: env:: var ( "BROADCAST_SECRET" ) . ok ( )
21+ }
1122
1223#[ derive( Debug , Deserialize ) ]
1324pub struct BroadcastEventRequest {
@@ -30,10 +41,28 @@ pub struct BroadcastEventResponse {
3041///
3142/// Called by challenge containers to notify validators of events.
3243/// Validators filter events by challenge_id to receive only relevant ones.
44+ ///
45+ /// Requires X-Broadcast-Secret header matching BROADCAST_SECRET env var.
3346pub async fn broadcast_event (
3447 State ( state) : State < Arc < AppState > > ,
48+ headers : HeaderMap ,
3549 Json ( req) : Json < BroadcastEventRequest > ,
3650) -> Result < Json < BroadcastEventResponse > , ( StatusCode , String ) > {
51+ // Verify broadcast secret
52+ if let Some ( expected_secret) = get_broadcast_secret ( ) {
53+ let provided_secret = headers
54+ . get ( "X-Broadcast-Secret" )
55+ . and_then ( |v| v. to_str ( ) . ok ( ) )
56+ . unwrap_or ( "" ) ;
57+
58+ if provided_secret != expected_secret {
59+ warn ! (
60+ "Unauthorized broadcast attempt for challenge: {}" ,
61+ req. challenge_id
62+ ) ;
63+ return Err ( ( StatusCode :: UNAUTHORIZED , "Invalid broadcast secret" . into ( ) ) ) ;
64+ }
65+ }
3766 // Create the custom event
3867 let event = ChallengeCustomEvent {
3968 challenge_id : req. challenge_id . clone ( ) ,
0 commit comments