Um app Lovable é uma página no navegador com um projeto Supabase por trás, sem servidor próprio. O Bot Protection entra em duas peças: a tag na página e uma Edge Function do Supabase que pergunta ao Bot Protection antes de criar a conta ou começar o teste grátis. O hook do guia do Supabase Auth fecha o caminho por fora. O Lovable escreve tudo isso a partir do prompt abaixo; o resto desta página é o que ele escreve, para você conseguir ler o diff.
O prompt
Cole no chat do seu projeto. Se você estiver logado, esta página já traz a sua site key. Depois adicione a sua chave de API secreta aos secrets de Edge Function do projeto como KORZEN_API_KEY quando o Lovable pedir, nunca no chat.
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.
A tag
Em index.html, no <head>. A tag observa a página e entrega o token dela para a função a cada cadastro. O seu endereço do Lovable e o seu domínio próprio são aprendidos como domínios do seu site nas primeiras visitas.
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
A Edge Function
O Supabase roda as Edge Functions no Deno, que roda o SDK de Node como ele é, importado do npm. A função pergunta ao Bot Protection e depois cria a conta com a sua anon key, como o formulário fazia antes. Qualquer erro, um timeout acima de 200 ms ou uma chave errada não dá decisão, e o cadastro segue.
// 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 }); });
Faça o deploy com verify_jwt = false para esta função em supabase/config.toml: um visitante que se cadastra ainda não tem sessão. O Lovable faz as duas coisas quando escreve a função.
O formulário
O formulário chama a função no lugar de supabase.auth.signUp, com o token da tag num cabeçalho: a página e a função ficam em domínios diferentes, então o cookie não viaja.
const token = (await window.korzen?.ready()) ?? ""; const { data, error } = await supabase.functions.invoke("signup", { body: { email, password }, headers: { "x-korzen-token": token }, });
O teste grátis
Onde o app começa um teste grátis numa Edge Function, a mesma chamada vem primeiro, com o path /trial. Ali o visitante já está logado, então mantenha a verificação de JWT do Supabase ligada, e envie o token da página do mesmo jeito.
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
Feche o caminho por fora
A anon key está na página, então um bot ainda consegue chamar o cadastro do Supabase direto. O hook Before User Created do guia do Supabase Auth recusa cadastros que não passaram pela sua função: dê à função o passe que a server action daquele guia gera, com a Web Crypto API no lugar de node:crypto. Adicione o hook quando o modo monitor mostrar as decisões que você espera.
O IP do visitante
Numa Edge Function, o endereço do visitante é a primeira entrada do X-Forwarded-For, como os próprios exemplos do Supabase o leem; ipFrom.vercel lê essa entrada. Confira uma vez: envie um cadastro com um X-Forwarded-For seu e veja o IP da decisão no painel. Se aparecer o endereço que você escreveu, escreva para nós antes de ligar o modo bloqueio.
Teste
- Abra a prévia do seu app e a página de cadastro. Nas ferramentas de desenvolvedor do navegador, o cookie
_kzaparece depois de alguns segundos. - Cadastre-se. A decisão aparece na página Decisões do painel, em
/signup. - Confira o fail open: coloque uma
KORZEN_API_KEYerrada e cadastre-se de novo. A chamada responde401, e o cadastro passa.
A conta começa no modo monitor, em que a action é sempre allow: nada muda para os visitantes até você ligar o modo bloqueio.
Erros comuns
- A chave de API na página ou no chat. Ela fica só nos secrets da função; a página tem apenas a site key.
- O formulário ainda chamando
supabase.auth.signUppor conta própria. Aí a função nunca roda: procure essa chamada no código depois da mudança do Lovable. - Sem
x-korzen-tokennos cabeçalhos de CORS. Aí o navegador recusa a chamada antes de ela sair. - Um erro da chamada que trava o cadastro. Sem decisão quer dizer: deixe o visitante passar.