
# SDK server Go

Il modulo `github.com/keenauth/keenauth-go` è il client Go dell'API di gestione di KeenAuth. I metodi tipizzati per ogni risorsa `/v1` sono generati dal documento OpenAPI (pacchetto `management`, incluso in `keenauth.Client`). Il core scritto a mano aggiunge autenticazione, retry, chiavi di idempotenza, errori tipizzati e avvisi di deprecazione. Il modulo usa solo la libreria standard. Non è ancora pubblicato.

Per accedere in un'app `net/http`, vedi [Sessioni Go net/http](/docs/sdk/go-nethttp). Per proteggere la tua API, vedi [Proteggere la tua API](/docs/sdk/protect-api).

## Crea un client con una API key

```go
import (
	"context"
	"fmt"
	"os"

	keenauth "github.com/keenauth/keenauth-go"
)

c, err := keenauth.NewClient("https://api.eu.keenauth.com/v1", os.Getenv("KEENAUTH_API_KEY"))
if err != nil {
	panic(err)
}
user, _, err := c.UsersAPI.GetUser(context.Background(), "usr_01j9zzzzzzzzzzzzzzzzzzzz").Execute()
if err != nil {
	panic(err)
}
fmt.Println(user.GetEmail())
```

L'URL di base è l'indirizzo API della tua regione seguito da `/v1`. La chiave (`idp_sk_…` o `idp_rk_…`) viene inviata solo nell'header `Authorization` e solo a quell'indirizzo; un redirect verso un altro host o verso `http` semplice non la trasporta mai. Il protocollo `http` semplice è accettato solo per `localhost`.

Opzioni:

- `keenauth.WithHTTPClient(h)`: il tuo `http.Client` (il suo transport viene avvolto, non modificato).
- `keenauth.WithTimeout(d)`: tempo massimo totale di una chiamata, retry inclusi (predefinito 30 secondi).
- `keenauth.WithClientName("my-tool", "1.4.0")`: aggiunge il tuo strumento a `User-Agent` e `Idp-Client`.
- `keenauth.WithLogger(l)`: il `*slog.Logger` per gli avvisi dell'SDK (predefinito `slog.Default()`).

## Autentica con private_key_jwt

Invece di una chiave statica, un'applicazione di gestione può accedere con una propria coppia di chiavi. L'SDK ottiene un token di gestione dall'endpoint token del tuo ambiente, lo tiene in memoria e lo sostituisce 60 secondi prima della scadenza. Le chiamate concorrenti condividono una sola richiesta di token.

1. Nella dashboard, crea un'applicazione di gestione e registra la sua chiave pubblica (come URL JWKS o direttamente; fino a 3 chiavi attive).
2. Conserva la chiave privata sul tuo server, per esempio in un file segreto o in un KMS.

```go
import (
	"os"

	keenauth "github.com/keenauth/keenauth-go"
	"github.com/keenauth/keenauth-go/assertion"
)

pemBytes, err := os.ReadFile("/run/secrets/keenauth-mgmt-key.pem")
if err != nil {
	panic(err)
}
key, err := assertion.ParsePrivateKeyPEM(pemBytes)
if err != nil {
	panic(err)
}
c, err := keenauth.NewClientWithPrivateKeyJWT("https://api.eu.keenauth.com/v1", keenauth.PrivateKeyJWT{
	Issuer:   "https://acme.eu.keenauth.app", // your environment's issuer, exactly
	ClientID: "app_01j9zzzzzzzzzzzzzzzzzzzz",
	Key:      key,
	KeyID:    "mgmt-key-2026-11", // the kid you registered; optional with a single key
	Scope:    "users:read users:write", // optional; default: the grant's default scopes
})
```

Cosa invia l'SDK:

- `POST <Issuer>/oauth2/token` con `grant_type=client_credentials`, `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`, un nuovo `client_assertion` e `resource` impostato sull'indirizzo API (`https://api.eu.keenauth.com`; cambialo con `Audience`).
- L'assertion è un JWT con `iss` e `sub` impostati al tuo client ID, `aud` impostato all'issuer, un `jti` casuale e una durata di 2 minuti. È firmato con ES256 (chiave P-256), EdDSA (Ed25519), RS256 o PS256 (RSA, 2048 bit o più; imposta `Algorithm: "PS256"` per PSS).

