Skip to content

Align SharedID with Prebid's cookie name, source and retention - #1565

Closed
dhruv8sh wants to merge 69 commits into
feat/s2s-eid-vendorsfrom
fix/1488-sharedid-prebid-alignment
Closed

dhruv8sh wants to merge 69 commits into
feat/s2s-eid-vendorsfrom
fix/1488-sharedid-prebid-alignment

Conversation

@dhruv8sh

@dhruv8sh dhruv8sh commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Makes the SharedID provider follow Prebid's definition: it reads the cookie named in Prebid's sharedId module config, defaults its source to pubcid.org (atype 1), and keeps a stored SharedID for as long as the row exists.
  • Swaps core's blanket deploy-time rejection of enabled identity providers for the registered-kind check that ts config already runs. Core can't see the provider crate's kinds, so it refused every enabled provider at deploy.

Changes

File Change
crates/trusted-server-core/src/ec/provider.rs New IdentityProvider::keeps_stored_uids hook, defaulting to false. When it returns true, a live stored UID wins over a different cookie value.
crates/trusted-server-core/src/ec/coordinator/mod.rs Skips cookie IDs for sources that already hold a live UID when the provider keeps stored UIDs. Two tests cover it.
crates/trusted-server-core/src/settings.rs A sharedid provider with no sources claims pubcid.org. This uses serde(remote = "Self") so the default is applied once, at deserialization.
crates/trusted-server-identity-providers/src/sharedid.rs Cookie and generated IDs carry no expiry, so they last as long as the row. Turns on keeps_stored_uids. Updates the docs and tests.
crates/trusted-server-identity-providers/src/lib.rs Fills cookie_names from integrations.prebid.managed_user_ids[].storage.name for the sharedId/pubCommonId module when its storage type is a cookie and the provider doesn't set its own names. Adds a test that validate_providers refuses an unregistered kind.
crates/trusted-server-core/src/config.rs Removes reject_providers_without_a_kind. ts config builds each enabled provider and refuses unregistered kinds.
trusted-server.example.toml Updates the SharedID example to match.

Behaviour

  • With storage = { type = "cookie", name = "_pubcid" } on the Prebid sharedId module, only _pubcid is read. A cookie named sharedId is not.
  • A stored SharedID survives a request that carries a different cookie value. A cookie is taken only when the row holds no live SharedID.
  • The _pubcid_optout cookie still wins. Reading and storing still need EC consent plus EID consent (TCF Purposes 1 and 4), which the coordinator already enforces before reading cookies.

Not in this PR

  • ts config checks provider kinds but not the target adapter. Pushing an enabled provider to Axum, Cloudflare or Spin still fails at startup, as before.
  • LiveRamp's Custom ID mapping still needs a decision.

Closes

Follow-up into #1488 (base branch feat/s2s-eid-vendors). No linked issue.

Spec

Spec: implements review follow-ups to #1488, whose spec lives in that stack.

Test plan

  • cargo test-fastly && cargo test-axum (also cargo test-cloudflare, cargo test-spin)
  • cargo clippy-fastly && cargo clippy-axum (also clippy-cloudflare, clippy-cloudflare-wasm, clippy-spin-native, clippy-spin-wasm, clippy-cli, clippy-codegen)
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run (1838 passed, 1 skipped, on the pinned Node 24.20.0)
  • JS format: cd crates/trusted-server-js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: ./scripts/test-cli.sh; cargo test -p trusted-server-identity-providers (110 passed)

Checklist

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>
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>
…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>
…o large

