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

@codeluum/compliance-web

v0.1.2

Published

Browser helpers for Codeluum Content API (cookie consent, publishable key)

Readme

@codeluum/compliance-web

npm version license

Browser + React widgets for Codeluum consent and privacy compliance (GDPR / NDPR — Nigeria Data Protection Act). Drop-in cookie banner, preference center, JIT consent prompts, DSAR buttons, policy links, sensitive-field protection, and the statutory NDPR notices (SNAG / Article 40, cross-border transfer disclosure, pre-install statement) — all driven by the Codeluum Content API.

Every widget authenticates with your publishable key (pk_…), which is safe to ship to the browser. For server-side calls with the secret key (sk_…), use @codeluum/compliance.

  • Framework-free core (vanilla DOM) and first-class React bindings.
  • Tree-shakeable subpath exports — import only what you use.
  • Ships TypeScript types. ESM only.
  • React is an optional peer dependency (only needed for @codeluum/compliance-web/react).

Installation

npm install @codeluum/compliance-web
# React is only required if you use the /react entry point:
npm install react react-dom

Key concepts

  • Publishable key (pk_…) — the only credential this package needs. It can record consent and read public config/policies, but can't enumerate audit history. Never put your secret key (sk_…) in browser code.
  • externalUserId — a stable identifier for the current visitor/user, used to attribute consent records. Can be a string or a (sync/async) function returning one.
  • Consent cookie — a single cookie (cl_consent_v1 by default) stores the visitor's decision. The consent-state bus reads it synchronously and notifies subscribers (including across tabs) on change.
  • Purposes vs. categories — categories are the toggles shown in the banner; purposes are the granular consent records derived from them.

Quick start — vanilla cookie banner

import { mountCodeluumBanner } from '@codeluum/compliance-web';

const banner = await mountCodeluumBanner({
  publishableKey: 'pk_live_…',
  apiBaseUrl: 'https://api.codeluum.com',
  externalUserId: () => getCurrentUserId() ?? crypto.randomUUID(),
  position: 'bottom',
  gate: { gtm: {} },                 // auto-activate gated scripts + Google Consent Mode v2
  onDecision: (purposes) => console.log('granted:', purposes),
});

// Later — e.g. from a "Cookie settings" footer link:
banner.openPreferences();

The banner injects its own styles by default (injectDefaultStyles: false to opt out), writes the consent cookie, POSTs the decision, and fires the consent bus so the rest of the page reacts.

Quick start — React

import {
  CodeluumBannerProvider,
  useConsent,
  RetentionNotice,
} from '@codeluum/compliance-web/react';

function App() {
  return (
    <CodeluumBannerProvider
      publishableKey="pk_live_…"
      apiBaseUrl="https://api.codeluum.com"
      externalUserId={userId}
      gate
    >
      <Analytics />
    </CodeluumBannerProvider>
  );
}

function Analytics() {
  const { granted } = useConsent('analytics'); // re-renders on accept/withdraw, even cross-tab
  return granted ? <Tracker /> : null;
}

Entry points

Import widgets from the main barrel or from focused subpaths (better tree-shaking):

| Import | Exports | |---|---| | @codeluum/compliance-web | mountCodeluumBanner, createCodeluumContentClient, initCodeluumConsent, purposesFromCategories, recordConsentFromToggles, CodeluumContentError, and re-exports of everything below | | …/react | React components + hooks (see React) | | …/consent-state | getConsentState, getConsentSnapshot, subscribeConsent, notifyConsentChange, CONSENT_COOKIE | | …/gate | applyConsentGates | | …/i18n | resolveStrings, detectLocale, isRtlLocale, SUPPORTED_LOCALES | | …/request-consent | requestConsent | | …/submit-with-consent | submitWithConsent | | …/purpose-registry | getPurposeRegistry, refreshPurposeRegistry, getPurposeById, subscribePurposeRegistry | | …/policy | mountCodeluumPolicyLink, mountCodeluumPolicyText | | …/consent-receipt | mountCodeluumConsentReceipt | | …/badge | mountCodeluumComplianceBadge | | …/dsar | createDsarRequest, pollDsarStatus, TERMINAL_STATUSES | | …/dsar-button | mountCodeluumDsarButton | | …/preference-center | mountCodeluumPreferenceCenter | | …/retention-notice | mountCodeluumRetentionNotice | | …/field | mountCodeluumField | | …/sensitive-field | mountCodeluumSensitiveField, SENSITIVE_PATTERNS | | …/snag-form | mountCodeluumSnagForm, SCHEDULE_9_VIOLATION_CODES | | …/transfer-disclosure | mountCodeluumTransferDisclosure | | …/pre-install-statement | generatePreInstallStatement, PRE_INSTALL_FIELDS |

