# API設計書

## 概要

本ドキュメントは、Decap CMS（旧Netlify CMS）プロジェクトが提供・利用するAPIの設計書である。Decap CMSはGitベースのヘッドレスCMSであり、以下の2つのカテゴリのAPIを持つ。

1. **Decap Server Proxy API** — ローカル開発用プロキシサーバーが提供するRESTful API（Express.js）
2. **Netlify Function（Slack連携）** — Slackスラッシュコマンドからの公開トリガー用サーバーレス関数

また、CMSバックエンドとして外部Gitホスティングサービス（GitHub、GitLab、Bitbucket、Gitea、Azure DevOps）のAPIを利用する。本ドキュメントではプロジェクトが提供するAPIと、利用する外部APIの両方を記載する。

## 共通仕様

### ベースURL

```
# Decap Server Proxy API
http://localhost:8081

# Netlify Function（Slack連携）
https://{netlify-site}.netlify.app/.netlify/functions/publish
```

### 認証方式

| API | 認証方式 | 説明 |
| --- | --- | --- |
| Decap Server Proxy API | 不要 | ローカル開発用のため認証なし。CORSで制御 |
| Netlify Function | Slack署名検証 | `X-Slack-Signature`ヘッダーによるHMAC-SHA256署名検証 |
| GitHub API | Bearer Token | `Authorization: token {TOKEN}` |
| GitLab API | Private-Token / Bearer | `Authorization: Private-Token {TOKEN}` または `Bearer {TOKEN}` |
| Bitbucket API | Bearer Token | `Authorization: Bearer {TOKEN}` |
| Gitea API | Token | `Authorization: token {TOKEN}` |
| Azure DevOps API | Bearer Token | `Authorization: Bearer {TOKEN}` |

### 共通ヘッダー

| ヘッダー名 | 必須 | 説明 |
| --- | --- | --- |
| Content-Type | ○ | `application/json` （Proxy API）/ `application/x-www-form-urlencoded`（Slack） |
| Accept | - | `application/json` |

### 共通エラーレスポンス

| ステータスコード | 説明 |
| --- | --- |
| 401 | Unauthorized - 認証エラー（Slack署名検証失敗等） |
| 404 | Not Found - リソース未検出 |
| 422 | Unprocessable Entity - バリデーションエラー / 不明なアクション |
| 500 | Internal Server Error - サーバーエラー |

## API一覧

### Decap Server Proxy API

| カテゴリ | エンドポイント | メソッド | アクション | 説明 |
| --- | --- | --- | --- | --- |
| システム情報 | /api/v1 | POST | `info` | サーバー情報の取得 |
| エントリー管理 | /api/v1 | POST | `entriesByFolder` | フォルダ内エントリー一覧取得 |
| エントリー管理 | /api/v1 | POST | `entriesByFiles` | 指定ファイルのエントリー取得 |
| エントリー管理 | /api/v1 | POST | `getEntry` | 単一エントリー取得 |
| エントリー管理 | /api/v1 | POST | `persistEntry` | エントリーの保存 |
| ワークフロー | /api/v1 | POST | `unpublishedEntries` | 未公開エントリー一覧取得 |
| ワークフロー | /api/v1 | POST | `unpublishedEntry` | 未公開エントリー詳細取得 |
| ワークフロー | /api/v1 | POST | `unpublishedEntryDataFile` | 未公開エントリーデータファイル取得 |
| ワークフロー | /api/v1 | POST | `unpublishedEntryMediaFile` | 未公開エントリーメディアファイル取得 |
| ワークフロー | /api/v1 | POST | `deleteUnpublishedEntry` | 未公開エントリー削除 |
| ワークフロー | /api/v1 | POST | `updateUnpublishedEntryStatus` | 未公開エントリーステータス更新 |
| ワークフロー | /api/v1 | POST | `publishUnpublishedEntry` | 未公開エントリー公開 |
| メディア管理 | /api/v1 | POST | `getMedia` | メディアファイル一覧取得 |
| メディア管理 | /api/v1 | POST | `getMediaFile` | メディアファイル取得 |
| メディア管理 | /api/v1 | POST | `persistMedia` | メディアファイル保存 |
| ファイル管理 | /api/v1 | POST | `deleteFile` | 単一ファイル削除 |
| ファイル管理 | /api/v1 | POST | `deleteFiles` | 複数ファイル削除 |
| デプロイ | /api/v1 | POST | `getDeployPreview` | デプロイプレビュー取得 |

### Netlify Function

| カテゴリ | エンドポイント | メソッド | 説明 |
| --- | --- | --- | --- |
| Slack連携 | /.netlify/functions/publish | POST | Slackスラッシュコマンドによる公開トリガー |

