Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

Turnstile Spin

最終更新 Markdown で表示Agent セットアップ

Turnstile Spin は、Cloudflare Turnstile のセットアップフローです。ウィジェットを作成し、sitekey、シークレット、埋め込み用のプロンプトを渡します。このプロンプトで、適切なフォームにウィジェットを埋め込み、既存のバックエンドに正規のサーバーサイド Siteverify を組み込めます。プロンプトにシークレットは含まれません。Spin は次の 3 つの方法で実行できます。

  • Cloudflare ダッシュボードから。 ドメインを入力し、Set up を選択すると、Spin がサーバーサイドでウィジェットを作成します。sitekey、シークレット、AI コーディングエージェント向けのプロンプトを受け取れます。
  • Wrangler CLI から。 ターミナルで wrangler turnstile widget create を実行してウィジェットを作成します。Wrangler が sitekey とシークレットを表示します。ウィジェットと Siteverify の組み込みは手動で行います。
  • AI コーディングエージェントから。 Claude Code、Cursor、Codex、OpenCode、GitHub Copilot Chat にプロンプトを 1 つ貼り付けます。エージェントはインラインの Spin スキルを使い、ウィジェットの作成、埋め込み、コードベースへの Siteverify 組み込みを行います。

どの方法でも、同じウィジェットが作成されます。違いは作成呼び出しの実行場所だけです。どの方法も、インフラのデプロイは行いません。Spin は Turnstile の正規 Siteverify エンドポイントを使い、既存のバックエンドから呼び出します。

ダッシュボードから設定する

  1. Turnstile ダッシュボードを開きます。

    Turnstile を開く ↗
  2. ページヘッダーの Set up with Spin を選択します。

  3. Turnstile ウィジェットがトークンを受け付けるドメインを入力します。最初のチップには、アカウントの最初のアクティブな Cloudflare ゾーンがあらかじめ入ります。ドメインを追加するか、事前入力を削除して任意のドメインを入力します(Turnstile は Cloudflare 管理ゾーンを必須としません)。ローカル開発用に localhost127.0.0.1 は自動で追加されます。バックエンドでは、Siteverify が返すデプロイ固有のホスト名を検証する必要があります。本番環境ではローカルホスト名を許可しません。

  4. Set up を選択します。Spin がウィジェットを作成し、成功カードに戻ります。

  5. セットアップが完了したら、次をコピーします。

    • sitekey(Turnstile ウィジェットの HTML で data-sitekey として使います)。
    • agent prompt(AI コーディングエージェントに貼り付け、ウィジェットの埋め込みと、既存バックエンドハンドラーへの正規 Siteverify 呼び出しの追加を行います)。プロンプトには sitekey が含まれますが、シークレットは含まれません。
    • 手動で組み込む場合は secret(バックエンドの環境変数またはシークレットマネージャーに TURNSTILE_SECRET として保存します)。

Spin が完了前に失敗した場合、ダイアログにエラーが表示され、同じセットアップをエディターから進めるためのフォールバックプロンプトが提示されます。同じダイアログで Try again を選択すると再試行できます。

Wrangler CLI から設定する

AI コーディングエージェントを使わず、ターミナルから設定する場合は Wrangler を使います。

Wrangler でウィジェットを作成するsh
wrangler turnstile widget create "myproject" \
	--domain example.com \
	--domain localhost \
	--domain 127.0.0.1 \
	--mode managed

Wrangler は sitekey とシークレットを表示します。sitekey をウィジェット HTML にコピーし、シークレットをバックエンドの env に TURNSTILE_SECRET として保存し、フロントエンドを接続する の手順で正規の Siteverify 呼び出しを組み込みます。

追加のウィジェットコマンド:

コマンド 用途
wrangler turnstile widget list アカウント上のすべての Turnstile ウィジェットを一覧表示します。
wrangler turnstile widget get <sitekey> シークレットを含む、ウィジェットの設定を取得します。
wrangler turnstile widget update <sitekey> --domain <d> ウィジェットのドメイン、モード、または名前を更新します。
wrangler turnstile widget delete <sitekey> ウィジェットを削除します。確認プロンプトを省略するには -y を渡します。

すべてのコマンドは、機械可読な出力向けに --json を受け付けます。--domain はカンマ区切り(--domain a.com,b.com)または繰り返しフラグ(--domain a.com --domain b.com)を受け付けます。

