Zum Inhalt springen
BeeuniBeeuni
Deutsch
Esc
↑↓navigieren↵öffnen⌘Jvorschau
Auf dieser Seite

Apps

Erstelle, deploye und steuere Beeuni Apps über das SDK und React.

Apps sind projektgebundene Bereitstellungen mit jeweils einer stabilen URL, unveränderlichen Releases, Maschinen pro Prozessgruppe und persistenten Volumes. Verzweige anhand von app.capabilities. Diese Seite behandelt die SDK-Schnittstelle: die apps-Fassade auf einem Projekt-Handle, die Aufrufe für Artefakte, Releases, Maschinen und Volumes, Anmeldetokens, die exportierten Typen und die React-Hooks.

Informationen zum Modell, zur Release-Konfiguration, zu den Quelltypen, zur CLI, zur stabilen URL und zum Aufwecken aus dem Ruhezustand findest du unter Apps.

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

Die apps-Fassade

Methode Kapselt Funktion
apps.list() GET /projects/:pid/apps Listet die Apps auf, die der Aufrufer öffnen darf (ein Projektmanager sieht alle)
apps.create(input) POST …/apps Erstellt eine App und weist ihr eine stabile URL zu
apps.get(appId) GET …/apps/:id Liest eine App aus
apps.update(appId, input) PATCH …/apps/:id Benennt sie um oder ändert Maschine, Leerlaufzeitlimit, Betriebsmodus (always_on), uses oder labels. Der Betriebsmodus wird innerhalb von 5 Minuten wirksam; eine Maschinenänderung gilt ab dem nächsten Release
apps.remove(appId) DELETE …/apps/:id Löscht die App, ihre Maschinen und alle Release-Images; gibt images: {released, pending} zurück. Ihre Volumes behalten einen final-Snapshot für 7 Tage
apps.start(appId) POST …/apps/:id/start Setzt desired_state auf running und fährt die Laufzeitumgebung hoch. Eine statische App antwortet mit 409 static_app_no_runtime
apps.stop(appId) POST …/apps/:id/stop Hält die Rechenleistung sofort an; die nächste Anfrage setzt sie fort. Eine statische App antwortet mit 409 static_app_no_runtime: Sie hat keine Laufzeitumgebung und liefert Inhalte aus, solange sie eine aktive Bereitstellung hat
apps.rollback(appId, deploymentId) POST …/apps/:id/rollback Erstellt ein NEUES Release aus dem Artefakt und der Konfiguration eines bereiten Releases und rollt es wie eine Bereitstellung aus (202). Volumes behalten ihre Daten

Artefakte sind die unveränderliche Eingabe für eine Bereitstellung:

Methode Kapselt Funktion
apps.artifacts.register(input) POST …/apps/artifacts Registriert ein archive oder ein oci_image; gibt für ein Archiv die Upload-URL zurück
apps.artifacts.uploadArchive(bytes, options?) — Registriert, lädt hoch, hasht und finalisiert ein .tar.gz in einem einzigen Aufruf
apps.artifacts.finalize(artifactId, input) POST …/apps/artifacts/:id/finalize Bestätigt sha256 und size_bytes für einen manuellen Upload

Releases (Bereitstellungen) sind unveränderlich und nummeriert:

Methode Kapselt Funktion
apps.deployments.create(appId, input) POST …/apps/:id/deployments Startet ein Release aus einem Artefakt, einer Quelle und einem optionalen Release-config
apps.deployments.list(appId) GET …/apps/:id/deployments Listet den Bereitstellungsverlauf auf
apps.deployments.get(appId, deploymentId) GET …/deployments/:did Liest eine Bereitstellung samt ihren Ereignissen aus
apps.deployments.logs(appId, deploymentId, options?) GET …/deployments/:did/logs Liest Laufzeitprotokolle mit einem Cursor aus
apps.deployments.remove(appId, deploymentId) DELETE …/deployments/:did Löscht eine Bereitstellung und ihr Image; 409 bei der aktiven Bereitstellung oder einem laufenden Build. Gibt image: 'released' | 'pending' | 'none' zurück

Maschinen, Skalierung und exec (eine Server-App; eine App ohne Maschinen antwortet mit 409 app_no_machines):

