Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ee70600
Add EC module with lifecycle management, consent gating, and config m…
ChristianPavilonis Mar 25, 2026
b6a6edf
Add KV identity graph with CAS concurrency control
ChristianPavilonis Mar 25, 2026
c9cafaf
Add partner registry and admin registration endpoint
ChristianPavilonis Mar 25, 2026
a4b75b4
Fix 8 EC spec deviations identified in branch audit
ChristianPavilonis Mar 26, 2026
6a4cc2e
Migrate admin endpoints to /_ts/admin namespace
ChristianPavilonis Apr 2, 2026
28a23ae
Add design spec for Sourcepoint GPP consent support (#640)
ChristianPavilonis Apr 15, 2026
2f90d22
Add implementation plan for Sourcepoint GPP consent support (#640)
ChristianPavilonis Apr 15, 2026
d5797b5
Add us_sale_opt_out field to GppConsent
ChristianPavilonis Apr 15, 2026
f5475a0
Decode US sale opt-out from GPP sections
ChristianPavilonis Apr 15, 2026
e899df7
Recognize GPP US sale opt-out in EC consent gating
ChristianPavilonis Apr 15, 2026
595b95f
Add Sourcepoint JS integration for GPP consent cookie mirroring
ChristianPavilonis Apr 15, 2026
413f7e3
tmp fix for toml
ChristianPavilonis Apr 16, 2026
20dfa2e
Add design spec for Prebid User ID Module support
ChristianPavilonis Apr 16, 2026
a491610
Add implementation plan for Prebid User ID Module support
ChristianPavilonis Apr 16, 2026
5e65466
Fix ESM path resolution in Prebid User ID plan regression guard
ChristianPavilonis Apr 16, 2026
34bb026
Add Vitest coverage for Prebid ts-eids cookie sync
ChristianPavilonis Apr 16, 2026
1947595
Bundle Prebid User ID core and submodules in Prebid integration
ChristianPavilonis Apr 16, 2026
99b4e47
Correct Prebid User ID plan + spec — drop pubCommonIdSystem (removed …
ChristianPavilonis Apr 16, 2026
fda4017
Drop liveIntentIdSystem from Prebid bundle
ChristianPavilonis Apr 16, 2026
5a0588d
revert toml change
ChristianPavilonis Apr 16, 2026
64f55a4
Make Prebid User ID submodule set configurable at build time
ChristianPavilonis Apr 16, 2026
e09b956
Clear stale consent cookies and aggregate US GPP opt-outs
ChristianPavilonis Apr 16, 2026
d423ce6
Add Secure flag and Max-Age to Sourcepoint GPP cookies
ChristianPavilonis Apr 16, 2026
aa025e9
Revert accidental proxy_secret change in trusted-server.toml
ChristianPavilonis Apr 16, 2026
3255a8f
Remove orphaned sync_pixel.rs (deleted in base branch)
ChristianPavilonis Apr 16, 2026
a8b5b1c
Fix leftover conflict markers from rebase — restore base branch versions
ChristianPavilonis Apr 16, 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
132 changes: 132 additions & 0 deletions crates/js/lib/build-all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
* 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_IDS — Comma-separated list of Prebid.js User ID
* submodule filenames (no `.js` extension) to include in the bundle
* (e.g. "sharedIdSystem,id5IdSystem,criteoIdSystem"). The `userId.js`
* core module is always included and is not configurable here.
* Default: the full ship-set of 13 submodules (see
* DEFAULT_PREBID_USER_IDS below).
*/

import fs from 'node:fs';
Expand Down Expand Up @@ -107,6 +113,132 @@ function generatePrebidAdapters() {

generatePrebidAdapters();

// ---------------------------------------------------------------------------
// Prebid User ID submodule generation
// ---------------------------------------------------------------------------

/**
* Default set of Prebid User ID submodules bundled when TSJS_PREBID_USER_IDS
* is unset. Matches the set originally hardcoded in index.ts when User ID
* support first shipped. `userId.js` (the core module) is imported
* unconditionally by index.ts and is not in this list.
*/
const DEFAULT_PREBID_USER_IDS = [
'sharedIdSystem',
'criteoIdSystem',
'33acrossIdSystem',
'pubProvidedIdSystem',
'quantcastIdSystem',
'id5IdSystem',
'identityLinkIdSystem',
'uid2IdSystem',
'euidIdSystem',
'intentIqIdSystem',
'lotamePanoramaIdSystem',
'connectIdSystem',
'merkleIdSystem',
].join(',');

/**
* Modules known to be incompatible with the current esbuild pipeline.
*
* `liveIntentIdSystem` uses a dynamic `require()` inside a build-flag-guarded
* branch that Prebid's own gulp pipeline dead-codes via constant folding.
* esbuild leaves the `require()` in the bundle, which throws
* `ReferenceError: require is not defined` at browser runtime. See spec
* follow-up #4 in `docs/superpowers/specs/2026-04-16-prebid-user-id-module-design.md`.
*/
const PREBID_USER_ID_DENYLIST = new Set(['liveIntentIdSystem']);

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

/**
* Generate `_user_ids.generated.ts` with import statements for each User ID
* submodule listed in the TSJS_PREBID_USER_IDS environment variable.
*
* Invalid submodule names (those without a matching module in prebid.js) or
* known-broken modules in the denylist are logged and skipped.
*/
function generatePrebidUserIds() {
const raw = process.env.TSJS_PREBID_USER_IDS || DEFAULT_PREBID_USER_IDS;
const names = raw
.split(',')
.map((s) => s.trim())
.filter(Boolean);

if (names.length === 0) {
console.warn(
'[build-all] TSJS_PREBID_USER_IDS is empty, falling back to default set',
);
names.push(...DEFAULT_PREBID_USER_IDS.split(','));
}

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

const imports = [];
const includedNames = [];
for (const name of names) {
if (PREBID_USER_ID_DENYLIST.has(name)) {
console.error(
`[build-all] WARNING: Prebid User ID submodule "${name}" is on the ` +
`esbuild-incompatibility denylist and will not be bundled. See ` +
`docs/superpowers/specs/2026-04-16-prebid-user-id-module-design.md ` +
`follow-up #4.`,
);
continue;
}
const moduleFile = `${name}.js`;
const modulePath = path.join(modulesDir, moduleFile);
// Some modules ship as .ts in modules/ but resolve via the exports map
// to dist/src/public/*.js (e.g. sharedIdSystem). Accept either form.
const distPath = path.join(__dirname, 'node_modules', 'prebid.js', 'dist', 'src', 'public', moduleFile);
if (!fs.existsSync(modulePath) && !fs.existsSync(distPath)) {
console.error(
`[build-all] WARNING: Prebid User ID submodule "${name}" not found (expected ${moduleFile}), skipping`,
);
continue;
}
imports.push(`import 'prebid.js/modules/${moduleFile}';`);
includedNames.push(name);
}

if (imports.length === 0) {
console.error(
'[build-all] WARNING: No valid Prebid User ID submodules found, ' +
'bundle will resolve no EIDs even if publisher configures userSync.userIds',
);
}

const content = [
'// Auto-generated by build-all.mjs — manual edits will be overwritten at build time.',
'//',
'// Controls which Prebid.js User ID submodules are included in the bundle.',
'// Set the TSJS_PREBID_USER_IDS environment variable to a comma-separated',
'// list of submodule filenames without the `.js` extension',
'// (e.g. "sharedIdSystem,id5IdSystem,criteoIdSystem") before building.',
'// The userId.js core module is always included via a static import in',
'// index.ts and is not configurable here.',
'',
...imports,
'',
].join('\n');

fs.writeFileSync(USER_IDS_FILE, content);

console.log('[build-all] Prebid User ID submodules:', includedNames);
}

generatePrebidUserIds();

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

// Clean dist directory
Expand Down
13 changes: 6 additions & 7 deletions crates/js/lib/src/core/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@ import NORMALIZE_CSS from './styles/normalize.css?inline';
import IFRAME_TEMPLATE from './templates/iframe.html?raw';

// Sandbox permissions granted to creative iframes.
// Ad creatives routinely contain scripts for tracking, click handling, and
// viewability measurement, so allow-scripts and allow-same-origin are required
// for creatives to render correctly. Server-side sanitization is the primary
// defense against malicious markup; the sandbox provides defense-in-depth.
// Notably absent:
// allow-scripts, allow-same-origin — prevent JS execution and same-origin
// access, which are the primary attack vectors for malicious creatives.
// allow-forms — server-side sanitization strips <form> elements, so form
// submission from creatives is not a supported use case. Omitting this token
// is consistent with that server-side policy and reduces the attack surface.
const CREATIVE_SANDBOX_TOKENS = [
'allow-forms',
'allow-popups',
'allow-popups-to-escape-sandbox',
'allow-same-origin',
'allow-scripts',
'allow-top-navigation-by-user-activation',
] as const;

Expand Down
22 changes: 22 additions & 0 deletions crates/js/lib/src/integrations/prebid/_user_ids.generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Auto-generated by build-all.mjs — manual edits will be overwritten at build time.
//
// Controls which Prebid.js User ID submodules are included in the bundle.
// Set the TSJS_PREBID_USER_IDS environment variable to a comma-separated
// list of submodule filenames without the `.js` extension
// (e.g. "sharedIdSystem,id5IdSystem,criteoIdSystem") before building.
// The userId.js core module is always included via a static import in
// index.ts and is not configurable here.

import 'prebid.js/modules/sharedIdSystem.js';
import 'prebid.js/modules/criteoIdSystem.js';
import 'prebid.js/modules/33acrossIdSystem.js';
import 'prebid.js/modules/pubProvidedIdSystem.js';
import 'prebid.js/modules/quantcastIdSystem.js';
import 'prebid.js/modules/id5IdSystem.js';
import 'prebid.js/modules/identityLinkIdSystem.js';
import 'prebid.js/modules/uid2IdSystem.js';
import 'prebid.js/modules/euidIdSystem.js';
import 'prebid.js/modules/intentIqIdSystem.js';
import 'prebid.js/modules/lotamePanoramaIdSystem.js';
import 'prebid.js/modules/connectIdSystem.js';
import 'prebid.js/modules/merkleIdSystem.js';
23 changes: 21 additions & 2 deletions crates/js/lib/src/integrations/prebid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ import 'prebid.js/modules/consentManagementTcf.js';
import 'prebid.js/modules/consentManagementGpp.js';
import 'prebid.js/modules/consentManagementUsp.js';

// Prebid User ID Module core — always bundled. Exposes
// `pbjs.getUserIdsAsEids` and registers the submodule machinery that each
// ID submodule in `_user_ids.generated.ts` hooks into. ID submodules
// activate only when the publisher's origin-side `pbjs.setConfig({
// userSync: { userIds: [...] } })` call runs during `processQueue()`.
import 'prebid.js/modules/userId.js';

// Prebid User ID submodules — self-register with the core on import.
// The set of submodules is controlled by the TSJS_PREBID_USER_IDS env var
// at build time. See _user_ids.generated.ts (written by build-all.mjs).
import './_user_ids.generated';

// Client-side bid adapters — self-register with prebid.js on import.
// The set of adapters is controlled by the TSJS_PREBID_ADAPTERS env var at
// build time. See _adapters.generated.ts (written by build-all.mjs).
Expand Down Expand Up @@ -355,6 +367,10 @@ const EID_COOKIE_NAME = 'ts-eids';
/** Cookie max-age in seconds (1 day). */
const EID_COOKIE_MAX_AGE = 86400;

function clearPrebidEidsCookie(): void {
document.cookie = `${EID_COOKIE_NAME}=; Path=/; Secure; SameSite=Lax; Max-Age=0`;
}

interface PrebidEid {
source: string;
uids?: Array<{ id: string; atype?: number }>;
Expand All @@ -373,11 +389,13 @@ interface FlatEid {
function syncPrebidEidsCookie(): void {
try {
if (typeof pbjs.getUserIdsAsEids !== 'function') {
clearPrebidEidsCookie();
return;
}

const rawEids: PrebidEid[] = pbjs.getUserIdsAsEids() ?? [];
if (rawEids.length === 0) {
clearPrebidEidsCookie();
return;
}

Expand All @@ -395,6 +413,7 @@ function syncPrebidEidsCookie(): void {
}

if (flat.length === 0) {
clearPrebidEidsCookie();
return;
}

Expand All @@ -407,11 +426,11 @@ function syncPrebidEidsCookie(): void {
}

if (encoded.length > MAX_EID_COOKIE_BYTES) {
clearPrebidEidsCookie();
return; // Single EID too large — skip.
}

document.cookie =
`${EID_COOKIE_NAME}=${encoded}; Path=/; Secure; SameSite=Lax; Max-Age=${EID_COOKIE_MAX_AGE}`;
document.cookie = `${EID_COOKIE_NAME}=${encoded}; Path=/; Secure; SameSite=Lax; Max-Age=${EID_COOKIE_MAX_AGE}`;

log.debug(`[tsjs-prebid] synced ${payload.length} EIDs to cookie`);
} catch (err) {
Expand Down
93 changes: 93 additions & 0 deletions crates/js/lib/src/integrations/sourcepoint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { log } from '../../core/log';

const SP_CONSENT_PREFIX = '_sp_user_consent_';
const GPP_COOKIE_NAME = '__gpp';
const GPP_SID_COOKIE_NAME = '__gpp_sid';

interface SourcepointGppData {
gppString: string;
applicableSections: number[];
}

interface SourcepointConsentPayload {
gppData?: SourcepointGppData;
}

function findSourcepointConsent(): SourcepointConsentPayload | null {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key?.startsWith(SP_CONSENT_PREFIX)) continue;

const raw = localStorage.getItem(key);
if (!raw) continue;

try {
const payload = JSON.parse(raw) as SourcepointConsentPayload;
if (payload.gppData?.gppString) {
return payload;
}
} catch {
log.debug('sourcepoint: failed to parse localStorage value', { key });
}
}
return null;
}

const GPP_COOKIE_MAX_AGE = 86400;

function writeCookie(name: string, value: string): void {
document.cookie = `${name}=${value}; path=/; Secure; SameSite=Lax; Max-Age=${GPP_COOKIE_MAX_AGE}`;
}

function clearCookie(name: string): void {
document.cookie = `${name}=; path=/; Secure; SameSite=Lax; Max-Age=0`;
}

/**
* Reads Sourcepoint consent from localStorage and mirrors it into
* `__gpp` and `__gpp_sid` cookies for Trusted Server to read.
*
* Returns `true` if cookies were written, `false` otherwise.
*/
export function mirrorSourcepointConsent(): boolean {
if (typeof localStorage === 'undefined' || typeof document === 'undefined') {
return false;
}

const payload = findSourcepointConsent();
if (!payload?.gppData) {
clearCookie(GPP_COOKIE_NAME);
clearCookie(GPP_SID_COOKIE_NAME);
log.debug('sourcepoint: no GPP data found in localStorage');
return false;
}

const { gppString, applicableSections } = payload.gppData;
if (!gppString) {
clearCookie(GPP_COOKIE_NAME);
clearCookie(GPP_SID_COOKIE_NAME);
log.debug('sourcepoint: gppString is empty');
return false;
}

writeCookie(GPP_COOKIE_NAME, gppString);

if (Array.isArray(applicableSections) && applicableSections.length > 0) {
writeCookie(GPP_SID_COOKIE_NAME, applicableSections.join(','));
} else {
clearCookie(GPP_SID_COOKIE_NAME);
}

log.info('sourcepoint: mirrored GPP consent to cookies', {
gppLength: gppString.length,
sections: applicableSections,
});

return true;
}

if (typeof window !== 'undefined') {
mirrorSourcepointConsent();
}

export default mirrorSourcepointConsent;
6 changes: 3 additions & 3 deletions crates/js/lib/test/core/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ describe('render', () => {
expect(iframe.srcdoc).toContain('<span>ad</span>');
expect(div.querySelector('iframe')).toBe(iframe);
const sandbox = iframe.getAttribute('sandbox') ?? '';
expect(sandbox).toContain('allow-forms');
expect(sandbox).not.toContain('allow-forms');
expect(sandbox).toContain('allow-popups');
expect(sandbox).toContain('allow-popups-to-escape-sandbox');
expect(sandbox).toContain('allow-top-navigation-by-user-activation');
expect(sandbox).toContain('allow-same-origin');
expect(sandbox).toContain('allow-scripts');
expect(sandbox).not.toContain('allow-same-origin');
expect(sandbox).not.toContain('allow-scripts');
});

it('preserves dollar sequences when building the creative document', async () => {
Expand Down
Loading
Loading