npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mycookies/widget

v0.2.4

Published

Privacy-first cookie consent client library with embeddable widget and headless SDK from My Cookies service.

Readme

@mycookies/widget

Privacy-first cookie consent client library for the MyCookies service. Ships two consumption modes — an embeddable consent UI widget and a headless SDK. Tenant configuration is delivered as a server-signed remote payload (categories, branding, integrations). When enabled on your subscription, consent decisions are recorded to the MyCookies backend for compliance audit.

  • Zero runtime dependencies — no bloat on your end-user pages
  • Shadow DOM isolation — widget styles never conflict with your site
  • GDPR, ePrivacy, CCPA/CPRA, TCF v2 compliant consent flows
  • ECDSA-verified remote config — tenant configuration is signed server-side and verified client-side before use
  • Tree-shakeable — headless entry omits all UI code

Table of Contents


Installation

npm install @mycookies/widget
# or
yarn add @mycookies/widget

Prerequisites: A MyCookies subscription UUID is required. Sign up at mycookies.ws to obtain one. All production factory functions require a valid UUID v4 subscription identifier.


Quick Start

Embeddable Widget

Drop in a fully managed consent banner and modal. The widget resolves your tenant configuration from the MyCookies backend, verifies the ECDSA signature, and renders the UI automatically.

import { createWidget } from '@mycookies/widget';

const widget = await createWidget({
  subscriptionId: 'xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx',
});

// Programmatically open the preferences modal
widget.showPreferences();

// Access the underlying engine
const engine = widget.getEngine();
engine.onConsentChange((state) => {
  console.log('Consent updated:', state);
});

// Tear down on SPA route change
widget.destroy();

Headless SDK

Use the engine directly without any UI — ideal for SPAs and custom consent experiences.

import { createValidatedEngine } from '@mycookies/widget/headless';

const { engine, integrations, poller } = await createValidatedEngine({
  subscriptionId: 'xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx',
});

// React to consent changes
engine.onConsentChange((state) => {
  if (state.analytics) {
    initAnalytics();
  }
});

// Check a specific category
if (engine.hasConsent('marketing')) {
  loadMarketingPixel();
}

// Tear down on SPA route change
poller?.destroy();
integrations.scriptManager?.destroy();
integrations.consentLogger?.destroy();

CDN Script Tag

For non-bundled sites, load the IIFE bundle directly. The script auto-initializes using the data-subscription attribute.

<!-- Full widget with consent UI -->
<script
  src="https://cdn.mycookies.ws/mycookies.min.iife.js"
  data-subscription="xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx"
  data-theme="auto"
></script>

After the script loads, the global window.MyCookies is populated:

// Access the widget and engine from anywhere on the page
window.MyCookies.widget.showPreferences();
window.MyCookies.engine.hasConsent('analytics');
window.MyCookies.poller; // ConfigPoller instance, if polling is enabled

Headless CDN bundle — engine only, no UI, smaller footprint:

<script
  src="https://cdn.mycookies.ws/mycookies-headless.min.iife.js"
  data-subscription="xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx"
></script>

<script>
  // window.MyCookies.engine is ready after DOMContentLoaded
  document.addEventListener('DOMContentLoaded', () => {
    window.MyCookies.engine.onConsentChange((state) => {
      console.log(state);
    });
  });
</script>

Note: Both CDN bundles inject Google Consent Mode v2 defaults synchronously at script evaluation time — before GTM loads — when GCM is enabled on your subscription. This solves the GTM chicken-and-egg timing problem automatically.


Entry Points

The package exposes three sub-entry points:

| Entry point | Use case | Includes UI | |---|---|---| | @mycookies/widget | Full library — widget + engine + all utilities | Yes | | @mycookies/widget/headless | Engine only — no UI imports, smaller bundle | No |

Import from @mycookies/widget/headless in projects that use a custom consent UI to avoid pulling in Shadow DOM and rendering code.


Factory Functions

All production factory functions require a valid UUID v4 subscriptionId and perform:

  1. UUID v4 format validation
  2. Remote config fetch with ECDSA signature verification
  3. Domain authorization check against allowedDomains in the signed payload
  4. Integration auto-wiring from the remote integrations config

