walkeros-create-cmp-source

star 340

Use when creating a new walkerOS CMP (consent management platform) source. Structured fill-in-the-blanks workflow that turns any CMP's consent API into a walkerOS source package. Covers CookieFirst, Usercentrics, CookiePro/OneTrust patterns and generalizes to any CMP.

elbwalker By elbwalker schedule Updated 6/15/2026

name: walkeros-create-cmp-source description: Use when creating a new walkerOS CMP (consent management platform) source. Structured fill-in-the-blanks workflow that turns any CMP's consent API into a walkerOS source package. Covers CookieFirst, Usercentrics, CookiePro/OneTrust patterns and generalizes to any CMP.

Create a CMP Source

A CMP source is a specialized walkerOS source that listens to a consent management platform's events and translates consent states into elb('walker consent', state) calls.

Every CMP source follows the same skeleton with only 5-6 decision points that vary per CMP. This skill turns "build a new CMP source" into a structured fill-in-the-blanks workflow.

Prerequisites

Before starting, read these skills:

Supporting files

This skill includes reference files you can copy:

  • examples/ - Generic CMP consent examples to adapt for your CMP
    • inputs.ts - Consent input scenarios (full, partial, minimal, revocation)
    • outputs.ts - Expected walkerOS consent states after mapping
    • env.ts - Mock environment factories (createMockElb, createMockWindow)
  • templates/cmp/ - Complete CMP source implementation template
    • index.ts - Source implementation with detection paths + handleConsent
    • types.ts - Type definitions (Settings, CMP API, Types bundle)
    • test-utils.ts - Test utilities (MockWindow, createMockElb, createCmpSource)
    • index.test.ts - Test suite skeleton (8 describe blocks)

Note: These are generic templates for the skill. The actual CMP package's examples go in packages/web/sources/cmps/[name]/src/examples/.

Canonical template

CookieFirst at packages/web/sources/cmps/cookiefirst/ is the canonical template. Copy its structure for every new CMP source.

Note: The CookieFirst README and package.json predate this skill and are missing some sections required by mandatory check #10 (walkerOS.json, Type definitions, Related, Timing considerations) and the walkerOS-source keyword. New CMP sources MUST include all required sections. The CookieFirst package will be updated to match.

Process overview

1. Research     -> Fill in the CMP research template
2. Examples     -> Create input/output examples (full, partial, minimal, revocation, edge cases)
3. Mapping      -> Define category map with sensible defaults
4. Scaffold     -> Copy from CookieFirst template
5. Convention   -> walkerOS.json, buildDev
6. Test         -> Write tests FIRST (TDD): 25-32 tests across 8-9 describe blocks
7. Implement    -> Wire up detection paths + handleConsent + destroy
8. Document     -> README + update existing consent guide page

Phase 1: Research the CMP

Goal: Fill in every field of this template before writing any code.

CMP research template

Fill in ALL of these fields for the target CMP:

Field Description Example (CookieFirst)
CMP name Official product name CookieFirst
Global window object window.X shape and key properties window.CookieFirst.consent (boolean map)
SDK events Event names, CustomEvent detail structure cf_init (Event), cf_consent (CustomEvent with consent detail)
Callbacks/hooks Function wrapping patterns None
Category naming Human-readable vs opaque IDs Human-readable (necessary, functional, performance, advertising)
Consent access pattern Is consent read from a property (window.CMP.consent) or an API method (CMP.getConsent())? Property: window.CookieFirst.consent
Explicit consent detection API or mechanism to check if user actively chose consent === null means no explicit choice
"Already loaded" detection How to detect CMP loaded before source window.CookieFirst.consent exists and is non-null
Official docs URL Link to CMP's developer/API documentation CookieFirst Public API docs
npm packages / TS types Available type packages None (define own types)
Timing constraints Does the source need to load before/after the CMP? Any require config needed? No require -- consent sources should init immediately
Event registration mechanism How does the CMP register event listeners? addEventListener, callback assignment, SDK method? addEventListener (standard DOM events)
Cleanup/unsubscribe mechanism How to remove listeners on destroy? removeEventListener, nullify callback, SDK unsubscribe method? removeEventListener (standard DOM cleanup)
SDK readiness pattern How does the CMP signal its SDK is ready? DOM event, callback array, global flag, Promise? cf_init DOM event

Detection paths

Every CMP source needs up to 3 detection paths:

