
# SDK di gestione per Node.js

`@keenauth/node` è l'SDK TypeScript per la management API di KeenAuth. Richiede Node.js 22 o successivo e non ha dipendenze di runtime. Non è ancora pubblicato su npm.

Il pacchetto ha questi entry point:

| Import | Usalo per |
|---|---|
| `@keenauth/node` | Il client della management API, i token di gestione, la paginazione e gli errori |
| `@keenauth/node/webhooks` | Verificare le firme dei webhook, senza caricare il client API |
| `@keenauth/node/bff` | Accesso per le app server: [Express](/docs/sdk/express) e [Fastify](/docs/sdk/fastify) |
| `@keenauth/node/middleware` | [Proteggere la tua API](/docs/sdk/protect-api) con bearer token |
| `@keenauth/node/testing` | Il [kit di test offline](/docs/sdk/testing) |

## Crea un client

Con una chiave API secret o restricted:

```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` è l'indirizzo API della tua regione con `/v1`. Deve usare `https`; `http` semplice è accettato solo per `localhost`. La chiave viene inviata solo nell'header `Authorization` e solo a quell'indirizzo. I redirect non vengono seguiti.

## Autenticati senza una chiave API

Un'applicazione di tipo `management` può chiamare l'API con un token a breve durata invece di una chiave statica. L'applicazione firma un client assertion con la propria chiave privata (`private_key_jwt`, RFC 7523), e l'SDK lo scambia all'endpoint dei token del tuo ambiente.

1. Nella dashboard, crea un'applicazione di tipo `management` e un record di client di gestione con le autorizzazioni di cui ha bisogno (vedi [Crea un client di gestione](/docs/api/management/management-clients/create)).
2. Genera una coppia di chiavi e registra la chiave pubblica come credenziale `public_key` dell'applicazione. Per esempio, per ES256: `openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out management-key.pem`.
3. Conserva la chiave privata nel tuo secret store. Passala all'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
  },
});
```

Cosa fa l'SDK:

- Invia `grant_type=client_credentials` a `<issuer>/oauth2/token` con `resource` impostato all'identificatore della management API. Il valore predefinito è l'origine di `baseUrl`, per esempio `https://api.eu.keenauth.com`; imposta `credentials.audience` per cambiarlo.
- Ogni richiesta riceve un nuovo client assertion: `iss` e `sub` sono il client ID, `aud` è l'issuer, `jti` è casuale e l'assertion è valida per 120 secondi. L'header `kid` è il thumbprint RFC 7638 della chiave, con cui KeenAuth trova la chiave quando l'applicazione ne ha più d'una (per la rotazione delle chiavi).
- L'algoritmo segue la chiave: le chiavi RSA firmano con `RS256` (imposta `alg: "PS256"` per RSA-PSS), le chiavi P-256 con `ES256` e le chiavi Ed25519 con `EdDSA`. Le chiavi RSA devono avere almeno 2048 bit.
- Il token di accesso viene messo in cache e rinnovato 60 secondi prima della scadenza. Le chiamate concorrenti condividono un solo rinnovo. Se l'API risponde `401`, il token in cache viene scartato e la chiamata successiva ne ottiene uno nuovo.

Una richiesta di token non riuscita genera `TokenError` con il `code` OAuth (per esempio `invalid_client` o `invalid_scope`), `description` e `status`. Un errore di configurazione, come una chiave pubblica al posto di una privata, viene generato al momento della creazione del client.

`apiKey` e `credentials` non possono essere usati insieme.

## Retry

L'SDK ritenta una richiesta al massimo 3 volte in totale quando l'API risponde `429`, `502`, `503` o `504`, oppure quando la connessione fallisce. Ritenta solo richieste `GET`, `PUT` e `DELETE` e richieste `POST` che includono un `Idempotency-Key`. Una `PATCH` che include `If-Match` viene ritentata solo su `429`: il rate limiter rifiuta la richiesta prima che venga eseguita, e `If-Match` evita che una modifica venga applicata due volte. Qualsiasi altra `PATCH` non viene mai ritentata.

- Se la risposta ha un header `Retry-After` (secondi o data HTTP), l'SDK attende quel tempo. Un `Retry-After` superiore a 60 secondi non viene atteso: l'SDK restituisce a te quella risposta.
- Altrimenti attende 0,5 secondi, poi 1 secondo, ciascuno con jitter casuale di ±25%.
- `timeoutMs` (predefinito 30 secondi) limita l'intera chiamata, retry inclusi. Se un'attesa andrebbe oltre, l'SDK restituisce invece l'ultima risposta.

