@cap.js/widget 0.1.39 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/cap.min.js +1 -1
  2. package/package.json +1 -1
  3. package/src/cap.js +317 -305
package/cap.min.js CHANGED
@@ -1 +1 @@
1
- (()=>{const e="vibrate"in navigator&&!window.matchMedia("(prefers-reduced-motion: reduce)").matches;if("undefined"==typeof window)return;const t=(e,t={})=>window?.CAP_CUSTOM_FETCH?window.CAP_CUSTOM_FETCH(e,t):fetch(e,t);function r(e,t){let r=function(e){let t=2166136261;for(let r=0;r<e.length;r++)t^=e.charCodeAt(r),t+=(t<<1)+(t<<4)+(t<<7)+(t<<8)+(t<<24);return t>>>0}(e),n="";function s(){return r^=r<<13,r^=r>>>17,r^=r<<5,r>>>0}for(;n.length<t;){n+=s().toString(16).padStart(8,"0")}return n.substring(0,t)}async function n(e){var t=(e=>{const t=atob(e),r=new Uint8Array(t.length);for(let e=0;e<t.length;e++)r[e]=t.charCodeAt(e);return r})(e);const r=await new Promise((e,r)=>{try{var n=new DecompressionStream("deflate-raw"),s=n.writable.getWriter(),i=n.readable.getReader(),o=[];i.read().then(function t(n){if(n.done){for(var s=0,a=0,l=0;l<o.length;l++)s+=o[l].length;var c=new Uint8Array(s);for(l=0;l<o.length;l++)c.set(o[l],a),a+=o[l].length;e((new TextDecoder).decode(c))}else o.push(n.value),i.read().then(t).catch(r)}).catch(r),s.write(t).then(()=>{s.close()}).catch(r)}catch(e){r(e)}});return new Promise(e=>{var t=setTimeout(()=>{i(),e({__timeout:!0})},2e4),n=document.createElement("iframe");n.setAttribute("sandbox","allow-scripts"),n.setAttribute("aria-hidden","true"),n.style.cssText="position:absolute;width:1px;height:1px;top:-9999px;left:-9999px;border:none;opacity:0;pointer-events:none;";var s=!1;function i(){s||(s=!0,clearTimeout(t),window.removeEventListener("message",o),n.parentNode&&n.parentNode.removeChild(n))}function o(t){var r=t.data;r&&"object"==typeof r&&("cap:instr"===r.type?(i(),r.blocked?e({__blocked:!0,blockReason:r.blockReason||"automated_browser"}):r.result?e(r.result):e({__timeout:!0})):"cap:error"===r.type&&(i(),e({__timeout:!0})))}window.addEventListener("message",o),n.srcdoc='<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><script>'+r+"\n<\/script></body></html>",document.body.appendChild(n)})}let s=null;const i=()=>{if(s)return s;const e=window.CAP_CUSTOM_WASM_URL||"https://cdn.jsdelivr.net/npm/@cap.js/wasm@0.0.6/browser/cap_wasm_bg.wasm";return s=fetch(e).then(e=>{if(!e.ok)throw new Error(`Failed to fetch wasm: ${e.status}`);return e.arrayBuffer()}).then(e=>WebAssembly.compile(e)).catch(e=>{throw s=null,e}),s};"object"==typeof WebAssembly&&"function"==typeof WebAssembly.compile&&i().catch(()=>{});const o={state:"idle",challengeResp:null,challenges:null,results:[],completedCount:0,solvePromise:null,promoteFn:null,_listeners:[],pendingPromotion:null,token:null,tokenExpires:null,notify(){for(const e of this._listeners)e();this._listeners=[]},onSettled(e){"done"===this.state||"error"===this.state?e():this._listeners.push(e)}};function a(){o.state="idle",o.challengeResp=null,o.challenges=null,o.results=[],o.completedCount=0,o.solvePromise=null,o.promoteFn=null,o.pendingPromotion=null,o._listeners=[],o.token=null,o.tokenExpires=null,h()}let l=null;let c=null;function d(){c&&(window.removeEventListener("mousemove",c),window.removeEventListener("touchstart",c),window.removeEventListener("keydown",c),c=null)}function h(){d();const e=()=>{d(),"idle"===o.state&&(o.state="waiting",l=setTimeout(()=>{p()},2500))};c=e,window.addEventListener("mousemove",e,{passive:!0}),window.addEventListener("touchstart",e,{passive:!0}),window.addEventListener("keydown",e,{passive:!0})}async function p(){if("waiting"!==o.state)return;o.state="fetching";const e=document.querySelector("cap-widget");if(!e)return void(o.state="idle");let s=e.getAttribute("data-cap-api-endpoint");if(!s&&window?.CAP_CUSTOM_FETCH&&(s="/"),s){s.endsWith("/")||(s+="/");try{const e=await t(`${s}challenge`,{method:"POST"});let a;try{a=await e.json()}catch{throw new Error("Failed to parse speculative challenge response")}if(a.error)throw new Error(a.error);a._apiEndpoint=s,o.challengeResp=a;const{challenge:l,token:c}=a;let d=l;if(!Array.isArray(d)){let e=0;d=Array.from({length:l.c},()=>(e++,[r(`${c}${e}`,l.s),r(`${c}${e}d`,l.d)]))}o.challenges=d,o.state="solving",o.solvePromise=async function(e){g();let r=null;try{r=await i()}catch{}v.setWasm(r);const s=e.length,a=new Array(s);let l=1,c=!1;o.promoteFn=e=>{c||(c=!0,l=e,v._size=e,v._ensureSize(e))},null!==o.pendingPromotion&&(o.promoteFn(o.pendingPromotion),o.pendingPromotion=null);let d=0;for(;d<s;){const t=l,r=[],n=[];for(let i=0;i<t&&d<s;i++)n.push(d),r.push(e[d]),d++;v._ensureSize(Math.max(l,t));const i=await Promise.all(r.map(e=>v.run(e[0],e[1]).then(e=>(o.completedCount++,e))));for(let e=0;e<n.length;e++)a[n[e]]=i[e];!c&&d<s&&await new Promise(e=>setTimeout(e,120))}return o.results=a,o.state="redeeming",async function(e){try{const r=o.challengeResp,s=r._apiEndpoint;if(!s)throw new Error("[cap] speculative redeem: missing apiEndpoint");let i=null;if(r.instrumentation&&(i=await n(r.instrumentation),i?.__timeout||i?.__blocked))return o.state="done",void o.notify();const a=await t(`${s}redeem`,{method:"POST",body:JSON.stringify({token:r.token,solutions:e,...i&&{instr:i}}),headers:{"Content-Type":"application/json"}});let l;try{l=await a.json()}catch{throw new Error("Failed to parse speculative redeem response")}if(!l.success)throw new Error(l.error||"Speculative redeem failed");o.token=l.token,o.tokenExpires=new Date(l.expires).getTime(),o.state="done",o.notify()}catch(e){console.warn("[cap] speculative redeem failed (will redo on click):",e),o.state="done",o.notify()}}(a),a}(d)}catch(e){console.warn("[cap] speculative challenge fetch failed:",e),o.state="error",o.notify()}}else o.state="idle"}h();let u=null;function g(){return u||(u=URL.createObjectURL(new Blob(['(()=>{const e=async({salt:e,target:t})=>{let n=0;const r=new TextEncoder,o=4*t.length,s=Math.floor(o/8),l=o%8,a=t.length%2==0?t:t+"0",c=a.length/2,i=new Uint8Array(c);for(let e=0;e<c;e++)i[e]=parseInt(a.substring(2*e,2*e+2),16);const f=l>0?255<<8-l&255:0;for(;;)try{for(let t=0;t<5e4;t++){const t=e+n,o=r.encode(t),a=await crypto.subtle.digest("SHA-256",o),c=new Uint8Array(a);let g=!0;for(let e=0;e<s;e++)if(c[e]!==i[e]){g=!1;break}if(g&&l>0&&(c[s]&f)!==(i[s]&f)&&(g=!1),g)return void self.postMessage({nonce:n,found:!0});n++}}catch(e){return console.error("[cap worker]",e),void self.postMessage({found:!1,error:e.message})}};if("object"!=typeof WebAssembly||"function"!=typeof WebAssembly?.instantiate)return console.warn("[cap worker] wasm not supported, falling back to alternative solver. this will be significantly slower."),void(self.onmessage=async({data:{salt:t,target:n}})=>e({salt:t,target:n}));let t=null;self.onmessage=async({data:{salt:n,target:r,wasmModule:o}})=>{if(o instanceof WebAssembly.Module&&null===t){const s=(e=>{try{let n,r=0,o=null;const s=()=>(null!==o&&0!==o.byteLength||(o=new Uint8Array(n.memory.buffer)),o),l=new TextEncoder,a=(e,t,n)=>{if(void 0===n){const n=l.encode(e),o=t(n.length,1)>>>0;return s().subarray(o,o+n.length).set(n),r=n.length,o}let o=e.length,a=t(o,1)>>>0;const c=s();let i=0;for(;i<o;i++){const t=e.charCodeAt(i);if(t>127)break;c[a+i]=t}if(i!==o){0!==i&&(e=e.slice(i)),a=n(a,o,o=i+3*e.length,1)>>>0;const t=s().subarray(a+i,a+o),{written:r}=l.encodeInto(e,t);i+=r,a=n(a,o,i,1)>>>0}return r=i,a},c={wbg:{}};c.wbg.__wbindgen_init_externref_table=()=>{const e=n.__wbindgen_export_0,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)};const i=new WebAssembly.Instance(e,c);return n=i.exports,n.__wbindgen_start&&n.__wbindgen_start(),t=(e,t)=>{const o=a(e,n.__wbindgen_malloc,n.__wbindgen_realloc),s=r,l=a(t,n.__wbindgen_malloc,n.__wbindgen_realloc),c=r;return BigInt.asUintN(64,n.solve_pow(o,s,l,c))},!0}catch(e){return console.error("[cap worker] failed to init wasm from module:",e),!1}})(o);if(!s)return console.warn("[cap worker] wasm init failed, falling back to JS solver."),e({salt:n,target:r})}if(null===t)return console.warn("[cap worker] no wasm module provided, falling back to JS solver."),e({salt:n,target:r});try{const e=performance.now(),o=t(n,r),s=performance.now();self.postMessage({nonce:Number(o),found:!0,durationMs:(s-e).toFixed(2)})}catch(e){console.error("[cap worker]",e),self.postMessage({found:!1,error:e.message||String(e)})}},self.onerror=e=>{self.postMessage({found:!1,error:e})}})();'],{type:"application/javascript"})),u)}class m{constructor(e){this._size=e,this._workers=[],this._idle=[],this._queue=[],this._wasmModule=null,this._spawnFailures=0}setWasm(e){this._wasmModule=e}_spawn(){const e=g(),t=new Worker(e);return t._busy=!1,this._workers.push(t),this._idle.push(t),t}_replaceWorker(e){const t=this._workers.indexOf(e);-1!==t&&this._workers.splice(t,1);const r=this._idle.indexOf(e);-1!==r&&this._idle.splice(r,1);try{e.terminate()}catch{}return this._spawnFailures++,this._spawnFailures>3?(console.error("[cap] worker spawn failed repeatedly, not retrying"),null):this._spawn()}_ensureSize(e){for(;this._workers.length<e;)this._spawn()}run(e,t){return new Promise((r,n)=>{this._queue.push({salt:e,target:t,resolve:r,reject:n}),this._dispatch()})}_dispatch(){for(;this._idle.length>0&&this._queue.length>0;){const e=this._idle.shift(),{salt:t,target:r,resolve:n,reject:s}=this._queue.shift();let i=!1;const o=({data:t})=>{i||(i=!0,e.removeEventListener("message",o),e.removeEventListener("error",a),this._spawnFailures=0,this._idle.push(e),t.found?n(t.nonce):s(new Error(t.error||"worker failed")),this._dispatch())},a=t=>{if(i)return;i=!0,e.removeEventListener("message",o),e.removeEventListener("error",a);const r=this._replaceWorker(e);s(t),r&&this._dispatch()};e.addEventListener("message",o),e.addEventListener("error",a),this._wasmModule?e.postMessage({salt:t,target:r,wasmModule:this._wasmModule},[]):e.postMessage({salt:t,target:r})}}terminate(){for(const e of this._workers)try{e.terminate()}catch{}this._workers=[],this._idle=[],this._queue=[]}}const v=new m(1);v._spawn();class b extends HTMLElement{#e=null;#t=navigator.hardwareConcurrency||8;token=null;#r;#n;#s;#i=!1;#o;getI18nText(e,t){return this.getAttribute(`data-cap-i18n-${e}`)||t}static get observedAttributes(){return["onsolve","onprogress","onreset","onerror","data-cap-worker-count","data-cap-i18n-initial-state"]}constructor(){super(),this.#o&&this.#o.forEach((e,t)=>{this.removeEventListener(t.slice(2),e)}),this.#o=new Map,this.boundHandleProgress=this.handleProgress.bind(this),this.boundHandleSolve=this.handleSolve.bind(this),this.boundHandleError=this.handleError.bind(this),this.boundHandleReset=this.handleReset.bind(this)}initialize(){g()}attributeChangedCallback(e,t,r){if(e.startsWith("on")){const t=e.slice(2),n=this.#o.get(e);if(n&&this.removeEventListener(t,n),r){const r=t=>{const r=this.getAttribute(e);"function"==typeof window[r]&&window[r].call(this,t)};this.#o.set(e,r),this.addEventListener(t,r)}}"data-cap-worker-count"===e&&this.setWorkersCount(parseInt(r,10)),"data-cap-i18n-initial-state"===e&&this.#n&&this.#n?.querySelector(".label.active")&&this.animateLabel(this.getI18nText("initial-state","Verify you're human"))}async connectedCallback(){this.#s=this,this.#r=this.attachShadow({mode:"open"}),this.#n=document.createElement("div"),this.createUI(),this.addEventListeners(),this.initialize(),this.#n.removeAttribute("disabled");const e=this.getAttribute("data-cap-worker-count"),t=e?parseInt(e,10):null;this.setWorkersCount(t||navigator.hardwareConcurrency||8);const r=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.#s.innerHTML=`<input type="hidden" name="${r}">`,"idle"!==o.state&&"waiting"!==o.state||(l&&(clearTimeout(l),l=null),o.state="waiting",l=setTimeout(()=>p(),2500))}async solve(){if(!this.#i)try{this.#i=!0,this.updateUI("verifying",this.getI18nText("verifying-label","Verifying..."),!0),this.#n.setAttribute("aria-label",this.getI18nText("verifying-aria-label","Verifying you're a human, please wait")),this.dispatchEvent("progress",{progress:0}),e&&navigator.vibrate(5);try{let s,i,c=this.getAttribute("data-cap-api-endpoint");if(!c&&window?.CAP_CUSTOM_FETCH)c="/";else if(!c)throw new Error("Missing API endpoint. Either custom fetch or an API endpoint must be provided.");if(c.endsWith("/")||(c+="/"),"done"===o.state&&o.token&&o.tokenExpires&&Date.now()<o.tokenExpires){this.dispatchEvent("progress",{progress:100});const t=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.querySelector(`input[name='${t}']`)&&(this.querySelector(`input[name='${t}']`).value=o.token),this.dispatchEvent("solve",{token:o.token}),this.token=o.token;const r=o.tokenExpires-Date.now();return this.#e&&clearTimeout(this.#e),this.#e=setTimeout(()=>this.reset(),r),this.#n.setAttribute("aria-label",this.getI18nText("verified-aria-label","We have verified you're a human, you may now continue")),e&&navigator.vibrate([10,50,20,30,40]),a(),void(this.#i=!1)}if("done"===o.state)s=o.results,i=o.challengeResp,this.dispatchEvent("progress",{progress:100});else if("solving"===o.state||"redeeming"===o.state||"fetching"===o.state||"waiting"===o.state){"waiting"===o.state&&(l&&(clearTimeout(l),l=null),o.state="waiting",p()),o.pendingPromotion=this.#t,o.promoteFn&&o.promoteFn(this.#t);const t=setInterval(()=>{if("solving"!==o.state&&"redeeming"!==o.state)return void clearInterval(t);const e=o.challenges?o.challenges.length:1,r=o.completedCount,n="redeeming"===o.state?99:Math.min(98,Math.round(r/e*100));this.dispatchEvent("progress",{progress:n})},150);if(await new Promise(e=>o.onSettled(e)),clearInterval(t),"done"!==o.state)throw new Error("Speculative solve failed – please try again");if(o.token&&o.tokenExpires&&Date.now()<o.tokenExpires){this.dispatchEvent("progress",{progress:100});const t=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.querySelector(`input[name='${t}']`)&&(this.querySelector(`input[name='${t}']`).value=o.token),this.dispatchEvent("solve",{token:o.token}),this.token=o.token;const r=o.tokenExpires-Date.now();return this.#e&&clearTimeout(this.#e),this.#e=setTimeout(()=>this.reset(),r),this.#n.setAttribute("aria-label",this.getI18nText("verified-aria-label","We have verified you're a human, you may now continue")),e&&navigator.vibrate([10,50,20,30,40]),a(),void(this.#i=!1)}s=o.results,i=o.challengeResp,this.dispatchEvent("progress",{progress:100})}else{const e=await t(`${c}challenge`,{method:"POST"});try{i=await e.json()}catch{throw new Error("Failed to parse challenge response from server")}if(i.error)throw new Error(i.error);const{challenge:n,token:o}=i;let a=n;if(!Array.isArray(a)){let e=0;a=Array.from({length:n.c},()=>(e++,[r(`${o}${e}`,n.s),r(`${o}${e}d`,n.d)]))}s=await this.solveChallenges(a)}const d=i.instrumentation?n(i.instrumentation):Promise.resolve(null),h=await d;if(h?.__timeout||h?.__blocked){this.updateUIBlocked(this.getI18nText("error-label","Error"),h?.__blocked),this.#n.setAttribute("aria-label",this.getI18nText("error-aria-label","An error occurred, please try again")),this.removeEventListener("error",this.boundHandleError);const e=new CustomEvent("error",{bubbles:!0,composed:!0,detail:{isCap:!0,message:"Instrumentation failed"}});return super.dispatchEvent(e),this.addEventListener("error",this.boundHandleError),this.executeAttributeCode("onerror",e),console.error("[cap]","Instrumentation failed"),void(this.#i=!1)}const{token:u}=i,g=await t(`${c}redeem`,{method:"POST",body:JSON.stringify({token:u,solutions:s,...h&&{instr:h}}),headers:{"Content-Type":"application/json"}});let m;try{m=await g.json()}catch{throw new Error("Failed to parse server response")}if(this.dispatchEvent("progress",{progress:100}),!m.success)throw new Error(m.error||"Invalid solution");const v=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.querySelector(`input[name='${v}']`)&&(this.querySelector(`input[name='${v}']`).value=m.token),this.dispatchEvent("solve",{token:m.token}),this.token=m.token,a(),this.#e&&clearTimeout(this.#e);const b=new Date(m.expires).getTime()-Date.now();return b>0&&b<864e5?this.#e=setTimeout(()=>this.reset(),b):this.error("Invalid expiration time"),this.#n.setAttribute("aria-label",this.getI18nText("verified-aria-label","We have verified you're a human, you may now continue")),e&&navigator.vibrate([10,50,20,30,40]),{success:!0,token:this.token}}catch(e){throw this.#n.setAttribute("aria-label",this.getI18nText("error-aria-label","An error occurred, please try again")),this.error(e.message),e}}finally{this.#i=!1}}async solveChallenges(e){const t=e.length;let r=0;let n=null;const s="object"==typeof WebAssembly&&"function"==typeof WebAssembly.instantiate;if(s)try{n=await i()}catch(e){console.warn("[cap] wasm unavailable, falling back to JS solver:",e)}if(!s&&!this.#r.querySelector(".warning")){const e=document.createElement("div");e.className="warning",e.style.cssText="width:var(--cap-widget-width,230px);background:rgb(237,56,46);color:white;padding:4px 6px;padding-bottom:calc(var(--cap-border-radius,14px) + 5px);font-size:10px;box-sizing:border-box;font-family:system-ui;border-top-left-radius:8px;border-top-right-radius:8px;text-align:center;user-select:none;margin-bottom:-35.5px;opacity:0;transition:margin-bottom .3s,opacity .3s;",e.innerText=this.getI18nText("wasm-disabled","Enable WASM for significantly faster solving"),this.#r.insertBefore(e,this.#r.firstChild),setTimeout(()=>{e.style.marginBottom="calc(-1 * var(--cap-border-radius, 14px))",e.style.opacity=1},10)}const o=new m(this.#t);o.setWasm(n),o._ensureSize(this.#t);const a=[];try{for(let n=0;n<e.length;n+=this.#t){const s=e.slice(n,Math.min(n+this.#t,e.length)),i=await Promise.all(s.map(([e,n])=>o.run(e,n).then(e=>{r++;const n=Math.min(99,Math.round((0+r)/t*100));return this.dispatchEvent("progress",{progress:n}),e})));a.push(...i)}}finally{o.terminate()}return a}setWorkersCount(e){const t=parseInt(e,10),r=Math.min(navigator.hardwareConcurrency||8,16);this.#t=!Number.isNaN(t)&&t>0&&t<=r?t:navigator.hardwareConcurrency||8}createUI(){this.#n.classList.add("captcha"),this.#n.setAttribute("role","button"),this.#n.setAttribute("tabindex","0"),this.#n.setAttribute("aria-label",this.getI18nText("verify-aria-label","Click to verify you're a human")),this.#n.setAttribute("aria-live","polite"),this.#n.setAttribute("disabled","true"),this.#n.innerHTML=`<div class="checkbox" part="checkbox"><svg class="progress-ring" viewBox="0 0 32 32"><circle class="progress-ring-bg" cx="16" cy="16" r="14"></circle><circle class="progress-ring-circle" cx="16" cy="16" r="14"></circle></svg></div><p part="label" class="label-wrapper"><span class="label active">${this.getI18nText("initial-state","Verify you're human")}</span></p><a part="attribution" aria-label="Secured by Cap" href="https://capjs.js.org/" class="credits" target="_blank" rel="follow noopener" title="Secured by Cap: Self-hosted CAPTCHA for the modern web.">Cap</a>`,this.#r.innerHTML=`<style${window.CAP_CSS_NONCE?` nonce=${window.CAP_CSS_NONCE}`:""}>@media (prefers-reduced-motion:reduce){.captcha,.captcha *{transition:none!important;animation:none!important}.label{filter:none!important;opacity:1!important;transition:none!important;transform:none!important}.label:not(.active),.label.exit{display:none!important}}.captcha,.captcha *{box-sizing:border-box}.captcha{background-color:var(--cap-background,#fdfdfd);border:1px solid var(--cap-border-color,#dddddd8f);border-radius:var(--cap-border-radius,14px);-webkit-user-select:none;user-select:none;height:var(--cap-widget-height,58px);width:var(--cap-widget-width,260px);padding:var(--cap-widget-padding,14px);align-items:center;gap:var(--cap-gap,15px);cursor:pointer;-webkit-tap-highlight-color:#fff0;color:var(--cap-color,#212121);transition:filter .2s,transform .2s;display:flex;position:relative;overflow:hidden}.captcha:not([data-state]):active{transform:scale(.97)}.captcha:hover{filter:brightness(98%)}.captcha *{font-family:var(--cap-font,system, -apple-system, "BlinkMacSystemFont", ".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif)}.checkbox{width:var(--cap-checkbox-size,25px);height:var(--cap-checkbox-size,25px);border:var(--cap-checkbox-border,1px solid #aaaaaad1);border-radius:var(--cap-checkbox-border-radius,6px);background-color:var(--cap-checkbox-background,#fafafa91);margin-top:var(--cap-checkbox-margin,2px);margin-bottom:var(--cap-checkbox-margin,2px);transition:opacity .2s}.captcha p{-webkit-user-select:none;user-select:none;margin:0;font-size:15px;font-weight:500}.label-wrapper{flex:1;align-items:center;min-width:0;height:2em;display:flex;position:relative;overflow:hidden}.label-wrapper:before,.label-wrapper:after{content:"";pointer-events:none;z-index:1;width:100%;height:.6em;position:absolute;left:0}.label-wrapper:before{background:linear-gradient(to bottom, var(--cap-background,#fdfdfd), transparent);top:0}.label-wrapper:after{background:linear-gradient(to top, var(--cap-background,#fdfdfd), transparent);bottom:0}.label{white-space:nowrap;opacity:0;filter:blur(2px);transition:transform .5s cubic-bezier(.25,1,.5,1),opacity .5s,filter .5s;position:absolute;transform:translateY(100%)}.label.active{opacity:1;filter:none;transform:translateY(0)}.label.exit{opacity:0;filter:blur(2px);transform:translateY(-100%)}.checkbox .progress-ring{width:100%;height:100%;display:none;transform:rotate(-90deg)}.checkbox .progress-ring-bg{fill:none;stroke:var(--cap-spinner-background-color,#eee);stroke-width:var(--cap-spinner-thickness,3)}.checkbox .progress-ring-circle{fill:none;stroke:var(--cap-spinner-color,#000);stroke-width:var(--cap-spinner-thickness,3);stroke-linecap:round;stroke-dasharray:87.96;stroke-dashoffset:87.96px;transition:stroke-dashoffset .3s}.captcha[data-state=verifying] .checkbox{background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;display:flex;transform:scale(1.1)}.captcha[data-state=verifying] .checkbox .progress-ring{display:block}.captcha[data-state=done] .checkbox{background-image:var(--cap-checkmark,url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cstyle%3E%40keyframes%20anim%7B0%25%7Bstroke-dashoffset%3A23.21320343017578px%7Dto%7Bstroke-dashoffset%3A0%7D%7D%3C%2Fstyle%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%2300a67d%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m5%2012%205%205L20%207%22%20style%3D%22stroke-dashoffset%3A0%3Bstroke-dasharray%3A23.21320343017578px%3Banimation%3Aanim%20.5s%20ease%22%2F%3E%3C%2Fsvg%3E));background-size:cover;border:1px solid #0000}.captcha[data-state=done] .checkbox .progress-ring{display:none}.captcha[data-state=error] .checkbox{background-image:var(--cap-error-cross,url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 24 24'%3E%3Cpath fill='%23f55b50' d='M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8'/%3E%3C%2Fsvg%3E"));background-size:cover;border:1px solid #0000}.captcha[data-state=error] .checkbox .progress-ring{display:none}.captcha[disabled]{cursor:not-allowed}.captcha[disabled][data-state=verifying]{cursor:progress}.captcha[disabled][data-state=done]{cursor:default}.captcha .credits{color:var(--cap-color,#212121);opacity:.8;text-underline-offset:.18em;z-index:6;font-size:12px;text-decoration-thickness:.08em;position:absolute;bottom:10px;right:10px}.cap-troubleshoot-link{color:#06c;text-underline-offset:.15em;cursor:pointer;font-weight:500;font-size:inherit;text-decoration:underline;text-decoration-thickness:.08em}.cap-troubleshoot-link:hover{opacity:.8}</style>`,this.#r.appendChild(this.#n)}addEventListeners(){this.#n&&(this.#n.querySelector("a").addEventListener("click",e=>{e.stopPropagation(),e.preventDefault(),window.open("https://capjs.js.org","_blank")}),this.#n.addEventListener("click",()=>{this.#n.hasAttribute("disabled")||this.solve()}),this.#n.addEventListener("keydown",e=>{"Enter"!==e.key&&" "!==e.key||this.#n.hasAttribute("disabled")||(e.preventDefault(),e.stopPropagation(),this.solve())}),this.addEventListener("progress",this.boundHandleProgress),this.addEventListener("solve",this.boundHandleSolve),this.addEventListener("error",this.boundHandleError),this.addEventListener("reset",this.boundHandleReset))}animateLabel(e){if(!this.#n)return;const t=this.#n.querySelector(".label-wrapper");if(!t)return;if(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches){const r=t.querySelector(".label.active");if(r)r.textContent=e;else{const r=document.createElement("span");r.className="label active",r.textContent=e,t.appendChild(r)}return}const r=t.querySelector(".label.active"),n=document.createElement("span");n.className="label",n.textContent=e,t.appendChild(n),n.offsetWidth,n.classList.add("active"),r&&(r.classList.remove("active"),r.classList.add("exit"),r.addEventListener("transitionend",()=>r.remove(),{once:!0}))}updateUI(e,t,r=!1){this.#n&&(this.#n.setAttribute("data-state",e),this.animateLabel(t),r?this.#n.setAttribute("disabled","true"):this.#n.removeAttribute("disabled"))}updateUIBlocked(e,t=!1){if(!this.#n)return;this.#n.setAttribute("data-state","error"),this.#n.removeAttribute("disabled");const r=this.#n.querySelector(".label-wrapper");if(!r)return;const n=this.getAttribute("data-cap-troubleshooting-url")||"https://capjs.js.org/guide/troubleshooting/instrumentation.html",s=r.querySelector(".label.active"),i=document.createElement("span");i.className="label",i.innerHTML=t?`${e} · <a class="cap-troubleshoot-link" href="${n}" target="_blank" rel="noopener">${this.getI18nText("troubleshooting-label","Troubleshoot")}</a>`:e,r.appendChild(i),i.offsetWidth,i.classList.add("active"),s&&(s.classList.remove("active"),s.classList.add("exit"),s.addEventListener("transitionend",()=>s.remove(),{once:!0}));const o=i.querySelector(".cap-troubleshoot-link");o&&(console.log("linkblud"),o.addEventListener("click",e=>{e.stopPropagation()}))}handleProgress(e){if(!this.#n)return;const t=this.#n.querySelector(".progress-ring-circle");if(t){const r=2*Math.PI*14,n=r-e.detail.progress/100*r;t.style.strokeDashoffset=n}const r=this.#n.querySelector(".label-wrapper");if(r){const e=r.querySelector(".label.active");e&&(e.textContent=`${this.getI18nText("verifying-label","Verifying...")}`)}this.executeAttributeCode("onprogress",e)}handleSolve(e){this.updateUI("done",this.getI18nText("solved-label","You're a human"),!0),this.executeAttributeCode("onsolve",e)}handleError(t){this.updateUI("error",this.getI18nText("error-label","Error. Try again.")),this.executeAttributeCode("onerror",t),e&&navigator.vibrate([10,40,10])}handleReset(e){this.updateUI("",this.getI18nText("initial-state","I'm a human")),this.executeAttributeCode("onreset",e)}executeAttributeCode(e,t){const r=this.getAttribute(e);r&&(console.error("[cap] using `onxxx='…'` is strongly discouraged and will be deprecated soon. please use `addEventListener` callbacks instead."),new Function("event",r).call(this,t))}error(e="Unknown error"){console.error("[cap]",e),this.dispatchEvent("error",{isCap:!0,message:e})}dispatchEvent(e,t={}){const r=new CustomEvent(e,{bubbles:!0,composed:!0,detail:t});super.dispatchEvent(r)}reset(){this.#e&&(clearTimeout(this.#e),this.#e=null),this.dispatchEvent("reset"),this.token=null;const e=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.querySelector(`input[name='${e}']`)&&(this.querySelector(`input[name='${e}']`).value="")}get tokenValue(){return this.token}disconnectedCallback(){this.removeEventListener("progress",this.boundHandleProgress),this.removeEventListener("solve",this.boundHandleSolve),this.removeEventListener("error",this.boundHandleError),this.removeEventListener("reset",this.boundHandleReset),this.#o.forEach((e,t)=>{this.removeEventListener(t.slice(2),e)}),this.#o.clear(),this.#r&&(this.#r.innerHTML=""),this.reset(),this.cleanup()}cleanup(){this.#e&&(clearTimeout(this.#e),this.#e=null)}}class f{constructor(e={},t){const r=t||document.createElement("cap-widget");if(Object.entries(e).forEach(([e,t])=>{r.setAttribute(e,t)}),!e.apiEndpoint&&!window?.CAP_CUSTOM_FETCH)throw r.remove(),new Error("Missing API endpoint. Either custom fetch or an API endpoint must be provided.");e.apiEndpoint&&r.setAttribute("data-cap-api-endpoint",e.apiEndpoint),this.widget=r,this.solve=this.widget.solve.bind(this.widget),this.reset=this.widget.reset.bind(this.widget),this.addEventListener=this.widget.addEventListener.bind(this.widget),Object.defineProperty(this,"token",{get:()=>r.token,configurable:!0,enumerable:!0}),t||(r.style.display="none",document.documentElement.appendChild(r))}}window.Cap=f,customElements.get("cap-widget")||window?.CAP_DONT_SKIP_REDEFINE?customElements.get("cap-widget")&&console.warn("[cap] the cap-widget element has already been defined, skipping re-defining it.\nto prevent this, set window.CAP_DONT_SKIP_REDEFINE to true"):customElements.define("cap-widget",b),"object"==typeof exports&&"undefined"!=typeof module?module.exports=f:"function"==typeof define&&define.amd&&define([],()=>f),"undefined"!=typeof exports&&(exports.default=f)})();
1
+ (()=>{const e="vibrate"in navigator&&!window.matchMedia("(prefers-reduced-motion: reduce)").matches;if("undefined"==typeof window)return;const t=(e,t={})=>window?.CAP_CUSTOM_FETCH?window.CAP_CUSTOM_FETCH(e,t):fetch(e,t);function i(e,t){let i=function(e){let t=2166136261;for(let i=0;i<e.length;i++)t^=e.charCodeAt(i),t+=(t<<1)+(t<<4)+(t<<7)+(t<<8)+(t<<24);return t>>>0}(e),s="";function r(){return i^=i<<13,i^=i>>>17,i^=i<<5,i>>>0}for(;s.length<t;){s+=r().toString(16).padStart(8,"0")}return s.substring(0,t)}async function s(e){var t=(e=>{const t=atob(e),i=new Uint8Array(t.length);for(let e=0;e<t.length;e++)i[e]=t.charCodeAt(e);return i})(e);const i=await new Promise((e,i)=>{try{var s=new DecompressionStream("deflate-raw"),r=s.writable.getWriter(),a=s.readable.getReader(),n=[];a.read().then(function t(s){if(s.done){for(var r=0,o=0,l=0;l<n.length;l++)r+=n[l].length;var c=new Uint8Array(r);for(l=0;l<n.length;l++)c.set(n[l],o),o+=n[l].length;e((new TextDecoder).decode(c))}else n.push(s.value),a.read().then(t).catch(i)}).catch(i),r.write(t).then(()=>{r.close()}).catch(i)}catch(e){i(e)}});return new Promise(e=>{var t=setTimeout(()=>{a(),e({__timeout:!0})},2e4),s=document.createElement("iframe");s.setAttribute("sandbox","allow-scripts"),s.setAttribute("aria-hidden","true"),s.style.cssText="position:absolute;width:1px;height:1px;top:-9999px;left:-9999px;border:none;opacity:0;pointer-events:none;";var r=!1;function a(){r||(r=!0,clearTimeout(t),window.removeEventListener("message",n),s.parentNode&&s.parentNode.removeChild(s))}function n(t){var i=t.data;i&&"object"==typeof i&&("cap:instr"===i.type?(a(),i.blocked?e({__blocked:!0,blockReason:i.blockReason||"automated_browser"}):i.result?e(i.result):e({__timeout:!0})):"cap:error"===i.type&&(a(),e({__timeout:!0})))}window.addEventListener("message",n),s.srcdoc='<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><script>'+i+"\n<\/script></body></html>",document.body.appendChild(s)})}let r=null;const a=()=>{if(r)return r;const e=window.CAP_CUSTOM_WASM_URL||"https://cdn.jsdelivr.net/npm/@cap.js/wasm@0.0.6/browser/cap_wasm_bg.wasm";return r=fetch(e).then(e=>{if(!e.ok)throw new Error(`Failed to fetch wasm: ${e.status}`);return e.arrayBuffer()}).then(e=>WebAssembly.compile(e)).catch(e=>{throw r=null,e}),r};"object"==typeof WebAssembly&&"function"==typeof WebAssembly.compile&&a().catch(()=>{});let n=null;function o(){return n||(n=URL.createObjectURL(new Blob(['(()=>{const e=async({salt:e,target:t})=>{let n=0;const r=new TextEncoder,o=4*t.length,s=Math.floor(o/8),l=o%8,a=t.length%2==0?t:t+"0",c=a.length/2,i=new Uint8Array(c);for(let e=0;e<c;e++)i[e]=parseInt(a.substring(2*e,2*e+2),16);const f=l>0?255<<8-l&255:0;for(;;)try{for(let t=0;t<5e4;t++){const t=e+n,o=r.encode(t),a=await crypto.subtle.digest("SHA-256",o),c=new Uint8Array(a);let g=!0;for(let e=0;e<s;e++)if(c[e]!==i[e]){g=!1;break}if(g&&l>0&&(c[s]&f)!==(i[s]&f)&&(g=!1),g)return void self.postMessage({nonce:n,found:!0});n++}}catch(e){return console.error("[cap worker]",e),void self.postMessage({found:!1,error:e.message})}};if("object"!=typeof WebAssembly||"function"!=typeof WebAssembly?.instantiate)return console.warn("[cap worker] wasm not supported, falling back to alternative solver. this will be significantly slower."),void(self.onmessage=async({data:{salt:t,target:n}})=>e({salt:t,target:n}));let t=null;self.onmessage=async({data:{salt:n,target:r,wasmModule:o}})=>{if(o instanceof WebAssembly.Module&&null===t){const s=(e=>{try{let n,r=0,o=null;const s=()=>(null!==o&&0!==o.byteLength||(o=new Uint8Array(n.memory.buffer)),o),l=new TextEncoder,a=(e,t,n)=>{if(void 0===n){const n=l.encode(e),o=t(n.length,1)>>>0;return s().subarray(o,o+n.length).set(n),r=n.length,o}let o=e.length,a=t(o,1)>>>0;const c=s();let i=0;for(;i<o;i++){const t=e.charCodeAt(i);if(t>127)break;c[a+i]=t}if(i!==o){0!==i&&(e=e.slice(i)),a=n(a,o,o=i+3*e.length,1)>>>0;const t=s().subarray(a+i,a+o),{written:r}=l.encodeInto(e,t);i+=r,a=n(a,o,i,1)>>>0}return r=i,a},c={wbg:{}};c.wbg.__wbindgen_init_externref_table=()=>{const e=n.__wbindgen_export_0,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)};const i=new WebAssembly.Instance(e,c);return n=i.exports,n.__wbindgen_start&&n.__wbindgen_start(),t=(e,t)=>{const o=a(e,n.__wbindgen_malloc,n.__wbindgen_realloc),s=r,l=a(t,n.__wbindgen_malloc,n.__wbindgen_realloc),c=r;return BigInt.asUintN(64,n.solve_pow(o,s,l,c))},!0}catch(e){return console.error("[cap worker] failed to init wasm from module:",e),!1}})(o);if(!s)return console.warn("[cap worker] wasm init failed, falling back to JS solver."),e({salt:n,target:r})}if(null===t)return console.warn("[cap worker] no wasm module provided, falling back to JS solver."),e({salt:n,target:r});try{const e=performance.now(),o=t(n,r),s=performance.now();self.postMessage({nonce:Number(o),found:!0,durationMs:(s-e).toFixed(2)})}catch(e){console.error("[cap worker]",e),self.postMessage({found:!1,error:e.message||String(e)})}},self.onerror=e=>{self.postMessage({found:!1,error:e})}})();'],{type:"application/javascript"})),n)}class l{constructor(e){this._size=e,this._workers=[],this._idle=[],this._queue=[],this._wasmModule=null,this._spawnFailures=0}setWasm(e){this._wasmModule=e}_spawn(){const e=o(),t=new Worker(e);return t._busy=!1,this._workers.push(t),this._idle.push(t),t}_replaceWorker(e){const t=this._workers.indexOf(e);-1!==t&&this._workers.splice(t,1);const i=this._idle.indexOf(e);-1!==i&&this._idle.splice(i,1);try{e.terminate()}catch{}return this._spawnFailures++,this._spawnFailures>3?(console.error("[cap] worker spawn failed repeatedly, not retrying"),null):this._spawn()}_ensureSize(e){for(;this._workers.length<e;)this._spawn()}run(e,t){return new Promise((i,s)=>{this._queue.push({salt:e,target:t,resolve:i,reject:s}),this._dispatch()})}_dispatch(){for(;this._idle.length>0&&this._queue.length>0;){const e=this._idle.shift(),{salt:t,target:i,resolve:s,reject:r}=this._queue.shift();let a=!1;const n=({data:t})=>{a||(a=!0,e.removeEventListener("message",n),e.removeEventListener("error",o),this._spawnFailures=0,this._idle.push(e),t.found?s(t.nonce):r(new Error(t.error||"worker failed")),this._dispatch())},o=t=>{if(a)return;a=!0,e.removeEventListener("message",n),e.removeEventListener("error",o);const i=this._replaceWorker(e);r(t),i&&this._dispatch()};e.addEventListener("message",n),e.addEventListener("error",o),this._wasmModule?e.postMessage({salt:t,target:i,wasmModule:this._wasmModule},[]):e.postMessage({salt:t,target:i})}}terminate(){for(const e of this._workers)try{e.terminate()}catch{}this._workers=[],this._idle=[],this._queue=[]}}class c extends HTMLElement{#e=null;#t=navigator.hardwareConcurrency||8;token=null;#i;#s;#r;#a=!1;#n;#o=null;#l=null;#c=null;#h=null;#d(){return{state:"idle",challengeResp:null,challenges:null,results:[],completedCount:0,solvePromise:null,promoteFn:null,_listeners:[],pendingPromotion:null,token:null,tokenExpires:null,notify(){for(const e of this._listeners)e();this._listeners=[]},onSettled(e){"done"===this.state||"error"===this.state?e():this._listeners.push(e)}}}#p(){this.#o=this.#d(),this.#u()}#v(){this.#h&&(window.removeEventListener("mousemove",this.#h),window.removeEventListener("touchstart",this.#h),window.removeEventListener("keydown",this.#h),this.#h=null)}#u(){this.#v();const e=()=>{this.#v(),this.#g()};this.#h=e,window.addEventListener("mousemove",e,{passive:!0}),window.addEventListener("touchstart",e,{passive:!0}),window.addEventListener("keydown",e,{passive:!0})}#m(){return"function"==typeof this.checkVisibility?this.checkVisibility({checkOpacity:!0,checkVisibilityCSS:!0}):!!(this.offsetParent||this.getClientRects().length>0)}#g(){"idle"===this.#o.state&&this.#m()&&(this.#o.state="waiting",this.#l=setTimeout(()=>{this.#b()},2500))}async#b(){if("waiting"!==this.#o.state)return;this.#o.state="fetching";let e=this.getAttribute("data-cap-api-endpoint");if(!e&&window?.CAP_CUSTOM_FETCH&&(e="/"),e){e.endsWith("/")||(e+="/");try{const s=await t(`${e}challenge`,{method:"POST"});let r;try{r=await s.json()}catch{throw new Error("Failed to parse speculative challenge response")}if(r.error)throw new Error(r.error);r._apiEndpoint=e,this.#o.challengeResp=r;const{challenge:a,token:n}=r;let o=a;if(!Array.isArray(o)){let e=0;o=Array.from({length:a.c},()=>(e++,[i(`${n}${e}`,a.s),i(`${n}${e}d`,a.d)]))}this.#o.challenges=o,this.#o.state="solving",this.#o.solvePromise=this.#f(o)}catch(e){console.warn("[cap] speculative challenge fetch failed:",e),this.#o.state="error",this.#o.notify()}}else this.#o.state="idle"}async#f(e){o();let t=null;try{t=await a()}catch{}this.#c||(this.#c=new l(1),this.#c._spawn()),this.#c.setWasm(t);const i=e.length,s=new Array(i);let r=1,n=!1;this.#o.promoteFn=e=>{n||(n=!0,r=e,this.#c._size=e,this.#c._ensureSize(e))},null!==this.#o.pendingPromotion&&(this.#o.promoteFn(this.#o.pendingPromotion),this.#o.pendingPromotion=null);let c=0;for(;c<i;){const t=r,a=[],o=[];for(let s=0;s<t&&c<i;s++)o.push(c),a.push(e[c]),c++;this.#c._ensureSize(Math.max(r,t));const l=await Promise.all(a.map(e=>this.#c.run(e[0],e[1]).then(e=>(this.#o.completedCount++,e))));for(let e=0;e<o.length;e++)s[o[e]]=l[e];!n&&c<i&&await new Promise(e=>setTimeout(e,120))}return this.#o.results=s,this.#o.state="redeeming",this.#w(s),s}async#w(e){try{const i=this.#o.challengeResp,r=i._apiEndpoint;if(!r)throw new Error("[cap] speculative redeem: missing apiEndpoint");let a=null;if(i.instrumentation&&(a=await s(i.instrumentation),a?.__timeout||a?.__blocked))return this.#o.state="done",void this.#o.notify();const n=await t(`${r}redeem`,{method:"POST",body:JSON.stringify({token:i.token,solutions:e,...a&&{instr:a}}),headers:{"Content-Type":"application/json"}});let o;try{o=await n.json()}catch{throw new Error("Failed to parse speculative redeem response")}if(!o.success)throw new Error(o.error||"Speculative redeem failed");this.#o.token=o.token,this.#o.tokenExpires=new Date(o.expires).getTime(),this.#o.state="done",this.#o.notify()}catch(e){console.warn("[cap] speculative redeem failed (will redo on click):",e),this.#o.state="done",this.#o.notify()}}getI18nText(e,t){return this.getAttribute(`data-cap-i18n-${e}`)||t}static get observedAttributes(){return["onsolve","onprogress","onreset","onerror","data-cap-worker-count","data-cap-i18n-initial-state"]}constructor(){super(),this.#n&&this.#n.forEach((e,t)=>{this.removeEventListener(t.slice(2),e)}),this.#n=new Map,this.boundHandleProgress=this.handleProgress.bind(this),this.boundHandleSolve=this.handleSolve.bind(this),this.boundHandleError=this.handleError.bind(this),this.boundHandleReset=this.handleReset.bind(this)}initialize(){o(),this.#o||(this.#o=this.#d()),this.#c||(this.#c=new l(1),this.#c._spawn())}attributeChangedCallback(e,t,i){if(e.startsWith("on")){const t=e.slice(2),s=this.#n.get(e);if(s&&this.removeEventListener(t,s),i){const i=t=>{const i=this.getAttribute(e);"function"==typeof window[i]&&window[i].call(this,t)};this.#n.set(e,i),this.addEventListener(t,i)}}"data-cap-worker-count"===e&&this.setWorkersCount(parseInt(i,10)),"data-cap-i18n-initial-state"===e&&this.#s&&this.#s?.querySelector(".label.active")&&this.animateLabel(this.getI18nText("initial-state","Verify you're human"))}async connectedCallback(){this.#r=this,this.#i=this.attachShadow({mode:"open"}),this.#s=document.createElement("div"),this.createUI(),this.addEventListeners(),this.initialize(),this.#s.removeAttribute("disabled");const e=this.getAttribute("data-cap-worker-count"),t=e?parseInt(e,10):null;this.setWorkersCount(t||navigator.hardwareConcurrency||8);const i=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.#r.innerHTML=`<input type="hidden" name="${i}">`,this.#u()}async solve(){if(!this.#a)try{this.#a=!0,this.updateUI("verifying",this.getI18nText("verifying-label","Verifying..."),!0),this.#s.setAttribute("aria-label",this.getI18nText("verifying-aria-label","Verifying you're a human, please wait")),this.dispatchEvent("progress",{progress:0});try{let r,a,n=this.getAttribute("data-cap-api-endpoint");if(!n&&window?.CAP_CUSTOM_FETCH)n="/";else if(!n)throw new Error("Missing API endpoint. Either custom fetch or an API endpoint must be provided.");if(n.endsWith("/")||(n+="/"),"done"===this.#o.state&&this.#o.token&&this.#o.tokenExpires&&Date.now()<this.#o.tokenExpires){this.dispatchEvent("progress",{progress:100});const t=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.querySelector(`input[name='${t}']`)&&(this.querySelector(`input[name='${t}']`).value=this.#o.token),this.dispatchEvent("solve",{token:this.#o.token}),this.token=this.#o.token;const i=this.#o.tokenExpires-Date.now();return this.#e&&clearTimeout(this.#e),this.#e=setTimeout(()=>this.reset(),i),this.#s.setAttribute("aria-label",this.getI18nText("verified-aria-label","We have verified you're a human, you may now continue")),e&&navigator.vibrate([10,50,20,30,40]),this.#p(),this.#a=!1,{success:!0,token:this.token}}if("done"===this.#o.state)r=this.#o.results,a=this.#o.challengeResp,this.dispatchEvent("progress",{progress:100});else if("solving"===this.#o.state||"redeeming"===this.#o.state||"fetching"===this.#o.state||"waiting"===this.#o.state){"waiting"===this.#o.state&&(this.#l&&(clearTimeout(this.#l),this.#l=null),this.#o.state="waiting",this.#b()),this.#o.pendingPromotion=this.#t,this.#o.promoteFn&&this.#o.promoteFn(this.#t);const t=setInterval(()=>{if("solving"!==this.#o.state&&"redeeming"!==this.#o.state)return void clearInterval(t);const e=this.#o.challenges?this.#o.challenges.length:1,i=this.#o.completedCount,s="redeeming"===this.#o.state?99:Math.min(98,Math.round(i/e*100));this.dispatchEvent("progress",{progress:s})},150);if(await new Promise(e=>this.#o.onSettled(e)),clearInterval(t),"done"!==this.#o.state)throw new Error("Speculative solve failed – please try again");if(this.#o.token&&this.#o.tokenExpires&&Date.now()<this.#o.tokenExpires){this.dispatchEvent("progress",{progress:100});const t=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.querySelector(`input[name='${t}']`)&&(this.querySelector(`input[name='${t}']`).value=this.#o.token),this.dispatchEvent("solve",{token:this.#o.token}),this.token=this.#o.token;const i=this.#o.tokenExpires-Date.now();return this.#e&&clearTimeout(this.#e),this.#e=setTimeout(()=>this.reset(),i),this.#s.setAttribute("aria-label",this.getI18nText("verified-aria-label","We have verified you're a human, you may now continue")),e&&navigator.vibrate([10,50,20,30,40]),this.#p(),this.#a=!1,{success:!0,token:this.token}}r=this.#o.results,a=this.#o.challengeResp,this.dispatchEvent("progress",{progress:100})}else{const e=await t(`${n}challenge`,{method:"POST"});try{a=await e.json()}catch{throw new Error("Failed to parse challenge response from server")}if(a.error)throw new Error(a.error);const{challenge:s,token:o}=a;let l=s;if(!Array.isArray(l)){let e=0;l=Array.from({length:s.c},()=>(e++,[i(`${o}${e}`,s.s),i(`${o}${e}d`,s.d)]))}r=await this.solveChallenges(l)}const o=a.instrumentation?s(a.instrumentation):Promise.resolve(null),l=await o;if(l?.__timeout||l?.__blocked){this.updateUIBlocked(this.getI18nText("error-label","Error"),l?.__blocked),this.#s.setAttribute("aria-label",this.getI18nText("error-aria-label","An error occurred, please try again")),this.removeEventListener("error",this.boundHandleError);const e=new CustomEvent("error",{bubbles:!0,composed:!0,detail:{isCap:!0,message:"Instrumentation failed"}});return super.dispatchEvent(e),this.addEventListener("error",this.boundHandleError),this.executeAttributeCode("onerror",e),console.error("[cap]","Instrumentation failed"),void(this.#a=!1)}const{token:c}=a,h=await t(`${n}redeem`,{method:"POST",body:JSON.stringify({token:c,solutions:r,...l&&{instr:l}}),headers:{"Content-Type":"application/json"}});let d;try{d=await h.json()}catch{throw new Error("Failed to parse server response")}if(this.dispatchEvent("progress",{progress:100}),!d.success)throw new Error(d.error||"Invalid solution");const p=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.querySelector(`input[name='${p}']`)&&(this.querySelector(`input[name='${p}']`).value=d.token),this.dispatchEvent("solve",{token:d.token}),this.token=d.token,this.#p(),this.#e&&clearTimeout(this.#e);const u=new Date(d.expires).getTime()-Date.now();return u>0&&u<864e5?this.#e=setTimeout(()=>this.reset(),u):this.error("Invalid expiration time"),this.#s.setAttribute("aria-label",this.getI18nText("verified-aria-label","We have verified you're a human, you may now continue")),e&&navigator.vibrate([10,50,20,30,40]),{success:!0,token:this.token}}catch(e){throw this.#s.setAttribute("aria-label",this.getI18nText("error-aria-label","An error occurred, please try again")),this.error(e.message),e}}finally{this.#a=!1}}async solveChallenges(e){const t=e.length;let i=0;let s=null;const r="object"==typeof WebAssembly&&"function"==typeof WebAssembly.instantiate;if(r)try{s=await a()}catch(e){console.warn("[cap] wasm unavailable, falling back to JS solver:",e)}if(!r&&!this.#i.querySelector(".warning")){const e=document.createElement("div");e.className="warning",e.style.cssText="width:var(--cap-widget-width,230px);background:rgb(237,56,46);color:white;padding:4px 6px;padding-bottom:calc(var(--cap-border-radius,14px) + 5px);font-size:10px;box-sizing:border-box;font-family:system-ui;border-top-left-radius:8px;border-top-right-radius:8px;text-align:center;user-select:none;margin-bottom:-35.5px;opacity:0;transition:margin-bottom .3s,opacity .3s;",e.innerText=this.getI18nText("wasm-disabled","Enable WASM for significantly faster solving"),this.#i.insertBefore(e,this.#i.firstChild),setTimeout(()=>{e.style.marginBottom="calc(-1 * var(--cap-border-radius, 14px))",e.style.opacity=1},10)}const n=new l(this.#t);n.setWasm(s),n._ensureSize(this.#t);const o=[];try{for(let s=0;s<e.length;s+=this.#t){const r=e.slice(s,Math.min(s+this.#t,e.length)),a=await Promise.all(r.map(([e,s])=>n.run(e,s).then(e=>{i++;const s=Math.min(99,Math.round((0+i)/t*100));return this.dispatchEvent("progress",{progress:s}),e})));o.push(...a)}}finally{n.terminate()}return o}setWorkersCount(e){const t=parseInt(e,10),i=Math.min(navigator.hardwareConcurrency||8,16);this.#t=!Number.isNaN(t)&&t>0&&t<=i?t:navigator.hardwareConcurrency||8}createUI(){this.#s.classList.add("captcha"),this.#s.setAttribute("role","button"),this.#s.setAttribute("tabindex","0"),this.#s.setAttribute("aria-label",this.getI18nText("verify-aria-label","Click to verify you're a human")),this.#s.setAttribute("aria-live","polite"),this.#s.setAttribute("disabled","true"),this.#s.innerHTML=`<div class="checkbox" part="checkbox"><svg class="progress-ring" viewBox="0 0 32 32"><circle class="progress-ring-bg" cx="16" cy="16" r="14"></circle><circle class="progress-ring-circle" cx="16" cy="16" r="14"></circle></svg></div><p part="label" class="label-wrapper"><span class="label active">${this.getI18nText("initial-state","Verify you're human")}</span></p><a part="attribution" aria-label="Secured by Cap" href="https://capjs.js.org/" class="credits" target="_blank" rel="follow noopener" title="Secured by Cap: Self-hosted CAPTCHA for the modern web.">Cap</a>`,this.#i.innerHTML=`<style${window.CAP_CSS_NONCE?` nonce=${window.CAP_CSS_NONCE}`:""}>@media (prefers-reduced-motion:reduce){.captcha,.captcha *{transition:none!important;animation:none!important}.label{filter:none!important;opacity:1!important;transition:none!important;transform:none!important}.label:not(.active),.label.exit{display:none!important}}.captcha,.captcha *{box-sizing:border-box}.captcha{background-color:var(--cap-background,#fdfdfd);border:1px solid var(--cap-border-color,#dddddd8f);border-radius:var(--cap-border-radius,14px);-webkit-user-select:none;user-select:none;height:var(--cap-widget-height,58px);width:var(--cap-widget-width,260px);padding:var(--cap-widget-padding,14px);align-items:center;gap:var(--cap-gap,15px);cursor:pointer;-webkit-tap-highlight-color:#fff0;color:var(--cap-color,#212121);transition:filter .2s,transform .2s;display:flex;position:relative;overflow:hidden}.captcha:not([data-state]):active{transform:scale(.97)}.captcha:hover{filter:brightness(98%)}.captcha *{font-family:var(--cap-font,system, -apple-system, "BlinkMacSystemFont", ".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif)}.checkbox{width:var(--cap-checkbox-size,25px);height:var(--cap-checkbox-size,25px);border:var(--cap-checkbox-border,1px solid #aaaaaad1);border-radius:var(--cap-checkbox-border-radius,6px);background-color:var(--cap-checkbox-background,#fafafa91);margin-top:var(--cap-checkbox-margin,2px);margin-bottom:var(--cap-checkbox-margin,2px);transition:opacity .2s}.captcha p{-webkit-user-select:none;user-select:none;margin:0;font-size:15px;font-weight:500}.label-wrapper{flex:1;align-items:center;min-width:0;height:2em;display:flex;position:relative;overflow:hidden}.label-wrapper:before,.label-wrapper:after{content:"";pointer-events:none;z-index:1;width:100%;height:.6em;position:absolute;left:0}.label-wrapper:before{background:linear-gradient(to bottom, var(--cap-background,#fdfdfd), transparent);top:0}.label-wrapper:after{background:linear-gradient(to top, var(--cap-background,#fdfdfd), transparent);bottom:0}.label{white-space:nowrap;opacity:0;filter:blur(2px);transition:transform .5s cubic-bezier(.25,1,.5,1),opacity .5s,filter .5s;position:absolute;transform:translateY(100%)}.label.active{opacity:1;filter:none;transform:translateY(0)}.label.exit{opacity:0;filter:blur(2px);transform:translateY(-100%)}.checkbox .progress-ring{width:100%;height:100%;display:none;transform:rotate(-90deg)}.checkbox .progress-ring-bg{fill:none;stroke:var(--cap-spinner-background-color,#eee);stroke-width:var(--cap-spinner-thickness,3)}.checkbox .progress-ring-circle{fill:none;stroke:var(--cap-spinner-color,#000);stroke-width:var(--cap-spinner-thickness,3);stroke-linecap:round;stroke-dasharray:87.96;stroke-dashoffset:87.96px;transition:stroke-dashoffset .3s}.captcha[data-state=verifying] .checkbox{background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;display:flex;transform:scale(1.1)}.captcha[data-state=verifying] .checkbox .progress-ring{display:block}.captcha[data-state=done] .checkbox{background-image:var(--cap-checkmark,url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cstyle%3E%40keyframes%20anim%7B0%25%7Bstroke-dashoffset%3A23.21320343017578px%7Dto%7Bstroke-dashoffset%3A0%7D%7D%3C%2Fstyle%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%2300a67d%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m5%2012%205%205L20%207%22%20style%3D%22stroke-dashoffset%3A0%3Bstroke-dasharray%3A23.21320343017578px%3Banimation%3Aanim%20.5s%20ease%22%2F%3E%3C%2Fsvg%3E));background-size:cover;border:1px solid #0000}.captcha[data-state=done] .checkbox .progress-ring{display:none}.captcha[data-state=error] .checkbox{background-image:var(--cap-error-cross,url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 24 24'%3E%3Cpath fill='%23f55b50' d='M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8'/%3E%3C%2Fsvg%3E"));background-size:cover;border:1px solid #0000}.captcha[data-state=error] .checkbox .progress-ring{display:none}.captcha[disabled]{cursor:not-allowed}.captcha[disabled][data-state=verifying]{cursor:progress}.captcha[disabled][data-state=done]{cursor:default}.captcha .credits{color:var(--cap-color,#212121);opacity:.8;text-underline-offset:.18em;z-index:6;font-size:12px;text-decoration-thickness:.08em;position:absolute;bottom:10px;right:10px}.cap-troubleshoot-link{color:#06c;text-underline-offset:.15em;cursor:pointer;font-weight:500;font-size:inherit;text-decoration:underline;text-decoration-thickness:.08em}.cap-troubleshoot-link:hover{opacity:.8}</style>`,this.#i.appendChild(this.#s)}addEventListeners(){this.#s&&(this.#s.querySelector("a").addEventListener("click",e=>{e.stopPropagation(),e.preventDefault(),window.open("https://capjs.js.org","_blank")}),this.#s.addEventListener("click",()=>{this.#s.hasAttribute("disabled")||this.solve()}),this.#s.addEventListener("mousedown",()=>{!this.#s.hasAttribute("disabled")&&e&&navigator.vibrate(5)}),this.#s.addEventListener("keydown",e=>{"Enter"!==e.key&&" "!==e.key||this.#s.hasAttribute("disabled")||(e.preventDefault(),e.stopPropagation(),this.solve())}),this.addEventListener("progress",this.boundHandleProgress),this.addEventListener("solve",this.boundHandleSolve),this.addEventListener("error",this.boundHandleError),this.addEventListener("reset",this.boundHandleReset))}animateLabel(e){if(!this.#s)return;const t=this.#s.querySelector(".label-wrapper");if(!t)return;if(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches){const i=t.querySelector(".label.active");if(i)i.textContent=e;else{const i=document.createElement("span");i.className="label active",i.textContent=e,t.appendChild(i)}return}const i=t.querySelector(".label.active"),s=document.createElement("span");s.className="label",s.textContent=e,t.appendChild(s),s.offsetWidth,s.classList.add("active"),i&&(i.classList.remove("active"),i.classList.add("exit"),i.addEventListener("transitionend",()=>i.remove(),{once:!0}))}updateUI(e,t,i=!1){this.#s&&(this.#s.setAttribute("data-state",e),this.animateLabel(t),i?this.#s.setAttribute("disabled","true"):this.#s.removeAttribute("disabled"))}updateUIBlocked(e,t=!1){if(!this.#s)return;this.#s.setAttribute("data-state","error"),this.#s.removeAttribute("disabled");const i=this.#s.querySelector(".label-wrapper");if(!i)return;const s=this.getAttribute("data-cap-troubleshooting-url")||"https://capjs.js.org/guide/troubleshooting/instrumentation.html",r=i.querySelector(".label.active"),a=document.createElement("span");a.className="label",a.innerHTML=t?`${e} · <a class="cap-troubleshoot-link" href="${s}" target="_blank" rel="noopener">${this.getI18nText("troubleshooting-label","Troubleshoot")}</a>`:e,i.appendChild(a),a.offsetWidth,a.classList.add("active"),r&&(r.classList.remove("active"),r.classList.add("exit"),r.addEventListener("transitionend",()=>r.remove(),{once:!0}));const n=a.querySelector(".cap-troubleshoot-link");n&&n.addEventListener("click",e=>{e.stopPropagation()})}handleProgress(e){if(!this.#s)return;const t=this.#s.querySelector(".progress-ring-circle");if(t){const i=2*Math.PI*14,s=i-e.detail.progress/100*i;t.style.strokeDashoffset=s}const i=this.#s.querySelector(".label-wrapper");if(i){const e=i.querySelector(".label.active");e&&(e.textContent=`${this.getI18nText("verifying-label","Verifying...")}`)}this.executeAttributeCode("onprogress",e)}handleSolve(e){this.updateUI("done",this.getI18nText("solved-label","You're a human"),!0),this.executeAttributeCode("onsolve",e)}handleError(t){this.updateUI("error",this.getI18nText("error-label","Error. Try again.")),this.executeAttributeCode("onerror",t),e&&navigator.vibrate([10,40,10])}handleReset(e){this.updateUI("",this.getI18nText("initial-state","I'm a human")),this.executeAttributeCode("onreset",e)}executeAttributeCode(e,t){const i=this.getAttribute(e);i&&(console.error("[cap] using `onxxx='…'` is strongly discouraged and will be deprecated soon. please use `addEventListener` callbacks instead."),new Function("event",i).call(this,t))}error(e="Unknown error"){console.error("[cap]",e),this.dispatchEvent("error",{isCap:!0,message:e})}dispatchEvent(e,t={}){const i=new CustomEvent(e,{bubbles:!0,composed:!0,detail:t});super.dispatchEvent(i)}reset(){this.#e&&(clearTimeout(this.#e),this.#e=null),this.dispatchEvent("reset"),this.token=null;const e=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.querySelector(`input[name='${e}']`)&&(this.querySelector(`input[name='${e}']`).value="")}get tokenValue(){return this.token}disconnectedCallback(){this.removeEventListener("progress",this.boundHandleProgress),this.removeEventListener("solve",this.boundHandleSolve),this.removeEventListener("error",this.boundHandleError),this.removeEventListener("reset",this.boundHandleReset),this.#n.forEach((e,t)=>{this.removeEventListener(t.slice(2),e)}),this.#n.clear(),this.#i&&(this.#i.innerHTML=""),this.reset(),this.cleanup()}cleanup(){this.#e&&(clearTimeout(this.#e),this.#e=null),this.#v(),this.#l&&(clearTimeout(this.#l),this.#l=null),this.#c&&(this.#c.terminate(),this.#c=null),this.#o&&(this.#o.state="error",this.#o.notify(),this.#o=null)}}class h{constructor(e={},t){const i=t||document.createElement("cap-widget");if(Object.entries(e).forEach(([e,t])=>{i.setAttribute(e,t)}),!e.apiEndpoint&&!window?.CAP_CUSTOM_FETCH)throw i.remove(),new Error("Missing API endpoint. Either custom fetch or an API endpoint must be provided.");e.apiEndpoint&&i.setAttribute("data-cap-api-endpoint",e.apiEndpoint),this.widget=i,this.solve=this.widget.solve.bind(this.widget),this.reset=this.widget.reset.bind(this.widget),this.addEventListener=this.widget.addEventListener.bind(this.widget),Object.defineProperty(this,"token",{get:()=>i.token,configurable:!0,enumerable:!0}),t||(i.style.display="none",document.documentElement.appendChild(i))}}window.Cap=h,customElements.get("cap-widget")||window?.CAP_DONT_SKIP_REDEFINE?customElements.get("cap-widget")&&console.warn("[cap] the cap-widget element has already been defined, skipping re-defining it.\nto prevent this, set window.CAP_DONT_SKIP_REDEFINE to true"):customElements.define("cap-widget",c),"object"==typeof exports&&"undefined"!=typeof module?module.exports=h:"function"==typeof define&&define.amd&&define([],()=>h),"undefined"!=typeof exports&&(exports.default=h)})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap.js/widget",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "Client-side widget for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.",
5
5
  "keywords": [
6
6
  "algorithm",
package/src/cap.js CHANGED
@@ -175,253 +175,6 @@
175
175
  const SPECULATIVE_WORKERS = 1;
176
176
  const SPECULATIVE_YIELD_MS = 120;
177
177
 
178
- const speculative = {
179
- state: "idle",
180
- challengeResp: null,
181
- challenges: null,
182
- results: [],
183
- completedCount: 0,
184
- solvePromise: null,
185
- promoteFn: null,
186
- _listeners: [],
187
- pendingPromotion: null,
188
- token: null,
189
- tokenExpires: null,
190
-
191
- notify() {
192
- for (const fn of this._listeners) fn();
193
- this._listeners = [];
194
- },
195
-
196
- onSettled(fn) {
197
- if (this.state === "done" || this.state === "error") {
198
- fn();
199
- } else {
200
- this._listeners.push(fn);
201
- }
202
- },
203
- };
204
-
205
- function _resetSpeculativeState() {
206
- speculative.state = "idle";
207
- speculative.challengeResp = null;
208
- speculative.challenges = null;
209
- speculative.results = [];
210
- speculative.completedCount = 0;
211
- speculative.solvePromise = null;
212
- speculative.promoteFn = null;
213
- speculative.pendingPromotion = null;
214
- speculative._listeners = [];
215
- speculative.token = null;
216
- speculative.tokenExpires = null;
217
-
218
- _attachInteractionListeners();
219
- }
220
-
221
- let _speculativeTimer = null;
222
-
223
- function _onFirstInteraction() {
224
- if (speculative.state !== "idle") return;
225
- speculative.state = "waiting";
226
-
227
- _speculativeTimer = setTimeout(() => {
228
- _beginSpeculativeSolve();
229
- }, SPECULATIVE_DELAY_MS);
230
- }
231
-
232
- let _currentInteractionHandler = null;
233
-
234
- function _detachInteractionListeners() {
235
- if (_currentInteractionHandler) {
236
- window.removeEventListener("mousemove", _currentInteractionHandler);
237
- window.removeEventListener("touchstart", _currentInteractionHandler);
238
- window.removeEventListener("keydown", _currentInteractionHandler);
239
- _currentInteractionHandler = null;
240
- }
241
- }
242
-
243
- function _attachInteractionListeners() {
244
- _detachInteractionListeners();
245
-
246
- const handler = () => {
247
- _detachInteractionListeners();
248
- _onFirstInteraction();
249
- };
250
- _currentInteractionHandler = handler;
251
- window.addEventListener("mousemove", handler, { passive: true });
252
- window.addEventListener("touchstart", handler, { passive: true });
253
- window.addEventListener("keydown", handler, { passive: true });
254
- }
255
-
256
- _attachInteractionListeners();
257
-
258
- async function _beginSpeculativeSolve() {
259
- if (speculative.state !== "waiting") return;
260
- speculative.state = "fetching";
261
-
262
- const widget = document.querySelector("cap-widget");
263
- if (!widget) {
264
- speculative.state = "idle";
265
- return;
266
- }
267
-
268
- let apiEndpoint = widget.getAttribute("data-cap-api-endpoint");
269
- if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
270
- apiEndpoint = "/";
271
- }
272
- if (!apiEndpoint) {
273
- speculative.state = "idle";
274
- return;
275
- }
276
- if (!apiEndpoint.endsWith("/")) apiEndpoint += "/";
277
-
278
- try {
279
- const raw = await capFetch(`${apiEndpoint}challenge`, { method: "POST" });
280
- let resp;
281
- try {
282
- resp = await raw.json();
283
- } catch {
284
- throw new Error("Failed to parse speculative challenge response");
285
- }
286
- if (resp.error) throw new Error(resp.error);
287
-
288
- resp._apiEndpoint = apiEndpoint;
289
- speculative.challengeResp = resp;
290
-
291
- const { challenge, token } = resp;
292
- let challenges = challenge;
293
- if (!Array.isArray(challenges)) {
294
- let i = 0;
295
- challenges = Array.from({ length: challenge.c }, () => {
296
- i++;
297
- return [prng(`${token}${i}`, challenge.s), prng(`${token}${i}d`, challenge.d)];
298
- });
299
- }
300
- speculative.challenges = challenges;
301
- speculative.state = "solving";
302
-
303
- speculative.solvePromise = _speculativeSolveAll(challenges);
304
- } catch (e) {
305
- console.warn("[cap] speculative challenge fetch failed:", e);
306
- speculative.state = "error";
307
- speculative.notify();
308
- }
309
- }
310
-
311
- async function _speculativeSolveAll(challenges) {
312
- _getSharedWorkerUrl();
313
-
314
- let wasmModule = null;
315
- try {
316
- wasmModule = await getWasmModule();
317
- } catch {}
318
-
319
- _speculativePool.setWasm(wasmModule);
320
-
321
- const total = challenges.length;
322
- const results = new Array(total);
323
-
324
- let concurrency = SPECULATIVE_WORKERS;
325
- let promoted = false;
326
-
327
- speculative.promoteFn = (fullCount) => {
328
- if (promoted) return;
329
- promoted = true;
330
- concurrency = fullCount;
331
- _speculativePool._size = fullCount;
332
- _speculativePool._ensureSize(fullCount);
333
- };
334
-
335
- if (speculative.pendingPromotion !== null) {
336
- speculative.promoteFn(speculative.pendingPromotion);
337
- speculative.pendingPromotion = null;
338
- }
339
-
340
- let nextIndex = 0;
341
-
342
- while (nextIndex < total) {
343
- const batchSize = concurrency;
344
- const batch = [];
345
- const batchIndices = [];
346
-
347
- for (let i = 0; i < batchSize && nextIndex < total; i++) {
348
- batchIndices.push(nextIndex);
349
- batch.push(challenges[nextIndex]);
350
- nextIndex++;
351
- }
352
-
353
- _speculativePool._ensureSize(Math.max(concurrency, batchSize));
354
-
355
- const batchResults = await Promise.all(
356
- batch.map((challenge) =>
357
- _speculativePool.run(challenge[0], challenge[1]).then((nonce) => {
358
- speculative.completedCount++;
359
- return nonce;
360
- }),
361
- ),
362
- );
363
-
364
- for (let i = 0; i < batchIndices.length; i++) {
365
- results[batchIndices[i]] = batchResults[i];
366
- }
367
-
368
- if (!promoted && nextIndex < total) {
369
- await new Promise((resolve) => setTimeout(resolve, SPECULATIVE_YIELD_MS));
370
- }
371
- }
372
-
373
- speculative.results = results;
374
- speculative.state = "redeeming";
375
- _speculativeRedeem(results);
376
- return results;
377
- }
378
-
379
- async function _speculativeRedeem(solutions) {
380
- try {
381
- const challengeResp = speculative.challengeResp;
382
- const apiEndpoint = challengeResp._apiEndpoint;
383
- if (!apiEndpoint) throw new Error("[cap] speculative redeem: missing apiEndpoint");
384
-
385
- let instrOut = null;
386
- if (challengeResp.instrumentation) {
387
- instrOut = await runInstrumentationChallenge(challengeResp.instrumentation);
388
- if (instrOut?.__timeout || instrOut?.__blocked) {
389
- speculative.state = "done";
390
- speculative.notify();
391
- return;
392
- }
393
- }
394
-
395
- const redeemRaw = await capFetch(`${apiEndpoint}redeem`, {
396
- method: "POST",
397
- body: JSON.stringify({
398
- token: challengeResp.token,
399
- solutions,
400
- ...(instrOut && { instr: instrOut }),
401
- }),
402
- headers: { "Content-Type": "application/json" },
403
- });
404
-
405
- let resp;
406
- try {
407
- resp = await redeemRaw.json();
408
- } catch {
409
- throw new Error("Failed to parse speculative redeem response");
410
- }
411
-
412
- if (!resp.success) throw new Error(resp.error || "Speculative redeem failed");
413
-
414
- speculative.token = resp.token;
415
- speculative.tokenExpires = new Date(resp.expires).getTime();
416
- speculative.state = "done";
417
- speculative.notify();
418
- } catch (e) {
419
- console.warn("[cap] speculative redeem failed (will redo on click):", e);
420
- speculative.state = "done";
421
- speculative.notify();
422
- }
423
- }
424
-
425
178
  let _sharedWorkerUrl = null;
426
179
 
427
180
  function _getSharedWorkerUrl() {
@@ -543,9 +296,6 @@
543
296
  }
544
297
  }
545
298
 
546
- const _speculativePool = new WorkerPool(1);
547
- _speculativePool._spawn();
548
-
549
299
  class CapWidget extends HTMLElement {
550
300
  #resetTimer = null;
551
301
  #workersCount = navigator.hardwareConcurrency || 8;
@@ -556,6 +306,248 @@
556
306
  #solving = false;
557
307
  #eventHandlers;
558
308
 
309
+ #speculative = null;
310
+ #speculativeTimer = null;
311
+ #speculativePool = null;
312
+ #interactionHandler = null;
313
+
314
+ #makeSpeculativeState() {
315
+ return {
316
+ state: "idle",
317
+ challengeResp: null,
318
+ challenges: null,
319
+ results: [],
320
+ completedCount: 0,
321
+ solvePromise: null,
322
+ promoteFn: null,
323
+ _listeners: [],
324
+ pendingPromotion: null,
325
+ token: null,
326
+ tokenExpires: null,
327
+
328
+ notify() {
329
+ for (const fn of this._listeners) fn();
330
+ this._listeners = [];
331
+ },
332
+
333
+ onSettled(fn) {
334
+ if (this.state === "done" || this.state === "error") {
335
+ fn();
336
+ } else {
337
+ this._listeners.push(fn);
338
+ }
339
+ },
340
+ };
341
+ }
342
+
343
+ #resetSpeculativeState() {
344
+ this.#speculative = this.#makeSpeculativeState();
345
+ this.#attachInteractionListeners();
346
+ }
347
+
348
+ #detachInteractionListeners() {
349
+ if (this.#interactionHandler) {
350
+ window.removeEventListener("mousemove", this.#interactionHandler);
351
+ window.removeEventListener("touchstart", this.#interactionHandler);
352
+ window.removeEventListener("keydown", this.#interactionHandler);
353
+ this.#interactionHandler = null;
354
+ }
355
+ }
356
+
357
+ #attachInteractionListeners() {
358
+ this.#detachInteractionListeners();
359
+ const handler = () => {
360
+ this.#detachInteractionListeners();
361
+ this.#onFirstInteraction();
362
+ };
363
+ this.#interactionHandler = handler;
364
+ window.addEventListener("mousemove", handler, { passive: true });
365
+ window.addEventListener("touchstart", handler, { passive: true });
366
+ window.addEventListener("keydown", handler, { passive: true });
367
+ }
368
+
369
+ #isVisible() {
370
+ if (typeof this.checkVisibility === "function") {
371
+ return this.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true });
372
+ }
373
+ // Fallback: offsetParent is null for display:none; also check the style directly
374
+ return !!(this.offsetParent || this.getClientRects().length > 0);
375
+ }
376
+
377
+ #onFirstInteraction() {
378
+ if (this.#speculative.state !== "idle") return;
379
+ if (!this.#isVisible()) return;
380
+ this.#speculative.state = "waiting";
381
+ this.#speculativeTimer = setTimeout(() => {
382
+ this.#beginSpeculativeSolve();
383
+ }, SPECULATIVE_DELAY_MS);
384
+ }
385
+
386
+ async #beginSpeculativeSolve() {
387
+ if (this.#speculative.state !== "waiting") return;
388
+ this.#speculative.state = "fetching";
389
+
390
+ let apiEndpoint = this.getAttribute("data-cap-api-endpoint");
391
+ if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
392
+ apiEndpoint = "/";
393
+ }
394
+ if (!apiEndpoint) {
395
+ this.#speculative.state = "idle";
396
+ return;
397
+ }
398
+ if (!apiEndpoint.endsWith("/")) apiEndpoint += "/";
399
+
400
+ try {
401
+ const raw = await capFetch(`${apiEndpoint}challenge`, { method: "POST" });
402
+ let resp;
403
+ try {
404
+ resp = await raw.json();
405
+ } catch {
406
+ throw new Error("Failed to parse speculative challenge response");
407
+ }
408
+ if (resp.error) throw new Error(resp.error);
409
+
410
+ resp._apiEndpoint = apiEndpoint;
411
+ this.#speculative.challengeResp = resp;
412
+
413
+ const { challenge, token } = resp;
414
+ let challenges = challenge;
415
+ if (!Array.isArray(challenges)) {
416
+ let i = 0;
417
+ challenges = Array.from({ length: challenge.c }, () => {
418
+ i++;
419
+ return [prng(`${token}${i}`, challenge.s), prng(`${token}${i}d`, challenge.d)];
420
+ });
421
+ }
422
+ this.#speculative.challenges = challenges;
423
+ this.#speculative.state = "solving";
424
+
425
+ this.#speculative.solvePromise = this.#speculativeSolveAll(challenges);
426
+ } catch (e) {
427
+ console.warn("[cap] speculative challenge fetch failed:", e);
428
+ this.#speculative.state = "error";
429
+ this.#speculative.notify();
430
+ }
431
+ }
432
+
433
+ async #speculativeSolveAll(challenges) {
434
+ _getSharedWorkerUrl();
435
+
436
+ let wasmModule = null;
437
+ try {
438
+ wasmModule = await getWasmModule();
439
+ } catch {}
440
+
441
+ if (!this.#speculativePool) {
442
+ this.#speculativePool = new WorkerPool(1);
443
+ this.#speculativePool._spawn();
444
+ }
445
+ this.#speculativePool.setWasm(wasmModule);
446
+
447
+ const total = challenges.length;
448
+ const results = new Array(total);
449
+
450
+ let concurrency = SPECULATIVE_WORKERS;
451
+ let promoted = false;
452
+
453
+ this.#speculative.promoteFn = (fullCount) => {
454
+ if (promoted) return;
455
+ promoted = true;
456
+ concurrency = fullCount;
457
+ this.#speculativePool._size = fullCount;
458
+ this.#speculativePool._ensureSize(fullCount);
459
+ };
460
+
461
+ if (this.#speculative.pendingPromotion !== null) {
462
+ this.#speculative.promoteFn(this.#speculative.pendingPromotion);
463
+ this.#speculative.pendingPromotion = null;
464
+ }
465
+
466
+ let nextIndex = 0;
467
+
468
+ while (nextIndex < total) {
469
+ const batchSize = concurrency;
470
+ const batch = [];
471
+ const batchIndices = [];
472
+
473
+ for (let i = 0; i < batchSize && nextIndex < total; i++) {
474
+ batchIndices.push(nextIndex);
475
+ batch.push(challenges[nextIndex]);
476
+ nextIndex++;
477
+ }
478
+
479
+ this.#speculativePool._ensureSize(Math.max(concurrency, batchSize));
480
+
481
+ const batchResults = await Promise.all(
482
+ batch.map((challenge) =>
483
+ this.#speculativePool.run(challenge[0], challenge[1]).then((nonce) => {
484
+ this.#speculative.completedCount++;
485
+ return nonce;
486
+ }),
487
+ ),
488
+ );
489
+
490
+ for (let i = 0; i < batchIndices.length; i++) {
491
+ results[batchIndices[i]] = batchResults[i];
492
+ }
493
+
494
+ if (!promoted && nextIndex < total) {
495
+ await new Promise((resolve) => setTimeout(resolve, SPECULATIVE_YIELD_MS));
496
+ }
497
+ }
498
+
499
+ this.#speculative.results = results;
500
+ this.#speculative.state = "redeeming";
501
+ this.#speculativeRedeem(results);
502
+ return results;
503
+ }
504
+
505
+ async #speculativeRedeem(solutions) {
506
+ try {
507
+ const challengeResp = this.#speculative.challengeResp;
508
+ const apiEndpoint = challengeResp._apiEndpoint;
509
+ if (!apiEndpoint) throw new Error("[cap] speculative redeem: missing apiEndpoint");
510
+
511
+ let instrOut = null;
512
+ if (challengeResp.instrumentation) {
513
+ instrOut = await runInstrumentationChallenge(challengeResp.instrumentation);
514
+ if (instrOut?.__timeout || instrOut?.__blocked) {
515
+ this.#speculative.state = "done";
516
+ this.#speculative.notify();
517
+ return;
518
+ }
519
+ }
520
+
521
+ const redeemRaw = await capFetch(`${apiEndpoint}redeem`, {
522
+ method: "POST",
523
+ body: JSON.stringify({
524
+ token: challengeResp.token,
525
+ solutions,
526
+ ...(instrOut && { instr: instrOut }),
527
+ }),
528
+ headers: { "Content-Type": "application/json" },
529
+ });
530
+
531
+ let resp;
532
+ try {
533
+ resp = await redeemRaw.json();
534
+ } catch {
535
+ throw new Error("Failed to parse speculative redeem response");
536
+ }
537
+
538
+ if (!resp.success) throw new Error(resp.error || "Speculative redeem failed");
539
+
540
+ this.#speculative.token = resp.token;
541
+ this.#speculative.tokenExpires = new Date(resp.expires).getTime();
542
+ this.#speculative.state = "done";
543
+ this.#speculative.notify();
544
+ } catch (e) {
545
+ console.warn("[cap] speculative redeem failed (will redo on click):", e);
546
+ this.#speculative.state = "done";
547
+ this.#speculative.notify();
548
+ }
549
+ }
550
+
559
551
  getI18nText(key, defaultValue) {
560
552
  return this.getAttribute(`data-cap-i18n-${key}`) || defaultValue;
561
553
  }
