---
title: "アプリ"
description: "SDKおよびReactからBeeuniアプリを作成、デプロイ、制御します。"
---

Apps はプロジェクト単位のデプロイで、それぞれに固定 URL が1つあります。
イミュータブルなリリース、プロセスグループごとのマシン、永続ボリュームを備えています。`app.capabilities` に応じて分岐してください。このページでは SDK の機能として、
プロジェクトハンドル上の `apps` ファサード、アーティファクト、リリース、マシン、ボリュームの
呼び出し、サインイントークン、エクスポートされる型、React フックを説明します。

モデル、リリース設定、ソースの種類、CLI、固定 URL、
コールド状態からの起動動作については、[Apps](/docs/ja/feature-flags/apps) を参照してください。

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

:::info[Apps は機能フラグです]
プロジェクトで Apps を有効にするまで、すべての Apps ルートは `{ error, code: "feature_disabled", feature: "apps" }` を伴う
`403` を返します。`isFeatureDisabledError(error)` を使って
分岐してください。[機能フラグ](/docs/ja/feature-flags) を参照してください。
:::

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

## apps ファサード

| メソッド | ラップする API | 動作 |
|---|---|---|
| `apps.list()` | `GET /projects/:pid/apps` | 呼び出し元が開ける Apps を一覧表示します（プロジェクト管理者にはすべて表示されます） |
| `apps.create(input)` | `POST …/apps` | App を作成し、固定 URL を割り当てます |
| `apps.get(appId)` | `GET …/apps/:id` | 1つの App を読み取ります |
| `apps.update(appId, input)` | `PATCH …/apps/:id` | 名前を変更するか、マシン、アイドルタイムアウト、実行モード（`always_on`）、`uses`、`labels` を変更します。実行モードは5分以内に適用され、マシンの変更は次のリリースに適用されます |
| `apps.remove(appId)` | `DELETE …/apps/:id` | App、そのマシン、すべてのリリースイメージを削除し、`images: {released, pending}` を返します。ボリュームには `final` スナップショットが7日間保持されます |
| `apps.start(appId)` | `POST …/apps/:id/start` | `desired_state` を `running` に設定し、ランタイムをウォームアップします。静的 App は `409 static_app_no_runtime` を返します |
| `apps.stop(appId)` | `POST …/apps/:id/stop` | コンピュートを即座に停止し、次のリクエストで再開します。静的 App は `409 static_app_no_runtime` を返します。ランタイムを持たず、アクティブなデプロイがある間は配信を続けます |
| `apps.rollback(appId, deploymentId)` | `POST …/apps/:id/rollback` | 準備完了のリリースのアーティファクトと設定から新しいリリースを作成し、デプロイと同様にロールアウトします（`202`）。ボリュームのデータは保持されます |

アーティファクトは、デプロイへのイミュータブルな入力です。

| メソッド | ラップする API | 動作 |
|---|---|---|
| `apps.artifacts.register(input)` | `POST …/apps/artifacts` | `archive` または `oci_image` を登録します。アーカイブの場合はアップロード URL を返します |
| `apps.artifacts.uploadArchive(bytes, options?)` | — | 1回の呼び出しで、1つの `.tar.gz` の登録、アップロード、ハッシュ計算、確定を行います |
| `apps.artifacts.finalize(artifactId, input)` | `POST …/apps/artifacts/:id/finalize` | 手動アップロードの `sha256` と `size_bytes` を確認します |

リリース（デプロイ）はイミュータブルで、番号が付けられます。

| メソッド | ラップする API | 動作 |
|---|---|---|
| `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` | 1つのデプロイとそのイベントを読み取ります |
| `apps.deployments.logs(appId, deploymentId, options?)` | `GET …/deployments/:did/logs` | カーソルを使ってランタイムログを読み取ります |
| `apps.deployments.remove(appId, deploymentId)` | `DELETE …/deployments/:did` | 1つのデプロイとそのイメージを削除します。稼働中のデプロイまたは進行中のビルドの場合は `409`。`image: 'released' \| 'pending' \| 'none'` を返します |

