Skip to content

Add managed LiveRamp RampID integration - #1054

Draft
prk-Jr wants to merge 29 commits into
mainfrom
issue-355-liveramp-integration
Draft

Add managed LiveRamp RampID integration#1054
prk-Jr wants to merge 29 commits into
mainfrom
issue-355-liveramp-integration

Conversation

@prk-Jr

@prk-Jr prk-Jr commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add managed LiveRamp RampID support to Prebid so operators can configure IdentityLink centrally while preserving publisher User ID modules.
  • Carry opaque liveramp.com EIDs through the existing auction, consent, edge-cookie, and EC/KV paths, with generated-artifact coverage for exact default TCF behavior.
  • Keep ATS Direct audience segments and server-side identity resolution out of scope; live validation remains credential-gated.

Status

Code complete; live LiveRamp validation pending IABTechLab/uid2-optout#385.

Changes

File Change
.cargo/config.toml Add host-target clippy aliases for the CLI and OpenRTB codegen crates.
CLAUDE.md Include the new host-target aliases in the documented CI gates.
crates/trusted-server-cli/src/prebid_bundle.rs Reject LiveRamp bundle configuration that explicitly omits identityLinkIdSystem.
crates/trusted-server-core/src/consent/mod.rs Cover source-agnostic EID consent gating with multiple identity sources.
crates/trusted-server-core/src/ec/prebid_eids.rs Verify opaque LiveRamp envelopes survive bounded edge-cookie persistence.
crates/trusted-server-core/src/integrations/prebid.rs Add typed LiveRamp configuration, validation, defaults, and browser injection.
crates/trusted-server-js/lib/build-prebid-external.mjs Bundle IdentityLink and tcfControl with generated Prebid artifacts.
crates/trusted-server-js/lib/src/integrations/prebid/index.ts Install and protect the operator-managed IdentityLink entry while preserving publisher entries.
crates/trusted-server-js/lib/test/build-prebid-external.test.mjs Cover generated bundle metadata and explicit IdentityLink selection.
crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts Cover ownership, queue ordering, API guards, EID transport, privacy, and idempotence.
crates/trusted-server-js/lib/test/integrations/prebid/user_id_modules.test.ts Cover the LiveRamp module mapping and default preset.
crates/trusted-server-js/lib/test/prebid-artifact-integration.test.mjs Exercise the real bundle and shim, including partial userSync updates and auction EID output.
crates/trusted-server-js/lib/test/prebid-consent-enforcement.test.mjs Verify Purpose 1, Purposes 3/4, and LiveRamp vendor consent independently against generated artifacts.
docs/guide/configuration.md Document LiveRamp settings, defaults, and validation constraints.
docs/guide/integrations/prebid.md Document setup, ownership limits, exact TCF defaults, network/storage effects, failure modes, and live validation.
docs/superpowers/plans/2026-08-21-liveramp-integration.md Record the reviewed implementation, correction, and verification plan.
docs/superpowers/specs/2026-08-21-liveramp-integration-design.md Specify architecture, consent behavior, security boundaries, tests, and excluded ATS Direct scope.
trusted-server.example.toml Add an example managed LiveRamp configuration.

Closes

Closes #355

Test plan

  • cargo test-fastly && cargo test-axum && cargo test-cloudflare && cargo test-spin
  • cargo clippy-fastly && cargo clippy-axum && cargo clippy-cloudflare && cargo clippy-cloudflare-wasm && cargo clippy-spin-native && cargo clippy-spin-wasm && cargo clippy-cli && cargo clippy-codegen
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run
  • JS build: cd crates/trusted-server-js/lib && node build-all.mjs
  • JS lint/format: cd crates/trusted-server-js/lib && npm run lint && npm run format
  • CLI tests: ./scripts/test-cli.sh
  • 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: generated Prebid artifact tests cover managed configuration and the independent TCF matrix.

Hardening note

This change does not add config-derived regular-expression or pattern compilation. Invalid enabled LiveRamp configuration is rejected by typed validation and covered by Rust and CLI regression tests.

Checklist

  • Changes follow CLAUDE.md conventions.
  • No new unwrap() calls were introduced in production code.
  • No direct stdout logging was introduced in production code.
  • New behavior has automated coverage.
  • No secrets, credentials, live Placement IDs, or RampID envelope values are committed.

Acceptance boundary

RampID identity envelopes traverse the existing Prebid auction path; ATS Direct audience segments are not passed by this implementation.

