SDK de gestión para Node.js

@keenauth/node es el SDK de TypeScript para la API de gestión de KeenAuth. Necesita Node.js 22 o posterior y no tiene dependencias en tiempo de ejecución. Todavía no está publicado en npm.

El paquete tiene estos puntos de entrada:

Importación Para qué sirve
@keenauth/node El cliente de la API de gestión, los tokens de gestión, la paginación y los errores
@keenauth/node/webhooks Verificar las firmas de los webhooks, sin cargar el cliente de la API
@keenauth/node/bff Iniciar sesión en aplicaciones de servidor: Express y Fastify
@keenauth/node/middleware Proteger tu API con tokens de portador
@keenauth/node/testing El kit de pruebas sin conexión

Crear un cliente

Con una clave de API secreta o restringida:

import { KeenAuth } from "@keenauth/node";

const keenauth = new KeenAuth({
  baseUrl: "https://api.eu.keenauth.com/v1",
  apiKey: process.env.KEENAUTH_API_KEY!,
});

const user = await keenauth.users.getUser({ userId: "usr_01j9zzzzzzzzzzzzzzzzzzzz" });

baseUrl es la dirección de la API de tu región con /v1. Debe usar https; http simple solo se acepta para localhost. La clave solo se envía en la cabecera Authorization y solo a esa dirección. No se siguen las redirecciones.

Autenticarse sin clave de API

Una aplicación management puede llamar a la API con un token de corta duración en lugar de una clave estática. La aplicación firma una aserción de cliente con su clave privada (private_key_jwt, RFC 7523) y el SDK la intercambia en el endpoint de token de tu entorno.

  1. En el panel, crea una aplicación de tipo management y un registro de cliente de gestión con los permisos que necesite (consulta Crear un cliente de gestión).
  2. Genera un par de claves y registra la clave pública como credencial public_key de la aplicación. Por ejemplo, para ES256: openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out management-key.pem.
  3. Guarda la clave privada en tu almacén de secretos. Pásala al SDK:
import { readFileSync } from "node:fs";
import { KeenAuth } from "@keenauth/node";

const keenauth = new KeenAuth({
  baseUrl: "https://api.eu.keenauth.com/v1",
  credentials: {
    issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
    clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
    privateKey: readFileSync("management-key.pem", "utf8"), // PEM, a private JWK or a KeyObject
    scope: ["users:read", "users:write"], // optional; default: the application's default scopes
  },
});

Lo que hace el SDK:

Una solicitud de token fallida lanza TokenError con el code de OAuth (por ejemplo invalid_client o invalid_scope), description y status. Un error de configuración, como una clave pública en lugar de una privada, lanza una excepción al crear el cliente.

apiKey y credentials no se pueden usar juntos.

Reintentos

El SDK reintenta una solicitud como máximo 3 veces en total cuando la API responde 429, 502, 503 o 504, o cuando falla la conexión. Solo reintenta solicitudes GET, PUT y DELETE, y solicitudes POST que incluyan un Idempotency-Key. Un PATCH que incluye If-Match solo se reintenta con 429: el limitador de frecuencia rechaza la solicitud antes de ejecutarla, y If-Match evita que un cambio se aplique dos veces. Cualquier otro PATCH nunca se reintenta.

El SDK añade una Idempotency-Key aleatoria a cada POST cuya operación la acepte, para que una creación reintentada no genere un segundo objeto. Para que tus propios reintentos también sean seguros, pasa tú la misma clave:

await keenauth.users.createUser({
  userCreateRequest: { email: "ada@example.com" },
  idempotencyKey: "signup-7d1f0c2a-0d7e-4c55",
});

Paginación

Los endpoints de listado devuelven una página: data y listMetadata con los cursores before y after. paginate sigue los cursores por ti:

import { paginate } from "@keenauth/node";

for await (const user of paginate((p) => keenauth.users.listUsers(p), { limit: 100 })) {
  console.log(user.id);
  if (user.email === "ada@example.com") break; // stops without loading the next page
}

Errores

