Ce guide vaut pour l'App Router, Next.js 15 et plus récent. Vous ajoutez le tag au layout racine, et appelez Bot Protection depuis la server action ou le route handler qui crée le compte.
Le tag
Dans le layout racine, pour que chaque page l'ait :
// app/layout.js export default function RootLayout({ children }) { return ( <html lang="en"> <head> <script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script> </head> <body>{children}</body> </html> ); }
L'appel
Le client tient en un seul fichier sans dépendance : il interroge /v1/decide, abandonne à votre timeout et laisse passer le visiteur à la moindre erreur. Téléchargez-le dans lib/, et lisez-le avant de le committer :
curl -o lib/bot-protection.js https://korzen.dev/bot-protection/sdk/korzen.js curl -o lib/bot-protection.d.ts https://korzen.dev/bot-protection/sdk/korzen.d.ts # types, for TypeScript
Puis une fonction pour chaque action protégée :
// lib/korzen.js import { headers } from "next/headers"; import { createClient, fromHeaders, ipFrom } from "./bot-protection.js"; export const client = createClient({ apiKey: process.env.KORZEN_API_KEY, url: "https://korzen.dev", timeoutMs: 200 }); // decide asks Bot Protection about the request being handled. It returns the answer, // or null on any error: the caller then goes on as on allow (fail open). export async function decide(path) { // ipFrom.vercel on Vercel; see "The visitor's IP" return client.decide(fromHeaders(await headers(), { path, ip: ipFrom.vercel })); }
Puis, dans la server action qui crée le compte :
// app/signup/actions.js "use server"; import { decide } from "@/lib/korzen"; export async function signUp(prevState, formData) { const d = await decide("/signup"); if (d?.action === "block") return { error: "We could not create your account." }; if (d?.action === "challenge") { // your CAPTCHA, or an email or phone check, before going on } const user = await createAccount(formData); // your own code await saveRequestID(user.id, d?.request_id); // see "Keep the request_id" // ... }
Dans un route handler, withKorzen l'enveloppe : il répond 403 sur block, et passe la réponse au handler après ses propres arguments :
// app/api/signup/route.js import { ipFrom, withKorzen } from "@/lib/bot-protection"; import { client } from "@/lib/korzen"; export const POST = withKorzen( async (request, ctx, d) => { if (d?.action === "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); return Response.json({ ok: true }); }, // onChallenge answers nothing, so the handler runs its own check; without it, a // challenged post is refused (403) { client, ip: ipFrom.vercel, onChallenge: () => undefined }, );
L'IP du visiteur
Sur Vercel, x-forwarded-for contient l'IP du visiteur : Vercel l'écrase, donc un visiteur ne peut pas écrire la sienne. Ailleurs, lisez l'en-tête que pose votre propre edge : cf-connecting-ip derrière Cloudflare, x-real-ip si votre nginx le pose. Derrière un proxy qui ajoute à x-forwarded-for au lieu de l'écraser, la première entrée est ce que le visiteur a envoyé : prenez la dernière que votre proxy a ajoutée.
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, à challenge.url : voir la page de challenge.
Sans onChallenge, le middleware ne laisse passer aucun challenge : un chargement de page (GET) va vers notre page de challenge, et l'envoi d'un formulaire ou un appel d'API est refusé avec 403. Protégez aussi la page qui affiche le formulaire : une personne y rencontre le challenge, et son pass laisse passer l'envoi. Avec une vérification à vous, passez onChallenge et lancez-la là. Le middleware renvoie la personne à l'adresse demandée, lue dans X-Forwarded-Host et X-Forwarded-Proto quand votre proxy les pose ; derrière un proxy qui transmet Host: localhost et aucun des deux (nginx par défaut), donnez publicURL: "https://shop.example".
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.
Tester sur votre machine
Lancez next dev et ouvrez http://localhost:3000. 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. Dans Docker sur votre machine, l'app voit à la place l'adresse du bridge, comme 172.17.0.1 : la page de challenge vous ramène quand même sur localhost, puisque le tag y a vu votre page.
- Définissez
KORZEN_API_KEYdans.env.localet 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
- Lire la première entrée de
x-forwarded-forderrière un proxy qui y ajoute. Un bot choisit alors sa propre IP. - Un formulaire envoyé par JavaScript dès le chargement de la page, avant que le tag n'ait rapporté. Appelez
await window.korzen?.ready()avant de l'envoyer ; cela se résout en quelques secondes même quand Bot Protection est lent. - L'appel dans un middleware. Il s'exécute à chaque requête, et chaque appel compte comme une décision : appelez-le seulement dans l'action que vous protégez.
- Appeler
/v1/decidedepuis le navigateur. L'API key atteindrait chaque visiteur ; l'appel a sa place sur votre serveur. - 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. - Une ligne de votre log qui commence par
korzen: the visitor's IP sent to Bot Protection. Le client l'écrit une fois, à la première décision dont l'IP est celle de votre propre réseau (d'un load balancer, d'un proxy, d'un conteneur), que Bot Protection juge comme sans IP : donnez àipl'adresse du visiteur (l'IP du visiteur). Dans Docker sur votre machine, c'est l'adresse du bridge, et là elle est sans conséquence.