Path Purpose Questions to answer
Already loaded CMP loaded before source Is there a global object to check? What state does it expose? Is consent read from a property or an API method?
Init listener CMP loads after source What event/callback fires on SDK init? Is it a DOM event, callback assignment, or SDK readiness array?
Change listener User updates consent What event fires on consent change? Is it the same as init?

Event registration patterns

Not all CMPs use addEventListener. Fill in "Event registration mechanism" in the research template to determine which pattern applies:

Pattern CMPs Registration Cleanup
DOM events CookieFirst, CookiePro addEventListener(name, handler) removeEventListener(name, handler)
Callback assignment Cookiebot (onaccept) window.CMP.onaccept = handler window.CMP.onaccept = original
SDK readiness array Didomi (didomiOnReady) window.didomiOnReady.push(handler) No unsubscribe (fires once)
SDK method Didomi (on) CMP.on('consent.changed', handler) Vendor-specific (check docs)

This affects the source skeleton (Phase 7), MockWindow shape (Phase 6), and destroy implementation.

Decision matrix

Fill in this matrix for your CMP. Reference implementations for comparison:

Decision CookieFirst Usercentrics CookiePro
Already loaded? window.CookieFirst.consent None (events only) window.OneTrust + window.OptanonActiveGroups
Init listener cf_init event Same as change event (ucEvent) OptanonWrapper callback (self-unwrap)
Change listener cf_consent event ucEvent / UC_SDK_EVENT OneTrustGroupsUpdated event
Consent shape Boolean map { category: bool } Mixed object (groups + services) Comma-separated IDs ,C0001,C0003,
Category naming Human-readable Admin-configured Opaque IDs (C0001-C0005)
Explicit check consent === null Official consent metadata (V3 consent.type; V2 an EXPLICIT entry in service consent history) IsAlertBoxClosed()
Default categoryMap Populated (human names to walkerOS) Empty (pass-through) Populated (opaque IDs need mapping)
Number of change events 1 (cf_consent) 1 (ucEvent) 2 (OptanonWrapper + OneTrustGroupsUpdated)
Consent layers Single (categories only) Dual (groups + services) Single (categories only)
Consent access Property (window.CookieFirst.consent) Event detail (event.detail) Property (window.OptanonActiveGroups)
Event registration addEventListener addEventListener Callback assignment + addEventListener

Gate: Research complete

  • All fields in the research template filled
  • Detection paths identified (which of the 3 apply)
  • Decision matrix row completed for the new CMP
  • Official docs URL captured

Phase 2: Create examples

Goal: Define realistic consent data BEFORE writing implementation.

Required example inputs

Create at minimum these scenarios in src/examples/inputs.ts. See inputs.ts for the generic template.

Example Purpose Description
fullConsent All categories accepted User clicked "Accept All"
partialConsent Some categories accepted User customized consent
minimalConsent Only essential/necessary User clicked "Deny All" or similar
implicitConsent Page-load defaults (if applicable) CMP loaded but user hasn't chosen. Note: behavior varies by CMP -- some grant nothing by default, others grant functional/necessary. Research the specific CMP's default consent state.
noConsent No consent data available CMP hasn't loaded yet
revocationInput Consent withdrawal User goes from full to partial

Add CMP-specific edge cases:

  • Case sensitivity: Uppercase/lowercase variants of consent fields
  • Service-level consent: If CMP supports individual service booleans
  • Custom categories: Non-standard category IDs
  • Empty/malformed data: Edge cases for the consent shape

Required example outputs

Create expected walkerOS consent states in src/examples/outputs.ts. See outputs.ts for the generic template.