createWidget

import { createWidget } from '@mycookies/widget';

const widget = await createWidget(options);

Options (CreateWidgetOptions):

| Option | Type | Required | Description | |---|---|---|---| | subscriptionId | string | Yes | UUID v4 subscription identifier | | remote.timeoutMs | number | No | Config fetch timeout in ms (default: 5000) | | remote.onError | (error: ConfigError) => void | No | Called when remote config fails | | theme | 'light' \| 'dark' \| 'auto' | No | Widget color scheme. 'auto' follows OS preference | | customCssUrl | string | No | URL of a CSS file injected into the widget Shadow DOM | | scrollLock | boolean | No | Lock body scroll when modal is open (default: true) | | onScrollLockChange | (locked: boolean) => void | No | Custom scroll lock handler for framework integrations | | banner | BannerConfig | No | Banner UI overrides | | modal | ModalConfig | No | Modal UI overrides | | floatingButton | FloatingButtonConfig | No | Floating action button overrides | | engine | ConsentEngine | No | Provide a pre-created engine — skips integration auto-wiring | | storage | IStorage | No | Custom storage adapter (default: auto-detected) | | polling | PollingFactoryOptions | No | Local overrides for background config polling |

Returns: Promise<IWidgetController>

interface IWidgetController {
  /** Programmatically open the preferences modal. */
  showPreferences(): void;
  /** Get the underlying ConsentEngine. */
  getEngine(): ConsentEngine;
  /** Destroy the widget and release all resources. */
  destroy(): void;
}

createValidatedEngine

import { createValidatedEngine } from '@mycookies/widget/headless';

const { engine, integrations, poller } = await createValidatedEngine(options);

Options (CreateValidatedEngineOptions):

| Option | Type | Required | Description | |---|---|---|---| | subscriptionId | string | Yes | UUID v4 subscription identifier | | remote.timeoutMs | number | No | Config fetch timeout in ms (default: 5000) | | remote.onError | (error: ConfigError) => void | No | Called when remote config fails | | storage | IStorage | No | Custom storage adapter | | polling | PollingFactoryOptions | No | Local overrides for background config polling |

Returns: Promise<ValidatedEngineResult>

interface ValidatedEngineResult {
  engine: ConsentEngine;
  integrations: WiredIntegrations; // { scriptManager, consentLogger }
  poller: ConfigPoller | null;
}

Polling overrides (PollingFactoryOptions):

const { engine } = await createValidatedEngine({
  subscriptionId: '...',
  polling: {
    enabled: false,   // disable background polling entirely
    intervalMs: 300_000, // override poll interval (min: 60000ms)
  },
});

Consent Engine API

The ConsentEngine manages consent state, persists preferences, and notifies subscribers of changes. The engine is always created and initialized internally by the factory functions — access it via widget.getEngine() or the engine property of ValidatedEngineResult.

const widget = await createWidget({ subscriptionId: '...' });
const engine = widget.getEngine();

// or via headless:
const { engine } = await createValidatedEngine({ subscriptionId: '...' });

Methods

| Method | Signature | Description | |---|---|---| | init | (config: ConsentConfig) => void | Initialize with categories and subscription ID. Throws if called twice. | | updateConsent | (update: Partial<ConsentState>) => void | Update one or more consent categories. Always persists — even when state is unchanged, so the widget knows the user has actively decided. | | getConsentState | () => ConsentState | Returns a shallow copy of the current consent state. | | hasConsent | (category: string) => boolean | Returns true if the given category is explicitly granted. | | hasStoredConsent | () => boolean | Returns true if a persisted consent envelope exists for this subscription. | | onConsentChange | (callback: ConsentChangeCallback) => () => void | Subscribe to consent changes. Returns an unsubscribe function. Can be called before init(). | | registerProvider | (provider: IConsentProvider) => void | Register a consent provider plugin. Late registration calls provider.init() immediately. | | getEventBus | () => EventBus | Access the internal event bus for channel-based communication. |

Listening to consent changes

const unsubscribe = engine.onConsentChange((state) => {
  // state is a ConsentState: Record<string, boolean>
  if (state.analytics) {
    enableAnalytics();
  } else {
    disableAnalytics();
  }
});

