Aller au contenu
BeeuniBeeuni
Français
Esc
↑↓naviguer↵ouvrir⌘Japerçu
Sur cette page

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é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 :

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-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.

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 :

  • 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.

Cette page vous a-t-elle été utile ?