Key rule: denied groups MUST have explicit false, not be omitted (see mandatory check #1).

Example env

Create mock factories in src/examples/env.ts. See env.ts for the generic template.

Export via dev.ts

// src/dev.ts
export * as examples from './examples';

Gate: Examples valid

  • All example files compile
  • Can trace: each input -> expected output for each example
  • Edge cases included (case sensitivity, empty data, revocation)

Phase 3: Define category mapping

Goal: Decide default categoryMap and document mapping rationale.

When to use a populated default map

Use a populated default when the CMP uses opaque or non-standard category names that are meaningless without mapping:

// CookiePro: opaque IDs require mapping
export const DEFAULT_CATEGORY_MAP: Record<string, string> = {
  C0001: 'functional', // Strictly Necessary
  C0002: 'analytics', // Performance
  C0003: 'functional', // Functional
  C0004: 'marketing', // Targeting
  C0005: 'marketing', // Social Media
};

When to use an empty default map

Use an empty default when the CMP uses human-readable, admin-configured category names that can pass through as-is:

// Usercentrics: admin-configured names pass through
const settings: Settings = {
  categoryMap: config?.settings?.categoryMap ?? {},
};

Merging behavior

Custom entries merge with (and override) defaults:

const mergedCategoryMap = {
  ...DEFAULT_CATEGORY_MAP,
  ...(config?.settings?.categoryMap ?? {}),
};

OR logic for many-to-one mappings

When multiple CMP categories map to the same walkerOS group, use OR logic: if ANY source category is true, the target group is true.

// OR logic: once true, stays true
state[mapped] = state[mapped] || value;

Dual-layer consent (categories + services/vendors)

Some CMPs expose consent at multiple layers (e.g., Usercentrics: groups + services; Didomi: purposes + vendors). When the decision matrix shows "Consent layers: Dual," decide how to handle:

Option A: Map primary layer only (recommended for most cases). Use the category/purpose layer and ignore the service/vendor layer. This matches walkerOS's category-level WalkerOS.Consent model directly.

Option B: Expose both layers via settings. Add a setting like consentLayer: 'categories' | 'services' and map whichever the user chooses. Use this when the CMP's service-level consent is meaningfully different from its category-level consent.

Document the chosen approach in the README under "How it works."

Gate: Mapping defined

  • Default categoryMap chosen (populated or empty, with rationale)
  • OR logic for many-to-one mappings documented
  • Merge behavior with user overrides documented
  • Dual-layer strategy decided (if applicable per decision matrix)

Phase 4: Scaffold

Goal: Create package structure mirroring CookieFirst.

Directory structure

packages/web/sources/cmps/[name]/
├── package.json
├── tsconfig.json
├── tsup.config.ts
├── jest.config.mjs
├── README.md
└── src/
    ├── index.ts                    # Main source export
    ├── dev.ts                      # Dev exports (examples)
    ├── types/
    │   └── index.ts                # Types, Settings, CMP API interface, declare global
    ├── examples/
    │   ├── index.ts                # Re-exports
    │   ├── inputs.ts               # CMP consent input examples
    │   ├── outputs.ts              # Expected walkerOS consent outputs
    │   └── env.ts                  # Mock factories
    └── __tests__/
        ├── index.test.ts           # Full test suite
        └── test-utils.ts           # createMockElb, createMockWindow, createSource

package.json template

{
  "name": "@walkeros/web-source-cmp-[name]",
  "description": "[CMP Name] consent management source for walkerOS",
  "version": "1.0.0",
  "license": "MIT",
  "walkerOS": { "type": "source", "platform": "web" },
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.js"
    },
    "./examples": {
      "types": "./dist/examples/index.d.ts",
      "import": "./dist/examples/index.mjs",
      "require": "./dist/examples/index.js"
    },
    "./dev": {
      "types": "./dist/dev.d.ts",
      "import": "./dist/dev.mjs",
      "require": "./dist/dev.js"
    }
  },
  "files": ["dist/**"],
  "scripts": {
    "build": "tsup --silent",
    "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
    "dev": "jest --watchAll --colors",
    "lint": "tsc && eslint \"**/*.ts*\"",
    "test": "jest",
    "update": "npx npm-check-updates -u && npm update"
  },
  "dependencies": {
    "@walkeros/core": "1.0.0",
    "@walkeros/collector": "1.0.0"
  },
  "repository": {
    "url": "git+https://github.com/elbwalker/walkerOS.git",
    "directory": "packages/web/sources/cmps/[name]"
  },
  "author": "elbwalker <hello@elbwalker.com>",
  "keywords": [
    "walker",
    "walkerOS",
    "walkerOS-source",
    "source",
    "web",
    "[name]",
    "consent",
    "cmp"
  ]
}

Config files

Copy these exactly from CookieFirst, updating only the globalName:

  • tsconfig.json - extends @walkeros/config/tsconfig/web.json
  • tsup.config.ts - uses buildModules, buildExamples, buildBrowser, buildES5
  • jest.config.mjs - extends @walkeros/config/jest/web.config

Phase 5: walkerOS.json convention

Add walkerOS field to package.json

{ "walkerOS": { "type": "source", "platform": "web" } }

Use buildDev() in tsup.config.ts

