Skip to content

feat(extensions): discord-bot — Cloudflare Workers + D1 で Discord 統合 #508

@yu23ki14

Description

@yu23ki14

One-liner

Cloudflare Workers + D1 で動く Discord bot。/toban-setup, /thx, /balance, /toban-link を提供。extensions/identity を import し、ThanksToken の mintFrom を呼ぶ。bot の署名鍵は Turnkey の TEE で管理し、Worker は秘密鍵を一切保持しない。

Background

Discord 上で /thx @user 10 ありがとう と打つだけで THX が送れるようにする。Bot は permanent state をほぼ持たず、identity(共通ライブラリ)と on-chain mintAllowance を読んで動く。

Identity 検証は bot-mediated 方式: Discord 自身がスラッシュコマンドを認証する → bot が snowflake を確実に把握する → bot が verifier_token (JWT) を発行する → ユーザーが wallet で IdentityBinding に署名する → identity worker が両方を検証して紐付け確定。Privy には依存しない(Privy は frontend での wallet 署名手段としてだけ使う)。

mintFrom 送信に使う bot signer は Turnkey の sub-organization 内 TEE (AWS Nitro Enclave) に置く。Worker は Turnkey HTTP API を P-256 stamper credentials で叩いて署名を依頼するだけで、Ethereum 秘密鍵自体は Worker・Cloudflare・開発者のいずれも保持しない。Turnkey 側 policy engine で mintFrom 以外の関数呼び出しを構造的に拒否する。

Design

パッケージ構成

pkgs/extensions/discord-bot/
├── src/
│   ├── index.ts                  # Workers entry
│   ├── interactions/             # Ed25519 検証 + dispatch
│   ├── commands/
│   │   ├── toban-link.ts         # admin が guild を tree_id に紐付け
│   │   ├── toban-setup.ts        # verifier_token (JWT) 発行 + DM URL
│   │   ├── thx.ts                # mintFrom 実行
│   │   └── balance.ts            # mintAllowance 表示
│   ├── api/
│   │   └── install/callback.ts   # Discord OAuth bot install callback
│   ├── signer/
│   │   └── turnkey.ts            # Turnkey 署名 → viem LocalAccount 互換 wrapper
│   ├── verifier.ts               # snowflake → verifier_token JWT 発行
│   ├── chain.ts                  # viem clients
│   └── env.ts                    # bindings 型定義
├── turnkey/
│   └── policy.json               # Turnkey policy を版管理
├── docs/
│   ├── turnkey-setup.md          # Turnkey sub-organization セットアップ手順
│   └── key-rotation.md           # bot signer rotation runbook
├── migrations/                   # discord-bot 固有テーブル(thx_log 等、オプション)
└── wrangler.toml

wrangler.toml

name = "toban-discord-bot"
compatibility_date = "2026-01-01"

[[d1_databases]]
binding = "DB"
database_name = "toban-identity"
database_id = "<shared with future bots>"

[vars]
THANKS_TOKEN_ADDRESS = "0x..."
TOBAN_FRONTEND_URL = "https://toban.xyz"
RPC_URL = "https://mainnet.base.org"
TURNKEY_ORGANIZATION_ID = "<sub-organization id>"
TURNKEY_BOT_SIGNER_ADDRESS = "0x..."   # Turnkey 上の signing key に対応する Ethereum address

# secrets (wrangler secret put):
# - DISCORD_PUBLIC_KEY            # Discord interactions Ed25519 検証用
# - DISCORD_BOT_TOKEN
# - DISCORD_APP_ID
# - TURNKEY_API_PUBLIC_KEY        # Turnkey API stamper 公開鍵 (P-256)
# - TURNKEY_API_PRIVATE_KEY       # Turnkey API stamper 秘密鍵 (P-256)
# - VERIFIER_PRIVATE_KEY          # verifier_token 署名用(identity 側で対応公開鍵を保持)

