Skip to content

Commit e45f458

Browse files
committed
feat: add support for reading Extended User IDs from the ts-eids cookie in auction requests
1 parent 0762999 commit e45f458

11 files changed

Lines changed: 136 additions & 14 deletions

File tree

crates/trusted-server-core/src/auction/formats.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ pub fn convert_tsjs_to_auction_request(
206206
id: ec_id,
207207
fresh_id,
208208
consent: Some(consent),
209+
eids: None,
209210
},
210211
device,
211212
site: Some(SiteInfo {

crates/trusted-server-core/src/auction/orchestrator.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,7 @@ mod tests {
10901090
id: "user-123".to_string(),
10911091
fresh_id: "fresh-456".to_string(),
10921092
consent: None,
1093+
eids: None,
10931094
},
10941095
device: None,
10951096
site: None,

crates/trusted-server-core/src/auction/types.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ pub struct UserInfo {
9090
/// cookies/headers, not from stored data.
9191
#[serde(skip)]
9292
pub consent: Option<crate::consent::ConsentContext>,
93+
/// Extended User IDs parsed from the [`crate::constants::COOKIE_TS_EIDS`] cookie.
94+
///
95+
/// Raw (un-gated) values from the browser; consent gating via
96+
/// [`crate::consent::gate_eids_by_consent`] is applied in the provider
97+
/// layer before any EID reaches a bid request.
98+
#[serde(skip)]
99+
pub eids: Option<Vec<crate::openrtb::Eid>>,
93100
}
94101

95102
/// Device information from request.

crates/trusted-server-core/src/consent/mod.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -435,10 +435,6 @@ pub fn build_us_privacy_from_gpc(config: &ConsentConfig) -> Option<types::UsPriv
435435
///
436436
/// Returns [`None`] if consent is missing or insufficient, stripping all EIDs
437437
/// from the outgoing bid request.
438-
///
439-
/// **Note:** This function is implemented and tested but not yet wired into
440-
/// the bid request path. It will be connected when identity provider
441-
/// integration populates EIDs (see `prebid.rs` where `eids: None`).
442438
#[must_use]
443439
pub fn gate_eids_by_consent<T>(
444440
eids: Option<Vec<T>>,

crates/trusted-server-core/src/constants.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use http::header::HeaderName;
22

33
pub const COOKIE_TS_EC: &str = "ts-ec";
4+
/// Cookie written by the Trusted Server JS SDK containing a standard-base64-encoded
5+
/// JSON array of Extended User IDs (`[{ source, uids }]`) from identity providers.
6+
pub const COOKIE_TS_EIDS: &str = "ts-eids";
47

58
pub const HEADER_X_PUB_USER_ID: HeaderName = HeaderName::from_static("x-pub-user-id");
69
pub const HEADER_X_TS_EC: HeaderName = HeaderName::from_static("x-ts-ec");

crates/trusted-server-core/src/cookies.rs

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
use std::borrow::Cow;
77

8+
use base64::{engine::general_purpose::STANDARD, Engine as _};
89
use cookie::{Cookie, CookieJar};
910
use edgezero_core::body::Body as EdgeBody;
1011
use error_stack::{Report, ResultExt};
@@ -13,7 +14,8 @@ use http::Request;
1314
use http::Response;
1415

1516
use crate::constants::{
16-
COOKIE_EUCONSENT_V2, COOKIE_GPP, COOKIE_GPP_SID, COOKIE_TS_EC, COOKIE_US_PRIVACY,
17+
COOKIE_EUCONSENT_V2, COOKIE_GPP, COOKIE_GPP_SID, COOKIE_TS_EC, COOKIE_TS_EIDS,
18+
COOKIE_US_PRIVACY,
1719
};
1820
use crate::error::TrustedServerError;
1921
use crate::settings::Settings;
@@ -119,6 +121,35 @@ pub fn handle_request_cookies(
119121
}
120122
}
121123

124+
/// Parse Extended User IDs from the [`COOKIE_TS_EIDS`] cookie.
125+
///
126+
/// The cookie value is a standard-base64-encoded JSON array of
127+
/// [`crate::openrtb::Eid`] objects written by the Trusted Server JS SDK via
128+
/// `btoa(JSON.stringify(eids))`.
129+
///
130+
/// Returns `None` if the cookie is absent, base64-malformed, JSON-malformed,
131+
/// or the decoded array is empty. Parse failures are logged at `debug` level
132+
/// so operators can diagnose JS SDK / server mismatches.
133+
#[must_use]
134+
pub(crate) fn parse_ts_eids_cookie(jar: Option<&CookieJar>) -> Option<Vec<crate::openrtb::Eid>> {
135+
let value = jar?.get(COOKIE_TS_EIDS)?.value().to_owned();
136+
let decoded = match STANDARD.decode(&value) {
137+
Ok(b) => b,
138+
Err(e) => {
139+
log::debug!("ts-eids cookie: base64 decode failed: {e}");
140+
return None;
141+
}
142+
};
143+
match serde_json::from_slice::<Vec<crate::openrtb::Eid>>(&decoded) {
144+
Ok(eids) if !eids.is_empty() => Some(eids),
145+
Ok(_) => None,
146+
Err(e) => {
147+
log::debug!("ts-eids cookie: JSON parse failed: {e}");
148+
None
149+
}
150+
}
151+
}
152+
122153
/// Strips named cookies from a `Cookie` header value string.
123154
///
124155
/// Parses the semicolon-separated cookie pairs, filters out any whose name
@@ -759,4 +790,73 @@ mod tests {
759790
let stripped = strip_cookies(header, CONSENT_COOKIE_NAMES);
760791
assert_eq!(stripped, "session=abc=123=def");
761792
}
793+
794+
fn make_jar_with(name: &str, value: &str) -> CookieJar {
795+
parse_cookies_to_jar(&format!("{name}={value}"))
796+
}
797+
798+
fn encode_eids(eids: &[serde_json::Value]) -> String {
799+
use base64::{engine::general_purpose::STANDARD, Engine as _};
800+
STANDARD.encode(serde_json::to_string(eids).expect("should serialize eids"))
801+
}
802+
803+
#[test]
804+
fn parse_ts_eids_cookie_returns_eids_for_valid_input() {
805+
let encoded = encode_eids(&[serde_json::json!({
806+
"source": "id5-sync.com",
807+
"uids": [{"id": "abc123", "atype": 1}]
808+
})]);
809+
let jar = make_jar_with(COOKIE_TS_EIDS, &encoded);
810+
let eids = parse_ts_eids_cookie(Some(&jar)).expect("should parse valid ts-eids cookie");
811+
assert_eq!(eids.len(), 1, "should return one EID");
812+
assert_eq!(eids[0].source, "id5-sync.com", "should preserve source");
813+
assert_eq!(eids[0].uids[0].id, "abc123", "should preserve uid");
814+
}
815+
816+
#[test]
817+
fn parse_ts_eids_cookie_returns_none_when_cookie_absent() {
818+
let jar = CookieJar::new();
819+
assert!(
820+
parse_ts_eids_cookie(Some(&jar)).is_none(),
821+
"should return None when cookie absent"
822+
);
823+
}
824+
825+
#[test]
826+
fn parse_ts_eids_cookie_returns_none_for_empty_array() {
827+
let encoded = encode_eids(&[]);
828+
let jar = make_jar_with(COOKIE_TS_EIDS, &encoded);
829+
assert!(
830+
parse_ts_eids_cookie(Some(&jar)).is_none(),
831+
"should return None for empty EID array"
832+
);
833+
}
834+
835+
#[test]
836+
fn parse_ts_eids_cookie_returns_none_for_corrupt_base64() {
837+
let jar = make_jar_with(COOKIE_TS_EIDS, "not!!valid!!base64");
838+
assert!(
839+
parse_ts_eids_cookie(Some(&jar)).is_none(),
840+
"should return None for corrupt base64"
841+
);
842+
}
843+
844+
#[test]
845+
fn parse_ts_eids_cookie_returns_none_for_invalid_json() {
846+
use base64::{engine::general_purpose::STANDARD, Engine as _};
847+
let encoded = STANDARD.encode(b"this is not json");
848+
let jar = make_jar_with(COOKIE_TS_EIDS, &encoded);
849+
assert!(
850+
parse_ts_eids_cookie(Some(&jar)).is_none(),
851+
"should return None for invalid JSON"
852+
);
853+
}
854+
855+
#[test]
856+
fn parse_ts_eids_cookie_returns_none_for_none_jar() {
857+
assert!(
858+
parse_ts_eids_cookie(None).is_none(),
859+
"should return None when jar is None"
860+
);
861+
}
762862
}

crates/trusted-server-core/src/integrations/adserver_mock.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,7 @@ mod tests {
508508
id: "user-123".to_string(),
509509
fresh_id: "fresh-456".to_string(),
510510
consent: None,
511+
eids: None,
511512
},
512513
device: Some(DeviceInfo {
513514
user_agent: Some("Mozilla/5.0".to_string()),
@@ -686,6 +687,7 @@ mod tests {
686687
id: "user-1".to_string(),
687688
fresh_id: "fresh-1".to_string(),
688689
consent: None,
690+
eids: None,
689691
},
690692
device: None,
691693
site: None,

crates/trusted-server-core/src/integrations/aps.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,7 @@ mod tests {
719719
id: "user-123".to_string(),
720720
fresh_id: "fresh-456".to_string(),
721721
consent: None,
722+
eids: None,
722723
},
723724
device: Some(DeviceInfo {
724725
user_agent: Some("Mozilla/5.0".to_string()),
@@ -805,6 +806,7 @@ mod tests {
805806
id: "user-1".to_string(),
806807
fresh_id: "fresh-1".to_string(),
807808
consent: None,
809+
eids: None,
808810
},
809811
device: None,
810812
site: None,

crates/trusted-server-core/src/integrations/prebid.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::auction::types::{
1616
};
1717
use crate::backend::BackendConfig;
1818
use crate::compat;
19+
use crate::consent::gate_eids_by_consent;
1920
use crate::consent_config::ConsentForwardingMode;
2021
use crate::error::TrustedServerError;
2122
use crate::http_util::RequestInfo;
@@ -1010,9 +1011,13 @@ impl PrebidAuctionProvider {
10101011
.map(|ac| ConsentedProvidersSettings {
10111012
consented_providers: Some(ac.clone()),
10121013
}),
1013-
// EIDs will be populated by identity providers; consent gating
1014-
// is applied via `gate_eids_by_consent` before they are set here.
1015-
eids: None,
1014+
// Use the full consent context regardless of `consent_forwarding` mode.
1015+
// EID transmission rights (TCF Purpose 1 + 4) are independent of
1016+
// whether consent strings travel in the OpenRTB body or cookies.
1017+
eids: gate_eids_by_consent(
1018+
request.user.eids.clone(),
1019+
request.user.consent.as_ref(),
1020+
),
10161021
ec_fresh: Some(request.user.fresh_id.clone()),
10171022
}
10181023
.to_ext(),
@@ -1688,6 +1693,7 @@ mod tests {
16881693
id: "user-123".to_string(),
16891694
fresh_id: "fresh-456".to_string(),
16901695
consent: None,
1696+
eids: None,
16911697
},
16921698
device: None,
16931699
site: None,
@@ -3178,6 +3184,7 @@ server_url = "https://prebid.example"
31783184
id: "synth-123".to_string(),
31793185
fresh_id: "fresh-456".to_string(),
31803186
consent: None,
3187+
eids: None,
31813188
},
31823189
device: Some(DeviceInfo {
31833190
user_agent: Some("test-agent".to_string()),

crates/trusted-server-core/src/openrtb.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use serde::Serialize;
1+
use serde::{Deserialize, Serialize};
22
use serde_json::Value;
33

44
use crate::auction::types::OrchestratorExt;
@@ -75,7 +75,7 @@ pub struct ConsentedProvidersSettings {
7575
}
7676

7777
/// An Extended User ID entry from an identity provider.
78-
#[derive(Debug, Serialize)]
78+
#[derive(Debug, Clone, Serialize, Deserialize)]
7979
pub struct Eid {
8080
/// Identity provider domain (e.g. `"id5-sync.com"`).
8181
pub source: String,
@@ -84,7 +84,7 @@ pub struct Eid {
8484
}
8585

8686
/// A single user identifier within an [`Eid`] entry.
87-
#[derive(Debug, Serialize)]
87+
#[derive(Debug, Clone, Serialize, Deserialize)]
8888
pub struct Uid {
8989
/// The identifier value.
9090
pub id: String,

0 commit comments

Comments
 (0)