Skip to content

Integración del SDK

Integración del SDK de Node.js

Añade licenciamiento de Keyright a una aplicación Node.js: instala el paquete keyright, verifica licencias sin conexión, activa en línea, restringe características según los derechos, y gestiona pruebas y máquinas aisladas (air-gapped) — fallando cerrado por diseño.

Esta es la contraparte en Node.js del recorrido del SDK de .NET — mismos formatos de licencia y lease, misma clave pública, mismas garantías de fallo cerrado, pero para un runtime de Node. El paquete keyright verifica licencias sin conexión contra tu clave pública embebida y, cuando quieres aplicación de puestos y revocación, activa en línea para almacenar en caché un lease firmado de vida corta. Cada bloque de código de abajo usa la superficie real del SDK; cópialos tal cual.

Como todos los SDK de Keyright verifican los mismos formatos de protocolo, una línea de productos multilenguaje puede compartir un tenant de Keyright y una clave pública. Si tu producto también incluye un componente .NET, Python o Java, todos pueden validar exactamente las mismas claves y leases.

El modelo, en un párrafo

Emites claves de licencia del lado del servidor en Keyright — cada tenant las firma con una clave privada RSA que nunca sale del servidor. Tu aplicación Node embebe solo la clave pública correspondiente y verifica licencias sin conexión, de modo que una comprobación de licencia no necesita red. Para la aplicación de puestos y la revocación la aplicación también activa en línea: intercambia una clave de licencia por un lease firmado de vida corta vinculado a la máquina, y luego sigue funcionando sin conexión hasta que ese lease caduca. Cualquier cosa que no cuadre — firma errónea, producto equivocado, caducidad, revocación, un reloj retrasado — se resuelve a la edición Free en lugar de lanzar una excepción. Falla cerrado.

Configuración del proveedor (hazlo una vez)

Los pasos del panel son independientes del lenguaje e idénticos a la guía de .NET, así que esta página los mantiene breves:

  1. Crea tu producto y anota su slug (p. ej. acme-app) — esa cadena es lo que tu cliente envía como product.
  2. Copia tu clave pública — la SubjectPublicKeyInfo en base64 de la pestaña Integration del panel. Se distribuye dentro de tu aplicación y no es un secreto; solo puede verificar firmas, nunca acuñarlas.
  3. Define tus niveles y derechos — cada nivel lleva un número de puestos y las banderas/límites nombrados que se incorporan a cada licencia de ese nivel.
  4. Emite claves — desde el panel, tu webhook de facturación o la API de administración.

Para la versión completa de estos pasos (capturas del panel, llamadas curl, el modelo de nivel/derecho y cómo llegan las claves a los clientes), consulta Primeros pasos y los pasos 1–4 y 8 del recorrido del SDK de .NET. El resto de esta página es todo código de cliente de Node.

Instala el SDK

keyright está en npm (>= 1.1.4). Es un paquete CommonJS con cero dependencias — la verificación de firmas se ejecuta sobre el crypto integrado de Node.

npm install keyright
const { KeyrightClient } = require('keyright');

Inicializa el cliente

Construye un cliente en el arranque con KeyrightClient.initialize. Las opciones son un objeto plano con claves en camelCase — no hay una clase de opciones que instanciar con new:

const { KeyrightClient } = require('keyright');

const license = KeyrightClient.initialize({
  product:         'acme-app',                          // must match the product slug you issue keys for
  publicKeyBase64: 'MIIBIjANBgkq...',                   // the base64 public key from vendor setup
  serviceUrl:      'https://keyright.delta1labs.com',   // omit if you ship offline license files only

  // Optional: keep an old key valid during a rotation window
  // additionalPublicKeysBase64: ['<previous public key>'],
});

Solo product y publicKeyBase64 son obligatorios; initialize lanza excepción de inmediato si falta publicKeyBase64, y analiza la(s) clave(s) por adelantado. serviceUrl solo se necesita para activate/deactivate en línea.