@@ -588,6 +580,13 @@
588
580
 
589
581
  initialize() {
590
582
  _getSharedWorkerUrl();
583
+ if (!this.#speculative) {
584
+ this.#speculative = this.#makeSpeculativeState();
585
+ }
586
+ if (!this.#speculativePool) {
587
+ this.#speculativePool = new WorkerPool(1);
588
+ this.#speculativePool._spawn();
589
+ }
591
590
  }
592
591
 
593
592
  attributeChangedCallback(name, _, value) {
@@ -638,14 +637,7 @@
638
637
  const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
639
638
  this.#host.innerHTML = `<input type="hidden" name="${fieldName}">`;
640
639
 
641
- if (speculative.state === "idle" || speculative.state === "waiting") {
642
- if (_speculativeTimer) {
643
- clearTimeout(_speculativeTimer);
644
- _speculativeTimer = null;
645
- }
646
- speculative.state = "waiting";
647
- _speculativeTimer = setTimeout(() => _beginSpeculativeSolve(), SPECULATIVE_DELAY_MS);
648
- }
640
+ this.#attachInteractionListeners();
649
641
  }
650
642
 
651
643
  async solve() {
@@ -662,8 +654,6 @@
662
654
  );
663
655
  this.dispatchEvent("progress", { progress: 0 });
664
656
 
665
- if (hasHaptics) navigator.vibrate(5);
666
-
667
657
  try {
668
658
  let apiEndpoint = this.getAttribute("data-cap-api-endpoint");
669
659
  if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
@@ -679,21 +669,21 @@
679
669
  let challengeResp;
680
670
 
681
671
  if (
682
- speculative.state === "done" &&
683
- speculative.token &&
684
- speculative.tokenExpires &&
685
- Date.now() < speculative.tokenExpires
672
+ this.#speculative.state === "done" &&
673
+ this.#speculative.token &&
674
+ this.#speculative.tokenExpires &&
675
+ Date.now() < this.#speculative.tokenExpires
686
676
  ) {
687
677
  this.dispatchEvent("progress", { progress: 100 });
688
678
 
689
679
  const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
690
680
  if (this.querySelector(`input[name='${fieldName}']`)) {
691
- this.querySelector(`input[name='${fieldName}']`).value = speculative.token;
681
+ this.querySelector(`input[name='${fieldName}']`).value = this.#speculative.token;
692
682
  }
693
- this.dispatchEvent("solve", { token: speculative.token });
694
- this.token = speculative.token;
683
+ this.dispatchEvent("solve", { token: this.#speculative.token });
684
+ this.token = this.#speculative.token;
695
685
 
696
- const expiresIn = speculative.tokenExpires - Date.now();
686
+ const expiresIn = this.#speculative.tokenExpires - Date.now();
697
687
  if (this.#resetTimer) clearTimeout(this.#resetTimer);
698
688
  this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
699
689
 
@@ -706,71 +696,74 @@
706
696
  );
707
697
  if (hasHaptics) navigator.vibrate([10, 50, 20, 30, 40]);
708
698
 
709
- _resetSpeculativeState();
699
+ this.#resetSpeculativeState();
710
700
  this.#solving = false;
711
- return;
701
+ return { success: true, token: this.token };
712
702
  }
713
703
 
714
- if (speculative.state === "done") {
715
- solutions = speculative.results;
716
- challengeResp = speculative.challengeResp;
704
+ if (this.#speculative.state === "done") {
705
+ solutions = this.#speculative.results;
706
+ challengeResp = this.#speculative.challengeResp;
717
707
  this.dispatchEvent("progress", { progress: 100 });
718
708
  } else if (
719
- speculative.state === "solving" ||
720
- speculative.state === "redeeming" ||
721
- speculative.state === "fetching" ||
722
- speculative.state === "waiting"
709
+ this.#speculative.state === "solving" ||
710
+ this.#speculative.state === "redeeming" ||
711
+ this.#speculative.state === "fetching" ||
712
+ this.#speculative.state === "waiting"
723
713
  ) {
724
- if (speculative.state === "waiting") {
725
- if (_speculativeTimer) {
726
- clearTimeout(_speculativeTimer);
727
- _speculativeTimer = null;
714
+ if (this.#speculative.state === "waiting") {
715
+ if (this.#speculativeTimer) {
716
+ clearTimeout(this.#speculativeTimer);
717
+ this.#speculativeTimer = null;
728
718
  }
729
- speculative.state = "waiting";
730
- _beginSpeculativeSolve();
719
+ this.#speculative.state = "waiting";
720
+ this.#beginSpeculativeSolve();
731
721
  }
732
722
 
733
- speculative.pendingPromotion = this.#workersCount;
734
- if (speculative.promoteFn) {
735
- speculative.promoteFn(this.#workersCount);
723
+ this.#speculative.pendingPromotion = this.#workersCount;
724
+ if (this.#speculative.promoteFn) {
725
+ this.#speculative.promoteFn(this.#workersCount);
736
726
  }
737
727
 
738
728
  const progressInterval = setInterval(() => {
739
- if (speculative.state !== "solving" && speculative.state !== "redeeming") {
729
+ if (
730
+ this.#speculative.state !== "solving" &&
731
+ this.#speculative.state !== "redeeming"
732
+ ) {
740
733
  clearInterval(progressInterval);
741
734
  return;
742
735
  }
743
- const total = speculative.challenges ? speculative.challenges.length : 1;
744
- const done = speculative.completedCount;
736
+ const total = this.#speculative.challenges ? this.#speculative.challenges.length : 1;
737
+ const done = this.#speculative.completedCount;
745
738
  const visual =
746
- speculative.state === "redeeming"
739
+ this.#speculative.state === "redeeming"
747
740
  ? 99
748
741
  : Math.min(98, Math.round((done / total) * 100));
749
742
  this.dispatchEvent("progress", { progress: visual });
750
743
  }, 150);
751
744
 
752
- await new Promise((resolve) => speculative.onSettled(resolve));
745
+ await new Promise((resolve) => this.#speculative.onSettled(resolve));
753
746
  clearInterval(progressInterval);
754
747
 
755
- if (speculative.state !== "done") {
748
+ if (this.#speculative.state !== "done") {
756
749
  throw new Error("Speculative solve failed – please try again");
757
750
  }
758
751
 
759
752
  if (
760
- speculative.token &&
761
- speculative.tokenExpires &&
762
- Date.now() < speculative.tokenExpires
753
+ this.#speculative.token &&
754
+ this.#speculative.tokenExpires &&
755
+ Date.now() < this.#speculative.tokenExpires
763
756
  ) {
764
757
  this.dispatchEvent("progress", { progress: 100 });
765
758
 
766
759
  const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
767
760
  if (this.querySelector(`input[name='${fieldName}']`)) {
768
- this.querySelector(`input[name='${fieldName}']`).value = speculative.token;
761
+ this.querySelector(`input[name='${fieldName}']`).value = this.#speculative.token;
769
762
  }
770
- this.dispatchEvent("solve", { token: speculative.token });
771
- this.token = speculative.token;
763
+ this.dispatchEvent("solve", { token: this.#speculative.token });
764
+ this.token = this.#speculative.token;
772
765
 
773
- const expiresIn = speculative.tokenExpires - Date.now();
766
+ const expiresIn = this.#speculative.tokenExpires - Date.now();
774
767
  if (this.#resetTimer) clearTimeout(this.#resetTimer);
775
768
  this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
776
769
 
@@ -783,13 +776,13 @@
783
776
  );
784
777
  if (hasHaptics) navigator.vibrate([10, 50, 20, 30, 40]);
785
778
 
786
- _resetSpeculativeState();
779
+ this.#resetSpeculativeState();
787
780
  this.#solving = false;
788
- return;
781
+ return { success: true, token: this.token };
789
782
  }
790
783
 
791
- solutions = speculative.results;
792
- challengeResp = speculative.challengeResp;
784
+ solutions = this.#speculative.results;
785
+ challengeResp = this.#speculative.challengeResp;
793
786
  this.dispatchEvent("progress", { progress: 100 });
794
787
  } else {
795
788
  const challengeRaw = await capFetch(`${apiEndpoint}challenge`, {
@@ -871,7 +864,7 @@
871
864
  this.dispatchEvent("solve", { token: resp.token });
872
865
  this.token = resp.token;
873
866
 
874
- _resetSpeculativeState();
867
+ this.#resetSpeculativeState();
875
868
 
876
869
  if (this.#resetTimer) clearTimeout(this.#resetTimer);
877
870
  const expiresIn = new Date(resp.expires).getTime() - Date.now();
@@ -1010,6 +1003,11 @@
1010
1003
  this.#div.addEventListener("click", () => {
1011
1004
  if (!this.#div.hasAttribute("disabled")) this.solve();
1012
1005
  });
1006
+ this.#div.addEventListener("mousedown", () => {
1007
+ if (!this.#div.hasAttribute("disabled") && hasHaptics) {
1008
+ navigator.vibrate(5);
1009
+ }
1010
+ });
1013
1011
 
1014
1012
  this.#div.addEventListener("keydown", (e) => {
1015
1013
  if ((e.key === "Enter" || e.key === " ") && !this.#div.hasAttribute("disabled")) {
@@ -1109,7 +1107,6 @@
1109
1107
 
1110
1108
  const link = next.querySelector(".cap-troubleshoot-link");
1111
1109
  if (link) {
1112
- console.log("linkblud");
1113
1110
  link.addEventListener("click", (e) => {
1114
1111
  e.stopPropagation();
1115
1112
  });
@@ -1223,6 +1220,21 @@
1223
1220
  clearTimeout(this.#resetTimer);
1224
1221
  this.#resetTimer = null;
1225
1222
  }
1223
+
1224
+ this.#detachInteractionListeners();
1225
+ if (this.#speculativeTimer) {
1226
+ clearTimeout(this.#speculativeTimer);
1227
+ this.#speculativeTimer = null;
1228
+ }
1229
+ if (this.#speculativePool) {
1230
+ this.#speculativePool.terminate();
1231
+ this.#speculativePool = null;
1232
+ }
1233
+ if (this.#speculative) {
1234
+ this.#speculative.state = "error";
1235
+ this.#speculative.notify();
1236
+ this.#speculative = null;
1237
+ }
1226
1238
  }
1227
1239
  }
1228
1240