@authhero/widget 0.28.2 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ import{r as t,c as e,g as i,h as s}from"./p-BFP_5sHV.js";function n(t){const e=t.match(/^#([0-9a-f]{3})$/i)||t.match(/^#([0-9a-f]{6})$/i);if(!e)return null;let i=e[1];3===i.length&&(i=i[0]+i[0]+i[1]+i[1]+i[2]+i[2]);const s=parseInt(i,16);return[s>>16&255,s>>8&255,255&s]}function r(t){const e=n(t);if(!e)return NaN;const[i,s,r]=e.map((t=>{const e=t/255;return e<=.04045?e/12.92:Math.pow((e+.055)/1.055,2.4)}));return.2126*i+.7152*s+.0722*r}function o(t,e){const i=r(t),s=r(e);return isNaN(i)||isNaN(s)?NaN:(Math.max(i,s)+.05)/(Math.min(i,s)+.05)}function a(t,e="light"){const i=o(t,"#000000"),s=o(t,"#ffffff");return"light"===e?i>1.35*s?"#000000":"#ffffff":1.35*i>s?"#000000":"#ffffff"}function c(t,e){const i=n(t);if(!i)return t;const[s,r,o]=i,a=t=>Math.max(0,Math.round(t*(1-e))).toString(16).padStart(2,"0");return`#${a(s)}${a(r)}${a(o)}`}function h(t,e){const i=n(t);if(!i)return t;const[s,r,o]=i,a=t=>Math.min(255,Math.round(t+(255-t)*e)).toString(16).padStart(2,"0");return`#${a(s)}${a(r)}${a(o)}`}function l(t,e,i=4.5){if(o(t,e)>=i)return t;const s=r(e)>.5;let n=t;for(let r=1;r<=10;r++)if(n=s?c(t,.1*r):h(t,.1*r),o(n,e)>=i)return n;return s?"#000000":"#ffffff"}function d(t,e){if(void 0!==e)return`${e}px`;switch(t){case"pill":return"9999px";case"rounded":return"8px";case"sharp":return"0";default:return}}function f(t){if(!t)return{};const e={};if(t.colors?.primary&&(e["--ah-color-primary"]=t.colors.primary,e["--ah-color-primary-hover"]=t.colors.primary),t.colors?.page_background){const i=t.colors.page_background;"solid"===i.type&&i.start?e["--ah-page-bg"]=i.start:"gradient"===i.type&&i.start&&i.end&&(e["--ah-page-bg"]=`linear-gradient(${i.angle_deg??180}deg, ${i.start}, ${i.end})`)}return t.logo_url&&(e["--ah-logo-url"]=`url(${t.logo_url})`),t.font?.url&&(e["--ah-font-url"]=t.font.url),e}function u(t){if(!t)return{};const e={};if(t.borders){const i=t.borders;void 0!==i.widget_corner_radius&&(e["--ah-widget-radius"]=`${i.widget_corner_radius}px`),void 0!==i.widget_border_weight&&(e["--ah-widget-border-width"]=`${i.widget_border_weight}px`),!1===i.show_widget_shadow&&(e["--ah-widget-shadow"]="none");const s=d(i.buttons_style,i.button_border_radius);s&&(e["--ah-btn-radius"]=s),void 0!==i.button_border_weight&&(e["--ah-btn-border-width"]=`${i.button_border_weight}px`);const n=d(i.inputs_style,i.input_border_radius);n&&(e["--ah-input-radius"]=n),void 0!==i.input_border_weight&&(e["--ah-input-border-width"]=`${i.input_border_weight}px`)}if(t.colors){const i=t.colors;if(i.primary_button)if(e["--ah-color-primary"]=i.primary_button,e["--ah-color-primary-hover"]=i.primary_button,i.primary_button_label&&o(i.primary_button_label,i.primary_button)>=4.5)e["--ah-color-text-on-primary"]=i.primary_button_label;else{e["--ah-color-text-on-primary"]=a(i.primary_button,"light");const t=a(i.primary_button,"dark");t!==e["--ah-color-text-on-primary"]&&(e["--ah-color-text-on-primary-dark"]=t)}else i.primary_button_label&&(e["--ah-color-text-on-primary"]=i.primary_button_label);i.secondary_button_border&&(e["--ah-btn-secondary-border"]=i.secondary_button_border),i.secondary_button_label&&(e["--ah-btn-secondary-text"]=i.secondary_button_label),i.body_text&&(e["--ah-color-text"]=i.body_text),i.header&&(e["--ah-color-text-header"]=i.header),i.input_labels_placeholders&&(e["--ah-color-text-label"]=i.input_labels_placeholders,e["--ah-color-text-muted"]=i.input_labels_placeholders),i.input_filled_text&&(e["--ah-color-input-text"]=i.input_filled_text),i.widget_background&&(e["--ah-color-bg"]=i.widget_background),i.input_background&&(e["--ah-color-input-bg"]=i.input_background),i.widget_border&&(e["--ah-widget-border-color"]=i.widget_border),i.input_border&&(e["--ah-color-border"]=i.input_border),i.links_focused_components&&(e["--ah-color-link"]=l(i.links_focused_components,i.widget_background||"#ffffff")),i.base_focus_color&&(e["--ah-color-focus-ring"]=i.base_focus_color),i.base_hover_color&&(e["--ah-color-primary-hover"]=i.base_hover_color),i.error&&(e["--ah-color-error"]=i.error),i.success&&(e["--ah-color-success"]=i.success),i.icons&&(e["--ah-color-icon"]=i.icons);const s=i.widget_background||"#ffffff",n=i.input_background||s,r=e["--ah-color-border"]||i.input_border||"#c9cace",c=o(r,s),h=o(r,n);Math.min(c,h)<3&&(e["--ah-color-border"]=l(r,c<h?s:n,3))}if(t.fonts){const i=t.fonts,s=i.reference_text_size||16,n=t=>t>=50?Math.round(t/100*s):t;i.font_url&&(e["--ah-font-url"]=i.font_url),i.reference_text_size&&(e["--ah-font-size-base"]=`${i.reference_text_size}px`),i.title?.size&&(e["--ah-font-size-title"]=`${n(i.title.size)}px`),i.subtitle?.size&&(e["--ah-font-size-subtitle"]=`${n(i.subtitle.size)}px`),i.body_text?.size&&(e["--ah-font-size-body"]=`${n(i.body_text.size)}px`),i.input_labels?.size&&(e["--ah-font-size-label"]=`${n(i.input_labels.size)}px`),i.buttons_text?.size&&(e["--ah-font-size-btn"]=`${n(i.buttons_text.size)}px`),i.links?.size&&(e["--ah-font-size-link"]=`${n(i.links.size)}px`),"underlined"===i.links_style&&(e["--ah-link-decoration"]="underline"),void 0!==i.title?.bold&&(e["--ah-font-weight-title"]=i.title.bold?"700":"400"),void 0!==i.subtitle?.bold&&(e["--ah-font-weight-subtitle"]=i.subtitle.bold?"700":"400"),void 0!==i.body_text?.bold&&(e["--ah-font-weight-body"]=i.body_text.bold?"700":"400"),void 0!==i.input_labels?.bold&&(e["--ah-font-weight-label"]=i.input_labels.bold?"700":"400"),void 0!==i.buttons_text?.bold&&(e["--ah-font-weight-btn"]=i.buttons_text.bold?"600":"400"),void 0!==i.links?.bold&&(e["--ah-font-weight-link"]=i.links.bold?"700":"400")}if(t.widget){const i=t.widget;if(i.header_text_alignment&&(e["--ah-title-align"]=i.header_text_alignment),i.logo_height&&(e["--ah-logo-height"]=`${i.logo_height}px`),i.logo_position){const t={center:"center",left:"flex-start",right:"flex-end"};"none"===i.logo_position?e["--ah-logo-display"]="none":e["--ah-logo-align"]=t[i.logo_position]??"center"}i.social_buttons_layout&&("top"===i.social_buttons_layout?(e["--ah-social-order"]="0",e["--ah-divider-order"]="1",e["--ah-fields-order"]="2"):(e["--ah-social-order"]="2",e["--ah-divider-order"]="1",e["--ah-fields-order"]="0"))}if(t.page_background){const i=t.page_background;i.background_color&&(e["--ah-page-bg"]=i.background_color),i.background_image_url&&(e["--ah-page-bg-image"]=`url(${i.background_image_url})`)}return e}const p={br:[],em:[],i:[],strong:[],b:[],u:[],span:["class"],a:["href","class"]};function g(t){if(!t)return"";if(!t.includes("<"))return t;let e=t;e=e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;");for(const[t,i]of Object.entries(p)){if("br"===t){e=e.replace(/&lt;br\s*\/?&gt;/gi,"<br>");continue}const s=new RegExp(`&lt;${t}((?:\\s+[a-z-]+(?:=&quot;[^&]*&quot;|=&#39;[^&]*&#39;)?)*)\\s*&gt;`,"gi");e=e.replace(s,((e,s)=>{const n=[];if(s){const t=s.replace(/&quot;/g,'"').replace(/&#39;/g,"'").replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">"),e=/([a-z-]+)=["']([^"']*)["']/gi;let r;for(;null!==(r=e.exec(t));){const[,t,e]=r;t&&i.includes(t.toLowerCase())&&("href"===t.toLowerCase()?m(e||"")&&n.push(`${t}="${x(e||"")}"`):n.push(`${t}="${x(e||"")}"`))}}"a"===t&&(n.push('target="_blank"'),n.push('rel="noopener noreferrer"'));const r=n.length?" "+n.join(" "):"";return`<${t}${r}>`}));const n=new RegExp(`&lt;/${t}&gt;`,"gi");e=e.replace(n,`</${t}>`)}return e}function m(t){if(!t)return!1;if(t.startsWith("/")||t.startsWith("#")||t.startsWith("?"))return!0;try{const e=new URL(t,"https://example.com");return"http:"===e.protocol||"https:"===e.protocol}catch{return!1}}function x(t){return t.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}const w=class{constructor(i){t(this,i),this.formSubmit=e(this,"formSubmit"),this.buttonClick=e(this,"buttonClick"),this.linkClick=e(this,"linkClick"),this.navigate=e(this,"navigate"),this.flowComplete=e(this,"flowComplete"),this.flowError=e(this,"flowError"),this.screenChange=e(this,"screenChange")}get el(){return i(this)}screen;apiUrl;baseUrl;state;screenId;watchScreenId(){this.updateDataScreenAttribute()}authParams;statePersistence="memory";storageKey="authhero_widget";branding;theme;loading=!1;autoSubmit=!1;autoNavigate;_screen;_authParams;_branding;_theme;formData={};formSubmit;buttonClick;linkClick;navigate;flowComplete;flowError;screenChange;watchScreen(t){if("string"==typeof t)try{this._screen=JSON.parse(t)}catch{console.error("Failed to parse screen JSON")}else this._screen=t;this._screen&&(this.formData={},this.initFormDataFromDefaults(this._screen),this.screenChange.emit(this._screen),this.updateDataScreenAttribute())}initFormDataFromDefaults(t){const e={};for(const i of t.components||[])if("config"in i&&i.config&&"default_value"in i.config&&i.config.default_value){const t=i.config.default_value;"string"==typeof t&&""!==t&&(e[i.id]=t)}Object.keys(e).length>0&&(this.formData={...e,...this.formData})}updateDataScreenAttribute(){const t=this._screen?.name||this.screenId;t?this.el.setAttribute("data-screen",t):this.el.removeAttribute("data-screen");const e=this.el.closest("[data-authhero-widget-container]");e&&(t?e.setAttribute("data-screen",t):e.removeAttribute("data-screen"))}watchBranding(t){if("string"==typeof t)try{this._branding=JSON.parse(t)}catch{console.error("Failed to parse branding JSON")}else this._branding=t;this.applyThemeStyles()}watchTheme(t){if("string"==typeof t)try{this._theme=JSON.parse(t)}catch{console.error("Failed to parse theme JSON")}else this._theme=t;this.applyThemeStyles()}watchAuthParams(t){if("string"==typeof t)try{this._authParams=JSON.parse(t)}catch{console.error("Failed to parse authParams JSON")}else this._authParams=t}applyThemeStyles(){const t=(e=this._theme,{...f(this._branding),...u(e)});var e;!function(t,e){Object.entries(e).forEach((([e,i])=>{t.style.setProperty(e,i)}))}(this.el,t)}focusFirstInput(){requestAnimationFrame((()=>{const t=this.el.shadowRoot;if(!t)return;const e=t.querySelectorAll("authhero-node");for(const t of Array.from(e)){const e=t.shadowRoot;if(e){const t=e.querySelector('input:not([type="hidden"]):not([type="checkbox"]):not([disabled]), textarea:not([disabled])');if(t)return void t.focus()}}}))}get shouldAutoNavigate(){return this.autoNavigate??this.autoSubmit}buildUrl(t){return this.baseUrl?new URL(t,this.baseUrl).toString():t}loadPersistedState(){if("url"===this.statePersistence){const t=new URL(window.location.href).searchParams.get("state");t&&!this.state&&(this.state=t)}else if("session"===this.statePersistence)try{const t=sessionStorage.getItem(`${this.storageKey}_state`);t&&!this.state&&(this.state=t);const e=sessionStorage.getItem(`${this.storageKey}_screenId`);e&&!this.screenId&&(this.screenId=e)}catch{}}persistState(){if("url"===this.statePersistence){const t=new URL(window.location.href);this.state&&t.searchParams.set("state",this.state),this.screenId&&t.searchParams.set("screen",this.screenId),window.history.replaceState({},"",t.toString())}else if("session"===this.statePersistence)try{this.state&&sessionStorage.setItem(`${this.storageKey}_state`,this.state),this.screenId&&sessionStorage.setItem(`${this.storageKey}_screenId`,this.screenId)}catch{}}handlePopState=t=>{if(!this.apiUrl)return;t.state?.state&&(this.state=t.state.state);const e=t.state?.screen??this.extractScreenIdFromHref(location.href);e&&this.fetchScreen(e)};connectedCallback(){window.addEventListener("popstate",this.handlePopState)}disconnectedCallback(){window.removeEventListener("popstate",this.handlePopState)}async componentWillLoad(){if(!this._screen){const t=this.screen||this.el?.getAttribute("screen");t&&this.watchScreen(t)}this._branding||this.watchBranding(this.branding),this._theme||this.watchTheme(this.theme),this._authParams||this.watchAuthParams(this.authParams),this.loadPersistedState(),this.apiUrl&&!this._screen&&await this.fetchScreen(this.screenId)}async fetchScreen(t,e){if(!this.apiUrl)return!1;const i=t||this.screenId;let s=this.apiUrl;i&&s.includes("{screenId}")&&(s=s.replace("{screenId}",encodeURIComponent(i)));const n=new URL(s,this.baseUrl||window.location.origin);this.state&&n.searchParams.set("state",this.state),e&&n.searchParams.set("nodeId",e),this.loading=!0;try{const t=await fetch(this.buildUrl(n.pathname+n.search),{credentials:"include",headers:{Accept:"application/json"}});if(t.ok){const e=await t.json();if(e.screen?(this._screen=e.screen,e.branding&&(this._branding=e.branding,this.applyThemeStyles()),e.state&&(this.state=e.state),e.screenId&&(this.screenId=e.screenId)):this._screen=e,this._screen)return i&&i!==this.screenId&&(this.screenId=i),this.initFormDataFromDefaults(this._screen),this.screenChange.emit(this._screen),this.updateDataScreenAttribute(),this.persistState(),this.focusFirstInput(),!0}else{const e=await t.json().catch((()=>({message:"Failed to load screen"})));this.flowError.emit({message:e.message||"Failed to load screen"})}}catch(t){console.error("Failed to fetch screen:",t),this.flowError.emit({message:t instanceof Error?t.message:"Failed to fetch screen"})}finally{this.loading=!1}return!1}handleInputChange=(t,e)=>{this.formData={...this.formData,[t]:e}};handleSubmit=async(t,e)=>{if(t.preventDefault(),!this._screen||this.loading)return;let i={...this.formData,...e||{}};const s=this.el.shadowRoot?.querySelector("form");if(s&&s.querySelectorAll('input[type="hidden"]').forEach((t=>{t.name&&t.value&&(i[t.name]=t.value)})),this.formSubmit.emit({screen:this._screen,data:i}),this.autoSubmit)if(this._screen.method&&"GET"!==this._screen.method.toUpperCase()){this.loading=!0;try{const t=await fetch(this.buildUrl(this._screen.action),{method:this._screen.method,credentials:"include",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({data:i})}),e=t.headers.get("content-type");if(e?.includes("application/json")){const e=await t.json();e.redirect?(this.flowComplete.emit({redirectUrl:e.redirect}),this.navigate.emit({url:e.redirect}),this.shouldAutoNavigate&&(window.location.href=e.redirect)):!t.ok&&e.screen?(this._screen=e.screen,this.initFormDataFromDefaults(e.screen),this.screenChange.emit(e.screen),this.updateDataScreenAttribute(),this.focusFirstInput()):e.screen?(this._screen=e.screen,this.formData={},this.initFormDataFromDefaults(e.screen),this.screenChange.emit(e.screen),this.updateDataScreenAttribute(),e.screenId&&(this.screenId=e.screenId),this.persistState(),e.navigateUrl&&this.shouldAutoNavigate&&window.history.pushState({screen:e.screenId,state:this.state},"",e.navigateUrl),e.branding&&(this._branding=e.branding,this.applyThemeStyles()),e.state&&(this.state=e.state,this.persistState()),e.ceremony&&this.performWebAuthnCeremony(e.ceremony),this.focusFirstInput()):e.complete?this.flowComplete.emit({}):!t.ok&&e.error&&(this._screen&&(this._screen={...this._screen,messages:[...this._screen.messages||[],{text:e.error,type:"error"}]}),this.flowError.emit({message:e.error}))}}catch(t){console.error("Form submission failed:",t),this.flowError.emit({message:t instanceof Error?t.message:"Form submission failed"})}finally{this.loading=!1}}else window.location.href=this.buildUrl(this._screen.action)};overrideFormSubmit(){const t=this.el.shadowRoot;if(!t)return;const e=t.querySelector("form");e&&(e.submit=()=>{const t=new FormData(e),i={};t.forEach(((t,e)=>{"string"==typeof t&&(i[e]=t)})),this.handleSubmit({preventDefault:()=>{}},i)})}performWebAuthnCeremony(t){this.isValidWebAuthnCeremony(t)?requestAnimationFrame((()=>{this.overrideFormSubmit(),this.executeWebAuthnRegistration(t)})):console.error("Invalid WebAuthn ceremony payload",t)}isValidWebAuthnCeremony(t){if("object"!=typeof t||null===t)return!1;if("webauthn-registration"!==t.type)return!1;if("string"!=typeof t.successAction)return!1;const e=t.options;if("object"!=typeof e||null===e)return!1;if("string"!=typeof e.challenge)return!1;const i=e.rp;if("object"!=typeof i||null===i)return!1;if("string"!=typeof i.id||"string"!=typeof i.name)return!1;const s=e.user;return"object"==typeof s&&null!==s&&("string"==typeof s.id&&"string"==typeof s.name&&"string"==typeof s.displayName&&!!Array.isArray(e.pubKeyCredParams))}async executeWebAuthnRegistration(t){const e=t.options,i=t=>{for(t=t.replace(/-/g,"+").replace(/_/g,"/");t.length%4;)t+="=";const e=atob(t),i=new Uint8Array(e.length);for(let t=0;t<e.length;t++)i[t]=e.charCodeAt(t);return i.buffer},s=t=>{const e=new Uint8Array(t);let i="";for(let t=0;t<e.length;t++)i+=String.fromCharCode(e[t]);return btoa(i).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")},n=()=>{const t=this.el?.shadowRoot;if(t){const e=t.querySelector("form");if(e)return e}return document.querySelector("form")};try{const r={challenge:i(e.challenge),rp:{id:e.rp.id,name:e.rp.name},user:{id:i(e.user.id),name:e.user.name,displayName:e.user.displayName},pubKeyCredParams:e.pubKeyCredParams.map((t=>({alg:t.alg,type:t.type}))),timeout:e.timeout,attestation:e.attestation||"none",authenticatorSelection:e.authenticatorSelection?{residentKey:e.authenticatorSelection.residentKey||"preferred",userVerification:e.authenticatorSelection.userVerification||"preferred"}:void 0};e.excludeCredentials?.length&&(r.excludeCredentials=e.excludeCredentials.map((t=>({id:i(t.id),type:t.type,transports:t.transports||[]}))));const o=await navigator.credentials.create({publicKey:r}),a=o.response,c={id:o.id,rawId:s(o.rawId),type:o.type,response:{attestationObject:s(a.attestationObject),clientDataJSON:s(a.clientDataJSON)},clientExtensionResults:o.getClientExtensionResults(),authenticatorAttachment:o.authenticatorAttachment||void 0};"function"==typeof a.getTransports&&(c.response.transports=a.getTransports());const h=n();if(h){const e=h.querySelector('[name="credential-field"]')||h.querySelector("#credential-field"),i=h.querySelector('[name="action-field"]')||h.querySelector("#action-field");e&&(e.value=JSON.stringify(c)),i&&(i.value=t.successAction),h.submit()}}catch(t){console.error("WebAuthn registration error:",t);const e=n();if(e){const t=e.querySelector('[name="action-field"]')||e.querySelector("#action-field");t&&(t.value="error"),e.submit()}}}handleButtonClick=t=>{if("submit"!==t.type)if(this.buttonClick.emit(t),"SOCIAL"===t.type&&t.value&&this.shouldAutoNavigate){const e=this.getProviderHref(t.value);if(e){const t=this.extractScreenIdFromHref(e);return void(t&&this.apiUrl?this.navigateToScreen(t,e):window.location.href=e)}this.handleSocialLogin(t.value)}else"RESEND_BUTTON"===t.type&&this.shouldAutoNavigate&&this.handleResend();else{if((!this._screen?.method||"GET"===this._screen.method.toUpperCase())&&this._screen?.action)return void(window.location.href=this.buildUrl(this._screen.action));const e={...this.formData,[t.id]:"true"};this.formData=e,this.handleSubmit({preventDefault:()=>{}},e)}};handleSocialLogin(t){const e=this._authParams||{},i={connection:t};this.state?i.state=this.state:e.state&&(i.state=e.state),e.client_id&&(i.client_id=e.client_id),e.redirect_uri&&(i.redirect_uri=e.redirect_uri),e.scope&&(i.scope=e.scope),e.audience&&(i.audience=e.audience),e.nonce&&(i.nonce=e.nonce),e.response_type&&(i.response_type=e.response_type);const s=this.buildUrl("/authorize?"+new URLSearchParams(i).toString());this.navigate.emit({url:s}),window.location.href=s}async handleResend(){if(this._screen?.action)try{const t=this._screen.action+(this._screen.action.includes("?")?"&":"?")+"action=resend";await fetch(this.buildUrl(t),{method:"POST",credentials:"include"})}catch(t){console.error("Resend failed:",t)}}extractScreenIdFromHref(t){try{const e=new URL(t,window.location.origin).pathname,i=e.match(/\/u2\/login\/([^/]+)$/);if(i)return i[1];const s=e.match(/\/u2\/([^/]+)$/);return s&&"login"!==s[1]&&"screen"!==s[1]?s[1]:null}catch{return null}}handleLinkClick=(t,e)=>{if(this.linkClick.emit({id:e.id,href:e.href,text:e.text}),!this.shouldAutoNavigate)return void t.preventDefault();const i=this.extractScreenIdFromHref(e.href);return i&&this.apiUrl?(t.preventDefault(),void this.navigateToScreen(i,e.href)):void 0};async navigateToScreen(t,e){await this.fetchScreen(t)?window.history.pushState({screen:t,state:this.state},"",e):window.location.href=e}getProviderHref(t){if(!this._screen)return null;for(const e of this._screen.components){const i=e;if("SOCIAL"===i.type&&i.config?.provider_details){const e=i.config.provider_details.find((e=>e.name===t));if(e?.href)return e.href}}return null}isSocialComponent(t){return"SOCIAL"===t.type}isDividerComponent(t){return"DIVIDER"===t.type}render(){const t=this._screen;if(this.loading&&!t)return s("div",{class:"widget-container"},s("div",{class:"loading-spinner"}));if(!t)return s("div",{class:"widget-container"},s("div",{class:"error-message"},"No screen configuration provided"));const e=t.messages?.filter((t=>"error"===t.type))||[],i=t.messages?.filter((t=>"success"===t.type))||[],n=[...t.components??[]],r=n.filter((t=>!1!==t.visible)).sort(((t,e)=>(t.order??0)-(e.order??0))),o=n.filter((t=>!1===t.visible)),a=r.filter((t=>this.isSocialComponent(t))),c=r.filter((t=>!this.isSocialComponent(t)&&!this.isDividerComponent(t))),h=r.find((t=>this.isDividerComponent(t))),l=!!h,d=h?.config?.text||"Or",f=t=>{const e=t.config;return["social-buttons","button","button-secondary","button-social","button-social-content","button-social-text","button-social-subtitle","social-icon",...(e?.providers??[]).flatMap((t=>{const e=t.replace(/[^a-zA-Z0-9-]/g,"-");return[`button-social-${e}`,`button-social-content-${e}`,`button-social-text-${e}`,`button-social-subtitle-${e}`,`social-icon-${e}`]}))].join(", ")},u=this._theme?.widget?.logo_url||this._branding?.logo_url;return s("div",{class:"widget-container",part:"container","data-authstack-container":!0},s("header",{class:"widget-header",part:"header"},u&&s("div",{class:"logo-wrapper",part:"logo-wrapper"},s("img",{class:"logo",part:"logo",src:u,alt:"Logo"})),t.title&&s("h1",{class:"title",part:"title",innerHTML:g(t.title)}),t.description&&s("p",{class:"description",part:"description",innerHTML:g(t.description)})),s("div",{class:"widget-body",part:"body"},e.map((t=>s("div",{class:"message message-error",part:"message message-error",key:t.id??t.text},t.text))),i.map((t=>s("div",{class:"message message-success",part:"message message-success",key:t.id??t.text},t.text))),s("form",{onSubmit:this.handleSubmit,part:"form"},o.map((t=>s("input",{type:"hidden",name:t.id,id:t.id,key:t.id,value:this.formData[t.id]||""}))),s("div",{class:"form-content"},a.length>0&&s("div",{class:"social-section",part:"social-section"},a.map((t=>s("authhero-node",{key:t.id,component:t,value:this.formData[t.id],onFieldChange:t=>this.handleInputChange(t.detail.id,t.detail.value),onButtonClick:t=>this.handleButtonClick(t.detail),disabled:this.loading,exportparts:f(t)})))),a.length>0&&c.length>0&&l&&s("div",{class:"divider",part:"divider"},s("span",{class:"divider-text"},d)),s("div",{class:"fields-section",part:"fields-section"},c.map((t=>s("authhero-node",{key:t.id,component:t,value:this.formData[t.id],onFieldChange:t=>this.handleInputChange(t.detail.id,t.detail.value),onButtonClick:t=>this.handleButtonClick(t.detail),disabled:this.loading})))))),t.links&&t.links.length>0&&s("div",{class:"links",part:"links"},t.links.map((t=>s("span",{class:"link-wrapper",part:"link-wrapper",key:t.id??t.href},t.linkText?s("span",null,t.text," ",s("a",{href:t.href,class:"link",part:"link",onClick:e=>this.handleLinkClick(e,{id:t.id,href:t.href,text:t.linkText||t.text})},t.linkText)):s("a",{href:t.href,class:"link",part:"link",onClick:e=>this.handleLinkClick(e,{id:t.id,href:t.href,text:t.text})},t.text))))),t.footer&&s("div",{class:"widget-footer",part:"footer",innerHTML:g(t.footer)})))}static get watchers(){return{screenId:[{watchScreenId:0}],screen:[{watchScreen:0}],branding:[{watchBranding:0}],theme:[{watchTheme:0}],authParams:[{watchAuthParams:0}]}}};w.style=":host{display:block;font-family:var(--ah-font-family, 'ulp-font', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, sans-serif);font-size:var(--ah-font-size-base, 16px);line-height:var(--ah-line-height-base, 1.5);color:var(--ah-color-text, #1e212a);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.widget-container{width:var(--ah-widget-max-width, 400px);margin:0 auto;background-color:var(--ah-color-bg, #ffffff);border-radius:var(--ah-widget-radius, 5px);box-shadow:var(--ah-widget-shadow, 0 4px 22px 0 rgba(0, 0, 0, 0.11));box-sizing:border-box}.widget-header{padding:var(--ah-header-padding, 40px 48px 24px)}.widget-body{padding:var(--ah-body-padding, 0 48px 40px)}.logo-wrapper{display:var(--ah-logo-display, flex);justify-content:var(--ah-logo-align, center);margin-bottom:8px}.logo{display:block;height:var(--ah-logo-height, 52px);max-width:100%;width:auto;object-fit:contain}.title{font-size:var(--ah-font-size-title, 24px);font-weight:var(--ah-font-weight-title, 700);text-align:var(--ah-title-align, center);margin:var(--ah-title-margin, 24px 0 24px);color:var(--ah-color-header, #1e212a);line-height:1.2}.description{font-size:var(--ah-font-size-subtitle, 16px);text-align:var(--ah-title-align, center);margin:var(--ah-description-margin, 0 0 8px);color:var(--ah-color-text, #1e212a);line-height:1.5}.message{padding:12px 16px;border-radius:4px;margin-bottom:16px;font-size:14px;line-height:1.5}.message-error{background-color:var(--ah-color-error-bg, #ffeaea);color:var(--ah-color-error, #d03c38);border-left:3px solid var(--ah-color-error, #d03c38)}.message-success{background-color:var(--ah-color-success-bg, #e6f9f1);color:var(--ah-color-success, #13a769);border-left:3px solid var(--ah-color-success, #13a769)}form{display:flex;flex-direction:column}.form-content{display:flex;flex-direction:column}.social-section{display:flex;flex-direction:column;gap:8px;order:var(--ah-social-order, 2)}.fields-section{display:flex;flex-direction:column;order:var(--ah-fields-order, 0)}.divider{display:flex;align-items:center;text-align:center;margin:16px 0;order:var(--ah-divider-order, 1)}.divider::before,.divider::after{content:'';flex:1;border-bottom:1px solid var(--ah-color-border-muted, #c9cace)}.divider-text{padding:0 10px;font-size:12px;font-weight:400;color:var(--ah-color-text-muted, #65676e);text-transform:uppercase;letter-spacing:0}.links{display:flex;flex-direction:column;align-items:center;gap:8px;margin-top:16px}.link-wrapper{font-size:14px;color:var(--ah-color-text, #1e212a)}.link{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:14px;font-weight:var(--ah-font-weight-link, 400);transition:color 150ms ease}.link:hover{text-decoration:underline}.link:focus-visible{outline:2px solid var(--ah-color-link, #635dff);outline-offset:2px;border-radius:2px}.widget-footer{margin-top:16px;text-align:center;font-size:12px;color:var(--ah-color-text-muted, #65676e)}.widget-footer a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:12px;transition:color 150ms ease}.widget-footer a:hover{text-decoration:underline}.widget-footer a:focus-visible{outline:2px solid var(--ah-color-link, #635dff);outline-offset:2px;border-radius:2px}.loading-spinner{width:32px;height:32px;margin:24px auto;border:3px solid var(--ah-color-border-muted, #e0e1e3);border-top-color:var(--ah-color-primary, #635dff);border-radius:50%;animation:spin 0.8s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.error-message{text-align:center;color:var(--ah-color-error, #d03c38);padding:16px;font-size:14px}@media (max-width: 480px){:host{display:block;width:100%;min-height:100vh;background-color:var(--ah-color-bg, #ffffff)}.widget-container{box-shadow:none;border-radius:0;width:100%;margin:0}.widget-header{padding:24px 16px 16px}.widget-body{padding:0 16px 24px}}";export{w as authhero_widget}
@@ -1021,7 +1021,18 @@ const AuthheroWidget = class {
1021
1021
  e.preventDefault();
1022
1022
  if (!this._screen || this.loading)
1023
1023
  return;
1024
- const submitData = overrideData || this.formData;
1024
+ let submitData = { ...this.formData, ...(overrideData || {}) };
1025
+ // Merge hidden input values from DOM (may have been set programmatically
1026
+ // by inline scripts, e.g. passkey management buttons)
1027
+ const form = this.el.shadowRoot?.querySelector("form");
1028
+ if (form) {
1029
+ const hiddenInputs = form.querySelectorAll('input[type="hidden"]');
1030
+ hiddenInputs.forEach((input) => {
1031
+ if (input.name && input.value) {
1032
+ submitData[input.name] = input.value;
1033
+ }
1034
+ });
1035
+ }
1025
1036
  // Always emit the submit event
1026
1037
  this.formSubmit.emit({
1027
1038
  screen: this._screen,
@@ -1097,6 +1108,10 @@ const AuthheroWidget = class {
1097
1108
  this.state = result.state;
1098
1109
  this.persistState();
1099
1110
  }
1111
+ // Perform WebAuthn ceremony if present (structured data, not script)
1112
+ if (result.ceremony) {
1113
+ this.performWebAuthnCeremony(result.ceremony);
1114
+ }
1100
1115
  // Focus first input on new screen
1101
1116
  this.focusFirstInput();
1102
1117
  }
@@ -1130,6 +1145,192 @@ const AuthheroWidget = class {
1130
1145
  this.loading = false;
1131
1146
  }
1132
1147
  };
1148
+ /**
1149
+ * Override form.submit() so that scripts (e.g. WebAuthn ceremony) that call
1150
+ * form.submit() go through the widget's JSON fetch pipeline instead of a
1151
+ * native form-urlencoded POST.
1152
+ */
1153
+ overrideFormSubmit() {
1154
+ const shadowRoot = this.el.shadowRoot;
1155
+ if (!shadowRoot)
1156
+ return;
1157
+ const form = shadowRoot.querySelector("form");
1158
+ if (!form)
1159
+ return;
1160
+ form.submit = () => {
1161
+ const formData = new FormData(form);
1162
+ const data = {};
1163
+ formData.forEach((value, key) => {
1164
+ if (typeof value === "string") {
1165
+ data[key] = value;
1166
+ }
1167
+ });
1168
+ const syntheticEvent = { preventDefault: () => { } };
1169
+ this.handleSubmit(syntheticEvent, data);
1170
+ };
1171
+ }
1172
+ /**
1173
+ * Validate and execute a structured WebAuthn ceremony returned by the server.
1174
+ * Instead of injecting arbitrary script content, this parses the ceremony JSON,
1175
+ * validates the expected fields, and calls the WebAuthn API natively.
1176
+ */
1177
+ performWebAuthnCeremony(ceremony) {
1178
+ if (!this.isValidWebAuthnCeremony(ceremony)) {
1179
+ console.error("Invalid WebAuthn ceremony payload", ceremony);
1180
+ return;
1181
+ }
1182
+ requestAnimationFrame(() => {
1183
+ this.overrideFormSubmit();
1184
+ this.executeWebAuthnRegistration(ceremony);
1185
+ });
1186
+ }
1187
+ /**
1188
+ * Schema validation for WebAuthn ceremony payloads.
1189
+ * Checks required fields and types before invoking browser APIs.
1190
+ */
1191
+ isValidWebAuthnCeremony(data) {
1192
+ if (typeof data !== "object" || data === null)
1193
+ return false;
1194
+ const obj = data;
1195
+ if (obj.type !== "webauthn-registration")
1196
+ return false;
1197
+ if (typeof obj.successAction !== "string")
1198
+ return false;
1199
+ const opts = obj.options;
1200
+ if (typeof opts !== "object" || opts === null)
1201
+ return false;
1202
+ const o = opts;
1203
+ if (typeof o.challenge !== "string")
1204
+ return false;
1205
+ const rp = o.rp;
1206
+ if (typeof rp !== "object" || rp === null)
1207
+ return false;
1208
+ if (typeof rp.id !== "string" ||
1209
+ typeof rp.name !== "string")
1210
+ return false;
1211
+ const user = o.user;
1212
+ if (typeof user !== "object" || user === null)
1213
+ return false;
1214
+ const u = user;
1215
+ if (typeof u.id !== "string" ||
1216
+ typeof u.name !== "string" ||
1217
+ typeof u.displayName !== "string")
1218
+ return false;
1219
+ if (!Array.isArray(o.pubKeyCredParams))
1220
+ return false;
1221
+ return true;
1222
+ }
1223
+ /**
1224
+ * Perform the WebAuthn navigator.credentials.create() ceremony and submit
1225
+ * the credential result via the form.
1226
+ */
1227
+ async executeWebAuthnRegistration(ceremony) {
1228
+ const opts = ceremony.options;
1229
+ const b64uToBuf = (s) => {
1230
+ s = s.replace(/-/g, "+").replace(/_/g, "/");
1231
+ while (s.length % 4)
1232
+ s += "=";
1233
+ const b = atob(s);
1234
+ const a = new Uint8Array(b.length);
1235
+ for (let i = 0; i < b.length; i++)
1236
+ a[i] = b.charCodeAt(i);
1237
+ return a.buffer;
1238
+ };
1239
+ const bufToB64u = (b) => {
1240
+ const a = new Uint8Array(b);
1241
+ let s = "";
1242
+ for (let i = 0; i < a.length; i++)
1243
+ s += String.fromCharCode(a[i]);
1244
+ return btoa(s)
1245
+ .replace(/\+/g, "-")
1246
+ .replace(/\//g, "_")
1247
+ .replace(/=+$/, "");
1248
+ };
1249
+ const findForm = () => {
1250
+ const shadowRoot = this.el?.shadowRoot;
1251
+ if (shadowRoot) {
1252
+ const f = shadowRoot.querySelector("form");
1253
+ if (f)
1254
+ return f;
1255
+ }
1256
+ return document.querySelector("form");
1257
+ };
1258
+ try {
1259
+ const publicKey = {
1260
+ challenge: b64uToBuf(opts.challenge),
1261
+ rp: { id: opts.rp.id, name: opts.rp.name },
1262
+ user: {
1263
+ id: b64uToBuf(opts.user.id),
1264
+ name: opts.user.name,
1265
+ displayName: opts.user.displayName,
1266
+ },
1267
+ pubKeyCredParams: opts.pubKeyCredParams.map((p) => ({
1268
+ alg: p.alg,
1269
+ type: p.type,
1270
+ })),
1271
+ timeout: opts.timeout,
1272
+ attestation: (opts.attestation || "none"),
1273
+ authenticatorSelection: opts.authenticatorSelection
1274
+ ? {
1275
+ residentKey: (opts.authenticatorSelection.residentKey ||
1276
+ "preferred"),
1277
+ userVerification: (opts.authenticatorSelection
1278
+ .userVerification ||
1279
+ "preferred"),
1280
+ }
1281
+ : undefined,
1282
+ };
1283
+ if (opts.excludeCredentials?.length) {
1284
+ publicKey.excludeCredentials = opts.excludeCredentials.map((c) => ({
1285
+ id: b64uToBuf(c.id),
1286
+ type: c.type,
1287
+ transports: (c.transports || []),
1288
+ }));
1289
+ }
1290
+ const cred = (await navigator.credentials.create({
1291
+ publicKey,
1292
+ }));
1293
+ const response = cred.response;
1294
+ const resp = {
1295
+ id: cred.id,
1296
+ rawId: bufToB64u(cred.rawId),
1297
+ type: cred.type,
1298
+ response: {
1299
+ attestationObject: bufToB64u(response.attestationObject),
1300
+ clientDataJSON: bufToB64u(response.clientDataJSON),
1301
+ },
1302
+ clientExtensionResults: cred.getClientExtensionResults(),
1303
+ authenticatorAttachment: cred.authenticatorAttachment || undefined,
1304
+ };
1305
+ if (typeof response.getTransports === "function") {
1306
+ resp.response.transports =
1307
+ response.getTransports();
1308
+ }
1309
+ const form = findForm();
1310
+ if (form) {
1311
+ const cf = form.querySelector('[name="credential-field"]') ||
1312
+ form.querySelector("#credential-field");
1313
+ const af = form.querySelector('[name="action-field"]') ||
1314
+ form.querySelector("#action-field");
1315
+ if (cf)
1316
+ cf.value = JSON.stringify(resp);
1317
+ if (af)
1318
+ af.value = ceremony.successAction;
1319
+ form.submit();
1320
+ }
1321
+ }
1322
+ catch (e) {
1323
+ console.error("WebAuthn registration error:", e);
1324
+ const form = findForm();
1325
+ if (form) {
1326
+ const af = form.querySelector('[name="action-field"]') ||
1327
+ form.querySelector("#action-field");
1328
+ if (af)
1329
+ af.value = "error";
1330
+ form.submit();
1331
+ }
1332
+ }
1333
+ }
1133
1334
  handleButtonClick = (detail) => {
1134
1335
  // If this is a submit button click, trigger form submission
1135
1336
  if (detail.type === "submit") {
@@ -1332,9 +1533,11 @@ const AuthheroWidget = class {
1332
1533
  // Use the local screen variable for all rendering
1333
1534
  const screenErrors = screen.messages?.filter((m) => m.type === "error") || [];
1334
1535
  const screenSuccesses = screen.messages?.filter((m) => m.type === "success") || [];
1335
- const components = [...(screen.components ?? [])]
1536
+ const allComponents = [...(screen.components ?? [])];
1537
+ const components = allComponents
1336
1538
  .filter((c) => c.visible !== false)
1337
1539
  .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
1540
+ const hiddenComponents = allComponents.filter((c) => c.visible === false);
1338
1541
  // Separate social, divider, and field components for layout ordering
1339
1542
  const socialComponents = components.filter((c) => this.isSocialComponent(c));
1340
1543
  const fieldComponents = components.filter((c) => !this.isSocialComponent(c) && !this.isDividerComponent(c));
@@ -1369,7 +1572,7 @@ const AuthheroWidget = class {
1369
1572
  };
1370
1573
  // Get logo URL from theme.widget (takes precedence) or branding
1371
1574
  const logoUrl = this._theme?.widget?.logo_url || this._branding?.logo_url;
1372
- return (index.h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, index.h("header", { class: "widget-header", part: "header" }, logoUrl && (index.h("div", { class: "logo-wrapper", part: "logo-wrapper" }, index.h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (index.h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (index.h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), index.h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (index.h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (index.h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), index.h("form", { onSubmit: this.handleSubmit, part: "form" }, index.h("div", { class: "form-content" }, socialComponents.length > 0 && (index.h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (index.h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
1575
+ return (index.h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, index.h("header", { class: "widget-header", part: "header" }, logoUrl && (index.h("div", { class: "logo-wrapper", part: "logo-wrapper" }, index.h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (index.h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (index.h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), index.h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (index.h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (index.h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), index.h("form", { onSubmit: this.handleSubmit, part: "form" }, hiddenComponents.map((c) => (index.h("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), index.h("div", { class: "form-content" }, socialComponents.length > 0 && (index.h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (index.h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
1373
1576
  fieldComponents.length > 0 &&
1374
1577
  hasDivider && (index.h("div", { class: "divider", part: "divider" }, index.h("span", { class: "divider-text" }, dividerText))), index.h("div", { class: "fields-section", part: "fields-section" }, fieldComponents.map((component) => (index.h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading })))))), screen.links && screen.links.length > 0 && (index.h("div", { class: "links", part: "links" }, screen.links.map((link) => (index.h("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (index.h("span", null, link.text, " ", index.h("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
1375
1578
  id: link.id,
@@ -1582,20 +1582,38 @@ var decodeURIComponent_ = decodeURIComponent;
1582
1582
  // src/utils/cookie.ts
1583
1583
  var validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/;
1584
1584
  var validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/;
1585
+ var trimCookieWhitespace = (value) => {
1586
+ let start = 0;
1587
+ let end = value.length;
1588
+ while (start < end) {
1589
+ const charCode = value.charCodeAt(start);
1590
+ if (charCode !== 32 && charCode !== 9) {
1591
+ break;
1592
+ }
1593
+ start++;
1594
+ }
1595
+ while (end > start) {
1596
+ const charCode = value.charCodeAt(end - 1);
1597
+ if (charCode !== 32 && charCode !== 9) {
1598
+ break;
1599
+ }
1600
+ end--;
1601
+ }
1602
+ return start === 0 && end === value.length ? value : value.slice(start, end);
1603
+ };
1585
1604
  var parse = (cookie, name) => {
1586
- const pairs = cookie.trim().split(";");
1605
+ const pairs = cookie.split(";");
1587
1606
  const parsedCookie = {};
1588
- for (let pairStr of pairs) {
1589
- pairStr = pairStr.trim();
1607
+ for (const pairStr of pairs) {
1590
1608
  const valueStartPos = pairStr.indexOf("=");
1591
1609
  if (valueStartPos === -1) {
1592
1610
  continue;
1593
1611
  }
1594
- const cookieName = pairStr.substring(0, valueStartPos).trim();
1612
+ const cookieName = trimCookieWhitespace(pairStr.substring(0, valueStartPos));
1595
1613
  if (!validCookieNameRegEx.test(cookieName)) {
1596
1614
  continue;
1597
1615
  }
1598
- let cookieValue = pairStr.substring(valueStartPos + 1).trim();
1616
+ let cookieValue = trimCookieWhitespace(pairStr.substring(valueStartPos + 1));
1599
1617
  if (cookieValue.startsWith('"') && cookieValue.endsWith('"')) {
1600
1618
  cookieValue = cookieValue.slice(1, -1);
1601
1619
  }
@@ -491,7 +491,18 @@ export class AuthheroWidget {
491
491
  e.preventDefault();
492
492
  if (!this._screen || this.loading)
493
493
  return;
494
- const submitData = overrideData || this.formData;
494
+ let submitData = { ...this.formData, ...(overrideData || {}) };
495
+ // Merge hidden input values from DOM (may have been set programmatically
496
+ // by inline scripts, e.g. passkey management buttons)
497
+ const form = this.el.shadowRoot?.querySelector("form");
498
+ if (form) {
499
+ const hiddenInputs = form.querySelectorAll('input[type="hidden"]');
500
+ hiddenInputs.forEach((input) => {
501
+ if (input.name && input.value) {
502
+ submitData[input.name] = input.value;
503
+ }
504
+ });
505
+ }
495
506
  // Always emit the submit event
496
507
  this.formSubmit.emit({
497
508
  screen: this._screen,
@@ -567,6 +578,10 @@ export class AuthheroWidget {
567
578
  this.state = result.state;
568
579
  this.persistState();
569
580
  }
581
+ // Perform WebAuthn ceremony if present (structured data, not script)
582
+ if (result.ceremony) {
583
+ this.performWebAuthnCeremony(result.ceremony);
584
+ }
570
585
  // Focus first input on new screen
571
586
  this.focusFirstInput();
572
587
  }
@@ -600,6 +615,192 @@ export class AuthheroWidget {
600
615
  this.loading = false;
601
616
  }
602
617
  };
618
+ /**
619
+ * Override form.submit() so that scripts (e.g. WebAuthn ceremony) that call
620
+ * form.submit() go through the widget's JSON fetch pipeline instead of a
621
+ * native form-urlencoded POST.
622
+ */
623
+ overrideFormSubmit() {
624
+ const shadowRoot = this.el.shadowRoot;
625
+ if (!shadowRoot)
626
+ return;
627
+ const form = shadowRoot.querySelector("form");
628
+ if (!form)
629
+ return;
630
+ form.submit = () => {
631
+ const formData = new FormData(form);
632
+ const data = {};
633
+ formData.forEach((value, key) => {
634
+ if (typeof value === "string") {
635
+ data[key] = value;
636
+ }
637
+ });
638
+ const syntheticEvent = { preventDefault: () => { } };
639
+ this.handleSubmit(syntheticEvent, data);
640
+ };
641
+ }
642
+ /**
643
+ * Validate and execute a structured WebAuthn ceremony returned by the server.
644
+ * Instead of injecting arbitrary script content, this parses the ceremony JSON,
645
+ * validates the expected fields, and calls the WebAuthn API natively.
646
+ */
647
+ performWebAuthnCeremony(ceremony) {
648
+ if (!this.isValidWebAuthnCeremony(ceremony)) {
649
+ console.error("Invalid WebAuthn ceremony payload", ceremony);
650
+ return;
651
+ }
652
+ requestAnimationFrame(() => {
653
+ this.overrideFormSubmit();
654
+ this.executeWebAuthnRegistration(ceremony);
655
+ });
656
+ }
657
+ /**
658
+ * Schema validation for WebAuthn ceremony payloads.
659
+ * Checks required fields and types before invoking browser APIs.
660
+ */
661
+ isValidWebAuthnCeremony(data) {
662
+ if (typeof data !== "object" || data === null)
663
+ return false;
664
+ const obj = data;
665
+ if (obj.type !== "webauthn-registration")
666
+ return false;
667
+ if (typeof obj.successAction !== "string")
668
+ return false;
669
+ const opts = obj.options;
670
+ if (typeof opts !== "object" || opts === null)
671
+ return false;
672
+ const o = opts;
673
+ if (typeof o.challenge !== "string")
674
+ return false;
675
+ const rp = o.rp;
676
+ if (typeof rp !== "object" || rp === null)
677
+ return false;
678
+ if (typeof rp.id !== "string" ||
679
+ typeof rp.name !== "string")
680
+ return false;
681
+ const user = o.user;
682
+ if (typeof user !== "object" || user === null)
683
+ return false;
684
+ const u = user;
685
+ if (typeof u.id !== "string" ||
686
+ typeof u.name !== "string" ||
687
+ typeof u.displayName !== "string")
688
+ return false;
689
+ if (!Array.isArray(o.pubKeyCredParams))
690
+ return false;
691
+ return true;
692
+ }
693
+ /**
694
+ * Perform the WebAuthn navigator.credentials.create() ceremony and submit
695
+ * the credential result via the form.
696
+ */
697
+ async executeWebAuthnRegistration(ceremony) {
698
+ const opts = ceremony.options;
699
+ const b64uToBuf = (s) => {
700
+ s = s.replace(/-/g, "+").replace(/_/g, "/");
701
+ while (s.length % 4)
702
+ s += "=";
703
+ const b = atob(s);
704
+ const a = new Uint8Array(b.length);
705
+ for (let i = 0; i < b.length; i++)
706
+ a[i] = b.charCodeAt(i);
707
+ return a.buffer;
708
+ };
709
+ const bufToB64u = (b) => {
710
+ const a = new Uint8Array(b);
711
+ let s = "";
712
+ for (let i = 0; i < a.length; i++)
713
+ s += String.fromCharCode(a[i]);
714
+ return btoa(s)
715
+ .replace(/\+/g, "-")
716
+ .replace(/\//g, "_")
717
+ .replace(/=+$/, "");
718
+ };
719
+ const findForm = () => {
720
+ const shadowRoot = this.el?.shadowRoot;
721
+ if (shadowRoot) {
722
+ const f = shadowRoot.querySelector("form");
723
+ if (f)
724
+ return f;
725
+ }
726
+ return document.querySelector("form");
727
+ };
728
+ try {
729
+ const publicKey = {
730
+ challenge: b64uToBuf(opts.challenge),
731
+ rp: { id: opts.rp.id, name: opts.rp.name },
732
+ user: {
733
+ id: b64uToBuf(opts.user.id),
734
+ name: opts.user.name,
735
+ displayName: opts.user.displayName,
736
+ },
737
+ pubKeyCredParams: opts.pubKeyCredParams.map((p) => ({
738
+ alg: p.alg,
739
+ type: p.type,
740
+ })),
741
+ timeout: opts.timeout,
742
+ attestation: (opts.attestation || "none"),
743
+ authenticatorSelection: opts.authenticatorSelection
744
+ ? {
745
+ residentKey: (opts.authenticatorSelection.residentKey ||
746
+ "preferred"),
747
+ userVerification: (opts.authenticatorSelection
748
+ .userVerification ||
749
+ "preferred"),
750
+ }
751
+ : undefined,
752
+ };
753
+ if (opts.excludeCredentials?.length) {
754
+ publicKey.excludeCredentials = opts.excludeCredentials.map((c) => ({
755
+ id: b64uToBuf(c.id),
756
+ type: c.type,
757
+ transports: (c.transports || []),
758
+ }));
759
+ }
760
+ const cred = (await navigator.credentials.create({
761
+ publicKey,
762
+ }));
763
+ const response = cred.response;
764
+ const resp = {
765
+ id: cred.id,
766
+ rawId: bufToB64u(cred.rawId),
767
+ type: cred.type,
768
+ response: {
769
+ attestationObject: bufToB64u(response.attestationObject),
770
+ clientDataJSON: bufToB64u(response.clientDataJSON),
771
+ },
772
+ clientExtensionResults: cred.getClientExtensionResults(),
773
+ authenticatorAttachment: cred.authenticatorAttachment || undefined,
774
+ };
775
+ if (typeof response.getTransports === "function") {
776
+ resp.response.transports =
777
+ response.getTransports();
778
+ }
779
+ const form = findForm();
780
+ if (form) {
781
+ const cf = form.querySelector('[name="credential-field"]') ||
782
+ form.querySelector("#credential-field");
783
+ const af = form.querySelector('[name="action-field"]') ||
784
+ form.querySelector("#action-field");
785
+ if (cf)
786
+ cf.value = JSON.stringify(resp);
787
+ if (af)
788
+ af.value = ceremony.successAction;
789
+ form.submit();
790
+ }
791
+ }
792
+ catch (e) {
793
+ console.error("WebAuthn registration error:", e);
794
+ const form = findForm();
795
+ if (form) {
796
+ const af = form.querySelector('[name="action-field"]') ||
797
+ form.querySelector("#action-field");
798
+ if (af)
799
+ af.value = "error";
800
+ form.submit();
801
+ }
802
+ }
803
+ }
603
804
  handleButtonClick = (detail) => {
604
805
  // If this is a submit button click, trigger form submission
605
806
  if (detail.type === "submit") {
@@ -802,9 +1003,11 @@ export class AuthheroWidget {
802
1003
  // Use the local screen variable for all rendering
803
1004
  const screenErrors = screen.messages?.filter((m) => m.type === "error") || [];
804
1005
  const screenSuccesses = screen.messages?.filter((m) => m.type === "success") || [];
805
- const components = [...(screen.components ?? [])]
1006
+ const allComponents = [...(screen.components ?? [])];
1007
+ const components = allComponents
806
1008
  .filter((c) => c.visible !== false)
807
1009
  .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
1010
+ const hiddenComponents = allComponents.filter((c) => c.visible === false);
808
1011
  // Separate social, divider, and field components for layout ordering
809
1012
  const socialComponents = components.filter((c) => this.isSocialComponent(c));
810
1013
  const fieldComponents = components.filter((c) => !this.isSocialComponent(c) && !this.isDividerComponent(c));
@@ -839,7 +1042,7 @@ export class AuthheroWidget {
839
1042
  };
840
1043
  // Get logo URL from theme.widget (takes precedence) or branding
841
1044
  const logoUrl = this._theme?.widget?.logo_url || this._branding?.logo_url;
842
- return (h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, h("header", { class: "widget-header", part: "header" }, logoUrl && (h("div", { class: "logo-wrapper", part: "logo-wrapper" }, h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), h("form", { onSubmit: this.handleSubmit, part: "form" }, h("div", { class: "form-content" }, socialComponents.length > 0 && (h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
1045
+ return (h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, h("header", { class: "widget-header", part: "header" }, logoUrl && (h("div", { class: "logo-wrapper", part: "logo-wrapper" }, h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), h("form", { onSubmit: this.handleSubmit, part: "form" }, hiddenComponents.map((c) => (h("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), h("div", { class: "form-content" }, socialComponents.length > 0 && (h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
843
1046
  fieldComponents.length > 0 &&
844
1047
  hasDivider && (h("div", { class: "divider", part: "divider" }, h("span", { class: "divider-text" }, dividerText))), h("div", { class: "fields-section", part: "fields-section" }, fieldComponents.map((component) => (h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading })))))), screen.links && screen.links.length > 0 && (h("div", { class: "links", part: "links" }, screen.links.map((link) => (h("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (h("span", null, link.text, " ", h("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
845
1048
  id: link.id,