Methode Kapselt Funktion
apps.machines.list(appId) GET …/apps/:id/machines Jede Maschine: machine_id, process_group, state, release, vm, volume
apps.machines.start/stop/restart(appId, machineId) POST …/machines/:mid/{start,stop,restart} Führt eine Aktion auf einer Maschine aus; liefert die Maschinenliste zurück
apps.machines.destroy(appId, machineId) DELETE …/machines/:mid Zerstört eine Maschine. Ihr Volume bleibt erhalten; ihre Gruppe betreibt eine Maschine weniger
apps.scale(appId, count) PUT …/apps/:id/scale Maschinen pro Prozessgruppe ({ worker: 3 }, 0–20), im Hintergrund angewendet (202). Liefert scale und machines zurück
apps.exec(appId, { command, machine_id?, timeout_s? }) POST …/apps/:id/exec Führt einen Befehl in einer laufenden Maschine aus (project.app.admin; auditprotokolliert). timeout_s 1–300, Standard 60. Liefert exit_code, stdout, stderr (jeweils 1 MiB), truncated zurück

Volumes (persistente Datenträger):

Methode Kapselt Funktion
apps.volumes.list(appId) GET …/apps/:id/volumes Jedes 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 Erstellt ein Volume vorab; eine spätere Maschine seiner Gruppe mit einem Mount dieses Namens übernimmt es
apps.volumes.update(appId, volumeId, { size_gb?, snapshot_retention_days? }) PATCH …/volumes/:vid Vergrößert ein Volume (verkleinert es nie; höchstens 100 GB) oder ändert seine Aufbewahrungsdauer (1–60 Tage)
apps.volumes.destroy(appId, volumeId, confirmName) DELETE …/volumes/:vid?confirm=<name> project.app.admin. Wird mit 409 volume_attached abgelehnt, solange eine Maschine es eingebunden hat. Behält einen final-Snapshot für 7 Tage
apps.volumes.snapshots(appId, volumeId) GET …/volumes/:vid/snapshots daily-, manual- und final-Snapshots
apps.volumes.snapshot(appId, volumeId) POST …/volumes/:vid/snapshots Ein manual-Snapshot jetzt
apps.volumes.restore(appId, volumeId, snapshotId) POST …/volumes/:vid/restore project.app.admin. Die Maschine stoppt, das Volume wird auf den Snapshot zurückgesetzt, die Maschine startet darauf (202)
apps.volumes.fork(appId, volumeId, { name, snapshot_id? }) POST …/volumes/:vid/fork Ein neues, nicht angehängtes Volume in derselben App, aus einem Snapshot oder dem aktuellen Inhalt

Anmeldetokens (Capability member_tokens; 409 app_capability_unsupported, wenn sie der App fehlt):

Methode Kapselt Funktion
apps.token(appId) POST …/apps/:id/token Ein 15 Minuten gültiges Anmeldetoken für die App, das den Aufrufer benennt

Access ist die eigene Autorisierungsrichtlinie der App:

Methode Kapselt Funktion
apps.access.get(appId) GET …/apps/:id/access Liest die Richtlinie aus. Erfordert project.app.read
apps.access.update(appId, input) PATCH …/apps/:id/access Ersetzt die Richtlinie und erhöht ihre Revision
apps.access.session(appId) POST …/apps/:id/access-session Erstellt eine fünf Minuten gültige URL, die gegen ein Host-only-Cookie eingetauscht wird

Eine statische Website bereitstellen

uploadArchive übernimmt den gesamten Artefakt-Handshake: Es registriert das Artefakt, prüft es gegen max_bytes, überträgt die Bytes per PUT, berechnet den SHA-256-Hash und finalisiert es.

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 akzeptiert auch die Maschinenfelder: cpu, memory_gb, disk_gb, idle_timeout_seconds und always_on. Lass sie weg, um die Standardwerte zu verwenden. Apps haben kein monatliches Budget: Kontoguthaben bezahlt die Rechenleistung. Eine dauerhaft aktive Server-App wird rund um die Uhr abgerechnet; eine bedarfsgesteuerte nur, solange sie aktiv ist. always_on: true betreibt eine Server-App rund um die Uhr (WebSockets, Hintergrundschleifen); false stoppt sie nach idle_timeout_seconds und weckt sie bei der nächsten Anfrage auf. Eine statische App hat keine Laufzeitumgebung: always_on ist false.

