Korzen
Bot Protection
Dans cette documentation

Lovable

Bot Protection dans une app Lovable sur Supabase : le tag dans index.html, les inscriptions et les essais gratuits via une Edge Function, et un prompt qui ajoute les deux.

Une app Lovable, c'est une page dans le navigateur avec un projet Supabase derrière, sans serveur à elle. Bot Protection s'y place en deux pièces : le tag dans la page, et une Edge Function Supabase qui interroge Bot Protection avant de créer le compte ou de démarrer l'essai gratuit. Le hook du guide Supabase Auth ferme le chemin de contournement. Lovable écrit tout cela à partir du prompt ci-dessous ; le reste de cette page est ce qu'il écrit, pour que vous puissiez lire le diff.

Le prompt

Collez-le dans le chat de votre projet. Si vous êtes connecté, cette page y met votre propre site key. Ajoutez ensuite votre clé API secrète aux secrets Edge Function du projet sous le nom KORZEN_API_KEY quand Lovable la demande, jamais dans le 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.

Le tag

Dans index.html, dans le <head>. Le tag observe la page et transmet son token à la fonction à chaque inscription. Votre adresse Lovable et votre propre domaine sont tous deux appris comme domaines de votre site lors des premières visites.

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

L'Edge Function

Supabase exécute les Edge Functions sur Deno, qui exécute le SDK Node tel quel, importé depuis npm. La fonction interroge Bot Protection, puis crée le compte avec votre clé anon, comme le faisait le formulaire. Toute erreur, un timeout au-delà de 200 ms ou une mauvaise clé ne donne aucune décision, et l'inscription continue.

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

Déployez-la avec verify_jwt = false pour cette fonction dans supabase/config.toml : un visiteur qui s'inscrit n'a pas encore de session. Lovable fait les deux quand il écrit la fonction.

Le formulaire

Le formulaire appelle la fonction à la place de supabase.auth.signUp, avec le token du tag dans un en-tête : la page et la fonction sont sur des domaines différents, donc le cookie ne suit pas.

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

L'essai gratuit

Là où l'app démarre un essai gratuit dans une Edge Function, le même appel passe en premier, avec le chemin /trial. Le visiteur y est connecté, alors gardez la vérification JWT de Supabase activée, et envoyez le token depuis la page de la même façon.

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

Fermer le contournement

La clé anon est dans la page, donc un bot peut toujours appeler directement l'inscription de Supabase. Le hook Before User Created du guide Supabase Auth refuse les inscriptions qui ne sont pas passées par votre fonction : donnez à la fonction le laissez-passer que crée la server action de ce guide, avec la Web Crypto API à la place de node:crypto. Ajoutez-le une fois que le mode surveillance montre les décisions attendues.

L'IP du visiteur

Dans une Edge Function, l'adresse du visiteur est la première entrée de X-Forwarded-For, comme la lisent les propres exemples de Supabase ; ipFrom.vercel lit celle-là. Vérifiez-le une fois : envoyez une inscription avec un X-Forwarded-For de votre choix et regardez l'IP de la décision dans le tableau de bord. Si elle affiche l'adresse que vous avez écrite, écrivez-nous avant d'activer le mode blocage.

Le tester

  1. Ouvrez l'aperçu de votre app et sa page d'inscription. Dans les outils de développement du navigateur, le cookie _kz apparaît après quelques secondes.
  2. Inscrivez-vous. La décision apparaît sur la page Décisions du tableau de bord, sous /signup.
  3. Vérifiez le fail-open : mettez une mauvaise KORZEN_API_KEY et inscrivez-vous à nouveau. L'appel répond 401, et l'inscription passe.

Le compte commence en mode surveillance, où action est toujours allow : rien ne change pour les visiteurs tant que vous n'activez pas le mode blocage.

Erreurs fréquentes

  • La clé API dans la page ou dans le chat. Elle a sa place dans les secrets de la fonction seulement ; la page n'a que la site key.
  • Le formulaire qui appelle encore supabase.auth.signUp lui-même. La fonction ne s'exécute alors jamais : cherchez cet appel dans le code après la modification de Lovable.
  • Pas de x-korzen-token dans les en-têtes CORS. Le navigateur refuse alors l'appel avant qu'il parte.
  • Une erreur de l'appel qui bloque l'inscription. Pas de décision signifie : laissez passer le visiteur.