
# Node.js-Verwaltungs-SDK

`@keenauth/node` ist das TypeScript-SDK für die Verwaltungs-API von KeenAuth. Es benötigt Node.js 22 oder neuer und hat keine Laufzeitabhängigkeiten. Es ist noch nicht in npm veröffentlicht.

Das Paket bietet folgende Einstiegspunkte:

| Import | Verwendung für |
|---|---|
| `@keenauth/node` | Den Client der Verwaltungs-API, Verwaltungs-Tokens, Paginierung und Fehler |
| `@keenauth/node/webhooks` | Überprüfung von Webhook-Signaturen, ohne den API-Client zu laden |
| `@keenauth/node/bff` | Anmeldung für Server-Anwendungen: [Express](/docs/sdk/express) und [Fastify](/docs/sdk/fastify) |
| `@keenauth/node/middleware` | [Schutz Ihrer API](/docs/sdk/protect-api) mit Bearer-Tokens |
| `@keenauth/node/testing` | Das [Offline-Testkit](/docs/sdk/testing) |

## Client erstellen

Mit einem geheimen oder eingeschränkten API-Schlüssel:

```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` ist die API-Adresse Ihrer Region mit `/v1`. Sie muss `https` verwenden; einfaches `http` ist nur für `localhost` zulässig. Der Schlüssel wird ausschließlich im `Authorization`-Header und nur an diese Adresse gesendet. Weiterleitungen werden nicht verfolgt.

## Ohne API-Schlüssel authentifizieren

Eine `management`-Anwendung kann die API mit einem kurzlebigen Token statt mit einem statischen Schlüssel aufrufen. Die Anwendung signiert eine Client-Assertion mit ihrem privaten Schlüssel (`private_key_jwt`, RFC 7523), und das SDK tauscht sie am Token-Endpunkt Ihrer Umgebung gegen ein Token ein.

1. Erstellen Sie im Dashboard eine Anwendung vom Typ `management` und einen Verwaltungsclient-Eintrag mit den benötigten Berechtigungen (siehe [Verwaltungsclient erstellen](/docs/api/management/management-clients/create)).
2. Erzeugen Sie ein Schlüsselpaar und registrieren Sie den öffentlichen Schlüssel als `public_key`-Credential der Anwendung. Für ES256 zum Beispiel: `openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out management-key.pem`.
3. Bewahren Sie den privaten Schlüssel in Ihrem Secret-Store auf. Übergeben Sie ihn an das 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
  },
});
```

Das SDK arbeitet dabei wie folgt:

- Es sendet `grant_type=client_credentials` an `<issuer>/oauth2/token`, wobei `resource` auf den Bezeichner der Verwaltungs-API gesetzt ist. Standard ist der Ursprung von `baseUrl`, zum Beispiel `https://api.eu.keenauth.com`; setzen Sie `credentials.audience`, um ihn zu ändern.
- Jede Anfrage erhält eine neue Client-Assertion: `iss` und `sub` sind die Client-ID, `aud` ist der Issuer, `jti` ist zufällig und die Assertion ist 120 Sekunden gültig. Der `kid`-Header ist der RFC-7638-Thumbprint des Schlüssels; damit findet KeenAuth den Schlüssel, wenn die Anwendung mehrere besitzt (etwa bei einer Schlüsselrotation).
- Der Algorithmus richtet sich nach dem Schlüssel: RSA-Schlüssel signieren mit `RS256` (setzen Sie `alg: "PS256"` für RSA-PSS), P-256-Schlüssel mit `ES256` und Ed25519-Schlüssel mit `EdDSA`. RSA-Schlüssel müssen mindestens 2048 Bit haben.
- Das Zugriffstoken wird zwischengespeichert und 60 Sekunden vor Ablauf erneuert. Gleichzeitige Aufrufe teilen sich eine Erneuerung. Antwortet die API mit `401`, wird das zwischengespeicherte Token verworfen und der nächste Aufruf erhält ein neues.

Schlägt eine Token-Anfrage fehl, wirft das SDK `TokenError` mit dem OAuth-`code` (zum Beispiel `invalid_client` oder `invalid_scope`), `description` und `status`. Ein Konfigurationsfehler, etwa ein öffentlicher statt eines privaten Schlüssels, wird beim Erstellen des Clients ausgelöst.

`apiKey` und `credentials` können nicht gleichzeitig verwendet werden.

## Wiederholungen

Das SDK wiederholt eine Anfrage insgesamt höchstens 3-mal, wenn die API mit `429`, `502`, `503` oder `504` antwortet oder wenn die Verbindung fehlschlägt. Wiederholt werden nur `GET`-, `PUT`- und `DELETE`-Anfragen sowie `POST`-Anfragen mit einem `Idempotency-Key`. Ein `PATCH` mit `If-Match` wird nur bei `429` wiederholt: Der Rate-Limiter lehnt die Anfrage ab, bevor sie ausgeführt wird, und `If-Match` verhindert, dass eine Änderung doppelt angewendet wird. Jeder andere `PATCH` wird nie wiederholt.

- Enthält die Antwort einen `Retry-After`-Header (Sekunden oder HTTP-Datum), wartet das SDK so lange. Ein `Retry-After` von mehr als 60 Sekunden wird nicht abgewartet: Das SDK gibt diese Antwort an Sie zurück.
- Andernfalls wartet es 0,5 Sekunden, dann 1 Sekunde, jeweils mit einer zufälligen Abweichung von ±25 %.
- `timeoutMs` (Standard 30 Sekunden) begrenzt den gesamten Aufruf einschließlich Wiederholungen. Würde eine Wartezeit darüber hinausgehen, gibt das SDK stattdessen die letzte Antwort zurück.

