# Applications

Créez, déployez et contrôlez les applications Beeuni depuis le SDK et depuis React.

Canonical page: https://beeuni.ai/docs/fr/sdk/apps

Les Apps sont des déploiements rattachés à un projet, chacun doté d’une URL stable, avec des versions immuables, machines par groupe de processus et volumes persistants. Adaptez le comportement selon `app.capabilities`. Cette page couvre l’interface du SDK : la façade
`apps` sur un handle de projet, les appels relatifs aux artefacts, versions, machines et volumes, les jetons de connexion, les types exportés et les hooks React.

Pour le modèle, la configuration des versions, les types de sources, la CLI, l’URL stable et le comportement de réveil à froid, consultez [Apps](/docs/fr/feature-flags/apps).

```ts
const apps = beeuni.project(projectId).apps;
```

> **Apps est un indicateur de fonctionnalité**
>
> Chaque route Apps répond `403` avec `{ error, code: "feature_disabled", feature: "apps" }`
> tant que le projet n’a pas activé Apps. Utilisez `isFeatureDisabledError(error)` pour adapter le comportement en conséquence. Consultez [Indicateurs de fonctionnalité](/docs/fr/feature-flags).

<a id="the-apps-facade"></a>

## La façade apps

| Méthode | Encapsule | Fonction |
|---|---|---|
| `apps.list()` | `GET /projects/:pid/apps` | Liste les Apps que l’appelant peut ouvrir (un gestionnaire de projet les voit toutes) |
| `apps.create(input)` | `POST …/apps` | Crée une App et lui attribue son URL stable |
| `apps.get(appId)` | `GET …/apps/:id` | Lit une App |
| `apps.update(appId, input)` | `PATCH …/apps/:id` | La renomme ou modifie la machine, le délai d’inactivité, le mode d’exécution (`always_on`), `uses` ou `labels`. Le mode d’exécution s’applique sous 5 minutes ; un changement de machine s’applique à la version suivante |
| `apps.remove(appId)` | `DELETE …/apps/:id` | Supprime l’App, ses machines et toutes les images de version ; renvoie `images: {released, pending}`. Ses volumes conservent un instantané `final` pendant 7 jours |
| `apps.start(appId)` | `POST …/apps/:id/start` | Définit `desired_state` sur `running` et préchauffe l’environnement d’exécution. Une App statique répond `409 static_app_no_runtime` |
| `apps.stop(appId)` | `POST …/apps/:id/stop` | Suspend immédiatement le calcul ; la requête suivante le reprend. Une App statique répond `409 static_app_no_runtime` : elle n’a pas d’environnement d’exécution et sert le contenu tant qu’elle a un déploiement actif |
| `apps.rollback(appId, deploymentId)` | `POST …/apps/:id/rollback` | Crée une NOUVELLE version à partir de l’artefact et de la configuration d’une version prête, puis la déploie comme un déploiement (`202`). Les volumes conservent leurs données |

Les artefacts sont les entrées immuables d’un déploiement :

| Méthode | Encapsule | Fonction |
|---|---|---|
| `apps.artifacts.register(input)` | `POST …/apps/artifacts` | Enregistre un `archive` ou un `oci_image` ; renvoie l’URL de téléversement pour une archive |
| `apps.artifacts.uploadArchive(bytes, options?)` | — | Enregistre, téléverse, hache et finalise un `.tar.gz` en un seul appel |
| `apps.artifacts.finalize(artifactId, input)` | `POST …/apps/artifacts/:id/finalize` | Confirme `sha256` et `size_bytes` pour un téléversement manuel |

Les versions (déploiements) sont immuables et numérotées :

| Méthode | Encapsule | Fonction |
|---|---|---|
| `apps.deployments.create(appId, input)` | `POST …/apps/:id/deployments` | Lance une version à partir d’un artefact, d’une source et d’un `config` de version facultatif |
| `apps.deployments.list(appId)` | `GET …/apps/:id/deployments` | Liste l’historique des déploiements |
| `apps.deployments.get(appId, deploymentId)` | `GET …/deployments/:did` | Lit un déploiement et ses événements |
| `apps.deployments.logs(appId, deploymentId, options?)` | `GET …/deployments/:did/logs` | Lit les journaux d’exécution avec un curseur |
| `apps.deployments.remove(appId, deploymentId)` | `DELETE …/deployments/:did` | Supprime un déploiement et son image ; `409` pour le déploiement actif ou une compilation en cours. Renvoie `image: 'released' \| 'pending' \| 'none'` |

Machines, mise à l’échelle et exec (une App serveur ; une App sans machines répond
`409 app_no_machines`) :

| Méthode | Encapsule | Fonction |
|---|---|---|
| `apps.machines.list(appId)` | `GET …/apps/:id/machines` | Toutes les machines : `machine_id`, `process_group`, `state`, `release`, `vm`, `volume` |
| `apps.machines.start/stop/restart(appId, machineId)` | `POST …/machines/:mid/{start,stop,restart}` | Agit sur une machine ; se résout avec la liste des machines |
| `apps.machines.destroy(appId, machineId)` | `DELETE …/machines/:mid` | Détruit une machine. Son volume reste ; son groupe exécute une machine de moins |
| `apps.scale(appId, count)` | `PUT …/apps/:id/scale` | Machines par groupe de processus (`{ worker: 3 }`, 0–20), appliquées en arrière-plan (`202`). Se résout avec `scale` et `machines` |
| `apps.exec(appId, { command, machine_id?, timeout_s? })` | `POST …/apps/:id/exec` | Exécute une commande dans une machine en cours d’exécution (`project.app.admin` ; audité). `timeout_s` 1–300, 60 par défaut. Se résout avec `exit_code`, `stdout`, `stderr` (1 Mio chacun), `truncated` |

