# Апликације

Креирајте, постављајте и контролишите Beeuni апликације из SDK-а и из React-а.

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

Апликације су поставке у оквиру пројекта, свака са једним сталним URL-ом,
непроменљивим издањима, машинама по групи процеса и трајним волуменима. Гранајте логику према `app.capabilities`. Ова страница покрива SDK интерфејс:
фасаду `apps` на објекту пројекта, позиве за артефакте, издања, машине и волумене,
токене за пријаву, извезене типове и React куке.

За модел, конфигурацију издања, врсте извора, CLI, стални URL
и понашање при покретању из мировања, прочитајте [Апликације](/docs/sr/feature-flags/apps).

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

> **Apps је заставица функције**
>
> Свака Apps рута одговара са `403` и `{ error, code: "feature_disabled", feature: "apps" }`
> док пројекат не укључи Apps. Користите `isFeatureDisabledError(error)` за гранање
> на основу тога. Погледајте [Заставице функција](/docs/sr/feature-flags).

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

## Фасада apps

| Метод | Обавија | Шта ради |
|---|---|---|
| `apps.list()` | `GET /projects/:pid/apps` | Наводи апликације које позивалац може да отвори (менаџер пројекта види све) |
| `apps.create(input)` | `POST …/apps` | Креира апликацију и додељује јој стални URL |
| `apps.get(appId)` | `GET …/apps/:id` | Чита једну апликацију |
| `apps.update(appId, input)` | `PATCH …/apps/:id` | Преименује је или мења машину, време мировања до заустављања, режим рада (`always_on`), `uses` или `labels`. Режим рада се примењује у року од 5 минута; промена машине се примењује на следеће издање |
| `apps.remove(appId)` | `DELETE …/apps/:id` | Брише апликацију, њене машине и све слике издања; враћа `images: {released, pending}`. Њени волумени задржавају снимак `final` 7 дана |
| `apps.start(appId)` | `POST …/apps/:id/start` | Поставља `desired_state` на `running` и припрема извршно окружење. Статичка апликација одговара са `409 static_app_no_runtime` |
| `apps.stop(appId)` | `POST …/apps/:id/stop` | Одмах обуставља извршавање; следећи захтев га наставља. Статичка апликација одговара са `409 static_app_no_runtime`: нема извршно окружење и служи садржај док има активну поставку |
| `apps.rollback(appId, deploymentId)` | `POST …/apps/:id/rollback` | Креира НОВО издање из артефакта и конфигурације спремног издања и пушта га у рад као при постављању (`202`). Волумени задржавају своје податке |

Артефакти су непроменљиви улаз за поставку:

| Метод | Обавија | Шта ради |
|---|---|---|
| `apps.artifacts.register(input)` | `POST …/apps/artifacts` | Региструје `archive` или `oci_image`; враћа URL за отпремање архиве |
| `apps.artifacts.uploadArchive(bytes, options?)` | — | Региструје, отпрема, хешира и финализује један `.tar.gz` у једном позиву |
| `apps.artifacts.finalize(artifactId, input)` | `POST …/apps/artifacts/:id/finalize` | Потврђује `sha256` и `size_bytes` за ручно отпремање |

Издања (поставке) су непроменљива и нумерисана:

| Метод | Обавија | Шта ради |
|---|---|---|
| `apps.deployments.create(appId, input)` | `POST …/apps/:id/deployments` | Покреће издање из артефакта, извора и опционог `config` издања |
| `apps.deployments.list(appId)` | `GET …/apps/:id/deployments` | Наводи историју постављања |
| `apps.deployments.get(appId, deploymentId)` | `GET …/deployments/:did` | Чита једну поставку и њене догађаје |
| `apps.deployments.logs(appId, deploymentId, options?)` | `GET …/deployments/:did/logs` | Чита евиденције извршног окружења помоћу курсора |
| `apps.deployments.remove(appId, deploymentId)` | `DELETE …/deployments/:did` | Брише једну поставку и њену слику; `409` за активну поставку или изградњу у току. Враћа `image: 'released' \| 'pending' \| 'none'` |

Машине, скалирање и exec (серверска апликација; апликација без машина одговара са
`409 app_no_machines`):

