[← Docs](/docs/)  |  [Home](/)

# RunProof API Reference（v0.1.1 / 現行実装準拠）
RunProof — Execution Proof API by Chamia  
Docs（freeze）: https://runproof-chamia.pages.dev/freeze/freeze_v2.md

---

## ⚠️ 重要：入力データの制限（必読）
RunProof は **ハッシュ値＋短い補助情報（tags）**による「実行の証跡」を記録・検証する **低出力システム**です。  
**生データ（本文）を送るサービスではありません。**

### 入力禁止（PII / 認証情報 / 機密情報）
次の情報を、**いかなるフィールド（tags 等の任意文字列を含む）にも含めない**でください。

- **個人情報（PII）**：氏名、住所、電話番号、メールアドレス、マイナンバー、クレジットカード番号 等
- **認証情報**：パスワード、秘密鍵、APIキー、アクセストークン、Bearer トークン 等
- **機密情報**：未公開の営業秘密、保護されるべき医療情報（PHI） 等

### ハッシュ化の義務（送信するのはハッシュ）
入力データ等の証跡を残す場合、ユーザーは **自分の環境で**不可逆なハッシュ（例：SHA-256）へ変換し、
RunProof には **`sha256:<hex>` 形式のハッシュ値**のみを送信してください（例：`input_hash`）。

### 運営者の対応（削除・停止）
運営者は、上記に違反するデータ（平文の個人情報等）や法的に不適切と判断されるデータが保存されたと検知した場合、  
**事前通知なく当該データの削除**および **APIアクセスの一時/恒久停止**を行うことがあります。

> 注：将来的に誤入力を減らすため、tags 等の文字列フィールドに対して形式的なスキャン（メール/カード番号らしきパターン等）を行い、
> 400 で拒否する仕組みを追加する場合があります。検知は完全ではありません。

---

## 1. Base URL（公開入口）
- API（Workers）: `https://runproof.chamia-20260215.workers.dev`
  - `GET /` と `GET /health` は **同じ入口情報**（`env` / `docs_url` / endpoints）
- Docs（Pages）: `https://runproof-chamia.pages.dev/`

---

## 1.5 用途別テンプレート（Release / Document / Audit Pack）
このテンプレート群は新しいAPIではありません。既存の <code>/v1/receipts</code> / <code>/v1/verify</code> を用途別に使いやすくするための資料です。

- <code>receipt_kind</code> は用途テンプレ識別と冪等性安定化のために使います。
- ただし、<strong>署名・verify ロジックの分岐条件には用いません</strong>。
- 現行本番実装では、補助情報の送信には <code>tags</code> を使用します。Spec v0 の概念では <code>subject_digest</code> は <code>input_hash</code>、補助情報は <code>tags</code> に相当します。

---

## 2. 認証（API Key）
### 2.1 Authorization ヘッダ
認証が必要なエンドポイントでは、以下を指定します。

```
Authorization: Bearer <api_key>
Content-Type: application/json
```

- api_key は **16〜256文字**の範囲で扱います（極端な入力を拒否）。
- サーバ側は `sha256(api_key)` を計算し、D1 の `api_keys.api_key_hash`（`sha256:<hex>`）と照合します。
- `revoked_at IS NULL` のキーのみ有効です（失効キーは 401）。

---

## 2.2 APIキーの受け取り（Claim）
RunProof の APIキーは **生キーをサーバに保存しない**設計のため、発行後の受け取りは **one-time reveal** の **claim（表示）**で行います。表示された時点で必ずコピーし、安全な場所に保存してください。


> **重要**  
> このセッションでは、APIキーは**一度だけ**表示されます。再表示はできません。  
> **表示された時点で必ずコピーして、安全な場所に保存してください。**  
> 決済完了ページ（success）から開き直しても再表示されません。  
> 必要な場合は、**再購入ではなく再発行**が必要です。  
>
> **Important**  
> This session reveals the API key **only once**. It cannot be shown again.  
> **Copy it immediately and store it in a safe place when displayed.**  
> Reopening from the checkout success page will not reveal it again.  
> If needed, **reissue is required rather than repurchasing**.


### 2.2.1 有料（Basic/Pro）：Stripe決済 → Claim
- Pages（受け取り画面）：`/claim/`
- Worker（発行API）：`POST /v1/claim`
- 入力：

```json
{ "session_id": "cs_live_..." }
```

- `session_id` は Stripe Checkout の `{CHECKOUT_SESSION_ID}` です。
- 返却：`api_key` を **一度だけ** 表示します。保存方法と再表示不可の注意は、上の **重要** ボックスを参照してください。

