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: 2 additions & 0 deletions .claude/agents/pr-creator.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Using the `.github/pull_request_template.md` structure, draft:
- **Changes table**: list each file modified and what changed.
- **Closes**: `Closes #<issue-number>` to auto-close the linked issue.
- **Test plan**: check off which verification steps were run.
- **Hardening note**: when config-derived regex or pattern compilation is touched, state how invalid enabled config fails startup and which regression tests cover that path.
- **Checklist**: verify each item applies.

### 5. Create the PR
Expand Down Expand Up @@ -172,6 +173,7 @@ Do **not** use labels as a substitute for types.
- Use sentence case for the title.
- Use imperative mood (e.g., "Add caching to proxy" not "Added caching").
- The summary should focus on _why_, not just _what_.
- Do not describe config-derived regex/pattern compilation as safe unless invalid enabled config is handled without `panic!`, `unwrap()`, or `expect()`.
- Always base PRs against `main` unless told otherwise.
- Always assign the PR to the current user (`--assignee @me`).
- Never force-push or rebase without explicit user approval.
Expand Down
3 changes: 3 additions & 0 deletions .claude/agents/pr-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ For each changed file, evaluate:
- `expect("should ...")` instead of `unwrap()` in production code
- `error-stack` (`Report<E>`) with `derive_more::Display` for errors (not thiserror/anyhow)
- `log` macros (not `println!`)
- Config-derived regex/pattern compilation must not use panic-prone `expect()`/`unwrap()`; invalid enabled config should surface as startup/config errors
- Invalid enabled integrations/providers must not be silently logged-and-disabled during startup or registration
- `vi.hoisted()` for mock definitions in JS tests
- Integration IDs match JS directory names
- Colocated tests with `#[cfg(test)]`
Expand Down Expand Up @@ -105,6 +107,7 @@ For each changed file, evaluate:

- Are new code paths tested?
- Are edge cases covered (empty input, max values, error paths)?
- If config-derived regex/pattern compilation changed: are invalid enabled-config startup failures and explicit `enabled = false` bypass cases both covered?
- Rust tests: `cargo test --workspace`
- JS tests: `npx vitest run` in `crates/js/lib/`

Expand Down
19 changes: 14 additions & 5 deletions crates/common/src/auction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
//! Note: Individual auction providers are located in the `integrations` module
//! (e.g., `crate::integrations::aps`, `crate::integrations::prebid`).

use error_stack::Report;

use crate::error::TrustedServerError;
use crate::settings::Settings;
use std::sync::Arc;

Expand All @@ -27,7 +30,8 @@ pub use types::{
};

/// Type alias for provider builder functions.
type ProviderBuilder = fn(&Settings) -> Vec<Arc<dyn AuctionProvider>>;
type ProviderBuilder =
fn(&Settings) -> Result<Vec<Arc<dyn AuctionProvider>>, Report<TrustedServerError>>;