Más allá de esos, el cliente resuelve una licencia a partir de varias fuentes en orden de precedencia (de mayor a menor):

OpciónQué es
licenseStringUna cadena JSON de licencia que suministras directamente.
licenseFilePathUna ruta a un archivo de licencia.
envVarNameNombre de una variable de entorno que contiene una ruta de archivo de licencia. Ponla a 'KEYRIGHT_LICENSE' para honrar esa convención — no se lee salvo que la nombres.
configLicensePathUna ruta de archivo de licencia suministrada por configuración.
(predeterminado)La ruta de datos de aplicación del SO …/Keyright/<product>/license.json.
(lease)El lease de activación en caché — escrito por ti por activate.

Normalmente no defines ninguna de estas — la activación escribe la caché del lease automáticamente. Otros ajustes opcionales: revocationListJson / revocationListPath (distribuye una lista de revocación firmada para compilaciones sin conexión), nodeLockTolerance (por defecto 1), clockTamperToleranceHours (por defecto 24), additionalPublicKeysBase64 y machineComponents (personaliza qué identifica a una máquina).

Restringe características sin conexión

client.validate() es síncrono. Resuelve la mejor licencia de las fuentes de arriba, verifica la firma RSA, la coincidencia de producto, el bloqueo por equipo, la caducidad, la lista de revocación opcional distribuida y el estado de prueba/manipulación-de-reloj — todo sin conexión — y nunca lanza una excepción. Devuelve { info, source }: desestructúralo. Ante cualquier fallo, info es un LicenseInfo en la edición free que lleva el motivo, y source te dice qué fuente ganó (p. ej. 'lease', 'default', 'none').

const { info, source } = license.validate();

if (info.isPaid) {                 // true for any edition above Free
  // unlock paid features
}

if (info.edition === 'enterprise') {
  // unlock enterprise-only features
}

LicenseInfo expone campos y getters en camelCase: status, isValid, isPaid, isTrial, edition, licensee, message, expiryUtc (un Date o null), daysRemaining (un getter, null cuando no hay caducidad), statusBadge (p. ej. "Enterprise Trial") y entitlements. status es una de las cadenas LicenseStatus en minúsculas: 'valid', 'no_license', 'malformed', 'signature_invalid', 'expired', 'machine_mismatch', 'revoked', 'clock_tampered'. edition es 'free', 'licensed' o 'enterprise'.

Restringe características individuales según los derechos en lugar de la edición, para que cambiar la plantilla de un nivel no implique distribuir código nuevo. info.entitlements es un EntitlementSet con isEnabled(name) y getLimit(name, fallback), y ambos fallan cerrado:

const { info } = license.validate();

// Boolean flag — missing/unparseable reads as disabled
if (info.entitlements.isEnabled('export')) {
  showExportCommand();
}

// Numeric limit — pass the fail-closed fallback yourself; 'unlimited' reads as a huge number
const maxProjects = info.entitlements.getLimit('max-projects', 1);
if (currentProjectCount >= maxProjects) {
  promptToUpgrade();
}

isEnabled trata 'true', '1', 'yes', 'enabled' (sin distinguir mayúsculas/minúsculas) como activado; cualquier otra cosa, o una bandera ausente, está desactivado. getLimit devuelve tu fallback para un valor ausente o no entero, y un entero máximo seguro para el literal 'unlimited'. Los nombres de derechos no distinguen mayúsculas/minúsculas.

El cliente también ofrece envoltorios de conveniencia de un solo uso que validan en el acto — license.isEnabled('export'), license.getLimit('max-projects', 1), license.entitlements() y license.edition() — pero si compruebas varios derechos a la vez, llama a validate() una vez y reutiliza info.entitlements para evitar trabajo de verificación repetido.

Para averiguar por qué falló una comprobación (para un diálogo de “Register” o diagnóstico), lee info.status y el info.message orientado a personas.

Activa en línea con una clave de licencia

