# App

Crea, distribuisci e controlla le app Beeuni dall'SDK e da React.

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

Le App sono distribuzioni limitate al progetto, ciascuna con un URL stabile,
release immutabili, macchine per gruppo di processi e volumi persistenti. Usa `app.capabilities` per scegliere il ramo. Questa pagina descrive l'interfaccia dell'SDK: la
facade `apps` su un handle di progetto, le chiamate per artefatti, release, macchine e volumi,
i token di accesso, i tipi esportati e gli hook React.

Per il modello, la configurazione delle release, i tipi di sorgente, la CLI, l'URL stabile
e il comportamento di riattivazione a freddo, leggi [Apps](/docs/it/feature-flags/apps).

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

> **Apps è un feature flag**
>
> Ogni route Apps risponde `403` con `{ error, code: "feature_disabled", feature: "apps" }`
> finché il progetto non attiva Apps. Usa `isFeatureDisabledError(error)` per scegliere
> il ramo in base a questo flag. Vedi [Feature flag](/docs/it/feature-flags).

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

## La facade apps

| Metodo | Incapsula | Cosa fa |
|---|---|---|
| `apps.list()` | `GET /projects/:pid/apps` | Elenca le App che il chiamante può aprire (un responsabile del progetto le vede tutte) |
| `apps.create(input)` | `POST …/apps` | Crea un'App e le assegna un URL stabile |
| `apps.get(appId)` | `GET …/apps/:id` | Legge un'App |
| `apps.update(appId, input)` | `PATCH …/apps/:id` | La rinomina o modifica macchina, timeout di inattività, modalità di esecuzione (`always_on`), `uses` o `labels`. La modalità di esecuzione viene applicata entro 5 minuti; una modifica alla macchina viene applicata alla release successiva |
| `apps.remove(appId)` | `DELETE …/apps/:id` | Elimina l'App, le sue macchine e tutte le immagini delle release; restituisce `images: {released, pending}`. I suoi volumi conservano uno snapshot `final` per 7 giorni |
| `apps.start(appId)` | `POST …/apps/:id/start` | Imposta `desired_state` su `running` e preriscalda il runtime. Un'App statica risponde `409 static_app_no_runtime` |
| `apps.stop(appId)` | `POST …/apps/:id/stop` | Sospende subito il calcolo; la richiesta successiva lo riprende. Un'App statica risponde `409 static_app_no_runtime`: non ha un runtime e serve i contenuti finché ha una distribuzione attiva |
| `apps.rollback(appId, deploymentId)` | `POST …/apps/:id/rollback` | Crea una NUOVA release dall'artefatto e dalla configurazione di una release pronta e la distribuisce come un deploy (`202`). I volumi conservano i propri dati |

Gli artefatti sono l'input immutabile di una distribuzione:

| Metodo | Incapsula | Cosa fa |
|---|---|---|
| `apps.artifacts.register(input)` | `POST …/apps/artifacts` | Registra un `archive` o un `oci_image`; restituisce l'URL di caricamento per un archivio |
| `apps.artifacts.uploadArchive(bytes, options?)` | — | Registra, carica, calcola l'hash e finalizza un `.tar.gz` in un'unica chiamata |
| `apps.artifacts.finalize(artifactId, input)` | `POST …/apps/artifacts/:id/finalize` | Conferma `sha256` e `size_bytes` per un caricamento manuale |

Le release (distribuzioni) sono immutabili e numerate:

| Metodo | Incapsula | Cosa fa |
|---|---|---|
| `apps.deployments.create(appId, input)` | `POST …/apps/:id/deployments` | Avvia una release da un artefatto, una sorgente e un `config` di release facoltativo |
| `apps.deployments.list(appId)` | `GET …/apps/:id/deployments` | Elenca la cronologia delle distribuzioni |
| `apps.deployments.get(appId, deploymentId)` | `GET …/deployments/:did` | Legge una distribuzione e i suoi eventi |
| `apps.deployments.logs(appId, deploymentId, options?)` | `GET …/deployments/:did/logs` | Legge i log del runtime con un cursore |
| `apps.deployments.remove(appId, deploymentId)` | `DELETE …/deployments/:did` | Elimina una distribuzione e la sua immagine; `409` per la distribuzione attiva o una build in corso. Restituisce `image: 'released' \| 'pending' \| 'none'` |