### 外部API（バックエンドが利用）

| カテゴリ | サービス | 主なエンドポイント | 説明 |
| --- | --- | --- | --- |
| GitHub | GitHub REST API v3 | /repos/{owner}/{repo}/* | リポジトリ・コンテンツ・PR操作 |
| GitHub | GitHub GraphQL API v4 | /graphql | 効率的なデータ取得 |
| GitLab | GitLab REST API v4 | /projects/{id}/* | プロジェクト・ファイル・MR操作 |
| Bitbucket | Bitbucket REST API 2.0 | /repositories/{repo}/* | リポジトリ・PR操作 |
| Gitea | Gitea REST API v1 | /repos/{owner}/{repo}/* | リポジトリ・PR操作 |
| Azure DevOps | Azure DevOps REST API | /{org}/{project}/_apis/git/* | リポジトリ・コミット・PR操作 |

---

## 各APIエンドポイント定義

### Decap Server Proxy API — システム情報

#### 1. サーバー情報取得（info）

サーバーの基本情報（リポジトリ名、サポートする公開モード、サーバータイプ）を返す。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `info` |
| 認証 | 不要 |
| 権限 | なし |
| 対応モード | Local FS / Local Git |

**リクエストボディ**

```json
{
  "action": "info"
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| action | string | ○ | `"info"` | 実行するアクション |

**レスポンス（成功時 — Local Gitモード）**

ステータスコード: `200 OK`

```json
{
  "repo": "my-repository",
  "publish_modes": ["simple", "editorial_workflow"],
  "type": "local_git"
}
```

**レスポンス（成功時 — Local FSモード）**

ステータスコード: `200 OK`

```json
{
  "repo": "my-repository",
  "publish_modes": ["simple"],
  "type": "local_fs"
}
```

| フィールド名 | 型 | 説明 |
| --- | --- | --- |
| repo | string | リポジトリディレクトリ名 |
| publish_modes | string[] | サポートする公開モード |
| type | string | サーバータイプ（`local_git` または `local_fs`） |

---

### Decap Server Proxy API — エントリー管理

#### 2. フォルダ内エントリー一覧取得（entriesByFolder）

指定フォルダ内のエントリー（コンテンツファイル）を一覧で取得する。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `entriesByFolder` |
| 認証 | 不要 |
| 権限 | なし |

**リクエストボディ**

```json
{
  "action": "entriesByFolder",
  "params": {
    "branch": "main",
    "folder": "content/posts",
    "extension": "md",
    "depth": 1
  }
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| action | string | ○ | `"entriesByFolder"` | 実行するアクション |
| params.branch | string | ○ | - | 対象ブランチ名 |
| params.folder | string | ○ | パストラバーサル検証 | 対象フォルダパス |
| params.extension | string | ○ | - | ファイル拡張子フィルタ |
| params.depth | number | ○ | - | 探索の深さ |

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
[
  {
    "data": "---\ntitle: Hello World\n---\nContent here...",
    "file": {
      "path": "content/posts/hello-world.md",
      "label": null,
      "id": "a1b2c3d4e5f6..."
    }
  }
]
```

| フィールド名 | 型 | 説明 |
| --- | --- | --- |
| [].data | string | ファイルの生コンテンツ |
| [].file.path | string | ファイルパス |
| [].file.label | string\|null | ラベル |
| [].file.id | string\|null | コンテンツのSHA-256ハッシュ |

**レスポンス（エラー時 — ブランチ不存在）**

ステータスコード: `422 Unprocessable Entity`

```json
{
  "error": "Default branch 'main' doesn't exist"
}
```

---

#### 3. 指定ファイルのエントリー取得（entriesByFiles）

指定したファイルパスのエントリーを取得する。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `entriesByFiles` |
| 認証 | 不要 |
| 権限 | なし |

**リクエストボディ**

```json
{
  "action": "entriesByFiles",
  "params": {
    "branch": "main",
    "files": [
      { "path": "content/posts/hello.md" },
      { "path": "content/posts/world.md", "label": "World Post" }
    ]
  }
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| action | string | ○ | `"entriesByFiles"` | 実行するアクション |
| params.branch | string | ○ | - | 対象ブランチ名 |
| params.files | object[] | ○ | - | 取得対象ファイルの配列 |
| params.files[].path | string | ○ | パストラバーサル検証 | ファイルパス |
| params.files[].label | string | - | - | オプションのラベル |

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
[
  {
    "data": "---\ntitle: Hello\n---\nContent...",
    "file": {
      "path": "content/posts/hello.md",
      "label": null,
      "id": "a1b2c3d4..."
    }
  }
]
```

---

#### 4. 単一エントリー取得（getEntry）

指定パスの単一エントリーを取得する。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `getEntry` |
| 認証 | 不要 |
| 権限 | なし |

**リクエストボディ**

```json
{
  "action": "getEntry",
  "params": {
    "branch": "main",
    "path": "content/posts/hello.md"
  }
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| action | string | ○ | `"getEntry"` | 実行するアクション |
| params.branch | string | ○ | - | 対象ブランチ名 |
| params.path | string | ○ | パストラバーサル検証 | ファイルパス |

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
{
  "data": "---\ntitle: Hello\n---\nContent...",
  "file": {
    "path": "content/posts/hello.md",
    "label": null,
    "id": "a1b2c3d4..."
  }
}
```

---

#### 5. エントリー保存（persistEntry）

エントリーとアセットをリポジトリに保存する。ワークフローモード時はCMS用ブランチに保存する。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `persistEntry` |
| 認証 | 不要 |
| 権限 | なし |
| 対応モード | Local FS（simpleのみ）/ Local Git（simple + editorial_workflow） |

**リクエストボディ（simpleモード）**

```json
{
  "action": "persistEntry",
  "params": {
    "branch": "main",
    "dataFiles": [
      {
        "slug": "hello-world",
        "path": "content/posts/hello-world.md",
        "raw": "---\ntitle: Hello World\n---\nContent here..."
      }
    ],
    "assets": [
      {
        "path": "static/images/photo.jpg",
        "content": "base64encodedcontent...",
        "encoding": "base64"
      }
    ],
    "options": {
      "commitMessage": "Create post hello-world",
      "useWorkflow": false,
      "status": "draft"
    }
  }
}
```

**リクエストボディ（editorial_workflowモード）**

```json
{
  "action": "persistEntry",
  "params": {
    "branch": "main",
    "cmsLabelPrefix": "cms/",
    "dataFiles": [
      {
        "slug": "hello-world",
        "path": "content/posts/hello-world.md",
        "raw": "---\ntitle: Hello World\n---\nContent here..."
      }
    ],
    "assets": [],
    "options": {
      "collectionName": "posts",
      "commitMessage": "Create post hello-world",
      "useWorkflow": true,
      "status": "draft"
    }
  }
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| action | string | ○ | `"persistEntry"` | 実行するアクション |
| params.branch | string | ○ | - | 対象ブランチ名 |
| params.cmsLabelPrefix | string | - | - | CMSラベルのプレフィックス |
| params.entry | object | △ | dataFilesと排他 | 後方互換用の単一エントリー |
| params.dataFiles | object[] | △ | entryと排他 | データファイルの配列 |
| params.dataFiles[].slug | string | ○ | - | エントリーのスラッグ |
| params.dataFiles[].path | string | ○ | パストラバーサル検証 | ファイルパス |
| params.dataFiles[].raw | string | ○ | - | ファイルの生コンテンツ |
| params.dataFiles[].newPath | string | - | パストラバーサル検証 | リネーム先パス |
| params.assets | object[] | ○ | - | アセットの配列 |
| params.assets[].path | string | ○ | パストラバーサル検証 | アセットのパス |
| params.assets[].content | string | ○ | - | Base64エンコードコンテンツ |
| params.assets[].encoding | string | ○ | `"base64"` | エンコーディング |
| params.options.collectionName | string | - | - | コレクション名（ワークフロー時必要） |
| params.options.commitMessage | string | ○ | - | コミットメッセージ |
| params.options.useWorkflow | boolean | ○ | - | ワークフロー使用フラグ |
| params.options.status | string | ○ | - | ワークフローステータス |

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
{
  "message": "entry persisted"
}
```

---

### Decap Server Proxy API — ワークフロー管理

> ワークフロー関連APIは **Local Gitモード**（`MODE=git`）でのみ使用可能。Local FSモードでは未サポート。

#### 6. 未公開エントリー一覧取得（unpublishedEntries）

CMSブランチプレフィックス（`cms/`）で始まるブランチの一覧を返す。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `unpublishedEntries` |
| 認証 | 不要 |
| 権限 | なし |
| 対応モード | Local Git のみ |

**リクエストボディ**

```json
{
  "action": "unpublishedEntries",
  "params": {
    "branch": "main"
  }
}
```

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
["posts/hello-world", "pages/about"]
```

---

#### 7. 未公開エントリー詳細取得（unpublishedEntry）

特定の未公開エントリーの差分、ステータス、更新日時を取得する。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `unpublishedEntry` |
| 認証 | 不要 |
| 権限 | なし |
| 対応モード | Local Git のみ |

**リクエストボディ**

```json
{
  "action": "unpublishedEntry",
  "params": {
    "branch": "main",
    "id": "posts/hello-world",
    "collection": "posts",
    "slug": "hello-world",
    "cmsLabelPrefix": "cms/"
  }
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| params.branch | string | ○ | - | デフォルトブランチ名 |
| params.id | string | - | - | コンテンツキー（`collection/slug`形式） |
| params.collection | string | - | - | コレクション名（idなしの場合） |
| params.slug | string | - | - | スラッグ（idなしの場合） |
| params.cmsLabelPrefix | string | - | - | CMSラベルプレフィックス |

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
{
  "collection": "posts",
  "slug": "hello-world",
  "status": "draft",
  "diffs": [
    {
      "oldPath": "",
      "newPath": "content/posts/hello-world.md",
      "status": "added",
      "newFile": true,
      "path": "content/posts/hello-world.md",
      "id": "content/posts/hello-world.md",
      "binary": false
    }
  ],
  "updatedAt": "2026-02-25T10:00:00.000Z"
}
```

**レスポンス（エラー時 — 未検出）**

ステータスコード: `404 Not Found`

```json
{
  "message": "Not Found"
}
```

---

#### 8. 未公開エントリーデータファイル取得（unpublishedEntryDataFile）

未公開エントリーのデータファイル内容を取得する。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `unpublishedEntryDataFile` |
| 認証 | 不要 |
| 権限 | なし |
| 対応モード | Local Git のみ |

**リクエストボディ**

```json
{
  "action": "unpublishedEntryDataFile",
  "params": {
    "branch": "main",
    "collection": "posts",
    "slug": "hello-world",
    "id": "posts/hello-world",
    "path": "content/posts/hello-world.md"
  }
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| params.branch | string | ○ | - | デフォルトブランチ名 |
| params.collection | string | ○ | - | コレクション名 |
| params.slug | string | ○ | - | スラッグ |
| params.id | string | ○ | - | コンテンツキー |
| params.path | string | ○ | パストラバーサル検証 | ファイルパス |

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
{
  "data": "---\ntitle: Hello World\n---\nContent here..."
}
```

---

#### 9. 未公開エントリーメディアファイル取得（unpublishedEntryMediaFile）

未公開エントリーに関連するメディアファイルを取得する。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `unpublishedEntryMediaFile` |
| 認証 | 不要 |
| 権限 | なし |
| 対応モード | Local Git のみ |

**リクエストボディ**

```json
{
  "action": "unpublishedEntryMediaFile",
  "params": {
    "branch": "main",
    "collection": "posts",
    "slug": "hello-world",
    "id": "posts/hello-world",
    "path": "static/images/photo.jpg"
  }
}
```

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
{
  "id": "sha256hash...",
  "content": "base64encodedcontent...",
  "encoding": "base64",
  "path": "static/images/photo.jpg",
  "name": "photo.jpg"
}
```

---

#### 10. 未公開エントリー削除（deleteUnpublishedEntry）

未公開エントリーに対応するCMSブランチを削除する。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `deleteUnpublishedEntry` |
| 認証 | 不要 |
| 権限 | なし |
| 対応モード | Local Git のみ |

**リクエストボディ**

```json
{
  "action": "deleteUnpublishedEntry",
  "params": {
    "branch": "main",
    "collection": "posts",
    "slug": "hello-world"
  }
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| params.branch | string | ○ | - | デフォルトブランチ名 |
| params.collection | string | ○ | - | コレクション名 |
| params.slug | string | ○ | - | スラッグ |

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
{
  "message": "deleted branch: cms/posts/hello-world"
}
```

---

#### 11. 未公開エントリーステータス更新（updateUnpublishedEntryStatus）

未公開エントリーのワークフローステータスを更新する。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `updateUnpublishedEntryStatus` |
| 認証 | 不要 |
| 権限 | なし |
| 対応モード | Local Git のみ |

**リクエストボディ**

```json
{
  "action": "updateUnpublishedEntryStatus",
  "params": {
    "branch": "main",
    "collection": "posts",
    "slug": "hello-world",
    "newStatus": "pending_review",
    "cmsLabelPrefix": "cms/"
  }
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| params.branch | string | ○ | - | デフォルトブランチ名 |
| params.collection | string | ○ | - | コレクション名 |
| params.slug | string | ○ | - | スラッグ |
| params.newStatus | string | ○ | - | 新しいステータス（`draft`, `pending_review`, `pending_publish`） |
| params.cmsLabelPrefix | string | - | - | CMSラベルプレフィックス |

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
{
  "message": "main description was updated to cms/pending_review"
}
```

---

#### 12. 未公開エントリー公開（publishUnpublishedEntry）

未公開エントリーのCMSブランチをメインブランチにマージし、CMSブランチを削除する。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `publishUnpublishedEntry` |
| 認証 | 不要 |
| 権限 | なし |
| 対応モード | Local Git のみ |

**リクエストボディ**

```json
{
  "action": "publishUnpublishedEntry",
  "params": {
    "branch": "main",
    "collection": "posts",
    "slug": "hello-world"
  }
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| params.branch | string | ○ | - | マージ先ブランチ名 |
| params.collection | string | ○ | - | コレクション名 |
| params.slug | string | ○ | - | スラッグ |

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
{
  "message": "branch cms/posts/hello-world merged to main"
}
```

---

### Decap Server Proxy API — メディア管理

#### 13. メディアファイル一覧取得（getMedia）

指定メディアフォルダ内のファイル一覧を取得する。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `getMedia` |
| 認証 | 不要 |
| 権限 | なし |

**リクエストボディ**

```json
{
  "action": "getMedia",
  "params": {
    "branch": "main",
    "mediaFolder": "static/images"
  }
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| params.branch | string | ○ | - | 対象ブランチ名 |
| params.mediaFolder | string | ○ | パストラバーサル検証 | メディアフォルダのパス |

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
[
  {
    "id": "sha256hash...",
    "content": "base64encodedcontent...",
    "encoding": "base64",
    "path": "static/images/photo.jpg",
    "name": "photo.jpg"
  }
]
```

| フィールド名 | 型 | 説明 |
| --- | --- | --- |
| [].id | string | コンテンツのSHA-256ハッシュ |
| [].content | string | Base64エンコードされたファイル内容 |
| [].encoding | string | エンコーディング方式（`"base64"`） |
| [].path | string | ファイルパス |
| [].name | string | ファイル名 |

---

#### 14. メディアファイル取得（getMediaFile）

単一のメディアファイルを取得する。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `getMediaFile` |
| 認証 | 不要 |
| 権限 | なし |

**リクエストボディ**

```json
{
  "action": "getMediaFile",
  "params": {
    "branch": "main",
    "path": "static/images/photo.jpg"
  }
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| params.branch | string | ○ | - | 対象ブランチ名 |
| params.path | string | ○ | パストラバーサル検証 | ファイルパス |

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
{
  "id": "sha256hash...",
  "content": "base64encodedcontent...",
  "encoding": "base64",
  "path": "static/images/photo.jpg",
  "name": "photo.jpg"
}
```

---

#### 15. メディアファイル保存（persistMedia）

メディアファイルをリポジトリに保存する。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `persistMedia` |
| 認証 | 不要 |
| 権限 | なし |

**リクエストボディ**

```json
{
  "action": "persistMedia",
  "params": {
    "branch": "main",
    "asset": {
      "path": "static/images/new-photo.jpg",
      "content": "base64encodedcontent...",
      "encoding": "base64"
    },
    "options": {
      "commitMessage": "Upload new-photo.jpg"
    }
  }
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| params.branch | string | ○ | - | 対象ブランチ名 |
| params.asset.path | string | ○ | パストラバーサル検証 | アセットのパス |
| params.asset.content | string | ○ | - | Base64エンコードコンテンツ |
| params.asset.encoding | string | ○ | `"base64"` | エンコーディング |
| params.options.commitMessage | string | ○ | - | コミットメッセージ |

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
{
  "id": "sha256hash...",
  "content": "base64encodedcontent...",
  "encoding": "base64",
  "path": "static/images/new-photo.jpg",
  "name": "new-photo.jpg"
}
```

---

### Decap Server Proxy API — ファイル管理

#### 16. 単一ファイル削除（deleteFile）

指定ファイルを削除しコミットする。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `deleteFile` |
| 認証 | 不要 |
| 権限 | なし |

**リクエストボディ**

```json
{
  "action": "deleteFile",
  "params": {
    "branch": "main",
    "path": "content/posts/old-post.md",
    "options": {
      "commitMessage": "Delete old-post"
    }
  }
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| params.branch | string | ○ | - | 対象ブランチ名 |
| params.path | string | ○ | パストラバーサル検証 | 削除対象ファイルパス |
| params.options.commitMessage | string | ○ | - | コミットメッセージ |

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
{
  "message": "deleted file content/posts/old-post.md"
}
```

---

#### 17. 複数ファイル削除（deleteFiles）

複数のファイルを一括削除しコミットする。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `deleteFiles` |
| 認証 | 不要 |
| 権限 | なし |

**リクエストボディ**

```json
{
  "action": "deleteFiles",
  "params": {
    "branch": "main",
    "paths": [
      "content/posts/old-post-1.md",
      "content/posts/old-post-2.md"
    ],
    "options": {
      "commitMessage": "Delete old posts"
    }
  }
}
```

| フィールド名 | 型 | 必須 | バリデーション | 説明 |
| --- | --- | --- | --- | --- |
| params.branch | string | ○ | - | 対象ブランチ名 |
| params.paths | string[] | ○ | 最低1件、パストラバーサル検証 | 削除対象ファイルパスの配列 |
| params.options.commitMessage | string | ○ | - | コミットメッセージ |

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
{
  "message": "deleted files content/posts/old-post-1.md, content/posts/old-post-2.md"
}
```

---

### Decap Server Proxy API — デプロイ

#### 18. デプロイプレビュー取得（getDeployPreview）

デプロイプレビューの情報を返す。ローカルサーバーでは常にnullを返す。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /api/v1` |
| アクション | `getDeployPreview` |
| 認証 | 不要 |
| 権限 | なし |

**リクエストボディ**

```json
{
  "action": "getDeployPreview",
  "params": {
    "branch": "main",
    "collection": "posts",
    "slug": "hello-world"
  }
}
```

**レスポンス（成功時）**

ステータスコード: `200 OK`

```json
null
```

---

### Netlify Function — Slack連携

#### 19. Slackスラッシュコマンドによる公開トリガー

Slackスラッシュコマンドを受信し、GitHub Actionsのリポジトリディスパッチイベントを発火する。

**基本情報**

| 項目 | 内容 |
| --- | --- |
| エンドポイント | `POST /.netlify/functions/publish` |
| 認証 | Slack署名検証（HMAC-SHA256） |
| 権限 | 環境変数`ALLOWED_USERS`で指定されたSlackユーザーIDのみ |

**リクエスト**

Slackスラッシュコマンドの標準形式（`application/x-www-form-urlencoded`）。

**リクエストヘッダー**

| ヘッダー名 | 必須 | 説明 |
| --- | --- | --- |
| X-Slack-Request-Timestamp | ○ | リクエストのタイムスタンプ（UNIX秒） |
| X-Slack-Signature | ○ | Slack署名（`v0=hmac-sha256`形式） |
| Content-Type | ○ | `application/x-www-form-urlencoded` |

**リクエストボディパラメータ**

| パラメータ名 | 型 | 必須 | 説明 |
| --- | --- | --- | --- |
| command | string | ○ | スラッシュコマンド名 |
| user_id | string | ○ | SlackユーザーID |
| user_name | string | - | Slackユーザー名（エラーメッセージ用） |

**環境変数**

| 変数名 | 必須 | 説明 |
| --- | --- | --- |
| SLACK_SIGNING_SECRET | ○ | Slack署名検証用シークレット |
| PUBLISH_COMMAND | ○ | 許可するスラッシュコマンド名 |
| ALLOWED_USERS | ○ | 許可するユーザーID（カンマ区切り） |
| GITHUB_TOKEN | ○ | GitHub APIトークン |
| GITHUB_REPO | ○ | 対象リポジトリ（`owner/repo`形式） |

**レスポンス（成功時）**

ステータスコード: `200`

```
Dispatched event to GitHub
```

**レスポンス（エラー時）**

ステータスコード: `401`

```
Unauthorized
```

**内部動作: GitHub API呼び出し**

成功時に以下のGitHub APIを呼び出す。

```
POST https://api.github.com/repos/{owner}/{repo}/dispatches
Authorization: token {GITHUB_TOKEN}
Content-Type: application/json

{
  "event_type": "on-demand-github-action"
}
```

---

### 外部API — GitHub REST API

> Decap CMSの`decap-cms-backend-github`パッケージが利用するGitHub API。

#### Git操作

| メソッド | エンドポイント | 説明 |
| --- | --- | --- |
| GET | `/repos/{owner}/{repo}` | リポジトリ情報取得 |
| GET | `/user` | 認証ユーザー情報取得 |
| GET | `/repos/{owner}/{repo}/branches/{branch}` | ブランチ情報取得 |
| POST | `/repos/{owner}/{repo}/git/refs` | Gitリファレンス作成 |
| PATCH | `/repos/{owner}/{repo}/git/refs/{ref}` | Gitリファレンス更新 |
| GET | `/repos/{owner}/{repo}/git/trees/{tree_sha}` | Gitツリー取得 |
| POST | `/repos/{owner}/{repo}/git/trees` | Gitツリー作成 |
| GET | `/repos/{owner}/{repo}/git/blobs/{blob_sha}` | Gitブロブ取得 |
| POST | `/repos/{owner}/{repo}/git/blobs` | Gitブロブ作成 |
| POST | `/repos/{owner}/{repo}/git/commits` | コミット作成 |
| GET | `/repos/{owner}/{repo}/commits` | コミット一覧取得 |
| GET | `/repos/{owner}/{repo}/compare/{base}...{head}` | コミット比較 |

#### Pull Request操作

| メソッド | エンドポイント | 説明 |
| --- | --- | --- |
| POST | `/repos/{owner}/{repo}/pulls` | PR作成 |
| GET | `/repos/{owner}/{repo}/pulls` | PR一覧取得 |
| PATCH | `/repos/{owner}/{repo}/pulls/{number}` | PR更新 |
| PUT | `/repos/{owner}/{repo}/pulls/{number}/merge` | PRマージ |
| GET | `/repos/{owner}/{repo}/pulls/{number}/commits` | PRコミット取得 |
| POST | `/repos/{owner}/{repo}/issues/{number}/labels` | ラベル追加 |
| GET | `/repos/{owner}/{repo}/commits/{ref}/statuses` | コミットステータス取得 |

#### GraphQL API

| メソッド | エンドポイント | 説明 |
| --- | --- | --- |
| POST | `/graphql` | GraphQLクエリ実行（Apollo Client使用） |

---

### 外部API — GitLab REST API

> Decap CMSの`decap-cms-backend-gitlab`パッケージが利用するGitLab API v4。

#### ファイル・ブランチ操作

| メソッド | エンドポイント | 説明 |
| --- | --- | --- |
| GET | `/projects/{id}` | プロジェクト情報取得 |
| GET | `/projects/{id}/repository/branches` | ブランチ一覧取得 |
| GET | `/projects/{id}/repository/files/{path}/raw` | ファイル内容取得 |
| POST | `/projects/{id}/repository/files/{path}` | ファイル作成・更新 |
| DELETE | `/projects/{id}/repository/files/{path}` | ファイル削除 |
| GET | `/projects/{id}/repository/tree` | ディレクトリツリー取得 |
| POST | `/projects/{id}/repository/commits` | コミット作成 |
| GET | `/projects/{id}/repository/compare` | リビジョン比較 |

#### Merge Request操作

| メソッド | エンドポイント | 説明 |
| --- | --- | --- |
| POST | `/projects/{id}/merge_requests` | MR作成 |
| GET | `/projects/{id}/merge_requests` | MR一覧取得 |
| PATCH | `/projects/{id}/merge_requests/{iid}` | MR更新 |
| PUT | `/projects/{id}/merge_requests/{iid}/merge` | MRマージ |
| PUT | `/projects/{id}/merge_requests/{iid}/rebase` | MRリベース |

---

### 外部API — Bitbucket REST API

> Decap CMSの`decap-cms-backend-bitbucket`パッケージが利用するBitbucket API 2.0。

#### リポジトリ・ファイル操作

| メソッド | エンドポイント | 説明 |
| --- | --- | --- |
| GET | `/repositories/{workspace}/{repo}` | リポジトリ情報取得 |
| GET | `/repositories/{workspace}/{repo}/refs/branches` | ブランチ一覧取得 |
| GET | `/repositories/{workspace}/{repo}/src/{node}/{path}` | ファイル内容取得 |
| POST | `/repositories/{workspace}/{repo}/src` | ファイルコミット |
| GET | `/repositories/{workspace}/{repo}/diff/{source}..{dest}` | 差分取得 |

#### Pull Request操作

| メソッド | エンドポイント | 説明 |
| --- | --- | --- |
| POST | `/repositories/{workspace}/{repo}/pullrequests` | PR作成 |
| GET | `/repositories/{workspace}/{repo}/pullrequests` | PR一覧取得 |
| PATCH | `/repositories/{workspace}/{repo}/pullrequests/{id}` | PR更新 |
| PUT | `/repositories/{workspace}/{repo}/pullrequests/{id}/merge` | PRマージ |
| POST | `/repositories/{workspace}/{repo}/pullrequests/{id}/decline` | PR却下 |

---

### 外部API — Gitea REST API

> Decap CMSの`decap-cms-backend-gitea`パッケージが利用するGitea API v1。

| メソッド | エンドポイント | 説明 |
| --- | --- | --- |
| GET | `/user` | 認証ユーザー情報取得 |
| GET | `/repos/{owner}/{repo}` | リポジトリ情報取得 |
| GET | `/repos/{owner}/{repo}/branches` | ブランチ一覧取得 |
| GET | `/repos/{owner}/{repo}/raw/branch/{branch}/{path}` | ファイル内容取得 |
| GET | `/repos/{owner}/{repo}/git/trees/{sha}` | Gitツリー取得 |
| GET | `/repos/{owner}/{repo}/git/blobs/{sha}` | Gitブロブ取得 |
| POST | `/repos/{owner}/{repo}/pulls` | PR作成 |
| GET | `/repos/{owner}/{repo}/pulls` | PR一覧取得 |

---

### 外部API — Azure DevOps REST API

> Decap CMSの`decap-cms-backend-azure`パッケージが利用するAzure DevOps API。

#### ベースパス

```
{apiRoot}/{org}/{project}/_apis/git/repositories/{repo}
```

| メソッド | エンドポイント | 説明 |
| --- | --- | --- |
| GET | `https://app.vssps.visualstudio.com/_apis/profile/profiles/me` | ユーザープロファイル取得 |
| GET | `/refs` | リファレンス一覧取得 |
| POST | `/refs` | リファレンス作成 |
| POST | `/commits` | コミット作成（pushes） |
| GET | `/commits` | コミット一覧取得 |
| GET | `/commits/{id}/changes` | コミット変更取得 |
| GET | `/commits/{id}/statuses` | コミットステータス取得 |
| GET | `/pullrequests` | PR一覧取得 |
| POST | `/pullrequests` | PR作成 |
| PATCH | `/pullrequests/{id}` | PR更新 |
| GET | `/diffs/commits` | コミット差分取得 |

---

## サーバー環境設定

### Decap Server

| 環境変数 | デフォルト値 | 説明 |
| --- | --- | --- |
| PORT | `8081` | サーバーポート番号 |
| BIND_HOST | - | バインドホストアドレス |
| MODE | `fs` | 動作モード（`fs`: ファイルシステム、`git`: Git） |
| LOG_LEVEL | `info` | ログレベル |
| ORIGIN | `*` | CORS許可オリジン |
| GIT_REPO_DIRECTORY | `process.cwd()` | Gitリポジトリのパス |

### Netlify Function（publish）

| 環境変数 | 必須 | 説明 |
| --- | --- | --- |
| SLACK_SIGNING_SECRET | ○ | Slack署名検証用シークレット |
| PUBLISH_COMMAND | ○ | 許可するスラッシュコマンド名 |
| ALLOWED_USERS | ○ | 許可するSlackユーザーID（カンマ区切り） |
| GITHUB_TOKEN | ○ | GitHub APIトークン |
| GITHUB_REPO | ○ | 対象リポジトリ（`owner/repo`形式） |

## バリデーション

Decap Server Proxy APIは、Joiライブラリ（`@hapi/joi`）によるリクエストボディの厳格なバリデーションを実施する。

### バリデーションルール

| 項目 | ルール |
| --- | --- |
| action | 許可されたアクション名のいずれかであること |
| params.branch | 必須文字列 |
| params.path / folder / mediaFolder | 必須文字列 + パストラバーサル検証 |
| params.assets[].encoding | `"base64"` のみ許可 |
| params.dataFiles / entry | 排他（xor）: いずれか一方のみ指定可 |
| params.paths | 最低1件の配列 |

### パストラバーサル検証

ファイルパスパラメータには、リポジトリディレクトリ外へのアクセスを防止するパストラバーサル検証が適用される。

### バリデーションエラー時のレスポンス

ステータスコード: `422 Unprocessable Entity`

```json
{
  "error": "\"params.folder\" is not allowed to contain '..'"
}
```

## ミドルウェア構成

```
リクエスト
  → morgan（ログ出力）
  → cors（CORS制御）
  → express.json（50MB制限）
  → joi（バリデーション）
  → localFs / localGit ミドルウェア
  → レスポンス
```

## 備考

- Decap Server Proxy APIは全て単一エンドポイント `POST /api/v1` にアクションベースのルーティングで実装されている（RPCスタイル）
- Local Gitモードではmutex（3秒タイムアウト）による排他制御があり、同時に1つのGit操作のみ実行可能
- Local FSモードではワークフロー関連のアクション（`unpublishedEntries`, `unpublishedEntry`等）は未サポート。`publish_modes`として`simple`のみ返す
- 外部API（GitHub, GitLab, Bitbucket, Gitea, Azure DevOps）へのアクセスは、各バックエンドパッケージ内のAPIクライアントクラスで実装されており、レートリミット対応（指数バックオフ、最大5回リトライ）が含まれる
- リクエストボディサイズ上限は50MB（`express.json({ limit: '50mb' })`）
- Git Gateway経由の場合、JWTベアラートークンでの認証が使用され、LFS（Large File Storage）もサポートされる
