Chaque serveur de cette page donne à votre code le Request de l'API Fetch, donc un seul client les sert tous : korzen.js, un seul fichier sans dépendance, qui interroge /v1/decide, abandonne à votre timeout et laisse passer le visiteur à la moindre erreur. Seuls changent l'endroit où arrive la requête et celui où se trouve l'IP du visiteur. Pour Next.js, voir Next.js ; pour un Worker Cloudflare, Cloudflare Workers.
Le tag
Placez le tag dans le <head> du layout qui rend vos pages d'inscription, de connexion et de checkout :
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
Dans la page, avant d'envoyer le formulaire, appelez await window.korzen?.ready() : il signale ce que le visiteur a fait depuis le dernier signalement du tag, et se résout en quelques secondes quoi qu'il arrive. Le tag
Le client
Téléchargez-le dans le dossier de votre code serveur, et lisez-le avant de le committer :
curl -o bot-protection.js https://korzen.dev/bot-protection/sdk/korzen.js curl -o bot-protection.d.ts https://korzen.dev/bot-protection/sdk/korzen.d.ts # types, for TypeScript
Puis un client pour le processus, à côté. Les exemples ci-dessous l'importent depuis korzen.js ; dans React Router, nommez-le app/korzen.server.js, dans SvelteKit src/lib/server/korzen.js, pour qu'il n'atteigne jamais le bundle du navigateur :
// korzen.js: one client for the process import { createClient } from "./bot-protection.js"; export const korzen = createClient({ apiKey: process.env.KORZEN_API_KEY, url: "https://korzen.dev", timeoutMs: 200 }); export { fromHeaders, fromRequest, ipFrom } from "./bot-protection.js";
korzen.decide(fromRequest(request, { ip })) lit le User-Agent de la requête, les headers que lit Bot Protection, le cookie _kz, le chemin et la méthode, et renvoie la réponse, ou null à la moindre erreur. Il ne lit pas le corps, donc votre code lit encore le formulaire après lui. Les Headers de l'API Fetch ne gardent aucun ordre, donc aucun ordre des headers n'est envoyé.
Hono
Un middleware sur la route qui crée le compte ; la même application tourne sur Node, Bun et Deno :
// app.js import { Hono } from "hono"; import { serve } from "@hono/node-server"; // on Bun or Deno, serve app.fetch with their own server import { fromRequest, ipFrom, korzen } from "./korzen.js"; const app = new Hono(); // Asks before the handler, and keeps the answer as c.get("korzen") (null when there is none). async function guard(c, next) { const d = await korzen.decide(fromRequest(c.req.raw, { ip: ipFrom.lastForwarded })); // see "The visitor's IP" if (d?.action === "block") return c.text("We could not create your account.", 400); // "challenge": your CAPTCHA, or an email or phone check, before going on c.set("korzen", d); await next(); } app.post("/signup", guard, async (c) => { const user = await createAccount(await c.req.parseBody()); // your own code await saveRequestID(user.id, c.get("korzen")?.request_id); // see "Keep the request_id" return c.redirect("/welcome", 303); }); serve({ fetch: app.fetch, port: 3000 });
Bun
Avec les routes de Bun.serve (Bun 1.2.3 et plus récent), le serveur donne l'adresse de la connexion :
// server.js: bun run server.js import { fromRequest, korzen } from "./korzen.js"; Bun.serve({ port: 3000, routes: { "/signup": { POST: async (req, server) => { const d = await korzen.decide(fromRequest(req, { ip: server.requestIP(req)?.address })); // see "The visitor's IP" if (d?.action === "block") return new Response("We could not create your account.", { status: 400 }); // "challenge": your CAPTCHA, or an email or phone check, before going on const user = await createAccount(await req.formData()); // your own code await saveRequestID(user.id, d?.request_id); // see "Keep the request_id" return Response.redirect(new URL("/welcome", req.url), 303); }, }, }, });
Deno
// server.js: deno run --allow-net --allow-env server.js import { fromRequest, korzen } from "./korzen.js"; Deno.serve({ port: 3000 }, async (req, info) => { if (req.method !== "POST" || new URL(req.url).pathname !== "/signup") return new Response("Not found", { status: 404 }); const d = await korzen.decide(fromRequest(req, { ip: info.remoteAddr.hostname })); // see "The visitor's IP" if (d?.action === "block") return new Response("We could not create your account.", { status: 400 }); // "challenge": your CAPTCHA, or an email or phone check, before going on const user = await createAccount(await req.formData()); // your own code await saveRequestID(user.id, d?.request_id); // see "Keep the request_id" return Response.redirect(new URL("/welcome", req.url), 303); });
React Router et Remix
Dans l'action de la route qui crée le compte (le mode framework de React Router, qu'est devenu Remix) :
// app/routes/signup.jsx import { data, redirect } from "react-router"; import { fromRequest, ipFrom, korzen } from "../korzen.server.js"; export async function action({ request }) { const d = await korzen.decide(fromRequest(request, { ip: ipFrom.lastForwarded })); // see "The visitor's IP" if (d?.action === "block") return data({ error: "We could not create your account." }, { status: 400 }); // "challenge": your CAPTCHA, or an email or phone check, before going on const user = await createAccount(await request.formData()); // your own code await saveRequestID(user.id, d?.request_id); // see "Keep the request_id" return redirect("/welcome"); }
Astro
Un endpoint rendu à la demande, avec un adaptateur serveur (comme @astrojs/node), qui donne clientAddress :
// src/pages/api/signup.js import { fromRequest, korzen } from "../../korzen.js"; export const prerender = false; export async function POST({ request, clientAddress, redirect }) { const d = await korzen.decide(fromRequest(request, { ip: clientAddress, path: "/signup" })); // see "The visitor's IP" if (d?.action === "block") return new Response("We could not create your account.", { status: 400 }); // "challenge": your CAPTCHA, or an email or phone check, before going on const user = await createAccount(await request.formData()); // your own code await saveRequestID(user.id, d?.request_id); // see "Keep the request_id" return redirect("/welcome", 303); }
SvelteKit
Dans l'action de formulaire de la page d'inscription ; getClientAddress() est l'IP que lit votre adaptateur :
// src/routes/signup/+page.server.js import { fail, redirect } from "@sveltejs/kit"; import { fromRequest, korzen } from "$lib/server/korzen.js"; export const actions = { default: async ({ request, getClientAddress }) => { const d = await korzen.decide(fromRequest(request, { ip: getClientAddress() })); // see "The visitor's IP" if (d?.action === "block") return fail(400, { error: "We could not create your account." }); // "challenge": your CAPTCHA, or an email or phone check, before going on const user = await createAccount(await request.formData()); // your own code await saveRequestID(user.id, d?.request_id); // see "Keep the request_id" redirect(303, "/welcome"); }, };
Nuxt
Une route serveur. fromHeaders(event.headers) lit les headers de la requête et laisse son corps à readFormData ; construire un second Request avec toWebRequest(event) laisserait le formulaire non lu :
// server/api/signup.post.js import { fromHeaders, ipFrom, korzen } from "../utils/korzen.js"; export default defineEventHandler(async (event) => { const d = await korzen.decide(fromHeaders(event.headers, { ip: ipFrom.lastForwarded, path: "/api/signup" })); // see "The visitor's IP" if (d?.action === "block") { setResponseStatus(event, 400); return "We could not create your account."; } // "challenge": your CAPTCHA, or an email or phone check, before going on const user = await createAccount(await readFormData(event)); // your own code await saveRequestID(user.id, d?.request_id); // see "Keep the request_id" return sendRedirect(event, "/welcome", 303); });
L'IP du visiteur
Envoyez l'adresse qu'a vue votre propre proxy, jamais une que le visiteur aurait pu écrire. Sans rien devant votre serveur, c'est l'adresse de la connexion : server.requestIP(req) dans Bun, info.remoteAddr dans Deno. Derrière un load balancer ou un nginx à vous, c'est la dernière entrée de X-Forwarded-For, celle que votre proxy a ajoutée : ipFrom.lastForwarded. Derrière Cloudflare, ipFrom.cloudflare lit CF-Connecting-IP, si votre serveur ne reçoit du trafic que de Cloudflare.
Le clientAddress d'Astro et le getClientAddress() de SvelteKit sont ce que lit l'adaptateur. Avec @sveltejs/adapter-node derrière un proxy, définissez ADDRESS_HEADER=X-Forwarded-For et XFF_DEPTH=1 (un proxy à vous), là où chaque requête passe par ce proxy : l'adaptateur refuse une requête sans ce header. Dans Nuxt, le getRequestIP(event, { xForwardedFor: true }) de h3 prend la première entrée, que le visiteur écrit : utilisez ipFrom.lastForwarded, comme ci-dessus.
Appliquer l'action
Ne lisez que action. Avec block, répondez avec la même erreur que l'action affiche quand elle échoue pour toute autre raison, pour qu'un bot n'apprenne rien. Avec challenge, utilisez la vérification que vous avez déjà (un CAPTCHA, un code par e-mail ou téléphone) ; sans aucune, continuez. Le compte commence en mode monitor, où action est toujours allow : rien ne change pour les visiteurs tant que vous n'activez pas le mode block. Ou envoyez le visiteur vers notre page de challenge : challengeRedirect(d, request.url) donne l'adresse et le cookie avec lesquels répondre un 303 (voir la page de challenge).
Garder le request_id
Chaque réponse a un request_id. Gardez-le avec ce que l'action a créé, dans une colonne du compte ou de la commande, ou dans une ligne de log avec leur identifiant. Il désigne cette décision quand vous nous interrogez à son sujet, et vous permet de retrouver plus tard quelle réponse a laissé entrer un faux compte. Quand le compte se révèle bon ou mauvais, renvoyez-le : await korzen.feedback([{ request_id, outcome: "chargeback" }]) (voir le feedback).
Tester sur votre machine
Lancez votre serveur de développement et ouvrez-le sur localhost. Les pages sur localhost acceptent toujours le tag et n'apparaissent jamais comme domaine. En local, l'IP est 127.0.0.1 ou ::1, et c'est très bien ; avec ipFrom.lastForwarded et aucun proxy devant, il n'y en a pas, et c'est très bien aussi.
- Définissez
KORZEN_API_KEYdans l'environnement du serveur et ouvrez la page d'inscription. Dans les outils de développement du navigateur, le cookie_kzapparaît après quelques secondes. - Inscrivez-vous. La décision apparaît sur la page Décisions du tableau de bord, sous le chemin que vous avez envoyé.
- Vérifiez le fail-open : mettez une mauvaise clé et inscrivez-vous à nouveau. L'appel répond
401, et l'inscription passe.
Erreurs fréquentes
- La première entrée de
X-Forwarded-For, telle que la lisentipFrom.vercelou lexForwardedForde h3, derrière un proxy qui y ajoute la sienne. Le visiteur écrit cette entrée, donc un bot choisit sa propre IP.ipFrom.verceln'est juste que sur Vercel, qui écrase le header. - Le client importé dans du code qui atteint le navigateur. L'API key atteindrait chaque visiteur ; gardez-le dans un module réservé au serveur.
- Une page Astro laissée prérendue. Sans
prerender = false(ououtput: "server"), l'endpoint n'a aucune requête à juger, etclientAddresslève une erreur. - Pas de timeout, ou une erreur qui bloque l'inscription. Toute réponse autre que
200, ou l'absence de réponse à temps, signifie : laissez passer le visiteur.