All mount* functions accept a target (string selector or HTMLElement) and return a handle with unmount() (plus widget-specific methods). They inject default styles unless you pass injectDefaultStyles: false, and every visible string / CSS class is overridable via strings / classNames.


Cookie banner — mountCodeluumBanner

const banner = await mountCodeluumBanner(options): Promise<BannerHandle>;

Renders a sticky (bottom/top) or centered-modal banner with Accept All / Essentials Only / Customize. Manages its own DOM, cookie, and the POST to record the decision.

Key options (MountBannerOptions):

| Option | Type | Notes | |---|---|---| | publishableKey | string | Required. | | apiBaseUrl | string | Required. | | externalUserId | string \| () => string \| Promise<string> | Required. Stable visitor id. | | position | 'bottom' \| 'top' | Default 'bottom'. | | gate | boolean \| { gtm?: GtmGateOptions } | Auto-activate gated scripts and (optionally) update Google Consent Mode v2. | | locale | string | e.g. 'en', 'fr-FR', 'ar-EG'. Defaults to <html lang>navigator.language'en'. | | policyLinks | { cookiePolicyUrl?, privacyPolicyUrl?, external? } | Render policy links in the body. | | linkRenderer | (props) => HTMLElement | Custom anchor (React-Router / Next Link). | | constructiveDismiss | { mode: 'centered' } | Article 17(8)(b) centered modal + dismiss-to-required. | | strings / classNames | Partial<…> | Override copy and CSS hooks. | | cookieName, cookieMaxAgeDays, cookieDomain, cookiePath, cookieSameSite, cookieSecure | | Cookie attributes. | | render | (host, api) => (() => void) \| void | Full UI escape hatch — you own the DOM, api gives categories, strings, decide(), dismiss(). | | onDecision / onError | callbacks | |

BannerHandle: unmount(), openPreferences(), hasDecided(), getDecision(). The default cookie name is exported as BANNER_COOKIE_NAME.


React bindings — @codeluum/compliance-web/react

Components (each is a thin wrapper over the matching mount* function and tears down on unmount):

<CodeluumBanner />, <CodeluumBannerProvider>, <CodeluumPreferenceCenter />, <CodeluumComplianceBadge />, <CodeluumPolicyLink />, <CodeluumPolicyText />, <CodeluumConsentReceipt />, <CodeluumDsarButton />, <CodeluumField>, <CodeluumSensitiveField />, <RetentionNotice />, <CodeluumSnagForm />, <CodeluumTransferDisclosure />. Components that expose an imperative handle accept a handleRef.

Hooks:

| Hook | Returns | Purpose | |---|---|---| | useConsent(purposeId, opts?) | ConsentState | Subscribe to one purpose; re-renders on accept/withdraw (cross-tab). | | useConsentSnapshot(opts?) | ConsentSnapshot \| null | Subscribe to the full decision. | | useCodeluumBanner() | { openPreferences, hasDecided, getDecision } | Control the banner mounted by CodeluumBannerProvider. | | usePurposeRegistry(arg) | PurposeRegistryEntry[] | Module-cached purpose catalog; updates live on refresh. | | useDsarStatus(ticketId, opts) | DsarStatusSnapshot \| null | Poll a DSAR ticket to completion. |

const { granted, decided, decidedAt } = useConsent('marketing');
const { openPreferences } = useCodeluumBanner();

Consent state bus — @codeluum/compliance-web/consent-state

Framework-free, synchronous reads of the consent cookie plus a change subscription (cross-tab via BroadcastChannel where available).