| Метод | Обавија | Шта ради |
|---|---|---|
| `apps.machines.list(appId)` | `GET …/apps/:id/machines` | Свака машина: `machine_id`, `process_group`, `state`, `release`, `vm`, `volume` |
| `apps.machines.start/stop/restart(appId, machineId)` | `POST …/machines/:mid/{start,stop,restart}` | Делује на једну машину; по завршетку враћа листу машина |
| `apps.machines.destroy(appId, machineId)` | `DELETE …/machines/:mid` | Уништава једну машину. Њен волумен остаје; њена група ради са једном машином мање |
| `apps.scale(appId, count)` | `PUT …/apps/:id/scale` | Машине по групи процеса (`{ worker: 3 }`, 0–20), примењује се у позадини (`202`). По завршетку враћа `scale` и `machines` |
| `apps.exec(appId, { command, machine_id?, timeout_s? })` | `POST …/apps/:id/exec` | Извршава једну команду у покренутој машини (`project.app.admin`; бележи се у ревизијској евиденцији). `timeout_s` 1–300, подразумевано 60. По завршетку враћа `exit_code`, `stdout`, `stderr` (по 1 MiB), `truncated` |

Волумени (трајни дискови):

| Метод | Обавија | Шта ради |
|---|---|---|
| `apps.volumes.list(appId)` | `GET …/apps/:id/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` | Унапред креира волумен; каснија машина из његове групе са тачком монтирања тог имена преузима га |
| `apps.volumes.update(appId, volumeId, { size_gb?, snapshot_retention_days? })` | `PATCH …/volumes/:vid` | Повећава волумен (никада га не смањује; највише 100 GB) или мења период задржавања (1–60 дана) |
| `apps.volumes.destroy(appId, volumeId, confirmName)` | `DELETE …/volumes/:vid?confirm=<name>` | `project.app.admin`. Одбија се са `409 volume_attached` док га машина користи као монтирани волумен. Задржава снимак `final` 7 дана |
| `apps.volumes.snapshots(appId, volumeId)` | `GET …/volumes/:vid/snapshots` | Снимци `daily`, `manual` и `final` |
| `apps.volumes.snapshot(appId, volumeId)` | `POST …/volumes/:vid/snapshots` | Снимак `manual` одмах |
| `apps.volumes.restore(appId, volumeId, snapshotId)` | `POST …/volumes/:vid/restore` | `project.app.admin`. Машина се зауставља, волумен се враћа на снимак, машина се покреће на њему (`202`) |
| `apps.volumes.fork(appId, volumeId, { name, snapshot_id? })` | `POST …/volumes/:vid/fork` | Нов, неповезан волумен у истој апликацији, из снимка или тренутног садржаја |

Токени за пријаву (могућност `member_tokens`; `409 app_capability_unsupported`
када је апликација нема):

| Метод | Обавија | Шта ради |
|---|---|---|
| `apps.token(appId)` | `POST …/apps/:id/token` | Токен за пријаву у апликацију који важи 15 минута и идентификује позиваоца |

Приступ је сопствена политика ауторизације апликације:

| Метод | Обавија | Шта ради |
|---|---|---|
| `apps.access.get(appId)` | `GET …/apps/:id/access` | Чита политику. Захтева `project.app.read` |
| `apps.access.update(appId, input)` | `PATCH …/apps/:id/access` | Замењује политику и повећава њену ревизију |
| `apps.access.session(appId)` | `POST …/apps/:id/access-session` | Генерише URL који важи пет минута и размењује се за колачић ограничен на хост |

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

## Постављање статичког сајта

`uploadArchive` обавља цео поступак за артефакт: региструје артефакт,
проверава га према `max_bytes`, отпрема бајтове помоћу `PUT`, израчунава SHA-256 и
финализује га.

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

const app = await apps.create({ slug: 'docs', name: 'Docs' });
const artifact = await apps.artifacts.uploadArchive(tarGzBytes, {
  onProgress: (uploaded, total) => console.log(`${uploaded}/${total}`),
});

const deployment = await apps.deployments.create(app.app_id, {
  artifact_id: artifact.artifact_id,
  source: { kind: 'static', spa: true },
});

