
# SDK JavaScript pour navigateur

`@keenauth/js` est le cœur navigateur de KeenAuth. Il connecte les utilisateurs à votre application monopage, conserve leurs jetons et appelle votre API. Il n'a aucune dépendance d'exécution, et les composants React reposent sur lui. Le package n'est pas encore publié sur npm.

## Se connecter avec une redirection

Le SDK est un client public OpenID Connect standard :

- Il utilise PKCE (S256) et des jetons liés par DPoP.
- Il vérifie `state`, `nonce` et `iss` lors du retour de redirection.
- Il valide la signature et les revendications du jeton d'identité à l'aide des clés de votre environnement.

```ts
import { KeenAuth } from "@keenauth/js";

const auth = new KeenAuth({
  issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  redirectUri: "https://app.acme.example/callback",
  postLogoutRedirectUri: "https://app.acme.example/",
});

// On your sign-in button:
await auth.signInRedirect({ appState: "/orders" });

// On https://app.acme.example/callback:
const { appState } = await auth.handleRedirectCallback();
```

`handleRedirectCallback` n'accepte une réponse qu'une seule fois. Il échoue avec une `KeenAuthError` lorsque :

- `state` ou `iss` ne correspond pas ;
- la connexion en attente date de plus de 10 minutes ;
- le jeton d'identité n'est pas valide.

Une erreur renvoyée par le serveur est levée sous la forme d'une `KeenAuthOAuthError` contenant le code `error` d'OAuth.

## Appeler votre API

Pour obtenir un jeton d'accès pour votre API, transmettez son identifiant dans `resource` (RFC 8707). Il est envoyé dans la requête d'autorisation, l'échange du code et chaque renouvellement, et le `aud` du jeton correspond à cet identifiant :

```ts
const auth = new KeenAuth({
  issuer: "https://acme.eu.keenauth.app",
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  redirectUri: "https://app.acme.example/callback",
  resource: "https://api.acme.example", // your API's identifier
  scope: "openid profile email offline_access messages:read",
});
```

- La valeur doit être une URI absolue sans fragment (`#`). Elle est envoyée exactement telle que vous l'écrivez ; utilisez donc l'identifiant de l'API tel qu'il est enregistré. Une valeur invalide lève une `KeenAuthError` avec le code `config_invalid` depuis le constructeur.
- `signInRedirect({ resource })` la remplace pour une seule connexion.
- Sans `resource`, le serveur utilise l'API par défaut de l'application, si elle est définie ; sinon, aucune autorisation d'API n'est accordée.
- L'option accepte aussi un tableau, envoyé sous forme de paramètres `resource` répétés, comme le permet la RFC 8707. Un jeton d'accès KeenAuth possède exactement une audience ; le serveur rejette donc plus d'une valeur avec `invalid_target` (une `KeenAuthOAuthError`).

```ts
const res = await auth.authorizedFetch("https://api.acme.example/v1/orders");

// Or take the token yourself; it is refreshed when it is about to expire.
const token = await auth.getAccessToken();
```

`authorizedFetch` envoie `Authorization: DPoP` avec une preuve récente liée au jeton. Si le serveur demande un nonce DPoP, la requête est relancée une fois avec ce nonce. Les appels simultanés partagent une seule requête de renouvellement, et chaque renouvellement remplace le jeton de rafraîchissement. Pour vérifier ces jetons dans votre API, consultez [Vérifier les jetons dans votre API](/docs/sdk/token-verification).

## Se déconnecter

```ts
await auth.signOut();
```

`signOut` supprime les jetons et révoque le jeton de rafraîchissement. Il redirige ensuite le navigateur vers le point de terminaison de déconnexion de votre environnement, qui le renvoie vers `postLogoutRedirectUri`.

## Où sont conservés les jetons

- Les jetons d'accès, de rafraîchissement et d'identité sont conservés uniquement en mémoire, jamais dans localStorage, sessionStorage, IndexedDB, des cookies ou l'URL. Après un rechargement de la page, l'utilisateur doit se connecter à nouveau par redirection ; le SDK ne restaure pas la session.
- sessionStorage contient uniquement les valeurs à usage unique d'une connexion en cours.
- La clé DPoP est une clé WebCrypto non extractible, conservée en mémoire par défaut.

## Connexion intégrée

Le même objet fournit également aux composants intégrés un client d'API frontale :

- `transactions` pour les étapes de connexion ;
- `me` pour le profil du compte et ses `user_metadata`, les adresses e-mail (avec vérification par code), les clés d'accès et les sessions ;
- `webauthn` pour les cérémonies de clé d'accès.

Ces appels utilisent les cookies de votre environnement et un en-tête CSRF. Le serveur ne les accepte que depuis l'origine de connexion de l'environnement.

## Se connecter sans redirection

