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.
Créez votre compte
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.
Envoie un token falsifié contenant des empreintes d'automatisation.
Faites d'abord la vérification en tant qu'humain, puis rejouez ce token.
Humain vérifié
Voici ce que voient vos visiteurs : une coche verte et rien d'autre.
Bot bloqué
Réponse brute du serveur
- 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.
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.
-
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.
Vos clés sont prêtes
Clé de site (publique)
Clé secrète (privée)
Traitez votre clé secrète comme un mot de passe : elle ne quitte jamais votre serveur.
1 · Widget : collez-le dans votre formulaire
/*! 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' };
})();
<!-- VeilGuard widget — place inside your <form> -->
<div class="veilguard" data-sitekey="vg_YOUR_SITE_KEY" data-mode="static"></div>
<!-- Load the widget once per page (before </body>) -->
<script src="veilguard-widget.js"></script>
Les sites statiques n'ont pas de backend ; la vérification s'exécute donc dans le navigateur du visiteur. Cela bloque les crawlers naïfs et la plupart des bots ; associez-le à n'importe quel backend pour une vérification cryptographique.
1 · Widget : collez-le dans votre formulaire
/*! 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' };
})();
<!-- 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
<?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;
}
<?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)));
<?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>
1 · Widget : collez-le dans votre formulaire
/*! 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' };
})();
<!-- VeilGuard widget — place inside your <form> -->
<div class="veilguard" data-sitekey="vg_YOUR_SITE_KEY" data-challenge-url="/veilguard-challenge"></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 v1.0.0 — self-hosted verification library for Node.js
* Docs: https://veilguard.online
*/
"use strict";
const crypto = require("crypto");
const VG_MIN_SCORE = 55;
const VG_TTL_MS = 5 * 60 * 1000;
const VG_GRACE_MS = 60 * 1000;
const VG_MIN_SOLVE_MS = 250;
function b64uDecode(s) {
return Buffer.from(s, "base64url").toString("utf8");
}
/** Issue a signed single-use challenge (used by your challenge endpoint). */
function issueChallenge(siteKey, secret) {
const now = Date.now();
const payload = JSON.stringify({
n: crypto.randomBytes(8).toString("hex"),
i: now,
e: now + VG_TTL_MS,
k: siteKey,
d: 12,
});
const body = Buffer.from(payload, "utf8").toString("base64url");
const sig = crypto.createHmac("sha256", secret).update(body, "utf8").digest("hex");
return body + "." + sig;
}
// Single-use nonce store (process lifetime). Swap for Redis/DB in multi-instance setups.
const usedNonces = new Map();
/** Verify a token submitted by the visitor. */
function verifyToken(token, secret, siteKey, opts) {
opts = opts || {};
const minScore = opts.minScore || VG_MIN_SCORE;
const fail = (reason) => ({ success: false, reason });
if (typeof token !== "string" || token.length > 4096 || !token.startsWith("v1.")) {
return fail("Malformed token");
}
let att;
try { att = JSON.parse(b64uDecode(token.slice(3))); } catch (e) { return fail("Malformed payload"); }
if (!att || typeof att.ch !== "string") return fail("Malformed payload");
// 1. Cryptographic challenge validation (HMAC-SHA256)
const ch = att.ch;
const dot = ch.lastIndexOf(".");
if (dot <= 0) return fail("Malformed challenge");
const body = ch.slice(0, dot);
const sig = ch.slice(dot + 1);
const expected = crypto.createHmac("sha256", secret).update(body, "utf8").digest("hex");
const a = Buffer.from(sig);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return fail("Invalid challenge signature");
}
let p;
try { p = JSON.parse(b64uDecode(body)); } catch (e) { return fail("Malformed challenge payload"); }
if (p.k !== siteKey) return fail("Site key mismatch");
// 2. Timing window — server clock only
const now = Date.now();
if (now > p.e + VG_GRACE_MS) 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
if (!att.pw || typeof att.pw.p === "undefined") return fail("Missing proof of work");
const hash = crypto.createHash("sha256").update(ch + "." + att.pw.p, "utf8").digest("hex");
const zeros = Math.ceil(p.d / 4);
if (!hash.startsWith("0".repeat(zeros))) return fail("Invalid proof of work");
// 4. Automation fingerprints reported by the widget (bitmask)
const hl = typeof att.hl === "number" ? att.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
const score = typeof att.sc === "number" ? att.sc : 0;
if (score < minScore) return fail("Score below threshold");
// 6. Single-use nonce — replay protection
if (usedNonces.has(p.n)) return fail("Token replay detected");
usedNonces.set(p.n, p.e + VG_GRACE_MS);
if (usedNonces.size > 50000) {
const t = Date.now();
for (const [k, exp] of usedNonces) { if (exp < t) usedNonces.delete(k); }
}
return { success: true, score };
}
module.exports = { issueChallenge, verifyToken };
/**
* VeilGuard + Express example — https://veilguard.online
* Install: npm install express
*/
"use strict";
const express = require("express");
const { issueChallenge, verifyToken } = require("./veilguard.js");
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(express.static(__dirname)); // serves veilguard-widget.js + your pages
const SITE_KEY = "vg_YOUR_SITE_KEY";
const SECRET_KEY = "vgs_YOUR_SECRET_KEY";
// 1. Challenge endpoint — the widget calls this automatically.
app.get("/veilguard-challenge", (req, res) => {
res.set("Cache-Control", "no-store").json({ challenge: issueChallenge(SITE_KEY, SECRET_KEY) });
});
// 2. Protect any POST route — read the hidden "veilguard-token" field.
app.post("/signup", (req, res) => {
const result = verifyToken(req.body["veilguard-token"] || "", SECRET_KEY, SITE_KEY);
if (!result.success) {
return res.status(403).send("Bot detected: " + result.reason);
}
// Human confirmed — continue with your logic.
res.send("Welcome, human! Trust score: " + result.score);
});
app.listen(3000, () => console.log("VeilGuard example running on http://localhost:3000"));
1 · Widget : collez-le dans votre formulaire
/*! 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' };
})();
<!-- VeilGuard widget — place inside your <form> -->
<div class="veilguard" data-sitekey="vg_YOUR_SITE_KEY" data-challenge-url="/veilguard-challenge"></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 v1.0.0 — self-hosted verification library for Python
Docs: https://veilguard.online
"""
import base64
import hashlib
import hmac
import json
import os
import time
MIN_SCORE = 55
TTL_MS = 5 * 60 * 1000
GRACE_MS = 60 * 1000
MIN_SOLVE_MS = 250
_used_nonces = {} # single-use nonce store (process lifetime)
def _b64u_decode(s: str) -> bytes:
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
def issue_challenge(site_key: str, secret: str) -> str:
"""Issue a signed single-use challenge (used by your challenge endpoint)."""
now = int(time.time() * 1000)
payload = json.dumps({
"n": os.urandom(8).hex(),
"i": now,
"e": now + TTL_MS,
"k": site_key,
"d": 12,
}, separators=(",", ":"))
body = base64.urlsafe_b64encode(payload.encode()).rstrip(b"=").decode()
sig = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest()
return body + "." + sig
def verify_token(token: str, secret: str, site_key: str, min_score: int = MIN_SCORE) -> dict:
"""Verify a token submitted by the visitor."""
def fail(reason: str) -> dict:
return {"success": False, "reason": reason}
if not isinstance(token, str) or len(token) > 4096 or not token.startswith("v1."):
return fail("Malformed token")
try:
att = json.loads(_b64u_decode(token[3:]))
except Exception:
return fail("Malformed payload")
if not isinstance(att, dict) or not isinstance(att.get("ch"), str):
return fail("Malformed payload")
# 1. Cryptographic challenge validation (HMAC-SHA256)
ch = att["ch"]
dot = ch.rfind(".")
if dot <= 0:
return fail("Malformed challenge")
body, sig = ch[:dot], ch[dot + 1:]
expected = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
return fail("Invalid challenge signature")
try:
p = json.loads(_b64u_decode(body))
except Exception:
return fail("Malformed challenge payload")
if p.get("k") != site_key:
return fail("Site key mismatch")
# 2. Timing window — server clock only
now = int(time.time() * 1000)
if now > p["e"] + GRACE_MS:
return fail("Challenge expired")
if now < p["i"]:
return fail("Challenge issued in the future")
if now - p["i"] < MIN_SOLVE_MS:
return fail("Solved impossibly fast")
# 3. Proof-of-work bound to this challenge
pw = att.get("pw") or {}
if "p" not in pw:
return fail("Missing proof of work")
h = hashlib.sha256((ch + "." + str(pw["p"])).encode()).hexdigest()
zeros = -(-p["d"] // 4) # ceil division
if not h.startswith("0" * zeros):
return fail("Invalid proof of work")
# 4. Automation fingerprints reported by the widget (bitmask)
flags = att.get("hl", 255)
for bit, reason in ((1, "Automation framework detected"),
(2, "Known bot user agent"),
(4, "Automation framework detected"),
(8, "Headless browser detected"),
(16, "Software renderer detected")):
if flags & bit:
return fail(reason)
# 5. Behavioral score threshold
score = att.get("sc", 0)
if score < min_score:
return fail("Score below threshold")
# 6. Single-use nonce — replay protection
if p["n"] in _used_nonces:
return fail("Token replay detected")
_used_nonces[p["n"]] = p["e"] + GRACE_MS
if len(_used_nonces) > 50000:
cutoff = time.time() * 1000
for k in [k for k, v in _used_nonces.items() if v < cutoff]:
del _used_nonces[k]
return {"success": True, "score": score}
"""
VeilGuard + Flask example — https://veilguard.online
Install: pip install flask
"""
from flask import Flask, request, jsonify, send_from_directory
from veilguard import issue_challenge, verify_token
app = Flask(__name__)
SITE_KEY = "vg_YOUR_SITE_KEY"
SECRET_KEY = "vgs_YOUR_SECRET_KEY"
@app.get("/")
def index():
return send_from_directory(".", "index.html") # your form page
@app.get("/veilguard-widget.js")
def widget():
return send_from_directory(".", "veilguard-widget.js")
@app.get("/veilguard-challenge")
def challenge():
resp = jsonify({"challenge": issue_challenge(SITE_KEY, SECRET_KEY)})
resp.headers["Cache-Control"] = "no-store"
return resp
@app.post("/signup")
def signup():
result = verify_token(request.form.get("veilguard-token", ""), SECRET_KEY, SITE_KEY)
if not result["success"]:
return jsonify(result), 403
# Human confirmed — continue with your logic.
return jsonify(result)
if __name__ == "__main__":
app.run(port=3000)
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.
1 · Dans votre gestionnaire de formulaire
<?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.
2 · stats.php - page autonome
<?php
// stats.php — your VeilGuard statistics as a standalone page.
$file = __DIR__ . '/data/stats.json';
$rows = is_file($file) ? (json_decode((string) file_get_contents($file), true) ?: []) : [];
// 14-day skeleton + totals.
$days = array();
for ($i = 13; $i >= 0; $i--) {
$days[gmdate('Y-m-d', time() - $i * 86400)] = array('h' => 0, 'b' => 0);
}
$humans = 0;
foreach ($rows as $r) {
if (!is_array($r)) continue;
$human = ($r['v'] ?? '') === 'human';
if ($human) $humans++;
$day = substr((string) ($r['t'] ?? ''), 0, 10);
if (isset($days[$day])) $days[$day][$human ? 'h' : 'b']++;
}
$total = count($rows);
$bots = $total - $humans;
$today = $days[gmdate('Y-m-d')]['h'] + $days[gmdate('Y-m-d')]['b'];
$max = 1;
foreach ($days as $d) { $max = max($max, $d['h'] + $d['b']); }
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VeilGuard stats</title>
<style>
body { margin: 0; padding: 2rem 1rem; font: 14px/1.5 system-ui, sans-serif;
background: #fafaf9; color: #1c1917; }
.card { max-width: 26rem; margin: 0 auto; border: 1px solid #e7e5e4;
border-radius: .75rem; padding: 1.25rem; background: #fff; }
.nums { display: grid; grid-template-columns: repeat(4, 1fr); gap: .75rem; }
.nums b { display: block; font-size: 1.25rem; line-height: 1.75rem; }
.nums span { font-size: .75rem; opacity: .7; }
.chart { display: flex; align-items: flex-end; gap: 4px; height: 72px;
margin-top: 1rem; }
.chart i { flex: 1; display: flex; flex-direction: column;
justify-content: flex-end; gap: 2px; height: 100%; font-style: normal; }
.chart .h { display: block; border-radius: 2px; background: #10b981; }
.chart .b { display: block; border-radius: 2px; background: #ef4444; }
.legend { display: flex; gap: 1rem; margin-top: .5rem; font-size: .75rem; opacity: .8; }
@media (prefers-color-scheme: dark) {
body { background: #1c1917; color: #fafaf9; }
.card { background: inherit; border-color: #44403c; }
}
</style>
</head>
<body>
<div class="card">
<div class="nums">
<div><b><?= $total ?></b><span>verifications</span></div>
<div><b><?= $humans ?></b><span>humans</span></div>
<div><b><?= $bots ?></b><span>bots</span></div>
<div><b><?= $today ?></b><span>today</span></div>
</div>
<div class="chart" role="img" aria-label="verifications per day">
<?php foreach ($days as $d): ?>
<i>
<span class="h" style="height:<?= (int) round($d['h'] / $max * 68) ?>px"></span>
<span class="b" style="height:<?= (int) round($d['b'] / $max * 68) ?>px"></span>
</i>
<?php endforeach; ?>
</div>
<div class="legend">
<span><b style="color:#10b981">•</b> humans</span>
<span><b style="color:#ef4444">•</b> bots</span>
</div>
</div>
</body>
</html>
À quoi cela pourrait ressembler
1248
Vérifications
1173
Humains
75
Robots
14
Aujourd'hui
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- 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
Message envoyé !
Merci ! Votre message a été stocké sur ce serveur et sera lu rapidement.