import {
  getConsentState, getConsentSnapshot, subscribeConsent, notifyConsentChange, CONSENT_COOKIE,
} from '@codeluum/compliance-web/consent-state';

const { granted, decided, decidedAt } = getConsentState('analytics'); // pure, render-safe
const unsubscribe = subscribeConsent((snap) => { /* snap: { purposes, decidedAt } | null */ });

// If you write the cookie yourself, tell subscribers:
notifyConsentChange();

getConsentState(purposeId, opts?) → { granted: boolean; decided: boolean; decidedAt: string | null }. opts.cookieName overrides the default (CONSENT_COOKIE).


Script gating — @codeluum/compliance-web/gate

Activate every <script type="text/plain" data-codeluum-purpose="…" data-src="…"> whose purpose is granted. Idempotent. Optionally maps purposes to Google Consent Mode v2.

<script type="text/plain" data-codeluum-purpose="analytics" data-src="https://…/ga.js"></script>
import { applyConsentGates } from '@codeluum/compliance-web/gate';

applyConsentGates(['necessary', 'analytics'], {
  gtm: { purposeMap: { analytics: 'analytics_storage', marketing: 'ad_storage' } },
});

(The banner can do this for you automatically via its gate option.)


Internationalization — @codeluum/compliance-web/i18n

Built-in dictionaries for SUPPORTED_LOCALES = ['en', 'es', 'fr', 'de', 'pt', 'ar'] (Arabic renders RTL).

import { resolveStrings, detectLocale, isRtlLocale, SUPPORTED_LOCALES } from '@codeluum/compliance-web/i18n';

const locale = detectLocale();               // explicit → <html lang> → navigator.language → 'en'
const strings = resolveStrings(locale, { title: 'Cookies 🍪' }); // exact → language-prefix → English, then overrides
const rtl = isRtlLocale(locale);

Just-in-time consent — @codeluum/compliance-web/request-consent

Promise-returning inline prompt for a single purpose at the moment it's needed. Resolves true on accept, false on reject.

import { requestConsent } from '@codeluum/compliance-web/request-consent';

const ok = await requestConsent({
  publishableKey: 'pk_live_…',
  apiBaseUrl: 'https://api.codeluum.com',
  externalUserId: userId,
  purpose: 'newsletter_signup',
  reason: 'We’ll email you product updates. Unsubscribe anytime.',
  alwaysInclude: ['necessary'],
});
if (ok) await subscribe();

Gated form submit — @codeluum/compliance-web/submit-with-consent

