// Proof-of-Work Captcha Service // Client must find nonce where SHA256(challenge + nonce) has N leading zeros // Server-first: tries /pow/challenge endpoint, falls back to local generation const POW_SERVER = 'https://pow.dgray.io' const DIFFICULTY = 4 const SERVER_TIMEOUT_MS = 1500 function localGenerateChallenge() { const challenge = crypto.randomUUID() const timestamp = Date.now() return { challenge, difficulty: DIFFICULTY, timestamp, signature: btoa(`${challenge}:${timestamp}:${DIFFICULTY}`), source: 'local' } } export async function generateChallenge() { try { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), SERVER_TIMEOUT_MS) const response = await fetch(`${POW_SERVER}/challenge`, { method: 'GET', signal: controller.signal }) clearTimeout(timeout) if (!response.ok) throw new Error(`HTTP ${response.status}`) const data = await response.json() return { ...data, source: 'server' } } catch { return localGenerateChallenge() } } export async function verifySolution(solution) { try { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), SERVER_TIMEOUT_MS) const response = await fetch(`${POW_SERVER}/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(solution), signal: controller.signal }) clearTimeout(timeout) if (!response.ok) throw new Error(`HTTP ${response.status}`) const data = await response.json() return { ok: data.ok === true, token: data.captcha_token || null } } catch { const hash = await sha256(`${solution.challenge}${solution.nonce}`) const prefix = '0'.repeat(solution.difficulty || DIFFICULTY) return { ok: hash.startsWith(prefix), token: null } } } export async function solveChallenge(challenge, difficulty, onProgress) { let nonce = 0 const prefix = '0'.repeat(difficulty) const startTime = Date.now() while (true) { const hash = await sha256(`${challenge}${nonce}`) if (hash.startsWith(prefix)) { return { nonce, hash, duration: Date.now() - startTime } } nonce++ if (onProgress && nonce % 1000 === 0) { onProgress({ attempts: nonce, elapsed: Date.now() - startTime }) } if (nonce % 100 === 0) { await new Promise(r => setTimeout(r, 0)) } } } async function sha256(message) { const msgBuffer = new TextEncoder().encode(message) const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer) const hashArray = Array.from(new Uint8Array(hashBuffer)) return hashArray.map(b => b.toString(16).padStart(2, '0')).join('') }