Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions crates/js/lib/src/integrations/sourcepoint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { log } from '../../core/log';

import { installSourcepointGuard } from './script_guard';

if (typeof window !== 'undefined') {
installSourcepointGuard();
log.info('Sourcepoint integration initialized');
}
53 changes: 53 additions & 0 deletions crates/js/lib/src/integrations/sourcepoint/script_guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createScriptGuard } from '../../shared/script_guard';

const SOURCEPOINT_CDN_HOST = 'cdn.privacy-mgmt.com';

function normalizeSourcepointUrl(url: string): string | null {
if (!url) return null;

const trimmed = url.trim();
if (!trimmed) return null;

if (trimmed.startsWith('//')) return `https:${trimmed}`;
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) return trimmed;

// Bare domain or path — attempt to parse as https URL.
// The host === check in isSourcepointUrl rejects non-matching domains.
return `https://${trimmed}`;
}

function parseSourcepointUrl(url: string): URL | null {
const normalized = normalizeSourcepointUrl(url);
if (!normalized) return null;

try {
return new URL(normalized);
} catch {
return null;
}
}

export function isSourcepointUrl(url: string): boolean {
const parsed = parseSourcepointUrl(url);
return parsed?.host === SOURCEPOINT_CDN_HOST;
}

export function rewriteSourcepointUrl(originalUrl: string): string {
const parsed = parseSourcepointUrl(originalUrl);
if (!parsed) return originalUrl;

const query = parsed.search || '';

return `${window.location.origin}/integrations/sourcepoint/cdn${parsed.pathname}${query}`;
}

const guard = createScriptGuard({
displayName: 'Sourcepoint',
id: 'sourcepoint',
isTargetUrl: isSourcepointUrl,
rewriteUrl: rewriteSourcepointUrl,
});

export const installSourcepointGuard = guard.install;
export const isGuardInstalled = guard.isInstalled;
export const resetGuardState = guard.reset;
82 changes: 82 additions & 0 deletions crates/js/lib/test/integrations/sourcepoint/script_guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import {
installSourcepointGuard,
isGuardInstalled,
isSourcepointUrl,
resetGuardState,
rewriteSourcepointUrl,
} from '../../../src/integrations/sourcepoint/script_guard';

describe('Sourcepoint SDK Script Interception Guard', () => {
let originalAppendChild: typeof Element.prototype.appendChild;
let originalInsertBefore: typeof Element.prototype.insertBefore;

beforeEach(() => {
resetGuardState();
originalAppendChild = Element.prototype.appendChild;
originalInsertBefore = Element.prototype.insertBefore;
});

afterEach(() => {
resetGuardState();
});

it('detects Sourcepoint CDN URLs', () => {
expect(isSourcepointUrl('https://cdn.privacy-mgmt.com/wrapper/v2/messages')).toBe(true);
expect(isSourcepointUrl('//cdn.privacy-mgmt.com/mms/v2/get_site_data')).toBe(true);
expect(isSourcepointUrl('cdn.privacy-mgmt.com/consent/tcfv2')).toBe(true);
expect(isSourcepointUrl('https://example.com/script.js')).toBe(false);
expect(isSourcepointUrl('https://geo.privacymanager.io/')).toBe(false);
});

it('rejects subdomain-spoofing URLs', () => {
expect(isSourcepointUrl('cdn.privacy-mgmt.com.evil.com/script.js')).toBe(false);
expect(isSourcepointUrl('https://cdn.privacy-mgmt.com.evil.com/')).toBe(false);
expect(isSourcepointUrl('notcdn.privacy-mgmt.com/path')).toBe(false);
});

it('rewrites CDN URLs to the first-party proxy path', () => {
expect(rewriteSourcepointUrl('https://cdn.privacy-mgmt.com/wrapper/v2/messages?env=prod')).toBe(
`${window.location.origin}/integrations/sourcepoint/cdn/wrapper/v2/messages?env=prod`
);
});

it('installs and resets the guard', () => {
expect(isGuardInstalled()).toBe(false);
installSourcepointGuard();
expect(isGuardInstalled()).toBe(true);
expect(Element.prototype.appendChild).not.toBe(originalAppendChild);
expect(Element.prototype.insertBefore).not.toBe(originalInsertBefore);
resetGuardState();
expect(Element.prototype.appendChild).toBe(originalAppendChild);
expect(Element.prototype.insertBefore).toBe(originalInsertBefore);
});

it('rewrites dynamically inserted Sourcepoint scripts', () => {
installSourcepointGuard();

const container = document.createElement('div');
const script = document.createElement('script');
script.src = 'https://cdn.privacy-mgmt.com/wrapperMessagingWithoutDetection.js';

container.appendChild(script);

expect(script.src).toContain(
'/integrations/sourcepoint/cdn/wrapperMessagingWithoutDetection.js'
);
expect(script.src).not.toContain('cdn.privacy-mgmt.com');
});

it('does not rewrite unrelated scripts', () => {
installSourcepointGuard();

const container = document.createElement('div');
const script = document.createElement('script');
script.src = 'https://example.com/app.js';

container.appendChild(script);

expect(script.src).toBe('https://example.com/app.js');
});
});
2 changes: 2 additions & 0 deletions crates/trusted-server-core/src/integrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub mod nextjs;
pub mod permutive;
pub mod prebid;
mod registry;
pub mod sourcepoint;
pub mod testlight;

pub use registry::{
Expand All @@ -37,6 +38,7 @@ pub(crate) fn builders() -> &'static [IntegrationBuilder] {
permutive::register,
lockr::register,
didomi::register,
sourcepoint::register,
google_tag_manager::register,
datadome::register,
gpt::register,
Expand Down
Loading
Loading