Deze gids is voor Fastify 5, en NestJS 12 zoals nest new het aanmaakt (een ES-moduleproject op Express), op Node 22 of nieuwer, met het clientbestand dat de Express-gids gebruikt: korzen.js, één bestand zonder dependencies, dat /v1/decide vraagt, opgeeft bij je timeout en de bezoeker bij elke fout doorlaat.
De tag
Zet de tag in de <head> van de layout die je registratie-, inlog- en checkoutpagina's rendert:
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
Roep op de pagina, voordat het formulier wordt verstuurd, await window.korzen?.ready() aan: het meldt wat de bezoeker deed sinds de tag het laatst meldde, en is hoe dan ook binnen een paar seconden klaar. De tag
De call
Download de client in je project, en lees hem voordat je hem commit:
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
De fetch van Node sluit een verbinding na 4 seconden zonder verkeer; een undici-Agent met een langere keepAliveTimeout, meegegeven als dispatcher, houdt hem open (zie Express voor waarom undici 7).
Fastify
Een preHandler-hook op de route die het account aanmaakt. Hij bewaart het antwoord op request.korzen, of null als er geen is. fromNodeRequest(request.raw) leest het eigen request van Node, dus de call bevat de volgorde waarin de headers binnenkwamen:
// 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 draait op Express, dus protect, de Express-middleware van de client, beschermt de route in main.ts. Een nieuw project bouwt alleen TypeScript naar dist/: zet korzen.js bij de assets in nest-cli.json, zodat de build het naast main.js kopieert:
{
"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);
Daarna staat het antwoord in de controller op het request:
// 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" } }
Zonder onChallenge laat protect geen challenge door: het laden van een pagina (GET) gaat naar onze challengepagina en komt terug, en een verstuurd formulier wordt geweigerd met 403. Wie /signup beschermt, beschermt ook de pagina van het formulier: daar krijgt een persoon de challenge, en zijn pass laat het formulier door.
Het IP-adres van de bezoeker
Fastify leest X-Forwarded-For alleen van de proxy's die trustProxy noemt, op adres of netwerk: "loopback,uniquelocal" vertrouwt een proxy op deze machine of op een privénetwerk (10.x, 172.16–31.x, 192.168.x, fc00::/7), en request.ip is dan de entry die die proxy heeft toegevoegd. Fastify 5 accepteert geen aantal hops: een getal vertrouwt niets. true vertrouwt elke entry, en de eerste daarvan schrijft de bezoeker. In NestJS op Express is trust proxy het aantal proxy's ervoor, zoals in de Express-gids. Achter Cloudflare lees je CF-Connecting-IP (ipFrom.cloudflare), als je server alleen verkeer van Cloudflare accepteert.
Voer de actie uit
Lees alleen action. Bij block antwoord je met dezelfde foutmelding die de actie toont als hij om een andere reden mislukt, zodat een bot niets leert. Bij challenge gebruik je de controle die je al hebt (een CAPTCHA, een code per e-mail of telefoon); heb je er geen, ga dan door. Het account begint in monitor mode, waar action altijd allow is: voor bezoekers verandert er niets tot je block mode aanzet.
Bewaar de request_id
Elk antwoord heeft een request_id. Bewaar die bij wat de actie heeft aangemaakt, in een kolom van het account of de bestelling, of in een logregel met hun id. Zo weet je welke beslissing het was als je ons ernaar vraagt, en kun je later nagaan welk antwoord een nepaccount binnenliet. Blijkt het account goed of slecht, meld het dan terug: await korzen.feedback([{ request_id, outcome: "chargeback" }]) (zie feedback).
Test het op je eigen machine
Start je server met node --env-file=.env app.js (in NestJS npm run start:dev) en open hem op localhost. Pagina's op localhost accepteren de tag altijd en staan nooit als domein in de lijst. Lokaal is het IP-adres 127.0.0.1 of ::1, en dat is prima.
- Zet
KORZEN_API_KEYin.enven open de registratiepagina. In de developer tools van de browser verschijnt na een paar seconden de_kz-cookie. - Registreer je. De beslissing verschijnt op de pagina Beslissingen van het dashboard, onder het pad dat je stuurde.
- Controleer fail-open: zet een verkeerde key en registreer je opnieuw. De call antwoordt met
401, en de registratie gaat door.
Veelgemaakte fouten
trustProxy: 1in Fastify 5. Dat vertrouwt geen enkele proxy, dusrequest.ipis voor elke bezoeker het adres van je load balancer.trustProxy: true, oftrust proxyoptruein NestJS. Elke hop wordt vertrouwd, dus het IP-adres wordt wat de bezoeker inX-Forwarded-Forschreef.- In NestJS ontbreekt
korzen.jsin de assets vannest-cli.json: de build heeft geendist/korzen.js, en de server start niet. /v1/decideaanroepen vanuit de browser. De API key zou dan bij elke bezoeker terechtkomen; de call hoort op je server.- Geen timeout, of een fout die de registratie tegenhoudt. Elk ander antwoord dan
200, en geen antwoord op tijd, betekent: laat de bezoeker door.