Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dcac51f
Add EC module with lifecycle management, consent gating, and config m…
ChristianPavilonis Mar 25, 2026
374efc4
Add KV identity graph with CAS concurrency control
ChristianPavilonis Mar 25, 2026
132ec58
Add partner registry and admin registration endpoint
ChristianPavilonis Mar 25, 2026
72e6a11
Fix 8 EC spec deviations identified in branch audit
ChristianPavilonis Mar 26, 2026
da70b83
Migrate admin endpoints to /_ts/admin namespace
ChristianPavilonis Apr 2, 2026
0998cd1
Add design spec for Sourcepoint GPP consent support (#640)
ChristianPavilonis Apr 15, 2026
a301437
Add implementation plan for Sourcepoint GPP consent support (#640)
ChristianPavilonis Apr 15, 2026
304009f
Add us_sale_opt_out field to GppConsent
ChristianPavilonis Apr 15, 2026
f3f16d6
Decode US sale opt-out from GPP sections
ChristianPavilonis Apr 15, 2026
6c0fce0
Recognize GPP US sale opt-out in EC consent gating
ChristianPavilonis Apr 15, 2026
e41a21a
Add Sourcepoint JS integration for GPP consent cookie mirroring
ChristianPavilonis Apr 15, 2026
fd7fcc7
tmp fix for toml
ChristianPavilonis Apr 16, 2026
0825f06
Add design spec for Prebid User ID Module support
ChristianPavilonis Apr 16, 2026
30f1988
Add implementation plan for Prebid User ID Module support
ChristianPavilonis Apr 16, 2026
76bf59f
Fix ESM path resolution in Prebid User ID plan regression guard
ChristianPavilonis Apr 16, 2026
bb6fe99
Add Vitest coverage for Prebid ts-eids cookie sync
ChristianPavilonis Apr 16, 2026
948035d
Bundle Prebid User ID core and submodules in Prebid integration
ChristianPavilonis Apr 16, 2026
770d831
Correct Prebid User ID plan + spec — drop pubCommonIdSystem (removed …
ChristianPavilonis Apr 16, 2026
a3ef683
Drop liveIntentIdSystem from Prebid bundle
ChristianPavilonis Apr 16, 2026
7524c3c
revert toml change
ChristianPavilonis Apr 16, 2026
f242d4a
Make Prebid User ID submodule set configurable at build time
ChristianPavilonis Apr 16, 2026
22f34d1
Clear stale consent cookies and aggregate US GPP opt-outs
ChristianPavilonis Apr 16, 2026
9e4f097
Add Secure flag and Max-Age to Sourcepoint GPP cookies
ChristianPavilonis Apr 16, 2026
6597543
Revert accidental proxy_secret change in trusted-server.toml
ChristianPavilonis Apr 16, 2026
f21dded
Remove orphaned sync_pixel.rs (deleted in base branch)
ChristianPavilonis Apr 16, 2026
1e33bc3
Fix leftover conflict markers from rebase — restore base branch versions
ChristianPavilonis Apr 16, 2026
7518a35
support ec partners map for env overrides
ChristianPavilonis Apr 17, 2026
8a6df3a
Scope Sourcepoint consent PR and address review feedback
ChristianPavilonis Apr 21, 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
1 change: 0 additions & 1 deletion crates/integration-tests/tests/common/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ pub enum TestError {
#[display("Expected HTTP status {expected}, got {actual}")]
UnexpectedStatusCode { expected: u16, actual: u16 },


#[display("JSON field assertion failed: {field}")]
JsonFieldMismatch { field: String },

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';
3 changes: 2 additions & 1 deletion crates/js/lib/src/integrations/prebid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,8 @@ function syncPrebidEidsCookie(): void {
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
95 changes: 95 additions & 0 deletions crates/js/lib/src/integrations/sourcepoint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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 {
// Sourcepoint stores one consent payload per property under `_sp_user_consent_*`.
// We intentionally take the first valid match and mirror that origin-scoped payload.
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;
Comment thread
ChristianPavilonis marked this conversation as resolved.
}

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

function clearCookie(name: string): void {
// Trusted Server is the only intended writer for these mirrored cookies, so
// clearing the origin-scoped cookie is sufficient for this integration.
document.cookie = `${name}=; path=/; Secure; SameSite=Lax; Max-Age=0`;
}
Comment thread
ChristianPavilonis marked this conversation as resolved.

/**
* 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;
8 changes: 5 additions & 3 deletions crates/js/lib/test/integrations/prebid/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ const {
mockProcessQueue,
mockRequestBids,
mockRegisterBidAdapter,
mockGetUserIdsAsEids,
mockPbjs,
mockGetBidAdapter,
mockGetUserIdsAsEids,
mockAdapterManager,
} = vi.hoisted(() => {
const mockSetConfig = vi.fn();
const mockProcessQueue = vi.fn();
const mockRequestBids = vi.fn();
const mockRegisterBidAdapter = vi.fn();
const mockGetBidAdapter = vi.fn();
const mockGetUserIdsAsEids = vi.fn();
const mockGetUserIdsAsEids = vi.fn(
() => [] as Array<{ source: string; uids?: Array<{ id: string; atype?: number }> }>
);
const mockPbjs = {
setConfig: mockSetConfig,
processQueue: mockProcessQueue,
Expand All @@ -33,9 +35,9 @@ const {
mockProcessQueue,
mockRequestBids,
mockRegisterBidAdapter,
mockGetUserIdsAsEids,
mockPbjs,
mockGetBidAdapter,
mockGetUserIdsAsEids,
mockAdapterManager,
};
});
Expand Down
151 changes: 151 additions & 0 deletions crates/js/lib/test/integrations/sourcepoint/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { mirrorSourcepointConsent } from '../../../src/integrations/sourcepoint';

describe('integrations/sourcepoint', () => {
function clearAllCookies(): void {
document.cookie.split(';').forEach((c) => {
const name = c.split('=')[0].trim();
if (name) document.cookie = `${name}=; path=/; Max-Age=0`;
});
}

function getCookie(name: string): string | undefined {
const match = document.cookie.split('; ').find((c) => c.startsWith(`${name}=`));
return match ? match.split('=').slice(1).join('=') : undefined;
}

beforeEach(() => {
// Clear cookies and localStorage before each test.
clearAllCookies();
localStorage.clear();
});

afterEach(() => {
clearAllCookies();
localStorage.clear();
});

it('mirrors __gpp and __gpp_sid from _sp_user_consent_* localStorage as session cookies', () => {
const payload = {
gppData: {
gppString: 'DBABLA~BVQqAAAAAgA.QA',
applicableSections: [7],
},
};
localStorage.setItem('_sp_user_consent_36026', JSON.stringify(payload));

const result = mirrorSourcepointConsent();

expect(result).toBe(true);
expect(document.cookie).toContain('__gpp=DBABLA~BVQqAAAAAgA.QA');
expect(document.cookie).toContain('__gpp_sid=7');
});

it('handles multiple applicable sections', () => {
const payload = {
gppData: {
gppString: 'DBABLA~BVQqAAAAAgA.QA',
applicableSections: [7, 8],
},
};
localStorage.setItem('_sp_user_consent_99999', JSON.stringify(payload));

mirrorSourcepointConsent();

expect(document.cookie).toContain('__gpp_sid=7,8');
});

it('returns false when no _sp_user_consent_* key exists', () => {
localStorage.setItem('unrelated_key', 'value');

const result = mirrorSourcepointConsent();

expect(result).toBe(false);
expect(document.cookie).not.toContain('__gpp=');
expect(document.cookie).not.toContain('__gpp_sid=');
});

it('clears stale mirrored cookies when no valid Sourcepoint payload exists', () => {
document.cookie = '__gpp=stale-gpp; path=/';
document.cookie = '__gpp_sid=7,8; path=/';
localStorage.setItem('unrelated_key', 'value');

const result = mirrorSourcepointConsent();

expect(result).toBe(false);
expect(getCookie('__gpp')).toBeUndefined();
expect(getCookie('__gpp_sid')).toBeUndefined();
});

it('returns false for malformed JSON in localStorage', () => {
localStorage.setItem('_sp_user_consent_12345', 'not-json!!!');

const result = mirrorSourcepointConsent();

expect(result).toBe(false);
expect(document.cookie).not.toContain('__gpp=');
});

it('skips malformed entries when a later Sourcepoint key is valid', () => {
localStorage.setItem('_sp_user_consent_12345', 'not-json!!!');
localStorage.setItem(
'_sp_user_consent_67890',
JSON.stringify({
gppData: {
gppString: 'DBABLA~BVQqAAAAAgA.QA',
applicableSections: [7],
},
})
);

const result = mirrorSourcepointConsent();

expect(result).toBe(true);
expect(getCookie('__gpp')).toBe('DBABLA~BVQqAAAAAgA.QA');
expect(getCookie('__gpp_sid')).toBe('7');
});

it('returns false when gppData is missing from payload', () => {
localStorage.setItem('_sp_user_consent_12345', JSON.stringify({ otherField: true }));

const result = mirrorSourcepointConsent();

expect(result).toBe(false);
expect(document.cookie).not.toContain('__gpp=');
});

it('returns false when gppString is empty', () => {
const payload = {
gppData: {
gppString: '',
applicableSections: [7],
},
};
localStorage.setItem('_sp_user_consent_12345', JSON.stringify(payload));

const result = mirrorSourcepointConsent();

expect(result).toBe(false);
expect(document.cookie).not.toContain('__gpp=');
});

it('clears stale __gpp_sid when the payload has no applicable sections', () => {
document.cookie = '__gpp_sid=7,8; path=/';
localStorage.setItem(
'_sp_user_consent_12345',
JSON.stringify({
gppData: {
gppString: 'DBABLA~BVQqAAAAAgA.QA',
applicableSections: [],
},
})
);

const result = mirrorSourcepointConsent();

expect(result).toBe(true);
expect(getCookie('__gpp')).toBe('DBABLA~BVQqAAAAAgA.QA');
expect(getCookie('__gpp_sid')).toBeUndefined();
});
});
Loading
Loading