`Key` accetta qualsiasi `crypto.Signer`, quindi una chiave conservata in un KMS o HSM funziona senza essere esportata. Il token di gestione dura 10 minuti. Se l'API risponde `401`, l'SDK scarta il token in cache e la chiamata successiva ne ottiene uno nuovo.

Una richiesta di token rifiutata restituisce un `*keenauth.TokenError` con lo stato HTTP e il codice `error` OAuth, per esempio `invalid_client` per una chiave sconosciuta o un `KeyID` errato. Non contiene mai l'assertion né un token:

```go
var te *keenauth.TokenError
if errors.As(err, &te) {
	log.Printf("token request refused: %d %s", te.Status, te.Code)
}
```

## Retry e chiavi di idempotenza

L'SDK ritenta una chiamata al massimo 3 volte in totale, e solo quando sono vere entrambe le condizioni:

- la risposta è `429`, `502`, `503` o `504`, oppure la connessione è fallita (un errore di rete, non di certificato);
- il metodo è `GET`, `PUT` o `DELETE`, oppure un `POST` con un `Idempotency-Key`.

Ogni operazione `POST` che accetta un `Idempotency-Key` (tutte tranne le chiamate `users:search` e `…:preview`, prive di effetti collaterali, e `users/{id}:unlock`) riceve una nuova chiave UUIDv4, e ogni retry di quella chiamata invia la stessa chiave e lo stesso corpo, quindi l'operazione viene eseguita al massimo una volta. Per rendere una chiamata idempotente anche attraverso i riavvii del tuo sistema, passa una chiave tua (da 16 a 64 caratteri di `A-Z a-z 0-9 _ -`), per esempio `.IdempotencyKey(orderKey)`; l'SDK non la sostituisce mai. Un `PATCH` che include `If-Match` (ogni aggiornamento con versione) viene ritentato 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 altro `PATCH` non viene mai ritentato.

Tra un tentativo e l'altro l'SDK attende quanto indicato da `Retry-After` (secondi o data HTTP). Senza, attende 0,5 secondi, poi 1 secondo, ciascuno con un jitter di ±25 %. Se `Retry-After` chiede più di 60 secondi, o l'attesa supererebbe la scadenza del tuo context, l'SDK restituisce la risposta invece di attendere. Annullare il context interrompe l'attesa.

## Scorri una lista

I metodi di lista restituiscono una sola pagina: `data` e `list_metadata` con i cursori `before` e `after`. `keenauth.All` segue `after` e restituisce ogni elemento. Recupera la pagina successiva solo quando la raggiungi, quindi uscire dal ciclo interrompe le richieste successive. Imposta dimensione della pagina e filtri sulla richiesta come al solito:

```go
for u, err := range keenauth.All(c.UsersAPI.ListUsers(ctx).Limit(100).Status("active")) {
	if err != nil {
		return err // the iteration ends after an error
	}
	fmt.Println(u.GetId(), u.GetEmail())
}
```

`keenauth.AllBefore` segue invece `before`, per esempio verso le voci più recenti di `ListLogs`. Ogni metodo di lista generato funziona con entrambi, eccetto `ListEvents` dell'API Events, che pagina con `next_cursor` e `has_more`. Un ciclo range-over-func richiede Go 1.23 o successivo.

## Verifica i webhook

Il pacchetto `github.com/keenauth/keenauth-go/webhook` verifica la firma di una consegna webhook (Standard Webhooks). Passagli il segreto di firma dell'endpoint (`idp_whsec_…`) e passa il corpo grezzo esattamente come ricevuto:

```go
import (
	"io"
	"net/http"
	"os"

	"github.com/keenauth/keenauth-go/webhook"
)

verifier, err := webhook.NewVerifier([]string{os.Getenv("KEENAUTH_WEBHOOK_SECRET")})
if err != nil {
	panic(err) // webhook.ErrSecret: the secret is not well formed
}

http.HandleFunc("POST /webhooks/keenauth", func(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
	if err != nil {
		http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
		return
	}
	if err := verifier.Verify(r.Header, body); err != nil {
		http.Error(w, "invalid webhook", http.StatusBadRequest)
		return
	}
	// Handle the event. Store webhook-id and skip a delivery you have already processed.
	w.WriteHeader(http.StatusNoContent)
})
```

`Verify` accetta una consegna quando `webhook-timestamp` è entro 5 minuti da adesso (in entrambe le direzioni) e una delle firme `v1` in `webhook-signature` corrisponde. Le firme vengono confrontate in tempo costante, e le altre versioni di firma vengono ignorate. L'errore indica perché una consegna è stata rifiutata; ciascuno corrisponde a uno di `webhook.ErrHeaders` (manca un header), `webhook.ErrTimestamp`, `webhook.ErrSignature` o `webhook.ErrSecret`, verificabili con `errors.Is`.