L'SDK aggiunge un `Idempotency-Key` casuale a ogni `POST` la cui operazione lo accetta, così una creazione ritentata non crea un secondo oggetto. Per rendere sicuri anche i tuoi retry, passa tu la stessa chiave:

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

## Paginazione

Gli endpoint di lista restituiscono una pagina: `data` e `listMetadata` con i cursori `before` e `after`. `paginate` segue i cursori al posto tuo:

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

- Senza `before`, avanza con `after` finché il cursore è `null`. Con `before`, procede all'indietro.
- `limit` è la dimensione della pagina (1-100) ed è inviato in ogni richiesta; anche gli altri parametri vengono mantenuti.
- `paginatePages` restituisce intere pagine invece dei singoli elementi.
- Se il server restituisce due volte lo stesso cursore, il ciclo si interrompe con `PaginationError`.

## Errori

Una risposta di errore diventa un `ProblemError` (RFC 9457) con `status`, `code`, `title`, `detail`, `requestId` e gli `errors` dei singoli campi. Controlla `code`, non il messaggio. I codici che l'SDK non conosce restano disponibili come stringhe.

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

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

Una risposta senza documento di problema, come una pagina HTML di un proxy, diventa il `ResponseError` generato con la `response` grezza.

## Verifica i webhook

KeenAuth firma i webhook con lo schema Standard Webhooks v1. Verifica la firma con il corpo grezzo della richiesta, prima di analizzarlo:

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

- L'SDK controlla gli header `webhook-id`, `webhook-timestamp` e `webhook-signature`. Il timestamp deve trovarsi entro 5 minuti dall'orologio del tuo server, in entrambe le direzioni.
- Durante la rotazione di un secret, passa entrambi: `new Webhook([newSecret, oldSecret])`. Una richiesta viene accettata quando una qualsiasi firma `v1` corrisponde a un qualsiasi secret.
- Le firme vengono confrontate in tempo costante.
- `e.kind` indica cosa non ha funzionato: `headers` (manca un header), `timestamp` (non è un numero o è fuori dalla tolleranza), `signature` (nessuna firma corrisponde) o `secret` (il secret configurato non è un valore `idp_whsec_` valido, errore di configurazione generato da `new Webhook`). Ogni tipo ha anche una propria classe, come `WebhookSignatureError`.
- `verify` restituisce il corpo JSON analizzato. `verifyRaw` restituisce i byte verificati.

I webhook possono arrivare più di una volta. Usa `webhook-id` per ignorare i duplicati.

## Eventi tipizzati e claim

`@keenauth/node/events` ha un tipo per ogni evento cliente, generato dagli schemi pubblici degli eventi. `parseEvent` controlla un corpo verificato e restituisce `KeenAuthEvent | UnknownEvent`. Dopo `isKnownEvent`, `switch (event.type)` restringe l'evento a un tipo specifico. `verifyEvent` in `@keenauth/node/webhooks` esegue entrambi i passaggi:

```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` accetta testo JSON, byte o un valore già decodificato. Controlla l'envelope e i membri dichiarati dallo schema del tipo. Un membro mancante o di tipo errato genera `EventParseError` con il suo percorso, come `data.object.reason`.
- Un tipo sconosciuto, o un tipo noto con uno `schema_version` che questo SDK non modella, è un `UnknownEvent`. Non è un errore.
- I membri non dichiarati dallo schema restano sull'oggetto.
- I campi enumerati sono tipizzati come i valori noti più `string`, quindi un nuovo valore inviato dal server passa comunque il controllo di tipo e viene analizzato.

`@keenauth/node/claims` tipizza i claim di un token verificato: `parseIdTokenClaims` (OpenID Connect Core) e `parseAccessTokenClaims` (RFC 9068). Controllano i tipi dei claim ma non verificano nulla, quindi chiamali sui `claims` restituiti dal verificatore:

```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 che non corrisponde genera `ClaimsParseError` con il suo nome.

## Avvisi di deprecazione

Quando una risposta contiene un header `Deprecation` (RFC 9745) o `Sunset` (RFC 8594), l'SDK scrive un avviso per ogni endpoint e valore dell'header per tutta la vita del processo. L'avviso indica il metodo, il percorso con gli ID sostituiti da `{id}` e i valori dell'header; non contiene mai token, query string o dati utente. Non genera mai eccezioni.

Per impostazione predefinita l'avviso va a `process.emitWarning` con il tipo `KeenAuthDeprecationWarning`. Per inviarlo al tuo logger:

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

## Header di telemetria

Ogni richiesta contiene `Idp-Client: keenauth-ts/<version> (node/<version>)`. Non contiene dati personali e non può essere disattivato; KeenAuth lo usa per il supporto e per avvisarti delle versioni dell'SDK deprecate.