@prk-Jr prk-Jr self-assigned this Aug 21, 2026
prk-Jr and others added 8 commits August 21, 2026 16:57
The bundle imported consentManagementTcf, consentManagementGpp and
consentManagementUsp, which only retrieve the consent signal. The activity
controls that act on it live in Prebid's tcfControl module, which was not
bundled, so a denied purpose changed nothing.

Reproduced with the real bundle and shim in JSDOM against a CMP stub denying
Purpose 1 and vendor 97: identityLink still issued a credentialed GET to
LiveRamp's envelope endpoint, still wrote idl_env and its bookkeeping cookies,
and the envelope still reached the ts-eids cookie. The run was identical to the
consent-granted one. Server-side gating stops EID forwarding only, not the
browser's collection and storage.

Import tcfControl alongside the consent collectors and lock the behavior with a
regression test that evaluates both production artifacts: denied purposes must
produce no vendor call and no storage, granted purposes must still resolve.

Note the scope: tcfControl enforces for every bidder and User ID module in the
bundle, not only LiveRamp. Regenerating a bundle is a manual step, so this
reaches a publisher only when `ts prebid bundle` is re-run.
`[integrations.prebid.liveramp]` combined with a pinned
`bundle.user_id_modules` list that lacks identityLinkIdSystem is valid config
that builds a bundle unable to serve it. The managed identityLink entry the
server injects has no submodule to drive, and the only signal is a browser-side
diagnostic.

Fail in `ts prebid bundle`, which already reads both keys from the same file. An
absent list still takes the generator's default preset, which includes the
module.
Three fixes to the setConfig and mergeConfig guards:

Skip seeding when getConfig is missing. `hasPrebidJsApi` only requires
registerBidAdapter, so getConfig can be absent. The previous fallback read an
empty entry list and then seeded the managed entry alone, silently dropping
every publisher module already configured. Log and leave the wrappers installed
instead, so a later publisher userIds call still gets the managed entry.

Build the managed entry per call. Prebid retains what it receives as
`submodule.config` for the life of the page, so one shared instance let any
mutation there leak into every later normalization.

Keep the normalization guard but report the error. Publisher configuration is
arbitrary page data and a throwing accessor must not break the publisher's own
setConfig call, so the guard stays; it now logs what failed instead of
discarding it. The redundant hasUserIdsPath check is dropped since
withManagedLiveRampUserId already performs it.
merge_auction_eids_deduplicates_liveramp_and_prefers_resolved_metadata was
merge_auction_eids_prefers_server_resolved_metadata_on_conflict with the source
string changed. merge_auction_eids is source-agnostic, so it added no coverage.

gate_eids_by_consent is all-or-nothing across sources. Rename its test and give
it a second source so the name no longer implies per-source gating that does not
exist.
The inline closure that mapped PrebidLiveRampConfig into its camel-cased
injection struct pushed rustfmt into indenting the whole payload literal an
extra level. A From implementation keeps the mapping next to the struct it
builds and restores the surrounding formatting.
The degraded-behavior table said only that no EID is forwarded when consent is
absent, which read as though nothing happened. Separate the TCF case, where
tcfControl now blocks resolution outright, from the US-state case, where the
server's forwarding gate is what applies.

Add the browser-side footprint the previous text omitted: the credentialed
request the page makes to LiveRamp's envelope endpoint, that Trusted Server does
not proxy it, the connect-src allowance a strict CSP needs, and every storage
entry IdentityLink writes. Note that the submodule prefers an ATS library
already on the page, and that this is therefore not a first-party replacement
for LiveRamp recognition.

Also record that the environment override cannot introduce the subsection, that
`ts prebid bundle` rejects a module list without identityLinkIdSystem, that
regenerating a bundle now enforces TCF for every bidder and User ID module, and
that KV persistence needs a matching [[ec.partners]] entry. Correct the design
document's consent section, which assumed the consent-management modules were
themselves the enforcement point, and flag the remaining GPP gap.
prk-Jr added 7 commits August 24, 2026 14:05
CI lints trusted-server-cli and trusted-server-openrtb-codegen explicitly
because no adapter-scoped alias covers them, but both invocations pin
x86_64-unknown-linux-gnu. There was no command a developer could run locally to
reproduce them, and CLAUDE.md's CI gate list named only the six adapter aliases,
so running the documented gates locally passed while CI failed on a
clippy::doc_markdown error in the CLI.

Add clippy-cli and clippy-codegen. Both omit --target so they build for the
host, and record them in the gate list.
CodeQL flagged js/incomplete-url-substring-sanitization on both assertions.
These are not sanitizers — the URLs come from the test's own fetch spy — so the
alert is not a vulnerability, but the substring test is still too loose for an
assertion: it would also match an unrelated host carrying api.rlcdn.com in its
name or query string, letting the granted-consent case count the wrong request.

