Skip to content

Commit c7779e7

Browse files
Support Sourcepoint GPP consent for EC generation (#642)
1 parent 0c542e3 commit c7779e7

32 files changed

Lines changed: 2793 additions & 120 deletions

File tree

.github/actions/setup-integration-test-env/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ runs:
8080
env:
8181
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL: http://127.0.0.1:${{ inputs.origin-port }}
8282
TRUSTED_SERVER__PUBLISHER__PROXY_SECRET: integration-test-proxy-secret
83-
TRUSTED_SERVER__EC__PASSPHRASE: integration-test-ec-secret
83+
TRUSTED_SERVER__EC__PASSPHRASE: integration-test-ec-secret-padded-32
8484
TRUSTED_SERVER__PROXY__CERTIFICATE_CHECK: "false"
8585
run: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
8686

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
env:
5656
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL: http://127.0.0.1:8080
5757
TRUSTED_SERVER__PUBLISHER__PROXY_SECRET: integration-test-proxy-secret
58-
TRUSTED_SERVER__EC__PASSPHRASE: integration-test-ec-secret
58+
TRUSTED_SERVER__EC__PASSPHRASE: integration-test-ec-secret-padded-32
5959
TRUSTED_SERVER__PROXY__CERTIFICATE_CHECK: "false"
6060
run: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
6161

crates/integration-tests/fixtures/configs/viceroy-template.toml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,29 @@
2525
key = "placeholder"
2626
data = "placeholder"
2727

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

35+
[[local_server.kv_stores.ec_identity_store]]
36+
key = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.test02"
37+
data = '{"v":1,"created":1700000000,"consent":{"ok":true,"updated":1700000000},"geo":{"country":"US","region":"CA"}}'
38+
39+
[[local_server.kv_stores.ec_identity_store]]
40+
key = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.test03"
41+
data = '{"v":1,"created":1700000000,"consent":{"ok":true,"updated":1700000000},"geo":{"country":"US","region":"CA"}}'
42+
43+
[[local_server.kv_stores.ec_identity_store]]
44+
key = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd.test04"
45+
data = '{"v":1,"created":1700000000,"consent":{"ok":true,"updated":1700000000},"geo":{"country":"US","region":"CA"}}'
46+
47+
[[local_server.kv_stores.ec_identity_store]]
48+
key = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.test05"
49+
data = '{"v":1,"created":1700000000,"consent":{"ok":true,"updated":1700000000},"geo":{"country":"US","region":"CA"}}'
50+
3351
[[local_server.kv_stores.ec_partner_store]]
3452
key = "placeholder"
3553
data = "placeholder"

crates/integration-tests/tests/frameworks/scenarios.rs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -500,16 +500,17 @@ impl EcScenario {
500500
/// US Privacy signal that explicitly allows storage in the default Viceroy
501501
/// integration-test geo (US-CA).
502502
const ALLOW_US_PRIVACY_COOKIE: &str = "1YNN";
503-
const SEEDED_EC_ID: &str =
504-
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.test01";
505-
506503
fn allow_ec_generation(client: &EcTestClient) {
507504
client.set_cookie("us_privacy", ALLOW_US_PRIVACY_COOKIE);
508505
}
509506

510-
fn use_seeded_ec(client: &EcTestClient) -> String {
511-
client.set_cookie("ts-ec", SEEDED_EC_ID);
512-
normalize_ec_id(SEEDED_EC_ID)
507+
fn seeded_ec_id(hex_digit: char, suffix: &str) -> String {
508+
format!("{}.{suffix}", hex_digit.to_string().repeat(64))
509+
}
510+
511+
fn use_seeded_ec(client: &EcTestClient, ec_id: &str) -> String {
512+
client.set_cookie("ts-ec", ec_id);
513+
normalize_ec_id(ec_id)
513514
}
514515

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

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

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

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

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

711717
// Batch sync writes a UID for this EC ID (partner "inttest" is in config)

crates/js/lib/build-all.mjs

Lines changed: 154 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,22 @@
1313
* names to include in the bundle (e.g. "rubicon,appnexus,openx").
1414
* Each name must have a corresponding {name}BidAdapter.js module in
1515
* the prebid.js package. Default: "rubicon".
16+
*
17+
* TSJS_PREBID_USER_ID_MODULES — Ignored for production builds. User ID
18+
* modules are selected from src/integrations/prebid/user_id_modules.json
19+
* so attested bundles are deterministic. For local experiments only, use
20+
* TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE.
1621
*/
1722

23+
import crypto from 'node:crypto';
1824
import fs from 'node:fs';
25+
import { createRequire } from 'node:module';
1926
import path from 'node:path';
2027
import { fileURLToPath } from 'node:url';
2128
import { build } from 'vite';
2229

2330
const __dirname = path.dirname(fileURLToPath(import.meta.url));
31+
const require = createRequire(import.meta.url);
2432
const srcDir = path.resolve(__dirname, 'src');
2533
const distDir = path.resolve(__dirname, '..', 'dist');
2634
const integrationsDir = path.join(srcDir, 'integrations');
@@ -30,10 +38,27 @@ const integrationsDir = path.join(srcDir, 'integrations');
3038
// ---------------------------------------------------------------------------
3139

3240
const DEFAULT_PREBID_ADAPTERS = 'rubicon';
33-
const ADAPTERS_FILE = path.join(
41+
const ADAPTERS_FILE = path.join(integrationsDir, 'prebid', '_adapters.generated.ts');
42+
const USER_IDS_FILE = path.join(integrationsDir, 'prebid', '_user_ids.generated.ts');
43+
44+
const USER_ID_REGISTRY_FILE = path.join(integrationsDir, 'prebid', 'user_id_modules.json');
45+
const USER_IDS_MANIFEST_FILE = path.join(distDir, 'prebid-user-id-modules.json');
46+
const LIVE_INTENT_SHIM_ALIAS = 'prebid.js/modules/liveIntentIdSystem.js';
47+
const PREBID_PACKAGE_DIR = path.join(__dirname, 'node_modules', 'prebid.js');
48+
const PREBID_LIVE_INTENT_STANDARD = path.join(
49+
PREBID_PACKAGE_DIR,
50+
'dist',
51+
'src',
52+
'libraries',
53+
'liveIntentId',
54+
'idSystem.js'
55+
);
56+
const PREBID_GLOBAL_MODULE = path.join(PREBID_PACKAGE_DIR, 'dist', 'src', 'src', 'prebidGlobal.js');
57+
const LIVE_INTENT_SHIM = path.join(
3458
integrationsDir,
3559
'prebid',
36-
'_adapters.generated.ts',
60+
'prebid_modules',
61+
'liveIntentIdSystem.ts'
3762
);
3863

3964
/**
@@ -53,17 +78,12 @@ function generatePrebidAdapters() {
5378
if (names.length === 0) {
5479
console.warn(
5580
'[build-all] TSJS_PREBID_ADAPTERS is empty, falling back to default:',
56-
DEFAULT_PREBID_ADAPTERS,
81+
DEFAULT_PREBID_ADAPTERS
5782
);
5883
names.push(DEFAULT_PREBID_ADAPTERS);
5984
}
6085

61-
const modulesDir = path.join(
62-
__dirname,
63-
'node_modules',
64-
'prebid.js',
65-
'modules',
66-
);
86+
const modulesDir = path.join(__dirname, 'node_modules', 'prebid.js', 'modules');
6787

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

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

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

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

128+
function readUserIdRegistry() {
129+
return JSON.parse(fs.readFileSync(USER_ID_REGISTRY_FILE, 'utf8'));
130+
}
131+
132+
function requireExistingFile(filePath, description) {
133+
if (!fs.existsSync(filePath)) {
134+
throw new Error(`[build-all] Missing ${description}: ${filePath}`);
135+
}
136+
}
137+
138+
function prebidPackageVersion() {
139+
const packageJsonPath = path.join(PREBID_PACKAGE_DIR, 'package.json');
140+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
141+
return packageJson.version;
142+
}
143+
144+
function sourceToModuleMap(entries) {
145+
const map = {};
146+
for (const entry of entries) {
147+
for (const source of entry.eidSources ?? []) {
148+
map[source] = entry.moduleName;
149+
}
150+
}
151+
return map;
152+
}
153+
154+
function validateUserIdImport(entry) {
155+
requireExistingFile(LIVE_INTENT_SHIM, 'LiveIntent ESM shim');
156+
requireExistingFile(PREBID_LIVE_INTENT_STANDARD, 'Prebid LiveIntent standard ESM module');
157+
requireExistingFile(PREBID_GLOBAL_MODULE, 'Prebid global module');
158+
159+
if (entry.moduleName === 'liveIntentIdSystem') {
160+
return;
161+
}
162+
163+
try {
164+
require.resolve(entry.importPath, { paths: [__dirname] });
165+
} catch (error) {
166+
throw new Error(
167+
`[build-all] Required Prebid user ID module "${entry.moduleName}" could not be resolved from ${entry.importPath}: ${error.message}`
168+
);
169+
}
170+
}
171+
172+
/**
173+
* Generate `_user_ids.generated.ts` with deterministic User ID imports.
174+
*
175+
* Production builds intentionally ignore TSJS_PREBID_USER_ID_MODULES so the
176+
* attested JS artifact does not vary per publisher. A dev-only override exists
177+
* for local experiments and should not be used for trusted deployments.
178+
*/
179+
function generatePrebidUserIdModules() {
180+
const registry = readUserIdRegistry();
181+
const entriesByModule = new Map(registry.modules.map((entry) => [entry.moduleName, entry]));
182+
const override = process.env.TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE;
183+
const moduleNames = override
184+
? override
185+
.split(',')
186+
.map((s) => s.trim())
187+
.filter(Boolean)
188+
: registry.defaultPreset;
189+
190+
if (process.env.TSJS_PREBID_USER_ID_MODULES && !override) {
191+
console.warn(
192+
'[build-all] TSJS_PREBID_USER_ID_MODULES is ignored for deterministic attested builds. ' +
193+
'Use TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE only for local experiments.'
194+
);
195+
}
196+
197+
if (override) {
198+
console.warn(
199+
'[build-all] WARNING: using TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE. ' +
200+
'This changes the Prebid bundle and breaks production attestation assumptions.'
201+
);
202+
}
203+
204+
const selectedEntries = moduleNames.map((moduleName) => {
205+
const entry = entriesByModule.get(moduleName);
206+
if (!entry) {
207+
throw new Error(`[build-all] Unknown Prebid user ID module in preset: ${moduleName}`);
208+
}
209+
validateUserIdImport(entry);
210+
return entry;
211+
});
212+
213+
const imports = selectedEntries.map((entry) => `import '${entry.importPath}';`);
214+
215+
const content = [
216+
'// Auto-generated by build-all.mjs — manual edits will be overwritten at build time.',
217+
'//',
218+
'// Deterministic Prebid.js user ID module preset for attested builds.',
219+
'// TSJS_PREBID_USER_ID_MODULES is intentionally ignored in production builds.',
220+
'// Use TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE only for local experiments.',
221+
`// Modules: ${moduleNames.join(', ')}`,
222+
'',
223+
...imports,
224+
'',
225+
].join('\n');
226+
227+
fs.writeFileSync(USER_IDS_FILE, content);
228+
229+
const manifest = {
230+
prebidVersion: prebidPackageVersion(),
231+
deterministic: !override,
232+
modules: moduleNames,
233+
sourceToModule: sourceToModuleMap(registry.modules),
234+
generatedFileHash: crypto.createHash('sha256').update(content).digest('hex'),
235+
};
236+
237+
console.log('[build-all] Prebid user ID modules:', moduleNames);
238+
return manifest;
239+
}
240+
108241
generatePrebidAdapters();
242+
const prebidUserIdManifest = generatePrebidUserIdModules();
109243

110244
// ---------------------------------------------------------------------------
111245

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

116251
// Discover integration modules: directories in src/integrations/ with index.ts
117252
const integrationModules = fs.existsSync(integrationsDir)
@@ -120,8 +255,7 @@ const integrationModules = fs.existsSync(integrationsDir)
120255
.filter((name) => {
121256
const fullPath = path.join(integrationsDir, name);
122257
return (
123-
fs.statSync(fullPath).isDirectory() &&
124-
fs.existsSync(path.join(fullPath, 'index.ts'))
258+
fs.statSync(fullPath).isDirectory() && fs.existsSync(path.join(fullPath, 'index.ts'))
125259
);
126260
})
127261
.sort()
@@ -139,11 +273,15 @@ async function buildModule(name, entryPath) {
139273
root: __dirname,
140274
resolve: {
141275
alias: {
276+
[LIVE_INTENT_SHIM_ALIAS]: LIVE_INTENT_SHIM,
277+
'prebid.js/modules/liveIntentIdSystem': LIVE_INTENT_SHIM,
278+
'tsjs-prebid/liveIntentIdSystemStandard': PREBID_LIVE_INTENT_STANDARD,
279+
'tsjs-prebid/prebidGlobal': PREBID_GLOBAL_MODULE,
142280
// prebid.js doesn't expose src/adapterManager.js via its package
143281
// "exports" map, but we need it for client-side bidder validation.
144282
'prebid.js/src/adapterManager.js': path.resolve(
145283
__dirname,
146-
'node_modules/prebid.js/dist/src/src/adapterManager.js',
284+
'node_modules/prebid.js/dist/src/src/adapterManager.js'
147285
),
148286
},
149287
},
@@ -176,9 +314,7 @@ async function buildModule(name, entryPath) {
176314
await buildModule('core', path.join(srcDir, 'core', 'index.ts'));
177315

178316
await Promise.all(
179-
integrationModules.map((name) =>
180-
buildModule(name, path.join(integrationsDir, name, 'index.ts')),
181-
),
317+
integrationModules.map((name) => buildModule(name, path.join(integrationsDir, name, 'index.ts')))
182318
);
183319

184320
// List all built files

0 commit comments

Comments
 (0)