Something went wrong. Try again.
HeCAPTe is a privacy-first, stateless CAPTCHA that uses Equihash proof-of-work to verify users without tracking them. tangled.org/katsuricata.com/HeCAPTe
privacy stateless spam-protection security captcha equihash go cloudron
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216<!DOCTYPE html><html lang="{{.Lang}}">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{.Msg.Title}}</title> <link rel="preconnect" href="https://fonts.bunny.net"> <link href="https://fonts.bunny.net/css?family=rajdhani:600,700|zilla-slab:ital,wght@0,400;0,500;0,600;0,700;1,400|ibm-plex-mono:400,700" rel="stylesheet"> <link rel="stylesheet" href="/static/style.css"></head>
<body> <a href="#main-content" class="skip-link">{{.Msg.SkipToContent}}</a> <main id="main-content" class="container narrow"> <div class="top-actions"> <div> <h1><span class="bracket" aria-hidden="true">[</span> {{.Msg.Heading}} <span class="bracket" aria-hidden="true">]</span></h1> <p class="muted">{{.Msg.Description}}</p> </div> </div>
{{if .Error}} <div class="error" role="alert">{{.Error}}</div> {{end}}
<div class="card"> <form method="POST" action="/admin/setup" id="setup-form"> <input type="hidden" name="csrf_token" value="{{.CSRFToken}}"> <label for="password">{{.Msg.PasswordLabel}}</label> <input type="password" id="password" name="password" required minlength="8" autocomplete="new-password" aria-describedby="pow-status"> <input type="hidden" id="pow_data" name="pow_data"> <div class="actions"> <button type="submit" id="save-btn">{{.Msg.SaveBtn}}</button> </div> <p id="pow-status" class="muted status-subtle" aria-live="polite" role="status"></p> </form> </div> </main> <script> // selfOrigin is the origin this page runs on: exactly the string // the browser sent as the Origin header of the challenge fetch. // location.origin is null on file:// URLs, which then honestly // binds the empty origin. function selfOrigin() { return (location.origin === null || location.origin === 'null') ? '' : location.origin; }
// Load and initialize the WASM solver inside a Web Worker, then // require a proof of work before the setup form submits. This // hardens the first-run window against automated password // guessing. The worker wrapper round-trips the module handshake at // load, and the page echoes the worker's attestation struct with // the submission: a caller that never stood up a real Worker with // the real module cannot pass the server-side handshake gate. let solverWorker = null; let workerMsgSeq = 0; let wasmReady = false; let wasmError = false; let workerCaps = null;
try { solverWorker = new Worker('/static/worker.js'); } catch (err) { wasmError = true; }
if (solverWorker) { solverWorker.onmessage = function (e) { const { type, payload, caps } = e.data; if (caps) workerCaps = caps; if (type === 'STATUS' && payload === 'READY') { wasmReady = true; } else if (type === 'ERROR') { console.error('Worker error:', payload); } }; solverWorker.onerror = function (err) { console.error('Failed to start solver worker:', err); wasmError = true; const status = document.getElementById('pow-status'); if (status) { status.textContent = 'The solver did not load. Reload the page and try again.'; } }; } else { const status = document.getElementById('pow-status'); if (status) { status.textContent = 'The solver did not load. Reload the page and try again.'; } }
// solveInWorker posts one SOLVE slice to the worker and resolves // with {result, hs}, where hs is the module handshake the worker // sealed from the module itself. function solveInWorker(payload) { return new Promise(function (resolve, reject) { const id = ++workerMsgSeq; function onMessage(e) { const data = e.data || {}; if (data.id !== id) return; solverWorker.removeEventListener('message', onMessage); if (data.type === 'ERROR') { reject(new Error(String(data.payload))); return; } if (data.type === 'RESULT') { resolve({ result: data.payload, hs: data.hs || (data.payload && data.payload.hs) || '' }); } } solverWorker.addEventListener('message', onMessage); solverWorker.postMessage({ id, type: 'SOLVE', payload }); }); }
document.getElementById('setup-form').addEventListener('submit', async function (e) { e.preventDefault();
const saveBtn = document.getElementById('save-btn'); const powStatus = document.getElementById('pow-status'); const powDataField = document.getElementById('pow_data');
if (wasmError) { powStatus.textContent = 'The solver did not load. Reload the page and try again.'; return; }
if (!wasmReady) { powStatus.textContent = 'The solver is not ready. Wait while it loads.'; return; }
saveBtn.disabled = true; powStatus.textContent = 'Wait while the proof of work is generated.';
try { const chalResp = await fetch('/admin/setup/challenge', { method: 'POST' }); if (!chalResp.ok) { throw new Error('Failed to fetch challenge'); } const challenge = await chalResp.json();
// Solve in the worker. One call walks one 10,000-nonce // slice of the signed window; an unlucky window resumes at // lastNonce until a solution turns up. powStatus.textContent = 'Wait while the challenge is solved.'; let result = null; let hs = ''; let resume = challenge.nonce_min; while (result === null) { const slice = await solveInWorker({ salt: challenge.salt, ts: challenge.ts, n: challenge.diff.n, k: challenge.diff.k, nonceMin: resume, nonceMax: challenge.nonce_max }); if (slice.hs !== '') hs = slice.hs; if (slice.result.error) { if (typeof slice.result.lastNonce === 'number' && slice.result.lastNonce < challenge.nonce_max) { resume = slice.result.lastNonce; continue; } throw new Error(slice.result.error); } result = slice.result; }
// flags echoes the signed request classification (always 0 // on this route). Verification re-signs over it. The nonce // window and the solver generation echo the same way. The // origin rides unsigned: the signature binds the true // origin, so a lie here breaks the signature check. The // module handshake (hs) and the worker attestation (att) // ride unsigned too: both are attestation evidence the // server gates, never secrets. const att = { w: typeof Worker !== 'undefined' ? 1 : 0, a: (typeof WebAssembly === 'object' && typeof WebAssembly.validate === 'function') ? 1 : 0 }; if (workerCaps && workerCaps.w === 1 && workerCaps.a === 1 && typeof workerCaps.mod === 'string' && workerCaps.mod !== '' && workerCaps.ready === 1) { att.c = workerCaps; } powDataField.value = JSON.stringify({ nonce: result.nonce, salt: challenge.salt, ts: challenge.ts, diff: challenge.diff, sig: challenge.sig, sol: result.solution, flags: challenge.flags, nonce_min: challenge.nonce_min, nonce_max: challenge.nonce_max, sv: result.sv, hs: hs, att: att, origin: selfOrigin() });
powStatus.textContent = 'Verification in progress.'; this.submit(); } catch (err) { console.error('Setup error:', err); powStatus.textContent = 'Error: ' + err.message; saveBtn.disabled = false; } }); </script></body>
</html>