Un captcha qui arrête les bots,
pas les humains.

VeilGuard vérifie les visiteurs avec des signaux invisibles. Pas de casse-têtes d'images, pas d'appels à des tiers. Tout s'exécute sur votre propre serveur.

Sans inscription, sans carte bancaire. L'installation prend environ deux minutes.

example.com/signup

Créez votre compte

Verified!
VeilGuard

Essayez-le vous-même

Le widget ci-dessous est réel : défi, preuve de travail, analyse des signaux et validation côté serveur s'exécutent tous sur cette page.

Exemple de formulaire d'inscription
example.com/signup — veilguard-token

Envoie un token falsifié contenant des empreintes d'automatisation.

Verdict du serveur
POST verify.php — Les tokens sont validés localement par HMAC-SHA256. Les humains passent, les bots non.
Envoyez le formulaire pour voir ce que votre serveur déciderait.
Ce que VeilGuard vérifie
  • Frameworks d'automatisation (WebDriver, Puppeteer, Playwright)
  • Navigateurs headless et moteurs de rendu logiciel
  • User agents de bots et crawlers connus
  • Entropie comportementale : signaux de souris, de clavier et tactiles
  • Preuve de travail liée à un défi à usage unique
  • Analyse temporelle et anti-rejeu sur votre serveur

Ce que fait VeilGuard

Six choses qu'il fait bien.

Taux de blocage proche de 100 %

Un moteur multi-signaux combine empreintes d'automatisation, entropie comportementale, preuve de travail et analyse temporelle côté serveur.

Bloque aussi les bots IA

Chrome headless, automatisation par CDP et agents scriptés sont identifiés par leur empreinte et bloqués, pas seulement les crawlers naïfs.

Intégration en 2 minutes

Un extrait de code pour le widget, un appel sur votre serveur. Copier-coller pour PHP, Node.js, Python et sites statiques.

Fonctionne à 100 % sur votre serveur

La vérification est un simple contrôle HMAC-SHA256 local. Même si veilguard.online tombe, votre protection reste active.

Invisible par conception

Ni feux tricolores, ni bornes d'incendie. Les humains cliquent une fois et voient « Vérifié ! ».

Priorité à la vie privée, compatible RGPD

Pas de cookies, pas de pixels de suivi, pas de requêtes tierces. Les signaux éphémères vivent uniquement dans le token.

Comment ça marche

Trois étapes, sans création de compte.

  1. 1.

    Générez des clés gratuites

    Sans inscription, sans e-mail. Vous obtenez immédiatement une clé de site et une clé secrète.

  2. 2.

    Collez deux extraits de code

    Le script du widget va dans votre formulaire, l'appel de vérification dans votre backend.

  3. 3.

    Terminé

    Les tokens sont validés localement par HMAC-SHA256. Les humains passent, les bots non.

Intégration en 2 minutes

Générez vos clés gratuites, choisissez votre stack, collez le code. Rien ne quitte votre serveur.

1. Générez des clés gratuites
Sans inscription, sans e-mail. Vous obtenez immédiatement une clé de site et une clé secrète.
2. Collez deux extraits de code
Le script du widget va dans votre formulaire, l'appel de vérification dans votre backend.

1 · Widget : collez-le dans votre formulaire