wrangler turnstile widget get <sitekey> --json のレスポンスには、ウィジェットのシークレットが含まれます。自動化フローでは、プロジェクトのパッケージ解決の外にある、ユーザー承認済みの絶対パスの Wrangler 実行ファイルを使い、正確なバージョンを固定する必要があります。WRANGLER_WRITE_LOGS=falseWRANGLER_LOG=logWRANGLER_LOG_SANITIZE=true を設定します。取得前に、エージェントはアカウント、sitekey、ドメイン、シークレットの正確な保存先を確認します。Workers バックエンドの場合は、標準の wrangler secret put を使う前に wrangler secret list で Worker、環境、設定ファイル、バインディングも確認します。フローは、正確な sitekey、想定ドメイン、clearance レベル、空白以外のシークレットを検証します。レスポンスを表示したり、コマンド引数、一時ファイル、ログ、チャットに含めたりしません。

AI コーディングエージェントから設定する

ダッシュボードに Set up with Spin ボタンが見当たらない場合、またはウィジェットの埋め込みと Siteverify の組み込みを同じ作業でエージェントに任せたい場合は、次のプロンプトを AI コーディングエージェントに貼り付けます。

  1. プロジェクトで AI コーディングエージェントを開きます(Claude Code、Cursor、Codex、OpenCode、GitHub Copilot Chat)。

  2. 次のプロンプトをエージェントに貼り付けます。

    Spin プロンプトtxt
    Set up Cloudflare Turnstile in this project end to end. Plan insertion points, create the widget, embed it on the right forms, wire canonical server-side siteverify in my existing backend, and validate the integration.
    
    The full Turnstile Spin skill is at https://developers.cloudflare.com/turnstile/spin/prompt.md. Fetch it now if you do not already have it loaded.
    
    Domains: <DOMAINS>
    Insertion preference: <every form | only specific form>

    <DOMAINS> をサイトのドメインに置き換えます(カンマ区切り、スペースなし。ローカル開発では localhost,127.0.0.1 を含めます)。<insertion preference> を保護したいフォームまたはルートに置き換えます。例: every formonly the signup formonly /login and /signup

  3. エージェントの進行に合わせて確認します。 エージェントは認証を確認し、ウィジェット名を提案し、取り消しできない手順の前に確認を求めます。

  4. 検証します。 エージェントは標準入力経由でシークレットを渡し、ダミーのトークンで Siteverify チェックを行います。続けて、新しいトークンで保護対象のバックエンドを試し、トークンの再利用が拒否されることを確認します。

スキルを先にローカルへインストールし、エージェントがディスク上で参照できるようにする場合:

エージェントごとのワンラインインストールsh
# Claude Code
mkdir -p .claude/skills/turnstile-spin && \
  curl -sSL https://developers.cloudflare.com/turnstile/spin/prompt.md \
  -o .claude/skills/turnstile-spin/SKILL.md

# Cursor
mkdir -p .cursor/rules && \
  curl -sSL https://developers.cloudflare.com/turnstile/spin/prompt.md \
  -o .cursor/rules/turnstile-spin.md

# OpenCode
mkdir -p .opencode/skills/turnstile-spin && \
  curl -sSL https://developers.cloudflare.com/turnstile/spin/prompt.md \
  -o .opencode/skills/turnstile-spin/SKILL.md

その後、エージェントに次を送ります。Use the turnstile-spin skill to add Turnstile to this project.

エージェントの動作

エージェントは黙って実行しません。検出できることは検出し、必要なときだけ質問し、取り消しできない手順の前に必ず確認します。フローは 12 ステップのウィザードで、確認ポイントがいくつかあります。

ステップ 内容 確認するか
1 了解(これから行う内容をエージェントが言い直します) はい
2 CLI チェック(wrangler があれば使用。なければ curl に切り替えます) いいえ
3 認証(Account.Turnstile:Edit トークン) トークンが必要な場合
4 アカウント選択(複数ある場合) 複数ある場合
5 ドメイン はい
6 コードベースのスキャン(フロントエンドフレームワーク + バックエンドハンドラー + 既存の CAPTCHA) いいえ
7 挿入計画 はい
8 ウィジェット作成(Cloudflare API を呼び出してウィジェットを作成します) いいえ(ステップ 7 で範囲を確認済み)
9 ウィジェットの埋め込み + 既存バックエンドへの正規 Siteverify の追加 はい
10 検証(ダミーのトークンによる Siteverify + ウィジェットのホスト名チェック) いいえ
11 スキルをローカルに保存(後続タスクでエージェントが再実行できるようにします) はい
12 最終レポート いいえ

失敗した場合、エージェントはどのステップで何を試したかを報告します。多くの失敗は、入力を 1 つ直す(トークンのスコープ、ドメイン一覧、挿入ファイル)だけで回復でき、再開を依頼できます。