マシン、スケール、exec（サーバー App 用。マシンのない App は
`409 app_no_machines` を返します）：

| メソッド | ラップする API | 動作 |
|---|---|---|
| `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}` | 1つのマシンを操作します。マシン一覧を返して完了します |
| `apps.machines.destroy(appId, machineId)` | `DELETE …/machines/:mid` | 1つのマシンを破棄します。ボリュームは残り、そのグループの稼働マシン数が1つ減ります |
| `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` | 稼働中のマシンで1つのコマンドを実行します（`project.app.admin`、監査対象）。`timeout_s` は1～300、デフォルトは60。`exit_code`、`stdout`、`stderr`（それぞれ1 MiB）、`truncated` を返して完了します |

ボリューム（永続ディスク）：

| メソッド | ラップする API | 動作 |
|---|---|---|
| `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` | スナップショットまたは現在の内容から、同じ App 内に新しい未接続のボリュームを作成します |

サインイントークン（ケイパビリティ `member_tokens`。App がこれを持たない場合は
`409 app_capability_unsupported`）：

| メソッド | ラップする API | 動作 |
|---|---|---|
| `apps.token(appId)` | `POST …/apps/:id/token` | 呼び出し元を識別する、App 用の有効期間15分のサインイントークン |

アクセスは App 独自の認可ポリシーです。

| メソッド | ラップする API | 動作 |
|---|---|---|
| `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` | ホスト限定 Cookie と交換できる、有効期間5分の 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`。デフォルトを使う場合は省略してください。
Apps に月次予算はありません。コンピュートの料金はアカウントのクレジットで支払われます。常時稼働の
サーバー App は24時間365日課金され、オンデマンドの App は起動中のみ課金されます。
`always_on: true` はサーバー App を24時間365日稼働させます
（WebSocket、バックグラウンドループ）。`false` は `idle_timeout_seconds` の後に停止し、
次のリクエストで起動します。静的 App にはランタイムがなく、`always_on` は
`false` です。

古いクライアントが `monthly_budget_usd` を送信しても、API は無視します（`200`、
エラーなし）。App のレスポンスには含まれません。

`uses` は、この App がサインイントークンを取得でき、
バインディングのマウント経由でアクセスできる Apps をスラッグで一覧表示します。
デフォルトは空で、`update(appId, { uses })` は一覧を置き換えます（
[App が使用する Apps](/docs/ja/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` は `oci_image` に対して `upload: null` を返します。アップロード URL を取得できるのは
`archive` のみです。

`CreateAppDeploymentInput` は `environment`（機密ではないランタイムの
値）、`secrets`（ランタイムのキーからプロジェクトのシークレット名への対応）、`config`（`AppReleaseConfigInput`、
リリース設定）も受け付けます。無効な `config`
は `issues: [{ path, message }]` を伴う `400 app_config_invalid` を返します。ボリュームをサポートしないランタイムでの
マウントは `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、ステータス、所要時間、
レスポンスヘッダーを記録します。リクエストヘッダーはゲートの認証情報（ビューアートークンとID、プロバイダーのイングレストークン）を含むため、
決して記録されません。

<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台のマシンが一度にマウントできるボリュームは1つです。リストアは
ボリュームの新しいバージョンです。置き換えられた内容は履歴に残ります。マウントされたボリュームの
スナップショットには、直近約1秒分の書き込みが含まれない場合があります。

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

## Convexバックエンド

Convexバックエンドは、`convex`テンプレート（`beeuni apps launch --template convex --name db`。詳しくは
[Template `convex`](/docs/ja/feature-flags/apps#template-convex-a-convex-backend)）から作成した、ボリューム付きの通常のサーバーAppです。
SDKにConvex専用の呼び出しはありません。`exec`でマシンから管理キーを読み取り、
`uses`でUI Appからバックエンドにアクセスできるようにします。

```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、つまり`http`（ConvexクライアントURL）と
`site`（HTTPアクション）が格納されます。

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