Macchine, scalabilità ed exec (un'App server; un'App senza macchine risponde
`409 app_no_machines`):

| Metodo | Incapsula | Cosa fa |
|---|---|---|
| `apps.machines.list(appId)` | `GET …/apps/:id/machines` | Ogni macchina: `machine_id`, `process_group`, `state`, `release`, `vm`, `volume` |
| `apps.machines.start/stop/restart(appId, machineId)` | `POST …/machines/:mid/{start,stop,restart}` | Agisce su una macchina; si risolve con l'elenco delle macchine |
| `apps.machines.destroy(appId, machineId)` | `DELETE …/machines/:mid` | Distrugge una macchina. Il suo volume rimane; il suo gruppo esegue una macchina in meno |
| `apps.scale(appId, count)` | `PUT …/apps/:id/scale` | Macchine per gruppo di processi (`{ worker: 3 }`, 0–20), applicate in background (`202`). Si risolve con `scale` e `machines` |
| `apps.exec(appId, { command, machine_id?, timeout_s? })` | `POST …/apps/:id/exec` | Esegue un comando in una macchina in esecuzione (`project.app.admin`; registrato ai fini di audit). `timeout_s` 1–300, valore predefinito 60. Si risolve con `exit_code`, `stdout`, `stderr` (1 MiB ciascuno), `truncated` |

Volumi (dischi persistenti):

| Metodo | Incapsula | Cosa fa |
|---|---|---|
| `apps.volumes.list(appId)` | `GET …/apps/:id/volumes` | Ogni volume: `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` | Crea in anticipo un volume; una macchina successiva del suo gruppo con un mount di quel nome lo utilizza |
| `apps.volumes.update(appId, volumeId, { size_gb?, snapshot_retention_days? })` | `PATCH …/volumes/:vid` | Aumenta le dimensioni di un volume (non le riduce mai; massimo 100 GB) o ne modifica la conservazione (1–60 giorni) |
| `apps.volumes.destroy(appId, volumeId, confirmName)` | `DELETE …/volumes/:vid?confirm=<name>` | `project.app.admin`. Rifiutato con `409 volume_attached` mentre una macchina lo monta. Conserva uno snapshot `final` per 7 giorni |
| `apps.volumes.snapshots(appId, volumeId)` | `GET …/volumes/:vid/snapshots` | Snapshot `daily`, `manual` e `final` |
| `apps.volumes.snapshot(appId, volumeId)` | `POST …/volumes/:vid/snapshots` | Uno snapshot `manual` immediato |
| `apps.volumes.restore(appId, volumeId, snapshotId)` | `POST …/volumes/:vid/restore` | `project.app.admin`. La macchina si arresta, il volume torna allo snapshot, la macchina si avvia su di esso (`202`) |
| `apps.volumes.fork(appId, volumeId, { name, snapshot_id? })` | `POST …/volumes/:vid/fork` | Un nuovo volume non collegato nella stessa App, da uno snapshot o dai contenuti attuali |

Token di accesso (funzionalità `member_tokens`; `409 app_capability_unsupported`
quando l'App non la possiede):

| Metodo | Incapsula | Cosa fa |
|---|---|---|
| `apps.token(appId)` | `POST …/apps/:id/token` | Un token di accesso all'App valido per 15 minuti, che identifica il chiamante |

L'accesso è la policy di autorizzazione propria dell'App:

| Metodo | Incapsula | Cosa fa |
|---|---|---|
| `apps.access.get(appId)` | `GET …/apps/:id/access` | Legge la policy. Richiede `project.app.read` |
| `apps.access.update(appId, input)` | `PATCH …/apps/:id/access` | Sostituisce la policy e ne incrementa la revisione |
| `apps.access.session(appId)` | `POST …/apps/:id/access-session` | Genera un URL valido per cinque minuti che viene scambiato con un cookie limitato all'host |

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

## Distribuire un sito statico

`uploadArchive` esegue l'intero handshake dell'artefatto: registra l'artefatto,
lo verifica rispetto a `max_bytes`, invia i byte con `PUT`, calcola lo SHA-256 e
finalizza.

```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` accetta anche i campi della macchina: `cpu`, `memory_gb`,
`disk_gb`, `idle_timeout_seconds` e `always_on`. Omettili per usare i valori predefiniti.
Le App non hanno un budget mensile: i crediti dell'account pagano il calcolo. Un'App server
sempre attiva viene fatturata 24/7; una on-demand solo mentre è attiva.
`always_on: true` esegue un'App server 24/7
(websocket, loop in background); `false` la arresta dopo `idle_timeout_seconds`
e la riattiva alla richiesta successiva. Un'App statica non ha un runtime: `always_on` è
`false`.

L'API ignora `monthly_budget_usd` se un client meno recente lo invia (`200`, nessun
errore). Nessuna risposta dell'App lo include.