Use the standard buildDev() helper from @walkeros/config/tsup (consistent with create-source and create-destination skills):

import { buildDev } from '@walkeros/config/tsup';
// In defineConfig array:
buildDev(),

Note: The CookieFirst template uses buildModules({ entry: ['src/dev.ts'] }) instead. New CMP sources should prefer buildDev() for consistency.

Hints (Optional)

If your CMP source has non-obvious behaviors or troubleshooting patterns, add hints. See walkeros-writing-documentation skill for full guidelines and walkeros-create-source skill for the hints pattern.

Gate: Convention met

  • walkerOS field in package.json with type and platform
  • Dev build configured in tsup.config.ts
  • Keywords include walkerOS and walkerOS-source

Phase 6: Test (TDD -- write tests BEFORE implementation)

Goal: Write the full test suite first. Watch it fail. Then implement.

Test structure (8-9 describe blocks)

Describe block Tests What it covers
initialization 4-6 No errors, correct type, default settings, custom settings, listener registration
explicit consent filtering 3-4 Explicit events processed, implicit ignored/processed based on setting, case-insensitive type
non-consent event filtering 2 Non-consent events ignored, events without detail ignored
category mapping 5-8 Full/partial/minimal consent, custom mapping, unmapped categories, OR logic
[CMP-specific parsing] 2-4 CMP-specific consent format parsing (service-level, string parsing, etc.)
event handling 3 Consent changes, multiple changes, revocation
consent revocation 2 Full->partial, full->minimal (explicit false values verified)
cleanup 2 Destroy removes listeners, restores wrapped functions
no window environment 1 Handles missing window gracefully

Test utilities pattern (MockWindow)

See test-utils.ts for the complete template including MockWindow interface, createMockElb, createMockWindow, and createCmpSource factories.

For CMPs that use callback assignment or SDK methods instead of addEventListener (e.g., Didomi's onReady, Cookiebot's onaccept), adapt MockWindow to expose those callbacks as testable properties.

Test template

Use the test template: index.test.ts

