@cap.js/widget 0.0.8 → 0.0.10

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 (4) hide show
  1. package/LICENSE +15 -0
  2. package/cap.min.js +1 -1
  3. package/package.json +2 -2
  4. package/src/cap.js +181 -210
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ Cap — A modern, lightning-quick PoW captcha
2
+ Copyright © 2025 Tiago Rangel
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU Affero General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU Affero General Public License for more details.
13
+
14
+ You should have received a copy of the GNU Affero General Public License
15
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
package/cap.min.js CHANGED
@@ -1 +1 @@
1
- "use strict";!function(){let e;const t=(e,t=1e4)=>new Promise(((r,a)=>{const s=setTimeout((()=>{a(new Error("Initialize timeout"))}),t),i=()=>{e()?(clearTimeout(s),r()):setTimeout(i,500)};i()}));class r{#e="";#t=null;#r=null;#a=navigator.hardwareConcurrency||8;#s=null;async initialize(){this.#e&&URL.revokeObjectURL(this.#e);try{await t((()=>!!e)),this.#e=URL.createObjectURL(new Blob([e],{type:"application/javascript"}))}catch(e){throw this.error("Failed to initialize worker"),e}}async solve(){await t((()=>!!this.#e)),this.dispatchEvent("progress",{progress:0});try{const e=this.#t.getAttribute("data-cap-api-endpoint");if(!e)throw new Error("Missing API endpoint");const{challenge:t,target:r,token:a}=await(await fetch(`${e}challenge`,{method:"POST"})).json(),s=await this.solveChallenges({challenge:t,target:r,token:a}),i=await(await fetch(`${e}redeem`,{method:"POST",body:JSON.stringify({token:a,solutions:s}),headers:{"Content-Type":"application/json"}})).json();if(!i.success)throw new Error("Invalid solution");this.dispatchEvent("progress",{progress:100}),this.dispatchEvent("solve",{token:i.token}),this.#s=i.token,this.#t.querySelector("input[name='cap-token']")&&(this.#t.querySelector("input[name='cap-token']").value=i.token),this.#r&&clearTimeout(this.#r);const n=new Date(i.expires).getTime()-Date.now();return n>0&&n<864e5?this.#r=setTimeout((()=>this.reset()),n):this.error("Invalid expiration time"),{success:!0,token:this.#s}}catch(e){throw this.error(e.message),e}}async solveChallenges({challenge:e,target:t}){const r=e.length;let a=0;const s=Array(this.#a).fill(null).map((()=>new Worker(this.#e))),i=([e,t],i)=>new Promise(((n,o)=>{const c=s[i],d=setTimeout((()=>{c.terminate(),s[i]=new Worker(this.#e),o(new Error("Worker timeout"))}),3e4);c.onmessage=({data:s})=>{s.found&&(clearTimeout(d),a++,this.dispatchEvent("progress",{progress:Math.round(a/r*100)}),n([e,t,s.nonce]))},c.onerror=e=>{clearTimeout(d),this.error(`Error in worker: ${e}`),o(e)},c.postMessage({salt:e,target:t})})),n=[];try{for(let t=0;t<e.length;t+=this.#a){const r=e.slice(t,Math.min(t+this.#a,e.length)),a=await Promise.all(r.map(((e,t)=>i(e,t))));n.push(...a)}}finally{s.forEach((e=>e.terminate()))}return n}reset(){this.#r&&(clearTimeout(this.#r),this.#r=null),this.dispatchEvent("reset"),this.#s=null,this.#t.querySelector("input[name='cap-token']")&&(this.#t.querySelector("input[name='cap-token']").value="")}error(e="Unknown error"){console.error("[Cap] Error:",e),this.dispatchEvent("error",{isCap:!0,message:e})}dispatchEvent(e,t={}){const r=new CustomEvent(e,{bubbles:!0,composed:!0,detail:t});this.#t.dispatchEvent(r)}setElement(e){this.#t=e}setWorkersCount(e){const t=parseInt(e,10),r=Math.min(navigator.hardwareConcurrency||8,16);this.#a=!isNaN(t)&&t>0&&t<=r?t:navigator.hardwareConcurrency||8}getToken(){return this.#s}cleanup(){this.#r&&(clearTimeout(this.#r),this.#r=null),this.#e&&(URL.revokeObjectURL(this.#e),this.#e="")}}class a extends HTMLElement{#i=new r;#n;#o;#c;#d=!1;eventHandlers;static get observedAttributes(){return["onsolve","onprogress","onreset","onerror","workers"]}constructor(){super(),this.eventHandlers&&this.eventHandlers.forEach(((e,t)=>{this.removeEventListener(t.slice(2),e)})),this.eventHandlers=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)}attributeChangedCallback(e,t,r){if(e.startsWith("on")){const t=e.slice(2),a=this.eventHandlers.get(e);if(a&&this.removeEventListener(t,a),r){const r=t=>{const r=this.getAttribute(e);"function"==typeof window[r]&&window[r].call(this,t)};this.eventHandlers.set(e,r),this.addEventListener(t,r)}}}async connectedCallback(){this.#c=this,this.#n=this.attachShadow({mode:"open"}),this.#i.setElement(this),this.#o=document.createElement("div"),this.createUI(),this.addEventListeners(),await this.#i.initialize(),this.#o.removeAttribute("disabled");const e=this.getAttribute("data-cap-worker-count");this.#i.setWorkersCount(parseInt(e)?parseInt(e,10):navigator.hardwareConcurrency||8),this.#c.innerHTML='<input type="hidden" name="cap-token">'}createUI(){this.#o.classList.add("captcha"),this.#o.setAttribute("role","button"),this.#o.setAttribute("tabindex","0"),this.#o.setAttribute("disabled","true"),this.#o.innerHTML='<div class="checkbox"></div><p>I\'m a human</p><a href="#" class="credits" target="_blank"><span>Secured by&nbsp;</span>Cap</a>',this.#n.innerHTML='<style>.captcha{background-color:var(--cap-background);border:1px solid var(--cap-border-color);border-radius:var(--cap-border-radius);width:var(--cap-widget-width);display:flex;align-items:center;padding:var(--cap-widget-padding);gap:var(--cap-gap);cursor:pointer;transition:filter var(--cap-transition-duration),transform var(--cap-transition-duration);position:relative;-webkit-tap-highlight-color:rgba(255,255,255,0);overflow:hidden;color:var(--cap-color)}.captcha:hover{filter:var(--cap-hover-filter)}.captcha:not([disabled]):active{transform:scale(var(--cap-active-scale))}.checkbox{width:var(--cap-checkbox-size);height:var(--cap-checkbox-size);border:var(--cap-checkbox-border);border-radius:var(--cap-checkbox-border-radius);background-color:var(--cap-checkbox-background);transition:opacity var(--cap-transition-duration);margin-top:var(--cap-checkbox-margin);margin-bottom:var(--cap-checkbox-margin)}.captcha *{font-family:var(--cap-font)}.captcha p{margin:0;font-weight:500;font-size:15px;user-select:none;transition:opacity var(--cap-transition-duration)}.captcha[data-state=verifying] .checkbox{background: none;display:flex;align-items:center;justify-content:center;transform: scale(1.1);border: none;border-radius: 50%;background: conic-gradient(var(--cap-spinner-color) 0%, var(--cap-spinner-color) var(--progress, 0%), var(--cap-spinner-background-color) var(--progress, 0%), var(--cap-spinner-background-color) 100%);position: relative;}.captcha[data-state=verifying] .checkbox::after {content: "";background-color: var(--cap-background);width: calc(100% - var(--cap-spinner-thickness));height: calc(100% - var(--cap-spinner-thickness));border-radius: 50%;margin:calc(var(--cap-spinner-thickness) / 2)}.captcha[data-state=done] .checkbox{border:1px solid transparent;background-image:var(--cap-checkmark);background-size:cover}.captcha[data-state=error] .checkbox{border:1px solid transparent;background-image:var(--cap-error-cross);background-size:cover}.captcha[disabled]{\ncursor:not-allowed}.captcha[disabled][data-state=verifying]{cursor:progress}.captcha[disabled][data-state=done]{cursor:default}.captcha .credits{position:absolute;bottom:10px;right:10px;font-size:var(--cap-credits-font-size);color:var(--cap-color);opacity:var(--cap-opacity-hover)}.captcha .credits span{display:none;text-decoration:underline}.captcha .credits:hover span{display:inline-block}</style>',this.#n.appendChild(this.#o)}addEventListeners(){this.#o.querySelector("a").addEventListener("click",(e=>{e.stopPropagation(),e.preventDefault(),window.open("#","_blank")})),this.#o.addEventListener("click",(()=>{this.#o.hasAttribute("disabled")||this.solve()})),this.addEventListener("progress",this.boundHandleProgress),this.addEventListener("solve",this.boundHandleSolve),this.addEventListener("error",this.boundHandleError),this.addEventListener("reset",this.boundHandleReset)}async solve(){if(!this.#d)try{this.#d=!0,this.updateUI("verifying","Verifying...",!0);return await this.#i.solve()}finally{this.#d=!1}}updateUI(e,t,r=!1){this.#o.setAttribute("data-state",e),this.#o.querySelector("p").innerText=t,r?this.#o.setAttribute("disabled","true"):this.#o.removeAttribute("disabled")}handleProgress(e){const t=this.#o.querySelector("p");t&&(this.#o.querySelector(".checkbox").style.setProperty("--progress",`${e.detail.progress}%`),t.innerText=`Verifying... ${e.detail.progress}%`),this.executeAttributeCode("onprogress",e)}handleSolve(e){this.updateUI("done","You're a human",!0),this.executeAttributeCode("onsolve",e)}handleError(e){this.updateUI("error","Error. Try again."),this.executeAttributeCode("onerror",e)}handleReset(e){this.updateUI("","I'm a human"),this.executeAttributeCode("onreset",e)}executeAttributeCode(e,t){const r=this.getAttribute(e);if(!r)return;new Function("event",r).call(this,t)}reset(){this.#i.reset()}get token(){return this.#i.getToken()}disconnectedCallback(){this.removeEventListener("progress",this.boundHandleProgress),this.removeEventListener("solve",this.boundHandleSolve),this.removeEventListener("error",this.boundHandleError),this.removeEventListener("reset",this.boundHandleReset),this.eventHandlers.forEach(((e,t)=>{this.removeEventListener(t.slice(2),e)})),this.eventHandlers.clear(),this.#n&&(this.#n.innerHTML=""),this.#i.reset(),this.#i.cleanup()}}class s{constructor(e,t={}){let a=new r,s=e||document.createElement("div");e||(s.style.display="none"),Object.entries(t).forEach((([e,t])=>{s.setAttribute(e,t)})),t.apiEndpoint&&s.setAttribute("data-cap-api-endpoint",t.apiEndpoint),a.setElement(s),a.setWorkersCount(t.workers||navigator.hardwareConcurrency||8),a.initialize(),this.solve=async function(){return await a.solve()},this.reset=function(){a.reset()},this.addEventListener=function(e,t){s.addEventListener(e,t)},Object.defineProperty(this,"token",{get:()=>a.getToken(),configurable:!0,enumerable:!0})}}const i=new CSSStyleSheet;i.replaceSync('html{--cap-font:system,-apple-system,"BlinkMacSystemFont",".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande","Ubuntu","arial",sans-serif;--cap-color:#212121;--cap-background:#fdfdfd;--cap-border-color:#dddddd8f;--cap-border-radius:14px;--cap-checkbox-border:1px solid #aaaaaad1;--cap-checkbox-border-radius:6px;--cap-checkbox-background:#fafafa91;--cap-widget-width:240px;--cap-widget-padding:14px;--cap-checkbox-size:24px;--cap-checkbox-margin:2px;--cap-transition-duration:0.2s;--cap-gap:15px;--cap-opacity-hover:0.8;--cap-hover-filter:brightness(97%);--cap-active-scale:0.98;--cap-credits-font-size:12px;--cap-spinner-color:black;--cap-spinner-background-color:#eee;--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/svg%3E");--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");--cap-spinner-thickness:5px;}'),document.adoptedStyleSheets.push(i);const n=function(){let e;self.onmessage=async({data:{salt:t,target:r}})=>{e||(e=await hashwasm.createSHA256());let a=0;let s=0;const i=new Uint8Array(128),n=new TextEncoder;for(;;)try{for(let s=0;s<5e4;s++){const s=t+a.toString(),o=n.encode(s);i.set(o),e.init(),e.update(i.subarray(0,o.length));if(e.digest("hex").startsWith(r))return void self.postMessage({nonce:a,found:!0});a++}s+=5e4,s>=5e5&&(self.postMessage({nonce:a,found:!1}),s=0)}catch(e){return void self.postMessage({found:!1,error:e.message})}}};setTimeout((async function(){e=await(await fetch("https://cdn.jsdelivr.net/npm/@cap.js/widget/wasm-hashes.min.js")).text()+n.toString().replace(/^function\s*\([^\)]*\)\s*{|\}$/g,"").trim()}),1),window.Cap=s,customElements.get("cap-widget")?console.warn("The cap-widget element has already been defined. Skipping re-defining it."):customElements.define("cap-widget",a),"object"==typeof exports&&"undefined"!=typeof module?module.exports=s:"function"==typeof define&&define.amd&&define([],(function(){return s})),"undefined"!=typeof exports&&(exports.default=s)}();
1
+ "use strict";!function(){let e;const t=(e,t=1e4)=>new Promise(((r,s)=>{const a=setTimeout((()=>{s(new Error("Initialize timeout"))}),t),i=()=>{e()?(clearTimeout(a),r()):setTimeout(i,500)};i()}));class r extends HTMLElement{#e="";#t=null;#r=navigator.hardwareConcurrency||8;#s=null;#a;#i;#n;#o=!1;#c;static get observedAttributes(){return["onsolve","onprogress","onreset","onerror","workers"]}constructor(){super(),this.#c&&this.#c.forEach(((e,t)=>{this.removeEventListener(t.slice(2),e)})),this.#c=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)}async initialize(){this.#e&&URL.revokeObjectURL(this.#e);try{await t((()=>!!e)),this.#e=URL.createObjectURL(new Blob([e],{type:"application/javascript"}))}catch(e){throw this.error("Failed to initialize worker"),e}}attributeChangedCallback(e,t,r){if(e.startsWith("on")){const t=e.slice(2),s=this.#c.get(e);if(s&&this.removeEventListener(t,s),r){const r=t=>{const r=this.getAttribute(e);"function"==typeof window[r]&&window[r].call(this,t)};this.#c.set(e,r),this.addEventListener(t,r)}}}async connectedCallback(){this.#n=this,this.#a=this.attachShadow({mode:"open"}),this.#i=document.createElement("div"),this.createUI(),this.addEventListeners(),await this.initialize(),this.#i.removeAttribute("disabled");const e=this.getAttribute("data-cap-worker-count");this.setWorkersCount(parseInt(e)?parseInt(e,10):navigator.hardwareConcurrency||8),this.#n.innerHTML='<input type="hidden" name="cap-token">'}async solve(){if(!this.#o)try{this.#o=!0,this.updateUI("verifying","Verifying...",!0),await t((()=>!!this.#e)),this.dispatchEvent("progress",{progress:0});try{const e=this.getAttribute("data-cap-api-endpoint");if(!e)throw new Error("Missing API endpoint");const{challenge:t,target:r,token:s}=await(await fetch(`${e}challenge`,{method:"POST"})).json(),a=await this.solveChallenges({challenge:t,target:r,token:s}),i=await(await fetch(`${e}redeem`,{method:"POST",body:JSON.stringify({token:s,solutions:a}),headers:{"Content-Type":"application/json"}})).json();if(!i.success)throw new Error("Invalid solution");this.dispatchEvent("progress",{progress:100}),this.dispatchEvent("solve",{token:i.token}),this.#s=i.token,this.querySelector("input[name='cap-token']")&&(this.querySelector("input[name='cap-token']").value=i.token),this.#t&&clearTimeout(this.#t);const n=new Date(i.expires).getTime()-Date.now();return n>0&&n<864e5?this.#t=setTimeout((()=>this.reset()),n):this.error("Invalid expiration time"),{success:!0,token:this.#s}}catch(e){throw this.error(e.message),e}}finally{this.#o=!1}}async solveChallenges({challenge:e,target:t}){const r=e.length;let s=0;const a=Array(this.#r).fill(null).map((()=>new Worker(this.#e))),i=([e,t],i)=>new Promise(((n,o)=>{const c=a[i],d=setTimeout((()=>{c.terminate(),a[i]=new Worker(this.#e),o(new Error("Worker timeout"))}),3e4);c.onmessage=({data:a})=>{a.found&&(clearTimeout(d),s++,this.dispatchEvent("progress",{progress:Math.round(s/r*100)}),n([e,t,a.nonce]))},c.onerror=e=>{clearTimeout(d),this.error(`Error in worker: ${e}`),o(e)},c.postMessage({salt:e,target:t})})),n=[];try{for(let t=0;t<e.length;t+=this.#r){const r=e.slice(t,Math.min(t+this.#r,e.length)),s=await Promise.all(r.map(((e,t)=>i(e,t))));n.push(...s)}}finally{a.forEach((e=>e.terminate()))}return n}setWorkersCount(e){const t=parseInt(e,10),r=Math.min(navigator.hardwareConcurrency||8,16);this.#r=!isNaN(t)&&t>0&&t<=r?t:navigator.hardwareConcurrency||8}createUI(){this.#i.classList.add("captcha"),this.#i.setAttribute("role","button"),this.#i.setAttribute("tabindex","0"),this.#i.setAttribute("disabled","true"),this.#i.innerHTML='<div class="checkbox"></div><p>I\'m a human</p><a href="#" class="credits" target="_blank"><span>Secured by&nbsp;</span>Cap</a>',this.#a.innerHTML='<style>.captcha{background-color:var(--cap-background);border:1px solid var(--cap-border-color);border-radius:var(--cap-border-radius);width:var(--cap-widget-width);display:flex;align-items:center;padding:var(--cap-widget-padding);gap:var(--cap-gap);cursor:pointer;transition:filter var(--cap-transition-duration),transform var(--cap-transition-duration);position:relative;-webkit-tap-highlight-color:rgba(255,255,255,0);overflow:hidden;color:var(--cap-color)}.captcha:hover{filter:var(--cap-hover-filter)}.captcha:not([disabled]):active{transform:scale(var(--cap-active-scale))}.checkbox{width:var(--cap-checkbox-size);height:var(--cap-checkbox-size);border:var(--cap-checkbox-border);border-radius:var(--cap-checkbox-border-radius);background-color:var(--cap-checkbox-background);transition:opacity var(--cap-transition-duration);margin-top:var(--cap-checkbox-margin);margin-bottom:var(--cap-checkbox-margin)}.captcha *{font-family:var(--cap-font)}.captcha p{margin:0;font-weight:500;font-size:15px;user-select:none;transition:opacity var(--cap-transition-duration)}.captcha[data-state=verifying] .checkbox{background: none;display:flex;align-items:center;justify-content:center;transform: scale(1.1);border: none;border-radius: 50%;background: conic-gradient(var(--cap-spinner-color) 0%, var(--cap-spinner-color) var(--progress, 0%), var(--cap-spinner-background-color) var(--progress, 0%), var(--cap-spinner-background-color) 100%);position: relative;}.captcha[data-state=verifying] .checkbox::after {content: "";background-color: var(--cap-background);width: calc(100% - var(--cap-spinner-thickness));height: calc(100% - var(--cap-spinner-thickness));border-radius: 50%;margin:calc(var(--cap-spinner-thickness) / 2)}.captcha[data-state=done] .checkbox{border:1px solid transparent;background-image:var(--cap-checkmark);background-size:cover}.captcha[data-state=error] .checkbox{border:1px solid transparent;background-image:var(--cap-error-cross);background-size:cover}.captcha[disabled]{cursor:not-allowed}.captcha[disabled][data-state=verifying]{cursor:progress}.captcha[disabled][data-state=done]{cursor:default}.captcha .credits{position:absolute;bottom:10px;right:10px;font-size:var(--cap-credits-font-size);color:var(--cap-color);opacity:var(--cap-opacity-hover)}.captcha .credits span{display:none;text-decoration:underline}.captcha .credits:hover span{display:inline-block}</style>',this.#a.appendChild(this.#i)}addEventListeners(){this.#i.querySelector("a").addEventListener("click",(e=>{e.stopPropagation(),e.preventDefault(),window.open("#","_blank")})),this.#i.addEventListener("click",(()=>{this.#i.hasAttribute("disabled")||this.solve()})),this.addEventListener("progress",this.boundHandleProgress),this.addEventListener("solve",this.boundHandleSolve),this.addEventListener("error",this.boundHandleError),this.addEventListener("reset",this.boundHandleReset)}updateUI(e,t,r=!1){this.#i.setAttribute("data-state",e),this.#i.querySelector("p").innerText=t,r?this.#i.setAttribute("disabled","true"):this.#i.removeAttribute("disabled")}handleProgress(e){const t=this.#i.querySelector("p");t&&(this.#i.querySelector(".checkbox").style.setProperty("--progress",`${e.detail.progress}%`),t.innerText=`Verifying... ${e.detail.progress}%`),this.executeAttributeCode("onprogress",e)}handleSolve(e){this.updateUI("done","You're a human",!0),this.executeAttributeCode("onsolve",e)}handleError(e){this.updateUI("error","Error. Try again."),this.executeAttributeCode("onerror",e)}handleReset(e){this.updateUI("","I'm a human"),this.executeAttributeCode("onreset",e)}executeAttributeCode(e,t){const r=this.getAttribute(e);if(!r)return;new Function("event",r).call(this,t)}error(e="Unknown error"){console.error("[Cap] Error:",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.#t&&(clearTimeout(this.#t),this.#t=null),this.dispatchEvent("reset"),this.#s=null,this.querySelector("input[name='cap-token']")&&(this.querySelector("input[name='cap-token']").value="")}get token(){return this.#s}disconnectedCallback(){this.removeEventListener("progress",this.boundHandleProgress),this.removeEventListener("solve",this.boundHandleSolve),this.removeEventListener("error",this.boundHandleError),this.removeEventListener("reset",this.boundHandleReset),this.#c.forEach(((e,t)=>{this.removeEventListener(t.slice(2),e)})),this.#c.clear(),this.#a&&(this.#a.innerHTML=""),this.reset(),this.cleanup()}cleanup(){this.#t&&(clearTimeout(this.#t),this.#t=null),this.#e&&(URL.revokeObjectURL(this.#e),this.#e="")}}class s{constructor(e={},t){let r=t||document.createElement("cap-widget");if(Object.entries(e).forEach((([e,t])=>{r.setAttribute(e,t)})),!e.apiEndpoint)throw r.remove(),new Error("Missing API endpoint");console.log(e),r.setAttribute("data-cap-api-endpoint",e.apiEndpoint),this.widget=r,this.solve=r.solve,this.reset=r.reset,this.addEventListener=r.addEventListener,Object.defineProperty(this,"token",{get:()=>r.getToken(),configurable:!0,enumerable:!0})}}const a=new CSSStyleSheet;a.replaceSync('html{--cap-font:system,-apple-system,"BlinkMacSystemFont",".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande","Ubuntu","arial",sans-serif;--cap-color:#212121;--cap-background:#fdfdfd;--cap-border-color:#dddddd8f;--cap-border-radius:14px;--cap-checkbox-border:1px solid #aaaaaad1;--cap-checkbox-border-radius:6px;--cap-checkbox-background:#fafafa91;--cap-widget-width:240px;--cap-widget-padding:14px;--cap-checkbox-size:24px;--cap-checkbox-margin:2px;--cap-transition-duration:0.2s;--cap-gap:15px;--cap-opacity-hover:0.8;--cap-hover-filter:brightness(97%);--cap-active-scale:0.98;--cap-credits-font-size:12px;--cap-spinner-color:black;--cap-spinner-background-color:#eee;--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/svg%3E");--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");--cap-spinner-thickness:5px;}'),document.adoptedStyleSheets.push(a);const i=function(){let e;self.onmessage=async({data:{salt:t,target:r}})=>{e||(e=await hashwasm.createSHA256());let s=0;let a=0;const i=new Uint8Array(128),n=new TextEncoder;for(;;)try{for(let a=0;a<5e4;a++){const a=t+s.toString(),o=n.encode(a);i.set(o),e.init(),e.update(i.subarray(0,o.length));if(e.digest("hex").startsWith(r))return void self.postMessage({nonce:s,found:!0});s++}a+=5e4,a>=5e5&&(self.postMessage({nonce:s,found:!1}),a=0)}catch(e){return void self.postMessage({found:!1,error:e.message})}}};setTimeout((async function(){e=await(await fetch("https://cdn.jsdelivr.net/npm/@cap.js/widget/wasm-hashes.min.js")).text()+i.toString().replace(/^function\s*\([^\)]*\)\s*{|\}$/g,"").trim()}),1),window.Cap=s,customElements.get("cap-widget")?console.warn("The cap-widget element has already been defined. Skipping re-defining it."):customElements.define("cap-widget",r),"object"==typeof exports&&"undefined"!=typeof module?module.exports=s:"function"==typeof define&&define.amd&&define([],(function(){return s})),"undefined"!=typeof exports&&(exports.default=s)}();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap.js/widget",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Cap widget",
5
5
  "keywords": [
6
6
  "captcha",
@@ -21,7 +21,7 @@
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/tiagorangel1/cap.git"
23
23
  },
24
- "license": "AGPL-3.0",
24
+ "license": "AGPL-3.0-only",
25
25
  "author": "Tiago Rangel",
26
26
  "type": "commonjs",
27
27
  "main": "cap.min.js",
package/src/cap.js CHANGED
@@ -19,12 +19,35 @@
19
19
  });
20
20
  };
21
21
 
22
- class CapBase {
22
+ class CapWidget extends HTMLElement {
23
23
  #workerUrl = "";
24
- #el = null;
25
24
  #resetTimer = null;
26
25
  #workersCount = navigator.hardwareConcurrency || 8;
27
26
  #token = null;
27
+ #shadow;
28
+ #div;
29
+ #host;
30
+ #solving = false;
31
+ #eventHandlers;
32
+
33
+ static get observedAttributes() {
34
+ return ["onsolve", "onprogress", "onreset", "onerror", "workers"];
35
+ }
36
+
37
+ constructor() {
38
+ super();
39
+ if (this.#eventHandlers) {
40
+ this.#eventHandlers.forEach((handler, eventName) => {
41
+ this.removeEventListener(eventName.slice(2), handler);
42
+ });
43
+ }
44
+
45
+ this.#eventHandlers = new Map();
46
+ this.boundHandleProgress = this.handleProgress.bind(this);
47
+ this.boundHandleSolve = this.handleSolve.bind(this);
48
+ this.boundHandleError = this.handleError.bind(this);
49
+ this.boundHandleReset = this.handleReset.bind(this);
50
+ }
28
51
 
29
52
  async initialize() {
30
53
  if (this.#workerUrl) {
@@ -44,60 +67,104 @@
44
67
  }
45
68
  }
46
69
 
70
+ attributeChangedCallback(name, oldValue, newValue) {
71
+ if (name.startsWith("on")) {
72
+ const eventName = name.slice(2);
73
+ const oldHandler = this.#eventHandlers.get(name);
74
+ if (oldHandler) {
75
+ this.removeEventListener(eventName, oldHandler);
76
+ }
77
+
78
+ if (newValue) {
79
+ const handler = (event) => {
80
+ const callback = this.getAttribute(name);
81
+ if (typeof window[callback] === "function") {
82
+ window[callback].call(this, event);
83
+ }
84
+ };
85
+ this.#eventHandlers.set(name, handler);
86
+ this.addEventListener(eventName, handler);
87
+ }
88
+ }
89
+ }
90
+
91
+ async connectedCallback() {
92
+ this.#host = this;
93
+ this.#shadow = this.attachShadow({ mode: "open" });
94
+ this.#div = document.createElement("div");
95
+ this.createUI();
96
+ this.addEventListeners();
97
+ await this.initialize();
98
+ this.#div.removeAttribute("disabled");
99
+
100
+ const workers = this.getAttribute("data-cap-worker-count");
101
+ this.setWorkersCount(
102
+ parseInt(workers)
103
+ ? parseInt(workers, 10)
104
+ : navigator.hardwareConcurrency || 8
105
+ );
106
+ this.#host.innerHTML = `<input type="hidden" name="cap-token">`;
107
+ }
108
+
47
109
  async solve() {
48
- await until(() => !!this.#workerUrl);
49
- this.dispatchEvent("progress", { progress: 0 });
110
+ if (this.#solving) {
111
+ return;
112
+ }
50
113
 
51
114
  try {
52
- const apiEndpoint = this.#el.getAttribute("data-cap-api-endpoint");
53
- if (!apiEndpoint) throw new Error("Missing API endpoint");
115
+ this.#solving = true;
116
+ this.updateUI("verifying", "Verifying...", true);
54
117
 
55
- const { challenge, target, token } = await (
56
- await fetch(
57
- `${apiEndpoint}challenge`,
58
- {
59
- method: "POST",
60
- }
61
- )
62
- ).json();
63
- const solutions = await this.solveChallenges({
64
- challenge,
65
- target,
66
- token,
67
- });
118
+ await until(() => !!this.#workerUrl);
119
+ this.dispatchEvent("progress", { progress: 0 });
120
+
121
+ try {
122
+ const apiEndpoint = this.getAttribute("data-cap-api-endpoint");
123
+ if (!apiEndpoint) throw new Error("Missing API endpoint");
68
124
 
69
- const resp = await (
70
- await fetch(
71
- `${apiEndpoint}redeem`,
72
- {
125
+ const { challenge, target, token } = await (
126
+ await fetch(`${apiEndpoint}challenge`, {
127
+ method: "POST",
128
+ })
129
+ ).json();
130
+ const solutions = await this.solveChallenges({
131
+ challenge,
132
+ target,
133
+ token,
134
+ });
135
+
136
+ const resp = await (
137
+ await fetch(`${apiEndpoint}redeem`, {
73
138
  method: "POST",
74
139
  body: JSON.stringify({ token, solutions }),
75
140
  headers: { "Content-Type": "application/json" },
76
- }
77
- )
78
- ).json();
141
+ })
142
+ ).json();
79
143
 
80
- if (!resp.success) throw new Error("Invalid solution");
144
+ if (!resp.success) throw new Error("Invalid solution");
81
145
 
82
- this.dispatchEvent("progress", { progress: 100 });
83
- this.dispatchEvent("solve", { token: resp.token });
84
- this.#token = resp.token;
85
- if (this.#el.querySelector("input[name='cap-token']")) {
86
- this.#el.querySelector("input[name='cap-token']").value = resp.token;
87
- }
146
+ this.dispatchEvent("progress", { progress: 100 });
147
+ this.dispatchEvent("solve", { token: resp.token });
148
+ this.#token = resp.token;
149
+ if (this.querySelector("input[name='cap-token']")) {
150
+ this.querySelector("input[name='cap-token']").value = resp.token;
151
+ }
88
152
 
89
- if (this.#resetTimer) clearTimeout(this.#resetTimer);
90
- const expiresIn = new Date(resp.expires).getTime() - Date.now();
91
- if (expiresIn > 0 && expiresIn < 24 * 60 * 60 * 1000) {
92
- this.#resetTimer = setTimeout(() => this.reset(), expiresIn); // 24h
93
- } else {
94
- this.error("Invalid expiration time");
95
- }
153
+ if (this.#resetTimer) clearTimeout(this.#resetTimer);
154
+ const expiresIn = new Date(resp.expires).getTime() - Date.now();
155
+ if (expiresIn > 0 && expiresIn < 24 * 60 * 60 * 1000) {
156
+ this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
157
+ } else {
158
+ this.error("Invalid expiration time");
159
+ }
96
160
 
97
- return { success: true, token: this.#token };
98
- } catch (err) {
99
- this.error(err.message);
100
- throw err;
161
+ return { success: true, token: this.#token };
162
+ } catch (err) {
163
+ this.error(err.message);
164
+ throw err;
165
+ }
166
+ } finally {
167
+ this.#solving = false;
101
168
  }
102
169
  }
103
170
 
@@ -156,36 +223,6 @@
156
223
  return results;
157
224
  }
158
225
 
159
- reset() {
160
- if (this.#resetTimer) {
161
- clearTimeout(this.#resetTimer);
162
- this.#resetTimer = null;
163
- }
164
- this.dispatchEvent("reset");
165
- this.#token = null;
166
- if (this.#el.querySelector("input[name='cap-token']")) {
167
- this.#el.querySelector("input[name='cap-token']").value = "";
168
- }
169
- }
170
-
171
- error(message = "Unknown error") {
172
- console.error("[Cap] Error:", message);
173
- this.dispatchEvent("error", { isCap: true, message });
174
- }
175
-
176
- dispatchEvent(eventName, detail = {}) {
177
- const event = new CustomEvent(eventName, {
178
- bubbles: true,
179
- composed: true,
180
- detail,
181
- });
182
- this.#el.dispatchEvent(event);
183
- }
184
-
185
- setElement(el) {
186
- this.#el = el;
187
- }
188
-
189
226
  setWorkersCount(workers) {
190
227
  const parsedWorkers = parseInt(workers, 10);
191
228
  const maxWorkers = Math.min(navigator.hardwareConcurrency || 8, 16);
@@ -197,89 +234,6 @@
197
234
  : navigator.hardwareConcurrency || 8;
198
235
  }
199
236
 
200
- getToken() {
201
- return this.#token;
202
- }
203
-
204
- cleanup() {
205
- if (this.#resetTimer) {
206
- clearTimeout(this.#resetTimer);
207
- this.#resetTimer = null;
208
- }
209
-
210
- if (this.#workerUrl) {
211
- URL.revokeObjectURL(this.#workerUrl);
212
- this.#workerUrl = "";
213
- }
214
- }
215
- }
216
-
217
- class CapWidget extends HTMLElement {
218
- #capBase = new CapBase();
219
- #shadow;
220
- #div;
221
- #host;
222
- #solving = false;
223
- eventHandlers;
224
-
225
- static get observedAttributes() {
226
- return ["onsolve", "onprogress", "onreset", "onerror", "workers"];
227
- }
228
-
229
- constructor() {
230
- super();
231
- if (this.eventHandlers) {
232
- this.eventHandlers.forEach((handler, eventName) => {
233
- this.removeEventListener(eventName.slice(2), handler);
234
- });
235
- }
236
- this.eventHandlers = new Map();
237
- this.boundHandleProgress = this.handleProgress.bind(this);
238
- this.boundHandleSolve = this.handleSolve.bind(this);
239
- this.boundHandleError = this.handleError.bind(this);
240
- this.boundHandleReset = this.handleReset.bind(this);
241
- }
242
-
243
- attributeChangedCallback(name, oldValue, newValue) {
244
- if (name.startsWith("on")) {
245
- const eventName = name.slice(2);
246
- const oldHandler = this.eventHandlers.get(name);
247
- if (oldHandler) {
248
- this.removeEventListener(eventName, oldHandler);
249
- }
250
-
251
- if (newValue) {
252
- const handler = (event) => {
253
- const callback = this.getAttribute(name);
254
- if (typeof window[callback] === "function") {
255
- window[callback].call(this, event);
256
- }
257
- };
258
- this.eventHandlers.set(name, handler);
259
- this.addEventListener(eventName, handler);
260
- }
261
- }
262
- }
263
-
264
- async connectedCallback() {
265
- this.#host = this;
266
- this.#shadow = this.attachShadow({ mode: "open" });
267
- this.#capBase.setElement(this);
268
- this.#div = document.createElement("div");
269
- this.createUI();
270
- this.addEventListeners();
271
- await this.#capBase.initialize();
272
- this.#div.removeAttribute("disabled");
273
- const workers = this.getAttribute("data-cap-worker-count");
274
-
275
- this.#capBase.setWorkersCount(
276
- parseInt(workers)
277
- ? parseInt(workers, 10)
278
- : navigator.hardwareConcurrency || 8
279
- );
280
- this.#host.innerHTML = `<input type="hidden" name="cap-token">`;
281
- }
282
-
283
237
  createUI() {
284
238
  this.#div.classList.add("captcha");
285
239
  this.#div.setAttribute("role", "button");
@@ -309,29 +263,14 @@
309
263
  this.addEventListener("reset", this.boundHandleReset);
310
264
  }
311
265
 
312
- async solve() {
313
- if (this.#solving) {
314
- return;
315
- }
316
-
317
- try {
318
- this.#solving = true;
319
- this.updateUI("verifying", "Verifying...", true);
320
- const result = await this.#capBase.solve();
321
- return result;
322
- } finally {
323
- this.#solving = false;
324
- }
325
- }
326
-
327
266
  updateUI(state, text, disabled = false) {
328
- this.#div.setAttribute("data-state", state);
329
- this.#div.querySelector("p").innerText = text;
330
- if (disabled) {
331
- this.#div.setAttribute("disabled", "true");
332
- } else {
333
- this.#div.removeAttribute("disabled");
334
- }
267
+ this.#div.setAttribute("data-state", state);
268
+ this.#div.querySelector("p").innerText = text;
269
+ if (disabled) {
270
+ this.#div.setAttribute("disabled", "true");
271
+ } else {
272
+ this.#div.removeAttribute("disabled");
273
+ }
335
274
  }
336
275
 
337
276
  handleProgress(event) {
@@ -369,12 +308,34 @@
369
308
  func.call(this, event);
370
309
  }
371
310
 
311
+ error(message = "Unknown error") {
312
+ console.error("[Cap] Error:", message);
313
+ this.dispatchEvent("error", { isCap: true, message });
314
+ }
315
+
316
+ dispatchEvent(eventName, detail = {}) {
317
+ const event = new CustomEvent(eventName, {
318
+ bubbles: true,
319
+ composed: true,
320
+ detail,
321
+ });
322
+ super.dispatchEvent(event);
323
+ }
324
+
372
325
  reset() {
373
- this.#capBase.reset();
326
+ if (this.#resetTimer) {
327
+ clearTimeout(this.#resetTimer);
328
+ this.#resetTimer = null;
329
+ }
330
+ this.dispatchEvent("reset");
331
+ this.#token = null;
332
+ if (this.querySelector("input[name='cap-token']")) {
333
+ this.querySelector("input[name='cap-token']").value = "";
334
+ }
374
335
  }
375
336
 
376
337
  get token() {
377
- return this.#capBase.getToken();
338
+ return this.#token;
378
339
  }
379
340
 
380
341
  disconnectedCallback() {
@@ -383,57 +344,63 @@
383
344
  this.removeEventListener("error", this.boundHandleError);
384
345
  this.removeEventListener("reset", this.boundHandleReset);
385
346
 
386
- this.eventHandlers.forEach((handler, eventName) => {
347
+ this.#eventHandlers.forEach((handler, eventName) => {
387
348
  this.removeEventListener(eventName.slice(2), handler);
388
349
  });
389
- this.eventHandlers.clear();
350
+ this.#eventHandlers.clear();
390
351
 
391
352
  if (this.#shadow) {
392
353
  this.#shadow.innerHTML = "";
393
354
  }
394
355
 
395
- this.#capBase.reset();
396
- this.#capBase.cleanup();
356
+ this.reset();
357
+ this.cleanup();
358
+ }
359
+
360
+ cleanup() {
361
+ if (this.#resetTimer) {
362
+ clearTimeout(this.#resetTimer);
363
+ this.#resetTimer = null;
364
+ }
365
+
366
+ if (this.#workerUrl) {
367
+ URL.revokeObjectURL(this.#workerUrl);
368
+ this.#workerUrl = "";
369
+ }
397
370
  }
398
371
  }
399
372
 
400
373
  class Cap {
401
- constructor(el, config = {}) {
402
- let capBase = new CapBase();
403
- let element = el || document.createElement("div");
374
+ constructor(config = {}, el) {
375
+ let widget = el || document.createElement("cap-widget");
404
376
 
405
- if (!el) element.style.display = "none";
406
377
  Object.entries(config).forEach(([a, b]) => {
407
- element.setAttribute(a, b);
378
+ widget.setAttribute(a, b);
408
379
  });
409
380
 
410
381
  if (config.apiEndpoint) {
411
- element.setAttribute("data-cap-api-endpoint", config.apiEndpoint);
382
+ console.log(config);
383
+ widget.setAttribute("data-cap-api-endpoint", config.apiEndpoint);
384
+ } else {
385
+ widget.remove();
386
+ throw new Error("Missing API endpoint");
412
387
  }
413
388
 
414
- capBase.setElement(element);
415
- capBase.setWorkersCount(
416
- config.workers || navigator.hardwareConcurrency || 8
417
- );
418
- capBase.initialize();
419
-
420
- this.solve = async function () {
421
- return await capBase.solve();
422
- };
423
-
424
- this.reset = function () {
425
- capBase.reset();
426
- };
427
-
428
- this.addEventListener = function (event, callback) {
429
- element.addEventListener(event, callback);
430
- };
389
+ this.widget = widget;
390
+ this.solve = this.widget.solve.bind(this.widget);
391
+ this.reset = this.widget.reset.bind(this.widget);
392
+ this.addEventListener = this.widget.addEventListener.bind(this.widget);
431
393
 
432
394
  Object.defineProperty(this, "token", {
433
- get: () => capBase.getToken(),
395
+ get: () => widget.getToken(),
434
396
  configurable: true,
435
397
  enumerable: true,
436
398
  });
399
+
400
+ if (!el) {
401
+ widget.style.display = "none";
402
+ document.documentElement.appendChild(widget);
403
+ }
437
404
  }
438
405
  }
439
406
 
@@ -493,7 +460,11 @@
493
460
 
494
461
  setTimeout(async function () {
495
462
  workerScript =
496
- (await (await fetch("https://cdn.jsdelivr.net/npm/@cap.js/widget/wasm-hashes.min.js")).text()) +
463
+ (await (
464
+ await fetch(
465
+ "https://cdn.jsdelivr.net/npm/@cap.js/widget/wasm-hashes.min.js"
466
+ )
467
+ ).text()) +
497
468
  workerFunct
498
469
  .toString()
499
470
  .replace(/^function\s*\([^\)]*\)\s*{|\}$/g, "")