Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -828,9 +828,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 @@ -891,9 +891,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