Questa guida è per Express 4 e 5 su Node 22 o successivi. Il fetch di Node chiude una connessione inattiva da 4 secondi, quindi un server che chiede meno spesso apre una nuova connessione TLS per la maggior parte delle chiamate; un Agent di undici con un keepAliveTimeout più lungo, passato come dispatcher, la tiene aperta.
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
Il client è un solo file senza dipendenze, korzen.js: chiede a /v1/decide, rinuncia allo scadere del tuo timeout e lascia passare il visitatore a ogni errore. Scaricalo nel tuo progetto e leggilo prima del commit:
curl -o korzen.js https://korzen.dev/bot-protection/sdk/korzen.js curl -o korzen.d.ts https://korzen.dev/bot-protection/sdk/korzen.d.ts # types, for TypeScript
Poi protect, come middleware sulla route che crea l'account. Tiene la risposta in req.korzen, o null quando non c'è:
// app.js import express from "express"; import { Agent } from "undici"; // optional (npm install undici@7): keeps the connection open between calls 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 = express(); app.set("trust proxy", 1); // one proxy of yours in front: see "The visitor's IP" app.use(express.urlencoded({ extended: false })); const guard = protect({ client: korzen, ip: (_get, req) => req.ip, // with "trust proxy" set: see "The visitor's IP" 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() }); app.get("/signup", guard, (req, res) => res.render("signup")); // the form's page: a challenge meets the person here app.post("/signup", guard, async (req, res) => { const user = await createAccount(req.body); // your own code await saveRequestID(user.id, req.korzen?.request_id); // see "Keep the request_id" res.redirect("/welcome"); });
Misurato contro korzen.dev il 2026-10-18: Cloudflare non invia l'indicazione Keep-Alive, quindi da Node 18 a 26 fetch da solo ha aperto una nuova connessione dopo 5 secondi di inattività, e con l'Agent una sola connessione ha servito chiamate a 120 secondi l'una dall'altra. Installa undici 7: il suo Agent funziona con il fetch di Node da 20 a 26. undici 8, che oggi installa un semplice npm install undici, fa fallire ogni chiamata su Node 22 e 24 (invalid onRequestStart method), e una chiamata fallita lascia passare il visitatore: lo mostrano solo le decisioni che mancano.
Per chiedere dentro un handler, await korzen.decide(fromNodeRequest(req, { ip: (_get, r) => r.ip, path: "/signup" })) restituisce la risposta, o null a ogni errore. Una risposta il cui action non è allow, challenge o block conta anch'essa come errore. Per registrare il motivo, dai a createClient un onAnswer: dopo ogni chiamata riceve lo status, Korzen-Request-Id, gli header RateLimit e, in caso di errore, il code del problem.
L'IP del visitatore
req.ip è l'indirizzo della connessione, a meno che tu non dica a Express quanti proxy ci sono davanti. Imposta trust proxy su quel numero (1 per un load balancer o nginx): Express prende allora l'indirizzo che quel proxy ha aggiunto a X-Forwarded-For, e ignora quello che il visitatore ha scritto prima. true si fida della voce più a sinistra, che un visitatore può scrivere. Dietro Cloudflare, passa invece a protect 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. Oppure manda il visitatore alla nostra pagina di challenge, in challenge.url: vedi la pagina di challenge.
Senza onChallenge, il middleware non lascia passare nessuna challenge: il caricamento di una pagina (GET) va alla nostra pagina di challenge, e l'invio di un modulo o una chiamata API viene rifiutato con 403. Proteggi anche la pagina che mostra il modulo: lì una persona incontra la challenge, e il suo pass lascia passare l'invio. Con una verifica tua, passa onChallenge ed eseguila lì. Il middleware rimanda la persona all'indirizzo che ha chiesto, letto da X-Forwarded-Host e X-Forwarded-Proto quando il tuo proxy li imposta; dietro un proxy che inoltra Host: localhost e nessuno dei due (nginx di default), dai publicURL: "https://shop.example".
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 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ì. In Docker sulla tua macchina, l'app vede invece l'indirizzo del bridge, come 172.17.0.1: la pagina di challenge ti riporta comunque su localhost, perché il tag ha visto lì la tua pagina.
- 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
app.set("trust proxy", true). Si fida di ogni hop, quindireq.ipdiventa quello che il visitatore ha scritto inX-Forwarded-For.- Nessun
trust proxydietro un load balancer. Ogni visitatore ha allora l'IP privato del balancer, che Bot Protection tratta come nessun IP: vede molto meno, e la pagina Configurazione ti avvisa. - 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. - Una riga nel tuo log che inizia con
korzen: the visitor's IP sent to Bot Protection. Il client la scrive una volta, alla prima decisione il cui IP è della tua rete (di un load balancer, di un proxy, di un container), che Bot Protection giudica come senza IP: impostaipsull'indirizzo del visitatore (l'IP del visitatore). In Docker sulla tua macchina è l'indirizzo del bridge, e lì non fa danni.