veilguard-widget.js
/*! VeilGuard v1.0.0 - self-hosted bot protection - https://veilguard.online */
(function () {
  'use strict';
  var LINK = 'https://veilguard.online';
  var STR = {
    en: { nb: "I'm not a bot", vd: 'Verified!', ck: 'Verifying…', er: 'Verification failed — click to retry' },
    de: { nb: 'Ich bin kein Bot', vd: 'Verifiziert!', ck: 'Prüfung…', er: 'Fehlgeschlagen — zum Wiederholen klicken' },
    es: { nb: 'No soy un bot', vd: '¡Verificado!', ck: 'Verificando…', er: 'Error — haz clic para reintentar' },
    fr: { nb: "Je ne suis pas un robot", vd: 'Vérifié !', ck: 'Vérification…', er: 'Échec — cliquez pour réessayer' },
    pt: { nb: 'Não sou um bot', vd: 'Verificado!', ck: 'Verificando…', er: 'Falhou — clique para tentar de novo' },
    ru: { nb: 'Я не робот', vd: 'Проверено!', ck: 'Проверка…', er: 'Ошибка — нажмите для повтора' },
    zh: { nb: '我不是机器人', vd: '已验证!', ck: '验证中…', er: '验证失败 — 点击重试' },
    ja: { nb: 'ロボットではありません', vd: '認証済み!', ck: '確認中…', er: '失敗 — クリックで再試行' },
    ar: { nb: 'لست روبوتًا', vd: 'تم التحقق!', ck: 'جارٍ التحقق…', er: 'فشل — انقر للمحاولة مجددًا' }
  };
  var BOT_UA = /bot|crawl|spider|slurp|headless|phantom|selenium|puppeteer|playwright|webdriver|curl|wget|python|scrapy|axios|go-http|httpclient|okhttp|libwww|lighthouse|facebookexternalhit|telegram|whatsapp|discord/i;

  function sha256(ascii) {
    function rightRotate(value, amount) { return (value >>> amount) | (value << (32 - amount)); }
    var mathPow = Math.pow, maxWord = mathPow(2, 32), result = '';
    var words = [], asciiBitLength = ascii.length * 8;
    var hash = sha256.h = sha256.h || [], k = sha256.k = sha256.k || [], primeCounter = k.length;
    var isComposite = {};
    for (var candidate = 2; primeCounter < 64; candidate++) {
      if (!isComposite[candidate]) {
        for (var i = 0; i < 313; i += candidate) { isComposite[i] = candidate; }
        hash[primeCounter] = (mathPow(candidate, 0.5) * maxWord) | 0;
        k[primeCounter++] = (mathPow(candidate, 1 / 3) * maxWord) | 0;
      }
    }
    ascii += '\x80';
    while (ascii.length % 64 - 56) ascii += '\x00';
    for (i = 0; i < ascii.length; i++) {
      var j = ascii.charCodeAt(i);
      if (j >> 8) return '';
      words[i >> 2] |= j << ((3 - i) % 4) * 8;
    }
    words[words.length] = ((asciiBitLength / maxWord) | 0);
    words[words.length] = (asciiBitLength);
    for (j = 0; j < words.length;) {
      var w = words.slice(j, j += 16), oldHash = hash;
      hash = hash.slice(0, 8);
      for (i = 0; i < 64; i++) {
        var w15 = w[i - 15], w2 = w[i - 2];
        var a = hash[0], e = hash[4];
        var temp1 = hash[7]
          + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25))
          + ((e & hash[5]) ^ ((~e) & hash[6]))
          + k[i]
          + (w[i] = (i < 16) ? w[i] : (
            w[i - 16]
            + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15 >>> 3))
            + w[i - 7]
            + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10))
          ) | 0);
        var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22))
          + ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2]));
        hash = [(temp1 + temp2) | 0].concat(hash);
        hash[4] = (hash[4] + temp1) | 0;
      }
      for (i = 0; i < 8; i++) { hash[i] = (hash[i] + oldHash[i]) | 0; }
    }
    for (i = 0; i < 8; i++) {
      for (j = 3; j + 1; j--) {
        var b = (hash[i] >> (j * 8)) & 255;
        result += ((b < 16) ? 0 : '') + b.toString(16);
      }
    }
    return result;
  }

  function b64u(s) {
    try {
      return btoa(unescape(encodeURIComponent(s)))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
    } catch (e) { return ''; }
  }
  function unb64u(s) {
    try { return decodeURIComponent(escape(atob(s.replace(/-/g, '+').replace(/_/g, '/')))); }
    catch (e) { return ''; }
  }
  function rand(n) {
    var s = '', chars = '0123456789abcdef';
    try {
      var a = new Uint8Array(n);
      (window.crypto || window.msCrypto).getRandomValues(a);
      for (var i = 0; i < n; i++) s += chars[a[i] & 15];
    } catch (e) {
      for (var j = 0; j < n; j++) s += chars[Math.floor(Math.random() * 16)];
    }
    return s;
  }
  function resolveLang(lang) {
    if (lang && lang !== 'auto' && STR[lang]) return lang;
    var nav = ((navigator.languages && navigator.languages[0]) || navigator.language || 'en').slice(0, 2).toLowerCase();
    return STR[nav] ? nav : 'en';
  }
  function resolveTheme(theme) {
    if (theme === 'light' || theme === 'dark') return theme;
    try { return (window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light'; }
    catch (e) { return 'light'; }
  }
  function powBits(challenge) {
    try {
      var dot = challenge.lastIndexOf('.');
      var p = JSON.parse(unb64u(challenge.slice(0, dot)));
      var d = p && typeof p.d === 'number' ? p.d : 12;
      if (d <= 0 || d > 24) d = 12;
      return Math.round(d / 4) * 4;
    } catch (e) { return 12; }
  }
  function solvePow(challenge, bits) {
    var zeros = Math.max(1, Math.round(bits / 4));
    var prefix = new Array(zeros + 1).join('0');
    for (var p = 0; p < 2000000; p++) {
      var h = sha256(challenge + '.' + p);
      if (h && h.slice(0, zeros) === prefix) return { p: p, h: h };
    }
    return { p: p, h: '' };
  }

  var STYLE = '.vg-root{display:inline-block;width:100%;max-width:340px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;direction:ltr}'
    + '.vg-box{border:1px solid #d4d4d8;border-radius:10px;background:#fff;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.07);cursor:pointer;transition:border-color .2s,box-shadow .2s;-webkit-user-select:none;user-select:none}'
    + '.vg-box:focus-visible{outline:2px solid #10b981;outline-offset:2px}'
    + '.vg-dark{background:#18181b;border-color:#3f3f46}'
    + '.vg-main{display:flex;align-items:center;gap:11px;padding:15px 16px;min-height:52px}'
    + '.vg-cb{width:24px;height:24px;border:2px solid #a1a1aa;border-radius:5px;background:#fff;flex:none;display:flex;align-items:center;justify-content:center;position:relative;transition:border-color .15s,background .15s}'
    + '.vg-dark .vg-cb{background:transparent;border-color:#52525b}'
    + '.vg-box:hover .vg-cb{border-color:#10b981}'
    + '.vg-cb svg{width:15px;height:15px;opacity:0;transform:scale(.3);transition:opacity .18s ease,transform .18s ease}'
    + '.vg-ok .vg-cb{background:#10b981;border-color:#10b981}'
    + '.vg-ok .vg-cb svg{opacity:1;transform:scale(1)}'
    + '.vg-label{font-size:14px;color:#3f3f46;font-weight:500;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}'
    + '.vg-dark .vg-label{color:#e4e4e7}'
    + '.vg-ok .vg-label{color:#059669;font-weight:600}'
    + '.vg-err .vg-label{color:#dc2626;font-weight:500}'
    + '.vg-brand{display:flex;align-items:center;gap:5px;color:#71717a;font-size:11.5px;font-weight:700;letter-spacing:.01em;flex:none}'
    + '.vg-brand svg{width:18px;height:18px;display:block}'
    + '.vg-foot{display:flex;justify-content:flex-end;align-items:center;gap:5px;padding:4px 12px 6px;border-top:1px solid #f4f4f5;background:#fafafa}'
    + '.vg-dark .vg-foot{border-top-color:#27272a;background:#131316}'
    + '.vg-foot svg{width:10px;height:10px;display:block;color:#a1a1aa}'
    + '.vg-foot a{font-size:10.5px;color:#a1a1aa;text-decoration:none;font-weight:500}'
    + '.vg-foot a:hover{color:#10b981;text-decoration:underline}'
    + '.vg-spinner{position:absolute;inset:2px;border-radius:50%;border:2px solid #e4e4e7;border-top-color:#10b981;opacity:0;transition:opacity .15s;animation:vg-spin .8s linear infinite}'
    + '.vg-dark .vg-spinner{border-color:#3f3f46}'
    + '.vg-checking .vg-spinner{opacity:1}'
    + '.vg-ok{border-color:#10b981 !important;box-shadow:0 0 0 3px rgba(16,185,129,.14)}'
    + '.vg-err{border-color:#f87171 !important}'
    + '@keyframes vg-spin{to{transform:rotate(360deg)}}'
    + '@media (prefers-reduced-motion:reduce){.vg-spinner{animation-duration:2s}}';

  var LOGO = '<svg viewBox="0 0 24 24" fill="none"><path d="M12 2.5 20 5.5v6c0 4.8-3.3 9-8 10.7C7.3 20.5 4 16.3 4 11.5v-6L12 2.5Z" fill="#10b981" fill-opacity="0.14" stroke="#10b981" stroke-width="1.5" stroke-linejoin="round"/><path d="m8.6 12 2.4 2.4 4.4-4.8" stroke="#10b981" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
  var CHECK = '<svg viewBox="0 0 24 24" fill="none"><path d="m5 13 4.2 4.2L19 7" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
  var MINI = '<svg viewBox="0 0 24 24" fill="none"><path d="M12 2.5 20 5.5v6c0 4.8-3.3 9-8 10.7C7.3 20.5 4 16.3 4 11.5v-6L12 2.5Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>';

  function ensureStyle() {
    if (document.getElementById('vg-style')) return;
    var st = document.createElement('style');
    st.id = 'vg-style';
    st.textContent = STYLE;
    document.head.appendChild(st);
  }

  function render(target, opts) {
    opts = opts || {};
    var el = typeof target === 'string' ? document.querySelector(target) : target;
    if (!el || !el.appendChild) return null;
    ensureStyle();
    var lang = resolveLang(opts.lang);
    var t = STR[lang];
    var theme = resolveTheme(opts.theme);
    var sitekey = opts.sitekey || '';
    var inputName = opts.inputName || 'veilguard-token';
    var mode = opts.mode === 'static' ? 'static' : 'server';
    var challengeUrl = opts.challengeUrl || ('/veilguard-challenge?sitekey=' + encodeURIComponent(sitekey));

    var st = { mm: 0, ds: 0, kd: 0, td: 0, pd: 0, scc: 0, load: Date.now(), token: '', done: false, busy: false };
    var lastXY = null;

    function track(evt, fn) { document.addEventListener(evt, fn, { passive: true, capture: true }); }
    track('mousemove', function (e) {
      st.mm++;
      if (lastXY) { var dx = e.clientX - lastXY[0], dy = e.clientY - lastXY[1]; st.ds += Math.sqrt(dx * dx + dy * dy); }
      lastXY = [e.clientX, e.clientY];
    });
    track('keydown', function () { st.kd++; });
    track('touchstart', function () { st.td++; });
    track('pointerdown', function () { st.pd++; });
    track('scroll', function () { st.scc++; });

    el.innerHTML = '';
    el.className += ' vg-root';
    var box = document.createElement('div');
    box.className = 'vg-box' + (theme === 'dark' ? ' vg-dark' : '');
    box.setAttribute('role', 'checkbox');
    box.setAttribute('aria-checked', 'false');
    box.setAttribute('aria-label', t.nb);
    box.tabIndex = 0;
    box.innerHTML = '<div class="vg-main">'
      + '<div class="vg-cb"><span class="vg-spinner"></span>' + CHECK + '</div>'
      + '<span class="vg-label" aria-live="polite">' + t.nb + '</span>'
      + '<div class="vg-brand">' + LOGO + '<span>VeilGuard</span></div>'
      + '</div>'
      + '<div class="vg-foot">' + MINI + '<a href="' + LINK + '" target="_blank" rel="noopener noreferrer">veilguard.online</a></div>';
    var input = document.createElement('input');
    input.type = 'hidden';
    input.name = inputName;
    el.appendChild(box);
    el.appendChild(input);

    var label = box.querySelector('.vg-label');

    function setState(s) {
      var cls = 'vg-box' + (theme === 'dark' ? ' vg-dark' : '');
      if (s === 'checking') cls += ' vg-checking';
      if (s === 'ok') cls += ' vg-ok';
      if (s === 'err') cls += ' vg-err';
      box.className = cls;
      box.setAttribute('aria-checked', s === 'ok' ? 'true' : 'false');
      label.textContent = s === 'checking' ? t.ck : s === 'ok' ? t.vd : s === 'err' ? t.er : t.nb;
    }

    function detect() {
      var flags = 0;
      try { if (navigator.webdriver === true || !!window.webdriver) flags |= 1; } catch (e) {}
      if (BOT_UA.test(navigator.userAgent)) flags |= 2;
      try {
        if (window._phantom || window.callPhantom || window.__nightmare || window.domAutomation || window.__selenium_unwrapped) flags |= 4;
      } catch (e) {}
      try { if ((window.outerWidth === 0 || window.outerHeight === 0) && window.innerWidth > 0) flags |= 8; } catch (e) {}
      try {
        var c = document.createElement('canvas');
        var gl = c.getContext && (c.getContext('webgl') || c.getContext('experimental-webgl'));
        if (gl) {
          var dbg = gl.getExtension('WEBGL_debug_renderer_info');
          var r = dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : '';
          if (typeof r === 'string' && /swiftshader|llvmpipe|software|basic render/i.test(r)) flags |= 16;
        }
      } catch (e) {}
      try { if (!navigator.languages || !navigator.languages.length) flags |= 32; } catch (e) {}
      var isChromeUA = navigator.userAgent.indexOf('Chrome/') !== -1 && !/Edg|OPR|SamsungBrowser/i.test(navigator.userAgent);
      if (isChromeUA && !window.chrome) flags |= 64;
      if (isChromeUA && (!navigator.plugins || navigator.plugins.length === 0)) flags |= 128;

      var score = 100;
      if (flags & 31) { score = 0; }
      else {
        if (flags & 32) score -= 20;
        if (flags & 64) score -= 15;
        if (flags & 128) score -= 10;
        var dt = Date.now() - st.load;
        if (dt < 350) score -= 30;
        if (st.mm === 0 && st.td === 0 && st.kd === 0 && st.pd <= 1) score -= 25;
        else if (st.mm > 0 && st.ds < 40) score -= 10;
        if (score < 0) score = 0;
        if (score > 100) score = 100;
      }
      return { flags: flags, score: score };
    }

    function staticChallenge() {
      var now = Date.now();
      return b64u(JSON.stringify({ n: rand(16), i: now, e: now + 300000, k: sitekey, d: 12 }));
    }

    function activate() {
      if (st.done || st.busy) return;
      st.busy = true;
      setState('checking');
      var started = Date.now();
      var finish = function (challenge) {
        if (!challenge) { setState('err'); st.busy = false; return; }
        try {
          var det = detect();
          var pow = solvePow(challenge, powBits(challenge));
          var att = {
            v: 1, ch: challenge, ts: Date.now(), sc: det.score, hl: det.flags,
            lg: lang, ua: navigator.userAgent.slice(0, 180),
            ev: { mm: st.mm, ds: Math.round(st.ds), kd: st.kd, td: st.td, pd: st.pd, sc: st.scc, dt: started - st.load },
            pw: pow
          };
          st.token = 'v1.' + b64u(JSON.stringify(att));
          input.value = st.token;
        } catch (e) { st.token = ''; }
        var wait = Math.max(0, 550 + Math.random() * 350 - (Date.now() - started));
        setTimeout(function () {
          st.busy = false;
          if (st.token) {
            st.done = true;
            setState('ok');
            if (typeof opts.callback === 'function') { try { opts.callback(st.token); } catch (e2) {} }
          } else { setState('err'); }
        }, wait);
      };
      if (mode === 'static') { finish(staticChallenge()); return; }
      try {
        fetch(challengeUrl, { credentials: 'omit' })
          .then(function (r) { if (!r.ok) throw new Error('http ' + r.status); return r.json(); })
          .then(function (d) { finish(d && d.challenge ? String(d.challenge) : ''); })
          .catch(function () { setState('err'); st.busy = false; });
      } catch (e) { setState('err'); st.busy = false; }
    }

    box.addEventListener('click', activate);
    box.addEventListener('keydown', function (e) {
      if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') { e.preventDefault(); activate(); }
    });

    return {
      reset: function () { st.done = false; st.busy = false; st.token = ''; input.value = ''; setState('idle'); },
      getResponse: function () { return st.token; },
      getSiteKey: function () { return sitekey; }
    };
  }

  function auto() {
    var nodes = document.querySelectorAll('.veilguard[data-sitekey]');
    for (var i = 0; i < nodes.length; i++) {
      var n = nodes[i];
      if (n.getAttribute('data-vg-init') === '1') continue;
      n.setAttribute('data-vg-init', '1');
      render(n, {
        sitekey: n.getAttribute('data-sitekey') || '',
        lang: n.getAttribute('data-lang') || 'auto',
        theme: n.getAttribute('data-theme') || 'auto',
        mode: n.getAttribute('data-mode') || 'server',
        challengeUrl: n.getAttribute('data-challenge-url') || undefined,
        inputName: n.getAttribute('data-input-name') || 'veilguard-token'
      });
    }
  }

  if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', auto); }
  else { auto(); }

  window.VeilGuard = { render: render, auto: auto, version: '1.0.0' };
})();
form.html
<!-- VeilGuard widget — place inside your <form> -->
<div class="veilguard" data-sitekey="vg_YOUR_SITE_KEY" data-challenge-url="veilguard-challenge.php"></div>

