Cuando tu sitio está detrás de Cloudflare, un Worker en las rutas que proteges puede preguntar a Bot Protection antes de que la petición llegue a tu origen, sin ningún cambio en tu aplicación. Corre en el borde de Cloudflare, cerca de korzen.dev, que también está detrás de Cloudflare. En un sitio estático, una Cloudflare Pages Function hace lo mismo para una acción, como en nuestro propio blog.
El tag
El tag sigue en tus páginas, en el <head>, para que la cookie _kz esté ahí cuando el visitante envíe el formulario:
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
El Worker
El cliente es un solo archivo sin dependencias: pregunta a /v1/decide, se rinde al llegar a tu timeout y deja pasar al visitante ante cualquier error. Descárgalo junto a tu Worker y léelo antes de hacer commit:
curl -o src/bot-protection.js https://korzen.dev/bot-protection/sdk/korzen.js
createWorker es el Worker entero: pregunta por cada petición de sus rutas con tu timeout y toma su configuración del entorno del Worker:
// src/index.js: a Worker on the routes Bot Protection judges, in front of the cache and your origin. // wrangler.toml: // routes = [ // { pattern = "tickets.example/api/resale/*", zone_name = "tickets.example" }, // { pattern = "tickets.example/orders/*", zone_name = "tickets.example" }, // ] // [vars] // KORZEN_URL = "https://korzen.dev" // KORZEN_TIMEOUT_MS = "200" // KORZEN_REQUIRE_TOKEN = "/orders/*" // The key: npx wrangler secret put KORZEN_API_KEY import { createWorker } from "./bot-protection.js"; export default createWorker();
KORZEN_API_KEY: tu clave secreta, como secret del Worker. Sin ella, cada petición sigue sin juzgarse y el log del Worker lo dice una vez.KORZEN_URL:https://korzen.dev.KORZEN_TIMEOUT_MS: cuánto puede tardar una decisión, de 10 a 1000 ms (200 si no se define). Pasado ese tiempo, la petición sigue.KORZEN_REQUIRE_TOKEN: las rutas cuyas acciones necesitan el token del tag, separadas por comas; una que termina en*es un prefijo.
Con block, el Worker responde 403. Con challenge, en block mode, una carga de página va a la página de challenge y vuelve con un token, que guarda como cookie _kz; un envío de formulario o una llamada a la API con challenge recibe 403. Todo lo demás sigue hacia la caché de Cloudflare y tu origen, con los headers Korzen-Request-Id y Korzen-Action que tu aplicación puede registrar. Cualquier error o timeout deja pasar la petición: si Bot Protection va lento o está caído, tu sitio funciona como sin el Worker.
Acciones que necesitan un token
Una acción en una ruta de KORZEN_REQUIRE_TOKEN (su POST, PUT, PATCH o DELETE: una carga de página no gasta token) solo sigue con un token que el tag ganó en ese navegador, una vez por sesión. Un script que nunca ejecuta JavaScript se rechaza en cada intento, venga de la dirección que venga. Pon el tag en la página que lleva a la acción, y esa página también en las rutas del Worker, para que una persona vea el challenge al cargarla.
Consultas servidas desde la caché
El Worker se ejecuta antes de la caché de Cloudflare: una petición que responde la caché nunca llega a tu servidor, pero sí al Worker. Pon el Worker en las rutas que se consultan (un inventario, un mapa de asientos) y cada consulta se juzga; las permitidas se siguen sirviendo desde la caché. Cada petición en las rutas del Worker es una decisión del volumen de tu plan, así que enruta solo lo que quieres juzgar.
Lo que añade Cloudflare
Con cada decisión, el Worker envía lo que Cloudflare midió del visitante (request.cf): en todos los planes la red (ASN), el país, la versión de HTTP y el tiempo de ida y vuelta TCP entre el visitante y Cloudflare (ninguno sobre HTTP/3); con Enterprise Bot Management también el fingerprint JA4 y los detection ids. Bot Protection compara ese tiempo de ida y vuelta con el que el tag midió en la página: una grabación hecha en otra red declara un tiempo que la propia conexión del visitante no puede tener. Ninguno de ellos bloquea a un visitante por sí solo.
Tu propio handler
createWorker cubre el caso habitual. Para un Worker que hace más, envuelve tu propio fetch con withKorzen; también envía lo que Cloudflare midió.
withKorzen envuelve el fetch del Worker: lee lo que Cloudflare sabe del visitante y pregunta con tu timeout. Con block, responde 403 él mismo. Con challenge, en block mode, una carga de página va a la página de desafío y vuelve con un token, que guarda como la cookie _kz: la tag lleva esa sesión adelante, y sus siguientes decisiones son allow. Todo lo demás sigue hacia tu origen, con el id y la acción de la decisión en cabeceras que tu aplicación puede registrar:
// src/index.js: a Worker on the routes you protect, in front of your origin. // wrangler.toml: routes = [{ pattern = "shop.example/signup*", zone_name = "shop.example" }] // The key: npx wrangler secret put KORZEN_API_KEY import { createClient, ipFrom, withKorzen } from "./bot-protection.js"; let protect; // made once per isolate, with the key from env export default { fetch(request, env, ctx) { protect ??= withKorzen( async (request, env, ctx, d) => { // on to your origin, with the decision's id and action const forward = new Request(request); if (d?.request_id) forward.headers.set("Korzen-Request-Id", d.request_id); if (d?.action) forward.headers.set("Korzen-Action", d.action); return fetch(forward); }, { client: createClient({ apiKey: env.KORZEN_API_KEY, url: "https://korzen.dev", timeoutMs: 200 }), ip: ipFrom.cloudflare, // CF-Connecting-IP: Cloudflare sets it }, ); return protect(request, env, ctx); }, };
El envío de un formulario desafiado no puede ir a la página y volver: withKorzen lo rechaza con 403. Con una verificación propia en el origen, pasa onChallenge: () => undefined, y el envío llega a tu origen con Korzen-Action: challenge. Pon también la página que muestra el formulario en las rutas del Worker, y el visitante es desafiado cuando carga.
La IP del visitante
CF-Connecting-IP lo pone Cloudflare y el visitante no puede escribirlo, así que un Worker siempre tiene la dirección correcta. Tu origen debe aceptar tráfico solo de Cloudflare, o un bot se salta el Worker llamándolo directamente.
Los headers y su orden
withKorzen envía los headers cuyos valores fija una versión de navegador (Accept, Accept-Encoding, los client hints, Sec-Fetch-*) y Accept-Language: un script que copia el User-Agent de un navegador rara vez los copia todos. No envía su orden (header_order): el request.headers de un Worker lista los nombres ordenados alfabéticamente, no como llegaron, así que el campo queda fuera y la decisión lee el resto. En tu propio servidor Node, fromNodeRequest envía el orden a partir de rawHeaders.
Pruébalo
npx wrangler dev ejecuta el Worker en tu máquina; pon la clave en .dev.vars como KORZEN_API_KEY=…. Después npx wrangler deploy, y sigue las decisiones en la página Decisiones del panel.
Errores comunes
- Un Worker en todas las rutas: cada petición en sus rutas es una decisión. Enruta solo lo que quieres juzgar: las consultas que vigilas, registro, inicio de sesión, checkout.
- Una ruta en
KORZEN_REQUIRE_TOKENcuyas páginas no tienen el tag: ahí ningún visitante tiene token, así que se rechaza cada acción. - Leer el cuerpo de la petición para decidir y luego reenviar un cuerpo ya consumido: el cuerpo de un Request se lee una sola vez.
- Un origen abierto a todo internet: el Worker solo protege las peticiones que pasan por Cloudflare.