Das SDK fügt jedem `POST` mit einer Operation, die einen `Idempotency-Key` akzeptiert, einen zufälligen Schlüssel hinzu, damit ein wiederholtes Anlegen kein zweites Objekt erzeugt. Damit auch Ihre eigenen Wiederholungen sicher sind, übergeben Sie denselben Schlüssel selbst:

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

## Paginierung

Listen-Endpunkte liefern eine Seite zurück: `data` und `listMetadata` mit den Cursorn `before` und `after`. `paginate` folgt den Cursorn für Sie:

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

- Ohne `before` läuft es mit `after` vorwärts, bis der Cursor `null` ist. Mit `before` läuft es rückwärts.
- `limit` ist die Seitengröße (1–100) und wird bei jeder Anfrage gesendet; die übrigen Parameter bleiben ebenfalls erhalten.
- `paginatePages` liefert ganze Seiten statt einzelner Elemente.
- Liefert der Server denselben Cursor zweimal, beendet `PaginationError` die Schleife.

## Fehler

Eine Fehlerantwort wird zu einem `ProblemError` (RFC 9457) mit `status`, `code`, `title`, `detail`, `requestId` und den Feldfehlern `errors`. Prüfen Sie `code`, nicht die Meldung. Codes, die das SDK nicht kennt, bleiben als Zeichenketten verfügbar.

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

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

Eine Antwort ohne Problem-Dokument, etwa eine HTML-Seite eines Proxys, wird zum generierten `ResponseError` mit der rohen `response`.

## Webhooks überprüfen

KeenAuth signiert Webhooks nach dem Standard Webhooks v1-Verfahren. Überprüfen Sie die Signatur mit dem rohen Anfragekörper, bevor Sie ihn parsen:

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

- Das SDK prüft die Header `webhook-id`, `webhook-timestamp` und `webhook-signature`. Der Zeitstempel muss innerhalb von 5 Minuten zur Uhr Ihres Servers liegen, in beide Richtungen.
- Während einer Geheimnisrotation übergeben Sie beide Geheimnisse: `new Webhook([newSecret, oldSecret])`. Eine Anfrage wird akzeptiert, wenn eine `v1`-Signatur zu einem beliebigen Geheimnis passt.
- Signaturen werden in konstanter Zeit verglichen.
- `e.kind` zeigt an, was fehlgeschlagen ist: `headers` (ein Header fehlt), `timestamp` (keine Zahl oder außerhalb der Toleranz), `signature` (keine Signatur passt) oder `secret` (das konfigurierte Geheimnis ist kein gültiger `idp_whsec_`-Wert; ein Konfigurationsfehler, der von `new Webhook` ausgelöst wird). Jede Art hat außerdem eine eigene Klasse, etwa `WebhookSignatureError`.
- `verify` gibt den geparsten JSON-Körper zurück. `verifyRaw` gibt die verifizierten Bytes zurück.

Webhooks können mehr als einmal eintreffen. Verwenden Sie `webhook-id`, um Duplikate zu ignorieren.

## Typisierte Ereignisse und Claims

`@keenauth/node/events` enthält einen Typ für jedes Kundenereignis, generiert aus den öffentlichen Ereignisschemata. `parseEvent` prüft einen verifizierten Körper und gibt `KeenAuthEvent | UnknownEvent` zurück. Nach `isKnownEvent` grenzt `switch (event.type)` das Ereignis auf einen Typ ein. `verifyEvent` in `@keenauth/node/webhooks` führt beide Schritte aus:

```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` akzeptiert JSON-Text, Bytes oder einen bereits dekodierten Wert. Es prüft die Hülle und die Felder, die das Schema des Typs deklariert. Ein fehlendes oder falsch typisiertes Feld wirft `EventParseError` mit seinem Pfad, etwa `data.object.reason`.
- Ein unbekannter Typ oder ein bekannter Typ mit einer `schema_version`, die dieses SDK nicht modelliert, ist ein `UnknownEvent`. Das ist kein Fehler.
- Felder, die das Schema nicht deklariert, bleiben am Objekt erhalten.
- Aufzählungsfelder sind als die bekannten Werte plus `string` typisiert, sodass ein neuer Wert vom Server weiterhin typgeprüft und geparst wird.

`@keenauth/node/claims` typisiert die Claims eines verifizierten Tokens: `parseIdTokenClaims` (OpenID Connect Core) und `parseAccessTokenClaims` (RFC 9068). Sie prüfen Claim-Typen, verifizieren aber nichts; rufen Sie sie daher mit den `claims` auf, die der Verifier zurückgibt:

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

Ein nicht passender Claim wirft `ClaimsParseError` mit seinem Namen.

## Warnungen zu veralteten Funktionen

Trägt eine Antwort einen `Deprecation`-Header (RFC 9745) oder `Sunset`-Header (RFC 8594), schreibt das SDK eine Warnung pro Endpunkt und Header-Wert für die Lebensdauer des Prozesses. Die Warnung nennt die Methode, den Pfad mit durch `{id}` ersetzten IDs und die Header-Werte; sie enthält nie Tokens, Query-Strings oder Nutzerdaten. Sie löst nie einen Fehler aus.

Standardmäßig geht die Warnung an `process.emitWarning` mit dem Typ `KeenAuthDeprecationWarning`. Um sie stattdessen an Ihren Logger zu senden:

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

## Telemetrie-Header

Jede Anfrage trägt `Idp-Client: keenauth-ts/<version> (node/<version>)`. Der Header enthält keine personenbezogenen Daten und kann nicht deaktiviert werden; KeenAuth verwendet ihn für den Support und um Sie über veraltete SDK-Versionen zu informieren.
