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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Validate synthetic ID format on inbound values from the `x-synthetic-id` header and `synthetic_id` cookie; values that do not match the expected format (`64-hex-hmac.6-alphanumeric-suffix`) are discarded and a fresh ID is generated rather than forwarded to response headers, cookies, or third-party APIs

### Fixed

- Protocol-relative creative URLs now honor `rewrite.exclude_domains`, so excluded creative assets stay direct and excluded absolute URLs submitted to `/first-party/sign` are rejected.

### Added

- Added the default-true `[auction].rewrite_creatives` option. Setting it to `false` preserves mandatory `/auction` creative sanitization while skipping first-party resource/click URL rewriting and creative TSJS injection.
- Added Osano consent mirror integration docs and public enablement guidance.
- Implemented basic authentication for configurable endpoint paths (#73)
- Added integrations guide with example `testlight` integration
Expand Down
194 changes: 194 additions & 0 deletions crates/trusted-server-cli/tests/config_env_overlay.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//! Regression coverage for typed app-config environment overlays.

use std::fs;
use std::process::{Command, Output};

use tempfile::TempDir;
use toml_edit::{DocumentMut, value};

const LEGACY_CONFIG: &str = include_str!(
"../../trusted-server-integration-tests/fixtures/configs/trusted-server.integration.toml"
);
const MANIFEST: &str = r#"
[app]
name = "trusted-server"

[adapters.axum.adapter]
crate = "crates/trusted-server-adapter-axum"

[adapters.axum.commands]
build = "echo"
deploy = "echo"
serve = "echo"

[stores.config]
ids = ["trusted_server_config"]

[stores.secrets]
ids = ["trusted_server_secrets"]
"#;
const REWRITE_ENV: &str = "TRUSTED_SERVER__AUCTION__REWRITE_CREATIVES";

struct MigratedProject {
directory: TempDir,
config_path: std::path::PathBuf,
manifest_path: std::path::PathBuf,
}

fn migrated_legacy_project() -> MigratedProject {
let directory = tempfile::tempdir().expect("should create temporary config directory");
let config_path = directory.path().join("trusted-server.toml");
let manifest_path = directory.path().join("edgezero.toml");
let mut document = LEGACY_CONFIG
.parse::<DocumentMut>()
.expect("should parse legacy integration config");
document["auction"]["rewrite_creatives"] = value(true);
fs::write(&config_path, document.to_string()).expect("should write migrated config");
fs::write(&manifest_path, MANIFEST).expect("should write test manifest");
MigratedProject {
directory,
config_path,
manifest_path,
}
}

fn validate_with_overlay(project: &MigratedProject, raw_value: &str) -> Output {
Command::new(env!("CARGO_BIN_EXE_ts"))
.args(["config", "validate", "--manifest"])
.arg(&project.manifest_path)
.arg("--app-config")
.arg(&project.config_path)
.current_dir(project.directory.path())
.env(REWRITE_ENV, raw_value)
.output()
.expect("should run ts config validate")
Comment thread
ChristianPavilonis marked this conversation as resolved.
}

#[test]
fn migrated_legacy_config_applies_rewrite_creatives_environment_override() {
let project = migrated_legacy_project();
let output = Command::new(env!("CARGO_BIN_EXE_ts"))
.args(["config", "push", "--adapter", "axum", "--manifest"])
.arg(&project.manifest_path)
.arg("--app-config")
.arg(&project.config_path)
.args(["--yes", "--no-diff"])
.current_dir(project.directory.path())
.env(REWRITE_ENV, "false")
.output()
.expect("should run ts config push");

assert!(
output.status.success(),
"valid boolean overlay should push successfully: {}",
String::from_utf8_lossy(&output.stderr)
);

let local_store_path = project
.directory
.path()
.join(".edgezero/local-config-trusted_server_config.json");
let local_store: serde_json::Value = serde_json::from_str(
&fs::read_to_string(local_store_path).expect("should read pushed local config"),
)
.expect("should parse local config store");
let envelope_json = local_store
.as_object()
.and_then(|entries| entries.values().next())
.and_then(serde_json::Value::as_str)
.expect("should contain a blob envelope");
let envelope: serde_json::Value =
serde_json::from_str(envelope_json).expect("should parse blob envelope");

assert_eq!(
envelope["data"]["auction"]["rewrite_creatives"],
serde_json::Value::Bool(false),
"pushed config should contain the environment override"
);
}

#[test]
fn migrated_legacy_config_default_rewrite_creatives_has_no_local_diff() {
let project = migrated_legacy_project();
let push = Command::new(env!("CARGO_BIN_EXE_ts"))
.args(["config", "push", "--adapter", "axum", "--manifest"])
.arg(&project.manifest_path)
.arg("--app-config")
.arg(&project.config_path)
.args(["--yes", "--no-diff"])
.current_dir(project.directory.path())
.output()
.expect("should run ts config push");

assert!(
push.status.success(),
"default rewrite setting should push successfully: {}",
String::from_utf8_lossy(&push.stderr)
);

let local_store_path = project
.directory
.path()
.join(".edgezero/local-config-trusted_server_config.json");
let local_store: serde_json::Value = serde_json::from_str(
&fs::read_to_string(local_store_path).expect("should read pushed local config"),
)
.expect("should parse local config store");
let envelope_json = local_store
.as_object()
.and_then(|entries| entries.values().next())
.and_then(serde_json::Value::as_str)
.expect("should contain a blob envelope");
let envelope: serde_json::Value =
serde_json::from_str(envelope_json).expect("should parse blob envelope");

assert!(
envelope["data"]["auction"]
.get("rewrite_creatives")
.is_none(),
"should omit the default rewrite setting from the pushed blob"
);

let diff = Command::new(env!("CARGO_BIN_EXE_ts"))
.args([
"config",
"diff",
"--adapter",
"axum",
"--local",
"--manifest",
])
.arg(&project.manifest_path)
.arg("--app-config")
.arg(&project.config_path)
.current_dir(project.directory.path())
.output()
.expect("should run ts config diff");

assert!(
diff.status.success(),
"default rewrite setting should have no local diff: {}",
String::from_utf8_lossy(&diff.stderr)
);
assert!(
String::from_utf8_lossy(&diff.stderr).contains("# no changes"),
"diff should report no changes: {}",
String::from_utf8_lossy(&diff.stderr)
);
}

#[test]
fn migrated_legacy_config_rejects_invalid_rewrite_creatives_environment_override() {
let project = migrated_legacy_project();
let output = validate_with_overlay(&project, "not-a-boolean");
let stderr = String::from_utf8_lossy(&output.stderr);

assert!(
!output.status.success(),
"invalid boolean overlay should fail validation"
);
assert!(
stderr.contains(REWRITE_ENV) && stderr.contains("boolean"),
"error should identify the invalid boolean overlay: {stderr}"
);
}
11 changes: 8 additions & 3 deletions crates/trusted-server-core/src/auction/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ When a request arrives at the `/auction` endpoint, it goes through the following
┌──────────────────────────────────────────────────────────────────────┐
│ 11. Transform to OpenRTB Response (mod.rs:274-322) │
│ - Build seatbid array (one per winning bid) │
│ - Rewrite creative HTML for first-party proxy │
│ - Always sanitize creative HTML │
│ - Rewrite creative HTML when enabled (default) │
│ - Add orchestrator metadata (timing, strategy, bid count) │
└──────────────────────────────────────────────────────────────────────┘
Expand Down Expand Up @@ -248,7 +249,10 @@ The orchestrator collects all bids and creates an OpenRTB response:
}
```

Note that creative HTML is rewritten to use the first-party proxy (`/first-party/proxy`) for privacy and security.
Creative HTML is always sanitized. By default, it is then rewritten to use the
first-party proxy (`/first-party/proxy`) and the creative runtime is injected.
Setting `[auction].rewrite_creatives = false` skips only that rewrite and
injection pass.

## Route Registration & Endpoints

Expand Down Expand Up @@ -379,7 +383,8 @@ The `/auction` endpoint is the primary entry point for auctions:
**Key Transformations:**
- `adUnits[].code` → `seatbid[].bid[].impid` (slot identifier)
- `mediaTypes.banner.sizes` → evaluated by providers, winning size in `bid.w` and `bid.h`
- Creative HTML is rewritten to use `/first-party/proxy` URLs
- Creative HTML is always sanitized, then rewritten to use `/first-party/proxy` URLs by default
- `[auction].rewrite_creatives = false` skips rewriting and creative runtime injection, not sanitization
- Multiple bids per slot become separate `seatbid` entries
- Orchestrator metadata added in `ext.orchestrator`

Expand Down
7 changes: 4 additions & 3 deletions crates/trusted-server-core/src/auction/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ const MAX_AUCTION_BODY_SIZE: usize = 256 * 1024;
/// ## Response
///
/// Returns an `OpenRTB 2.x` response. Creative HTML is inlined in each bid's
/// `adm` field after sanitisation and first-party URL rewriting. Response
/// headers include `X-TS-EC` (the caller's Edge Cookie ID) and
/// `X-TS-EC-Fresh` (a freshly generated ID for cookie renewal).
/// `adm` field after mandatory server-side sanitization. First-party resource
/// and click URL rewriting plus creative TSJS injection are enabled by default;
/// setting [`auction.rewrite_creatives`][`crate::auction_config_types::AuctionConfig::rewrite_creatives`]
/// to `false` skips only that rewrite pass.
///
/// ## Scroll, refresh, and SPA navigation
///
Expand Down
Loading
Loading