
# SDK de gestion pour Node.js

`@keenauth/node` est le SDK TypeScript de l'API de gestion KeenAuth. Il nécessite Node.js 22 ou une version ultérieure et n'a aucune dépendance d'exécution. Il n'est pas encore publié sur npm.

Le package dispose des points d'entrée suivants :

| Import | Utilisation |
|---|---|
| `@keenauth/node` | Le client de l'API de gestion, les jetons de gestion, la pagination et les erreurs |
| `@keenauth/node/webhooks` | La vérification des signatures des webhooks, sans charger le client de l'API |
| `@keenauth/node/bff` | La connexion pour les applications serveur : [Express](/docs/sdk/express) et [Fastify](/docs/sdk/fastify) |
| `@keenauth/node/middleware` | [La protection de votre API](/docs/sdk/protect-api) avec des jetons porteurs |
| `@keenauth/node/testing` | Le [kit de test hors ligne](/docs/sdk/testing) |

## Créer un client

Avec une clé d'API secrète ou restreinte :

```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` est l'adresse de l'API de votre région suivie de `/v1`. Elle doit utiliser `https` ; `http` n'est accepté que pour `localhost`. La clé n'est envoyée que dans l'en-tête `Authorization` et uniquement à cette adresse. Les redirections ne sont pas suivies.

## S'authentifier sans clé d'API

Une application `management` peut appeler l'API avec un jeton de courte durée au lieu d'une clé statique. L'application signe une assertion client avec sa clé privée (`private_key_jwt`, RFC 7523), et le SDK l'échange auprès du point de terminaison de jeton de votre environnement.

1. Dans le tableau de bord, créez une application de type `management` ainsi qu'un enregistrement de client de gestion avec les autorisations dont elle a besoin (voir [Créer un client de gestion](/docs/api/management/management-clients/create)).
2. Générez une paire de clés et enregistrez la clé publique comme identifiant de type `public_key` de l'application. Par exemple, pour ES256 : `openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out management-key.pem`.
3. Conservez la clé privée dans votre coffre de secrets. Transmettez-la au 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", // l'émetteur de votre environnement, à l'identique
    clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
    privateKey: readFileSync("management-key.pem", "utf8"), // PEM, un JWK privé ou un KeyObject
    scope: ["users:read", "users:write"], // facultatif ; par défaut : les scopes par défaut de l'application
  },
});
```

Ce que fait le SDK :

- Il envoie `grant_type=client_credentials` à `<issuer>/oauth2/token` avec `resource` défini sur l'identifiant de l'API de gestion. La valeur par défaut est l'origine de `baseUrl`, par exemple `https://api.eu.keenauth.com` ; définissez `credentials.audience` pour la modifier.
- Chaque requête reçoit une nouvelle assertion client : `iss` et `sub` correspondent à l'identifiant client, `aud` est l'émetteur, `jti` est aléatoire et l'assertion est valable 120 secondes. L'en-tête `kid` est l'empreinte RFC 7638 de la clé, ce qui permet à KeenAuth de retrouver la clé lorsque l'application en possède plusieurs (pour la rotation des clés).
- L'algorithme dépend de la clé : les clés RSA signent avec `RS256` (définissez `alg: "PS256"` pour RSA-PSS), les clés P-256 avec `ES256` et les clés Ed25519 avec `EdDSA`. Les clés RSA doivent comporter au moins 2048 bits.
- Le jeton d'accès est mis en cache et renouvelé 60 secondes avant son expiration. Les appels simultanés partagent un même renouvellement. Si l'API répond `401`, le jeton en cache est supprimé et l'appel suivant en obtient un nouveau.

Une requête de jeton en échec lève une `TokenError` contenant le `code` OAuth (par exemple `invalid_client` ou `invalid_scope`), la `description` et le `status`. Une erreur de configuration, comme une clé publique fournie à la place d'une clé privée, est levée lors de la création du client.

`apiKey` et `credentials` ne peuvent pas être utilisés ensemble.

## Nouvelles tentatives

Le SDK relance une requête au maximum 3 fois au total lorsque l'API répond `429`, `502`, `503` ou `504`, ou lorsque la connexion échoue. Il ne relance que les requêtes `GET`, `PUT` et `DELETE`, ainsi que les requêtes `POST` qui portent un `Idempotency-Key`. Une requête `PATCH` qui porte `If-Match` n'est relancée que sur `429` : le limiteur de débit refuse la requête avant son exécution, et `If-Match` empêche qu'une modification soit appliquée deux fois. Tout autre `PATCH` n'est jamais relancé.

- Si la réponse contient un en-tête `Retry-After` (en secondes ou sous forme de date HTTP), le SDK attend ce délai. Un `Retry-After` supérieur à 60 secondes n'est pas attendu : le SDK vous renvoie cette réponse.
- Sinon, il attend 0,5 seconde, puis 1 seconde, chacune avec une gigue aléatoire de ±25 %.
- `timeoutMs` (30 secondes par défaut) limite la durée totale de l'appel, nouvelles tentatives comprises. Si une attente devait dépasser cette limite, le SDK renvoie la dernière réponse à la place.

Le SDK ajoute un `Idempotency-Key` aléatoire à chaque `POST` dont l'opération accepte cet en-tête, afin qu'une création relancée ne produise pas un second objet. Pour rendre vos propres nouvelles tentatives sûres également, transmettez vous-même la même clé :

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