### 2.2.2 Free：Turnstile → Claim
- Pages（自動発行UI）：`/free/claim/`
- Worker（発行API）：`POST /v1/claim/free`
- 入力：

```json
{ "turnstile_token": "<token>" }
```

- Freeは放置運用のため、乱用対策として **Turnstile（人間確認）** を1回通します。
- 追加の摩擦：**1 IPあたり 1日1回**（KVでロック）
- 返却：`api_key` を **一度だけ** 表示します。保存方法と再表示不可の注意は、上の **重要** ボックスを参照してください。
- 紛失時：再表示ではなく、翌日以降に再度 Free claim で新規発行します。

> 運用メモ：Turnstile の Secret は Worker 側 `TURNSTILE_SECRET` に設定します（Pagesには sitekeyのみ）。


---

## 3. レート制限（KV）
RunProof は KV で 1分単位のレート制限を行います（`Retry-After` を返します）。

- `GET /v1/public-key`：IP 300/min
- `GET /v1/receipts/{id}`：IP 240/min（現状は公開read）
- `POST /v1/receipts`, `POST /v1/verify`：
  - **Bearer 欠如/不正**のときだけ「事前IPゲート」：IP 10/min（DoS/総当たりの盾）
  - 認証後：IP 180/min ＋ **api_key 1分上限（plan別）**
    - free: 60/min
    - basic: 300/min
    - pro: 600/min

---

## 4. 共通仕様
### 4.1 Content-Type
- JSON エンドポイントは `Content-Type: application/json` 必須
- 不一致は 415（unsupported_media_type）

### 4.2 エラー形式
エラーは次の形式で返ります。

```json
{
  "error": {
    "code": "bad_request",
    "message": "Human readable message",
    "details": { "any": "optional" }
  },
  "request_id": "cf-ray-or-uuid"
}
```

### 4.3 sha256 タグ形式
RunProof が受け付けるハッシュは次の形式のみです。

- `sha256:<hex>`（hex は 64文字）

例：`sha256:0123...`（64 hex）

---

## 5. エンドポイント一覧
| Path | Method | Auth | 概要 |
|---|---:|:---:|---|
| `/` | GET | - | 入口（env / docs_url / endpoints） |
| `/health` | GET | - | 入口（同上） |
| `/v1/public-key` | GET | - | 公開鍵配布（軽いIP制限） |
| `/v1/receipts` | POST | ✅ | レシート発行（入力ハッシュを署名） |
| `/v1/receipts/{receipt_id}` | GET | - | レシート取得（現状：公開read） |
| `/v1/verify` | POST | ✅ | 検証（project境界あり） |

---

## 6. GET `/` / GET `/health`
入口情報を返します（両者は同等）。

### Response 200
```json
{
  "product": "RunProof",
  "subtitle": "Execution Proof API by Chamia",
  "env": "prod",
  "docs_url": "https://runproof-chamia.pages.dev/freeze/freeze_v2.md",
  "endpoints": {
    "public_key": "/v1/public-key",
    "create_receipt": "/v1/receipts (POST, api_key required)",
    "get_receipt": "/v1/receipts/{receipt_id} (GET)",
    "verify": "/v1/verify (POST, api_key required)"
  }
}
```

---


### Monthly quota headers (plan enforcement)

`POST /v1/receipts` returns plan/quota information in response headers:

- `X-RunProof-Plan`
- `X-RunProof-Monthly-Limit`
- `X-RunProof-Monthly-Used`
- `X-RunProof-Monthly-Remaining`
- `X-RunProof-Monthly-Reset-At`

If the monthly quota is exceeded, the API returns `429 quota_exceeded`.

## 7. GET `/v1/public-key`
公開鍵（Ed25519）を返します。

### Rate limit
- IP 300/min

### Response 200
```json
{
  "keys": [
    { "kid": "k1", "alg": "Ed25519", "public_key_base64url": "<base64url>" }
  ],
  "active_kid": "k1"
}
```

### Response 500（例）
- `ED25519_PUBLIC_KEY_B64URL` も `ED25519_PRIVATE_KEY_B64URL` も設定されていない場合

---

## 8. POST `/v1/receipts`（レシート発行）
入力ハッシュ等を署名し、レシート（署名付き証跡）を発行します。

### Auth
- 必須（Bearer api_key）

### Rate limit
- 事前IPゲート（Authorization欠如/不正時）：IP 10/min
- 認証後：IP 180/min ＋ api_key bucket（plan別）

