From f23c4c4dcb0f9fe716242de1e4c5f0fc70d83c55 Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Fri, 3 Apr 2026 21:11:15 +0530 Subject: [PATCH 1/5] temp ja4 endpoint --- .../trusted-server-adapter-fastly/src/main.rs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/crates/trusted-server-adapter-fastly/src/main.rs b/crates/trusted-server-adapter-fastly/src/main.rs index bcbc5073..00497d50 100644 --- a/crates/trusted-server-adapter-fastly/src/main.rs +++ b/crates/trusted-server-adapter-fastly/src/main.rs @@ -85,6 +85,32 @@ fn main(req: Request) -> Result { )) } +fn build_ja4_debug_response(req: &Request) -> Response { + let ja4 = req.get_tls_ja4().unwrap_or("unavailable"); + let h2 = req.get_client_h2_fingerprint().unwrap_or("unavailable"); + let cipher = req.get_tls_cipher_openssl_name().unwrap_or("unavailable"); + let tls_version = req.get_tls_protocol().unwrap_or("unavailable"); + let ua = req.get_header_str("user-agent").unwrap_or("none"); + let ch_mobile = req.get_header_str("sec-ch-ua-mobile").unwrap_or("not sent"); + let ch_platform = req + .get_header_str("sec-ch-ua-platform") + .unwrap_or("not sent"); + + let body = format!( + "ja4: {ja4}\n\ + h2_fp: {h2}\n\ + cipher: {cipher}\n\ + tls_version: {tls_version}\n\ + user-agent: {ua}\n\ + ch-mobile: {ch_mobile}\n\ + ch-platform: {ch_platform}\n" + ); + + Response::from_status(fastly::http::StatusCode::OK) + .with_content_type(fastly::mime::TEXT_PLAIN_UTF_8) + .with_body(body) +} + async fn route_request( settings: &Settings, orchestrator: &AuctionOrchestrator, @@ -151,6 +177,9 @@ async fn route_request( // Unified auction endpoint (returns creative HTML inline) (Method::POST, "/auction") => handle_auction(settings, orchestrator, req).await, + // Temporary JA4/TLS debug endpoint for browser fingerprint inspection. + (Method::GET, "/_ts/debug/ja4") => Ok(build_ja4_debug_response(&req)), + // tsjs endpoints (Method::GET, "/first-party/proxy") => handle_first_party_proxy(settings, req).await, (Method::GET, "/first-party/click") => handle_first_party_click(settings, req).await, @@ -247,3 +276,58 @@ fn init_logger() { .apply() .expect("should initialize logger"); } + +#[cfg(test)] +mod tests { + use super::*; + use fastly::mime; + + #[test] + fn ja4_debug_response_uses_plain_text_and_fallback_values() { + let req = Request::get("https://example.com/_ts/debug/ja4"); + + let mut response = build_ja4_debug_response(&req); + + assert_eq!( + response.get_status(), + fastly::http::StatusCode::OK, + "should return 200 OK" + ); + assert_eq!( + response.get_content_type(), + Some(mime::TEXT_PLAIN_UTF_8), + "should return plain text content" + ); + + let body = response.take_body_str(); + + assert!( + body.contains("ja4: unavailable"), + "should include JA4 fallback" + ); + assert!( + body.contains("h2_fp: unavailable"), + "should include H2 fingerprint fallback" + ); + assert!( + body.contains("cipher: unavailable"), + "should include cipher fallback" + ); + assert!( + body.contains("tls_version: unavailable"), + "should include TLS version fallback" + ); + assert!( + body.contains("user-agent: none"), + "should include user-agent fallback" + ); + assert!( + body.contains("ch-mobile: not sent"), + "should include sec-ch-ua-mobile fallback" + ); + assert!( + body.contains("ch-platform: not sent"), + "should include sec-ch-ua-platform fallback" + ); + } +} From 0a32bb8d2d5f9acfed444369cc1f2b68b4b590d9 Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Wed, 22 Apr 2026 21:56:56 +0530 Subject: [PATCH 2/5] Add route coverage for JA4 debug endpoint --- .../src/route_tests.rs | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/crates/trusted-server-adapter-fastly/src/route_tests.rs b/crates/trusted-server-adapter-fastly/src/route_tests.rs index 0fd0113f..da4d3b89 100644 --- a/crates/trusted-server-adapter-fastly/src/route_tests.rs +++ b/crates/trusted-server-adapter-fastly/src/route_tests.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use edgezero_core::key_value_store::NoopKvStore; use error_stack::Report; use fastly::http::StatusCode; -use fastly::Request; +use fastly::{mime, Request}; use trusted_server_core::auction::build_orchestrator; use trusted_server_core::integrations::IntegrationRegistry; use trusted_server_core::platform::{ @@ -249,3 +249,64 @@ fn configured_missing_consent_store_only_breaks_consent_routes() { "should scope consent store failures to the consent-dependent routes" ); } + +#[test] +fn ja4_debug_route_returns_plain_text_fallback_response() { + let settings = create_test_settings(); + let orchestrator = build_orchestrator(&settings).expect("should build auction orchestrator"); + let integration_registry = + IntegrationRegistry::new(&settings).expect("should create integration registry"); + + let req = Request::get("https://test.com/_ts/debug/ja4"); + let runtime_services = test_runtime_services(&req); + let mut response = futures::executor::block_on(route_request( + &settings, + &orchestrator, + &integration_registry, + &runtime_services, + req, + )) + .expect("should route ja4 debug request"); + + assert_eq!( + response.get_status(), + StatusCode::OK, + "should return 200 OK for the ja4 debug route" + ); + assert_eq!( + response.get_content_type(), + Some(mime::TEXT_PLAIN_UTF_8), + "should return plain text content for the ja4 debug route" + ); + + let body = response.take_body_str(); + + assert!( + body.contains("ja4: unavailable"), + "should include the JA4 fallback when Fastly omits the fingerprint" + ); + assert!( + body.contains("h2_fp: unavailable"), + "should include the H2 fingerprint fallback when Fastly omits it" + ); + assert!( + body.contains("cipher: unavailable"), + "should include the cipher fallback when Fastly omits it" + ); + assert!( + body.contains("tls_version: unavailable"), + "should include the TLS version fallback when Fastly omits it" + ); + assert!( + body.contains("user-agent: none"), + "should include the user-agent fallback when the header is absent" + ); + assert!( + body.contains("ch-mobile: not sent"), + "should include the mobile client hints fallback when the header is absent" + ); + assert!( + body.contains("ch-platform: not sent"), + "should include the platform client hints fallback when the header is absent" + ); +} From f0b3bd2033db5c5497ab0a1aa7cef33c6b771582 Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Thu, 23 Apr 2026 12:31:18 +0530 Subject: [PATCH 3/5] Added cache control no-store private --- crates/trusted-server-adapter-fastly/src/main.rs | 8 +++++++- crates/trusted-server-adapter-fastly/src/route_tests.rs | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/trusted-server-adapter-fastly/src/main.rs b/crates/trusted-server-adapter-fastly/src/main.rs index 19247398..f036c22f 100644 --- a/crates/trusted-server-adapter-fastly/src/main.rs +++ b/crates/trusted-server-adapter-fastly/src/main.rs @@ -1,5 +1,5 @@ use error_stack::Report; -use fastly::http::Method; +use fastly::http::{header, Method}; use fastly::{Error, Request, Response}; use log_fastly::Logger; @@ -109,6 +109,7 @@ fn build_ja4_debug_response(req: &Request) -> Response { ); Response::from_status(fastly::http::StatusCode::OK) + .with_header(header::CACHE_CONTROL, "no-store, private") .with_content_type(fastly::mime::TEXT_PLAIN_UTF_8) .with_body(body) } @@ -339,6 +340,11 @@ mod tests { Some(mime::TEXT_PLAIN_UTF_8), "should return plain text content" ); + assert_eq!( + response.get_header_str("cache-control"), + Some("no-store, private"), + "should disable caching for the debug response" + ); let body = response.take_body_str(); diff --git a/crates/trusted-server-adapter-fastly/src/route_tests.rs b/crates/trusted-server-adapter-fastly/src/route_tests.rs index da4d3b89..aff4ecf2 100644 --- a/crates/trusted-server-adapter-fastly/src/route_tests.rs +++ b/crates/trusted-server-adapter-fastly/src/route_tests.rs @@ -278,6 +278,11 @@ fn ja4_debug_route_returns_plain_text_fallback_response() { Some(mime::TEXT_PLAIN_UTF_8), "should return plain text content for the ja4 debug route" ); + assert_eq!( + response.get_header_str("cache-control"), + Some("no-store, private"), + "should disable caching for the ja4 debug route" + ); let body = response.take_body_str(); From bf74be9fde31e9018341a27386e1a9b39b32c9b5 Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Thu, 30 Apr 2026 08:38:23 +0530 Subject: [PATCH 4/5] Resolve PR review findings --- .../trusted-server-adapter-fastly/src/main.rs | 10 ++- .../src/route_tests.rs | 66 ++++++++++++++++++- crates/trusted-server-core/src/settings.rs | 14 ++++ trusted-server.toml | 17 +++++ 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/crates/trusted-server-adapter-fastly/src/main.rs b/crates/trusted-server-adapter-fastly/src/main.rs index 915ce191..c97b6200 100644 --- a/crates/trusted-server-adapter-fastly/src/main.rs +++ b/crates/trusted-server-adapter-fastly/src/main.rs @@ -213,8 +213,14 @@ async fn route_request( } } - // Temporary JA4/TLS debug endpoint for browser fingerprint inspection. - (Method::GET, "/_ts/debug/ja4") => Ok(build_ja4_debug_response(&req)), + // JA4/TLS debug endpoint — only active when debug.ja4_endpoint_enabled = true. + (Method::GET, "/_ts/debug/ja4") => { + if settings.debug.ja4_endpoint_enabled { + Ok(build_ja4_debug_response(&req)) + } else { + Ok(Response::from_status(fastly::http::StatusCode::NOT_FOUND)) + } + } // tsjs endpoints (Method::GET, "/first-party/proxy") => { diff --git a/crates/trusted-server-adapter-fastly/src/route_tests.rs b/crates/trusted-server-adapter-fastly/src/route_tests.rs index aff4ecf2..df108f4c 100644 --- a/crates/trusted-server-adapter-fastly/src/route_tests.rs +++ b/crates/trusted-server-adapter-fastly/src/route_tests.rs @@ -251,12 +251,76 @@ fn configured_missing_consent_store_only_breaks_consent_routes() { } #[test] -fn ja4_debug_route_returns_plain_text_fallback_response() { +fn ja4_debug_route_returns_404_when_disabled() { let settings = create_test_settings(); let orchestrator = build_orchestrator(&settings).expect("should build auction orchestrator"); let integration_registry = IntegrationRegistry::new(&settings).expect("should create integration registry"); + let req = Request::get("https://test.com/_ts/debug/ja4"); + let runtime_services = test_runtime_services(&req); + let response = futures::executor::block_on(route_request( + &settings, + &orchestrator, + &integration_registry, + &runtime_services, + req, + )) + .expect("should route ja4 debug request"); + + assert_eq!( + response.get_status(), + StatusCode::NOT_FOUND, + "should return 404 when debug.ja4_endpoint_enabled is false" + ); +} + +fn create_debug_enabled_settings() -> Settings { + let base = r#" + [[handlers]] + path = "^/admin" + username = "admin" + password = "admin-pass" + + [publisher] + domain = "test-publisher.com" + cookie_domain = ".test-publisher.com" + origin_url = "https://origin.test-publisher.com" + proxy_secret = "unit-test-proxy-secret" + + [edge_cookie] + secret_key = "test-secret-key" + + [request_signing] + enabled = false + config_store_id = "test-config-store-id" + secret_store_id = "test-secret-store-id" + + [consent] + consent_store = "missing-consent-store" + + [integrations.prebid] + enabled = true + server_url = "https://test-prebid.com/openrtb2/auction" + + [auction] + enabled = true + providers = ["prebid"] + timeout_ms = 2000 + + [debug] + ja4_endpoint_enabled = true + "#; + Settings::from_toml(base).expect("should parse debug-enabled test settings") +} + +#[test] +fn ja4_debug_route_returns_plain_text_fallback_response() { + let settings = create_debug_enabled_settings(); + let orchestrator = build_orchestrator(&settings).expect("should build auction orchestrator"); + let integration_registry = + IntegrationRegistry::new(&settings).expect("should create integration registry"); + let req = Request::get("https://test.com/_ts/debug/ja4"); let runtime_services = test_runtime_services(&req); let mut response = futures::executor::block_on(route_request( diff --git a/crates/trusted-server-core/src/settings.rs b/crates/trusted-server-core/src/settings.rs index 78549262..f4564bad 100644 --- a/crates/trusted-server-core/src/settings.rs +++ b/crates/trusted-server-core/src/settings.rs @@ -399,6 +399,18 @@ impl Proxy { } } +/// Debug-only features. All flags default to `false` (off in production). +#[derive(Debug, Default, Clone, Deserialize, Serialize)] +pub struct DebugConfig { + /// Expose the JA4/TLS fingerprint debug endpoint at `GET /_ts/debug/ja4`. + /// + /// When `false` (the default), the endpoint returns 404. Enable only for + /// intentional Fastly/browser TLS investigation — the endpoint reflects + /// Fastly-observed TLS details that browser JS cannot normally read. + #[serde(default)] + pub ja4_endpoint_enabled: bool, +} + #[derive(Debug, Default, Clone, Deserialize, Serialize, Validate)] pub struct Settings { #[validate(nested)] @@ -423,6 +435,8 @@ pub struct Settings { pub consent: ConsentConfig, #[serde(default)] pub proxy: Proxy, + #[serde(default)] + pub debug: DebugConfig, } #[allow(unused)] diff --git a/trusted-server.toml b/trusted-server.toml index d9189aaa..b4e7aff2 100644 --- a/trusted-server.toml +++ b/trusted-server.toml @@ -184,6 +184,23 @@ enabled = false endpoint = "https://origin-mocktioneer.cdintel.com/adserver/mediate" timeout_ms = 1000 +# Debug configuration (all flags default to false — do not enable in production) +# [debug] +# Enable the JA4/TLS fingerprint debug endpoint at GET /_ts/debug/ja4. +# Returns a plain-text response with the following fields (Fastly-observed values): +# ja4 — JA4 TLS client fingerprint +# h2_fp — HTTP/2 client fingerprint +# cipher — TLS cipher suite (OpenSSL name) +# tls_version — TLS protocol version +# user-agent — User-Agent request header +# ch-mobile — Sec-CH-UA-Mobile client hint +# ch-platform — Sec-CH-UA-Platform client hint +# All fields fall back to "unavailable" or "not sent" when Fastly does not provide them. +# Response always carries Cache-Control: no-store, private. +# IMPORTANT: This endpoint reflects TLS details that browser JS cannot normally read. +# Disable after investigation is complete. +# ja4_endpoint_enabled = false + # Map auction-request context keys to mediation URL query parameters. # Each key is a context key from the JS client; the value becomes the # query parameter name. Arrays are joined with commas. From 2ebf4efd2c6d9e7d5d034242af55ed82e681f853 Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Wed, 6 May 2026 14:22:55 +0530 Subject: [PATCH 5/5] Resolve PR review findings --- .../trusted-server-adapter-fastly/src/main.rs | 51 ++++--- .../src/route_tests.rs | 132 +----------------- trusted-server.toml | 3 +- 3 files changed, 37 insertions(+), 149 deletions(-) diff --git a/crates/trusted-server-adapter-fastly/src/main.rs b/crates/trusted-server-adapter-fastly/src/main.rs index c97b6200..a0b17006 100644 --- a/crates/trusted-server-adapter-fastly/src/main.rs +++ b/crates/trusted-server-adapter-fastly/src/main.rs @@ -70,6 +70,17 @@ fn main() { }; log::debug!("Settings {settings:?}"); + // Short-circuit the ja4 debug probe before finalize_response so that + // Cache-Control: no-store, private cannot be replaced by operator [response_headers]. + if req.get_method() == Method::GET && req.get_path() == "/_ts/debug/ja4" { + if settings.debug.ja4_endpoint_enabled { + build_ja4_debug_response(&req).send_to_client(); + } else { + Response::from_status(fastly::http::StatusCode::NOT_FOUND).send_to_client(); + } + return; + } + // Build the auction orchestrator once at startup let orchestrator = match build_orchestrator(&settings) { Ok(o) => o, @@ -109,16 +120,27 @@ fn main() { } } +const FALLBACK_UNAVAILABLE: &str = "unavailable"; +const FALLBACK_NOT_SENT: &str = "not sent"; +const FALLBACK_NONE: &str = "none"; + +// TODO: remove after JA4 evaluation completes — see #645 fn build_ja4_debug_response(req: &Request) -> Response { - let ja4 = req.get_tls_ja4().unwrap_or("unavailable"); - let h2 = req.get_client_h2_fingerprint().unwrap_or("unavailable"); - let cipher = req.get_tls_cipher_openssl_name().unwrap_or("unavailable"); - let tls_version = req.get_tls_protocol().unwrap_or("unavailable"); - let ua = req.get_header_str("user-agent").unwrap_or("none"); - let ch_mobile = req.get_header_str("sec-ch-ua-mobile").unwrap_or("not sent"); + let ja4 = req.get_tls_ja4().unwrap_or(FALLBACK_UNAVAILABLE); + let h2 = req + .get_client_h2_fingerprint() + .unwrap_or(FALLBACK_UNAVAILABLE); + let cipher = req + .get_tls_cipher_openssl_name() + .unwrap_or(FALLBACK_UNAVAILABLE); + let tls_version = req.get_tls_protocol().unwrap_or(FALLBACK_UNAVAILABLE); + let ua = req.get_header_str("user-agent").unwrap_or(FALLBACK_NONE); + let ch_mobile = req + .get_header_str("sec-ch-ua-mobile") + .unwrap_or(FALLBACK_NOT_SENT); let ch_platform = req .get_header_str("sec-ch-ua-platform") - .unwrap_or("not sent"); + .unwrap_or(FALLBACK_NOT_SENT); let body = format!( "ja4: {ja4}\n\ @@ -132,6 +154,10 @@ fn build_ja4_debug_response(req: &Request) -> Response { Response::from_status(fastly::http::StatusCode::OK) .with_header(header::CACHE_CONTROL, "no-store, private") + .with_header( + header::VARY, + "User-Agent, Sec-CH-UA-Mobile, Sec-CH-UA-Platform", + ) .with_content_type(fastly::mime::TEXT_PLAIN_UTF_8) .with_body(body) } @@ -213,15 +239,6 @@ async fn route_request( } } - // JA4/TLS debug endpoint — only active when debug.ja4_endpoint_enabled = true. - (Method::GET, "/_ts/debug/ja4") => { - if settings.debug.ja4_endpoint_enabled { - Ok(build_ja4_debug_response(&req)) - } else { - Ok(Response::from_status(fastly::http::StatusCode::NOT_FOUND)) - } - } - // tsjs endpoints (Method::GET, "/first-party/proxy") => { handle_first_party_proxy(settings, runtime_services, req).await @@ -379,7 +396,7 @@ mod tests { "should return plain text content" ); assert_eq!( - response.get_header_str("cache-control"), + response.get_header_str(header::CACHE_CONTROL), Some("no-store, private"), "should disable caching for the debug response" ); diff --git a/crates/trusted-server-adapter-fastly/src/route_tests.rs b/crates/trusted-server-adapter-fastly/src/route_tests.rs index df108f4c..0fd0113f 100644 --- a/crates/trusted-server-adapter-fastly/src/route_tests.rs +++ b/crates/trusted-server-adapter-fastly/src/route_tests.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use edgezero_core::key_value_store::NoopKvStore; use error_stack::Report; use fastly::http::StatusCode; -use fastly::{mime, Request}; +use fastly::Request; use trusted_server_core::auction::build_orchestrator; use trusted_server_core::integrations::IntegrationRegistry; use trusted_server_core::platform::{ @@ -249,133 +249,3 @@ fn configured_missing_consent_store_only_breaks_consent_routes() { "should scope consent store failures to the consent-dependent routes" ); } - -#[test] -fn ja4_debug_route_returns_404_when_disabled() { - let settings = create_test_settings(); - let orchestrator = build_orchestrator(&settings).expect("should build auction orchestrator"); - let integration_registry = - IntegrationRegistry::new(&settings).expect("should create integration registry"); - - let req = Request::get("https://test.com/_ts/debug/ja4"); - let runtime_services = test_runtime_services(&req); - let response = futures::executor::block_on(route_request( - &settings, - &orchestrator, - &integration_registry, - &runtime_services, - req, - )) - .expect("should route ja4 debug request"); - - assert_eq!( - response.get_status(), - StatusCode::NOT_FOUND, - "should return 404 when debug.ja4_endpoint_enabled is false" - ); -} - -fn create_debug_enabled_settings() -> Settings { - let base = r#" - [[handlers]] - path = "^/admin" - username = "admin" - password = "admin-pass" - - [publisher] - domain = "test-publisher.com" - cookie_domain = ".test-publisher.com" - origin_url = "https://origin.test-publisher.com" - proxy_secret = "unit-test-proxy-secret" - - [edge_cookie] - secret_key = "test-secret-key" - - [request_signing] - enabled = false - config_store_id = "test-config-store-id" - secret_store_id = "test-secret-store-id" - - [consent] - consent_store = "missing-consent-store" - - [integrations.prebid] - enabled = true - server_url = "https://test-prebid.com/openrtb2/auction" - - [auction] - enabled = true - providers = ["prebid"] - timeout_ms = 2000 - - [debug] - ja4_endpoint_enabled = true - "#; - Settings::from_toml(base).expect("should parse debug-enabled test settings") -} - -#[test] -fn ja4_debug_route_returns_plain_text_fallback_response() { - let settings = create_debug_enabled_settings(); - let orchestrator = build_orchestrator(&settings).expect("should build auction orchestrator"); - let integration_registry = - IntegrationRegistry::new(&settings).expect("should create integration registry"); - - let req = Request::get("https://test.com/_ts/debug/ja4"); - let runtime_services = test_runtime_services(&req); - let mut response = futures::executor::block_on(route_request( - &settings, - &orchestrator, - &integration_registry, - &runtime_services, - req, - )) - .expect("should route ja4 debug request"); - - assert_eq!( - response.get_status(), - StatusCode::OK, - "should return 200 OK for the ja4 debug route" - ); - assert_eq!( - response.get_content_type(), - Some(mime::TEXT_PLAIN_UTF_8), - "should return plain text content for the ja4 debug route" - ); - assert_eq!( - response.get_header_str("cache-control"), - Some("no-store, private"), - "should disable caching for the ja4 debug route" - ); - - let body = response.take_body_str(); - - assert!( - body.contains("ja4: unavailable"), - "should include the JA4 fallback when Fastly omits the fingerprint" - ); - assert!( - body.contains("h2_fp: unavailable"), - "should include the H2 fingerprint fallback when Fastly omits it" - ); - assert!( - body.contains("cipher: unavailable"), - "should include the cipher fallback when Fastly omits it" - ); - assert!( - body.contains("tls_version: unavailable"), - "should include the TLS version fallback when Fastly omits it" - ); - assert!( - body.contains("user-agent: none"), - "should include the user-agent fallback when the header is absent" - ); - assert!( - body.contains("ch-mobile: not sent"), - "should include the mobile client hints fallback when the header is absent" - ); - assert!( - body.contains("ch-platform: not sent"), - "should include the platform client hints fallback when the header is absent" - ); -} diff --git a/trusted-server.toml b/trusted-server.toml index b4e7aff2..6bc7bb0f 100644 --- a/trusted-server.toml +++ b/trusted-server.toml @@ -195,7 +195,8 @@ timeout_ms = 1000 # user-agent — User-Agent request header # ch-mobile — Sec-CH-UA-Mobile client hint # ch-platform — Sec-CH-UA-Platform client hint -# All fields fall back to "unavailable" or "not sent" when Fastly does not provide them. +# Fastly TLS/fingerprint fields fall back to "unavailable"; client hints fall back +# to "not sent"; user-agent falls back to "none" when absent. # Response always carries Cache-Control: no-store, private. # IMPORTANT: This endpoint reflects TLS details that browser JS cannot normally read. # Disable after investigation is complete.