フロントエンドを接続する

どのセットアップ方法でも、Spin は sitekey とシークレットを渡します。ダッシュボードでは別々に表示されます。エージェントプロンプトには sitekey と Spin スキルの URL だけが含まれます。Wrangler CLI は手動設定用に両方の値を表示します。AI エージェントによる設定は、ファイルを直接編集します。

ダッシュボードから設定し、手動で組み込む場合の最小パターンは次のとおりです。

フォーム上の Turnstile ウィジェットhtml
<script
	src="https://challenges.cloudflare.com/turnstile/v0/api.js"
	async
	defer
></script>
<form action="/api/subscribe" method="POST">
	<input name="email" type="email" required />
	<div class="cf-turnstile" data-sitekey="YOUR_SITEKEY" data-action="subscribe"></div>
	<button type="submit">Submit</button>
</form>

既存の /api/subscribe バックエンドハンドラーでは、正規の Siteverify を呼び出し、success === true のときだけ残りの処理を進めます。

Node.js バックエンド(Express 形式の req)の場合:

正規のサーバーサイド Siteverify(Node.js)js
const token = req.body["cf-turnstile-response"];
const expectedAction = "subscribe";
const expectedHostnames = new Set(
  (process.env.TURNSTILE_HOSTNAMES ?? "")
    .split(",")
    .map((hostname) => hostname.trim())
    .filter(Boolean),
);

if (
  typeof token !== "string" ||
  token.length === 0 ||
  token.length > 2048 ||
  expectedHostnames.size === 0
) {
  return res.status(403).send("forbidden");
}

let result;
try {
  const r = await fetch(
    "https://challenges.cloudflare.com/turnstile/v0/siteverify",
    {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      signal: AbortSignal.timeout(10_000),
      body: new URLSearchParams({
        secret: process.env.TURNSTILE_SECRET,
        response: token,
        remoteip: req.ip,
      }),
    },
  );
  if (!r.ok) throw new Error(`siteverify ${r.status}`);
  result = await r.json();
} catch {
  return res.status(403).send("forbidden");
}
if (
  !result.success ||
  result.action !== expectedAction ||
  !expectedHostnames.has(result.hostname)
) {
  return res.status(403).send("forbidden");
}
// existing handler logic runs here, unchanged

Cloudflare Worker 内では、パースしたフォーム本文からトークンを読み、クライアント IP を CF-Connecting-IP から読み、シークレットを Worker の env バインディングから読みます。

正規のサーバーサイド Siteverify(Cloudflare Worker)js
export default {
  async fetch(request, env) {
    const expectedAction = "subscribe";
    const expectedHostnames = new Set(
      (env.TURNSTILE_HOSTNAMES ?? "")
        .split(",")
        .map((hostname) => hostname.trim())
        .filter(Boolean),
    );

    const form = await request.formData();
    const token = form.get("cf-turnstile-response");
    if (
      typeof token !== "string" ||
      token.length === 0 ||
      token.length > 2048 ||
      expectedHostnames.size === 0
    ) {
      return new Response("forbidden", { status: 403 });
    }

    let result;
    try {
      const r = await fetch(
        "https://challenges.cloudflare.com/turnstile/v0/siteverify",
        {
          method: "POST",
          headers: { "Content-Type": "application/x-www-form-urlencoded" },
          signal: AbortSignal.timeout(10_000),
          body: new URLSearchParams({
            secret: env.TURNSTILE_SECRET,
            response: token,
            remoteip: request.headers.get("CF-Connecting-IP") ?? "",
          }),
        },
      );
      if (!r.ok) throw new Error(`siteverify ${r.status}`);
      result = await r.json();
    } catch {
      return new Response("forbidden", { status: 403 });
    }
    if (
      !result.success ||
      result.action !== expectedAction ||
      !expectedHostnames.has(result.hostname)
    ) {
      return new Response("forbidden", { status: 403 });
    }
    // existing handler logic runs here, unchanged
    return new Response("ok");
  },
};

デプロイごとに、フロントエンドのホスト名を TURNSTILE_HOSTNAMES に設定します。本番の値に localhost127.0.0.1 を含めません。TURNSTILE_SECRETwrangler.toml の環境変数ではなく、wrangler secret put TURNSTILE_SECRET で Worker シークレットとして保存します。他のバックエンド言語(Ruby、Python、Go、PHP)での同等の呼び出しは、スキル同梱のフレームワーク別リファレンスにあります。