### Request body（最大 16KB）
- `input_hash` は必須
- `receipt_kind` は任意。MVP known values は `release` / `document` / `audit_pack` です。未指定時は応答でも `null` のままです。
- `output_hash` / `params_hash` / `env_hash` は任意
- `code_ref` / `run_id` は任意（最大 256文字）
- `tags` は任意（オブジェクト）
- `prev_receipt_hash` は任意（チェーン用）

> `project_id` / `actor_id` は api_key 由来が正です。  
> デバッグのため body に含めてもよいが、含めた場合は **一致必須**（不一致は 403）。

#### 例（Release テンプレート）
```json
{
  "input_hash": "sha256:<64hex>",
  "receipt_kind": "release",
  "tags": {
    "template": "release",
    "subject_name": "runproof_release_example.zip",
    "subject_type": "zip",
    "subject_version": "v0.1.2",
    "subject_ref": "git:dfcb7479"
  }
}
```

#### 例（汎用）
```json
{
  "input_hash": "sha256:<64hex>",
  "output_hash": "sha256:<64hex>",
  "params_hash": "sha256:<64hex>",
  "env_hash": "sha256:<64hex>",
  "code_ref": "git:<commit>",
  "run_id": "job-20260222-0001",
  "tags": { "kind": "report", "period": "2026-02" },
  "prev_receipt_hash": "sha256:<64hex>"
}
```

### tags の制約
- tags は **オブジェクト**（配列不可）
- 最大キー数：20
- key：1〜64文字
- value：1〜256文字（null は無視）
- 全体サイズ：2048 bytes（JSONとしてのバイト数）

### チェーン規則（server-assisted）
- `prev_receipt_hash` 未指定：サーバが `projects.last_receipt_hash` を補完（main継続）
- `prev_receipt_hash` を指定し、tip と一致しない：**branch 扱い**（main tip は維持）

### Response 201（新規発行）
```json
{
  "ok": true,
  "receipt": {
    "receipt_id": "rp_....",
    "project_id": "p_example",
    "actor_id": "actor_example",
    "created_at": "2026-02-22T00:00:00.000Z",
    "input_hash": "sha256:<64hex>",
    "tags": { "kind": "release", "note": "phase4-raw" },
    "receipt_kind": "release",
    "status": "issued",
    "prev_receipt_hash": "sha256:<64hex>|null",
    "receipt_hash": "sha256:<64hex>",
    "signature": "<base64url>",
    "sig_kid": "k1",
    "chain_status": "main|branch",
    "expected_prev_receipt_hash": "sha256:<64hex>|null"
  },
  "idempotency": {
    "hit": false,
    "key": "ik_<opaque>"
  },
  "chain": {
    "status": "main|branch",
    "expected_prev_receipt_hash": "sha256:<64hex>|null",
    "prev_receipt_hash": "sha256:<64hex>|null"
  },
  "quota": {
    "plan": "free",
    "monthly_limit": 10,
    "monthly_used": 1,
    "monthly_remaining": 9,
    "monthly_reset_at": "2026-03-01T00:00:00.000Z"
  }
}
```

### Response 200（冪等 replay）
```json
{
  "ok": true,
  "receipt": {
    "receipt_id": "rp_....",
    "project_id": "p_example",
    "actor_id": "actor_example",
    "created_at": "2026-02-22T00:00:00.000Z",
    "input_hash": "sha256:<64hex>",
    "tags": { "kind": "release", "note": "phase4-raw" },
    "receipt_kind": "release",
    "status": "issued",
    "prev_receipt_hash": "sha256:<64hex>|null",
    "receipt_hash": "sha256:<64hex>",
    "signature": "<base64url>",
    "sig_kid": "k1",
    "chain_status": "main|branch",
    "expected_prev_receipt_hash": "sha256:<64hex>|null"
  },
  "idempotency": {
    "hit": true,
    "key": "ik_<opaque>"
  }
}
```

> **Note:** 本ドキュメントでは署名鍵識別子を `sig_kid` と表記します。Spec v0 などで言う **`key_id`（鍵識別子）** は、この `sig_kid` と同義として扱ってください（将来の鍵ローテーション時の追跡に使います）。

- 新規発行時は通常 `201`、同一入力の replay は `200` を返します。
- `idempotency.key` は opaque なサーバ生成値です。201 では `hit=false`、同一 logical input の replay では `hit=true` を返します。
- `status` は MVP では常に `issued` です（`pending` は予約）。
- `receipt_kind` の MVP known values は `release` / `document` / `audit_pack` です。
- `receipt_kind` は応答に含まれますが、署名 payload や verify 分岐には使いません。
- 実装上の key identifier は現在 `sig_kid` です。