VERIFIER_PRIVATE_KEY の対応公開鍵を extensions/identity の env に登録すること。identity の providers/discord.ts がこの鍵で verifier_token を検証する。

Ethereum 秘密鍵は Workers Secrets に存在しない。Turnkey の TEE 内にあり、Worker は API stamper credentials だけを保持する。stamper key が漏洩しても、Turnkey policy で mintFrom 以外の関数呼び出しは拒否されるので被害は「全ユーザーの mintAllowance 上限まで」で頭打ち。

Slash commands

Command 誰が 何をする
/toban-link <workspace_url> admin Discord guild ↔ Toban tree_id を紐付け
/toban-setup 各ユーザー identity 連携 URL(verifier_token 同梱)を ephemeral DM
/thx @user <amount> [message] 各ユーザー THX を送る
/balance 各ユーザー 自分の mintAllowance(self, TURNKEY_BOT_SIGNER_ADDRESS)mintableAmount を表示

Bot install フロー(admin)

1. Frontend の Workspace ページで「Discord 連携」
2. Discord OAuth install URL に redirect(state JWT に { tree_id, admin_wallet, exp:10min })
3. admin が Discord で server 選択 → callback
4. Bot Worker:
   - state JWT を自分の公開鍵で検証
   - admin_wallet が tree_id の admin Hat 保有者かを on-chain 検証
   - identity.upsertPlatformLink('discord', guild_id, tree_id, admin_wallet)
   - Discord に slash commands を guild scope で登録
5. Discord に「✅ Toban に接続されました」メッセージ

/toban-setup フロー(各ユーザー)

1. Discord で /toban-setup
   → Discord が interaction 内に snowflake を含めて bot に送る(Discord 認証済み)
2. Bot:
   - verifier_token = JWT.sign(
       { provider: 'discord', accountId: snowflake, exp: now + 15min },
       VERIFIER_PRIVATE_KEY,
       { issuer: 'toban-discord-bot', alg: 'ES256' }
     )
   - ephemeral reply: https://toban.xyz/connect/discord?token=<verifier_token>
3. Frontend (/connect/discord):
   - verifier_token をデコードして UI に provider/snowflake を表示
   - Privy ログイン → user wallet 取得(Privy の役割はここまで)
   - IdentityBinding を構築:
       {
         wallet, provider:'discord', accountId: snowflake,
         verifierTokenHash: keccak256(verifier_token),
         expires: now + 30min, nonce: random
       }
   - Privy 経由で wallet が EIP-712 署名
   - POST identity worker /api/connect:
       {
         provider: 'discord',
         verifier_token,
         identity_binding: { typedData, signature }
       }
4. identity worker:
   - providers.discord.verifyVerifierToken(verifier_token) → accountId
   - EIP-712 sig recover → wallet
   - cross-check 全部合致 → identities upsert
5. ✅ 完了画面
   "Discord を連携しました。/thx を使うには bot に mint 許可を設定してください"
   [→ /allowance/discord-bot へ]

/thx シーケンス

1. Discord interaction POST → Workers
2. Ed25519 sig 検証
3. 即 DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE で返答(3秒制限)
4. ctx.waitUntil 内で:
   - identity.getPlatformLink('discord', guild_id) → tree_id
   - identity.getIdentity('discord', sender_sf) → sender wallet
   - identity.getIdentity('discord', recipient_sf) → recipient wallet
       → 無し: followup「相手が未連携です。/toban-setup を案内してください」
   - tree_id から ThanksToken アドレスを resolve(subgraph or registry)
   - viem: mintAllowance(sender, TURNKEY_BOT_SIGNER_ADDRESS) を読む
       → 不足: followup「残り N THX。https://toban.xyz/allowance/discord-bot で増やしてください」
   - signer/turnkey.ts: Turnkey API に mintFrom 用 RLP-encoded tx の署名依頼
       → Turnkey が policy 検証 → 通れば署名返却、拒否なら error followup
   - viem で broadcast
   - tx 確定後 → followup(送信内容+explorer link+残量)
   - revert: followup でエラー理由(mintableAmount 不足など)

