c15t
Version:
Headless JavaScript consent management platform for cookie banners, privacy preferences, consent storage, and script gating.
276 lines (275 loc) • 9.69 kB
TypeScript
import type { ConsentState } from '../../types/compliance';
import type { AllConsentNames } from '../../types/consent-types';
import type { HasCondition } from '../has';
/**
* Information passed to script callbacks
*
* @public
*/
export interface ScriptCallbackInfo {
/** The original script ID */
id: string;
/** The actual DOM element ID used (anonymized if enabled) */
elementId: string;
/** Has consent */
hasConsent: boolean;
/** The current consent state */
consents: ConsentState;
/**
* The script element (for load/error callbacks)
* Will be undefined for callback-only scripts
*/
element?: HTMLScriptElement;
/** Error information (for error callbacks) */
error?: Error;
}
/**
* Represents a script to be loaded based on consent conditions.
*
* Scripts can be one of three types:
* 1. Standard scripts - These load an external JavaScript file via a script tag
* 2. Text-based scripts - These contain inline JavaScript code that gets executed directly
* 3. Callback-only scripts - These don't add a script tag to the DOM but still execute callbacks
* based on consent state changes (useful for controlling existing libraries)
*
* @public
*/
export interface Script {
/** Unique identifier for the script */
id: string;
/** URL of the script to load */
src?: string;
/** Inline JavaScript code to execute */
textContent?: string;
/** Consent category or condition required to load this script */
category: HasCondition<AllConsentNames>;
/**
* Whether this is a callback-only script that doesn't need to load an external resource.
* When true, no script tag will be added to the DOM, only callbacks will be executed.
*
* This is useful for:
* - Managing consent for libraries already loaded on the page
* - Enabling/disabling tracking features based on consent changes
* - Running custom code when consent status changes without loading external scripts
*
* Example use cases:
* - Enabling/disabling Posthog tracking
* - Configuring Google Analytics consent mode
* - Managing cookie consent for embedded content
*
* @default false
*/
callbackOnly?: boolean;
/**
* Whether the script should persist after consent is revoked.
* @default false
*/
persistAfterConsentRevoked?: boolean;
/**
* Whether the script should always load regardless of consent state.
*
* This is useful for scripts like Google Tag Manager or PostHog that manage
* their own consent state internally. The script will load immediately and
* never be unloaded based on consent changes.
*
* Note: When using this option, you are responsible for ensuring the script
* itself respects user consent preferences through its own consent management.
*
* @default false
*
* @example
* ```ts
* const gtmScript: Script = {
* id: 'google-tag-manager',
* src: 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXX',
* category: 'measurement', // Category is still required but won't gate loading
* alwaysLoad: true, // GTM will always load and manage its own consent
* };
* ```
*/
alwaysLoad?: boolean;
/** Priority hint for browser resource loading */
fetchPriority?: 'high' | 'low' | 'auto';
/** Additional attributes to add to the script element */
attributes?: Record<string, string>;
/** Whether to use async loading */
async?: boolean;
/** Whether to defer script loading */
defer?: boolean;
/** Content Security Policy nonce */
nonce?: string;
/**
* Whether to use an anonymized ID for the script element, this helps ensure the script is not blocked by ad blockers
* @default true
*/
anonymizeId?: boolean;
/**
* Where to inject the script element in the DOM.
* - `'head'`: Scripts are appended to `<head>` (default)
* - `'body'`: Scripts are appended to `<body>`
*
* Use `'body'` for scripts that:
* - Need to manipulate DOM elements that don't exist until body loads
* - Should load after page content for performance reasons
* - Are required by third-party services to be in the body
*
* Use `'head'` (default) for scripts that:
* - Need to track early page events (analytics)
* - Should be available before page render
* - Most tracking/analytics scripts
*
* @default 'head'
*
* @example
* ```ts
* const script: Script = {
* id: 'my-script',
* src: 'https://example.com/script.js',
* category: 'analytics',
* target: 'body', // Load in body instead of head
* };
* ```
*/
target?: 'head' | 'body';
/**
* Callback executed before the script is loaded
* @param info - Information about the script and current consent state
*/
onBeforeLoad?: (info: ScriptCallbackInfo) => void;
/**
* Callback executed when the script loads successfully
* @param info - Information about the script and current consent state
*/
onLoad?: (info: ScriptCallbackInfo) => void;
/**
* Callback executed if the script fails to load
* @param info - Information about the script, error, and current consent state
*/
onError?: (info: ScriptCallbackInfo) => void;
/**
* Callback executed whenever the consent store is changed.
* This callback only applies to scripts already loaded.
*
* @param info - Information about the script and current consent state
*
* @example
* ```ts
* const script: Script = {
* id: 'analytics',
* src: 'https://analytics.example.com/script.js',
* category: 'analytics',
* onConsentChange: ({ consents }) => {
* // React to consent changes
* if (consents.analytics) {
* console.log('Analytics consent granted');
* }
* }
* };
* ```
*/
onConsentChange?: (info: ScriptCallbackInfo) => void;
/**
* IAB TCF vendor ID - links script to a registered vendor.
*
* When in IAB mode, the script will only load if this vendor has consent.
* Takes precedence over `category` when in IAB mode.
* Use custom vendor IDs (string or number) to gate non-IAB vendors too.
*
* @example
* ```ts
* const script: Script = {
* id: 'google-analytics',
* src: 'https://www.googletagmanager.com/gtag/js',
* category: 'measurement',
* vendorId: 755, // Google Advertising Products
* };
* ```
*/
vendorId?: number | string;
/**
* IAB TCF purpose IDs this script requires consent for.
*
* When in IAB mode and no vendorId is set, the script will only load
* if ALL specified purposes have consent.
*
* @example
* ```ts
* const script: Script = {
* id: 'ad-script',
* src: 'https://ads.example.com/script.js',
* category: 'marketing',
* iabPurposes: [2, 3, 4], // Advertising purposes
* };
* ```
*/
iabPurposes?: number[];
/**
* IAB TCF legitimate interest purpose IDs.
*
* These purposes can operate under legitimate interest instead of consent.
* The script loads if all iabPurposes have consent OR all iabLegIntPurposes
* have legitimate interest established.
*
* @example
* ```ts
* const script: Script = {
* id: 'analytics',
* src: 'https://analytics.example.com/script.js',
* category: 'measurement',
* iabPurposes: [7], // Measure ad performance (consent)
* iabLegIntPurposes: [9, 10], // Stats & development (legit interest)
* };
* ```
*/
iabLegIntPurposes?: number[];
/**
* IAB TCF special feature IDs this script requires.
*
* Special features require explicit opt-in:
* - 1: Use precise geolocation data
* - 2: Actively scan device characteristics for identification
*
* @example
* ```ts
* const script: Script = {
* id: 'location-tracker',
* src: 'https://geo.example.com/tracker.js',
* category: 'measurement',
* iabSpecialFeatures: [1], // Requires precise geolocation
* };
* ```
*/
iabSpecialFeatures?: number[];
}
/**
* Result of updating scripts
*
* @public
*/
export interface ScriptUpdateResult {
/** Array of script IDs that were loaded */
loaded: string[];
/** Array of script IDs that were unloaded */
unloaded: string[];
}
export type ScriptDebugSource = 'script-loader' | 'manifest-runtime';
export type ScriptDebugScope = 'lifecycle' | 'phase' | 'step';
export type ScriptLifecycleCallback = 'onBeforeLoad' | 'onLoad' | 'onConsentChange' | 'onError';
export type ScriptDebugAction = 'skipped' | 'already_loaded' | 'callback_start' | 'callback_complete' | 'callback_error' | 'element_appended' | 'loaded' | 'load_listener_attached' | 'error_listener_attached' | 'unloaded' | 'phase_start' | 'phase_complete' | 'step_executed' | 'step_error';
export interface ScriptDebugEvent {
source: ScriptDebugSource;
scope: ScriptDebugScope;
action: ScriptDebugAction;
message: string;
scriptId: string;
elementId?: string;
hasConsent?: boolean;
callback?: ScriptLifecycleCallback;
phase?: string;
stepType?: string;
stepIndex?: number;
data?: Record<string, unknown>;
timestamp: number;
}
export type ScriptDebugEventInput = Omit<ScriptDebugEvent, 'timestamp'>;
export type ScriptDebugListener = (event: ScriptDebugEvent) => void;