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

### Added

- Server-side identity providers for ID5 (`[integrations.id5]`), LiveIntent (`[integrations.liveintent]`), LiveRamp ATS (`[integrations.liveramp]`), UID2 (`[integrations.uid2]`) and Yahoo ConnectID (`[integrations.connectid]`), and revoke-only Lockr capture (`[integrations.lockr].capture_identity`). Each source they write needs an `[[ec.partners]]` entry. See [Identity Vendors](docs/guide/integrations/identity-vendors.md).
- `tsjs.setEmail`, `tsjs.setHashedEmail` and `tsjs.clearHashedEmail` store the SHA-256 of the visitor's normalized email in the first-party `ts-hem` cookie, the identity providers' hashed-email input. Trusted Server reads it per request, never stores it, and strips it from requests to the origin and to Prebid Server.
- Identity providers can declare and read first-party cookies with `Resolves::with_cookie` and `ResolveInput::cookie`.
- Server-side identity provider framework. An integration registers an `IdentityProvider` with `try_with_identity_provider`, and the coordinator handles consent, transport limits, backoff and the single KV write. Provider authors get a test harness (`ProviderHarness`, `check_conformance`, `output_eids`). See [Writing an Identity Provider](docs/guide/identity-providers.md). No vendor ships with the framework.
- Source ownership: `owner` on an `[[ec.partners]]` entry names the provider that owns a source. An owner's result replaces the source's whole entry set, and batch sync writes an owned source only when the row holds no value for it (an expired owned record counts as present and is not filled). Validation rejects an owner that is not enabled, is capture-only, does not claim the source, or collides with another provider. See [Source ownership](docs/guide/configuration.md#source-ownership-ec-partners-owner).
- Hold mode: `mode = "hold"` in `[ec.identity.providers.<id>]` runs a provider before the navigation's auction, within the shared `hold_budget_ms`, with `timeout_ms` and `after_send_timeout_ms` as call timeouts.
Expand Down
63 changes: 60 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ assert_cmd = "2"
async-stream = "0.3"
async-trait = "0.1"
axum = "0.8"
aes-gcm = "0.10"
base64 = "0.22"
brotli = "8.0"
build-print = "1.0.1"
Expand Down
1 change: 1 addition & 0 deletions crates/trusted-server-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ version = { workspace = true }
workspace = true

[dependencies]
aes-gcm = { workspace = true }
async-trait = { workspace = true }
async-stream = { workspace = true }
base64 = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions crates/trusted-server-core/src/auction/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,8 @@ mod tests {
user_agent: None,
dnt: false,
page_url: None,
hashed_email: None,
cookies: BTreeMap::new(),
},
);
let cookie = cookie_only_ts_eids();
Expand Down
68 changes: 65 additions & 3 deletions crates/trusted-server-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,24 @@ use crate::error::TrustedServerError;
use crate::integrations::{
adserver_mock::AdServerMockConfig,
aps::ApsConfig,
connectid::{CONNECTID_INTEGRATION_ID, ConnectIdConfig},
datadome::DataDomeConfig,
didomi::DidomiIntegrationConfig,
google_tag_manager::GoogleTagManagerConfig,
gpt::GptConfig,
gpt_diagnostics::GptDiagnosticsConfig,
id5::{ID5_INTEGRATION_ID, Id5Config},
js_asset_proxy::{JS_ASSET_PROXY_INTEGRATION_ID, JsAssetProxyConfig},
lockr::LockrConfig,
liveintent::{LIVEINTENT_INTEGRATION_ID, LiveIntentConfig},
liveramp::{LIVERAMP_INTEGRATION_ID, LiveRampConfig},
lockr::{LOCKR_INTEGRATION_ID, LockrConfig},
nextjs::NextJsIntegrationConfig,
osano::OsanoConfig,
permutive::PermutiveConfig,
prebid,
sourcepoint::SourcepointConfig,
testlight::TestlightConfig,
uid2::{UID2_INTEGRATION_ID, Uid2Config},
};
use crate::settings::{
AssetOriginAuth, IdentitySettings, IntegrationConfig, ProviderMode, Settings,
Expand Down Expand Up @@ -62,6 +67,11 @@ const DEPLOY_VALIDATED_INTEGRATION_IDS: &[&str] = &[
"datadome",
"gpt",
"gpt_diagnostics",
ID5_INTEGRATION_ID,
LIVEINTENT_INTEGRATION_ID,
LIVERAMP_INTEGRATION_ID,
UID2_INTEGRATION_ID,
CONNECTID_INTEGRATION_ID,
JS_ASSET_PROXY_INTEGRATION_ID,
];

Expand Down Expand Up @@ -203,6 +213,22 @@ impl edgezero_core::app_config::AppConfigMeta for TrustedServerAppConfig {
],
true,
),
field(
vec![
optional_object("integrations"),
optional_object("uid2"),
object("api_key"),
],
true,
),
field(
vec![
optional_object("integrations"),
optional_object("uid2"),
object("client_secret"),
],
true,
),
field(
vec![
optional_object("integrations"),
Expand Down Expand Up @@ -322,6 +348,37 @@ pub(crate) fn identity_descriptors_for_deploy(
#[allow(unused_mut)]
let mut descriptors =
crate::ec::identity::generic_pull::generic_pull_descriptors(&settings.ec.partners)?;
descriptors.extend(validate_identity_integration::<Id5Config>(
settings,
ID5_INTEGRATION_ID,
)?);
descriptors.extend(validate_identity_integration::<LiveIntentConfig>(
settings,
LIVEINTENT_INTEGRATION_ID,
)?);
descriptors.extend(validate_identity_integration::<LiveRampConfig>(
settings,
LIVERAMP_INTEGRATION_ID,
)?);
descriptors.extend(validate_identity_integration::<Uid2Config>(
settings,
UID2_INTEGRATION_ID,
)?);
descriptors.extend(validate_identity_integration::<ConnectIdConfig>(
settings,
CONNECTID_INTEGRATION_ID,
)?);
if let Some(lockr) = settings.integration_config::<LockrConfig>(LOCKR_INTEGRATION_ID)?
&& lockr.capture_identity
{
descriptors.push(lockr.build_descriptor().change_context(
TrustedServerError::Configuration {
message: format!(
"integrations.{LOCKR_INTEGRATION_ID}: the identity provider cannot be described"
),
},
)?);
}
#[cfg(test)]
descriptors.extend(validate_identity_integration::<
crate::ec::identity::examples::ExampleIdConfig,
Expand All @@ -343,8 +400,6 @@ pub(crate) fn identity_descriptors_for_deploy(
/// Returns [`TrustedServerError::Configuration`] when the integration's
/// config does not parse or validate, or its descriptor cannot be built.
/// Returns `Ok(None)` when the integration is absent or disabled.
// Temporary: the first production identity integration adds the first caller.
#[allow(dead_code)]
pub(crate) fn validate_identity_integration<T: IdentityProviderConfig + DeserializeOwned>(
settings: &Settings,
id: &str,
Expand Down Expand Up @@ -477,6 +532,11 @@ fn validate_enabled_integrations(
validate_integration::<NextJsIntegrationConfig>(settings, "nextjs")?;
validate_integration::<PermutiveConfig>(settings, "permutive")?;
validate_integration::<LockrConfig>(settings, "lockr")?;
validate_integration::<Id5Config>(settings, ID5_INTEGRATION_ID)?;
validate_integration::<LiveIntentConfig>(settings, LIVEINTENT_INTEGRATION_ID)?;
validate_integration::<LiveRampConfig>(settings, LIVERAMP_INTEGRATION_ID)?;
validate_integration::<Uid2Config>(settings, UID2_INTEGRATION_ID)?;
validate_integration::<ConnectIdConfig>(settings, CONNECTID_INTEGRATION_ID)?;
validate_integration::<DidomiIntegrationConfig>(settings, "didomi")?;
validate_integration::<SourcepointConfig>(settings, "sourcepoint")?;
validate_integration::<OsanoConfig>(settings, "osano")?;
Expand Down Expand Up @@ -1084,6 +1144,8 @@ formats = [{ width = 300, height = 250 }]
"integrations.datadome.server_side_key_secret_name".to_owned(),
true,
),
("integrations.uid2.api_key".to_owned(), true),
("integrations.uid2.client_secret".to_owned(), true),
(
"integrations.datadome.protection_test_bypass.credential_secret_name"
.to_owned(),
Expand Down
4 changes: 4 additions & 0 deletions crates/trusted-server-core/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ pub const COOKIE_TS_EIDS: &str = "ts-eids";
/// Client-only mirror of the servable server-side EIDs, written by the shim
/// and never read by the server (spec 6.2).
pub const COOKIE_TS_EIDS_S: &str = "__Host-ts-eids-s";
/// Cookie written by the Trusted Server JS SDK holding the lowercase hex
/// SHA-256 of the visitor's normalized email address. Read per request as the
/// identity providers' hashed-email input; never stored in KV.
pub const COOKIE_TS_HEM: &str = "ts-hem";
pub const COOKIE_TS_TESTER: &str = "ts-tester";
pub const COOKIE_SHAREDID: &str = "sharedId";

Expand Down
27 changes: 18 additions & 9 deletions crates/trusted-server-core/src/cookies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use http::header;

use crate::constants::{
COOKIE_EUCONSENT_V2, COOKIE_GPP, COOKIE_GPP_SID, COOKIE_TS_EC, COOKIE_TS_EIDS,
COOKIE_TS_EIDS_S, COOKIE_US_PRIVACY,
COOKIE_TS_EIDS_S, COOKIE_TS_HEM, COOKIE_US_PRIVACY,
};
use crate::error::TrustedServerError;

Expand All @@ -33,18 +33,26 @@ pub const CONSENT_COOKIE_NAMES: &[&str] = &[
///
/// Stripped from Prebid Server requests and never added to proxy or provider
/// requests.
pub const TS_IDENTITY_COOKIE_NAMES: &[&str] = &[COOKIE_TS_EC, COOKIE_TS_EIDS, COOKIE_TS_EIDS_S];
pub const TS_IDENTITY_COOKIE_NAMES: &[&str] = &[
COOKIE_TS_EC,
COOKIE_TS_EIDS,
COOKIE_TS_EIDS_S,
COOKIE_TS_HEM,
];

/// Identity cookies removed from publisher-origin requests by default.
///
/// `ts-ec` and `ts-eids` still reach the origin, as they did before the
/// `ts-eids-s` mirror existed, so the origin's `Cookie` header stays at its
/// earlier size. See [`TS_ORIGIN_STRIPPED_COOKIE_NAMES_WITH_EIDS`].
pub const TS_ORIGIN_STRIPPED_COOKIE_NAMES: &[&str] = &[COOKIE_TS_EIDS_S];
/// earlier size. The hashed email never leaves the edge. See
/// [`TS_ORIGIN_STRIPPED_COOKIE_NAMES_WITH_EIDS`].
pub const TS_ORIGIN_STRIPPED_COOKIE_NAMES: &[&str] = &[COOKIE_TS_EIDS_S, COOKIE_TS_HEM];

/// Identity cookies removed from publisher-origin requests when the operator
/// sets `ec.strip_eids_cookie_from_origin`: `ts-eids` as well as the mirror.
pub const TS_ORIGIN_STRIPPED_COOKIE_NAMES_WITH_EIDS: &[&str] = &[COOKIE_TS_EIDS, COOKIE_TS_EIDS_S];
/// sets `ec.strip_eids_cookie_from_origin`: `ts-eids` as well as the mirror
/// and the hashed email.
pub const TS_ORIGIN_STRIPPED_COOKIE_NAMES_WITH_EIDS: &[&str] =
&[COOKIE_TS_EIDS, COOKIE_TS_EIDS_S, COOKIE_TS_HEM];

/// Parses a cookie string into a [`CookieJar`].
///
Expand Down Expand Up @@ -520,14 +528,15 @@ mod tests {

#[test]
fn strip_cookie_pairs_removes_exact_names_only() {
let header =
"ts-ec=abc; ts-eids=W10; __Host-ts-eids-s=eyJ2IjoxfQ; other=1; ts-eids-extra=keep";
let header = "ts-ec=abc; ts-eids=W10; __Host-ts-eids-s=eyJ2IjoxfQ; ts-hem=00ff; other=1; \
ts-eids-extra=keep";

let stripped = strip_cookie_pairs(header, TS_ORIGIN_STRIPPED_COOKIE_NAMES_WITH_EIDS);

assert_eq!(
stripped, "ts-ec=abc; other=1; ts-eids-extra=keep",
"should drop the ID cookies and keep ts-ec and a cookie that only shares a prefix"
"should drop the ID and hashed-email cookies and keep ts-ec and a cookie that only \
shares a prefix"
);

assert_eq!(
Expand Down
2 changes: 2 additions & 0 deletions crates/trusted-server-core/src/ec/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1939,6 +1939,8 @@ mod tests {
user_agent: None,
dnt: false,
page_url: None,
hashed_email: None,
cookies: std::collections::BTreeMap::new(),
});
}

Expand Down
Loading
Loading