Gate: Tests fail for the right reason

  • Tests fail with "Cannot find module '../index'" (module doesn't exist yet)
  • Tests use example outputs for assertions (not hardcoded values)
  • Consent revocation test included (full grant -> revoke -> verify explicit false)

Phase 7: Implement

Goal: Wire up detection paths, handleConsent, and destroy. Make tests pass.

v4 source identity

CMP sources register their type literal via SourceMap in src/types.ts. CMPs are web by default - platform is optional in the augmentation:

import type { Source } from '@walkeros/core';

declare module '@walkeros/core' {
  interface SourceMap {
    // Replace with the CMP's package-level identifier.
    cookiefirst: {
      type: 'cookiefirst';
      platform?: 'web'; // optional: CMP runs on web by default
    };
  }
}

The source returns { type: 'cookiefirst', config, push } from Source.Init

  • the type literal must match the SourceMap key.

CMP sources do NOT set source.url or source.referrer. A CMP only knows about consent state, not the page that produced it. Setting page context belongs to a future web-context transformer that runs in the source's next chain. Don't reach for window.location in CMP source code, leave those fields untouched.

Settings interface pattern

See types.ts for the complete type definitions including Settings, InitSettings, Types bundle, CMP API interfaces, and declare global window augmentation.

Every CMP source has these core settings:

  • categoryMap?: Record<string, string> -- map CMP categories to walkerOS consent groups
  • explicitOnly?: boolean -- only process explicit consent (default: true)
  • globalName?: string -- CMP-specific: global object name

Source skeleton

Every CMP source follows this skeleton with 5-6 decision points. See index.ts for the complete template.

Key implementation steps:

  1. Resolve window (env.window fallback to globalThis)
  2. Merge settings with defaults
  3. Track listener references for cleanup
  4. handleConsent function (explicitOnly, categoryMap with OR logic, elb('walker consent', state))
  5. Detection path: Already loaded
  6. Detection path: Init listener
  7. Detection path: Change listener

The factory MUST be side-effect-free; do the detection paths in init(). Steps 5-7 attach listeners and perform the "already loaded" static consent read, which emits elb('walker consent', state). Put them inside an init() method on the returned instance, NOT in the factory body. The collector runs the factory in Pass 1 (before all sources are merged) and init() in Pass 2. Emitting consent from the factory races source merge order and can leave a later require:["consent"] source parked. Return { type, config, push, init, destroy }; init runs the static read + attaches listeners, destroy removes them. (The collector also matches require against current recorded state, so a consent read from init() still activates dependent steps regardless of order — but a side-effect-free factory is the contract.)

Note on init listeners: Some init listeners (like CookieFirst's cf_init) read consent from the global window object, not from event.detail. Others (like cf_consent) receive consent via event.detail. Check which pattern your CMP uses for each detection path.

Gate: All tests pass

  • npm run verify:touched -- <cmp-name> passes (L1: typecheck + lint + test)
  • npm run build passes

Phase 8: Document

README structure

Follow this structure (sentence case headings, imports in code examples):

# @walkeros/web-source-cmp-[name]

[CMP Name] consent management source for walkerOS.

Source Code | NPM | Documentation

## Installation

## Usage (with imports)

## Configuration

### Settings (table)

### Default category mapping (if applicable)

### Custom mapping example

## How it works (numbered list of detection paths)

### Timing considerations

## [CMP] API reference (links to CMP docs)

## walkerOS.json

## Type definitions

## Related (links to consent guide)

## License

Update consent guide page

Update website/docs/guides/consent/examples/[cmp-name].mdx to recommend the source package first, preserving the manual snippet as a fallback. Follow the Usercentrics page (usercentrics.mdx) as the reference pattern:

  1. "Using the source package (recommended)" heading first -- install command, usage code with imports, bullet list of what the source handles
  2. "Manual event listener" heading -- preserve the existing manual snippet as a fallback option

Mandatory checks

These are non-negotiable patterns every CMP source MUST follow. Violating any of these is a privacy compliance issue or a correctness bug.

1. Ensure every consent update includes explicit false for denied groups

The collector uses merge semantics (assign()), so omitting a key means "no change," NOT "denied." Every consent state passed to elb('walker consent', state) must include explicit false for denied groups, not just true for granted ones.

Boolean-map CMPs (CookieFirst, Usercentrics group-level): The CMP's consent object already contains explicit false values (e.g., { marketing: false, functional: true }). These flow through naturally via iteration -- no extra code needed.

Presence-based CMPs (CookiePro): Only active groups are listed (e.g., ",C0001,C0003,"). Absence means denied. You MUST initialize ALL mapped groups to false, then set active ones to true:

// Presence-based CMPs: initialize all groups to false, then set active to true
allMappedGroups.forEach((group) => {
  state[group] = false;
});
activeIds.forEach((id) => {
  if (map[id]) state[map[id]] = true;
});

2. Case-insensitive comparison for all CMP string fields

Use .toLowerCase(). CMP docs are inconsistent on casing.

// Explicit type check
if (settings.explicitOnly && detail.type?.toLowerCase() !== 'explicit') return;

// Category ID lookup
const mapped = normalizedMap[id.toLowerCase()];

3. Apply categoryMap consistently in ALL code paths

If there are multiple parsing branches (group-level vs service-level, or already-loaded vs event-listener), mapping MUST work identically in each.

4. Prevent dual-firing on consent events

Many CMPs fire multiple signals for the same consent action. If you don't guard against this, handleConsent fires twice per user action. Three known patterns:

The collector's exactly-once delivery does not solve this for you: it guarantees each subscriber sees a given state version once, but two upstream CMP signals (a callback plus a DOM event) are two distinct consent commands, so both are delivered legitimately. Coalescing duplicate upstream signals is the source's job, as below.

Pattern A: Callback + event (CookiePro) CMP fires both a callback (OptanonWrapper) and a DOM event (OneTrustGroupsUpdated) on the same action. Use the callback for init only and self-unwrap after first call:

actualWindow.OptanonWrapper = () => {
  if (originalWrapper) originalWrapper();
  handleConsent();
  actualWindow.OptanonWrapper = originalWrapper; // Self-unwrap
};

Pattern B: Multiple change events (Cookiebot) CMP fires separate events for accept, decline, and revoke. Each event carries the full consent state. Register the SAME handler for all change events -- no special dedup needed, but be aware of the multiplicity.

Pattern C: Init event re-fires on change (Usercentrics) Single event type (ucEvent) fires for both init and change. No dual-firing risk, but the init/change distinction must come from event detail (e.g., detail.type), not event name.

Research step: During Phase 1, fill in "Number of change events" in the decision matrix. If >1, determine which pattern applies and plan accordingly.

5. Document timing/race conditions in the README

What if the CMP loads before the source? What about explicitOnly: false? Each CMP has different timing behavior -- document it explicitly under "Timing considerations."

For "previous-choice" detection (was this an active user decision or a first-visit default?), read the CMP's official consent metadata, the consent type or decision history (e.g. Usercentrics V3 consent.type, V2 the service consent history), not a per-pageload event type field. A per-pageload event type conflates first-visit defaults with a returning visitor's restored choice, so a returning visitor's prior consent would be dropped under explicitOnly.

6. Test consent revocation end-to-end

Full grant -> revoke -> verify denied (explicit false values). This is the most common source of bugs.

test('handles consent withdrawal', async () => {
  // Initial: full consent
  mockWindow.__dispatchEvent('event', inputs.fullConsent);
  expect(consentCalls[0].consent).toEqual(outputs.fullConsentMapped);

  // User revokes marketing
  mockWindow.__dispatchEvent('event', inputs.partialConsent);
  expect(consentCalls[1].consent.marketing).toBe(false); // explicit false
});

7. Use MockWindow interface in tests

Properly typed, not as unknown as casts scattered through tests. Define one MockWindow interface in test-utils.ts that extends the source's narrowed CmpWindow (the surface the source actually touches), not the global Window. Because Env.window is narrowed to CmpWindow, the mock literal satisfies it directly — no as unknown as Window anywhere.

8. Store category keys in user-expected format

Normalize during init for case-insensitive lookup, but store the original keys in the config so users see what they configured.

// Store original casing in config
const mergedCategoryMap = { ...DEFAULT_CATEGORY_MAP, ...userMap };

// Build normalized lookup for internal use
const normalizedMap: Record<string, string> = {};
Object.entries(mergedCategoryMap).forEach(([key, value]) => {
  normalizedMap[key.toLowerCase()] = value;
});

9. walkerOS.json convention

Add "walkerOS": { "type": "source", "platform": "web" } to package.json.

10. README requirements

Must include: Source Code/NPM/Documentation links, walkerOS.json section, Type definitions section, Related section, License section, sentence case headings, imports in all code examples, timing considerations section.


Validation checklist

Beyond understanding-development requirements (build, test, lint, no any):

  • All 10 mandatory checks pass
  • Research template fully filled
  • Decision matrix row complete
  • Examples include revocation and edge cases
  • Tests: 25-32 tests across 8-9 describe blocks
  • MockWindow interface in test-utils (not scattered casts)
  • Category mapping uses OR logic for many-to-one
  • Consent state always includes explicit false for denied groups
  • destroy() cleans up ALL listeners and restores wrapped functions
  • README follows required structure
  • Consent guide page updated

Known limitations

The skill's source skeleton and code templates are based on DOM-event CMPs (CookieFirst, Usercentrics, CookiePro). CMPs that deviate significantly from this pattern will require adaptation:

Limitation Affected CMPs Workaround
Source skeleton assumes addEventListener Didomi (SDK array/method), Cookiebot (callback assignment) Use the "Event registration patterns" table in Phase 1 to identify the correct pattern, then adapt the skeleton's listener setup and destroy() accordingly.
Consent read via property, not API method Didomi (getCurrentUserStatus()) If consent is accessed via an API method call rather than a window property, wrap the call in handleConsent and adjust the "already loaded" detection path.
destroy() may not be possible CMPs with SDK readiness arrays (fire-once, no unsubscribe) Document in README that the init callback cannot be removed. Only the change listener needs cleanup.
No vendor-level consent model Didomi (purposes + vendors as separate consent layers) Use the "Dual-layer consent" guidance in Phase 3. walkerOS Consent is category-level; vendor-level consent requires flattening or a consentLayer setting.

These are research-phase decisions -- the skill's phases, mandatory checks, and validation checklist still apply. The research template and decision matrix capture these variations so they are identified early.


Reference files

What Where
Skill examples examples/
Skill templates templates/cmp/
Canonical template packages/web/sources/cmps/cookiefirst/
Source types packages/core/src/types/source.ts
Consent guide website/docs/guides/consent/
Usercentrics plan docs/plans/2026-02-15-usercentrics-source.md
CookiePro plan docs/plans/2026-02-15-cookiepro-source.md

Related skills

Install via CLI
npx skills add https://github.com/elbwalker/walkerOS --skill walkeros-create-cmp-source
Repository Details
star Stars 340
call_split Forks 20
navigation Branch main
article Path SKILL.md
More from Creator