Un captcha que detiene a los bots,
no a los humanos.
VeilGuard verifica a los visitantes con señales invisibles. Sin puzzles de imágenes, sin llamadas a terceros. Todo se ejecuta en tu propio servidor.
Sin registro, sin tarjeta de crédito. La configuración tarda unos dos minutos.
Crea tu cuenta
Pruébalo por tu cuenta
El widget de abajo es real: el desafío, la prueba de trabajo, el análisis de señales y la validación en el servidor se ejecutan en esta página.
Envía un token falsificado con huellas de automatización.
Verifícate primero como humano y luego reutiliza ese token.
Humano verificado
Esto es lo que ven tus visitantes: una marca de verificación verde y nada más.
Bot bloqueado
Respuesta sin procesar del servidor
- Frameworks de automatización (WebDriver, Puppeteer, Playwright)
- Navegadores headless y renderizadores por software
- User agents de bots y crawlers conocidos
- Entropía del comportamiento: señales de ratón, teclado y táctil
- Prueba de trabajo ligada a un desafío de un solo uso
- Análisis de tiempos y repetición en tu servidor
Qué hace VeilGuard
Seis cosas que hace bien.
Tasa de bloqueo cercana al 100 %
Un motor multiseñal combina huellas de automatización, entropía del comportamiento, prueba de trabajo y análisis de tiempos en el servidor.
También detiene a los bots de IA
Chrome headless, la automatización por CDP y los agentes con script se identifican por su huella y se bloquean, no solo los crawlers ingenuos.
Integración en 2 minutos
Un fragmento para el widget y una llamada en tu servidor. Copia y pega para PHP, Node.js, Python y sitios estáticos.
Funciona 100 % en tu servidor
La verificación es una comprobación HMAC-SHA256 local. Aunque veilguard.online se caiga, tu protección sigue en pie.
Invisible por diseño
Ni semáforos ni hidrantes. Los humanos hacen un clic y ven «¡Verificado!».
Privacidad ante todo y compatible con el RGPD
Sin cookies, sin píxeles de rastreo, sin solicitudes a terceros. Las señales efímeras viven solo dentro del token.
Cómo funciona
Tres pasos, sin necesidad de cuenta.
-
1.
Genera claves gratis
Sin registro, sin correo. Al instante recibes una clave de sitio y una clave secreta.
-
2.
Pega dos fragmentos
El script del widget va en tu formulario y la llamada de verificación en tu backend.
-
3.
Listo
Los tokens se validan localmente con HMAC-SHA256. Los humanos pasan, los bots no.
Integración en 2 minutos
Genera tus claves gratis, elige tu stack y pega el código. Nada sale de tu servidor.
Tus claves están listas
Clave del sitio (pública)
Clave secreta (privada)
Trata tu clave secreta como una contraseña: nunca sale de tu servidor.
1 · Widget: pégalo en tu formulario
/*! 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>
Los sitios estáticos no tienen backend, así que la verificación se ejecuta en el navegador del visitante. Esto bloquea a los crawlers ingenuos y a la mayoría de los bots; combínalo con cualquier backend para verificación criptográfica.
1 · Widget: pégalo en tu formulario
/*! 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 · Verificación: se ejecuta en tu servidor
<?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: pégalo en tu formulario
/*! 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 · Verificación: se ejecuta en tu servidor
/**
* 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: pégalo en tu formulario
/*! 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 · Verificación: se ejecuta en tu servidor
"""
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)
Las claves también se pueden gestionar en el panel de administración.
Tus propias estadísticas
VeilGuard nunca envía datos a casa. Si quieres saber cuánto se usa tu captcha, registra tú mismo los resultados: dos fragmentos pequeños, un archivo JSON, sin base de datos.
1 · En tu gestor de formulario
<?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 - página independiente
<?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>
Así podría verse
1248
Verificaciones
1173
Humanos
75
Bots
14
Hoy
Datos de ejemplo - tu tarjeta muestra tus números reales.
El archivo de estadísticas queda en tu servidor - como todo en VeilGuard, no se envía nada. Bloquea el acceso web a data/ (p. ej. con .htaccess), como se describe en el kit.
- Verificaciones ilimitadas
- Sitios ilimitados
- Los 9 idiomas
- Verificación autoalojada
- Sin rastreo ni cookies
- Uso comercial permitido
FAQ
Escríbenos
¿Preguntas sobre la integración, sugerencias o un bot que se coló? Escríbenos: este formulario practica lo que VeilGuard predica, está protegido por su propio captcha.
- Respondemos normalmente en 48 horas
- Tu mensaje se guarda únicamente en este servidor
- Sin rastreo, sin boletines
¡Mensaje enviado!
¡Gracias! Tu mensaje se ha guardado en este servidor y se leerá pronto.