Applications
Créez, déployez et contrôlez les applications Beeuni depuis le SDK et depuis React.
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.
const apps = beeuni.project(projectId).apps;
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 |
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.
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).
create et update renvoient warnings, qui vaut toujours [].
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 :
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.
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 :
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.
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' },
},
});
Lire les journaux d’exécution
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).
Machines, mise à l’échelle et volumes
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.
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).
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 :
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).
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 :
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épond403 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épondent404. Le cookie de votre App et son propre en-têteAuthorizationn’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 :
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.
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.
Utiliser le jeton du visiteur
- Sur votre serveur, lisez-le à chaque requête. La passerelle place un jeton valide dans
x-beeuni-app-viewer-tokensur chaque requête transmise par proxy. Envoyez-le sous la formeAuthorization: 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/vieweret 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
membera besoin d’une autorisation sur l’agent que votre App démarre : sans celle-ci,POST /projects/:id/sessionsrépond403 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
passwordn’en a pas : le mot de passe prouve la connaissance d’un secret partagé, pas une identité. Une Apppublicn’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.
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.
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 :
// convex/members.ts
export const me = query({ args: {}, handler: async (ctx) => readBeeuniMember(await ctx.auth.getUserIdentity()) });
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.
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.
Gérer l’accès
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.
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.
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.
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.
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.
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.
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.
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.
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.
Erreurs
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 :
409pour un slug en double,402avecapp_quota_exceededlorsque le compte a atteint sa limite d’Apps, et400avecapp_machine_out_of_rangepour une spécification hors limites.400 app_not_found(slugs) pour un slug inconnu dansuses.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.