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
818 changes: 818 additions & 0 deletions crates/common/src/integrations/datadome.rs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions crates/common/src/integrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::settings::Settings;

pub mod adserver_mock;
pub mod aps;
pub mod datadome;
pub mod didomi;
pub mod lockr;
pub mod nextjs;
Expand All @@ -30,5 +31,6 @@ pub(crate) fn builders() -> &'static [IntegrationBuilder] {
permutive::register,
lockr::register,
didomi::register,
datadome::register,
]
}
23 changes: 23 additions & 0 deletions crates/js/lib/src/integrations/datadome/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { log } from '../../core/log';

import { installDataDomeGuard } from './script_guard';

/**
* DataDome integration for tsjs
*
* Installs a script guard to intercept dynamically inserted DataDome SDK
* scripts and rewrites them to use the first-party proxy endpoint.
*
* The guard intercepts:
* - Script elements with src containing js.datadome.co
* - Link preload elements for DataDome scripts
*
* URLs are rewritten to preserve the original path:
* - https://js.datadome.co/tags.js -> /integrations/datadome/tags.js
* - https://js.datadome.co/js/check -> /integrations/datadome/js/check
*/

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

/**
* DataDome SDK Script Interception Guard
*
* Intercepts any dynamically inserted script tag that loads the DataDome SDK
* and rewrites it to use the first-party domain proxy endpoint. This works
* across all frameworks (Next.js, Nuxt, Gatsby, vanilla JS, etc.) and catches
* scripts inserted via appendChild, insertBefore, or any other dynamic DOM
* manipulation.
*
* Built on the shared script_guard factory with custom URL rewriting to preserve
* the original path from the DataDome URL (e.g., /tags.js, /js/check).
*/

/** Regex to match js.datadome.co as a domain in URLs */
const DATADOME_URL_PATTERN = /^(?:https?:)?\/\/js\.datadome\.co(?:\/|$)|^js\.datadome\.co(?:\/|$)/i;

/**
* Check if a URL is a DataDome SDK URL.
* Matches URLs where js.datadome.co is the host (not just a substring).
*
* Valid patterns:
* - https://js.datadome.co/...
* - //js.datadome.co/...
* - js.datadome.co/... (bare domain)
*
* Invalid:
* - https://cdn.example.com/js.datadome.co.js (domain is not js.datadome.co)
*/
function isDataDomeSdkUrl(url: string): boolean {
return !!url && DATADOME_URL_PATTERN.test(url);
}