Volumes (disques persistants) :

| Méthode | Encapsule | Fonction |
|---|---|---|
| `apps.volumes.list(appId)` | `GET …/apps/:id/volumes` | Tous les volumes : `name`, `process_group`, `machine_id`, `size_gb`, `status`, `attached`, `snapshot_retention_days` |
| `apps.volumes.create(appId, { name, size_gb, process_group?, snapshot_retention_days? })` | `POST …/volumes` | Crée un volume à l’avance ; une machine ultérieure de son groupe ayant un montage de ce nom le récupère |
| `apps.volumes.update(appId, volumeId, { size_gb?, snapshot_retention_days? })` | `PATCH …/volumes/:vid` | Agrandit un volume (jamais de réduction ; 100 Go maximum) ou modifie sa durée de rétention (1–60 jours) |
| `apps.volumes.destroy(appId, volumeId, confirmName)` | `DELETE …/volumes/:vid?confirm=<name>` | `project.app.admin`. Refusé avec `409 volume_attached` tant qu’une machine le monte. Conserve un instantané `final` pendant 7 jours |
| `apps.volumes.snapshots(appId, volumeId)` | `GET …/volumes/:vid/snapshots` | Instantanés `daily`, `manual` et `final` |
| `apps.volumes.snapshot(appId, volumeId)` | `POST …/volumes/:vid/snapshots` | Un instantané `manual` immédiatement |
| `apps.volumes.restore(appId, volumeId, snapshotId)` | `POST …/volumes/:vid/restore` | `project.app.admin`. La machine s’arrête, le volume revient à l’instantané, la machine démarre dessus (`202`) |
| `apps.volumes.fork(appId, volumeId, { name, snapshot_id? })` | `POST …/volumes/:vid/fork` | Un nouveau volume non attaché dans la même App, à partir d’un instantané ou du contenu actuel |

Jetons de connexion (capacité `member_tokens` ; `409 app_capability_unsupported`
si l’App ne l’a pas) :

| Méthode | Encapsule | Fonction |
|---|---|---|
| `apps.token(appId)` | `POST …/apps/:id/token` | Un jeton de connexion de 15 minutes pour l’App, identifiant l’appelant |

L’accès est régi par la politique d’autorisation propre à l’App :

| Méthode | Encapsule | Fonction |
|---|---|---|
| `apps.access.get(appId)` | `GET …/apps/:id/access` | Lit la politique. Nécessite `project.app.read` |
| `apps.access.update(appId, input)` | `PATCH …/apps/:id/access` | Remplace la politique et incrémente sa révision |
| `apps.access.session(appId)` | `POST …/apps/:id/access-session` | Génère une URL valable cinq minutes qui s’échange contre un cookie limité à l’hôte |

<a id="deploy-a-static-site"></a>

## Déployer un site statique

`uploadArchive` effectue tout le protocole d’échange de l’artefact : il enregistre l’artefact,
le vérifie par rapport à `max_bytes`, transfère les octets via `PUT`, calcule le SHA-256 et
finalise.

```ts
const apps = beeuni.project(projectId).apps;

const app = await apps.create({ slug: 'docs', name: 'Docs' });
const artifact = await apps.artifacts.uploadArchive(tarGzBytes, {
  onProgress: (uploaded, total) => console.log(`${uploaded}/${total}`),
});

const deployment = await apps.deployments.create(app.app_id, {
  artifact_id: artifact.artifact_id,
  source: { kind: 'static', spa: true },
});

console.log(app.url, deployment.status); // https://…apps.beeuni.ai  queued
```

`create` accepte aussi les champs de machine : `cpu`, `memory_gb`,
`disk_gb`, `idle_timeout_seconds` et `always_on`. Omettez-les pour utiliser les valeurs par défaut.
Les Apps n’ont pas de budget mensuel : les crédits du compte paient le calcul. Une App serveur toujours active est facturée 24 h/24, 7 j/7 ; une App à la demande, uniquement lorsqu’elle est éveillée.
`always_on: true` fait fonctionner une App serveur 24 h/24, 7 j/7
(websockets, boucles en arrière-plan) ; `false` l’arrête après `idle_timeout_seconds`
et la réveille à la requête suivante. Une App statique n’a pas d’environnement d’exécution : `always_on` est
`false`.

L’API ignore `monthly_budget_usd` si un ancien client l’envoie (`200`, aucune
erreur). Aucune réponse d’App ne le contient.

