Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
188fd03
Add design spec for Sourcepoint GPP consent support (#640)
ChristianPavilonis Apr 15, 2026
b049897
Add implementation plan for Sourcepoint GPP consent support (#640)
ChristianPavilonis Apr 15, 2026
869e8cd
Add us_sale_opt_out field to GppConsent
ChristianPavilonis Apr 15, 2026
cb9e2b5
Decode US sale opt-out from GPP sections
ChristianPavilonis Apr 15, 2026
d226377
Recognize GPP US sale opt-out in EC consent gating
ChristianPavilonis Apr 15, 2026
5ba2bc5
Add Sourcepoint JS integration for GPP consent cookie mirroring
ChristianPavilonis Apr 15, 2026
945f39a
Add design spec for Prebid User ID Module support
ChristianPavilonis Apr 16, 2026
f71e55f
Add implementation plan for Prebid User ID Module support
ChristianPavilonis Apr 16, 2026
d19e888
Fix ESM path resolution in Prebid User ID plan regression guard
ChristianPavilonis Apr 16, 2026
7660ce8
Add Vitest coverage for Prebid ts-eids cookie sync
ChristianPavilonis Apr 16, 2026
fe2d065
Bundle Prebid User ID core and submodules in Prebid integration
ChristianPavilonis Apr 16, 2026
25194f0
Correct Prebid User ID plan + spec — drop pubCommonIdSystem (removed …
ChristianPavilonis Apr 16, 2026
6f5644e
Drop liveIntentIdSystem from Prebid bundle
ChristianPavilonis Apr 16, 2026
3cae39c
Make Prebid User ID submodule set configurable at build time
ChristianPavilonis Apr 16, 2026
b8dbf2e
Clear stale consent cookies and aggregate US GPP opt-outs
ChristianPavilonis Apr 16, 2026
00cc54b
Add Secure flag and Max-Age to Sourcepoint GPP cookies
ChristianPavilonis Apr 16, 2026
275e892
support ec partners map for env overrides
ChristianPavilonis Apr 17, 2026
be9a930
Scope Sourcepoint consent PR and address review feedback
ChristianPavilonis Apr 21, 2026
4980e77
Remove generated Prebid user ID shim
ChristianPavilonis May 1, 2026
2b4a610
Address Sourcepoint consent review feedback
ChristianPavilonis May 5, 2026
d0dd47b
Address Sourcepoint review feedback
ChristianPavilonis May 6, 2026
86b9453
Isolate EC lifecycle integration seeds
ChristianPavilonis May 7, 2026
3d5f2a2
Promote EC lifecycle logs to info
ChristianPavilonis May 8, 2026
6dee8ed
why
ChristianPavilonis May 8, 2026
35bca7e
Support Sourcepoint usnat consent storage
ChristianPavilonis May 8, 2026
985a7b5
Add build-time Prebid User ID module generation
ChristianPavilonis May 8, 2026
9c32370
Skip Prebid User ID modules that require require
ChristianPavilonis May 8, 2026
e3d45e3
added sequence diagram for eids in bidstream
ChristianPavilonis May 12, 2026
68d61bc
Remove EC seen domain visit counts
ChristianPavilonis May 12, 2026
5826317
Make Prebid user ID modules deterministic
ChristianPavilonis May 12, 2026
3ac249c
Document deterministic Prebid module rationale
ChristianPavilonis May 12, 2026
9fb8c1a
add batch batch-sync example script
ChristianPavilonis May 14, 2026
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
2 changes: 1 addition & 1 deletion .github/actions/setup-integration-test-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ runs:
env:
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL: http://127.0.0.1:${{ inputs.origin-port }}
TRUSTED_SERVER__PUBLISHER__PROXY_SECRET: integration-test-proxy-secret
TRUSTED_SERVER__EC__PASSPHRASE: integration-test-ec-secret
TRUSTED_SERVER__EC__PASSPHRASE: integration-test-ec-secret-padded-32
TRUSTED_SERVER__PROXY__CERTIFICATE_CHECK: "false"
run: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
env:
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL: http://127.0.0.1:8080
TRUSTED_SERVER__PUBLISHER__PROXY_SECRET: integration-test-proxy-secret
TRUSTED_SERVER__EC__PASSPHRASE: integration-test-ec-secret
TRUSTED_SERVER__EC__PASSPHRASE: integration-test-ec-secret-padded-32
TRUSTED_SERVER__PROXY__CERTIFICATE_CHECK: "false"
run: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1

Expand Down
20 changes: 19 additions & 1 deletion crates/integration-tests/fixtures/configs/viceroy-template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,29 @@
key = "placeholder"
data = "placeholder"

# Pre-seeded EC row for KV-backed EC lifecycle tests.
# Pre-seeded EC rows for KV-backed EC lifecycle tests. Each scenario
# uses a separate row so withdrawal tombstones do not leak across
# sequential scenario execution in the same Viceroy instance.
[[local_server.kv_stores.ec_identity_store]]
key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.test01"
data = '{"v":1,"created":1700000000,"consent":{"ok":true,"updated":1700000000},"geo":{"country":"US","region":"CA"}}'

[[local_server.kv_stores.ec_identity_store]]
key = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.test02"
data = '{"v":1,"created":1700000000,"consent":{"ok":true,"updated":1700000000},"geo":{"country":"US","region":"CA"}}'

[[local_server.kv_stores.ec_identity_store]]
key = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.test03"
data = '{"v":1,"created":1700000000,"consent":{"ok":true,"updated":1700000000},"geo":{"country":"US","region":"CA"}}'

[[local_server.kv_stores.ec_identity_store]]
key = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd.test04"
data = '{"v":1,"created":1700000000,"consent":{"ok":true,"updated":1700000000},"geo":{"country":"US","region":"CA"}}'

[[local_server.kv_stores.ec_identity_store]]
key = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.test05"
data = '{"v":1,"created":1700000000,"consent":{"ok":true,"updated":1700000000},"geo":{"country":"US","region":"CA"}}'

[[local_server.kv_stores.ec_partner_store]]
key = "placeholder"
data = "placeholder"
Expand Down
28 changes: 17 additions & 11 deletions crates/integration-tests/tests/frameworks/scenarios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,16 +500,17 @@ impl EcScenario {
/// US Privacy signal that explicitly allows storage in the default Viceroy
/// integration-test geo (US-CA).
const ALLOW_US_PRIVACY_COOKIE: &str = "1YNN";
const SEEDED_EC_ID: &str =
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.test01";

fn allow_ec_generation(client: &EcTestClient) {
client.set_cookie("us_privacy", ALLOW_US_PRIVACY_COOKIE);
}

fn use_seeded_ec(client: &EcTestClient) -> String {
client.set_cookie("ts-ec", SEEDED_EC_ID);
normalize_ec_id(SEEDED_EC_ID)
fn seeded_ec_id(hex_digit: char, suffix: &str) -> String {
format!("{}.{suffix}", hex_digit.to_string().repeat(64))
}

fn use_seeded_ec(client: &EcTestClient, ec_id: &str) -> String {
client.set_cookie("ts-ec", ec_id);
normalize_ec_id(ec_id)
}

/// Full lifecycle: seeded EC → batch sync → identify (Bearer auth) with scoped UID.
Expand All @@ -518,7 +519,8 @@ fn use_seeded_ec(client: &EcTestClient) -> String {
fn ec_full_lifecycle(base_url: &str) -> TestResult<()> {
let client = EcTestClient::new(base_url);
allow_ec_generation(&client);
let ec_id = use_seeded_ec(&client);
let seeded_ec_id = seeded_ec_id('a', "test01");
let ec_id = use_seeded_ec(&client, &seeded_ec_id);
log::info!("EC full lifecycle: using seeded EC ID = {ec_id}");

// 2. Batch sync writes partner UID (partner "inttest" is in config)
Expand Down Expand Up @@ -576,7 +578,8 @@ fn ec_full_lifecycle(base_url: &str) -> TestResult<()> {
fn ec_consent_withdrawal(base_url: &str) -> TestResult<()> {
let client = EcTestClient::new(base_url);
allow_ec_generation(&client);
let ec_id = use_seeded_ec(&client);
let seeded_ec_id = seeded_ec_id('b', "test02");
let ec_id = use_seeded_ec(&client, &seeded_ec_id);
log::info!("EC consent withdrawal: using seeded EC = {ec_id}");

// GPC overrides the allow cookie in US-CA, so this is an explicit
Expand Down Expand Up @@ -623,7 +626,8 @@ fn ec_identify_without_ec(base_url: &str) -> TestResult<()> {
fn ec_identify_consent_denied(base_url: &str) -> TestResult<()> {
let client = EcTestClient::new(base_url);
allow_ec_generation(&client);
let _ec_id = use_seeded_ec(&client);
let seeded_ec_id = seeded_ec_id('c', "test03");
let _ec_id = use_seeded_ec(&client, &seeded_ec_id);

// Identify with GPC=1 — in the default US-CA test geo, GPC is an explicit
// denial that must override the allow cookie. Per spec §11.4, consent is
Expand All @@ -647,7 +651,8 @@ fn ec_identify_consent_denied(base_url: &str) -> TestResult<()> {
fn ec_concurrent_partner_syncs(base_url: &str) -> TestResult<()> {
let client = EcTestClient::new(base_url);
allow_ec_generation(&client);
let ec_id = use_seeded_ec(&client);
let seeded_ec_id = seeded_ec_id('d', "test04");
let ec_id = use_seeded_ec(&client, &seeded_ec_id);
log::info!("EC concurrent syncs: using seeded EC = {ec_id}");

// Batch sync both partners (both are pre-configured in trusted-server.toml)
Expand Down Expand Up @@ -705,7 +710,8 @@ fn ec_concurrent_partner_syncs(base_url: &str) -> TestResult<()> {
fn ec_batch_sync_happy_path(base_url: &str) -> TestResult<()> {
let client = EcTestClient::new(base_url);
allow_ec_generation(&client);
let ec_id = use_seeded_ec(&client);
let seeded_ec_id = seeded_ec_id('e', "test05");
let ec_id = use_seeded_ec(&client, &seeded_ec_id);
log::info!("EC batch sync happy path: using seeded ec_id = {ec_id}");

// Batch sync writes a UID for this EC ID (partner "inttest" is in config)
Expand Down
172 changes: 154 additions & 18 deletions crates/js/lib/build-all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,22 @@
* names to include in the bundle (e.g. "rubicon,appnexus,openx").
* Each name must have a corresponding {name}BidAdapter.js module in
* the prebid.js package. Default: "rubicon".
*
* TSJS_PREBID_USER_ID_MODULES — Ignored for production builds. User ID
* modules are selected from src/integrations/prebid/user_id_modules.json
* so attested bundles are deterministic. For local experiments only, use
* TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE.
*/

import crypto from 'node:crypto';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { build } from 'vite';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const srcDir = path.resolve(__dirname, 'src');
const distDir = path.resolve(__dirname, '..', 'dist');
const integrationsDir = path.join(srcDir, 'integrations');
Expand All @@ -30,10 +38,27 @@ const integrationsDir = path.join(srcDir, 'integrations');
// ---------------------------------------------------------------------------

const DEFAULT_PREBID_ADAPTERS = 'rubicon';
const ADAPTERS_FILE = path.join(
const ADAPTERS_FILE = path.join(integrationsDir, 'prebid', '_adapters.generated.ts');
const USER_IDS_FILE = path.join(integrationsDir, 'prebid', '_user_ids.generated.ts');

const USER_ID_REGISTRY_FILE = path.join(integrationsDir, 'prebid', 'user_id_modules.json');
const USER_IDS_MANIFEST_FILE = path.join(distDir, 'prebid-user-id-modules.json');
const LIVE_INTENT_SHIM_ALIAS = 'prebid.js/modules/liveIntentIdSystem.js';
const PREBID_PACKAGE_DIR = path.join(__dirname, 'node_modules', 'prebid.js');
const PREBID_LIVE_INTENT_STANDARD = path.join(
PREBID_PACKAGE_DIR,
'dist',
'src',
'libraries',
'liveIntentId',
'idSystem.js'
);
const PREBID_GLOBAL_MODULE = path.join(PREBID_PACKAGE_DIR, 'dist', 'src', 'src', 'prebidGlobal.js');
const LIVE_INTENT_SHIM = path.join(
integrationsDir,
'prebid',
'_adapters.generated.ts',
'prebid_modules',
'liveIntentIdSystem.ts'
);

/**
Expand All @@ -53,17 +78,12 @@ function generatePrebidAdapters() {
if (names.length === 0) {
console.warn(
'[build-all] TSJS_PREBID_ADAPTERS is empty, falling back to default:',
DEFAULT_PREBID_ADAPTERS,
DEFAULT_PREBID_ADAPTERS
);
names.push(DEFAULT_PREBID_ADAPTERS);
}

const modulesDir = path.join(
__dirname,
'node_modules',
'prebid.js',
'modules',
);
const modulesDir = path.join(__dirname, 'node_modules', 'prebid.js', 'modules');

// Validate each adapter and build import lines
const imports = [];
Expand All @@ -72,7 +92,7 @@ function generatePrebidAdapters() {
const modulePath = path.join(modulesDir, moduleFile);
if (!fs.existsSync(modulePath)) {
console.error(
`[build-all] WARNING: Prebid adapter "${name}" not found (expected ${moduleFile}), skipping`,
`[build-all] WARNING: Prebid adapter "${name}" not found (expected ${moduleFile}), skipping`
);
continue;
}
Expand All @@ -81,7 +101,7 @@ function generatePrebidAdapters() {

if (imports.length === 0) {
console.error(
'[build-all] WARNING: No valid Prebid adapters found, bundle will have no client-side adapters',
'[build-all] WARNING: No valid Prebid adapters found, bundle will have no client-side adapters'
);
}

Expand All @@ -100,18 +120,133 @@ function generatePrebidAdapters() {
fs.writeFileSync(ADAPTERS_FILE, content);

const adapterNames = names.filter((name) =>
fs.existsSync(path.join(modulesDir, `${name}BidAdapter.js`)),
fs.existsSync(path.join(modulesDir, `${name}BidAdapter.js`))
);
console.log('[build-all] Prebid adapters:', adapterNames);
}

function readUserIdRegistry() {
return JSON.parse(fs.readFileSync(USER_ID_REGISTRY_FILE, 'utf8'));
}

function requireExistingFile(filePath, description) {
if (!fs.existsSync(filePath)) {
throw new Error(`[build-all] Missing ${description}: ${filePath}`);
}
}

function prebidPackageVersion() {
const packageJsonPath = path.join(PREBID_PACKAGE_DIR, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return packageJson.version;
}

function sourceToModuleMap(entries) {
const map = {};
for (const entry of entries) {
for (const source of entry.eidSources ?? []) {
map[source] = entry.moduleName;
}
}
return map;
}

function validateUserIdImport(entry) {
requireExistingFile(LIVE_INTENT_SHIM, 'LiveIntent ESM shim');
requireExistingFile(PREBID_LIVE_INTENT_STANDARD, 'Prebid LiveIntent standard ESM module');
requireExistingFile(PREBID_GLOBAL_MODULE, 'Prebid global module');

if (entry.moduleName === 'liveIntentIdSystem') {
return;
}

try {
require.resolve(entry.importPath, { paths: [__dirname] });
} catch (error) {
throw new Error(
`[build-all] Required Prebid user ID module "${entry.moduleName}" could not be resolved from ${entry.importPath}: ${error.message}`
);
}
}

/**
* Generate `_user_ids.generated.ts` with deterministic User ID imports.
*
* Production builds intentionally ignore TSJS_PREBID_USER_ID_MODULES so the
* attested JS artifact does not vary per publisher. A dev-only override exists
* for local experiments and should not be used for trusted deployments.
*/
function generatePrebidUserIdModules() {
const registry = readUserIdRegistry();
const entriesByModule = new Map(registry.modules.map((entry) => [entry.moduleName, entry]));
const override = process.env.TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE;
const moduleNames = override
? override
.split(',')
.map((s) => s.trim())
.filter(Boolean)
: registry.defaultPreset;

if (process.env.TSJS_PREBID_USER_ID_MODULES && !override) {
console.warn(
'[build-all] TSJS_PREBID_USER_ID_MODULES is ignored for deterministic attested builds. ' +
'Use TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE only for local experiments.'
);
}

if (override) {
console.warn(
'[build-all] WARNING: using TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE. ' +
'This changes the Prebid bundle and breaks production attestation assumptions.'
);
}

const selectedEntries = moduleNames.map((moduleName) => {
const entry = entriesByModule.get(moduleName);
if (!entry) {
throw new Error(`[build-all] Unknown Prebid user ID module in preset: ${moduleName}`);
}
validateUserIdImport(entry);
return entry;
});

const imports = selectedEntries.map((entry) => `import '${entry.importPath}';`);

const content = [
'// Auto-generated by build-all.mjs — manual edits will be overwritten at build time.',
'//',
'// Deterministic Prebid.js user ID module preset for attested builds.',
'// TSJS_PREBID_USER_ID_MODULES is intentionally ignored in production builds.',
'// Use TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE only for local experiments.',
`// Modules: ${moduleNames.join(', ')}`,
'',
...imports,
'',
].join('\n');

fs.writeFileSync(USER_IDS_FILE, content);

const manifest = {
prebidVersion: prebidPackageVersion(),
deterministic: !override,
modules: moduleNames,
sourceToModule: sourceToModuleMap(registry.modules),
generatedFileHash: crypto.createHash('sha256').update(content).digest('hex'),
};

console.log('[build-all] Prebid user ID modules:', moduleNames);
return manifest;
}

generatePrebidAdapters();
const prebidUserIdManifest = generatePrebidUserIdModules();

// ---------------------------------------------------------------------------

// Clean dist directory
fs.rmSync(distDir, { recursive: true, force: true });
fs.mkdirSync(distDir, { recursive: true });
fs.writeFileSync(USER_IDS_MANIFEST_FILE, `${JSON.stringify(prebidUserIdManifest, null, 2)}\n`);

// Discover integration modules: directories in src/integrations/ with index.ts
const integrationModules = fs.existsSync(integrationsDir)
Expand All @@ -120,8 +255,7 @@ const integrationModules = fs.existsSync(integrationsDir)
.filter((name) => {
const fullPath = path.join(integrationsDir, name);
return (
fs.statSync(fullPath).isDirectory() &&
fs.existsSync(path.join(fullPath, 'index.ts'))
fs.statSync(fullPath).isDirectory() && fs.existsSync(path.join(fullPath, 'index.ts'))
);
})
.sort()
Expand All @@ -139,11 +273,15 @@ async function buildModule(name, entryPath) {
root: __dirname,
resolve: {
alias: {
[LIVE_INTENT_SHIM_ALIAS]: LIVE_INTENT_SHIM,
'prebid.js/modules/liveIntentIdSystem': LIVE_INTENT_SHIM,
'tsjs-prebid/liveIntentIdSystemStandard': PREBID_LIVE_INTENT_STANDARD,
'tsjs-prebid/prebidGlobal': PREBID_GLOBAL_MODULE,
// prebid.js doesn't expose src/adapterManager.js via its package
// "exports" map, but we need it for client-side bidder validation.
'prebid.js/src/adapterManager.js': path.resolve(
__dirname,
'node_modules/prebid.js/dist/src/src/adapterManager.js',
'node_modules/prebid.js/dist/src/src/adapterManager.js'
),
},
},
Expand Down Expand Up @@ -176,9 +314,7 @@ async function buildModule(name, entryPath) {
await buildModule('core', path.join(srcDir, 'core', 'index.ts'));

await Promise.all(
integrationModules.map((name) =>
buildModule(name, path.join(integrationsDir, name, 'index.ts')),
),
integrationModules.map((name) => buildModule(name, path.join(integrationsDir, name, 'index.ts')))
);

// List all built files
Expand Down
Loading
Loading