// Stop listening
unsubscribe();

Storage key format

Consent state is persisted under the key _mycookies_consent_v1_{subscriptionId} using the configured storage adapter (localStorage by default).


Integrations

Integrations are configured server-side in your MyCookies portal and delivered via the signed remote payload. The factory functions auto-wire enabled integrations before engine.init() so they receive the initial consent state.

Google Consent Mode v2

When enabled on your subscription, the factory automatically registers a GoogleConsentModeProvider that updates gtag('consent', 'update', ...) whenever consent changes.

The CDN bundles also inject gtag('consent', 'default', ...) synchronously at script evaluation time — before GTM loads — using cached config on repeat visits or safe all-denied defaults on the first visit.

Manual registration (advanced):

import { GoogleConsentModeProvider } from '@mycookies/widget/headless';

const gcm = new GoogleConsentModeProvider({
  consentMode: 'advanced',           // 'basic' | 'advanced'
  categoryMapping: {
    analytics_storage: 'analytics',
    ad_storage: 'marketing',
    ad_user_data: 'marketing',
    ad_personalization: 'marketing',
  },
  waitForUpdate: 500,                // ms to wait before GTM fires tags
});

engine.registerProvider(gcm);

Script Manager

ScriptManager manages consent-gated <script> tags. When a category is granted, it activates matching scripts; when revoked, it removes them from the DOM.

Mark scripts with data-consent-category:

<script type="text/plain" data-consent-category="analytics">
  // This script runs only when 'analytics' consent is granted
  gtag('config', 'GA-XXXXX');
</script>

Manual usage:

import { ScriptManager } from '@mycookies/widget/headless';

const sm = new ScriptManager(engine, {
  observe: true,       // Watch for dynamically added scripts (MutationObserver)
  scanExisting: true,  // Scan <script> tags already in the DOM on start
});

sm.start();

// Clean up
sm.destroy();

Security note: ScriptManager strips all on* event handler attributes (onclick, onerror, etc.) from activated scripts to prevent XSS via attribute passthrough.

Consent Event Logging

When your subscription has at least one legal framework configured (e.g. GDPR), the remote payload includes consentLogging: true. The factory automatically wires a ConsentLogger that sends an event record to the MyCookies backend after every user consent decision.

What is logged:

| Field | Value | |---|---| | visitorId | Stable anonymous ID stored in localStorage (_mycookies_visitor_v1) | | action | accept_all, reject_all, or save_preferences | | granted | Array of accepted category IDs | | denied | Array of rejected category IDs | | configSignature | Config envelope signature for server-side audit | | timestamp | ISO-8601 client-side time of the decision |

Logging is fire-and-forget — it never blocks the consent UI flow and never throws.

For headless consumers, access the logger from ValidatedEngineResult:

const { integrations } = await createValidatedEngine({ subscriptionId: '...' });

// integrations.consentLogger is null when logging is not enabled
integrations.consentLogger?.destroy(); // call during teardown

Remote Configuration

The resolveConfig function runs the full 7-step verification pipeline independently of the factory functions.

import { resolveConfig, ConfigErrorCode } from '@mycookies/widget';

const result = await resolveConfig({
  subscriptionId: 'xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx',
  timeoutMs: 5000,
});

if (result.ok) {
  console.log(result.payload.categories);
  console.log(result.payload.tier); // 'free' | 'pro' | 'enterprise'
} else {
  console.error(result.error.code, result.error.message);
}

ConfigErrorCode values:

| Code | Trigger | |---|---| | INVALID_SUBSCRIPTION_ID | Not a valid UUID v4 | | FETCH_FAILED | Network error | | FETCH_TIMEOUT | Request exceeded timeoutMs | | HTTP_NOT_FOUND | API returned 404 | | HTTP_FORBIDDEN | API returned 403 | | HTTP_ERROR | Other non-2xx status | | MALFORMED_RESPONSE | Invalid JSON or unexpected shape | | CRYPTO_UNAVAILABLE | crypto.subtle not available (non-HTTPS context) | | UNKNOWN_KEY_ID | kid does not match any embedded public key | | SIGNATURE_INVALID | ECDSA verification failed | | DOMAIN_UNAUTHORIZED | Current hostname not in allowedDomains |