/**
* Extract the path from a DataDome URL to preserve it in the rewrite.
* e.g., "https://js.datadome.co/tags.js" -> "/tags.js"
* "https://js.datadome.co/js/check" -> "/js/check"
*/
function extractDataDomePath(url: string): string {
try {
// Normalize to absolute URL for parsing
const normalizedUrl = url.startsWith('//')
? `https:${url}`
: url.startsWith('http')
? url
: `https://${url}`;

const parsed = new URL(normalizedUrl);
return parsed.pathname + parsed.search;
} catch {
// Fallback: extract path after js.datadome.co
const match = url.match(/js\.datadome\.co(\/[^'"]*)?/i);
return match?.[1] || '/tags.js';
}
}

/**
* Build a first-party URL from the current page origin and the DataDome path.
*/
function rewriteDataDomeUrl(originalUrl: string): string {
return `${window.location.origin}/integrations/datadome${extractDataDomePath(originalUrl)}`;
}

const guard = createScriptGuard({
name: 'DataDome',
isTargetUrl: isDataDomeSdkUrl,
rewriteUrl: rewriteDataDomeUrl,
});

/**
* Install the DataDome guard to intercept dynamic script loading.
* Patches Element.prototype.appendChild and insertBefore to catch
* ANY dynamically inserted DataDome SDK script elements and rewrite their URLs
* before insertion. Works across all frameworks and vanilla JavaScript.
*/
export const installDataDomeGuard = guard.install;

/**
* Check if the guard is currently installed.
*/
export const isGuardInstalled = guard.isInstalled;

/**
* Reset the guard installation state (primarily for testing).
*/
export const resetGuardState = guard.reset;

// Export for testing
export { isDataDomeSdkUrl, extractDataDomePath, rewriteDataDomeUrl };
60 changes: 46 additions & 14 deletions crates/js/lib/src/shared/script_guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,46 @@ import { log } from '../core/log';
* Shared Script Guard Factory
*
* Creates a DOM interception guard that patches appendChild and insertBefore
* to intercept dynamically inserted script (and preload link) elements whose
* URLs match an integration's SDK. The matched URLs are rewritten to a
* to intercept dynamically inserted script (and preload/prefetch link) elements
* whose URLs match an integration's SDK. The matched URLs are rewritten to a
* first-party proxy endpoint before the element is inserted into the DOM.
*
* Each call to createScriptGuard() produces an independent guard with its own
* installation state, so multiple integrations can coexist without interference.
*/

export interface ScriptGuardConfig {
/**
* Base configuration shared by all guard types.
*/
interface ScriptGuardConfigBase {
/** Integration name used in log messages (e.g. "Lockr", "Permutive"). */
name: string;
/** Return true if the URL belongs to this integration's SDK. */
isTargetUrl: (url: string) => boolean;
}

/**
* Config using a fixed proxy path (original behavior).
* The entire URL is replaced with `{origin}{proxyPath}`.
*/
interface ScriptGuardConfigWithProxyPath extends ScriptGuardConfigBase {
/** First-party proxy path to rewrite to (e.g. "/integrations/lockr/sdk"). */
proxyPath: string;
rewriteUrl?: never;
}

/**
* Config using a custom URL rewriter function.
* Allows integrations like DataDome to preserve the original path.
*/
interface ScriptGuardConfigWithRewriter extends ScriptGuardConfigBase {
proxyPath?: never;
/** Custom function to rewrite the original URL to a first-party URL. */
rewriteUrl: (originalUrl: string) => string;
}

export type ScriptGuardConfig = ScriptGuardConfigWithProxyPath | ScriptGuardConfigWithRewriter;

export interface ScriptGuard {
/** Patch appendChild/insertBefore to intercept matching scripts. */
install: () => void;
Expand All @@ -34,20 +57,18 @@ export interface ScriptGuard {
* Build a first-party URL from the current page origin and the configured proxy path.
*/
function rewriteToFirstParty(proxyPath: string): string {
const protocol = window.location.protocol === 'https:' ? 'https' : 'http';
const host = window.location.host;
return `${protocol}://${host}${proxyPath}`;
return `${window.location.origin}${proxyPath}`;
}

/**
* Determine whether a DOM node is a script or preload-link element whose URL
* matches the guard's target pattern.
* Determine whether a DOM node is a script or preload/prefetch link element
* whose URL matches the guard's target pattern.
*/
function shouldRewriteElement(
node: Node,
isTargetUrl: (url: string) => boolean
): node is HTMLScriptElement | HTMLLinkElement {
if (!node || !(node instanceof HTMLElement)) {
if (!(node instanceof HTMLElement)) {
return false;
}

Expand All @@ -57,10 +78,11 @@ function shouldRewriteElement(
return !!src && isTargetUrl(src);
}

// Link preload elements
// Link preload/prefetch elements
if (node.tagName === 'LINK') {
const link = node as HTMLLinkElement;
if (link.getAttribute('rel') !== 'preload' || link.getAttribute('as') !== 'script') {
const rel = link.getAttribute('rel');
if ((rel !== 'preload' && rel !== 'prefetch') || link.getAttribute('as') !== 'script') {
return false;
}
const href = link.href || link.getAttribute('href');
Expand All @@ -70,6 +92,16 @@ function shouldRewriteElement(
return false;
}

/**
* Get the rewritten URL using either the custom rewriter or the proxy path.
*/
function getRewrittenUrl(originalUrl: string, config: ScriptGuardConfig): string {
if (config.rewriteUrl) {
return config.rewriteUrl(originalUrl);
}
return rewriteToFirstParty(config.proxyPath);
}

/**
* Rewrite the URL attribute on a matched element to the first-party proxy.
*/
Expand All @@ -84,7 +116,7 @@ function rewriteElement(
const originalSrc = script.src || script.getAttribute('src');
if (!originalSrc) return;

const rewritten = rewriteToFirstParty(config.proxyPath);
const rewritten = getRewrittenUrl(originalSrc, config);

log.info(`${prefix}: rewriting dynamically inserted SDK script`, {
original: originalSrc,
Expand All @@ -99,9 +131,9 @@ function rewriteElement(
const originalHref = link.href || link.getAttribute('href');
if (!originalHref) return;

const rewritten = rewriteToFirstParty(config.proxyPath);
const rewritten = getRewrittenUrl(originalHref, config);

log.info(`${prefix}: rewriting SDK preload link`, {
log.info(`${prefix}: rewriting SDK ${link.getAttribute('rel')} link`, {
original: originalHref,
rewritten,
rel: link.getAttribute('rel'),
Expand Down
Loading