## Appは誰が見ているかをすでに把握しています

BeeuniがホストするAppは、Beeuniに**すでにサインインしている**人によって開かれます。Appsゲートは最初のバイトが配信される前にユーザーを認証するため、App独自のログインは不要です。2つ目のパスワードも、同意画面も、リダイレクトも必要ありません。

ブラウザーでは次のようになります。

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

ブラウザーはApp自身のオリジン上にある`/_beeuni/api/v1/*`を呼び出し、`https://api.beeuni.ai/v1`を直接呼び出すことはありません。APIはAppオリジンからの
CORSプリフライトに応答しないため、ブラウザーからの直接呼び出しは失敗します。ゲートは各呼び出しを、サインイン済みのビューアーとして、ビューアーのAppスコープトークンとともにBeeuni APIへ転送します。ルールは次のとおりです。

- `viewer_token_scope: 'api'`が必要です。そうでない場合は
  `403 viewer_api_disabled`を返します。
- ゲートがサインインさせたビューアー（Beeuniログインまたはアクセスリンク）に対してのみ動作します。ビューアーがいない場合は`401 no_viewer_identity`を返します。
- App自身のオリジンからのリクエストにのみ応答します。クロスサイトリクエストには`403 cross_site_request`を返します。
- `/v1/oauth/*`とMCPエンドポイントを除く`/v1/*`を転送します。これらには`404`を返します。AppのCookieと独自の`Authorization`ヘッダーがAPIに到達することはなく、APIがAppにCookieを設定することもできません。

Appのサーバーでは、ゲートがすべてのリクエストにIDを署名します。

```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`は、Beeuniがデプロイ時にAppへ注入する`BEEUNI_APP_VIEWER_SECRET`を使って、ヘッダーのHMACを検証します。偽造されたヘッダーが通ることはありません。ゲートはクライアントが送信したコピーを転送前に削除し、署名にはAppごとに導出されたシークレットを使用します。

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

### Appに通知される情報の範囲

Appのアクセスポリシーにある1つの設定、Beeuniの**Settings → Access**、`beeuni apps access <app> --viewer off|identity|api`、または`PATCH /projects/:id/apps/:appId/access`上の`viewer_token_scope`で設定します。

| スコープ | Appが受け取る情報 |
|---|---|
| `identity`（デフォルト） | ビューアーのID、メールアドレス、名前、画像、アカウントロール、Beeuniグループ（このAppのアカウント内におけるIDと名前）、および`profile email`トークン。各ユーザーに自身のデータを表示し、グループでアクセスを制限するのに十分です。 |
| `api` | 上記に加え、Beeuni API上でそのユーザー**として**動作するトークン（ユーザー自身のロールによる制限付き）。 |
| `off` | 何も受け取りません。 |

このトークンはユーザーのBeeuniセッションではありません。有効期間は1時間で、これらのスコープのみを持ちます。また、Appが発行したすべてのトークンは、Appが削除されるかアクセスポリシーが変更されると無効になります。

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

### ビューアートークンの使用

- **サーバーではリクエストごとに読み取ります。** ゲートはプロキシされた各リクエストの`x-beeuni-app-viewer-token`に有効なトークンを配置します。これを`Authorization: Bearer`としてBeeuni APIに送信します。`createAppViewerBeeuni(request)`がまさにそれを行います。リクエストごとにクライアントを1つ作成し、トークンを保存しないでください。
- **アクセスポリシーの変更後は、古いトークンは機能しません。** 次にゲートを通るリクエストには新しいトークンが含まれます。ブラウザーでは、`beeuniAppViewerToken()`が自動的に復旧します。APIがトークンを拒否すると、SDKは`/_beeuni/viewer`を再度読み取り、呼び出しを1回だけ再実行します。
- **ビューアーのロールが上限です。** トークンで実行できるのはBeeuni上でビューアーが実行できることだけで、それを超えることはありません。エージェント権限を持つプロジェクトでは、`member`に、Appが起動するエージェントへの許可が必要です。許可がない場合、`POST /projects/:id/sessions`は`403 no_agent_access`を返します。ビューアーには、より広いロールではなくエージェントを付与してください。
- **ビューアーを持つユーザー。** Beeuniログイン、またはそのユーザーとしてサインインさせるアクセスリンクでAppを開いた人はビューアーを持ちます。`password` Appにはビューアーがありません。パスワードは共有シークレットを証明するものであり、個人を証明するものではないためです。`public` Appがビューアーを持つのは、アクセスリンク経由で訪れたユーザーに対してのみです。URLを直接開いた場合は匿名のままです。
- **これは認証情報です。** BeeuniはAppのアクセスログからリクエストヘッダーを除外します（`beeuni apps logs`）。独自コードでこのヘッダーをログに記録しないでください。

