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 mit403 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 mit404antworten. Das Cookie deiner App und ihr eigenerAuthorization-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-tokenab. Sende es alsAuthorization: Beareran 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/viewererneut 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
membereine Freigabe für den Agenten, den deine App startet: Ohne eine solche antwortetPOST /projects/:id/sessionsmit403 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. Einepublic-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:
409für einen doppelten Slug,402mitapp_quota_exceeded, wenn das Konto sein App-Limit erreicht hat, und400mitapp_machine_out_of_rangefür eine Spezifikation außerhalb der zulässigen Grenzen.400 app_not_found(slugs) für einen unbekannten Slug inuses.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.