`uses` elenca, per slug, le App
per cui questa App può ottenere token di accesso e che può raggiungere tramite il suo mount dei binding; per
impostazione predefinita non ce ne sono e `update(appId, { uses })` sostituisce l'elenco (vedi
[App usate da un'App](/docs/it/feature-flags/apps#apps-an-app-uses-sign-in-tokens-and-bindings)).

`create` e `update` restituiscono `warnings`, che è sempre `[]`.

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

Attendi la distribuzione interrogandone periodicamente lo stato:

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

I valori di stato sono `queued`, `validating`, `building`, `provisioning`,
`checking`, `ready`, `failed` e `cancelled`.

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

## Distribuire un'immagine OCI

Registra il riferimento immutabile dell'immagine, poi dichiara il comando del processo e
la porta di destinazione pubblica:

```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` restituisce `upload: null` per un `oci_image`. Solo un `archive` ottiene un
URL di caricamento.

`CreateAppDeploymentInput` accetta anche `environment` (valori del runtime non segreti),
`secrets` (chiave del runtime associata al nome del segreto del progetto) e `config` (`AppReleaseConfigInput`, la
configurazione della release). Un `config` non valido
risponde `400 app_config_invalid` con `issues: [{ path, message }]`; un mount
su un runtime senza volumi risponde `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>

## Leggere i log del runtime

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

Ogni voce contiene `cursor`, `time`, `source` (`app`, `appd`, `caddy`) e
`line`. Una riga `caddy` è il log degli accessi: metodo, URI, stato, durata e
header della risposta. Non contiene mai gli header della richiesta, perché contengono le
credenziali del gateway (il token e l'identità del visitatore, il token di ingresso del provider).

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

## Macchine, scalabilità e volumi

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

Una macchina mantiene il proprio `machine_id` tra una release e l'altra; ogni release sostituisce il suo
box (`instance_id`). Una macchina monta un volume alla volta. Un ripristino crea una
nuova versione del volume: i contenuti sostituiti restano nella sua cronologia. Uno
snapshot di un volume montato può non includere l'ultimo secondo circa di scritture.

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

## Un backend Convex

Un backend Convex è una normale App server con un volume, creata dal
template `convex` (`beeuni apps launch --template convex --name db`; vedi
[Template `convex`](/docs/it/feature-flags/apps#template-convex-a-convex-backend)).
L'SDK non ha chiamate specifiche per Convex. Leggi la chiave amministratore dalla macchina con
`exec` e concedi a un'App UI l'accesso al backend con `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` contiene gli URL del backend: `http` (l'URL del client Convex) e
`site` (azioni HTTP).

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

## La tua App sa già chi la sta guardando

Un'App ospitata da Beeuni viene aperta da qualcuno che Beeuni **ha già autenticato**. Il
gateway delle App lo autentica prima che venga servito il primo byte, quindi la tua App non ha bisogno
di un proprio login: nessuna seconda password, nessuna schermata di consenso, nessun reindirizzamento.

Nel browser:

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

Il browser chiama `/_beeuni/api/v1/*` sull'origine della tua App, mai
`https://api.beeuni.ai/v1` direttamente: l'API non risponde alle richieste preliminari CORS provenienti
dall'origine di un'App, quindi una chiamata diretta fallisce nel browser. Il gateway inoltra ogni
chiamata all'API Beeuni come visitatore autenticato, con il token del visitatore limitato
all'App. Regole:

- Richiede `viewer_token_scope: 'api'`. Altrimenti risponde
  `403 viewer_api_disabled`.
- Agisce solo per un visitatore autenticato dal gateway (login Beeuni o un link
  di accesso). In assenza di un visitatore, risponde `401 no_viewer_identity`.
- Risponde solo a richieste provenienti dall'origine dell'App. A una richiesta cross-site
  risponde `403 cross_site_request`.
- Inoltra `/v1/*` tranne `/v1/oauth/*` e gli endpoint MCP, che rispondono
  `404`. Il cookie della tua App e il suo header `Authorization` non raggiungono mai
  l'API, e l'API non può impostare un cookie sulla tua App.

Sul server della tua App, il gateway inserisce l'identità firmata in ogni richiesta:

```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` verifica un HMAC sull'header con `BEEUNI_APP_VIEWER_SECRET`,
che Beeuni inserisce nella tua App al momento del deploy. Un header contraffatto non supera mai la verifica: il
gateway elimina qualsiasi copia fornita dal client prima dell'inoltro, e la firma viene
creata con un segreto derivato per ogni App.

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

### Quali informazioni riceve la tua App

Un'impostazione nella policy di accesso dell'App: **Settings → Access** in Beeuni,
`beeuni apps access <app> --viewer off|identity|api`, oppure `viewer_token_scope`
su `PATCH /projects/:id/apps/:appId/access`:

| Ambito | La tua App riceve |
|---|---|
| `identity` (predefinito) | Id, email, nome, immagine, ruolo nell'account e gruppi Beeuni del visitatore (id e nomi, solo nell'account di questa App), oltre a un token `profile email`. Sufficiente per mostrare a ogni persona i propri dati e limitare l'accesso per gruppo. |
| `api` | Quanto sopra, più un token che agisce **come** quella persona sull'API Beeuni, entro i limiti del suo ruolo. |
| `off` | Nulla. |

Il token non è mai la sessione Beeuni dell'utente: dura un'ora, include solo
quegli ambiti e ogni token emesso da un'App viene invalidato quando l'App viene eliminata o la sua
policy di accesso cambia.

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

### Uso del token del visitatore

- **Sul tuo server, leggilo a ogni richiesta.** Il gateway inserisce un token valido in
  `x-beeuni-app-viewer-token` in ogni richiesta inoltrata tramite proxy. Invialo come
  `Authorization: Bearer` all'API Beeuni: `createAppViewerBeeuni(request)`
  fa esattamente questo. Crea un client per ogni richiesta; non memorizzare il token.
- **Dopo una modifica alla policy di accesso, i vecchi token smettono di funzionare.** La richiesta successiva
  attraverso il gateway ne contiene uno nuovo. Nel browser, `beeuniAppViewerToken()`
  si ripristina automaticamente: quando l'API rifiuta il suo token, l'SDK rilegge
  `/_beeuni/viewer` e ripete la chiamata una volta.
- **Il ruolo del visitatore è il limite massimo.** Il token può fare esattamente ciò che
  il visitatore può fare in Beeuni, mai di più. In un progetto con autorizzazioni per gli agenti, un
  `member` necessita di un'autorizzazione per l'agente avviato dalla tua App: senza,
  `POST /projects/:id/sessions` risponde `403 no_agent_access`. Concedi ai tuoi
  visitatori l'accesso all'agente, non un ruolo più ampio.
- **Quando è presente un visitatore.** È presente quando una persona apre l'App con il proprio login Beeuni, oppure
  con un link di accesso, che la autentica come quella persona. Un'App
  `password` non ne ha: la password dimostra la conoscenza di un segreto condiviso, non l'identità di una persona. Un'App
  `public` ne ha uno solo se il visitatore arriva tramite un link di accesso;
  il semplice URL resta anonimo.
- **È una credenziale.** Beeuni esclude gli header delle richieste dal log degli accessi dell'App
  (`beeuni apps logs`). Non registrare l'header nei log del tuo codice.

Un'App servita sul **proprio dominio** (non `*.apps.beeuni.ai`) non ha un gateway
davanti: usa invece [Accedi con Beeuni](/docs/it/sdk/sign-in).

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

## Un solo membro, in ogni runtime

Beeuni garantisce l'identità della stessa persona in tre contesti: il browser interroga il gateway
(`/_beeuni/viewer`), il server dell'App legge l'header firmato e un'App che
verifica i token di autenticazione (un backend Convex o il tuo server) riceve un
token firmato da Beeuni. `readBeeuniMember` converte ciascuna di queste forme in un unico
`BeeuniMember`, e `requireBeeuniMember` decide chi può procedere. Nessun
framework e nessuna dipendenza: lo stesso codice funziona in un browser, Node, Bun, un
Worker e una funzione 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 });
```

In React, non chiamare `useConvexAuth()`: genera un'eccezione con un semplice
`ConvexProvider` (richiede `ConvexProviderWithAuth` e `beeuniBinding` non viene
verificato con quel provider). Leggi il membro autenticato con una query:

```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` restituisce `null` per un chiamante anonimo. Usa
`requireBeeuniMember` solo dove la funzione deve generare un'eccezione.

| Campo | Valore |
|---|---|
| `userId` | Id utente Beeuni. Stabile; usalo come chiave delle righe. |
| `email`, `name`, `picture` | Dal profilo del membro. `null` se sconosciuto. |
| `groups`, `groupIds` | I gruppi Beeuni del membro in questo account, per nome e per id. |
| `role` | Ruolo nell'account: `owner`, `admin` o `member`. |
| `accountId`, `projectId` | Dove risiede l'App. |

`requireBeeuniMember(claims, { groups, roles })` restituisce il membro oppure genera
`BeeuniMemberError` con `code` `unauthenticated` (nessuno, oppure un token che
non ha superato una verifica) o `forbidden` (al di fuori dei gruppi o dei ruoli). Ogni elenco significa
"uno qualsiasi di questi"; un gruppo corrisponde per nome o id; un elenco vuoto non ammette nessuno.

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

### Token di autenticazione

Un unico emittente per progetto firma i token di ogni App: `iss` =
`<API origin>/v1/projects/<projectId>`, `aud` = l'id dell'App a cui il token è
destinato, 15 minuti. Il suo set di chiavi è pubblico su `<issuer>/jwks.json`, identificato da
`<issuer>/.well-known/openid-configuration`. Ogni App contiene i valori che
verificano i suoi token: `app.auth.issuer`, `app.auth.audience`, `app.auth.jwks_uri`.
Un token contiene `sub`, `email`, `name`, `picture`, `groups`, `group_ids`,
`role`, `account_id` e `project_id`; il token di un agente contiene
`kind: 'agent'` e nessun gruppo o ruolo.

| Funzione | Cosa fa |
|---|---|
| `beeuniToken({ audience? })` | Una funzione per recuperare token (nel formato che i client realtime accettano come callback di autenticazione) per l'App stessa (predefinito) o per un'App che usa. Legge `/_beeuni/token` sull'origine dell'App stessa. Il risultato viene memorizzato nella cache fino a poco prima della scadenza; `{ forceRefreshToken: true }` lo richiede di nuovo. Restituisce `null` in assenza di un visitatore autenticato e `null` più un unico avviso nella console per un'App che non usa (`403 app_not_linked`). Non genera mai eccezioni. |
| `beeuniBinding(slug, { origin? })` | `{ url, token }` per un'App usata da questa App: `url` = `<origin>/_beeuni/apps/<slug>` (HTTP e WebSocket, stessa origine, senza CORS), `token` = `beeuniToken({ audience: slug })`. |
| `verifyBeeuniToken(bearer, { issuer?, audience?, jwks? })` | Verifica la firma ES256, la scadenza (tolleranza di 60 s), l'emittente e il destinatario. Valori predefiniti: `BEEUNI_AUTH_ISSUER`, `BEEUNI_AUTH_AUDIENCE`, `BEEUNI_AUTH_JWKS`. Beeuni inserisce tutti e tre nel runtime di ogni App server (emittente: il progetto; destinatario: l'id dell'App stessa; set di chiavi: un URI `data:`), quindi `verifyBeeuniToken(bearer)` funziona in quel contesto senza opzioni. Non impostare mai `BEEUNI_AUTH_*` in `env` di un'App server: il prefisso `BEEUNI_` è riservato e `beeuni validate` lo rifiuta. Il set di chiavi è un oggetto, testo JSON, un URI `data:` o un URL https (`auth.jwks_uri`, recuperato una volta per processo). Ogni errore genera `BeeuniMemberError`, inclusa l'assenza di un set di chiavi o di un destinatario: una sola chiave del progetto firma i token di tutte le App, quindi è `aud` a impedire l'accesso con il token di un'altra App. `audience: false` accetta un token per qualsiasi App del progetto. |
| `apps.token(appId)` | Un token che identifica il chiamante, dall'API (`POST …/apps/:id/token`). Una sessione agente ne riceve uno che identifica il suo agente. |

Un membro rimosso dal progetto o da un gruppo perde l'accesso entro la durata
di un token; il controllo di accesso dell'App lo respinge al successivo caricamento della pagina. I gruppi Beeuni richiedono
il piano Enterprise; i ruoli funzionano con tutti i piani.

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

## Gestire l'accesso

```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` riporta `password_configured`, mai la password o il suo
hash. Imposta una password con `{ mode: 'password', password }`. Ogni aggiornamento
incrementa `revision`, revocando i cookie dell'App esistenti.

<a id="types"></a>

## Tipi

Ogni tipo qui sotto viene esportato da `@beeuni/sdk`.

| Tipo | Cosa contiene |
|---|---|
| `App` | Identità, `capabilities`, `labels`, `scale`, `services` (`AppServiceSummary[]`), `machines` (`total`, `running`, `stopped`), `volumes` (`count`, `size_gb`), `config` (`AppReleaseConfig` della release in produzione, oppure `null`), `uses`, `used_by`, `auth`, `url`, `access_mode`, `access_revision`, `desired_state`, `active_deployment_id`, `hosting_type` (della release attiva: `static`, `sandbox` oppure `null` prima del primo deployment), `machine` (la dimensione del gruppo che serve la release in produzione), `idle_timeout_seconds`, `always_on`, `retained_deployments`, `warnings` (solo creazione e aggiornamento; sempre `[]`), `last_request_at`, `viewer_can_access` |
| `AppCapability` | `'deployments' \| 'rollback' \| 'preview' \| 'sleep' \| 'static' \| 'member_tokens' \| 'machines' \| 'scale' \| 'exec' \| 'volumes' \| 'snapshots'` |
| `AppReleaseConfig` · `AppReleaseConfigInput` | La configurazione risolta della release (`processes`, `services`, `mounts`, `vm`, `deploy`) · la forma definita dall'autore (`processes`, `http_service`, `services`, `mounts`, `vm`, `deploy` e le forme abbreviate `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` quando l'etichetta dell'host sarebbe troppo lunga) |
| `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'id dell'App), `jwks_uri`: i valori pubblici che verificano i token di accesso dell'App |
| `AppToken` | `token`, `expires_at` |
| `BeeuniTokenOptions` · `BeeuniTokenFetcher` · `BeeuniBinding` · `VerifyBeeuniTokenOptions` | Le funzioni di supporto per i token di accesso descritte sopra |
| `AppDeployment` | `version`, `status`, `source_kind`, `hosting_type` (`static`: servito dallo storage, senza runtime; `sandbox`: macchine), `hosting_provider`, `runtime_spec`, `build_spec`, `error_code`, `attempt_count`, `created_by`, `actor_type`, `source_session_id`, `config` (la configurazione risolta della release), `strategy`, `release_command_status` (`running`, `succeeded`, `failed` oppure `null`) |
| `AppRollback` | Il risultato con cui si risolve `apps.rollback`: `App` più la release in coda in `deployment` |
| `AppDeploymentDetail` | Un `deployment` più il suo `events`: eventi del ciclo di vita e log di build (righe `build_log` e un evento `log_truncated` quando la build ha prodotto più di 5.000 righe) |
| `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` | Una riga di log e una pagina più `next_cursor` |

`apps.list()` e `useProjectApps` restituiscono solo le App che il chiamante può aprire
in base alla policy di accesso di ciascuna App (privata: il creatore; limitata: i
membri e i gruppi elencati; progetto, password e pubblica: il team del progetto). Un responsabile del progetto
vede tutte le App, quindi un'App privata resta gestibile quando il suo creatore
lascia il progetto. `viewer_can_access` è quindi `true` per ogni App elencata. Un `get` di
un'App che il chiamante non può aprire risponde `404`, non `403`. Considera `undefined` come
sconosciuto, non come negato.

`AppAccessMode` è un'impostazione di visibilità per risorsa che si aggiunge al modello dei ruoli,
non un ruolo. `restricted` identifica utenti e gruppi, gli stessi tipi di entità usati dal
modello dei ruoli. Vedi
[Account e accesso](/docs/it/accounts#per-feature-access-settings).

`DockerfileAppSource` e `OciImageAppSource` richiedono `command`. `port` è la
porta del servizio principale: omettila solo per una release con soli worker (un `config`
con `processes` e nessun servizio). `StaticAppSource` e `BundleAppSource` non accettano
nessuno dei due.

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

## Hook React

`@beeuni/sdk/react` esporta sette hook per le App.

### useProjectApps(projectId)

Le App che il chiamante può aprire, più le relative mutazioni del ciclo di vita. Ogni mutazione
invalida l'inventario in caso di successo.

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

Restituisce i campi della query più `create`, `update`, `start`, `stop` e
`remove`.

### useAppDeployments(projectId, appId)

La cronologia immutabile dei deployment, recuperata di nuovo ogni 5 s affinché una build in esecuzione
avanzi autonomamente.

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

Entrambe le mutazioni invalidano l'elenco dei deployment e l'inventario delle App.

### useAppDeployment(projectId, appId, deploymentId)

Un deployment e i suoi eventi, incluso il log di build (`AppDeploymentDetail`). Esegue
il polling ogni 2 s finché il deployment non è `ready`, `failed` o `cancelled`. Passa
`null` come `deploymentId` per mantenerlo inattivo.

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

Una build conserva le prime 2.500 e le ultime 2.500 righe. Le righe vengono scritte in
batch, al massimo 1 s dopo essere state stampate. Un deployment non riuscito o annullato
conserva il log della build per 14 giorni.

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

La policy di accesso e una sessione di accesso di breve durata. Le due parti sono query
separate e ciascuna è facoltativa.

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

| Opzione | Valore predefinito | Usa `false` quando |
|---|---|---|
| `policy` | `true` | L'interfaccia mostra solo un'anteprima dell'App. `GET …/access` è una lettura amministrativa e risponde `403` a un chiamante senza permessi di gestione del progetto. |
| `session` | `true` | Il chiamante non può aprire l'App. Passa `app.viewer_can_access` (sempre `true` per un'App elencata). |

Una griglia di App che lascia entrambe le opzioni su `true` esegue una lettura della policy e una
creazione di sessione per ogni App, e ciascuna restituisce `403` per un membro che non può aprire
quell'App.

### useAppMachines(projectId, appId)

Le macchine di un'App, interrogate ogni 5 s, più `start`, `stop`, `restart`,
`destroy` (ciascuna accetta un `machineId`) e `scale` (accetta
`{ [group]: count }`). Ogni mutazione aggiorna le macchine e l'inventario delle App.
`appMachinesKey(projectId, appId)` è la chiave della query.

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

### useAppVolumes(projectId, appId)

I volumi di un'App, più `snapshot` (`volumeId`), `restore`
(`{ volumeId, snapshotId }`), `fork` (`{ volumeId, name, snapshotId? }`),
`extend` (`{ volumeId, sizeGb }`) e `destroy` (`{ volumeId, name }`). Ogni
mutazione aggiorna i volumi. `appVolumesKey(projectId, appId)` è la chiave
della query.

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

### useAppVolumeSnapshots(projectId, appId, volumeId)

Gli snapshot di un volume. Passa `null` come `volumeId` per mantenerla inattiva.

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

## Errori

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

Altre risposte da gestire:

- `409` per uno slug duplicato, `402` con `app_quota_exceeded` quando l'account
  ha raggiunto il limite di App e `400` con `app_machine_out_of_range` per una specifica fuori dai limiti consentiti.
- `400 app_not_found` (`slugs`) per uno slug sconosciuto in `uses`.
- `409 app_capability_unsupported` (`capability`) per una chiamata che l'App non
  offre.
- Un deployment: `400 app_config_invalid` (`issues`), `400 app_volumes_unsupported`.
  Un rollback: `409 release_not_ready`.
- Macchine ed 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`.
- Volumi: `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()` risponde con `Cache-Control: no-store`.