<!-- Load the widget once per page (before </body>) -->
<script src="veilguard-widget.js"></script>

2 · Vérification : s'exécute sur votre serveur

veilguard.php
<?php
/**
 * VeilGuard v1.0.0 — self-hosted verification library for PHP
 * Docs: https://veilguard.online
 */

define('VG_MIN_SCORE', 55);
define('VG_TTL', 300);        // seconds — challenge lifetime
define('VG_GRACE', 60);       // seconds — grace after expiry
define('VG_MIN_SOLVE_MS', 250);

function vg_b64u_decode(string $s): string {
    return base64_decode(strtr($s, '-_', '+/'));
}

/** Issue a signed single-use challenge (used by your challenge endpoint). */
function vg_issue_challenge(string $site_key, string $secret): string {
    $now = (int) (microtime(true) * 1000);
    $payload = json_encode(array(
        'n' => bin2hex(random_bytes(8)),
        'i' => $now,
        'e' => $now + VG_TTL * 1000,
        'k' => $site_key,
        'd' => 12,
    ));
    $body = rtrim(strtr(base64_encode($payload), '+/', '-_'), '=');
    return $body . '.' . hash_hmac('sha256', $body, $secret);
}

/**
 * Verify a token submitted by the visitor.
 * Returns array('success' => bool, 'score' => int, 'reason' => string)
 */
