Questa guida è per Fastify 5, e NestJS 12 come lo crea nest new (un progetto ES module su Express), su Node 22 o successivi, con il file del client che usa la guida Express: korzen.js, un solo file senza dipendenze, che chiede a /v1/decide, rinuncia allo scadere del tuo timeout e lascia passare il visitatore a ogni errore.
Il tag
Metti il tag nel <head> del layout che renderizza le pagine di registrazione, login e checkout:
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
Nella pagina, prima di inviare il form, chiama await window.korzen?.ready(): riporta ciò che il visitatore ha fatto dall'ultimo rapporto del tag e si risolve entro pochi secondi qualunque cosa accada. Il tag
La chiamata
Scarica il client nel tuo progetto e leggilo prima del 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
Il fetch di Node chiude una connessione inattiva da 4 secondi; un Agent di undici con un keepAliveTimeout più lungo, passato come dispatcher, la tiene aperta (vedi Express per il perché di undici 7).
Fastify
Un hook preHandler sulla route che crea l'account. Tiene la risposta in request.korzen, o null quando non c'è. fromNodeRequest(request.raw) legge la richiesta di Node stessa, così la chiamata porta l'ordine in cui sono arrivati gli header:
// 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 gira su Express, quindi protect, il middleware Express del client, protegge la route in main.ts. Un nuovo progetto compila in dist/ solo il TypeScript: aggiungi korzen.js agli assets in nest-cli.json, così la build lo copia accanto a 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);
Poi, nel controller, la risposta è sulla richiesta:
// 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" } }
Senza onChallenge, protect non lascia passare nessuna challenge: il caricamento di una pagina (GET) va alla nostra pagina di challenge e torna indietro, e l'invio di un modulo viene rifiutato con 403. Proteggere /signup protegge anche la pagina del modulo: lì una persona incontra la challenge, e il suo pass lascia passare l'invio.
L'IP del visitatore
Fastify legge X-Forwarded-For solo dai proxy che trustProxy indica, per indirizzo o rete: "loopback,uniquelocal" si fida di un proxy su questa macchina o su una rete privata (10.x, 172.16–31.x, 192.168.x, fc00::/7), e request.ip è allora la voce che quel proxy ha aggiunto. Fastify 5 non accetta un numero di hop: un numero non si fida di nessuno. true si fida di ogni voce, la prima delle quali la scrive il visitatore. In NestJS su Express, trust proxy è il numero di proxy davanti, come nella guida Express. Dietro Cloudflare, leggi CF-Connecting-IP (ipFrom.cloudflare), se il tuo server accetta traffico solo da Cloudflare.
Applica l'azione
Leggi solo action. Su block, rispondi con lo stesso errore che l'azione mostra quando fallisce per qualsiasi altro motivo, così il bot non impara niente. Su challenge, usa la verifica che hai già (un CAPTCHA, un codice via email o telefono); se non ne hai, prosegui. L'account parte in monitor mode, dove action è sempre allow: per i visitatori non cambia niente finché non attivi il block mode.
Conserva il request_id
Ogni risposta ha un request_id. Conservalo insieme a ciò che l'azione ha creato, in una colonna dell'account o dell'ordine, o in una riga di log con il loro id. Identifica quella decisione quando ci chiedi informazioni, e ti permette di scoprire in seguito quale risposta ha fatto entrare un account falso. Quando l'account si rivela buono o cattivo, comunicalo: await korzen.feedback([{ request_id, outcome: "chargeback" }]) (vedi feedback).
Provalo sulla tua macchina
Avvia il server con node --env-file=.env app.js (in NestJS, npm run start:dev) e aprilo su localhost. Le pagine su localhost accettano sempre il tag e non compaiono mai tra i domini. In locale l'IP è 127.0.0.1 o ::1, e va bene così.
- Imposta
KORZEN_API_KEYin.enve apri la pagina di registrazione. Negli strumenti per sviluppatori del browser, il cookie_kzcompare dopo qualche secondo. - Registrati. La decisione compare nella pagina Decisioni della dashboard, sotto il path che hai inviato.
- Verifica il fail-open: imposta una chiave sbagliata e registrati di nuovo. La chiamata risponde
401, e la registrazione va a buon fine.
Errori comuni
trustProxy: 1in Fastify 5. Non si fida di nessun proxy, quindirequest.ipè l'indirizzo del tuo load balancer per ogni visitatore.trustProxy: true, otrust proxyimpostato atruein NestJS. Si fida di ogni hop, quindi l'IP diventa quello che il visitatore ha scritto inX-Forwarded-For.- In NestJS,
korzen.jsassente dagli assets dinest-cli.json: la build non hadist/korzen.js, e il server non parte. - Chiamare
/v1/decidedal browser. L'API key arriverebbe a ogni visitatore; la chiamata va fatta dal tuo server. - Nessun timeout, o un errore che blocca la registrazione. Qualsiasi risposta diversa da
200, o nessuna risposta in tempo, significa: lascia passare il visitatore.