**独自ドメイン**（`*.apps.beeuni.ai`ではない）で提供されるAppの前にはゲートがありません。そこで代わりに[Sign in with Beeuni](/docs/ja/sdk/sign-in)を使用してください。

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

## すべてのランタイムで同じメンバー

Beeuniは同じユーザーを3か所で保証します。ブラウザーはゲートに問い合わせ（`/_beeuni/viewer`）、Appのサーバーは署名済みヘッダーを読み取り、サインイントークンを検証するApp（Convexバックエンドまたは独自サーバー）はBeeuni署名済みトークンを受け取ります。`readBeeuniMember`はこれらの形式を1つの`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 })`はメンバーを返すか、`code` `unauthenticated`（ユーザーがいない、またはチェックに失敗したトークン）の場合は`BeeuniMemberError`を、`forbidden`（グループまたはロールの範囲外）の場合は例外を発生させます。各リストは「いずれか1つ」を意味します。グループは名前またはIDで一致し、空のリストでは誰も許可されません。

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

### サインイントークン

プロジェクトごとに1つの発行者がすべてのAppのトークンに署名します。`iss` =
`<API origin>/v1/projects/<projectId>`、`aud` = トークンの対象となるAppのIDで、有効期間は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? })` | App 自体（デフォルト）またはその App が使用する App のトークン取得関数（リアルタイムクライアントが認証コールバックとして受け取る形式）。App 自身のオリジンで `/_beeuni/token` を読み取ります。有効期限の少し前までキャッシュされ、`{ forceRefreshToken: true }` で再取得します。サインイン済みの閲覧者がいない場合は `null` を返し、使用していない App の場合は `null` を返してコンソール警告を1回出力します（`403 app_not_linked`）。例外はスローしません。 |
| `beeuniBinding(slug, { origin? })` | この App が使用する App の `{ url, token }`：`url` = `<origin>/_beeuni/apps/<slug>`（HTTP と WebSocket、同一オリジン、CORS なし）、`token` = `beeuniToken({ audience: slug })`。 |
| `verifyBeeuniToken(bearer, { issuer?, audience?, jwks? })` | ES256 署名、有効期限（60秒の時刻ずれを許容）、発行者、対象を検証します。デフォルト：`BEEUNI_AUTH_ISSUER`、`BEEUNI_AUTH_AUDIENCE`、`BEEUNI_AUTH_JWKS`。Beeuni はすべてのサーバー App のランタイムにこの3つを注入します（発行者：プロジェクト、対象：App 自身の ID、鍵セット：`data:` URI）。そのため、そこでは `verifyBeeuniToken(bearer)` がオプションなしで動作します。サーバー App の `env` に `BEEUNI_AUTH_*` を設定しないでください。`BEEUNI_` プレフィックスは予約済みで、`beeuni validate` が拒否します。鍵セットはオブジェクト、JSON テキスト、`data:` URI、または https URL（`auth.jwks_uri`、プロセスごとに1回取得）です。鍵セットや対象がない場合を含め、すべての失敗で `BeeuniMemberError` をスローします。1つのプロジェクト鍵がすべての App のトークンに署名するため、別の App のトークンを排除するのは `aud` です。`audience: false` はプロジェクト内のどの App のトークンでも受け入れます。 |
| `apps.token(appId)` | API から取得する、呼び出し元を示すトークン（`POST …/apps/:id/token`）。エージェントセッションには、そのエージェントを示すトークンが渡されます。 |

プロジェクトまたはグループから削除したメンバーは、トークンの有効期間内にアクセスを失います。App のアクセスゲートは、次のページ読み込み時にそのメンバーを拒否します。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` が増加し、既存の App Cookie が無効になります。

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