function vg_verify_token(string $token, string $secret, string $site_key, int $min_score = VG_MIN_SCORE): array {
    $fail = function (string $reason): array { return array('success' => false, 'reason' => $reason); };

    if (strlen($token) > 4096 || strpos($token, 'v1.') !== 0) {
        return $fail('Malformed token');
    }
    $json = json_decode(vg_b64u_decode(substr($token, 3)), true);
    if (!is_array($json) || !isset($json['ch'])) {
        return $fail('Malformed payload');
    }

    // 1. Cryptographic challenge validation (HMAC-SHA256)
    $ch = $json['ch'];
    $dot = strrpos($ch, '.');
    if ($dot === false) { return $fail('Malformed challenge'); }
    $body = substr($ch, 0, $dot);
    $sig  = substr($ch, $dot + 1);
    if (!hash_equals(hash_hmac('sha256', $body, $secret), $sig)) {
        return $fail('Invalid challenge signature');
    }
    $p = json_decode(vg_b64u_decode($body), true);
    if (!is_array($p)) { return $fail('Malformed challenge payload'); }
    if (($p['k'] ?? '') !== $site_key) { return $fail('Site key mismatch'); }

    // 2. Timing window — server clock only
    $now = (int) (microtime(true) * 1000);
    if ($now > $p['e'] + VG_GRACE * 1000) { return $fail('Challenge expired'); }
    if ($now < $p['i']) { return $fail('Challenge issued in the future'); }
    if ($now - $p['i'] < VG_MIN_SOLVE_MS) { return $fail('Solved impossibly fast'); }

    // 3. Proof-of-work bound to this challenge
    $pow = $json['pw'] ?? null;
    if (!is_array($pow) || !isset($pow['p'])) { return $fail('Missing proof of work'); }
    $hash = hash('sha256', $ch . '.' . $pow['p']);
    $zeros = (int) ceil($p['d'] / 4);
    if (substr($hash, 0, $zeros) !== str_repeat('0', $zeros)) {
        return $fail('Invalid proof of work');
    }

    // 4. Automation fingerprints reported by the widget (bitmask)
    $hl = (int) ($json['hl'] ?? 255);
    if ($hl & 1)  { return $fail('Automation framework detected'); }
    if ($hl & 2)  { return $fail('Known bot user agent'); }
    if ($hl & 4)  { return $fail('Automation framework detected'); }
    if ($hl & 8)  { return $fail('Headless browser detected'); }
    if ($hl & 16) { return $fail('Software renderer detected'); }

    // 5. Behavioral score threshold
    $score = (int) ($json['sc'] ?? 0);
    if ($score < $min_score) { return $fail('Score below threshold'); }

    // 6. Single-use nonce — replay protection
    if (!vg_nonce_fresh($p['n'], $p['e'])) { return $fail('Token replay detected'); }

    return array('success' => true, 'score' => $score);
}