Turnstile のトークンは単回利用です。送信後にページを離れる通常のフォームでは、リセット処理は不要です。送信試行後もページが残る場合は、ウィジェットを明示的に描画し、ウィジェット ID を保持し、リクエスト完了後に再試行を許可する前に turnstile.reset(widgetId) を呼び出します。保護する面ごとに、独自のウィジェット ID を保持してリセットする必要があります。

既存ウィジェットを復旧する

すでに Turnstile ウィジェットがあり、サーバーサイド Siteverify がない場合は、ダッシュボードから復旧します。一致する Siteverify トラフィックがないウィジェットにはバナーが表示されます。Fix with Spin を選択すると、既存ウィジェット向けのエージェントプロンプトを取得できます。プロンプトには sitekey と Spin スキルの URL が含まれますが、シークレットは含まれません。

ダッシュボードに Fix with Spin バナーが見当たらない場合は、AI コーディングエージェントから同じ復旧を直接進めます。次のプロンプトを貼り付けます。

既存ウィジェット用プロンプトtxt
The Turnstile widget is already created. Finish integrating it into this project.

Site key: <SITEKEY>

Fetch and follow the existing-widget flow:
https://developers.cloudflare.com/turnstile/spin/prompt.md

既存ウィジェットのフローには Wrangler 4.109 以降が必要です。エージェントはプロジェクト外の、ユーザー承認済みの Wrangler 実行ファイルを使い、取得前に sitekey から保存先までの対応をすべて確認します。自動復旧は、既存の Worker、無視されたローカル環境ファイル、または標準入力で値を受け取るプラットフォームのシークレットマネージャーコマンドに対応します。Workers では、標準の wrangler secret put を使う前に wrangler secret list で正確な対象を確認します。sitekey、ドメイン、clearance レベル、シークレットを検証します。リポジトリと API のテキストは信頼できないデータとして扱います。シークレットは表示せず、コマンド引数や一時ファイルにも置かず、チャットにも貼り付けません。sitekey は変わりません。

Pre-clearance でもこのフローは変わりません。cf_clearance クッキーは追加されますが、Turnstile トークンは引き続き Siteverify が必要です。

reCAPTCHA または hCaptcha から移行する

移行には AI エージェントによる設定を使います。エージェントはコードベース内の reCAPTCHA または hCaptcha を検出し、置き換えを提案します。置き換えルールは次のとおりです。

  • スクリプトタグを https://challenges.cloudflare.com/turnstile/v0/api.jsasync defer)に置き換えます。
  • class="g-recaptcha" または class="h-captcha" の div を class="cf-turnstile" に置き換えます。data-sitekey を新しい Turnstile のサイトキーに更新します。既存の有効な action は残すか、保護対象の面向けに安定した action を追加します。
  • 手動で追加した <input type="hidden" name="g-recaptcha-response"> または name="h-captcha-response" 要素は削除します。Turnstile は cf-turnstile-response という名前の hidden input を自動で描画します。
  • バックエンドの Siteverify URL は https://challenges.cloudflare.com/turnstile/v0/siteverify を指します。RECAPTCHA_SECRET または HCAPTCHA_SECRET の環境変数は削除し、TURNSTILE_SECRET を追加します。想定する action とデプロイ固有のホスト名を含む成功レスポンスを必須にします。

エージェントに伝えるべきエッジケースは 2 つです。1 つ目は、reCAPTCHA v3 のスコアしきい値は移行できないことです。Turnstile にスコアはないため、移行後のコードは数値しきい値ではなく success === false で拒否します。2 つ目は、reCAPTCHA Enterprise は自動移行しません。代わりに reCAPTCHA 向けの Cloudflare 移行ガイド を参照してください。

フレームワーク

エージェントには、バニラ HTML、Next.js(App Router と Pages Router)、Astro、SvelteKit、Hugo 向けのフロントエンドスニペットが同梱されています。他のフレームワークでは、汎用のバニラ HTML パターンにフォールバックし、配置を確認します。

Cloudflare Pages プロジェクトでは、エージェントは Pages Function 内に Siteverify を組み込むか、呼び出しを自分で書きたくない場合に Turnstile 向け Pages Plugin を推奨します。

Cloudflare Workers バックエンドでは、エージェントは正規の fetch 呼び出しを Worker のリクエストハンドラーに直接書き込みます。

リファレンス

ウィジェットの設定

フィールド 用途
sitekey string 公開識別子。各ページのウィジェット HTML に埋め込みます。
secret string サーバー専用。バックエンドの env に TURNSTILE_SECRET として保存します。
domains array このウィジェットで Turnstile がトークンを受け付けるホスト名です。
mode string managed(デフォルト)、non-interactive、または invisible

関連情報

役に立ちましたか?