consent-nexus-int
v0.3.3
Published
Backend + browser SDK to collect and verify consent for PII form flows, backed by Consent Nexus CMP.
Readme
consent-nexus-int
Official TypeScript SDK for integrating your applications with Consent Nexus (Consent Management Platform). Use it on the server to authenticate securely, fetch consent notices and purposes, verify whether a user has valid consent, record consent artefacts, and handle CMP webhooks. Browser and React entry points are provided as scaffolds for upcoming in-app consent UI.
Package folder: pii-consent-sdk
npm name: consent-nexus-int
License: MIT
Table of contents
- Who this is for
- What you can build
- Requirements
- Installation
- Package entry points
- Configuration
- Quick start
- CMP client API
- Consent verification
- Recording consent
- Webhooks
- Express integration
- Encrypted authentication
- Signed consent artefacts
- Reachability checks
- Low-level utilities
- Browser and React (preview)
- Security practices
- Error handling
- Development and publishing
Who this is for
- Backend engineers wiring forms, APIs, or batch jobs that must not process PII until consent is valid for a given purpose.
- Integration owners connecting a data fiduciary’s systems to Consent Nexus using API keys and per-fiduciary encryption keys from the CMP dashboard.
- Teams that need typed TypeScript clients, optional Express routers, and webhook signature verification without hand-rolling auth and signing.
The SDK talks to your CMP base URL (the Consent Nexus deployment URL you were given). Route shapes and OpenAPI details live in your CMP integration documentation—not in this package.
What you can build
| Capability | SDK support |
|------------|-------------|
| Health / connectivity check before auth | checkCmpReachable, cmp.ping() |
| Encrypted token exchange + auto refresh | createCmpClient (default) |
| List notices and purpose categories | getConsentNotices, getPurposeCategories |
| Resolve email/phone → principal | resolvePrincipal |
| Verify consent for one or many purposes | verifyConsent, verifyConsentBatch |
| Record standard or HMAC-signed consent | recordConsent, recordSignedConsent |
| List / fetch consent records for audit | listConsents, getConsent |
| Send consent requests to users | requestConsent |
| Verify inbound CMP webhooks | verifyWebhook, Express createCmpWebhookRouter |
| Legacy API-key-only verify flow | createVerifyConsentClient |
Requirements
- Node.js 18+ (uses native
fetchand Web Crypto–compatible APIs vianode:crypto) - TypeScript 5.x recommended for types; JavaScript works with JSDoc or plain imports
- A Consent Nexus tenant with:
- Data fiduciary ID
- Integration API key and secret
- Integration encryption key (64 hex characters, from CMP → Integrations)
- CMP base URL (your deployment root, no trailing slash required—the SDK normalizes it)
Optional: Express 4+ if you use the bundled routers (peerDependency, optional).
Installation
npm install consent-nexus-int
# or
pnpm add consent-nexus-int
# or
yarn add consent-nexus-intPackage entry points
The package is published with multiple subpath exports so you only bundle what you need:
| Import path | Use when |
|-------------|----------|
| consent-nexus-int | Default: server API + shared types (same as ./server today) |
| consent-nexus-int/server | Node/backend: client, auth, webhooks, Express helpers |
| consent-nexus-int/browser | Future: framework-agnostic browser modal (scaffold) |
| consent-nexus-int/react | Future: React wrappers (scaffold) |
Server builds ship as ESM and CommonJS with declaration files. Browser is ESM-only (no require build).
Configuration
Create a client with CmpClientConfig:
| Field | Description |
|-------|-------------|
| cmpBaseUrl | Root URL of your Consent Nexus CMP instance |
| dataFiduciaryId | UUID of your data fiduciary in CMP |
| apiKey | Integration API key |
| apiSecret | Integration API secret (also used for webhook HMAC and signed consent) |
| integrationEncryptionKey | 64-character hex key (32 bytes) for cmp_encrypted_v1 auth envelopes |
| tokenRefreshSkewMs | Optional. Refresh access token this many ms before expiry (default: 60_000) |
Store secrets in environment variables or a secrets manager—never commit them to source control.
# Example environment variable names (choose your own convention)
CMP_BASE_URL=https://your-cmp.example.com
CMP_FIDUCIARY_ID=00000000-0000-0000-0000-000000000000
CMP_API_KEY=...
CMP_API_SECRET=...
CMP_INTEGRATION_ENCRYPTION_KEY=... # 64 hex chars from CMP IntegrationsQuick start
import { createCmpClient } from 'consent-nexus-int/server';
const cmp = createCmpClient({
cmpBaseUrl: process.env.CMP_BASE_URL!,
dataFiduciaryId: process.env.CMP_FIDUCIARY_ID!,
apiKey: process.env.CMP_API_KEY!,
apiSecret: process.env.CMP_API_SECRET!,
integrationEncryptionKey: process.env.CMP_INTEGRATION_ENCRYPTION_KEY!,
});
// Encrypted auth and token refresh run automatically on the first API call
const notices = await cmp.getConsentNotices({ status: 'active' });
const verification = await cmp.verifyConsent({
user: { kind: 'email', email: '[email protected]' },
purpose_id: 'purpose-uuid-here',
});
if (!verification.valid) {
// Block PII processing; show notice / collect consent
}
await cmp.recordSignedConsent({
notice_id: notices[0].id,
notice_record_hash: notices[0].record_hash!,
purpose_ids: ['purpose-uuid-here'],
consent_status: 'granted',
principal: { email: '[email protected]' },
});CMP client API
createCmpClient(config) returns a client object. All methods use Bearer tokens from the internal token manager unless noted.
Authentication
| Method | Description |
|--------|-------------|
| authenticate(requestedScopes?) | Explicit encrypted token exchange. Scopes: 'read' \| 'write'. |
| getAuthHeaders() | Returns { Authorization: 'Bearer …' } for custom fetch calls. |
| fetchJson<T>(path, init?) | Authenticated JSON request; retries once on 401 after refreshing token. |
Catalogue and principals
| Method | Description |
|--------|-------------|
| getConsentNotices({ status? }) | Returns active, draft, or archived notices (filter via status). |
| getPurposeCategories() | Purpose catalogue for your fiduciary. |
| resolvePrincipal({ email?, phone? }) | Returns { exists, principal_id, status }. |
Verification
| Method | Description |
|--------|-------------|
| verifyConsent({ user, purpose_id }) | Whether the user has valid consent for one purpose. |
| verifyConsentBatch({ user, purpose_ids }) | Parallel checks; array of { purpose_id, result }. |
Recording and sync
| Method | Description |
|--------|-------------|
| recordConsent(input) | Standard consent artefact (see Recording consent). |
| recordSignedConsent(input) | High-assurance record with canonical payload + HMAC (see Signed consent artefacts). |
| listConsents(filters?) | Paginated list; filters: status, data_principal_id, notice_id, updated_since, page, limit. |
| getConsent(consentId) | Single consent detail for audit. |
| requestConsent({ notice_id, email?, phone?, recipients? }) | Trigger a consent request to one or more recipients. |
Connectivity
| Method | Description |
|--------|-------------|
| ping(options?) | Same as checkCmpReachable for this client’s cmpBaseUrl. |
Webhooks
| Method | Description |
|--------|-------------|
| verifyWebhook(rawBody, signatureHeader) | Validates X-Webhook-Signature (HMAC-SHA256 hex). |
| acknowledgeWebhookDelivery(deliveryId) | Best-effort delivery ack when X-Webhook-Delivery-Id is present. |
Properties
| Property | Description |
|----------|-------------|
| config | Read-only copy of CmpClientConfig used to create the client. |
Consent verification
Identifying users (ConsentUser)
Verification accepts a discriminated user object:
type ConsentUser =
| { kind: 'principal_id'; principal_id: string }
| { kind: 'email'; email: string }
| { kind: 'phone'; phone: string }
| { kind: 'hash'; hash: string };Email is normalized to lowercase for cache keys and resolution. Phone and hash are trimmed.
Result shape (VerifyConsentResult)
{
valid: boolean;
consent?: {
id?: string;
notice_id?: string;
consent_status?: string;
expires_at?: string | null;
purpose_ids?: string[];
updated_at?: string;
};
principal_id?: string | null;
cache?: 'hit' | 'miss'; // when using createVerifyConsentClient cache
}Recommended path (full CMP client)
Use cmp.verifyConsent so auth, principal resolution, and integration verify routes stay consistent:
const result = await cmp.verifyConsent({
user: { kind: 'email', email: '[email protected]' },
purpose_id: 'purpose-uuid',
});Legacy verify-only client
For existing integrations that supply their own auth headers (e.g. raw API key headers):
import { createVerifyConsentClient } from 'consent-nexus-int/server';
const verify = createVerifyConsentClient({
cmpBaseUrl: process.env.CMP_BASE_URL!,
dataFiduciaryId: process.env.CMP_FIDUCIARY_ID!,
getAuthHeaders: async () => ({
'x-api-key': process.env.CMP_API_KEY!,
'x-api-secret': process.env.CMP_API_SECRET!,
}),
cache: {
enabled: true,
maxEntries: 10_000,
ttlMs: 60_000,
negativeTtlMs: 15_000,
},
});
const result = await verify.verifyConsent({
user: { kind: 'email', email: '[email protected]' },
purpose_id: 'purpose-uuid',
});Optional getPrincipalId lets you resolve principals from your own directory without calling CMP resolve.
When cmpClient is passed inside verify config (used internally by createCmpClient), verification uses the integration auth path and Bearer tokens.
Recording consent
recordConsent
Posts a consent artefact with method default consent_nexus_int_v1. Important fields:
| Field | Required | Notes |
|-------|----------|-------|
| notice_id | Yes | Notice UUID |
| purpose_ids | Yes | One or more purpose UUIDs |
| consent_status | Yes | 'granted' or 'denied' |
| principal | Often | { email?, phone? } when no data_principal_id |
| data_principal_id | Optional | If already known |
| session_id, language_preference, expires_at | Optional | Audit / UX metadata |
| ip_address, user_agent, device_info | Optional | Collection context |
| collection_metadata, ui_proof, org_map_id | Optional | Evidence and mapping |
recordSignedConsent
Extends recordConsent with:
| Field | Required | Notes |
|-------|----------|-------|
| notice_record_hash | Yes | Hash from the notice row at time of collection (tamper-evident linkage) |
The SDK builds a canonical JSON payload, signs it with your API secret (HMAC-SHA256), and sends the signature in a dedicated request header. Use this for high-assurance flows where notice version integrity matters.
Always use the current record_hash from getConsentNotices when the user accepts a notice.
Webhooks
CMP can POST events (e.g. consent created, withdrawn) to your backend.
Signature verification
- Algorithm: HMAC-SHA256 over the raw request body, hex-encoded digest.
- Header:
X-Webhook-Signature - Secret: your integration
apiSecret
const ok = cmp.verifyWebhook(rawBody, req.headers['x-webhook-signature']);Use the raw body string or buffer—do not re-serialize parsed JSON before verifying.
Delivery acknowledgement
If CMP sends X-Webhook-Delivery-Id, call acknowledgeWebhookDelivery(deliveryId) after you process the event (the Express helper does this when a full cmpClient config is provided).
Event handling
Event payloads are JSON objects; shape depends on event type. Handle them in your onEvent callback and keep processing idempotent.
Express integration
Express is an optional peer dependency. Pass your express module so the SDK does not pin a duplicate copy.
Verify proxy router
Mount createConsentSdkExpressRouter to expose a small JSON API for your frontend or BFF:
POST /verify— body:{ purpose_id: uuid, user: ConsentUser }→{ success: true, data: VerifyConsentResult }
import express from 'express';
import { createConsentSdkExpressRouter, createCmpClient } from 'consent-nexus-int/server';
const app = express();
app.use(express.json());
const cmp = createCmpClient({ /* ... */ });
app.use(
'/consent-sdk',
createConsentSdkExpressRouter({
express,
verify: {
cmpBaseUrl: process.env.CMP_BASE_URL!,
dataFiduciaryId: process.env.CMP_FIDUCIARY_ID!,
cmpClient: cmp, // preferred: uses encrypted auth
},
}),
);Webhook router
Mount createCmpWebhookRouter with express.raw({ type: 'application/json' }) on the same path so rawBody is available for signature verification:
app.use(
'/webhooks/cmp',
express.raw({ type: 'application/json' }),
createCmpWebhookRouter({
express,
apiSecret: process.env.CMP_API_SECRET!,
cmpClient: { /* full CmpClientConfig for auto-ack */ },
onEvent: async (event) => {
// Handle consent_withdrawn, consent_created, etc.
},
}),
);Encrypted authentication
By default, createCmpClient does not send API keys on every request. Instead it:
- Builds a
cmp_encrypted_v1envelope: AES-256-GCM over JSON{ api_key, api_secret, nonce, issued_at, requested_scopes? }. - Exchanges the envelope for access and refresh tokens via CMP’s integration auth API.
- Attaches
Authorization: Bearer <accessToken>to subsequent calls. - Refreshes before expiry (with configurable skew) and retries once on HTTP 401.
The integration encryption key must be exactly 64 hexadecimal characters (32 bytes). Obtain it from CMP → Integrations for your fiduciary.
Advanced use: import encryptAuthEnvelope, decryptAuthEnvelope, and CMP_ENCRYPTED_FORMAT_V1 for testing or custom auth flows. Full wire format is specified in your CMP integration documentation.
createTokenManager(config) exposes the same lifecycle for custom clients.
Signed consent artefacts
Exported helpers for custom pipelines:
| Export | Role |
|--------|------|
| buildSignedArtefactPayload(...) | Stable field set for signing |
| canonicalizeJson(value) | Deterministic JSON serialization (sorted keys) |
| signCanonicalPayload(canonical, apiSecret) | HMAC-SHA256 hex signature |
recordSignedConsent uses these internally. Payload includes fiduciary id, notice id, notice record hash, purpose ids, consent status, principal identifiers, issued_at, and a random nonce.
Reachability checks
Verify CMP is up before exchanging credentials (no API key required):
import { checkCmpReachable, createCmpClient } from 'consent-nexus-int/server';
const ping = await checkCmpReachable(process.env.CMP_BASE_URL!, {
timeoutMs: 10_000,
deep: true, // optional: deeper dependency check when supported by your CMP version
});
if (!ping.reachable) {
throw new Error(ping.error ?? 'CMP unreachable');
}
console.log(ping.status, ping.latencyMs, ping.version);Or via the client:
const status = await cmp.ping({ deep: false });CmpPingResult includes reachable, status ('ok' | 'degraded' | 'error'), latencyMs, optional version / timestamp, and error on failure.
Low-level utilities
| Export | Module | Purpose |
|--------|--------|---------|
| createCmpClient | server | Main integration client |
| createVerifyConsentClient | server | Verify-only with optional LRU cache |
| checkCmpReachable | server | Unauthenticated health check |
| createTokenManager | server | Token lifecycle without full client |
| encryptAuthEnvelope / decryptAuthEnvelope | server | cmp_encrypted_v1 crypto |
| verifyWebhookSignature | server | Standalone webhook HMAC check |
| acknowledgeWebhookDelivery | server | Standalone delivery ack |
| createConsentSdkExpressRouter | server | Express verify BFF |
| createCmpWebhookRouter | server | Express webhook handler |
| Shared types | main / server | CmpClientConfig, ConsentNotice, etc. |
Browser and React (preview)
Entry points consent-nexus-int/browser and consent-nexus-int/react are scaffolds for a future embedded consent modal (initialize, record consent, framework-agnostic events). They currently export minimal types and placeholders—do not rely on them in production until released in changelog notes.
Planned direction:
initialize({ apiBaseUrl, language? })— load notice config and render UIrecordConsent— submit choices back through CMP- React wrappers around the same core
Track package version releases for browser support.
Security practices
- Run the SDK only on trusted servers—API secret and encryption key must never ship to browsers or mobile apps.
- Use encrypted auth (
createCmpClient) instead of sending raw API secrets on every request when CMP supports it. - Rotate API keys and integration encryption keys through CMP when credentials may be exposed.
- For webhooks, always verify
X-Webhook-Signatureon the raw body; reject unsigned or mismatched requests with HTTP 401. - Treat
verifyConsentas a gate before PII processing; cache negative results briefly but respect withdrawal webhooks to invalidate local state. - Log errors without logging secrets, full tokens, or decrypted envelopes.
Error handling
- Failed HTTP responses throw
Errorwith CMP’serrormessage when present, or a genericRequest failed (status)message. - Auth failures surface as
Auth failed (status)orAuth response missing tokens. - Invalid
integrationEncryptionKeythrows at encrypt time: must be 64 hex characters. - Webhook ack failures throw with CMP error text when available.
- Express routers return 400 for validation errors, 401 for bad webhook signatures, 500 for unexpected verify failures.
Wrap client calls in your application’s retry/backoff policy for transient network errors; the client already retries once on 401 after token refresh.
Development and publishing
From this package directory:
pnpm install
pnpm run typecheck
pnpm run build
pnpm test| Script | Description |
|--------|-------------|
| pnpm run build | tsup → dist/ (ESM + CJS + .d.ts) |
| pnpm run dev | Watch mode rebuild |
| pnpm run test | Vitest unit tests |
| pnpm run test:live | Optional live tests against a running CMP (set CMP_BASE_URL in env) |
| pnpm run lint | ESLint on src/ |
prepublishOnly runs typecheck, build, and tests before npm publish.
Live integration tests
Point tests at your CMP instance (must be running and configured with valid fiduciary credentials in your test environment):
CMP_BASE_URL=https://your-cmp.example.com pnpm run test:liveDo not commit real secrets; use CI secrets or local .env files ignored by git.
Typings
TypeScript definitions ship in dist/*.d.ts. Import types from the same paths as runtime:
import type { CmpClientConfig, VerifyConsentResult, ConsentNotice } from 'consent-nexus-int/server';Versioning and support
- Follow semantic versioning on the npm package.
- Pin a major version in production and read release notes before upgrading.
- For CMP API changes (new fields, auth formats), upgrade SDK and CMP together per your vendor’s compatibility matrix.
Related documentation
- Consent Nexus CMP — integration keys, notices, purposes, and fiduciary setup (your deployment’s admin docs).
- Integration API reference — authoritative request/response schemas and route list (provided with your CMP license; not duplicated here to avoid drift and exposure of deployment-specific internals).
License
MIT © ProgIST Solutions. See LICENSE.
