// PoW Captcha Web Component import { generateChallenge, solveChallenge, verifySolution } from '../services/pow-captcha.js' import { t, i18n } from '../i18n.js' export class PowCaptcha extends HTMLElement { constructor() { super() this.solved = false this.solving = false this.solution = null this.unsubscribe = null } connectedCallback() { this.render() this.unsubscribe = i18n.subscribe(() => this.render()) } disconnectedCallback() { if (this.unsubscribe) this.unsubscribe() } render() { this.innerHTML = `
` if (!this.solved && !this.solving) { this.querySelector('.pow-captcha-label').addEventListener('click', () => this.solve()) } } async solve() { if (this.solving || this.solved) return this.solving = true this.render() try { const { challenge, difficulty, timestamp, signature, source } = await generateChallenge() const result = await solveChallenge(challenge, difficulty) const solution = { challenge, difficulty, nonce: result.nonce, signature, timestamp, source } const verification = await verifySolution(solution) if (!verification.ok) throw new Error('Verification failed') this.solution = { ...solution, token: verification.token } this.solving = false this.solved = true this.dispatchEvent(new CustomEvent('solved', { detail: this.solution })) this.render() } catch (error) { this.solving = false this.render() console.error('PoW Captcha error:', error) } } // Get solution for form submission getSolution() { return this.solution } // Check if solved isSolved() { return this.solved } // Reset captcha reset() { this.solved = false this.solution = null this.render() } } customElements.define('pow-captcha', PowCaptcha) // Add styles const style = document.createElement('style') style.textContent = ` .pow-captcha { display: inline-block; padding: var(--space-md) var(--space-lg); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-surface, var(--color-bg-secondary)); } .pow-captcha-label { display: flex; align-items: center; gap: var(--space-md); cursor: pointer; user-select: none; } .pow-captcha--solving .pow-captcha-label, .pow-captcha--solved .pow-captcha-label { cursor: default; } .pow-captcha-checkbox { width: 24px; height: 24px; border: 2px solid var(--color-border); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; background: var(--color-bg); transition: all 0.2s ease; } .pow-captcha:hover:not(.pow-captcha--solved):not(.pow-captcha--solving) .pow-captcha-checkbox { border-color: var(--color-primary); } .pow-captcha--solved .pow-captcha-checkbox { background: var(--color-success, #22c55e); border-color: var(--color-success, #22c55e); } .pow-captcha-check { width: 16px; height: 16px; color: white; } .pow-captcha-spinner { width: 16px; height: 16px; border: 2px solid var(--color-border); border-top-color: var(--color-primary); border-radius: 50%; animation: pow-spin 0.8s linear infinite; } @keyframes pow-spin { to { transform: rotate(360deg); } } .pow-captcha-text { font-size: var(--font-size-sm); color: var(--color-text); } .pow-captcha--solved .pow-captcha-text { color: var(--color-success, #22c55e); font-weight: 500; } ` document.head.appendChild(style)