## Pagination

Les points de terminaison de liste renvoient une seule page : `data` et `listMetadata` avec les curseurs `before` et `after`. `paginate` parcourt les curseurs à votre place :

```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; // s'arrête sans charger la page suivante
}
```

- Sans `before`, il avance avec `after` jusqu'à ce que le curseur vaille `null`. Avec `before`, il recule.
- `limit` correspond à la taille de page (1 à 100) et est envoyé à chaque requête ; les autres paramètres sont conservés.
- `paginatePages` renvoie des pages entières au lieu d'éléments.
- Si le serveur renvoie deux fois le même curseur, la boucle s'arrête avec une `PaginationError`.

## Erreurs

Une réponse d'erreur devient une `ProblemError` (RFC 9457) avec `status`, `code`, `title`, `detail`, `requestId` et les `errors` par champ. Vérifiez `code`, et non le message. Les codes que le SDK ne connaît pas restent disponibles sous forme de chaînes.

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

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

Une réponse sans document de problème, comme une page HTML provenant d'un proxy, devient la `ResponseError` générée, avec la `response` brute.

## Vérifier les webhooks

KeenAuth signe les webhooks selon le schéma Standard Webhooks v1. Vérifiez la signature à partir du corps brut de la requête, avant de l'analyser :

```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;
  }
  // traiter event.type, puis répondre rapidement
  res.sendStatus(204);
});
```

- Le SDK vérifie les en-têtes `webhook-id`, `webhook-timestamp` et `webhook-signature`. L'horodatage doit se situer à 5 minutes près de l'horloge de votre serveur, dans un sens comme dans l'autre.
- Pendant une rotation de secret, transmettez les deux secrets : `new Webhook([newSecret, oldSecret])`. Une requête est acceptée dès qu'une signature `v1` correspond à l'un des secrets.
- Les signatures sont comparées en temps constant.
- `e.kind` indique ce qui a échoué : `headers` (un en-tête est absent), `timestamp` (non numérique ou hors de la tolérance), `signature` (aucune signature ne correspond) ou `secret` (le secret configuré n'est pas une valeur `idp_whsec_` valide, erreur de configuration levée par `new Webhook`). Chaque type possède aussi sa propre classe, comme `WebhookSignatureError`.
- `verify` renvoie le corps JSON analysé. `verifyRaw` renvoie les octets vérifiés.

Les webhooks peuvent arriver plusieurs fois. Utilisez `webhook-id` pour ignorer les doublons.

## Événements typés et revendications

`@keenauth/node/events` dispose d'un type pour chaque événement client, généré à partir des schémas publics des événements. `parseEvent` vérifie un corps vérifié et renvoie `KeenAuthEvent | UnknownEvent`. Après `isKnownEvent`, `switch (event.type)` restreint l'événement à un type précis. `verifyEvent` dans `@keenauth/node/webhooks` effectue ces deux étapes :

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

const event = verifyEvent(webhook, req.body, req.headers); // WebhookVerificationError ou EventParseError
if (!isKnownEvent(event)) {
  // Un type que cette version du SDK ne connaît pas encore. event.type et event.data.object.id restent typés.
  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` accepte du texte JSON, des octets ou une valeur déjà décodée. Il vérifie l'enveloppe et les membres que le schéma du type déclare. Un membre absent ou d'un type incorrect lève une `EventParseError` avec son chemin, par exemple `data.object.reason`.
- Un type inconnu, ou un type connu avec un `schema_version` que ce SDK ne modélise pas, donne un `UnknownEvent`. Ce n'est pas une erreur.
- Les membres non déclarés dans le schéma restent sur l'objet.
- Les champs énumérés sont typés avec les valeurs connues plus `string`, si bien qu'une nouvelle valeur envoyée par le serveur reste compatible avec le typage et l'analyse.

`@keenauth/node/claims` type les revendications d'un jeton vérifié : `parseIdTokenClaims` (OpenID Connect Core) et `parseAccessTokenClaims` (RFC 9068). Elles contrôlent les types des revendications mais ne vérifient rien ; appelez-les donc sur les `claims` renvoyés par le vérificateur :

```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 : vos propres revendications
```

Une revendication non conforme lève une `ClaimsParseError` avec son nom.

## Avertissements de dépréciation

Lorsqu'une réponse contient un en-tête `Deprecation` (RFC 9745) ou `Sunset` (RFC 8594), le SDK écrit un avertissement par point de terminaison et par valeur d'en-tête pendant toute la durée du processus. L'avertissement indique la méthode, le chemin dont les identifiants sont remplacés par `{id}`, ainsi que les valeurs des en-têtes ; il ne contient jamais de jetons, de chaînes de requête ni de données utilisateur. Il ne lève jamais d'exception.

Par défaut, l'avertissement est transmis à `process.emitWarning` avec le type `KeenAuthDeprecationWarning`. Pour l'envoyer à votre journal à la place :

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

## En-tête de télémétrie

Chaque requête contient `Idp-Client: keenauth-ts/<version> (node/<version>)`. Cet en-tête ne contient aucune donnée personnelle et ne peut pas être désactivé ; KeenAuth l'utilise pour l'assistance et pour vous informer des versions du SDK devenues obsolètes.
