# Beeuni でサインイン

1 つのルートで、自分のアプリへのアクセスを Beeuni の認証で制限し、SDK を通じてサインイン済みユーザーとして操作できます。

Canonical page: https://beeuni.ai/docs/ja/sdk/sign-in

「Beeuni でサインイン」を使うと、Beeuni が運用中のアプリの ID プロバイダーになります。
ダッシュボード、社内ツール、Beeuni 上に構築した業界特化型製品などが対象です。ユーザーは
Beeuni アカウントでサインインし、サーバーはユーザーを識別できます。アプリからの
すべての Beeuni 呼び出しは、そのユーザーに割り当てられたロールで、そのユーザーとして
実行されます。フロー全体は `@beeuni/sdk` にあります。アプリがブラウザーに
Beeuni トークンを保存することはありません。

これは標準の OAuth 2.1（認可コード + PKCE）で、Beeuni API の
`api.beeuni.ai` から提供されます。

> **Beeuni がホストする** App（`*.apps.beeuni.ai`）を構築していますか？その場合、ここで説明するものは
> 一切不要です。Apps のゲートですでに閲覧者が認証されています。
> `beeuniAppViewerToken()` / `readAppViewer()` で閲覧者を取得し、
> `beeuniToken()` でその App またはその App が使用する App のサインイントークンを取得してください。
> 下記の [App のサインイントークン](#sign-in-tokens-for-apps)を参照してください。

<a id="1-register-your-app"></a>

## 1. アプリを登録する

**Account → Tokens → OAuth apps → Register app** に進むか、SDK を呼び出します。

```ts
const app = await beeuni.iam.oauthClients.create(accountId, {
  name: 'Dashboards',
  client_type: 'confidential',            // 'public' for a browser/native app (PKCE only, no secret)
  redirect_uris: ['https://dashboards.example.com/api/beeuni/auth/callback'],
  scopes: ['profile', 'email', 'beeuni'],
});
// app.client_id, app.client_secret (shown once)
```

登録には、アカウントの `token.create` が必要です。リダイレクト URI は
バイト単位で比較されます。`localhost` を除き、`https` が必須です。

| スコープ | アプリに付与するもの |
|---|---|
| `profile` | ユーザーの ID、メールアドレス、アカウントへの所属情報（`GET /v1/accounts/me`）。 |
| `email` | メールアドレス（OIDC 形式のクライアント向けの別名）。 |
| `beeuni` | Beeuni API 全体でユーザーとして操作する権限 — プロジェクト、セッション、ファイル、IAM プローブ。これがない場合、トークンは ID 認証専用です。 |

<a id="2-mount-the-handler"></a>

## 2. ハンドラーをマウントする

```ts
// lib/beeuni-auth.ts
import { createBeeuniAuth } from '@beeuni/sdk/server';

export const auth = createBeeuniAuth({
  backendUrl: 'https://api.beeuni.ai/v1',
  clientId: process.env.BEEUNI_OAUTH_CLIENT_ID!,
  clientSecret: process.env.BEEUNI_OAUTH_CLIENT_SECRET,   // omit for a public client
  redirectUri: 'https://dashboards.example.com/api/beeuni/auth/callback',
  cookieSecret: process.env.BEEUNI_AUTH_COOKIE_SECRET!,   // ≥ 32 chars; encrypts the session cookie
});
```

```ts
// app/api/beeuni/auth/[...beeuni]/route.ts  (Next.js App Router)
import { auth } from '@/lib/beeuni-auth';
const handle = (request: Request) => auth.handler(request);
export { handle as GET, handle as POST };
```

ハンドラーは `basePath` 配下のすべてのルートを処理します（リダイレクト
URI、上記の `/api/beeuni/auth` から導出）。

| パス | 動作 |
|---|---|
| `/signin?return_to=/path` | サインインを開始し（PKCE S256 + state を有効期限 10 分の Cookie に保存）、Beeuni にリダイレクトします。 |
| `/callback` | コードを交換し、暗号化された `HttpOnly` セッション Cookie を設定して、`return_to` にリダイレクトします。 |
| `/refresh?return_to=` | トークンのペアをローテーションし、リダイレクトします。`requireViewer` が使用します。 |
| `/signout?return_to=` | Beeuni でリフレッシュトークンを失効させ、Cookie を消去します。 |
| `/me` | 閲覧者を JSON で返すか、`401` を返します。アクセストークンが期限切れの場合、その場で更新します。 |
| `/proxy/*` | 閲覧者として Beeuni API に転送します。ブラウザー SDK の `backendUrl` です。 |

`return_to` は常に同一オリジンのパスに限定されます。

<a id="3-gate-pages-and-act-as-the-user"></a>

## 3. ページへのアクセスを制限し、ユーザーとして操作する

```ts
// middleware.ts — every page needs a viewer
import { auth } from '@/lib/beeuni-auth';

export async function middleware(request: Request) {
  const gate = await auth.requireViewer(request);
  if (gate.response) return gate.response;   // 302 → /refresh or /signin
}
export const config = { matcher: ['/((?!api/beeuni/auth|_next).*)'] };
```

```ts
// a server component / route handler
const viewer = await auth.viewer(request);       // { userId, email, accounts, scopes, token, expiresAt } | null
const beeuni = await auth.beeuni(request);       // request-scoped client acting as the viewer
const projects = await beeuni.projects.list();
const allowed = await beeuni.iam.can(accountId, viewer!.userId, { action: 'project.write', resourceType: 'project', resourceId });
```

`viewer()` は読み取り専用で、1 回限りのリフレッシュトークンを消費しません。
ミドルウェアで `requireViewer()` を使うと、リフレッシュトークンがまだ有効なユーザーに対して
ページがサインアウト状態で表示されることを防げます。

<a id="4-the-browser"></a>

## 4. ブラウザー

```tsx
import { createBeeuni } from '@beeuni/sdk';
import { SignInWithBeeuni, useBeeuniViewer } from '@beeuni/sdk/react';

const beeuni = createBeeuni(auth.clientConfig());   // backendUrl = '/api/beeuni/auth/proxy'

function Header() {
  const { status, viewer } = useBeeuniViewer();
  if (status === 'signed-in') return <span>{viewer.email}</span>;
  return <SignInWithBeeuni className="button" />;
}
```

ブラウザークライアントはセンチネルの Bearer トークンを送信します。`/proxy` がサーバー上で
それを閲覧者の実際のトークンに置き換えます。`useSession`、`beeuni.project(id).sessions.*`、
その他すべての SDK 呼び出しは、変更せずにこれを通じて動作します。

<a id="what-the-user-sees"></a>

## ユーザーに表示されるもの

初回は、Beeuni がアプリ名とスコープを示す同意画面を表示します。
Beeuni はユーザーとアプリごとに選択を記憶するため、以後のサインインでは
そのままアプリにリダイレクトされます。アプリの認可を取り消すと、そのアプリが発行したすべてのトークンが削除されます。

<a id="discovery"></a>

## ディスカバリー

`GET https://api.beeuni.ai/.well-known/oauth-authorization-server`（`/v1/oauth/.well-known/…` 配下にもあります）は、
汎用 OAuth クライアント向けのエンドポイントを公開します。
SDK では不要です。すべてのエンドポイントを `backendUrl` から導出します。

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

## App のサインイントークン

Beeuni がホストする App 内のコードと、その App が使用する App は、もう 1 種類の
トークンを受け入れます。Beeuni が 1 つの App 向けに発行する短い ES256 JWT です。
OAuth フロー、リダイレクト、クライアントシークレットは不要です。

| | 値 |
|---|---|
| 発行者 | `<API origin>/v1/projects/<projectId>`、プロジェクトごとに 1 つ |
| ディスカバリー | `<issuer>/.well-known/openid-configuration` と `<issuer>/jwks.json`、公開 |
| オーディエンス | トークンの対象となる App の ID |
| 有効期間 | 15 分 |
| クレーム | `sub`、`email`、`name`、`picture`、`groups`、`group_ids`、`role`、`account_id`、`project_id`。エージェントのトークンには `kind: "agent"` が追加され、グループもロールもありません |

すべての App は、トークンの検証に使う値を持ちます。`app.auth.issuer`、
`app.auth.audience`、`app.auth.jwks_uri` です。

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

// In the App's browser code: a token fetcher for this App, or an App it uses.
const forThisApp = beeuniToken();
const forDb = beeuniToken({ audience: 'db' });
const db = beeuniBinding('db');   // { url: '<origin>/_beeuni/apps/db', token: forDb }

// On a server that receives the token:
const member = await verifyBeeuniToken(bearer, {
  issuer: app.auth.issuer,
  audience: app.auth.audience,
  jwks: app.auth.jwks_uri,
});
requireBeeuniMember(member, { roles: ['owner', 'admin'] });
```

サーバー App では、Beeuni が `BEEUNI_AUTH_ISSUER`（プロジェクトの発行者）、
`BEEUNI_AUTH_AUDIENCE`（App 自身の ID）、`BEEUNI_AUTH_JWKS`（プロジェクトの
キーセット、`data:` URI 形式）を注入するため、`verifyBeeuniToken(bearer)` にオプションは不要です。
`BEEUNI_AUTH_*` を自分で設定しないでください。`BEEUNI_` プレフィックスは予約されており、
`beeuni validate` が拒否します。

`verifyBeeuniToken` は、オーディエンス（オプションまたは
`BEEUNI_AUTH_AUDIENCE`）が設定されていない場合、トークンを拒否します。1 つのプロジェクトキーが
プロジェクト内のすべての App のトークンに署名するため、別の App のトークンを排除するのは
`aud` です。プロジェクト内の任意の App のトークンを受け入れる場合にのみ、
`audience: false` を渡してください。

`beeuniToken` は、App 自身のオリジンで
`GET /_beeuni/token?audience=<slug or id>` を読み取ります。オーディエンスは App 自身（デフォルト）、またはその App が使用する App
（`beeuni apps link <app> --uses <other>`）です。それ以外の場合は
`403 app_not_linked` を返します。メンバー、スクリプト、エージェントは、代わりに API を通じて
トークンを発行します。`beeuni.project(projectId).apps.token(appId)` または
`beeuni apps token <app>` です。完全な仕様、バインディングのマウント、
Convex バックエンドのセットアップについては、[SDK → Apps](/docs/ja/sdk/apps) と
[Apps → App が使用する App](/docs/ja/feature-flags/apps#apps-an-app-uses-sign-in-tokens-and-bindings)を参照してください。
