diff --git a/crates/workspace-server/src/observation.rs b/crates/workspace-server/src/observation.rs index a750877f..fce73f30 100644 --- a/crates/workspace-server/src/observation.rs +++ b/crates/workspace-server/src/observation.rs @@ -10,7 +10,7 @@ use tokio_tungstenite::tungstenite::{Error as TungsteniteError, Message as Tungs use worker_runtime::http_server::{RuntimeWorkerEventWsEnvelope, RuntimeWorkerEventWsFrame}; /// Backend-private source for a runtime worker observation stream. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] pub struct RuntimeObservationSourceConfig { pub runtime_id: String, pub worker_id: String, @@ -18,6 +18,20 @@ pub struct RuntimeObservationSourceConfig { pub bearer_token: Option, } +impl std::fmt::Debug for RuntimeObservationSourceConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RuntimeObservationSourceConfig") + .field("runtime_id", &self.runtime_id) + .field("worker_id", &self.worker_id) + .field("endpoint", &"") + .field( + "bearer_token", + &self.bearer_token.as_ref().map(|_| ""), + ) + .finish() + } +} + /// Event consumed from a Runtime-owned worker observation WebSocket. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RuntimeObservationUpstreamEvent { @@ -181,12 +195,21 @@ pub struct BackendObservationOpen { } /// Backend-owned in-memory v0 observation proxy state. -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct BackendObservationProxy { sources: Arc>, state: Arc>, } +impl std::fmt::Debug for BackendObservationProxy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BackendObservationProxy") + .field("source_count", &self.sources.len()) + .field("state", &"") + .finish() + } +} + impl BackendObservationProxy { pub fn new(sources: Vec) -> Self { let sources = sources @@ -475,3 +498,56 @@ impl RuntimeWsObservationClient { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn sensitive_source() -> RuntimeObservationSourceConfig { + RuntimeObservationSourceConfig { + runtime_id: "remote-runtime".to_string(), + worker_id: "worker-1".to_string(), + endpoint: "wss://remote.example.invalid/private/workers/worker-1/events/ws".to_string(), + bearer_token: Some("top-secret-bearer-token".to_string()), + } + } + + #[test] + fn runtime_observation_source_debug_redacts_endpoint_and_token() { + let debug = format!("{:?}", sensitive_source()); + + assert!(debug.contains("remote-runtime")); + assert!(debug.contains("worker-1")); + assert!(debug.contains("")); + assert!(debug.contains("")); + for forbidden in [ + "remote.example.invalid", + "/private/workers/worker-1/events/ws", + "top-secret-bearer-token", + ] { + assert!( + !debug.contains(forbidden), + "debug leaked {forbidden}: {debug}" + ); + } + } + + #[test] + fn backend_observation_proxy_debug_redacts_contained_sources() { + let proxy = BackendObservationProxy::new(vec![sensitive_source()]); + let debug = format!("{proxy:?}"); + + assert!(debug.contains("BackendObservationProxy")); + assert!(debug.contains("source_count")); + for forbidden in [ + "remote.example.invalid", + "/private/workers/worker-1/events/ws", + "top-secret-bearer-token", + ] { + assert!( + !debug.contains(forbidden), + "debug leaked {forbidden}: {debug}" + ); + } + } +}