Cuando un cliente introduce una clave, llama a await client.activate(key). Hace POST de la clave más un id de máquina estable a <serviceUrl>/v1/activate; el servicio consume un puesto y devuelve un lease firmado de vida corta vinculado a esa máquina. El SDK verifica el lease contra tu clave pública embebida y lo almacena en caché localmente, de modo que cada validate() posterior tiene éxito sin red hasta que transcurre la ventana de gracia del lease.

activate devuelve un LicenseInfo y no lanza excepción para las rutas de fallo ordinarias (clave errónea, límite de puestos, sin conexión, revocada) — como validate(), devuelve un LicenseInfo que falla cerrado. Inspecciona el resultado:

const info = await license.activate(customerEnteredKey);

if (info.isValid && info.isPaid) {
  // Activated. The lease is cached; the app now works offline until it expires.
  showLicensedUi(info.statusBadge);        // e.g. "Enterprise" or "Enterprise Trial"
} else {
  // Surface info.message; the app stays in Free mode.
  showActivationError(info.message);       // "All seats for this license are in use.", etc.
}
  • Los puestos se aplican del lado del servidor. Activar más máquinas de las que la licencia permite devuelve un resultado de límite de puestos (message: "All seats for this license are in use.") y ningún lease. Reactivar una máquina que ya está vinculada es idempotente — no se consume un puesto extra.
  • Gracia sin conexión. Si el servicio es inalcanzable, activate recurre a cualquier lease en caché que siga siendo válido y lo devuelve, de modo que una breve caída no deja fuera al usuario. Solo si no hay un lease en caché válido devuelve un info Free con el error de conexión en message.
  • La revocación surte efecto en el siguiente refresco.

activate solo lanza excepción por errores de programación: un serviceUrl ausente o una clave vacía.

El id de máquina que envía es MachineFingerprint.current().toBoundString() — una huella digital estable por dispositivo. nodeLockTolerance (por defecto 1) deja pasar una NIC o un disco cambiados sin dejar fuera al usuario.

Desactiva — libera un puesto

Para liberar el puesto de esta máquina (antes de dar de baja un equipo, o para que el cliente pueda mover la licencia), llama a await client.deactivate(key). Hace POST de { key, machineId, product } a <serviceUrl>/v1/free-seat y devuelve la cadena de estado — 'ok' o 'not_found'. Ante 'ok' también borra el lease en caché local, de modo que el siguiente validate() vuelve a Free.

A diferencia de activate, deactivate no tiene fallback sin conexión: si la llamada HTTP falla lanza excepción. Envuélvela:

try {
  const status = await license.deactivate(customerEnteredKey);
  if (status === 'ok') {
    // Seat released and local lease cleared.
  } else {
    // 'not_found' — nothing was bound for this key/machine.
  }
} catch (err) {
  // Could not reach the issuing service — try again when back online.
}

Máquinas sin conexión y aisladas (air-gapped)

Para una máquina que nunca puede alcanzar el servicio, un operador firma un lease sin conexión para su id de máquina (panel Licenses → offline lease, o POST /admin/licenses/{id}/offline-lease) y entrega el JSON por archivo. El id de máquina a dar al operador es exactamente lo que el SDK deriva:

const { MachineFingerprint } = require('keyright');
console.log(MachineFingerprint.current().toBoundString());

Importa el lease firmado con client.importOfflineLease(json). Verifica el lease por la misma ruta de código que usa un lease en caché (firma, producto, máquina, caducidad), lo almacena en caché para que las llamadas validate() sin conexión posteriores tengan éxito, y devuelve un LicenseInfo. A diferencia de activate, lanza excepción si el lease es inválido — manéjalo:

const fs = require('fs');

try {
  const info = license.importOfflineLease(fs.readFileSync('acme.lease.json', 'utf8'));
  // info.isValid === true; the lease is now cached on this machine.
} catch (err) {
  // wrong signature / product / machine, or expired
  showActivationError('This offline lease could not be verified: ' + err.message);
}

