diff --git a/crates/js/lib/src/integrations/sourcepoint/index.ts b/crates/js/lib/src/integrations/sourcepoint/index.ts
new file mode 100644
index 00000000..017494d1
--- /dev/null
+++ b/crates/js/lib/src/integrations/sourcepoint/index.ts
@@ -0,0 +1,8 @@
+import { log } from '../../core/log';
+
+import { installSourcepointGuard } from './script_guard';
+
+if (typeof window !== 'undefined') {
+ installSourcepointGuard();
+ log.info('Sourcepoint integration initialized');
+}
diff --git a/crates/js/lib/src/integrations/sourcepoint/script_guard.ts b/crates/js/lib/src/integrations/sourcepoint/script_guard.ts
new file mode 100644
index 00000000..21eaea12
--- /dev/null
+++ b/crates/js/lib/src/integrations/sourcepoint/script_guard.ts
@@ -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;
diff --git a/crates/js/lib/test/integrations/sourcepoint/script_guard.test.ts b/crates/js/lib/test/integrations/sourcepoint/script_guard.test.ts
new file mode 100644
index 00000000..c075600a
--- /dev/null
+++ b/crates/js/lib/test/integrations/sourcepoint/script_guard.test.ts
@@ -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');
+ });
+});
diff --git a/crates/trusted-server-core/src/integrations/mod.rs b/crates/trusted-server-core/src/integrations/mod.rs
index 92f30219..29657166 100644
--- a/crates/trusted-server-core/src/integrations/mod.rs
+++ b/crates/trusted-server-core/src/integrations/mod.rs
@@ -16,6 +16,7 @@ pub mod nextjs;
pub mod permutive;
pub mod prebid;
mod registry;
+pub mod sourcepoint;
pub mod testlight;
pub use registry::{
@@ -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,
diff --git a/crates/trusted-server-core/src/integrations/sourcepoint.rs b/crates/trusted-server-core/src/integrations/sourcepoint.rs
new file mode 100644
index 00000000..f155c0a5
--- /dev/null
+++ b/crates/trusted-server-core/src/integrations/sourcepoint.rs
@@ -0,0 +1,902 @@
+//! Sourcepoint integration for first-party CMP (Consent Management Platform) delivery.
+//!
+//! Proxies Sourcepoint's CDN (`cdn.privacy-mgmt.com`) through Trusted Server so
+//! the browser loads consent management assets from first-party paths.
+//!
+//! ## Rewriting layers
+//!
+//! | Layer | Mechanism | What it catches |
+//! |-------|-----------|-----------------|
+//! | HTML attributes | `IntegrationAttributeRewriter` | Static `",
+ ),
+ cdn_host = SOURCEPOINT_CDN_HOST,
+ cdn_prefix = SOURCEPOINT_CDN_PREFIX,
+ )]
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::integrations::{IntegrationDocumentState, IntegrationRegistry};
+ use crate::test_support::tests::create_test_settings;
+ use fastly::http::Method;
+ use serde_json::json;
+
+ fn config(enabled: bool) -> SourcepointConfig {
+ SourcepointConfig {
+ enabled,
+ rewrite_sdk: true,
+ cdn_origin: default_cdn_origin(),
+ cache_ttl_seconds: default_cache_ttl(),
+ }
+ }
+
+ #[test]
+ fn strips_cdn_prefix_from_routes() {
+ assert_eq!(
+ SourcepointIntegration::strip_cdn_prefix(
+ "/integrations/sourcepoint/cdn/wrapper/v2/messages"
+ ),
+ Some("/wrapper/v2/messages")
+ );
+ assert_eq!(
+ SourcepointIntegration::strip_cdn_prefix("/integrations/sourcepoint/cdn"),
+ Some("/")
+ );
+ assert_eq!(
+ SourcepointIntegration::strip_cdn_prefix("/some/other/path"),
+ None
+ );
+ }
+
+ #[test]
+ fn rewrites_cdn_urls_to_first_party_paths() {
+ let integration = SourcepointIntegration::new(Arc::new(config(true)));
+ let ctx = IntegrationAttributeContext {
+ attribute_name: "src",
+ request_host: "edge.example.com",
+ request_scheme: "https",
+ origin_host: "origin.example.com",
+ };
+
+ let rewritten = integration.rewrite(
+ "src",
+ "https://cdn.privacy-mgmt.com/mms/v2/get_site_data?account_id=821",
+ &ctx,
+ );
+
+ assert_eq!(
+ rewritten,
+ AttributeRewriteAction::replace(
+ "https://edge.example.com/integrations/sourcepoint/cdn/mms/v2/get_site_data?account_id=821",
+ )
+ );
+ }
+
+ #[test]
+ fn leaves_non_sourcepoint_urls_unchanged() {
+ let integration = SourcepointIntegration::new(Arc::new(config(true)));
+ let ctx = IntegrationAttributeContext {
+ attribute_name: "src",
+ request_host: "edge.example.com",
+ request_scheme: "https",
+ origin_host: "origin.example.com",
+ };
+
+ assert_eq!(
+ integration.rewrite("src", "https://example.com/script.js", &ctx),
+ AttributeRewriteAction::keep()
+ );
+ }
+
+ #[test]
+ fn rewrites_quoted_cdn_urls_to_root_relative_paths() {
+ let input = r#"var fallback="https://cdn.privacy-mgmt.com";var api="https://cdn.privacy-mgmt.com/consent/tcfv2";"#;
+ let output = SourcepointIntegration::rewrite_script_content(input);
+
+ assert_eq!(
+ output,
+ r#"var fallback="/integrations/sourcepoint/cdn";var api="/integrations/sourcepoint/cdn/consent/tcfv2";"#
+ );
+ }
+
+ #[test]
+ fn rewrites_protocol_relative_cdn_urls() {
+ let input = r#"url="//cdn.privacy-mgmt.com/mms/v2/get_site_data""#;
+ let output = SourcepointIntegration::rewrite_script_content(input);
+
+ assert!(
+ output.contains("\"/integrations/sourcepoint/cdn/mms/v2/get_site_data\""),
+ "Should rewrite protocol-relative CDN URL. Got: {output}",
+ );
+ }
+
+ #[test]
+ fn rewrites_origin_plus_unified_chunk_pattern() {
+ let input = r#"return t.origin+"/unified/4.40.1/"}"#;
+ let output = SourcepointIntegration::rewrite_script_content(input);
+
+ assert_eq!(
+ output,
+ r#"return t.origin+"/integrations/sourcepoint/cdn/unified/4.40.1/"}"#
+ );
+ }
+
+ #[test]
+ fn rewrites_both_patterns_in_realistic_snippet() {
+ // Mirrors the real Sourcepoint webpack public path resolution:
+ // try { ... return t.origin+"/unified/4.40.1/" }
+ // catch(e) {} return e+"/unified/4.40.1/"
+ // where e defaults to "https://cdn.privacy-mgmt.com"
+ let input = concat!(
+ r#"var e="https://cdn.privacy-mgmt.com";"#,
+ r#"try{var t=document.createElement("a");"#,
+ r#"t.href=document.currentScript.src;"#,
+ r#"return t.origin+"/unified/4.40.1/"}"#,
+ r#"catch(n){}return e+"/unified/4.40.1/""#,
+ );
+
+ let output = SourcepointIntegration::rewrite_script_content(input);
+
+ assert!(
+ output.contains(r#"var e="/integrations/sourcepoint/cdn";"#),
+ "Fallback CDN default should be rewritten. Got: {output}",
+ );
+ assert!(
+ output.contains(r#"t.origin+"/integrations/sourcepoint/cdn/unified/4.40.1/"}"#),
+ "Origin chunk path should be prefixed. Got: {output}",
+ );
+ assert!(
+ output.contains(r#"e+"/unified/4.40.1/""#),
+ "Fallback concatenation should keep /unified/ since e is already rewritten. Got: {output}",
+ );
+ }
+
+ #[test]
+ fn preserves_non_sourcepoint_urls() {
+ let input = r#"var cdn="https://example.com/script.js";var x=t.origin+"/assets/app.js""#;
+ let output = SourcepointIntegration::rewrite_script_content(input);
+
+ assert_eq!(output, input, "Non-Sourcepoint URLs should be untouched");
+ }
+
+ #[test]
+ fn registers_sourcepoint_routes() {
+ let mut settings = create_test_settings();
+ settings
+ .integrations
+ .insert_config(SOURCEPOINT_INTEGRATION_ID, &json!({ "enabled": true }))
+ .expect("should insert config");
+
+ let registry = IntegrationRegistry::new(&settings).expect("should create registry");
+ assert!(
+ registry.has_route(
+ &Method::GET,
+ "/integrations/sourcepoint/cdn/wrapper/v2/messages"
+ ),
+ "should register CDN proxy route"
+ );
+ }
+
+ #[test]
+ fn attribute_rewriter_skips_when_rewrite_disabled() {
+ let mut cfg = config(true);
+ cfg.rewrite_sdk = false;
+ let integration = SourcepointIntegration::new(Arc::new(cfg));
+
+ assert!(
+ !integration.handles_attribute("src"),
+ "should not handle src when rewrite_sdk is false"
+ );
+ assert!(
+ !integration.handles_attribute("href"),
+ "should not handle href when rewrite_sdk is false"
+ );
+ }
+
+ #[test]
+ fn identifies_likely_javascript_paths() {
+ assert!(SourcepointIntegration::is_likely_javascript_path(
+ "/unified/4.40.1/gdpr-tcf.bundle.js"
+ ));
+ assert!(SourcepointIntegration::is_likely_javascript_path(
+ "/wrapper/v2/messages"
+ ));
+ assert!(SourcepointIntegration::is_likely_javascript_path(
+ "/wrapperMessagingWithoutDetection.js"
+ ));
+ assert!(!SourcepointIntegration::is_likely_javascript_path(
+ "/mms/v2/get_site_data"
+ ));
+ assert!(!SourcepointIntegration::is_likely_javascript_path(
+ "/consent/tcfv2"
+ ));
+ }
+
+ #[test]
+ fn head_injector_emits_sp_property_trap() {
+ let integration = SourcepointIntegration::new(Arc::new(config(true)));
+ let document_state = IntegrationDocumentState::default();
+ let ctx = IntegrationHtmlContext {
+ request_host: "ts.autoblog.com",
+ request_scheme: "https",
+ origin_host: "origin.autoblog.com",
+ document_state: &document_state,
+ };
+
+ let inserts = integration.head_inserts(&ctx);
+ assert_eq!(inserts.len(), 1, "should produce exactly one head insert");
+
+ let script = &inserts[0];
+ assert!(
+ script.starts_with(""),
+ "should be wrapped in script tags: {script}",
+ );
+ assert!(
+ script.contains("cdn.privacy-mgmt.com"),
+ "should reference the CDN host to rewrite: {script}",
+ );
+ assert!(
+ script.contains("/integrations/sourcepoint/cdn"),
+ "should contain the first-party CDN prefix: {script}",
+ );
+ assert!(
+ script.contains("Object.defineProperty"),
+ "should install a property trap on window._sp_: {script}",
+ );
+ assert!(
+ script.contains("baseEndpoint"),
+ "should patch baseEndpoint in the config: {script}",
+ );
+ assert!(
+ script.contains("metricUrl"),
+ "should patch metricUrl: {script}",
+ );
+ }
+
+ #[test]
+ fn head_injector_returns_empty_when_rewrite_disabled() {
+ let mut cfg = config(true);
+ cfg.rewrite_sdk = false;
+ let integration = SourcepointIntegration::new(Arc::new(cfg));
+ let document_state = IntegrationDocumentState::default();
+ let ctx = IntegrationHtmlContext {
+ request_host: "ts.autoblog.com",
+ request_scheme: "https",
+ origin_host: "origin.autoblog.com",
+ document_state: &document_state,
+ };
+
+ let inserts = integration.head_inserts(&ctx);
+ assert!(
+ inserts.is_empty(),
+ "should not inject anything when rewrite_sdk is false"
+ );
+ }
+
+ #[test]
+ fn rejects_cdn_origin_outside_privacy_mgmt_domain() {
+ let cfg = SourcepointConfig {
+ enabled: true,
+ rewrite_sdk: true,
+ cdn_origin: "http://169.254.169.254".to_string(),
+ cache_ttl_seconds: default_cache_ttl(),
+ };
+ assert!(
+ cfg.validate().is_err(),
+ "should reject cdn_origin not on *.privacy-mgmt.com"
+ );
+ }
+
+ #[test]
+ fn accepts_valid_cdn_origin() {
+ let cfg = SourcepointConfig {
+ enabled: true,
+ rewrite_sdk: true,
+ cdn_origin: "https://cdn.privacy-mgmt.com".to_string(),
+ cache_ttl_seconds: default_cache_ttl(),
+ };
+ assert!(
+ cfg.validate().is_ok(),
+ "should accept cdn_origin on *.privacy-mgmt.com"
+ );
+ }
+
+ #[test]
+ fn rewrites_single_quoted_origin_plus_unified_pattern() {
+ let input = r#"return t.origin+'/unified/4.40.1/'}"#;
+ let output = SourcepointIntegration::rewrite_script_content(input);
+
+ assert_eq!(
+ output, r#"return t.origin+'/integrations/sourcepoint/cdn/unified/4.40.1/'}"#,
+ "should rewrite single-quoted unified path"
+ );
+ }
+}
diff --git a/docs/guide/integrations-overview.md b/docs/guide/integrations-overview.md
index 1c312584..2274245c 100644
--- a/docs/guide/integrations-overview.md
+++ b/docs/guide/integrations-overview.md
@@ -4,12 +4,13 @@ Trusted Server provides built-in integrations with popular third-party services,
## Quick Comparison
-| Integration | Type | Endpoints | HTML Rewriting | Primary Use Case | Status |
-| ------------- | ---------------- | ---------- | ---------------------------- | --------------------------- | ----------- |
-| **Prebid** | Proxy + Rewriter | 2-3 routes | Removes Prebid.js scripts | Server-side header bidding | Production |
-| **Next.js** | Script Rewriter | None | Rewrites Next.js data | First-party Next.js routing | Production |
-| **Permutive** | Proxy + Rewriter | 6 routes | Rewrites SDK URLs | First-party audience data | Production |
-| **Testlight** | Proxy + Rewriter | 1 route | Rewrites integration scripts | Testing/development | Development |
+| Integration | Type | Endpoints | HTML Rewriting | Primary Use Case | Status |
+| --------------- | ---------------- | ---------- | ---------------------------- | --------------------------- | ----------- |
+| **Prebid** | Proxy + Rewriter | 2-3 routes | Removes Prebid.js scripts | Server-side header bidding | Production |
+| **Next.js** | Script Rewriter | None | Rewrites Next.js data | First-party Next.js routing | Production |
+| **Permutive** | Proxy + Rewriter | 6 routes | Rewrites SDK URLs | First-party audience data | Production |
+| **Sourcepoint** | Proxy + Rewriter | 2 routes | Rewrites CMP asset URLs | First-party CMP delivery | Development |
+| **Testlight** | Proxy + Rewriter | 1 route | Rewrites integration scripts | Testing/development | Development |
## Integration Details
@@ -119,6 +120,38 @@ rewrite_sdk = true
---
+### Sourcepoint
+
+**What it does:** Proxies Sourcepoint CMP CDN endpoints through Trusted Server and rewrites publisher references to first-party paths.
+
+**Key Features:**
+
+- CDN proxy for `cdn.privacy-mgmt.com`
+- HTML attribute rewriting for Sourcepoint assets
+- JavaScript body rewriting for webpack chunks and API URLs
+- Head-injected `window._sp_` property trap for runtime config
+- Client-side script guard for dynamic script insertion
+
+**Configuration:**
+
+```toml
+[integrations.sourcepoint]
+enabled = true
+rewrite_sdk = true
+cdn_origin = "https://cdn.privacy-mgmt.com"
+cache_ttl_seconds = 3600
+```
+
+**Endpoints:**
+
+- `GET/POST /integrations/sourcepoint/cdn/*` - Sourcepoint CDN proxy
+
+**When to use:** You load Sourcepoint CMP assets and want them to flow through first-party paths without introducing an open-ended proxy.
+
+**Learn more:** [Sourcepoint Integration](./integrations/sourcepoint.md)
+
+---
+
### Testlight
**What it does:** Testing/development integration for validating the integration system with OpenRTB-like auctions.
@@ -198,6 +231,10 @@ Do you use Permutive for audience data?
├─ Yes → Enable Permutive integration
└─ No → Skip Permutive
+Do you use Sourcepoint for consent management?
+├─ Yes → Enable Sourcepoint integration
+└─ No → Skip Sourcepoint
+
Are you developing/testing integrations?
├─ Yes → Enable Testlight integration
└─ No → Skip Testlight
@@ -205,12 +242,13 @@ Are you developing/testing integrations?
## Performance Considerations
-| Integration | Performance Impact | Caching Strategy | Notes |
-| ------------- | ------------------ | --------------------------- | -------------------------------------------- |
-| **Prebid** | Medium | Response caching possible | Timeout configurable (default 1s) |
-| **Next.js** | Low | N/A (streaming rewrite) | Minimal overhead, runs during HTML streaming |
-| **Permutive** | Low | SDK cached (1 hour default) | API calls proxied in real-time |
-| **Testlight** | Low | No caching | Development use only |
+| Integration | Performance Impact | Caching Strategy | Notes |
+| --------------- | ------------------ | --------------------------- | -------------------------------------------- |
+| **Prebid** | Medium | Response caching possible | Timeout configurable (default 1s) |
+| **Next.js** | Low | N/A (streaming rewrite) | Minimal overhead, runs during HTML streaming |
+| **Permutive** | Low | SDK cached (1 hour default) | API calls proxied in real-time |
+| **Sourcepoint** | Low | CDN cached (1 hour default) | JS rewriting adds minor overhead |
+| **Testlight** | Low | No caching | Development use only |
## Environment Variables
@@ -230,6 +268,10 @@ TRUSTED_SERVER__INTEGRATIONS__NEXTJS__ENABLED=true
TRUSTED_SERVER__INTEGRATIONS__PERMUTIVE__ORGANIZATION_ID="neworg"
TRUSTED_SERVER__INTEGRATIONS__PERMUTIVE__WORKSPACE_ID="workspace-123"
+# Sourcepoint
+TRUSTED_SERVER__INTEGRATIONS__SOURCEPOINT__ENABLED=true
+TRUSTED_SERVER__INTEGRATIONS__SOURCEPOINT__CDN_ORIGIN="https://cdn.privacy-mgmt.com"
+
# Testlight
TRUSTED_SERVER__INTEGRATIONS__TESTLIGHT__ENDPOINT="https://test.example.com"
```
diff --git a/docs/guide/integrations/sourcepoint.md b/docs/guide/integrations/sourcepoint.md
new file mode 100644
index 00000000..857e88ae
--- /dev/null
+++ b/docs/guide/integrations/sourcepoint.md
@@ -0,0 +1,65 @@
+# Sourcepoint Integration
+
+Sourcepoint provides consent and privacy messaging for publishers. This integration proxies the Sourcepoint CDN endpoint through Trusted Server so the browser loads it from a first-party path.
+
+## Overview
+
+The Sourcepoint integration:
+
+- Proxies `cdn.privacy-mgmt.com` requests through `/integrations/sourcepoint/cdn/*`
+- Rewrites matching `src` and `href` attributes during HTML processing
+- Rewrites JavaScript response bodies so webpack chunks and API calls route through the proxy
+- Injects a `window._sp_` property trap for config URLs set by Next.js hydration chunks
+- Installs a client-side script guard for dynamically inserted Sourcepoint assets
+
+## Configuration
+
+Add the following to `trusted-server.toml`:
+
+```toml
+[integrations.sourcepoint]
+enabled = true
+rewrite_sdk = true
+cdn_origin = "https://cdn.privacy-mgmt.com"
+cache_ttl_seconds = 3600
+```
+
+### Configuration Options
+
+| Option | Type | Default | Description |
+| ------------------- | ------- | ------------------------------ | --------------------------------------------------------------------------------- |
+| `enabled` | boolean | `false` | Enable the Sourcepoint integration |
+| `rewrite_sdk` | boolean | `true` | Rewrite matching Sourcepoint URLs in HTML |
+| `cdn_origin` | string | `https://cdn.privacy-mgmt.com` | Sourcepoint CDN origin |
+| `cache_ttl_seconds` | integer | `3600` | Cache TTL applied to successful CDN responses when the origin omits cache headers |
+
+## Endpoints
+
+| Method | Path | Description |
+| ---------- | --------------------------------- | --------------------------------------------- |
+| `GET/POST` | `/integrations/sourcepoint/cdn/*` | Proxy Sourcepoint CDN assets and wrapper APIs |
+
+## HTML Rewriting
+
+When `rewrite_sdk = true`, Trusted Server rewrites matching Sourcepoint URLs in HTML responses:
+
+```html
+
+
+
+
+
+```
+
+## Client-Side Guard
+
+Single-page apps often insert CMP scripts after the initial HTML response. The `sourcepoint` tsjs module installs a DOM insertion guard so dynamically inserted Sourcepoint script and preload URLs are rewritten to first-party paths before the browser fetches them.
+
+## Notes
+
+- This version scopes the integration to `cdn.privacy-mgmt.com`. Additional Sourcepoint domains (e.g., `geo.privacymanager.io`) can be added later if publishers require them.
+
+## See Also
+
+- [Integration Guide](/guide/integration-guide)
+- [Integrations Overview](/guide/integrations-overview)
diff --git a/trusted-server.toml b/trusted-server.toml
index d9189aaa..f7de4630 100644
--- a/trusted-server.toml
+++ b/trusted-server.toml
@@ -69,6 +69,12 @@ enabled = false
sdk_origin = "https://sdk.privacy-center.org"
api_origin = "https://api.privacy-center.org"
+[integrations.sourcepoint]
+enabled = false
+rewrite_sdk = true
+cdn_origin = "https://cdn.privacy-mgmt.com"
+cache_ttl_seconds = 3600
+
[integrations.permutive]
enabled = false
organization_id = ""
@@ -190,4 +196,3 @@ timeout_ms = 1000
[integrations.adserver_mock.context_query_params]
permutive_segments = "permutive"
-