diff --git a/Assets/css/altcha.css b/Assets/css/altcha.css new file mode 100644 index 0000000..0e78306 --- /dev/null +++ b/Assets/css/altcha.css @@ -0,0 +1 @@ +:root{--altcha-border-color:var(--altcha-color-neutral);--altcha-border-width:1px;--altcha-border-radius:6px;--altcha-color-base:light-dark(oklch(100% 0.00011 271.152),oklch(20.904% 0.00002 271.152));--altcha-color-base-content:light-dark(oklch(20.904% 0.00002 271.152),oklch(100% 0.00011 271.152));--altcha-color-error:oklch(51.284% 0.20527 28.678);--altcha-color-error-content:oklch(100% 0.00011 271.152);--altcha-color-neutral:light-dark(oklch(83.591% 0.0001 271.152),oklch(46.04% 0.00005 271.152));--altcha-color-neutral-content:light-dark(oklch(46.76% 0.00005 271.152),oklch(100% 0.00011 271.152));--altcha-color-primary:oklch(40.279% 0.2449 268.131);--altcha-color-primary-content:oklch(100% 0.00011 271.152);--altcha-color-success:oklch(55.748% 0.18968 142.511);--altcha-color-success-content:oklch(100% 0.00011 271.152);--altcha-checkbox-border-color:light-dark(oklch(66.494% 0.00233 15.434),oklch(51.028% 0.00006 271.152));--altcha-checkbox-border-radius:5px;--altcha-checkbox-border-width:var(--altcha-border-width);--altcha-checkbox-outline:2px solid var(--altcha-checkbox-outline-color);--altcha-checkbox-outline-color:-webkit-focus-ring-color;--altcha-checkbox-outline-offset:2px;--altcha-checkbox-size:22px;--altcha-checkbox-transition-duration:var(--altcha-transition-duration);--altcha-input-background-color:var(--altcha-color-base);--altcha-input-border-radius:3px;--altcha-input-border-width:1px;--altcha-input-color:var(--altcha-color-base-content);--altcha-max-width:320px;--altcha-padding:0.75rem;--altcha-popover-arrow-size:6px;--altcha-popover-color:var(--altcha-border-color);--altcha-shadow:drop-shadow(3px 3px 6px oklch(0% 0 0/0.2));--altcha-spinner-color:var(--altcha-color-base-content);--altcha-switch-background-color:var(--altcha-color-neutral);--altcha-switch-border-radius:calc(infinity*1px);--altcha-switch-height:var(--altcha-checkbox-size);--altcha-switch-padding:0.25rem;--altcha-switch-width:calc(var(--altcha-checkbox-size)*1.75);--altcha-switch-toggle-border-radius:100%;--altcha-switch-toggle-color:var(--altcha-color-neutral-content);--altcha-switch-toggle-size:calc(var(--altcha-switch-height) - var(--altcha-switch-padding)*2);--altcha-transition-duration:0.6s;--altcha-z-index:99999999;--altcha-z-index-popover:999999999}@supports (-moz-appearance:none){:root{--altcha-checkbox-outline-color:var(--altcha-color-primary)}}.altcha{all:revert-layer;display:none;font-family:inherit;font-size:inherit;position:relative}.altcha[data-visible]{display:block}.altcha *,.altcha-popover,.altcha-popover *{all:revert-layer;box-sizing:border-box;font-family:inherit;font-size:inherit;line-height:1.25}.altcha a,.altcha-popover a{color:currentColor;text-decoration:none}.altcha a:hover,.altcha-popover a:hover{color:currentColor}.altcha-main{align-items:start;background-color:var(--altcha-color-base);border:var(--altcha-border-width,1px) solid var(--altcha-border-color);border-radius:var(--altcha-border-radius,0);color:var(--altcha-color-base-content);display:flex;flex-direction:column;gap:.5rem;justify-content:space-between;max-width:var(--altcha-max-width,100%);padding:var(--altcha-padding)}.altcha-main>*{display:flex;width:100%}.altcha-main>:first-child{flex-grow:1}.altcha-checkbox-wrap{align-items:center;display:flex;flex-direction:row;flex-grow:1;gap:.5rem}.altcha-checkbox-wrap>*{display:flex}.altcha-logo{opacity:.7}.altcha-footer{align-items:center;display:flex;flex-grow:1;font-size:.7rem;gap:.5rem;justify-content:flex-end;opacity:.7}.altcha-footer p{margin:0;padding:0}.altcha-error{font-size:.85rem}.altcha-button{align-items:center;background:var(--altcha-color-primary);border:var(--altcha-input-border-width) solid var(--altcha-color-primary);border-radius:var(--altcha-input-border-radius);color:var(--altcha-color-primary-content);cursor:pointer;display:flex;font-size:.9rem;gap:.5rem;padding:.35rem}.altcha-button:focus{border-color:var(--altcha-color-primary);outline:var(--altcha-checkbox-outline);outline-offset:var(--altcha-checkbox-outline-offset)}.altcha-button>.altcha-spinner,.altcha-button>svg{height:20px;width:20px}.altcha-button-secondary{background:transparent;border-color:var(--altcha-color-neutral);color:var(--altcha-color-neutral-content)}.altcha-input{background:var(--altcha-input-background-color);border:var(--altcha-input-border-width) solid var(--altcha-color-neutral);border-radius:var(--altcha-input-border-radius);color:var(--altcha-input-color);flex-grow:1;font-size:1rem;min-width:0;padding:.25rem;width:auto}.altcha-input:focus{border-color:var(--altcha-color-primary);outline:var(--altcha-checkbox-outline);outline-offset:var(--altcha-checkbox-outline-offset)}.altcha-spinner{animation:altcha-rotate .6s linear infinite;border:var(--altcha-checkbox-border-width) solid var(--altcha-spinner-color);border-bottom-color:transparent;border-radius:100%;border-right-color:transparent;opacity:.7}.altcha-popover{background-color:var(--altcha-color-base);border:var(--altcha-border-width) solid var(--altcha-border-color);border-radius:var(--altcha-border-radius);color:var(--altcha-color-base-content);filter:var(--altcha-shadow);left:calc(var(--altcha-padding)/2);max-width:calc(var(--altcha-max-width) - var(--altcha-padding));position:absolute;top:calc(var(--altcha-padding) + var(--altcha-checkbox-size) + var(--altcha-popover-arrow-size));z-index:var(--altcha-z-index-popover)}.altcha-popover-arrow{border-bottom-color:transparent;border:var(--altcha-popover-arrow-size) solid transparent;border-bottom:var(--altcha-popover-arrow-size) solid var(--altcha-popover-color);content:"";height:0;left:calc(var(--altcha-checkbox-size)/2);position:absolute;top:calc(var(--altcha-popover-arrow-size)*-2);width:0}.altcha-popover-content{max-height:100dvh;overflow:auto;padding:var(--altcha-padding)}.altcha-popover[data-top=true][data-display=standard]{bottom:calc(100% - var(--altcha-padding) + var(--altcha-popover-arrow-size));top:auto}.altcha-popover[data-top=true][data-display=standard] .altcha-popover-arrow{border-bottom-color:transparent;border-top-color:var(--altcha-popover-color);bottom:calc(var(--altcha-popover-arrow-size)*-2);top:auto}.altcha-popover[data-variant=error]{--altcha-popover-color:var(--altcha-color-error);background-color:var(--altcha-color-error);border-color:var(--altcha-color-error);color:var(--altcha-color-error-content)}.altcha-popover[data-variant=error] .altcha-popover-content{padding:calc(var(--altcha-padding)/1.5) var(--altcha-padding)}.altcha-popover[data-display=overlay]{animation:altcha-overlay-slidein .5s forwards;top:45%;transform:translate(-50%,-50%)}.altcha-popover[data-display=bottomsheet],.altcha-popover[data-display=overlay]{left:50%;position:fixed;width:var(--altcha-max-width);z-index:var(--altcha-z-index)}.altcha-popover[data-display=bottomsheet]{animation:altcha-bottomsheet-slideup .5s forwards;border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0;bottom:-100%;top:auto;transform:translate(-50%)}.altcha-popover[data-display=bottomsheet] .altcha-popover-content{padding-bottom:calc(var(--altcha-padding)*2)}.altcha-popover-backdrop{background:var(--altcha-color-base-content);bottom:0;left:0;opacity:.1;position:fixed;right:0;top:0;transition:opacity .5s;z-index:var(--altcha-z-index)}.altcha-popover-close{color:var(--altcha-color-base-content);cursor:pointer;display:inline-block;font-size:1rem;height:1.25rem;line-height:.95;position:absolute;right:0;text-align:center;text-shadow:0 0 1px var(--altcha-color-base);top:-1.5rem;width:1.25rem;z-index:var(--altcha-z-index)}[dir=rtl] .altcha-popover{left:auto;right:calc(var(--altcha-padding)/2)}[dir=rtl] .altcha-popover-arrow{left:auto;right:calc(var(--altcha-checkbox-size)/2)}[dir=rtl] .altcha-popover-close{left:0;right:auto}.altcha-popover[data-display=bottomsheet] .altcha-footer,.altcha-popover[data-display=overlay] .altcha-footer{align-items:center;gap:.5rem;justify-content:center;padding-top:1rem}.altcha-popover[data-display=bottomsheet] .altcha-footer svg,.altcha-popover[data-display=overlay] .altcha-footer svg{height:18px;vertical-align:middle;width:18px}.altcha-code-challenge>form{display:flex;flex-direction:column;gap:.5rem}.altcha-code-challenge-title{font-weight:600}.altcha-code-challenge-text{font-size:.85rem}.altcha-code-challenge-image{background:#fff;border:var(--altcha-input-border-width) solid var(--altcha-color-neutral);border-radius:var(--altcha-input-border-radius);height:50px;object-fit:contain}.altcha-code-challenge-row{display:flex;gap:.5rem}.altcha-code-challenge-buttons{align-items:center;display:flex;flex-direction:column;gap:.5rem;justify-content:space-between;margin-top:var(--altcha-padding)}.altcha-code-challenge-buttons button{justify-content:center;width:100%}.altcha-checkbox{position:relative}.altcha-checkbox,.altcha-checkbox input{cursor:pointer;height:var(--altcha-checkbox-size);width:var(--altcha-checkbox-size)}.altcha-checkbox input{appearance:none;background:var(--altcha-input-background-color);border:var(--altcha-checkbox-border-width,2px) solid var(--altcha-checkbox-border-color);border-radius:var(--altcha-checkbox-border-radius);left:0;margin:0;padding:0;position:absolute;top:0}@supports (hanging-punctuation:first) and (font:-apple-system-body) and (-webkit-appearance:none){.altcha-checkbox input:focus{outline-style:solid;outline-width:2px}}.altcha-checkbox input:before{background:var(--altcha-color-neutral);border-radius:var(--altcha-checkbox-border-radius);content:"";display:block;height:100%;transform:scale(0);width:100%}.altcha-checkbox input:checked{background-color:var(--altcha-color-success);border-color:var(--altcha-color-success)}.altcha-checkbox input:checked:before{background-color:var(--altcha-color-success);opacity:0;transform:scale(2.2);transition:all var(--altcha-checkbox-transition-duration) ease;transition-delay:.1s}.altcha-checkbox svg{--altcha-radio-svg-size:calc(var(--altcha-checkbox-size)*0.5);--altcha-radio-svg-offset:calc(var(--altcha-checkbox-size)*0.25);fill:none;height:var(--altcha-radio-svg-size);left:var(--altcha-radio-svg-offset);opacity:0;position:absolute;stroke:currentColor;stroke-dasharray:16px;stroke-dashoffset:16px;stroke-linecap:round;stroke-linejoin:round;stroke-width:2;top:var(--altcha-radio-svg-offset);transform:translateZ(0);width:var(--altcha-radio-svg-size)}.altcha-checkbox input:checked+svg{color:var(--altcha-color-success-content);opacity:1;stroke-dashoffset:0;transition:all var(--altcha-checkbox-transition-duration) ease;transition-delay:.1s}.altcha-checkbox-spinner{display:none;height:var(--altcha-checkbox-size);left:0;position:absolute;top:0;width:var(--altcha-checkbox-size)}.altcha-checkbox[data-loading=true] input{appearance:none;opacity:0;pointer-events:none}.altcha-checkbox[data-loading=true] .altcha-checkbox-spinner{display:block}.altcha-checkbox-native{position:relative}.altcha-checkbox-native,.altcha-checkbox-native input{height:var(--altcha-checkbox-size);width:var(--altcha-checkbox-size)}.altcha-checkbox-native input{margin:0}.altcha-checkbox-native-spinner{display:none;height:var(--altcha-checkbox-size);left:0;position:absolute;top:0;width:var(--altcha-checkbox-size)}.altcha-checkbox-native[data-loading=true] input{appearance:none;opacity:0;pointer-events:none}.altcha-checkbox-native[data-loading=true] .altcha-checkbox-native-spinner{display:block}.altcha-switch{align-items:center;background-color:var(--altcha-switch-background-color);border-radius:var(--altcha-switch-border-radius);display:flex;height:var(--altcha-switch-height);padding:var(--altcha-switch-padding);position:relative;width:var(--altcha-switch-width)}.altcha-switch:focus-within{outline:var(--altcha-checkbox-outline);outline-offset:var(--altcha-checkbox-outline-offset)}.altcha-switch input{appearance:none;cursor:pointer;height:100%;left:0;opacity:0;position:absolute;top:0;width:100%}.altcha-switch-toggle{align-items:center;background-color:var(--altcha-switch-toggle-color);border-radius:var(--altcha-switch-toggle-border-radius);cursor:pointer;display:flex;justify-content:center;left:var(--altcha-switch-padding);position:absolute;transition:width .15s ease-out,left .15s ease-out}.altcha-switch-spinner,.altcha-switch-toggle{height:var(--altcha-switch-toggle-size);width:var(--altcha-switch-toggle-size)}.altcha-switch-spinner{display:none}.altcha-switch[data-loading=true]{pointer-events:none}.altcha-switch[data-loading=true] .altcha-switch-spinner{display:block}.altcha-switch[data-loading=true] .altcha-switch-toggle{background-color:transparent;left:calc(50% - var(--altcha-switch-toggle-size)/2)}[data-state=verified] .altcha-switch{--altcha-switch-background-color:var(--altcha-color-success)}[data-state=verified] .altcha-switch-toggle{background-color:var(--altcha-color-success-content)}[data-state=verified] .altcha-switch-toggle,[dir=rtl] .altcha-switch-toggle{left:calc(100% - var(--altcha-switch-height) + var(--altcha-switch-padding))}[dir=rtl][data-state=verified] .altcha-switch-toggle{left:var(--altcha-switch-padding)}.altcha-floating-arrow{border:6px solid transparent;border-bottom:6px solid var(--altcha-border-color);content:"";height:0;left:12px;position:absolute;top:-12px;width:0}.altcha-overlay-backdrop{bottom:0;left:0;position:fixed;right:0;top:0;transition:opacity var(--altcha-transition-duration);z-index:var(--altcha-z-index)}.altcha-overlay-close{color:currentColor;cursor:pointer;display:inline-block;font-size:1rem;height:1rem;line-height:.85;position:absolute;right:0;text-align:center;text-shadow:0 0 1px var(--altcha-color-base);top:-1.5rem;width:1rem;z-index:var(--altcha-z-index)}.altcha[data-display=overlay]{animation:altcha-overlay-slidein var(--altcha-transition-duration) forwards;filter:var(--altcha-shadow);left:50%;opacity:0;position:fixed;top:45%;transform:translate(-50%,-50%);z-index:var(--altcha-z-index)}.altcha[data-display=overlay] .altcha-main{width:var(--altcha-max-width)}.altcha[data-display=floating]{display:none;filter:var(--altcha-shadow);left:var(--altcha-floating-left,-100%);position:fixed;top:var(--altcha-floating-top,-100%);z-index:var(--altcha-z-index)}.altcha[data-display=floating] .altcha-main{width:var(--altcha-max-width)}.altcha[data-display=floating][data-floating-position=top] .altcha-floating-arrow{border-bottom-color:transparent;border-top-color:var(--altcha-border-color);bottom:-12px;top:auto}.altcha[data-display=floating][data-visible]{display:flex}.altcha[data-display=bar]{bottom:-100%;filter:var(--altcha-shadow);left:0;position:fixed;right:0;transition:bottom var(--altcha-transition-duration),top var(--altcha-transition-duration);z-index:var(--altcha-z-index)}.altcha[data-display=bar] .altcha-main{align-items:center;border-radius:0;border-width:var(--altcha-border-width) 0 0 0;flex-direction:row;max-width:100%!important}.altcha[data-display=bar] .altcha-main>*{width:auto}.altcha[data-display=bar][data-placement=top]{bottom:auto;top:-100%}.altcha[data-display=bar][data-placement=top] .altcha-main{border-width:0 0 var(--altcha-border-width) 0}.altcha[data-display=bar][data-placement=bottom]:not([data-state=unverified]){bottom:0}.altcha[data-display=bar][data-placement=top]:not([data-state=unverified]){top:0}.altcha[data-display=invisible]{display:none}@keyframes altcha-rotate{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes altcha-bottomsheet-slideup{to{bottom:0}}@keyframes altcha-overlay-slidein{to{opacity:1;top:50%}} \ No newline at end of file diff --git a/Assets/img/altcha.png b/Assets/img/altcha.png new file mode 100644 index 0000000..5e57cf8 Binary files /dev/null and b/Assets/img/altcha.png differ diff --git a/Assets/js/altcha.min.js b/Assets/js/altcha.min.js new file mode 100644 index 0000000..e903280 --- /dev/null +++ b/Assets/js/altcha.min.js @@ -0,0 +1 @@ +const e=!1;var t=Array.isArray,n=Array.prototype.indexOf,r=Array.prototype.includes,a=Array.from,o=Object.keys,i=Object.defineProperty,l=Object.getOwnPropertyDescriptor,s=Object.getOwnPropertyDescriptors,c=Object.prototype,u=Array.prototype,f=Object.getPrototypeOf,d=Object.isExtensible;const h=()=>{};function p(){var e,t;return{promise:new Promise((n,r)=>{e=n,t=r}),resolve:e,reject:t}}const v=1<<24,g=16,m=32,b=64,y=512,w=1024,x=2048,k=4096,_=8192,$=16384,S=32768,C=1<<25,E=65536,T=1<<17,A=1<<19,I=65536,P=1<<21,O=1<<23,R=Symbol("$state"),L=Symbol("legacy props"),M=Symbol(""),D=Symbol("attributes"),U=Symbol("class"),V=Symbol("style"),N=Symbol("text"),z=Symbol("form reset"),j=new class extends Error{name="StaleReactionError";message="The reaction that called `getAbortSignal()` was re-run or destroyed"},B=!!globalThis.document?.contentType&&globalThis.document.contentType.includes("xml");function F(e){return e===this.v}function H(e,t){return e!=e?t==t:e!==t||null!==e&&"object"==typeof e||"function"==typeof e}function K(e){return!H(e,this.v)}const q={},Y=Symbol("uninitialized"),G="http://www.w3.org/1999/xhtml";let W=null;function J(e){W=e}function Z(e,t=!1,n){W={p:W,i:!1,c:null,e:null,s:e,x:null,r:jt,l:null}}function X(e){var t=W,n=t.e;if(null!==n)for(var r of(t.e=null,n))gn(r);return void 0!==e&&(t.x=e),t.i=!0,W=t.p,e??{}}let Q=[];function ee(){var e=Q;Q=[],function(e){for(var t=0;t{t===Q&&ee()})}Q.push(e)}function ne(){for(;Q.length>0;)ee()}function re(e){console.warn("https://svelte.dev/e/hydration_mismatch")}let ae,oe=!1;function ie(e){oe=e}function le(e){if(null===e)throw re(),q;return ae=e}function se(){return le($e(ae))}function ce(e){if(oe){if(null!==$e(ae))throw re(),q;ae=e}}function ue(e=1){if(oe){for(var t=e,n=ae;t--;)n=$e(n);ae=n}}function fe(e=!0){for(var t=0,n=ae;;){if(8===n.nodeType){var r=n.data;if("]"===r){if(0===t)return n;t-=1}else("["===r||"[!"===r||"["===r[0]&&!isNaN(Number(r.slice(1))))&&(t+=1)}var a=$e(n);e&&n.remove(),n=a}}function de(e){if(!e||8!==e.nodeType)throw re(),q;return e.data}function he(e){if("object"!=typeof e||null===e||R in e)return e;const n=f(e);if(n!==c&&n!==u)return e;var r=new Map,a=t(e),o=St(0),i=Jt,s=e=>{if(Jt===i)return e();var t=Vt,n=Jt;zt(null),Zt(i);var r=e();return zt(t),Zt(n),r};return a&&r.set("length",St(e.length)),new Proxy(e,{defineProperty(e,t,n){"value"in n&&!1!==n.configurable&&!1!==n.enumerable&&!1!==n.writable||function(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}();var a=r.get(t);return void 0===a?s(()=>{var e=St(n.value);return r.set(t,e),e}):Et(a,n.value,!0),!0},deleteProperty(e,t){var n=r.get(t);if(void 0===n){if(t in e){const e=s(()=>St(Y));r.set(t,e),At(o)}}else Et(n,Y),At(o);return!0},get(t,n,a){if(n===R)return e;var o=r.get(n),i=n in t;if(void 0!==o||i&&!l(t,n)?.writable||(o=s(()=>St(he(i?t[n]:Y))),r.set(n,o)),void 0!==o){var c=ln(o);return c===Y?void 0:c}return Reflect.get(t,n,a)},getOwnPropertyDescriptor(e,t){var n=Reflect.getOwnPropertyDescriptor(e,t);if(n&&"value"in n){var a=r.get(t);a&&(n.value=ln(a))}else if(void 0===n){var o=r.get(t),i=o?.v;if(void 0!==o&&i!==Y)return{enumerable:!0,configurable:!0,value:i,writable:!0}}return n},has(e,t){if(t===R)return!0;var n=r.get(t),a=void 0!==n&&n.v!==Y||Reflect.has(e,t);if((void 0!==n||null!==jt&&(!a||l(e,t)?.writable))&&(void 0===n&&(n=s(()=>St(a?he(e[t]):Y)),r.set(t,n)),ln(n)===Y))return!1;return a},set(e,t,n,i){var c=r.get(t),u=t in e;if(a&&"length"===t)for(var f=n;fSt(Y)),r.set(f+"",d))}void 0===c?u&&!l(e,t)?.writable||(Et(c=s(()=>St(void 0)),he(n)),r.set(t,c)):(u=c.v!==Y,Et(c,s(()=>he(n))));var h=Reflect.getOwnPropertyDescriptor(e,t);if(h?.set&&h.set.call(i,n),!u){if(a&&"string"==typeof t){var p=r.get("length"),v=Number(t);Number.isInteger(v)&&v>=p.v&&Et(p,v+1)}At(o)}return!0},ownKeys(e){ln(o);var t=Reflect.ownKeys(e).filter(e=>{var t=r.get(e);return void 0===t||t.v!==Y});for(var[n,a]of r)a.v===Y||n in e||t.push(n);return t},setPrototypeOf(){!function(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}()}})}function pe(e){try{if(null!==e&&"object"==typeof e&&R in e)return e[R]}catch{}return e}function ve(e,t){return Object.is(pe(e),pe(t))}var ge,me,be,ye,we;function xe(){if(void 0===ge){ge=window,me=document,be=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;ye=l(t,"firstChild").get,we=l(t,"nextSibling").get,d(e)&&(e[U]=void 0,e[D]=null,e[V]=void 0,e.__e=void 0),d(n)&&(n[N]=void 0)}}function ke(e=""){return document.createTextNode(e)}function _e(e){return ye.call(e)}function $e(e){return we.call(e)}function Se(e,t){if(!oe)return _e(e);var n=_e(ae);if(null===n)n=ae.appendChild(ke());else if(t&&3!==n.nodeType){var r=ke();return n?.before(r),le(r),r}return t&&Ae(n),le(n),n}function Ce(e,t=!1){if(!oe){var n=_e(e);return n instanceof Comment&&""===n.data?$e(n):n}if(t){if(3!==ae?.nodeType){var r=ke();return ae?.before(r),le(r),r}Ae(ae)}return ae}function Ee(e,t=1,n=!1){let r=oe?ae:e;for(var a;t--;)a=r,r=$e(r);if(!oe)return r;if(n){if(3!==r?.nodeType){var o=ke();return null===r?a?.after(o):r.before(o),le(o),o}Ae(r)}return le(r),r}function Te(e,t,n){let r;return document.createElementNS(t??G,e,r)}function Ae(e){if(e.nodeValue.length<65536)return;let t=e.nextSibling;for(;null!==t&&3===t.nodeType;)t.remove(),e.nodeValue+=t.nodeValue,t=e.nextSibling}function Ie(e){var t=jt;if(null===t)return Vt.f|=O,e;if(0===(t.f&S)&&!(4&t.f))throw e;Pe(e,t)}function Pe(e,t){for(;null!==t;){if(128&t.f){if(0===(t.f&S))throw e;try{return void t.b.error(e)}catch(t){e=t}}t=t.parent}throw e}const Oe=-7169;function Re(e,t){e.f=e.f&Oe|t}function Le(e){0!==(e.f&y)||null===e.deps?Re(e,w):Re(e,k)}function Me(e){if(null!==e)for(const t of e)2&t.f&&0!==(t.f&I)&&(t.f^=I,Me(t.deps))}function De(e,t,n){0!==(e.f&x)?t.add(e):0!==(e.f&k)&&n.add(e),Me(e.deps),Re(e,w)}function Ue(e,t,n){if(null==e)return t(void 0),h;const r=un(()=>e.subscribe(t,n));return r.unsubscribe?()=>r.unsubscribe():r}const Ve=[];function Ne(e,t=h){let n=null;const r=new Set;function a(t){if(H(e,t)&&(e=t,n)){const t=!Ve.length;for(const t of r)t[1](),Ve.push(t,e);if(t){for(let e=0;e{r.delete(s),0===r.size&&n&&(n(),n=null)}}}}function ze(e){let t;return Ue(e,e=>t=e)(),t}let je=Symbol("unmounted");function Be(e,t,n){const r=n[t]??={store:null,source:Ct(void 0),unsubscribe:h};if(r.store!==e&&!(je in n))if(r.unsubscribe(),r.store=e??null,null==e)r.source.v=void 0,r.unsubscribe=h;else{var a=!0;r.unsubscribe=Ue(e,e=>{a?r.source.v=e:Et(r.source,e)}),a=!1}return e&&je in n?ze(e):ln(r.source)}let Fe=null,He=null,Ke=null,qe=null,Ye=null,Ge=null,We=!1,Je=!1,Ze=null,Xe=null;var Qe=0;let et=1;class tt{id=et++;#e=!1;linked=!0;#t=null;#n=null;async_deriveds=new Map;current=new Map;previous=new Map;unblocked=new Set;#r=new Set;#a=new Set;#o=new Set;#i=0;#l=new Map;#s=null;#c=[];#u=[];#f=new Set;#d=new Set;#h=new Map;#p=new Set;is_fork=!1;#v=!1;#g(){if(this.is_fork)return!0;for(const n of this.#l.keys()){for(var e=n,t=!1;null!==e.parent;){if(this.#h.has(e)){t=!0;break}e=e.parent}if(!t)return!0}return!1}skip_effect(e){this.#h.has(e)||this.#h.set(e,{d:[],m:[]}),this.#p.delete(e)}unskip_effect(e,t=e=>this.schedule(e)){var n=this.#h.get(e);if(n){for(var r of(this.#h.delete(e),n.d))Re(r,x),t(r);for(r of n.m)Re(r,k),t(r)}this.#p.add(e)}#m(){if(this.#e=!0,Qe++>1e3&&(this.#b(),function(){try{!function(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}()}catch(e){Pe(e,Ge)}}()),!this.#g()){for(const e of this.#f)this.#d.delete(e),Re(e,x),this.schedule(e);for(const e of this.#d)Re(e,k),this.schedule(e)}const e=this.#c;this.#c=[],this.apply();var t=Ze=[],n=[],r=Xe=[];for(const r of e)try{this.#y(r,t,n)}catch(e){throw ct(r),e}if(Ke=null,r.length>0){var a=tt.ensure();for(const e of r)a.schedule(e)}if(Ze=null,Xe=null,this.#g()){this.#w(n),this.#w(t);for(const[e,t]of this.#h)st(e,t);return void(r.length>0&&Ke.#m())}const o=this.#x();if(o)o.#k(this);else{this.#f.clear(),this.#d.clear();for(const e of this.#r)e(this);this.#r.clear(),qe=this,at(n),at(t),qe=null,this.#s?.resolve();var i=Ke;if(this.linked&&0===this.#i&&this.#b(),this.#c.length>0){null===i&&(i=this,this.#_());const e=i;e.#c.push(...this.#c.filter(t=>!e.#c.includes(t)))}null!==i&&i.#m()}}#y(e,t,n){e.f^=w;for(var r=e.first;null!==r;){var a=r.f,o=!!(96&a);if(!(o&&0!==(a&w)||0!==(a&_)||this.#h.has(r))&&null!==r.fn){o?r.f^=w:4&a?t.push(r):Qt(r)&&(0!==(a&g)&&this.#d.add(r),an(r));var i=r.first;if(null!==i){r=i;continue}}for(;null!==r;){var l=r.next;if(null!==l){r=l;break}r=r.parent}}}#x(){for(var e=this.#t;null!==e;){if(!e.is_fork)for(const[t,[,n]]of this.current)if(e.current.has(t)&&!n)return e;e=e.#t}return null}#k(e){for(const[t,n]of e.current)!this.previous.has(t)&&e.previous.has(t)&&this.previous.set(t,e.previous.get(t)),this.current.set(t,n);for(const[t,n]of e.async_deriveds){const e=this.async_deriveds.get(t);e&&n.promise.then(e.resolve)}const t=e=>{var n=e.reactions;if(null!==n)for(const e of n){var r=e.f;if(2&r)t(e);else{var a=e;4194320&r&&!this.async_deriveds.has(a)&&(this.#d.delete(a),Re(a,x),this.schedule(a))}}};for(const e of this.current.keys())t(e);this.oncommit(()=>e.discard()),e.#b(),Ke=this,this.#m()}#w(e){for(var t=0;t!this.current.has(e));if(0===r.length)e&&c.discard();else if(t.length>0){if(e)for(const e of this.#p)c.unskip_effect(e,e=>{4194320&e.f?c.schedule(e):c.#w([e])});c.activate();var a=new Set,o=new Map;for(var i of t)ot(i,r,a,o);o=new Map;var l=[...c.current.keys()].filter(e=>!this.current.has(e)||this.current.get(e)[0]!==e.v);if(l.length>0)for(const e of this.#u)155648&e.f||!it(e,l,o)||(4194320&e.f?(Re(e,x),c.schedule(e)):c.#f.add(e));if(c.#c.length>0){for(var s of(c.apply(),c.#c))c.#y(s,[],[]);c.#c=[]}c.deactivate()}}}}increment(e,t){if(this.#i+=1,e){let e=this.#l.get(t)??0;this.#l.set(t,e+1)}}decrement(e,t){if(this.#i-=1,e){let e=this.#l.get(t)??0;1===e?this.#l.delete(t):this.#l.set(t,e-1)}this.#v||(this.#v=!0,te(()=>{this.#v=!1,this.linked&&this.flush()}))}transfer_effects(e,t){for(const t of e)this.#f.add(t);for(const e of t)this.#d.add(e);e.clear(),t.clear()}oncommit(e){this.#r.add(e)}ondiscard(e){this.#a.add(e)}on_fork_commit(e){this.#o.add(e)}run_fork_commit_callbacks(){for(const e of this.#o)e(this);this.#o.clear()}settled(){return(this.#s??=p()).promise}static ensure(){if(null===Ke){const e=Ke=new tt;e.#_(),Je||We||te(()=>{e.#e||e.flush()})}return Ke}apply(){Ye=null}schedule(e){if(Ge=e,e.b?.is_pending&&16777228&e.f&&0===(e.f&S))e.b.defer_effect(e);else{for(var t=e;null!==t.parent;){var n=(t=t.parent).f;if(!(null===Ze||t!==jt||null!==Vt&&2&Vt.f))return;if(96&n){if(0===(n&w))return;t.f^=w}}this.#c.push(t)}}#_(){null===He?Fe=He=this:(He.#n=this,this.#t=He),He=this}#b(){var e=this.#t,t=this.#n;null===e?Fe=t:e.#n=t,null===t?He=e:t.#t=e,this.linked=!1}}function nt(e){var t=We;We=!0;try{for(;;){if(ne(),null===Ke)return;Ke.flush()}}finally{We=t}}let rt=null;function at(e){var t=e.length;if(0!==t){for(var n=0;n0)){kt.clear();for(const e of rt){if(24576&e.f)continue;const t=[e];let n=e.parent;for(;null!==n;)rt.has(n)&&(rt.delete(n),t.push(n)),n=n.parent;for(let e=t.length-1;e>=0;e--){const n=t[e];24576&n.f||an(n)}}rt.clear()}}rt=null}}function ot(e,t,n,r){if(!n.has(e)&&(n.add(e),null!==e.reactions))for(const a of e.reactions){const e=a.f;2&e?ot(a,t,n,r):4194320&e&&0===(e&x)&&it(a,t,r)&&(Re(a,x),lt(a))}}function it(e,t,n){const a=n.get(e);if(void 0!==a)return a;if(null!==e.deps)for(const a of e.deps){if(r.call(t,a))return!0;if(2&a.f&&it(a,t,n))return n.set(a,!0),!0}return n.set(e,!1),!1}function lt(e){Ke.schedule(e)}function st(e,t){if(0===(e.f&m)||0===(e.f&w)){0!==(e.f&x)?t.d.push(e):0!==(e.f&k)&&t.m.push(e),Re(e,w);for(var n=e.first;null!==n;)st(n,t),n=n.next}}function ct(e){Re(e,w);for(var t=e.first;null!==t;)ct(t),t=t.next}class ut{parent;is_pending=!1;transform_error;#S;#C=oe?ae:null;#E;#T;#A;#I=null;#P=null;#O=null;#R=null;#L=0;#M=0;#D=!1;#f=new Set;#d=new Set;#U=null;#V=function(e){let t,n=0,r=$t(0);return()=>{hn()&&(ln(r),bn(()=>(0===n&&(t=un(()=>e(()=>At(r)))),n+=1,()=>{te(()=>{n-=1,0===n&&(t?.(),t=void 0,At(r))})})))}}(()=>(this.#U=$t(this.#L),()=>{this.#U=null}));constructor(e,t,n,r){this.#S=e,this.#E=t,this.#T=e=>{var t=jt;t.b=this,t.f|=128,n(e)},this.parent=jt.b,this.transform_error=r??this.parent?.transform_error??(e=>e),this.#A=wn(()=>{if(oe){const e=this.#C;se();const t="[!"===e.data;if(e.data.startsWith("[?")){const t=JSON.parse(e.data.slice(2));this.#N(t)}else t?this.#z():this.#j()}else this.#B()},589824),oe&&(this.#S=ae)}#j(){try{this.#I=kn(()=>this.#T(this.#S))}catch(e){this.error(e)}}#N(e){const t=this.#E.failed;t&&(this.#O=kn(()=>{t(this.#S,()=>e,()=>()=>{})}))}#z(){const e=this.#E.pending;e&&(this.is_pending=!0,this.#P=kn(()=>e(this.#S)),te(()=>{var e=this.#R=document.createDocumentFragment(),t=ke();e.append(t),this.#I=this.#F(()=>kn(()=>this.#T(t))),0===this.#M&&(this.#S.before(e),this.#R=null,Tn(this.#P,()=>{this.#P=null}),this.#H(Ke))}))}#B(){try{if(this.is_pending=this.has_pending_snippet(),this.#M=0,this.#L=0,this.#I=kn(()=>{this.#T(this.#S)}),this.#M>0){var e=this.#R=document.createDocumentFragment();On(this.#I,e);const t=this.#E.pending;this.#P=kn(()=>t(this.#S))}else this.#H(Ke)}catch(e){this.error(e)}}#H(e){this.is_pending=!1,e.transfer_effects(this.#f,this.#d)}defer_effect(e){De(e,this.#f,this.#d)}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!this.#E.pending}#F(e){var t=jt,n=Vt,r=W;Bt(this.#A),zt(this.#A),J(this.#A.ctx);try{return tt.ensure(),e()}catch(e){return Ie(e),null}finally{Bt(t),zt(n),J(r)}}#K(e,t){this.has_pending_snippet()?(this.#M+=e,0===this.#M&&(this.#H(t),this.#P&&Tn(this.#P,()=>{this.#P=null}),this.#R&&(this.#S.before(this.#R),this.#R=null))):this.parent&&this.parent.#K(e,t)}update_pending_count(e,t){this.#K(e,t),this.#L+=e,this.#U&&!this.#D&&(this.#D=!0,te(()=>{this.#D=!1,this.#U&&Tt(this.#U,this.#L)}))}get_effect_pending(){return this.#V(),ln(this.#U)}error(e){if(!this.#E.onerror&&!this.#E.failed)throw e;Ke?.is_fork?(this.#I&&Ke.skip_effect(this.#I),this.#P&&Ke.skip_effect(this.#P),this.#O&&Ke.skip_effect(this.#O),Ke.on_fork_commit(()=>{this.#q(e)})):this.#q(e)}#q(e){this.#I&&(Sn(this.#I),this.#I=null),this.#P&&(Sn(this.#P),this.#P=null),this.#O&&(Sn(this.#O),this.#O=null),oe&&(le(this.#C),ue(),le(fe()));var t=this.#E.onerror;let n=this.#E.failed;var r=!1,a=!1;const o=()=>{r?console.warn("https://svelte.dev/e/svelte_boundary_reset_noop"):(r=!0,a&&function(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}(),null!==this.#O&&Tn(this.#O,()=>{this.#O=null}),this.#F(()=>{this.#B()}))},i=e=>{try{a=!0,t?.(e,o),a=!1}catch(e){Pe(e,this.#A&&this.#A.parent)}n&&(this.#O=this.#F(()=>{try{return kn(()=>{var t=jt;t.b=this,t.f|=128,n(this.#S,()=>e,()=>o)})}catch(e){return Pe(e,this.#A.parent),null}}))};te(()=>{var t;try{t=this.transform_error(e)}catch(e){return void Pe(e,this.#A&&this.#A.parent)}null!==t&&"object"==typeof t&&"function"==typeof t.then?t.then(i,e=>Pe(e,this.#A&&this.#A.parent)):i(t)})}}function ft(e,t,n,r){const a=pt;var o=e.filter(e=>!e.settled);if(0!==n.length||0!==o.length){var i,l,s,c,u=jt,f=(i=jt,l=Vt,s=W,c=Ke,function(e=!0){Bt(i),zt(l),J(s),e&&0===(i.f&$)&&(c?.activate(),c?.apply())}),d=1===o.length?o[0].promise:o.length>1?Promise.all(o.map(e=>e.promise)):null,h=ht();0!==n.length?d?d.then(()=>{f(),v(),dt()}):v():d.then(()=>p(t.map(a))).finally(h)}else r(t.map(a));function p(e){if(0===(u.f&$)){f();try{r(e)}catch(e){Pe(e,u)}dt()}}function v(){Promise.all(n.map(e=>gt(e))).then(e=>p([...t.map(a),...e])).catch(e=>Pe(e,u)).finally(h)}}function dt(e=!0){Bt(null),zt(null),J(null),e&&Ke?.deactivate()}function ht(){var e=jt,t=e.b,n=Ke,r=t.is_rendered();return t.update_pending_count(1,n),n.increment(r,e),()=>{t.update_pending_count(-1,n),n.decrement(r,e)}}function pt(e){null!==jt&&(jt.f|=A);return{ctx:W,deps:null,effects:null,equals:F,f:2050,fn:e,reactions:null,rv:0,v:Y,wv:0,parent:jt,ac:null}}const vt=Symbol("obsolete");function gt(e,t,n){let r=jt;null===r&&function(){throw new Error("https://svelte.dev/e/async_derived_orphan")}();var a=void 0,o=$t(Y),i=!Vt,l=new Set;return function(e){dn(4718592,e)}(()=>{var t=jt,n=p();a=n.promise;try{Promise.resolve(e()).then(n.resolve,e=>{e!==j&&n.reject(e)}).finally(dt)}catch(e){n.reject(e),dt()}var s=Ke;if(i){if(0!==(t.f&S))var c=ht();if(r.b.is_rendered())s.async_deriveds.get(t)?.reject(vt);else for(const e of l.values())e.reject(vt);l.add(n),s.async_deriveds.set(t,n)}const u=(e,t=void 0)=>{c?.(),l.delete(n),t!==vt&&(s.activate(),t?(o.f|=O,Tt(o,t)):(0!==(o.f&O)&&(o.f^=O),Tt(o,e)),s.deactivate())};n.promise.then(u,e=>u(null,e||"unknown"))}),pn(()=>{for(const e of l)e.reject(vt)}),new Promise(e=>{!function t(n){function r(){n===a?e(o):t(a)}n.then(r,r)}(a)})}function mt(e){const t=pt(e);return Ht(t),t}function bt(e){var t,n=jt,r=e.parent;if(!Dt&&null!==r&&e.v!==Y&&24576&r.f)return console.warn("https://svelte.dev/e/derived_inert"),e.v;Bt(r);try{e.f&=-65537,function(e){var t=e.effects;if(null!==t){e.effects=null;for(var n=0;n0&&!_t&&function(){_t=!1;for(const e of xt){let t;0!==(e.f&w)&&Re(e,k);try{t=Qt(e)}catch{t=!0}t&&an(e)}xt.clear()}()}return t}function At(e){Et(e,e.v+1)}function It(e,t,n){var r=e.reactions;if(null!==r)for(var a=r.length,o=0;o{document.activeElement===t&&e.focus()})}}let Ot=!1;function Rt(){Ot||(Ot=!0,document.addEventListener("reset",e=>{Promise.resolve().then(()=>{if(!e.defaultPrevented)for(const t of e.target.elements)t[z]?.()})},{capture:!0}))}function Lt(e){var t=Vt,n=jt;zt(null),Bt(null);try{return e()}finally{zt(t),Bt(n)}}let Mt=!1,Dt=!1;function Ut(e){Dt=e}let Vt=null,Nt=!1;function zt(e){Vt=e}let jt=null;function Bt(e){jt=e}let Ft=null;function Ht(e){null!==Vt&&(null===Ft?Ft=[e]:Ft.push(e))}let Kt=null,qt=0,Yt=null;let Gt=1,Wt=0,Jt=Wt;function Zt(e){Jt=e}function Xt(){return++Gt}function Qt(e){var t=e.f;if(0!==(t&x))return!0;if(2&t&&(e.f&=-65537),0!==(t&k)){for(var n=e.deps,r=n.length,a=0;ae.wv)return!0}0!==(t&y)&&null===Ye&&Re(e,w)}return!1}function en(e,t,n=!0){var a=e.reactions;if(null!==a&&(null===Ft||!r.call(Ft,e)))for(var o=0;o{e.ac.abort(j)}),e.ac=null);try{e.f|=P;var u=(0,e.fn)();e.f|=S;var f=e.deps,d=Ke?.is_fork;if(null!==Kt){var h;if(d||rn(e,qt),null!==f&&qt>0)for(f.length=qt+Kt.length,h=0;h{dn(8,()=>e(...t.map(ln)))})}function wn(e,t=0){return dn(g|t,e)}function xn(e,t=0){return dn(v|t,e)}function kn(e){return dn(524320,e)}function _n(e){var t=e.teardown;if(null!==t){const e=Dt,n=Vt;Ut(!0),zt(null);try{t.call(null)}finally{Ut(e),zt(n)}}}function $n(e,t=!1){var n=e.first;for(e.first=e.last=null;null!==n;){const e=n.ac;null!==e&&Lt(()=>{e.abort(j)});var r=n.next;0!==(n.f&b)?n.parent=null:Sn(n,t),n=r}}function Sn(e,t=!0){var n=!1;(t||262144&e.f)&&null!==e.nodes&&null!==e.nodes.end&&(Cn(e.nodes.start,e.nodes.end),n=!0),Re(e,C),$n(e,t&&!n),rn(e,0);var r=e.nodes&&e.nodes.t;if(null!==r)for(const e of r)e.stop();_n(e),e.f^=C,e.f|=$;var a=e.parent;null!==a&&null!==a.first&&En(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=e.b=null}function Cn(e,t){for(;null!==e;){var n=e===t?null:$e(e);e.remove(),e=n}}function En(e){var t=e.parent,n=e.prev,r=e.next;null!==n&&(n.next=r),null!==r&&(r.prev=n),null!==t&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function Tn(e,t,n=!0){var r=[];An(e,r,!0);var a=()=>{n&&Sn(e),t&&t()},o=r.length;if(o>0){var i=()=>--o||a();for(var l of r)l.out(i)}else a()}function An(e,t,n){if(0===(e.f&_)){e.f^=_;var r=e.nodes&&e.nodes.t;if(null!==r)for(const e of r)(e.is_global||n)&&t.push(e);for(var a=e.first;null!==a;){var o=a.next;if(0===(a.f&b))An(a,t,!!(0!==(a.f&E)||0!==(a.f&m)&&0!==(e.f&g))&&n);a=o}}}function In(e){Pn(e,!0)}function Pn(e,t){if(0!==(e.f&_)){e.f^=_,0===(e.f&w)&&(Re(e,x),tt.ensure().schedule(e));for(var n=e.first;null!==n;){var r=n.next;Pn(n,!!(0!==(n.f&E)||0!==(n.f&m))&&t),n=r}var a=e.nodes&&e.nodes.t;if(null!==a)for(const e of a)(e.is_global||t)&&e.in()}}function On(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;null!==n;){var a=n===r?null:$e(n);t.append(n),n=a}}function Rn(e){const t={get:e=>ze(t.store)[e],set:(e,n)=>{"string"==typeof e?Object.assign(ze(t.store),{[e]:n}):Object.assign(ze(t.store),e),t.store.set(ze(t.store))},store:Ne(e)};return t}globalThis.$altcha=globalThis.$altcha||{algorithms:new Map,defaults:Rn({}),i18n:Rn({}),instances:new Set,plugins:new Set};globalThis.$altcha.i18n.set("en",{ariaLinkLabel:"Altcha (official website)",cancel:"Cancel",enterCode:"Enter code",enterCodeAria:"Enter code you hear. Press Space to play audio.",enterCodeFromImage:"To proceed, please enter the code from the image below.",error:"Verification failed. Try again later.",expired:"Verification expired. Try again.",footer:'Protected by ALTCHA',getAudioChallenge:"Get an audio challenge",label:"I'm not a robot",loading:"Loading...",reload:"Reload",verify:"Verify",verificationRequired:"Verification required!",verified:"Verified",verifying:"Verifying...",waitAlert:"Verifying... please wait."});"undefined"!=typeof window&&((window.__svelte??={}).v??=new Set).add("5");const Ln=Symbol("events"),Mn=new Set,Dn=new Set;function Un(e,t,n,r={}){function a(e){if(r.capture||Bn.call(t,e),!e.cancelBubble)return Lt(()=>n?.call(this,e))}return e.startsWith("pointer")||e.startsWith("touch")||"wheel"===e?te(()=>{t.addEventListener(e,a,r)}):t.addEventListener(e,a,r),a}function Vn(e,t,n,r,a){var o={capture:r,passive:a},i=Un(e,t,n,o);(t===document.body||t===window||t===document||t instanceof HTMLMediaElement)&&pn(()=>{t.removeEventListener(e,i,o)})}function Nn(e,t,n){(t[Ln]??={})[e]=n}function zn(e){for(var t=0;to||n});var f=Vt,d=jt;zt(null),Bt(null);try{for(var h,p=[];null!==o;){var v=o.assignedSlot||o.parentNode||o.host||null;try{var g=o[Ln]?.[r];null==g||o.disabled&&e.target!==o||g.call(o,e)}catch(e){h?p.push(e):h=e}if(e.cancelBubble||v===t||null===v)break;o=v}if(h){for(let e of p)queueMicrotask(()=>{throw e});throw h}}finally{e[Ln]=t,delete e.currentTarget,zt(f),Bt(d)}}}const Fn=globalThis?.window?.trustedTypes&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:e=>e});function Hn(e){var t=Te("template");return t.innerHTML=function(e){return Fn?.createHTML(e)??e}(e.replaceAll("","\x3c!----\x3e")),t.content}function Kn(e,t){var n=jt;null===n.nodes&&(n.nodes={start:e,end:t,a:null,t:null})}function qn(e,t){var n,r=!!(1&t),a=!!(2&t),o=!e.startsWith("");return()=>{if(oe)return Kn(ae,null),ae;void 0===n&&(n=Hn(o?e:""+e),r||(n=_e(n)));var t=a||be?document.importNode(n,!0):n.cloneNode(!0);r?Kn(_e(t),t.lastChild):Kn(t,t);return t}}function Yn(e,t,n="svg"){var r,a=`<${n}>${!e.startsWith("")?e:""+e}`;return()=>{if(oe)return Kn(ae,null),ae;if(!r){var e=Hn(a);r=_e(_e(e))}var t=r.cloneNode(!0);return Kn(t,t),t}}function Gn(e,t){return Yn(e,0,"svg")}function Wn(e=""){if(!oe){var t=ke(e+"");return Kn(t,t),t}var n=ae;return 3!==n.nodeType?(n.before(n=ke()),le(n)):Ae(n),Kn(n,n),n}function Jn(){if(oe)return Kn(ae,null),ae;var e=document.createDocumentFragment(),t=document.createComment(""),n=ke();return e.append(t,n),Kn(t,n),e}function Zn(e,t){if(oe){var n=jt;return 0!==(n.f&S)&&null!==n.nodes.end||(n.nodes.end=ae),void se()}null!==e&&e.before(t)}function Xn(e){return e.endsWith("capture")&&"gotpointercapture"!==e&&"lostpointercapture"!==e}const Qn=["beforeinput","click","change","dblclick","contextmenu","focusin","focusout","input","keydown","keyup","mousedown","mousemove","mouseout","mouseover","mouseup","pointerdown","pointermove","pointerout","pointerover","pointerup","touchend","touchmove","touchstart"];function er(e){return Qn.includes(e)}const tr={formnovalidate:"formNoValidate",ismap:"isMap",nomodule:"noModule",playsinline:"playsInline",readonly:"readOnly",defaultvalue:"defaultValue",defaultchecked:"defaultChecked",srcobject:"srcObject",novalidate:"noValidate",allowfullscreen:"allowFullscreen",disablepictureinpicture:"disablePictureInPicture",disableremoteplayback:"disableRemotePlayback"};function nr(e){return e=e.toLowerCase(),tr[e]??e}const rr=["touchstart","touchmove"];function ar(e){return rr.includes(e)}function or(e,t){var n=null==t?"":"object"==typeof t?`${t}`:t;n!==(e[N]??=e.nodeValue)&&(e[N]=n,e.nodeValue=`${n}`)}function ir(e,t){return cr(e,t)}function lr(e,t){xe(),t.intro=t.intro??!1;const n=t.target,r=oe,a=ae;try{for(var o=_e(n);o&&(8!==o.nodeType||"["!==o.data);)o=$e(o);if(!o)throw q;ie(!0),le(o);const r=cr(e,{...t,anchor:o});return ie(!1),r}catch(r){if(r instanceof Error&&r.message.split("\n").some(e=>e.startsWith("https://svelte.dev/e/")))throw r;return r!==q&&console.warn("Failed to hydrate: ",r),!1===t.recover&&function(){throw new Error("https://svelte.dev/e/hydration_failed")}(),xe(),n.textContent="",ie(!1),ir(e,t)}finally{ie(r),le(a)}}const sr=new Map;function cr(e,{target:t,anchor:n,props:r={},events:o,context:i,intro:l=!0,transformError:s}){xe();var c=void 0,u=function(e){tt.ensure();const t=dn(524352,e);return(e={})=>new Promise(n=>{e.outro?Tn(t,()=>{Sn(t),n(void 0)}):(Sn(t),n(void 0))})}(()=>{var l=n??t.appendChild(ke());!function(e,t,n,r){new ut(e,t,n,r)}(l,{pending:()=>{}},t=>{if(Z({}),i&&(W.c=i),o&&(r.$$events=o),oe&&Kn(t,null),c=e(t,r)||{},oe&&(jt.nodes.end=ae,null===ae||8!==ae.nodeType||"]"!==ae.data))throw re(),q;X()},s);var u=new Set,f=e=>{for(var n=0;n{for(var e of u)for(const n of[t,document]){var r=sr.get(n),a=r.get(e);0==--a?(n.removeEventListener(e,Bn),r.delete(e),0===r.size&&sr.delete(n)):r.set(e,a)}Dn.delete(f),l!==n&&l.parentNode?.removeChild(l)}});return ur.set(c,u),c}let ur=new WeakMap;class fr{anchor;#Y=new Map;#G=new Map;#W=new Map;#J=new Set;#Z=!0;constructor(e,t=!0){this.anchor=e,this.#Z=t}#$=e=>{if(this.#Y.has(e)){var t=this.#Y.get(e),n=this.#G.get(t);if(n)In(n),this.#J.delete(t);else{var r=this.#W.get(t);r&&(this.#G.set(t,r.effect),this.#W.delete(t),r.fragment.lastChild.remove(),this.anchor.before(r.fragment),n=r.effect)}for(const[t,n]of this.#Y){if(this.#Y.delete(t),t===e)break;const r=this.#W.get(n);r&&(Sn(r.effect),this.#W.delete(n))}for(const[e,r]of this.#G){if(e===t||this.#J.has(e))continue;const a=()=>{if(Array.from(this.#Y.values()).includes(e)){var t=document.createDocumentFragment();On(r,t),t.append(ke()),this.#W.set(e,{effect:r,fragment:t})}else Sn(r);this.#J.delete(e),this.#G.delete(e)};this.#Z||!n?(this.#J.add(e),Tn(r,a,!1)):a()}}};#X=e=>{this.#Y.delete(e);const t=Array.from(this.#Y.values());for(const[e,n]of this.#W)t.includes(e)||(Sn(n.effect),this.#W.delete(e))};ensure(e,t){var n=Ke;!t||this.#G.has(e)||this.#W.has(e)||this.#G.set(e,kn(()=>t(this.anchor)));this.#Y.set(n,e),oe&&(this.anchor=ae),this.#$(n)}}function dr(e){null===W&&function(){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}(),vn(()=>{const t=un(e);if("function"==typeof t)return t})}function hr(e,t,n=!1){var r;oe&&(r=ae,se());var a=new fr(e);function o(e,t){if(oe){var n=de(r);if(e!==parseInt(n.substring(1))){var o=fe();return le(o),a.anchor=o,ie(!1),a.ensure(e,t),void ie(!0)}}a.ensure(e,t)}wn(()=>{var e=!1;t((t,n=0)=>{e=!0,o(n,t)}),e||o(-1,null)},n?E:0)}const pr=Symbol("NaN");function vr(e,t,n=!1,r=!1,a=!1,o=!1){var i=e,l="";if(n){var s=e;oe&&(i=le(_e(s)))}yn(()=>{var e=jt;if(l!==(l=t()??"")){if(n&&!oe)return e.nodes=null,s.innerHTML=l,void(""!==l&&Kn(_e(s),s.lastChild));if(null!==e.nodes&&(Cn(e.nodes.start,e.nodes.end),e.nodes=null),""!==l){if(oe){ae.data;for(var o=se(),c=o;null!==o&&(8!==o.nodeType||""!==o.data);)c=o,o=$e(o);if(null===o)throw re(),q;return Kn(ae,c),void(i=le(o))}var u=Te(r?"svg":a?"math":"template",r?"http://www.w3.org/2000/svg":a?"http://www.w3.org/1998/Math/MathML":void 0);u.innerHTML=l;var f=r||a?u:u.content;if(Kn(_e(f),f.lastChild),r||a)for(;_e(f);)i.before(_e(f));else i.before(f)}}else oe&&se()})}function gr(e,t){var n,r=void 0;xn(()=>{r!==(r=t())&&(n&&(Sn(n),n=null),r&&(n=kn(()=>{mn(()=>r(e))})))})}function mr(e){var t,n,r="";if("string"==typeof e||"number"==typeof e)r+=e;else if("object"==typeof e)if(Array.isArray(e)){var a=e.length;for(t=0;t=0;){var l=i+o;0!==i&&!yr.includes(r[i-1])||l!==r.length&&!yr.includes(r[l])?i=l:r=(0===i?"":r.substring(0,i))+r.substring(l+1)}return""===r?null:r}(n,0,o);oe&&l===e.getAttribute("class")||(null==l?e.removeAttribute("class"):t?e.className=l:e.setAttribute("class",l)),e[U]=n}else if(o&&a!==o)for(var s in o){var c=!!o[s];null!=a&&c===!!a[s]||e.classList.toggle(s,c)}return o}function _r(e,t={},n,r){for(var a in n){var o=n[a];t[a]!==o&&(null==n[a]?e.style.removeProperty(a):e.style.setProperty(a,o,r))}}function $r(e,t,n,r){var a=e[V];if(oe||a!==t){var o=function(e,t){if(t){var n,r,a="";if(Array.isArray(t)?(n=t[0],r=t[1]):n=t,e){e=String(e).replaceAll(/\s*\/\*.*?\*\/\s*/g,"").trim();var o=!1,i=0,l=!1,s=[];n&&s.push(...Object.keys(n).map(xr)),r&&s.push(...Object.keys(r).map(xr));var c=0,u=-1;const t=e.length;for(var f=0;f{if(!t){if(t=!0,e.hasAttribute("value")){var n=e.value;Vr(e,"value",null),e.value=n}if(e.hasAttribute("checked")){var r=e.checked;Vr(e,"checked",null),e.checked=r}}};e[z]=n,te(n),Rt()}}function Ur(e,t){t?e.hasAttribute("selected")||e.setAttribute("selected",""):e.removeAttribute("selected")}function Vr(e,t,n,r){var a=zr(e);oe&&(a[t]=e.getAttribute(t),"src"===t||"srcset"===t||"href"===t&&e.nodeName===Pr)||a[t]!==(a[t]=n)&&("loading"===t&&(e[M]=n),null==n?e.removeAttribute(t):"string"!=typeof n&&Br(e).includes(t)?e[t]=n:e.setAttribute(t,n))}function Nr(e,t,n=[],r=[],a=[],o,i=!1,l=!1){ft(a,n,r,n=>{var r=void 0,a={},o=e.nodeName===Lr,s=!1;if(xn(()=>{var c=t(...n.map(ln)),u=function(e,t,n,r,a=!1){if(oe&&a&&e.nodeName===Or){var o=e;("checkbox"===o.type?"defaultChecked":"defaultValue")in n||Dr(o)}var i=zr(e),l=i[Ar],s=!i[Ir];let c=oe&&l;c&&ie(!1);var u=t||{},f=e.nodeName===Rr;for(var d in t)d in n||(n[d]=null);n.class?n.class=br(n.class):n[Er]&&(n.class=null),n[Tr]&&(n.style??=null);var h=Br(e);for(const r in n){let a=n[r];if(f&&"value"===r&&null==a)e.value=e.__value="",u[r]=a;else if("class"!==r)if("style"!==r){var p=u[r];if(a!==p||void 0===a&&e.hasAttribute(r)){u[r]=a;var v=r[0]+r[1];if("$$"!==v)if("on"===v){const t={},n="$$"+r;let o=r.slice(2);var g=er(o);if(Xn(o)&&(o=o.slice(0,-7),t.capture=!0),!g&&p){if(null!=a)continue;e.removeEventListener(o,u[n],t),u[n]=null}if(g)Nn(o,e,a),zn([o]);else if(null!=a){let a=function(e){u[r].call(this,e)};u[n]=Un(o,e,a,t)}}else if("style"===r)Vr(e,r,a);else if("autofocus"===r)Pt(e,Boolean(a));else if(l||"__value"!==r&&("value"!==r||null==a))if("selected"===r&&f)Ur(e,a);else{var m=r;s||(m=nr(m));var b="defaultValue"===m||"defaultChecked"===m;if(null!=a||l||b)b||h.includes(m)&&(l||"string"!=typeof a)?(e[m]=a,m in i&&(i[m]=Y)):"function"!=typeof a&&Vr(e,m,a);else if(i[r]=null,"value"===m||"checked"===m){let n=e;const r=void 0===t;if("value"===m){let e=n.defaultValue;n.removeAttribute(m),n.defaultValue=e,n.value=n.__value=r?e:null}else{let e=n.defaultChecked;n.removeAttribute(m),n.defaultChecked=e,n.checked=!!r&&e}}else e.removeAttribute(r)}else e.value=e.__value=a}}else $r(e,a,t?.[Tr],n[Tr]),u[r]=a,u[Tr]=n[Tr];else kr(e,"http://www.w3.org/1999/xhtml"===e.namespaceURI,a,0,t?.[Er],n[Er]),u[r]=a,u[Er]=n[Er]}return c&&ie(!0),u}(e,r,c,0,i,l);s&&o&&"value"in c&&Sr(e,c.value);for(let e of Object.getOwnPropertySymbols(a))c[e]||Sn(a[e]);for(let t of Object.getOwnPropertySymbols(c)){var f=c[t];"@attach"!==t.description||r&&f===r[t]||(a[t]&&Sn(a[t]),a[t]=kn(()=>gr(e,()=>f))),u[t]=f}r=u}),o){var c=e;mn(()=>{Sr(c,r.value,!0),function(e){var t=new MutationObserver(()=>{Sr(e,e.__value)});t.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),pn(()=>{t.disconnect()})}(c)})}s=!0})}function zr(e){return e[D]??={[Ar]:e.nodeName.includes("-"),[Ir]:e.namespaceURI===G}}var jr=new Map;function Br(e){var t,n=e.getAttribute("is")||e.nodeName,r=jr.get(n);if(r)return r;jr.set(n,r=[]);for(var a=e,o=Element.prototype;o!==a;){for(var i in t=s(a))t[i].set&&"innerHTML"!==i&&"textContent"!==i&&"innerText"!==i&&r.push(i);a=f(a)}return r}function Fr(e,t,n=t){var r=new WeakSet;!function(e,t,n,r=n){e.addEventListener(t,()=>Lt(n));const a=e[z];e[z]=a?()=>{a(),r(!0)}:()=>r(!0),Rt()}(e,"input",async a=>{var o=a?e.defaultValue:e.value;if(o=Hr(e)?Kr(o):o,n(o),null!==Ke&&r.add(Ke),await on(),o!==(o=t())){var i=e.selectionStart,l=e.selectionEnd,s=e.value.length;if(e.value=o??"",null!==l){var c=e.value.length;i===l&&l===s&&c>s?(e.selectionStart=c,e.selectionEnd=c):(e.selectionStart=i,e.selectionEnd=Math.min(l,c))}}}),(oe&&e.defaultValue!==e.value||null==un(t)&&e.value)&&(n(Hr(e)?Kr(e.value):e.value),null!==Ke&&r.add(Ke)),bn(()=>{var n=t();if(e===document.activeElement){var a=Ke;if(r.has(a))return}Hr(e)&&n===Kr(e.value)||("date"!==e.type||n||e.value)&&n!==e.value&&(e.value=n??"")})}function Hr(e){var t=e.type;return"number"===t||"range"===t}function Kr(e){return""===e?null:+e}function qr(e,t){return e===t||e?.[R]===t}function Yr(e={},t,n,r){var a=W.r,o=jt;return mn(()=>{var r,i;return bn(()=>{r=i,i=[],un(()=>{qr(n(...i),e)||(t(e,...i),r&&qr(n(...r),e)&&t(null,...r))})}),()=>{let r=o;for(;r!==a&&null!==r.parent&&r.parent.f&C;)r=r.parent;const l=r.teardown;r.teardown=()=>{i&&qr(n(...i),e)&&t(null,...i),l?.()}}}),e}const Gr={get(e,t){if(!e.exclude.includes(t))return e.props[t]},set:(e,t)=>!1,getOwnPropertyDescriptor(e,t){if(!e.exclude.includes(t))return t in e.props?{enumerable:!0,configurable:!0,value:e.props[t]}:void 0},has:(e,t)=>!e.exclude.includes(t)&&t in e.props,ownKeys:e=>Reflect.ownKeys(e.props).filter(t=>!e.exclude.includes(t))};function Wr(e,t,n){return new Proxy({props:e,exclude:t},Gr)}function Jr(e,t,n,r){var a,o=r,i=!0,l=()=>(i&&(i=!1,o=r),o);void 0===e[t]&&void 0!==r&&l(),a=()=>{var n=e[t];return void 0===n?l():(i=!0,n)};var s=!1,c=pt(()=>(s=!1,a())),u=jt;return function(e,t){if(arguments.length>0){const n=t?ln(c):e;return Et(c,n),s=!0,void 0!==o&&(o=n),e}return Dt&&s||0!==(u.f&$)?c.v:ln(c)}}class Zr{#Q;#ee;constructor(e){var t=new Map,n=(e,n)=>{var r=Ct(n,!1,!1);return t.set(e,r),r};const r=new Proxy({...e.props||{},$$events:{}},{get:(e,r)=>ln(t.get(r)??n(r,Reflect.get(e,r))),has:(e,r)=>r===L||(ln(t.get(r)??n(r,Reflect.get(e,r))),Reflect.has(e,r)),set:(e,r,a)=>(Et(t.get(r)??n(r,a),a),Reflect.set(e,r,a))});this.#ee=(e.hydrate?lr:ir)(e.component,{target:e.target,anchor:e.anchor,props:r,context:e.context,intro:e.intro??!1,recover:e.recover,transformError:e.transformError}),e?.props?.$$host&&!1!==e.sync||nt(),this.#Q=r.$$events;for(const e of Object.keys(this.#ee))"$set"!==e&&"$destroy"!==e&&"$on"!==e&&i(this,e,{get(){return this.#ee[e]},set(t){this.#ee[e]=t},enumerable:!0});this.#ee.$set=e=>{Object.assign(r,e)},this.#ee.$destroy=()=>{!function(e,t){const n=ur.get(e);n?(ur.delete(e),n(t)):Promise.resolve()}(this.#ee)}}$set(e){this.#ee.$set(e)}$on(e,t){this.#Q[e]=this.#Q[e]||[];const n=(...e)=>t.call(this,...e);return this.#Q[e].push(n),()=>{this.#Q[e]=this.#Q[e].filter(e=>e!==n)}}$destroy(){this.#ee.$destroy()}}let Xr=class{};function Qr(e,t,n,r){const a=n[e]?.type;if(t="Boolean"===a&&"boolean"!=typeof t?null!=t:t,!r||!n[e])return t;if("toAttribute"===r)switch(a){case"Object":case"Array":return null==t?null:JSON.stringify(t);case"Boolean":return t?"":null;case"Number":return null==t?null:t;default:return t}else switch(a){case"Object":case"Array":return t&&JSON.parse(t);case"Boolean":default:return t;case"Number":return null!=t?+t:t}}function ea(e,t,n,r,a,s){let c=class extends Xr{constructor(){super(e,n,a),this.$$p_d=t}static get observedAttributes(){return o(t).map(e=>(t[e].attribute||e).toLowerCase())}};return o(t).forEach(e=>{i(c.prototype,e,{get(){return this.$$c&&e in this.$$c?this.$$c[e]:this.$$d[e]},set(n){n=Qr(e,n,t),this.$$d[e]=n;var r=this.$$c;if(r){var a=l(r,e)?.get;a?r[e]=n:r.$set({[e]:n})}}})}),r.forEach(e=>{i(c.prototype,e,{get(){return this.$$c?.[e]}})}),e.element=c,c}"function"==typeof HTMLElement&&(Xr=class extends HTMLElement{$$ctor;$$s;$$c;$$cn=!1;$$d={};$$r=!1;$$p_d={};$$l={};$$l_u=new Map;$$me;$$shadowRoot=null;constructor(e,t,n){super(),this.$$ctor=e,this.$$s=t,n&&(this.$$shadowRoot=this.attachShadow(n))}addEventListener(e,t,n){if(this.$$l[e]=this.$$l[e]||[],this.$$l[e].push(t),this.$$c){const n=this.$$c.$on(e,t);this.$$l_u.set(t,n)}super.addEventListener(e,t,n)}removeEventListener(e,t,n){if(super.removeEventListener(e,t,n),this.$$c){const e=this.$$l_u.get(t);e&&(e(),this.$$l_u.delete(t))}}async connectedCallback(){if(this.$$cn=!0,!this.$$c){let t=function(e){return t=>{const n=Te("slot");"default"!==e&&(n.name=e),Zn(t,n)}};if(await Promise.resolve(),!this.$$cn||this.$$c)return;const n={},r=function(e){const t={};return e.childNodes.forEach(e=>{t[e.slot||"default"]=!0}),t}(this);for(const e of this.$$s)e in r&&("default"!==e||this.$$d.children?n[e]=t(e):(this.$$d.children=t(e),n.default=!0));for(const e of this.attributes){const t=this.$$g_p(e.name);t in this.$$d||(this.$$d[t]=Qr(t,e.value,this.$$p_d,"toProp"))}for(const e in this.$$p_d)e in this.$$d||void 0===this[e]||(this.$$d[e]=this[e],delete this[e]);this.$$c=(e={component:this.$$ctor,target:this.$$shadowRoot||this,props:{...this.$$d,$$slots:n,$$host:this}},new Zr(e)),this.$$me=function(e){tt.ensure();const t=dn(524352,e);return()=>{Sn(t)}}(()=>{bn(()=>{this.$$r=!0;for(const e of o(this.$$c)){if(!this.$$p_d[e]?.reflect)continue;this.$$d[e]=this.$$c[e];const t=Qr(e,this.$$d[e],this.$$p_d,"toAttribute");null==t?this.removeAttribute(this.$$p_d[e].attribute||e):this.setAttribute(this.$$p_d[e].attribute||e,t)}this.$$r=!1})});for(const e in this.$$l)for(const t of this.$$l[e]){const n=this.$$c.$on(e,t);this.$$l_u.set(t,n)}this.$$l={}}var e}attributeChangedCallback(e,t,n){this.$$r||(e=this.$$g_p(e),this.$$d[e]=Qr(e,n,this.$$p_d,"toProp"),this.$$c?.$set({[e]:this.$$d[e]}))}disconnectedCallback(){this.$$cn=!1,Promise.resolve().then(()=>{!this.$$cn&&this.$$c&&(this.$$c.$destroy(),this.$$me(),this.$$c=void 0)})}$$g_p(e){return o(this.$$p_d).find(t=>this.$$p_d[t].attribute===e||!this.$$p_d[t].attribute&&t.toLowerCase()===e)||e}});var ta=qn('
');function na(e,t){Z(t,!0);let n,r=Jr(t,"loading"),a=Wr(t,["$$slots","$$events","$$legacy","$$host","loading"]);var o={get loading(){return r()},set loading(e){r(e),nt()}},i=ta(),l=Se(i);Nr(l,()=>({type:"checkbox",...a}),void 0,void 0,void 0,0,!0),Yr(l,e=>n=e,()=>n);var s=Ee(l,2);return ue(2),ce(i),yn(()=>Vr(i,"data-loading",r())),Nn("click",s,function(){n?.click()}),Zn(e,i),X(o)}zn(["click"]),ea(na,{loading:{}},[],[],{mode:"open"});var ra=qn('
');function aa(e,t){Z(t,!0);let n=Jr(t,"loading"),r=Wr(t,["$$slots","$$events","$$legacy","$$host","loading"]);var a={get loading(){return n()},set loading(e){n(e),nt()}},o=ra();return Nr(Se(o),()=>({type:"checkbox",...r}),void 0,void 0,void 0,0,!0),ue(2),ce(o),yn(()=>Vr(o,"data-loading",n())),Zn(e,o),X(a)}ea(aa,{loading:{}},[],[],{mode:"open"});var oa=qn('
');function ia(e,t){Z(t,!0);let n=Jr(t,"strings");var r={get strings(){return n()},set strings(e){n(e),nt()}},a=oa(),o=Se(a);return Vr(o,"href","https://altcha.org"),ce(a),yn(()=>Vr(o,"aria-label",n().ariaLinkLabel)),Zn(e,a),X(r)}ea(ia,{strings:{}},[],[],{mode:"open"});var la=qn('');function sa(e,t){Z(t,!0);let n=Jr(t,"logo"),r=Jr(t,"strings");var a={get logo(){return n()},set logo(e){n(e),nt()},get strings(){return r()},set strings(e){r(e),nt()}},o=la(),i=Se(o);vr(i,()=>r().footer,!0),ce(i);var l=Ee(i,2),s=e=>{ia(e,{get strings(){return r()}})};return hr(l,e=>{n()&&e(s)}),ce(o),Zn(e,o),X(a)}ea(sa,{logo:{},strings:{}},[],[],{mode:"open"});var ca=qn('
');function ua(e,t){Z(t,!0);let n,r=Jr(t,"loading"),a=Wr(t,["$$slots","$$events","$$legacy","$$host","loading"]);var o={get loading(){return r()},set loading(e){r(e),nt()}},i=ca(),l=Se(i);Nr(l,()=>({type:"checkbox",...a}),void 0,void 0,void 0,0,!0),Yr(l,e=>n=e,()=>n);var s=Ee(l,2);return ce(i),yn(()=>Vr(i,"data-loading",r())),Nn("click",s,function(){n?.click()}),Zn(e,i),X(o)}zn(["click"]),ea(ua,{loading:{}},[],[],{mode:"open"});var fa=(e=>(e.ERROR="error",e.LOADING="loading",e.PLAYING="playing",e.PAUSED="paused",e.READY="ready",e))(fa||{}),da=(e=>(e.CODE="code",e.ERROR="error",e.VERIFIED="verified",e.VERIFYING="verifying",e.UNVERIFIED="unverified",e.EXPIRED="expired",e))(da||{}),ha=qn('
'),pa=qn('
'),va=Gn(''),ga=Gn(''),ma=Gn(''),ba=qn(''),ya=qn(''),wa=qn('
');function xa(e,t){Z(t,!0);let n=Jr(t,"audioUrl"),r=Jr(t,"codeChallenge"),a=Jr(t,"config"),o=Jr(t,"imageUrl"),i=Jr(t,"onCancel"),l=Jr(t,"onReload"),s=Jr(t,"onSubmit"),c=Jr(t,"strings"),u=St(void 0),f=St(void 0),d=St(void 0),h=St(!1),p=St(""),v=St(!1);function g(){Et(u,fa.PAUSED,!0)}function m(e){Et(u,fa.ERROR,!0)}function b(){Et(u,fa.READY,!0)}function y(){Et(u,fa.LOADING,!0)}function w(){Et(u,fa.PLAYING,!0)}function x(){Et(u,fa.PAUSED,!0)}function k(){ln(f)?ln(u)===fa.LOADING||(ln(f).paused?(n()&&ln(f).src!==n()&&(ln(f).src=n()),ln(f).currentTime=0,ln(f).play()):ln(f).pause()):(Et(v,!0),requestAnimationFrame(()=>{ln(f)&&n()&&(ln(f).src=n(),ln(f).play())}))}dr(()=>(a().disableAutoFocus||on().then(()=>{ln(d)?.focus()}),()=>{ln(f)&&(ln(f).pause(),Et(f,void 0))}));var _={get audioUrl(){return n()},set audioUrl(e){n(e),nt()},get codeChallenge(){return r()},set codeChallenge(e){r(e),nt()},get config(){return a()},set config(e){a(e),nt()},get imageUrl(){return o()},set imageUrl(e){o(e),nt()},get onCancel(){return i()},set onCancel(e){i(e),nt()},get onReload(){return l()},set onReload(e){l(e),nt()},get onSubmit(){return s()},set onSubmit(e){s(e),nt()},get strings(){return c()},set strings(e){c(e),nt()}},$=wa(),S=Se($),C=Se(S),E=e=>{var t=ha(),n=Se(t,!0);ce(t),yn(()=>or(n,c().verificationRequired)),Zn(e,t)};hr(C,e=>{"standard"!==a().codeChallengeDisplay&&e(E)});var T=Ee(C,2),A=Se(T,!0);ce(T);var I=Ee(T,2),P=Ee(I,2),O=Se(P);Dr(O),O.disabled=ln(h),Yr(O,e=>Et(d,e),()=>ln(d));var R=Ee(O,2),L=e=>{var t=ba(),n=Se(t),r=e=>{Zn(e,pa())},a=e=>{Zn(e,va())},o=e=>{Zn(e,ga())},i=e=>{Zn(e,ma())};hr(n,e=>{ln(u)===fa.LOADING?e(r):ln(u)===fa.ERROR?e(a,1):ln(u)===fa.PLAYING?e(o,2):e(i,-1)}),ce(t),yn(()=>{Vr(t,"title",c().getAudioChallenge),t.disabled=ln(u)===fa.LOADING||ln(u)===fa.ERROR,Vr(t,"aria-label",ln(u)===fa.LOADING?c().loading:c().getAudioChallenge)}),Vn("click",t,()=>k(),!0),Zn(e,t)};hr(R,e=>{r().audio&&e(L)});var M=Ee(R,2);ce(P);var D=Ee(P,2),U=Se(D),V=Se(U,!0);ce(U);var N=Ee(U,2),z=Se(N,!0);ce(N),ce(D),ce(S);var j=Ee(S,2),B=e=>{var t=ya();Yr(t,e=>Et(f,e),()=>ln(f)),Vn("error",t,m),Vn("loadstart",t,y),Vn("canplay",t,b),Vn("pause",t,x),Vn("playing",t,w),Vn("ended",t,g),Zn(e,t)};return hr(j,e=>{ln(v)&&e(B)}),ce($),yn(()=>{or(A,c().enterCodeFromImage),Vr(I,"src",o()),Vr(O,"minlength",r().length||1),Vr(O,"maxlength",r().length),Vr(O,"placeholder",c().enterCode),Vr(O,"aria-label",ln(u)===fa.LOADING?c().loading:ln(u)===fa.PLAYING?"":c().enterCodeAria),Vr(O,"aria-live",ln(u)?"assertive":"polite"),Vr(O,"aria-busy",ln(u)===fa.LOADING),Vr(M,"title",c().reload),Vr(M,"aria-label",c().reload),Vr(U,"aria-label",c().verify),or(V,c().verify),Vr(N,"aria-label",c().cancel),or(z,c().cancel)}),Vn("submit",S,function(e){e.preventDefault(),e.stopPropagation(),s()?.(ln(p))},!0),Nn("keydown",O,function(e){"Space"===e.code?(e.preventDefault(),e.stopPropagation(),k()):"Escape"===e.code&&(e.preventDefault(),e.stopPropagation(),i()?.())}),Fr(O,()=>ln(p),e=>Et(p,e)),Vn("click",M,()=>l()?.(),!0),Vn("click",N,()=>i()?.(),!0),Zn(e,$),X(_)}zn(["keydown"]),ea(xa,{audioUrl:{},codeChallenge:{},config:{},imageUrl:{},onCancel:{},onReload:{},onSubmit:{},strings:{}},[],[],{mode:"open"});var ka=qn('
'),_a=qn('
'),$a=qn('
×
'),Sa=qn('
',1);function Ca(e,t){Z(t,!0);let n=Jr(t,"anchor"),r=Jr(t,"children"),a=Jr(t,"display",0,"standard"),o=Jr(t,"backdrop",0,!1),i=Jr(t,"onClickOutside"),l=Jr(t,"onClickOutsideDelay",0,600),s=Jr(t,"onClose"),c=Jr(t,"placement",0,"auto"),u=Jr(t,"updateUISignal"),f=Jr(t,"variant",0,"neutral"),d=Wr(t,["$$slots","$$events","$$legacy","$$host","anchor","children","display","backdrop","onClickOutside","onClickOutsideDelay","onClose","placement","updateUISignal","variant"]),p=St(void 0),v=St(void 0),g=St(!1),m=St(0);function b(){s()?.()}function y(){if(n()&&"auto"===c()&&ln(p)){const e=n().getBoundingClientRect(),t=document.documentElement.clientHeight-(e.top+e.height){"auto"!==c()&&Et(g,"top"===c())}),vn(()=>{u()&&y()}),dr(()=>{const e="bottomsheet"===a()||"overlay"===a();return e&&(ln(v)&&document.body.append(ln(v)),ln(p)&&document.body.append(ln(p))),y(),on().then(()=>{Et(m,Date.now(),!0)}),()=>{e&&(ln(v)&&document.body.removeChild(ln(v)),ln(p)&&document.body.removeChild(ln(p)))}});var w={get anchor(){return n()},set anchor(e){n(e),nt()},get children(){return r()},set children(e){r(e),nt()},get display(){return a()},set display(e="standard"){a(e),nt()},get backdrop(){return o()},set backdrop(e=!1){o(e),nt()},get onClickOutside(){return i()},set onClickOutside(e){i(e),nt()},get onClickOutsideDelay(){return l()},set onClickOutsideDelay(e=600){l(e),nt()},get onClose(){return s()},set onClose(e){s(e),nt()},get placement(){return c()},set placement(e="auto"){c(e),nt()},get updateUISignal(){return u()},set updateUISignal(e){u(e),nt()},get variant(){return f()},set variant(e="neutral"){f(e),nt()}},x=Sa();Vn("click",ge,function(e){const t=e.target;ln(p)?.contains(t)||l()&&!(ln(m)+l(){var t=ka();Yr(t,e=>Et(v,e),()=>ln(v)),Zn(e,t)};hr(k,e=>{o()&&e(_)});var $=Ee(k,2);Nr($,()=>({...d,class:`altcha-popover ${(t.class||"")??""}`,"data-popover":!0,"data-variant":f(),"data-top":ln(g),"data-display":a()}));var S=Se($),C=e=>{Zn(e,_a())};hr(S,e=>{"standard"===a()&&e(C)});var T=Ee(S,2),A=e=>{var t=$a();Vn("click",t,b,!0),Zn(e,t)};hr(T,e=>{"standard"!==a()&&e(A)});var I=Ee(T,2);return function(e,t,...n){var r=new fr(e);wn(()=>{const e=t()??null;r.ensure(e,e&&(t=>e(t,...n)))},E)}(Se(I),()=>r()??h),ce(I),ce($),Yr($,e=>Et(p,e),()=>ln(p)),Zn(e,x),X(w)}async function Ea(e){const{challenge:t,concurrency:n=navigator.hardwareConcurrency,controller:r=new AbortController,createWorker:a,onOutOfMemory:o=e=>e>1?Math.floor(e/2):0,counterMode:i,timeout:l=9e4}=e,s=Math.min(16,Math.max(1,n)),c=[],u=()=>{for(const e of c)e.terminate()};for(let e=0;e(r.signal.addEventListener("abort",()=>{e.postMessage({type:"abort"})}),new Promise((r,a)=>{e.addEventListener("error",e=>{a(e)}),e.addEventListener("message",t=>{if(t.data){for(const t of c)t!==e&&t.postMessage({type:"abort"});if(t.data.error)return a(new Error(t.data.error))}r(t.data)}),e.postMessage({challenge:t,counterMode:i,counterStart:n,counterStep:s,timeout:l,type:"work"})}))))}catch(n){if(n instanceof Error&&!!n?.message?.includes("Out of memory")&&o){u();const n=o(s);if(n)return Ea({...e,challenge:t,controller:r,concurrency:n,createWorker:a})}throw n}finally{u()}return r.signal.aborted?null:f||null}ea(Ca,{anchor:{},children:{},display:{},backdrop:{},onClickOutside:{},onClickOutsideDelay:{},onClose:{},placement:{},updateUISignal:{},variant:{}},[],[],{mode:"open"});class Ta{TAG_CODES={INPUT:1,TEXTAREA:2,SELECT:3,BUTTON:4,A:5,DETAILS:6,SUMMARY:7,IFRAME:8,VIDEO:9,AUDIO:10};maxSamples;sampleInterval;target;focusStartTime=0;focusInteraction=0;focusInteractionTimer=null;lastPointerSample=0;lastTouchSample=0;lastScrollSample=0;pendingPointer=null;pendingTouch=null;focus=[];pointer=[];scroll=[];touch=[];constructor(e={}){const{maxSamples:t=60,sampleInterval:n=50,target:r=window}=e;this.maxSamples=t,this.sampleInterval=n,this.target=r,this.attach()}destroy(){const e={capture:!0};this.target.removeEventListener("focusin",this.onFocus,e),this.target.removeEventListener("keydown",this.onInteraction,e),this.target.removeEventListener("pointerdown",this.onInteraction,e),this.target.removeEventListener("pointermove",this.onPointer,e),this.target.removeEventListener("scroll",this.onScroll,e),this.target.removeEventListener("touchmove",this.onTouchMove,e)}export(){return{focus:this.focus,maxTouchPoints:navigator.maxTouchPoints||0,pointer:this.pointer,scroll:this.scroll,time:Date.now(),touch:this.touch}}attach(){const e={passive:!0,capture:!0};this.target.addEventListener("focusin",this.onFocus,e),this.target.addEventListener("keydown",this.onInteraction,e),this.target.addEventListener("pointerdown",this.onInteraction,e),this.target.addEventListener("pointermove",this.onPointer,e),this.target.addEventListener("scroll",this.onScroll,e),this.target.addEventListener("touchmove",this.onTouchMove,e)}evict(e){e.length>this.maxSamples&&e.splice(0,e.length-this.maxSamples)}onFocus=e=>{if(2===this.focusInteraction)return;const t=e.target;if(!(t instanceof Element))return;const n=performance.now();0===this.focusStartTime&&(this.focusStartTime=n),this.focus.push([Math.round(n-this.focusStartTime),t.tabIndex,this.TAG_CODES[t.tagName]??0,this.focusInteraction?1:0]),this.evict(this.focus)};onInteraction=e=>{this.focusInteraction="keyCode"in e?1:2,this.focusInteractionTimer&&clearTimeout(this.focusInteractionTimer),this.focusInteractionTimer=setTimeout(()=>{this.focusInteraction=0},100)};onPointer=e=>{if("touch"===e.pointerType)return;const t=e.timeStamp||performance.now();this.pendingPointer=[Math.round(e.clientX),Math.round(e.clientY),Math.round(t)],t-this.lastPointerSample>=this.sampleInterval&&(this.pointer.push(this.pendingPointer),this.lastPointerSample=t,this.pendingPointer=null,this.evict(this.pointer))};onScroll=()=>{const e=performance.now();e-this.lastScrollSample{const t=e.timeStamp||performance.now(),n=e.touches[0];n&&(this.pendingTouch=[Math.round(n.clientX),Math.round(n.clientY),Math.round(t),Math.round(1e3*n.force)/1e3,Math.round(n.radiusX||0),Math.round(n.radiusY||0)],t-this.lastTouchSample>=this.sampleInterval&&(this.touch.push(this.pendingTouch),this.lastTouchSample=t,this.pendingTouch=null,this.evict(this.touch)))}}var Aa=qn('
'),Ia=qn('
'),Pa=qn('
×
',1),Oa=qn('
'),Ra=qn(''),La=qn('
Secure context (HTTPS) required.
'),Ma=qn('
'),Da=qn('
'),Ua=qn(" ",1),Va=qn('
',1);"undefined"!=typeof window&&window.customElements&&!customElements.get("altcha-widget")&&customElements.define("altcha-widget",ea(function(e,t){Z(t,!0);const[n,r]=function(){const e={};return[e,function(){pn(()=>{for(var t in e)e[t].unsubscribe();i(e,je,{enumerable:!1,value:!0})})}]}(),a=["ar","fa","he","ur"],{isSecureContext:o}=globalThis,{store:l}=globalThis.$altcha.defaults,s=navigator.hardwareConcurrency||2,c=navigator.deviceMemory||0,u=c&&c<=4?Math.min(4,s):s,f=globalThis.$altcha.i18n.store,d=t.$$host,h=(e,t)=>{on().then(()=>{d?.dispatchEvent(new CustomEvent(e,{detail:t}))})};let p=null,v=St(he(new URL(location.origin))),g=St(!1),m=St(null),b=St(null),y=St(null),w=St(he(da.UNVERIFIED)),x=St(void 0),k=St(void 0),_=St(null),$=St(void 0),S=St(null),C=St(null),T=St(null),A=St(null),I=St(he([])),P=St(0),O=St(he({})),R=St(!0);const L=mt(()=>({fetch:(e,t)=>fetch(e,t),audioChallengeLanguage:"",auto:"off",barPlacement:"bottom",challenge:"",codeChallenge:null,codeChallengeDisplay:"standard",credentials:null,debug:!1,disableAutoFocus:!1,display:"standard",floatingAnchor:"",floatingOffset:8,floatingPersist:!1,floatingPlacement:"auto",hideFooter:!1,hideLogo:!1,humanInteractionSignature:!0,language:"",mockError:!1,minDuration:500,overlayContent:"",name:"altcha",popoverPlacement:"auto",retryOnOutOfMemoryError:!0,setCookie:null,serverVerificationFields:!1,serverVerificationTimeZone:!1,test:!1,timeout:9e4,type:"checkbox",validationMessage:"",verifyFunction:null,verifyUrl:"",workers:u,...Be(l,"$altchaDefaults",n),...ln(O)})),M=mt(()=>`altcha-checkbox-${t.id||Math.floor(1e12*Math.random()).toString(16)}`),D=mt(()=>function(e){switch(e){case"checkbox":return na;case"switch":return ua;default:return aa}}(ln(L).type)),U=mt(()=>ln(L).auto),V=mt(()=>ln(w)===da.VERIFYING),N=mt(()=>!ln(L).hideFooter),z=mt(()=>!ln(L).hideLogo&&"bar"!==ln(L).display),j=mt(()=>function(e,t){const n=Object.keys(e).map(e=>e.toLowerCase());let r=t.reduce((t,r)=>(r=r.toLowerCase(),t||(e[r]?r:null)||n.find(e=>r.split("-")[0]===e.split("-")[0])||null),null);e[r||""]||(r="en");return{language:r,strings:e[r]}}(Be(f,"$altchaI18nStore",n),[ln(L).language,document.documentElement.lang,...navigator.languages])),B=mt(()=>a.includes(ln(j).language)?"rtl":void 0),F=mt(()=>({...ln(j).strings})),H=mt(()=>ln(m)?.audio?.match(/^(https?:)?\//)?Q(ln(m).audio,ln(v),{language:ln(L).audioChallengeLanguage||ln(j).language}).toString():ln(m)?.audio),K=mt(()=>ln(m)?.image?.match(/^(https?:)?\//)?Q(ln(m).image,ln(v)):ln(m)?.image);async function q(e,...t){let n;for(const r of ln(I))n=await r[e].call(r,...t);return n}async function Y(e){await new Promise(t=>setTimeout(t,e))}async function G(e=ln(L).challenge,t){const n=await q("onFetchChallenge",e);let r=null;if(void 0!==n)return n;if("string"==typeof e)if(e.startsWith("{")){_e("parsing JSON challenge");try{r=JSON.parse(e)}catch{throw new Error("Unable to parse JSON challenge.")}}else{_e("fetching challenge from",t?.method||"GET",e),Et(v,new URL(e,location.origin),!0);const n=await ln(L).fetch(e,{credentials:ln(L).credentials||void 0,...t});await ye(n);const a=n.headers.get("x-altcha-config");a&&function(e){try{const t=JSON.parse(e);t&&"object"==typeof t&&xe({serverVerificationFields:t?.sentinel?.fields,serverVerificationTimeZone:t?.sentinel?.timeZone,verifyUrl:t.verifyurl,...t})}catch(e){_e("unable to configure from x-altcha-config header",e)}}(a);const o=await n.json();if(o&&"his"in o&&o.his){if(_e("requested HIS"),!p)throw new Error("Server requested HIS data but collector is disabled.");return G(Q(o.his.url,ln(v)),{body:JSON.stringify({his:p.export()}),headers:{"content-type":"application/json"},method:"POST"})}o&&"hisResult"in o&&o.hisResult&&_e("HIS result",o.hisResult),r=o}else if(e&&"object"==typeof e)try{r=JSON.parse(JSON.stringify(e))}catch{throw new Error("Unable to parse JSON challenge.")}if(function(e){return"object"==typeof e&&"challenge"in e}(r)&&(r=function(e){const[t,n]=e.salt.split("?"),r={};if(n)try{Object.assign(r,Object.fromEntries(new URLSearchParams(n).entries()))}catch{}const a={codeChallenge:e.codeChallenge,parameters:{algorithm:e.algorithm,cost:1,data:r,expiresAt:r?.expires?parseInt(r.expires,10):void 0,keyLength:"SHA-512"===e.algorithm?64:"SHA-384"===e.algorithm?48:32,nonce:(o=(new TextEncoder).encode(e.salt),Array.from(new Uint8Array(o)).map(e=>e.toString(16).padStart(2,"0")).join("")),keyPrefix:e.challenge,salt:""},signature:e.signature};var o;return Object.defineProperties(a,{_originalSalt:{enumerable:!1,value:e.salt,writable:!1},_version:{enumerable:!1,value:1,writable:!1}}),a}(r)),!function(e){return!!e&&"object"==typeof e&&"parameters"in e&&!!e.parameters&&"object"==typeof e.parameters&&"algorithm"in e.parameters&&"nonce"in e.parameters&&"salt"in e.parameters&&"keyPrefix"in e.parameters}(r))throw new Error("Challenge validation failed.");return r}function W(){return document.getElementById(ln(M))}function J(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{}}function Q(e,t,n){const r=new URL(e,t);if(r.search||(r.search=t.search),n)for(const e in n)void 0!==n[e]&&null!==n[e]&&r.searchParams.set(e,n[e]);return r.toString()}function ee(e){!ln(g)&&e.currentTarget.checked?(e.preventDefault(),e.currentTarget.checked=!1,ln(w)!==da.VERIFYING&&Pe()):e.currentTarget.checked||(e.preventDefault(),$e())}function te(e){ln(w)===da.VERIFYING?e.currentTarget.setCustomValidity(ln(F).waitAlert):ln(L).validationMessage&&e.currentTarget.setCustomValidity(ln(L).validationMessage)}function ne(){be(ln(L).display),$e()}function re(e){"onfocus"===ln(U)&&ln(w)===da.UNVERIFIED&&Pe()}function ue(){be(ln(L).display),$e()}function pe(e){const t=e.target;"true"!==t?.getAttribute("data-code-challenge")&&"onsubmit"===ln(U)&&ln(w)===da.UNVERIFIED&&(e.preventDefault(),e.stopPropagation(),Et(S,e.submitter,!0),Ae(),Pe().then(e=>{e&&!ln(m)&&on().then(()=>{ve(ln(S))})}))}function ve(e){ln(_)&&"requestSubmit"in ln(_)?ln(_).requestSubmit(e):ln(_)?.reportValidity()&&(e?e.click():ln(_).submit())}function be(e){switch(e){case"bar":case"floating":case"overlay":ke(),ln(U)&&"off"!==ln(U)||(ln(O).auto="onsubmit");break;case"standard":Ae()}ln(y)!==e&&Et(y,e,!0)}async function ye(e){if(e.status>=400){if(e.headers.get("content-type")?.includes("/json")){let t;try{t=await e.json()}catch{}if(t&&"error"in t)throw new Error(`Server responded with ${e.status} - ${t.error}`)}throw new Error(`Server responded with ${e.status}.`)}const t=e.headers.get("content-type");if(!t||!t.includes("/json"))throw new Error(`Server responded with invalid content-type. Expected application/json, received ${t}.`)}async function we(e){if(!ln(A))return void Te(da.ERROR,"Cannot verify code challenge without PoW payload.");Te(da.VERIFYING);let t=null;if(ln(L).verifyUrl)t=await async function(e,t){const n=await q("onRequestServerVerification",e,t);if(void 0!==n)return n;if(_e("requesting server verification from",ln(L).verifyUrl),!ln(L).verifyUrl)throw new Error("Parameter verifyUrl must be set for server verification.");const r=await ln(L).fetch(Q(ln(L).verifyUrl,ln(v)),{body:JSON.stringify({code:t,fields:ln(L).serverVerificationFields?[...ln(_)?.querySelectorAll('input[type="text"]:not([data-no-spamfilter]), textarea:not([data-no-spamfilter])')||[]].reduce((e,t)=>{const n=t.name,r=t.value;return n&&r&&(e[n]=/\n/.test(r)?r.replace(new RegExp("(?{ve(ln(S))})):Te(da.ERROR,t?.reason||"Verification failed."),ln(L).disableAutoFocus||W()?.focus()}function xe(e){Object.assign(ln(O),{...Object.fromEntries(Object.entries(e).filter(([e,t])=>void 0!==t))})}function ke(){Et(R,!1)}function _e(...e){(ln(L).debug||e.some(e=>e instanceof Error))&&console[e[0]instanceof Error?"error":"log"]("ALTCHA",`[name=${ln(L).name}]`,...e)}function $e(e=da.UNVERIFIED,t=null){Et(g,!1),Et(C,t,!0),Et(A,null),ln(b)&&ln(b).abort(),ln(T)&&(clearTimeout(ln(T)),Et(T,null)),Te(e)}function Te(e,t=null){Et(w,e,!0),Et(C,t,!0),h("statechange",{payload:ln(A),state:ln(w)})}function Ae(){Et(R,!0),on().then(()=>{Ie()})}function Ie(){if("floating"===ln(L).display)return function(e=20){if(!ln($))return;const t=ln(L).floatingPlacement;if(!ln(k)&&(Et(k,(ln(L).floatingAnchor instanceof HTMLElement?ln(L).floatingAnchor:ln(L).floatingAnchor?document.querySelector(ln(L).floatingAnchor):ln(_)?.querySelector('input[type="submit"], button[type="submit"], button:not([type="button"]):not([type="reset"])'))||ln(_),!0),!ln(k)))return void _e("unable to find floating anchor element");const n=parseInt(ln(L).floatingOffset,10)||12,r=ln(k).getBoundingClientRect(),a=ln($).getBoundingClientRect(),o=document.documentElement.clientHeight,i=document.documentElement.clientWidth,l=t&&"auto"!==t?"top"===t:r.bottom+a.height+n+e>o,s=Math.max(e,Math.min(i-e-a.width,r.left+r.width/2-a.width/2));if(ln($).style.setProperty("--altcha-floating-left",`${s}px`),ln($).style.setProperty("--altcha-floating-top",l?r.top-(a.height+n)+"px":`${r.bottom+n}px`),ln($).setAttribute("data-floating-position",l?"top":"bottom"),ln(x)){const e=ln(x).getBoundingClientRect();ln(x).style.left=r.left-s+r.width/2-e.width/2+"px"}}();Et(P,ln(P)+1)}async function Pe(e={}){const{concurrency:t=Math.max(1,ln(L).workers),controller:n=new AbortController,minDuration:r=ln(L).minDuration}=e,a=performance.now();let i=null,l=null,s=!1;const c=await q("onVerify",e);if(void 0!==c)return c;$e(da.VERIFYING),Et(b,n,!0);try{if(!o)throw new Error("Secure context (HTTPS) required.");if(ln(L).mockError)throw new Error("Mock error.");if(ln(L).test)return _e("running test mode with null challenge"),await Y(Math.max(0,r-(performance.now()-a))),ln(b)?.signal.aborted?($e(),null):(Et(A,btoa(JSON.stringify({challenge:null,solution:null,test:!0})),!0),_e("verified"),Te(da.VERIFIED),h("verified",{payload:ln(A)}),{payload:ln(A)});if(i=await G(),!i)throw new Error("Failed to fetch challenge.");_e("challenge",i),"configuration"in i&&(_e("re-configuring from challenge",i.configuration),xe(i.configuration)),i.parameters.expiresAt&&function(e){ln(T)&&clearTimeout(ln(T));const t=()=>{ln(w)!==da.UNVERIFIED?(Et(g,!1),Te(da.EXPIRED)):$e(),h("expired")},n=1e3*e-Date.now();n>=1?Et(T,setTimeout(t,n),!0):t()}(i.parameters.expiresAt),s="_version"in i&&1===i._version;const e=globalThis.$altcha.algorithms.get(i.parameters.algorithm);if(!e)throw new Error(`Unsupported algorithm ${i.parameters.algorithm}.`);if(l=await Ea({challenge:i,concurrency:t,controller:n,createWorker:e,counterMode:s?"string":"uint32",onOutOfMemory:e=>{if(_e("out of memory error received"),h("outofmemory"),ln(L).retryOnOutOfMemoryError&&e>1){const t=Math.floor(e/2);return _e(`retrying with ${t} workers...`),t}},timeout:ln(L).timeout}),ln(b)?.signal.aborted)return $e(),null;if(!l)throw new Error("Failed to find solution.");_e("solution",l),await Y(Math.max(0,r-(performance.now()-a))),Et(m,i.codeChallenge||ln(L).codeChallenge||null,!0),Et(A,s?btoa(JSON.stringify(function(e,t){return{algorithm:e.parameters.algorithm,challenge:e.parameters.keyPrefix,number:t.counter,salt:"_originalSalt"in e?e._originalSalt:e.parameters.nonce,signature:e.signature,took:t.time||0}}(i,l))):btoa(JSON.stringify({challenge:{parameters:i.parameters,signature:i.signature},solution:l})),!0),ln(m)?(_e("requesting code verification"),Te(da.CODE),h("codechallenge",{codeChallenge:ln(m)})):ln(L).verifyUrl?await we():(_e("verified"),Te(da.VERIFIED),h("verified",{payload:ln(A)}))}catch(e){return _e("verification failed",e),Te(da.ERROR,String(e)),null}finally{Et(b,null)}return{challenge:i,payload:ln(A),solution:l}}vn(()=>{xe({auto:t.auto,challenge:t.challenge,display:t.display,language:t.language,name:t.name,type:t.type,workers:t.workers})}),vn(()=>{t.theme?d?.setAttribute("theme",t.theme):d?.removeAttribute("theme")}),vn(()=>{if(t.configuration)try{xe(JSON.parse(t.configuration))}catch{_e("unable to parse the `configuration` attribute (JSON expected)")}}),vn(()=>{ln(y)!==ln(L).display&&be(ln(L).display)}),vn(()=>{ln(g)&&ln(w)===da.VERIFYING&&Et(g,!1)}),vn(()=>{ln(g)||ln(w)!==da.VERIFIED||Et(g,!0)}),vn(()=>{if(!ln(g)){const e=W();e&&e.checked&&(e.checked=!1)}}),vn(()=>{ln(w)===da.VERIFIED&&W()?.setCustomValidity("")}),vn(()=>{if("onload"===ln(U)){const e=setTimeout(()=>{Pe()},1);return()=>{e&&clearTimeout(e)}}}),vn(()=>{ln(C)&&_e("error:",ln(C))}),vn(()=>{ln(A)&&ln(L).setCookie&&function(e,t={}){const{domain:n,name:r=ln(L).name,maxAge:a,path:o,sameSite:i,secure:l}=t;let s=`${encodeURIComponent(r)}=${encodeURIComponent(e)}`;n&&(s+=`; Domain=${n}`);null!=a&&(s+=`; Max-Age=${a}`);o&&(s+=`; Path=${o}`);i&&(s+=`; SameSite=${i}`);l&&(s+="; Secure");document.cookie=s}(ln(A),ln(L).setCookie)}),dr(()=>(_e("mounted","3.2.2"),d&&globalThis.$altcha.instances.add(d),Et(_,ln($)?.closest("form"),!0),ln(_)?.addEventListener("reset",ue),ln(_)?.addEventListener("submit",pe,{capture:!0}),ln(_)?.addEventListener("focusin",re),function(){Et(I,[...globalThis.$altcha.plugins].map(e=>new e(d)),!0),_e("activating plugins",ln(I).map(e=>e.constructor.name));for(const e of ln(I))e.activate()}(),ln(L).humanInteractionSignature&&(_e("human interaction signature enabled"),p=new Ta),h("load"),o||_e("secure context (HTTPS) required"),()=>{!function(){for(const e of ln(I))e.destroy()}(),d&&globalThis.$altcha.instances.delete(d),ln(T)&&clearTimeout(ln(T)),ln(_)?.removeEventListener("reset",ue),ln(_)?.removeEventListener("submit",pe,{capture:!0}),ln(_)?.removeEventListener("focusin",re),p?.destroy()}));var Oe={configure:xe,getConfiguration:function(){return{...ln(L)}},getState:function(){return ln(w)},hide:ke,log:_e,reset:$e,setState:Te,show:Ae,updateUI:Ie,verify:Pe},Re=Va();Vn("scroll",me,function(){Ie()}),Vn("click",me,function(e){const t=e.target;"floating"!==ln(L).display||!t||d?.contains(t)||t.hasAttribute("data-backdrop")||t.closest("[data-popover]")||ln(w)===da.VERIFIED||ln(L).floatingPersist||ke()}),Vn("pageshow",ge,function(e){e.persisted&&(be(ln(L).display),$e())}),Vn("resize",ge,function(){Ie()});var Le=Ce(Re),Me=e=>{Zn(e,Aa())};hr(Le,e=>{"overlay"===ln(L).display&&ln(R)&&e(Me)});var De=Ee(Le,2),Ue=Se(De),Ve=e=>{var t=Pa(),n=Ce(t),r=Ee(n,2),a=e=>{var t=Ia();vr(t,()=>document.querySelector(ln(L).overlayContent)?.innerHTML,!0),ce(t),Zn(e,t)};hr(r,e=>{ln(L).overlayContent&&e(a)}),Vn("click",n,ne,!0),Zn(e,t)};hr(Ue,e=>{"overlay"===ln(L).display&&ln(R)&&e(Ve)});var Ne=Ee(Ue,2),ze=Se(Ne),Fe=Se(ze),He=Se(Fe);{let e=mt(()=>"standard"===ln(L).display&&"onsubmit"!==ln(U)||ln(w)===da.VERIFYING);!function(e,t,n){var r;oe&&(r=ae,se());var a=new fr(e);wn(()=>{var e=t()??null;if(oe&&"["===de(r)!=(null!==e)){var o=fe();return le(o),a.anchor=o,ie(!1),a.ensure(e,e&&(t=>n(t,e))),void ie(!0)}a.ensure(e,e&&(t=>n(t,e)))},E)}(He,()=>ln(D),(t,n)=>{n(t,{get id(){return ln(M)},name:"",get required(){return ln(e)},get loading(){return ln(V)},get checked(){return ln(g)},onchange:ee,oninvalid:te})})}var Ke=Ee(He,2),qe=Se(Ke),Ye=e=>{var t=Wn();yn(()=>or(t,ln(F).verificationRequired)),Zn(e,t)},Ge=e=>{var t=Wn();yn(()=>or(t,ln(F).verifying)),Zn(e,t)},We=e=>{var t=Wn();yn(()=>or(t,ln(F).verified)),Zn(e,t)},Je=e=>{var t=Wn();yn(()=>or(t,ln(F).label)),Zn(e,t)};hr(qe,e=>{ln(w)===da.CODE&&ln(m)?e(Ye):ln(w)===da.VERIFYING?e(Ge,1):ln(w)===da.VERIFIED?e(We,2):e(Je,-1)}),ce(Ke),ce(Fe);var Ze=Ee(Fe,2),Xe=e=>{ia(e,{get strings(){return ln(F)}})};hr(Ze,e=>{ln(z)&&e(Xe)}),ce(ze);var Qe=Ee(ze,2),et=e=>{{let t=mt(()=>"bar"===ln(L).display&&ln(z));sa(e,{get logo(){return ln(t)},get strings(){return ln(F)}})}};hr(Qe,e=>{ln(N)&&e(et)});var tt=Ee(Qe,2),nt=e=>{var t=Oa();Yr(t,e=>Et(x,e),()=>ln(x)),Zn(e,t)};hr(tt,e=>{"floating"===ln(L).display&&e(nt)});var rt=Ee(tt,2),at=e=>{var t=Ra();Dr(t),yn(()=>{var e,n,r;Vr(t,"name",ln(L).name),e=t,n=ln(A),(r=zr(e)).value!==(r.value=n??void 0)&&(e.value!==n||0===n&&e.nodeName===Mr)&&(e.value=n??"")}),Zn(e,t)};hr(rt,e=>{ln(L).setCookie||e(at)}),ce(Ne);var ot=Ee(Ne,2),it=e=>{Ca(e,{get anchor(){return ln($)},onClickOutside:()=>{o&&$e()},get placement(){return ln(L).popoverPlacement},role:"alert",variant:"error",get dir(){return ln(B)},get updateUISignal(){return ln(P)},children:(e,t)=>{var n=Jn(),r=Ce(n),a=e=>{Zn(e,La())},i=e=>{var t=Ma(),n=Se(t,!0);ce(t),yn(()=>or(n,ln(F).expired)),Zn(e,t)},l=e=>{var t=Da(),n=Se(t,!0);ce(t),yn(()=>{Vr(t,"title",ln(C)),or(n,ln(F).error)}),Zn(e,t)};hr(r,e=>{ln(C)||o?ln(C)||ln(w)!==da.EXPIRED?e(l,-1):e(i,1):e(a)}),Zn(e,n)},$$slots:{default:!0}})},lt=e=>{var t=Jn();!function(e,t,n){oe&&se();var r=new fr(e);wn(()=>{var e=t();e!=e&&(e=pr),r.ensure(e,n)})}(Ce(t),()=>ln(m),e=>{{let t=mt(()=>"standard"!==ln(L).codeChallengeDisplay);Ca(e,{get anchor(){return ln($)},get backdrop(){return ln(t)},get display(){return ln(L).codeChallengeDisplay},onClose:()=>{$e()},get placement(){return ln(L).popoverPlacement},role:"dialog",get"aria-label"(){return ln(F).verificationRequired},get dir(){return ln(B)},get updateUISignal(){return ln(P)},children:(e,t)=>{var n=Ua(),r=Ce(n);xa(r,{get audioUrl(){return ln(H)},get imageUrl(){return ln(K)},onCancel:()=>$e(),onReload:()=>Pe(),onSubmit:e=>we(e),get codeChallenge(){return ln(m)},get config(){return ln(L)},get strings(){return ln(F)}});var a=Ee(r,2),o=e=>{sa(e,{get logo(){return ln(z)},get strings(){return ln(F)}})};hr(a,e=>{ln(N)&&"standard"!==ln(L).codeChallengeDisplay&&e(o)}),Zn(e,n)},$$slots:{default:!0}})}}),Zn(e,t)};hr(ot,e=>{ln(C)||ln(w)===da.EXPIRED||!o?e(it):ln(m)&&ln(w)===da.CODE&&e(lt,1)}),ce(De),Yr(De,e=>Et($,e),()=>ln($)),yn(e=>{Vr(De,"data-state",ln(w)),Vr(De,"data-display",ln(L).display||void 0),Vr(De,"data-placement",e),Vr(De,"data-visible",ln(R)||void 0),Vr(De,"dir",ln(B)),Vr(Ke,"for",ln(M)),De.dir=De.dir},[()=>function(e){switch(e){case"bar":return ln(L).barPlacement||"bottom";case"floating":return ln(L).floatingPlacement||"auto";default:return}}(ln(L).display)]),Zn(e,Re);var st=X(Oe);return r(),st},{auto:{type:"String"},challenge:{type:"String"},configuration:{type:"String"},display:{type:"String"},language:{type:"String"},name:{type:"String"},theme:{type:"String"},type:{type:"String"},workers:{type:"Number"}},[],["configure","getConfiguration","getState","hide","log","reset","setState","show","updateUI","verify"]));const Na='(function() {\n "use strict";\n function bufferStartsWith(buffer, prefix) {\n if (prefix.length > buffer.length) {\n return false;\n }\n for (let i = 0; i < prefix.length; i++) {\n if (buffer[i] !== prefix[i]) {\n return false;\n }\n }\n return true;\n }\n function bufferToHex(buffer) {\n return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");\n }\n function concatBuffers(a, b) {\n const out = new Uint8Array(a.length + b.length);\n out.set(a, 0);\n out.set(b, a.length);\n return out;\n }\n function hexToBuffer(hex) {\n if (hex.length % 2 !== 0) {\n throw new Error(`Hex string must have an even length. Got: ${hex}`);\n }\n const buffer = new ArrayBuffer(hex.length / 2);\n const view = new DataView(buffer);\n for (let i = 0; i < hex.length; i += 2) {\n const byteString = hex.substring(i, i + 2);\n const byteValue = parseInt(byteString, 16);\n view.setUint8(i / 2, byteValue);\n }\n return new Uint8Array(buffer);\n }\n async function delay(ms) {\n await new Promise((resolve) => setTimeout(resolve, ms));\n }\n function timeDuration(start) {\n return Math.floor((performance.now() - start) * 10) / 10;\n }\n class PasswordBuffer {\n constructor(nonce, mode = "uint32") {\n this.nonce = nonce;\n this.mode = mode;\n this.buffer = new Uint8Array(this.nonce.length + this.COUNTER_BYTES);\n this.buffer.set(this.nonce, 0);\n this.dataView = new DataView(this.buffer.buffer);\n }\n nonce;\n mode;\n COUNTER_BYTES = 4;\n buffer;\n dataView;\n encoder = new TextEncoder();\n /**\n * Appends the counter to the nonce buffer.\n * In \'string\' mode, encodes the counter as a UTF-8 string.\n * In \'uint32\' mode, writes the counter as a big-endian 32-bit integer.\n */\n setCounter(n) {\n if (this.mode === "string") {\n return concatBuffers(this.nonce, this.encoder.encode(n.toString()));\n }\n this.dataView.setUint32(this.nonce.length, n, false);\n return this.buffer;\n }\n }\n async function solveChallenge(options) {\n const {\n challenge,\n controller,\n counterMode = "uint32",\n counterStart = 0,\n counterStep = 1,\n deriveKey: deriveKey2,\n timeout = 9e4\n } = options;\n const { nonce, keyPrefix, salt } = challenge.parameters;\n const nonceBuf = hexToBuffer(nonce);\n const saltBuf = hexToBuffer(salt);\n const keyPrefixBuf = keyPrefix.length % 2 === 0 ? hexToBuffer(keyPrefix) : null;\n const password = new PasswordBuffer(nonceBuf, counterMode);\n const start = performance.now();\n let counter = counterStart;\n let iterations = 0;\n let derivedKeyHex = "";\n let lastYield = start;\n while (true) {\n if (controller?.signal.aborted || timeout && iterations % 10 === 0 && performance.now() - start > timeout) {\n return null;\n }\n const { derivedKey } = await deriveKey2(\n challenge.parameters,\n saltBuf,\n password.setCounter(counter)\n );\n if (iterations % 10 === 0 && performance.now() - lastYield > 200) {\n await delay(0);\n lastYield = performance.now();\n }\n if (keyPrefixBuf ? bufferStartsWith(derivedKey, keyPrefixBuf) : bufferToHex(derivedKey).startsWith(keyPrefix)) {\n derivedKeyHex = bufferToHex(derivedKey);\n break;\n }\n counter = counter + counterStep;\n iterations = iterations + 1;\n }\n return {\n counter,\n derivedKey: derivedKeyHex,\n time: timeDuration(start)\n };\n }\n function handler(options) {\n const { deriveKey: deriveKey2 } = options;\n let controller = void 0;\n self.onmessage = async (message) => {\n const { challenge, counterMode, counterStart, counterStep, timeout, type } = message.data;\n if (type === "abort") {\n controller?.abort();\n } else if (type === "work") {\n controller = new AbortController();\n let solution;\n try {\n solution = await solveChallenge({\n challenge,\n controller,\n counterStart,\n counterStep,\n deriveKey: deriveKey2,\n counterMode,\n timeout\n });\n } catch (err) {\n return self.postMessage({ error: err });\n }\n self.postMessage(solution);\n }\n };\n }\n function getDigest(algorithm) {\n switch (algorithm) {\n case "PBKDF2/SHA-512":\n return "SHA-512";\n case "PBKDF2/SHA-384":\n return "SHA-384";\n case "PBKDF2/SHA-256":\n default:\n return "SHA-256";\n }\n }\n async function deriveKey(parameters, salt, password) {\n const { algorithm, cost, keyLength = 32 } = parameters;\n const passwordKey = await crypto.subtle.importKey(\n "raw",\n password,\n { name: "PBKDF2" },\n false,\n ["deriveKey"]\n );\n const derivedKey = await crypto.subtle.deriveKey(\n {\n name: "PBKDF2",\n salt,\n iterations: cost,\n hash: getDigest(algorithm)\n },\n passwordKey,\n { name: "AES-GCM", length: keyLength * 8 },\n true,\n ["encrypt"]\n );\n return {\n derivedKey: new Uint8Array(await crypto.subtle.exportKey("raw", derivedKey))\n };\n }\n handler({\n deriveKey\n });\n})();\n',za="undefined"!=typeof self&&self.Blob&&new Blob(["(self.URL || self.webkitURL).revokeObjectURL(self.location.href);",Na],{type:"text/javascript;charset=utf-8"});function ja(e){let t;try{if(t=za&&(self.URL||self.webkitURL).createObjectURL(za),!t)throw"";const n=new Worker(t,{name:e?.name});return n.addEventListener("error",()=>{(self.URL||self.webkitURL).revokeObjectURL(t)}),n}catch(t){return new Worker("data:text/javascript;charset=utf-8,"+encodeURIComponent(Na),{name:e?.name})}}const Ba='(function() {\n "use strict";\n function bufferStartsWith(buffer, prefix) {\n if (prefix.length > buffer.length) {\n return false;\n }\n for (let i = 0; i < prefix.length; i++) {\n if (buffer[i] !== prefix[i]) {\n return false;\n }\n }\n return true;\n }\n function bufferToHex(buffer) {\n return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");\n }\n function concatBuffers(a, b) {\n const out = new Uint8Array(a.length + b.length);\n out.set(a, 0);\n out.set(b, a.length);\n return out;\n }\n function hexToBuffer(hex) {\n if (hex.length % 2 !== 0) {\n throw new Error(`Hex string must have an even length. Got: ${hex}`);\n }\n const buffer = new ArrayBuffer(hex.length / 2);\n const view = new DataView(buffer);\n for (let i = 0; i < hex.length; i += 2) {\n const byteString = hex.substring(i, i + 2);\n const byteValue = parseInt(byteString, 16);\n view.setUint8(i / 2, byteValue);\n }\n return new Uint8Array(buffer);\n }\n async function delay(ms) {\n await new Promise((resolve) => setTimeout(resolve, ms));\n }\n function timeDuration(start) {\n return Math.floor((performance.now() - start) * 10) / 10;\n }\n class PasswordBuffer {\n constructor(nonce, mode = "uint32") {\n this.nonce = nonce;\n this.mode = mode;\n this.buffer = new Uint8Array(this.nonce.length + this.COUNTER_BYTES);\n this.buffer.set(this.nonce, 0);\n this.dataView = new DataView(this.buffer.buffer);\n }\n nonce;\n mode;\n COUNTER_BYTES = 4;\n buffer;\n dataView;\n encoder = new TextEncoder();\n /**\n * Appends the counter to the nonce buffer.\n * In \'string\' mode, encodes the counter as a UTF-8 string.\n * In \'uint32\' mode, writes the counter as a big-endian 32-bit integer.\n */\n setCounter(n) {\n if (this.mode === "string") {\n return concatBuffers(this.nonce, this.encoder.encode(n.toString()));\n }\n this.dataView.setUint32(this.nonce.length, n, false);\n return this.buffer;\n }\n }\n async function solveChallenge(options) {\n const {\n challenge,\n controller,\n counterMode = "uint32",\n counterStart = 0,\n counterStep = 1,\n deriveKey: deriveKey2,\n timeout = 9e4\n } = options;\n const { nonce, keyPrefix, salt } = challenge.parameters;\n const nonceBuf = hexToBuffer(nonce);\n const saltBuf = hexToBuffer(salt);\n const keyPrefixBuf = keyPrefix.length % 2 === 0 ? hexToBuffer(keyPrefix) : null;\n const password = new PasswordBuffer(nonceBuf, counterMode);\n const start = performance.now();\n let counter = counterStart;\n let iterations = 0;\n let derivedKeyHex = "";\n let lastYield = start;\n while (true) {\n if (controller?.signal.aborted || timeout && iterations % 10 === 0 && performance.now() - start > timeout) {\n return null;\n }\n const { derivedKey } = await deriveKey2(\n challenge.parameters,\n saltBuf,\n password.setCounter(counter)\n );\n if (iterations % 10 === 0 && performance.now() - lastYield > 200) {\n await delay(0);\n lastYield = performance.now();\n }\n if (keyPrefixBuf ? bufferStartsWith(derivedKey, keyPrefixBuf) : bufferToHex(derivedKey).startsWith(keyPrefix)) {\n derivedKeyHex = bufferToHex(derivedKey);\n break;\n }\n counter = counter + counterStep;\n iterations = iterations + 1;\n }\n return {\n counter,\n derivedKey: derivedKeyHex,\n time: timeDuration(start)\n };\n }\n function handler(options) {\n const { deriveKey: deriveKey2 } = options;\n let controller = void 0;\n self.onmessage = async (message) => {\n const { challenge, counterMode, counterStart, counterStep, timeout, type } = message.data;\n if (type === "abort") {\n controller?.abort();\n } else if (type === "work") {\n controller = new AbortController();\n let solution;\n try {\n solution = await solveChallenge({\n challenge,\n controller,\n counterStart,\n counterStep,\n deriveKey: deriveKey2,\n counterMode,\n timeout\n });\n } catch (err) {\n return self.postMessage({ error: err });\n }\n self.postMessage(solution);\n }\n };\n }\n async function deriveKey(parameters, salt, password) {\n const { algorithm, keyLength = 32 } = parameters;\n const iterations = Math.max(1, parameters.cost);\n let data = void 0;\n let derivedKey = void 0;\n for (let i = 0; i < iterations; i++) {\n if (i === 0) {\n data = concatBuffers(salt, password);\n } else {\n data = derivedKey;\n }\n derivedKey = new Uint8Array(\n (await crypto.subtle.digest(algorithm, data)).slice(0, keyLength)\n );\n }\n return {\n parameters: {},\n derivedKey\n };\n }\n handler({\n deriveKey\n });\n})();\n',Fa="undefined"!=typeof self&&self.Blob&&new Blob(["(self.URL || self.webkitURL).revokeObjectURL(self.location.href);",Ba],{type:"text/javascript;charset=utf-8"});function Ha(e){let t;try{if(t=Fa&&(self.URL||self.webkitURL).createObjectURL(Fa),!t)throw"";const n=new Worker(t,{name:e?.name});return n.addEventListener("error",()=>{(self.URL||self.webkitURL).revokeObjectURL(t)}),n}catch(t){return new Worker("data:text/javascript;charset=utf-8,"+encodeURIComponent(Ba),{name:e?.name})}}!function(e,t="altcha-css"){if("undefined"!=typeof document&&document&&!document.getElementById(t)){const n=document.createElement("style");n.id=t,n.textContent=e;const r=document.currentScript?.nonce??document.querySelector('meta[name="csp-nonce"]')?.content;r&&(n.nonce=r),document.head.appendChild(n)}}(':root {\n --altcha-border-color: var(--altcha-color-neutral);\n --altcha-border-width: 1px;\n --altcha-border-radius: 6px;\n --altcha-color-base: light-dark(oklch(100% 0.00011 271.152), oklch(20.904% 0.00002 271.152));\n --altcha-color-base-content: light-dark(\n \toklch(20.904% 0.00002 271.152),\n \toklch(100% 0.00011 271.152)\n );\n --altcha-color-error: oklch(51.284% 0.20527 28.678);\n --altcha-color-error-content: oklch(100% 0.00011 271.152);\n --altcha-color-neutral: light-dark(oklch(83.591% 0.0001 271.152), oklch(46.04% 0.00005 271.152));\n --altcha-color-neutral-content: light-dark(\n \toklch(46.76% 0.00005 271.152),\n \toklch(100% 0.00011 271.152)\n );\n --altcha-color-primary: oklch(40.279% 0.2449 268.131);\n --altcha-color-primary-content: oklch(100% 0.00011 271.152);\n --altcha-color-success: oklch(55.748% 0.18968 142.511);\n --altcha-color-success-content: oklch(100% 0.00011 271.152);\n --altcha-checkbox-border-color: light-dark(\n \toklch(66.494% 0.00233 15.434),\n \toklch(51.028% 0.00006 271.152)\n );\n --altcha-checkbox-border-radius: 5px;\n --altcha-checkbox-border-width: var(--altcha-border-width);\n --altcha-checkbox-outline: 2px solid var(--altcha-checkbox-outline-color);\n --altcha-checkbox-outline-color: -webkit-focus-ring-color;\n --altcha-checkbox-outline-offset: 2px;\n --altcha-checkbox-size: 22px;\n --altcha-checkbox-transition-duration: var(--altcha-transition-duration);\n --altcha-input-background-color: var(--altcha-color-base);\n --altcha-input-border-radius: 3px;\n --altcha-input-border-width: 1px;\n --altcha-input-color: var(--altcha-color-base-content);\n --altcha-max-width: 320px;\n --altcha-padding: 0.75rem;\n --altcha-popover-arrow-size: 6px;\n --altcha-popover-color: var(--altcha-border-color);\n --altcha-shadow: drop-shadow(3px 3px 6px oklch(0% 0 0 / 0.2));\n --altcha-spinner-color: var(--altcha-color-base-content);\n --altcha-switch-background-color: var(--altcha-color-neutral);\n --altcha-switch-border-radius: calc(infinity * 1px);\n --altcha-switch-height: var(--altcha-checkbox-size);\n --altcha-switch-padding: 0.25rem;\n --altcha-switch-width: calc(var(--altcha-checkbox-size) * 1.75);\n --altcha-switch-toggle-border-radius: 100%;\n --altcha-switch-toggle-color: var(--altcha-color-neutral-content);\n --altcha-switch-toggle-size: calc(\n \tvar(--altcha-switch-height) - calc(var(--altcha-switch-padding) * 2)\n );\n --altcha-transition-duration: 0.6s;\n --altcha-z-index: 99999999;\n --altcha-z-index-popover: 999999999;\n}\n\n@supports (-moz-appearance: none) {\n :root {\n --altcha-checkbox-outline-color: var(--altcha-color-primary);\n }\n}\n.altcha {\n all: revert-layer;\n display: none;\n font-family: inherit;\n font-size: inherit;\n position: relative;\n}\n.altcha[data-visible] {\n display: block;\n}\n.altcha-popover, .altcha-popover * {\n all: revert-layer;\n box-sizing: border-box;\n font-family: inherit;\n font-size: inherit;\n line-height: 1.25;\n}\n.altcha * {\n all: revert-layer;\n box-sizing: border-box;\n font-family: inherit;\n font-size: inherit;\n line-height: 1.25;\n}\n.altcha a, .altcha-popover a {\n color: currentColor;\n text-decoration: none;\n}\n.altcha a:hover, .altcha-popover a:hover {\n color: currentColor;\n}\n.altcha-main {\n align-items: start;\n background-color: var(--altcha-color-base);\n border: var(--altcha-border-width, 1px) solid var(--altcha-border-color);\n border-radius: var(--altcha-border-radius, 0);\n color: var(--altcha-color-base-content);\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n justify-content: space-between;\n padding: var(--altcha-padding);\n max-width: var(--altcha-max-width, 100%);\n}\n.altcha-main > * {\n display: flex;\n width: 100%;\n}\n.altcha-main > *:first-child {\n flex-grow: 1;\n}\n.altcha-checkbox-wrap {\n align-items: center;\n display: flex;\n flex-direction: row;\n flex-grow: 1;\n gap: 0.5rem;\n}\n.altcha-checkbox-wrap > * {\n display: flex;\n}\n.altcha-logo {\n opacity: 0.7;\n}\n.altcha-footer {\n align-items: center;\n display: flex;\n flex-grow: 1;\n gap: 0.5rem;\n justify-content: flex-end;\n font-size: 0.7rem;\n opacity: 0.7;\n}\n.altcha-footer p {\n margin: 0;\n padding: 0;\n}\n.altcha-error {\n font-size: 0.85rem;\n}\n.altcha-button {\n align-items: center;\n background: var(--altcha-color-primary);\n border: var(--altcha-input-border-width) solid var(--altcha-color-primary);\n border-radius: var(--altcha-input-border-radius);\n color: var(--altcha-color-primary-content);\n cursor: pointer;\n display: flex;\n font-size: 0.9rem;\n gap: 0.5rem;\n padding: 0.35rem;\n}\n.altcha-button:focus {\n border-color: var(--altcha-color-primary);\n outline: var(--altcha-checkbox-outline);\n outline-offset: var(--altcha-checkbox-outline-offset);\n}\n.altcha-button > .altcha-spinner, .altcha-button > svg {\n height: 20px;\n width: 20px;\n}\n.altcha-button-secondary {\n background: transparent;\n border-color: var(--altcha-color-neutral);\n color: var(--altcha-color-neutral-content);\n}\n.altcha-input {\n background: var(--altcha-input-background-color);\n border: var(--altcha-input-border-width) solid var(--altcha-color-neutral);\n border-radius: var(--altcha-input-border-radius);\n color: var(--altcha-input-color);\n flex-grow: 1;\n font-size: 1rem;\n min-width: 0;\n padding: 0.25rem;\n width: auto;\n}\n.altcha-input:focus {\n border-color: var(--altcha-color-primary);\n outline: var(--altcha-checkbox-outline);\n outline-offset: var(--altcha-checkbox-outline-offset);\n}\n.altcha-spinner {\n animation: altcha-rotate 0.6s linear infinite;\n border-radius: 100%;\n border: var(--altcha-checkbox-border-width) solid var(--altcha-spinner-color);\n border-bottom-color: transparent;\n border-right-color: transparent;\n opacity: 0.7;\n}\n.altcha-popover {\n background-color: var(--altcha-color-base);\n border: var(--altcha-border-width) solid var(--altcha-border-color);\n border-radius: var(--altcha-border-radius);\n color: var(--altcha-color-base-content);\n filter: var(--altcha-shadow);\n position: absolute;\n left: calc(var(--altcha-padding) / 2);\n max-width: calc(var(--altcha-max-width) - var(--altcha-padding));\n top: calc(var(--altcha-padding) + var(--altcha-checkbox-size) + var(--altcha-popover-arrow-size));\n z-index: var(--altcha-z-index-popover);\n}\n.altcha-popover-arrow {\n border: var(--altcha-popover-arrow-size) solid transparent;\n border-bottom-color: var(--altcha-popover-color);\n content: "";\n height: 0;\n left: calc(var(--altcha-checkbox-size) / 2);\n position: absolute;\n top: calc(var(--altcha-popover-arrow-size) * -2);\n width: 0;\n}\n.altcha-popover-content {\n max-height: 100dvh;\n overflow: auto;\n padding: var(--altcha-padding);\n}\n.altcha-popover[data-top=true][data-display=standard] {\n bottom: calc(100% - (var(--altcha-padding) - var(--altcha-popover-arrow-size)));\n top: auto;\n}\n.altcha-popover[data-top=true][data-display=standard] .altcha-popover-arrow {\n border-bottom-color: transparent;\n border-top-color: var(--altcha-popover-color);\n bottom: calc(var(--altcha-popover-arrow-size) * -2);\n top: auto;\n}\n.altcha-popover[data-variant=error] {\n --altcha-popover-color: var(--altcha-color-error);\n background-color: var(--altcha-color-error);\n border-color: var(--altcha-color-error);\n color: var(--altcha-color-error-content);\n}\n.altcha-popover[data-variant=error] .altcha-popover-content {\n padding: calc(var(--altcha-padding) / 1.5) var(--altcha-padding);\n}\n.altcha-popover[data-display=overlay] {\n animation: altcha-overlay-slidein 0.5s forwards;\n left: 50%;\n position: fixed;\n top: 45%;\n transform: translate(-50%, -50%);\n width: var(--altcha-max-width);\n z-index: var(--altcha-z-index);\n}\n.altcha-popover[data-display=bottomsheet] {\n animation: altcha-bottomsheet-slideup 0.5s forwards;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n border-bottom: 0;\n bottom: -100%;\n left: 50%;\n position: fixed;\n top: auto;\n transform: translate(-50%, 0);\n width: var(--altcha-max-width);\n z-index: var(--altcha-z-index);\n}\n.altcha-popover[data-display=bottomsheet] .altcha-popover-content {\n padding-bottom: calc(var(--altcha-padding) * 2);\n}\n.altcha-popover-backdrop {\n background: var(--altcha-color-base-content);\n bottom: 0;\n left: 0;\n opacity: 0.1;\n position: fixed;\n right: 0;\n top: 0;\n transition: opacity 0.5s;\n z-index: var(--altcha-z-index);\n}\n.altcha-popover-close {\n color: var(--altcha-color-base-content);\n cursor: pointer;\n display: inline-block;\n font-size: 1rem;\n height: 1.25rem;\n line-height: 0.95;\n position: absolute;\n right: 0;\n text-align: center;\n text-shadow: 0 0 1px var(--altcha-color-base);\n top: -1.5rem;\n width: 1.25rem;\n z-index: var(--altcha-z-index);\n}\n[dir=rtl] .altcha-popover {\n left: auto;\n right: calc(var(--altcha-padding) / 2);\n}\n[dir=rtl] .altcha-popover-arrow {\n left: auto;\n right: calc(var(--altcha-checkbox-size) / 2);\n}\n[dir=rtl] .altcha-popover-close {\n left: 0;\n right: auto;\n}\n.altcha-popover[data-display=bottomsheet] .altcha-footer, .altcha-popover[data-display=overlay] .altcha-footer {\n align-items: center;\n justify-content: center;\n padding-top: 1rem;\n gap: 0.5rem;\n}\n.altcha-popover[data-display=bottomsheet] .altcha-footer svg, .altcha-popover[data-display=overlay] .altcha-footer svg {\n height: 18px;\n width: 18px;\n vertical-align: middle;\n}\n.altcha-code-challenge > form {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n.altcha-code-challenge-title {\n font-weight: 600;\n}\n.altcha-code-challenge-text {\n font-size: 0.85rem;\n}\n.altcha-code-challenge-image {\n background: white;\n border: var(--altcha-input-border-width) solid var(--altcha-color-neutral);\n border-radius: var(--altcha-input-border-radius);\n object-fit: contain;\n height: 50px;\n}\n.altcha-code-challenge-row {\n display: flex;\n gap: 0.5rem;\n}\n.altcha-code-challenge-buttons {\n align-items: center;\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n margin-top: var(--altcha-padding);\n justify-content: space-between;\n}\n.altcha-code-challenge-buttons button {\n justify-content: center;\n width: 100%;\n}\n.altcha-checkbox {\n cursor: pointer;\n height: var(--altcha-checkbox-size);\n position: relative;\n width: var(--altcha-checkbox-size);\n}\n.altcha-checkbox input {\n appearance: none;\n background: var(--altcha-input-background-color);\n border: var(--altcha-checkbox-border-width, 2px) solid var(--altcha-checkbox-border-color);\n border-radius: var(--altcha-checkbox-border-radius);\n cursor: pointer;\n height: var(--altcha-checkbox-size);\n left: 0;\n margin: 0;\n padding: 0;\n position: absolute;\n top: 0;\n width: var(--altcha-checkbox-size);\n}\n@supports (hanging-punctuation: first) and (font: -apple-system-body) and (-webkit-appearance: none) {\n .altcha-checkbox input {\n /* Safari-only: fixes focus outline */\n }\n .altcha-checkbox input:focus {\n outline-width: 2px;\n outline-style: solid;\n }\n}\n.altcha-checkbox input:before {\n border-radius: var(--altcha-checkbox-border-radius);\n content: "";\n width: 100%;\n height: 100%;\n background: var(--altcha-color-neutral);\n display: block;\n transform: scale(0);\n}\n.altcha-checkbox input:checked {\n background-color: var(--altcha-color-success);\n border-color: var(--altcha-color-success);\n}\n.altcha-checkbox input:checked::before {\n background-color: var(--altcha-color-success);\n opacity: 0;\n transform: scale(2.2);\n transition: all var(--altcha-checkbox-transition-duration) ease;\n transition-delay: 0.1s;\n}\n.altcha-checkbox svg {\n --altcha-radio-svg-size: calc(var(--altcha-checkbox-size) * 0.5);\n --altcha-radio-svg-offset: calc(var(--altcha-checkbox-size) * 0.25);\n fill: none;\n left: var(--altcha-radio-svg-offset);\n height: var(--altcha-radio-svg-size);\n opacity: 0;\n position: absolute;\n stroke: currentColor;\n stroke-width: 2;\n stroke-linecap: round;\n stroke-linejoin: round;\n stroke-dasharray: 16px;\n stroke-dashoffset: 16px;\n top: var(--altcha-radio-svg-offset);\n transform: translate3d(0, 0, 0);\n width: var(--altcha-radio-svg-size);\n}\n.altcha-checkbox input:checked + svg {\n color: var(--altcha-color-success-content);\n opacity: 1;\n stroke-dashoffset: 0;\n transition: all var(--altcha-checkbox-transition-duration) ease;\n transition-delay: 0.1s;\n}\n.altcha-checkbox-spinner {\n display: none;\n left: 0;\n height: var(--altcha-checkbox-size);\n position: absolute;\n top: 0;\n width: var(--altcha-checkbox-size);\n}\n.altcha-checkbox[data-loading=true] input {\n appearance: none;\n opacity: 0;\n pointer-events: none;\n}\n.altcha-checkbox[data-loading=true] .altcha-checkbox-spinner {\n display: block;\n}\n.altcha-checkbox-native {\n height: var(--altcha-checkbox-size);\n position: relative;\n width: var(--altcha-checkbox-size);\n}\n.altcha-checkbox-native input {\n height: var(--altcha-checkbox-size);\n margin: 0;\n width: var(--altcha-checkbox-size);\n}\n.altcha-checkbox-native-spinner {\n display: none;\n left: 0;\n height: var(--altcha-checkbox-size);\n position: absolute;\n top: 0;\n width: var(--altcha-checkbox-size);\n}\n.altcha-checkbox-native[data-loading=true] input {\n appearance: none;\n opacity: 0;\n pointer-events: none;\n}\n.altcha-checkbox-native[data-loading=true] .altcha-checkbox-native-spinner {\n display: block;\n}\n.altcha-switch {\n align-items: center;\n border-radius: var(--altcha-switch-border-radius);\n background-color: var(--altcha-switch-background-color);\n display: flex;\n height: var(--altcha-switch-height);\n padding: var(--altcha-switch-padding);\n position: relative;\n width: var(--altcha-switch-width);\n}\n.altcha-switch:focus-within {\n outline: var(--altcha-checkbox-outline);\n outline-offset: var(--altcha-checkbox-outline-offset);\n}\n.altcha-switch input {\n appearance: none;\n cursor: pointer;\n height: 100%;\n left: 0;\n opacity: 0;\n position: absolute;\n top: 0;\n width: 100%;\n}\n.altcha-switch-toggle {\n align-items: center;\n background-color: var(--altcha-switch-toggle-color);\n border-radius: var(--altcha-switch-toggle-border-radius);\n cursor: pointer;\n display: flex;\n height: var(--altcha-switch-toggle-size);\n justify-content: center;\n left: var(--altcha-switch-padding);\n position: absolute;\n transition: width 150ms ease-out, left 150ms ease-out;\n width: var(--altcha-switch-toggle-size);\n}\n.altcha-switch-spinner {\n display: none;\n height: var(--altcha-switch-toggle-size);\n width: var(--altcha-switch-toggle-size);\n}\n.altcha-switch[data-loading=true] {\n pointer-events: none;\n}\n.altcha-switch[data-loading=true] .altcha-switch-spinner {\n display: block;\n}\n.altcha-switch[data-loading=true] .altcha-switch-toggle {\n background-color: transparent;\n left: calc(50% - var(--altcha-switch-toggle-size) / 2);\n}\n[data-state=verified] .altcha-switch {\n --altcha-switch-background-color: var(--altcha-color-success);\n}\n[data-state=verified] .altcha-switch-toggle {\n background-color: var(--altcha-color-success-content);\n left: calc(100% - var(--altcha-switch-height) + var(--altcha-switch-padding));\n}\n[dir=rtl] .altcha-switch-toggle {\n left: calc(100% - var(--altcha-switch-height) + var(--altcha-switch-padding));\n}\n[dir=rtl][data-state=verified] .altcha-switch-toggle {\n left: var(--altcha-switch-padding);\n}\n.altcha-floating-arrow {\n border: 6px solid transparent;\n border-bottom-color: var(--altcha-border-color);\n content: "";\n height: 0;\n left: 12px;\n position: absolute;\n top: -12px;\n width: 0;\n}\n.altcha-overlay-backdrop {\n bottom: 0;\n left: 0;\n position: fixed;\n right: 0;\n top: 0;\n transition: opacity var(--altcha-transition-duration);\n z-index: var(--altcha-z-index);\n}\n.altcha-overlay-close {\n display: inline-block;\n color: currentColor;\n cursor: pointer;\n font-size: 1rem;\n height: 1rem;\n line-height: 0.85;\n position: absolute;\n right: 0;\n text-align: center;\n text-shadow: 0 0 1px var(--altcha-color-base);\n top: -1.5rem;\n width: 1rem;\n z-index: var(--altcha-z-index);\n}\n.altcha[data-display=overlay] {\n animation: altcha-overlay-slidein var(--altcha-transition-duration) forwards;\n filter: var(--altcha-shadow);\n left: 50%;\n opacity: 0;\n position: fixed;\n top: 45%;\n transform: translate(-50%, -50%);\n z-index: var(--altcha-z-index);\n}\n.altcha[data-display=overlay] .altcha-main {\n width: var(--altcha-max-width);\n}\n.altcha[data-display=floating] {\n display: none;\n filter: var(--altcha-shadow);\n left: var(--altcha-floating-left, -100%);\n position: fixed;\n top: var(--altcha-floating-top, -100%);\n z-index: var(--altcha-z-index);\n}\n.altcha[data-display=floating] .altcha-main {\n width: var(--altcha-max-width);\n}\n.altcha[data-display=floating][data-floating-position=top] .altcha-floating-arrow {\n border-bottom-color: transparent;\n border-top-color: var(--altcha-border-color);\n bottom: -12px;\n top: auto;\n}\n.altcha[data-display=floating][data-visible] {\n display: flex;\n}\n.altcha[data-display=bar] {\n bottom: -100%;\n filter: var(--altcha-shadow);\n left: 0;\n position: fixed;\n right: 0;\n transition: bottom var(--altcha-transition-duration), top var(--altcha-transition-duration);\n z-index: var(--altcha-z-index);\n}\n.altcha[data-display=bar] .altcha-main {\n align-items: center;\n border-radius: 0;\n border-width: var(--altcha-border-width) 0 0 0;\n flex-direction: row;\n max-width: 100% !important;\n}\n.altcha[data-display=bar] .altcha-main > * {\n width: auto;\n}\n.altcha[data-display=bar][data-placement=top] {\n bottom: auto;\n top: -100%;\n}\n.altcha[data-display=bar][data-placement=top] .altcha-main {\n border-width: 0 0 var(--altcha-border-width) 0;\n}\n.altcha[data-display=bar][data-placement=bottom]:not([data-state=unverified]) {\n bottom: 0;\n}\n.altcha[data-display=bar][data-placement=top]:not([data-state=unverified]) {\n top: 0;\n}\n.altcha[data-display=invisible] {\n display: none;\n}\n\n@keyframes altcha-rotate {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n}\n@keyframes altcha-bottomsheet-slideup {\n 100% {\n bottom: 0;\n }\n}\n@keyframes altcha-overlay-slidein {\n 100% {\n opacity: 1;\n top: 50%;\n }\n}'),$altcha.algorithms.set("SHA-256",()=>new Ha),$altcha.algorithms.set("SHA-384",()=>new Ha),$altcha.algorithms.set("SHA-512",()=>new Ha),$altcha.algorithms.set("PBKDF2/SHA-256",()=>new ja),$altcha.algorithms.set("PBKDF2/SHA-384",()=>new ja),$altcha.algorithms.set("PBKDF2/SHA-512",()=>new ja); diff --git a/Assets/js/cap.min.js b/Assets/js/cap.min.js new file mode 100644 index 0000000..a25d2de --- /dev/null +++ b/Assets/js/cap.min.js @@ -0,0 +1 @@ +(()=>{const e="vibrate"in navigator&&!window.matchMedia("(prefers-reduced-motion: reduce)").matches;if("undefined"==typeof window)return;const t=["#f5c2e7","#cba6f7","#f38ba8","#fab387","#f9e2af","#a6e3a1","#94e2d5","#89dceb","#b4befe"],i=(e,i,s)=>{const r=0===i?"9999px":"0",n=i===s-1?"9999px":"0";return`color:#1e1e2e;background:${(e=>{if("cap"===e)return"#89b4fa";let i=0;for(let t=0;t{if(window.CAP_SILENT||"debug"===e&&!window.CAP_DEBUG)return;const r=t.map(e=>`%c${e}`).join(" "),n=t.map((e,s)=>i(e,s,t.length));console["debug"===e?"log":e](r,...n,...s)};const r=e=>e?["cap",e]:["cap"],n=e=>`${Math.round(performance.now()-e)}ms`,a=(e,t)=>Object.assign(new Error(t),{code:e}),o=(e,t={})=>window?.CAP_CUSTOM_FETCH?window.CAP_CUSTOM_FETCH(e,t):fetch(e,t),l=(e,t)=>t?t.aborted?Promise.reject(a("aborted","aborted")):Promise.race([e,new Promise((e,i)=>t.addEventListener("abort",()=>i(a("aborted","aborted")),{once:!0}))]):e,c="initial-state,verifying-label,solved-label,error-label,required-label,troubleshooting-label,group-aria-label,verify-aria-label,verifying-aria-label,verified-aria-label,error-aria-label".split(","),h={"zh-cn":"确认你是真人/正在验证.../你是真人/出错了。请重试。/请确认你是真人/故障排除/Cap 验证/点击以确认你是真人/正在验证你是真人,请稍候/我们已确认你是真人,你可以继续了/发生错误,请重试",es:"Verifica que eres humano/Verificando.../Eres humano/Error. Inténtalo de nuevo./Verifica que eres humano/Solucionar problemas/Verificación de Cap/Haz clic para verificar que eres humano/Verificando que eres humano, espera/Hemos verificado que eres humano, ya puedes continuar/Se produjo un error, inténtalo de nuevo",hi:"पुष्टि करें कि आप मानव हैं/सत्यापित हो रहा है.../आप मानव हैं/त्रुटि। पुनः प्रयास करें।/कृपया पुष्टि करें कि आप मानव हैं/समस्या निवारण/Cap सत्यापन/पुष्टि करने के लिए क्लिक करें कि आप मानव हैं/सत्यापित किया जा रहा है कि आप मानव हैं, प्रतीक्षा करें/हमने पुष्टि कर दी है कि आप मानव हैं, अब आप जारी रख सकते हैं/एक त्रुटि हुई, पुनः प्रयास करें",ar:"تحقق من أنك إنسان/جارٍ التحقق.../أنت إنسان/خطأ. حاول مرة أخرى./يرجى التحقق من أنك إنسان/استكشاف الأخطاء وإصلاحها/تحقق Cap/انقر للتحقق من أنك إنسان/جارٍ التحقق من أنك إنسان، يرجى الانتظار/لقد تحققنا من أنك إنسان، يمكنك المتابعة الآن/حدث خطأ، يرجى المحاولة مرة أخرى",pt:"Sou humano/Verificando.../Você é humano/Erro. Tente novamente./Verifique que você é humano/Solução de problemas/Verificação Cap/Clique para verificar que você é humano/Verificando que você é humano, aguarde/Verificamos que você é humano, você pode continuar/Ocorreu um erro, tente novamente",ru:"Я человек/Проверка.../Вы человек/Ошибка. Повторите./Подтвердите, что вы человек/Устранение неполадок/Проверка Cap/Нажмите, чтобы подтвердить, что вы человек/Проверяем, что вы человек, подождите/Мы подтвердили, что вы человек, можете продолжить/Произошла ошибка, повторите попытку",ja:"私は人間です/確認中.../あなたは人間です/エラー。再試行を。/あなたが人間であることを確認してください/トラブルシューティング/Cap 認証/クリックして人間であることを確認/あなたが人間であることを確認しています。お待ちください/あなたが人間であることを確認しました。続行できます/エラーが発生しました。もう一度お試しください",de:"Ich bin ein Mensch/Wird überprüft.../Sie sind ein Mensch/Fehler. Erneut versuchen./Bitte bestätigen Sie, dass Sie ein Mensch sind/Fehlerbehebung/Cap-Verifizierung/Klicken, um zu bestätigen, dass Sie ein Mensch sind/Überprüfung, ob Sie ein Mensch sind, bitte warten/Wir haben bestätigt, dass Sie ein Mensch sind, Sie können fortfahren/Ein Fehler ist aufgetreten, bitte erneut versuchen",fr:"Je suis humain/Vérification.../Vous êtes humain/Erreur. Réessayez./Veuillez vérifier que vous êtes humain/Dépannage/Vérification Cap/Cliquez pour vérifier que vous êtes humain/Vérification que vous êtes humain, veuillez patienter/Nous avons vérifié que vous êtes humain, vous pouvez continuer/Une erreur s'est produite, veuillez réessayer"};function d(e,t){let i=function(e){let t=2166136261;for(let i=0;i>>0}(e),s="";function r(){return i^=i<<13,i^=i>>>17,i^=i<<5,i>>>0}for(;s.length{const t=atob(e),i=new Uint8Array(t.length);for(let e=0;ei.close()).catch(()=>{});const r=[];for(;;){const{done:e,value:t}=await s.read();if(e)break;r.push(t)}let n=0;for(const e of r)n+=e.length;const a=new Uint8Array(n);let o=0;for(const e of r)a.set(e,o),o+=e.length;return a}catch{}return u||(u=new Promise((e,t)=>{const i=window.CAP_PAKO_URL||"https://cdn.jsdelivr.net/npm/pako@2.1.0/dist/pako_inflate.min.js";s.debug(r("instr"),"DecompressionStream unavailable, loading pako from",i);const n=document.createElement("script");n.src=i;const a=window.CAP_SCRIPT_NONCE||window.CAP_CSS_NONCE;a&&n.setAttribute("nonce",a),n.onload=()=>{window.pako?.inflateRaw?e(window.pako):t(new Error("pako loaded but inflateRaw is missing"))},n.onerror=()=>{u=null,t(new Error(`failed to load pako fallback from ${i}`))},document.head.appendChild(n)})),(await u).inflateRaw(e)}(t));return new Promise(e=>{var t=setTimeout(()=>{n(),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 n(){r||(r=!0,clearTimeout(t),window.removeEventListener("message",a),s.parentNode&&s.parentNode.removeChild(s))}function a(t){if(s.contentWindow&&t.source===s.contentWindow){var i=t.data;i&&"object"==typeof i&&("cap:instr"===i.type?(n(),i.blocked?e({__blocked:!0,blockReason:i.blockReason||"automated_browser"}):i.result?e(i.result):e({__timeout:!0})):"cap:error"===i.type&&(n(),e({__timeout:!0})))}}window.addEventListener("message",a);const o=window.CAP_SCRIPT_NONCE||window.CAP_CSS_NONCE,l=o?` nonce="${String(o).replace(/"/g,""")}"`:"";s.srcdoc='"+i+"\n<\/script>",document.body.appendChild(s)})}let g=null;const v=()=>{if(g)return g;const e=window.CAP_CUSTOM_WASM_URL||"https://cdn.jsdelivr.net/npm/@cap.js/wasm@0.0.7/browser/cap_wasm_bg.wasm",t=performance.now();return s.debug(r("wasm"),"fetching",e),g=fetch(e).then(e=>{if(!e.ok)throw new Error(`Failed to fetch wasm: ${e.status}`);return e.arrayBuffer()}).then(e=>WebAssembly.compile(e)).then(e=>(s.debug(r("wasm"),`ready in ${n(t)}`),e)).catch(e=>{throw g=null,s.warn(r("wasm"),`load failed (${n(t)}):`,e.message||e),e}),g};"object"==typeof WebAssembly&&"function"==typeof WebAssembly.compile&&v().catch(()=>{});let b=null;function m(){return b||(b=URL.createObjectURL(new Blob(['(()=>{const e=(e,...t)=>{self.CAP_SILENT||console[e]("[cap worker]",...t)},t=async({salt:t,target:n})=>{let s=0;const r=new TextEncoder,o=4*n.length,a=Math.floor(o/8),l=o%8,i=n.length%2==0?n:`${n}0`,c=i.length/2,f=new Uint8Array(c);for(let e=0;e0?255<<8-l&255:0;for(;;)try{for(let e=0;e<5e4;e++){const e=t+s,n=r.encode(e),o=await crypto.subtle.digest("SHA-256",n),i=new Uint8Array(o);let c=!0;for(let e=0;e0&&(i[a]&g)!==(f[a]&g)&&(c=!1),c)return void self.postMessage({nonce:s,found:!0});s++}}catch(t){return e("error","fallback solver crashed:",t.message||t),void self.postMessage({found:!1,error:t.message})}},n=e=>{const t=BigInt("0x"+e.N);let n=BigInt("0x"+e.x);const s=0|e.t,r=Math.max(64,Math.floor(s/50)),o=performance.now();for(let e=0;e"rsw"===e?.kind?n(e):t({salt:e.salt,target:e.target}));let s=null;self.onmessage=async({data:r})=>{if("rsw"===r?.kind)return n(r);const{salt:o,target:a,wasmModule:l}=r;if(l instanceof WebAssembly.Module&&null===s){const n=(t=>{try{let e,n=0,r=null;const o=()=>(null!==r&&0!==r.byteLength||(r=new Uint8Array(e.memory.buffer)),r),a=new TextEncoder,l=(e,t,s)=>{if(void 0===s){const s=a.encode(e),r=t(s.length,1)>>>0;return o().subarray(r,r+s.length).set(s),n=s.length,r}let r=e.length,l=t(r,1)>>>0;const i=o();let c=0;for(;c127)break;i[l+c]=t}if(c!==r){0!==c&&(e=e.slice(c));const t=c+3*e.length;l=s(l,r,t,1)>>>0,r=t;const n=o().subarray(l+c,l+r),{written:i}=a.encodeInto(e,n);c+=i,l=s(l,r,c,1)>>>0}return n=c,l},i={wbg:{}};i.wbg.__wbindgen_init_externref_table=()=>{const t=e.__wbindgen_export_0,n=t.grow(4);t.set(0,void 0),t.set(n+0,void 0),t.set(n+1,null),t.set(n+2,!0),t.set(n+3,!1)};const c=new WebAssembly.Instance(t,i);return e=c.exports,e.__wbindgen_start&&e.__wbindgen_start(),s=(t,s)=>{const r=l(t,e.__wbindgen_malloc,e.__wbindgen_realloc),o=n,a=l(s,e.__wbindgen_malloc,e.__wbindgen_realloc),i=n;return BigInt.asUintN(64,e.solve_pow(r,o,a,i))},!0}catch(t){return e("error","wasm init failed:",t.message||t),!1}})(l);if(!n)return e("warn","wasm init failed, falling back to JS solver"),t({salt:o,target:a})}if(null===s)return e("warn","no wasm module provided, falling back to JS solver"),t({salt:o,target:a});try{const e=performance.now(),t=s(o,a),n=performance.now();self.postMessage({nonce:Number(t),found:!0,durationMs:(n-e).toFixed(2)})}catch(t){e("error","solve_pow threw:",t.message||t),self.postMessage({found:!1,error:t.message||String(t)})}},self.onerror=e=>{self.postMessage({found:!1,error:e})}})();'],{type:"application/javascript"})),b)}class f{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=m(),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++,s.warn(r("pool"),`worker died, replacing (attempt ${this._spawnFailures}/3)`),this._spawnFailures>3?(s.error(r("pool"),"worker spawn failed repeatedly, giving up"),null):this._spawn()}_ensureSize(e){for(;this._workers.length{this._queue.push({salt:e,target:t,resolve:i,reject:s}),this._dispatch()})}runMsg(e,t){return new Promise((i,s)=>{this._queue.push({msg:e,onProgress:t,resolve:i,reject:s}),this._dispatch()})}_dispatch(){for(;this._idle.length>0&&this._queue.length>0;){const e=this._idle.shift(),t=this._queue.shift(),{resolve:i,reject:s}=t,r=void 0!==t.msg;let n=!1;const a=({data:l})=>{if(!n)if(!l||"number"!=typeof l.progress||l.found)n=!0,e.removeEventListener("message",a),e.removeEventListener("error",o),this._spawnFailures=0,this._idle.push(e),l.found?i(r?l:l.nonce):s(new Error(l.error||"worker failed")),this._dispatch();else if(t.onProgress)try{t.onProgress(l.progress)}catch{}},o=t=>{if(n)return;n=!0,e.removeEventListener("message",a),e.removeEventListener("error",o);const i=this._replaceWorker(e);s(t),i&&this._dispatch()};e.addEventListener("message",a),e.addEventListener("error",o),r?e.postMessage(t.msg):this._wasmModule?e.postMessage({salt:t.salt,target:t.target,wasmModule:this._wasmModule},[]):e.postMessage({salt:t.salt,target:t.target})}}terminate(){for(const e of this._workers)try{e.terminate()}catch{}this._workers=[],this._idle=[],this._queue=[]}}class w extends HTMLElement{static formAssociated=!0;#e=null;#t=navigator.hardwareConcurrency||8;token=null;#i;#s;#r;#n;#a;#o;#l=!1;#c;#h;#d=null;#u=null;#p=null;#g=null;#v=null;#b=null;get#m(){return e&&!window.CAP_DISABLE_HAPTICS&&!this.hasAttribute("data-cap-disable-haptics")}#f(){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)}}}#w(){this.#d=this.#f(),this.#y()}#k(){this.#g&&(window.removeEventListener("mousemove",this.#g),window.removeEventListener("touchstart",this.#g),window.removeEventListener("keydown",this.#g),this.#g=null)}#y(){this.#k();const e=()=>{this.#k(),this.#x()};this.#g=e,window.addEventListener("mousemove",e,{passive:!0}),window.addEventListener("touchstart",e,{passive:!0}),window.addEventListener("keydown",e,{passive:!0})}#_(){return"function"==typeof this.checkVisibility?this.checkVisibility({checkOpacity:!0,checkVisibilityCSS:!0}):!!(this.offsetParent||this.getClientRects().length>0)}#E(){this.#_()||s.info(r("challenges"),"solved invisible challenge")}#x(){"idle"===this.#d.state&&this.#_()&&(this.#d.state="waiting",this.#u=setTimeout(()=>{this.#C()},2500))}async#C(){if("waiting"!==this.#d.state)return;this.#d.state="fetching",this.#d._t0=performance.now();let e=this.getAttribute("data-cap-api-endpoint");if(!e&&window?.CAP_CUSTOM_FETCH&&(e="/"),e){e.endsWith("/")||(e+="/");try{const t=await o(`${e}challenge`,{method:"POST",signal:this.#b?.signal});let i;try{i=await t.json()}catch{throw new Error("Failed to parse speculative challenge response")}if(i.error)throw new Error(i.error);if(!this.#d)return;if(i._apiEndpoint=e,this.#d.challengeResp=i,2===i.format&&Array.isArray(i.challenges))return this.#d.state="idle",void this.#d.notify();const{challenge:s,token:r}=i;let n=s;if(!Array.isArray(n)){let e=0;n=Array.from({length:s.c},()=>(e++,[d(`${r}${e}`,s.s),d(`${r}${e}d`,s.d)]))}this.#d.challenges=n,this.#d.state="solving",this.#d.solvePromise=this.#A(n)}catch{if(!this.#d)return;this.#d.state="error",this.#d.notify()}}else this.#d.state="idle"}async#A(e){m();let t=null;try{t=await v()}catch{}this.#p||(this.#p=new f(1),this.#p._spawn()),this.#p.setWasm(t);const i=e.length,s=new Array(i);let r=1,n=!1;this.#d.promoteFn=e=>{n||(n=!0,r=e,this.#p._size=e,this.#p._ensureSize(e))},null!==this.#d.pendingPromotion&&(this.#d.promoteFn(this.#d.pendingPromotion),this.#d.pendingPromotion=null);let a=0;for(;athis.#p.run(e[0],e[1]).then(e=>(this.#d&&this.#d.completedCount++,e))));for(let e=0;esetTimeout(e,120))}return this.#d?(this.#d.results=s,this.#d.state="redeeming",this.#S(s),s):s}async#S(e){try{if(!this.#d)return;const t=this.#d.challengeResp,i=t._apiEndpoint;if(!i)throw a("missing_endpoint","speculative redeem: missing apiEndpoint");let s=null;if(t.instrumentation){if(s=await p(t.instrumentation),!this.#d)return;if(s?.__timeout||s?.__blocked)return this.#d.state="done",void this.#d.notify()}const r=await o(`${i}redeem`,{method:"POST",body:JSON.stringify({token:t.token,solutions:e,...s&&{instr:s}}),headers:{"Content-Type":"application/json"},signal:this.#b?.signal});let l;try{l=await r.json()}catch{throw new Error("Failed to parse speculative redeem response")}if(!this.#d)return;if(!l.success)throw new Error(l.error||"Speculative redeem failed");this.#d.token=l.token,this.#d.tokenExpires=new Date(l.expires).getTime(),this.#d.state="done",this.#d._invisibleElapsed=this.#d._t0?n(this.#d._t0):"?",this.#d.notify()}catch{if(!this.#d)return;this.#d.state="done",this.#d.notify()}}get#T(){return this.getAttribute("data-cap-hidden-field-name")||"cap-token"}#L(e){const t=this.querySelector(`input[name='${this.#T}']`);t&&(t.value=e)}getI18nText(e,t){return this.getAttribute(`data-cap-i18n-${e}`)||this.#v?.[e]||t}#I(){s.debug(r("solve"),`served from speculative cache (saved ${this.#d._invisibleElapsed||"?"})`),this.dispatchEvent("progress",{progress:100}),this.#L(this.#d.token),this.dispatchEvent("solve",{token:this.#d.token}),this.token=this.#d.token;const e=this.#d.tokenExpires-Date.now();return this.#e&&clearTimeout(this.#e),this.#e=setTimeout(()=>this.reset(),e),this.#r.setAttribute("aria-label",this.getI18nText("verified-aria-label","We have verified you're a human, you may now continue")),this.#m&&navigator.vibrate([10,50,20,30,40]),this.#E(),this.#w(),this.#l=!1,{success:!0,token:this.token}}#P(){this.#v=function(e){const t=e?[e]:navigator.languages||[navigator.language||""];for(let e of t){if(!e)continue;if(e=e.toLowerCase(),"en"===e||e.startsWith("en-"))return null;const t=h[e]?e:h[e.split("-")[0]]?e.split("-")[0]:null;if(!t)return null;const i=h[t].split("/"),s={};return c.forEach((e,t)=>{s[e]=i[t]}),s}return null}(window.CAP_LANG||this.getAttribute("data-cap-lang"))}static get observedAttributes(){return["onsolve","onprogress","onreset","onerror","data-cap-worker-count","data-cap-i18n-initial-state","required"]}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);try{this.#h=this.attachInternals()}catch{}}#M(){this.#h?.setValidity&&(this.hasAttribute("required")&&!this.token?this.#h.setValidity({valueMissing:!0},this.getI18nText("required-label","Please verify you're human"),this.#s||this):this.#h.setValidity({}))}initialize(){m(),this.#b=new AbortController,this.#d||(this.#d=this.#f()),this.#p||(this.#p=new f(1),this.#p._spawn())}attributeChangedCallback(e,t,i){if(e.startsWith("on")){const t=e.slice(2),s=this.#c.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.#c.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")),"required"===e&&this.#M()}async connectedCallback(){this.#o=this,this.shadowRoot?this.#i=this.shadowRoot:this.#i=this.attachShadow({mode:"open"}),this.#s||(this.#s=document.createElement("div")),this.#P(),this.createUI(),this.addEventListeners(),this.initialize(),this.#r.removeAttribute("disabled");const e=this.getAttribute("data-cap-worker-count"),t=e?parseInt(e,10):null;this.setWorkersCount(t||navigator.hardwareConcurrency||8),this.#o.innerHTML=``,s.debug(r(),`widget ready (workers=${this.#t}, haptics=${this.#m})`),this.#y(),this.#M(),this.addEventListener("invalid",this.#H)}#H=()=>{if(this.#s){try{this.scrollIntoView({behavior:"smooth",block:"center"})}catch{this.scrollIntoView()}this.#s.classList.remove("invalid"),this.#s.offsetWidth,this.#s.classList.add("invalid"),setTimeout(()=>this.#s?.classList.remove("invalid"),1500)}};async solve(){if(this.#l)return;this.#$();const e=performance.now(),t=this.#b?.signal;s.debug(r("solve"),"starting");try{this.#l=!0,this.updateUI("verifying",this.getI18nText("verifying-label","Verifying..."),!0),this.#r.setAttribute("aria-label",this.getI18nText("verifying-aria-label","Verifying you're a human, please wait")),this.dispatchEvent("progress",{progress:0});try{let i,l,c=this.getAttribute("data-cap-api-endpoint");if(!c&&window?.CAP_CUSTOM_FETCH)c="/";else if(!c)throw a("missing_endpoint","Missing API endpoint. Either custom fetch or an API endpoint must be provided.");if(c.endsWith("/")||(c+="/"),"done"===this.#d.state&&this.#d.token&&this.#d.tokenExpires&&Date.now(){if(t?.aborted||!this.#d)return void clearInterval(e);const i=this.#d.state;if("done"===i||"error"===i)return void clearInterval(e);const s=this.#d.challenges?this.#d.challenges.length:1,r=this.#d.completedCount,n="redeeming"===i?99:"fetching"===i||"waiting"===i?0:Math.min(98,Math.round(r/s*100));this.dispatchEvent("progress",{progress:n})},150);if(await new Promise(e=>this.#d.onSettled(e)),clearInterval(e),t?.aborted||!this.#d)return;if("idle"===this.#d.state&&2===this.#d.challengeResp?.format&&Array.isArray(this.#d.challengeResp.challenges))l=this.#d.challengeResp,this.#d.challengeResp=null,i=await this.solveChallengesV2(l.challenges,t);else{if("done"!==this.#d.state)throw a("solve_failed","Unable to solve challenge, self-hosted instance likely down. This is not an issue with Cap.");if(this.#d.token&&this.#d.tokenExpires&&Date.now()(t++,[d(`${s}${t}`,e.s),d(`${s}${t}d`,e.d)]))}i=await this.solveChallenges(r,t)}}const h=l.instrumentation?p(l.instrumentation):Promise.resolve(null),u=await h;if(t?.aborted||!this.#d)return;if(u?.__timeout||u?.__blocked){o(`${c}redeem`,{method:"POST",body:JSON.stringify({token:l.token,solutions:i,...u.__blocked&&{instr_blocked:!0},...u.__timeout&&{instr_timeout:!0}}),headers:{"Content-Type":"application/json"},signal:t}).catch(()=>{}),this.updateUIBlocked(this.getI18nText("error-label","Error"),u?.__blocked),this.#r.setAttribute("aria-label",this.getI18nText("error-aria-label","An error occurred, please try again"));const e=u?.__blocked?"instr_blocked":"instr_timeout",n=u?.__blocked?`Instrumentation blocked (${u.blockReason||"automated_browser"})`:"Instrumentation timed out";this.removeEventListener("error",this.boundHandleError);const a=new CustomEvent("error",{bubbles:!0,composed:!0,detail:{isCap:!0,code:e,message:n}});return super.dispatchEvent(a),this.addEventListener("error",this.boundHandleError),this.executeAttributeCode("onerror",a),s.error(r("instr"),`[${e}] ${n}`),void(this.#l=!1)}const{token:g}=l,v=await o(`${c}redeem`,{method:"POST",body:JSON.stringify({token:g,solutions:i,...u&&{instr:u}}),headers:{"Content-Type":"application/json"},signal:t});let b;try{b=await v.json()}catch{throw a("redeem_failed","Failed to parse server response")}if(t?.aborted||!this.#d)return;if(this.dispatchEvent("progress",{progress:100}),!b.success)throw a("invalid_solution",b.error||"Invalid solution");this.#L(b.token),this.dispatchEvent("solve",{token:b.token}),this.token=b.token,this.#w(),this.#e&&clearTimeout(this.#e);const m=new Date(b.expires).getTime()-Date.now();return m>0&&m<864e5?this.#e=setTimeout(()=>this.reset(),m):this.error("Invalid expiration time","invalid_expires"),this.#r.setAttribute("aria-label",this.getI18nText("verified-aria-label","We have verified you're a human, you may now continue")),this.#m&&navigator.vibrate([10,50,20,30,40]),s.debug(r("solve"),`verified in ${n(e)}`),this.#E(),{success:!0,token:this.token}}catch(e){if(t?.aborted||!this.#d)return;throw this.#r.setAttribute("aria-label",this.getI18nText("error-aria-label","An error occurred, please try again")),this.error(e.message,e.code||"solve_failed"),e}}finally{this.#l=!1}}async solveChallengesV2(e,t){const i=e.length;let n=0;const o=new Array(e.length);for(let t=0;tPromise.race([e,new Promise((e,i)=>setTimeout(()=>i(new Error(`[cap] ${t} timed out after 60000ms`)),6e4))]),g=new Array(e.length).fill(0),b=()=>{const e=g.reduce((e,t)=>e+t,0)+n,t=Math.min(99,Math.round(e/i*100));this.dispatchEvent("progress",{progress:t})};try{await l(Promise.all(e.map((e,t)=>"sha256-pow"===e.protocol?u(d.run(e.payload.salt,e.payload.target),`sha256-pow worker #${t}`).then(e=>{o[t]={nonce:Number(e)},g[t]=0,n++,b()}):"rsw"===e.protocol?u(d.runMsg({kind:"rsw",N:e.payload.N,x:e.payload.x,t:0|e.payload.t},e=>{g[t]=e,b()}),`rsw worker #${t}`).then(e=>{o[t]={y:e.y},g[t]=0,n++,b()}):p(e.payload.blob).then(e=>{o[t]=e?.__timeout?{timeout:!0}:e?.__blocked?{blocked:!0}:{instr:e},g[t]=0,n++,b()}))),t)}finally{d.terminate()}return o}async solveChallenges(e,t){const i=e.length;let n=0;let a=null;const o="object"==typeof WebAssembly&&"function"==typeof WebAssembly.instantiate;if(o)try{a=await v()}catch(e){s.warn(r("wasm"),"unavailable, falling back to JS solver:",e.message||e)}if(!o&&(s.warn(r("wasm"),"WebAssembly disabled in this browser, solver will be ~10x slower"),!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 c=new f(this.#t);c.setWasm(a),c._ensureSize(this.#t);const h=[];try{for(let s=0;sc.run(e,t).then(e=>{n++;const t=Math.min(99,Math.round((0+n)/i*100));return this.dispatchEvent("progress",{progress:t}),e}))),t);h.push(...a)}}finally{c.terminate()}return h}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","group"),this.#s.setAttribute("aria-label",this.getI18nText("group-aria-label","Cap verification")),this.#r=document.createElement("div"),this.#r.className="captcha-trigger",this.#r.setAttribute("part","trigger"),this.#r.setAttribute("role","button"),this.#r.setAttribute("tabindex","0"),this.#r.setAttribute("aria-label",this.getI18nText("verify-aria-label","Click to verify you're a human")),this.#r.setAttribute("aria-live","polite"),this.#r.setAttribute("disabled","true"),this.#r.innerHTML=`

${this.getI18nText("initial-state","Verify you're human")}

`,this.#s.appendChild(this.#r),this.#a=document.createElement("a"),this.#a.className="cap-troubleshoot-link",this.#a.setAttribute("part","troubleshoot"),this.#a.setAttribute("target","_blank"),this.#a.setAttribute("rel","noopener"),this.#a.hidden=!0,this.#s.appendChild(this.#a),this.#n=document.createElement("a"),this.#n.className="credits",this.#n.setAttribute("aria-label","Secured by Cap"),this.#n.setAttribute("href","https://trycap.dev"),this.#n.setAttribute("target","_blank"),this.#n.setAttribute("title","Secured by Cap: The self-hosted CAPTCHA for the modern web."),this.#n.textContent="Cap",this.#s.appendChild(this.#n),this.#i.innerHTML=`:host{display:inline-block}@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{width:var(--cap-widget-width,260px);height:var(--cap-widget-height,58px);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;cursor:pointer;-webkit-tap-highlight-color:#fff0;color:var(--cap-color,#212121);font-family:var(--cap-font,system, -apple-system, "BlinkMacSystemFont", ".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif);transition:filter .2s,transform .2s,height .2s;display:inline-block;position:relative;overflow:hidden}.captcha.has-troubleshoot{height:calc(var(--cap-widget-height,58px) + 26px)}.captcha.has-troubleshoot .captcha-trigger{inset-block-end:26px}.captcha:not([data-state]):active{transform:scale(.97)}.captcha:hover{filter:brightness(98%)}.captcha-trigger{padding:var(--cap-widget-padding,14px);align-items:center;gap:var(--cap-gap,15px);border-radius:inherit;transition:inset .2s;display:flex;position:absolute;inset:0}.captcha-trigger:focus-visible{outline:2px solid var(--cap-focus-ring,#06c);outline-offset:-3px}.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);flex-shrink:0;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;inset-inline-start: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.invalid{border-color:var(--cap-invalid-border-color,#f55b50);box-shadow:0 0 0 3px var(--cap-invalid-ring-color,#f55b5033);animation:.5s cubic-bezier(.36,.07,.19,.97) cap-shake}@keyframes cap-shake{10%,90%{transform:translate(-1px)}20%,80%{transform:translate(2px)}30%,50%,70%{transform:translate(-4px)}40%,60%{transform:translate(4px)}}@media (prefers-reduced-motion:reduce){.captcha.invalid{animation:none!important}}.captcha-trigger[disabled]{cursor:not-allowed}.captcha[data-state=verifying] .captcha-trigger{cursor:progress}.captcha[data-state=done] .captcha-trigger{cursor:default}.credits{color:var(--cap-color,#212121);text-underline-offset:.18em;z-index:2;border-radius:4px;justify-content:flex-end;align-items:center;min-width:24px;min-height:24px;padding:4px 6px;text-decoration:underline;text-decoration-thickness:.08em;position:absolute;inset-block-end:4px;inset-inline-end:4px;opacity:.8!important;font-size:12px!important;display:inline-flex!important}.captcha .credits{visibility:visible!important;pointer-events:all!important;display:inline-flex!important}.credits:focus-visible{outline:2px solid var(--cap-focus-ring,#06c);outline-offset:2px;opacity:1!important}.cap-troubleshoot-link{min-width:24px;min-height:24px;color:var(--cap-troubleshoot-color,#0050a0);text-underline-offset:.15em;cursor:pointer;z-index:2;border-radius:4px;align-items:center;padding:4px 6px;font-size:13px;font-weight:500;text-decoration:underline;text-decoration-thickness:.08em;display:inline-flex;position:absolute;inset-block-end:4px;inset-inline-start:14px}.cap-troubleshoot-link[hidden]{display:none}.cap-troubleshoot-link:hover{opacity:.85}.cap-troubleshoot-link:focus-visible{outline:2px solid var(--cap-focus-ring,#06c);outline-offset:2px}@media (forced-colors:active){.captcha,.checkbox{border-color:canvastext}:is(.captcha-trigger:focus-visible,.credits:focus-visible,.cap-troubleshoot-link:focus-visible){outline-color:highlight}.progress-ring-bg{stroke:graytext}.progress-ring-circle{stroke:canvastext}.captcha.invalid{box-shadow:none;border-color:mark}.credits{opacity:1!important}}`,this.#i.appendChild(this.#s),this.#$(),setTimeout(()=>this.#$(),100)}addEventListeners(){this.#r&&(this.#n.addEventListener("click",e=>{e.preventDefault(),window.open(`https://trycap.dev/?${new URLSearchParams({utm_source:"cap_widget",utm_medium:"referral",utm_campaign:"widget",utm_content:window.CAP_DISABLE_WIDGET_REF?"":location.hostname,ref:window.CAP_DISABLE_WIDGET_REF?"":location.href||"",sub:window.CAP_DISABLE_WIDGET_REF?"":document.referrer||""}).toString()}`,"_blank")}),this.#r.addEventListener("click",()=>{this.#r.hasAttribute("disabled")||this.solve()}),this.#r.addEventListener("mousedown",()=>{!this.#r.hasAttribute("disabled")&&this.#m&&navigator.vibrate(5)}),this.#r.addEventListener("keydown",e=>{e.target===this.#r&&("Enter"!==e.key&&" "!==e.key||this.#r.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))}#N(){if(!this.#o)return!1;const e=this.#o.getBoundingClientRect();if(0===e.width&&0===e.height)return!0;const t=window.getComputedStyle(this.#o);return"none"===t.display||"hidden"===t.visibility}#$(){this.#n&&this.#s&&!this.#N()&&(this.#n.isConnected&&this.#n.parentNode===this.#s||this.#s.appendChild(this.#n),this.#n.textContent&&this.#n.textContent.trim()||(this.#n.textContent="Cap"),"https://trycap.dev"!==this.#n.getAttribute("href")&&this.#n.setAttribute("href","https://trycap.dev"),this.#n.style.cssText=["display: inline-flex !important","visibility: visible !important","opacity: 0.8 !important","pointer-events: all !important","font-size: 12px !important","transform: none !important","clip-path: none !important","filter: none !important","position: absolute !important"].join("; "))}animateLabel(e){if(!this.#r)return;const t=this.#r.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.#r&&(this.#s.setAttribute("data-state",e),this.#s.classList.remove("has-troubleshoot"),this.animateLabel(t),this.#a&&(this.#a.hidden=!0),i?this.#r.setAttribute("disabled","true"):this.#r.removeAttribute("disabled"))}updateUIBlocked(e,t=!1){if(this.#s&&this.#r&&(this.#s.setAttribute("data-state","error"),this.#r.removeAttribute("disabled"),this.animateLabel(e),this.#a))if(t){const e=this.getAttribute("data-cap-troubleshooting-url")||"https://trycap.dev/guide/troubleshooting/instrumentation.html";this.#a.setAttribute("href",e),this.#a.textContent=this.getI18nText("troubleshooting-label","Troubleshoot"),this.#a.hidden=!1,this.#s.classList.add("has-troubleshoot")}else this.#a.hidden=!0,this.#s.classList.remove("has-troubleshoot")}handleProgress(e){if(!this.#r)return;const t=this.#r.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.#r.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),this.#h?.setValidity?.({}),this.#s?.classList.remove("invalid")}handleError(e){this.updateUI("error",this.getI18nText("error-label","Error. Try again.")),this.executeAttributeCode("onerror",e),this.#m&&navigator.vibrate([10,40,10])}handleReset(e){this.updateUI("",this.getI18nText("initial-state","I'm a human")),this.executeAttributeCode("onreset",e),this.#M()}executeAttributeCode(e,t){const i=this.getAttribute(e);i&&(s.warn(r(),"inline `onxxx='…'` handlers are deprecated. use `addEventListener` callbacks instead."),new Function("event",i).call(this,t))}error(e="Unknown error",t="unknown"){s.error(r("solve"),`[${t}] ${e}`),this.dispatchEvent("error",{isCap:!0,code:t,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.token=null,this.dispatchEvent("reset"),this.#L("")}get tokenValue(){return this.token}disconnectedCallback(){this.#b?.abort(),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.#i&&(this.#i.innerHTML=""),this.reset(),this.cleanup()}cleanup(){this.#e&&(clearTimeout(this.#e),this.#e=null),this.#k(),this.#u&&(clearTimeout(this.#u),this.#u=null),this.#p&&(this.#p.terminate(),this.#p=null),this.#d&&(this.#d.state="error",this.#d.notify(),this.#d=null)}}class y{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),t||i.hasAttribute("data-cap-disable-haptics")||i.setAttribute("data-cap-disable-haptics",""),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=y,customElements.get("cap-widget")||window?.CAP_DONT_SKIP_REDEFINE?customElements.get("cap-widget")&&s.warn(r(),"cap-widget custom element already defined, skipping re-define. set window.CAP_DONT_SKIP_REDEFINE = true to override"):customElements.define("cap-widget",w),"object"==typeof exports&&"undefined"!=typeof module?module.exports=y:"function"==typeof define&&define.amd&&define([],()=>y),"undefined"!=typeof exports&&(exports.default=y)})(); \ No newline at end of file diff --git a/Assets/js/cap_wasm_bg.wasm b/Assets/js/cap_wasm_bg.wasm new file mode 100644 index 0000000..c19affb Binary files /dev/null and b/Assets/js/cap_wasm_bg.wasm differ diff --git a/Assets/js/i18n/de.js b/Assets/js/i18n/de.js new file mode 100644 index 0000000..63f097b --- /dev/null +++ b/Assets/js/i18n/de.js @@ -0,0 +1,128 @@ +const noop = () => { +}; +function safe_not_equal(a, b) { + return a != a ? b == b : a !== b || a !== null && typeof a === "object" || typeof a === "function"; +} +function subscribe_to_store(store2, run, invalidate) { + if (store2 == null) { + run(void 0); + return noop; + } + const unsub = untrack( + () => store2.subscribe( + run, + // @ts-expect-error + invalidate + ) + ); + return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub; +} +const subscriber_queue = []; +function writable(value, start = noop) { + let stop = null; + const subscribers = /* @__PURE__ */ new Set(); + function set(new_value) { + if (safe_not_equal(value, new_value)) { + value = new_value; + if (stop) { + const run_queue = !subscriber_queue.length; + for (const subscriber of subscribers) { + subscriber[1](); + subscriber_queue.push(subscriber, value); + } + if (run_queue) { + for (let i = 0; i < subscriber_queue.length; i += 2) { + subscriber_queue[i][0](subscriber_queue[i + 1]); + } + subscriber_queue.length = 0; + } + } + } + } + function update(fn) { + set(fn( + /** @type {T} */ + value + )); + } + function subscribe(run, invalidate = noop) { + const subscriber = [run, invalidate]; + subscribers.add(subscriber); + if (subscribers.size === 1) { + stop = start(set, update) || noop; + } + run( + /** @type {T} */ + value + ); + return () => { + subscribers.delete(subscriber); + if (subscribers.size === 0 && stop) { + stop(); + stop = null; + } + }; + } + return { set, update, subscribe }; +} +function get(store2) { + let value; + subscribe_to_store(store2, (_) => value = _)(); + return value; +} +let untracking = false; +function untrack(fn) { + var previous_untracking = untracking; + try { + untracking = true; + return fn(); + } finally { + untracking = previous_untracking; + } +} +function store(defaultValue) { + const scope = { + get: (name) => { + return get(scope.store)[name]; + }, + set: (name, value) => { + if (typeof name === "string") { + Object.assign(get(scope.store), { + [name]: value + }); + } else { + Object.assign(get(scope.store), name); + } + scope.store.set(get(scope.store)); + }, + store: writable(defaultValue) + }; + return scope; +} +globalThis.$altcha = globalThis.$altcha || { + algorithms: /* @__PURE__ */ new Map(), + defaults: store({}), + i18n: store({}), + instances: /* @__PURE__ */ new Set(), + plugins: /* @__PURE__ */ new Set() +}; +const i18n = { + ariaLinkLabel: "Altcha (offizielle Website)", + enterCode: "Code eingeben", + enterCodeAria: "Geben Sie den Code ein, den Sie hören. Drücken Sie die Leertaste, um die Audio abzuspielen.", + error: "Überprüfung fehlgeschlagen. Bitte versuchen Sie es später erneut.", + expired: "Überprüfung abgelaufen. Bitte versuchen Sie es erneut.", + footer: 'Geschützt durch ALTCHA', + getAudioChallenge: "Audio-Herausforderung anfordern", + label: "Ich bin kein Roboter", + loading: "Lade...", + reload: "Neu laden", + verify: "Überprüfen", + verificationRequired: "Überprüfung erforderlich!", + verified: "Überprüft", + verifying: "Wird überprüft...", + waitAlert: "Überprüfung läuft... bitte warten.", + cancel: "Abbrechen", + enterCodeFromImage: "Um fortzufahren, geben Sie bitte den Code aus dem Bild unten ein." +}; +globalThis.$altcha.i18n.set("de", i18n); diff --git a/Assets/js/i18n/fi.js b/Assets/js/i18n/fi.js new file mode 100644 index 0000000..6c9d179 --- /dev/null +++ b/Assets/js/i18n/fi.js @@ -0,0 +1,128 @@ +const noop = () => { +}; +function safe_not_equal(a, b) { + return a != a ? b == b : a !== b || a !== null && typeof a === "object" || typeof a === "function"; +} +function subscribe_to_store(store2, run, invalidate) { + if (store2 == null) { + run(void 0); + return noop; + } + const unsub = untrack( + () => store2.subscribe( + run, + // @ts-expect-error + invalidate + ) + ); + return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub; +} +const subscriber_queue = []; +function writable(value, start = noop) { + let stop = null; + const subscribers = /* @__PURE__ */ new Set(); + function set(new_value) { + if (safe_not_equal(value, new_value)) { + value = new_value; + if (stop) { + const run_queue = !subscriber_queue.length; + for (const subscriber of subscribers) { + subscriber[1](); + subscriber_queue.push(subscriber, value); + } + if (run_queue) { + for (let i = 0; i < subscriber_queue.length; i += 2) { + subscriber_queue[i][0](subscriber_queue[i + 1]); + } + subscriber_queue.length = 0; + } + } + } + } + function update(fn) { + set(fn( + /** @type {T} */ + value + )); + } + function subscribe(run, invalidate = noop) { + const subscriber = [run, invalidate]; + subscribers.add(subscriber); + if (subscribers.size === 1) { + stop = start(set, update) || noop; + } + run( + /** @type {T} */ + value + ); + return () => { + subscribers.delete(subscriber); + if (subscribers.size === 0 && stop) { + stop(); + stop = null; + } + }; + } + return { set, update, subscribe }; +} +function get(store2) { + let value; + subscribe_to_store(store2, (_) => value = _)(); + return value; +} +let untracking = false; +function untrack(fn) { + var previous_untracking = untracking; + try { + untracking = true; + return fn(); + } finally { + untracking = previous_untracking; + } +} +function store(defaultValue) { + const scope = { + get: (name) => { + return get(scope.store)[name]; + }, + set: (name, value) => { + if (typeof name === "string") { + Object.assign(get(scope.store), { + [name]: value + }); + } else { + Object.assign(get(scope.store), name); + } + scope.store.set(get(scope.store)); + }, + store: writable(defaultValue) + }; + return scope; +} +globalThis.$altcha = globalThis.$altcha || { + algorithms: /* @__PURE__ */ new Map(), + defaults: store({}), + i18n: store({}), + instances: /* @__PURE__ */ new Set(), + plugins: /* @__PURE__ */ new Set() +}; +const i18n = { + ariaLinkLabel: "Altcha (virallinen verkkosivusto)", + enterCode: "Syötä koodi", + enterCodeAria: "Kirjoita kuulemasi koodi. Paina välilyöntiä toistaaksesi äänen.", + error: "Varmennus epäonnistui. Yritä myöhemmin uudelleen.", + expired: "Varmennus vanhentui. Yritä uudelleen.", + footer: 'Suojattu ALTCHA:lla', + getAudioChallenge: "Hae äänitehtävä", + label: "En ole robotti", + loading: "Ladataan...", + reload: "Lataa uudelleen", + verify: "Vahvista", + verificationRequired: "Vahvistus vaaditaan!", + verified: "Vahvistettu", + verifying: "Vahvistetaan...", + waitAlert: "Vahvistetaan... odota hetki.", + cancel: "Peruuta", + enterCodeFromImage: "Jatkaaksesi, syötä alla olevan kuvan koodi." +}; +globalThis.$altcha.i18n.set("fi", i18n); diff --git a/Assets/js/i18n/sv.js b/Assets/js/i18n/sv.js new file mode 100644 index 0000000..cd42816 --- /dev/null +++ b/Assets/js/i18n/sv.js @@ -0,0 +1,128 @@ +const noop = () => { +}; +function safe_not_equal(a, b) { + return a != a ? b == b : a !== b || a !== null && typeof a === "object" || typeof a === "function"; +} +function subscribe_to_store(store2, run, invalidate) { + if (store2 == null) { + run(void 0); + return noop; + } + const unsub = untrack( + () => store2.subscribe( + run, + // @ts-expect-error + invalidate + ) + ); + return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub; +} +const subscriber_queue = []; +function writable(value, start = noop) { + let stop = null; + const subscribers = /* @__PURE__ */ new Set(); + function set(new_value) { + if (safe_not_equal(value, new_value)) { + value = new_value; + if (stop) { + const run_queue = !subscriber_queue.length; + for (const subscriber of subscribers) { + subscriber[1](); + subscriber_queue.push(subscriber, value); + } + if (run_queue) { + for (let i = 0; i < subscriber_queue.length; i += 2) { + subscriber_queue[i][0](subscriber_queue[i + 1]); + } + subscriber_queue.length = 0; + } + } + } + } + function update(fn) { + set(fn( + /** @type {T} */ + value + )); + } + function subscribe(run, invalidate = noop) { + const subscriber = [run, invalidate]; + subscribers.add(subscriber); + if (subscribers.size === 1) { + stop = start(set, update) || noop; + } + run( + /** @type {T} */ + value + ); + return () => { + subscribers.delete(subscriber); + if (subscribers.size === 0 && stop) { + stop(); + stop = null; + } + }; + } + return { set, update, subscribe }; +} +function get(store2) { + let value; + subscribe_to_store(store2, (_) => value = _)(); + return value; +} +let untracking = false; +function untrack(fn) { + var previous_untracking = untracking; + try { + untracking = true; + return fn(); + } finally { + untracking = previous_untracking; + } +} +function store(defaultValue) { + const scope = { + get: (name) => { + return get(scope.store)[name]; + }, + set: (name, value) => { + if (typeof name === "string") { + Object.assign(get(scope.store), { + [name]: value + }); + } else { + Object.assign(get(scope.store), name); + } + scope.store.set(get(scope.store)); + }, + store: writable(defaultValue) + }; + return scope; +} +globalThis.$altcha = globalThis.$altcha || { + algorithms: /* @__PURE__ */ new Map(), + defaults: store({}), + i18n: store({}), + instances: /* @__PURE__ */ new Set(), + plugins: /* @__PURE__ */ new Set() +}; +const i18n = { + ariaLinkLabel: "Altcha (officiell webbplats)", + enterCode: "Ange kod", + enterCodeAria: "Ange koden du hör. Tryck på mellanslag för att spela upp ljudet.", + error: "Verifiering misslyckades. Försök igen senare.", + expired: "Verifieringen har gått ut. Försök igen.", + footer: 'Skyddad av ALTCHA', + getAudioChallenge: "Få ljudutmaning", + label: "Jag är inte en robot", + loading: "Laddar...", + reload: "Ladda om", + verify: "Verifiera", + verificationRequired: "Verifiering krävs!", + verified: "Verifierad", + verifying: "Verifierar...", + waitAlert: "Verifierar... vänligen vänta.", + cancel: "Avbryt", + enterCodeFromImage: "För att fortsätta, ange koden från bilden nedan." +}; +globalThis.$altcha.i18n.set("sv", i18n); diff --git a/CaptchaEvents.php b/CaptchaEvents.php index 4655453..eec084f 100644 --- a/CaptchaEvents.php +++ b/CaptchaEvents.php @@ -15,4 +15,6 @@ final class CaptchaEvents { public const HCAPTCHA_ON_FORM_VALIDATE = "mautic.plugin.hcaptcha.on_form_validate"; public const RECAPTCHA_ON_FORM_VALIDATE = "mautic.plugin.recaptcha.on_form_validate"; public const TURNSTILE_ON_FORM_VALIDATE = "mautic.plugin.turnstile.on_form_validate"; + public const ALTCHA_ON_FORM_VALIDATE = "mautic.plugin.altcha.on_form_validate"; + public const CAP_ON_FORM_VALIDATE = "mautic.plugin.cap.on_form_validate"; } diff --git a/Config/config.php b/Config/config.php index 71db2da..d1081ec 100644 --- a/Config/config.php +++ b/Config/config.php @@ -3,14 +3,24 @@ use MauticPlugin\MauticMultiCaptchaBundle\EventListener\HcaptchaFormSubscriber; use MauticPlugin\MauticMultiCaptchaBundle\EventListener\RecaptchaFormSubscriber; use MauticPlugin\MauticMultiCaptchaBundle\EventListener\TurnstileFormSubscriber; +use MauticPlugin\MauticMultiCaptchaBundle\EventListener\AltchaFormSubscriber; +use MauticPlugin\MauticMultiCaptchaBundle\EventListener\CapFormSubscriber; use MauticPlugin\MauticMultiCaptchaBundle\Service\HcaptchaClient; use MauticPlugin\MauticMultiCaptchaBundle\Service\RecaptchaClient; use MauticPlugin\MauticMultiCaptchaBundle\Service\TurnstileClient; +use MauticPlugin\MauticMultiCaptchaBundle\Service\AltchaClient; +use MauticPlugin\MauticMultiCaptchaBundle\Service\CapClient; use MauticPlugin\MauticMultiCaptchaBundle\Integration\HcaptchaIntegration; use MauticPlugin\MauticMultiCaptchaBundle\Integration\RecaptchaIntegration; use MauticPlugin\MauticMultiCaptchaBundle\Integration\TurnstileIntegration; +use MauticPlugin\MauticMultiCaptchaBundle\Integration\AltchaIntegration; +use MauticPlugin\MauticMultiCaptchaBundle\Integration\CapIntegration; + +use MauticPlugin\MauticMultiCaptchaBundle\Controller\ChallengeController; +use MauticPlugin\MauticMultiCaptchaBundle\Controller\CapAssetController; +use MauticPlugin\MauticMultiCaptchaBundle\Twig\AltchaExtension; use Mautic\CoreBundle\Helper\AppVersion; @@ -20,6 +30,26 @@ $mauticVersion = str_split((string)$mauticVersion); switch(true) { + case $mauticVersion[0] >= 7: + $defaultIntegrationArguments = [ + "event_dispatcher", + "mautic.helper.cache_storage", + "doctrine.orm.entity_manager", + "request_stack", + "router", + "translator", + "monolog.logger.mautic", + "mautic.helper.encryption", + "mautic.lead.model.lead", + "mautic.lead.model.company", + "mautic.helper.paths", + "mautic.core.model.notification", + "mautic.lead.model.field", + "mautic.plugin.model.integration_entity", + "mautic.lead.model.dnc", + "mautic.lead.field.fields_with_unique_identifier" + ]; + break; case $mauticVersion[0] >= 6: $defaultIntegrationArguments = [ "event_dispatcher", @@ -67,12 +97,23 @@ return [ "name" => "MultiCAPTCHA", - "description" => "Enables Google's reCAPTCHA, hCaptcha, and Cloudflare Turnstile integration for Mautic", + "description" => "Enables Google's reCAPTCHA, hCaptcha, Cloudflare Turnstile, ALTCHA, and Cap integration for Mautic", "version" => "1.0.9", "author" => "FireMultimedia B.V.", "routes" => [ + "public" => [ + "mautic_altcha_challenge" => [ + "path" => "/altcha/challenge", + "controller" => "mautic.altcha.controller.challenge" + ], + "mautic_cap_api_wasm" => [ + "path" => "/cap/api/wasm", + "controller" => "mautic.cap.controller.asset:wasmAction", + "method" => "GET" + ] + ] ], "services" => [ @@ -109,6 +150,28 @@ "mautic.lead.model.lead", "mautic.helper.integration" ] + ], + + "mautic.altcha.event_listener.form_subscriber" => [ + "class" => AltchaFormSubscriber::class, + + "arguments" => [ + "event_dispatcher", + "mautic.altcha.service.altcha_client", + "mautic.lead.model.lead", + "mautic.helper.integration" + ] + ], + + "mautic.cap.event_listener.form_subscriber" => [ + "class" => CapFormSubscriber::class, + + "arguments" => [ + "event_dispatcher", + "mautic.cap.service.cap_client", + "mautic.lead.model.lead", + "mautic.helper.integration" + ] ] ], @@ -116,6 +179,20 @@ ], + "controllers" => [ + "mautic.cap.controller.asset" => [ + "class" => CapAssetController::class, + "arguments" => [] + ], + + "mautic.altcha.controller.challenge" => [ + "class" => ChallengeController::class, + "arguments" => [ + "mautic.altcha.service.altcha_client" + ] + ] + ], + "others" => [ "mautic.hcaptcha.service.hcaptcha_client" => [ "class" => HcaptchaClient::class, @@ -139,6 +216,31 @@ "arguments" => [ "mautic.helper.integration" ] + ], + + "mautic.altcha.service.altcha_client" => [ + "class" => AltchaClient::class, + + "arguments" => [ + "mautic.helper.integration", + "router" + ] + ], + + "mautic.cap.service.cap_client" => [ + "class" => CapClient::class, + + "arguments" => [ + "mautic.helper.integration" + ] + ], + + "mautic.altcha.twig.extension" => [ + "class" => AltchaExtension::class, + "arguments" => [ + "mautic.altcha.service.altcha_client" + ], + "tag" => "twig.extension" ] ], @@ -156,6 +258,16 @@ "mautic.integration.turnstile" => [ "class" => TurnstileIntegration::class, "arguments" => $defaultIntegrationArguments + ], + + "mautic.integration.altcha" => [ + "class" => AltchaIntegration::class, + "arguments" => $defaultIntegrationArguments + ], + + "mautic.integration.cap" => [ + "class" => CapIntegration::class, + "arguments" => $defaultIntegrationArguments ] ] ], diff --git a/Controller/CapAssetController.php b/Controller/CapAssetController.php new file mode 100644 index 0000000..5a38908 --- /dev/null +++ b/Controller/CapAssetController.php @@ -0,0 +1,50 @@ + tag (not subject to CORS), but it fetches its WASM binary via + * fetch(), which the browser blocks unless the response carries an + * Access-Control-Allow-Origin header - something a plain static file server + * does not add for arbitrary plugin assets. No inheritance to avoid + * Mautic/Symfony DI conflicts, matching ChallengeController's approach. + */ +class CapAssetController +{ + private const WASM_PATH = __DIR__ . '/../Assets/js/cap_wasm_bg.wasm'; + + public function wasmAction(Request $request): Response + { + if ($request->getMethod() === 'OPTIONS') { + $response = new Response(); + $response->headers->set('Access-Control-Allow-Origin', '*'); + $response->headers->set('Access-Control-Allow-Methods', 'GET, OPTIONS'); + $response->headers->set('Access-Control-Allow-Headers', 'Content-Type, X-Requested-With'); + $response->headers->set('Access-Control-Max-Age', '86400'); + $response->setStatusCode(204); + + return $response; + } + + if (!is_file(self::WASM_PATH)) { + return new Response('', Response::HTTP_NOT_FOUND); + } + + $response = new BinaryFileResponse(self::WASM_PATH); + $response->headers->set('Content-Type', 'application/wasm'); + $response->headers->set('Access-Control-Allow-Origin', '*'); + $response->headers->set('Cache-Control', 'public, max-age=31536000, immutable'); + + return $response; + } +} diff --git a/Controller/ChallengeController.php b/Controller/ChallengeController.php new file mode 100644 index 0000000..aaff36e --- /dev/null +++ b/Controller/ChallengeController.php @@ -0,0 +1,88 @@ +Class ChallengeController + * + * A public (unauthenticated), read-only endpoint that the + * fetches a fresh challenge from directly, in self-hosted mode. + * + * This exists because Mautic caches an entire form's RENDERED HTML in + * `forms.cached_html` (see `Mautic\FormBundle\Model\FormModel::generateHtml()` + * / `getContent()`) and only regenerates it when the form itself is saved - + * not on every page view, and not every time preview is opened. A challenge + * embedded directly into that cached HTML would be reused by every visitor + * until the form is next saved, and would eventually expire while still + * being served - which is exactly what caused "Verification failed. Try + * again later." to appear on every attempt, deterministically, regardless + * of what the challenge JSON itself contained. Pointing the widget's + * "challengeurl" attribute at this endpoint instead (a URL, fetched live by + * the visitor's browser at the moment the widget actually loads) sidesteps + * that caching layer entirely - the same approach ALTCHA Sentinel already + * uses, and what ALTCHA's own docs describe as the standard "server + * integration" pattern for any dynamically-cached page. + * + * Deliberately a plain, invokable class (`__invoke()`), not a + * Mautic\CoreBundle\Controller\FormController with a named *Action method - + * Mautic's own plugin-config docs specifically show a bare `SomeClass::class` + * reference (assuming an invokable controller) as the pattern for routes + * registered under the "public" firewall, as opposed to the `[SomeClass, + * 'methodName']` array pairing used for "main"/authenticated routes. + * + * @package MauticPlugin\MauticMultiCaptchaBundle\Controller + * + * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html + */ +class ChallengeController { + + /** Hard floor/ceiling on the requested expiry, regardless of what's passed in the query string. */ + private const MIN_EXPIRE_SECONDS = 30; + private const MAX_EXPIRE_SECONDS = 3600; + + public function __construct(private readonly AltchaClient $altchaClient) { + + } + + public function __invoke(Request $request): JsonResponse { + // Handle CORS preflight - Mautic forms are routinely embedded on + // third-party domains, so the widget's fetch() must be allowed cross-origin. + if($request->getMethod() === "OPTIONS") { + $response = new JsonResponse(null, 204); + $response->headers->set("Access-Control-Allow-Origin", "*"); + $response->headers->set("Access-Control-Allow-Methods", "GET, OPTIONS"); + $response->headers->set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, X-Altcha-Spam-Filter, Cache-Control"); + $response->headers->set("Access-Control-Max-Age", "86400"); + return $response; + } + + $complexity = (string) $request->query->get("complexity", "medium"); + $expireSeconds = (int) $request->query->get("expire", (string) AltchaClient::DEFAULT_EXPIRE_SECONDS); + $expireSeconds = max(self::MIN_EXPIRE_SECONDS, min(self::MAX_EXPIRE_SECONDS, $expireSeconds)); + + $challenge = $this->altchaClient->createChallengeForComplexity($complexity, $expireSeconds); + + if($challenge === null) { + return new JsonResponse(["error" => "ALTCHA is not configured for self-hosted challenges."], 503); + } + + $response = new JsonResponse($challenge); + + // Never let any layer (browser, reverse proxy, CDN) cache this response - + // a stale/reused challenge is exactly what this endpoint exists to prevent. + $response->headers->set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"); + $response->headers->set("Pragma", "no-cache"); + + $response->headers->set("Access-Control-Allow-Origin", "*"); + $response->headers->set("Access-Control-Allow-Methods", "GET, OPTIONS"); + $response->headers->set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, X-Altcha-Spam-Filter, Cache-Control"); + + return $response; + } + +} diff --git a/DependencyInjection/Compiler/EncryptionHelperAliasPass.php b/DependencyInjection/Compiler/EncryptionHelperAliasPass.php new file mode 100644 index 0000000..a8ac19e --- /dev/null +++ b/DependencyInjection/Compiler/EncryptionHelperAliasPass.php @@ -0,0 +1,25 @@ +has('mautic.helper.encryption') + && $container->has(EncryptionHelper::class) + ) { + $container->setAlias('mautic.helper.encryption', EncryptionHelper::class) + ->setPublic(true); + } + } +} diff --git a/DependencyInjection/MauticMultiCaptchaExtension.php b/DependencyInjection/MauticMultiCaptchaExtension.php index 496a760..9e8d043 100644 --- a/DependencyInjection/MauticMultiCaptchaExtension.php +++ b/DependencyInjection/MauticMultiCaptchaExtension.php @@ -2,6 +2,7 @@ namespace MauticPlugin\MauticMultiCaptchaBundle\DependencyInjection; +use Mautic\CoreBundle\Helper\EncryptionHelper; use Symfony\Component\DependencyInjection\Extension\Extension; use \Exception; @@ -29,9 +30,15 @@ class MauticMultiCaptchaExtension extends Extension { * @return void */ public function load(array $configs, ContainerBuilder $container): void { - $loader = new PhpFileLoader($container, new FileLocator(__DIR__ . "/../Config")); - - $loader->load("services.php"); + // In Mautic 7 the mautic.helper.encryption alias was removed; the service + // is now only registered under its FQCN. Re-create the alias so the legacy + // plugin config.php service definitions keep working across all versions. + if (!$container->has('mautic.helper.encryption') + && $container->has(\Mautic\CoreBundle\Helper\EncryptionHelper::class) + ) { + $container->setAlias('mautic.helper.encryption', \Mautic\CoreBundle\Helper\EncryptionHelper::class) + ->setPublic(true); + } } } diff --git a/EventListener/AltchaFormSubscriber.php b/EventListener/AltchaFormSubscriber.php new file mode 100644 index 0000000..aef8968 --- /dev/null +++ b/EventListener/AltchaFormSubscriber.php @@ -0,0 +1,117 @@ +Class AltchaFormSubscriber + * + * @package MauticPlugin\MauticMultiCaptchaBundle\EventListener + * + * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html + */ +class AltchaFormSubscriber implements EventSubscriberInterface { + + private ?TranslatorInterface $translator = null; + + private bool $isConfigured = false; + + public function __construct( + private readonly EventDispatcherInterface $eventDispatcher, + private readonly AltchaClient $altchaClient, + private readonly LeadModel $leadModel, + + IntegrationHelper $integrationHelper + ) { + $integrationObject = $integrationHelper->getIntegrationObject(AltchaIntegration::INTEGRATION_NAME); + + if($integrationObject instanceof AbstractIntegration) { + $this->translator = $integrationObject->getTranslator(); + } + + $this->isConfigured = $this->altchaClient->isConfigured(); + } + + /** {@inheritDoc} */ + public static function getSubscribedEvents(): array { + return [ + FormEvents::FORM_ON_BUILD => ["onFormBuild", 0], + CaptchaEvents::ALTCHA_ON_FORM_VALIDATE => ["onFormValidate", 0] + ]; + } + + public function onFormBuild(FormBuilderEvent $event): void { + if(!$this->isConfigured) + return; + + $event->addFormField("plugin.altcha", [ + "label" => "strings.altcha.plugin.name", + "formType" => AltchaType::class, + "template" => "@MauticMultiCaptcha/Integration/altcha.html.twig", + + "builderOptions" => [ + "addLeadFieldList" => false, + "addIsRequired" => false, + "addDefaultValue" => false, + "addSaveResult" => false + ] + ]); + + $event->addValidator("plugin.altcha.validator", [ + "eventName" => CaptchaEvents::ALTCHA_ON_FORM_VALIDATE, + "fieldType" => "plugin.altcha" + ]); + } + + public function onFormValidate(ValidationEvent $event): void { + if(!$this->isConfigured) + return; + + $payload = (string) ($event->getValue() ?? ""); + + if($this->altchaClient->verify($payload)) + return; + + $event->failedValidation( + $this->translator === null + ? "ALTCHA verification was not successful." + : $this->translator->trans("strings.altcha.failure_message") + ); + + // Mirror the other CAPTCHA providers: if a lead was nonetheless + // created for this failed submission, remove it once the request + // finishes so no spam contacts pile up in the database. + $this->eventDispatcher->addListener(LeadEvents::LEAD_POST_SAVE, function(LeadEvent $event) { + if(!$event->isNew()) + return; + + $lead = $event->getLead(); + + $this->eventDispatcher->addListener("kernel.terminate", function() use ($lead) { + if($lead) + $this->leadModel->deleteEntity($lead); + }); + }, -255); + } + +} diff --git a/EventListener/CapFormSubscriber.php b/EventListener/CapFormSubscriber.php new file mode 100644 index 0000000..33a52db --- /dev/null +++ b/EventListener/CapFormSubscriber.php @@ -0,0 +1,155 @@ +Class CapFormSubscriber + * + * @package MauticPlugin\MauticMultiCaptchaBundle\EventListener + * + * @authors see: composer.json + * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html + */ +class CapFormSubscriber implements EventSubscriberInterface { + + public const MODEL_NAME_KEY_LEAD = "lead.lead"; + + private TranslatorInterface $translator; + + private bool $isConfigured = false; + + private ?string $serverUrl; + private ?string $siteKey; + + /** + *

FormSubscriber constructor.

+ * + * @param EventDispatcherInterface $eventDispatcher + * @param CapClient $capClient + * @param LeadModel $leadModel + * @param IntegrationHelper $integrationHelper + */ + public function __construct( + private readonly EventDispatcherInterface $eventDispatcher, + private readonly CapClient $capClient, + private readonly LeadModel $leadModel, + + IntegrationHelper $integrationHelper + ) { + $integrationObject = $integrationHelper->getIntegrationObject(CapIntegration::INTEGRATION_NAME); + + $this->translator = $integrationObject->getTranslator(); + + if($integrationObject instanceof AbstractIntegration) { + $keys = $integrationObject->getKeys(); + + $this->serverUrl = isset($keys["server_url"]) ? rtrim($keys["server_url"], "/") : null; + $this->siteKey = $keys["site_key"] ?? null; + + if($this->serverUrl && $this->siteKey && isset($keys["secret_key"])) + $this->isConfigured = true; + } + } + + /** {@inheritDoc} */ + public static function getSubscribedEvents() { + return [ + FormEvents::FORM_ON_BUILD => ["onFormBuild", 0], + CaptchaEvents::CAP_ON_FORM_VALIDATE => ["onFormValidate", 0] + ]; + } + + /** + *

onFormBuild

+ * + * @param FormBuilderEvent $event + * + * @throws BadConfigurationException + * + * @return void + */ + public function onFormBuild(FormBuilderEvent $event): void { + if(!$this->isConfigured) + return; + + $event->addFormField("plugin.cap", [ + "label" => "strings.cap.plugin.name", + "formType" => CapType::class, + "template" => "@MauticMultiCaptcha/Integration/cap.html.twig", + "server_url" => $this->serverUrl, + "site_key" => $this->siteKey, + + "builderOptions" => [ + "addLeadFieldList" => false, + "addIsRequired" => false, + "addDefaultValue" => false, + "addSaveResult" => true + ] + ]); + + $event->addValidator("plugin.cap.validator", [ + "eventName" => CaptchaEvents::CAP_ON_FORM_VALIDATE, + "fieldType" => "plugin.cap" + ]); + } + + /** + *

onFormValidate

+ * + * @param ValidationEvent $event + * + * @throws GuzzleException + * @throws JsonException + * + * @return void + */ + public function onFormValidate(ValidationEvent $event) { + if(!$this->isConfigured) + return; + + if($this->capClient->verify($_POST["cap-token"] ?? "")) + return; + + $event->failedValidation($this->translator === null ? "Cap CAPTCHA was not successful." : $this->translator->trans("strings.cap.failure_message")); + + $this->eventDispatcher->addListener(LeadEvents::LEAD_POST_SAVE, function(LeadEvent $event) { + if(!$event->isNew()) + return; + + $leadId = $event->getLead(); + + $this->eventDispatcher->addListener("kernel.terminate", function() use ($leadId) { + $lead = $this->leadModel->getEntity($leadId); + + if($lead) + $this->leadModel->deleteEntity($lead); + }); + }, -255); + } + +} diff --git a/Form/Type/AltchaType.php b/Form/Type/AltchaType.php new file mode 100644 index 0000000..a1049d7 --- /dev/null +++ b/Form/Type/AltchaType.php @@ -0,0 +1,140 @@ +Class AltchaType + * + * Note: unlike the other CAPTCHA providers, ALTCHA does not set cookies or + * load any third-party tracking script, so there is deliberately no + * "explicit consent" toggle here - there is nothing to consent to. + * + * @package MauticPlugin\MauticMultiCaptchaBundle\Form\Type + * + * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html + */ +class AltchaType extends AbstractType { + + /** {@inheritDoc} */ + public function buildForm(FormBuilderInterface $builder, array $options): void { + $builder->add("complexity", ChoiceType::class, [ + "label" => "strings.altcha.settings.complexity", + "required" => false, + "data" => $options["data"]["complexity"] ?? "medium", + + "choices" => [ + "strings.altcha.settings.complexity.option.low" => "low", + "strings.altcha.settings.complexity.option.medium" => "medium", + "strings.altcha.settings.complexity.option.high" => "high" + ], + + "label_attr" => [ + "class" => "control-label" + ], + + "attr" => [ + "tooltip" => "strings.altcha.settings.complexity.tooltip" + ] + ])->add("expire", IntegerType::class, [ + "label" => "strings.altcha.settings.expire", + "required" => false, + "data" => isset($options["data"]["expire"]) ? (int) $options["data"]["expire"] : 600, + + "label_attr" => [ + "class" => "control-label" + ], + + "attr" => [ + "class" => "form-control", + "tooltip" => "strings.altcha.settings.expire.tooltip" + ] + ])->add("auto", ChoiceType::class, [ + "label" => "strings.altcha.settings.auto", + "required" => false, + "data" => $options["data"]["auto"] ?? "onload", + + "choices" => [ + "strings.altcha.settings.auto.option.onload" => "onload", + "strings.altcha.settings.auto.option.off" => "off" + ], + + "label_attr" => [ + "class" => "control-label" + ], + + "attr" => [ + "tooltip" => "strings.altcha.settings.auto.tooltip" + ] + ])->add("display", ChoiceType::class, [ + "label" => "strings.altcha.settings.display", + "required" => false, + "data" => $options["data"]["display"] ?? "standard", + + "choices" => [ + "strings.altcha.settings.display.option.standard" => "standard", + "strings.altcha.settings.display.option.bar" => "bar", + "strings.altcha.settings.display.option.floating" => "floating", + "strings.altcha.settings.display.option.overlay" => "overlay", + "strings.altcha.settings.display.option.invisible" => "invisible" + ], + + "label_attr" => [ + "class" => "control-label" + ], + + "attr" => [ + "tooltip" => "strings.altcha.settings.display.tooltip" + ] + ])->add("hideFooter", YesNoButtonGroupType::class, [ + "label" => "strings.altcha.settings.hide_footer", + "required" => false, + "data" => $options["data"]["hideFooter"] ?? false, + + "label_attr" => [ + "class" => "control-label" + ] + ])->add("hideLogo", YesNoButtonGroupType::class, [ + "label" => "strings.altcha.settings.hide_logo", + "required" => false, + "data" => $options["data"]["hideLogo"] ?? false, + + "label_attr" => [ + "class" => "control-label" + ] + ])->add("language", TextType::class, [ + "label" => "strings.altcha.settings.language", + "required" => false, + "data" => $options["data"]["language"] ?? "", + + "label_attr" => [ + "class" => "control-label" + ], + + "attr" => [ + "class" => "form-control", + "placeholder" => "strings.altcha.settings.language.placeholder", + "tooltip" => "strings.altcha.settings.language.tooltip" + ] + ]); + + if(!empty($options["action"])) + $builder->setAction($options["action"]); + } + + /** {@inheritDoc} */ + public function getBlockPrefix(): string { + return AltchaIntegration::INTEGRATION_NAME; + } + +} diff --git a/Form/Type/CapType.php b/Form/Type/CapType.php new file mode 100644 index 0000000..e5d2db6 --- /dev/null +++ b/Form/Type/CapType.php @@ -0,0 +1,63 @@ +Class CapType + * + * Cap always combines its proof-of-work and instrumentation challenges (both + * are configured server-side on the self-hosted Cap Standalone instance), so + * the only per-field option exposed here is how/when the widget solves: + * manually on click, automatically while still visible, or automatically + * while hidden (mirrors ALTCHA's invisible mode). + * + * @package MauticPlugin\MauticMultiCaptchaBundle\Form\Type + * + * @authors see: composer.json + * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html + */ +class CapType extends AbstractType { + + public const MODE_MANUAL = "manual"; + public const MODE_AUTO = "auto"; + public const MODE_AUTO_HIDDEN = "auto_hidden"; + + /** {@inheritDoc} */ + public function buildForm(FormBuilderInterface $builder, array $options) { + $builder->add("mode", ChoiceType::class, [ + "label" => "strings.cap.settings.mode", + "required" => false, + "data" => $options["data"]["mode"] ?? self::MODE_MANUAL, + + "choices" => [ + "strings.cap.settings.mode.option.manual" => self::MODE_MANUAL, + "strings.cap.settings.mode.option.auto" => self::MODE_AUTO, + "strings.cap.settings.mode.option.auto_hidden" => self::MODE_AUTO_HIDDEN + ], + + "label_attr" => [ + "class" => "control-label" + ], + + "attr" => [ + "tooltip" => "strings.cap.settings.mode.tooltip" + ] + ]); + + if(!empty($options["action"])) + $builder->setAction($options["action"]); + } + + /** {@inheritDoc} */ + public function getBlockPrefix() { + return CapIntegration::INTEGRATION_NAME; + } + +} diff --git a/Integration/AltchaIntegration.php b/Integration/AltchaIntegration.php new file mode 100644 index 0000000..5eca036 --- /dev/null +++ b/Integration/AltchaIntegration.php @@ -0,0 +1,157 @@ +Class AltchaIntegration + * + * ALTCHA is self-hosted by default: there is no third-party API to + * authenticate against, so authentication type is "none". Two ways of + * running it are supported side by side in the same settings form: + * + * - Self-hosted: just an "hmac_secret" that Mautic uses to sign/verify + * challenges on its own, with no outbound requests at all. + * - ALTCHA Sentinel: a "sentinel_domain" (your Sentinel instance's base + * URL), a "sentinel_api_key", and a "sentinel_api_secret". Sentinel + * issues challenges directly to the visitor's browser; Mautic only + * verifies the resulting server signature locally (no outbound request). + * + * None of the four fields are added via getRequiredKeyFields() - that would + * force ALL of them to be filled in simultaneously, making it impossible to + * use just the self-hosted mode or just Sentinel. They are added as optional + * fields via appendToForm() instead, and isConfigured() enforces the + * "one full set or the other" rule at runtime. + * + * @package MauticPlugin\MauticMultiCaptchaBundle\Integration + * + * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html + */ +class AltchaIntegration extends AbstractIntegration { + + public const INTEGRATION_NAME = "Altcha"; + + /** {@inheritDoc} */ + public function getName(): string { + return self::INTEGRATION_NAME; + } + + /** {@inheritDoc} */ + public function getDisplayName(): string { + return "ALTCHA"; + } + + /** {@inheritDoc} */ + public function getAuthenticationType(): string { + return "none"; + } + + /** + * Deliberately empty - see the class docblock. Fields are added via + * appendToForm() so none of them are forced to be non-empty simultaneously. + */ + public function getRequiredKeyFields(): array { + return []; + } + + /** {@inheritDoc} */ + public function getSecretKeys(): array { + return [ + "hmac_secret", + "sentinel_api_secret" + ]; + } + + /** + * Either the self-hosted HMAC secret OR the full set of Sentinel + * credentials is required to count as configured. + */ + public function isConfigured(): bool { + $keys = $this->getKeys(); + + $sentinelReady = !empty($keys["sentinel_domain"]) && !empty($keys["sentinel_api_key"]) && !empty($keys["sentinel_api_secret"]); + $selfHostedReady = !empty($keys["hmac_secret"]); + + return $sentinelReady || $selfHostedReady; + } + + /** {@inheritDoc} */ + public function appendToForm(&$builder, $data, $formArea): void { + if("keys" !== $formArea) + return; + + $builder->add("hmac_secret", PasswordType::class, [ + "label" => "strings.altcha.settings.hmac_secret", + "required" => false, + "data" => $data["hmac_secret"] ?? "", + + "label_attr" => [ + "class" => "control-label" + ], + + "attr" => [ + "class" => "form-control", + "tooltip" => "strings.altcha.settings.hmac_secret.notice", + "placeholder" => "strings.altcha.settings.hmac_secret.placeholder" + ] + ])->add("sentinel_domain", UrlType::class, [ + "label" => "strings.altcha.settings.sentinel_domain", + "required" => false, + "data" => $data["sentinel_domain"] ?? "", + + "label_attr" => [ + "class" => "control-label" + ], + + "attr" => [ + "class" => "form-control", + "tooltip" => "strings.altcha.settings.sentinel_domain.notice" + ] + ])->add("sentinel_api_key", TextType::class, [ + "label" => "strings.altcha.settings.sentinel_api_key", + "required" => false, + "data" => $data["sentinel_api_key"] ?? "", + + "label_attr" => [ + "class" => "control-label" + ], + + "attr" => [ + "class" => "form-control", + "tooltip" => "strings.altcha.settings.sentinel_api_key.notice" + ] + ])->add("sentinel_api_secret", PasswordType::class, [ + "label" => "strings.altcha.settings.sentinel_api_secret", + "required" => false, + "data" => $data["sentinel_api_secret"] ?? "", + + "label_attr" => [ + "class" => "control-label" + ], + + "attr" => [ + "class" => "form-control", + "tooltip" => "strings.altcha.settings.sentinel_api_secret.notice", + "placeholder" => "strings.altcha.settings.sentinel_api_secret.placeholder" + ] + ]); + } + + /** {@inheritDoc} */ + public function getFormNotes($section): array { + if(in_array($section, ["keys", "custom"], true)) { + return [ + "strings.altcha.settings.notice", + "info" + ]; + } + + return parent::getFormNotes($section); + } + +} diff --git a/Integration/CapIntegration.php b/Integration/CapIntegration.php new file mode 100644 index 0000000..0cacd90 --- /dev/null +++ b/Integration/CapIntegration.php @@ -0,0 +1,43 @@ +Class CapIntegration + * + * @package MauticPlugin\MauticMultiCaptchaBundle\Integration + * + * @authors see: composer.json + * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html + */ +class CapIntegration extends AbstractIntegration { + + public const INTEGRATION_NAME = "Cap"; + + /** {@inheritDoc} */ + public function getName() { + return self::INTEGRATION_NAME; + } + + /** {@inheritDoc} */ + public function getDisplayName() { + return "Cap CAPTCHA"; + } + + /** {@inheritDoc} */ + public function getAuthenticationType() { + return "none"; + } + + /** {@inheritDoc} */ + public function getRequiredKeyFields() { + return [ + "server_url" => "strings.cap.settings.server_url", + "site_key" => "strings.cap.settings.site_key", + "secret_key" => "strings.cap.settings.secret_key" + ]; + } + +} diff --git a/MauticMultiCaptchaBundle.php b/MauticMultiCaptchaBundle.php index a3a2ddc..3e95e2d 100644 --- a/MauticMultiCaptchaBundle.php +++ b/MauticMultiCaptchaBundle.php @@ -3,6 +3,8 @@ namespace MauticPlugin\MauticMultiCaptchaBundle; use Mautic\PluginBundle\Bundle\PluginBundleBase; +use MauticPlugin\MauticMultiCaptchaBundle\DependencyInjection\Compiler\EncryptionHelperAliasPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; /** *

Class MauticMultiCaptchaBundle

@@ -14,4 +16,10 @@ */ class MauticMultiCaptchaBundle extends PluginBundleBase { + public function build(ContainerBuilder $container): void + { + parent::build($container); + $container->addCompilerPass(new EncryptionHelperAliasPass()); + } + } diff --git a/Resources/views/Integration/altcha.html.twig b/Resources/views/Integration/altcha.html.twig new file mode 100644 index 0000000..90b0b45 --- /dev/null +++ b/Resources/views/Integration/altcha.html.twig @@ -0,0 +1,199 @@ +{# +Variables + - field + - formName (optional, string) + - fieldPage + - contactFields + - companyFields + - inBuilder + - fields + - inForm (optional, bool) + - required (optional, bool) +#} +{% set defaultInputClass = 'text' %} +{% set containerType = 'div-wrapper' %} + +{# start: field_helper #} +{% set defaultInputFormClass = defaultInputFormClass|default('') %} +{% set defaultLabelClass = defaultLabelClass|default('label') %} +{% set formName = formName|default('') %} +{% set defaultInputClass = 'mauticform-' ~ defaultInputClass %} +{% set defaultLabelClass = 'mauticform-' ~ defaultLabelClass %} +{% set containerClass = containerClass|default(containerType) %} +{% set order = field.order|default(0) %} +{% set validationMessage = '' %} + +{% set inputAttributes = htmlAttributesStringToArray(field.inputAttributes|default('')) %} +{% set labelAttributes = htmlAttributesStringToArray(field.labelAttributes|default('')) %} +{% set containerAttributes = htmlAttributesStringToArray(field.containerAttributes|default('')) %} + +{% if ignoreName is not defined or (ignoreName is defined and ignoreName is empty) %} + {% set inputName = 'mauticform[' ~ field.alias ~ ']' %} + {% if field.properties.multiple is defined %} + {% set inputName = inputName ~ '[]' %} + {% endif %} + {% set inputAttributes = inputAttributes|merge({ + 'name': inputName, + }) %} +{% endif %} + +{% if field.type not in ['checkboxgrp', 'radiogrp', 'textarea'] %} + {% set inputAttributes = inputAttributes|merge({ + 'value': field.defaultValue|default(''), + }) %} +{% endif %} + +{% if ignoreId is not defined or (ignoreId is defined and ignoreId is empty) %} + {% set inputAttributes = inputAttributes|merge({ + 'id': 'mauticform_input' ~ formName ~ '_' ~ field.alias, + }) %} + {% set labelAttributes = labelAttributes|merge({ + 'id': 'mauticform_label' ~ formName ~ '_' ~ field.alias, + 'for': 'mauticform_input' ~ formName ~ '_' ~ field.alias, + }) %} +{% endif %} + +{# Label and input #} +{% if inForm is defined and (true == inForm or inForm is not empty) %} + {% set labelAttributes = labelAttributes|merge({ + 'class': labelAttributes.class|default([])|merge([defaultLabelClass]), + }) %} + {% set inputAttributes = inputAttributes|merge({ + 'disabled': 'disabled', + 'class': inputAttributes.class|default([])|merge([defaultInputClass, defaultInputFormClass]), + }) %} +{% else %} + {% set labelAttributes = labelAttributes|merge({ + 'class': labelAttributes.class|default([])|merge([defaultLabelClass]), + }) %} + {% set inputAttributes = inputAttributes|merge({ + 'class': inputAttributes.class|default([])|merge([defaultInputClass]), + }) %} +{% endif %} + +{# Container #} +{% set containerAttributes = containerAttributes|merge({ + 'id': 'mauticform' ~ formName|default('') ~ '_' ~ id, + 'class': containerAttributes.class|default([])|merge([ + 'mauticform-row', + 'mauticform-' ~ containerClass, + 'mauticform-field-' ~ order, + ]), +}) %} +{% if field.parent and fields[field.parent] is defined %} + {% set values = field.conditions.values|join('|') %} + + {% if field.conditions.any is not empty and 'notIn' != field.conditions.expr %} + {% set values = '*' %} + {% endif %} + + {% set containerAttributes = containerAttributes|merge({ + 'data-mautic-form-show-on': fields[field.parent].alias ~ ':' ~ values, + 'data-mautic-form-expr': field.conditions.expr, + 'class': containerAttributes.class|merge([ + 'mauticform-field-hidden', + ]), + }) %} +{% endif %} + +{# Field is required #} +{% if field.isRequired is defined and field.isRequired %} + {% set required = true %} + {% set validationMessage = field.validationMessage %} + {% if validationMessage is empty %} + {% set validationMessage = 'mautic.form.field.generic.required'|trans([], 'validators') %} + {% endif %} + {% set containerAttributes = containerAttributes|merge({ + 'class': containerAttributes.class|default([])|merge([ + 'mauticform-required', + ]), + 'data-validate': field.alias, + 'data-validation-type': field.type, + }) %} +{% elseif required is defined and true == required %} + {% set containerAttributes = containerAttributes|merge({ + 'class': containerAttributes.class|default([])|merge([ + 'mauticform-required', + ]), + }) %} +{% endif %} +{# end: field_helper #} + +{% set formName = formName|replace({'_': ''})|default('mauticform') %} + +{% block altcha %} + {% set complexity = field.properties.complexity|default('medium') %} + {% set expireSeconds = field.properties.expire|default(600) %} + {% set autoMode = field.properties.auto|default('onload') %} + {% set displayMode = field.properties.display|default('standard') %} + {% set hideFooter = field.properties.hideFooter|default(false) %} + {% set hideLogo = field.properties.hideLogo|default(false) %} + {% set widgetVersion = field.properties.widgetVersion|default('v3') %} + {% set language = field.properties.language|default('') %} + + {# Languages that have a companion i18n JS file under Assets/js/i18n/. + Loading the file registers the translations in globalThis.$altcha.i18n + so the widget can find them for the configured language. #} + {% set i18nLanguages = ['fi', 'sv', 'de'] %} + + {% if field.showLabel %} + + {% endif %} + + {% if inForm is defined and (true == inForm or inForm is not empty) %} +
+ ALTCHA widget ({{ complexity }} complexity, {{ widgetVersion }}) +
+ {% else %} + {# Always a URL - never inline JSON. Mautic caches form HTML in + forms.cached_html, so a baked-in challenge would go stale for + every visitor until the form is next saved. The widget fetches + this URL live each time it loads. #} + {% set challengeUrl = altcha_challenge(complexity, expireSeconds) %} + + + +
+
+ +
+ + {% if challengeUrl is not null %} + {# v3 widget (dist/main/altcha.min.js): uses `challenge` attribute. + `challengeurl` (v2) is not observed by the v3 custom element. #} + {% set widgetConfig = {} %} + {% if hideFooter %}{% set widgetConfig = widgetConfig|merge({'hideFooter': true}) %}{% endif %} + {% if hideLogo %}{% set widgetConfig = widgetConfig|merge({'hideLogo': true}) %}{% endif %} + + {% else %} +
+ ALTCHA is not configured yet. Under Plugins → ALTCHA, add either an HMAC secret (self-hosted) or your Sentinel domain + API key + API secret. +
+ {% endif %} +
+ {% endif %} +{% endblock %} diff --git a/Resources/views/Integration/cap.html.twig b/Resources/views/Integration/cap.html.twig new file mode 100644 index 0000000..d9d41bf --- /dev/null +++ b/Resources/views/Integration/cap.html.twig @@ -0,0 +1,218 @@ +{# +Varables + - field + - formName (optional, string) + - fieldPage + - contactFields + - companyFields + - inBuilder + - fields + - inForm (optional, bool) + - required (optional, bool) +#} +{% set defaultInputClass = 'text' %} +{% set containerType = 'div-wrapper' %} + +{# start: field_helper #} +{% set defaultInputFormClass = defaultInputFormClass|default('') %} +{% set defaultLabelClass = defaultLabelClass|default('label') %} +{% set formName = formName|default('') %} +{% set defaultInputClass = 'mauticform-' ~ defaultInputClass %} +{% set defaultLabelClass = 'mauticform-' ~ defaultLabelClass %} +{% set containerClass = containerClass|default(containerType) %} +{% set order = field.order|default(0) %} +{% set validationMessage = '' %} + +{% set inputAttributes = htmlAttributesStringToArray(field.inputAttributes|default('')) %} +{% set labelAttributes = htmlAttributesStringToArray(field.labelAttributes|default('')) %} +{% set containerAttributes = htmlAttributesStringToArray(field.containerAttributes|default('')) %} + +{% if ignoreName is not defined or (ignoreName is defined and ignoreName is empty) %} + {% set inputName = 'mauticform[' ~ field.alias ~ ']' %} + {% if field.properties.multiple is defined %} + {% set inputName = inputName ~ '[]' %} + {% endif %} + {% set inputAttributes = inputAttributes|merge({ + 'name': inputName, + }) %} +{% endif %} + +{% if field.type not in ['checkboxgrp', 'radiogrp', 'textarea'] %} + {% set inputAttributes = inputAttributes|merge({ + 'value': field.defaultValue|default(''), + }) %} +{% endif %} + +{% if ignoreId is not defined or (ignoreId is defined and ignoreId is empty) %} + {% set inputAttributes = inputAttributes|merge({ + 'id': 'mauticform_input' ~ formName ~ '_' ~ field.alias, + }) %} + {% set labelAttributes = labelAttributes|merge({ + 'id': 'mauticform_label' ~ formName ~ '_' ~ field.alias, + 'for': 'mauticform_input' ~ formName ~ '_' ~ field.alias, + }) %} +{% endif %} + +{% if field.properties.placeholder is defined and field.properties.placeholder is not empty %} + {% set inputAttributes = inputAttributes|merge({ + 'placeholder': field.properties.placeholder, + }) %} +{% endif %} + + +{# Label and input #} +{% if inForm is defined and (true == inForm or inForm is not empty) %} + {% if field.type in ['button', 'pagebreak'] %} + {% set defaultInputFormClass = defaultInputFormClass ~ ' btn btn-ghost' %} + {% endif %} + + {% set labelAttributes = labelAttributes|merge({ + 'class': labelAttributes.class|default([])|merge([defaultLabelClass]), + }) %} + {% set inputAttributes = inputAttributes|merge({ + 'disabled': 'disabled', + 'class': inputAttributes.class|default([])|merge([defaultInputClass, defaultInputFormClass]), + }) %} +{% else %} + {% set labelAttributes = labelAttributes|merge({ + 'class': labelAttributes.class|default([])|merge([defaultLabelClass]), + }) %} + {% set inputAttributes = inputAttributes|merge({ + 'class': inputAttributes.class|default([])|merge([defaultInputClass]), + }) %} +{% endif %} + +{# Container #} +{% set containerAttributes = containerAttributes|merge({ + 'id': 'mauticform' ~ formName|default('') ~ '_' ~ id, + 'class': containerAttributes.class|default([])|merge([ + 'mauticform-row', + 'mauticform-' ~ containerClass, + 'mauticform-field-' ~ order, + ]), +}) %} +{% if field.parent and fields[field.parent] is defined %} + {% set values = field.conditions.values|join('|') %} + + {% if field.conditions.any is not empty and 'notIn' != field.conditions.expr %} + {% set values = '*' %} + {% endif %} + + {% set containerAttributes = containerAttributes|merge({ + 'data-mautic-form-show-on': fields[field.parent].alias ~ ':' ~ values, + 'data-mautic-form-expr': field.conditions.expr, + 'class': containerAttributes.class|merge([ + 'mauticform-field-hidden', + ]), + }) %} +{% endif %} + + +{# Field is required #} +{% if field.isRequired is defined and field.isRequired %} + {% set required = true %} + {% set validationMessage = field.validationMessage %} + {% if validationMessage is empty %} + {% set validationMessage = 'mautic.form.field.generic.required'|trans([], 'validators') %} + {% endif %} + {% set containerAttributes = containerAttributes|merge({ + 'class': containerAttributes.class|default([])|merge([ + 'mauticform-required', + ]), + 'data-validate': field.alias, + 'data-validation-type': field.type, + }) %} + {% if field.properties.multiple is defined and field.properties.multiple is not empty %} + {% set containerAttributes = containerAttributes|merge({ + 'data-validate-multiple': 'true', + }) %} + {% endif %} +{% elseif required is defined and true == required %} + {# Forced to be required #} + {% set containerAttributes = containerAttributes|merge({ + 'class': containerAttributes.class|default([])|merge([ + 'mauticform-required', + ]), + }) %} +{% endif %} +{# end: field_helper #} + +{% set action = app.request.get('objectAction') %} +{% set settings = field.properties %} + +{% set integrations = [] %} +{% if settings.integrations is defined and settings.integrations is not empty %} + {% set integrations = settings.integrations[0:-1]|split(',') %} +{% endif %} + +{% set formName = formName|replace({'_': ''})|default('mauticform') %} + +{% block cap %} + {% set serverUrl = field.customParameters.server_url|default('')|trim('/', 'right') %} + {% set siteKey = field.customParameters.site_key|default(null) %} + {% set mode = field.properties.mode|default('manual') %} + {% set autoSolve = mode in ['auto', 'auto_hidden'] %} + {% set hidden = mode == 'auto_hidden' %} + {% set widgetId = inputAttributes.id ~ '_widget' %} + + {% if field.showLabel %} + + {% endif %} + +
+ {# Error message container #} +
+ +
+ + {% if inForm is defined and (true == inForm or inForm is not empty) %} + {# In backend form builder - show placeholder only, no active widget #} +
+ Cap CAPTCHA Widget ({% if mode == 'auto' %}auto-solve{% elseif mode == 'auto_hidden' %}auto-solve, hidden{% else %}manual{% endif %}) +
+ {% else %} + {# In frontend - point the self-hosted widget script at our self-hosted WASM solver. + Served through a controller route (not the raw static asset) so it can carry an + Access-Control-Allow-Origin header: the widget loads this via fetch(), which + enforces CORS, unlike its own + + + + {% if autoSolve %} + {# Cap's own anti-tamper logic actively restores its branding link + whenever the widget host is visible, so hiding just the link isn't + possible - only hiding the entire widget (auto_hidden) is honored. #} + + {% endif %} + + + {% endif %} +
+{% endblock %} diff --git a/Service/AltchaClient.php b/Service/AltchaClient.php new file mode 100644 index 0000000..8423dca --- /dev/null +++ b/Service/AltchaClient.php @@ -0,0 +1,192 @@ +Class AltchaClient + * + * Supports two mutually exclusive ways of running ALTCHA: + * + * 1. Self-hosted (default): Mautic generates and signs its own challenge + * with a local HMAC secret, and verifies the solution locally too. No + * outbound HTTP request is ever made - privacy-friendly (no cookies, + * no external requests, no tracking). + * + * 2. ALTCHA Sentinel: challenges are issued directly by a Sentinel instance. + * The widget points at Sentinel's /v1/challenge endpoint, and Mautic only + * verifies the server signature locally using the API key's secret. + * Still no outbound network call on verification. + * + * If both a self-hosted HMAC secret and Sentinel credentials are filled in, + * Sentinel takes precedence. + * + * IMPORTANT: the widget's "challengeurl" attribute always points at a URL - + * never an inline JSON challenge. Mautic caches the entire rendered form HTML + * in `forms.cached_html` and only regenerates it when the form is saved, not + * on every page view. A challenge baked into that cached HTML would be reused + * by every visitor until the form's next save, going stale and causing + * "Verification failed. Try again later." for everyone. A URL sidesteps this + * entirely since the widget fetches a fresh challenge live each time it loads. + * + * @package MauticPlugin\MauticMultiCaptchaBundle\Service + * + * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html + */ +class AltchaClient { + + public const DEFAULT_MAX_NUMBER = 100000; + public const DEFAULT_EXPIRE_SECONDS = 600; + + /** Maps the field's "complexity" property to a proof-of-work maxNumber. */ + private const COMPLEXITY_MAP = [ + "low" => 50000, + "medium" => 100000, + "high" => 400000 + ]; + + private ?string $hmacSecret = null; + + private ?string $sentinelDomain = null; + private ?string $sentinelApiKey = null; + private ?string $sentinelApiSecret = null; + + public function __construct( + IntegrationHelper $integrationHelper, + private readonly UrlGeneratorInterface $router + ) { + $integrationObject = $integrationHelper->getIntegrationObject(AltchaIntegration::INTEGRATION_NAME); + + if($integrationObject instanceof AbstractIntegration) { + $keys = $integrationObject->getKeys(); + + $this->hmacSecret = $keys["hmac_secret"] ?? null; + + $this->sentinelDomain = $keys["sentinel_domain"] ?? null; + $this->sentinelApiKey = $keys["sentinel_api_key"] ?? null; + $this->sentinelApiSecret = $keys["sentinel_api_secret"] ?? null; + } + } + + public function usesSentinel(): bool { + return !empty($this->sentinelDomain) && !empty($this->sentinelApiKey) && !empty($this->sentinelApiSecret); + } + + public function hasSelfHostedSecret(): bool { + return !empty($this->hmacSecret); + } + + public function isConfigured(): bool { + return $this->usesSentinel() || $this->hasSelfHostedSecret(); + } + + public function buildSentinelChallengeUrl(): string { + $domain = rtrim((string) $this->sentinelDomain, "/"); + + return $domain . "/v1/challenge?apiKey=" . rawurlencode((string) $this->sentinelApiKey); + } + + /** + * Generates a fresh signed challenge. Must be called on every request + * (via ChallengeController) - never cache or reuse a challenge. + * + * @return array{algorithm: string, challenge: string, salt: string, signature: string, maxNumber: int} + */ + public function createChallenge(int $maxNumber = self::DEFAULT_MAX_NUMBER, int $expireSeconds = self::DEFAULT_EXPIRE_SECONDS): array { + $altcha = new Altcha((string) $this->hmacSecret); + + $challenge = $altcha->createChallenge(new ChallengeOptions( + algorithm: Algorithm::SHA256, + maxNumber: $maxNumber, + expires: new \DateTimeImmutable("+{$expireSeconds} seconds") + )); + + return [ + "algorithm" => $challenge->algorithm, + "challenge" => $challenge->challenge, + "salt" => $challenge->salt, + "signature" => $challenge->signature, + // Must be camelCase - widgets >= v1.4.0 require "maxNumber", + // not "maxnumber". A lowercase key makes the widget treat the + // challenge as malformed and fail immediately client-side. + "maxNumber" => $challenge->maxNumber + ]; + } + + public function createChallengeForComplexity(string $complexity, int $expireSeconds = self::DEFAULT_EXPIRE_SECONDS): ?array { + if(!$this->hasSelfHostedSecret()) + return null; + + $maxNumber = self::COMPLEXITY_MAP[$complexity] ?? self::COMPLEXITY_MAP["medium"]; + + return $this->createChallenge($maxNumber, $expireSeconds); + } + + public function buildSelfHostedChallengeUrl(string $complexity, int $expireSeconds): string { + return $this->router->generate("mautic_altcha_challenge", [ + "complexity" => $complexity, + "expire" => $expireSeconds + ], UrlGeneratorInterface::ABSOLUTE_URL); + } + + /** + * Returns the URL the widget's "challengeurl" attribute should contain. + * Always a URL (never inline JSON) - see class docblock for why. + * + * @return string|null Null when neither mode is configured yet. + */ + public function buildWidgetChallenge(string $complexity = "medium", int $expireSeconds = self::DEFAULT_EXPIRE_SECONDS): ?string { + if($this->usesSentinel()) + return $this->buildSentinelChallengeUrl(); + + if(!$this->hasSelfHostedSecret()) + return null; + + return $this->buildSelfHostedChallengeUrl($complexity, $expireSeconds); + } + + /** + * Verifies the base64-encoded payload the submits. + * + * - Self-hosted: local HMAC signature + expiry + proof-of-work check. No network call. + * - Sentinel: verifies the server signature locally using the API key's secret. No network call. + */ + public function verify(string $payload): bool { + if("" === trim($payload)) + return false; + + if($this->usesSentinel()) { + try { + $result = ServerSignature::verifyServerSignature($payload, (string) $this->sentinelApiSecret); + + return (bool) $result->verified; + } catch(\Throwable) { + return false; + } + } + + if(!$this->hasSelfHostedSecret()) + return false; + + $altcha = new Altcha((string) $this->hmacSecret); + + try { + return $altcha->verifySolution($payload, true); + } catch(\Throwable) { + return false; + } + } + +} diff --git a/Service/CapClient.php b/Service/CapClient.php new file mode 100644 index 0000000..088fa27 --- /dev/null +++ b/Service/CapClient.php @@ -0,0 +1,79 @@ +Class CapClient + * + * @package MauticPlugin\MauticMultiCaptchaBundle\Service + * + * @authors see: composer.json + * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html + */ +class CapClient { + + private ?string $serverUrl; + private ?string $secretKey; + + private GuzzleClientInterface $httpClient; + + /** + *

CapClient constructor.

+ * + * @param IntegrationHelper $integrationHelper + * @param GuzzleClientInterface|null $httpClient Injectable for testing; a real Guzzle client is used when omitted. + */ + public function __construct(IntegrationHelper $integrationHelper, ?GuzzleClientInterface $httpClient = null) { + $integrationObject = $integrationHelper->getIntegrationObject(CapIntegration::INTEGRATION_NAME); + + if($integrationObject instanceof AbstractIntegration) { + $keys = $integrationObject->getKeys(); + + $this->serverUrl = isset($keys["server_url"]) ? rtrim($keys["server_url"], "/") : null; + $this->secretKey = $keys["secret_key"] ?? null; + } + + $this->httpClient = $httpClient ?? new GuzzleClient([ + "timeout" => 10 + ]); + } + + /** + *

verify

+ * + * Verifies a redeemed Cap token against the self-hosted Cap Standalone + * instance's /siteverify endpoint. The site key does not need to be sent + * separately, it is embedded in the redeemed token itself. + * + * @param string $token + * + * @throws GuzzleException + * @throws JsonException + * + * @return bool + */ + public function verify(string $token): bool { + $guzzleResponse = $this->httpClient->post("{$this->serverUrl}/siteverify", [ + "json" => [ + "secret" => $this->secretKey, + "response" => $token + ] + ]); + + $response = json_decode($guzzleResponse->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); + + return array_key_exists("success", $response) && $response["success"] === true; + } + +} diff --git a/Tests/Controller/AltchaApiControllerTest.php b/Tests/Controller/AltchaApiControllerTest.php new file mode 100644 index 0000000..58b1967 --- /dev/null +++ b/Tests/Controller/AltchaApiControllerTest.php @@ -0,0 +1,155 @@ +createMock(AltchaClient::class); + $client->expects($this->never())->method('createChallengeForComplexity'); + + $controller = new ChallengeController($client); + $request = new Request([], [], [], [], [], ['REQUEST_METHOD' => 'OPTIONS']); + + $response = $controller->__invoke($request); + + $this->assertEquals(Response::HTTP_NO_CONTENT, $response->getStatusCode()); + $this->assertEquals('*', $response->headers->get('Access-Control-Allow-Origin')); + $this->assertEquals('GET, OPTIONS', $response->headers->get('Access-Control-Allow-Methods')); + $this->assertNotEmpty($response->headers->get('Access-Control-Allow-Headers')); + } + + /** + * Test: GET with default params calls createChallengeForComplexity with 'medium' + default expire + * + * @test + */ + public function testApiUsesDefaultComplexityAndExpire(): void { + $client = $this->createMock(AltchaClient::class); + $client->expects($this->once()) + ->method('createChallengeForComplexity') + ->with('medium', AltchaClient::DEFAULT_EXPIRE_SECONDS) + ->willReturn([ + 'algorithm' => 'SHA-256', + 'challenge' => 'test', + 'salt' => 'test', + 'signature' => 'test', + 'maxNumber' => 100000, + ]); + + $controller = new ChallengeController($client); + $response = $controller->__invoke(new Request()); + + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } + + /** + * Test: GET with explicit complexity and expire passes them through + * + * @test + */ + public function testApiPassesComplexityAndExpireFromQueryString(): void { + $client = $this->createMock(AltchaClient::class); + $client->expects($this->once()) + ->method('createChallengeForComplexity') + ->with('high', 300) + ->willReturn([ + 'algorithm' => 'SHA-256', + 'challenge' => 'test', + 'salt' => 'test', + 'signature' => 'test', + 'maxNumber' => 400000, + ]); + + $controller = new ChallengeController($client); + $request = new Request(['complexity' => 'high', 'expire' => '300']); + + $response = $controller->__invoke($request); + + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } + + /** + * Test: returns 503 when client is not configured (createChallengeForComplexity returns null) + * + * @test + */ + public function testApiReturnsErrorWhenNotConfigured(): void { + $client = $this->createMock(AltchaClient::class); + $client->method('createChallengeForComplexity')->willReturn(null); + + $controller = new ChallengeController($client); + $response = $controller->__invoke(new Request()); + + $this->assertEquals(Response::HTTP_SERVICE_UNAVAILABLE, $response->getStatusCode()); + } + + /** + * Test: successful response includes CORS headers and no-store cache control + * + * @test + */ + public function testApiIncludesCorsAndCacheHeaders(): void { + $client = $this->createMock(AltchaClient::class); + $client->method('createChallengeForComplexity')->willReturn([ + 'algorithm' => 'SHA-256', + 'challenge' => 'test', + 'salt' => 'test', + 'signature' => 'test', + 'maxNumber' => 100000, + ]); + + $controller = new ChallengeController($client); + $response = $controller->__invoke(new Request()); + + $this->assertEquals('*', $response->headers->get('Access-Control-Allow-Origin')); + $this->assertStringContainsString('no-store', $response->headers->get('Cache-Control')); + } + + /** + * Property test: expire is clamped between 30 and 3600 regardless of query param + * + * @test + */ + public function testExpireIsClampedToSafeBounds(): void { + $cases = [ + ['input' => '0', 'expected_min' => 30, 'expected_max' => 30], + ['input' => '29', 'expected_min' => 30, 'expected_max' => 30], + ['input' => '600', 'expected_min' => 600, 'expected_max' => 600], + ['input' => '3600', 'expected_min' => 3600, 'expected_max' => 3600], + ['input' => '9999', 'expected_min' => 3600, 'expected_max' => 3600], + ]; + + foreach ($cases as $case) { + $capturedExpire = null; + + $client = $this->createMock(AltchaClient::class); + $client->method('createChallengeForComplexity') + ->willReturnCallback(function(string $complexity, int $expire) use (&$capturedExpire) { + $capturedExpire = $expire; + return ['algorithm' => 'SHA-256', 'challenge' => 'x', 'salt' => 'x', 'signature' => 'x', 'maxNumber' => 1]; + }); + + $controller = new ChallengeController($client); + $controller->__invoke(new Request(['expire' => $case['input']])); + + $this->assertGreaterThanOrEqual($case['expected_min'], $capturedExpire); + $this->assertLessThanOrEqual($case['expected_max'], $capturedExpire); + } + } + +} diff --git a/Tests/Controller/CapAssetControllerTest.php b/Tests/Controller/CapAssetControllerTest.php new file mode 100644 index 0000000..1922d7b --- /dev/null +++ b/Tests/Controller/CapAssetControllerTest.php @@ -0,0 +1,61 @@ + tag, but it fetches its WASM PoW + * solver via fetch(), which enforces CORS. This controller serves that + * binary with an explicit Access-Control-Allow-Origin header instead of + * relying on the plugin's raw static asset path. + */ +class CapAssetControllerTest extends TestCase { + + /** + * @test + */ + public function testWasmActionReturnsBinaryWithCorsHeader(): void { + $controller = new CapAssetController(); + $response = $controller->wasmAction(new Request()); + + $this->assertInstanceOf(BinaryFileResponse::class, $response); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + $this->assertEquals('*', $response->headers->get('Access-Control-Allow-Origin')); + $this->assertEquals('application/wasm', $response->headers->get('Content-Type')); + } + + /** + * @test + */ + public function testWasmActionServesTheActualBundledFile(): void { + $controller = new CapAssetController(); + $response = $controller->wasmAction(new Request()); + + $expectedPath = realpath(__DIR__ . '/../../Assets/js/cap_wasm_bg.wasm'); + + $this->assertEquals($expectedPath, realpath($response->getFile()->getPathname())); + } + + /** + * @test + */ + public function testWasmActionHandlesCorsPreflightRequest(): void { + $controller = new CapAssetController(); + $request = new Request([], [], [], [], [], ['REQUEST_METHOD' => 'OPTIONS']); + + $response = $controller->wasmAction($request); + + $this->assertEquals(Response::HTTP_NO_CONTENT, $response->getStatusCode()); + $this->assertEquals('*', $response->headers->get('Access-Control-Allow-Origin')); + $this->assertEquals('GET, OPTIONS', $response->headers->get('Access-Control-Allow-Methods')); + } + +} diff --git a/Tests/EventListener/AltchaFormSubscriberTest.php b/Tests/EventListener/AltchaFormSubscriberTest.php new file mode 100644 index 0000000..583f9c0 --- /dev/null +++ b/Tests/EventListener/AltchaFormSubscriberTest.php @@ -0,0 +1,210 @@ +createMock(EventDispatcherInterface::class); + $eventDispatcher->method('addListener') + ->willReturnCallback(function($eventName, $listener, $priority = 0) use (&$registeredListeners) { + $registeredListeners[] = ['event' => $eventName, 'listener' => $listener, 'priority' => $priority]; + }); + + $altchaClient = $this->createMock(AltchaClient::class); + $altchaClient->method('isConfigured')->willReturn(true); + $altchaClient->method('verify')->willReturn(false); + + $leadDeleteCalled = false; + $leadModel = $this->createMock(LeadModel::class); + $leadModel->method('deleteEntity')->willReturnCallback(function() use (&$leadDeleteCalled) { + $leadDeleteCalled = true; + }); + + $subscriber = $this->createSubscriber($eventDispatcher, $altchaClient, $leadModel); + + $validationEvent = new ValidationEvent(new Field(), 'invalid-payload'); + + $subscriber->onFormValidate($validationEvent); + + if ($validationEvent->isValid()) { + $failures[] = ['iteration' => $i, 'reason' => 'Validation did not fail']; + continue; + } + + // LEAD_POST_SAVE listener must have been registered + $leadPostSaveListener = null; + foreach ($registeredListeners as $l) { + if ($l['event'] === LeadEvents::LEAD_POST_SAVE) { + $leadPostSaveListener = $l; + break; + } + } + + if ($leadPostSaveListener === null) { + $failures[] = ['iteration' => $i, 'reason' => 'LEAD_POST_SAVE listener not registered']; + continue; + } + + if ($leadPostSaveListener['priority'] !== -255) { + $failures[] = ['iteration' => $i, 'reason' => 'Wrong LEAD_POST_SAVE priority', 'actual' => $leadPostSaveListener['priority']]; + continue; + } + + // Simulate LEAD_POST_SAVE with a new lead + $registeredListeners = []; + $lead = new Lead(); + $lead->setId(123); + ($leadPostSaveListener['listener'])(new LeadEvent($lead, true)); + + // kernel.terminate must be registered next + $kernelTerminateListener = null; + foreach ($registeredListeners as $l) { + if ($l['event'] === 'kernel.terminate') { + $kernelTerminateListener = $l; + break; + } + } + + if ($kernelTerminateListener === null) { + $failures[] = ['iteration' => $i, 'reason' => 'kernel.terminate listener not registered']; + continue; + } + + // Simulate kernel.terminate — lead must be deleted + ($kernelTerminateListener['listener'])(); + + if (!$leadDeleteCalled) { + $failures[] = ['iteration' => $i, 'reason' => 'Lead not deleted after kernel.terminate']; + } + } + + $this->assertEmpty($failures, sprintf( + "Lead cleanup after failed validation failed in %d/%d iterations:\n%s", + count($failures), $iterations, json_encode($failures, JSON_PRETTY_PRINT) + )); + } + + /** + * Test: Existing leads are NOT deleted after failed validation + * + * @test + */ + public function testLeadCleanupSkipsExistingLeads(): void { + $registeredListeners = []; + + $eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $eventDispatcher->method('addListener') + ->willReturnCallback(function($eventName, $listener, $priority = 0) use (&$registeredListeners) { + $registeredListeners[] = ['event' => $eventName, 'listener' => $listener, 'priority' => $priority]; + }); + + $altchaClient = $this->createMock(AltchaClient::class); + $altchaClient->method('isConfigured')->willReturn(true); + $altchaClient->method('verify')->willReturn(false); + + $leadDeleteCalled = false; + $leadModel = $this->createMock(LeadModel::class); + $leadModel->method('deleteEntity')->willReturnCallback(function() use (&$leadDeleteCalled) { + $leadDeleteCalled = true; + }); + + $subscriber = $this->createSubscriber($eventDispatcher, $altchaClient, $leadModel); + + $validationEvent = new ValidationEvent(new Field(), 'invalid-payload'); + $subscriber->onFormValidate($validationEvent); + + // Find LEAD_POST_SAVE listener + $leadPostSaveListener = null; + foreach ($registeredListeners as $l) { + if ($l['event'] === LeadEvents::LEAD_POST_SAVE) { + $leadPostSaveListener = $l; + break; + } + } + + $this->assertNotNull($leadPostSaveListener); + + // Simulate with existing lead (isNew = false) + $registeredListeners = []; + $lead = new Lead(); + $lead->setId(456); + ($leadPostSaveListener['listener'])(new LeadEvent($lead, false)); + + $hasKernelTerminate = !empty(array_filter($registeredListeners, fn($l) => $l['event'] === 'kernel.terminate')); + + $this->assertFalse($hasKernelTerminate, 'kernel.terminate should not be registered for existing leads'); + $this->assertFalse($leadDeleteCalled, 'Existing lead must not be deleted'); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private function createSubscriber( + EventDispatcherInterface $eventDispatcher, + AltchaClient $altchaClient, + LeadModel $leadModel + ): AltchaFormSubscriber { + $translator = $this->createMock(TranslatorInterface::class); + $translator->method('trans')->willReturn('ALTCHA verification failed.'); + + $integration = $this->createMock(AbstractIntegration::class); + $integration->method('getKeys')->willReturn(['hmac_secret' => 'test-key']); + $integration->method('getTranslator')->willReturn($translator); + + $integrationHelper = $this->createMock(IntegrationHelper::class); + $integrationHelper->method('getIntegrationObject') + ->with(AltchaIntegration::INTEGRATION_NAME) + ->willReturn($integration); + + $request = $this->createMock(Request::class); + $request->request = new \Symfony\Component\HttpFoundation\InputBag(['altcha' => 'invalid-payload']); + + $requestStack = $this->createMock(RequestStack::class); + $requestStack->method('getCurrentRequest')->willReturn($request); + + return new AltchaFormSubscriber( + $eventDispatcher, + $altchaClient, + $leadModel, + $requestStack, + $integrationHelper + ); + } + +} diff --git a/Tests/EventListener/CapFormSubscriberTest.php b/Tests/EventListener/CapFormSubscriberTest.php new file mode 100644 index 0000000..27e34ae --- /dev/null +++ b/Tests/EventListener/CapFormSubscriberTest.php @@ -0,0 +1,321 @@ +assertArrayHasKey(FormEvents::FORM_ON_BUILD, $events); + $this->assertArrayHasKey(CaptchaEvents::CAP_ON_FORM_VALIDATE, $events); + } + + /** + * @test + */ + public function testOnFormBuildAddsFieldWhenFullyConfigured(): void { + $subscriber = $this->createSubscriber( + $this->createMock(CapClient::class), + $this->createMock(LeadModel::class), + [ + 'server_url' => 'https://cap.example.com', + 'site_key' => 'site-key-123', + 'secret_key' => 'secret-abc' + ] + ); + + $event = new FormBuilderEvent($this->createMock(SymfonyTranslatorInterface::class)); + $subscriber->onFormBuild($event); + + $fields = $event->getFormFields(); + $this->assertArrayHasKey('plugin.cap', $fields); + $this->assertEquals('https://cap.example.com', $fields['plugin.cap']['server_url']); + $this->assertEquals('site-key-123', $fields['plugin.cap']['site_key']); + + // getValidators() organises by fieldType, not by key + $validators = $event->getValidators(); + $this->assertArrayHasKey('plugin.cap', $validators); + $this->assertEquals(CaptchaEvents::CAP_ON_FORM_VALIDATE, $validators['plugin.cap']); + } + + /** + * Property Test: onFormBuild is a no-op when any required key is missing + * + * @test + */ + public function testOnFormBuildSkipsFieldWhenNotFullyConfigured(): void { + $incompleteConfigs = [ + [], + ['server_url' => 'https://cap.example.com'], + ['server_url' => 'https://cap.example.com', 'site_key' => 'site-key-123'], + ['site_key' => 'site-key-123', 'secret_key' => 'secret-abc'], + ]; + + foreach ($incompleteConfigs as $keys) { + $subscriber = $this->createSubscriber( + $this->createMock(CapClient::class), + $this->createMock(LeadModel::class), + $keys + ); + + $event = new FormBuilderEvent($this->createMock(SymfonyTranslatorInterface::class)); + $subscriber->onFormBuild($event); + + $this->assertEmpty($event->getFormFields()); + // getValidators() always returns at least ['form' => []] - confirm no fieldType validators added + $validators = $event->getValidators(); + $this->assertArrayNotHasKey('plugin.cap', $validators); + } + } + + /** + * @test + */ + public function testOnFormValidatePassesWhenTokenVerifies(): void { + $_POST['cap-token'] = 'sitekey:id:secret'; + + $capClient = $this->createMock(CapClient::class); + $capClient->expects($this->once()) + ->method('verify') + ->with('sitekey:id:secret') + ->willReturn(true); + + $subscriber = $this->createSubscriber($capClient, $this->createMock(LeadModel::class)); + + $event = new ValidationEvent(new Field(), 'sitekey:id:secret'); + $subscriber->onFormValidate($event); + + $this->assertTrue($event->isValid()); + } + + /** + * @test + */ + public function testOnFormValidateTreatsMissingTokenAsEmptyString(): void { + unset($_POST['cap-token']); + + $capClient = $this->createMock(CapClient::class); + $capClient->expects($this->once()) + ->method('verify') + ->with('') + ->willReturn(false); + + $subscriber = $this->createSubscriber($capClient, $this->createMock(LeadModel::class)); + + $event = new ValidationEvent(new Field(), ''); + $subscriber->onFormValidate($event); + + $this->assertFalse($event->isValid()); + } + + /** + * @test + */ + public function testOnFormValidateIsNoOpWhenNotConfigured(): void { + $capClient = $this->createMock(CapClient::class); + $capClient->expects($this->never())->method('verify'); + + $subscriber = $this->createSubscriber($capClient, $this->createMock(LeadModel::class), []); + + $event = new ValidationEvent(new Field(), ''); + $subscriber->onFormValidate($event); + + $this->assertTrue($event->isValid()); + } + + /** + * Property Test: Lead cleanup after failed Cap validation + * + * @test + */ + public function testLeadCleanupAfterFailedValidation(): void { + $_POST['cap-token'] = 'sitekey:id:bad-secret'; + + $registeredListeners = []; + $eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $eventDispatcher->method('addListener') + ->willReturnCallback(function ($eventName, $listener, $priority = 0) use (&$registeredListeners) { + $registeredListeners[] = ['event' => $eventName, 'listener' => $listener, 'priority' => $priority]; + }); + + $capClient = $this->createMock(CapClient::class); + $capClient->method('verify')->willReturn(false); + + $leadModel = $this->createMock(LeadModel::class); + $leadDeleteCalled = false; + $leadModel->method('getEntity')->willReturn($this->createMock(Lead::class)); + $leadModel->method('deleteEntity') + ->willReturnCallback(function () use (&$leadDeleteCalled) { + $leadDeleteCalled = true; + }); + + $subscriber = $this->createSubscriber($capClient, $leadModel, [ + 'server_url' => 'https://cap.example.com', + 'site_key' => 'site-key-123', + 'secret_key' => 'secret-abc' + ], $eventDispatcher); + + $validationEvent = new ValidationEvent(new Field(), 'sitekey:id:bad-secret'); + $subscriber->onFormValidate($validationEvent); + + $this->assertFalse($validationEvent->isValid()); + + $leadPostSaveListener = null; + foreach ($registeredListeners as $listener) { + if ($listener['event'] === LeadEvents::LEAD_POST_SAVE) { + $leadPostSaveListener = $listener; + break; + } + } + + $this->assertNotNull($leadPostSaveListener, 'LEAD_POST_SAVE listener should be registered'); + $this->assertEquals(-255, $leadPostSaveListener['priority']); + + $lead = new Lead(); + $lead->setId(123); + $leadEvent = new LeadEvent($lead, true); + + $registeredListeners = []; + ($leadPostSaveListener['listener'])($leadEvent); + + $kernelTerminateListener = null; + foreach ($registeredListeners as $listener) { + if ($listener['event'] === 'kernel.terminate') { + $kernelTerminateListener = $listener; + break; + } + } + + $this->assertNotNull($kernelTerminateListener, 'kernel.terminate listener should be registered'); + + ($kernelTerminateListener['listener'])(); + + $this->assertTrue($leadDeleteCalled, 'Lead should be deleted after kernel.terminate'); + } + + /** + * @test + */ + public function testLeadCleanupSkipsExistingLeads(): void { + $_POST['cap-token'] = 'sitekey:id:bad-secret'; + + $registeredListeners = []; + $eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $eventDispatcher->method('addListener') + ->willReturnCallback(function ($eventName, $listener, $priority = 0) use (&$registeredListeners) { + $registeredListeners[] = ['event' => $eventName, 'listener' => $listener, 'priority' => $priority]; + }); + + $capClient = $this->createMock(CapClient::class); + $capClient->method('verify')->willReturn(false); + + $leadModel = $this->createMock(LeadModel::class); + $leadDeleteCalled = false; + $leadModel->method('deleteEntity') + ->willReturnCallback(function () use (&$leadDeleteCalled) { + $leadDeleteCalled = true; + }); + + $subscriber = $this->createSubscriber($capClient, $leadModel, [ + 'server_url' => 'https://cap.example.com', + 'site_key' => 'site-key-123', + 'secret_key' => 'secret-abc' + ], $eventDispatcher); + + $validationEvent = new ValidationEvent(new Field(), 'sitekey:id:bad-secret'); + $subscriber->onFormValidate($validationEvent); + + $leadPostSaveListener = null; + foreach ($registeredListeners as $listener) { + if ($listener['event'] === LeadEvents::LEAD_POST_SAVE) { + $leadPostSaveListener = $listener; + break; + } + } + + $this->assertNotNull($leadPostSaveListener); + + $lead = new Lead(); + $lead->setId(456); + $leadEvent = new LeadEvent($lead, false); // not new + + $registeredListeners = []; + ($leadPostSaveListener['listener'])($leadEvent); + + $hasKernelTerminate = false; + foreach ($registeredListeners as $listener) { + if ($listener['event'] === 'kernel.terminate') { + $hasKernelTerminate = true; + } + } + + $this->assertFalse($hasKernelTerminate); + $this->assertFalse($leadDeleteCalled); + } + + /** + * Helper: Create a CapFormSubscriber with mocked dependencies. + */ + private function createSubscriber( + CapClient $capClient, + LeadModel $leadModel, + array $keys = ['server_url' => 'https://cap.example.com', 'site_key' => 'site-key-123', 'secret_key' => 'secret-abc'], + ?EventDispatcherInterface $eventDispatcher = null + ): CapFormSubscriber { + $integrationHelper = $this->createMock(IntegrationHelper::class); + + $integration = $this->createMock(AbstractIntegration::class); + $integration->method('getKeys')->willReturn($keys); + + $translator = $this->createMock(TranslatorInterface::class); + $translator->method('trans')->willReturn('Cap CAPTCHA verification failed.'); + $integration->method('getTranslator')->willReturn($translator); + + $integrationHelper->method('getIntegrationObject') + ->with(CapIntegration::INTEGRATION_NAME) + ->willReturn($integration); + + return new CapFormSubscriber( + $eventDispatcher ?? $this->createMock(EventDispatcherInterface::class), + $capClient, + $leadModel, + $integrationHelper + ); + } + +} diff --git a/Tests/Form/Type/AltchaTypeTest.php b/Tests/Form/Type/AltchaTypeTest.php new file mode 100644 index 0000000..9efd286 --- /dev/null +++ b/Tests/Form/Type/AltchaTypeTest.php @@ -0,0 +1,96 @@ +formFactory = Forms::createFormFactoryBuilder() + ->addExtension(new \Symfony\Component\Form\Extension\Validator\ValidatorExtension( + Validation::createValidator() + )) + ->getFormFactory(); + } + + /** + * Test: complexity field accepts all valid values + * + * @test + */ + public function testComplexityFieldAcceptsValidValues(): void { + foreach (['low', 'medium', 'high'] as $complexity) { + $form = $this->formFactory->create(AltchaType::class, ['complexity' => $complexity]); + $form->submit(['complexity' => $complexity, 'expire' => 600, 'auto' => 'onsubmit', 'display' => 'standard', 'hideFooter' => false, 'hideLogo' => false]); + + $this->assertTrue( + $form->isValid(), + "Form should be valid for complexity: {$complexity}" + ); + } + } + + /** + * Test: display field accepts all valid values including invisible + * + * @test + */ + public function testDisplayFieldAcceptsValidValues(): void { + foreach (['standard', 'bar', 'floating', 'overlay', 'invisible'] as $display) { + $form = $this->formFactory->create(AltchaType::class, ['display' => $display]); + $form->submit(['complexity' => 'medium', 'expire' => 600, 'auto' => 'onsubmit', 'display' => $display, 'hideFooter' => false, 'hideLogo' => false]); + + $this->assertTrue( + $form->isValid(), + "Form should be valid for display mode: {$display}" + ); + } + } + + /** + * Test: auto field accepts all valid values + * + * @test + */ + public function testAutoFieldAcceptsValidValues(): void { + foreach (['onload', 'onsubmit', 'off'] as $auto) { + $form = $this->formFactory->create(AltchaType::class, ['auto' => $auto]); + $form->submit(['complexity' => 'medium', 'expire' => 600, 'auto' => $auto, 'display' => 'standard', 'hideFooter' => false, 'hideLogo' => false]); + + $this->assertTrue( + $form->isValid(), + "Form should be valid for auto mode: {$auto}" + ); + } + } + + /** + * Test: expire field accepts integer values + * + * @test + */ + public function testExpireFieldAcceptsIntegerValues(): void { + foreach ([30, 120, 600, 3600] as $expire) { + $form = $this->formFactory->create(AltchaType::class, ['expire' => $expire]); + $form->submit(['complexity' => 'medium', 'expire' => $expire, 'auto' => 'onsubmit', 'display' => 'standard', 'hideFooter' => false, 'hideLogo' => false]); + + $this->assertTrue( + $form->isValid(), + "Form should be valid for expire: {$expire}" + ); + } + } + +} diff --git a/Tests/Form/Type/CapTypeTest.php b/Tests/Form/Type/CapTypeTest.php new file mode 100644 index 0000000..ffb7702 --- /dev/null +++ b/Tests/Form/Type/CapTypeTest.php @@ -0,0 +1,92 @@ +formFactory = Forms::createFormFactoryBuilder()->getFormFactory(); + } + + /** + * @test + */ + public function testFormHasOnlyModeField(): void { + $form = $this->formFactory->create(CapType::class); + + $this->assertCount(1, $form->all()); + $this->assertTrue($form->has('mode')); + } + + /** + * @test + */ + public function testModeDefaultsToManual(): void { + $form = $this->formFactory->create(CapType::class, []); + + $this->assertEquals(CapType::MODE_MANUAL, $form->get('mode')->getData()); + } + + /** + * Property Test: every documented mode value is a valid, submittable choice + * + * @test + */ + public function testEveryModeChoiceIsSubmittable(): void { + foreach ([CapType::MODE_MANUAL, CapType::MODE_AUTO, CapType::MODE_AUTO_HIDDEN] as $mode) { + $form = $this->formFactory->create(CapType::class); + + $form->submit(['mode' => $mode]); + + $this->assertTrue($form->isValid(), "mode={$mode} should be a valid submission"); + $this->assertEquals($mode, $form->get('mode')->getData()); + } + } + + /** + * @test + */ + public function testModeRespectsStoredValue(): void { + $form = $this->formFactory->create(CapType::class, [ + 'mode' => CapType::MODE_AUTO_HIDDEN + ]); + + $this->assertEquals(CapType::MODE_AUTO_HIDDEN, $form->get('mode')->getData()); + } + + /** + * @test + */ + public function testBlockPrefixMatchesIntegrationName(): void { + $type = new CapType(); + + $this->assertEquals(CapIntegration::INTEGRATION_NAME, $type->getBlockPrefix()); + } + + /** + * @test + */ + public function testFormRespectsCustomAction(): void { + $form = $this->formFactory->create(CapType::class, null, [ + 'action' => '/some/custom/action' + ]); + + $this->assertEquals('/some/custom/action', $form->getConfig()->getAction()); + } + +} diff --git a/Tests/Integration/AltchaIntegrationTest.php b/Tests/Integration/AltchaIntegrationTest.php new file mode 100644 index 0000000..a614b08 --- /dev/null +++ b/Tests/Integration/AltchaIntegrationTest.php @@ -0,0 +1,151 @@ +makeIntegration(); + $this->assertEquals('Altcha', $integration->getName()); + } + + /** + * Test: getDisplayName returns the human-readable name + * + * @test + */ + public function testGetDisplayName(): void { + $integration = $this->makeIntegration(); + $this->assertEquals('ALTCHA', $integration->getDisplayName()); + } + + /** + * Test: getAuthenticationType is "none" (self-hosted, no third-party OAuth) + * + * @test + */ + public function testGetAuthenticationType(): void { + $integration = $this->makeIntegration(); + $this->assertEquals('none', $integration->getAuthenticationType()); + } + + /** + * Test: getRequiredKeyFields intentionally returns [] so that self-hosted and + * Sentinel fields can be filled independently without Mautic forcing all of them. + * + * @test + */ + public function testGetRequiredKeyFields(): void { + $integration = $this->makeIntegration(); + $this->assertSame([], $integration->getRequiredKeyFields()); + } + + /** + * Test: getSecretKeys includes hmac_secret and sentinel_api_secret so Mautic + * masks them in the UI and encrypts them at rest. + * + * @test + */ + public function testGetSecretKeys(): void { + $integration = $this->makeIntegration(); + $secretKeys = $integration->getSecretKeys(); + + $this->assertContains('hmac_secret', $secretKeys); + $this->assertContains('sentinel_api_secret', $secretKeys); + } + + /** + * Property Test: isConfigured returns true for any non-empty hmac_secret + * + * Generator: random alphanumeric strings (20-64 chars) + * Iterations: 100 + * + * @test + */ + public function testHmacKeyPersistence(): void { + $iterations = 100; + $failures = []; + + for ($i = 0; $i < $iterations; $i++) { + $secret = $this->randomAlphanumeric(rand(20, 64)); + + $integration = $this->makeIntegrationWithKeys(['hmac_secret' => $secret]); + + if (!$integration->isConfigured()) { + $failures[] = ['iteration' => $i, 'secret_length' => strlen($secret)]; + } + } + + $this->assertEmpty($failures, sprintf( + "isConfigured() returned false for a valid hmac_secret in %d/%d iterations:\n%s", + count($failures), $iterations, json_encode($failures, JSON_PRETTY_PRINT) + )); + } + + /** + * Test: isConfigured returns false when no credentials are set + * + * @test + */ + public function testIsNotConfiguredWithEmptyKeys(): void { + $integration = $this->makeIntegrationWithKeys([]); + $this->assertFalse($integration->isConfigured()); + } + + /** + * Test: isConfigured returns true when all three Sentinel fields are present + * + * @test + */ + public function testIsConfiguredWithSentinelCredentials(): void { + $integration = $this->makeIntegrationWithKeys([ + 'sentinel_domain' => 'https://sentinel.example.com', + 'sentinel_api_key' => 'key_abc', + 'sentinel_api_secret' => 'secret_xyz', + ]); + + $this->assertTrue($integration->isConfigured()); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /** Instantiate without parent constructor dependencies. */ + private function makeIntegration(): AltchaIntegration { + return new class extends AltchaIntegration { + public function __construct() {} + }; + } + + /** Instantiate and stub getKeys() to return the given array. */ + private function makeIntegrationWithKeys(array $keys): AltchaIntegration { + $integration = $this->getMockBuilder(AltchaIntegration::class) + ->disableOriginalConstructor() + ->onlyMethods(['getKeys']) + ->getMock(); + $integration->method('getKeys')->willReturn($keys); + return $integration; + } + + private function randomAlphanumeric(int $length): string { + $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $result = ''; + for ($i = 0; $i < $length; $i++) { + $result .= $chars[rand(0, strlen($chars) - 1)]; + } + return $result; + } + +} diff --git a/Tests/Integration/CapIntegrationTest.php b/Tests/Integration/CapIntegrationTest.php new file mode 100644 index 0000000..26e1bf2 --- /dev/null +++ b/Tests/Integration/CapIntegrationTest.php @@ -0,0 +1,97 @@ +assertEquals('Cap', $this->createIntegration()->getName()); + } + + /** + * @test + */ + public function testGetDisplayName(): void { + $this->assertEquals('Cap CAPTCHA', $this->createIntegration()->getDisplayName()); + } + + /** + * @test + */ + public function testGetAuthenticationType(): void { + $this->assertEquals('none', $this->createIntegration()->getAuthenticationType()); + } + + /** + * @test + */ + public function testGetRequiredKeyFields(): void { + $fields = $this->createIntegration()->getRequiredKeyFields(); + + $this->assertIsArray($fields); + $this->assertArrayHasKey('server_url', $fields); + $this->assertArrayHasKey('site_key', $fields); + $this->assertArrayHasKey('secret_key', $fields); + + $this->assertEquals('strings.cap.settings.server_url', $fields['server_url']); + $this->assertEquals('strings.cap.settings.site_key', $fields['site_key']); + $this->assertEquals('strings.cap.settings.secret_key', $fields['secret_key']); + } + + /** + * Property Test: Required key field persistence + * + * @test + */ + public function testRequiredKeyFieldsPersistence(): void { + $integration = $this->createIntegration(); + $requiredFields = $integration->getRequiredKeyFields(); + $failures = []; + + for ($i = 0; $i < 100; $i++) { + $values = [ + 'server_url' => 'https://cap-' . bin2hex(random_bytes(4)) . '.example.com', + 'site_key' => bin2hex(random_bytes(8)), + 'secret_key' => bin2hex(random_bytes(16)) + ]; + + foreach (array_keys($requiredFields) as $fieldName) { + $storage = [$fieldName => $values[$fieldName]]; + $retrieved = $storage[$fieldName] ?? null; + + if ($retrieved !== $values[$fieldName]) { + $failures[] = [ + 'iteration' => $i, + 'field' => $fieldName, + 'expected' => $values[$fieldName], + 'actual' => $retrieved + ]; + } + } + } + + $this->assertEmpty($failures, sprintf( + "Required key field persistence failed in %d cases:\n%s", + count($failures), + json_encode($failures, JSON_PRETTY_PRINT) + )); + } + +} diff --git a/Tests/README.md b/Tests/README.md new file mode 100644 index 0000000..4ab1fb0 --- /dev/null +++ b/Tests/README.md @@ -0,0 +1,53 @@ +# MauticMultiCaptchaBundle Tests + +## Overview + +This directory contains property-based and unit tests for the ALTCHA integration. + +## Running Tests + +Since this is a Mautic plugin, tests should be run within a Mautic installation context: + +### Option 1: Within Mautic Installation + +```bash +# From your Mautic root directory +php bin/phpunit plugins/MauticMultiCaptchaBundle/Tests +``` + +### Option 2: Standalone (requires dependencies) + +```bash +# Install dependencies first +composer install + +# Run tests +vendor/bin/phpunit +``` + +## Test Structure + +### Property-Based Tests + +Property-based tests verify universal properties across many random inputs (100 iterations minimum). + +- **AltchaIntegrationTest::testHmacKeyPersistence**: Verifies that the integration correctly defines the hmac_key field for persistence + +### Unit Tests + +Unit tests verify specific behaviors: + +- **testGetName**: Verifies integration name is "ALTCHA" +- **testGetDisplayName**: Verifies display name is "ALTCHA" +- **testGetAuthenticationType**: Verifies authentication type is "none" +- **testGetRequiredKeyFields**: Verifies hmac_key field is defined + +## Requirements + +- PHP 8.1 or higher +- PHPUnit 9.5 or higher +- Mautic 5.x, 6.x, or 7.x + +## Notes + +Tests are designed to run without external dependencies where possible, using mocks to simulate Mautic's integration system. diff --git a/Tests/Resources/AltchaTemplateTest.php b/Tests/Resources/AltchaTemplateTest.php new file mode 100644 index 0000000..4eddbfe --- /dev/null +++ b/Tests/Resources/AltchaTemplateTest.php @@ -0,0 +1,265 @@ +fail("Template file not found: {$templatePath}"); + } + + $templateContent = file_get_contents($templatePath); + + for ($i = 0; $i < $iterations; $i++) { + // Generate random widget configuration + $config = $this->generateRandomWidgetConfig(); + + // Extract all script sources from template + $scriptSources = $this->extractScriptSources($templateContent); + + // Verify each script source + foreach ($scriptSources as $source) { + $domain = $this->extractDomain($source); + + // Check if domain is approved + $isApproved = false; + foreach ($approvedDomains as $approvedDomain) { + if (empty($approvedDomain)) { + // Relative path (local) + if (!preg_match('/^https?:\/\//', $source)) { + $isApproved = true; + break; + } + } elseif (strpos($domain, $approvedDomain) !== false) { + $isApproved = true; + break; + } + } + + // Check if domain is blocked + $isBlocked = false; + foreach ($blockedDomains as $blockedDomain) { + if (strpos($domain, $blockedDomain) !== false) { + $isBlocked = true; + break; + } + } + + if (!$isApproved || $isBlocked) { + $failures[] = [ + 'iteration' => $i, + 'config' => $config, + 'source' => $source, + 'domain' => $domain, + 'approved' => $isApproved, + 'blocked' => $isBlocked, + ]; + } + } + } + + // Assert no failures occurred + $this->assertEmpty( + $failures, + sprintf( + "Local resource loading validation failed in %d/%d iterations:\n%s", + count($failures), + $iterations, + json_encode($failures, JSON_PRETTY_PRINT) + ) + ); + } + + /** + * Generate random widget configuration + */ + private function generateRandomWidgetConfig(): array { + return [ + 'maxNumber' => rand(1000, 1000000), + 'expires' => rand(10, 300), + 'invisible' => (bool) rand(0, 1), + 'showLabel' => (bool) rand(0, 1), + ]; + } + + /** + * Extract script sources from template content + */ + private function extractScriptSources(string $content): array { + $sources = []; + + // Match