/** File-based single-use nonce store (shared-hosting friendly). */
function vg_nonce_fresh(string $nonce, int $expires_ms): bool {
    $file = sys_get_temp_dir() . '/veilguard-nonces.json';
    $used = array();
    if (is_file($file)) {
        $used = json_decode((string) @file_get_contents($file), true) ?: array();
    }
    $now = (int) (microtime(true) * 1000);
    foreach ($used as $k => $exp) {
        if ((int) $exp < $now) { unset($used[$k]); }
    }
    if (isset($used[$nonce])) { return false; }
    $used[$nonce] = $expires_ms + 60000;
    @file_put_contents($file, json_encode($used), LOCK_EX);
    return true;
}
veilguard-challenge.php
<?php
/**
 * VeilGuard challenge endpoint — serves single-use challenges to the widget.
 * File: veilguard-challenge.php
 */
require __DIR__ . '/veilguard.php';

$siteKey = 'vg_YOUR_SITE_KEY';   // public
$secret  = 'vgs_YOUR_SECRET_KEY'; // private — never expose in HTML

header('Content-Type: application/json');
header('Cache-Control: no-store');
echo json_encode(array('challenge' => vg_issue_challenge($siteKey, $secret)));
signup.php
<?php
/**
 * VeilGuard example — a bot-proof signup form.
 * File: signup.php
 */