Acepta tanto una cadena JSON como un objeto ya analizado.

Pruebas

Una clave de prueba se activa por la misma ruta activate exacta que una de pago, y se reemplaza sin fisuras cuando el cliente activa luego una clave de pago. Lee el estado de la prueba directamente desde LicenseInfo:

const { info } = license.validate();

if (info.isTrial) {
  const left = info.daysRemaining;                    // e.g. 27, or null if no expiry
  showTrialBadge(info.statusBadge, left);             // statusBadge -> "Enterprise Trial"
  // info.expiryUtc is a Date you can format for a "trial ends on…" line
}

La duración de una prueba se cuenta desde la primera activación en la máquina: el SDK persiste un pequeño archivo de estado local y, una vez que transcurre la ventana de prueba, validate() devuelve un info Free con status: 'expired'. Si el reloj del sistema se mueve hacia atrás más allá de clockTamperToleranceHours (por defecto 24h) en una prueba o suscripción, validate() devuelve status: 'clock_tampered'; se recupera en la siguiente llamada una vez que se ajusta la hora correcta. Las licencias perpetuas nunca están sujetas a ninguna de las dos comprobaciones. Flujo completo: Pruebas gratuitas de autoservicio.

Un ejemplo mínimo de extremo a extremo

const { KeyrightClient } = require('keyright');

const license = KeyrightClient.initialize({
  product:         'acme-app',
  publicKeyBase64: 'MIIBIjANBgkq...',
  serviceUrl:      'https://keyright.delta1labs.com',
});

async function main() {
  // 1. Offline check on startup — no network needed.
  let { info } = license.validate();

  // 2. If unlicensed and the user pasted a key, activate online.
  if (!info.isPaid && process.env.LICENSE_KEY) {
    info = await license.activate(process.env.LICENSE_KEY);
  }

  // 3. Gate features on entitlements, failing closed.
  if (info.isPaid && info.entitlements.isEnabled('export')) {
    console.log('Export enabled for', info.licensee, '(' + info.statusBadge + ')');
  } else {
    console.log('Running in Free mode:', info.message || 'no paid license');
  }
}

main();

Solución de problemas

  • La activación dice que la clave no se reconoció. La causa más común es una discrepancia de producto: la clave se emitió para un producto (o un tenant) distinto del slug product con el que tu cliente está configurado. El servidor acota la activación por producto, así que una clave válida para acme-app no activará un cliente inicializado con product: 'other-app'. Confirma que el slug coincide con el producto bajo el que emitiste la clave.
  • Todo se lee como Free. Es el diseño — el SDK falla cerrado ante cualquier problema de verificación en lugar de lanzar una excepción. Lee info.status ('no_license', 'signature_invalid', 'expired', 'machine_mismatch', 'revoked', 'clock_tampered') e info.message para ver cuál es. Un 'signature_invalid' casi siempre significa que el publicKeyBase64 embebido no coincide con el tenant que firmó la clave.
  • Una licencia por tiempo limitado de repente no valida. Si el reloj del sistema se movió hacia atrás más allá de clockTamperToleranceHours (por defecto 24h) en una prueba o suscripción, el estado es 'clock_tampered'. No se queda pegado — ajusta la hora correcta y la validación se recupera en la siguiente llamada. Las licencias perpetuas nunca están sujetas a esta comprobación.
  • deactivate lanza excepción. No tiene fallback sin conexión por diseño — una llamada HTTP fallida lanza excepción en lugar de tener éxito silenciosamente. Solo cuando devuelve 'ok' se libera el puesto y se borra el lease local; reintenta cuando vuelva la conectividad.
  • importOfflineLease lanza excepción. El lease falló la verificación (firma/producto/máquina erróneos, o caducado) — confirma que el operador lo firmó para el id de máquina de MachineFingerprint.current().toBoundString() en este equipo.

Véase también