Ein Captcha, das Bots stoppt,
nicht Menschen.
VeilGuard prüft Besucher über unsichtbare Signale. Keine Bilderrätsel, keine Drittanbieter-Aufrufe. Alles läuft auf deinem eigenen Server.
Keine Registrierung, keine Kreditkarte. Das Setup dauert etwa zwei Minuten.
Konto erstellen
Probier es selbst aus
Das Widget unten ist echt: Challenge, Proof-of-Work, Signalanalyse und serverseitige Validierung laufen komplett auf dieser Seite.
Sendet ein gefälschtes Token mit Automatisierungs-Fingerabdrücken.
Verifiziere dich zuerst als Mensch, dann spiele das Token ab.
Mensch verifiziert
Das sehen deine Besucher: ein grüner Haken und nichts weiter.
Bot blockiert
Rohe Serverantwort
- Automatisierungs-Frameworks (WebDriver, Puppeteer, Playwright)
- Headless-Browser & Software-Renderer
- Bekannte Bot- & Crawler-Benutzeragenten
- Verhaltens-Entropie: Maus-, Tastatur- und Touch-Signale
- Proof-of-Work, gebunden an eine einmalige Challenge
- Timing- & Replay-Analyse auf deinem Server
Was VeilGuard macht
Sechs Dinge, die es gut kann.
Blockrate nahe 100 %
Eine Multi-Signal-Engine kombiniert Automatisierungs-Fingerabdrücke, Verhaltens-Entropie, Proof-of-Work und serverseitige Timing-Analyse.
Stoppt auch KI-Bots
Headless Chrome, CDP-Automatisierung und gescriptete Agenten werden erkannt und blockiert, nicht nur naive Crawler.
Integration in 2 Minuten
Ein Snippet für das Widget, ein Aufruf auf deinem Server. Copy-Paste für PHP, Node.js, Python und statische Seiten.
Läuft 100 % auf deinem Server
Die Verifizierung ist ein lokaler HMAC-SHA256-Check. Selbst wenn veilguard.online offline geht, bleibt dein Schutz aktiv.
Unsichtbar by Design
Keine Ampeln, keine Hydranten. Menschen klicken einmal und sehen „Verifiziert!“.
Privatsphäre zuerst & DSGVO-konform
Keine Cookies, keine Tracking-Pixel, keine Drittanbieter-Anfragen. Flüchtige Signale leben nur im Token.
So funktioniert es
Drei Schritte, ganz ohne Konto.
-
1.
Kostenlose Keys generieren
Keine Registrierung, keine E-Mail. Du erhältst sofort einen Site-Key und einen Secret-Key.
-
2.
Zwei Snippets einfügen
Das Widget-Skript kommt in dein Formular, der Verify-Aufruf in dein Backend.
-
3.
Fertig
Tokens werden lokal per HMAC-SHA256 geprüft. Menschen kommen durch, Bots nicht.
In 2 Minuten integrieren
Generiere deine kostenlosen Keys, wähle deinen Stack, füge den Code ein. Nichts verlässt deinen Server.
Deine Keys sind bereit
Site-Key (öffentlich)
Secret-Key (privat)
Behandle dein Secret wie ein Passwort: es verlässt niemals deinen Server.
1 · Widget: in dein Formular einfügen
/*! 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>
Statische Seiten haben kein Backend, daher läuft die Verifizierung im Browser des Besuchers. Das blockiert naive Crawler und die meisten Bots; kombiniere es mit einem Backend für kryptografische Verifizierung.
1 · Widget: in dein Formular einfügen
/*! 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 · Verifizierung: läuft auf deinem Server
<?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: in dein Formular einfügen
/*! 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 · Verifizierung: läuft auf deinem Server
/**
* 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: in dein Formular einfügen
/*! 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 · Verifizierung: läuft auf deinem Server
"""
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)
Deine eigene Statistik
VeilGuard telefoniert nicht nach Hause. Wenn du wissen willst, wie oft dein Captcha benutzt wird, logge die Ergebnisse einfach selbst - zwei kleine Snippets, eine JSON-Datei, keine Datenbank.
1 · In deinem Formular-Handler
<?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 - eigenständige Seite
<?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>
So könnte es aussehen
1248
Verifizierungen
1173
Menschen
75
Bots
14
Heute
Beispieldaten - deine Karte zeigt deine echten Zahlen.
Die Statistik-Datei bleibt auf deinem Server - wie alles bei VeilGuard wird nichts verschickt. Sperre den Web-Zugriff auf data/ (z. B. per .htaccess), wie im Kit beschrieben.
Preise
VeilGuard ist kostenlos, auch für kommerzielle Nutzung. Es gibt keine Paid-Tarife.
Keys holen- Unbegrenzte Verifizierungen
- Unbegrenzte Websites
- Alle 9 Sprachen
- Self-hosted Verifizierung
- Kein Tracking, keine Cookies
- Kommerzielle Nutzung erlaubt
FAQ
Schreib uns
Fragen zur Integration, Feedback oder ein Bot, der durchgerutscht ist? Schreib uns - dieses Formular übt, was VeilGuard predigt: Es ist durch sein eigenes Captcha geschützt.
- Antwort in der Regel innerhalb von 48 Stunden
- Deine Nachricht wird ausschließlich auf diesem Server gespeichert
- Kein Tracking, keine Newsletter
Nachricht gesendet!
Danke! Deine Nachricht wurde auf diesem Server gespeichert und wird in Kürze gelesen.