Quando ruoti il segreto, passa entrambi, nuovo e vecchio, finché quello vecchio non viene più usato: `webhook.NewVerifier([]string{newSecret, oldSecret})`. Durante la rotazione KeenAuth può inviare anche due firme in un header; basta che una sia valida.

## Eventi tipizzati e claim

Il pacchetto `github.com/keenauth/keenauth-go/events` contiene una struct per ogni tipo di evento cliente, generata dagli schemi pubblici degli eventi. `webhook.Verifier.VerifyEvent` verifica una consegna e la analizza; `events.Parse` analizza un corpo che hai già verificato. Usa uno switch sui tipi per ottenere i campi di un tipo:

```go
ev, err := verifier.VerifyEvent(r.Header, body)
if err != nil {
	http.Error(w, "invalid webhook", http.StatusBadRequest) // webhook.Err… or events.ErrParse
	return
}
switch e := ev.(type) {
case *events.UserCreated:
	log.Printf("user %s created", e.Data.Object.ID)
case *events.EmailSendingSuspended:
	log.Printf("sending suspended: %s", e.Data.Object.Reason)
case *events.Unknown:
	// A type this SDK version does not know yet. e.Type and e.Raw() are still available.
}
```

- `Parse` controlla l'envelope (`id`, `type`, `schema_version`, `environment_id`, `occurred_at`, `recorded_at`, `seq`, `data`, `context`) e i membri dichiarati dallo schema del tipo. Un membro mancante o di tipo errato restituisce un `*events.ParseError` con il suo percorso, come `data.object.reason`.
- Un tipo sconosciuto, o un tipo noto con un `schema_version` che questo SDK non modella, viene restituito come `*events.Unknown`. Non è un errore.
- I membri che lo schema non dichiara vengono conservati: `Raw()` restituisce l'intero evento e `ObjectJSON()` restituisce `data.object` (in modalità payload completo, la rappresentazione della risorsa dell'API di gestione).
- I valori dei campi enumerati non vengono confrontati con i valori noti, quindi un nuovo valore dal server non interrompe l'analisi.

Il pacchetto `github.com/keenauth/keenauth-go/claims` tipizza i claim di un token verificato: `claims.ParseIDToken` per un ID token (OpenID Connect Core) e `claims.ParseAccessToken` per un access token (RFC 9068). Controllano i tipi dei claim ma non verificano nulla, quindi chiamali sulla mappa `Claims` restituita dal pacchetto `verify`:

```go
at, err := v.VerifyAccessToken(ctx, token, nil)
if err != nil {
	return err
}
c, err := claims.ParseAccessToken(at.Claims)
if err != nil {
	return err
}
log.Printf("client %s, org %s, roles %v", c.ClientID, c.OrgID, c.OrgRoles)
_ = c.Custom["tenant_tier"] // your own claims
```

`aud` è sempre una slice (`claims.Audience`). I claim che non fanno parte del contratto sono tuoi claim personalizzati e vengono conservati in `Custom`.

## Errori

Un errore dell'API è un documento problem RFC 9457. `keenauth.AsProblem(err)` lo restituisce con il `Code` leggibile dalla macchina. Un codice sconosciuto viene restituito così com'è, quindi nuovi codici del server non interrompono il tuo client:

```go
if _, _, err := c.UsersAPI.GetUser(ctx, id).Execute(); err != nil {
	if pe, ok := keenauth.AsProblem(err); ok && pe.Code == "not_found" {
		// …
	}
}
```

## Avvisi di deprecazione

Quando una risposta contiene un header `Deprecation` (RFC 9745) o `Sunset` (RFC 8594), l'SDK registra un avviso per operazione e valore dell'header per l'intero processo, a livello `WARN` sul logger impostato con `WithLogger`. L'avviso indica il metodo e il percorso con gli identificativi sostituiti da `{id}`, i valori degli header e ogni `Link` con `rel="deprecation"` o `rel="sunset"`. Non contiene mai ID, token o chiavi, e la chiamata stessa non viene modificata.

```text
level=WARN msg="keenauth: the API operation is deprecated" operation="GET /users/{id}" deprecation=@1767225600 sunset="Wed, 01 Jul 2026 00:00:00 GMT"
```
