elquia-provisioner · Cloudflare Workers

Elquia 自動販売・VM払い出しシステムの流れ

PC の elquia-provisioner フォルダのソースコード・SQL・設定ファイルと、各ファイルの更新日時から作成したフロー図です。

会話のログそのものは参照できないため、「経緯」はファイルの更新日時からの推定です。Claude フォルダ内の Artifacts は保護領域のため読めませんでした。

1. 開発の経緯(ファイル更新日から推定)

8月に決済と GitHub 招待の土台を作り、10月にオーナー通知・VM 自動払い出しを追加した流れです。

flowchart TD
  A["2026-08-19
Workers プロジェクト作成
wrangler / TypeScript / vitest"] --> B["2026-08-19
schema.sql 作成
members・processed_events・reservations"] B --> C["Stripe 決済 + GitHub App 連携
決済 → GitHub 連携 → Organization 招待"] C --> D["在庫判定 capacity.ts
Proxmox の空きメモリ・ディスクから販売枠を計算"] D --> E["2026-10-13
migration-alerts.sql
請求状態 billing_state と owner_alerts 通知キュー"] E --> F["2026-10-14
migration-provision.sql
VM 払い出し用の列 vm_state など"] F --> G["2026-10-14
provision.ts / alerts.ts / github.ts 更新
wrangler.jsonc に Proxmox・メール設定"] G --> H["2026-10-14
check-members.sql
ポータル案内前のメール整合チェック"] H --> I["2026-10-15
index.ts / provision.ts 最終更新
資格情報の再発行 API など"]

2. 全体構成

Worker が Stripe・GitHub・Proxmox・メールの間を取り持ち、状態は D1 と KV に置きます。

flowchart LR
  U(["お客様"]) -->|"shop.mainnki.com"| W["Elquia Provisioner
Cloudflare Worker"] OW(["オーナー"]) -->|"Bearer トークン"| W W <-->|"Checkout / Webhook"| S["Stripe"] W <-->|"OAuth / Org 招待・除名"| G["GitHub App
org: mine-prox"] W <-->|"API 経由でクローン・設定・起動"| P["Proxmox VE
ノード prox / prox5"] W --> D1[("D1: elquia-members
契約・イベント・予約・通知")] W --> KV[("KV: TMP
ワンタイムトークン・資格情報")] W -->|"Resend"| M1["お客様へのメール"] W -->|"Email Routing / Discord"| M2["オーナー通知"] CR["Cron 5分ごと"] --> W

3. 申込から利用開始まで(お客様の流れ)

在庫の確認を2回挟み、決済後は GitHub 連携 → 招待 → VM 払い出し → ログイン情報のワンタイム表示へ進みます。

flowchart TD
  A["GET /api/checkout
申込内容の確認画面"] --> B{"在庫あり?"} B -->|"なし"| X1["満員ページ /api/soldout"] B -->|"確認できない"| X2["受付一時停止 503
フェイルクローズ"] B -->|"あり"| C["使い捨て nonce を KV に保存
15分有効"] C --> D["POST /api/checkout/confirm
同意して決済に進む"] D --> E{"nonce 有効?
在庫を再確認"} E -->|"NG"| X3["やり直し案内"] E -->|"OK"| F["Stripe Checkout 作成
枠を仮押さえ reservations"] F --> G{"決済"} G -->|"キャンセル"| X4["/api/canceled"] G -->|"完了"| H["GET /link
Stripe に問い合わせて支払い検証"] H --> I["members に pending_link で登録
link_token 発行 30分"] I --> J["GitHub OAuth 開始
state を KV に保存"] J --> K["/api/oauth/callback
state・token は単回使用"] K --> L{"GitHub アカウントが
他の有効契約で使用中?"} L -->|"はい"| X5["409 拒否
オーナーに通知"] L -->|"解約済み契約が保持"| L2["旧契約から解放して付け替え"] L -->|"いいえ"| M L2 --> M["status = invited
GitHub メールも保存"] M --> N{"Org 招待成功?"} N -->|"失敗"| X6["再試行ボタン表示
critical 通知"] N -->|"成功"| O["VM 払い出しをキューへ
vm_state = pending"] O --> P["連携完了画面
先に招待の受諾を案内"] O --> Q["VM 完成後にメール送信
/credentials のワンタイム URL 7日有効"] Q --> R["ログイン情報を1回だけ表示
表示後に KV から削除"] R --> S(["Proxmox 管理画面で
GitHub 認証してコンソール利用"])

4. VM 払い出しの状態遷移(vm_state)

Cron と連携直後のバックグラウンド処理が、1契約ずつ1段階ずつ前に進めます。

flowchart LR
  none(["none"]) -->|"招待成功"| pending["pending
前提チェック"] pending -->|"在庫なし・VMID 枯渇
次の Cron で再挑戦"| pending pending -->|"空きが最大のノードと
VMID 700-799 を D1 で確保"| cloning["cloning
テンプレートからクローン"] pending -->|"クローン発行失敗
VM 不在なら VMID 解放"| failed cloning -->|"タスク実行中"| cloning cloning -->|"exitstatus OK"| configuring["configuring
8コア / 8GB / 50GB
cloud-init・PVE ユーザー・ACL・起動"] cloning -->|"失敗"| failed["failed
オーナーへ critical 通知"] configuring -->|"資格情報を保存・メール送信"| ready(["ready"]) configuring -->|"情報不足など"| failed ready -->|"解約"| suspended["停止・ACL 解除
dormant タグ付与
削除はしない"] suspended -.->|"削除はオーナーが手動判断"| deleted(["deleted"])

5. 請求・解約の流れ(Stripe Webhook)

署名を検証し、同じイベントは一度しか処理しません。未払いでもアクセスは止めず、オーナーに知らせる方針です。

flowchart TD
  A["POST /api/stripe/webhook"] --> B{"署名 OK?"}
  B -->|"NG"| X["400"]
  B -->|"OK"| C{"処理済み event.id?"}
  C -->|"はい"| Y["200 duplicate"]
  C -->|"いいえ"| D{"イベント種別"}
  D -->|"checkout.session.completed"| E1["予約を confirmed
members 登録"] D -->|"checkout.session.expired"| E2["仮押さえを解放"] D -->|"invoice.paid"| E3["invited なら active に
未払い解消なら通知"] D -->|"invoice.payment_failed"| E4["billing_state = past_due
アクセスは継続・critical 通知"] D -->|"subscription.updated
解約予約の切替"| E5["解約予約・取消をオーナーに通知"] D -->|"subscription.deleted
canceled / unpaid"| E6["Org から除名
status = revoked
VM 停止 dormant"] D -->|"charge.dispute.created"| E7["チャージバック critical 通知"] D -->|"charge.refunded"| E8["返金を通知"] E1 & E2 & E3 & E4 & E5 & E6 & E7 & E8 --> Z["processed_events に記録
オーナー通知をすぐ送信"] D -.->|"処理中に例外"| R["500 を返して Stripe に再送させる"]

6. 5分ごとの定期処理(Cron)

各段階は独立して例外を捕まえるので、前の処理が失敗しても通知の送信までは必ず到達します。

flowchart TD
  T(["Cron */5 * * * *"]) --> A["① 期限切れの仮押さえを解放"]
  A --> B["② 静かな異常の検出
・未払い継続の日次リマインド
・GitHub 連携が放置された契約
・ストレージ逼迫 / 在庫ゼロ"] B --> C["③ VM 払い出しを1段階進める"] C --> D["④ 孤児 VM の検出
700-799 帯で契約に紐づかない VM"] D --> E["⑤ オーナー通知キューを送信
Discord / メール・失敗時はバックオフ再送"]

7. 管理者操作

Authorization ヘッダーのトークンのみ受け付け、未設定ならエンドポイント自体を 404 で隠します。

flowchart LR
  O(["オーナー"]) --> G{"ADMIN_TOKEN 一致?"}
  G -->|"未設定"| N["404"]
  G -->|"不一致"| U["401"]
  G -->|"一致"| S["GET /api/admin/status
契約数・未払い・連携待ち・予約・通知の集計"] G -->|"一致"| R["POST /api/admin/reissue
customer か vmid を指定"] R --> R1{"vm_state = ready?"} R1 -->|"いいえ"| R2["409"] R1 -->|"はい"| R3["ゲストエージェントで
パスワード再設定"] R3 --> R4["ワンタイム URL を返す
希望時はお客様にメール
監査用に通知を記録"] O -.-> Q["check-members.sql
wrangler d1 execute で
Stripe と GitHub のメール照合"]