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.
- En el panel, crea una aplicación de tipo
managementy un registro de cliente de gestión con los permisos que necesite (consulta Crear un cliente de gestión). - Genera un par de claves y registra la clave pública como credencial
public_keyde la aplicación. Por ejemplo, para ES256:openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out management-key.pem. - 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:
- Envía
grant_type=client_credentialsa<issuer>/oauth2/tokenconresourceestablecido al identificador de la API de gestión. El valor por defecto es el origen debaseUrl, por ejemplohttps://api.eu.keenauth.com; establececredentials.audiencepara cambiarlo. - Cada solicitud recibe una nueva aserción de cliente:
issysubson el ID de cliente,audes el issuer,jties aleatorio y la aserción es válida durante 120 segundos. La cabecerakides la huella RFC 7638 de la clave, que es como KeenAuth encuentra la clave cuando la aplicación tiene varias (para la rotación de claves). - El algoritmo sigue a la clave: las claves RSA firman con
RS256(establecealg: "PS256"para RSA-PSS), las claves P-256 conES256y las claves Ed25519 conEdDSA. Las claves RSA deben tener al menos 2048 bits. - El token de acceso se guarda en caché y se renueva 60 segundos antes de caducar. Las llamadas concurrentes comparten una sola renovación. Si la API responde
401, se descarta el token en caché y la siguiente llamada obtiene uno nuevo.
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.
- Si la respuesta tiene una cabecera
Retry-After(segundos o una fecha HTTP), el SDK espera ese tiempo. UnRetry-Afterde más de 60 segundos no se espera: el SDK te devuelve esa respuesta. - En caso contrario, espera 0,5 segundos, luego 1 segundo, cada uno con una variación aleatoria de ±25 %.
timeoutMs(por defecto 30 segundos) limita la llamada completa, incluidos los reintentos. Si una espera superara ese límite, el SDK devuelve la última respuesta.
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
}
- Sin
before, avanza conafterhasta que el cursor seanull. Conbefore, avanza hacia atrás. limites el tamaño de página (1-100) y se envía en cada solicitud; los demás parámetros también se conservan.paginatePageste devuelve páginas completas en lugar de elementos.- Si un servidor devuelve dos veces el mismo cursor, el bucle se detiene con
PaginationError.
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);
});
- El SDK comprueba las cabeceras
webhook-id,webhook-timestampywebhook-signature. La marca de tiempo debe estar dentro de 5 minutos del reloj de tu servidor, en cualquier dirección. - Durante una rotación de secretos, pasa ambos:
new Webhook([newSecret, oldSecret]). La solicitud se acepta cuando alguna firmav1coincide con algún secreto. - Las firmas se comparan en tiempo constante.
e.kindindica qué falló:headers(falta una cabecera),timestamp(no es un número o está fuera de la tolerancia),signature(ninguna firma coincide) osecret(el secreto configurado no es un valoridp_whsec_válido, un error de configuración que lanzanew Webhook). Cada tipo también tiene su propia clase, comoWebhookSignatureError.verifydevuelve el cuerpo JSON analizado.verifyRawdevuelve los bytes verificados.
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;
}
parseEventacepta texto JSON, bytes o un valor ya decodificado. Comprueba el envelope y los miembros que declara el esquema del tipo. Un miembro ausente o de tipo incorrecto lanzaEventParseErrorcon su ruta, comodata.object.reason.- Un tipo desconocido, o un tipo conocido con un
schema_versionque este SDK no modela, es unUnknownEvent. No es un error. - Los miembros que el esquema no declara se mantienen en el objeto.
- Los campos enumerados se tipan como los valores conocidos más
string, así que un valor nuevo del servidor sigue pasando la comprobación de tipos y se analiza.
@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.