require __DIR__ . '/veilguard.php';

$siteKey = 'vg_YOUR_SITE_KEY';
$secret  = 'vgs_YOUR_SECRET_KEY';
$error = null;
$success = null;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['veilguard-token'] ?? '';
    $result = vg_verify_token($token, $secret, $siteKey);
    if (!$result['success']) {
        http_response_code(403);
        $error = 'Bot detected: ' . $result['reason'];
    } else {
        // Human confirmed — continue with your logic here.
        $success = 'Welcome, human! Trust score: ' . $result['score'];
    }
}
?>
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Signup — protected by VeilGuard</title>
</head>
<body>
  <?php if ($success): ?><p style="color:green"><?= htmlspecialchars($success) ?></p><?php endif; ?>
  <?php if ($error): ?><p style="color:red"><?= htmlspecialchars($error) ?></p><?php endif; ?>

  <form method="post">
    <input type="email" name="email" placeholder="Email" required>

    <!-- VeilGuard widget -->
    <div class="veilguard" data-sitekey="<?= htmlspecialchars($siteKey) ?>"
         data-challenge-url="veilguard-challenge.php"></div>

    <button type="submit">Sign up</button>
  </form>

  <script src="veilguard-widget.js"></script>
</body>
</html>

Les clés peuvent également être gérées dans le panneau d'administration.

