Better Auth corre en tu propio servidor, así que la solicitud que registra a un visitante pasa por tu código. Un hook databaseHooks.user.create.before se ejecuta antes de que Better Auth cree cualquier usuario, desde el formulario de email y contraseña o desde el callback de un proveedor social, y recibe los headers de la solicitud: ahí pregunta a Bot Protection y rechaza al usuario con block. Esta guía es para Better Auth 1.x.
El tag
Como en la guía de tu framework (Next.js, Express), en el layout de tus páginas, para que la página de registro lo tenga:
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
La llamada
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 configuración de Better Auth y léelo antes de hacer commit:
curl -o lib/bot-protection.js https://korzen.dev/bot-protection/sdk/korzen.js curl -o lib/bot-protection.d.ts https://korzen.dev/bot-protection/sdk/korzen.d.ts # types, for TypeScript
Después, el hook, y un campo en el usuario que guarda el request_id de la decisión:
// lib/auth.js import { betterAuth } from "better-auth"; import { APIError } from "better-auth/api"; import { createClient, fromHeaders, ipFrom } from "./bot-protection.js"; const korzen = createClient({ apiKey: process.env.KORZEN_API_KEY, url: "https://korzen.dev", timeoutMs: 200 }); export const auth = betterAuth({ // your database, emailAndPassword, socialProviders ... user: { additionalFields: { korzenRequestId: { type: "string", required: false, input: false } }, }, databaseHooks: { user: { create: { before: async (user, ctx) => { if (!ctx?.headers) return; // a user your own code creates, outside a request const d = await korzen.decide( fromHeaders(ctx.headers, { ip: ipFrom.vercel, // your platform's: see "The visitor's IP" path: "/signup", method: ctx.request?.method ?? "POST", }), ); if (d?.action === "block") { throw new APIError("BAD_REQUEST", { message: "We could not create your account." }); } // "challenge": ask for a phone number, or another check, before the account does anything return { data: { ...user, korzenRequestId: d?.request_id ?? null } }; }, }, }, }, });
Después, npx auth@latest generate (o migrate, con el adapter Kysely del propio Better Auth) una vez, para que la base de datos tenga la nueva columna.
La IP del visitante
El hook corre en tu servidor, así que el IP es el que dice la guía de tu plataforma: ipFrom.vercel en Vercel, ipFrom.cloudflare si tu servidor recibe tráfico solo de Cloudflare, ipFrom.lastForwarded detrás de tu propio load balancer o nginx. Un header que el visitante puede escribir deja que un bot elija su propio IP.
Actúa según la action
Lee solo action. Con block, responde con el mismo error que la acción muestra cuando falla por cualquier otro motivo, para que un bot no aprenda nada. Con challenge, usa la verificación que ya tienes (un CAPTCHA, un código por email o teléfono); si no tienes ninguna, sigue adelante. La cuenta empieza en monitor mode, donde action es siempre allow: nada cambia para los visitantes hasta que actives el block mode.
Guarda el request_id
Cada respuesta trae un request_id. Guárdalo con lo que la acción creó, en una columna de la cuenta o del pedido, o en una línea de log con su id. Sirve para identificar esa decisión cuando nos preguntes por ella y para averiguar después qué respuesta dejó entrar una cuenta falsa.
Pruébalo en tu máquina
Arranca tu servidor y abre la página de registro en localhost. Las páginas en localhost siempre aceptan el tag y nunca aparecen como dominio. En local el IP es 127.0.0.1 o ::1, y está bien. En Docker en tu máquina, la app ve en su lugar la dirección del bridge, como 172.17.0.1: la página de desafío te devuelve igual a localhost, porque el tag vio tu página allí.
- Define
KORZEN_API_KEYen.envy abre la página de registro. En las herramientas de desarrollo del navegador, la cookie_kzaparece tras unos segundos. - Regístrate. La decisión aparece en la página Decisions del dashboard, bajo
/signup, y el nuevo usuario tiene sukorzenRequestId. - Comprueba el fail open: pon una clave incorrecta y regístrate de nuevo. La llamada responde
401, y el registro pasa.
Errores comunes
- Un
hooks.beforesolo en/sign-up/email. Los registros con Google o GitHub nunca pasan por él; el database hook ve a cada usuario nuevo. - Preguntar también en
session.create. Cada llamada cuenta como una decisión: aquí basta una por cuenta nueva; protege el login y el checkout en sus propios handlers. - Llamar a
/v1/decidedesde el navegador. La API key quedaría expuesta a cada visitante; la llamada se hace desde tu servidor. - No poner timeout, o dejar que un error detenga el registro. Cualquier respuesta distinta de
200, o la falta de respuesta a tiempo, significa: deja pasar al visitante. - Una línea en tu log que empieza por
korzen: the visitor's IP sent to Bot Protection. El cliente la escribe una vez, en la primera decisión cuya IP es de tu propia red (la de un balanceador de carga, un proxy, un contenedor), que Bot Protection juzga como sin IP: pon enipla dirección del visitante (la IP del visitante). En Docker en tu máquina es la dirección del bridge, y ahí no hace daño.