Compare the parsed hostname instead. Verified the test is still non-vacuous:
dropping the tcfControl import fails two of its three cases.
@jwrosewell

Copy link
Copy Markdown
Contributor

Flagging a design-guidance conflict before this goes further, with a concrete way to resolve it.

This PR puts a named vendor into trusted-server-core. In the current diff, crates/trusted-server-core/src/integrations/prebid.rs gains default_liveramp_expires_days(), default_liveramp_refresh_in_seconds(), and validate_liveramp_placement_id(); crates/trusted-server-core/src/consent/mod.rs and crates/trusted-server-core/src/ec/prebid_eids.rs gain liveramp.com handling in the consent and Edge Cookie identity paths.

That runs against the project's own provider guidance. The pluggable providers design spec (docs/superpowers/specs/2026-07-30-pluggable-providers-design.md, on specs/providers-and-permissions) states as a goal that "a deployment picks an implementation per concern (including none) without a code change to Trusted Server core", and as a non-goal that "no vendor provider ships in this epic beyond the host-platform implementations named below". The same principle is what #838 was asked to satisfy: vendor implementations live in their own crates and are injected by the adapter, so core never names a vendor.

The awkward part is the sequencing. The seam that would host this cleanly is sitting reviewed-ready in #1043 (the pluggable Edge Cookie provider seam) and #1045 (the permission model), which restructure the very files this PR edits: consent/mod.rs, ec/prebid_eids.rs, and the EC identity path. Whichever lands second pays for the collision.

Suggested way forward, which I think helps this PR rather than blocks it:

  1. Merge Add a pluggable Edge Cookie provider seam with the built-in HMAC provider #1043 first. It is green, mergeable, and adds [ec] provider with [ec.providers.<key>] configuration blocks plus adapter injection.
  2. Re-express the RampID work as a vendor provider in its own crate, selected by configuration, with the liveramp.com specifics out of trusted-server-core. The EID plumbing that is genuinely generic (opaque envelope pass-through, partner registry handling) stays in core, vendor-neutral.
  3. That leaves operators able to enable RampID by configuration alone, and leaves core free of any single vendor's naming, which is the outcome the spec asks for.

Happy to do the adaptation work myself if that is useful: the provider seam is mine and I can put up the RampID-as-provider version against #1043 quickly.

Raised with AI assistance under my direction; the file and line references above were checked against the current diff.

…ules

`trusted-server-core` named a single identity vendor: `PrebidLiveRampConfig`,
`PrebidLiveRampStorageType`, `validate_liveramp_placement_id`, and defaults
encoding LiveRamp's own recommendations. Which identity vendor a deployment
uses is an operator configuration choice, so core should not carry it.

Replace `[integrations.prebid.liveramp]` with an array of opaque entries:

    [[integrations.prebid.managed_user_ids]]
    name = "identityLink"
    params = { pid = "999", notUse3P = false }

    [integrations.prebid.managed_user_ids.storage]
    type = "cookie"
    name = "idl_env"

Core validates only what Prebid.js needs to address a module — a usable entry
name and storage key, positive expiry and refresh, unique names — and forwards
`params` without inspecting them. Values that were typed defaults in core
(`idl_env`, 15 days, 1800 seconds) are now operator-supplied, because each is a
property of the selected module rather than of Trusted Server. `expires` gains
no upper bound for the same reason.

The browser shim follows: `withManagedUserIds` keys on each configured name
instead of a hardcoded `identityLink`, so any Prebid User ID module works
without a code change. Entries are deep-cloned rather than spread, since
`params` now accepts nested tables and Prebid retains what it receives as
`submodule.config` for the life of the page.

Also bundle Prebid's `tcfControl` alongside the `consentManagement*` modules.
Without it a TC string that denies a purpose changed nothing: User ID
submodules still wrote storage and still called their vendor endpoints.

Drop the `ts prebid bundle` guard that cross-checked `[...liveramp]` against
`identityLinkIdSystem`. Pairing a managed entry with its bundle module is the
operator's responsibility; a name-to-module table in the CLI would be vendor
knowledge that duplicates the registry the bundle generator already reads and
would go stale against newly released Prebid modules. Documented as unvalidated.

Verified end to end in a browser against a running adapter: config reaches
`window.__tsjs_prebid.managedUserIds` verbatim, the shim installs and defends
the entry against publisher `setConfig`/`mergeConfig` overrides while
preserving unrelated entries, and an envelope flows opaquely through
`getUserIdsAsEids` to OpenRTB `user.ext.eids` and the `ts-eids` cookie.
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.

Investigate and document LiveRamp integration

3 participants