@walkeros/web-source-cmp-usercentrics 4.2.0 → 4.2.1-next-1781526381392
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +36 -3
- package/dist/dev.d.mts +69 -50
- package/dist/dev.d.ts +69 -50
- package/dist/dev.js +1 -1
- package/dist/dev.js.map +1 -1
- package/dist/dev.mjs +1 -1
- package/dist/dev.mjs.map +1 -1
- package/dist/examples/index.d.mts +65 -43
- package/dist/examples/index.d.ts +65 -43
- package/dist/examples/index.js +101 -180
- package/dist/examples/index.mjs +97 -172
- package/dist/index.browser.js +1 -1
- package/dist/index.d.mts +25 -16
- package/dist/index.d.ts +25 -16
- package/dist/index.es5.js +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/walkerOS.json +439 -140
- package/package.json +3 -3
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/parseConsent.ts","../src/lib/v2.ts","../src/lib/v3.ts","../src/types/index.ts","../src/index.ts"],"sourcesContent":["import type { WalkerOS } from '@walkeros/core';\nimport type { UsercentricsEventDetail, Settings } from '../types';\n\n/** Reserved keys in event.detail that are not service names */\nexport const RESERVED_KEYS = ['action', 'event', 'type', 'ucCategory'];\n\n/**\n * Determine if ucCategory represents group-level consent.\n * Group-level: all values are booleans.\n * Service-level: some values are non-boolean (e.g., 'partial').\n */\nexport function isGroupLevel(\n ucCategory: Record<string, boolean | unknown>,\n): boolean {\n return Object.values(ucCategory).every((val) => typeof val === 'boolean');\n}\n\n/**\n * Merge an incoming consent signal into the accumulating state using strict\n * AND semantics: absent key initialises from the incoming value; present key\n * becomes `true` only if both existing and incoming are `true`. Any single\n * `false` anywhere in the merge chain revokes consent for that key.\n *\n * This matches the consent rule of thumb: any deny signal denies. Permissive\n * (OR) and \"last-wins\" semantics are both unsafe for a privacy primitive.\n */\nfunction mergeConsent(\n state: WalkerOS.Consent,\n key: string,\n value: boolean,\n): void {\n state[key] = state[key] === undefined ? value : Boolean(state[key]) && value;\n}\n\n/**\n * Parse consent from Usercentrics event detail.\n *\n * Group-level: ucCategory values are all booleans → use them directly.\n * Service-level: ucCategory has non-booleans → mix boolean ucCategory entries\n * with top-level service keys from event.detail.\n *\n * categoryMap is applied in both modes to ucCategory boolean entries.\n * Strict AND: if multiple source categories map to the same target group,\n * the target is `true` only when ALL contributing sources are `true`.\n */\nexport function parseConsent(\n detail: UsercentricsEventDetail,\n settings: Settings,\n): WalkerOS.Consent {\n const state: WalkerOS.Consent = {};\n\n if (detail.ucCategory && isGroupLevel(detail.ucCategory)) {\n Object.entries(detail.ucCategory).forEach(([category, value]) => {\n if (typeof value !== 'boolean') return;\n const mapped = settings.categoryMap?.[category] ?? category;\n mergeConsent(state, mapped, value);\n });\n } else {\n if (detail.ucCategory) {\n Object.entries(detail.ucCategory).forEach(([key, value]) => {\n if (typeof value === 'boolean') {\n const mapped = settings.categoryMap?.[key] ?? key;\n mergeConsent(state, mapped, value);\n }\n });\n }\n\n Object.entries(detail).forEach(([key, value]) => {\n if (RESERVED_KEYS.includes(key)) return;\n if (typeof value !== 'boolean') return;\n const normalized = key.toLowerCase().replace(/ /g, '_');\n mergeConsent(state, normalized, value);\n });\n }\n\n return state;\n}\n","import type { Elb, Logger } from '@walkeros/core';\nimport type {\n Settings,\n UsercentricsEventDetail,\n UsercentricsV2Service,\n} from '../types';\nimport { parseConsent } from './parseConsent';\n\nexport interface V2AdapterContext {\n window: Window & typeof globalThis;\n elb: Elb.Fn;\n settings: Settings;\n logger: Logger.Instance;\n}\n\n/**\n * Aggregate V2 service array into a group-level ucCategory object using strict\n * AND logic. A category is `true` only if EVERY service in that category is\n * accepted. Any single denied service denies the whole category — this\n * matches the consent rule of thumb: any deny signal denies.\n */\nfunction aggregateByCategory(\n services: UsercentricsV2Service[],\n): Record<string, boolean> {\n const categories: Record<string, boolean> = {};\n services.forEach((service) => {\n const category = service.categorySlug;\n const status = service.consent?.status ?? false;\n categories[category] =\n categories[category] === undefined\n ? status\n : categories[category] && status;\n });\n return categories;\n}\n\n/**\n * Build a synthetic UsercentricsEventDetail from the V2 static API.\n * Marked as 'implicit' because UC_UI.isInitialized() === true does NOT prove\n * the read was user-initiated — it only proves the SDK has finished loading.\n * With settings.explicitOnly = true (the default), consumers correctly drop\n * this detail. Set explicitOnly = false to surface the static snapshot.\n */\nfunction buildDetailFromStatic(\n services: UsercentricsV2Service[],\n): UsercentricsEventDetail {\n return {\n event: 'consent_status',\n type: 'implicit',\n ucCategory: aggregateByCategory(services),\n };\n}\n\n/**\n * Set up the V2 adapter: listens on the configured event AND performs a\n * static read if UC_UI is already initialized.\n *\n * Returns a cleanup function that removes the event listener.\n */\nexport function setupV2Adapter(ctx: V2AdapterContext): () => void {\n const { window: win, elb, settings, logger } = ctx;\n const eventName = settings.eventName ?? 'ucEvent';\n\n const handleDetail = (detail: UsercentricsEventDetail) => {\n logger.debug('event received', detail);\n\n if (detail.event !== 'consent_status') return;\n if (settings.explicitOnly && detail.type?.toLowerCase() !== 'explicit')\n return;\n\n const state = parseConsent(detail, settings);\n if (Object.keys(state).length > 0) {\n elb('walker consent', state);\n }\n };\n\n const listener = (e: Event) => {\n const custom = e as CustomEvent<UsercentricsEventDetail>;\n if (custom.detail) handleDetail(custom.detail);\n };\n win.addEventListener(eventName, listener);\n\n // Static check: if UC_UI is already initialized, read current consent now.\n const api = win.UC_UI;\n if (api?.isInitialized?.() && api.getServicesBaseInfo) {\n const services = api.getServicesBaseInfo();\n if (services.length > 0) {\n handleDetail(buildDetailFromStatic(services));\n }\n }\n\n return () => {\n win.removeEventListener(eventName, listener);\n };\n}\n","import type { Elb, Logger } from '@walkeros/core';\nimport type {\n Settings,\n UsercentricsEventDetail,\n UsercentricsV3Api,\n UsercentricsV3CategoryState,\n UsercentricsV3CmpEventDetail,\n UsercentricsV3ConsentDetails,\n} from '../types';\nimport { parseConsent } from './parseConsent';\n\nexport interface V3AdapterContext {\n window: Window & typeof globalThis;\n elb: Elb.Fn;\n settings: Settings;\n logger: Logger.Instance;\n}\n\nconst V3_DEFAULT_EVENT_NAME = 'UC_UI_CMP_EVENT';\n\n/**\n * V3 CMP event types that represent a user consent decision. Presentation\n * events like `CMP_SHOWN` or `UI_INITIALIZED` must NOT trigger a consent\n * publish — they carry no user decision.\n */\nconst V3_DECISION_TYPES: ReadonlySet<string> = new Set([\n 'ACCEPT_ALL',\n 'DENY_ALL',\n 'SAVE',\n]);\n\n/**\n * Map a V3 CategoryData.state value to a walkerOS boolean using strict\n * semantics: only fully-accepted categories are `true`. Any partial or\n * denied state (including future/unknown states) resolves to `false`.\n *\n * This matches the consent rule of thumb: any deny signal denies.\n */\nfunction categoryStateToBoolean(state: UsercentricsV3CategoryState): boolean {\n return state === 'ALL_ACCEPTED';\n}\n\n/**\n * Build a synthetic UsercentricsEventDetail from V3 ConsentDetails.\n * Translates V3's structured consent state into the shape parseConsent expects.\n */\nfunction buildDetailFromV3(\n details: UsercentricsV3ConsentDetails,\n): UsercentricsEventDetail {\n const ucCategory: Record<string, boolean> = {};\n Object.entries(details.categories).forEach(([slug, category]) => {\n ucCategory[slug] = categoryStateToBoolean(category.state);\n });\n\n return {\n event: 'consent_status',\n type: details.consent.type === 'EXPLICIT' ? 'explicit' : 'implicit',\n ucCategory,\n };\n}\n\n/**\n * Set up the V3 adapter: registers a listener on the V3 consent event AND\n * (if __ucCmp is already initialized) performs a static read.\n *\n * Both paths re-fetch getConsentDetails() because the event payload does\n * not include consent state.\n *\n * Returns a cleanup function that removes the event listener.\n */\nexport async function setupV3Adapter(\n ctx: V3AdapterContext,\n): Promise<() => void> {\n const { window: win, elb, settings, logger } = ctx;\n const eventName = settings.v3EventName ?? V3_DEFAULT_EVENT_NAME;\n\n const publishConsent = async (cmp: UsercentricsV3Api): Promise<void> => {\n const details = await cmp.getConsentDetails();\n const detail = buildDetailFromV3(details);\n logger.debug('event received', detail);\n\n if (settings.explicitOnly && detail.type !== 'explicit') return;\n\n const state = parseConsent(detail, settings);\n if (Object.keys(state).length > 0) {\n elb('walker consent', state);\n }\n };\n\n const listener = (event: Event): void => {\n const cmp = win.__ucCmp;\n if (!cmp) return;\n const custom = event as CustomEvent<UsercentricsV3CmpEventDetail>;\n const detail = custom.detail;\n // Filter to decision events only: the CMP emits a broad set of events\n // (CMP_SHOWN, UI_INITIALIZED, VIEW_CHANGED, …) on this bus. Without\n // filtering, every view toggle would re-publish consent.\n if (!detail || detail.source !== 'CMP') return;\n if (!detail.type || !V3_DECISION_TYPES.has(detail.type)) return;\n publishConsent(cmp).catch(() => {\n // Swallow — a failed V3 fetch should not break the page.\n });\n };\n win.addEventListener(eventName, listener);\n\n // Static check: if __ucCmp is already initialized, fetch now.\n // Wrapped in try/catch so transient API failures during setup don't\n // reject setupV3Adapter() and tear down the whole flow — the listener\n // stays registered and will pick up the next real event.\n const cmp = win.__ucCmp;\n if (cmp) {\n try {\n const initialized = await cmp.isInitialized();\n if (initialized) {\n await publishConsent(cmp);\n }\n } catch (err) {\n logger.warn('v3 static consent read failed', err);\n }\n }\n\n return () => {\n win.removeEventListener(eventName, listener);\n };\n}\n","import type { Source, Elb } from '@walkeros/core';\n\ndeclare module '@walkeros/core' {\n interface SourceMap {\n usercentrics: { type: 'usercentrics'; platform?: 'web' };\n }\n}\n\n/**\n * Usercentrics consent event detail structure.\n *\n * Fired as event.detail on the configured window event (e.g. 'ucEvent').\n * Contains both category-level (ucCategory) and service-level consent.\n */\nexport interface UsercentricsEventDetail {\n /** Always 'consent_status' for consent events */\n event: string;\n /** 'explicit' when user actively chose, 'implicit' for page-load defaults (casing may vary) */\n type: string;\n /** Action taken: 'onAcceptAllServices', 'onDenyAllServices', 'onUpdateServices' */\n action?: string;\n /** Category-level consent booleans (e.g. { marketing: true, functional: false }) */\n ucCategory?: Record<string, boolean | unknown>;\n /** Service-level consent as top-level keys (e.g. 'Google Analytics': true) */\n [service: string]: unknown;\n}\n\ndeclare global {\n interface WindowEventMap {\n ucEvent: CustomEvent<UsercentricsEventDetail>;\n UC_UI_CMP_EVENT: CustomEvent<UsercentricsV3CmpEventDetail>;\n }\n}\n\n/**\n * Usercentrics V2 service info shape returned by `UC_UI.getServicesBaseInfo()`.\n * Only the fields we use are typed - minimal surface, not a full V2 API mirror.\n */\nexport interface UsercentricsV2Service {\n /** Category slug: 'essential' | 'functional' | 'marketing' | custom */\n categorySlug: string;\n /** Consent state for this service */\n consent: {\n status: boolean;\n };\n}\n\n/**\n * Usercentrics V2 window API (`window.UC_UI`).\n *\n * Methods are synchronous (unlike V3). All methods are optional because\n * Usercentrics does not guarantee every deployment exposes every method.\n */\nexport interface UsercentricsV2Api {\n isInitialized?: () => boolean;\n getServicesBaseInfo?: () => UsercentricsV2Service[];\n areAllConsentsAccepted?: () => boolean;\n}\n\n/**\n * Usercentrics V3 category state. The SDK may add future states; we handle\n * unknowns conservatively as non-accepting.\n */\nexport type UsercentricsV3CategoryState =\n | 'ALL_ACCEPTED'\n | 'ALL_DENIED'\n | 'SOME_ACCEPTED'\n | 'NO_STATE'\n | string;\n\n/**\n * Minimal V3 CategoryData - only fields the adapter reads.\n */\nexport interface UsercentricsV3CategoryData {\n state: UsercentricsV3CategoryState;\n name: string;\n}\n\n/**\n * Minimal V3 ConsentData - only the `type` field is used to distinguish\n * explicit vs implicit consent. Other fields (status, version, etc.) exist on\n * the real SDK but are not read by this adapter.\n */\nexport interface UsercentricsV3ConsentData {\n type: 'EXPLICIT' | 'IMPLICIT' | string;\n}\n\n/**\n * Minimal V3 ConsentDetails - only the fields the adapter reads.\n * The real SDK also exposes `services`, but the adapter currently operates\n * at category level only.\n */\nexport interface UsercentricsV3ConsentDetails {\n consent: UsercentricsV3ConsentData;\n categories: Record<string, UsercentricsV3CategoryData>;\n}\n\n/**\n * Usercentrics V3 window API (`window.__ucCmp`).\n *\n * Only the methods this adapter calls are typed - the real SDK surface is\n * much wider but we intentionally keep this narrow.\n */\nexport interface UsercentricsV3Api {\n isInitialized: () => Promise<boolean>;\n getConsentDetails: () => Promise<UsercentricsV3ConsentDetails>;\n}\n\n/**\n * V3 CMP event detail. Fired on `UC_UI_CMP_EVENT` (or custom name).\n * `source: 'CMP'` + a decision `type` tells us a user action has been taken.\n */\nexport interface UsercentricsV3CmpEventDetail {\n source?: string;\n type?: string;\n}\n\ndeclare global {\n interface Window {\n /**\n * Usercentrics V2 CMP API. Attached once the V2 Browser SDK is\n * initialized. Optional because the SDK loads asynchronously - guard\n * with a truthiness check before access.\n */\n UC_UI?: UsercentricsV2Api;\n /**\n * Usercentrics V3 CMP API. Attached once the V3 Browser SDK is\n * initialized. Optional because the SDK loads asynchronously - guard\n * with a truthiness check before access.\n */\n __ucCmp?: UsercentricsV3Api;\n }\n}\n\n/**\n * Settings for Usercentrics source\n */\nexport interface Settings {\n /**\n * Window event name to listen for.\n * Configured in Usercentrics admin under Implementation > Data Layer & Events.\n * Can also be set to 'UC_SDK_EVENT' for the built-in Browser SDK event.\n *\n * Default: 'ucEvent'\n */\n eventName?: string;\n\n /**\n * Map Usercentrics categories to walkerOS consent groups.\n * Keys: Usercentrics category names (from ucCategory)\n * Values: walkerOS consent group names\n *\n * Applied in both group-level and service-level consent modes.\n * When multiple source categories map to the same group, strict AND logic\n * applies: ALL contributing source categories must be true for the target\n * group to be true. Any single false denies consent.\n *\n * Default: {} (pass through category names as-is)\n */\n categoryMap?: Record<string, string>;\n\n /**\n * Only process explicit consent (user made a choice).\n * When true: Ignores events where type !== 'explicit'\n * When false: Processes any consent_status event including implicit/defaults\n *\n * Default: true\n */\n explicitOnly?: boolean;\n\n /**\n * Which Usercentrics API to target.\n * - 'auto' (default): detect at init. If `window.__ucCmp` is present, use V3.\n * If only `window.UC_UI` is present, use V2. If neither is present yet,\n * register listeners for both so late-loading CMPs are still caught.\n * - 'v2': force legacy `window.UC_UI` path.\n * - 'v3': force current `window.__ucCmp` path.\n */\n apiVersion?: 'auto' | 'v2' | 'v3';\n\n /**\n * V3 window event name to listen for consent changes.\n *\n * Usercentrics V3 hardcodes its built-in event names (`UC_UI_CMP_EVENT`,\n * `UC_UI_INITIALIZED`, `UC_UI_VIEW_CHANGED`, `UC_CONSENT`) - they cannot be\n * renamed. However, the Usercentrics admin dashboard (Implementation >\n * Data Layer & Events) lets admins configure an ADDITIONAL custom window\n * event. Use this setting to point at that custom event name if\n * configured; otherwise leave as default.\n *\n * Default: 'UC_UI_CMP_EVENT'\n */\n v3EventName?: string;\n}\n\n/**\n * User input settings (all optional)\n */\nexport type InitSettings = Partial<Settings>;\n\n/**\n * No mapping configuration for this source\n */\nexport interface Mapping {}\n\n/**\n * Push function type - uses elb for consent commands\n */\nexport type Push = Elb.Fn;\n\n/**\n * Environment interface for Usercentrics source\n */\nexport interface Env extends Source.BaseEnv {\n window?: Window & typeof globalThis;\n}\n\n/**\n * Types bundle for Usercentrics source\n */\nexport type Types = Source.Types<Settings, Mapping, Push, Env, InitSettings>;\n\n/**\n * Config type alias\n */\nexport type Config = Source.Config<Types>;\n","import type { Source } from '@walkeros/core';\nimport type { Types, Settings } from './types';\nimport { setupV2Adapter } from './lib/v2';\nimport { setupV3Adapter } from './lib/v3';\n\n// Export types for external usage\nexport * as SourceUsercentrics from './types';\n\n/**\n * Usercentrics consent management source for walkerOS.\n *\n * Listens to Usercentrics CMP events and translates consent states to\n * walkerOS consent commands. Supports both the legacy V2 API\n * (`window.UC_UI` + `ucEvent`) and the current V3 API\n * (`window.__ucCmp` + `UC_UI_CMP_EVENT`).\n *\n * @example\n * ```typescript\n * import { sourceUsercentrics } from '@walkeros/web-source-cmp-usercentrics';\n *\n * await startFlow({\n * sources: {\n * consent: {\n * code: sourceUsercentrics,\n * config: {\n * settings: {\n * eventName: 'ucEvent',\n * categoryMap: {\n * essential: 'functional',\n * functional: 'functional',\n * marketing: 'marketing',\n * },\n * },\n * },\n * },\n * },\n * });\n * ```\n */\nexport const sourceUsercentrics: Source.Init<Types> = async (context) => {\n const { config, env } = context;\n const { elb, logger } = env;\n\n // Resolve window with fallback to globalThis\n const actualWindow =\n env.window ??\n (typeof globalThis.window !== 'undefined' ? globalThis.window : undefined);\n\n // Merge user settings with defaults so adapters always see a fully-resolved Settings\n const settings: Settings = {\n eventName: config?.settings?.eventName ?? 'ucEvent',\n categoryMap: config?.settings?.categoryMap ?? {},\n explicitOnly: config?.settings?.explicitOnly ?? true,\n apiVersion: config?.settings?.apiVersion ?? 'auto',\n v3EventName: config?.settings?.v3EventName ?? 'UC_UI_CMP_EVENT',\n };\n\n const fullConfig: Source.Config<Types> = { settings };\n const cleanups: Array<() => void> = [];\n\n // Attach the CMP listeners and perform the static consent read. Deferred to\n // init() (Pass 2 of initSources) so the factory (Pass 1) stays side-effect\n // free: no listener registration and, critically, no `elb('walker consent')`\n // emit during construction. Emitting from the factory races source merge\n // order and can leave a later `require:[\"consent\"]` source parked.\n const init = async (): Promise<void> => {\n if (!actualWindow) return;\n\n const adapterCtx = { window: actualWindow, elb, settings, logger };\n const apiVersion = settings.apiVersion ?? 'auto';\n\n if (apiVersion === 'v2') {\n cleanups.push(setupV2Adapter(adapterCtx));\n } else if (apiVersion === 'v3') {\n cleanups.push(await setupV3Adapter(adapterCtx));\n } else {\n // auto: detect which API is present\n if (actualWindow.__ucCmp) {\n cleanups.push(await setupV3Adapter(adapterCtx));\n } else if (actualWindow.UC_UI) {\n cleanups.push(setupV2Adapter(adapterCtx));\n } else {\n // Neither loaded yet — attach both listeners so whichever loads later is caught.\n cleanups.push(setupV2Adapter(adapterCtx));\n cleanups.push(await setupV3Adapter(adapterCtx));\n }\n }\n };\n\n return {\n type: 'usercentrics',\n config: fullConfig,\n push: elb,\n init,\n destroy: async () => {\n cleanups.forEach((fn) => fn());\n },\n };\n};\n\nexport default sourceUsercentrics;\n"],"mappings":"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","names":["_a","cmp","_a"]}
|
|
1
|
+
{"version":3,"sources":["../src/lib/parseConsent.ts","../src/lib/v2.ts","../src/lib/v3.ts","../src/types/index.ts","../src/index.ts"],"sourcesContent":["import type { WalkerOS } from '@walkeros/core';\nimport type { UsercentricsEventDetail, Settings } from '../types';\n\n/** Reserved keys in event.detail that are not service names */\nexport const RESERVED_KEYS = ['action', 'event', 'type', 'ucCategory'];\n\n/**\n * Determine if ucCategory represents group-level consent.\n * Group-level: all values are booleans.\n * Service-level: some values are non-boolean (e.g., 'partial').\n */\nexport function isGroupLevel(\n ucCategory: Record<string, boolean | unknown>,\n): boolean {\n return Object.values(ucCategory).every((val) => typeof val === 'boolean');\n}\n\n/**\n * Merge an incoming consent signal into the accumulating state using strict\n * AND semantics: absent key initialises from the incoming value; present key\n * becomes `true` only if both existing and incoming are `true`. Any single\n * `false` anywhere in the merge chain revokes consent for that key.\n *\n * This matches the consent rule of thumb: any deny signal denies. Permissive\n * (OR) and \"last-wins\" semantics are both unsafe for a privacy primitive.\n */\nfunction mergeConsent(\n state: WalkerOS.Consent,\n key: string,\n value: boolean,\n): void {\n state[key] = state[key] === undefined ? value : Boolean(state[key]) && value;\n}\n\n/**\n * Parse consent from Usercentrics event detail.\n *\n * Group-level: ucCategory values are all booleans → use them directly.\n * Service-level: ucCategory has non-booleans → mix boolean ucCategory entries\n * with top-level service keys from event.detail.\n *\n * categoryMap is applied in both modes to ucCategory boolean entries.\n * Strict AND: if multiple source categories map to the same target group,\n * the target is `true` only when ALL contributing sources are `true`.\n */\nexport function parseConsent(\n detail: UsercentricsEventDetail,\n settings: Settings,\n): WalkerOS.Consent {\n const state: WalkerOS.Consent = {};\n\n if (detail.ucCategory && isGroupLevel(detail.ucCategory)) {\n Object.entries(detail.ucCategory).forEach(([category, value]) => {\n if (typeof value !== 'boolean') return;\n const mapped = settings.categoryMap?.[category] ?? category;\n mergeConsent(state, mapped, value);\n });\n } else {\n if (detail.ucCategory) {\n Object.entries(detail.ucCategory).forEach(([key, value]) => {\n if (typeof value === 'boolean') {\n const mapped = settings.categoryMap?.[key] ?? key;\n mergeConsent(state, mapped, value);\n }\n });\n }\n\n Object.entries(detail).forEach(([key, value]) => {\n if (RESERVED_KEYS.includes(key)) return;\n if (typeof value !== 'boolean') return;\n const normalized = key.toLowerCase().replace(/ /g, '_');\n mergeConsent(state, normalized, value);\n });\n }\n\n return state;\n}\n","import type { Elb, Logger } from '@walkeros/core';\nimport type {\n Settings,\n UsercentricsEventDetail,\n UsercentricsV2Service,\n UsercentricsV3CmpEventDetail,\n} from '../types';\nimport { parseConsent } from './parseConsent';\n\nexport interface V2AdapterContext {\n window: Window & typeof globalThis;\n elb: Elb.Fn;\n settings: Settings;\n logger: Logger.Instance;\n}\n\n/**\n * V3 CMP event `type` values that signal a real user decision. Other event\n * types (e.g. CMP_SHOWN, VIEW_CHANGED) carry no consent decision and are\n * ignored so the adapter only re-reads after an actual choice.\n */\nconst V2_DECISION_TYPES: ReadonlySet<string> = new Set([\n 'ACCEPT_ALL',\n 'DENY_ALL',\n 'SAVE',\n]);\n\n/**\n * Aggregate V2 service array into a group-level ucCategory object using strict\n * AND logic. A category is `true` only if EVERY service in that category is\n * accepted. Any single denied service denies the whole category — this\n * matches the consent rule of thumb: any deny signal denies.\n */\nfunction aggregateByCategory(\n services: UsercentricsV2Service[],\n): Record<string, boolean> {\n const categories: Record<string, boolean> = {};\n services.forEach((service) => {\n const category = service.categorySlug;\n const status = service.consent?.status ?? false;\n categories[category] =\n categories[category] === undefined\n ? status\n : categories[category] && status;\n });\n return categories;\n}\n\n/**\n * Whether any service carries an `explicit` entry in its consent history. An\n * explicit entry is Usercentrics' own proof that the user actively decided\n * (vs an implicit page-load default), so it correctly surfaces returning\n * visitors whose stored decision was explicit.\n */\nexport function hasExplicitDecision(\n services: UsercentricsV2Service[],\n): boolean {\n return services.some((s) =>\n (s.consent.history ?? []).some((h) => h.type?.toLowerCase() === 'explicit'),\n );\n}\n\n/**\n * Build a synthetic UsercentricsEventDetail from the V2 services array. The\n * `type` is derived from Usercentrics' own consent history: `explicit` when any\n * service records an explicit decision, `implicit` otherwise. Per-service name\n * keys are surfaced so parseConsent can map service-level consent.\n */\nexport function buildDetailFromServices(\n services: UsercentricsV2Service[],\n): UsercentricsEventDetail {\n const detail: UsercentricsEventDetail = {\n event: 'consent_status',\n type: hasExplicitDecision(services) ? 'explicit' : 'implicit',\n ucCategory: aggregateByCategory(services),\n };\n services.forEach((s) => {\n if (s.name) detail[s.name] = s.consent.status;\n });\n return detail;\n}\n\n/**\n * Set up the V2 adapter using Usercentrics' official events and getter.\n *\n * A single gated `read()` is reused by all triggers: the official\n * `UC_UI_INITIALIZED` lifecycle event, the official `UC_UI_CMP_EVENT` decision\n * events (filtered to real decisions), and a static read at setup time for the\n * common case where the CMP is already initialized when the source runs.\n *\n * Returns a cleanup function that removes both event listeners.\n */\nexport function setupV2Adapter(ctx: V2AdapterContext): () => void {\n const { window: win, elb, settings, logger } = ctx;\n\n const read = (): void => {\n const api = win.UC_UI;\n if (!api?.isInitialized?.() || !api.getServicesBaseInfo) return;\n const services = api.getServicesBaseInfo();\n if (!services.length) return;\n const detail = buildDetailFromServices(services);\n logger.debug('consent read', detail);\n if (settings.explicitOnly && detail.type !== 'explicit') return;\n const state = parseConsent(detail, settings);\n if (Object.keys(state).length > 0) elb('walker consent', state);\n };\n\n const onInitialized = (): void => read();\n const onCmpEvent = (e: Event): void => {\n const detail = (e as CustomEvent<UsercentricsV3CmpEventDetail>).detail;\n if (!detail?.type || !V2_DECISION_TYPES.has(detail.type)) return;\n read();\n };\n\n win.addEventListener('UC_UI_INITIALIZED', onInitialized);\n win.addEventListener('UC_UI_CMP_EVENT', onCmpEvent);\n\n read(); // static read: CMP already initialized when source runs\n\n return () => {\n win.removeEventListener('UC_UI_INITIALIZED', onInitialized);\n win.removeEventListener('UC_UI_CMP_EVENT', onCmpEvent);\n };\n}\n","import type { Elb, Logger } from '@walkeros/core';\nimport type {\n Settings,\n UsercentricsEventDetail,\n UsercentricsV3Api,\n UsercentricsV3CategoryState,\n UsercentricsV3CmpEventDetail,\n UsercentricsV3ConsentDetails,\n} from '../types';\nimport { parseConsent } from './parseConsent';\n\nexport interface V3AdapterContext {\n window: Window & typeof globalThis;\n elb: Elb.Fn;\n settings: Settings;\n logger: Logger.Instance;\n}\n\nconst V3_DEFAULT_EVENT_NAME = 'UC_UI_CMP_EVENT';\n\n/**\n * V3 CMP event types that represent a user consent decision. Presentation\n * events like `CMP_SHOWN` or `UI_INITIALIZED` must NOT trigger a consent\n * publish — they carry no user decision.\n */\nconst V3_DECISION_TYPES: ReadonlySet<string> = new Set([\n 'ACCEPT_ALL',\n 'DENY_ALL',\n 'SAVE',\n]);\n\n/**\n * Map a V3 CategoryData.state value to a walkerOS boolean using strict\n * semantics: only fully-accepted categories are `true`. Any partial or\n * denied state (including future/unknown states) resolves to `false`.\n *\n * This matches the consent rule of thumb: any deny signal denies.\n */\nfunction categoryStateToBoolean(state: UsercentricsV3CategoryState): boolean {\n return state === 'ALL_ACCEPTED';\n}\n\n/**\n * Build a synthetic UsercentricsEventDetail from V3 ConsentDetails.\n * Translates V3's structured consent state into the shape parseConsent expects.\n */\nfunction buildDetailFromV3(\n details: UsercentricsV3ConsentDetails,\n): UsercentricsEventDetail {\n const ucCategory: Record<string, boolean> = {};\n Object.entries(details.categories).forEach(([slug, category]) => {\n ucCategory[slug] = categoryStateToBoolean(category.state);\n });\n\n return {\n event: 'consent_status',\n type: details.consent.type === 'EXPLICIT' ? 'explicit' : 'implicit',\n ucCategory,\n };\n}\n\n/**\n * Set up the V3 adapter: registers a listener on the V3 consent event AND\n * (if __ucCmp is already initialized) performs a static read.\n *\n * Both paths re-fetch getConsentDetails() because the event payload does\n * not include consent state.\n *\n * Returns a cleanup function that removes the event listener.\n */\nexport async function setupV3Adapter(\n ctx: V3AdapterContext,\n): Promise<() => void> {\n const { window: win, elb, settings, logger } = ctx;\n const eventName = settings.v3EventName ?? V3_DEFAULT_EVENT_NAME;\n\n const publishConsent = async (cmp: UsercentricsV3Api): Promise<void> => {\n const details = await cmp.getConsentDetails();\n const detail = buildDetailFromV3(details);\n logger.debug('event received', detail);\n\n if (settings.explicitOnly && detail.type !== 'explicit') return;\n\n const state = parseConsent(detail, settings);\n if (Object.keys(state).length > 0) {\n elb('walker consent', state);\n }\n };\n\n const listener = (event: Event): void => {\n const cmp = win.__ucCmp;\n if (!cmp) return;\n const detail = (event as CustomEvent<UsercentricsV3CmpEventDetail>).detail;\n // Filter to decision events only: the CMP emits a broad set of events\n // (CMP_SHOWN, UI_INITIALIZED, VIEW_CHANGED, …) on this bus. Without\n // filtering, every view toggle would re-publish consent. The decision\n // `type` is the only reliable signal; `detail.source` is one of\n // none|button|first|second|embeddings|__ucCmp and must NOT be gated on.\n if (!detail?.type || !V3_DECISION_TYPES.has(detail.type)) return;\n publishConsent(cmp).catch(() => {\n // Swallow — a failed V3 fetch should not break the page.\n });\n };\n win.addEventListener(eventName, listener);\n\n // When the source loads BEFORE __ucCmp, no UC_UI_CMP_EVENT fires for a\n // choice made in a prior session. UC_UI_INITIALIZED signals the CMP is ready,\n // so perform the static read then to publish any already-made decision.\n const onInitialized = (): void => {\n const cmp = win.__ucCmp;\n if (cmp) publishConsent(cmp).catch(() => {});\n };\n win.addEventListener('UC_UI_INITIALIZED', onInitialized);\n\n // Static check: if __ucCmp is already initialized, fetch now.\n // Wrapped in try/catch so transient API failures during setup don't\n // reject setupV3Adapter() and tear down the whole flow — the listener\n // stays registered and will pick up the next real event.\n const cmp = win.__ucCmp;\n if (cmp) {\n try {\n const initialized = await cmp.isInitialized();\n if (initialized) {\n await publishConsent(cmp);\n }\n } catch (err) {\n logger.warn('v3 static consent read failed', err);\n }\n }\n\n return () => {\n win.removeEventListener(eventName, listener);\n win.removeEventListener('UC_UI_INITIALIZED', onInitialized);\n };\n}\n","import type { Source, Elb } from '@walkeros/core';\n\ndeclare module '@walkeros/core' {\n interface SourceMap {\n usercentrics: { type: 'usercentrics'; platform?: 'web' };\n }\n}\n\n/**\n * Usercentrics consent event detail structure.\n *\n * Fired as event.detail on the configured window event (e.g. 'ucEvent').\n * Contains both category-level (ucCategory) and service-level consent.\n */\nexport interface UsercentricsEventDetail {\n /** Always 'consent_status' for consent events */\n event: string;\n /** 'explicit' when user actively chose, 'implicit' for page-load defaults (casing may vary) */\n type: string;\n /** Action taken: 'onAcceptAllServices', 'onDenyAllServices', 'onUpdateServices' */\n action?: string;\n /** Category-level consent booleans (e.g. { marketing: true, functional: false }) */\n ucCategory?: Record<string, boolean | unknown>;\n /** Service-level consent as top-level keys (e.g. 'Google Analytics': true) */\n [service: string]: unknown;\n}\n\ndeclare global {\n interface WindowEventMap {\n ucEvent: CustomEvent<UsercentricsEventDetail>;\n UC_UI_INITIALIZED: Event;\n UC_UI_CMP_EVENT: CustomEvent<UsercentricsV3CmpEventDetail>;\n }\n}\n\n/** Usercentrics CONSENT_TYPE values (history entry type / V3 consent type). */\nexport type UsercentricsConsentType = 'explicit' | 'implicit';\n\n/** A single entry in a V2 service's consent history (getServicesBaseInfo). */\nexport interface UsercentricsV2ConsentHistoryEntry {\n type: UsercentricsConsentType;\n status: boolean;\n /** CONSENT_ACTION, e.g. 'onAcceptAllServices' | 'onInitialPageLoad'. Not read for the gate. */\n action?: string;\n timestamp?: number;\n}\n\n/**\n * Usercentrics V2 service info shape returned by `UC_UI.getServicesBaseInfo()`.\n * Only the fields we use are typed - minimal surface, not a full V2 API mirror.\n */\nexport interface UsercentricsV2Service {\n /** Category slug: 'essential' | 'functional' | 'marketing' | custom */\n categorySlug: string;\n /** Service display name (used for service-level consent keys) */\n name?: string;\n /** Consent state for this service */\n consent: {\n status: boolean;\n /** Consent decision history; an 'explicit' entry proves a real user decision. */\n history?: UsercentricsV2ConsentHistoryEntry[];\n };\n}\n\n/**\n * Usercentrics V2 window API (`window.UC_UI`).\n *\n * Methods are synchronous (unlike V3). All methods are optional because\n * Usercentrics does not guarantee every deployment exposes every method.\n */\nexport interface UsercentricsV2Api {\n isInitialized?: () => boolean;\n getServicesBaseInfo?: () => UsercentricsV2Service[];\n getServicesFullInfo?: () => Promise<UsercentricsV2Service[]>;\n areAllConsentsAccepted?: () => boolean;\n}\n\n/**\n * Usercentrics V3 category state. The SDK may add future states; we handle\n * unknowns conservatively as non-accepting.\n */\nexport type UsercentricsV3CategoryState =\n | 'ALL_ACCEPTED'\n | 'ALL_DENIED'\n | 'SOME_ACCEPTED'\n | 'NO_STATE'\n | string;\n\n/**\n * Minimal V3 CategoryData - only fields the adapter reads.\n */\nexport interface UsercentricsV3CategoryData {\n state: UsercentricsV3CategoryState;\n name: string;\n}\n\n/**\n * Minimal V3 ConsentData - only the `type` field is used to distinguish\n * explicit vs implicit consent. Other fields (status, version, etc.) exist on\n * the real SDK but are not read by this adapter.\n */\nexport interface UsercentricsV3ConsentData {\n type: 'EXPLICIT' | 'IMPLICIT' | string;\n}\n\n/**\n * Minimal V3 ConsentDetails - only the fields the adapter reads.\n * The real SDK also exposes `services`, but the adapter currently operates\n * at category level only.\n */\nexport interface UsercentricsV3ConsentDetails {\n consent: UsercentricsV3ConsentData;\n categories: Record<string, UsercentricsV3CategoryData>;\n}\n\n/**\n * Usercentrics V3 window API (`window.__ucCmp`).\n *\n * Only the methods this adapter calls are typed - the real SDK surface is\n * much wider but we intentionally keep this narrow.\n */\nexport interface UsercentricsV3Api {\n isInitialized: () => Promise<boolean>;\n getConsentDetails: () => Promise<UsercentricsV3ConsentDetails>;\n}\n\n/**\n * V3 CMP event detail. Fired on `UC_UI_CMP_EVENT` (or custom name).\n * `source: 'CMP'` + a decision `type` tells us a user action has been taken.\n */\nexport interface UsercentricsV3CmpEventDetail {\n source?:\n | 'none'\n | 'button'\n | 'first'\n | 'second'\n | 'embeddings'\n | '__ucCmp'\n | string;\n type?: string;\n abTestVariant?: string;\n}\n\ndeclare global {\n interface Window {\n /**\n * Usercentrics V2 CMP API. Attached once the V2 Browser SDK is\n * initialized. Optional because the SDK loads asynchronously - guard\n * with a truthiness check before access.\n */\n UC_UI?: UsercentricsV2Api;\n /**\n * Usercentrics V3 CMP API. Attached once the V3 Browser SDK is\n * initialized. Optional because the SDK loads asynchronously - guard\n * with a truthiness check before access.\n */\n __ucCmp?: UsercentricsV3Api;\n }\n}\n\n/**\n * Settings for Usercentrics source\n */\nexport interface Settings {\n /**\n * Map Usercentrics categories to walkerOS consent groups.\n * Keys: Usercentrics category names (from ucCategory)\n * Values: walkerOS consent group names\n *\n * Applied in both group-level and service-level consent modes.\n * When multiple source categories map to the same group, strict AND logic\n * applies: ALL contributing source categories must be true for the target\n * group to be true. Any single false denies consent.\n *\n * Default: {} (pass through category names as-is)\n */\n categoryMap?: Record<string, string>;\n\n /**\n * Only publish when the user has actively decided. V3: consent.type ===\n * EXPLICIT. V2: an EXPLICIT entry exists in service consent history.\n * Implicit/default page-load states are suppressed. Set false to publish any\n * consent snapshot including implicit. Default: true.\n */\n explicitOnly?: boolean;\n\n /**\n * Which Usercentrics API to target.\n * - 'auto' (default): detect at init. If `window.__ucCmp` is present, use V3.\n * If only `window.UC_UI` is present, use V2. If neither is present yet,\n * register listeners for both so late-loading CMPs are still caught.\n * - 'v2': force legacy `window.UC_UI` path.\n * - 'v3': force current `window.__ucCmp` path.\n */\n apiVersion?: 'auto' | 'v2' | 'v3';\n\n /**\n * V3 window event name to listen for consent changes.\n *\n * Usercentrics V3 hardcodes its built-in event names (`UC_UI_CMP_EVENT`,\n * `UC_UI_INITIALIZED`, `UC_UI_VIEW_CHANGED`, `UC_CONSENT`) - they cannot be\n * renamed. However, the Usercentrics admin dashboard (Implementation >\n * Data Layer & Events) lets admins configure an ADDITIONAL custom window\n * event. Use this setting to point at that custom event name if\n * configured; otherwise leave as default.\n *\n * Default: 'UC_UI_CMP_EVENT'\n */\n v3EventName?: string;\n}\n\n/**\n * User input settings (all optional)\n */\nexport type InitSettings = Partial<Settings>;\n\n/**\n * No mapping configuration for this source\n */\nexport interface Mapping {}\n\n/**\n * Push function type - uses elb for consent commands\n */\nexport type Push = Elb.Fn;\n\n/**\n * Environment interface for Usercentrics source\n */\nexport interface Env extends Source.BaseEnv {\n window?: Window & typeof globalThis;\n}\n\n/**\n * Types bundle for Usercentrics source\n */\nexport type Types = Source.Types<Settings, Mapping, Push, Env, InitSettings>;\n\n/**\n * Config type alias\n */\nexport type Config = Source.Config<Types>;\n","import type { Source } from '@walkeros/core';\nimport type { Types, Settings } from './types';\nimport { setupV2Adapter } from './lib/v2';\nimport { setupV3Adapter } from './lib/v3';\n\n// Export types for external usage\nexport * as SourceUsercentrics from './types';\n\n/**\n * Usercentrics consent management source for walkerOS.\n *\n * Listens to Usercentrics CMP events and translates consent states to\n * walkerOS consent commands. Supports both the legacy V2 API\n * (`window.UC_UI` + `ucEvent`) and the current V3 API\n * (`window.__ucCmp` + `UC_UI_CMP_EVENT`).\n *\n * @example\n * ```typescript\n * import { sourceUsercentrics } from '@walkeros/web-source-cmp-usercentrics';\n *\n * await startFlow({\n * sources: {\n * consent: {\n * code: sourceUsercentrics,\n * config: {\n * settings: {\n * categoryMap: {\n * essential: 'functional',\n * functional: 'functional',\n * marketing: 'marketing',\n * },\n * },\n * },\n * },\n * },\n * });\n * ```\n */\nexport const sourceUsercentrics: Source.Init<Types> = async (context) => {\n const { config, env } = context;\n const { elb, logger } = env;\n\n // Resolve window with fallback to globalThis\n const actualWindow =\n env.window ??\n (typeof globalThis.window !== 'undefined' ? globalThis.window : undefined);\n\n // Merge user settings with defaults so adapters always see a fully-resolved Settings\n const settings: Settings = {\n categoryMap: config?.settings?.categoryMap ?? {},\n explicitOnly: config?.settings?.explicitOnly ?? true,\n apiVersion: config?.settings?.apiVersion ?? 'auto',\n v3EventName: config?.settings?.v3EventName ?? 'UC_UI_CMP_EVENT',\n };\n\n const fullConfig: Source.Config<Types> = { settings };\n const cleanups: Array<() => void> = [];\n\n // Attach the CMP listeners and perform the static consent read. Deferred to\n // init() (Pass 2 of initSources) so the factory (Pass 1) stays side-effect\n // free: no listener registration and, critically, no `elb('walker consent')`\n // emit during construction. Emitting from the factory races source merge\n // order and can leave a later `require:[\"consent\"]` source parked.\n const init = async (): Promise<void> => {\n if (!actualWindow) return;\n\n const adapterCtx = { window: actualWindow, elb, settings, logger };\n const apiVersion = settings.apiVersion ?? 'auto';\n\n if (apiVersion === 'v2') {\n cleanups.push(setupV2Adapter(adapterCtx));\n } else if (apiVersion === 'v3') {\n cleanups.push(await setupV3Adapter(adapterCtx));\n } else {\n // auto: detect which API is present\n if (actualWindow.__ucCmp) {\n cleanups.push(await setupV3Adapter(adapterCtx));\n } else if (actualWindow.UC_UI) {\n cleanups.push(setupV2Adapter(adapterCtx));\n } else {\n // Neither loaded yet — attach both listeners so whichever loads later is caught.\n cleanups.push(setupV2Adapter(adapterCtx));\n cleanups.push(await setupV3Adapter(adapterCtx));\n }\n }\n };\n\n return {\n type: 'usercentrics',\n config: fullConfig,\n push: elb,\n init,\n destroy: async () => {\n cleanups.forEach((fn) => fn());\n },\n };\n};\n\nexport default sourceUsercentrics;\n"],"mappings":"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","names":["_a","cmp","_a"]}
|