Korzen
Bot Protection
Nesta documentação

Lovable

Bot Protection num app Lovable sobre o Supabase: a tag no index.html, cadastros e testes grátis por uma Edge Function, e um prompt que adiciona os dois.

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.

Prompt
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

  1. Abra a prévia do seu app e a página de cadastro. Nas ferramentas de desenvolvedor do navegador, o cookie _kz aparece depois de alguns segundos.
  2. Cadastre-se. A decisão aparece na página Decisões do painel, em /signup.
  3. Confira o fail open: coloque uma KORZEN_API_KEY errada e cadastre-se de novo. A chamada responde 401, 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.signUp por conta própria. Aí a função nunca roda: procure essa chamada no código depois da mudança do Lovable.
  • Sem x-korzen-token nos 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.