Înregistrarea e ruta pe care o atacă toată lumea, fiindcă e singura care creează conturi
gratuit. protectSignup() îți dă toată apărarea ei într-o singură funcție, iar challenge()
adaugă un strat în plus care cere clientului să demonstreze că nu e un simplu script.
protectSignup()
O singură funcție care rulează întreaga stivă anti-bot de sign-up și îți întoarce un verdict.
import { protectSignup, createConfigClient } from "@hazeloft/security";
const configClient = createConfigClient(); // ia setările din dashboard
export async function POST(request: Request) {
const body = await request.json();
const verdict = await protectSignup({
headers: request.headers,
body, // { email, name }
config: await configClient.get(),
});
if (verdict.conclusion === "deny") {
return Response.json({ error: "Înregistrare respinsă" }, { status: 403 });
}
// creează contul
}Ce verifică
| Verificare | Ce prinde |
|---|---|
| honeypot | boți care completează un câmp-capcană invizibil pentru om |
| detectare boți | clienți automatizați după cum se prezintă |
| validare email | adrese temporare, invalide sau prea lungi |
| validare nume | nume generate aleator |
| limită per adresă | prea multe conturi pe aceeași adresă de email |
| verificare domeniu | domenii de email inexistente |
Fiecare se pornește, se oprește sau se pune pe „doar observ" din dashboard, separat.
Verdictul
verdict.conclusion // "allow" sau "deny"
verdict.canonicalEmail // adresa normalizată (folosește-o la verificarea de cont unic)
verdict.skipped // verificări care n-au putut rula acum — vezi mai josHoneypot
Pui în formular un câmp invizibil pentru om. Un om nu-l vede, deci nu-l completează; un bot care completează automat tot formularul îl completează și pe el. Formularul trimite un antet special doar dacă acel câmp a fost completat:
import { SIGNUP_HONEYPOT_HEADER } from "@hazeloft/security";
// un input ascuns, cu tabIndex={-1} și autoComplete="off"
const headers: Record<string, string> = { "content-type": "application/json" };
if (honeypotValue) headers[SIGNUP_HONEYPOT_HEADER] = honeypotValue;Limită per adresă de email
Numără încercările pe aceeași adresă, tratând variațiile ca fiind aceeași: la gmail,
j.doe+1@gmail.com, jd.oe@gmail.com și jdoe@gmail.com lovesc un singur contor.
Verificare de domeniu
Un domeniu de email inexistent nu poate primi mail — deci nu are cum să fie real. Verificarea îl prinde chiar dacă nu e pe nicio listă de domenii temporare, fiindcă întreabă direct DNS-ul. Poți alege din dashboard cât de strict e (doar domenii inexistente, sau și domenii fără server de mail configurat).
Cont unic pe forma canonică
Ca să nu lași aceeași persoană să-și facă mai multe conturi cu variații de email, folosește forma canonică peste tot unde cauți un user după email — nu doar la sign-up:
import { canonicalizeAuthEmail } from "@hazeloft/security";
// la sign-up, login, resetare de parolă și retrimiterea verificării
record.email = canonicalizeAuthEmail(record.email);Challenge
Toate verificările de mai sus se uită la ce trimite clientul. Dar un script poate trimite
exact aceiași octeți ca un browser real. Challenge-ul e singura piesă în care clientul trebuie
să facă ceva — un mic calcul în browser — ca să dovedească faptul că nu e doar un POST
aruncat direct pe rută.
Montare: trei piese
Ai nevoie și de partea de browser:
npm i @hazeloft/security-client1. Un endpoint care emite challenge-ul (pe serverul tău):
import { issueChallenge } from "@hazeloft/security";
export async function GET() {
const { token, nonce, exp } = await issueChallenge({
secret: process.env.HAZELOFT_CHALLENGE_SECRET,
action: "sign-up",
});
await recordNonce(nonce, exp); // salvează-l: un tabel, Redis, orice depozit al tău
return Response.json({ token });
}2. În browser, formularul cere challenge-ul și îl rezolvă înainte de submit:
import { CHALLENGE_HEADER, fetchAndSolveChallenge } from "@hazeloft/security-client";
const header = await fetchAndSolveChallenge({ endpoint: "/api/challenge" });
await fetch("/api/sign-up", {
method: "POST",
headers: { "content-type": "application/json", [CHALLENGE_HEADER]: header },
body: JSON.stringify(form),
});3. Ruta protejată verifică challenge-ul (aceeași action și secret ca la emitere):
import { HazeloftSecurity, challenge } from "@hazeloft/security";
export const security = new HazeloftSecurity({
apiKey: process.env.HAZELOFT_SECURITY_API_KEY,
rules: [
challenge({
secret: process.env.HAZELOFT_CHALLENGE_SECRET,
action: "sign-up",
isFresh: consumeNonce, // marchează nonce-ul ca folosit, o singură dată
}),
],
});Lansează în modul „doar observ" întâi
Dacă pornești challenge-ul direct pe „blochează", vei bloca și clienții care încă nu trimit antetul — pagini rămase în cache, aplicații mobile neactualizate. Pornește-l din dashboard pe „doar observ", urmărește câteva zile ce ar fi blocat, apoi treci pe „blochează". Vezi Best practices.