
# 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](/docs/sdk/express) y [Fastify](/docs/sdk/fastify) |
| `@keenauth/node/middleware` | [Proteger tu API](/docs/sdk/protect-api) con tokens de portador |
| `@keenauth/node/testing` | El [kit de pruebas sin conexión](/docs/sdk/testing) |

## Crear un cliente

Con una clave de API secreta o restringida:

```ts
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](/docs/api/management/management-clients/create)).
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:

```ts
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_credentials` a `<issuer>/oauth2/token` con `resource` establecido al identificador de la API de gestión. El valor por defecto es el origen de `baseUrl`, por ejemplo `https://api.eu.keenauth.com`; establece `credentials.audience` para cambiarlo.
- Cada solicitud recibe una nueva aserción de cliente: `iss` y `sub` son el ID de cliente, `aud` es el issuer, `jti` es aleatorio y la aserción es válida durante 120 segundos. La cabecera `kid` es 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` (establece `alg: "PS256"` para RSA-PSS), las claves P-256 con `ES256` y las claves Ed25519 con `EdDSA`. 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. Un `Retry-After` de 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:

```ts
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:

```ts
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 con `after` hasta que el cursor sea `null`. Con `before`, avanza hacia atrás.
- `limit` es el tamaño de página (1-100) y se envía en cada solicitud; los demás parámetros también se conservan.
- `paginatePages` te 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.

```ts
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:

```ts
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-timestamp` y `webhook-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 firma `v1` coincide con algún secreto.
- Las firmas se comparan en tiempo constante.
- `e.kind` indica qué falló: `headers` (falta una cabecera), `timestamp` (no es un número o está fuera de la tolerancia), `signature` (ninguna firma coincide) o `secret` (el secreto configurado no es un valor `idp_whsec_` válido, un error de configuración que lanza `new Webhook`). Cada tipo también tiene su propia clase, como `WebhookSignatureError`.
- `verify` devuelve el cuerpo JSON analizado. `verifyRaw` devuelve 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:

```ts
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;
}
```

- `parseEvent` acepta 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 lanza `EventParseError` con su ruta, como `data.object.reason`.
- Un tipo desconocido, o un tipo conocido con un `schema_version` que este SDK no modela, es un `UnknownEvent`. 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:

```ts
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:

```ts
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.