Bot signer 運用(Turnkey)

  • Ethereum 秘密鍵は Turnkey の sub-organization 内 (AWS Nitro Enclave) に保管。生成も Turnkey 内、export 不可
  • Worker は Turnkey HTTP API (/public/v1/submit/sign_raw_payload 等) を P-256 API stamper で認証して署名を依頼する。stamper credentials だけが Workers Secrets にある
  • signer/turnkey.ts で viem の LocalAccount 互換 wrapper を実装(signTransaction / signMessage を Turnkey 経由に差し替え)
  • 環境分離: dev / prod で別 sub-organization。各々別 stamper credentials、別 signing address
  • Turnkey Policy engine で実行可能関数を構造制限turnkey/policy.json で版管理、PR レビュー対象):
    • eth_tx.to ∈ <ThanksToken contract registry> (登録済み Toban contract のみ)
    • eth_tx.data.selector == 0x<mintFrom selector>mintFrom 以外は拒否、approveMint も拒否、upgradeTo も当然拒否)
    • eth_tx.data.from ∈ <subgraph 同期の連携済み wallet 集合> (関係ない EOA からの mint も拒否)
    • rate limit(per stamper key、Turnkey 側機能成熟後)
  • 三重防壁:
    1. Turnkey TEE: 秘密鍵が外に出ない(開発者・Cloudflare・Worker いずれも触れない)
    2. Turnkey policy: stamper credential が漏れても mintFrom 以外は呼べない
    3. on-chain mintAllowance + mintableAmount: 上記が突破されても各ユーザー上限以上は mint 不可
  • Base ETH ガス補充は bot signer の Ethereum address 宛。残高監視 cron で閾値割り込み時にアラート

鍵 rotation 手順

pkgs/extensions/discord-bot/docs/key-rotation.md に runbook を同梱する。