Config Cache

ConfigCache provides localStorage-backed stale-while-revalidate caching. The factory functions use it automatically. Access it directly when you need synchronous reads (e.g. for GCM defaults before the async pipeline completes):

import { ConfigCache, LocalStorageAdapter } from '@mycookies/widget';

const cache = new ConfigCache(new LocalStorageAdapter());

// Read without signature verification — fast, synchronous
const cached = cache.getUnverified(subscriptionId);
if (cached?.integrations?.googleConsentMode?.enabled) {
  // GCM is enabled for this subscription
}

Config Poller

ConfigPoller runs a background setTimeout loop that refreshes the cache when the TTL expires. It is page-visibility-aware — it pauses when the tab is hidden and catches up on resume.

// The poller is returned by factory functions.
// Call destroy() during SPA teardown to stop background activity.
const { poller } = await createValidatedEngine({ subscriptionId: '...' });
poller?.destroy();

Note: The poller updates only the cache — it never disrupts the live engine state mid-session. New config takes effect on the next page load.


Storage Adapters

The library ships three storage adapters and an auto-detection helper:

import {
  LocalStorageAdapter,
  CookieStorageAdapter,
  detectStorage,
} from '@mycookies/widget';

// Auto-detect the best available adapter
const storage = detectStorage();

// Use a specific adapter
const storage = new LocalStorageAdapter();
const storage = new CookieStorageAdapter({ sameSite: 'Lax', secure: true });

// Pass to a factory
const { engine } = await createValidatedEngine({
  subscriptionId: '...',
  storage,
});

Implement a custom adapter by satisfying the IStorage interface:

import type { IStorage } from '@mycookies/widget';

class MyCustomAdapter implements IStorage {
  get<T>(key: string): T | null { /* ... */ }
  set<T>(key: string, value: T): void { /* ... */ }
  clear(key: string): void { /* ... */ }
}

TypeScript Types

All public types are exported from both entry points:

import type {
  // Factory options
  CreateWidgetOptions,
  CreateValidatedEngineOptions,
  RemoteFactoryOptions,
  PollingFactoryOptions,

  // Results
  ValidatedEngineResult,
  WiredIntegrations,
  IWidgetController,

  // Engine
  ConsentState,
  ConsentCategory,
  ConsentConfig,
  ConsentChangeCallback,

  // Remote config
  RemoteConfigPayload,
  RemoteConfigResponse,
  RemotePollingConfig,
  SubscriptionUuid,

  // Integrations
  IntegrationConfig,
  RemoteGoogleConsentModeConfig,
  RemoteScriptManagerConfig,
  ConsentAction,
  ConsentLoggerOptions,

  // Storage
  IStorage,

  // UI
  WidgetTheme,
  BannerConfig,
  ModalConfig,
  FloatingButtonConfig,
} from '@mycookies/widget';

Error Handling

Factory functions throw a ConfigError when remote resolution fails. Always wrap calls in try/catch or use the remote.onError callback for graceful degradation:

import { createWidget, ConfigError, ConfigErrorCode } from '@mycookies/widget';

try {
  const widget = await createWidget({
    subscriptionId: '...',
    remote: {
      onError: (error) => {
        // Called before the throw — use for logging/monitoring
        console.error('[consent] config error:', error.code);
      },
    },
  });
} catch (error) {
  if (error instanceof ConfigError) {
    switch (error.code) {
      case ConfigErrorCode.DOMAIN_UNAUTHORIZED:
        // This domain is not in the subscription's allowedDomains list
        break;
      case ConfigErrorCode.FETCH_TIMEOUT:
        // Network is slow — consider a longer timeoutMs
        break;
      default:
        // Handle other error codes
    }
  }
}

Note: resolveConfig never throws — it returns a discriminated union { ok: true, payload } or { ok: false, error }. Factory functions (createWidget, createValidatedEngine) do throw on failure so that await callers get standard try/catch error handling.


License

UNLICENSED — All rights reserved. MyCookies