### 主なエラー
- 401：Authorization欠如 / api_key不明 / revoked
- 403：`project_id` / `actor_id` の不一致（scope mismatch）
- 400：ハッシュ形式不正、tags不正、`code_ref/run_id` 長すぎ
- 404：project が存在しない（運用上は「project作成済み」で使う）
- 413：Payload too large（16KB超）
- 415：Content-Type 不正
- 429：rate limited（Retry-After）

---

## 9. GET `/v1/receipts/{receipt_id}`（レシート取得）
レシートを取得します。現状は **公開read** です（将来的にゲート可能）。

### Rate limit
- IP 240/min

### Response 200
```json
{
  "ok": true,
  "receipt": {
    "receipt_id": "rp_....",
    "project_id": "p_example",
    "actor_id": "actor_example",
    "created_at": "2026-02-22T00:00:00.000Z",
    "input_hash": "sha256:<64hex>",
    "output_hash": "sha256:<64hex>|null",
    "params_hash": "sha256:<64hex>|null",
    "env_hash": "sha256:<64hex>|null",
    "code_ref": "git:<commit>|null",
    "run_id": "job-...|null",
    "tags": { "k": "v" } | null,
    "prev_receipt_hash": "sha256:<64hex>|null",
    "receipt_hash": "sha256:<64hex>",
    "signature": "<base64url>",
    "sig_kid": "k1",
    "receipt_kind": "release|document|audit_pack|null",
    "status": "issued",

    "chain_status": "main|branch|null",
    "expected_prev_receipt_hash": "sha256:<64hex>|null"
  }
}
```

### 主なエラー
- 404：Receipt not found
- 429：rate limited

---

## 10. POST `/v1/verify`（照合検証）
受け取ったレシートが、指定した `input_hash`（必須）および `output_hash`（任意）と一致し、
署名・再計算・チェーン整合が取れるかを検証します。

### Auth
- 必須（Bearer api_key）
- **project 境界**：api_key の project と一致しないレシートは 403

### Rate limit
- 事前IPゲート（Authorization欠如/不正時）：IP 10/min
- 認証後：IP 180/min ＋ api_key bucket（plan別）

### Request body（最大 8KB）
- 必須：`receipt_id`（string）、`input_hash`（sha256:<hex>）
- 任意：`output_hash`（sha256:<hex>）

#### 例
```json
{ "receipt_id": "rp_....", "input_hash": "sha256:<64hex>", "output_hash": "sha256:<64hex>" }
```

### 実装のチェック（4点）
1. `receipt_hash` 再計算（DB行 → canonical JSON → sha256）
2. 署名検証（Ed25519）
3. hash_match（入力必須、出力任意）
4. chain_link（prev がある場合、同一project内に prev が存在し、prev 自体の再計算も一致）

### Response 200
```json
{
  "ok": true,
  "checks": {
    "signature": true,
    "hash_match": true,
    "receipt_hash_recompute": true,
    "chain_link": true
  },
  "sig_kid": "k1",
  "receipt_hash": "sha256:<64hex>",
  "recomputed_receipt_hash": "sha256:<64hex>",
  "chain": {
    "prev_receipt_hash": "sha256:<64hex>|null",
    "prev_found": true,
    "prev_receipt_hash_recompute": true
  }
}
```

### 主なエラー
- 400：`receipt_id` / `input_hash` 不足、`output_hash` 形式不正
- 401：Authorization欠如 / api_key不明 / revoked
- 403：project境界違反
- 404：Receipt not found
- 413：Payload too large（8KB超）
- 415：Content-Type 不正
- 429：rate limited

---

## 11. 付記：第三者検証の最短手順（APIに依存しない）
第三者が「RunProof API を信用し切らずに」検証する最短は次です。

1) `GET /v1/public-key` で公開鍵を取得  
2) `GET /v1/receipts/{id}` でレシートを取得  
3) 取得した `receipt_hash` と `signature` を Ed25519 で検証  
4) 手元の入力 JSON から `sha256:<hex>` を計算し、`input_hash` と一致確認  
5) 必要なら `prev_receipt_hash` を辿り、チェーン整合を確認

---

## 12. 変更方針（freeze）
本API reference は **v0.1.1 実装**に合わせた固定版です。  
互換性を壊す変更は新バージョン（v0.1.2 等）として別文書に切り出します。