## 型

以下のすべての型は `@beeuni/sdk` からエクスポートされます。

| 型 | 保持する内容 |
|---|---|
| `App` | ID 情報、`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`（App の ID）、`jwks_uri`：App のサインイントークンを検証する公開値 |
| `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` | 1つの `deployment` とその `events`：ライフサイクルイベントとビルドログ（`build_log` 行、およびビルドの出力が5,000行を超えた場合に1つの `log_truncated` イベント） |
| `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` | ログ1行、および1ページと `next_cursor` |

`apps.list()` と `useProjectApps` は、各 App のアクセスポリシーに従って呼び出し元が開ける App のみを返します（private：作成者、restricted：指定されたメンバーとグループ、project、password、public：プロジェクトチーム）。プロジェクトマネージャーにはすべての App が表示されるため、作成者が離脱しても private の App を管理できます。したがって、一覧にあるすべての App で `viewer_can_access` は `true` です。呼び出し元が開けない App の `get` は、`403` ではなく `404` を返します。`undefined` は拒否ではなく不明として扱ってください。

`AppAccessMode` はロールモデルに追加されるリソースごとの可視性設定であり、ロールではありません。`restricted` はユーザーとグループを指定します。これらはロールモデルで使用するものと同じプリンシパルの種類です。
[アカウントとアクセス](/docs/ja/accounts#per-feature-access-settings) を参照してください。

`DockerfileAppSource` と `OciImageAppSource` には `command` が必要です。`port` はプライマリサービスのポートです。省略できるのはワーカーのみのリリース（`processes` があり、サービスのない `config`）の場合だけです。`StaticAppSource` と `BundleAppSource` はどちらも受け取りません。

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

## React フック

`@beeuni/sdk/react` は App 用の7つのフックをエクスポートします。

### useProjectApps(projectId)

呼び出し元が開ける App と、そのライフサイクルのミューテーション。すべてのミューテーションは、成功時にインベントリを無効化します。

```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秒ごとに再取得します。

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

両方のミューテーションが、デプロイ一覧と App インベントリを無効化します。

### useAppDeployment(projectId, appId, deploymentId)

1つのデプロイとそのイベント。ビルドログも含みます（`AppDeploymentDetail`）。デプロイが `ready`、`failed`、または `cancelled` になるまで、2秒ごとにポーリングします。待機状態を維持するには、`deploymentId` として `null` を渡します。

```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秒以内に書き込まれます。失敗またはキャンセルされたデプロイの
ビルドログは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`を渡します（一覧に表示されるAppでは常に`true`）。 |

Appのグリッドで両方のオプションを`true`のままにすると、Appごとにポリシーの読み取りと
セッションの発行が1回ずつ実行され、そのAppを開けないメンバーに対しては、
それぞれ`403`になります。

### useAppMachines(projectId, appId)

5秒ごとにポーリングされるAppのマシンに加え、`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)

1つのボリュームのスナップショット。実行しない状態に保つには、`volumeId`として`null`を渡します。

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

処理すべきその他の応答：

- slugの重複には`409`、アカウントがApp数の上限に達している場合は
  `app_quota_exceeded`を伴う`402`、スペックが範囲外の場合は`app_machine_out_of_range`を伴う`400`。
- `uses`内の不明なslugには`400 app_not_found`（`slugs`）。
- Appが提供していない呼び出しには`409 app_capability_unsupported`
  （`capability`）。
- デプロイ：`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`で応答します。