Una respuesta de error se convierte en un ProblemError (RFC 9457) con status, code, title, detail, requestId y los errors de cada campo. Comprueba code, no el mensaje. Los códigos que el SDK no conoce siguen disponibles como cadenas.

import { ProblemError } from "@keenauth/node";

try {
  await keenauth.users.getUser({ userId });
} catch (e) {
  if (e instanceof ProblemError && e.code === "not_found") {
    // …
  }
}

Una respuesta sin documento de problema, como una página HTML de un proxy, se convierte en el ResponseError generado con la response original.

Verificar webhooks

KeenAuth firma los webhooks con el esquema Standard Webhooks v1. Verifica la firma con el cuerpo de la solicitud sin procesar, antes de analizarlo:

import express from "express";
import { Webhook, WebhookVerificationError } from "@keenauth/node/webhooks";

const webhook = new Webhook(process.env.KEENAUTH_WEBHOOK_SECRET!); // idp_whsec_…

const app = express();
app.post("/webhooks/keenauth", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = webhook.verify(req.body, req.headers);
  } catch (e) {
    if (e instanceof WebhookVerificationError) return res.status(400).send(e.kind);
    throw e;
  }
  // handle event.type, then answer quickly
  res.sendStatus(204);
});

Los webhooks pueden llegar más de una vez. Usa webhook-id para ignorar los duplicados.

Eventos y claims tipados

@keenauth/node/events tiene un tipo para cada evento de cliente, generado a partir de los esquemas públicos de eventos. parseEvent comprueba un cuerpo verificado y devuelve KeenAuthEvent | UnknownEvent. Tras isKnownEvent, switch (event.type) reduce el evento a un tipo concreto. verifyEvent en @keenauth/node/webhooks hace ambos pasos:

import { verifyEvent } from "@keenauth/node/webhooks";
import { isKnownEvent } from "@keenauth/node/events";

const event = verifyEvent(webhook, req.body, req.headers); // WebhookVerificationError or EventParseError
if (!isKnownEvent(event)) {
  // A type this SDK version does not know yet. event.type and event.data.object.id are still typed.
  return res.sendStatus(204);
}
switch (event.type) {
  case "user.created":
    console.log(event.data.object.id);
    break;
  case "email_sending.suspended":
    console.log(event.data.object.reason); // "bounce_rate" | "complaint_rate" | (string & {})
    break;
}

@keenauth/node/claims tipa los claims de un token verificado: parseIdTokenClaims (OpenID Connect Core) y parseAccessTokenClaims (RFC 9068). Comprueban los tipos de los claims pero no verifican nada, así que llámalas sobre los claims que devuelve el verificador:

import { parseAccessTokenClaims } from "@keenauth/node/claims";

const at = await verifier.verifyAccessToken(token);
const claims = parseAccessTokenClaims(at.claims);
claims.client_id; // string
claims.org_roles; // string[] | undefined
claims["tenant_tier"]; // unknown: your own claims

Un claim que no encaja lanza ClaimsParseError con su nombre.

Avisos de obsolescencia

Cuando una respuesta incluye una cabecera Deprecation (RFC 9745) o Sunset (RFC 8594), el SDK escribe un aviso por endpoint y valor de cabecera durante la vida del proceso. El aviso indica el método, la ruta con los IDs sustituidos por {id} y los valores de las cabeceras; nunca contiene tokens, cadenas de consulta ni datos de usuario. Nunca lanza una excepción.

Por defecto, el aviso va a process.emitWarning con el tipo KeenAuthDeprecationWarning. Para enviarlo a tu logger:

const keenauth = new KeenAuth({
  baseUrl: "https://api.eu.keenauth.com/v1",
  apiKey: process.env.KEENAUTH_API_KEY!,
  onWarning: (message) => logger.warn(message),
});

Cabecera de telemetría

Cada solicitud incluye Idp-Client: keenauth-ts/<version> (node/<version>). No contiene datos personales y no se puede desactivar; KeenAuth la usa para soporte y para avisarte de versiones del SDK obsoletas.