Una app de Lovable es una página en el navegador con un proyecto de Supabase detrás, sin servidor propio. Bot Protection entra en dos piezas: el tag en la página y una Edge Function de Supabase que pregunta a Bot Protection antes de crear la cuenta o empezar la prueba gratuita. El hook de la guía de Supabase Auth cierra el camino alternativo. Lovable escribe todo esto a partir del prompt de abajo; el resto de esta página es lo que escribe, para que puedas leer el diff.
El prompt
Pégalo en el chat de tu proyecto. Con la sesión iniciada, esta página rellena tu propia site key. Después añade tu clave de API secreta a los secrets de Edge Function del proyecto como KORZEN_API_KEY cuando Lovable te la pida, nunca en el 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.
El tag
En index.html, dentro del <head>. El tag observa la página y entrega su token a la función con cada registro. Tu dirección de Lovable y tu propio dominio se aprenden como dominios de tu sitio en las primeras visitas.
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
La Edge Function
Supabase ejecuta las Edge Functions en Deno, que ejecuta el SDK de Node tal cual, importado desde npm. La función pregunta a Bot Protection y luego crea la cuenta con tu anon key, como hacía antes el formulario. Cualquier error, un timeout de más de 200 ms o una clave incorrecta no dan decisión, y el registro sigue adelante.
// 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 }); });
Despliégala con verify_jwt = false para esta función en supabase/config.toml: un visitante que se registra aún no tiene sesión. Lovable hace ambas cosas cuando escribe la función.
El formulario
El formulario llama a la función en lugar de supabase.auth.signUp, con el token del tag en una cabecera: la página y la función están en dominios distintos, así que la cookie no viaja.
const token = (await window.korzen?.ready()) ?? ""; const { data, error } = await supabase.functions.invoke("signup", { body: { email, password }, headers: { "x-korzen-token": token }, });
La prueba gratuita
Donde la app empieza una prueba gratuita en una Edge Function, la misma llamada va primero, con el path /trial. Ahí el visitante ya ha iniciado sesión, así que deja activada la comprobación de JWT de Supabase, y envía el token desde la página del mismo modo.
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
Cierra el camino alternativo
La anon key está en la página, así que un bot todavía puede llamar directamente al registro de Supabase. El hook Before User Created de la guía de Supabase Auth rechaza los registros que no pasaron por tu función: dale a la función el pase que genera la server action de esa guía, con la Web Crypto API en lugar de node:crypto. Añádelo cuando el modo monitor muestre las decisiones que esperas.
La IP del visitante
En una Edge Function, la dirección del visitante es la primera entrada de X-Forwarded-For, como la leen los propios ejemplos de Supabase; ipFrom.vercel lee esa. Compruébalo una vez: envía un registro con un X-Forwarded-For tuyo y mira la IP de la decisión en el panel. Si muestra la dirección que escribiste, escríbenos antes de activar el modo bloqueo.
Pruébalo
- Abre la vista previa de tu app y su página de registro. En las herramientas de desarrollo del navegador, la cookie
_kzaparece tras unos segundos. - Regístrate. La decisión aparece en la página Decisiones del panel, en
/signup. - Comprueba el fail open: pon una
KORZEN_API_KEYincorrecta y regístrate de nuevo. La llamada responde401, y el registro pasa.
La cuenta empieza en modo monitor, donde action es siempre allow: nada cambia para los visitantes hasta que actives el modo bloqueo.
Errores comunes
- La clave de API en la página o en el chat. Solo va en los secrets de la función; la página tiene únicamente la site key.
- El formulario sigue llamando a
supabase.auth.signUppor su cuenta. Entonces la función nunca se ejecuta: búscalo en el código después del cambio de Lovable. - Falta
x-korzen-tokenen las cabeceras de CORS. Entonces el navegador rechaza la llamada antes de que salga. - Un error de la llamada que detiene el registro. Sin decisión significa: deja pasar al visitante.