`uses` liste, par slug, les Apps
pour lesquelles cette App peut obtenir des jetons de connexion et qu’elle peut atteindre via son montage de liaisons ; par défaut, aucune, et `update(appId, { uses })` remplace la liste (voir
[Apps utilisées par une App](/docs/fr/feature-flags/apps#apps-an-app-uses-sign-in-tokens-and-bindings)).

`create` et `update` renvoient `warnings`, qui vaut toujours `[]`.

```ts
const app = await beeuni.project(projectId).apps.create({ slug: 'worker', name: 'Worker', always_on: true });
console.log(app.always_on, app.hosting_type); // true null (until the first deploy)
```

Attendez le déploiement en interrogeant régulièrement son état :

```ts
async function waitForReady(appId: string, deploymentId: string) {
  for (;;) {
    const { deployment } = await apps.deployments.get(appId, deploymentId);
    if (deployment.status === 'ready') return deployment;
    if (deployment.status === 'failed' || deployment.status === 'cancelled') {
      throw new Error(deployment.error ?? deployment.error_code ?? deployment.status);
    }
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
}
```

Les valeurs d’état sont `queued`, `validating`, `building`, `provisioning`,
`checking`, `ready`, `failed` et `cancelled`.

<a id="deploy-an-oci-image"></a>

## Déployer une image OCI

Enregistrez la référence immuable de l’image, puis déclarez la commande du processus et
le port cible public :

```ts
const registered = await apps.artifacts.register({
  kind: 'oci_image',
  image: 'ghcr.io/hekmat/service:2026-08-07',
});

await apps.deployments.create(app.app_id, {
  artifact_id: registered.artifact.artifact_id,
  source: {
    kind: 'oci_image',
    image: 'ghcr.io/hekmat/service:2026-08-07',
    command: ['node', 'server.js'],
    port: 3000,
    readiness_path: '/health',
  },
});
```

`register` renvoie `upload: null` pour un `oci_image`. Seul un `archive` reçoit une
URL de téléversement.

`CreateAppDeploymentInput` accepte aussi `environment` (valeurs d’exécution
non secrètes), `secrets` (clé d’exécution vers le nom du secret du projet) et `config`
(`AppReleaseConfigInput`, la configuration de la version). Un `config` invalide
répond `400 app_config_invalid` avec `issues: [{ path, message }]` ; un montage
sur un environnement d’exécution sans volumes répond `400 app_volumes_unsupported`.

```ts
await apps.deployments.create(app.app_id, {
  artifact_id: registered.artifact.artifact_id,
  source: { kind: 'oci_image', image, command: ['node', 'server.js'] },
  config: {
    processes: { app: '', worker: 'node worker.js' },
    http_service: { internal_port: 3000, access: 'app', processes: ['app'] },
    mounts: [{ source: 'data', destination: '/data', initial_size_gb: 10, processes: ['app'] }],
    deploy: { release_command: 'node migrate.js' },
  },
});
```

<a id="read-runtime-logs"></a>

## Lire les journaux d’exécution

```ts
let cursor = 0;
for (;;) {
  const page = await apps.deployments.logs(app.app_id, deployment.deployment_id, {
    after: cursor,
    limit: 200,
  });
  for (const entry of page.entries) console.log(entry.source, entry.line);
  cursor = page.next_cursor;
  if (page.entries.length === 0) break;
}
```

Chaque entrée contient `cursor`, `time`, `source` (`app`, `appd`, `caddy`) et
`line`. Une ligne `caddy` constitue le journal d’accès : méthode, URI, statut, durée et
en-têtes de réponse. Elle ne contient jamais les en-têtes de requête, car ceux-ci contiennent les
identifiants de la passerelle (le jeton et l’identité du visiteur, le jeton d’entrée du fournisseur).

<a id="machines-scale-and-volumes"></a>

## Machines, mise à l’échelle et volumes

```ts
const apps = beeuni.project(projectId).apps;

const machines = await apps.machines.list(app.app_id);
await apps.scale(app.app_id, { worker: 3 });                     // 202: applied in the background

const { exit_code, stdout } = await apps.exec(app.app_id, { command: ['ls', '/data'] });

const [data] = await apps.volumes.list(app.app_id);
const snapshot = await apps.volumes.snapshot(app.app_id, data.volume_id);
await apps.volumes.restore(app.app_id, data.volume_id, snapshot.snapshot_id);   // project.app.admin
const copy = await apps.volumes.fork(app.app_id, data.volume_id, { name: 'data_copy' });
```

Une machine conserve son `machine_id` d’une version à l’autre ; chaque version remplace son
box (`instance_id`). Une machine monte un seul volume à la fois. Une restauration crée une
nouvelle version du volume : le contenu remplacé reste dans son historique. Un
instantané d’un volume monté peut omettre sa dernière seconde d’écritures environ.

<a id="a-convex-backend"></a>

## Un backend Convex

Un backend Convex est une App serveur ordinaire avec un volume, créée à partir du
modèle `convex` (`beeuni apps launch --template convex --name db` ; voir
[Modèle `convex`](/docs/fr/feature-flags/apps#template-convex-a-convex-backend)).
Le SDK ne propose aucun appel spécifique à Convex. Lisez la clé d’administration sur la machine avec
`exec`, et donnez à une App d’interface accès au backend avec `uses` :

```ts
const db = (await apps.list()).find((a) => a.slug === 'db')!;
const { stdout } = await apps.exec(db.app_id, { command: ['sh', '-c', 'cd /convex && ./generate_admin_key.sh'] });
// The admin key controls all code and data: never log it or ship it to a browser.

await apps.update(site.app_id, { uses: ['db'] }); // the site may mint db tokens and bind to it
```

`db.services` contient les URL du backend : `http` (l’URL du client Convex) et
`site` (actions HTTP).

<a id="your-app-already-knows-who-is-looking"></a>

## Votre App sait déjà qui la consulte

Une App hébergée par Beeuni est ouverte par une personne **déjà connectée à Beeuni**. La
passerelle des Apps l’authentifie avant que votre premier octet soit servi ; votre App n’a donc
pas besoin de sa propre connexion — ni second mot de passe, ni écran de consentement, ni redirection.

Dans le navigateur :

```ts
import { createBeeuni, beeuniAppViewerToken } from '@beeuni/sdk';
import { useBeeuniAppViewer } from '@beeuni/sdk/react';

const beeuni = createBeeuni({
  backendUrl: '/_beeuni/api/v1',             // the gate, on your App's own origin
  getToken: beeuniAppViewerToken(),          // the viewer's own App-scoped token
});

function Header() {
  const { status, viewer } = useBeeuniAppViewer();
  return <span>{status === 'viewer' ? viewer.email : 'Signed out'}</span>;
}
```

Le navigateur appelle `/_beeuni/api/v1/*` sur l’origine de votre App, jamais
`https://api.beeuni.ai/v1` directement : l’API ne répond à aucune requête préliminaire CORS provenant
d’une origine d’App, donc un appel direct échoue dans le navigateur. La passerelle transmet chaque
appel à l’API Beeuni au nom du visiteur connecté, avec le jeton du visiteur limité à l’App.
Règles :

- `viewer_token_scope: 'api'` est nécessaire. Sinon, elle répond
  `403 viewer_api_disabled`.
- Elle agit uniquement pour un visiteur connecté par la passerelle (connexion Beeuni ou lien
  d’accès). En l’absence de visiteur, elle répond `401 no_viewer_identity`.
- Elle répond uniquement aux requêtes provenant de l’origine de l’App. Une requête intersite
  reçoit `403 cross_site_request`.
- Elle transmet `/v1/*` sauf `/v1/oauth/*` et les points de terminaison MCP, qui répondent
  `404`. Le cookie de votre App et son propre en-tête `Authorization` n’atteignent jamais
  l’API, et l’API ne peut pas définir de cookie sur votre App.

Sur le serveur de votre App, la passerelle inclut l’identité signée dans chaque requête :

```ts
import { readAppViewer, createAppViewerBeeuni } from '@beeuni/sdk/server';

const viewer = await readAppViewer(request);
// { userId, email, name, picture, groupIds, groups, role, projectId, accountId, appId, accessMode, token }
if (!viewer) return new Response('Not found', { status: 404 });

// and, for an `api`-scoped App, act as them:
const beeuni = await createAppViewerBeeuni(request, { backendUrl });
await beeuni.projects.list();               // their projects, their role
```

`readAppViewer` vérifie un HMAC de l’en-tête avec `BEEUNI_APP_VIEWER_SECRET`,
que Beeuni injecte dans votre App au déploiement. Un en-tête falsifié ne passe jamais : la
passerelle supprime toute copie fournie par le client avant de transmettre la requête, et la signature est
créée avec un secret dérivé pour chaque App.

<a id="how-much-your-app-is-told"></a>

### Les informations communiquées à votre App

Un paramètre de la politique d’accès de l’App — **Settings → Access** dans Beeuni,
`beeuni apps access <app> --viewer off|identity|api`, ou `viewer_token_scope`
sur `PATCH /projects/:id/apps/:appId/access` :

| Portée | Votre App reçoit |
|---|---|
| `identity` (par défaut) | L’identifiant, l’e-mail, le nom, la photo, le rôle dans le compte et les groupes Beeuni du visiteur (identifiants et noms, uniquement dans le compte de cette App), ainsi qu’un jeton `profile email`. De quoi afficher à chaque personne ses propres données et contrôler l’accès par groupe. |
| `api` | Les informations ci-dessus, ainsi qu’un jeton qui agit **au nom de** cette personne sur l’API Beeuni — dans les limites de son propre rôle. |
| `off` | Rien. |

Le jeton n’est jamais la session Beeuni de l’utilisateur : il dure une heure, ne possède que
ces portées, et chaque jeton créé par une App expire lorsque l’App est supprimée ou que sa
politique d’accès change.

<a id="using-the-viewer-token"></a>

### Utiliser le jeton du visiteur

- **Sur votre serveur, lisez-le à chaque requête.** La passerelle place un jeton valide dans
  `x-beeuni-app-viewer-token` sur chaque requête transmise par proxy. Envoyez-le sous la forme
  `Authorization: Bearer` à l’API Beeuni — `createAppViewerBeeuni(request)`
  fait exactement cela. Créez un client par requête ; ne stockez pas le jeton.
- **Après une modification de la politique d’accès, les anciens jetons cessent de fonctionner.** La requête suivante
  passant par la passerelle en contient un nouveau. Dans le navigateur, `beeuniAppViewerToken()`
  se rétablit automatiquement : lorsque l’API refuse son jeton, le SDK relit
  `/_beeuni/viewer` et rejoue l’appel une fois.
- **Le rôle du visiteur constitue la limite maximale.** Le jeton peut faire exactement ce que le
  visiteur peut faire dans Beeuni, jamais plus. Sur un projet avec des autorisations d’agent, un
  `member` a besoin d’une autorisation sur l’agent que votre App démarre : sans celle-ci,
  `POST /projects/:id/sessions` répond `403 no_agent_access`. Accordez à vos
  visiteurs l’accès à l’agent, pas un rôle plus étendu.
- **Qui dispose d’une identité de visiteur.** Une personne qui ouvre l’App avec sa connexion Beeuni — ou
  avec un lien d’accès, qui la connecte sous cette identité — en dispose. Une
  App `password` n’en a pas : le mot de passe prouve la connaissance d’un secret partagé, pas une identité. Une
  App `public` n’en a une que pour un visiteur arrivé par un lien d’accès ;
  l’URL seule reste anonyme.
- **C’est un identifiant d’authentification.** Beeuni exclut les en-têtes de requête du journal d’accès de l’App
  (`beeuni apps logs`). Ne consignez pas cet en-tête dans votre propre code.

Une App servie sur son **propre domaine** (et non `*.apps.beeuni.ai`) n’a pas de passerelle
 devant elle — utilisez plutôt [Se connecter avec Beeuni](/docs/fr/sdk/sign-in).

<a id="one-member-on-every-runtime"></a>

## Un même membre, dans tous les environnements d’exécution

Beeuni atteste l’identité de la même personne à trois endroits : le navigateur interroge la passerelle
(`/_beeuni/viewer`), le serveur de l’App lit l’en-tête signé, et une App qui
vérifie les jetons de connexion (un backend Convex ou votre propre serveur) reçoit un
jeton signé par Beeuni. `readBeeuniMember` convertit chacun de ces formats en un
`BeeuniMember`, et `requireBeeuniMember` décide qui peut continuer. Aucun
framework ni dépendance : le même code s’exécute dans un navigateur, Node, Bun, un
Worker et une fonction Convex.

```ts
import {
  readBeeuniMember, requireBeeuniMember, verifyBeeuniToken,
  beeuniToken, beeuniBinding, fetchBeeuniAppViewer, BeeuniMemberError,
} from '@beeuni/sdk';

// Browser: who is looking (null when nobody).
const me = readBeeuniMember(await fetchBeeuniAppViewer());

// Browser → an App this App uses, on the same origin: its URL and a token fetcher.
const db = beeuniBinding('db');
const convex = new ConvexReactClient(db.url);
convex.setAuth(db.token);
// React: read the member with useQuery(api.members.me), never useConvexAuth() (see below).

// Browser: a token for this App itself, or for an App it uses.
const token = await beeuniToken()();
const dbToken = beeuniToken({ audience: 'db' });

// A Convex function: the runtime verified the token; enforce the rules.
const member = requireBeeuniMember(await ctx.auth.getUserIdentity(), { groups: ['Finance'] });

// A server App: verify the bearer. No options = the BEEUNI_AUTH_* env Beeuni injects.
// Any other server: pass the App's `auth` values.
const caller = await verifyBeeuniToken(bearer, { issuer: app.auth.issuer, audience: app.auth.audience, jwks: app.auth.jwks_uri });
```

Dans React, n’appelez pas `useConvexAuth()` : il lève une exception avec un simple
`ConvexProvider` (il nécessite `ConvexProviderWithAuth`, et `beeuniBinding` n’est pas
vérifié avec ce fournisseur). Récupérez le membre connecté avec une requête :

```ts
// convex/members.ts
export const me = query({ args: {}, handler: async (ctx) => readBeeuniMember(await ctx.auth.getUserIdentity()) });
```

```tsx
const me = useQuery(api.members.me);   // undefined: loading. null: signed out. Otherwise the member.
```

`readBeeuniMember` renvoie `null` pour un appelant anonyme. Utilisez
`requireBeeuniMember` uniquement lorsque la fonction doit lever une exception.

| Champ | Valeur |
|---|---|
| `userId` | Identifiant utilisateur Beeuni. Stable ; utilisez-le comme clé des lignes. |
| `email`, `name`, `picture` | Issus du profil du membre. `null` si inconnus. |
| `groups`, `groupIds` | Les groupes Beeuni du membre dans ce compte, par nom et par identifiant. |
| `role` | Rôle dans le compte : `owner`, `admin` ou `member`. |
| `accountId`, `projectId` | Où se trouve l’App. |

`requireBeeuniMember(claims, { groups, roles })` renvoie le membre ou lève
`BeeuniMemberError` avec `code` `unauthenticated` (personne, ou un jeton qui
n’a pas passé une vérification) ou `forbidden` (hors des groupes ou des rôles). Chaque liste signifie
« au moins un de ces éléments » ; un groupe correspond par son nom ou son identifiant ; une liste vide n’admet personne.

<a id="sign-in-tokens"></a>

### Jetons de connexion

Un émetteur par projet signe les jetons de chaque App : `iss` =
`<API origin>/v1/projects/<projectId>`, `aud` = l’identifiant de l’App à laquelle le jeton est
destiné, 15 minutes. Son jeu de clés est public à l’adresse `<issuer>/jwks.json`, identifié par
`<issuer>/.well-known/openid-configuration`. Chaque App contient les valeurs qui
vérifient ses jetons : `app.auth.issuer`, `app.auth.audience`, `app.auth.jwks_uri`.
Un jeton contient `sub`, `email`, `name`, `picture`, `groups`, `group_ids`,
`role`, `account_id` et `project_id` ; le jeton d’un agent contient
`kind: 'agent'` et aucun groupe ni rôle.

| Fonction | Ce qu’elle fait |
|---|---|
| `beeuniToken({ audience? })` | Une fonction de récupération de jeton (la forme attendue par les clients temps réel pour leur callback d’authentification) pour l’App elle-même (par défaut) ou une App qu’elle utilise. Lit `/_beeuni/token` sur l’origine propre à l’App. Mise en cache jusqu’à peu avant l’expiration ; `{ forceRefreshToken: true }` effectue une nouvelle demande. Renvoie `null` sans utilisateur connecté, et `null` avec un avertissement dans la console pour une App qu’elle n’utilise pas (`403 app_not_linked`). Ne lève jamais d’exception. |
| `beeuniBinding(slug, { origin? })` | `{ url, token }` pour une App utilisée par cette App : `url` = `<origin>/_beeuni/apps/<slug>` (HTTP et WebSocket, même origine, sans CORS), `token` = `beeuniToken({ audience: slug })`. |
| `verifyBeeuniToken(bearer, { issuer?, audience?, jwks? })` | Vérifie la signature ES256, l’expiration (tolérance de 60 s), l’émetteur et l’audience. Valeurs par défaut : `BEEUNI_AUTH_ISSUER`, `BEEUNI_AUTH_AUDIENCE`, `BEEUNI_AUTH_JWKS`. Beeuni injecte les trois dans l’environnement d’exécution de chaque App serveur (émetteur : le projet ; audience : l’identifiant propre à l’App ; jeu de clés : un URI `data:`), donc `verifyBeeuniToken(bearer)` y fonctionne sans options. Ne définissez jamais `BEEUNI_AUTH_*` dans le `env` d’une App serveur : le préfixe `BEEUNI_` est réservé et `beeuni validate` le rejette. Le jeu de clés est un objet, du texte JSON, un URI `data:` ou une URL https (`auth.jwks_uri`, récupérée une fois par processus). Tout échec lève `BeeuniMemberError`, y compris l’absence de jeu de clés ou d’audience : une seule clé de projet signe les jetons de toutes les Apps, c’est donc `aud` qui empêche l’utilisation du jeton d’une autre App. `audience: false` accepte un jeton pour n’importe quelle App du projet. |
| `apps.token(appId)` | Un jeton identifiant l’appelant, fourni par l’API (`POST …/apps/:id/token`). Une session d’agent en reçoit un identifiant son agent. |

Un membre que vous retirez du projet ou d’un groupe perd son accès au plus tard à l’expiration d’un jeton ; le contrôle d’accès de l’App le refuse au prochain chargement de page. Les groupes Beeuni nécessitent l’offre Enterprise ; les rôles fonctionnent avec toutes les offres.

<a id="manage-access"></a>

## Gérer l’accès

```ts
await apps.access.update(app.app_id, {
  mode: 'restricted',
  member_ids: [memberId],
  group_ids: [groupId],
});

const preview = await apps.access.session(app.app_id);
window.open(preview.url); // valid for five minutes
```

`AppAccessConfig` indique `password_configured`, jamais le mot de passe ni son
hachage. Définissez un mot de passe avec `{ mode: 'password', password }`. Chaque mise à jour
incrémente `revision`, ce qui révoque les cookies d’App existants.

## Types

Tous les types ci-dessous sont exportés depuis `@beeuni/sdk`.

| Type | Ce qu’il contient |
|---|---|
| `App` | Identité, `capabilities`, `labels`, `scale`, `services` (`AppServiceSummary[]`), `machines` (`total`, `running`, `stopped`), `volumes` (`count`, `size_gb`), `config` (le `AppReleaseConfig` de la version en production, ou `null`), `uses`, `used_by`, `auth`, `url`, `access_mode`, `access_revision`, `desired_state`, `active_deployment_id`, `hosting_type` (de la version active : `static`, `sandbox`, ou `null` avant le premier déploiement), `machine` (la taille du groupe servant la version en production), `idle_timeout_seconds`, `always_on`, `retained_deployments`, `warnings` (création et mise à jour uniquement ; toujours `[]`), `last_request_at`, `viewer_can_access` |
| `AppCapability` | `'deployments' \| 'rollback' \| 'preview' \| 'sleep' \| 'static' \| 'member_tokens' \| 'machines' \| 'scale' \| 'exec' \| 'volumes' \| 'snapshots'` |
| `AppReleaseConfig` · `AppReleaseConfigInput` | La configuration résolue de la version (`processes`, `services`, `mounts`, `vm`, `deploy`) · la forme rédigée (`processes`, `http_service`, `services`, `mounts`, `vm`, `deploy`, et les formes abrégées `port`, `command`, `readiness_path`, `always_on`, `resources`) |
| `AppServiceConfig` · `AppServiceAccess` · `AppAutoStop` · `AppCheck` | `name`, `internal_port`, `protocol`, `access`, `auto_stop_machines`, `auto_start_machines`, `min_machines_running`, `processes`, `checks` · `'public' \| 'app' \| 'admins'` · `'off' \| 'stop' \| 'suspend'` · `type`, `port`, `path`, `interval_s`, `timeout_s`, `grace_period_s` |
| `AppMountConfig` · `AppVmConfig` · `AppDeployStrategy` | `source`, `destination`, `initial_size_gb`, `snapshot_retention_days`, `processes` · `cpu`, `memory_gb`, `disk_gb`, `processes` · `'rolling' \| 'immediate' \| 'bluegreen' \| 'canary'` |
| `AppServiceSummary` | `name`, `protocol`, `access`, `internal_port`, `processes`, `url` (`null` lorsque le libellé d’hôte serait trop long) |
| `AppMachine` · `AppMachineState` | `machine_id`, `instance_id`, `process_group`, `state`, `release` (`deployment_id`, `version`), `vm`, `volume`, `created_at`, `started_at`, `stopped_at` · `'started' \| 'starting' \| 'stopped' \| 'stopping' \| 'provisioning'` |
| `AppVolume` · `AppVolumeSnapshot` · `CreateAppVolumeInput` | `volume_id`, `name`, `process_group`, `machine_id`, `size_gb`, `status`, `attached`, `snapshot_retention_days`, `created_at` · `snapshot_id`, `kind` (`daily`, `manual`, `final`), `commit_id`, `created_at` · `name`, `size_gb`, `process_group`, `snapshot_retention_days` |
| `AppExecInput` · `AppExecResult` | `command`, `machine_id`, `timeout_s` · `machine_id`, `exit_code`, `stdout`, `stderr`, `truncated` |
| `AppAuth` | `issuer`, `audience` (l’identifiant de l’App), `jwks_uri` : les valeurs publiques qui vérifient les jetons de connexion de l’App |
| `AppToken` | `token`, `expires_at` |
| `BeeuniTokenOptions` · `BeeuniTokenFetcher` · `BeeuniBinding` · `VerifyBeeuniTokenOptions` | Les utilitaires de jetons de connexion ci-dessus |
| `AppDeployment` | `version`, `status`, `source_kind`, `hosting_type` (`static` : servi depuis le stockage, sans environnement d’exécution ; `sandbox` : machines), `hosting_provider`, `runtime_spec`, `build_spec`, `error_code`, `attempt_count`, `created_by`, `actor_type`, `source_session_id`, `config` (la configuration résolue de la version), `strategy`, `release_command_status` (`running`, `succeeded`, `failed` ou `null`) |
| `AppRollback` | La valeur de résolution de `apps.rollback` : le `App` plus la version en attente dans `deployment` |
| `AppDeploymentDetail` | Un `deployment` plus son `events` : les événements du cycle de vie et le journal de build (lignes `build_log`, et un événement `log_truncated` lorsque le build a affiché plus de 5 000 lignes) |
| `AppAccessConfig` | `mode`, `revision`, `member_ids`, `group_ids`, `password_configured` |
| `AppAccessMode` | `'private' \| 'project' \| 'restricted' \| 'public' \| 'password'` |
| `AppSource` | `StaticAppSource \| BundleAppSource \| DockerfileAppSource \| OciImageAppSource` |
| `AppArtifact` | `kind`, `status`, `sha256`, `size_bytes`, `image_reference` |
| `AppLogEntry` · `AppLogsResponse` | Une ligne de journal, et une page plus `next_cursor` |

`apps.list()` et `useProjectApps` renvoient uniquement les Apps que l’appelant peut ouvrir
selon la politique d’accès de chaque App (privée : le créateur ; restreinte : les membres
et groupes répertoriés ; projet, mot de passe et publique : l’équipe du projet). Un gestionnaire
de projet voit toutes les Apps, ce qui permet de continuer à gérer une App privée après
le départ de son créateur. `viewer_can_access` vaut donc `true` pour chaque App répertoriée. Un `get` d’une
App que l’appelant ne peut pas ouvrir renvoie `404`, et non `403`. Traitez `undefined` comme
inconnu, et non comme refusé.

`AppAccessMode` est un paramètre de visibilité par ressource qui s’ajoute au modèle de rôles,
pas un rôle. `restricted` désigne des utilisateurs et des groupes — les mêmes types de principaux que ceux
utilisés par le modèle de rôles. Voir
[Comptes et accès](/docs/fr/accounts#per-feature-access-settings).

`DockerfileAppSource` et `OciImageAppSource` nécessitent `command`. `port` est le
port du service principal : ne l’omettez que pour une version composée uniquement de workers (un `config`
avec `processes` et sans service). `StaticAppSource` et `BundleAppSource` n’acceptent
ni l’un ni l’autre.

<a id="react-hooks"></a>

## Hooks React

`@beeuni/sdk/react` exporte sept hooks pour les Apps.

### useProjectApps(projectId)

Les Apps que l’appelant peut ouvrir, ainsi que leurs mutations de cycle de vie. Chaque mutation
invalide l’inventaire en cas de succès.

```tsx
import { useProjectApps } from '@beeuni/sdk/react';

function AppList({ projectId }: { projectId: string }) {
  const apps = useProjectApps(projectId);
  if (!apps.data) return null;

  return (
    <ul>
      {apps.data.map((app) => (
        <li key={app.app_id}>
          <a href={app.url}>{app.slug}</a>
          <button onClick={() => apps.stop.mutate(app.app_id)}>Stop</button>
        </li>
      ))}
    </ul>
  );
}
```

Il renvoie les champs de la requête ainsi que `create`, `update`, `start`, `stop` et
`remove`.

### useAppDeployments(projectId, appId)

L’historique immuable des déploiements, récupéré à nouveau toutes les 5 s pour qu’un build en cours
progresse automatiquement.

```tsx
const deployments = useAppDeployments(projectId, appId);

await deployments.deploy.mutateAsync({
  artifact_id: artifact.artifact_id,
  source: { kind: 'static', spa: true },
});
await deployments.rollback.mutateAsync(previousDeploymentId);
```

Les deux mutations invalident la liste des déploiements et l’inventaire des Apps.

### useAppDeployment(projectId, appId, deploymentId)

Un déploiement et ses événements, journal de build inclus (`AppDeploymentDetail`). Il
effectue une interrogation toutes les 2 s jusqu’à ce que le déploiement soit `ready`, `failed` ou `cancelled`. Passez
`null` comme `deploymentId` pour le laisser inactif.

```tsx
const detail = useAppDeployment(projectId, appId, deploymentId);
const log = (detail.data?.events ?? [])
  .map((event) => (event.type === 'build_log' ? event.message : `[${event.type}] ${event.message}`))
  .join('\n');
```

Un build conserve ses 2 500 premières et ses 2 500 dernières lignes. Les lignes sont écrites par
lots, au plus tard 1 s après leur affichage. Un déploiement échoué ou annulé
conserve son journal de build pendant 14 jours.

### useAppAccess(projectId, appId, options?)

La politique d’accès et une session d’accès de courte durée. Les deux parties sont des
requêtes distinctes, chacune étant facultative.

```tsx
const access = useAppAccess(projectId, appId, {
  policy: canEditAccess,
  session: app.viewer_can_access,
});

access.policy.data;  // AppAccessConfig
access.session.data; // { url, expires_at }
await access.update.mutateAsync({ mode: 'project' });
```

| Option | Valeur par défaut | Utilisez `false` quand |
|---|---|---|
| `policy` | `true` | L’interface affiche uniquement un aperçu de l’App. `GET …/access` est une lecture administrative et renvoie `403` pour un appelant sans droits de gestionnaire de projet. |
| `session` | `true` | L’appelant n’est pas autorisé à ouvrir l’App. Passez `app.viewer_can_access` (toujours `true` pour une App répertoriée). |

Une grille d’Apps qui laisse les deux options à `true` déclenche une lecture de politique et une
création de session par App, chacune renvoyant `403` pour un membre qui n’est pas autorisé à ouvrir cette
App.

### useAppMachines(projectId, appId)

Les machines d’une App, interrogées toutes les 5 s, ainsi que `start`, `stop`, `restart`,
`destroy` (chacun prend un `machineId`) et `scale` (prend
`{ [group]: count }`). Chaque mutation actualise les machines et l’inventaire des
Apps. `appMachinesKey(projectId, appId)` est la clé de requête.

```tsx
const machines = useAppMachines(projectId, app.app_id);
await machines.scale.mutateAsync({ worker: 2 });
await machines.restart.mutateAsync(machineId);
```

### useAppVolumes(projectId, appId)

Les volumes d’une App, ainsi que `snapshot` (`volumeId`), `restore`
(`{ volumeId, snapshotId }`), `fork` (`{ volumeId, name, snapshotId? }`),
`extend` (`{ volumeId, sizeGb }`) et `destroy` (`{ volumeId, name }`). Chaque
mutation actualise les volumes. `appVolumesKey(projectId, appId)` est la clé de
requête.

```tsx
const volumes = useAppVolumes(projectId, app.app_id);
await volumes.snapshot.mutateAsync(volumeId);
await volumes.restore.mutateAsync({ volumeId, snapshotId });
```

### useAppVolumeSnapshots(projectId, appId, volumeId)

Les instantanés d’un volume. Passez `null` comme `volumeId` pour laisser la requête inactive.

<a id="errors"></a>

## Erreurs

```ts
import { featureDisabledKey, isFeatureDisabledError } from '@beeuni/sdk';

try {
  await beeuni.project(projectId).apps.list();
} catch (error) {
  if (isFeatureDisabledError(error)) {
    console.log(`${featureDisabledKey(error)} is off for this project`);
  }
}
```

Autres réponses à gérer :

- `409` pour un slug en double, `402` avec `app_quota_exceeded` lorsque le compte
  a atteint sa limite d’Apps, et `400` avec `app_machine_out_of_range` pour une spécification hors limites.
- `400 app_not_found` (`slugs`) pour un slug inconnu dans `uses`.
- `409 app_capability_unsupported` (`capability`) pour un appel que l’App ne
  propose pas.
- Un déploiement : `400 app_config_invalid` (`issues`), `400 app_volumes_unsupported`.
  Un retour à une version précédente : `409 release_not_ready`.
- Machines et exec : `409 app_no_machines`, `404 machine_not_found`,
  `409 machine_not_running`, `400 process_group_unknown`,
  `400 scale_out_of_range`, `400 command_required`.
- Volumes : `400 volume_name_invalid`, `volume_size_invalid`,
  `volume_shrink_refused`, `volume_retention_invalid`, `snapshot_invalid`,
  `confirmation_required` ; `404 volume_not_found` ; `409 volume_attached`.

`token()` répond avec `Cache-Control: no-store`.
