@authhero/widget 0.28.2 → 0.29.0
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.
- package/dist/authhero-widget/authhero-widget.esm.js +1 -1
- package/dist/authhero-widget/index.esm.js +1 -1
- package/dist/authhero-widget/p-5428e2e1.entry.js +1 -0
- package/dist/cjs/authhero-widget.cjs.entry.js +206 -3
- package/dist/cjs/index.cjs.js +23 -5
- package/dist/collection/components/authhero-widget/authhero-widget.js +206 -3
- package/dist/components/authhero-widget.js +1 -1
- package/dist/components/index.js +1 -1
- package/dist/esm/authhero-widget.entry.js +206 -3
- package/dist/esm/index.js +23 -5
- package/dist/types/components/authhero-widget/authhero-widget.d.ts +22 -0
- package/hydrate/index.js +206 -3
- package/hydrate/index.mjs +206 -3
- package/package.json +1 -1
- package/dist/authhero-widget/p-b9ae0275.entry.js +0 -1
|
@@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");for(const[t,i]of Object.entries(p)){if("br"===t){e=e.replace(/<br\s*\/?>/gi,"<br>");continue}const s=new RegExp(`<${t}((?:\\s+[a-z-]+(?:="[^&]*"|='[^&]*')?)*)\\s*>`,"gi");e=e.replace(s,((e,s)=>{const n=[];if(s){const t=s.replace(/"/g,'"').replace(/'/g,"'").replace(/&/g,"&").replace(/</g,"<").replace(/>/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(`</${t}>`,"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,"&").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">")}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
|
-
|
|
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
|
|
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,
|
package/dist/cjs/index.cjs.js
CHANGED
|
@@ -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.
|
|
1605
|
+
const pairs = cookie.split(";");
|
|
1587
1606
|
const parsedCookie = {};
|
|
1588
|
-
for (
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
|
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,
|