Vos propres statistiques

VeilGuard ne téléphone jamais à la maison. Pour savoir combien de fois votre captcha est utilisé, journalisez vous-même les verdicts - deux petits extraits, un fichier JSON, pas de base de données.

Deux extraits
Pas de framework, pas de base de données - du PHP pur qui tourne sur n'importe quel hébergement. Le même schéma fonctionne en Node et Python.

1 · Dans votre gestionnaire de formulaire

form-handler.php
<?php
// form-handler.php — your existing form handler.
require __DIR__ . '/veilguard.php';

// Your key pair from the integration section.
$siteKey = 'vg_YOUR_SITE_KEY';
$secret  = 'vgs_YOUR_SECRET_KEY';

$result = vg_verify_token($_POST['veilguard-token'] ?? '', $secret, $siteKey);

// --- Statistics: one row per attempt, stored only on your server. ---
$file = __DIR__ . '/data/stats.json';
$rows = is_file($file) ? (json_decode((string) file_get_contents($file), true) ?: []) : [];
$rows[] = array(
    't' => gmdate('c'),                           // when
    'v' => $result['success'] ? 'human' : 'bot',  // verdict
    's' => (int) ($result['score'] ?? 0),         // score
);
@mkdir(dirname($file), 0755, true);
file_put_contents($file, json_encode($rows), LOCK_EX);

