Korzen
Bot Protection
In deze documentatie

Lovable

Bot Protection in een Lovable-app op Supabase: de tag in index.html, registraties en proefperiodes via een Edge Function, en een prompt die beide toevoegt.

Een Lovable-app is een pagina in de browser met een Supabase-project erachter, zonder eigen server. Bot Protection past erin in twee delen: de tag in de pagina, en een Supabase Edge Function die Bot Protection raadpleegt voordat die het account aanmaakt of de proefperiode start. De hook uit de Supabase Auth-gids sluit de weg eromheen af. Lovable schrijft dit allemaal vanuit de prompt hieronder; de rest van deze pagina is wat het schrijft, zodat je de diff kunt lezen.

De prompt

Plak hem in de chat van je project. Als je bent ingelogd, vult deze pagina je eigen site key in. Voeg daarna je geheime API-sleutel toe aan de Edge Function-secrets van het project als KORZEN_API_KEY wanneer Lovable erom vraagt, nooit in de 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.

De tag

In index.html, in de <head>. De tag bekijkt de pagina en geeft zijn token bij elke registratie mee aan de function. Je Lovable-adres en je eigen domein worden bij de eerste bezoeken allebei geleerd als domeinen van je site.

<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>

De Edge Function

Supabase draait Edge Functions op Deno, dat de Node-SDK draait zoals hij is, geïmporteerd uit npm. De function raadpleegt Bot Protection en maakt daarna het account aan met je anon key, zoals het formulier eerder deed. Elke fout, een timeout van meer dan 200 ms of een verkeerde sleutel geeft geen beslissing, en de registratie gaat door.

// 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 });
});

Deploy hem met verify_jwt = false voor deze function in supabase/config.toml: een bezoeker die zich registreert, heeft nog geen sessie. Lovable doet allebei wanneer het de function schrijft.

Het formulier

Het formulier roept de function aan in plaats van supabase.auth.signUp, met het token van de tag in een header: de pagina en de function staan op verschillende domeinen, dus de cookie reist niet mee.

const token = (await window.korzen?.ready()) ?? "";
const { data, error } = await supabase.functions.invoke("signup", {
  body: { email, password },
  headers: { "x-korzen-token": token },
});

De proefperiode

Waar de app een gratis proefperiode start in een Edge Function, gaat dezelfde call eerst, met het pad /trial. De bezoeker is daar ingelogd, dus laat de JWT-controle van Supabase aanstaan, en stuur het token vanaf de pagina op dezelfde manier.

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

Sluit de weg eromheen af

De anon key staat in de pagina, dus een bot kan de registratie van Supabase nog steeds rechtstreeks aanroepen. De Before User Created hook uit de Supabase Auth-gids weigert registraties die niet via je function kwamen: geef de function de pas die de server action uit die gids maakt, met de Web Crypto API in plaats van node:crypto. Voeg hem toe zodra de monitormodus de beslissingen laat zien die je verwacht.

Het IP-adres van de bezoeker

In een Edge Function is het adres van de bezoeker de eerste vermelding in X-Forwarded-For, zoals de eigen voorbeelden van Supabase het lezen; ipFrom.vercel leest precies die. Controleer het één keer: stuur een registratie met een eigen X-Forwarded-For en bekijk het IP-adres van de beslissing op het dashboard. Toont het het adres dat je zelf schreef, schrijf ons dan voordat je de blokkeermodus aanzet.

Test het

  1. Open de preview van je app en de registratiepagina. In de developer tools van de browser verschijnt na een paar seconden de _kz-cookie.
  2. Registreer je. De beslissing verschijnt op de pagina Beslissingen van het dashboard, onder /signup.
  3. Controleer fail-open: zet een verkeerde KORZEN_API_KEY en registreer je opnieuw. De call antwoordt met 401, en de registratie gaat door.

Het account begint in monitormodus, waar action altijd allow is: voor bezoekers verandert er niets tot je de blokkeermodus aanzet.

Veelgemaakte fouten

  • De API-sleutel in de pagina of in de chat. Die hoort alleen in de secrets van de function; de pagina heeft alleen de site key.
  • Het formulier dat supabase.auth.signUp nog zelf aanroept. De function draait dan nooit: zoek ernaar in de code na de wijziging van Lovable.
  • Geen x-korzen-token in de CORS-headers. De browser weigert de call dan voordat die vertrekt.
  • Een fout van de call die de registratie tegenhoudt. Geen beslissing betekent: laat de bezoeker door.