Lovable アプリは、ブラウザ上のページとその背後の Supabase プロジェクトでできており、独自のサーバーはありません。Bot Protection は 2 つの部品で組み込みます。ページのタグと、アカウントを作成したり無料トライアルを開始したりする前に Bot Protection に問い合わせる Supabase の Edge Function です。Supabase Auth ガイドのフックが、これを迂回する経路を塞ぎます。Lovable は下のプロンプトからこれらをすべて書きます。このページの残りは Lovable が書く内容なので、diff を読むときに使えます。
プロンプト
プロジェクトのチャットに貼り付けてください。サインインしていれば、このページにあなたのサイトキーが入ります。その後、Lovable に求められたら、シークレット API キーをプロジェクトの Edge Function のシークレットに KORZEN_API_KEY として追加します。チャットには決して書かないでください。
Add Korzen Bot Protection to this app, in monitor mode, failing open.
1. In index.html, inside <head>, add exactly:
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
2. Create a Supabase Edge Function "signup" (verify_jwt = false) that the sign-up form
calls instead of supabase.auth.signUp. It imports
npm:@korzen/bot-protection@2 and npm:@supabase/supabase-js@2, reads the secret
KORZEN_API_KEY, and for each POST {email, password}:
- builds the input with fromRequest(req, { ip: ipFrom.vercel, path: "/signup" }) and
calls client.decide(input). decide returns null on any error or timeout: then go on.
- if the action is "block" or "challenge", answers 403 with
{"error": "We could not create your account."} and creates nothing.
- otherwise calls supabase.auth.signUp with the anon key, passing
korzen_request_id (the decision's request_id) in options.data, and answers
what Supabase answered.
- answers OPTIONS for CORS and allows the headers authorization, x-client-info,
apikey, content-type and x-korzen-token.
3. In the sign-up form, before calling the function, get the token with
`await window.korzen?.ready()` and send it as the x-korzen-token header of
supabase.functions.invoke("signup", ...). Show the function's error as the form's
error. Never call /v1/decide from the browser.
4. If the app has a free trial, call the same check with path "/trial" in the Edge
Function that starts it, before the trial is written.
Do not add a CAPTCHA, change the sign-in, or log the API key. Show me the diff.
タグ
index.html の <head> に置きます。タグはページを見守り、新規登録のたびにトークンを関数に渡します。Lovable のアドレスと独自ドメインは、どちらも最初の訪問でサイトのドメインとして学習されます。
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
Edge Function
Supabase は Edge Function を Deno で動かし、Deno は npm からインポートした Node SDK をそのまま動かします。関数は Bot Protection に問い合わせてから、以前フォームがしていたように anon キーでアカウントを作成します。エラー、200 ms を超えるタイムアウト、誤ったキーのいずれでも判定は返らず、新規登録はそのまま進みます。
// supabase/functions/signup/index.ts import { createClient as korzen, fromRequest, ipFrom } from "npm:@korzen/bot-protection@2"; import { createClient } from "npm:@supabase/supabase-js@2"; const bp = korzen({ apiKey: Deno.env.get("KORZEN_API_KEY") ?? "" }); const cors = { "access-control-allow-origin": "*", "access-control-allow-headers": "authorization, x-client-info, apikey, content-type, x-korzen-token", }; Deno.serve(async (req) => { if (req.method === "OPTIONS") return new Response("ok", { headers: cors }); const { email, password } = await req.json(); // The token comes in x-korzen-token; the first X-Forwarded-For entry is the visitor's. const d = await bp.decide(fromRequest(req, { ip: ipFrom.vercel, path: "/signup" })); if (d?.action === "block" || d?.action === "challenge") { return Response.json({ error: "We could not create your account." }, { status: 403, headers: cors }); } const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!); const { data, error } = await supabase.auth.signUp({ email, password, options: { data: { korzen_request_id: d?.request_id ?? null } }, }); return Response.json(error ? { error: error.message } : data, { status: error ? 400 : 200, headers: cors }); });
supabase/config.toml でこの関数に verify_jwt = false を設定して deploy します。新規登録する訪問者にはまだセッションがないためです。Lovable は関数を書くときに両方を行います。
フォーム
フォームは supabase.auth.signUp の代わりに関数を呼び出し、タグのトークンをヘッダーに入れて送ります。ページと関数は別のドメインにあるため、Cookie は届きません。
const token = (await window.korzen?.ready()) ?? ""; const { data, error } = await supabase.functions.invoke("signup", { body: { email, password }, headers: { "x-korzen-token": token }, });
無料トライアル
アプリが Edge Function で無料トライアルを開始する場合は、パス /trial で同じ呼び出しを先に行います。そこでは訪問者はサインイン済みなので、Supabase の JWT チェックはオンのままにし、ページからのトークンも同じ方法で送ります。
const d = await bp.decide(fromRequest(req, { ip: ipFrom.vercel, path: "/trial" })); if (d?.action === "block" || d?.action === "challenge") { return Response.json({ error: "We could not start your trial." }, { status: 403, headers: cors }); } // start the trial as before, keeping d?.request_id with it
迂回経路を塞ぐ
anon キーはページに含まれているため、ボットは Supabase の新規登録を直接呼び出せてしまいます。Supabase Auth ガイドの Before User Created フックは、あなたの関数を経由しなかった新規登録を拒否します。そのガイドの server action が作る通行証を、node:crypto の代わりに Web Crypto API を使って関数に作らせてください。monitor mode で期待どおりの判定が出るのを確認してから追加します。
訪問者の IP
Edge Function では、Supabase 自身の例と同じく、訪問者のアドレスは X-Forwarded-For の最初のエントリです。ipFrom.vercel はそのエントリを読みます。一度確認してください。自分で書いた X-Forwarded-For を付けて新規登録を送り、ダッシュボードで判定の IP を見ます。書いたアドレスが表示された場合は、block mode をオンにする前にご連絡ください。
テストする
- アプリのプレビューを開き、新規登録ページを開きます。数秒後、ブラウザの開発者ツールに
_kzCookie が表示されます。 - 新規登録します。判定がダッシュボードの判定ページに、
/signupで表示されます。 - fail-open を確認します。誤った
KORZEN_API_KEYを設定してもう一度新規登録すると、呼び出しは401を返し、新規登録はそのまま完了します。
アカウントは monitor mode で始まり、その間 action は常に allow です。block mode をオンにするまで、訪問者にとっては何も変わりません。
よくある間違い
- API キーをページやチャットに書く。API キーは関数のシークレットにだけ置きます。ページにはサイトキーしかありません。
- フォームが
supabase.auth.signUpを自分で呼び出したままになっている。その場合、関数は一度も実行されません。Lovable の変更後にコードを検索してください。 - CORS ヘッダーに
x-korzen-tokenがない。その場合、ブラウザは送信前に呼び出しを拒否します。 - 呼び出しのエラーで新規登録が止まる。判定がないということは、訪問者を通すという意味です。