if (!$result['success']) {
    http_response_code(403);
    exit($result['reason'] ?? 'Bot detected');
}

// Human confirmed — continue with your logic here.

À quoi cela pourrait ressembler

VeilGuard
Statistiques auto-hébergées
données d'exemple

1248

Vérifications

1173

Humains

75

Robots

14

Aujourd'hui

Humains Robots

Données d'exemple - votre carte affiche vos vrais chiffres.

Le fichier de statistiques reste sur votre serveur - comme tout dans VeilGuard, rien n'est envoyé. Bloquez l'accès web à data/ (p. ex. via .htaccess), comme décrit dans le kit.

Tarifs

VeilGuard est gratuit, y compris pour un usage commercial. Il n'y a pas de formule payante.

Obtenez vos clés
$0 pour toujours
  • Vérifications illimitées
  • Sites illimités
  • Les 9 langues
  • Vérification auto-hébergée
  • Sans pistage ni cookies
  • Utilisation commerciale autorisée

FAQ

Écrivez-nous

Des questions sur l'intégration, des retours ou un bot qui s'est glissé entre les mailles ? Écrivez-nous - ce formulaire met en pratique ce que VeilGuard prêche : il est protégé par son propre captcha.

  • Réponse généralement sous 48 heures
  • Votre message est stocké uniquement sur ce serveur
  • Pas de suivi, pas de newsletter
Envoyer un message
Protégé par VeilGuard - les bots n'atteignent jamais la boîte de réception.