Ce guide vaut pour Fastify 5, et pour NestJS 12 tel que nest new le crée (un projet en modules ES sur Express), sur Node 22 ou plus récent, avec le fichier client qu'utilise le guide Express : korzen.js, un seul fichier sans dépendance, qui interroge /v1/decide, abandonne à votre timeout et laisse passer le visiteur à la moindre erreur.
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
L'appel
Téléchargez le client dans votre projet, et lisez-le avant de le committer :
curl -o src/korzen.js https://korzen.dev/bot-protection/sdk/korzen.js curl -o src/korzen.d.ts https://korzen.dev/bot-protection/sdk/korzen.d.ts # types, for TypeScript
Le fetch de Node ferme une connexion inactive depuis 4 secondes ; un Agent undici avec un keepAliveTimeout plus long, passé comme dispatcher, la garde ouverte (voir Express pour la raison d'undici 7).
Fastify
Un hook preHandler sur la route qui crée le compte. Il garde la réponse dans request.korzen, ou null quand il n'y en a pas. fromNodeRequest(request.raw) lit la requête de Node elle-même, donc l'appel transmet l'ordre dans lequel les headers sont arrivés :
// app.js import Fastify from "fastify"; import formbody from "@fastify/formbody"; import { Agent } from "undici"; // optional (npm install undici@7): keeps the connection open between calls import { createClient, fromNodeRequest } from "./src/korzen.js"; const korzen = createClient({ apiKey: process.env.KORZEN_API_KEY, url: "https://korzen.dev", timeoutMs: 200, dispatcher: new Agent({ keepAliveTimeout: 300_000 }) }); const app = Fastify({ trustProxy: "loopback,uniquelocal" }); // your proxy on a private network: see "The visitor's IP" app.decorateRequest("korzen", null); await app.register(formbody); // Asks before the handler, and keeps the answer on request.korzen (null when there is none). async function guard(request, reply) { request.korzen = await korzen.decide(fromNodeRequest(request.raw, { ip: request.ip })); if (request.korzen?.action === "block") return reply.code(400).send("We could not create your account."); // "challenge": your CAPTCHA, or an email or phone check, before going on } app.post("/signup", { preHandler: guard }, async (request, reply) => { const user = await createAccount(request.body); // your own code await saveRequestID(user.id, request.korzen?.request_id); // see "Keep the request_id" return reply.redirect("/welcome", 303); }); await app.listen({ port: 3000 });
NestJS
NestJS tourne sur Express, donc protect, le middleware Express du client, protège la route dans main.ts. Un nouveau projet ne compile que le TypeScript dans dist/ : ajoutez korzen.js aux assets de nest-cli.json, pour que le build le copie à côté de main.js :
{
"sourceRoot": "src",
"compilerOptions": { "assets": ["korzen.js"] }
}
// src/main.ts import { NestFactory } from "@nestjs/core"; import type { NestExpressApplication } from "@nestjs/platform-express"; import { Agent } from "undici"; // optional (npm install undici@7): keeps the connection open between calls import { AppModule } from "./app.module.js"; import { createClient, protect } from "./korzen.js"; const korzen = createClient({ apiKey: process.env.KORZEN_API_KEY!, url: "https://korzen.dev", timeoutMs: 200, dispatcher: new Agent({ keepAliveTimeout: 300_000 }) }); const app = await NestFactory.create<NestExpressApplication>(AppModule); app.set("trust proxy", 1); // one proxy of yours in front: see "The visitor's IP" // Asks before every route under /signup; the answer is on req.korzen, or null when there is none. app.use("/signup", protect({ client: korzen, ip: (_get, req) => req.ip, // with "trust proxy" set onBlock: (_req, res) => res.status(400).send("We could not create your account."), // onChallenge: (req, res, next, d) => your CAPTCHA, or an email or phone check, then next() })); await app.listen(process.env.PORT ?? 3000);
Puis, dans le contrôleur, la réponse est sur la requête :
// src/signup.controller.ts import { Body, Controller, Get, Post, Redirect, Req } from "@nestjs/common"; import type { Request } from "express"; import type { Decision } from "./korzen.js"; @Controller("signup") export class SignupController { @Get() // the form's page: a challenge meets the person here form() { return renderSignupForm(); // your own code } @Post() @Redirect("/welcome", 303) async create(@Req() req: Request & { korzen?: Decision | null }, @Body() body: Record<string, string>) { const user = await createAccount(body); // your own code await saveRequestID(user.id, req.korzen?.request_id); // see "Keep the request_id" } }
Sans onChallenge, protect ne laisse passer aucun challenge : un chargement de page (GET) va vers notre page de challenge et en revient, et l'envoi d'un formulaire est refusé avec 403. Protéger /signup protège aussi la page du formulaire : une personne y rencontre le challenge, et son pass laisse passer l'envoi.
L'IP du visiteur
Fastify ne lit X-Forwarded-For que depuis les proxies que trustProxy nomme, par adresse ou par réseau : "loopback,uniquelocal" fait confiance à un proxy sur cette machine ou sur un réseau privé (10.x, 172.16–31.x, 192.168.x, fc00::/7), et request.ip est alors l'entrée que ce proxy a ajoutée. Fastify 5 ne prend pas de nombre de sauts : un nombre ne fait confiance à rien. true fait confiance à chaque entrée, dont la première est écrite par le visiteur. Dans NestJS sur Express, trust proxy est le nombre de proxies devant, comme dans le guide Express. Derrière Cloudflare, lisez CF-Connecting-IP (ipFrom.cloudflare), si votre serveur ne reçoit du trafic que de Cloudflare.
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.
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 avec node --env-file=.env app.js (dans NestJS, npm run start:dev) 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.
- Définissez
KORZEN_API_KEYdans.envet 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
trustProxy: 1dans Fastify 5. Il ne fait confiance à aucun proxy, doncrequest.ipest l'adresse de votre load balancer pour chaque visiteur.trustProxy: true, outrust proxyréglé surtruedans NestJS. Chaque saut est de confiance, donc l'IP devient ce que le visiteur a écrit dansX-Forwarded-For.- Dans NestJS,
korzen.jsabsent des assets denest-cli.json: le build n'a pas dedist/korzen.js, et le serveur ne démarre pas. - 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.