Die API ignoriert monthly_budget_usd, wenn ein älterer Client es sendet (200, kein Fehler). Keine App-Antwort enthält es.

uses listet anhand ihres Slugs die Apps auf, für die diese App Anmeldetokens erhalten und die sie über ihren Bindings-Mount erreichen darf; die Standardeinstellung ist keine, und update(appId, { uses }) ersetzt die Liste (siehe Apps, die eine App verwendet).

create und update geben warnings zurück, das immer [] ist.

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)

Warte auf die Bereitstellung, indem du ihren Status regelmäßig abfragst:

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

Die Statuswerte sind queued, validating, building, provisioning, checking, ready, failed und cancelled.

Ein OCI-Image bereitstellen

Registriere die unveränderliche Image-Referenz und deklariere anschließend den Prozessbefehl und den öffentlichen Zielport:

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 gibt für ein oci_image den Wert upload: null zurück. Nur ein archive erhält eine Upload-URL.

CreateAppDeploymentInput akzeptiert außerdem environment (nicht geheime Laufzeitwerte), secrets (Laufzeitschlüssel zu Projekt-Secret-Namen) und config (AppReleaseConfigInput, die Release-Konfiguration). Ein ungültiges config antwortet mit 400 app_config_invalid und issues: [{ path, message }]; ein Mount in einer Laufzeitumgebung ohne Volumes antwortet mit 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' },
  },
});

Laufzeitprotokolle auslesen

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

Jeder Eintrag enthält cursor, time, source (app, appd, caddy) und line. Eine caddy-Zeile ist das Zugriffsprotokoll: Methode, URI, Status, Dauer und Response-Header. Sie enthält niemals Request-Header, da diese die Zugangsdaten des Gates enthalten (das Viewer-Token und die Identität, das Ingress-Token des Providers).

Maschinen, Skalierung und 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' });

Eine Maschine behält ihr machine_id über Releases hinweg; jedes Release ersetzt ihre Box (instance_id). Eine Maschine bindet jeweils ein Volume ein. Eine Wiederherstellung ist eine neue Version des Volumes: Die ersetzten Inhalte bleiben in dessen Verlauf erhalten. Ein Snapshot eines eingebundenen Volumes kann die letzten etwa 1 s an Schreibvorgängen verpassen.

Ein Convex-Backend

Ein Convex-Backend ist eine gewöhnliche Server-App mit einem Volume, erstellt aus der Vorlage convex (beeuni apps launch --template convex --name db; siehe Vorlage convex). Das SDK verfügt über keinen Convex-spezifischen Aufruf. Lies den Admin-Schlüssel mit exec aus der Maschine aus und gib einer UI-App mit uses Zugriff auf das Backend:

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 enthält die URLs des Backends: http (die Convex-Client-URL) und site (HTTP-Aktionen).

Deine App weiß bereits, wer sie betrachtet

Eine von Beeuni gehostete App wird von jemandem geöffnet, der sich bei Beeuni bereits angemeldet hat. Das Apps-Gate authentifiziert die Person, bevor dein erstes Byte ausgeliefert wird. Deine App benötigt daher keine eigene Anmeldung – kein zweites Passwort, keinen Zustimmungsbildschirm und keine Weiterleitung.

Im Browser:

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

