Este guia é para Fastify 5 e para NestJS 12 como o nest new o cria (um projeto ES module sobre o Express), no Node 22 em diante, com o arquivo de cliente que o guia do Express usa: korzen.js, um arquivo só, sem dependências, que pergunta ao /v1/decide, desiste no seu timeout e deixa o visitante passar em qualquer erro.
A tag
Coloque a tag no <head> do layout que renderiza as páginas de cadastro, login e checkout:
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
Na página, antes de enviar o formulário, chame await window.korzen?.ready(): ela informa o que o visitante fez desde o último relato da tag e resolve em poucos segundos, aconteça o que acontecer. A tag
A chamada
Baixe o cliente para o seu projeto e leia-o antes do 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
O fetch do Node fecha uma conexão parada há 4 segundos; um Agent do undici com um keepAliveTimeout maior, passado como dispatcher, a mantém aberta (veja no Express por que o undici 7).
Fastify
Um hook preHandler na rota que cria a conta. Ele guarda a resposta em request.korzen, ou null quando não há resposta. fromNodeRequest(request.raw) lê a requisição do próprio Node, então a chamada leva a ordem em que os headers chegaram:
// 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
O NestJS roda sobre o Express, então o protect, o middleware de Express do cliente, protege a rota em main.ts. Um projeto novo só compila TypeScript para dist/: adicione korzen.js aos assets em nest-cli.json, para o build copiá-lo ao lado 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);
Depois, no controller, a resposta está na requisição:
// 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" } }
Sem onChallenge, o protect não deixa passar nenhum desafio: um carregamento de página (GET) vai para a nossa página de desafio e volta, e um envio de formulário é recusado com 403. Proteger /signup protege também a página do formulário, para que a pessoa encontre o desafio ali e a aprovação dela deixe o envio passar.
O IP do visitante
O Fastify só lê o X-Forwarded-For dos proxies que o trustProxy nomeia, por endereço ou rede: "loopback,uniquelocal" confia num proxy nesta máquina ou numa rede privada (10.x, 172.16–31.x, 192.168.x, fc00::/7), e o request.ip passa a ser a entrada que esse proxy acrescentou. O Fastify 5 não aceita um número de saltos: um número não confia em nada. true confia em toda entrada, e a primeira delas quem escreve é o visitante. No NestJS sobre o Express, trust proxy é o número de proxies na frente, como no guia do Express. Atrás do Cloudflare, leia CF-Connecting-IP (ipFrom.cloudflare), se o seu servidor só recebe tráfego do Cloudflare.
Siga a action
Leia só a action. Em block, responda com o mesmo erro que a ação mostra quando falha por qualquer outro motivo, para o bot não aprender nada. Em challenge, use a verificação que você já tem (um CAPTCHA, um código por email ou telefone); sem nenhuma, siga em frente. A conta começa em monitor mode, em que a action é sempre allow: nada muda para os visitantes até você ligar o block mode.
Guarde o request_id
Toda resposta tem um request_id. Guarde-o junto do que a ação criou, numa coluna da conta ou do pedido, ou numa linha de log com o id deles. Com ele, você acha a decisão quando falar com a gente e descobre depois qual resposta deixou passar uma conta falsa. Quando a conta se mostrar boa ou ruim, avise: await korzen.feedback([{ request_id, outcome: "chargeback" }]) (veja feedback).
Teste na sua máquina
Rode o servidor com node --env-file=.env app.js (no NestJS, npm run start:dev) e abra em localhost. Páginas em localhost sempre aceitam a tag e nunca entram na lista de domínios. Localmente o IP é 127.0.0.1 ou ::1, e não tem problema.
- Defina
KORZEN_API_KEYem.enve abra a página de cadastro. Nas ferramentas de desenvolvedor do navegador, o cookie_kzaparece depois de alguns segundos. - Cadastre-se. A decisão aparece na página Decisões do painel, com o path que você enviou.
- Confira o fail open: coloque uma chave errada e cadastre-se de novo. A chamada responde
401, e o cadastro passa.
Erros comuns
trustProxy: 1no Fastify 5. Não confia em nenhum proxy, e orequest.ipé o endereço do seu load balancer para todo visitante.trustProxy: true, outrust proxycomotrueno NestJS. Confia em todo salto, e o IP vira o que o visitante escreveu noX-Forwarded-For.- No NestJS,
korzen.jsfora dos assets donest-cli.json: o build fica semdist/korzen.js, e o servidor não sobe. - Chamar
/v1/decidedo navegador. A API key ficaria exposta para todo visitante; a chamada sai do seu servidor. - Sem timeout, ou um erro que trava o cadastro. Se a resposta não for
200, ou não chegar a tempo, deixe o visitante passar.