Korzen
Bot Protection
In dieser Dokumentation

Lovable

Bot Protection in einer Lovable-App auf Supabase: der Tag in index.html, Registrierungen und Testphasen über eine Edge Function und ein Prompt, der beides einbaut.

Eine Lovable-App ist eine Seite im Browser mit einem Supabase-Projekt dahinter, ohne eigenen Server. Bot Protection passt in zwei Teile: den Tag in der Seite und eine Supabase Edge Function, die Bot Protection fragt, bevor sie das Konto anlegt oder die Testphase startet. Der Hook aus der Supabase-Auth-Anleitung schließt den Weg daran vorbei. Lovable schreibt das alles aus dem Prompt unten; der Rest dieser Seite zeigt, was es schreibt, damit Sie den Diff lesen können.

Der Prompt

Fügen Sie ihn in den Chat Ihres Projekts ein. Wenn Sie angemeldet sind, setzt diese Seite Ihren eigenen Site Key ein. Fügen Sie dann Ihren geheimen API-Schlüssel als KORZEN_API_KEY zu den Edge-Function-Secrets des Projekts hinzu, wenn Lovable danach fragt, nie in den 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.

Der Tag

In index.html, im <head>. Der Tag beobachtet die Seite und übergibt sein Token bei jeder Registrierung an die Funktion. Ihre Lovable-Adresse und Ihre eigene Domain werden bei den ersten Besuchen als Domains Ihrer Website gelernt.

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

Die Edge Function

Supabase führt Edge Functions auf Deno aus, das das Node-SDK unverändert ausführt, importiert aus npm. Die Funktion fragt Bot Protection und legt dann das Konto mit Ihrem Anon Key an, wie es vorher das Formular tat. Jeder Fehler, ein Timeout über 200 ms oder ein falscher Schlüssel ergibt keine Entscheidung, und die Registrierung läuft weiter.

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

Deployen Sie sie mit verify_jwt = false für diese Funktion in supabase/config.toml: Ein Besucher, der sich registriert, hat noch keine Session. Lovable erledigt beides, wenn es die Funktion schreibt.

Das Formular

Das Formular ruft die Funktion statt supabase.auth.signUp auf, mit dem Token des Tags in einem Header: Seite und Funktion liegen auf verschiedenen Domains, also reist das Cookie nicht mit.

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

Die Testphase

Wo die App eine kostenlose Testphase in einer Edge Function startet, kommt derselbe Aufruf zuerst, mit dem Pfad /trial. Dort ist der Besucher angemeldet, also lassen Sie die JWT-Prüfung von Supabase eingeschaltet und senden das Token von der Seite auf dieselbe Weise.

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

Den Weg daran vorbei schließen

Der Anon Key steht in der Seite, also kann ein Bot die Registrierung von Supabase weiterhin direkt aufrufen. Der Before User Created-Hook aus der Supabase-Auth-Anleitung lehnt Registrierungen ab, die nicht über Ihre Funktion kamen: Geben Sie der Funktion den Pass, den die Server Action dieser Anleitung erzeugt, mit der Web Crypto API statt node:crypto. Fügen Sie ihn hinzu, sobald der Monitor-Modus die Entscheidungen zeigt, die Sie erwarten.

Die IP des Besuchers

In einer Edge Function ist die Adresse des Besuchers der erste Eintrag von X-Forwarded-For, so wie die eigenen Beispiele von Supabase ihn lesen; ipFrom.vercel liest genau diesen. Prüfen Sie es einmal: Senden Sie eine Registrierung mit einem eigenen X-Forwarded-For und sehen Sie sich die IP der Entscheidung im Dashboard an. Zeigt sie die Adresse, die Sie geschrieben haben, schreiben Sie uns, bevor Sie den Block-Modus einschalten.

Testen

  1. Öffnen Sie die Vorschau Ihrer App und deren Registrierungsseite. In den Entwicklertools des Browsers erscheint nach ein paar Sekunden das _kz-Cookie.
  2. Registrieren Sie sich. Die Entscheidung erscheint auf der Seite Entscheidungen des Dashboards, unter /signup.
  3. Prüfen Sie fail-open: Setzen Sie einen falschen KORZEN_API_KEY und registrieren Sie sich erneut. Der Aufruf antwortet 401, und die Registrierung geht durch.

Das Konto beginnt im Monitor-Modus, in dem action immer allow ist: Für Besucher ändert sich nichts, bis Sie den Block-Modus einschalten.

Häufige Fehler

  • Der API-Schlüssel in der Seite oder im Chat. Er gehört allein in die Secrets der Funktion; die Seite hat nur den Site Key.
  • Das Formular ruft supabase.auth.signUp weiterhin selbst auf. Dann läuft die Funktion nie: Suchen Sie nach Lovables Änderung im Code danach.
  • Kein x-korzen-token in den CORS-Headern. Dann lehnt der Browser den Aufruf ab, bevor er rausgeht.
  • Ein Fehler beim Aufruf, der die Registrierung stoppt. Keine Entscheidung bedeutet: den Besucher durchlassen.