Der Browser ruft /_beeuni/api/v1/* über den eigenen Origin deiner App auf, niemals https://api.beeuni.ai/v1 direkt: Die API beantwortet keine CORS-Preflight-Anfrage von einem App-Origin, daher schlägt ein direkter Aufruf im Browser fehl. Das Gate leitet jeden Aufruf als der angemeldete Betrachter mit dessen App-bezogenem Token an die Beeuni-API weiter. Regeln:

  • Es benötigt viewer_token_scope: 'api'. Andernfalls antwortet es mit 403 viewer_api_disabled.
  • Es funktioniert nur für einen Betrachter, den das Gate angemeldet hat (Beeuni-Login oder Zugriffslink). Ohne einen solchen antwortet es mit 401 no_viewer_identity.
  • Es beantwortet eine Anfrage nur vom eigenen Origin der App. Eine Cross-Site-Anfrage beantwortet es mit 403 cross_site_request.
  • Es leitet /v1/* weiter, mit Ausnahme von /v1/oauth/* und MCP-Endpunkten, die mit 404 antworten. Das Cookie deiner App und ihr eigener Authorization-Header erreichen die API nie, und die API kann kein Cookie für deine App setzen.

Auf dem Server deiner App signiert das Gate die Identität in jede Anfrage:

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 überprüft mit BEEUNI_APP_VIEWER_SECRET einen HMAC über den Header, den Beeuni beim Deployment in deine App einschleust. Ein gefälschter Header wird niemals akzeptiert: Das Gate löscht jede vom Client gelieferte Kopie vor der Weiterleitung, und die Signatur wird mit einem pro App abgeleiteten Geheimnis erstellt.

Wie viele Informationen deine App erhält

Eine Einstellung in der Zugriffsrichtlinie der App – Settings → Access in Beeuni, beeuni apps access <app> --viewer off|identity|api oder viewer_token_scope in PATCH /projects/:id/apps/:appId/access:

Bereich Deine App erhält
identity (Standard) Die ID, E-Mail-Adresse, den Namen, das Bild und die Kontorolle des Betrachters sowie seine Beeuni-Gruppen (IDs und Namen, nur in diesem Konto der App) und ein profile email-Token. Ausreichend, um jeder Person ihre eigenen Daten zu zeigen und den Zugriff nach Gruppe zu steuern.
api Das Obige sowie ein Token, das als diese Person auf der Beeuni-API agiert – begrenzt durch ihre eigene Rolle.
off Nichts.

Das Token ist niemals die Beeuni-Sitzung des Benutzers: Es ist eine Stunde gültig, enthält nur diese Bereiche, und jedes von einer App ausgestellte Token verfällt, wenn die App gelöscht oder ihre Zugriffsrichtlinie geändert wird.

Das Viewer-Token verwenden

  • Lies es auf deinem Server für jede Anfrage aus. Das Gate legt bei jeder weitergeleiteten Anfrage ein gültiges Token in x-beeuni-app-viewer-token ab. Sende es als Authorization: Bearer an die Beeuni-API – createAppViewerBeeuni(request) erledigt genau das. Erstelle einen Client pro Anfrage; speichere das Token nicht.
  • Nach einer Änderung der Zugriffsrichtlinie funktionieren alte Tokens nicht mehr. Die nächste Anfrage durch das Gate enthält ein neues Token. Im Browser behebt beeuniAppViewerToken() das automatisch: Wenn die API das Token ablehnt, liest das SDK /_beeuni/viewer erneut aus und wiederholt den Aufruf einmal.
  • Die Rolle des Betrachters ist die Obergrenze. Das Token kann genau das tun, was der Betrachter in Beeuni tun kann, niemals mehr. In einem Projekt mit Agent-Berechtigungen benötigt ein member eine Freigabe für den Agenten, den deine App startet: Ohne eine solche antwortet POST /projects/:id/sessions mit 403 no_agent_access. Gewähre deinen Betrachtern den Agenten, nicht eine umfassendere Rolle.
  • Wer einen Betrachter hat. Eine Person, die die App mit ihrem Beeuni-Login oder mit einem Zugriffslink öffnet, der sie als diese Person anmeldet, hat einen. Eine password-App hat keinen: Das Passwort beweist ein gemeinsames Geheimnis, nicht die Identität einer Person. Eine public-App hat nur für einen Besucher einen, der über einen Zugriffslink gekommen ist; die einfache URL bleibt anonym.
  • Es ist eine Zugangsdaten. Beeuni lässt Request-Header aus dem Zugriffsprotokoll der App weg (beeuni apps logs). Protokolliere den Header nicht in deinem eigenen Code.

Eine App, die unter ihrer eigenen Domain (nicht *.apps.beeuni.ai) bereitgestellt wird, hat kein Gate davor – verwende dort stattdessen Sign in with Beeuni.

Ein Mitglied, in jeder Laufzeitumgebung

Beeuni bestätigt dieselbe Person an drei Stellen: Der Browser fragt das Gate (/_beeuni/viewer), der Server der App liest den signierten Header, und eine App, die Anmeldetokens überprüft (ein Convex-Backend oder dein eigener Server), erhält ein von Beeuni signiertes Token. readBeeuniMember wandelt jede dieser Formen in ein einziges BeeuniMember um, und requireBeeuniMember entscheidet, wer fortfahren darf. Kein Framework und keine Abhängigkeit: Derselbe Code läuft im Browser, in Node, Bun, einem Worker und einer Convex-Funktion.

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

Rufe in React nicht useConvexAuth() auf: Unter einem einfachen ConvexProvider löst es einen Fehler aus (es benötigt ConvexProviderWithAuth, und beeuniBinding wird mit diesem Provider nicht überprüft). Lies das angemeldete Mitglied mit einer Abfrage aus:

// 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 gibt für einen anonymen Aufrufer null zurück. Verwende requireBeeuniMember nur dort, wo die Funktion einen Fehler auslösen muss.

Feld Wert
userId Beeuni-Benutzer-ID. Stabil; verwende sie als Schlüssel für Zeilen.
email, name, picture Aus dem Profil des Mitglieds. null, wenn unbekannt.
groups, groupIds Die Beeuni-Gruppen des Mitglieds in diesem Konto, nach Name und ID.
role Kontorolle: owner, admin oder member.
accountId, projectId Wo die App lebt.

requireBeeuniMember(claims, { groups, roles }) gibt das Mitglied zurück oder löst BeeuniMemberError mit code unauthenticated aus (niemand oder ein Token, das eine Prüfung nicht bestanden hat) oder forbidden (außerhalb der Gruppen oder Rollen). Jede Liste bedeutet „beliebiges Element davon“; eine Gruppe stimmt nach Name oder ID überein; eine leere Liste lässt niemanden zu.

Anmeldetokens

Ein Aussteller pro Projekt signiert die Tokens jeder App: iss = <API origin>/v1/projects/<projectId>, aud = die ID der App, für die das Token bestimmt ist, 15 Minuten gültig. Sein Schlüsselsatz ist öffentlich unter <issuer>/jwks.json und wird durch <issuer>/.well-known/openid-configuration benannt. Jede App enthält die Werte, die ihre Tokens überprüfen: app.auth.issuer, app.auth.audience, app.auth.jwks_uri. Ein Token enthält sub, email, name, picture, groups, group_ids, role, account_id und project_id; das Token eines Agenten enthält kind: 'agent' und keine Gruppen oder Rolle.

Funktion Zweck
beeuniToken({ audience? }) Ein Token-Abrufer (in der Form, die Echtzeit-Clients als Auth-Callback erwarten) für die App selbst (Standard) oder eine von ihr verwendete App. Liest /_beeuni/token auf der eigenen Origin der App. Wird bis kurz vor Ablauf zwischengespeichert; { forceRefreshToken: true } fragt erneut ab. Liefert null ohne angemeldeten Betrachter und null plus eine Konsolenwarnung für eine App, die sie nicht verwendet (403 app_not_linked). Wirft nie einen Fehler.
beeuniBinding(slug, { origin? }) { url, token } für eine App, die diese App verwendet: url = <origin>/_beeuni/apps/<slug> (HTTP und WebSocket, gleiche Origin, kein CORS), token = beeuniToken({ audience: slug }).
verifyBeeuniToken(bearer, { issuer?, audience?, jwks? }) Prüft die ES256-Signatur, den Ablauf (60 s Zeittoleranz), den Aussteller und die Zielgruppe. Standardwerte: BEEUNI_AUTH_ISSUER, BEEUNI_AUTH_AUDIENCE, BEEUNI_AUTH_JWKS. Beeuni injiziert alle drei in die Laufzeit jeder Server-App (Aussteller: das Projekt; Zielgruppe: die eigene ID der App; Schlüsselsatz: eine data:-URI), sodass verifyBeeuniToken(bearer) dort ohne Optionen funktioniert. Setze niemals BEEUNI_AUTH_* in env einer Server-App: Das Präfix BEEUNI_ ist reserviert und beeuni validate lehnt es ab. Der Schlüsselsatz ist ein Objekt, JSON-Text, eine data:-URI oder eine HTTPS-URL (auth.jwks_uri, einmal pro Prozess abgerufen). Jeder Fehler wirft BeeuniMemberError, auch ein fehlender Schlüsselsatz oder eine fehlende Zielgruppe: Ein Projektschlüssel signiert die Tokens aller Apps, daher hält aud das Token einer anderen App fern. audience: false akzeptiert ein Token für jede App des Projekts.
apps.token(appId) Ein Token aus der API (POST …/apps/:id/token), das den Aufrufer benennt. Eine Agentensitzung erhält eines, das ihren Agenten benennt.

Ein Mitglied, das du aus dem Projekt oder einer Gruppe entfernst, verliert den Zugriff innerhalb einer Token-Lebensdauer; die App-Zugriffskontrolle weist es beim nächsten Laden der Seite ab. Beeuni-Gruppen erfordern den Enterprise-Tarif; Rollen funktionieren in jedem Tarif.

Zugriff verwalten

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 meldet password_configured, niemals das Passwort oder dessen Hash. Setze ein Passwort mit { mode: 'password', password }. Jede Aktualisierung erhöht revision und widerruft damit bestehende App-Cookies.

Typen

Alle folgenden Typen werden aus @beeuni/sdk exportiert.

Typ Inhalt
App Identität, capabilities, labels, scale, services (AppServiceSummary[]), machines (total, running, stopped), volumes (count, size_gb), config (AppReleaseConfig des Live-Releases oder null), uses, used_by, auth, url, access_mode, access_revision, desired_state, active_deployment_id, hosting_type (des aktiven Releases: static, sandbox oder null vor dem ersten Deployment), machine (die Größe der bereitstellenden Gruppe im Live-Release), idle_timeout_seconds, always_on, retained_deployments, warnings (nur beim Erstellen und Aktualisieren; immer []), last_request_at, viewer_can_access
AppCapability 'deployments' | 'rollback' | 'preview' | 'sleep' | 'static' | 'member_tokens' | 'machines' | 'scale' | 'exec' | 'volumes' | 'snapshots'
AppReleaseConfig · AppReleaseConfigInput Die aufgelöste Release-Konfiguration (processes, services, mounts, vm, deploy) · die verfasste Form (processes, http_service, services, mounts, vm, deploy und die Kurzformen 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, wenn das Host-Label zu lang wäre)
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 (die App-ID), jwks_uri: die öffentlichen Werte zur Überprüfung der Anmeldetokens der App
AppToken token, expires_at
BeeuniTokenOptions · BeeuniTokenFetcher · BeeuniBinding · VerifyBeeuniTokenOptions Die obigen Hilfsfunktionen für Anmeldetokens
AppDeployment version, status, source_kind, hosting_type (static: aus dem Speicher bereitgestellt, keine Laufzeit; sandbox: Maschinen), hosting_provider, runtime_spec, build_spec, error_code, attempt_count, created_by, actor_type, source_session_id, config (die aufgelöste Konfiguration des Releases), strategy, release_command_status (running, succeeded, failed oder null)
AppRollback Der Wert, mit dem apps.rollback aufgelöst wird: App plus das eingereihte Release in deployment
AppDeploymentDetail Ein deployment plus dessen events: Lebenszyklusereignisse und das Build-Log (build_log-Zeilen und ein log_truncated-Ereignis, wenn der Build mehr als 5.000 Zeilen ausgegeben hat)
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 Eine Log-Zeile und eine Seite plus next_cursor

apps.list() und useProjectApps geben nur die Apps zurück, die der Aufrufer gemäß der Zugriffsrichtlinie der jeweiligen App öffnen darf (privat: der Ersteller; eingeschränkt: die aufgeführten Mitglieder und Gruppen; Projekt, Passwort und öffentlich: das Projektteam). Ein Projektmanager sieht jede App, sodass eine private App verwaltbar bleibt, wenn ihr Ersteller das Projekt verlässt. viewer_can_access ist daher bei jeder aufgeführten App true. Ein get einer App, die der Aufrufer nicht öffnen darf, antwortet mit 404, nicht mit 403. Behandle undefined als unbekannt, nicht als verweigert.

AppAccessMode ist eine ressourcenspezifische Sichtbarkeitseinstellung zusätzlich zum Rollenmodell, keine Rolle. restricted benennt Benutzer und Gruppen — dieselben Prinzipaltypen, die das Rollenmodell verwendet. Siehe Konten & Zugriff.

DockerfileAppSource und OciImageAppSource erfordern command. port ist der Port des primären Dienstes: Lass ihn nur bei einem reinen Worker-Release weg (einem config mit processes und ohne Dienst). StaticAppSource und BundleAppSource nehmen keines von beiden entgegen.

React-Hooks

@beeuni/sdk/react exportiert sieben Hooks für Apps.

useProjectApps(projectId)

Die Apps, die der Aufrufer öffnen darf, plus ihre Lebenszyklus-Mutationen. Jede Mutation invalidiert bei Erfolg den Bestand.

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

Der Hook gibt die Abfragefelder plus create, update, start, stop und remove zurück.

useAppDeployments(projectId, appId)

Der unveränderliche Deployment-Verlauf, der alle 5 s erneut abgerufen wird, damit ein laufender Build von selbst fortschreitet.

const deployments = useAppDeployments(projectId, appId);

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

Beide Mutationen invalidieren die Deployment-Liste und den App-Bestand.

useAppDeployment(projectId, appId, deploymentId)

Ein Deployment und seine Ereignisse, einschließlich Build-Log (AppDeploymentDetail). Der Hook fragt alle 2 s ab, bis das Deployment ready, failed oder cancelled ist. Übergib null als deploymentId, damit er inaktiv bleibt.

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

Ein Build behält seine ersten 2.500 und seine letzten 2.500 Zeilen. Zeilen werden in Batches geschrieben, spätestens 1 s nach ihrer Ausgabe. Bei einem fehlgeschlagenen oder abgebrochenen Deployment wird das Build-Log 14 Tage lang aufbewahrt.

useAppAccess(projectId, appId, options?)

Die Zugriffsrichtlinie und eine kurzlebige Zugriffssitzung. Beide Teile sind separate Abfragen und jeweils optional.

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 Standard false verwenden, wenn
policy true Die Oberfläche nur eine Vorschau der App zeigt. GET …/access ist ein administrativer Lesezugriff und antwortet mit 403 für einen Aufrufer ohne Projektmanager-Berechtigungen.
session true Der Aufrufer die App nicht öffnen darf. app.viewer_can_access übergeben (für eine aufgelistete App immer true).

Ein Raster mit Apps, das beide Optionen auf true belässt, löst pro App einen Lesezugriff auf die Richtlinie und die Erstellung einer Sitzung aus. Beide ergeben für ein Mitglied, das diese App nicht öffnen darf, jeweils 403.

useAppMachines(projectId, appId)

Die Maschinen einer App, alle 5 s abgefragt, sowie start, stop, restart, destroy (jeweils mit einem machineId) und scale (mit { [group]: count }). Jede Mutation aktualisiert die Maschinen und das App- Inventar. appMachinesKey(projectId, appId) ist der Abfrageschlüssel.

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

useAppVolumes(projectId, appId)

Die Volumes einer App sowie snapshot (volumeId), restore ({ volumeId, snapshotId }), fork ({ volumeId, name, snapshotId? }), extend ({ volumeId, sizeGb }) und destroy ({ volumeId, name }). Jede Mutation aktualisiert die Volumes. appVolumesKey(projectId, appId) ist der Abfrage- schlüssel.

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

useAppVolumeSnapshots(projectId, appId, volumeId)

Die Snapshots eines Volumes. null als volumeId übergeben, damit die Abfrage inaktiv bleibt.

Fehler

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

Weitere Antworten, die Sie behandeln sollten:

  • 409 für einen doppelten Slug, 402 mit app_quota_exceeded, wenn das Konto sein App-Limit erreicht hat, und 400 mit app_machine_out_of_range für eine Spezifikation außerhalb der zulässigen Grenzen.
  • 400 app_not_found (slugs) für einen unbekannten Slug in uses.
  • 409 app_capability_unsupported (capability) für einen Aufruf, den die App nicht anbietet.
  • Ein Deployment: 400 app_config_invalid (issues), 400 app_volumes_unsupported. Ein Rollback: 409 release_not_ready.
  • Maschinen und 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() antwortet mit Cache-Control: no-store.

War diese Seite hilfreich?