Sur une page située hors de l'origine de connexion de votre environnement, démarrez la transaction avec votre `clientId`. Le SDK la lie alors à sa clé DPoP au lieu des cookies :

```ts
let tx = await auth.transactions.create({ client_id: "app_01j9...", organization: "acme" });
tx = await auth.transactions.step("identifier", { identifier: "ada@acme.example" });
tx = await auth.transactions.step("password", { password });
if (tx.state === "completed") await auth.complete(tx); // { kind: "signed_in" }
```

- L'application doit être une application first-party, et l'origine de la page doit figurer parmi ses origines web autorisées.
- Chaque appel contient une preuve DPoP et aucun cookie. Si le serveur demande un nonce DPoP, le SDK relance la requête une fois avec ce nonce.
- Le SDK crée lui-même le vérificateur PKCE, `state` et `nonce`, et les conserve en mémoire. `scope` et `resource` proviennent des options ; vous pouvez transmettre `organization` et `uiLocales` à `create`. Utilisez une seule valeur de `resource` ici.
- `complete` vérifie la réponse, échange le code d'autorisation avec la même clé DPoP et conserve les jetons en mémoire. La page ne change pas de destination, et le code n'est jamais renvoyé à votre code.
- Chaque appel `step`, avec ou sans `client_id`, envoie le fuseau horaire du navigateur dans l'en-tête `Idp-Zoneinfo`. Lorsque l'étape termine la connexion et que l'utilisateur n'a pas choisi de fuseau horaire, KeenAuth l'enregistre dans `zoneinfo` du profil, comme le fait la page de connexion hébergée. Si le navigateur ne signale aucun fuseau utilisable, l'en-tête est omis et l'étape reste inchangée.
- Si la connexion nécessite une page que l'utilisateur doit voir (consentement, conditions légales, choix d'une organisation), la dernière étape échoue avec une `KeenAuthProblemError` dont le code est `authorization_failed`. Utilisez `signInRedirect` pour cet utilisateur.

## Appels au compte depuis votre propre origine

Une fois l'utilisateur connecté et le SDK en possession d'un jeton de rafraîchissement (la portée par défaut demande `offline_access`), les appels `me` et `organizations` n'ont plus besoin du cookie de session de l'hôte de connexion :

- Le SDK effectue le rafraîchissement avec `resource=<issuer>/fapi` et obtient un jeton d'accès de courte durée pour l'API frontale. Il est lié par DPoP et conservé uniquement en mémoire, comme les autres jetons.
- Chaque appel au compte envoie `Authorization: DPoP <token>` et une preuve DPoP propre à cet appel, sans cookie. Si le serveur demande un nonce DPoP, le SDK relance la requête une fois avec ce nonce ; si le jeton n'est plus valide, il en obtient un nouveau et relance la requête une fois.
- Le jeton de rafraîchissement est remplacé à chaque renouvellement, donc le SDK exécute les renouvellements les uns après les autres.
- Cela ne fonctionne que pour une application first-party dont les origines web autorisées incluent l'origine de votre page.
- Sans jetons, le SDK utilise le cookie de session comme auparavant.

Consultez [Jeton d'accès à l'API frontale](/docs/api/fapi/front-api-token) pour les règles côté serveur.

## Changer d'organisation

`organizations.switch(orgId)` fait passer l'utilisateur connecté vers une autre organisation sans changer de page. Transmettez l'identifiant `org_…` d'une organisation renvoyée par `organizations.listMine()` :

```js
const { data } = await auth.organizations.listMine();
await auth.organizations.switch(data[1].id);
```

- Il requiert les mêmes conditions que les appels au compte ci-dessus : un jeton de rafraîchissement, `redirectUri` et une application first-party dont les origines web autorisées incluent l'origine de votre page.
- Le SDK envoie une [requête de changement](/docs/api/fapi/transactions/create) avec le jeton de l'API frontale, échange le code avec une preuve de la même clé et conserve les nouveaux jetons, dont le `org_id` correspond à l'organisation. Les jetons que vous possédiez restent en place tant que toutes ces étapes n'ont pas réussi.
- Si l'organisation nécessite une page que l'utilisateur doit voir (MFA, SSO, consentement), l'appel échoue avec une `KeenAuthProblemError` dont le code est `authorization_failed`. Utilisez `signInRedirect({ organization })` pour cet utilisateur.

## Limites actuelles

- Les écritures de profil (`me.update`, `me.updateMetadata`) envoient `If-Match` avec l'`ETag` de la dernière lecture ou écriture du profil. Si le profil a été modifié entre-temps, l'écriture échoue avec une `KeenAuthProblemError` dont le code est `precondition_failed` ; appelez à nouveau `me.get()` avant de réessayer.
- Les numéros de téléphone ne sont pas encore disponibles : `me.phones` vaut `undefined`.
