@axeptio/cmp-agents
v3.0.1
Published
Reverse-engineered interfaces able to interact with Consent Management Platforms online
Readme
CMP Agents
A collection of reverse-engineered JavaScript interfaces to interact with Consent Management Platforms (CMPs) online.
This library is intended to run in the browser. It detects and drives CMP UIs in the page context. When using NPM you must either (1) bundle your code and inject that script into the browser, or (2) inject the pre-built bundle (dist/cmp-agents.js) into the page and then run code that uses window.CMPAgents. It does not run in Node.js alone.
Looking at a live site
Every observation of a real site runs in the Docker harness, which pins the DNS, the browser build
and the browser profile. No browser window opens on your workstation — it runs in the container on
a virtual display — and everything it sees comes back to ./results in one format, so a person and
a tool read the same evidence.
# What a page exposes, and which CMP wins
./test/docker/run.sh --cli detect <url> --settle 6000
# Add what you want to see
./test/docker/run.sh --cli detect <url> --settle 6000 \
--dump-globals "Didomi,__tcfapi" --dump-selectors "#cc--main" --dump-resources
# What the page loaded that no integration claims — how a dead resource pattern is found
./test/docker/run.sh --cli detect <url> --dump-unmatched --unmatched-urls 40
# What the CMP does when it is driven
./test/docker/run.sh --cli detect <url> --act acceptAll --act-settle 6000
# Every URL an integration declares, as a JSON report
./test/docker/run.sh --cli check-urls <integration> --json --out /out/<label>.json
# A HAR of one URL, with a readable summary beside it
./test/docker/run.sh --cli detect <url> --settle 6000 --har --out /out/<label>.har
# → results/<label>.har and results/<label>.har.summary.txt
# A HAR of a failing test: the suite records one on the retry
./test/docker/run.sh --name <label> --mode complete -- --grep "<test title>"
# → results/<label>.json and results/playwright-report-<label>/data/network.har
npm run test:docker:compare -- results/<before>.json results/<after>.jsonAlways pass --name, or a run overwrites the previous report. Cite a capture, or it is a guess.
Overview
CMP Agents provides a unified API to detect, read, and interact with 40+ different Consent Management Platforms. Whether you're building a cookie scanner, consent automation tool, or privacy assistant, this library gives you the building blocks to work with CMPs programmatically.
Supported CMPs
| CMP | Detection | Accept/Reject | Granular Consent | |-----|-----------|---------------|------------------| | Axeptio | ✅ | ✅ | ✅ | | CookieBot | ✅ | ✅ | ✅ | | Didomi | ✅ | ✅ | ✅ | | OneTrust | ✅ | ✅ | ✅ | | UserCentrics | ✅ | ✅ | ✅ | | TrustArc | ✅ | ✅ | ✅ | | iubenda | ✅ | ✅ | ✅ | | CookieYes | ✅ | ✅ | ✅ | | Complianz | ✅ | ✅ | ✅ | | TarteAuCitron | ✅ | ✅ | ✅ | | ...and 30+ more | ✅ | ✅ | Varies |
See docs/integrations.md for the complete list.
Installation
NPM
npm install @axeptio/cmp-agentsUse the library in code that will run in the browser: bundle that code (e.g. with Webpack, Rollup) and inject the resulting script into the page, or inject the pre-built bundle (node_modules/@axeptio/cmp-agents/dist/cmp-agents.js) and call CMPAgents.create() from your own injected script.
CDN (Script tag)
<!-- Latest version -->
<script src="https://unpkg.com/@axeptio/cmp-agents/dist/cmp-agents.min.js"></script>
<!-- Or from jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/@axeptio/cmp-agents/dist/cmp-agents.min.js"></script>
<!-- Specific version -->
<script src="https://unpkg.com/@axeptio/[email protected]/dist/cmp-agents.min.js"></script>Command line
Installing the package puts cmp-agents on your path:
npx cmp-agents --helpThe commands that open a browser — detect, check-urls, debug, test, signals, cmp-list, add-url --validate and snapshot — need Playwright, which is an optional peer dependency. Install it when you want them:
npm install playwright && npx playwright install chromiumEvery other command runs without it. One that needs it and cannot find it says so and exits non-zero.
Requirements
- Browser: The library runs in the browser (script tag or injected bundle).
- Node.js >= 22.17.0 < 23.0.0 and npm >= 10 for development, CLI, and building your own bundle.
- Playwright (optional): required only by the browser-driving CLI commands listed above.
Quick Start
Browser Usage (CDN)
<script src="https://unpkg.com/@axeptio/cmp-agents/dist/cmp-agents.min.js"></script>
<script>
const agent = CMPAgents.create();
agent.on('cmp', async (cmp) => {
console.log('Detected:', cmp.getName());
const vendors = await cmp.getVendors();
console.log('Vendors:', vendors);
});
agent.run();
</script>NPM / Bundled script (browser)
Bundle this code and inject the bundle into the page so it runs in the browser:
import { CMPDetector, CookieBot, OneTrust, Didomi, CONSENT_INTENT } from '@axeptio/cmp-agents';
// Create a detector and register CMPs (runs in browser)
const cmpDetector = new CMPDetector();
cmpDetector.registerIntegrations(new CookieBot(), new OneTrust(), new Didomi());
// Listen for CMP detection
cmpDetector.on('cmp', async (cmp) => {
console.log(`Detected: ${cmp.getName()}`);
// Get vendors
const vendors = await cmp.getVendors();
console.log('Vendors:', vendors);
// Accept all using the new API
await cmp.applyPreferences({ intent: CONSENT_INTENT.ACCEPT_ALL });
});
// Start detection
cmpDetector.run();Using Individual Integrations
import CookieBot from '@axeptio/cmp-agents/integrations/cookiebot';
import { CONSENT_INTENT } from '@axeptio/cmp-agents';
const cookiebot = new CookieBot();
// Check if CookieBot is present
if (window.Cookiebot) {
cookiebot.trigger('detection');
// Get current consent status
await cookiebot.retrieveConsent();
console.log('Consent:', cookiebot.consent);
// Accept all vendors
await cookiebot.applyPreferences({ intent: CONSENT_INTENT.ACCEPT_ALL });
}Granular Vendor Consent
import { CONSENT_INTENT } from '@axeptio/cmp-agents';
const cmp = cmpDetector.getCMP();
const vendors = await cmp.getVendors();
// Build a map of vendor preferences
const vendorConsents = {};
// Allow Analytics vendor, deny Advertising
const analyticsVendor = vendors.find(v => v.name.includes('Analytics'));
const adsVendor = vendors.find(v => v.name.includes('Advertising'));
if (analyticsVendor) vendorConsents[analyticsVendor.name] = true;
if (adsVendor) vendorConsents[adsVendor.name] = false;
// Apply custom choices by passing vendors directly
await cmp.applyPreferences({
intent: CONSENT_INTENT.APPLY_CUSTOM,
vendors: vendorConsents
});Architecture
┌─────────────────────────────────────────────────────────────┐
│ CMPDetector │
│ - Registers CMP integrations │
│ - Manages detection lifecycle │
│ - Selects the operable CMP the evidence ranks first │
└─────────────────────┬───────────────────────────────────────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ CMP (Base) │ │ TCF_CMP │
│ │ │ (extends CMP) │
│ - getName() │ │ │
│ - getVendors() │ │ - TCF v2 API │
│ - applyPrefs() │ │ - getVendorList │
│ - acceptAll() │ │ - getTCData() │
│ - rejectAll() │ │ │
└────────┬────────┘ └────────┬────────┘
│ │
└───────────┬───────────┘
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│CookieBot│ │ OneTrust│ │ Didomi │
└─────────┘ └─────────┘ └─────────┘API Reference
CMP Base Class
All CMP integrations extend the CMP base class:
class CMP {
// Detection
getName(): string
getMatchingResources(): RegExp[]
onTick(): void
// Configuration
getLayout(): string // 'banner' | 'popup' | 'widget' | etc.
getBehaviors(): string[] // ['ACCEPTABLE', 'REJECTABLE', etc.]
getGranularity(): string // 'NONE' | 'GLOBAL' | 'VENDOR' | 'PURPOSE'
// What TCF the page speaks, and who is serving it
getCmpId(): number // this vendor's IAB registration, 0 when it holds none
getTcfInfo(options): Promise<TcfInfo> // measured, never assumed
getTcfCmpIdentity(options): Promise<TcfCmpIdentity | null> // the vendor the page's __tcfapi names
// Data retrieval
getVendors(): Promise<Vendor[]>
getPurposes(): Promise<Purpose[]>
retrieveConsent(): void
hasExistingConsent(): boolean
// Binary actions
acceptAll(): Promise<void>
rejectAll(): Promise<void>
// Lifecycle methods
applyPreferences(options): Promise<void>
show(screenType): Promise<void>
dismiss(): Promise<void>
}
// applyPreferences options:
// {
// intent: CONSENT_INTENT, // ACCEPT_ALL | REJECT_ALL | APPLY_CUSTOM
// vendors: { [id]: boolean }, // Vendor consent map (for APPLY_CUSTOM)
// purposes: { [id]: boolean }, // Purpose consent map (for APPLY_CUSTOM)
// autoDismiss: boolean // Auto-close CMP UI (default: true)
// }Constants
import {
BEHAVIORS,
LAYOUTS,
GRANULARITY,
CONSENT_INTENT,
SCREEN_TYPE
} from '@axeptio/cmp-agents';
// Behaviors
BEHAVIORS.ACCEPTABLE // User can accept
BEHAVIORS.REJECTABLE // User can reject
BEHAVIORS.CLOSABLE // CMP can be closed
BEHAVIORS.OBSTRUCTIVE // CMP blocks content
// Layouts
LAYOUTS.BANNER
LAYOUTS.POPUP
LAYOUTS.WIDGET
LAYOUTS.PAGE
// Granularity
GRANULARITY.NONE // No choice possible
GRANULARITY.GLOBAL // Accept/reject all only
GRANULARITY.VENDOR // Per-vendor choice
GRANULARITY.PURPOSE // Per-purpose choice
// Consent Intent (for applyPreferences)
CONSENT_INTENT.ACCEPT_ALL // Accept all vendors
CONSENT_INTENT.REJECT_ALL // Reject all vendors
CONSENT_INTENT.APPLY_CUSTOM // Apply vendor-specific choices
// Screen Type (for show)
SCREEN_TYPE.DEFAULT // Initial consent banner
SCREEN_TYPE.VENDORS // Vendor list view
SCREEN_TYPE.PURPOSES // Purpose/category view
SCREEN_TYPE.SETTINGS // Settings panelBrowser identity (@axeptio/cmp-agents/browser-identity)
A subpath export for code that needs to present the same browser this package does — the user agent, the client-hint brands and high-entropy hints, the platform, the languages, the timezone, the viewport and the request headers derived from them. Import it rather than restating the values: two declarations of which browser you are is drift waiting to happen.
import {
BROWSER_USER_AGENT,
BROWSER_USER_AGENT_METADATA,
BROWSER_REQUEST_HEADERS,
BROWSER_LOCALE,
BROWSER_TIMEZONE,
CHROME_IDENTITY
} from '@axeptio/cmp-agents/browser-identity';
CHROME_IDENTITY;
// { source: 'committed-snapshot', version: '152.0.7977.64', greaseBrand: ..., greaseBrandVersion: ... }Every value derives from one measurement of a real Google Chrome, committed as cli/lib/google-chrome.json and copied into dist/google-chrome.json by the build. Nothing resolves a browser at runtime, so the identity is fixed per release rather than per machine or per process: two runs of the same release present the same browser, and any run can be attributed to the build that froze it. The module reads that snapshot when it is imported and refuses to load if it is missing or malformed — there is no fallback identity.
Regenerate the snapshot from a real browser with the published CLI, which measures the Chrome installed on the machine and rewrites the file:
npx cmp-agents snapshotThe Playwright configuration this repository drives its own browser with is deliberately not exported: it is a harness, not an identity.
Adding a New Integration
Use the CLI to scaffold a new integration:
npm run cli:createOr create manually in src/integrations/yourCmp/:
import { CMP } from '../../cmp.js';
import { GRANULARITY, SCREEN_TYPE } from '../../constants.js';
export default class YourCMP extends CMP {
getName() {
return 'YourCMP';
}
getGranularity() {
return GRANULARITY.VENDOR;
}
getMatchingResources() {
return [/yourcmp\.com\/sdk/];
}
onTick() {
if (window.YourCMPSDK) {
this.trigger('detection');
}
}
async getVendors() {
return window.YourCMPSDK.getVendors().map(v => ({
name: v.name,
iabId: v.id
}));
}
async acceptAll() {
window.YourCMPSDK.acceptAll();
}
async rejectAll() {
window.YourCMPSDK.rejectAll();
}
retrieveConsent() {
this.consent = window.YourCMPSDK.hasConsent();
this.consentDate = window.YourCMPSDK.getConsentDate();
}
// Override for granular consent support
async _applyCustomConsent(options = {}) {
const { vendors = {}, purposes = {} } = options;
Object.entries(vendors).forEach(([id, enabled]) => {
window.YourCMPSDK.setVendorConsent(id, enabled);
});
Object.entries(purposes).forEach(([id, enabled]) => {
window.YourCMPSDK.setPurposeConsent(id, enabled);
});
window.YourCMPSDK.save();
}
// Override for UI control
async show(screenType = SCREEN_TYPE.DEFAULT) {
window.YourCMPSDK.open();
}
async dismiss() {
window.YourCMPSDK.close();
}
}See CONTRIBUTING.md for detailed guidelines.
Testing
# Run unit tests (fast, no external dependencies)
npm run test:unit
# Run unit tests in watch mode
npm run test:unit:watch
# Run unit tests with coverage
npm run test:unit:coverage
# Run E2E tests (requires build first)
npm test
# Run E2E tests for a specific CMP
npx playwright test --config=test/config.js -g "CookieBot"
# Run in complete mode (all test websites + extended flow tests)
TEST_MODE=complete npm testAn E2E run is two passes. The first runs in parallel with the configured retries. If anything
is still failing, the second re-runs exactly those tests one at a time, once each, spaced five
to ten seconds apart, and records the network activity of every attempt. A site that only ever
failed under contention passes there and the run is green; one that fails again comes back with
a HAR. The second pass writes results-final-pass.json and playwright-report-final-pass/
beside the first pass's report rather than over it, and ./test/docker/run.sh --name <label>
labels all four.
Test Structure
test/
├── unit/ # Unit tests (Vitest + jsdom)
│ ├── cmp.unit.test.js # CMP base class tests
│ ├── tcf-cmp.unit.test.js # TCF_CMP class tests
│ ├── vendor.unit.test.js # Vendor class tests
│ ├── cmpDetector.unit.test.js # CMPDetector + create() factory tests
│ ├── consent-signals.unit.test.js # Consent signal detection tests
│ └── consentManager.unit.test.js # ConsentManager integration tests
├── common.js # Shared E2E test helpers
└── config.js # Playwright configurationSee the Testing Guide for detailed instructions.
Documentation
Guides
- Development Guide - Project setup, build, debug, and manual testing
- Testing Guide - Unit tests (Vitest) and E2E tests (Playwright)
- Contributing - How to contribute
- v2.0 Migration Guide - Upgrading from v1.x
Reference
- CMP Class Reference - Base class API and usage examples
- Constants Reference - Layouts, behaviors, granularity, consent intent, screen types
- Consent Signals - Google GCM v2, Shopify, Microsoft UET, Meta LDU, Pinterest
- Integration List - All supported CMPs and their capabilities
- CLI Usage - Command-line tools
- Glossary - CMP terminology and concepts
Internal
- CI/CD - Continuous integration and release process
Use Cases
1. Cookie Scanner (Shake)
Detect and analyze CMPs on websites during automated scans:
// In Playwright test
await page.addScriptTag({ path: 'dist/cmp-agents.js' });
await page.evaluate(() => {
const agent = CMPAgents.create();
window.cmpAgent = agent;
agent.run();
});
const cmp = await page.evaluate(() => window.cmpAgent.getCMP());
const vendors = await page.evaluate(() => window.cmpAgent.getCMP().getVendors());2. Privacy Assistant (Taste)
Auto-consent on behalf of users in a browser extension:
import { CONSENT_INTENT, BEHAVIORS } from '@axeptio/cmp-agents';
// Content script
const cmp = cmpDetector.getCMP();
if (cmp && cmp.getBehaviors().includes(BEHAVIORS.REJECTABLE)) {
// Reject all vendors
await cmp.applyPreferences({ intent: CONSENT_INTENT.REJECT_ALL });
}3. Granular Consent Management
Apply specific vendor preferences:
import { CONSENT_INTENT } from '@axeptio/cmp-agents';
const cmp = cmpDetector.getCMP();
const vendors = await cmp.getVendors();
// Build vendor consent map
const vendorConsents = {};
// Find and configure specific vendors
const metaVendor = vendors.find(v => v.name.includes('Meta'));
const analyticsVendor = vendors.find(v => v.name.includes('Analytics'));
if (metaVendor) vendorConsents[metaVendor.name] = false; // Deny Meta
if (analyticsVendor) vendorConsents[analyticsVendor.name] = true; // Allow Analytics
// Apply custom choices by passing vendors directly
await cmp.applyPreferences({
intent: CONSENT_INTENT.APPLY_CUSTOM,
vendors: vendorConsents
});4. UI Control for Testing
Open specific CMP views for manual testing or screenshots:
import { SCREEN_TYPE, CONSENT_INTENT } from '@axeptio/cmp-agents';
const cmp = cmpDetector.getCMP();
// Open vendor list for inspection
await cmp.show(SCREEN_TYPE.VENDORS);
// Apply preferences but keep UI open
await cmp.applyPreferences({
intent: CONSENT_INTENT.APPLY_CUSTOM,
autoDismiss: false
});
// Take screenshot, then close
await cmp.dismiss();5. Research & Compliance
Analyze CMP configurations across websites:
const analysis = {
name: cmp.getName(),
layout: cmp.getLayout(),
behaviors: cmp.getBehaviors(),
granularity: cmp.getGranularity(),
vendorCount: (await cmp.getVendors()).length,
hasExistingConsent: cmp.hasExistingConsent()
};License
This project is licensed under a custom license. See LICENSE for details.
TL;DR: Free for CMP industry peers to use, modify, and contribute. Redistribution as a competing product requires permission.
Credits
Developed and maintained by Axeptio.