/// Returns the list of all available provider builder functions.
///
Expand All @@ -49,15 +53,20 @@ fn provider_builders() -> &'static [ProviderBuilder] {
///
/// # Arguments
/// * `settings` - Application settings used to configure the orchestrator and providers
#[must_use]
pub fn build_orchestrator(settings: &Settings) -> AuctionOrchestrator {
///
/// # Errors
///
/// Returns an error when an enabled auction provider has invalid configuration.
pub fn build_orchestrator(
settings: &Settings,
) -> Result<AuctionOrchestrator, Report<TrustedServerError>> {
log::info!("Building auction orchestrator");

let mut orchestrator = AuctionOrchestrator::new(settings.auction.clone());

// Auto-discover and register all auction providers from settings
for builder in provider_builders() {
for provider in builder(settings) {
for provider in builder(settings)? {
orchestrator.register_provider(provider);
}
}
Expand All @@ -67,5 +76,5 @@ pub fn build_orchestrator(settings: &Settings) -> AuctionOrchestrator {
orchestrator.provider_count()
);

orchestrator
Ok(orchestrator)
}
61 changes: 51 additions & 10 deletions crates/common/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,40 @@
use base64::{engine::general_purpose::STANDARD, Engine as _};
use error_stack::Report;
use fastly::http::{header, StatusCode};
use fastly::{Request, Response};

use crate::error::TrustedServerError;
use crate::settings::Settings;

const BASIC_AUTH_REALM: &str = r#"Basic realm="Trusted Server""#;

pub fn enforce_basic_auth(settings: &Settings, req: &Request) -> Option<Response> {
let handler = settings.handler_for_path(req.get_path())?;
/// Enforce HTTP basic auth for the matched handler, if any.
///
/// Returns `Ok(None)` when the request does not target a protected handler or
/// when the supplied credentials are valid. Returns `Ok(Some(Response))` with
/// the auth challenge when credentials are missing or invalid.
///
/// # Errors
///
/// Returns an error when handler configuration is invalid, such as an
/// un-compilable path regex.
pub fn enforce_basic_auth(
settings: &Settings,
req: &Request,
) -> Result<Option<Response>, Report<TrustedServerError>> {
let Some(handler) = settings.handler_for_path(req.get_path())? else {
return Ok(None);
};

let (username, password) = match extract_credentials(req) {
Some(credentials) => credentials,
None => return Some(unauthorized_response()),
None => return Ok(Some(unauthorized_response())),
};

if username == handler.username && password == handler.password {
None
Ok(None)
} else {
Some(unauthorized_response())
Ok(Some(unauthorized_response()))
}
}

Expand Down Expand Up @@ -72,15 +89,19 @@ mod tests {
let settings = settings_with_handlers();
let req = Request::new(Method::GET, "https://example.com/open");

assert!(enforce_basic_auth(&settings, &req).is_none());
assert!(enforce_basic_auth(&settings, &req)
.expect("should evaluate auth")
.is_none());
}

#[test]
fn challenge_when_missing_credentials() {
let settings = settings_with_handlers();
let req = Request::new(Method::GET, "https://example.com/secure");

let response = enforce_basic_auth(&settings, &req).expect("should challenge");
let response = enforce_basic_auth(&settings, &req)
.expect("should evaluate auth")
.expect("should challenge");
assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED);
let realm = response
.get_header(header::WWW_AUTHENTICATE)
Expand All @@ -95,7 +116,9 @@ mod tests {
let token = STANDARD.encode("user:pass");
req.set_header(header::AUTHORIZATION, format!("Basic {token}"));

assert!(enforce_basic_auth(&settings, &req).is_none());
assert!(enforce_basic_auth(&settings, &req)
.expect("should evaluate auth")
.is_none());
}

#[test]
Expand All @@ -105,7 +128,9 @@ mod tests {
let token = STANDARD.encode("user:wrong");
req.set_header(header::AUTHORIZATION, format!("Basic {token}"));

let response = enforce_basic_auth(&settings, &req).expect("should challenge");
let response = enforce_basic_auth(&settings, &req)
.expect("should evaluate auth")
.expect("should challenge");
assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED);
}

Expand All @@ -115,7 +140,23 @@ mod tests {
let mut req = Request::new(Method::GET, "https://example.com/secure");
req.set_header(header::AUTHORIZATION, "Bearer token");

let response = enforce_basic_auth(&settings, &req).expect("should challenge");
let response = enforce_basic_auth(&settings, &req)
.expect("should evaluate auth")
.expect("should challenge");
assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED);
}

#[test]
fn returns_error_for_invalid_handler_regex_without_panicking() {
let config = crate_test_settings_str().replace(r#"path = "^/secure""#, r#"path = "(""#);
let settings = Settings::from_toml(&config).expect("should parse invalid regex TOML");
let req = Request::new(Method::GET, "https://example.com/secure");

let err = enforce_basic_auth(&settings, &req).expect_err("should return config error");
assert!(
err.to_string()
.contains("Handler path regex `(` failed to compile"),
"should describe the invalid handler regex"
);
}
}
15 changes: 11 additions & 4 deletions crates/common/src/integrations/adserver_mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub struct AdServerMockConfig {
pub enabled: bool,

/// Mediation endpoint URL
#[validate(url)]
pub endpoint: String,

/// Timeout in milliseconds
Expand Down Expand Up @@ -379,8 +380,14 @@ impl AuctionProvider for AdServerMockProvider {
// ============================================================================

/// Auto-register ad server mock provider based on settings configuration.
#[must_use]
pub fn register_providers(settings: &Settings) -> Vec<Arc<dyn AuctionProvider>> {
///
/// # Errors
///
/// Returns an error when the ad server mock provider is enabled with invalid
/// configuration.
pub fn register_providers(
settings: &Settings,
) -> Result<Vec<Arc<dyn AuctionProvider>>, Report<TrustedServerError>> {
let mut providers: Vec<Arc<dyn AuctionProvider>> = Vec::new();

match settings.integration_config::<AdServerMockConfig>("adserver_mock") {
Expand All @@ -395,11 +402,11 @@ pub fn register_providers(settings: &Settings) -> Vec<Arc<dyn AuctionProvider>>
log::debug!("AdServer Mock config found but is disabled");
}
Err(e) => {
log::error!("Failed to load AdServer Mock config: {:?}", e);
return Err(e);
}
}

providers
Ok(providers)
}

// ============================================================================
Expand Down
15 changes: 11 additions & 4 deletions crates/common/src/integrations/aps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ pub struct ApsConfig {

/// APS API endpoint
#[serde(default = "default_endpoint")]
#[validate(url)]
pub endpoint: String,

/// Timeout in milliseconds
Expand Down Expand Up @@ -532,8 +533,14 @@ use std::sync::Arc;
/// Auto-register APS provider based on settings configuration.
///
/// Returns the APS provider if enabled in settings.
#[must_use]
pub fn register_providers(settings: &Settings) -> Vec<Arc<dyn AuctionProvider>> {
///
/// # Errors
///
/// Returns an error when the APS provider is enabled with invalid
/// configuration.
pub fn register_providers(
settings: &Settings,
) -> Result<Vec<Arc<dyn AuctionProvider>>, Report<TrustedServerError>> {
let mut providers: Vec<Arc<dyn AuctionProvider>> = Vec::new();

// Check for real APS provider configuration
Expand All @@ -550,11 +557,11 @@ pub fn register_providers(settings: &Settings) -> Vec<Arc<dyn AuctionProvider>>
log::debug!("APS integration config found but is disabled");
}
Err(e) => {
log::error!("Failed to load APS configuration: {:?}", e);
return Err(e);
}
}

providers
Ok(providers)
}

// ============================================================================
Expand Down
38 changes: 21 additions & 17 deletions crates/common/src/integrations/datadome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,17 +461,13 @@ impl IntegrationAttributeRewriter for DataDomeIntegration {
}
}

fn build(settings: &Settings) -> Option<Arc<DataDomeIntegration>> {
let config = match settings.integration_config::<DataDomeConfig>(DATADOME_INTEGRATION_ID) {
Ok(Some(config)) => config,
Ok(None) => {
log::debug!("[datadome] Integration disabled or not configured");
return None;
}
Err(err) => {
log::error!("[datadome] Failed to load integration config: {err:?}");
return None;
}
fn build(
settings: &Settings,
) -> Result<Option<Arc<DataDomeIntegration>>, Report<TrustedServerError>> {
let Some(config) = settings.integration_config::<DataDomeConfig>(DATADOME_INTEGRATION_ID)?
else {
log::debug!("[datadome] Integration disabled or not configured");
return Ok(None);
};

log::info!(
Expand All @@ -480,20 +476,28 @@ fn build(settings: &Settings) -> Option<Arc<DataDomeIntegration>> {
config.rewrite_sdk
);

Some(DataDomeIntegration::new(config))
Ok(Some(DataDomeIntegration::new(config)))
}

/// Register the `DataDome` integration with Trusted Server.
#[must_use]
pub fn register(settings: &Settings) -> Option<IntegrationRegistration> {
let integration = build(settings)?;
///
/// # Errors
///
/// Returns an error when the `DataDome` integration is enabled with invalid
/// configuration.
pub fn register(
settings: &Settings,
) -> Result<Option<IntegrationRegistration>, Report<TrustedServerError>> {
let Some(integration) = build(settings)? else {
return Ok(None);
};

Some(
Ok(Some(
IntegrationRegistration::builder(DATADOME_INTEGRATION_ID)
.with_proxy(integration.clone())
.with_attribute_rewriter(integration)
.build(),
)
))
}

#[cfg(test)]
Expand Down
38 changes: 23 additions & 15 deletions crates/common/src/integrations/didomi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,28 +153,36 @@ impl DidomiIntegration {
}
}

fn build(settings: &Settings) -> Option<Arc<DidomiIntegration>> {
let config = match settings.integration_config::<DidomiIntegrationConfig>(DIDOMI_INTEGRATION_ID)
{
Ok(Some(config)) => Arc::new(config),
Ok(None) => return None,
Err(err) => {
log::error!("Failed to load Didomi integration config: {err:?}");
return None;
}
fn build(
settings: &Settings,
) -> Result<Option<Arc<DidomiIntegration>>, Report<TrustedServerError>> {
let Some(config) =
settings.integration_config::<DidomiIntegrationConfig>(DIDOMI_INTEGRATION_ID)?
else {
return Ok(None);
};
Some(DidomiIntegration::new(config))

Ok(Some(DidomiIntegration::new(Arc::new(config))))
}

/// Register the Didomi consent notice integration when enabled.
#[must_use]
pub fn register(settings: &Settings) -> Option<IntegrationRegistration> {
let integration = build(settings)?;
Some(
///
/// # Errors
///
/// Returns an error when the Didomi integration is enabled with invalid
/// configuration.
pub fn register(
settings: &Settings,
) -> Result<Option<IntegrationRegistration>, Report<TrustedServerError>> {
let Some(integration) = build(settings)? else {
return Ok(None);
};

Ok(Some(
IntegrationRegistration::builder(DIDOMI_INTEGRATION_ID)
.with_proxy(integration)
.build(),
)
))
}

#[async_trait(?Send)]
Expand Down
Loading
Loading