Repository navigation
Conversation
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Add an optional IdentityProvider::read_request_cookies step that runs for providers with due sources before any lookup. Cookie IDs pass the same consent gates, validation and write plan as lookup results and replace the lookup. The provider endpoint becomes optional so cookie-only kinds need none; lookup kinds reject a missing endpoint. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Partner attributes such as bidswitch or magnite now carry the UID ext {provider: liveintent.com} that Prebid's LiveIntent module sets, while nonId stays bare. LiveIntent answers 400 Malformed did to any distributor ID that is not a did- ID, so the build rejects one.
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Send _lc2_fpi as duid, resolve attributes from LiveIntent's fixed attribute table, keep reply segments as the nonId EID ext and mark the Trade Desk ID with matcher and rtiPartner, as the reference provider and Prebid do. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Prebid's identityLink ID module stores the envelope it fetches bare under its storage name, commonly idl_env, rather than the base64 JSON that ATS writes to _lr_env. Read idl_env after _lr_env by default and accept a base64-alphabet value that does not decode to JSON as a bare envelope. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Reads the publisher common ID from _sharedid, falling back to _pubcid, as a device-based pubcid.org EID, and honours the _pubcid_optout cookie. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Stores Criteo's bid ID verbatim as the device-based EID Prebid sends, with no server call. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Stores The Trade Desk ID with the rtiPartner ext, inserter, matcher and mm that Prebid sets. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Providers now report found IDs, an opt-out or nothing from their first-party cookies. An opt-out skips the provider's lookup as skipped_opted_out and removes its stored sources in the write plan, without a browser fallback. ID5 (universal_uid 0), UID2 (Prebid latestToken optout) and SharedID (_pubcid_optout) report opt-outs. Cookies are read on every consented navigation, so an opt-out or changed ID is noticed while sources hold live UIDs; a cookie repeating the held UIDs is not rewritten, and lookups still respect due status. UID2 parses Prebid's stored-tokens JSON and stores only latestToken.advertising_token, never JSON verbatim. Provider-stated absolute expiry is clamped to 30 days after the request in validation. Adds LiveRamp envelope length tests around the 1024-byte UID limit. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
cf4983b to
1bc681f
Compare
|
@dhruv8sh, the same point as on #1487. Directionally, eight identity vendors as files in one shared crate is not compatible with V2 (the 55 pull requests tracked in #777, merged in order), where each vendor is a module in a folder of its own at The V2 spec is identifier-modules, with the folder layout in integrations-crate, both in #1342. The associated pull request is #1382 (a module hands core its identifiers, starting with the Prebid EID cookie). Could you evaluate the V2 spec and #1382 before progressing this pull request? |
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
…s server side UID2 refreshes its stored advertising token with the stored refresh token and decrypts the AES-256-GCM reply; an optout reply removes the token. SharedID generates a random UUID v4 into KV only, kept for 30 days, when no cookie or stored ID exists. LiveRamp, when given an endpoint, placement ID and Custom ID namespace, looks up a RampID envelope by Custom ID built from the stable EC ID; a 204 reply is an opt-out. Port the after-response collection, refresh-state, opt-out and rate_limit_per_hour framework changes, and update the vendor plans and operator docs. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
ts config validate, push and diff now build every enabled identity provider of a registered kind in both collection phases, so a provider-specific setting error (such as a liveramp endpoint without custom_id_namespace) fails the command before deploy. At runtime the same error disables every provider for each request. Partner tokens are replaced with placeholders for the check, as core deploy validation already does. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
…'s jurisdiction is GDPR Providers now name the sources they exclude where GDPR applies. For a visitor whose detected jurisdiction is GDPR, those sources are neither looked up nor stored, and the UIDs the row holds for them are removed. A TCF string alone no longer counts as GDPR applying. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me> # Conflicts: # crates/trusted-server-adapter-fastly/src/main.rs # crates/trusted-server-core/src/ec/coordinator/mod.rs # crates/trusted-server-core/src/ec/write_plan.rs # docs/guide/configuration.md # trusted-server.example.toml
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Also test merging cookie IDs with a failed, opted-out or found lookup. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Summary
Adds eight vendor identity providers and the coordinator hooks they need. Two opt-out handling defects and two UID2 refresh-lifecycle defects need correction before merging.
One inline comment carries a one-click GitHub
suggestion. Use Commit suggestion or Add suggestion to batch to apply it. The other fixes need manual changes across files or outside the diff.
Blocking
🔧 wrench
- UID2 SDK opt-outs leave cached tokens active — see inline at
crates/trusted-server-identity-providers/src/uid2.rs:347–349(suggestion). - LiveRamp's 204 opt-out never reaches its parser — see inline at
crates/trusted-server-identity-providers/src/liveramp.rs:265–267. - Overdue UID2 refresh credentials are permanently discarded — see the body-level finding below.
- A stale cookie rolls back a refreshed UID2 token — see inline at
crates/trusted-server-core/src/ec/coordinator/mod.rs:602–610.
Cross-cutting / body-level findings
🔧 wrench — Overdue UID2 refresh credentials are permanently discarded
References: crates/trusted-server-identity-providers/src/uid2.rs:173–176 and crates/trusted-server-core/src/ec/coordinator/validate.rs:151–155.
Two focused reproductions expose the same underlying problem:
- Importing a still-live identity after
refresh_fromdrops its refresh credentials, so subsequent navigations cannot refresh it. - If a refresh times out while the cookie repeats the stored token, validation drops the cookie's already-due refresh state. The cookie result survives the failed lookup and replaces the stored record without refresh credentials; future attempts stop.
Due refresh state must remain usable rather than becoming invalid merely because its scheduled time passed. The proposed validation change is:
Some(from) if usable => Ok(stored.with_refresh(KvRefresh {
state: refresh.state.into_owned(),
from,
})),Apply manually — the actual guard is outside a diff hunk; its documentation and existing test expectations also need updating. Retain the existing state-size checks and use the configured lookup rate limit to control retries. If the future-only policy is retained for lookup replies, distinguish cookie ingress instead of discarding its already-due credentials.
Verification
- Existing provider unit tests: 91 PASS locally.
- Five focused scratch reproductions confirmed the four findings on the reviewed head. The SDK opt-out regression passes with the suggestion applied in isolation.
- Exact suggestion bytes pass
cargo fmt --all -- --check, all eight clippy aliases,cargo check-fastly,cargo check-axum,cargo check-cloudflare, all four adapter test aliases,cargo test-fastly-reuse, and the cross-adapter parity suite. - The initial local Fastly attempt used Viceroy 0.17 and failed on an unsupported host import; rerunning with the repository-pinned Viceroy 0.21.0 passed.
- Byte-exact post-verification patch comparison: PASS. Scratch changes were discarded; the reviewer worktree is clean at the reviewed head.
Coverage: provider/coordinator implementations were read fully; large supporting files were reviewed through diffs and relevant context rather than exhaustively end-to-end. No live vendor service testing was performed. The prose fix sketches were not applied.
CI Status
- integration tests: PASS
- browser integration tests: PASS
- trace runtime and browser acceptance: PASS
- integration tests (Fastly EC lifecycle): PASS
- cargo test (axum native): PASS
- format-docs: PASS
- CI Node pin guard: PASS
- HTML EOF runtime (Fastly): PASS
- ts dev lint domains (changed lines): PASS
- cargo fmt: PASS
- prepare integration artifacts: PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- CLAUDE.md symlink guard: PASS
- worker-deadlines (spin): PASS
- docs-proposal scripts: PASS
- cargo test (cross-adapter parity): PASS
- format-typescript: PASS
- cargo test: PASS
- worker-deadlines (cloudflare): PASS
- vitest: PASS
No required checks were reported for this branch.
| let opted_out = self.cookies.any(cookies, |value| { | ||
| matches!(token_cookie(value), Some(TokenCookie::OptedOut)) | ||
| }); |
There was a problem hiding this comment.
🔧 wrench — UID2 SDK opt-outs leave cached tokens active
The UID2 SDK writes an opt-out identity as {"status":"optout",…} to its identity cookie (SDK implementation). This check recognizes only Prebid's latestToken: "optout" format. Parsing the SDK identity then fails because it has no advertising token, so a cached live token produces Skipped(NotDue) instead of being cleared. A leftover fallback cookie can also be accepted despite the SDK opt-out.
Check every duplicate of the SDK identity cookie before accepting any token:
| let opted_out = self.cookies.any(cookies, |value| { | |
| matches!(token_cookie(value), Some(TokenCookie::OptedOut)) | |
| }); | |
| let opted_out = cookies.get_all(&self.identity_cookie_name).any(|value| { | |
| serde_json::from_str::<serde_json::Value>(&percent_decoded(value)) | |
| .is_ok_and(|identity| identity["status"] == REFRESH_OPT_OUT) | |
| }) || self.cookies.any(cookies, |value| { | |
| matches!(token_cookie(value), Some(TokenCookie::OptedOut)) | |
| }); |
| if response.status == StatusCode::NO_CONTENT { | ||
| return Ok(LookupOutcome::OptedOut); | ||
| } |
There was a problem hiding this comment.
🔧 wrench — LiveRamp's 204 opt-out never reaches its parser
ProviderCoordinator::interpret intercepts both 404 and 204 as NoMatch before invoking this method. Through the actual coordinator, a LiveRamp 204 therefore produces NoMatch, not Skipped(OptedOut): the opt-out does not clear stored records or establish opt-out memory, and the provider can be called again on subsequent navigations. The direct parser test does not exercise this interception.
Delegate 204 responses to providers while retaining the ordinary no-content semantics of providers such as ID5:
// Coordinator: intercept only 404 here.
if response.status == http::StatusCode::NOT_FOUND {
return self.lookup(provider, LookupStatus::NoMatch, Vec::new());
}// ID5 parser: retain ordinary no-content semantics.
if response.status == http::StatusCode::NO_CONTENT {
return Ok(LookupOutcome::NoMatch);
}Apply manually — this changes multiple files, including coordinator lines outside the diff, and needs a coordinator-level regression test.
| if due.is_empty() && holds_same_uids(entry, &found, lookup.now) { | ||
| pending.early.push(skip(SkipReason::NotDue)); | ||
| continue; | ||
| } | ||
| pending.from_cookies.push(Tagged { | ||
| provider, | ||
| value: found, | ||
| }); | ||
| if due.is_empty() { |
There was a problem hiding this comment.
🔧 wrench — A stale cookie rolls back a refreshed UID2 token
A server-side UID2 refresh updates the row but does not rewrite __uid_2. On the following navigation, the older cookie token differs from the refreshed row token, so this path treats it as a changed identity and replaces the newer token and refresh credentials. This reproduces even when the older cookie's refresh_from is still in the future, independently of the overdue-refresh-state issue.
Add UID2-specific freshness reconciliation against current entries before accepting cookie results. For example, use the absolute identity expiry to retain a newer stored identity:
fn keep_stored_uid2(held: &KvUid, cookie: &KvUid, now: UnixSeconds) -> bool {
match (held.expiry(), cookie.expiry()) {
(Expiry::At(held_until), Expiry::At(cookie_until)) => {
held.is_live(now) && held_until > cookie_until
}
_ => false,
}
}Use that decision to retain the newer stored identity; explicit opt-outs must still take precedence.
Apply manually — this needs current-entry access in the UID2 cookie path, corresponding interface wiring, and lifecycle tests.
Stacked on #1487.
Summary
extPer vendor
/gs/v2fetch API with an RFC 3339tsandgpp_sidas an array; carrieslinkTypeand keepsext; auniversal_uidof0is an opt-outdid-distributor ID;segmentsonly when non-empty_lr_env, or the bare envelope Prebid stores inidl_env); with anendpoint,placement_idandcustom_id_namespace, a visitor without a cookie is looked up server side by Custom IDconnectIdfirst-party cookie; no server callendpoint, the token is refreshed server side before it expires_sharedid/_pubcidcookie as an atype 1 EID; honours_pubcid_optout; withgenerate = true(default off), generates a UUID stored only in the rowcto_bididcookie, stored verbatim as the atype 1 EID Prebid sendsTDIDfrom thepbjs-unifiedidcookie, withrtiPartnerext andadserver.orginserter/matcher, mm 4Consent and cookie handling
Known limitations
connectIdcookie is still read until it expires, and SharedID generation should be enabled only where the opt-out lives in the_pubcid_optoutcookie.The plans list the remaining unported reference behaviours.
Changes
crates/trusted-server-identity-providers/src/{id5,liveintent,liveramp,connectid,uid2,sharedid,criteo,unifiedid}.rscrates/trusted-server-identity-providers/src/common.rscrates/trusted-server-core/src/ec/provider.rs,ec/coordinator/crates/trusted-server-core/src/settings.rs,adapter-fastly/src/app.rstrusted-server.example.tomldocs/guide/configuration.md,docs/guide/api-reference.mddocs/superpowers/plans/2026-11-02-identity-provider-*.mdCloses
Part of #1246 and #1245. No issue is closed yet.
Spec
The spec is in #1487. This PR adds only plans, no new spec.
Test plan
cargo test-fastly && cargo test-fastly-reuse && cargo test-axum && cargo test-cloudflare && cargo test-spincargo fmt --all -- --checktemplate_build_digesttest and its clippy runscripts/lint-wasm-clock.sh./scripts/test-cli.sh, the node pin guard, docs-proposal scripts, andts dev lint domainsagainst the base branch/gs/v2fetch returned an ID that was stored in KV and attached to the auctionChecklist
unwrap()in production codelogmacros