20_API仕様
本章では、filix-shadowwork-api が提供する Application Programming Interface(API)の外部仕様を定義する。
各エンドポイントの目的、入力、出力、認証、およびエラー形式を示し、フロントエンドとの契約(contract)とする。
共通仕様
ベースURL
- 環境ごとに異なる(例: Cloudflare Workers のデプロイ先)
- 以降のパスはすべてベースURL配下の相対パスとして記載する
Content-Type
- リクエストボディがある場合は原則
application/json - レスポンスは原則
application/json
認証(Authentication)
本APIは、JWT による認証が必須です(/api/auth/exchange を除く)。
クライアントは以下の流れで認証を行います:
- Supabase Auth でログインし、access token を取得する
POST /api/auth/exchangeで Supabase access token を渡し、内部JWT(Cookie)を取得する- 以後の API リクエストでは、Cookie に含まれる JWT が自動的に送信される
- バックエンドは JWT の署名を検証し、
subクレーム(= Supabase user_id)を用いてユーザーを特定する
重要: すべてのエンドポイントで、クライアントは user_id パラメータを送信してはいけません。
ユーザーIDはすべて JWT から確定されます(認証なしでは処理されません)。
詳細は 50_セキュリティ の「認証方式(JWT + Cookie)」を参照。
注: 本章では API契約として「JWT で認証が必須」「user_id は送信不要」を記載する。
検証手順の詳細は50_セキュリティを参照。
利用権限(Authorization / 利用権限)
有料状態(paid)などの利用権限は、各機能のアクセス可否に影響する。
paid判定や webhook 反映の詳細は 40_課金と利用権限 に記載する。
クエリパラメータ
GETの取得系ではクエリパラメータを使用する場合がある(詳細は各エンドポイント)
共通エラー形式(Error response)
すべてのエンドポイントは、失敗時に共通の JSON 形式でエラーを返す。
{
"ok": false,
"error": {
"code": "BAD_REQUEST",
"message": "説明文(人間向け)"
}
}
ok: 成否フラグ(失敗時はfalse)error.code: エラー種別(例:BAD_REQUEST,UNAUTHORIZED,FORBIDDEN,NOT_FOUND,INTERNAL_ERROR)error.message: 画面表示やログのための説明文
HTTPステータスはエラー種別に応じて設定する(例: 400/401/403/404/500)。
※実装上のステータス割当は変更され得るが、成功時に ok: true を返すこと、失敗時に上記形式を返すことを契約とする。
代表的なエラー例
CORS 許可外 Origin(403)
{
"ok": false,
"error": {
"code": "FORBIDDEN",
"message": "origin not allowed"
}
}
外部APIタイムアウト等の上流失敗(502)
{
"ok": false,
"error": {
"code": "INTERNAL_ERROR",
"message": "OpenAI fetch failed"
}
}
上記 502 は OpenAI/Stripe/Supabase Auth などの外部依存に起因する失敗を含む。
外部API呼び出しのタイムアウト制御は EXTERNAL_API_TIMEOUT_MS に従う。
エンドポイント一覧
認証
POST /api/auth/exchange
Supabase access token を検証し、内部JWTを Cookie で返す。
ヘルスチェック
GET /
サービス稼働確認。
課金・利用権限
GET /api/paid
指定ユーザーの paid 状態を返す。POST /api/admin/set_paid
管理用途。指定ユーザーの paid 状態を更新する。
スレッド(thread)
POST /api/thread/start
スレッド開始。POST /api/thread/chat
LLM中継(平文を受け取り応答を返す。保存はしない)。POST /api/thread/message
暗号化済みメッセージを保存する。GET /api/thread/state
スレッド状態取得。POST /api/thread/close
スレッド終了。GET /api/thread/messages
スレッドのメッセージ一覧取得。
実行(run)
POST /api/run/start
実行開始。POST /api/run/restart
実行再開(続きから進める想定)。GET /api/runs/list
実行一覧取得。
スレッド一覧
GET /api/threads/list
スレッド一覧取得。
LLM(管理・検証用途)
POST /api/llm/ping
LLM疎通確認。POST /api/llm/respond
LLM応答生成(検証・開発用途を想定)。
エンドポイント仕様(詳細)
以下は「最低限の契約」を示す。
リクエスト/レスポンスの厳密なフィールド定義(JSON schema)は必要になった段階で追加する。
GET /
目的: 稼働確認。
認証: 不要。
成功レスポンス例:
{ "ok": true }
GET /api/paid
目的: 認証ユーザーの paid 状態を返す。
認証: 必要(方式は 50_セキュリティ)。
入力: なし(ユーザーは JWT から確定)
成功レスポンス例:
{ "ok": true, "paid": true }
POST /api/admin/set_paid
目的: 管理用途で paid 状態を更新する。
認証: 強い制限が必須(詳細は 50_セキュリティ)。
- JWT(Cookie)で管理者(memberId)を特定し、ADMIN_MEMBER_IDS allowlist に含まれること
- 管理トークンが一致すること(X-PAID-ADMIN-TOKEN: <token>)
入力: JSON(例)
{ "user_id": "xxx", "paid": true }
成功レスポンス例:
{ "ok": true }
POST /api/thread/start
目的: シャドウワークのスレッドを開始する。
認証: 必要。
入力: なし(ユーザーは JWT から確定)
成功レスポンス(例):
{ "ok": true, "thread_id": "t_xxx", "run_id": "r_xxx" }
POST /api/thread/chat
目的: AI応答生成のための中継。平文を受け取り、LLMに送信して応答を返す。
認証: 必要。
入力: JSON(例)
{
"message": "text"
}
成功レスポンス(例):
{ "ok": true, "reply": "text", "thread_state": { } }
補足:
- 本エンドポイントは平文を永続化しない。
- LLM利用料金の抑制を目的とした入力文字数制限は、本エンドポイントの平文入力(message)に対して適用する。
- 旧カード関連フィールドは廃止済みで、現行契約には含めない。
- action: "next" 指定時はLLM呼び出しを行わず、次アクション用の文面を返す。
- action: "next" 以外では、message をクエリとして embedding を生成し、Qdrant 検索の上位チャンクを追加の system context として LLM に渡す。
- RAG 文脈検索(embedding 生成または Qdrant search)に失敗した場合は、502 / INTERNAL_ERROR を返す。
POST /api/thread/message
目的: 暗号化済みメッセージ(暗号文+メタ情報)を保存する。
認証: 必要。
入力: JSON(例)
{
"thread_id": "t_xxx",
"role": "user",
"client_message_id": "cm_xxx",
"ciphertext": "base64...",
"iv": "base64...",
"alg": "AES-256-GCM",
"v": 1,
"kid": "k1",
"wrapped_key": "base64...",
"wrapped_key_alg": "KMS",
"wrapped_key_kid": "kek_1"
}
暗号化方式(封筒暗号)
本文は共通鍵(DEK)で暗号化し、DEKは別系統の鍵(KEK)で暗号化(ラップ)して保存する(封筒暗号)。
wrapped_key: ラップ済みDEK(base64)wrapped_key_alg: ラップ方式(例:KMS/RSA-OAEP等)wrapped_key_kid: KEK識別子(例: KMS Key ID/ARN)
上記 wrapped_key* 3項目はすべて必須。いずれかが欠けている、または空文字のときは 400 Bad Request を返す。
成功レスポンス(例):
{
"ok": true,
"thread_id": "t_xxx",
"message": {
"role": "user",
"client_message_id": "cm_xxx"
}
}
補足:
- サーバは平文を受け付けない。
- D1 上の本文は暗号文としてのみ保持する(平文は保存しない)。
- 復号(平文復元)は、クライアント側で行う設計を基本とする。
- client_message_id により保存リトライ時の重複登録を防ぐ(冪等)。
- 本エンドポイントの暗号文入力(ciphertext/iv/alg)は、LLM利用料金の制御対象ではない。
- 暗号文入力の検証は、保存API保護(異常入力・過大リクエスト対策)を目的として実施する。
client_message_id 採番規約(フロント実装)
- 1メッセージ(1回の保存対象)につき、クライアントが一意なIDを生成する。
- 形式は UUIDv4 などの十分なランダム性を持つ文字列を推奨する。
- 同じメッセージを再送する場合は、同じ
client_message_idを再利用する。 - 新しいメッセージを保存する場合は、必ず新しい
client_message_idを採番する。 client_message_idはメッセージ本文そのものや個人情報を含めない。
保存失敗時のUI状態(クライアント指針)
フロントは保存状態を明示し、少なくとも以下の状態を表現する。
saved: 保存済み(サーバがok: trueを返却)pending_retry: 保存失敗。再試行可能な状態not_saved: ユーザー離脱や再試行上限到達等で未保存が確定した状態
推奨フロー:
1. POST /api/thread/chat でAI応答を取得
2. 端末で暗号化し、POST /api/thread/message で保存
3. 保存失敗時は pending_retry を表示し、同じ client_message_id で再送
4. 最終的に保存できなかった場合は not_saved を表示
GET /api/thread/state
目的: スレッドの状態を取得する。
認証: 必要。
入力: なし(ユーザーは JWT から確定)
成功レスポンス(例):
{ "ok": true, "state": { } }
POST /api/thread/close
目的: スレッドを終了する。
認証: 必要。
入力: なし(ユーザーは JWT から確定)
成功レスポンス(例):
{ "ok": true }
GET /api/thread/messages
目的: スレッドに紐づくメッセージ一覧を取得する。
認証: 必要。
入力: クエリ(例)
- thread_id(必須)
- limit(任意)
成功レスポンス(例):
{
"ok": true,
"messages": [
{
"role": "user",
"client_message_id": "cm_xxx",
"ciphertext": "base64...",
"iv": "base64...",
"alg": "AES-256-GCM",
"v": 1,
"kid": "k1",
"wrapped_key": "base64...",
"wrapped_key_alg": "KMS",
"wrapped_key_kid": "kek_1"
}
]
}
補足:
- 返却は暗号文とメタ情報のみ(平文本文は返却しない)。
- wrapped_key, wrapped_key_alg, wrapped_key_kid は各メッセージで必ず返る前提とする。
GET /api/crypto/kms_public_key
目的: クライアント側で DEK をラップするために、AWS KMS の公開鍵を取得する。
認証: 不要。
入力: なし
成功レスポンス(例):
{
"ok": true,
"kid": "arn:aws:kms:ap-northeast-1:555569220922:key/ea3e43d1-bc62-47d5-af00-fc609843a46f",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkq...\n-----END PUBLIC KEY-----\n"
}
補足:
- kid は当面 KMS_KEY_ID をそのまま返す。
- public_key_pem は Web Crypto で RSA-OAEP ラップに利用できる SPKI PEM 形式とする。
- API は KMS の GetPublicKey を代理実行するが、秘密鍵素材は一切返さない。
クライアント実装上の注意(メッセージ保存)
POST /api/thread/chatとPOST /api/thread/messageは分離された責務である。- AI応答成功は保存成功を意味しないため、保存結果は別途判定する。
- 履歴の復号は、クライアント側で行う設計を基本とする(D1は暗号文のみ)。
- カード機能は廃止済みであり、文脈注入は今後 RAG 系の仕組みに一本化する。
- 「直近 n 回 + 要約」をAIに送る場合、フロントは履歴暗号文を復号して組み立てる。
- 履歴復号に必要な鍵が無い/不正な場合は、AI送信前にユーザーへ再設定を促す。
補足(RAG/ベクトル検索):
- ベクトルDBを活かすため、検索用チャンクは本文とは別に保存する(例: Qdrant)。チャンクはやむを得ず平文を含みうるため、取り扱いは 50_セキュリティ に従う。
- Qdrant payload は rag_chunk_v1 を現行仕様とし、少なくとも user_id, thread_id, message_id, chunk_no, text を保持する。client_message_id は取得できる場合のみ付与する。
- point の id は ${message_id}#${chunk_no} とし、同一 message 内で chunk を一意に更新できる形に固定する。
POST /api/run/start
目的: 実行(run)を開始する。
認証: 必要。
入力: なし(ユーザーは JWT から確定)
成功レスポンス(例):
{ "ok": true, "run_id": "r_xxx" }
POST /api/run/restart
目的: 実行(run)を再開する。
認証: 必要。
入力: なし(ユーザーは JWT から確定)
成功レスポンス(例):
{ "ok": true, "run_id": "r_xxx" }
GET /api/runs/list
目的: 認証ユーザーの実行(run)一覧を返す。
認証: 必要。
入力: なし(ユーザーは JWT から確定)
成功レスポンス(例):
{ "ok": true, "runs": [ { } ] }
GET /api/threads/list
目的: 認証ユーザーのスレッド一覧を返す。
認証: 必要。
入力: クエリ(例)
- run_no(任意)
成功レスポンス(例):
{ "ok": true, "threads": [ { } ] }
POST /api/llm/ping
目的: LLM疎通確認。
認証: 原則必要(開発用途のため制限推奨)。
入力: JSON(必要なら)
{ "ok": true }
POST /api/llm/respond
目的: LLM応答生成(検証用途)。
認証: 原則必要(開発用途のため制限推奨)。
入力: JSON(例)
{ "input": "text" }
認証エンドポイント詳細
POST /api/auth/exchange
目的: Supabase access token を検証し、内部JWT(Access Token)を発行して Cookie として返す。
認証: 不要(このエンドポイントのみ認証なし)
入力(JSON)
{
"token": "supabase-access-token-here"
}
token(必須):Supabase Auth が発行した access token
処理
1. Supabase JWT を JWKS / issuer / audience で検証し、sub を取得
2. 検証失敗時は 401 を返す
3. sub を内部JWTの sub として再発行する
4. Cookie に設定して返す
出力(JSON)
{
"ok": true,
"member_id": "supabase-user-uuid",
"token_type": "Bearer",
"expires_in": 900
}
member_id はレスポンス互換性のためのフィールド名であり、値は Supabase の sub(user_id)である。
Cookie設定
Set-Cookie: access_token=<JWT>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900
エラーレスポンス(例)
{
"ok": false,
"error": {
"code": "UNAUTHORIZED",
"message": "supabase token verification failed"
}
}
ステータスコード - 200:成功 - 400:リクエスト形式エラー(token がない等) - 401:認証失敗(Supabase access token が無効) - 500:サーバーエラー
````
成功レスポンス(例):
json
{ "ok": true, "reply": "text" }
備考(実装との整合性)
- 各リクエスト/レスポンスの詳細フィールドは、実装の進展に合わせて追記する。
- 本章は「詳細設計」ではなく、フロントとバックの契約として必要な事項に限定して記載する。