Korzen
Bot Protection
In questa documentazione

Lovable

Bot Protection in un'app Lovable su Supabase: il tag in index.html, registrazioni e prove gratuite tramite una Edge Function, e un prompt che aggiunge entrambi.

Un'app Lovable è una pagina nel browser con un progetto Supabase dietro, senza un server suo. Bot Protection entra in due pezzi: il tag nella pagina e una Edge Function di Supabase che chiede a Bot Protection prima di creare l'account o avviare la prova gratuita. L'hook della guida Supabase Auth chiude la strada che la aggira. Lovable scrive tutto questo dal prompt qui sotto; il resto della pagina è quello che scrive, così puoi leggere il diff.

Il prompt

Incollalo nella chat del tuo progetto. Se hai fatto l'accesso, questa pagina inserisce la tua site key. Poi aggiungi la tua chiave API segreta ai secret delle Edge Function del progetto come KORZEN_API_KEY quando Lovable la chiede, mai nella 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.

Il tag

In index.html, nel <head>. Il tag osserva la pagina e passa il suo token alla funzione a ogni registrazione. Il tuo indirizzo Lovable e il tuo dominio vengono entrambi appresi come domini del tuo sito alle prime visite.

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

La Edge Function

Supabase esegue le Edge Function su Deno, che esegue l'SDK Node così com'è, importato da npm. La funzione chiede a Bot Protection, poi crea l'account con la tua anon key, come faceva prima il form. Qualsiasi errore, un timeout oltre i 200 ms o una chiave sbagliata non danno alcuna decisione, e la registrazione prosegue.

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

Fai il deploy con verify_jwt = false per questa funzione in supabase/config.toml: un visitatore che si registra non ha ancora una sessione. Lovable fa entrambe le cose quando scrive la funzione.

Il form

Il form chiama la funzione al posto di supabase.auth.signUp, con il token del tag in un header: la pagina e la funzione sono su domini diversi, quindi il cookie non viaggia.

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

La prova gratuita

Dove l'app avvia una prova gratuita in una Edge Function, la stessa chiamata viene prima, con il path /trial. Lì il visitatore ha già fatto l'accesso, quindi lascia attivo il controllo JWT di Supabase e invia il token dalla pagina allo stesso 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

Chiudi la strada che la aggira

La anon key è nella pagina, quindi un bot può ancora chiamare direttamente la registrazione di Supabase. L'hook Before User Created della guida Supabase Auth rifiuta le registrazioni che non sono passate dalla tua funzione: dai alla funzione il lasciapassare che crea la server action di quella guida, con la Web Crypto API al posto di node:crypto. Aggiungilo quando la modalità monitor mostra le decisioni che ti aspetti.

L'IP del visitatore

In una Edge Function l'indirizzo del visitatore è la prima voce di X-Forwarded-For, come la leggono gli esempi di Supabase stessi; ipFrom.vercel legge proprio quella. Verificalo una volta: invia una registrazione con un X-Forwarded-For scelto da te e guarda l'IP della decisione nella dashboard. Se mostra l'indirizzo che hai scritto, scrivici prima di attivare la modalità blocco.

Provalo

  1. Apri l'anteprima della tua app e la sua pagina di registrazione. Negli strumenti per sviluppatori del browser, il cookie _kz compare dopo qualche secondo.
  2. Registrati. La decisione compare nella pagina Decisioni della dashboard, sotto /signup.
  3. Verifica il fail-open: imposta una KORZEN_API_KEY sbagliata e registrati di nuovo. La chiamata risponde 401, e la registrazione va a buon fine.

L'account parte in modalità monitor, dove action è sempre allow: per i visitatori non cambia niente finché non attivi la modalità blocco.

Errori comuni

  • La chiave API nella pagina o nella chat. Va solo nei secret della funzione; la pagina ha soltanto la site key.
  • Il form che chiama ancora supabase.auth.signUp da solo. Allora la funzione non gira mai: cercala nel codice dopo la modifica di Lovable.
  • Nessun x-korzen-token negli header CORS. Allora il browser rifiuta la chiamata prima che parta.
  • Un errore della chiamata che blocca la registrazione. Nessuna decisione significa: lascia passare il visitatore.