Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ If you cannot read `CLAUDE.md`, follow these rules:
4. Run `cargo fmt --all -- --check` and `cargo clippy --all-targets --all-features -- -D warnings`.
5. Run JS tests with `cd crates/js/lib && npx vitest run` when touching JS/TS code.
6. Use `error-stack` (`Report<E>`) for error handling — not anyhow, eyre, or thiserror.
7. Use `tracing` macros (not `println!`) and `expect("should ...")` (not `unwrap()`).
7. Use `log` macros (not `println!`) and `expect("should ...")` (not `unwrap()`).
8. Target is `wasm32-wasip1` — no Tokio or OS-specific dependencies in core crates.
3 changes: 3 additions & 0 deletions crates/common/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ mod error;
#[path = "src/auction_config_types.rs"]
mod auction_config_types;

#[path = "src/redacted.rs"]
mod redacted;

#[path = "src/settings.rs"]
mod settings;

Expand Down
2 changes: 1 addition & 1 deletion crates/common/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub fn enforce_basic_auth(settings: &Settings, req: &Request) -> Option<Response
None => return Some(unauthorized_response()),
};

if username == handler.username && password == handler.password {
if *handler.username.expose() == username && *handler.password.expose() == password {
None
} else {
Some(unauthorized_response())
Expand Down
8 changes: 4 additions & 4 deletions crates/common/src/http_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,13 @@ pub fn serve_static_with_etag(body: &str, req: &Request, content_type: &str) ->
#[must_use]
pub fn encode_url(settings: &Settings, plaintext_url: &str) -> String {
// Derive a 32-byte key via SHA-256(secret)
let key_bytes = Sha256::digest(settings.publisher.proxy_secret.as_bytes());
let key_bytes = Sha256::digest(settings.publisher.proxy_secret.expose().as_bytes());
let cipher = XChaCha20Poly1305::new(&key_bytes);

// Deterministic 24-byte nonce derived from secret and plaintext (stable tokens)
let mut hasher = Sha256::new();
hasher.update(b"ts-proxy-x1");
hasher.update(settings.publisher.proxy_secret.as_bytes());
hasher.update(settings.publisher.proxy_secret.expose().as_bytes());
hasher.update(plaintext_url.as_bytes());
let nonce_full = hasher.finalize();
let mut nonce = [0u8; 24];
Expand Down Expand Up @@ -260,7 +260,7 @@ pub fn decode_url(settings: &Settings, token: &str) -> Option<String> {
let nonce = XNonce::from_slice(nonce_bytes);
let ciphertext = &data[2 + 24..];

let key_bytes = Sha256::digest(settings.publisher.proxy_secret.as_bytes());
let key_bytes = Sha256::digest(settings.publisher.proxy_secret.expose().as_bytes());
let cipher = XChaCha20Poly1305::new(&key_bytes);
cipher
.decrypt(nonce, ciphertext)
Expand All @@ -278,7 +278,7 @@ pub fn decode_url(settings: &Settings, token: &str) -> Option<String> {
pub fn sign_clear_url(settings: &Settings, clear_url: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(b"ts-proxy-v2");
hasher.update(settings.publisher.proxy_secret.as_bytes());
hasher.update(settings.publisher.proxy_secret.expose().as_bytes());
hasher.update(clear_url.as_bytes());
let digest = hasher.finalize();
URL_SAFE_NO_PAD.encode(digest)
Expand Down
4 changes: 2 additions & 2 deletions crates/common/src/integrations/adserver_mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ impl AuctionProvider for AdServerMockProvider {
message: "Failed to build mediation request".to_string(),
})?;

log::debug!("AdServer Mock: mediation request: {:?}", mediation_req);
log::trace!("AdServer Mock: mediation request: {:?}", mediation_req);

// Build endpoint URL with context-driven query parameters
let endpoint_url = self.build_endpoint_url(request);
Expand Down Expand Up @@ -344,7 +344,7 @@ impl AuctionProvider for AdServerMockProvider {
message: "Failed to parse mediation response".to_string(),
})?;

log::debug!("AdServer Mock response: {:?}", response_json);
log::trace!("AdServer Mock response: {:?}", response_json);

let auction_response = self.parse_mediation_response(&response_json, response_time_ms);

Expand Down
4 changes: 2 additions & 2 deletions crates/common/src/integrations/aps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ impl AuctionProvider for ApsAuctionProvider {
message: "Failed to serialize APS bid request".to_string(),
})?;

log::debug!("APS: sending bid request: {:?}", aps_json);
log::trace!("APS: sending bid request: {:?}", aps_json);

// Create HTTP POST request
let mut aps_req = Request::new(Method::POST, &self.config.endpoint);
Expand Down Expand Up @@ -490,7 +490,7 @@ impl AuctionProvider for ApsAuctionProvider {
message: "Failed to parse APS response JSON".to_string(),
})?;

log::debug!("APS: received response: {:?}", response_json);
log::trace!("APS: received response: {:?}", response_json);

// Transform to unified format
let auction_response = self.parse_aps_response(&response_json, response_time_ms);
Expand Down
8 changes: 4 additions & 4 deletions crates/common/src/integrations/prebid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -829,9 +829,9 @@ impl AuctionProvider for PrebidAuctionProvider {
);

// Log the outgoing OpenRTB request for debugging
if log::log_enabled!(log::Level::Debug) {
if log::log_enabled!(log::Level::Trace) {
match serde_json::to_string_pretty(&openrtb) {
Ok(json) => log::debug!(
Ok(json) => log::trace!(
"Prebid OpenRTB request to {}/openrtb2/auction:\n{}",
self.config.server_url,
json
Expand Down Expand Up @@ -892,9 +892,9 @@ impl AuctionProvider for PrebidAuctionProvider {

// Log the full response body when debug is enabled to surface
// ext.debug.httpcalls, resolvedrequest, bidstatus, errors, etc.
if self.config.debug && log::log_enabled!(log::Level::Debug) {
if self.config.debug && log::log_enabled!(log::Level::Trace) {
match serde_json::to_string_pretty(&response_json) {
Ok(json) => log::debug!("Prebid OpenRTB response:\n{json}"),
Ok(json) => log::trace!("Prebid OpenRTB response:\n{json}"),
Err(e) => {
log::warn!("Prebid: failed to serialize response for logging: {e}");
}
Expand Down
1 change: 1 addition & 0 deletions crates/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub mod models;
pub mod openrtb;
pub mod proxy;
pub mod publisher;
pub mod redacted;
pub mod request_signing;
pub mod rsc_flight;
pub mod settings;
Expand Down
164 changes: 164 additions & 0 deletions crates/common/src/redacted.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//! A wrapper type that redacts sensitive values in [`Debug`] and [`Display`] output.
//!
//! Use [`Redacted`] for secrets, passwords, API keys, and other sensitive values
//! that must never appear in logs or error messages.

use core::fmt;

use serde::{Deserialize, Serialize};

/// Wraps a value so that [`Debug`] and [`Display`] print `[REDACTED]`
/// instead of the inner contents.
///
/// Access the real value via [`expose`](Redacted::expose). Callers must
/// never log or display the returned reference.
///
/// # Examples
///
/// ```
/// use trusted_server_common::redacted::Redacted;
///
/// let secret = Redacted::new("my-secret-key".to_string());
/// assert_eq!(format!("{:?}", secret), "[REDACTED]");
/// assert_eq!(secret.expose(), "my-secret-key");
/// ```
#[derive(Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Redacted<T>(T);

impl<T> Redacted<T> {
/// Creates a new [`Redacted`] value.
#[allow(dead_code)]
pub fn new(value: T) -> Self {
Self(value)
}

/// Exposes the inner value for use in operations that need the actual secret.
///
/// Callers should never log or display the returned reference.
pub fn expose(&self) -> &T {
&self.0
}
}

impl<T: Default> Default for Redacted<T> {
fn default() -> Self {
Self(T::default())
}
}

impl<T> fmt::Debug for Redacted<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[REDACTED]")
}
}

impl<T> fmt::Display for Redacted<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[REDACTED]")
}
}

impl From<String> for Redacted<String> {
fn from(value: String) -> Self {
Self(value)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn debug_output_is_redacted() {
let secret = Redacted::new("super-secret".to_string());
assert_eq!(
format!("{:?}", secret),
"[REDACTED]",
"should print [REDACTED] in debug output"
);
}

#[test]
fn display_output_is_redacted() {
let secret = Redacted::new("super-secret".to_string());
assert_eq!(
format!("{}", secret),
"[REDACTED]",
"should print [REDACTED] in display output"
);
}

#[test]
fn expose_returns_inner_value() {
let secret = Redacted::new("super-secret".to_string());
assert_eq!(
secret.expose(),
"super-secret",
"should return the inner value"
);
}

#[test]
fn default_creates_empty_redacted() {
let secret: Redacted<String> = Redacted::default();
assert_eq!(secret.expose(), "", "should default to empty string");
}

#[test]
fn from_string_creates_redacted() {
let secret = Redacted::from("my-key".to_string());
assert_eq!(secret.expose(), "my-key", "should create from String");
}

#[test]
fn clone_preserves_inner_value() {
let secret = Redacted::new("cloneable".to_string());
let cloned = secret.clone();
assert_eq!(
cloned.expose(),
"cloneable",
"should preserve value after clone"
);
}

#[test]
fn serde_roundtrip() {
let secret = Redacted::new("serialize-me".to_string());
let json = serde_json::to_string(&secret).expect("should serialize");
assert_eq!(json, "\"serialize-me\"", "should serialize transparently");

let deserialized: Redacted<String> =
serde_json::from_str(&json).expect("should deserialize");
assert_eq!(
deserialized.expose(),
"serialize-me",
"should deserialize transparently"
);
}

#[test]
fn struct_with_redacted_field_debug() {
#[derive(Debug)]
#[allow(dead_code)]
struct Config {
name: String,
api_key: Redacted<String>,
}

let config = Config {
name: "test".to_string(),
api_key: Redacted::new("secret-key-123".to_string()),
};

let debug = format!("{:?}", config);
assert!(
debug.contains("[REDACTED]"),
"should contain [REDACTED] for the api_key field"
);
assert!(
!debug.contains("secret-key-123"),
"should not contain the actual secret"
);
}
}
4 changes: 2 additions & 2 deletions crates/common/src/request_signing/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ pub fn handle_rotate_key(
mut req: Request,
) -> Result<Response, Report<TrustedServerError>> {
let (config_store_id, secret_store_id) = match &settings.request_signing {
Some(setting) => (&setting.config_store_id, &setting.secret_store_id),
Some(setting) => (&setting.config_store_id, setting.secret_store_id.expose()),
None => {
return Err(TrustedServerError::Configuration {
message: "Missing signing storage configuration.".to_string(),
Expand Down Expand Up @@ -253,7 +253,7 @@ pub fn handle_deactivate_key(
mut req: Request,
) -> Result<Response, Report<TrustedServerError>> {
let (config_store_id, secret_store_id) = match &settings.request_signing {
Some(setting) => (&setting.config_store_id, &setting.secret_store_id),
Some(setting) => (&setting.config_store_id, setting.secret_store_id.expose()),
None => {
return Err(TrustedServerError::Configuration {
message: "Missing signing storage configuration.".to_string(),
Expand Down
Loading
Loading