Collects every required purpose (prompting via requestConsent for any that aren't granted) and only runs submit(values) once all are granted. Stops on the first rejection.

import { submitWithConsent } from '@codeluum/compliance-web/submit-with-consent';

const result = await submitWithConsent(formValues, {
  publishableKey: 'pk_live_…',
  apiBaseUrl: 'https://api.codeluum.com',
  externalUserId: userId,
  requires: [
    'newsletter_signup',
    { purpose: 'profiling', reason: 'To personalize recommendations.' },
  ],
  submit: (values) => api.createAccount(values),
});

if (result.ok) {
  console.log(result.submitResult, result.granted);
} else {
  console.warn('blocked by', result.blockedBy); // purposes the user declined
}

Content API client — createCodeluumContentClient

A browser HTTP client for the public …/compliance/content/* endpoints. Most widgets create one for you, but you can share a single instance via the client option to avoid duplicate fetches.

import { createCodeluumContentClient } from '@codeluum/compliance-web';

const client = createCodeluumContentClient({
  publishableKey: 'pk_live_…',
  apiBaseUrl: 'https://api.codeluum.com',
});

await client.getConfig();      // banner categories
await client.getPolicies();    // published policies
await client.getPurposes();    // purpose registry
await client.consents.record({ subject, purpose, state: 'granted', basis: 'consent' });
await client.consents.has({ subject, purpose });

Failed requests throw CodeluumContentError (status, message, body).

Helpers on the barrel: initCodeluumConsent(options) prefetches config + policies (no UI); purposesFromCategories(selectedIds, opts?) derives purpose strings from category toggles; recordConsentFromToggles(client, params) records consent from a toggle selection.


DSAR (data-subject access requests) — @codeluum/compliance-web/dsar

Open and track export/delete/rectify requests. The browser never sees the secret key — requests go through your backend proxy (proxyUrl) or a custom createTicket callback.

import { createDsarRequest, pollDsarStatus, TERMINAL_STATUSES } from '@codeluum/compliance-web/dsar';

const ticket = await createDsarRequest({
  externalUserId: userId,
  type: 'export',                       // 'export' | 'delete' | 'rectify'
  proxyUrl: '/api/codeluum/dsar',        // your endpoint POSTs to the Content API with sk_…
});

const poll = pollDsarStatus(ticket.id, {
  publishableKey: 'pk_live_…',
  apiBaseUrl: 'https://api.codeluum.com',
  intervalMs: 5000,
  onChange: (snap) => {
    console.log(snap.status);
    if (TERMINAL_STATUSES.has(snap.status)) poll.stop();
  },
});

DSAR button — @codeluum/compliance-web/dsar-button

import { mountCodeluumDsarButton } from '@codeluum/compliance-web/dsar-button';

mountCodeluumDsarButton('#export-data', {
  type: 'export',
  externalUserId: userId,
  proxyUrl: '/api/codeluum/dsar',
  onTerminal: (snap) => console.log('done:', snap.status),
});

Compliance badge — @codeluum/compliance-web/badge

A transparency icon that opens a modal with tabs for Cookies, Purposes, Policies, optional DSAR Requests, and an optional "Your data" inventory.

import { mountCodeluumComplianceBadge } from '@codeluum/compliance-web/badge';

const badge = mountCodeluumComplianceBadge('#badge', {
  publishableKey: 'pk_live_…',
  apiBaseUrl: 'https://api.codeluum.com',
  externalUserId: userId,
  tabs: ['cookies', 'purposes', 'policies', 'requests', 'data'],
  dataInventory: [{ field: 'email', why: 'Account login', retentionDays: 365 }],
  dsar: { proxyUrl: '/api/codeluum/dsar' },
});
badge.open();

Preference center — @codeluum/compliance-web/preference-center

A full preferences page: decision summary, per-purpose toggles, DSAR buttons, and policy links — all live-updated via the consent bus.

import { mountCodeluumPreferenceCenter } from '@codeluum/compliance-web/preference-center';

mountCodeluumPreferenceCenter('#privacy', {
  publishableKey: 'pk_live_…',
  apiBaseUrl: 'https://api.codeluum.com',
  externalUserId: userId,
  sections: ['summary', 'purposes', 'requests', 'policies'],
  dsar: { proxyUrl: '/api/codeluum/dsar' },
});

Policy links & text — @codeluum/compliance-web/policy

import { mountCodeluumPolicyLink, mountCodeluumPolicyText } from '@codeluum/compliance-web/policy';

mountCodeluumPolicyLink('#privacy-link', {
  publishableKey: 'pk_live_…', apiBaseUrl: 'https://api.codeluum.com',
  type: 'privacy',        // 'privacy' | 'terms' | 'data_policy' | 'cookie'
  mode: 'modal',          // 'modal' opens an <a>+dialog; 'link' navigates to href
});

mountCodeluumPolicyText('#cookie-policy', {
  publishableKey: 'pk_live_…', apiBaseUrl: 'https://api.codeluum.com', type: 'cookie',
}); // inlines the published policy HTML

Consent receipt — @codeluum/compliance-web/consent-receipt

Shows "Saved on — manage in privacy settings" once a decision exists for a purpose.

import { mountCodeluumConsentReceipt } from '@codeluum/compliance-web/consent-receipt';
mountCodeluumConsentReceipt('#receipt', { purpose: 'newsletter' });

Retention notice — @codeluum/compliance-web/retention-notice

import { mountCodeluumRetentionNotice } from '@codeluum/compliance-web/retention-notice';
mountCodeluumRetentionNotice('#note', { purpose: 'support tickets', days: 365 }); // days: null → indefinite

Form fields

Field chrome — @codeluum/compliance-web/field

Wraps an input with a label, optional tooltip (purpose description), and retention notice sourced from the purpose registry.

import { mountCodeluumField } from '@codeluum/compliance-web/field';
mountCodeluumField('#email-field', {
  purpose: 'newsletter_signup',
  publishableKey: 'pk_live_…', apiBaseUrl: 'https://api.codeluum.com',
});

Sensitive field — @codeluum/compliance-web/sensitive-field

Mask/show toggle, paste validation, and clipboard wipe for high-risk inputs. Built-in patterns for bvn, nin, ssn, phone (exported as SENSITIVE_PATTERNS); pass a pattern for type: 'custom'.

import { mountCodeluumSensitiveField } from '@codeluum/compliance-web/sensitive-field';
const field = mountCodeluumSensitiveField('#bvn', { type: 'bvn', clipboardWipeMs: 10000 });
field.getValue();

NDPR statutory notices

SNAG form — @codeluum/compliance-web/snag-form

Standard Notice to Address Grievance (Article 40 + Schedule 9, NDP Act / GAID 2025). Posts to /compliance/content/snag (publishable key + server-side Origin allowlist). The 14 statutory violation codes are exported as SCHEDULE_9_VIOLATION_CODES.

import { mountCodeluumSnagForm } from '@codeluum/compliance-web/snag-form';
mountCodeluumSnagForm('#grievance', {
  publishableKey: 'pk_live_…', apiBaseUrl: 'https://api.codeluum.com',
  defaultRespondentName: 'Acme Ltd',
  onServed: (r) => console.log('ref', r.id, 'deadline', r.deadlineAt),
});

Transfer disclosure — @codeluum/compliance-web/transfer-disclosure

Article 27(3)(f) public list of cross-border destinations and their legal bases. Fetches from /compliance/content/transfers.

import { mountCodeluumTransferDisclosure } from '@codeluum/compliance-web/transfer-disclosure';
mountCodeluumTransferDisclosure('#transfers', {
  publishableKey: 'pk_live_…', apiBaseUrl: 'https://api.codeluum.com',
});

Pre-install statement — @codeluum/compliance-web/pre-install-statement

Pure function that assembles the Article 31(2)(e) pre-install statement (9 fields) into { text, html, json } for app-store listings or onboarding screens.

import { generatePreInstallStatement } from '@codeluum/compliance-web/pre-install-statement';

const stmt = generatePreInstallStatement({
  purposes: [{ purposeId: 'analytics', label: 'Usage analytics', retentionDays: 365 }],
  deviceType: 'mobile',                      // 'mobile' | 'desktop' | 'kiosk' | 'wearable'
  controllerName: 'Acme Ltd',
  controllerContact: '[email protected]',
  crossBorderDestinations: ['us', 'gb'],     // ISO-3166 alpha-2
});
element.innerHTML = stmt.html;

Purpose registry — @codeluum/compliance-web/purpose-registry

A module-cached catalog of your purposes (label, description, lawful basis, retention, sensitivity, required).

import {
  getPurposeRegistry, refreshPurposeRegistry, getPurposeById, subscribePurposeRegistry,
} from '@codeluum/compliance-web/purpose-registry';

const purposes = await getPurposeRegistry({ publishableKey: 'pk_live_…', apiBaseUrl: '…' });
getPurposeById('analytics');                  // sync; null until first load resolves
const off = subscribePurposeRegistry((list) => { /* re-render */ });

Styling

Every widget injects scoped default styles (disable with injectDefaultStyles: false) and exposes a classNames map so you can attach your own CSS to each part. Visible copy is overridable via strings, and locale selects a built-in translation.

TypeScript

All entry points ship .d.ts declarations. Notable exported types: MountBannerOptions, BannerHandle, ConsentState, ConsentSnapshot, ContentClientConfig, PurposeRegistryEntry, DsarType, DsarStatusSnapshot, CreateDsarRequestOptions, PolicyType, SensitiveType, SchedulNineViolationCode, TransferBasis, PreInstallStatement, LocaleCode, and the per-widget *Options / *Strings / *ClassNames / *Handle types.

Note: the top-level ConsentState is the cookie-state object { granted, decided, decidedAt }. The Content API's consent decision union ('granted' | 'withdrawn' | 'acknowledged') is exported as ContentConsentState.

License

MIT