Match Prebid's agent type for the resolved Trade Desk ID, and leave out a segments ext over MAX_EXT_BYTES instead of losing the whole nonId EID.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Treat a missing or zero ttl as 14 days, read a missing lastSynced as fresh, and accept the lowercase connectid key.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Correct the UID2, LiveRamp, ID5, ConnectID, LiveIntent and Criteo plans. Describe placeholder IDs as no ID rather than an opt-out, name the client settings that put tokens in cookies, and mark allowed_domains as required only with an endpoint.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Carry the request cookies through the after-response lookup path, require a consented row in the coordinator tests, and update the provider tests for the owner-scoped OwnedEntries view and the partner-aware after-response builder.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
The coordinator now reports only a 404 as no match before parsing. A 204 reaches the provider, so LiveRamp's 204 opt-out clears its sources and is remembered, while ID5 and UID2 map their own 204 to no match.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
LookupInput carries the coordinator's request time and RequestCookies carries it to cookie readers, so the UID2 and ConnectID expiry checks and ID5's ts field use the navigation's one clock reading. Provider tests now read cookies at a fixed time.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Validation dropped refresh credentials whose refresh time had passed, so a live UID2 identity imported after its refresh_from, or kept through a failed refresh, could never be refreshed again. Due state is now stored and the source stays due; rate_limit_per_hour bounds the retries.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
A server-side refresh updates the row but not the browser's cookies, so the next navigation's older __uid_2 token replaced the refreshed token and its refresh state. Providers can now ask the coordinator to keep a live stored UID whose absolute expiry outlives the cookie's; UID2 does. A cookie opt-out still wins, and a newer SDK token is still taken.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Cookie and consent-derived opt-outs no longer get the 30-day lockout: lookups now say whether they remember, keep or forget the provider's opt-out, and only an opt-out a vendor server reports is remembered. The vendor's cookies are read before a remembered opt-out is checked, so an ID in them forgets the opt-out and is stored. ID5's "0" stays an opt-out.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Validation already clamped absolute expiries; ProviderExpiry::After is now clamped to the same 30 days, so the bound no longer depends on each provider.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Generation reuses the generated UID the row still holds after it expires, so the ID stays stable for as long as the row lives rather than changing every 30 days.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
The unchanged-cookie and failed-refresh regressions now run dispatch, collect and plan_write for a 404, timeout, 502, invalid reply and rate limit, each with a no-cookie control, and assert that no write is planned.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
…ient

The fetch body now carries provider (provider_name, defaulting to the publisher domain), domain and origin like ID5's Prebid Server module, instead of o, v, tml and nbPage.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Prebid's Lockr AIM module does not run where GDPR applies, so a Lockr cookie on such a request is a leftover; cookie readers now ignore Lockr cookies there. The example tables use Lockr keys that differ from the vendors' own cookies, and the docs say the default Lockr keys are already read as own cookies.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
…pping file

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
LiveIntent now applies the shared usable-ID check, so a blank or 0 ID reads as no match. A LiveRamp envelope entry without a type no longer fails the whole reply.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Prebid's LiveIntent module sets the resolved Trade Desk ID's ext.provider to the page domain, falling back to liveintent.com, and copies the reply's m next to segments in the nonId EID ext. Carry the publisher domain to parse_response through the request context and build both ext values the same way.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
The coordinator drops sources a provider excludes where GDPR applies only when it writes the row, so a UID2 UID stored outside GDPR still reached bidders until the next committed write. Mark those sources in the partner registry for every configured provider and skip them in live_eids for a GDPR visitor.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Prebid's ID5 module sends the cached reply's signature as s so ID5 can return the same ID. Keep a non-empty reply signature as the UIDs' private refresh state, due a tenth of the TTL before they expire, and send it as s in the next fetch.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Read the cookie named by the Prebid sharedId module's storage.name, default the source to pubcid.org with atype 1, and keep a stored SharedID for as long as the row lives instead of letting a different cookie replace it after a day.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
…provider

Core cannot see the provider crate's kinds, so its deploy validation refused any enabled identity provider. ts config already builds every enabled provider, which refuses an unregistered kind; core now leaves that check to it.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
@dhruv8sh dhruv8sh self-assigned this Nov 12, 2026
@dhruv8sh dhruv8sh changed the title Handle SharedID the way Prebid defines it Align SharedID with Prebid's cookie name, source and retention Nov 12, 2026
@dhruv8sh
dhruv8sh force-pushed the feat/s2s-eid-vendors branch from aa66842 to 4fd9bd8 Compare November 12, 2026 14:11
@dhruv8sh
dhruv8sh marked this pull request as draft November 12, 2026 14:15
@dhruv8sh dhruv8sh closed this Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant