# 画面設計書 15-APIキー管理

## 概要

本ドキュメントは、OpenCodeコンソールのAPIキー管理画面の設計書である。ワークスペースのAPIキーの作成、表示、削除を行う管理画面である。

### 本画面の処理概要

APIキー管理画面は、Zenサービスへのプログラマティックアクセスに使用するAPIキーを管理するための画面である。ユーザーはこの画面で新しいAPIキーを作成し、既存のキーを確認・削除することができる。作成直後のAPIキーはフルキーを表示し、クリップボードへのコピー機能を提供する。

**業務上の目的・背景**：OpenCode Zenサービスをコマンドラインツールや自動化スクリプトから利用する場合、APIキーによる認証が必要となる。本画面はAPIキーのライフサイクル管理（作成、表示、削除）を提供し、セキュアなAPI利用を実現する。

**画面へのアクセス方法**：ワークスペース詳細レイアウトのナビゲーションから「API Keys」を選択、または`/workspace/:id/keys`に直接アクセス。

**主要な操作・処理内容**：
1. APIキー一覧の表示（キー名、マスクされたキー値、作成者、最終使用日時）
2. 新規APIキーの作成（キー名を指定）
3. 作成直後のフルキーのクリップボードコピー
4. 既存APIキーの削除

**画面遷移**：本画面内で完結する操作が主。キー作成・削除後もページ内に留まる。

**権限による表示制御**：ワークスペースのメンバーであれば誰でもAPIキーを作成・削除可能。自分が作成したキー以外も削除可能。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 28 | 認証管理 | 主機能 | APIキーの作成・削除・表示 |

## 画面種別

一覧 / 登録 / 削除

## URL/ルーティング

| 環境 | URL |
|------|-----|
| 本番 | `https://opencode.ai/workspace/:id/keys` |
| 開発 | `http://localhost:3000/workspace/:id/keys` |

ルーティング定義: `packages/console/app/src/routes/workspace/[id]/keys/index.tsx`

## 入出力項目

### 入力項目（APIキー作成）

| 項目名 | 項目ID | データ型 | 必須 | 入力形式 | バリデーション | 備考 |
|--------|--------|----------|------|----------|----------------|------|
| キー名 | name | string | Yes | テキスト入力 | 必須チェック、空白トリム | プレースホルダーは国際化対応 |
| ワークスペースID | workspaceID | string | Yes | hidden | - | URLパラメータから設定 |

### 出力項目（APIキー一覧）

| 項目名 | データ型 | 表示条件 | 備考 |
|--------|----------|----------|------|
| キー名 | string | 常時 | key.name |
| キー値（マスク） | string | 常時 | key.keyDisplay（例: oai-•••••abc） |
| キー値（フル） | string | 作成直後のみ | key.key（コピー可能） |
| 作成者メール | string | 常時 | key.email |
| 最終使用日時 | Date | 使用履歴あり | key.timeUsed |

## 表示項目

### セクションタイトル

| 要素 | 内容 |
|------|------|
| タイトル | workspace.keys.title |
| サブタイトル | workspace.keys.subtitle |
| 作成ボタン | workspace.keys.create |

### 作成フォーム（条件付き表示: store.show=true）

| 要素 | コンポーネント | 説明 |
|------|---------------|------|
| キー名入力 | input[type=text] | フォーカス自動設定 |
| エラーメッセージ | div | submission.result.error時に表示 |
| キャンセルボタン | button | フォームを閉じる |
| 作成ボタン | button | APIキー作成を実行 |

### APIキー一覧テーブル

| カラム | キー | 表示条件 | 説明 |
|--------|------|----------|------|
| Name | workspace.keys.table.name | 常時 | キー名 |
| Key | workspace.keys.table.key | 常時 | マスク/フルキー表示 |
| Created By | workspace.keys.table.createdBy | 常時 | 作成者メール |
| Last Used | workspace.keys.table.lastUsed | 常時 | 最終使用日時または「-」 |
| Actions | - | 常時 | 削除ボタン |

### 空状態

| 要素 | 表示条件 | 内容 |
|------|----------|------|
| 空状態メッセージ | keys.length === 0 | workspace.keys.empty |

## イベント仕様

### 1-作成ボタンクリック（フォーム表示）

| 項目 | 内容 |
|------|------|
| トリガー | 「Create」ボタンクリック |
| 処理 | show()関数が実行される |
| 詳細フロー | 1. submission.clear()で前回結果をクリア<br>2. setStore("show", true)でフォーム表示<br>3. setTimeout()で入力フィールドにフォーカス |

### 2-APIキー作成（フォーム送信）

| 項目 | 内容 |
|------|------|
| トリガー | 作成フォームの「Create」ボタンクリック |
| 処理 | createKey actionが実行される |
| 詳細フロー | 1. [SERVER] name取得、空白トリム、必須チェック<br>2. [SERVER] Key.create()でAPIキー作成<br>3. 成功時: フォームを閉じ、一覧を再取得<br>4. 失敗時: エラーメッセージを表示 |

### 3-キーコピー

| 項目 | 内容 |
|------|------|
| トリガー | キー値ボタンクリック（作成直後のフルキー表示時） |
| 処理 | navigator.clipboard.writeText()でコピー |
| 結果 | copiedをtrueに設定、1秒後にfalseに戻す |

### 4-APIキー削除

| 項目 | 内容 |
|------|------|
| トリガー | 一覧の「Delete」ボタンクリック |
| 処理 | removeKey actionが実行される |
| 詳細フロー | 1. [SERVER] id取得、必須チェック<br>2. [SERVER] Key.remove()でAPIキー削除<br>3. 一覧を再取得（revalidate: listKeys.key） |

## データベース更新仕様

### 操作別データベース影響一覧

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| ページロード | KeyTable | SELECT | APIキー一覧の取得 |
| キー作成 | KeyTable | INSERT | 新規APIキーの登録 |
| キー削除 | KeyTable | DELETE | APIキーの削除 |

### テーブル別更新項目詳細

#### listKeys クエリ

```typescript
const listKeys = query(async (workspaceID: string) => {
  return withActor(() => Key.list(), workspaceID)
}, "key.list")
```

#### createKey アクション

```typescript
const createKey = action(async (form: FormData) => {
  const name = form.get("name")?.toString().trim()
  if (!name) return { error: formError.nameRequired }
  return Key.create({
    userID: Actor.assert("user").properties.userID,
    name,
  })
}, "key.create")
```

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|--------|------|
| INSERT | name | フォーム入力値 | 空白トリム済み |
| INSERT | userID | 現在のユーザーID | Actor.assert("user").properties.userID |

#### removeKey アクション

```typescript
const removeKey = action(async (form: FormData) => {
  const id = form.get("id")?.toString()
  if (!id) return { error: formError.idRequired }
  return Key.remove({ id })
}, "key.remove")
```

| 操作 | 項目 | 削除条件 | 備考 |
|-----|------|---------|------|
| DELETE | - | id一致 | 論理削除の可能性あり |

## メッセージ仕様

| メッセージID | メッセージ種別 | 表示条件 | メッセージ内容（キー） |
|-------------|---------------|----------|----------------------|
| title | タイトル | 常時 | workspace.keys.title |
| subtitle | サブタイトル | 常時 | workspace.keys.subtitle |
| create | ボタン | 常時 | workspace.keys.create |
| placeholder | プレースホルダー | フォーム表示時 | workspace.keys.placeholder |
| empty | 空状態 | キーなし | workspace.keys.empty |
| delete | ボタン | 一覧表示時 | workspace.keys.delete |
| copyApiKey | ツールチップ | キー表示時 | workspace.keys.copyApiKey |
| creating | ボタン | 作成中 | common.creating |
| cancel | ボタン | フォーム表示時 | common.cancel |

## 例外処理

| 例外パターン | 検出方法 | 対応処理 | ユーザーへの通知 |
|------------|----------|---------|----------------|
| キー名未入力 | サーバーサイドバリデーション | { error: formError.nameRequired } | フォーム内エラー表示 |
| ワークスペースID未指定 | サーバーサイドバリデーション | { error: formError.workspaceRequired } | フォーム内エラー表示 |
| キーID未指定 | サーバーサイドバリデーション | { error: formError.idRequired } | （通常発生しない） |
| キー作成エラー | Key.create()のcatch | { error: e.message } | フォーム内エラー表示 |

## 備考

- APIキーのフルキーは作成直後のみ表示可能（セキュリティ上の理由）
- クリップボードコピー後、1秒間チェックアイコンに切り替わる
- formatDateForTable、formatDateUTCで日付フォーマット
- submission.pendingで作成中の二重クリック防止
- CSSモジュール（key-section.module.css）でスタイリング

---

## コードリーディングガイド

本画面を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

本コンポーネントでは3つのサーバーサイド関数を使用する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | key-section.tsx | `packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx` | listKeys（43-46行目）、createKey（22-41行目）、removeKey（13-20行目） |
| 1-2 | common.tsx | `packages/console/app/src/routes/workspace/common.tsx` | formatDateForTable、formatDateUTCユーティリティ |

**読解のコツ**: actionとqueryはSolidStartのデータミューテーションとフェッチング。revalidate: listKeys.keyで作成・削除後に一覧を再取得。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | index.tsx | `packages/console/app/src/routes/workspace/[id]/keys/index.tsx` | KeySectionコンポーネントの配置 |
| 2-2 | key-section.tsx | `packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx` | KeySection関数コンポーネント（48-179行目） |

**主要処理フロー**:
1. **49行目**: useParams()でワークスペースID取得
2. **51行目**: createAsync()でAPIキー一覧取得
3. **52行目**: useSubmission()で作成アクションの状態監視
4. **53行目**: createStore()でフォーム表示状態管理
5. **57-61行目**: createEffect()で作成成功時にフォームを閉じる
6. **63-70行目**: show()関数 - フォーム表示
7. **72-74行目**: hide()関数 - フォーム非表示
8. **76-178行目**: JSXによるUI描画

#### Step 3: コピー機能を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | key-section.tsx | `packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx` | コピーボタン処理（144-147行目） |

**読解のコツ**: navigator.clipboard.writeText()は非同期API。copiedシグナルでUIフィードバック（IconCopy → IconCheck）。

### プログラム呼び出し階層図

```
KeysPage (index.tsx)
    |
    +-- <KeySection />

KeySection (key-section.tsx)
    |
    +-- useParams() -> params.id取得
    |
    +-- createAsync(listKeys)
    |       +-- [SERVER] withActor()
    |               +-- Key.list() -> APIキー一覧
    |
    +-- useSubmission(createKey) -> submission状態
    |
    +-- createStore() -> { show: false }
    |
    +-- createEffect() -> 作成成功時にフォームを閉じる
    |
    +-- show() -> フォーム表示
    |
    +-- hide() -> フォーム非表示
    |
    +-- <form action={createKey}>
    |       +-- [SERVER] Key.create({ userID, name })
    |       +-- revalidate: listKeys.key
    |
    +-- <For each={keys()}>
            +-- createSignal(false) -> copied状態
            +-- コピーボタン -> navigator.clipboard.writeText()
            +-- <form action={removeKey}>
                    +-- [SERVER] Key.remove({ id })
                    +-- revalidate: listKeys.key
```

### データフロー図

```
[入力]                    [処理]                         [出力]

ページロード ─────────> listKeys(workspaceID)
                              |
                              v
                        [SERVER] Key.list()
                              |
                              v
                        APIキー一覧 ────────────────> テーブル表示

キー名入力 ───────────> createKey action
                              |
                              v
                        [SERVER] Key.create()
                              |
                              v
                        revalidate listKeys ─────────> テーブル再描画
                              |
                              v
                        新規キー（フルキー表示） ────> コピーボタン有効

削除ボタン ───────────> removeKey action
                              |
                              v
                        [SERVER] Key.remove()
                              |
                              v
                        revalidate listKeys ─────────> テーブル再描画
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.tsx | `packages/console/app/src/routes/workspace/[id]/keys/index.tsx` | ソース | ページエントリーポイント |
| key-section.tsx | `packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx` | ソース | メインコンポーネント |
| key-section.module.css | `packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css` | スタイル | セクション固有スタイル |
| common.tsx | `packages/console/app/src/routes/workspace/common.tsx` | ソース | 日付フォーマットユーティリティ |
| icon.tsx | `packages/console/app/src/component/icon.tsx` | ソース | IconCopy, IconCheckコンポーネント |
| form-error.ts | `packages/console/app/src/lib/form-error.ts` | ソース | エラーメッセージ定義、localizeError |
| key.js | `packages/console-core/src/key.js` | ソース | Keyドメインロジック |
| actor.js | `packages/console-core/src/actor.js` | ソース | アクター管理 |
