# Node.js SDK Integration Integrate CoreLicense with Express, Fastify, and NestJS using middleware and plugins. Canonical HTML URL: https://corelicense.net/docs/nodejs-sdk/integration Plain-text URL: https://corelicense.net/docs/nodejs-sdk/integration.txt ---# Integration Detailed guide for integrating `@corelicense/node` into a production system — for developers and AI agents generating code. **Read first:** [Installation](/docs/nodejs-sdk/quick-start) --- ## Table of Contents 1. [Architecture & Principles](#1-architecture--principles) 2. [Runtime Lifecycle](#2-runtime-lifecycle) 3. [Express Integration (Full)](#3-express-integration-full) 4. [Fastify Integration](#4-fastify-integration) 5. [NestJS Integration](#5-nestjs-integration) 6. [Feature Gate, Limit & Quota](#6-feature-gate-limit--quota) 7. [Worker & CLI Guard](#7-worker--cli-guard) 8. [Deep Integration — Module Registry & DI](#8-deep-integration--module-registry--di) 9. [Large Application Patterns](#9-large-application-patterns) 10. [Error Handling](#10-error-handling) 11. [Events & Background Sync](#11-events--background-sync) 12. [Security & Data Sent to Server](#12-security--data-sent-to-server) 13. [Production Checklist](#13-production-checklist) 14. [Troubleshooting](#14-troubleshooting) --- ## 1. Architecture & Principles ### Middleware Is Not Enough HTTP middleware only blocks requests. Attackers or bugs can call services/workers directly. **Deep integration** means: - Modules/services **must not be bound** if the feature is disabled in policy - Every `get()` / `resolve()` **re-checks** the license (revoke blocks immediately) - Workers/CLI have their own guards - Quota/limit enforced at the point of consumption ### Two Layers of Protection | Layer | Mechanism | When | |-------|-----------|------| | **HTTP** | `express()` / `fastifyPlugin()` / Nest guards | Every API request | | **Runtime** | `requireFeature`, registry, DI, worker guard | Service, job, CLI | ### Policy Is the Source of Truth The policy token (JWT, EdDSA) contains: - `status`: `active` | `suspended` | `revoked` | ... - `features`: map of feature → boolean - `limits`: maximum values (users, workers, ...) - `quotas` / local usage: daily consumption Admin changes policy on the server → SDK refreshes → runtime updates. **Binding modules/services** only happens at `bootstrap()` — changing a feature requires an **app restart** to mount/unmount modules; but `get()`/`resolve()` still block immediately on revoke. --- ## 2. Runtime Lifecycle ``` constructor → boot() → [bootstrap()] → app running ↓ background check (6h default) ↓ syncFromServer (mutex — no race) ↓ requireActive() on every critical gate ``` ### `boot()` — Required Before Everything ```ts await license.boot(); ``` - Fail-closed v0.5: if license is revoked/expired and offline grace is exhausted → **throw**, process should exit - Call `boot()` **once** when the process starts (before `listen()`) ### Instance ID ```ts instance: { strategy: 'install-id-domain-hash', // default — recommended for production domain: process.env.APP_DOMAIN, // e.g.: app.customer.com } ``` - `install-id-domain-hash`: `sha256(productId:licenseKey:installId:domain)` — install ID stored in `install.id` file under `cachePath` - `fixed`: use existing `instance.instanceId` (reuse a registered instance) - `custom`: `sha256(customId)` — custom container/K8s pod id The server counts **max instances** per license — each install-id + domain pair is one instance. --- ## 3. Express Integration (Full) ### App Skeleton ```ts import express from 'express'; import { CoreLicense, loadOptionsFromEnv, registerLicensedRoutes } from '@corelicense/node'; const app = express(); app.use(express.json()); const license = new CoreLicense( loadOptionsFromEnv({ cachePath: './storage/corelicense', instance: { strategy: 'install-id-domain-hash', domain: process.env.APP_DOMAIN }, checkIntervalMs: 6 * 60 * 60 * 1000, }), ); async function main() { await license.boot(); // --- Routes that do NOT require license --- app.get('/health', (_req, res) => res.json({ ok: true })); app.get('/license/status', async (_req, res) => { res.json(await license.getStatus()); }); // --- License middleware (entire app) --- app.use( license.express({ allowPaths: ['/health', '/license/status', '/license/reactivate'], mode: 'block', }), ); // --- Deep integration (optional) --- const { modules, services, skipped } = await license.bootstrap({ modules: [ { name: 'automation', feature: 'automation', factory: ({ license }) => new AutomationModule(license), }, { name: 'export', feature: 'export', factory: ({ license }) => new ExportModule(license), }, ], services: [ { name: 'ExportService', feature: 'export', singleton: true, factory: ({ license }) => new ExportService(license), }, ], }); console.log('Skipped (feature off):', skipped); // Mount router ONLY when module is registered registerLicensedRoutes(app, modules, [ { path: '/api/automation', module: 'automation', router: automationRouter }, { path: '/api/export', module: 'export', router: exportRouter }, ]); // --- Regular routes (already passed requireActive middleware) --- app.get('/api/me', async (_req, res) => { await license.requireFeature('dashboard'); res.json({ ok: true }); }); app.listen(process.env.PORT ?? 3000); } main().catch((err) => { console.error('Boot failed:', err); process.exit(1); }); ``` ### `allowPaths` Exact match on `req.path`. Always whitelist: - `/health` — load balancer - `/license/status` — ops dashboard - `/license/reactivate` — reactivation flow (if applicable) ### Middleware 403 Response ```json { "ok": false, "code": "LICENSE_REVOKED", "source": "core-license.requireActive", "message": "..." } ``` Use `error.toClientPayload()` — do not leak stack traces. ### `registerLicensedRoutes` - Only `app.use(path, router)` if `modules.has(name)` - Feature off → route **does not exist** (404), not 403 — harder to discover locked APIs --- ## 4. Fastify Integration ```ts import Fastify from 'fastify'; import { CoreLicense, loadOptionsFromEnv } from '@corelicense/node'; const app = Fastify({ logger: true }); const license = new CoreLicense(loadOptionsFromEnv({ cachePath: './storage/corelicense' })); async function main() { await license.boot(); app.get('/health', async () => ({ ok: true })); await app.register( license.fastifyPlugin({ allowPaths: ['/health', '/license/status'], }), ); app.get('/api/data', async () => { await license.requireFeature('api'); return { data: [] }; }); await app.listen({ port: 3000, host: '0.0.0.0' }); } main().catch((err) => { app.log.error(err); process.exit(1); }); ``` Direct import: ```ts import { createFastifyPlugin } from '@corelicense/node/fastify'; await app.register(createFastifyPlugin(license, { allowPaths: ['/health'] })); ``` --- ## 5. NestJS Integration ### `AppModule` ```ts // app.module.ts import { Module } from '@nestjs/common'; import { CoreLicenseModule } from '@corelicense/node/nestjs'; import { loadOptionsFromEnv } from '@corelicense/node'; import { AutomationModule } from './automation/automation.module.js'; @Module({ imports: [ CoreLicenseModule.forRoot({ ...loadOptionsFromEnv({ cachePath: './storage/corelicense', instance: { strategy: 'install-id-domain-hash', domain: process.env.APP_DOMAIN }, }), }), AutomationModule, ], }) export class AppModule {} ``` `CoreLicenseModule.forRoot()`: - Creates `CoreLicense`, calls `boot()` in the factory - Registers `CoreLicenseGuard`, `CoreLicenseFeatureGuard` - Registers `CoreLicenseExceptionFilter` → HTTP 403 + `toClientPayload()` ### Controller — License Active ```ts import { Controller, Get, UseGuards } from '@nestjs/common'; import { CoreLicense } from '@corelicense/node'; import { CoreLicenseGuard } from '@corelicense/node/nestjs'; @UseGuards(CoreLicenseGuard) @Controller('api') export class ApiController { constructor(private readonly license: CoreLicense) {} @Get('status') async status() { return this.license.getStatus(); } } ``` ### Controller — Specific Feature ```ts import { Controller, Get, UseGuards, } from '@nestjs/common'; import { CoreLicenseGuard, CoreLicenseFeatureGuard, RequireFeature, } from '@corelicense/node/nestjs'; @RequireFeature('automation') @UseGuards(CoreLicenseGuard, CoreLicenseFeatureGuard) @Controller('automation') export class AutomationController { @Get() list() { return { items: [] }; } } ``` ### Health Controller (No Guard) ```ts @Controller('health') export class HealthController { @Get() health() { return { ok: true }; } } ``` Place `HealthController` **without** `CoreLicenseGuard` — or use a global guard with `@SetMetadata` skip (app-dependent). ### Inject `CoreLicense` into Service ```ts import { Injectable } from '@nestjs/common'; import { CoreLicense, LicensedService } from '@corelicense/node'; @Injectable() export class ExportService extends LicensedService { constructor(license: CoreLicense) { super(license); } async exportUsers() { await this.require('export.users'); await this.license.consumeQuota('export.users', 1); // business logic... } } ``` --- ## 6. Feature Gate, Limit & Quota ### Feature ```ts await license.requireFeature('automation'); await license.requireFeature('export.pdf'); await license.requireFeature('worker.video'); ``` Example policy `features`: ```json { "automation": true, "export": true, "export.pdf": false, "worker.video": true } ``` `requireFeature` → audit log `feature_allowed`. Throws `FeatureDisabledError` (`FEATURE_DISABLED`). ### Limit (Current Value vs Policy Ceiling) ```ts const running = await jobQueue.countActive(); await license.checkLimit('maxConcurrentJobs', running); const userCount = await db.users.count(); await license.checkLimit('maxUsers', userCount); ``` Throws when `currentValue >= limit` in policy. ### Quota (Daily Cumulative Consumption, Local Cache) ```ts await license.consumeQuota('export.daily', 1); await license.consumeQuota('api.calls', 10); ``` - Usage stored in `usage.json` under `cachePath` - Resets by day (UTC date string) - `consumeQuota` / `checkLimit` / `getPolicy` all call `requireActive()` first ### Runtime Config ```ts const runtime = await license.getRuntimeConfig(); // { // status: 'active', // modules: { dashboard: true, automation: false, ... }, // limits: { maxWorkers: 2, maxUsers: 100 }, // policyVersion: 'v3', // workers: { ... } // if defined in policy // } ``` Use to **conditionally drive UI** or configure worker count — does not replace `requireFeature` for security. --- ## 7. Worker & CLI Guard ### BullMQ / Queue Worker ```ts import { Worker } from 'bullmq'; await license.boot(); await license.guardWorker('main-worker'); const worker = new Worker('jobs', async (job) => { await license.requireFeature(`job.${job.name}`); await license.consumeQuota(`job.${job.name}`, 1); return processJob(job); }); ``` `guardWorker(name)` checks whether policy allows the worker name — throws `WORKER_NOT_ALLOWED`. ### Cron / CLI ```ts async function main() { await license.boot(); await license.guardCommand('nightly-export'); await runExport(); } main().catch(console.error); ``` Throws `COMMAND_NOT_ALLOWED` if the feature/command is not licensed. --- ## 8. Deep Integration — Module Registry & DI ### When to Use - App has multiple large modules (automation, export, AI, ...) - Want to **not load code** for license-locked modules - Want to conditionally mount routes / register providers ### `bootstrap()` — Once After `boot()` ```ts const result = await license.bootstrap({ modules: ModuleDefinition[], services: ServiceDefinition[], }); // result.modules → LicensedModuleRegistry // result.services → LicensedServiceContainer // result.skipped → [{ name, feature, reason: 'feature_disabled' }] ``` ### `ModuleDefinition` ```ts { name: 'automation', // unique id feature: 'automation', // key in policy.features factory: ({ license, runtime }) => new AutomationModule(license), } ``` ### `ServiceDefinition` ```ts { name: 'ExportService', feature: 'export', singleton: true, // default true — factory runs once factory: ({ license }) => new ExportService(license), } ``` ### `modules.get(name)` ```ts const automation = await modules.get('automation'); await automation.runJob(payload); ``` - Module not registered → `ModuleNotRegisteredError` - Feature off at bootstrap → not registered → `ModuleNotLicensedError` - **Every `get()`** calls `requireFeature` — revoke/suspend blocks immediately ### `services.resolve(name)` ```ts const svc = await services.resolve('ExportService'); await svc.exportUsers(); ``` - Singleton: internal instance cache, but **every `resolve()` re-checks license** - Re-check does **not** write audit (unlike `license.requireFeature()`) ### Create Registry Manually (Advanced) ```ts const modules = license.createLicensedRegistry(); const services = license.createLicensedContainer(); const ctx = { license, runtime: await license.getRuntimeConfig(), }; await modules.bootstrap(definitions, ctx); ``` ### `LicensedService` Base Class ```ts import { LicensedService, CoreLicense } from '@corelicense/node'; export class ExportService extends LicensedService { constructor(license: CoreLicense) { super(license); } async exportUsers() { await this.require('export.users'); await this.license.consumeQuota('export.users', 1); // ... } } ``` --- ## 9. Large Application Patterns ### Suggested Directory Structure ``` src/ bootstrap/ license.ts # initialize CoreLicense + boot() modules.ts # define bootstrap modules/services modules/ automation/ automation.module.ts automation.router.ts export/ export.service.ts app.ts # express + registerLicensedRoutes ``` ### `bootstrap/license.ts` ```ts import { CoreLicense, loadOptionsFromEnv } from '@corelicense/node'; let license: CoreLicense; export async function initLicense() { license = new CoreLicense(loadOptionsFromEnv({ cachePath: './storage/corelicense' })); await license.boot(); return license; } export function getLicense() { if (!license) throw new Error('License not initialized'); return license; } ``` ### `bootstrap/modules.ts` ```ts import type { BootstrapResult } from '@corelicense/node'; import { getLicense } from './license.js'; import { AutomationModule } from '../modules/automation/automation.module.js'; import { ExportService } from '../modules/export/export.service.js'; export async function bootstrapApp(): Promise { return getLicense().bootstrap({ modules: [ { name: 'automation', feature: 'automation', factory: ({ license }) => new AutomationModule(license) }, ], services: [ { name: 'ExportService', feature: 'export', factory: ({ license }) => new ExportService(license) }, ], }); } ``` ### Graceful Shutdown ```ts process.on('SIGTERM', () => { license.stopBackgroundCheck(); process.exit(0); }); ``` ### Docker ```dockerfile ENV CORELICENSE_KEY=... ENV CORELICENSE_PRODUCT_ID=... ENV APP_DOMAIN=app.customer.com VOLUME ["/app/storage/corelicense"] ``` Persist `cachePath` — losing the volume = losing install-id → counts as a new instance. ### Multi-Tenant - **One license / one deploy** (recommended): each customer gets their own deployment + key - Do not share one `CoreLicense` instance across multiple tenants — instance ID and policy are tied to one license --- ## 10. Error Handling ### Type Guard ```ts import { isLicenseError, formatLicenseError, LicenseErrorCode, } from '@corelicense/node'; try { await license.boot(); } catch (error) { if (isLicenseError(error)) { console.error(error.code, error.source, error.message); // CONFIG_MISSING_ENV, LICENSE_REVOKED, OFFLINE_GRACE_EXPIRED, ... } throw error; } ``` ### HTTP API ```ts if (isLicenseError(error)) { res.status(403).json(error.toClientPayload()); } ``` ### Common Error Codes | Code | Meaning | |------|---------| | `CONFIG_MISSING_ENV` | Missing `CORELICENSE_KEY` / `CORELICENSE_PRODUCT_ID` | | `LICENSE_REVOKED` | Key revoked | | `LICENSE_SUSPENDED` | Suspended | | `LICENSE_EXPIRED` | Expired | | `OFFLINE_GRACE_EXPIRED` | Server unreachable for too long | | `FEATURE_DISABLED` | Feature not in policy | | `MODULE_NOT_LICENSED` | Module not bound / feature off | | `QUOTA_EXCEEDED` | Daily quota exceeded | | `MAX_INSTANCES` | Instance count exceeded | | `INSTANCE_BLOCKED` | Instance blocked in admin | | `POLICY_SIGNATURE_ERROR` | Token failed verification | See full list: [API Reference — Error codes](/docs/nodejs-sdk/api-reference#error-codes) --- ## 11. Events & Background Sync ### Event Bus ```ts license.on('licenseRevoked', (payload) => { console.error('License revoked', payload); process.exit(1); }); license.on('policyUpdated', (payload) => { console.log('Policy updated', payload); }); license.on('offlineMode', (payload) => { console.warn('Offline mode', payload); }); ``` Events: `statusChanged`, `policyUpdated`, `licenseRevoked`, `licenseSuspended`, `offlineMode` ### Background Check `boot()` auto-starts if `checkIntervalMs > 0`. Manual control: ```ts license.startBackgroundCheck(); license.stopBackgroundCheck(); ``` `syncFromServer` uses a **mutex** — parallel refresh does not race. ### Manual Refresh ```ts await license.refresh(); ``` `refresh` endpoint on the License Server. --- ## 12. Security & Data Sent to Server SDK **only sends** (activate): - `productId`, `licenseKey`, `instanceId` - `application.domainHash`, `application.environment` (from `NODE_ENV`) - SDK metadata (`name`, `version`, `runtime`, `runtimeVersion`, `appVersion`) SDK **only sends** (check/refresh): - `productId`, `licenseKey`, `instanceId` - `currentPolicyHash` (SHA-256 of current policy token) - SDK metadata **Does not send:** database, user data, business logs. ### apiUrl Pin (v0.5) The server may return `apiUrl` in the response. The SDK **only accepts** URLs in the whitelist (trusted apiUrl, env, embedded key). Malicious redirects are rejected + audit `api_url_redirect_rejected`. ### Public Key - Default: fetch `GET /v1/public/products/{productId}/keys` - Enterprise: pin `CORELICENSE_PUBLIC_KEY` --- ## 13. Production Checklist - [ ] `CORELICENSE_KEY` + `CORELICENSE_PRODUCT_ID` in env (secret manager) - [ ] `APP_DOMAIN` / `instance.domain` set correctly - [ ] `cachePath` persisted (volume) - [ ] `boot()` failure → `process.exit(1)` — do not start app "half-licensed" - [ ] `/health` in `allowPaths` - [ ] License Server: `SDK_PUBLIC_URL=https://api.corelicense.net` - [ ] Policy signer configured on Go API - [ ] Audit log path has rotation - [ ] Test revoke: admin revoke → app blocks within refresh interval / immediately at `requireFeature` --- ## 14. Troubleshooting | Symptom | Cause | Fix | |---------|-------|-----| | `CONFIG_MISSING_ENV` | Missing env | Set `CORELICENSE_*` | | `boot()` fails offline | Grace expired | Connect to server or temporarily set `offlineGrace: false` only if you understand the risk | | `MAX_INSTANCES` | Too many instances | Delete old instance in admin or increase limit | | `POLICY_SIGNATURE_ERROR` | Key rotation / wrong pin | Update public key or remove pin | | Middleware 403 on every route | Missing `allowPaths` | Add `/health`, ... | | Module 404 | Feature off | By design — `registerLicensedRoutes` skips | | Nest 403 without standard JSON | Missing filter | Use `CoreLicenseModule.forRoot()` (filter included) | --- ## Links - [Installation](/docs/nodejs-sdk/quick-start) - [API Reference](/docs/nodejs-sdk/api-reference) - [Client API (License Server)](/docs/client-api) - [README](https://www.npmjs.com/package/@corelicense/node)