Scheduled rotation (最低 6 ヶ月に 1 回):

  1. Turnkey で新 signing key を発行(同 sub-org 内、新 address 0xNEW
  2. turnkey/policy.json の対象に 0xNEW を追加する PR を作成・マージ・Turnkey に apply
  3. Workers env TURNKEY_BOT_SIGNER_ADDRESS0xNEW に更新し wrangler deploy
  4. frontend /allowance/discord-bot から旧 signer 0xOLD に紐づく allowance を持つユーザー一覧を表示し、approveMint(0xNEW, value) への移行 UI を提示
  5. platform_links の admin に Discord で告知(bot から各 guild に announce)
  6. 7 日間の grace period(旧鍵もまだ有効)後、Turnkey 側で 0xOLDinactive に切替(新規署名拒否)
  7. 14 日後に 0xOLDdisabled(完全停止 & policy から除去)
  8. docs/ops/rotation-log.md に append:
    • 日付 (YYYY-MM-DD)
    • reason: scheduled / personnel-change / incident-<id>
    • 旧 address / 新 address
    • 実行者
    • 移行率(旧 → 新へ approveMint し直したユーザーの割合)

Emergency rotation (鍵漏洩疑い・異常検知時):

  1. Turnkey で 0xOLD を即時 disabled(Worker からの署名 API が全失敗するようになる)
  2. 新 key 発行 + policy attach + wrangler deploy を 1 時間以内に完遂
  3. platform_links admin と全連携ユーザーに緊急通知(Discord DM + frontend バナー)
  4. 全ユーザーに「approveMint(0xOLD, 0) で旧 signer を revoke」を強く推奨する UI を /allowance/discord-bot に出す
  5. subgraph で MintFromspender == 0xOLD を全件抽出 → 不審 mint の identify と影響範囲確定
  6. インシデントレポート pkgs/extensions/discord-bot/docs/incidents/<YYYY-MM-DD>.md を作成(タイムライン・原因・影響・対策)

Rotation トリガー:

  • 期間: 最後の rotation から 6 ヶ月経過
  • 人事: bot コード maintainer または Turnkey sub-org 権限保持者の入れ替わり
  • 異常検知: Turnkey audit log で mintFrom 以外の API call、policy reject の急増、想定外の from address からの sign 要求
  • contract upgrade: ThanksToken の mintFrom selector が変わる migration 時

stamper key (Workers Secrets) の rotation も同基準:

  • Turnkey 上で新 API stamper を発行 → policy attach → wrangler secret put で更新 → 旧 stamper を Turnkey で revoke
  • stamper key は signing key より低リスク(policy で守られる)だが手順は同じく runbook 化

Verifier 鍵運用

  • VERIFIER_PRIVATE_KEYTurnkey 上の signing key とは別系統(責務分離: bot signer = on-chain mint、verifier = JWT 発行)
  • 対応公開鍵を extensions/identity の env に DISCORD_BOT_VERIFIER_PUBLIC_KEY として登録
  • MVP は Workers Secrets で運用。漏洩しても被害は「他人の Discord 連携を奪取できる」止まりで on-chain mint は奪えない(責務分離効果)
  • 将来は verifier key も Turnkey に移す検討
  • rotation 時は identity 側の公開鍵登録も同時更新(手順は key-rotation.md に同居)

持つ可能性のあるテーブル(オプション)

CREATE TABLE discord_bot_thx_log (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  guild_id      TEXT NOT NULL,
  sender_sf     TEXT NOT NULL,
  recipient_sf  TEXT NOT NULL,
  amount        TEXT NOT NULL,
  message       TEXT,
  tx_hash       TEXT,
  status        TEXT NOT NULL,
  created_at    INTEGER NOT NULL
);

→ subgraph で MintFrom をインデクスすれば代替可能。MVP では Discord 内表示の即時性のため bot ローカルにも持つ可能性あり。

Tasks

  • パッケージ初期化、wrangler.toml、env 型定義
  • Discord Interactions endpoint(Ed25519 検証)
  • verifier.ts: snowflake → JWT 発行(ES256)
  • /toban-link, /toban-setup, /thx, /balance 実装
  • /api/install/callback 実装(OAuth callback + admin Hat 検証)
  • signer/turnkey.ts: Turnkey API で tx 署名する viem LocalAccount 互換 wrapper
  • turnkey/policy.json: mintFrom 専用 policy を版管理(contract registry, selector, from 集合, rate limit)
  • docs/turnkey-setup.md: Turnkey sub-organization 作成・signing key 発行・stamper credentials 発行・policy apply の手順(dev / prod 別)
  • docs/key-rotation.md: scheduled / emergency rotation runbook
  • viem で mintAllowance / mintFrom 呼び出し(署名は Turnkey 経由)
  • Followup message 整形(explorer link, 残量表示)
  • エラーパス: 未連携・残量不足・Turnkey policy reject・revert reason の表示
  • Bot signer の Base ETH 残高監視 cron(gas 補充アラート)
  • Turnkey audit log を週次で pull する Worker cron(オプション、異常検知用)
  • Discord App 設定ドキュメント(slash command manifest, install URL 生成手順)

Out of scope(別 issue)

  • 活動歴ベースの自動 RoleShare 配布(issue feat(contract): 予約分配モジュール(Scheduled Distribution Module) #505 と連携)
  • pending THX(相手未連携時の保留)
  • 多 channel → 多 workspace のマッピング(MVP は guild = 1 workspace)
  • Admin layer の鍵管理(contract owner / upgrade authority。別系統で扱う、本 issue では触れない)
  • 高額 /thx のユーザー対面署名フォールバック(将来)
  • TEE attested deployment of Worker code(Turnkey で基本要件を満たすので将来要件)
  • verifier key の Turnkey 化(MVP は Workers Secrets)

Depends on

  • ThanksToken の mint allowance 実装(child issue)
  • extensions/identity の実装(child issue)
  • frontend の /connect/discord ページ実装

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions