From 5c5375cb9aebd992189ee6defd6a4cdfc4d5de40 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 18 Mar 2026 08:56:48 -0300 Subject: [PATCH] implement challenge verification flow with altcha integration and add challenge page --- containers/auth-gateway/auth_gateway/main.py | 9 ++- .../services/challenge_service.py | 69 ++++++++++++++++++ .../auth-gateway/auth_gateway/settings.py | 2 + .../auth_gateway/static/altcha.min.js | 8 +++ .../auth_gateway/static/challenge.js | 35 +++++++++ .../auth_gateway/static/style.css | 72 +++++++++++++++++++ .../auth_gateway/templates/challenge.html | 39 ++++++++++ .../auth_gateway/web/challenge_routes.py | 67 +++++++++++++++++ .../auth_gateway/web/dependencies.py | 16 +++++ .../auth_gateway/web/login_routes.py | 10 +++ 10 files changed, 325 insertions(+), 2 deletions(-) create mode 100644 containers/auth-gateway/auth_gateway/services/challenge_service.py create mode 100644 containers/auth-gateway/auth_gateway/static/altcha.min.js create mode 100644 containers/auth-gateway/auth_gateway/static/challenge.js create mode 100644 containers/auth-gateway/auth_gateway/templates/challenge.html create mode 100644 containers/auth-gateway/auth_gateway/web/challenge_routes.py diff --git a/containers/auth-gateway/auth_gateway/main.py b/containers/auth-gateway/auth_gateway/main.py index cd4f9d9..f5efe01 100644 --- a/containers/auth-gateway/auth_gateway/main.py +++ b/containers/auth-gateway/auth_gateway/main.py @@ -10,6 +10,7 @@ from auth_gateway.services.session_service import SessionService from auth_gateway.settings import settings from auth_gateway.storage.sqlite import SQLiteStorage from auth_gateway.web.auth_routes import router as auth_router +from auth_gateway.web.challenge_routes import router as challenge_router from auth_gateway.web.login_routes import router as login_router from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse @@ -70,12 +71,15 @@ app.add_exception_handler(RateLimitExceeded, _rate_limit_handler) @app.middleware("http") async def security_headers(request: Request, call_next): response = await call_next(request) + is_challenge = request.url.path == "/challenge" + script_src = "'self'" if is_challenge else "'none'" + worker_src = "worker-src 'self' blob:; " if is_challenge else "" response.headers["X-Frame-Options"] = "DENY" response.headers["X-Content-Type-Options"] = "nosniff" response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" response.headers["Content-Security-Policy"] = ( - "default-src 'self'; script-src 'none'; style-src 'self'; " - "img-src 'self' data:; frame-ancestors 'none'" + f"default-src 'self'; script-src {script_src}; style-src 'self'; " + f"img-src 'self' data:; {worker_src}frame-ancestors 'none'" ) return response @@ -94,6 +98,7 @@ async def access_log(request: Request, call_next): app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static") app.include_router(auth_router) +app.include_router(challenge_router) app.include_router(login_router) diff --git a/containers/auth-gateway/auth_gateway/services/challenge_service.py b/containers/auth-gateway/auth_gateway/services/challenge_service.py new file mode 100644 index 0000000..3340a5c --- /dev/null +++ b/containers/auth-gateway/auth_gateway/services/challenge_service.py @@ -0,0 +1,69 @@ +import base64 +import hashlib +import hmac +import json +import os +import time + +ALTCHA_MAX_NUMBER = 1_000_000 +CHALLENGE_TTL_SECONDS = 300 # 5 minutes +CHALLENGE_COOKIE_NAME = "auth_gateway_challenge" + + +def generate_altcha_challenge(hmac_key: str) -> dict: + salt = os.urandom(12).hex() + number = int.from_bytes(os.urandom(4), "big") % ALTCHA_MAX_NUMBER + challenge = hashlib.sha256(f"{salt}{number}".encode()).hexdigest() + signature = hmac.new(hmac_key.encode(), challenge.encode(), hashlib.sha256).hexdigest() + return { + "algorithm": "SHA-256", + "challenge": challenge, + "salt": salt, + "signature": signature, + "maxnumber": ALTCHA_MAX_NUMBER, + } + + +def verify_altcha_solution(payload_b64: str, hmac_key: str) -> bool: + try: + padding = (4 - len(payload_b64) % 4) % 4 + data = base64.b64decode(payload_b64 + "=" * padding) + payload = json.loads(data.decode()) + algorithm = payload.get("algorithm", "") + challenge = payload.get("challenge", "") + salt = payload.get("salt", "") + number = payload.get("number") + signature = payload.get("signature", "") + if algorithm != "SHA-256" or not all([challenge, salt, signature, number is not None]): + return False + expected_sig = hmac.new(hmac_key.encode(), challenge.encode(), hashlib.sha256).hexdigest() + if not hmac.compare_digest(expected_sig, signature): + return False + computed = hashlib.sha256(f"{salt}{number}".encode()).hexdigest() + return hmac.compare_digest(computed, challenge) + except Exception: + return False + + +def generate_challenge_cookie(secret: str) -> str: + timestamp = str(int(time.time())) + nonce = os.urandom(8).hex() + message = f"{timestamp}.{nonce}" + sig = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest() + return f"{message}.{sig}" + + +def verify_challenge_cookie(cookie_value: str, secret: str) -> bool: + try: + last_dot = cookie_value.rfind(".") + if last_dot == -1: + return False + message = cookie_value[:last_dot] + sig = cookie_value[last_dot + 1:] + expected_sig = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest() + if not hmac.compare_digest(expected_sig, sig): + return False + timestamp = int(message.split(".")[0]) + return (time.time() - timestamp) < CHALLENGE_TTL_SECONDS + except Exception: + return False diff --git a/containers/auth-gateway/auth_gateway/settings.py b/containers/auth-gateway/auth_gateway/settings.py index 724f6bf..ab564f4 100644 --- a/containers/auth-gateway/auth_gateway/settings.py +++ b/containers/auth-gateway/auth_gateway/settings.py @@ -1,4 +1,5 @@ from pathlib import Path +from secrets import token_hex from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict @@ -15,6 +16,7 @@ class Settings(BaseSettings): secure_cookies: bool = Field(default=True) session_default_minutes: int = Field(default=720) oidc_state_ttl_minutes: int = Field(default=10) + challenge_secret: str = Field(default_factory=lambda: token_hex(32)) settings = Settings() diff --git a/containers/auth-gateway/auth_gateway/static/altcha.min.js b/containers/auth-gateway/auth_gateway/static/altcha.min.js new file mode 100644 index 0000000..1f20697 --- /dev/null +++ b/containers/auth-gateway/auth_gateway/static/altcha.min.js @@ -0,0 +1,8 @@ +/** + * Minified by jsDelivr using Terser v5.39.0. + * Original file: /gh/altcha-org/altcha@2.3.0/dist/altcha.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +const Yn='(function(){"use strict";const d=new TextEncoder;function p(e){return[...new Uint8Array(e)].map(t=>t.toString(16).padStart(2,"0")).join("")}async function b(e,t,r){if(typeof crypto>"u"||!("subtle"in crypto)||!("digest"in crypto.subtle))throw new Error("Web Crypto is not available. Secure context is required (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).");return p(await crypto.subtle.digest(r.toUpperCase(),d.encode(e+t)))}function w(e,t,r="SHA-256",n=1e6,l=0){const o=new AbortController,a=Date.now();return{promise:(async()=>{for(let c=l;c<=n;c+=1){if(o.signal.aborted)return null;if(await b(t,c,r)===e)return{number:c,took:Date.now()-a}}return null})(),controller:o}}function h(e){const t=atob(e),r=new Uint8Array(t.length);for(let n=0;n{for(let i=n;i<=r;i+=1){if(o.signal.aborted||!c||!u)return null;try{const f=await crypto.subtle.decrypt({name:l,iv:g(i)},c,u);if(f)return{clearText:new TextDecoder().decode(f),took:Date.now()-a}}catch{}}return null};let c=null,u=null;try{u=h(e);const i=await crypto.subtle.digest("SHA-256",d.encode(t));c=await crypto.subtle.importKey("raw",i,l,!1,["decrypt"])}catch{return{promise:Promise.reject(),controller:o}}return{promise:s(),controller:o}}let y;onmessage=async e=>{const{type:t,payload:r,start:n,max:l}=e.data;let o=null;if(t==="abort")y?.abort(),y=void 0;else if(t==="work"){if("obfuscated"in r){const{key:a,obfuscated:s}=r||{};o=await m(s,a,l,n)}else{const{algorithm:a,challenge:s,salt:c}=r||{};o=w(s,c,a,l,n)}y=o.controller,o.promise.then(a=>{self.postMessage(a&&{...a,worker:!0})})}}})();\n',Dn=typeof self<"u"&&self.Blob&&new Blob(["(self.URL || self.webkitURL).revokeObjectURL(self.location.href);",Yn],{type:"text/javascript;charset=utf-8"});function Ni(e){let t;try{if(t=Dn&&(self.URL||self.webkitURL).createObjectURL(Dn),!t)throw"";const n=new Worker(t,{name:e?.name});return n.addEventListener("error",(()=>{(self.URL||self.webkitURL).revokeObjectURL(t)})),n}catch{return new Worker("data:text/javascript;charset=utf-8,"+encodeURIComponent(Yn),{name:e?.name})}}const Li="5";typeof window<"u"&&((window.__svelte??={}).v??=new Set).add(Li);const Pi=1,Oi=4,Fi=8,Mi=16,Vi=1,Ui=2,Mr="[",Zn="[!",zn="]",bt={},ae=Symbol(),ji="http://www.w3.org/1999/xhtml",Nn=!1;function Jn(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}var Kn=Array.isArray,qi=Array.prototype.indexOf,Bi=Array.from,or=Object.keys,Mt=Object.defineProperty,rt=Object.getOwnPropertyDescriptor,Hi=Object.getOwnPropertyDescriptors,Gi=Object.prototype,Wi=Array.prototype,Xn=Object.getPrototypeOf,Ln=Object.isExtensible;const yt=()=>{};function Qn(e){for(var t=0;t{var t=$;Re(a);var n=e();return Re(t),n};return r&&n.set("length",N(e.length)),new Proxy(e,{defineProperty(e,t,r){(!("value"in r)||!1===r.configurable||!1===r.enumerable||!1===r.writable)&&na();var o=n.get(t);return void 0===o?(o=l((()=>N(r.value))),n.set(t,o)):b(o,l((()=>Me(r.value)))),!0},deleteProperty(e,t){var i=n.get(t);if(void 0===i)t in e&&(n.set(t,l((()=>N(ae)))),Ir(o));else{if(r&&"string"==typeof t){var a=n.get("length"),s=Number(t);Number.isInteger(s)&&sN(Me(s?t[r]:ae)))),n.set(r,a)),void 0!==a){var c=i(a);return c===ae?void 0:c}return Reflect.get(t,r,o)},getOwnPropertyDescriptor(e,t){var r=Reflect.getOwnPropertyDescriptor(e,t);if(r&&"value"in r){var o=n.get(t);o&&(r.value=i(o))}else if(void 0===r){var a=n.get(t),l=a?.v;if(void 0!==a&&l!==ae)return{enumerable:!0,configurable:!0,value:l,writable:!0}}return r},has(e,t){if(t===Ot)return!0;var r=n.get(t),o=void 0!==r&&r.v!==ae||Reflect.has(e,t);if((void 0!==r||null!==S&&(!o||rt(e,t)?.writable))&&(void 0===r&&(r=l((()=>N(o?Me(e[t]):ae))),n.set(t,r)),i(r)===ae))return!1;return o},set(e,t,i,a){var s=n.get(t),c=t in e;if(r&&"length"===t)for(var u=i;uN(ae))),n.set(u+"",f))}void 0===s?(!c||rt(e,t)?.writable)&&(b(s=l((()=>N(void 0))),l((()=>Me(i)))),n.set(t,s)):(c=s.v!==ae,b(s,l((()=>Me(i)))));var d=Reflect.getOwnPropertyDescriptor(e,t);if(d?.set&&d.set.call(a,i),!c){if(r&&"string"==typeof t){var h=n.get("length"),v=Number(t);Number.isInteger(v)&&v>=h.v&&b(h,v+1)}Ir(o)}return!0},ownKeys(e){i(o);var t=Reflect.ownKeys(e).filter((e=>{var t=n.get(e);return void 0===t||t.v!==ae}));for(var[r,a]of n)a.v!==ae&&!(r in e)&&t.push(r);return t},setPrototypeOf(){oa()}})}function Ir(e,t=1){b(e,e.v+t)}var Pn,no,oo,io;function Tr(){if(void 0===Pn){Pn=window,no=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;oo=rt(t,"firstChild").get,io=rt(t,"nextSibling").get,Ln(e)&&(e.__click=void 0,e.__className=void 0,e.__attributes=null,e.__style=void 0,e.__e=void 0),Ln(n)&&(n.__t=void 0)}}function vr(e=""){return document.createTextNode(e)}function ve(e){return oo.call(e)}function Be(e){return io.call(e)}function z(e,t){if(!O)return ve(e);var n=ve(P);return null===n&&(n=P.appendChild(vr())),Ue(n),n}function Nt(e,t){if(!O){var n=ve(e);return n instanceof Comment&&""===n.data?Be(n):n}return P}function J(e,t=1,n=!1){let r=O?P:e;for(var o;t--;)o=r,r=Be(r);if(!O)return r;var i=r?.nodeType;if(n&&3!==i){var a=vr();return null===r?o?.after(a):r.before(a),Ue(a),a}return Ue(r),r}function sa(e){e.textContent=""}function ao(e){return e===this.v}function lo(e,t){return e!=e?t==t:e!==t||null!==e&&"object"==typeof e||"function"==typeof e}function jr(e){return!lo(e,this.v)}function gr(e){var t=2050,n=null!==$&&2&$.f?$:null;return null===S||null!==n&&n.f&fe?t|=fe:S.f|=to,{ctx:ne,deps:null,effects:null,equals:ao,f:t,fn:e,reactions:null,rv:0,v:null,wv:0,parent:n??S}}function Lt(e){const t=gr(e);return wo(t),t}function ua(e){const t=gr(e);return t.equals=jr,t}function so(e){var t=e.effects;if(null!==t){e.effects=null;for(var n=0;n{je(t)}}function va(e){const t=lt(64,e,!0);return(e={})=>new Promise((n=>{e.outro?Lr(t,(()=>{je(t),n(void 0)})):(je(t),n(void 0))}))}function Br(e){return lt(4,e,!1)}function Hr(e){return lt(8,e,!0)}function Ce(e,t=[],n=gr){const r=t.map(n);return fo((()=>e(...r.map(i))))}function fo(e,t=0){return lt(24|t,e,!0)}function Nr(e,t=!0){return lt(40,e,!0,t)}function ho(e){var t=e.teardown;if(null!==t){const e=qt,n=$;Fn(!0),Re(null);try{t.call(null)}finally{Fn(e),Re(n)}}}function vo(e,t=!1){var n=e.first;for(e.first=e.last=null;null!==n;){var r=n.next;64&n.f?n.parent=null:je(n,t),n=r}}function ga(e){for(var t=e.first;null!==t;){var n=t.next;!(32&t.f)&&je(t),t=n}}function je(e,t=!0){var n=!1;(t||!!(e.f&zi))&&null!==e.nodes_start&&(go(e.nodes_start,e.nodes_end),n=!0),vo(e,t&&!n),cr(e,0),_e(e,dr);var r=e.transitions;if(null!==r)for(const e of r)e.stop();ho(e);var o=e.parent;null!==o&&null!==o.first&&po(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes_start=e.nodes_end=null}function go(e,t){for(;null!==e;){var n=e===t?null:Be(e);e.remove(),e=n}}function po(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 Lr(e,t){var n=[];mo(e,n,!0),pa(n,(()=>{je(e),t&&t()}))}function pa(e,t){var n=e.length;if(n>0){var r=()=>--n||t();for(var o of e)o.out(r)}else t()}function mo(e,t,n){if(!(e.f&wt)){if(e.f^=wt,null!==e.transitions)for(const r of e.transitions)(r.is_global||n)&&t.push(r);for(var r=e.first;null!==r;){var o=r.next;mo(r,t,!!(!!(r.f&Ur)||!!(32&r.f))&&n),r=o}}}function On(e){_o(e,!0)}function _o(e,t){if(e.f&wt){e.f^=wt,!(e.f&le)&&(e.f^=le),Bt(e)&&(_e(e,Ie),mr(e));for(var n=e.first;null!==n;){var r=n.next;_o(n,!!(!!(n.f&Ur)||!!(32&n.f))&&t),n=r}if(null!==e.transitions)for(const n of e.transitions)(n.is_global||t)&&n.in()}}const ma=typeof requestIdleCallback>"u"?e=>setTimeout(e,1):requestIdleCallback;let Vt=[],Ut=[];function bo(){var e=Vt;Vt=[],Qn(e)}function yo(){var e=Ut;Ut=[],Qn(e)}function Gr(e){0===Vt.length&&queueMicrotask(bo),Vt.push(e)}function _a(e){0===Ut.length&&ma(yo),Ut.push(e)}function ba(){Vt.length>0&&bo(),Ut.length>0&&yo()}let tr=!1,lr=!1,sr=null,nt=!1,qt=!1;function Fn(e){qt=e}let Ft=[],$=null,ke=!1;function Re(e){$=e}let S=null;function qe(e){S=e}let Te=null;function wo(e){null!==$&&$.f&Sr&&(null===Te?Te=[e]:Te.push(e))}let re=null,ce=0,he=null;function ya(e){he=e}let Eo=1,ur=0,Ve=!1;function xo(){return++Eo}function Bt(e){var t=e.f;if(t&Ie)return!0;if(t&at){var n=e.deps,r=!!(t&fe);if(null!==n){var o,i,a=!!(t&ar),l=r&&null!==S&&!Ve,s=n.length;if(a||l){var c=e,u=c.parent;for(o=0;oe.wv)return!0}(!r||null!==S&&!Ve)&&_e(e,le)}return!1}function wa(e,t){for(var n=t;null!==n;){if(n.f&ir)try{return void n.fn(e)}catch{n.f^=ir}n=n.parent}throw tr=!1,e}function Mn(e){return!(e.f&dr||null!==e.parent&&e.parent.f&ir)}function pr(e,t,n,r){if(tr){if(null===n&&(tr=!1),Mn(t))throw e}else if(null!==n&&(tr=!0),wa(e,t),Mn(t))throw e}function Co(e,t,n=!0){var r=e.reactions;if(null!==r)for(var o=0;o0)for(f.length=ce+re.length,d=0;d0;){t++>1e3&&xa();var n=Ft,r=n.length;Ft=[];for(var o=0;o{r.d=!0}))}function So(e){const t=ne;if(null!==t){void 0!==e&&(t.x=e);const a=t.e;if(null!==a){var n=S,r=$;t.e=null;try{for(var o=0;o{document.activeElement===t&&e.focus()}))}}let Un=!1;function Do(){Un||(Un=!0,document.addEventListener("reset",(e=>{Promise.resolve().then((()=>{if(!e.defaultPrevented)for(const t of e.target.elements)t.__on_r?.()}))}),{capture:!0}))}function No(e){var t=$,n=S;Re(null),qe(null);try{return e()}finally{Re(t),qe(n)}}function Ta(e,t,n,r=n){e.addEventListener(t,(()=>No(n)));const o=e.__on_r;e.__on_r=o?()=>{o(),r(!0)}:()=>r(!0),Do()}const Lo=new Set,Pr=new Set;function Da(e,t,n,r={}){function o(e){if(r.capture||Pt.call(t,e),!e.cancelBubble)return No((()=>n?.call(this,e)))}return e.startsWith("pointer")||e.startsWith("touch")||"wheel"===e?Gr((()=>{t.addEventListener(e,o,r)})):t.addEventListener(e,o,r),o}function Fe(e,t,n,r,o){var i={capture:r,passive:o},a=Da(e,t,n,i);(t===document.body||t===window||t===document)&&qr((()=>{t.removeEventListener(e,a,i)}))}function Na(e){for(var t=0;ti||n});var u=$,f=S;Re(null),qe(null);try{for(var d,h=[];null!==i;){var v=i.assignedSlot||i.parentNode||i.host||null;try{var p=i["__"+r];if(null!=p&&(!i.disabled||e.target===i))if(Kn(p)){var[g,...b]=p;g.apply(i,[e,...b])}else p.call(i,e)}catch(e){d?h.push(e):d=e}if(e.cancelBubble||v===t||null===v)break;i=v}if(d){for(let e of h)queueMicrotask((()=>{throw e}));throw d}}finally{e.__root=t,delete e.currentTarget,Re(u),qe(f)}}}function Zr(e){var t=document.createElement("template");return t.innerHTML=e,t.content}function Ae(e,t){var n=S;null===n.nodes_start&&(n.nodes_start=e,n.nodes_end=t)}function be(e,t){var n,r=!!(1&t),o=!!(2&t),i=!e.startsWith("");return()=>{if(O)return Ae(P,null),P;void 0===n&&(n=Zr(i?e:""+e),r||(n=ve(n)));var t=o||no?document.importNode(n,!0):n.cloneNode(!0);r?Ae(ve(t),t.lastChild):Ae(t,t);return t}}function _r(e,t,n="svg"){var r,o=`<${n}>${!e.startsWith("")?e:""+e}`;return()=>{if(O)return Ae(P,null),P;if(!r){var e=Zr(o);r=ve(ve(e))}var t=r.cloneNode(!0);return Ae(t,t),t}}function Xt(){if(O)return Ae(P,null),P;var e=document.createDocumentFragment(),t=document.createComment(""),n=vr();return e.append(t,n),Ae(t,n),e}function B(e,t){if(O)return S.nodes_end=P,void Et();null!==e&&e.before(t)}function La(e,t){var n=null==t?"":"object"==typeof t?t+"":t;n!==(e.__t??=e.nodeValue)&&(e.__t=n,e.nodeValue=n+"")}function Po(e,t){return Oo(e,t)}function Pa(e,t){Tr(),t.intro=t.intro??!1;const n=t.target,r=O,o=P;try{for(var i=ve(n);i&&(8!==i.nodeType||i.data!==Mr);)i=Be(i);if(!i)throw bt;_t(!0),Ue(i),Et();const r=Oo(e,{...t,anchor:i});if(null===P||8!==P.nodeType||P.data!==zn)throw hr(),bt;return _t(!1),r}catch(r){if(r===bt)return!1===t.recover&&ta(),Tr(),sa(n),_t(!1),Po(e,t);throw r}finally{_t(r),Ue(o)}}const pt=new Map;function Oo(e,{target:t,anchor:n,props:r={},events:o,context:i,intro:a=!0}){Tr();var l=new Set,s=e=>{for(var n=0;n{var a=n??t.appendChild(vr());return Nr((()=>{i&&($o({}),ne.c=i);o&&(r.$$events=o),O&&Ae(a,null),c=e(a,r)||{},O&&(S.nodes_end=P),i&&So()})),()=>{for(var e of l){t.removeEventListener(e,Pt);var r=pt.get(e);0==--r?(document.removeEventListener(e,Pt),pt.delete(e)):pt.set(e,r)}Pr.delete(s),a!==n&&a.parentNode?.removeChild(a)}}));return Or.set(c,u),c}let Or=new WeakMap;function Oa(e,t){const n=Or.get(e);return n?(Or.delete(e),n(t)):Promise.resolve()}function K(e,t,[n,r]=[0,0]){O&&0===n&&Et();var o=e,i=null,a=null,l=ae,s=!1;const c=(e,t=!0)=>{s=!0,u(t,e)},u=(e,t)=>{if(l===(l=e))return;let s=!1;if(O&&-1!==r){if(0===n){const e=o.data;e===Mr?r=0:e===Zn?r=1/0:(r=parseInt(e.substring(1)))!=r&&(r=l?1/0:-1)}!!l===r>n&&(Ue(o=aa()),_t(!1),s=!0,r=-1)}l?(i?On(i):t&&(i=Nr((()=>t(o)))),a&&Lr(a,(()=>{a=null}))):(a?On(a):t&&(a=Nr((()=>t(o,[n+1,r])))),i&&Lr(i,(()=>{i=null}))),s&&_t(!0)};fo((()=>{s=!1,t(c),s||u(null,null)}),n>0?Ur:0),O&&(o=P)}function tt(e,t,n=!1,r=!1,o=!1){var i=e,a="";Ce((()=>{var e=S;if(a!==(a=t()??"")){if(null!==e.nodes_start&&(go(e.nodes_start,e.nodes_end),e.nodes_start=e.nodes_end=null),""!==a){if(O){P.data;for(var o=Et(),l=o;null!==o&&(8!==o.nodeType||""!==o.data);)l=o,o=Be(o);if(null===o)throw hr(),bt;return Ae(P,l),void(i=Ue(o))}var s=a+"";n?s=`${s}`:r&&(s=`${s}`);var c=Zr(s);if((n||r)&&(c=ve(c)),Ae(ve(c),c.lastChild),n||r)for(;ve(c);)i.before(ve(c));else i.before(c)}}else O&&Et()}))}function Fa(e,t,n,r,o){O&&Et();var i=t.$$slots?.[n],a=!1;!0===i&&(i=t.children,a=!0),void 0===i||i(e,a?()=>r:r)}const jn=[..." \t\n\r\f \v\ufeff"];function Ma(e,t,n){var r=""+e;if(n)for(var o in n)if(n[o])r=r?r+" "+o:o;else if(r.length)for(var i=o.length,a=0;(a=r.indexOf(o,a))>=0;){var l=a+i;0!==a&&!jn.includes(r[a-1])||l!==r.length&&!jn.includes(r[l])?a=l:r=(0===a?"":r.substring(0,a))+r.substring(l+1)}return""===r?null:r}function Va(e,t,n,r,o,i){var a=e.__className;if(O||a!==n||void 0===a){var l=Ma(n,r,i);(!O||l!==e.getAttribute("class"))&&(null==l?e.removeAttribute("class"):e.className=l),e.__className=n}else if(i&&o!==i)for(var s in i){var c=!!i[s];(null==o||c!==!!o[s])&&e.classList.toggle(s,c)}return i}const Ua=Symbol("is custom element"),ja=Symbol("is html");function qn(e){if(O){var t=!1,n=()=>{if(!t){if(t=!0,e.hasAttribute("value")){var n=e.value;R(e,"value",null),e.value=n}if(e.hasAttribute("checked")){var r=e.checked;R(e,"checked",null),e.checked=r}}};e.__on_r=n,_a(n),Do()}}function qa(e,t){var n=Fo(e);n.value===(n.value=t??void 0)||e.value===t&&(0!==t||"PROGRESS"!==e.nodeName)||(e.value=t??"")}function R(e,t,n,r){var o=Fo(e);O&&(o[t]=e.getAttribute(t),"src"===t||"srcset"===t||"href"===t&&"LINK"===e.nodeName)||o[t]!==(o[t]=n)&&("loading"===t&&(e[Ji]=n),null==n?e.removeAttribute(t):"string"!=typeof n&&Ba(e).includes(t)?e[t]=n:e.setAttribute(t,n))}function Fo(e){return e.__attributes??={[Ua]:e.nodeName.includes("-"),[ja]:e.namespaceURI===ji}}var Bn=new Map;function Ba(e){var t=Bn.get(e.nodeName);if(t)return t;Bn.set(e.nodeName,t=[]);for(var n,r=e,o=Element.prototype;o!==r;){for(var i in n=Hi(r))n[i].set&&t.push(i);r=Xn(r)}return t}function Ha(e,t,n=t){Ta(e,"change",(t=>{var r=t?e.defaultChecked:e.checked;n(r)})),(O&&e.defaultChecked!==e.checked||null==ot(t))&&n(e.checked),Hr((()=>{var n=t();e.checked=!!n}))}function Hn(e,t){return e===t||e?.[Ot]===t}function Qt(e={},t,n,r){return Br((()=>{var r,o;return Hr((()=>{r=o,o=[],ot((()=>{e!==n(...o)&&(t(e,...o),r&&Hn(n(...r),e)&&t(null,...r))}))})),()=>{Gr((()=>{o&&Hn(n(...o),e)&&t(null,...o)}))}})),e}function Mo(e){null===ne&&Jn(),Dr((()=>{const t=ot(e);if("function"==typeof t)return t}))}function Ga(e){null===ne&&Jn(),Mo((()=>()=>ot(e)))}function Vo(e,t,n){if(null==e)return t(void 0),yt;const r=ot((()=>e.subscribe(t,n)));return r.unsubscribe?()=>r.unsubscribe():r}const mt=[];function Wa(e,t=yt){let n=null;const r=new Set;function o(t){if(lo(e,t)&&(e=t,n)){const t=!mt.length;for(const t of r)t[1](),mt.push(t,e);if(t){for(let e=0;e{r.delete(s),0===r.size&&n&&(n(),n=null)}}}}function rr(e){let t;return Vo(e,(e=>t=e))(),t}let Uo,er=!1,Fr=Symbol();function Ya(e,t,n){const r=n[t]??={store:null,source:Yr(void 0),unsubscribe:yt};if(r.store!==e&&!(Fr in n))if(r.unsubscribe(),r.store=e??null,null==e)r.source.v=void 0,r.unsubscribe=yt;else{var o=!0;r.unsubscribe=Vo(e,(e=>{o?r.source.v=e:b(r.source,e)})),o=!1}return e&&Fr in n?rr(e):i(r.source)}function Za(){const e={};return[e,function(){qr((()=>{for(var t in e)e[t].unsubscribe();Mt(e,Fr,{enumerable:!1,value:!0})}))}]}function za(e){var t=er;try{return er=!1,[e(),er]}finally{er=t}}function Gn(e){return e.ctx?.d??!1}function x(e,t,n,r){var o,a=!!(1&n),l=!!(8&n),s=!!(16&n),c=!1;l?[o,c]=za((()=>e[t])):o=e[t];var u,f=Ot in e||ro in e,d=l&&(rt(e,t)?.set??(f&&t in e&&(n=>e[t]=n)))||void 0,h=r,v=!0,p=!1,g=()=>(p=!0,v&&(v=!1,h=s?ot(r):r),h);if(void 0===o&&void 0!==r&&(d&&ra(),o=g(),d&&d(o)),u=()=>{var n=e[t];return void 0===n?g():(v=!0,p=!1,n)},!(4&n))return u;if(d){var m=e.$$legacy;return function(e,t){return arguments.length>0?((!t||m||c)&&d(t?u():e),e):u()}}var y=!1,w=Yr(o),x=gr((()=>{var e=u(),t=i(w);return y?(y=!1,t):w.v=e}));return l&&i(x),a||(x.equals=jr),function(e,t){if(arguments.length>0){const n=t?i(x):l?Me(e):e;if(!x.equals(n)){if(y=!0,b(w,n),p&&void 0!==h&&(h=n),Gn(x))return e;ot((()=>i(x)))}return e}return Gn(x)?x.v:i(x)}}function Ja(e){return new Ka(e)}class Ka{#e;#t;constructor(e){var t=new Map,n=(e,n)=>{var r=Yr(n);return t.set(e,r),r};const r=new Proxy({...e.props||{},$$events:{}},{get:(e,r)=>i(t.get(r)??n(r,Reflect.get(e,r))),has:(e,r)=>r===ro||(i(t.get(r)??n(r,Reflect.get(e,r))),Reflect.has(e,r)),set:(e,r,o)=>(b(t.get(r)??n(r,o),o),Reflect.set(e,r,o))});this.#t=(e.hydrate?Pa:Po)(e.component,{target:e.target,anchor:e.anchor,props:r,context:e.context,intro:e.intro??!1,recover:e.recover}),(!e?.props?.$$host||!1===e.sync)&&E(),this.#e=r.$$events;for(const e of Object.keys(this.#t))"$set"===e||"$destroy"===e||"$on"===e||Mt(this,e,{get(){return this.#t[e]},set(t){this.#t[e]=t},enumerable:!0});this.#t.$set=e=>{Object.assign(r,e)},this.#t.$destroy=()=>{Oa(this.#t)}}$set(e){this.#t.$set(e)}$on(e,t){this.#e[e]=this.#e[e]||[];const n=(...e)=>t.call(this,...e);return this.#e[e].push(n),()=>{this.#e[e]=this.#e[e].filter((e=>e!==n))}}$destroy(){this.#t.$destroy()}}function nr(e,t,n,r){const o=n[e]?.type;if(t="Boolean"===o&&"boolean"!=typeof t?null!=t:t,!r||!n[e])return t;if("toAttribute"===r)switch(o){case"Object":case"Array":return null==t?null:JSON.stringify(t);case"Boolean":return t?"":null;case"Number":return t??null;default:return t}else switch(o){case"Object":case"Array":return t&&JSON.parse(t);case"Boolean":default:return t;case"Number":return null!=t?+t:t}}function Xa(e){const t={};return e.childNodes.forEach((e=>{t[e.slot||"default"]=!0})),t}function Qa(e,t,n,r,o,i){let a=class extends Uo{constructor(){super(e,n,o),this.$$p_d=t}static get observedAttributes(){return or(t).map((e=>(t[e].attribute||e).toLowerCase()))}};return or(t).forEach((e=>{Mt(a.prototype,e,{get(){return this.$$c&&e in this.$$c?this.$$c[e]:this.$$d[e]},set(n){n=nr(e,n,t),this.$$d[e]=n;var r=this.$$c;if(r){var o=rt(r,e)?.get;o?r[e]=n:r.$set({[e]:n})}}})})),r.forEach((e=>{Mt(a.prototype,e,{get(){return this.$$c?.[e]}})})),e.element=a,a}"function"==typeof HTMLElement&&(Uo=class extends HTMLElement{$$ctor;$$s;$$c;$$cn=!1;$$d={};$$r=!1;$$p_d={};$$l={};$$l_u=new Map;$$me;constructor(e,t,n){super(),this.$$ctor=e,this.$$s=t,n&&this.attachShadow({mode:"open"})}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 e=function(e){return t=>{const n=document.createElement("slot");"default"!==e&&(n.name=e),B(t,n)}};if(await Promise.resolve(),!this.$$cn||this.$$c)return;const t={},n=Xa(this);for(const r of this.$$s)r in n&&("default"!==r||this.$$d.children?t[r]=e(r):(this.$$d.children=e(r),t.default=!0));for(const e of this.attributes){const t=this.$$g_p(e.name);t in this.$$d||(this.$$d[t]=nr(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=Ja({component:this.$$ctor,target:this.shadowRoot||this,props:{...this.$$d,$$slots:t,$$host:this}}),this.$$me=ha((()=>{Hr((()=>{this.$$r=!0;for(const e of or(this.$$c)){if(!this.$$p_d[e]?.reflect)continue;this.$$d[e]=this.$$c[e];const t=nr(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={}}}attributeChangedCallback(e,t,n){this.$$r||(e=this.$$g_p(e),this.$$d[e]=nr(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 or(this.$$p_d).find((t=>this.$$p_d[t].attribute===e||!this.$$p_d[t].attribute&&t.toLowerCase()===e))||e}});const jo=new TextEncoder;function el(e){return[...new Uint8Array(e)].map((e=>e.toString(16).padStart(2,"0"))).join("")}async function tl(e,t="SHA-256",n=1e5){const r=Date.now().toString(16);e||(e=Math.round(Math.random()*n));return{algorithm:t,challenge:await qo(r,e,t),salt:r,signature:""}}async function qo(e,t,n){if(typeof crypto>"u"||!("subtle"in crypto)||!("digest"in crypto.subtle))throw new Error("Web Crypto is not available. Secure context is required (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).");return el(await crypto.subtle.digest(n.toUpperCase(),jo.encode(e+t)))}function rl(e,t,n="SHA-256",r=1e6,o=0){const i=new AbortController,a=Date.now();return{promise:(async()=>{for(let l=o;l<=r;l+=1){if(i.signal.aborted)return null;if(await qo(t,l,n)===e)return{number:l,took:Date.now()-a}}return null})(),controller:i}}function Wn(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{}}function nl(e){const t=atob(e),n=new Uint8Array(t.length);for(let e=0;e{for(let e=r;e<=n;e+=1){if(i.signal.aborted||!l||!s)return null;try{const t=await crypto.subtle.decrypt({name:o,iv:ol(e)},l,s);if(t)return{clearText:(new TextDecoder).decode(t),took:Date.now()-a}}catch{}}return null})(),controller:i}}var y=(e=>(e.CODE="code",e.ERROR="error",e.VERIFIED="verified",e.VERIFYING="verifying",e.UNVERIFIED="unverified",e.EXPIRED="expired",e))(y||{}),Q=(e=>(e.ERROR="error",e.LOADING="loading",e.PLAYING="playing",e.PAUSED="paused",e.READY="ready",e))(Q||{});globalThis.altchaPlugins=globalThis.altchaPlugins||[],globalThis.altchaI18n=globalThis.altchaI18n||{get:e=>rr(globalThis.altchaI18n.store)[e],set:(e,t)=>{Object.assign(rr(globalThis.altchaI18n.store),{[e]:t}),globalThis.altchaI18n.store.set(rr(globalThis.altchaI18n.store))},store:Wa({})};const al={ariaLinkLabel:"Visit Altcha.org",enterCode:"Enter code",enterCodeAria:"Enter code you hear. Press Space to play audio.",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."};globalThis.altchaI18n.set("en",al);const $r=(e,t)=>{let n=ua((()=>Yi(t?.(),24)));var r=cl();Ce((()=>{R(r,"width",i(n)),R(r,"height",i(n))})),B(e,r)};function ll(e,t){"Space"===e.code&&(e.preventDefault(),e.stopImmediatePropagation(),t())}function sl(e,t){e.preventDefault(),t()}function ul(e,t,n,r,o,a,l,s){[y.UNVERIFIED,y.ERROR,y.EXPIRED,y.CODE].includes(i(t))?!1!==n()&&!1===i(r)?.reportValidity()?b(o,!1):a()?l():s():b(o,!0)}var cl=_r(''),fl=be(''),dl=be('
'),hl=_r(''),vl=_r(''),gl=_r(''),pl=be(''),ml=be(""),_l=be(''),bl=be("
"),yl=be("
"),wl=be('
'),El=be(''),xl=be('
'),Cl=be('
',1);function kl(e,t){$o(t,!0);const[n,r]=Za(),o=()=>Ya(X,"$altchaI18nStore",n);let a=x(t,"auto",7,void 0),l=x(t,"blockspam",7,void 0),s=x(t,"challengeurl",7,void 0),c=x(t,"challengejson",7,void 0),u=x(t,"credentials",7,void 0),f=x(t,"customfetch",7,void 0),d=x(t,"debug",7,!1),h=x(t,"delay",7,0),v=x(t,"disableautofocus",7,!1),p=x(t,"refetchonexpire",7,!0),g=x(t,"disablerefetchonexpire",23,(()=>!p())),m=x(t,"expire",7,void 0),w=x(t,"floating",7,void 0),$=x(t,"floatinganchor",7,void 0),C=x(t,"floatingoffset",7,void 0),_=x(t,"floatingpersist",7,!1),k=x(t,"hidefooter",7,!1),A=x(t,"hidelogo",7,!1),I=x(t,"id",7,void 0),S=x(t,"language",7,void 0),L=x(t,"name",7,"altcha"),O=x(t,"maxnumber",7,1e6),P=x(t,"mockerror",7,!1),D=x(t,"obfuscated",7,void 0),V=x(t,"overlay",7,void 0),M=x(t,"overlaycontent",7,void 0),j=x(t,"plugins",7,void 0),T=x(t,"sentinel",7,void 0),F=x(t,"spamfilter",7,!1),U=x(t,"strings",7,void 0),q=x(t,"test",7,!1),H=x(t,"verifyurl",7,void 0),G=x(t,"workers",23,(()=>Math.min(16,navigator.hardwareConcurrency||8))),W=x(t,"workerurl",7,void 0);const{altchaI18n:Y}=globalThis,X=Y.store,ee=["SHA-256","SHA-384","SHA-512"],te=(e,n)=>{t.$$host.dispatchEvent(new CustomEvent(e,{detail:n}))},ne=document.documentElement.lang?.split("-")?.[0],re=Lt((()=>s()&&new URL(s(),location.origin).host.endsWith(".altcha.org")&&!!s()?.includes("apiKey=ckey_"))),oe=Lt((()=>c()?Ye(c()):void 0)),ie=Lt((()=>U()?Ye(U()):{})),ae=Lt((()=>({...Se(o()),...i(ie)}))),le=Lt((()=>`${I()||L()}_checkbox_${Math.round(1e8*Math.random())}`));let se=N(null),ce=N(!1),ue=N(null),fe=N(Me(y.UNVERIFIED)),de=N(void 0),he=N(null),ve=N(null),pe=N(null),ge=N(null),be=N(null),me=N(null),ye=N(null),we=N(null),xe=null,$e=N(null),Ee=N(!1),Re=[],_e=N(!1),ke=N(null);function Ae(e,t){return btoa(JSON.stringify({algorithm:e.algorithm,challenge:e.challenge,number:t.number,salt:e.salt,signature:e.signature,test:!!q()||void 0,took:t.took}))}function Ne(){s()&&!g()&&i(fe)===y.VERIFIED?wt():gt(y.EXPIRED,i(ae).expired)}function Ie(){let e=fetch;if(f())if(Pe("using customfetch"),"string"==typeof f()){if(e=globalThis[f()]||null,!e)throw new Error(`Custom fetch function not found: ${f()}`)}else e=f();return e}function Se(e,t=[S()||"",document.documentElement.lang||"",...navigator.languages]){const n=Object.keys(e).map((e=>e.toLowerCase())),r=t.reduce(((t,r)=>(r=r.toLowerCase(),t||(e[r]?r:null)||n.find((e=>r.split("-")[0]===e.split("-")[0]))||null)),null);return e[r||"en"]}function Le(e){return[...i(me)?.querySelectorAll(e?.length?e.map((e=>`input[name="${e}"]`)).join(", "):'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("(?e instanceof Error)))&&console[e[0]instanceof Error?"error":"log"]("ALTCHA",`[name=${L()}]`,...e)}function De(){b($e,Q.PAUSED,!0)}function Ve(e){b($e,Q.ERROR,!0)}function Be(){b($e,Q.READY,!0)}function je(){b($e,Q.LOADING,!0)}function Te(){b($e,Q.PLAYING,!0)}function Ue(){b($e,Q.PAUSED,!0)}function qe(e){if(e.preventDefault(),e.stopPropagation(),i(ue)){const t=new FormData(e.target),n=String(t.get("code"));if(H()?.startsWith("fn:")){const e=H().replace(/^fn:/,"");if(Pe(`calling ${e} function instead of verifyurl`),!(e in globalThis))throw new Error(`Global function "${e}" is undefined.`);return globalThis[e]({challenge:i(ue).challenge,code:n,solution:i(ue).solution})}b(Ee,!0),nt(Ae(i(ue).challenge,i(ue).solution),n).then((({reason:e,verified:t})=>{t?(b(ue,null),mt(y.VERIFIED),Pe("verified"),Rr().then((()=>{i(ge)?.focus(),te("verified",{payload:i(ke)}),"onsubmit"===a()?rt(i(ye)):V()&&vt()}))):(gt(),b(we,e||"Verification failed",!0))})).catch((e=>{b(ue,null),mt(y.ERROR,e),Pe("sentinel verification failed:",e)})).finally((()=>{b(Ee,!1)}))}}function Ze(e){const t=e.target;w()&&t&&!i(de).contains(t)&&(i(fe)===y.VERIFIED&&!1===_()||i(fe)===y.VERIFIED&&"focus"===_()&&!i(me)?.matches(":focus-within")||"off"===a()&&i(fe)===y.UNVERIFIED)&&vt()}function ze(){w()&&i(fe)!==y.UNVERIFIED&&pt()}function He(e){i(fe)===y.UNVERIFIED?wt():w()&&"focus"===_()&&i(fe)===y.VERIFIED&&yt()}function Ge(e){e.target?.hasAttribute("data-code-challenge-form")||(b(ye,e.submitter,!0),i(me)&&"onsubmit"===a()?(i(ye)?.blur(),i(fe)===y.UNVERIFIED?(e.preventDefault(),e.stopPropagation(),wt().then((()=>{rt(i(ye))}))):i(fe)!==y.VERIFIED&&(e.preventDefault(),e.stopPropagation(),i(fe)===y.VERIFYING&&Ke())):i(me)&&w()&&"off"===a()&&i(fe)===y.UNVERIFIED&&(e.preventDefault(),e.stopPropagation(),yt()))}function Je(){gt()}function Ke(){i(fe)===y.VERIFYING&&i(ae).waitAlert&&alert(i(ae).waitAlert)}function We(){i(ve)?i(ve).paused?(i(ve).currentTime=0,i(ve).play()):i(ve).pause():(b(_e,!0),requestAnimationFrame((()=>{i(ve)?.play()})))}function Qe(){w()&&pt()}function Ye(e){return JSON.parse(e)}function Xe(e){const t=new URLSearchParams(e.split("?")?.[1]),n=t.get("expires")||t.get("expire");if(n){const e=new Date(1e3*+n),t=isNaN(e.getTime())?0:e.getTime()-Date.now();t>0&&ot(t)}else xe&&(clearTimeout(xe),xe=null)}async function et(e){if(!H())throw new Error("Attribute verifyurl not set.");Pe("requesting server verification from",H());const t={payload:e};if(!1!==F()){const{blockedCountries:e,classifier:n,disableRules:r,email:o,expectedLanguages:a,expectedCountries:l,fields:s,ipAddress:c,text:u,timeZone:f}="ipAddress"===F()?{blockedCountries:void 0,classifier:void 0,disableRules:void 0,email:!1,expectedCountries:void 0,expectedLanguages:void 0,fields:!1,ipAddress:void 0,text:void 0,timeZone:void 0}:"object"==typeof F()?F():{blockedCountries:void 0,classifier:void 0,disableRules:void 0,email:void 0,expectedCountries:void 0,expectedLanguages:void 0,fields:void 0,ipAddress:void 0,text:void 0,timeZone:void 0};t.blockedCountries=e,t.classifier=n,t.disableRules=r,t.email=!1===o?void 0:function(e){const t=i(me)?.querySelector("string"==typeof e?`input[name="${e}"]`:'input[type="email"]:not([data-no-spamfilter])');return t?.value?.slice(t.value.indexOf("@"))||void 0}(o),t.expectedCountries=l,t.expectedLanguages=a||(ne?[ne]:void 0),t.fields=!1===s?void 0:Le(s),t.ipAddress=!1===c?void 0:c||"auto",t.text=u,t.timeZone=!1===f?void 0:f||Wn()}const n=await Ie()(H(),{body:JSON.stringify(t),headers:{"content-type":"application/json"},method:"POST"});if(!(n&&n instanceof Response))throw new Error("Custom fetch function did not return a response.");if(200!==n.status)throw new Error(`Server responded with ${n.status}.`);const r=await n.json();if(r?.payload&&b(ke,r.payload,!0),te("serververification",r),l()&&"BAD"===r.classification)throw new Error("SpamFilter returned negative classification.")}async function nt(e,t){if(!H())throw new Error("Attribute verifyurl not set.");Pe("requesting sentinel verification from",H());const n={code:t,payload:e};T()&&(n.fields=T().fields?Le():void 0,n.timeZone=T().timeZone?Wn():void 0);const r=await Ie()(H(),{body:JSON.stringify(n),headers:{"content-type":"application/json"},method:"POST"});if(!(r&&r instanceof Response))throw new Error("Fetch function did not return a response.");if(200!==r.status)throw new Error(`Server responded with ${r.status}.`);const o=await r.json();return o?.payload&&b(ke,o.payload,!0),te("sentinelverification",o),o}function rt(e){i(me)&&"requestSubmit"in i(me)?i(me).requestSubmit(e):i(me)?.reportValidity()&&(e?e.click():i(me).submit())}function ot(e){Pe("expire",e),xe&&(clearTimeout(xe),xe=null),e<1?Ne():xe=setTimeout(Ne,e)}function it(e){Pe("floating",e),w()!==e&&(i(de).style.left="",i(de).style.top=""),w(!0===e||""===e?"auto":!1===e||"false"===e?void 0:w()),w()?(a()||a("onsubmit"),document.addEventListener("scroll",ze),document.addEventListener("click",Ze),window.addEventListener("resize",Qe)):"onsubmit"===a()&&a(void 0)}function at(e){if(Pe("overlay",e),V(e),e){if(a()||a("onsubmit"),i(pe)&&i(de).parentElement&&i(pe).replaceWith(i(de).parentElement),i(de)?.parentElement?.parentElement){b(pe,document.createElement("div"),!0),i(de).parentElement.parentElement.appendChild(i(pe));const e=document.createElement("div"),t=document.createElement("button");t.type="button",t.innerHTML="×",t.addEventListener("click",(e=>{e.preventDefault(),gt()})),i(pe).classList.add("altcha-overlay-backdrop"),t.classList.add("altcha-overlay-close-button"),e.classList.add("altcha-overlay"),i(pe).append(e),e.append(t),M()&&e.append(...document.querySelectorAll(M())),e.append(i(de).parentElement)}}else i(pe)&&i(de).parentElement&&(i(pe).replaceWith(i(de).parentElement),i(de).style.display="block")}function lt(e){if(!e.algorithm)throw new Error("Invalid challenge. Property algorithm is missing.");if(void 0===e.signature)throw new Error("Invalid challenge. Property signature is missing.");if(!ee.includes(e.algorithm.toUpperCase()))throw new Error(`Unknown algorithm value. Allowed values: ${ee.join(", ")}`);if(!e.challenge||e.challenge.length<40)throw new Error("Challenge is too short. Min. 40 chars.");if(!e.salt||e.salt.length<10)throw new Error("Salt is too short. Min. 10 chars.")}async function st(e){let t=null,n=null;if("Worker"in window){try{t=function(e,t=("number"==typeof q()?q():e.maxNumber||e.maxnumber||O()),n=Math.ceil(G())){const r=new AbortController,o=[];n=Math.min(16,t,Math.max(1,n));for(let e=0;e{const t=await Promise.all(o.map(((t,n)=>{const a=n*i;return r.signal.addEventListener("abort",(()=>{t.postMessage({type:"abort"})})),new Promise((n=>{t.addEventListener("message",(e=>{if(e.data)for(const e of o)e!==t&&e.postMessage({type:"abort"});n(e.data)})),t.postMessage({payload:e,max:a+i,start:a,type:"work"})}))})));for(const e of o)e.terminate();return t.find((e=>!!e))||null})(),controller:r}}(e,e.maxNumber||e.maxnumber||O()),b(se,t.controller,!0),n=await t.promise}catch(e){Pe(e)}finally{b(se,null)}if(null===n||void 0!==n?.number||"obfuscated"in e)return{data:e,solution:n}}if("obfuscated"in e){const t=await il(e.obfuscated,e.key,e.maxNumber||e.maxnumber);return{data:e,solution:await t.promise}}t=rl(e.challenge,e.salt,e.algorithm,e.maxNumber||e.maxnumber||O()),b(se,t.controller,!0);try{n=await t.promise}catch(e){Pe(e)}finally{b(se,null)}return{data:e,solution:n}}async function ct(){if(!D())return void mt(y.ERROR);const e=Re.find((e=>"obfuscation"===e.constructor.pluginName));return e&&"clarify"in e?"clarify"in e&&"function"==typeof e.clarify?e.clarify():void 0:(mt(y.ERROR),void Pe("Plugin `obfuscation` not found. Import `altcha/plugins/obfuscation` to load it."))}function ut(e){void 0!==e.obfuscated&&D(e.obfuscated),void 0!==e.auto&&(a(e.auto),"onload"===a()&&(D()?ct():wt())),void 0!==e.blockspam&&l(!!e.blockspam),void 0!==e.customfetch&&f(e.customfetch),void 0!==e.floatinganchor&&$(e.floatinganchor),void 0!==e.delay&&h(e.delay),void 0!==e.floatingoffset&&C(e.floatingoffset),void 0!==e.floating&&it(e.floating),void 0!==e.expire&&(ot(e.expire),m(e.expire)),e.challenge&&(c("string"==typeof e.challenge?e.challenge:JSON.stringify(e.challenge)),lt(i(oe))),void 0!==e.challengeurl&&s(e.challengeurl),void 0!==e.debug&&d(!!e.debug),void 0!==e.hidefooter&&k(!!e.hidefooter),void 0!==e.hidelogo&&A(!!e.hidelogo),void 0!==e.language&&U(Se(o(),[e.language])),void 0!==e.maxnumber&&O(+e.maxnumber),void 0!==e.mockerror&&P(!!e.mockerror),void 0!==e.name&&L(e.name),void 0!==e.overlaycontent&&M(e.overlaycontent),void 0!==e.overlay&&at(e.overlay),void 0!==e.refetchonexpire&&g(!e.refetchonexpire),void 0!==e.disablerefetchonexpire&&g(!e.disablerefetchonexpire),void 0!==e.sentinel&&"object"==typeof e.sentinel&&T(e.sentinel),void 0!==e.spamfilter&&F("object"==typeof e.spamfilter?e.spamfilter:!!e.spamfilter),e.strings&&U("string"==typeof e.strings?e.strings:JSON.stringify(e.strings)),void 0!==e.test&&q("number"==typeof e.test?e.test:!!e.test),void 0!==e.verifyurl&&H(e.verifyurl),void 0!==e.workers&&G(+e.workers),void 0!==e.workerurl&&W(e.workerurl)}function ft(){return{auto:a(),blockspam:l(),challengeurl:s(),debug:d(),delay:h(),disableautofocus:v(),disablerefetchonexpire:g(),expire:m(),floating:w(),floatinganchor:$(),floatingoffset:C(),hidefooter:k(),hidelogo:A(),name:L(),maxnumber:O(),mockerror:P(),obfuscated:D(),overlay:V(),refetchonexpire:!g(),spamfilter:F(),strings:i(ae),test:q(),verifyurl:H(),workers:G(),workerurl:W()}}function dt(){return i(be)}function ht(){return i(fe)}function vt(){i(de).style.display="none",V()&&i(pe)&&(i(pe).style.display="none")}function pt(e=20){if(i(de))if(i(be)||b(be,($()?document.querySelector($()):i(me)?.querySelector('input[type="submit"], button[type="submit"], button:not([type="button"]):not([type="reset"])'))||i(me),!0),i(be)){const t=parseInt(C(),10)||12,n=i(be).getBoundingClientRect(),r=i(de).getBoundingClientRect(),o=document.documentElement.clientHeight,a=document.documentElement.clientWidth,l="auto"===w()?n.bottom+r.height+t+e>o:"top"===w(),s=Math.max(e,Math.min(a-e-r.width,n.left+n.width/2-r.width/2));if(i(de).style.top=l?n.top-(r.height+t)+"px":`${n.bottom+t}px`,i(de).style.left=`${s}px`,i(de).setAttribute("data-floating",l?"top":"bottom"),i(he)){const e=i(he).getBoundingClientRect();i(he).style.left=n.left-s+n.width/2-e.width/2+"px"}}else Pe("unable to find floating anchor element")}function gt(e=y.UNVERIFIED,t=null){i(se)&&(i(se).abort(),b(se,null)),b(ce,!1),b(ke,null),b(ue,null),b(_e,!1),b($e,null),mt(e,t)}function bt(e){b(be,e,!0)}function mt(e,t=null){b(fe,e,!0),b(we,t,!0),te("statechange",{payload:i(ke),state:i(fe)})}function yt(){i(de).style.display="block",w()&&pt(),V()&&i(pe)&&(i(pe).style.display="flex")}async function wt(){return gt(y.VERIFYING),await new Promise((e=>setTimeout(e,h()||0))),async function(){if(P())throw Pe("mocking error"),new Error("Mocked error.");if(i(oe))return Pe("using provided json data"),Xe(i(oe).salt),i(oe);if(q())return Pe("generating test challenge",{test:q()}),tl("boolean"!=typeof q()?+q():void 0);{if(!s()&&i(me)){const e=i(me).getAttribute("action");e?.includes("/form/")&&s(e+"/altcha")}if(!s())throw new Error("Attribute challengeurl not set.");Pe("fetching challenge from",s());const e={credentials:"boolean"==typeof u()?"include":u(),headers:!1!==F()?{"x-altcha-spam-filter":"1"}:{}},t=await Ie()(s(),e);if(!(t&&t instanceof Response))throw new Error("Custom fetch function did not return a response.");if(200!==t.status)throw new Error(`Server responded with ${t.status}.`);const n=t.headers.get("X-Altcha-Config"),r=await t.json();if(Xe(r.salt),n)try{const e=JSON.parse(n);e&&"object"==typeof e&&(e.verifyurl&&!e.verifyurl.startsWith("fn:")&&(e.verifyurl=Oe(e.verifyurl)),ut(e))}catch(e){Pe("unable to configure from X-Altcha-Config",e)}return r}}().then((e=>(lt(e),Pe("challenge",e),st(e)))).then((({data:e,solution:t})=>{if(Pe("solution",t),!t||e&&"challenge"in e&&!("clearText"in t))if(void 0!==t?.number&&"challenge"in e)if(H()&&"codeChallenge"in e)["INPUT","BUTTON","SELECT","TEXTAREA"].includes(document.activeElement?.tagName||"")&&!1===v()&&document.activeElement.blur(),b(ue,{challenge:e,solution:t},!0);else{if(H()&&void 0!==T())return nt(Ae(e,t));if(H())return et(Ae(e,t));b(ke,Ae(e,t),!0),Pe("payload",i(ke))}else if(i(fe)!==y.EXPIRED)throw Pe("Unable to find a solution. Ensure that the 'maxnumber' attribute is greater than the randomly generated number."),new Error("Unexpected result returned.")})).then((()=>{i(ue)?(mt(y.CODE),Rr().then((()=>{te("code",{codeChallenge:i(ue)})}))):i(ke)&&(mt(y.VERIFIED),Pe("verified"),Rr().then((()=>{te("verified",{payload:i(ke)}),V()&&vt()})))})).catch((e=>{Pe(e),mt(y.ERROR,e.message)}))}Dr((()=>{!function(){for(const e of Re)"function"==typeof e.onErrorChange&&e.onErrorChange(i(we))}(i(we))})),Dr((()=>{!function(){for(const e of Re)"function"==typeof e.onStateChange&&e.onStateChange(i(fe));w()&&i(fe)!==y.UNVERIFIED&&requestAnimationFrame((()=>{pt()})),b(ce,i(fe)===y.VERIFIED),V()&&i(pe)&&(i(fe)!==y.UNVERIFIED?yt():vt())}(i(fe))})),Ga((()=>{(function(){for(const e of Re)e.destroy()})(),b(ye,null),i(me)&&(i(me).removeEventListener("submit",Ge),i(me).removeEventListener("reset",Je),i(me).removeEventListener("focusin",He),b(me,null)),xe&&(clearTimeout(xe),xe=null),document.removeEventListener("click",Ze),document.removeEventListener("scroll",ze),window.removeEventListener("resize",Qe)})),Mo((()=>{Pe("mounted","2.2.4"),Pe("workers",G()),function(){const e=void 0!==j()?j().split(","):void 0;for(const t of globalThis.altchaPlugins)(!e||e.includes(t.pluginName))&&Re.push(new t({el:i(de),clarify:ct,dispatch:te,getConfiguration:ft,getFloatingAnchor:dt,getState:ht,log:Pe,reset:gt,solve:st,setState:mt,setFloatingAnchor:bt,verify:wt}))}(),Pe("plugins",Re.length?Re.map((e=>e.constructor.pluginName)).join(", "):"none"),q()&&Pe("using test mode"),m()&&ot(m()),void 0!==a()&&Pe("auto",a()),void 0!==w()&&it(w()),b(me,i(de)?.closest("form"),!0),i(me)&&(i(me).addEventListener("submit",Ge,{capture:!0}),i(me).addEventListener("reset",Je),("onfocus"===a()||"focus"===_())&&i(me).addEventListener("focusin",He)),V()&&at(!0),"onload"===a()&&(D()?ct():wt()),i(re)&&(k()||A())&&Pe("Attributes hidefooter and hidelogo ignored because usage with free API Keys requires attribution."),requestAnimationFrame((()=>{te("load")}))}));var xt=Cl(),$t=Nt(xt);Fa($t,t,"default",{});var Et=J($t,2),Ct=z(Et),Rt=z(Ct);let _t;var kt=z(Rt),At=e=>{$r(e)};K(kt,(e=>{i(fe)===y.VERIFYING&&e(At)}));var It=J(kt,2);qn(It),It.__change=[ul,fe,F,me,ce,D,ct,wt],Qt(It,(e=>b(ge,e)),(()=>i(ge))),Z(Rt);var St=J(Rt,2),Ot=z(St),Pt=e=>{var t=Xt();tt(Nt(t),(()=>i(ae).verified)),B(e,t)},Dt=(e,t)=>{var n=e=>{var t=Xt();tt(Nt(t),(()=>i(ae).verifying)),B(e,t)},r=(e,t)=>{var n=e=>{var t=Xt();tt(Nt(t),(()=>i(ae).verificationRequired)),B(e,t)},r=e=>{var t=Xt();tt(Nt(t),(()=>i(ae).label)),B(e,t)};K(e,(e=>{i(fe)===y.CODE?e(n):e(r,!1)}),t)};K(e,(e=>{i(fe)===y.VERIFYING?e(n):e(r,!1)}),t)};K(Ot,(e=>{i(fe)===y.VERIFIED?e(Pt):e(Dt,!1)})),Z(St);var Vt=J(St,2),Bt=e=>{var t=fl();qn(t),Ce((()=>{R(t,"name",L()),qa(t,i(ke))})),B(e,t)};K(Vt,(e=>{i(fe)===y.VERIFIED&&e(Bt)}));var Mt=J(Vt,2),jt=e=>{var t=dl(),n=z(t);R(n,"href","https://altcha.org/"),Z(t),Ce((()=>R(n,"aria-label",i(ae).ariaLinkLabel))),B(e,t)};K(Mt,(e=>{(!0!==A()||i(re))&&e(jt)}));var Tt=J(Mt,2),Ft=e=>{var t=_l(),n=J(z(t),2),r=z(n),o=J(r,2);Sa(o,!v()),o.__keydown=[ll,We];var a=J(o,2),l=z(a),s=z(l),c=e=>{var t=pl();t.__click=We;var n=z(t),r=e=>{$r(e,(()=>20))},o=(e,t)=>{var n=e=>{B(e,hl())},r=(e,t)=>{var n=e=>{B(e,vl())},r=e=>{B(e,gl())};K(e,(e=>{i($e)===Q.PLAYING?e(n):e(r,!1)}),t)};K(e,(e=>{i($e)===Q.ERROR?e(n):e(r,!1)}),t)};K(n,(e=>{i($e)===Q.LOADING?e(r):e(o,!1)})),Z(t),Ce((()=>{R(t,"title",i(ae).getAudioChallenge),t.disabled=i($e)===Q.LOADING||i($e)===Q.ERROR||i(Ee),R(t,"aria-label",i($e)===Q.LOADING?i(ae).loading:i(ae).getAudioChallenge)})),B(e,t)};K(s,(e=>{i(ue).challenge.codeChallenge.audio&&e(c)}));var u=J(s,2);u.__click=[sl,wt],Z(l);var f=J(l,2),d=z(f),h=e=>{$r(e,(()=>16))};K(d,(e=>{i(Ee)&&e(h)}));var p=J(d);Z(f),Z(a);var g=J(a,2),m=e=>{var t=ml(),n=z(t);Z(t),Qt(t,(e=>b(ve,e)),(()=>i(ve))),Ce((e=>R(n,"src",e)),[()=>Oe(i(ue).challenge.codeChallenge.audio,{language:S()})]),Fe("loadstart",t,je),Fe("canplay",t,Be),Fe("pause",t,Ue),Fe("playing",t,Te),Fe("ended",t,De),Fe("error",n,Ve),B(e,t)};K(g,(e=>{i(ue).challenge.codeChallenge.audio&&i(_e)&&e(m)})),Z(n),Z(t),Ce((()=>{R(t,"aria-label",i(ae).verificationRequired),R(r,"src",i(ue).challenge.codeChallenge.image),R(o,"minlength",i(ue).challenge.codeChallenge.length||1),R(o,"maxlength",i(ue).challenge.codeChallenge.length),R(o,"placeholder",i(ae).enterCode),R(o,"aria-label",i($e)===Q.LOADING?i(ae).loading:i($e)===Q.PLAYING?"":i(ae).enterCodeAria),R(o,"aria-live",i($e)?"assertive":"polite"),R(o,"aria-busy",i($e)===Q.LOADING),o.disabled=i(Ee),R(u,"aria-label",i(ae).reload),R(u,"title",i(ae).reload),u.disabled=i(Ee),f.disabled=i(Ee),R(f,"aria-label",i(ae).verify),La(p,` ${i(ae).verify??""}`)})),Fe("submit",n,qe,!0),B(e,t)};K(Tt,(e=>{i(ue)?.challenge.codeChallenge&&e(Ft)})),Z(Ct);var Ut=J(Ct,2),qt=e=>{var t=wl(),n=J(z(t),2),r=e=>{var t=bl();tt(z(t),(()=>i(ae).expired)),Z(t),Ce((()=>R(t,"title",i(we)))),B(e,t)},o=e=>{var t=yl();tt(z(t),(()=>i(ae).error)),Z(t),Ce((()=>R(t,"title",i(we)))),B(e,t)};K(n,(e=>{i(fe)===y.EXPIRED?e(r):e(o,!1)})),Z(t),B(e,t)};K(Ut,(e=>{(i(we)||i(fe)===y.EXPIRED)&&e(qt)}));var Zt=J(Ut,2),zt=e=>{var t=El(),n=z(t);tt(z(n),(()=>i(ae).footer)),Z(n),Z(t),B(e,t)};K(Zt,(e=>{i(ae).footer&&(!0!==k()||i(re))&&e(zt)}));var Ht=J(Zt,2),Gt=e=>{var t=xl();Qt(t,(e=>b(he,e)),(()=>i(he))),B(e,t)};K(Ht,(e=>{w()&&e(Gt)})),Z(Et),Qt(Et,(e=>b(de,e)),(()=>i(de))),Ce((e=>{R(Et,"data-state",i(fe)),R(Et,"data-floating",w()),R(Et,"data-overlay",V()),_t=Va(Rt,1,"altcha-checkbox",null,_t,e),R(It,"id",i(le)),It.required="onsubmit"!==a()&&(!w()||"off"!==a()),R(St,"for",i(le))}),[()=>({"altcha-checkbox-verifying":i(fe)===y.VERIFYING})]),Fe("invalid",It,Ke),Ha(It,(()=>i(ce)),(e=>b(ce,e))),B(e,xt);var Jt=So({clarify:ct,configure:ut,getConfiguration:ft,getFloatingAnchor:dt,getPlugin:function(e){return Re.find((t=>t.constructor.pluginName===e))},getState:ht,hide:vt,repositionFloating:pt,reset:gt,setFloatingAnchor:bt,setState:mt,show:yt,verify:wt,get auto(){return a()},set auto(e=void 0){a(e),E()},get blockspam(){return l()},set blockspam(e=void 0){l(e),E()},get challengeurl(){return s()},set challengeurl(e=void 0){s(e),E()},get challengejson(){return c()},set challengejson(e=void 0){c(e),E()},get credentials(){return u()},set credentials(e=void 0){u(e),E()},get customfetch(){return f()},set customfetch(e=void 0){f(e),E()},get debug(){return d()},set debug(e=!1){d(e),E()},get delay(){return h()},set delay(e=0){h(e),E()},get disableautofocus(){return v()},set disableautofocus(e=!1){v(e),E()},get refetchonexpire(){return p()},set refetchonexpire(e=!0){p(e),E()},get disablerefetchonexpire(){return g()},set disablerefetchonexpire(e=!p){g(e),E()},get expire(){return m()},set expire(e=void 0){m(e),E()},get floating(){return w()},set floating(e=void 0){w(e),E()},get floatinganchor(){return $()},set floatinganchor(e=void 0){$(e),E()},get floatingoffset(){return C()},set floatingoffset(e=void 0){C(e),E()},get floatingpersist(){return _()},set floatingpersist(e=!1){_(e),E()},get hidefooter(){return k()},set hidefooter(e=!1){k(e),E()},get hidelogo(){return A()},set hidelogo(e=!1){A(e),E()},get id(){return I()},set id(e=void 0){I(e),E()},get language(){return S()},set language(e=void 0){S(e),E()},get name(){return L()},set name(e="altcha"){L(e),E()},get maxnumber(){return O()},set maxnumber(e=1e6){O(e),E()},get mockerror(){return P()},set mockerror(e=!1){P(e),E()},get obfuscated(){return D()},set obfuscated(e=void 0){D(e),E()},get overlay(){return V()},set overlay(e=void 0){V(e),E()},get overlaycontent(){return M()},set overlaycontent(e=void 0){M(e),E()},get plugins(){return j()},set plugins(e=void 0){j(e),E()},get sentinel(){return T()},set sentinel(e=void 0){T(e),E()},get spamfilter(){return F()},set spamfilter(e=!1){F(e),E()},get strings(){return U()},set strings(e=void 0){U(e),E()},get test(){return q()},set test(e=!1){q(e),E()},get verifyurl(){return H()},set verifyurl(e=void 0){H(e),E()},get workers(){return G()},set workers(e=Math.min(16,navigator.hardwareConcurrency||8)){G(e),E()},get workerurl(){return W()},set workerurl(e=void 0){W(e),E()}});return r(),Jt}Na(["change","keydown","click"]),customElements.define("altcha-widget",Qa(kl,{blockspam:{type:"Boolean"},debug:{type:"Boolean"},delay:{type:"Number"},disableautofocus:{type:"Boolean"},disablerefetchonexpire:{type:"Boolean"},expire:{type:"Number"},floatingoffset:{type:"Number"},hidefooter:{type:"Boolean"},hidelogo:{type:"Boolean"},maxnumber:{type:"Number"},mockerror:{type:"Boolean"},refetchonexpire:{type:"Boolean"},test:{type:"Boolean"},workers:{type:"Number"},auto:{},challengeurl:{},challengejson:{},credentials:{},customfetch:{},floating:{},floatinganchor:{},floatingpersist:{},id:{},language:{},name:{},obfuscated:{},overlay:{},overlaycontent:{},plugins:{},sentinel:{},spamfilter:{},strings:{},verifyurl:{},workerurl:{}},["default"],["clarify","configure","getConfiguration","getFloatingAnchor","getPlugin","getState","hide","repositionFloating","reset","setFloatingAnchor","setState","show","verify"],!1));const Bo='@keyframes overlay-slidein{to{opacity:1;top:50%}}@keyframes altcha-spinner{to{transform:rotate(360deg)}}.altcha{background:var(--altcha-color-base, transparent);border:var(--altcha-border-width, 1px) solid var(--altcha-color-border, #a0a0a0);border-radius:var(--altcha-border-radius, 3px);color:var(--altcha-color-text, currentColor);display:flex;flex-direction:column;max-width:var(--altcha-max-width, 260px);position:relative}.altcha:focus-within{border-color:var(--altcha-color-border-focus, currentColor)}.altcha[data-floating]{background:var(--altcha-color-base, white);display:none;filter:drop-shadow(3px 3px 6px rgba(0,0,0,.2));left:-100%;position:fixed;top:-100%;width:var(--altcha-max-width, 260px);z-index:999999}.altcha[data-floating=top] .altcha-anchor-arrow{border-bottom-color:transparent;border-top-color:var(--altcha-color-border, #a0a0a0);bottom:-12px;top:auto}.altcha[data-floating=bottom]:focus-within::after{border-bottom-color:var(--altcha-color-border-focus, currentColor)}.altcha[data-floating=top]:focus-within::after{border-top-color:var(--altcha-color-border-focus, currentColor)}.altcha[data-floating]:not([data-state=unverified]){display:block}.altcha-anchor-arrow{border:6px solid transparent;border-bottom-color:var(--altcha-color-border, #a0a0a0);content:"";height:0;left:12px;position:absolute;top:-12px;width:0}.altcha-main{align-items:center;display:flex;gap:.4rem;padding:.7rem;position:relative}.altcha-code-challenge{background:var(--altcha-color-base, white);border:1px solid var(--altcha-color-border-focus, currentColor);border-radius:var(--altcha-border-radius, 3px);filter:drop-shadow(3px 3px 6px rgba(0,0,0,.2));padding:.5rem;position:absolute;top:2.5rem;z-index:9999999}.altcha-code-challenge>form{display:flex;flex-direction:column;gap:.5rem}.altcha-code-challenge-input{border:1px solid currentColor;border-radius:3px;box-sizing:border-box;outline:0;font-size:16px;padding:.35rem;width:220px}.altcha-code-challenge-input:focus{outline:2px solid color-mix(in srgb,var(--altcha-color-active, #1D1DC9) 20%,transparent)}.altcha-code-challenge-input:disabled{opacity:.7}.altcha-code-challenge-image{background-color:#fff;border:1px solid currentColor;border-radius:3px;box-sizing:border-box;object-fit:contain;height:50px;width:220px}.altcha-code-challenge-audio,.altcha-code-challenge-reload{background:color-mix(in srgb,var(--altcha-color-text, currentColor) 10%,transparent);border:0;border-radius:3px;color:var(--altcha-color-text, currentColor);cursor:pointer;display:flex;align-items:center;justify-content:center;padding:.35rem}.altcha-code-challenge-audio:disabled,.altcha-code-challenge-reload:disabled,.altcha-code-challenge-verify:disabled{opacity:.7;pointer-events:none}.altcha-code-challenge-audio>*,.altcha-code-challenge-reload>*{height:20px;width:20px}.altcha-code-challenge-buttons{display:flex;justify-content:space-between}.altcha-code-challenge-buttons-left{display:flex;gap:.25rem}.altcha-code-challenge-verify{align-items:center;background:var(--altcha-color-active, #1D1DC9);border:0;border-radius:3px;color:#fff;cursor:pointer;display:flex;gap:.5rem;font-size:100%;padding:.35rem 1rem}.altcha-code-challenge-arrow{border:6px solid transparent;border-bottom-color:var(--altcha-color-border, currentColor);content:"";height:0;left:.15rem;position:absolute;top:-12px;width:0}.altcha[data-floating=top] .altcha-code-challenge{top:-150px}.altcha[data-floating=top] .altcha-code-challenge-arrow{border-bottom-color:transparent;border-top-color:var(--altcha-color-border, currentColor);bottom:-12px;top:auto}.altcha-label{cursor:pointer;flex-grow:1}.altcha-logo{color:currentColor!important;opacity:.7}.altcha-footer:hover,.altcha-logo:hover{opacity:1}.altcha-error{color:var(--altcha-color-error-text, #f23939);display:flex;font-size:.85rem;gap:.3rem;padding:0 .7rem .7rem}.altcha-footer{align-items:center;background-color:var(--altcha-color-footer-bg, transparent);display:flex;font-size:.75rem;opacity:.7;justify-content:end;padding:.2rem .7rem}.altcha-footer a{color:currentColor}.altcha-checkbox{display:flex;align-items:center;justify-content:center;height:24px;position:relative;width:24px}.altcha-checkbox .altcha-spinner{bottom:0;left:0;position:absolute;right:0;top:0}.altcha-checkbox input{width:18px;height:18px;margin:0}.altcha-checkbox-verifying input{appearance:none;opacity:0;pointer-events:none}.altcha-spinner{animation:altcha-spinner .75s infinite linear;transform-origin:center}.altcha-overlay{--altcha-color-base:#fff;--altcha-color-text:#000;animation:overlay-slidein .5s forwards;display:flex;flex-direction:column;gap:.5rem;left:50%;width:260px;opacity:0;position:fixed;top:45%;transform:translate(-50%,-50%)}.altcha-overlay-backdrop{background:rgba(0,0,0,.5);bottom:0;display:none;left:0;position:fixed;right:0;top:0;z-index:99999999}.altcha-overlay-close-button{align-self:flex-end;background:0 0;border:0;padding:.25rem;cursor:pointer;color:currentColor;font-size:130%;line-height:1;opacity:.7}@media (max-height:450px){.altcha-overlay{top:10%!important;transform:translate(-50%,0)}}';function Ho(e,t="__altcha-css"){if(!document.getElementById(t)){const n=document.createElement("style");n.id=t,n.textContent=e,document.head.appendChild(n)}}globalThis.altchaCreateWorker=e=>e?new Worker(new URL(e)):new Ni,Ho(Bo),Ho(Bo);export{kl as Altcha}; +//# sourceMappingURL=/sm/2e0fd2382ba0f4c2525d5a6a0d13423ff9e02efb653e53f4d8cfa83af626b1ea.map \ No newline at end of file diff --git a/containers/auth-gateway/auth_gateway/static/challenge.js b/containers/auth-gateway/auth_gateway/static/challenge.js new file mode 100644 index 0000000..cee1645 --- /dev/null +++ b/containers/auth-gateway/auth_gateway/static/challenge.js @@ -0,0 +1,35 @@ +document.addEventListener("DOMContentLoaded", function () { + var widget = document.getElementById("altcha-widget"); + var progressFill = document.getElementById("progress-fill"); + var statusText = document.getElementById("challenge-status"); + var challengeIcon = document.getElementById("challenge-icon"); + + if (!widget) return; + + var fakeProgress = 0; + var progressInterval = setInterval(function () { + fakeProgress = Math.min(fakeProgress + Math.random() * 2.5, 88); + progressFill.style.width = fakeProgress + "%"; + }, 150); + + widget.addEventListener("statechange", function (ev) { + if (ev.detail.state === "verified") { + clearInterval(progressInterval); + progressFill.style.width = "100%"; + progressFill.classList.add("progress-done"); + challengeIcon.textContent = "✓"; + challengeIcon.classList.add("challenge-icon-done"); + statusText.textContent = "Verification complete. Redirecting..."; + document.getElementById("altcha-payload").value = ev.detail.payload; + setTimeout(function () { + document.getElementById("challenge-form").submit(); + }, 500); + } else if (ev.detail.state === "error") { + clearInterval(progressInterval); + challengeIcon.textContent = "✕"; + challengeIcon.classList.add("challenge-icon-error"); + statusText.textContent = "Verification failed. Please reload the page."; + progressFill.classList.add("progress-error"); + } + }); +}); diff --git a/containers/auth-gateway/auth_gateway/static/style.css b/containers/auth-gateway/auth_gateway/static/style.css index 2d03e45..5edc301 100644 --- a/containers/auth-gateway/auth_gateway/static/style.css +++ b/containers/auth-gateway/auth_gateway/static/style.css @@ -238,6 +238,78 @@ input:focus { margin: 2px 2px 2px 0; } +/* ── Challenge page ── */ +.challenge-wrapper { + text-align: center; + padding: 8px 0 4px; +} + +.challenge-icon-wrap { + margin-bottom: 16px; +} + +.challenge-icon { + font-size: 44px; + line-height: 1; + display: inline-block; + transition: transform 300ms ease; +} + +.challenge-icon-done { + color: #22c55e; + font-style: normal; + font-size: 40px; + font-weight: 700; +} + +.challenge-icon-error { + color: var(--danger); + font-style: normal; + font-size: 40px; + font-weight: 700; +} + +.challenge-error-text { + color: var(--danger); +} + +.challenge-progress { + margin-top: 24px; +} + +.altcha-wrapper { + position: absolute; + left: -9999px; + top: -9999px; + width: 0; + height: 0; + overflow: hidden; +} + +.progress-bar { + width: 100%; + height: 5px; + background: var(--line); + border-radius: 99px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + width: 0%; + background: var(--accent); + border-radius: 99px; + transition: width 200ms ease, background 300ms ease; +} + +.progress-done { + background: #22c55e !important; +} + +.progress-error { + background: var(--danger) !important; +} + /* ── Dark mode toggle ── */ #dark-mode-toggle { position: fixed; diff --git a/containers/auth-gateway/auth_gateway/templates/challenge.html b/containers/auth-gateway/auth_gateway/templates/challenge.html new file mode 100644 index 0000000..87bfeaa --- /dev/null +++ b/containers/auth-gateway/auth_gateway/templates/challenge.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% block title %}Verifying your browser — Gatekeeper{% endblock %} +{% block content %} +
+
+ 🛡 +
+

Verifying your browser

+ {% if error %} +

Verification failed. Please reload and try again.

+ {% else %} +

Please wait a moment...

+ {% endif %} +
+
+
+
+
+
+ +
+ + +
+ + {% if not error %} + + + + {% endif %} +{% endblock %} diff --git a/containers/auth-gateway/auth_gateway/web/challenge_routes.py b/containers/auth-gateway/auth_gateway/web/challenge_routes.py new file mode 100644 index 0000000..b39b194 --- /dev/null +++ b/containers/auth-gateway/auth_gateway/web/challenge_routes.py @@ -0,0 +1,67 @@ +from urllib.parse import urlsplit + +from auth_gateway.services.challenge_service import ( + CHALLENGE_COOKIE_NAME, + generate_altcha_challenge, + generate_challenge_cookie, + verify_altcha_solution, +) +from fastapi import APIRouter, Form, Request +from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse + +router = APIRouter() + + +def _safe_next(url: str | None, external_path: str) -> str: + if not url: + return f"{external_path}/login" + parts = urlsplit(url) + if parts.scheme or parts.netloc: + return f"{external_path}/login" + path = parts.path or "/" + if not path.startswith("/"): + return f"{external_path}/login" + return f"{path}?{parts.query}" if parts.query else path + + +@router.get("/challenge", response_class=HTMLResponse) +async def challenge_page(request: Request, next: str = "/"): + external_path = request.app.state.settings.external_path + return request.app.state.templates.TemplateResponse( + "challenge.html", + {"request": request, "external_path": external_path, "next": next}, + ) + + +@router.get("/challenge/data") +async def challenge_data(request: Request): + hmac_key = request.app.state.settings.challenge_secret + challenge = generate_altcha_challenge(hmac_key) + return JSONResponse(challenge) + + +@router.post("/challenge/verify") +async def challenge_verify(request: Request, next: str = Form("/"), altcha: str = Form(...)): + hmac_key = request.app.state.settings.challenge_secret + external_path = request.app.state.settings.external_path + + if not verify_altcha_solution(altcha, hmac_key): + return request.app.state.templates.TemplateResponse( + "challenge.html", + {"request": request, "external_path": external_path, "next": next, "error": True}, + status_code=400, + ) + + cookie_value = generate_challenge_cookie(hmac_key) + safe_next = _safe_next(next, external_path) + response = RedirectResponse(safe_next, status_code=303) + response.set_cookie( + key=CHALLENGE_COOKIE_NAME, + value=cookie_value, + httponly=True, + secure=request.app.state.settings.secure_cookies, + samesite="strict", + path="/", + max_age=300, + ) + return response diff --git a/containers/auth-gateway/auth_gateway/web/dependencies.py b/containers/auth-gateway/auth_gateway/web/dependencies.py index 78e2dad..faeb906 100644 --- a/containers/auth-gateway/auth_gateway/web/dependencies.py +++ b/containers/auth-gateway/auth_gateway/web/dependencies.py @@ -110,3 +110,19 @@ def validate_csrf(request: Request, submitted_token: str | None) -> None: cookie_token = request.cookies.get(cookie_name) if not cookie_token or not submitted_token or cookie_token != submitted_token: raise HTTPException(status_code=403, detail="CSRF validation failed.") + + +def challenge_is_valid(request: Request) -> bool: + from auth_gateway.services.challenge_service import CHALLENGE_COOKIE_NAME, verify_challenge_cookie + cookie = request.cookies.get(CHALLENGE_COOKIE_NAME) + if not cookie: + return False + return verify_challenge_cookie(cookie, request.app.state.settings.challenge_secret) + + +def build_challenge_url(request: Request, login_route: str, next_url: str) -> str: + external_path = request.app.state.settings.external_path.rstrip("/") + login_path = f"{external_path}{login_route}" + if next_url and next_url != "/": + login_path = f"{login_path}?{urlencode({'next': next_url})}" + return build_external_url(request, "/challenge", next=login_path) diff --git a/containers/auth-gateway/auth_gateway/web/login_routes.py b/containers/auth-gateway/auth_gateway/web/login_routes.py index 41d2e8b..dc5acf5 100644 --- a/containers/auth-gateway/auth_gateway/web/login_routes.py +++ b/containers/auth-gateway/auth_gateway/web/login_routes.py @@ -9,7 +9,9 @@ from auth_gateway.services.policy_engine import evaluate_ip_access, extract_clie from auth_gateway.services.resolver import normalize_host from auth_gateway.services.totp_service import verify_totp from auth_gateway.web.dependencies import ( + build_challenge_url, build_external_url, + challenge_is_valid, get_effective_expiration, get_effective_policy, get_oidc_method, @@ -147,6 +149,8 @@ async def login_page(request: Request, next: str = "/"): @router.get("/login/password", response_class=HTMLResponse) async def login_password_page(request: Request, next: str = "/"): + if not challenge_is_valid(request): + return RedirectResponse(build_challenge_url(request, "/login/password", next), status_code=303) runtime_config = get_runtime_config(request) context = resolve_context_from_request(request, runtime_config, next) effective_policy = get_effective_policy(runtime_config, context.policy_name) @@ -171,6 +175,8 @@ async def login_password_submit( password: str = Form(...), csrf_token: str = Form(...), ): + if not challenge_is_valid(request): + return RedirectResponse(build_challenge_url(request, "/login/password", next), status_code=303) runtime_config = get_runtime_config(request) context = resolve_context_from_request(request, runtime_config, next) effective_policy = get_effective_policy(runtime_config, context.policy_name) @@ -226,6 +232,8 @@ async def login_password_submit( @router.get("/login/totp", response_class=HTMLResponse) async def login_totp_page(request: Request, next: str = "/"): + if not challenge_is_valid(request): + return RedirectResponse(build_challenge_url(request, "/login/totp", next), status_code=303) runtime_config = get_runtime_config(request) context = resolve_context_from_request(request, runtime_config, next) effective_policy = get_effective_policy(runtime_config, context.policy_name) @@ -248,6 +256,8 @@ async def login_totp_page(request: Request, next: str = "/"): @router.post("/login/totp") @limiter.limit(AUTH_RATE_LIMIT) async def login_totp_submit(request: Request, next: str = Form("/"), token: str = Form(...), csrf_token: str = Form(...)): + if not challenge_is_valid(request): + return RedirectResponse(build_challenge_url(request, "/login/totp", next), status_code=303) runtime_config = get_runtime_config(request) context = resolve_context_from_request(request, runtime_config, next) effective_policy = get_effective_policy(runtime_config, context.policy_name)