# OAuth と開発者アプリ

> client.developerApps で Nodaro の OAuth アプリを登録・管理し、client.oauth でコードの交換、トークンの取り消し、同意画面用データの読み取りを行います。

Source: https://nodaro.ai/ja/docs/developers/sdk/developer-apps

**`client.developerApps`** は、あなたが所有する OAuth アプリを管理します。これは、ほかの Nodaro ユーザーが自分のアカウントへのアクセスをあなたのソフトウェアに許可するためのアプリです。**`client.oauth`** は、OAuth 2.0 フローのサーバー側を担います。認可コードをアクセストークンと交換し、トークンを取り消し、同意画面が表示する公開データを返します。同意フローの全体については、[OAuth](https://nodaro.ai/docs/developers/oauth) で説明しています。

## メソッド
| メソッド | 内容 |
| --- | --- |
| [`developerApps.list()`](#developerappslist) | 自分のアプリを一覧表示します |
| [`developerApps.get(id)`](#developerappsgetid) | 1 つのアプリを読み取ります |
| [`developerApps.create(input)`](#developerappscreateinput) | アプリを登録し、そのシークレットを取得します |
| [`developerApps.update(id, input)`](#developerappsupdateid-input) | アプリを変更します |
| [`developerApps.delete(id)`](#developerappsdeleteid) | アプリを削除します |
| [`developerApps.rotateSecret(id)`](#developerappsrotatesecretid) | アプリのシークレットを置き換えます |
| [`oauth.exchangeCode(input)`](#oauthexchangecodeinput) | 認可コードをアクセストークンと交換します |
| [`oauth.revoke(token)`](#oauthrevoketoken) | アクセストークンを取り消します |
| [`oauth.getAppInfo(clientId, redirectUri?)`](#oauthgetappinfoclientid-redirecturi) | 同意画面用に、アプリの公開データを読み取ります |

## スコープ
アプリはスコープを要求し、ユーザーがそれを承認します。サーバーは、次のスコープを受け付けます。

| スコープ | 許可される操作 |
| --- | --- |
| `workflows:read` | ワークフローの読み取り |
| `workflows:write` | ワークフローの作成と変更 |
| `workflows:execute` | ワークフローの実行 |
| `jobs:read` | ジョブの読み取り |
| `assets:read` | キャラクター、ロケーション、オブジェクトなどのアセットの読み取り |
| `assets:write` | アセットの作成と変更 |
| `credits:read` | クレジット残高の読み取り |
| `apps:read` | 公開されたアプリの読み取り |
| `pipelines:read` | パイプラインの読み取り |
| `pipelines:execute` | パイプラインの開始とキャンセル |
| `pipelines:approve` | パイプラインのステージの承認と却下 |
| `presets:read` | ノードプリセットの読み取り |
| `workspaces:read` | ユーザーが所属するワークスペースの一覧表示 |
| `workspaces:write` | アプリが動作するワークスペースの選択 |

各スコープについて同意画面に表示される内容は、[OAuth アプリ](https://nodaro.ai/docs/developers/oauth#scopes)に記載されています。必要なスコープがない呼び出しは、`missingScope` にそのスコープが示された `ForbiddenError` をスローします。[スコープと不足している権限](https://nodaro.ai/docs/developers/sdk/auth#scopes-and-missing-permissions)を参照してください。

## client.developerApps
アプリを読み取ったり変更したりできるのは、所有者だけです。シークレットが返されるのは 1 回だけです。

### developerApps.list()
自分のアプリを一覧表示します。

```ts
list(): Promise<{ data: DeveloperApp[] }>
```

```ts
const { data: apps } = await client.developerApps.list()
const mine = apps.filter((app) => (app.kind ?? "user") === "user")
```

`DeveloperApp` は、`id`、`name`、`description`、`logoUrl`、`homepageUrl`、`redirectUris`、`allowedOrigins`、`scopesRequested`、`clientId`、`status`（`active`、`suspended`、`pending_review` のいずれか）、`kind`、`createdAt`、`updatedAt` を持ちます。`kind` は、あなたが登録したアプリでは `"user"` です。それ以外の値である `dynamic_mcp`、`first_party_mcp`、`community_instance` は、自己登録したクライアントを示します。5 つまでの上限に数えられるのは、`"user"` のアプリだけです。

### developerApps.get(id)
1 つのアプリを読み取ります。シークレットが含まれることはありません。

```ts
get(id: string): Promise<{ data: DeveloperApp }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "アプリの ID です。" },
}}
/>

```ts
const { data: app } = await client.developerApps.get(appId)
```

### developerApps.create(input)
アプリを登録します。応答には `clientSecret` が含まれます。サーバーはそのハッシュしか保存しないため、今すぐ保管してください。

```ts
create(input: CreateDeveloperAppInput): Promise<{ data: DeveloperApp & { clientSecret: string } }>
```

<TypeTable
type={{
name: { type: 'string', required: true, description: "同意画面でユーザーに表示されるアプリ名です。" },
redirectUris: { type: 'string[]', required: true, description: "1〜10 個の URI で、それぞれ https:// または http://localhost です。" },
scopesRequested: { type: 'DeveloperAppScope[]', required: true, description: "少なくとも 1 つのスコープです。" },
allowedOrigins: { type: 'string[]', description: "このアプリのトークンを使ってブラウザーから API を呼び出せる、パス・クエリ・ハッシュを含まない単純なオリジンで、最大 5 個です。" },
description: { type: 'string', description: "同意画面用の説明です。" },
homepageUrl: { type: 'string', description: "あなたのアプリのホームページです。" },
logoUrl: { type: 'string', description: "あなたのアプリのロゴです。" },
}}
/>

```ts
const { data } = await client.developerApps.create({
name: "My integration",
redirectUris: ["https://example.com/oauth/callback"],
scopesRequested: ["workflows:read", "workflows:execute"],
})
console.log(data.clientId, data.clientSecret) // save both now
```

アプリは 5 つまで登録できます。6 つ目は `400 limit_reached` で失敗します。

### developerApps.update(id, input)
アプリを変更します。変更するフィールドだけを渡してください。それぞれに `create()` と同じルールが適用されます。

```ts
update(id: string, input: UpdateDeveloperAppInput): Promise<{ data: DeveloperApp }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "アプリの ID です。" },
name: { type: 'string', description: "アプリ名です。" },
description: { type: 'string', description: "説明です。" },
homepageUrl: { type: 'string', description: "ホームページです。" },
logoUrl: { type: 'string', description: "ロゴです。" },
redirectUris: { type: 'string[]', description: "リダイレクト URI の新しい完全なリストです。" },
allowedOrigins: { type: 'string[]', description: "ブラウザーオリジンの新しい完全なリストです。" },
scopesRequested: { type: 'DeveloperAppScope[]', description: "スコープの新しい完全なリストです。" },
}}
/>

```ts
await client.developerApps.update(appId, {
redirectUris: ["https://example.com/oauth/callback", "https://staging.example.com/oauth/callback"],
})
```

### developerApps.delete(id)
アプリを削除します。

```ts
delete(id: string): Promise<{ success: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "アプリの ID です。" },
}}
/>

```ts
await client.developerApps.delete(appId)
```

ID が存在しない場合や、そのアプリがあなたのものではない場合は、`NotFoundError` をスローします。

### developerApps.rotateSecret(id)
新しいクライアントシークレットを作成し、古いものを即座に無効にします。新しいシークレットが返されるのは 1 回だけです。

```ts
rotateSecret(id: string): Promise<{ clientSecret: string }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "アプリの ID です。" },
}}
/>

```ts
const { clientSecret } = await client.developerApps.rotateSecret(appId)
```

古いシークレットでのコード交換は機能しなくなるため、サーバーのシークレットはすぐに更新してください。

## client.oauth
あなたのアプリのサーバーが呼び出す、OAuth 2.0 のエンドポイントです。フィールド名は、OAuth の標準と同じく snake_case です。

### oauth.exchangeCode(input)
同意後のリダイレクトから得た認可コードを、アクセストークンと交換します（`POST /v1/oauth/token`）。SDK が `grant_type: "authorization_code"` を自動的に追加します。

**ブラウザーから呼び出さないでください。**このリクエストにはクライアントシークレットが含まれており、サーバーに置いたままにする必要があります。

```ts
exchangeCode(input: {
client_id: string
client_secret: string
code: string
redirect_uri: string
}): Promise<{ access_token: string; token_type: "Bearer"; scope: string; expires_in: number }>
```

<TypeTable
type={{
client_id: { type: 'string', required: true, description: "あなたのアプリのクライアント ID です。" },
client_secret: { type: 'string', required: true, description: "あなたのアプリのクライアントシークレットです。" },
code: { type: 'string', required: true, description: "同意後のリダイレクトから得たコードです。" },
redirect_uri: { type: 'string', required: true, description: "コードの取得に使ったものと同じリダイレクト URI です。" },
}}
/>

```ts

const tokens = await client.oauth.exchangeCode({
client_id: process.env.NODARO_CLIENT_ID!,
client_secret: process.env.NODARO_CLIENT_SECRET!,
code: req.query.code as string,
redirect_uri: "https://example.com/oauth/callback",
})

// Act for the user who approved your app
const userClient = createClient({
baseUrl: "https://app.nodaro.ai",
auth: new StaticTokenAuth(tokens.access_token),
})
```

`scope` には、付与されたスコープが空白区切りで並び、`expires_in` はトークンの有効期間を秒単位で示します。

### oauth.revoke(token)
アクセストークンを取り消します（`POST /v1/oauth/revoke`、RFC 7009）。標準仕様により、トークンが有効だったかどうかを明かすことが禁じられているため、未知のトークンに対しても、常に `{ success: true }` を返します。

```ts
revoke(token: string): Promise<{ success: true }>
```

<TypeTable
type={{
token: { type: 'string', required: true, description: "取り消すアクセストークンです。" },
}}
/>

```ts
await client.oauth.revoke(accessToken)
```

### oauth.getAppInfo(clientId, redirectUri?)
同意画面用に、アプリの公開データを返します（`GET /v1/oauth/app-info`）。トークンは不要です。

```ts
getAppInfo(clientId: string, redirectUri?: string): Promise<OAuthAppInfo>
```

<TypeTable
type={{
clientId: { type: 'string', required: true, description: "アプリのクライアント ID です。" },
redirectUri: { type: 'string', description: "確認するリダイレクト URI です。指定すると、応答は、この URI が正確に登録されているかどうかを示します。" },
}}
/>

```ts
const info = await client.oauth.getAppInfo(clientId, "https://yourapp.com/oauth/callback")
if (!info.redirectUriRegistered) throw new Error("Unregistered redirect URI")
```

応答には、`name`、`description`、`logoUrl`、`homepageUrl`、`scopesRequested`、`redirectUriRegistered` が含まれます。`redirectUriRegistered` は、URI が正確に登録されている場合だけ `true` になり、`redirectUri` を渡さなかった場合は `null` になります。これにより、同意画面は、URI の一覧を明かすことなく、未登録のリダイレクトを拒否できます。

## Frequently asked questions

### SDK で OAuth アプリを登録するにはどうすればよいですか？

name、少なくとも 1 つのリダイレクト URI、必要なスコープを指定して client.developerApps.create を呼び出します。応答には clientId と clientSecret が含まれます。シークレットが表示されるのは 1 回だけなので、すぐに保管してください。

### ブラウザーで OAuth のコードを交換できますか？

できません。client.oauth.exchangeCode にはクライアントシークレットが必要で、これはサーバーに置いたままにする必要があります。交換はサーバーのコードで実行し、ブラウザーには必要なものだけを渡してください。

### OAuth アプリはいくつ登録できますか？

自分で登録するアプリは 5 つまでです。6 つ目の作成は 400 limit_reached で失敗します。MCP クライアントのように自己登録したアプリは、数に含まれません。

### クライアントシークレットを再発行すると、どうなりますか？

client.developerApps.rotateSecret は、新しいシークレットを作成し、古いシークレットを即座に無効にします。新しいシークレットが返されるのは 1 回だけです。