console.log(app.url, deployment.status); // https://…apps.beeuni.ai  queued
```

`create` прихвата и поља машине: `cpu`, `memory_gb`,
`disk_gb`, `idle_timeout_seconds` и `always_on`. Изоставите их за подразумеване вредности.
Апликације немају месечни буџет: кредити налога плаћају рачунарске ресурсе. Стално укључена
серверска апликација наплаћује се 24/7; она која ради на захтев само док је активна.
`always_on: true` покреће серверску апликацију 24/7
(websockets, позадинске петље); `false` је зауставља после `idle_timeout_seconds`
и покреће је при следећем захтеву. Статичка апликација нема извршно окружење: `always_on` је
`false`.

API игнорише `monthly_budget_usd` ако га старији клијент пошаље (`200`, без
грешке). Ниједан одговор апликације га не садржи.

`uses` наводи апликације, по slug-у,
за које ова апликација може да добије токене за пријаву и којима може да приступи преко монтираних веза;
подразумевано нема ниједне, а `update(appId, { uses })` замењује листу (погледајте
[Апликације које апликација користи](/docs/sr/feature-flags/apps#apps-an-app-uses-sign-in-tokens-and-bindings)).

`create` и `update` враћају `warnings`, што је увек `[]`.

```ts
const app = await beeuni.project(projectId).apps.create({ slug: 'worker', name: 'Worker', always_on: true });
console.log(app.always_on, app.hosting_type); // true null (until the first deploy)
```

Сачекајте поставку периодичном провером њеног статуса:

```ts
async function waitForReady(appId: string, deploymentId: string) {
  for (;;) {
    const { deployment } = await apps.deployments.get(appId, deploymentId);
    if (deployment.status === 'ready') return deployment;
    if (deployment.status === 'failed' || deployment.status === 'cancelled') {
      throw new Error(deployment.error ?? deployment.error_code ?? deployment.status);
    }
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
}
```

Вредности статуса су `queued`, `validating`, `building`, `provisioning`,
`checking`, `ready`, `failed` и `cancelled`.

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

## Постављање OCI слике

Региструјте непроменљиву референцу слике, затим наведите команду процеса и
јавни циљни порт:

```ts
const registered = await apps.artifacts.register({
  kind: 'oci_image',
  image: 'ghcr.io/hekmat/service:2026-08-07',
});

await apps.deployments.create(app.app_id, {
  artifact_id: registered.artifact.artifact_id,
  source: {
    kind: 'oci_image',
    image: 'ghcr.io/hekmat/service:2026-08-07',
    command: ['node', 'server.js'],
    port: 3000,
    readiness_path: '/health',
  },
});
```

`register` враћа `upload: null` за `oci_image`. Само `archive` добија
URL за отпремање.

`CreateAppDeploymentInput` прихвата и `environment` (вредности извршног окружења које нису тајне),
`secrets` (кључ извршног окружења повезан са именом тајне пројекта) и `config`
(`AppReleaseConfigInput`, конфигурација издања). Неважећи `config`
даје одговор `400 app_config_invalid` са `issues: [{ path, message }]`; монтирање
у извршном окружењу без волумена даје одговор `400 app_volumes_unsupported`.

```ts
await apps.deployments.create(app.app_id, {
  artifact_id: registered.artifact.artifact_id,
  source: { kind: 'oci_image', image, command: ['node', 'server.js'] },
  config: {
    processes: { app: '', worker: 'node worker.js' },
    http_service: { internal_port: 3000, access: 'app', processes: ['app'] },
    mounts: [{ source: 'data', destination: '/data', initial_size_gb: 10, processes: ['app'] }],
    deploy: { release_command: 'node migrate.js' },
  },
});
```

<a id="read-runtime-logs"></a>

## Читање евиденција извршног окружења

```ts
let cursor = 0;
for (;;) {
  const page = await apps.deployments.logs(app.app_id, deployment.deployment_id, {
    after: cursor,
    limit: 200,
  });
  for (const entry of page.entries) console.log(entry.source, entry.line);
  cursor = page.next_cursor;
  if (page.entries.length === 0) break;
}
```

Сваки унос садржи `cursor`, `time`, `source` (`app`, `appd`, `caddy`) и
`line`. Ред `caddy` је дневник приступа: метод, URI, статус, трајање и
заглавља одговора. Он никада не садржи заглавља захтева, јер она садрже
акредитиве гејта (токен и идентитет прегледача, улазни токен добављача).

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

## Машине, скалирање и волумени

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

const machines = await apps.machines.list(app.app_id);
await apps.scale(app.app_id, { worker: 3 });                     // 202: applied in the background

const { exit_code, stdout } = await apps.exec(app.app_id, { command: ['ls', '/data'] });

const [data] = await apps.volumes.list(app.app_id);
const snapshot = await apps.volumes.snapshot(app.app_id, data.volume_id);
await apps.volumes.restore(app.app_id, data.volume_id, snapshot.snapshot_id);   // project.app.admin
const copy = await apps.volumes.fork(app.app_id, data.volume_id, { name: 'data_copy' });
```

Машина задржава свој `machine_id` између издања; свако издање замењује њену
кутију (`instance_id`). Једна машина истовремено монтира један волумен. Враћање је
нова верзија волумена: замењени садржај остаје у његовој историји. Снимак
монтираног волумена може да изостави последњих приближно 1 s уписа.

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

## Convex backend

Convex backend је обична серверска App са волуменом, направљена на основу
шаблона `convex` (`beeuni apps launch --template convex --name db`; погледајте
[Template `convex`](/docs/sr/feature-flags/apps#template-convex-a-convex-backend)).
SDK нема позив специфичан за Convex. Прочитајте администраторски кључ са машине
помоћу `exec` и омогућите UI App-у приступ backend-у помоћу `uses`:

```ts
const db = (await apps.list()).find((a) => a.slug === 'db')!;
const { stdout } = await apps.exec(db.app_id, { command: ['sh', '-c', 'cd /convex && ./generate_admin_key.sh'] });
// The admin key controls all code and data: never log it or ship it to a browser.

await apps.update(site.app_id, { uses: ['db'] }); // the site may mint db tokens and bind to it
```

`db.services` садржи URL адресе backend-а: `http` (URL Convex клијента) и
`site` (HTTP радње).

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

## Ваша App већ зна ко је гледа

App чији је хост Beeuni отвара неко ко је већ **пријављен на Beeuni**. Гејт за
Apps их аутентификује пре него што се пошаље први бајт, па вашој App није
потребна сопствена пријава — ни друга лозинка, ни екран за давање сагласности,
ни преусмеравање.

У прегледачу:

```ts
import { createBeeuni, beeuniAppViewerToken } from '@beeuni/sdk';
import { useBeeuniAppViewer } from '@beeuni/sdk/react';

const beeuni = createBeeuni({
  backendUrl: '/_beeuni/api/v1',             // the gate, on your App's own origin
  getToken: beeuniAppViewerToken(),          // the viewer's own App-scoped token
});

function Header() {
  const { status, viewer } = useBeeuniAppViewer();
  return <span>{status === 'viewer' ? viewer.email : 'Signed out'}</span>;
}
```

Прегледач позива `/_beeuni/api/v1/*` на сопственом изворишту ваше App, никада
`https://api.beeuni.ai/v1` директно: API не одговара на CORS preflight са
изворишта App, па директан позив у прегледачу не успева. Гејт прослеђује сваки
позив Beeuni API-ју као пријављени прегледач, са токеном прегледача ограниченим
на App. Правила:

- Потребан му је `viewer_token_scope: 'api'`. У супротном одговара са
  `403 viewer_api_disabled`.
- Делује само за прегледач који је гејт пријавио (Beeuni пријава или приступни
  линк). Без тога одговара са `401 no_viewer_identity`.
- Одговара само на захтев са сопственог изворишта App. Захтев са другог
  сајта добија одговор `403 cross_site_request`.
- Прослеђује `/v1/*` осим `/v1/oauth/*` и MCP крајњих тачака, које дају
  одговор `404`. Колачић ваше App и њено сопствено заглавље
  `Authorization` никада не стижу до API-ја, а API не може да постави колачић на
  вашој App.

На серверу ваше App, гејт потписује идентитет у сваком захтеву:

```ts
import { readAppViewer, createAppViewerBeeuni } from '@beeuni/sdk/server';

const viewer = await readAppViewer(request);
// { userId, email, name, picture, groupIds, groups, role, projectId, accountId, appId, accessMode, token }
if (!viewer) return new Response('Not found', { status: 404 });

// and, for an `api`-scoped App, act as them:
const beeuni = await createAppViewerBeeuni(request, { backendUrl });
await beeuni.projects.list();               // their projects, their role
```

`readAppViewer` проверава HMAC над заглављем помоћу `BEEUNI_APP_VIEWER_SECRET`,
који Beeuni убацује у вашу App приликом деплојовања. Фалсификовано заглавље
никада не пролази: гејт брише сваку копију коју је послао клијент пре прослеђивања,
a потпис се прави тајном изведеном за сваку App.

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

### Колико ваша App сазнаје

Једно подешавање у смерницама приступа App — **Settings → Access** у Beeuni-ју,
`beeuni apps access <app> --viewer off|identity|api` или `viewer_token_scope`
на `PATCH /projects/:id/apps/:appId/access`:

| Опсег | Шта ваша App прима |
|---|---|
| `identity` (подразумевано) | ID, имејл, име, слику, улогу налога и Beeuni групе прегледача (ID-јеве и називе, само у налогу ове App), плус токен `profile email`. Довољно за приказивање сопствених података свакој особи и ограничење приступа по групи. |
| `api` | Наведено изнад, плус токен који делује **као** та особа на Beeuni API-ју — ограничен њеном сопственом улогом. |
| `off` | Ништа. |

Токен никада није Beeuni сесија корисника: траје сат времена, садржи само те
опсеге, а сваки токен који App изда престаје да важи када се App обрише или се
промене њене смернице приступа.

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

### Коришћење токена прегледача

- **На серверу га читајте за сваки захтев.** Гејт ставља активни токен у
  `x-beeuni-app-viewer-token` у сваки проксирани захтев. Пошаљите га као
  `Authorization: Bearer` Beeuni API-ју — `createAppViewerBeeuni(request)`
  ради управо то. Направите једног клијента по захтеву; не чувајте токен.
- **После промене смерница приступа, стари токени престају да раде.** Следећи
  захтев кроз гејт садржи нови. У прегледачу, `beeuniAppViewerToken()`
  се сам опоравља: када API одбије његов токен, SDK поново чита
  `/_beeuni/viewer` и једном понавља позив.
- **Улога прегледача је горња граница.** Токен може да уради тачно оно што
  прегледач може у Beeuni-ју, никада више. На пројекту са дозволама агената,
  `member` треба да има одобрење за агента који ваша App покреће: без њега
  `POST /projects/:id/sessions` одговара са `403 no_agent_access`. Одобрите прегледачима агента, а не ширу улогу.
- **Ко има прегледач.** Особа која отвори App својом Beeuni пријавом — или
  приступним линком, који је пријављује као ту особу — има један. App
  `password` нема ниједан: лозинка доказује дељену тајну, а не особу. App
  `public` има један само за посетиоца који је стигао преко приступног линка;
  голи URL остаје анониман.
- **То је акредитив.** Beeuni изоставља заглавља захтева из дневника приступа App
  (`beeuni apps logs`). Немојте евидентирати заглавље у сопственом коду.

App која се испоручује на **сопственом домену** (не на `*.apps.beeuni.ai`) нема гејт
испред себе — тамо користите [Sign in with Beeuni](/docs/sr/sdk/sign-in).

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

## Један члан, у сваком окружењу

Beeuni потврђује исту особу на три места: прегледач пита гејт
(`/_beeuni/viewer`), сервер App чита потписано заглавље, а App која
проверава токене за пријаву (Convex backend или ваш сопствени сервер) прима
Beeuni-јем потписан токен. `readBeeuniMember` претвара сваки од тих облика у један
`BeeuniMember`, а `requireBeeuniMember` одлучује ко може да настави. Нема
оквира нити зависности: исти код ради у прегледачу, Node-у, Bun-у, Worker-у и
Convex функцији.

```ts
import {
  readBeeuniMember, requireBeeuniMember, verifyBeeuniToken,
  beeuniToken, beeuniBinding, fetchBeeuniAppViewer, BeeuniMemberError,
} from '@beeuni/sdk';

// Browser: who is looking (null when nobody).
const me = readBeeuniMember(await fetchBeeuniAppViewer());

// Browser → an App this App uses, on the same origin: its URL and a token fetcher.
const db = beeuniBinding('db');
const convex = new ConvexReactClient(db.url);
convex.setAuth(db.token);
// React: read the member with useQuery(api.members.me), never useConvexAuth() (see below).

// Browser: a token for this App itself, or for an App it uses.
const token = await beeuniToken()();
const dbToken = beeuniToken({ audience: 'db' });

// A Convex function: the runtime verified the token; enforce the rules.
const member = requireBeeuniMember(await ctx.auth.getUserIdentity(), { groups: ['Finance'] });

// A server App: verify the bearer. No options = the BEEUNI_AUTH_* env Beeuni injects.
// Any other server: pass the App's `auth` values.
const caller = await verifyBeeuniToken(bearer, { issuer: app.auth.issuer, audience: app.auth.audience, jwks: app.auth.jwks_uri });
```

У React-у немојте позивати `useConvexAuth()`: он баца грешку у обичном
`ConvexProvider` (потребан му је `ConvexProviderWithAuth`, а `beeuniBinding` није
верификован код тог добављача). Прочитајте пријављеног члана упитом:

```ts
// convex/members.ts
export const me = query({ args: {}, handler: async (ctx) => readBeeuniMember(await ctx.auth.getUserIdentity()) });
```

```tsx
const me = useQuery(api.members.me);   // undefined: loading. null: signed out. Otherwise the member.
```

`readBeeuniMember` враћа `null` за анонимног позиваоца. Користите
`requireBeeuniMember` само тамо где функција мора да баци грешку.

| Поље | Вредност |
|---|---|
| `userId` | Beeuni ID корисника. Стабилан; на њему заснивајте редове. |
| `email`, `name`, `picture` | Из профила члана. `null` када је непознато. |
| `groups`, `groupIds` | Beeuni групе члана у овом налогу, по називу и ID-ју. |
| `role` | Улога у налогу: `owner`, `admin` или `member`. |
| `accountId`, `projectId` | Где се App налази. |

`requireBeeuniMember(claims, { groups, roles })` враћа члана или баца
`BeeuniMemberError` са `code` `unauthenticated` (нико или токен
који није прошао проверу) или `forbidden` (изван група или улога). Свака листа значи
„било која од ових“; група се подудара по називу или ID-ју; празна листа не
дозвољава приступ никоме.

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

### Токени за пријаву

Један издавалац по пројекту потписује токене сваке App: `iss` =
`<API origin>/v1/projects/<projectId>`, `aud` = ID App за коју је токен
намењен, 15 минута. Његов скуп кључева је јаван на `<issuer>/jwks.json`, а назив му даје
`<issuer>/.well-known/openid-configuration`. Свака App садржи вредности које
верификују њене токене: `app.auth.issuer`, `app.auth.audience`, `app.auth.jwks_uri`.
Токен садржи `sub`, `email`, `name`, `picture`, `groups`, `group_ids`,
`role`, `account_id` и `project_id`; токен агента садржи
`kind: 'agent'` и нема групе ни улогу.

| Функција | Шта ради |
|---|---|
| `beeuniToken({ audience? })` | Функција за преузимање токена (облик који клијенти за рад у реалном времену користе као повратну функцију за аутентификацију) за саму апликацију (подразумевано) или апликацију коју она користи. Чита `/_beeuni/token` на сопственом извору апликације. Кешира се до непосредно пре истека; `{ forceRefreshToken: true }` поново шаље захтев. Враћа `null` када нема пријављеног посетиоца, а `null` уз једно упозорење у конзоли за апликацију коју не користи (`403 app_not_linked`). Никада не баца изузетак. |
| `beeuniBinding(slug, { origin? })` | `{ url, token }` за апликацију коју ова апликација користи: `url` = `<origin>/_beeuni/apps/<slug>` (HTTP и WebSocket, исти извор, без CORS-а), `token` = `beeuniToken({ audience: slug })`. |
| `verifyBeeuniToken(bearer, { issuer?, audience?, jwks? })` | Проверава ES256 потпис, истек (толеранција од 60 s), издаваоца и публику. Подразумеване вредности: `BEEUNI_AUTH_ISSUER`, `BEEUNI_AUTH_AUDIENCE`, `BEEUNI_AUTH_JWKS`. Beeuni убацује све три у извршно окружење сваке серверске апликације (издавалац: пројекат; публика: сопствени ID апликације; скуп кључева: `data:` URI), па `verifyBeeuniToken(bearer)` тамо ради без опција. Никада не постављајте `BEEUNI_AUTH_*` у `env` серверске апликације: префикс `BEEUNI_` је резервисан и `beeuni validate` га одбацује. Скуп кључева је објекат, JSON текст, `data:` URI или https URL (`auth.jwks_uri`, преузима се једном по процесу). Сваки неуспех баца `BeeuniMemberError`, укључујући недостајући скуп кључева или публику: један кључ пројекта потписује токене свих апликација, па је `aud` оно што спречава прихватање токена друге апликације. `audience: false` прихвата токен за било коју апликацију пројекта. |
| `apps.token(appId)` | Токен који идентификује позиваоца, из API-ја (`POST …/apps/:id/token`). Сесија агента добија токен који идентификује њеног агента. |

Члан кога уклоните из пројекта или групе губи приступ у року од једног периода
важења токена; контрола приступа апликацији одбија га при следећем учитавању странице. Beeuni групе захтевају
Enterprise план; улоге раде на сваком плану.

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

## Управљање приступом

```ts
await apps.access.update(app.app_id, {
  mode: 'restricted',
  member_ids: [memberId],
  group_ids: [groupId],
});

const preview = await apps.access.session(app.app_id);
window.open(preview.url); // valid for five minutes
```

`AppAccessConfig` приказује `password_configured`, никада лозинку или њен
хеш. Поставите лозинку помоћу `{ mode: 'password', password }`. Свако ажурирање
увећава `revision`, чиме се опозивају постојећи колачићи апликације.

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

## Типови

Сваки тип у наставку извози се из `@beeuni/sdk`.

| Тип | Шта садржи |
|---|---|
| `App` | Идентитет, `capabilities`, `labels`, `scale`, `services` (`AppServiceSummary[]`), `machines` (`total`, `running`, `stopped`), `volumes` (`count`, `size_gb`), `config` (`AppReleaseConfig` активног издања, или `null`), `uses`, `used_by`, `auth`, `url`, `access_mode`, `access_revision`, `desired_state`, `active_deployment_id`, `hosting_type` (активног издања: `static`, `sandbox` или `null` пре првог постављања), `machine` (величина групе која опслужује захтеве у активном издању), `idle_timeout_seconds`, `always_on`, `retained_deployments`, `warnings` (само креирање и ажурирање; увек `[]`), `last_request_at`, `viewer_can_access` |
| `AppCapability` | `'deployments' \| 'rollback' \| 'preview' \| 'sleep' \| 'static' \| 'member_tokens' \| 'machines' \| 'scale' \| 'exec' \| 'volumes' \| 'snapshots'` |
| `AppReleaseConfig` · `AppReleaseConfigInput` | Разрешена конфигурација издања (`processes`, `services`, `mounts`, `vm`, `deploy`) · изворно написани облик (`processes`, `http_service`, `services`, `mounts`, `vm`, `deploy` и скраћени облици `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` када би ознака хоста била предугачка) |
| `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` (ID апликације), `jwks_uri`: јавне вредности које проверавају токене за пријављивање у апликацију |
| `AppToken` | `token`, `expires_at` |
| `BeeuniTokenOptions` · `BeeuniTokenFetcher` · `BeeuniBinding` · `VerifyBeeuniTokenOptions` | Помоћне функције за токене за пријављивање наведене изнад |
| `AppDeployment` | `version`, `status`, `source_kind`, `hosting_type` (`static`: испоручује се из складишта, без извршног окружења; `sandbox`: машине), `hosting_provider`, `runtime_spec`, `build_spec`, `error_code`, `attempt_count`, `created_by`, `actor_type`, `source_session_id`, `config` (разрешена конфигурација издања), `strategy`, `release_command_status` (`running`, `succeeded`, `failed` или `null`) |
| `AppRollback` | Вредност којом се разрешава `apps.rollback`: `App` уз издање у реду чекања у `deployment` |
| `AppDeploymentDetail` | Један `deployment` уз његов `events`: догађаји животног циклуса и дневник изградње (редови `build_log` и један догађај `log_truncated` када изградња испише више од 5.000 редова) |
| `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` | Један ред дневника и једна страница уз `next_cursor` |

`apps.list()` и `useProjectApps` враћају само апликације које позивалац сме да отвори
према правилима приступа сваке апликације (приватно: креатор; ограничено: наведени
чланови и групе; пројекат, лозинка и јавно: тим пројекта). Менаџер
пројекта види сваку апликацију, па приватном апликацијом и даље може да се управља када њен креатор
оде. Зато је `viewer_can_access` једнако `true` на свакој наведеној апликацији. `get`
апликације коју позивалац не може да отвори враћа `404`, а не `403`. Третирајте `undefined` као
непознато, а не као одбијено.

`AppAccessMode` је подешавање видљивости по ресурсу над моделом улога,
а не улога. `restricted` наводи кориснике и групе — исте типове субјеката које
користи модел улога. Погледајте
[Налози и приступ](/docs/sr/accounts#per-feature-access-settings).

`DockerfileAppSource` и `OciImageAppSource` захтевају `command`. `port` је
порт примарног сервиса: изоставите га само за издање које садржи само радни процес (`config`
са `processes` и без сервиса). `StaticAppSource` и `BundleAppSource` не прихватају
ниједно од та два.

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

## React куке

`@beeuni/sdk/react` извози седам кука за апликације.

### useProjectApps(projectId)

Апликације које позивалац сме да отвори, уз мутације њиховог животног циклуса. Свака мутација
поништава кеш списка након успеха.

```tsx
import { useProjectApps } from '@beeuni/sdk/react';

function AppList({ projectId }: { projectId: string }) {
  const apps = useProjectApps(projectId);
  if (!apps.data) return null;

  return (
    <ul>
      {apps.data.map((app) => (
        <li key={app.app_id}>
          <a href={app.url}>{app.slug}</a>
          <button onClick={() => apps.stop.mutate(app.app_id)}>Stop</button>
        </li>
      ))}
    </ul>
  );
}
```

Враћа поља упита уз `create`, `update`, `start`, `stop` и
`remove`.

### useAppDeployments(projectId, appId)

Непроменљива историја постављања, која се поново преузима сваких 5 s, тако да се приказ изградње која је у току
сам ажурира.

```tsx
const deployments = useAppDeployments(projectId, appId);

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

Обе мутације поништавају кеш списка постављања и списка апликација.

### useAppDeployment(projectId, appId, deploymentId)

Једно постављање и његови догађаји, укључујући дневник изградње (`AppDeploymentDetail`).
Проверава сваких 2 s док постављање не буде `ready`, `failed` или `cancelled`. Проследите
`null` као `deploymentId` да остане неактивно.

```tsx
const detail = useAppDeployment(projectId, appId, deploymentId);
const log = (detail.data?.events ?? [])
  .map((event) => (event.type === 'build_log' ? event.message : `[${event.type}] ${event.message}`))
  .join('\n');
```

Једна израда задржава првих 2.500 и последњих 2.500 редова. Редови се уписују
у пакетима, најкасније 1 s након њиховог исписивања. Неуспешно или отказано
постављање задржава евиденцију израде 14 дана.

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

Политика приступа и краткотрајна сесија приступа. Оба дела су засебни
упити, а сваки од њих је опционалан.

```tsx
const access = useAppAccess(projectId, appId, {
  policy: canEditAccess,
  session: app.viewer_can_access,
});

access.policy.data;  // AppAccessConfig
access.session.data; // { url, expires_at }
await access.update.mutateAsync({ mode: 'project' });
```

| Опција | Подразумевано | Користите `false` када |
|---|---|---|
| `policy` | `true` | Површина само приказује App. `GET …/access` је административно читање и враћа `403` за позиваоца без дозвола менаџера пројекта. |
| `session` | `true` | Позивалац не сме да отвори App. Проследите `app.viewer_can_access` (увек `true` за наведени App). |

Мрежа Apps која оставља обе опције на `true` покреће једно читање политике и једно
креирање сесије по App-у, а сваки од њих је `403` за члана који не сме да отвори тај
App.

### useAppMachines(projectId, appId)

Машине једног App-а, које се проверавају сваких 5 s, плус `start`, `stop`, `restart`,
`destroy` (сваки захтева `machineId`) и `scale` (захтева
`{ [group]: count }`). Свака мутација освежава машине и инвентар App-а. `appMachinesKey(projectId, appId)` је кључ
упита.

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

### useAppVolumes(projectId, appId)

Волумени једног App-а, плус `snapshot` (`volumeId`), `restore`
(`{ volumeId, snapshotId }`), `fork` (`{ volumeId, name, snapshotId? }`),
`extend` (`{ volumeId, sizeGb }`) и `destroy` (`{ volumeId, name }`). Свака
мутација освежава волумене. `appVolumesKey(projectId, appId)` је кључ
упита.

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

### useAppVolumeSnapshots(projectId, appId, volumeId)

Снимци једног волумена. Проследите `null` као `volumeId` да би остао неактиван.

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

## Грешке

```ts
import { featureDisabledKey, isFeatureDisabledError } from '@beeuni/sdk';

try {
  await beeuni.project(projectId).apps.list();
} catch (error) {
  if (isFeatureDisabledError(error)) {
    console.log(`${featureDisabledKey(error)} is off for this project`);
  }
}
```

Остале одговоре које треба да обрадите:

- `409` за дупликат slug-а, `402` са `app_quota_exceeded` када је налог достигао ограничење броја Apps, и `400` са `app_machine_out_of_range` за спецификацију изван њених граница.
- `400 app_not_found` (`slugs`) за непознат slug у `uses`.
- `409 app_capability_unsupported` (`capability`) за позив који App не
  нуди.
- Постављање: `400 app_config_invalid` (`issues`), `400 app_volumes_unsupported`.
  Враћање: `409 release_not_ready`.
- Машине и 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`.
- Волумени: `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()` одговара са `Cache-Control: no-store`.
