@qrauth/web-components 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # @qrauth/web-components
2
2
 
3
+ ## 0.4.0
4
+
5
+ - **`<qrauth-login>` and `<qrauth-2fa>`: new `redirect-uri` attribute.** After the user taps Approve on the mobile CTA path, the QRAuth hosted approval page now navigates back to the consumer site instead of landing on a "you can close this page" dead end. The attribute is forwarded to `POST /api/v1/auth-sessions` as `redirectUrl` and validated server-side against the app's registered `redirectUrls` allowlist (same security model as OAuth 2.0 redirect URIs — strips trailing slash and ignores query + fragment before comparison). No-op on the desktop QR flow since polling in the original tab already handles the transition.
6
+ - **Backend requirement:** running against QRAuth API ≥ 2026-04-19 (the server-side allowlist check ships in the same release train).
7
+
3
8
  ## 0.3.0
4
9
 
5
10
  - `<qrauth-2fa>`: mobile-aware pending state. Primary CTA opens the hosted approval page (`/a/:token`) in a new tab; QR demoted to a "Use another device" expander. Polling in the original tab continues to fire `qrauth:verified` on approval. Adds `force-mode` and `mobile-fallback-only` attributes.
package/dist/login.d.ts CHANGED
@@ -21,6 +21,20 @@ import { QRAuthElement } from './base.js';
21
21
  * alternative same-device login path and the
22
22
  * component is only there as the cross-device scan
23
23
  * option.
24
+ * redirect-uri — Where the QRAuth hosted approval page should send
25
+ * the user after they tap Approve. Required for the
26
+ * mobile-CTA flow to feel complete — without it,
27
+ * the new tab lands on a "you can close this page"
28
+ * message and the user has to manually switch tabs.
29
+ * Must match one of the app's registered
30
+ * `redirectUrls` (configured in the QRAuth
31
+ * dashboard) — same allowlist semantics as OAuth
32
+ * 2.0 redirect URIs. Comparison strips trailing
33
+ * slashes and ignores query string + fragment, so
34
+ * consumers can pass `?from=qrauth` etc. without
35
+ * re-registering. The desktop QR flow ignores this
36
+ * attribute (the user is already on the consumer
37
+ * site when polling fires qrauth:authenticated).
24
38
  *
25
39
  * Events (bubbles + composed, cross shadow-DOM):
26
40
  * qrauth:authenticated — detail: { sessionId, user, signature }
@@ -46,6 +60,12 @@ export declare class QRAuthLogin extends QRAuthElement {
46
60
  get onAuth(): string | null;
47
61
  get display(): string;
48
62
  get animated(): boolean;
63
+ /**
64
+ * Where the hosted approval page should send the user after Approve.
65
+ * Forwarded as `redirectUrl` in the auth-session create POST body and
66
+ * validated server-side against the app's registered redirectUrls.
67
+ */
68
+ get redirectUri(): string | null;
49
69
  get forceMode(): 'mobile' | 'desktop' | 'auto';
50
70
  get mobileFallbackOnly(): boolean;
51
71
  disconnectedCallback(): void;
@@ -1,5 +1,5 @@
1
- /* @qrauth/web-components v0.3.0 — https://qrauth.io */
2
- var w=Object.defineProperty;var k=(h,d,t)=>d in h?w(h,d,{enumerable:!0,configurable:!0,writable:!0,value:t}):h[d]=t;var n=(h,d,t)=>k(h,typeof d!="symbol"?d+"":d,t);var p=class extends HTMLElement{constructor(){super();n(this,"shadow");n(this,"_sseConnection",null);n(this,"_pollInterval",null);this.shadow=this.attachShadow({mode:"open"})}static get observedAttributes(){return["tenant","theme","base-url","force-mode","mobile-fallback-only"]}get tenant(){return this.getAttribute("tenant")||""}get theme(){return this.getAttribute("theme")||"light"}get baseUrl(){let t=this.getAttribute("base-url");return t!==null?t:"https://qrauth.io"}connectedCallback(){this.render()}disconnectedCallback(){this.cleanup()}attributeChangedCallback(t,e,i){e!==i&&this.render()}isMobileLike(){if(typeof window>"u")return!1;let t=this.getAttribute("force-mode");if(t==="mobile")return!0;if(t==="desktop"||this.hasAttribute("mobile-fallback-only"))return!1;if(typeof window.matchMedia=="function"){let e=window.matchMedia("(pointer: coarse)").matches,i=window.matchMedia("(hover: none)").matches;if(e&&i)return!0}return/Android|iPhone|iPad|iPod|Mobile|Tablet|BlackBerry|Opera Mini/i.test(navigator.userAgent)}connectSSE(t){this.disconnectSSE(),this._sseConnection=new EventSource(t),this._sseConnection.onerror=()=>{this.disconnectSSE()}}onSSEEvent(t,e){this._sseConnection&&this._sseConnection.addEventListener(t,i=>{let s=i;try{e(JSON.parse(s.data))}catch{}})}disconnectSSE(){this._sseConnection&&(this._sseConnection.close(),this._sseConnection=null)}startPolling(t,e,i,s){this.stopPolling();let r=async()=>{try{let a=await fetch(t,{headers:s});a.ok&&i(await a.json())}catch{}};r(),this._pollInterval=setInterval(r,e)}stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}cleanup(){this.disconnectSSE(),this.stopPolling()}emit(t,e){this.dispatchEvent(new CustomEvent(t,{bubbles:!0,composed:!0,detail:e}))}async generateCodeVerifier(){let t=new Uint8Array(32);return crypto.getRandomValues(t),this._base64url(t)}async computeCodeChallenge(t){let i=new TextEncoder().encode(t),s=await crypto.subtle.digest("SHA-256",i);return this._base64url(new Uint8Array(s))}_base64url(t){let e=Array.from(t,i=>String.fromCodePoint(i)).join("");return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}getBaseStyles(){return`
1
+ /* @qrauth/web-components v0.4.0 — https://qrauth.io */
2
+ var w=Object.defineProperty;var k=(h,d,t)=>d in h?w(h,d,{enumerable:!0,configurable:!0,writable:!0,value:t}):h[d]=t;var n=(h,d,t)=>k(h,typeof d!="symbol"?d+"":d,t);var p=class extends HTMLElement{constructor(){super();n(this,"shadow");n(this,"_sseConnection",null);n(this,"_pollInterval",null);this.shadow=this.attachShadow({mode:"open"})}static get observedAttributes(){return["tenant","theme","base-url","force-mode","mobile-fallback-only"]}get tenant(){return this.getAttribute("tenant")||""}get theme(){return this.getAttribute("theme")||"light"}get baseUrl(){let t=this.getAttribute("base-url");return t!==null?t:"https://qrauth.io"}connectedCallback(){this.render()}disconnectedCallback(){this.cleanup()}attributeChangedCallback(t,e,i){e!==i&&this.render()}isMobileLike(){if(typeof window>"u")return!1;let t=this.getAttribute("force-mode");if(t==="mobile")return!0;if(t==="desktop"||this.hasAttribute("mobile-fallback-only"))return!1;if(typeof window.matchMedia=="function"){let e=window.matchMedia("(pointer: coarse)").matches,i=window.matchMedia("(hover: none)").matches;if(e&&i)return!0}return/Android|iPhone|iPad|iPod|Mobile|Tablet|BlackBerry|Opera Mini/i.test(navigator.userAgent)}connectSSE(t){this.disconnectSSE(),this._sseConnection=new EventSource(t),this._sseConnection.onerror=()=>{this.disconnectSSE()}}onSSEEvent(t,e){this._sseConnection&&this._sseConnection.addEventListener(t,i=>{let r=i;try{e(JSON.parse(r.data))}catch{}})}disconnectSSE(){this._sseConnection&&(this._sseConnection.close(),this._sseConnection=null)}startPolling(t,e,i,r){this.stopPolling();let s=async()=>{try{let a=await fetch(t,{headers:r});a.ok&&i(await a.json())}catch{}};s(),this._pollInterval=setInterval(s,e)}stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}cleanup(){this.disconnectSSE(),this.stopPolling()}emit(t,e){this.dispatchEvent(new CustomEvent(t,{bubbles:!0,composed:!0,detail:e}))}async generateCodeVerifier(){let t=new Uint8Array(32);return crypto.getRandomValues(t),this._base64url(t)}async computeCodeChallenge(t){let i=new TextEncoder().encode(t),r=await crypto.subtle.digest("SHA-256",i);return this._base64url(new Uint8Array(r))}_base64url(t){let e=Array.from(t,i=>String.fromCodePoint(i)).join("");return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}getBaseStyles(){return`
3
3
  :host {
4
4
  display: inline-block;
5
5
  font-family: var(--qrauth-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
@@ -52,7 +52,7 @@ var w=Object.defineProperty;var k=(h,d,t)=>d in h?w(h,d,{enumerable:!0,configura
52
52
  </svg>`,E=`<svg class="spinner" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
53
53
  <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2.5"
54
54
  stroke-dasharray="31.416" stroke-dashoffset="10" stroke-linecap="round"/>
55
- </svg>`,g=class extends p{constructor(){super(...arguments);n(this,"_sessionId",null);n(this,"_sessionToken",null);n(this,"_codeVerifier",null);n(this,"_status","idle");n(this,"_expiresAt",null);n(this,"_timerInterval",null);n(this,"_pollTimeout",null)}static get observedAttributes(){return[...super.observedAttributes,"scopes","redirect-url","on-auth","display","animated"]}get scopes(){return this.getAttribute("scopes")||"identity"}get redirectUrl(){return this.getAttribute("redirect-url")}get onAuth(){return this.getAttribute("on-auth")}get display(){return this.getAttribute("display")||"button"}get animated(){return this.hasAttribute("animated")}get forceMode(){let t=this.getAttribute("force-mode");return t==="mobile"||t==="desktop"?t:"auto"}get mobileFallbackOnly(){return this.hasAttribute("mobile-fallback-only")}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}render(){this.display==="inline"?this._renderInline():this._renderButton()}_renderButton(){this.shadow.innerHTML=`
55
+ </svg>`,g=class extends p{constructor(){super(...arguments);n(this,"_sessionId",null);n(this,"_sessionToken",null);n(this,"_codeVerifier",null);n(this,"_status","idle");n(this,"_expiresAt",null);n(this,"_timerInterval",null);n(this,"_pollTimeout",null)}static get observedAttributes(){return[...super.observedAttributes,"scopes","redirect-url","on-auth","display","animated","redirect-uri"]}get scopes(){return this.getAttribute("scopes")||"identity"}get redirectUrl(){return this.getAttribute("redirect-url")}get onAuth(){return this.getAttribute("on-auth")}get display(){return this.getAttribute("display")||"button"}get animated(){return this.hasAttribute("animated")}get redirectUri(){return this.getAttribute("redirect-uri")}get forceMode(){let t=this.getAttribute("force-mode");return t==="mobile"||t==="desktop"?t:"auto"}get mobileFallbackOnly(){return this.hasAttribute("mobile-fallback-only")}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}render(){this.display==="inline"?this._renderInline():this._renderButton()}_renderButton(){this.shadow.innerHTML=`
56
56
  <style>
57
57
  ${this.getBaseStyles()}
58
58
  :host { display: inline-block; }
@@ -506,7 +506,7 @@ var w=Object.defineProperty;var k=(h,d,t)=>d in h?w(h,d,{enumerable:!0,configura
506
506
  }
507
507
  .footer a { color: var(--_disabled); text-decoration: none; }
508
508
  .footer a:hover { text-decoration: underline; }
509
- `}_bindButtonEvents(){this.shadow.getElementById("open-btn")?.addEventListener("click",()=>{this._openModal(),this._startAuth()});let e=this.shadow.getElementById("overlay");e?.addEventListener("click",i=>{i.target===e&&this._closeModal()}),this._bindModalButtons()}_bindInlineEvents(){this.shadow.getElementById("start-btn")?.addEventListener("click",()=>{this._startAuth()})}_bindModalButtons(){this.shadow.getElementById("close-btn")?.addEventListener("click",()=>this._closeModal()),this.shadow.getElementById("retry-btn")?.addEventListener("click",()=>this._retry()),this.shadow.getElementById("mobile-cta")?.addEventListener("click",()=>this._openApprovalPage())}_openApprovalPage(){if(!this._sessionToken)return;let t=`${this.baseUrl}/a/${this._sessionToken}`;window.open(t,"_blank","noopener")||(window.location.href=t)}_openModal(){this.shadow.getElementById("overlay")?.classList.remove("hidden"),this._updateBody(this._bodyForStatus("idle"))}_closeModal(){this.shadow.getElementById("overlay")?.classList.add("hidden"),this._clearTimers(),this._sessionId=null,this._sessionToken=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null}async _startAuth(){this._status="loading",this._updateBody(this._bodyForStatus("loading"));try{this._codeVerifier=await this.generateCodeVerifier();let t=await this.computeCodeChallenge(this._codeVerifier),e={"Content-Type":"application/json","X-Client-Id":this.tenant},i={scopes:this.scopes.split(/\s+/).filter(Boolean),codeChallenge:t,codeChallengeMethod:"S256"},s=await fetch(`${this.baseUrl}/api/v1/auth-sessions`,{method:"POST",headers:e,body:JSON.stringify(i)});if(!s.ok){let a=await s.json().catch(()=>({message:"Failed to create session"}));throw new Error(a.message??"Failed to create session")}let r=await s.json();this._sessionId=r.sessionId,this._sessionToken=r.token||(r.qrUrl?r.qrUrl.split("/").pop()??null:null),this._expiresAt=new Date(r.expiresAt).getTime(),this._status="pending",this._updateBody(this._bodyForStatus("pending",{qrUrl:r.qrUrl})),this._bindModalButtons(),this._startCountdown(),this._beginPolling(r.sessionId)}catch(t){let e=t instanceof Error?t.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:e})),this._bindModalButtons(),this.emit("qrauth:error",{message:e})}}_retry(){if(this._clearTimers(),this._sessionId=null,this._sessionToken=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null,this.display==="inline"){let t=this.shadow.getElementById("inline-body");t&&(t.innerHTML=this._inlineIdleContent(),this._bindInlineEvents())}else this._startAuth()}_beginPolling(t){let e="PENDING",i=2e3,s=5e3,r=0,a=20,c=async()=>{if(!this._sessionId)return;let o=`${this.baseUrl}/api/v1/auth-sessions/${t}`;this._codeVerifier&&(o+=`?code_verifier=${encodeURIComponent(this._codeVerifier)}`);try{let l=await(await fetch(o,{headers:{"X-Client-Id":this.tenant}})).json();if(r=0,i=2e3,l.status!==e&&(e=l.status,this._handleStatusChange(l.status,l),["APPROVED","DENIED","EXPIRED"].includes(l.status)))return;this._pollTimeout=setTimeout(c,i)}catch{if(r++,r>=a){this._handleStatusChange("EXPIRED",void 0);return}i=Math.min(i*1.5,s),this._pollTimeout=setTimeout(c,i)}};this._pollTimeout=setTimeout(c,i)}_handleStatusChange(t,e){switch(this._status=t,t){case"SCANNED":{let i=this.shadow.querySelector(".qr-frame img")?.src,s=i?decodeURIComponent(i.replace(/.*[?&]data=/,"").split("&")[0]):"";this._updateBody(this._bodyForStatus("SCANNED",{qrUrl:s,timerHtml:this._formatTimer()})),this._bindModalButtons(),this.emit("qrauth:scanned",e);break}case"APPROVED":{this._clearTimers(),this._updateBody(this._bodyForStatus("APPROVED")),this._bindModalButtons(),this.emit("qrauth:authenticated",{sessionId:e?.sessionId,user:e?.user,signature:e?.signature});let i=this.onAuth;if(i){let s=window[i];typeof s=="function"&&s(e)}this.redirectUrl?setTimeout(()=>{window.location.href=this.redirectUrl},1500):this.display==="button"&&setTimeout(()=>this._closeModal(),2200);break}case"DENIED":this._clearTimers(),this._updateBody(this._bodyForStatus("DENIED")),this._bindModalButtons(),this.emit("qrauth:denied");break;case"EXPIRED":this._clearTimers(),this._status="EXPIRED",this._updateBody(this._bodyForStatus("EXPIRED")),this._bindModalButtons(),this.emit("qrauth:expired");break}}_startCountdown(){this._timerInterval=setInterval(()=>{let t=this.shadow.getElementById("timer");if(!t||!this._expiresAt)return;let e=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));t.textContent=e>0?this._formatTimer(e):"",e<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,(this._status==="pending"||this._status==="SCANNED")&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(t){let e=t??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(e/60),s=e%60;return`Expires in ${i}:${s<10?"0":""}${s}`}_updateBody(t){let e=this.display==="inline"?this.shadow.getElementById("inline-body"):this.shadow.getElementById("modal-body");e&&(e.innerHTML=t)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-login",g);var b=`<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
509
+ `}_bindButtonEvents(){this.shadow.getElementById("open-btn")?.addEventListener("click",()=>{this._openModal(),this._startAuth()});let e=this.shadow.getElementById("overlay");e?.addEventListener("click",i=>{i.target===e&&this._closeModal()}),this._bindModalButtons()}_bindInlineEvents(){this.shadow.getElementById("start-btn")?.addEventListener("click",()=>{this._startAuth()})}_bindModalButtons(){this.shadow.getElementById("close-btn")?.addEventListener("click",()=>this._closeModal()),this.shadow.getElementById("retry-btn")?.addEventListener("click",()=>this._retry()),this.shadow.getElementById("mobile-cta")?.addEventListener("click",()=>this._openApprovalPage())}_openApprovalPage(){if(!this._sessionToken)return;let t=`${this.baseUrl}/a/${this._sessionToken}`;window.open(t,"_blank","noopener")||(window.location.href=t)}_openModal(){this.shadow.getElementById("overlay")?.classList.remove("hidden"),this._updateBody(this._bodyForStatus("idle"))}_closeModal(){this.shadow.getElementById("overlay")?.classList.add("hidden"),this._clearTimers(),this._sessionId=null,this._sessionToken=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null}async _startAuth(){this._status="loading",this._updateBody(this._bodyForStatus("loading"));try{this._codeVerifier=await this.generateCodeVerifier();let t=await this.computeCodeChallenge(this._codeVerifier),e={"Content-Type":"application/json","X-Client-Id":this.tenant},i={scopes:this.scopes.split(/\s+/).filter(Boolean),codeChallenge:t,codeChallengeMethod:"S256"};this.redirectUri&&(i.redirectUrl=this.redirectUri);let r=await fetch(`${this.baseUrl}/api/v1/auth-sessions`,{method:"POST",headers:e,body:JSON.stringify(i)});if(!r.ok){let a=await r.json().catch(()=>({message:"Failed to create session"}));throw new Error(a.message??"Failed to create session")}let s=await r.json();this._sessionId=s.sessionId,this._sessionToken=s.token||(s.qrUrl?s.qrUrl.split("/").pop()??null:null),this._expiresAt=new Date(s.expiresAt).getTime(),this._status="pending",this._updateBody(this._bodyForStatus("pending",{qrUrl:s.qrUrl})),this._bindModalButtons(),this._startCountdown(),this._beginPolling(s.sessionId)}catch(t){let e=t instanceof Error?t.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:e})),this._bindModalButtons(),this.emit("qrauth:error",{message:e})}}_retry(){if(this._clearTimers(),this._sessionId=null,this._sessionToken=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null,this.display==="inline"){let t=this.shadow.getElementById("inline-body");t&&(t.innerHTML=this._inlineIdleContent(),this._bindInlineEvents())}else this._startAuth()}_beginPolling(t){let e="PENDING",i=2e3,r=5e3,s=0,a=20,c=async()=>{if(!this._sessionId)return;let o=`${this.baseUrl}/api/v1/auth-sessions/${t}`;this._codeVerifier&&(o+=`?code_verifier=${encodeURIComponent(this._codeVerifier)}`);try{let l=await(await fetch(o,{headers:{"X-Client-Id":this.tenant}})).json();if(s=0,i=2e3,l.status!==e&&(e=l.status,this._handleStatusChange(l.status,l),["APPROVED","DENIED","EXPIRED"].includes(l.status)))return;this._pollTimeout=setTimeout(c,i)}catch{if(s++,s>=a){this._handleStatusChange("EXPIRED",void 0);return}i=Math.min(i*1.5,r),this._pollTimeout=setTimeout(c,i)}};this._pollTimeout=setTimeout(c,i)}_handleStatusChange(t,e){switch(this._status=t,t){case"SCANNED":{let i=this.shadow.querySelector(".qr-frame img")?.src,r=i?decodeURIComponent(i.replace(/.*[?&]data=/,"").split("&")[0]):"";this._updateBody(this._bodyForStatus("SCANNED",{qrUrl:r,timerHtml:this._formatTimer()})),this._bindModalButtons(),this.emit("qrauth:scanned",e);break}case"APPROVED":{this._clearTimers(),this._updateBody(this._bodyForStatus("APPROVED")),this._bindModalButtons(),this.emit("qrauth:authenticated",{sessionId:e?.sessionId,user:e?.user,signature:e?.signature});let i=this.onAuth;if(i){let r=window[i];typeof r=="function"&&r(e)}this.redirectUrl?setTimeout(()=>{window.location.href=this.redirectUrl},1500):this.display==="button"&&setTimeout(()=>this._closeModal(),2200);break}case"DENIED":this._clearTimers(),this._updateBody(this._bodyForStatus("DENIED")),this._bindModalButtons(),this.emit("qrauth:denied");break;case"EXPIRED":this._clearTimers(),this._status="EXPIRED",this._updateBody(this._bodyForStatus("EXPIRED")),this._bindModalButtons(),this.emit("qrauth:expired");break}}_startCountdown(){this._timerInterval=setInterval(()=>{let t=this.shadow.getElementById("timer");if(!t||!this._expiresAt)return;let e=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));t.textContent=e>0?this._formatTimer(e):"",e<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,(this._status==="pending"||this._status==="SCANNED")&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(t){let e=t??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(e/60),r=e%60;return`Expires in ${i}:${r<10?"0":""}${r}`}_updateBody(t){let e=this.display==="inline"?this.shadow.getElementById("inline-body"):this.shadow.getElementById("modal-body");e&&(e.innerHTML=t)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-login",g);var b=`<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
510
510
  <path d="M48 24 H152 Q166 24 166 38 V108 Q166 156 100 182 Q34 156 34 108 V38 Q34 24 48 24 Z"
511
511
  fill="none" stroke="#fff" stroke-width="5"/>
512
512
  <g fill="#fff" opacity="0.2">
@@ -672,7 +672,7 @@ var w=Object.defineProperty;var k=(h,d,t)=>d in h?w(h,d,{enumerable:!0,configura
672
672
  <div class="spinner-wrap">${S}</div>
673
673
  <p class="status-text">Generating access QR...</p>
674
674
  </div>
675
- `;case"active":{let i=e?.claimUrl?`https://api.qrserver.com/v1/create-qr-code/?size=440x440&data=${encodeURIComponent(e.claimUrl)}`:"",s=e?.useCount??0,r=e?.maxUses??1,c=r>1?`<div class="use-badge">${s}/${r} claimed</div>`:"";return`
675
+ `;case"active":{let i=e?.claimUrl?`https://api.qrserver.com/v1/create-qr-code/?size=440x440&data=${encodeURIComponent(e.claimUrl)}`:"",r=e?.useCount??0,s=e?.maxUses??1,c=s>1?`<div class="use-badge">${r}/${s} claimed</div>`:"";return`
676
676
  <h3>Scan to Access</h3>
677
677
  <p class="subtitle">Present this QR code to gain entry</p>
678
678
  <div class="qr-frame">
@@ -890,7 +890,7 @@ var w=Object.defineProperty;var k=(h,d,t)=>d in h?w(h,d,{enumerable:!0,configura
890
890
  }
891
891
  .footer a { color: var(--_disabled); text-decoration: none; }
892
892
  .footer a:hover { text-decoration: underline; }
893
- `}_bindButtonEvents(){this.shadow.getElementById("open-btn")?.addEventListener("click",()=>{this._openModal(),this._startSession()});let e=this.shadow.getElementById("overlay");e?.addEventListener("click",i=>{i.target===e&&this._closeModal()}),this._bindModalButtons()}_bindInlineEvents(){this.shadow.getElementById("start-btn")?.addEventListener("click",()=>{this._startSession()})}_bindModalButtons(){this.shadow.getElementById("close-btn")?.addEventListener("click",()=>this._closeModal()),this.shadow.getElementById("retry-btn")?.addEventListener("click",()=>this._retry())}_openModal(){this.shadow.getElementById("overlay")?.classList.remove("hidden"),this._updateBody(this._bodyForStatus("idle"))}_closeModal(){this.shadow.getElementById("overlay")?.classList.add("hidden"),this._clearTimers(),this._sessionId=null,this._claimUrl=null,this._status="idle",this._expiresAt=null}async _startSession(){this._status="loading",this._updateBody(this._bodyForStatus("loading"));try{let t=await fetch(`${this.baseUrl}/api/v1/ephemeral`,{method:"POST",headers:{"Content-Type":"application/json","X-Client-Id":this.tenant},body:JSON.stringify({scopes:this.scopes.split(/\s+/).filter(Boolean),ttl:this.ttl,maxUses:this.maxUses,deviceBinding:this.deviceBinding})});if(!t.ok){let i=await t.json().catch(()=>({message:"Failed to create session"}));throw new Error(i.message??"Failed to create session")}let e=await t.json();this._sessionId=e.sessionId,this._claimUrl=e.claimUrl,this._expiresAt=new Date(e.expiresAt).getTime(),this._status="active",this._updateBody(this._bodyForStatus("active",{claimUrl:e.claimUrl,timerHtml:this._formatTimer(),useCount:0,maxUses:e.maxUses})),this._bindModalButtons(),this._startCountdown(),this._beginPolling(e.sessionId)}catch(t){let e=t instanceof Error?t.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:e})),this._bindModalButtons(),this.emit("qrauth:error",{message:e})}}_retry(){if(this._clearTimers(),this._sessionId=null,this._claimUrl=null,this._status="idle",this._expiresAt=null,this.display==="inline"){let t=this.shadow.getElementById("inline-body");t&&(t.innerHTML=this._inlineIdleContent(),this._bindInlineEvents())}else this._startSession()}_beginPolling(t){let e=2e3,i=5e3,s=0,r=20,a=async()=>{if(this._sessionId)try{let o=await(await fetch(`${this.baseUrl}/api/v1/ephemeral/${t}`,{headers:{"X-Client-Id":this.tenant}})).json();if(s=0,e=2e3,this._handleStatusChange(o.status,o),["CLAIMED","EXPIRED","REVOKED"].includes(o.status)){if(o.status==="CLAIMED"&&o.useCount<o.maxUses){this._updateBody(this._bodyForStatus("active",{claimUrl:this._claimUrl??void 0,timerHtml:this._formatTimer(),useCount:o.useCount,maxUses:o.maxUses})),this._bindModalButtons(),this._pollTimeout=setTimeout(a,e);return}return}this._pollTimeout=setTimeout(a,e)}catch{if(s++,s>=r){this._handleStatusChange("EXPIRED",void 0);return}e=Math.min(e*1.5,i),this._pollTimeout=setTimeout(a,e)}};this._pollTimeout=setTimeout(a,e)}_handleStatusChange(t,e){switch(t){case"CLAIMED":{this._clearTimers(),this._status="CLAIMED",this._updateBody(this._bodyForStatus("CLAIMED")),this._bindModalButtons(),this.emit("qrauth:claimed",{sessionId:e?.sessionId,status:e?.status,scopes:e?.scopes,metadata:e?.metadata,useCount:e?.useCount,maxUses:e?.maxUses}),this.display==="button"&&setTimeout(()=>this._closeModal(),2200);break}case"EXPIRED":this._clearTimers(),this._status="EXPIRED",this._updateBody(this._bodyForStatus("EXPIRED")),this._bindModalButtons(),this.emit("qrauth:expired");break;case"REVOKED":this._clearTimers(),this._status="REVOKED",this._updateBody(this._bodyForStatus("REVOKED")),this._bindModalButtons(),this.emit("qrauth:revoked");break}}_startCountdown(){this._timerInterval=setInterval(()=>{let t=this.shadow.getElementById("timer");if(!t||!this._expiresAt)return;let e=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));t.textContent=e>0?this._formatTimer(e):"",e<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,this._status==="active"&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(t){let e=t??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(e/60),s=e%60;return`Expires in ${i}:${s<10?"0":""}${s}`}_updateBody(t){let e=this.display==="inline"?this.shadow.getElementById("inline-body"):this.shadow.getElementById("modal-body");e&&(e.innerHTML=t)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-ephemeral",m);var v=`<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
893
+ `}_bindButtonEvents(){this.shadow.getElementById("open-btn")?.addEventListener("click",()=>{this._openModal(),this._startSession()});let e=this.shadow.getElementById("overlay");e?.addEventListener("click",i=>{i.target===e&&this._closeModal()}),this._bindModalButtons()}_bindInlineEvents(){this.shadow.getElementById("start-btn")?.addEventListener("click",()=>{this._startSession()})}_bindModalButtons(){this.shadow.getElementById("close-btn")?.addEventListener("click",()=>this._closeModal()),this.shadow.getElementById("retry-btn")?.addEventListener("click",()=>this._retry())}_openModal(){this.shadow.getElementById("overlay")?.classList.remove("hidden"),this._updateBody(this._bodyForStatus("idle"))}_closeModal(){this.shadow.getElementById("overlay")?.classList.add("hidden"),this._clearTimers(),this._sessionId=null,this._claimUrl=null,this._status="idle",this._expiresAt=null}async _startSession(){this._status="loading",this._updateBody(this._bodyForStatus("loading"));try{let t=await fetch(`${this.baseUrl}/api/v1/ephemeral`,{method:"POST",headers:{"Content-Type":"application/json","X-Client-Id":this.tenant},body:JSON.stringify({scopes:this.scopes.split(/\s+/).filter(Boolean),ttl:this.ttl,maxUses:this.maxUses,deviceBinding:this.deviceBinding})});if(!t.ok){let i=await t.json().catch(()=>({message:"Failed to create session"}));throw new Error(i.message??"Failed to create session")}let e=await t.json();this._sessionId=e.sessionId,this._claimUrl=e.claimUrl,this._expiresAt=new Date(e.expiresAt).getTime(),this._status="active",this._updateBody(this._bodyForStatus("active",{claimUrl:e.claimUrl,timerHtml:this._formatTimer(),useCount:0,maxUses:e.maxUses})),this._bindModalButtons(),this._startCountdown(),this._beginPolling(e.sessionId)}catch(t){let e=t instanceof Error?t.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:e})),this._bindModalButtons(),this.emit("qrauth:error",{message:e})}}_retry(){if(this._clearTimers(),this._sessionId=null,this._claimUrl=null,this._status="idle",this._expiresAt=null,this.display==="inline"){let t=this.shadow.getElementById("inline-body");t&&(t.innerHTML=this._inlineIdleContent(),this._bindInlineEvents())}else this._startSession()}_beginPolling(t){let e=2e3,i=5e3,r=0,s=20,a=async()=>{if(this._sessionId)try{let o=await(await fetch(`${this.baseUrl}/api/v1/ephemeral/${t}`,{headers:{"X-Client-Id":this.tenant}})).json();if(r=0,e=2e3,this._handleStatusChange(o.status,o),["CLAIMED","EXPIRED","REVOKED"].includes(o.status)){if(o.status==="CLAIMED"&&o.useCount<o.maxUses){this._updateBody(this._bodyForStatus("active",{claimUrl:this._claimUrl??void 0,timerHtml:this._formatTimer(),useCount:o.useCount,maxUses:o.maxUses})),this._bindModalButtons(),this._pollTimeout=setTimeout(a,e);return}return}this._pollTimeout=setTimeout(a,e)}catch{if(r++,r>=s){this._handleStatusChange("EXPIRED",void 0);return}e=Math.min(e*1.5,i),this._pollTimeout=setTimeout(a,e)}};this._pollTimeout=setTimeout(a,e)}_handleStatusChange(t,e){switch(t){case"CLAIMED":{this._clearTimers(),this._status="CLAIMED",this._updateBody(this._bodyForStatus("CLAIMED")),this._bindModalButtons(),this.emit("qrauth:claimed",{sessionId:e?.sessionId,status:e?.status,scopes:e?.scopes,metadata:e?.metadata,useCount:e?.useCount,maxUses:e?.maxUses}),this.display==="button"&&setTimeout(()=>this._closeModal(),2200);break}case"EXPIRED":this._clearTimers(),this._status="EXPIRED",this._updateBody(this._bodyForStatus("EXPIRED")),this._bindModalButtons(),this.emit("qrauth:expired");break;case"REVOKED":this._clearTimers(),this._status="REVOKED",this._updateBody(this._bodyForStatus("REVOKED")),this._bindModalButtons(),this.emit("qrauth:revoked");break}}_startCountdown(){this._timerInterval=setInterval(()=>{let t=this.shadow.getElementById("timer");if(!t||!this._expiresAt)return;let e=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));t.textContent=e>0?this._formatTimer(e):"",e<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,this._status==="active"&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(t){let e=t??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(e/60),r=e%60;return`Expires in ${i}:${r<10?"0":""}${r}`}_updateBody(t){let e=this.display==="inline"?this.shadow.getElementById("inline-body"):this.shadow.getElementById("modal-body");e&&(e.innerHTML=t)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-ephemeral",m);var v=`<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
894
894
  <path d="M48 24 H152 Q166 24 166 38 V108 Q166 156 100 182 Q34 156 34 108 V38 Q34 24 48 24 Z"
895
895
  fill="none" stroke="#fff" stroke-width="5"/>
896
896
  <g fill="#fff" opacity="0.2">
@@ -912,7 +912,7 @@ var w=Object.defineProperty;var k=(h,d,t)=>d in h?w(h,d,{enumerable:!0,configura
912
912
  </svg>`,A=`<svg class="spinner" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
913
913
  <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2.5"
914
914
  stroke-dasharray="31.416" stroke-dashoffset="10" stroke-linecap="round"/>
915
- </svg>`,x=class extends p{constructor(){super(...arguments);n(this,"_sessionId",null);n(this,"_sessionToken",null);n(this,"_codeVerifier",null);n(this,"_status","idle");n(this,"_expiresAt",null);n(this,"_timerInterval",null);n(this,"_pollTimeout",null)}static get observedAttributes(){return[...super.observedAttributes,"session-token","scopes","auto-start"]}get sessionToken(){return this.getAttribute("session-token")}get scopes(){return this.getAttribute("scopes")||"identity"}get autoStart(){return this.hasAttribute("auto-start")}get forceMode(){let t=this.getAttribute("force-mode");return t==="mobile"||t==="desktop"?t:"auto"}get mobileFallbackOnly(){return this.hasAttribute("mobile-fallback-only")}connectedCallback(){super.connectedCallback(),this.autoStart&&this._startAuth()}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}render(){this.shadow.innerHTML=`
915
+ </svg>`,x=class extends p{constructor(){super(...arguments);n(this,"_sessionId",null);n(this,"_sessionToken",null);n(this,"_codeVerifier",null);n(this,"_status","idle");n(this,"_expiresAt",null);n(this,"_timerInterval",null);n(this,"_pollTimeout",null)}static get observedAttributes(){return[...super.observedAttributes,"session-token","scopes","auto-start","redirect-uri"]}get sessionToken(){return this.getAttribute("session-token")}get scopes(){return this.getAttribute("scopes")||"identity"}get autoStart(){return this.hasAttribute("auto-start")}get redirectUri(){return this.getAttribute("redirect-uri")}get forceMode(){let t=this.getAttribute("force-mode");return t==="mobile"||t==="desktop"?t:"auto"}get mobileFallbackOnly(){return this.hasAttribute("mobile-fallback-only")}connectedCallback(){super.connectedCallback(),this.autoStart&&this._startAuth()}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}render(){this.shadow.innerHTML=`
916
916
  <style>
917
917
  ${this.getBaseStyles()}
918
918
  :host { display: block; }
@@ -1009,13 +1009,13 @@ var w=Object.defineProperty;var k=(h,d,t)=>d in h?w(h,d,{enumerable:!0,configura
1009
1009
  <div class="spinner-wrap">${A}</div>
1010
1010
  <p class="status-text">Generating QR code...</p>
1011
1011
  </div>
1012
- `;case"pending":case"SCANNED":{if(this.isMobileLike())return this._mobilePendingBody(t,e,i);let s=t==="SCANNED";return`
1012
+ `;case"pending":case"SCANNED":{if(this.isMobileLike())return this._mobilePendingBody(t,e,i);let r=t==="SCANNED";return`
1013
1013
  ${i}
1014
1014
  <h3>Scan to verify</h3>
1015
1015
  <p class="subtitle">Use your phone camera to complete verification</p>
1016
- ${this._qrFrameHtml(e?.qrUrl??"",s)}
1017
- <div class="status-text ${s?"status-scanned":""}">
1018
- ${s?"&#10003; Scanned &mdash; waiting for approval...":"Waiting for scan..."}
1016
+ ${this._qrFrameHtml(e?.qrUrl??"",r)}
1017
+ <div class="status-text ${r?"status-scanned":""}">
1018
+ ${r?"&#10003; Scanned &mdash; waiting for approval...":"Waiting for scan..."}
1019
1019
  </div>
1020
1020
  <div class="timer" id="timer">${e?.timerHtml??""}</div>
1021
1021
  <div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
@@ -1052,7 +1052,7 @@ var w=Object.defineProperty;var k=(h,d,t)=>d in h?w(h,d,{enumerable:!0,configura
1052
1052
  <div class="qr-badge">${v}</div>
1053
1053
  ${e?'<div class="scanned-overlay"><span class="scanned-check">'+_+"</span></div>":""}
1054
1054
  </div>
1055
- `}_mobilePendingBody(t,e,i=""){let s=t==="SCANNED";return`
1055
+ `}_mobilePendingBody(t,e,i=""){let r=t==="SCANNED";return`
1056
1056
  ${i}
1057
1057
  <h3>Verify your identity</h3>
1058
1058
  <p class="subtitle">Tap continue to verify on this device.</p>
@@ -1060,13 +1060,13 @@ var w=Object.defineProperty;var k=(h,d,t)=>d in h?w(h,d,{enumerable:!0,configura
1060
1060
  <span class="btn-icon">${v}</span>
1061
1061
  Continue with QRAuth
1062
1062
  </button>
1063
- <div class="status-text ${s?"status-scanned":""}">
1064
- ${s?"&#10003; Scanned &mdash; waiting for approval...":"Waiting for approval..."}
1063
+ <div class="status-text ${r?"status-scanned":""}">
1064
+ ${r?"&#10003; Scanned &mdash; waiting for approval...":"Waiting for approval..."}
1065
1065
  </div>
1066
1066
  <div class="timer" id="timer">${e?.timerHtml??""}</div>
1067
1067
  <details class="alt-device">
1068
1068
  <summary>Use another device</summary>
1069
- ${this._qrFrameHtml(e?.qrUrl??"",s)}
1069
+ ${this._qrFrameHtml(e?.qrUrl??"",r)}
1070
1070
  </details>
1071
1071
  <div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
1072
1072
  `}_sharedStyles(){return`
@@ -1300,5 +1300,5 @@ var w=Object.defineProperty;var k=(h,d,t)=>d in h?w(h,d,{enumerable:!0,configura
1300
1300
  }
1301
1301
  .footer a { color: var(--_disabled); text-decoration: none; }
1302
1302
  .footer a:hover { text-decoration: underline; }
1303
- `}_bindBodyEvents(){this.shadow.getElementById("start-btn")?.addEventListener("click",()=>this._startAuth()),this.shadow.getElementById("retry-btn")?.addEventListener("click",()=>this._retry()),this.shadow.getElementById("mobile-cta")?.addEventListener("click",()=>this._openApprovalPage())}_openApprovalPage(){if(!this._sessionToken)return;let t=`${this.baseUrl}/a/${this._sessionToken}`;window.open(t,"_blank","noopener")||(window.location.href=t)}async _startAuth(){this._status="loading",this._updateBody(this._bodyForStatus("loading"));try{this._codeVerifier=await this.generateCodeVerifier();let t=await this.computeCodeChallenge(this._codeVerifier),e={"Content-Type":"application/json","X-Client-Id":this.tenant},i={scopes:this.scopes.split(/\s+/).filter(Boolean),codeChallenge:t,codeChallengeMethod:"S256"},s=await fetch(`${this.baseUrl}/api/v1/auth-sessions`,{method:"POST",headers:e,body:JSON.stringify(i)});if(!s.ok){let a=await s.json().catch(()=>({message:"Failed to create session"}));throw new Error(a.message??"Failed to create session")}let r=await s.json();this._sessionId=r.sessionId,this._sessionToken=r.token||(r.qrUrl?r.qrUrl.split("/").pop()??null:null),this._expiresAt=new Date(r.expiresAt).getTime(),this._status="pending",this._updateBody(this._bodyForStatus("pending",{qrUrl:r.qrUrl})),this._bindBodyEvents(),this._startCountdown(),this._beginPolling(r.sessionId)}catch(t){let e=t instanceof Error?t.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:e})),this._bindBodyEvents(),this.emit("qrauth:error",{message:e})}}_retry(){this._clearTimers(),this._sessionId=null,this._sessionToken=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null,this._updateBody(this._bodyForStatus("idle")),this._bindBodyEvents()}_beginPolling(t){let e="PENDING",i=2e3,s=5e3,r=0,a=20,c=async()=>{if(!this._sessionId)return;let o=`${this.baseUrl}/api/v1/auth-sessions/${t}`;this._codeVerifier&&(o+=`?code_verifier=${encodeURIComponent(this._codeVerifier)}`);try{let l=await(await fetch(o,{headers:{"X-Client-Id":this.tenant}})).json();if(r=0,i=2e3,l.status!==e&&(e=l.status,this._handleStatusChange(l.status,l),["APPROVED","DENIED","EXPIRED"].includes(l.status)))return;this._pollTimeout=setTimeout(c,i)}catch{if(r++,r>=a){this._handleStatusChange("EXPIRED",void 0);return}i=Math.min(i*1.5,s),this._pollTimeout=setTimeout(c,i)}};this._pollTimeout=setTimeout(c,i)}_handleStatusChange(t,e){switch(this._status=t,t){case"SCANNED":{let i=this.shadow.querySelector(".qr-frame img")?.src,s=i?decodeURIComponent(i.replace(/.*[?&]data=/,"").split("&")[0]):"";this._updateBody(this._bodyForStatus("SCANNED",{qrUrl:s,timerHtml:this._formatTimer()})),this._bindBodyEvents();break}case"APPROVED":{this._clearTimers(),this._updateBody(this._bodyForStatus("APPROVED")),this.emit("qrauth:verified",{sessionId:e?.sessionId,user:e?.user,signature:e?.signature});break}case"DENIED":this._clearTimers(),this._updateBody(this._bodyForStatus("DENIED")),this._bindBodyEvents(),this.emit("qrauth:denied");break;case"EXPIRED":this._clearTimers(),this._status="EXPIRED",this._updateBody(this._bodyForStatus("EXPIRED")),this._bindBodyEvents(),this.emit("qrauth:expired");break}}_startCountdown(){this._timerInterval=setInterval(()=>{let t=this.shadow.getElementById("timer");if(!t||!this._expiresAt)return;let e=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));t.textContent=e>0?this._formatTimer(e):"",e<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,(this._status==="pending"||this._status==="SCANNED")&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(t){let e=t??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(e/60),s=e%60;return`Expires in ${i}:${s<10?"0":""}${s}`}_updateBody(t){let e=this.shadow.getElementById("body");e&&(e.innerHTML=t)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-2fa",x);export{p as QRAuthElement,m as QRAuthEphemeral,g as QRAuthLogin,x as QRAuthTwoFA};
1303
+ `}_bindBodyEvents(){this.shadow.getElementById("start-btn")?.addEventListener("click",()=>this._startAuth()),this.shadow.getElementById("retry-btn")?.addEventListener("click",()=>this._retry()),this.shadow.getElementById("mobile-cta")?.addEventListener("click",()=>this._openApprovalPage())}_openApprovalPage(){if(!this._sessionToken)return;let t=`${this.baseUrl}/a/${this._sessionToken}`;window.open(t,"_blank","noopener")||(window.location.href=t)}async _startAuth(){this._status="loading",this._updateBody(this._bodyForStatus("loading"));try{this._codeVerifier=await this.generateCodeVerifier();let t=await this.computeCodeChallenge(this._codeVerifier),e={"Content-Type":"application/json","X-Client-Id":this.tenant},i={scopes:this.scopes.split(/\s+/).filter(Boolean),codeChallenge:t,codeChallengeMethod:"S256"};this.redirectUri&&(i.redirectUrl=this.redirectUri);let r=await fetch(`${this.baseUrl}/api/v1/auth-sessions`,{method:"POST",headers:e,body:JSON.stringify(i)});if(!r.ok){let a=await r.json().catch(()=>({message:"Failed to create session"}));throw new Error(a.message??"Failed to create session")}let s=await r.json();this._sessionId=s.sessionId,this._sessionToken=s.token||(s.qrUrl?s.qrUrl.split("/").pop()??null:null),this._expiresAt=new Date(s.expiresAt).getTime(),this._status="pending",this._updateBody(this._bodyForStatus("pending",{qrUrl:s.qrUrl})),this._bindBodyEvents(),this._startCountdown(),this._beginPolling(s.sessionId)}catch(t){let e=t instanceof Error?t.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:e})),this._bindBodyEvents(),this.emit("qrauth:error",{message:e})}}_retry(){this._clearTimers(),this._sessionId=null,this._sessionToken=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null,this._updateBody(this._bodyForStatus("idle")),this._bindBodyEvents()}_beginPolling(t){let e="PENDING",i=2e3,r=5e3,s=0,a=20,c=async()=>{if(!this._sessionId)return;let o=`${this.baseUrl}/api/v1/auth-sessions/${t}`;this._codeVerifier&&(o+=`?code_verifier=${encodeURIComponent(this._codeVerifier)}`);try{let l=await(await fetch(o,{headers:{"X-Client-Id":this.tenant}})).json();if(s=0,i=2e3,l.status!==e&&(e=l.status,this._handleStatusChange(l.status,l),["APPROVED","DENIED","EXPIRED"].includes(l.status)))return;this._pollTimeout=setTimeout(c,i)}catch{if(s++,s>=a){this._handleStatusChange("EXPIRED",void 0);return}i=Math.min(i*1.5,r),this._pollTimeout=setTimeout(c,i)}};this._pollTimeout=setTimeout(c,i)}_handleStatusChange(t,e){switch(this._status=t,t){case"SCANNED":{let i=this.shadow.querySelector(".qr-frame img")?.src,r=i?decodeURIComponent(i.replace(/.*[?&]data=/,"").split("&")[0]):"";this._updateBody(this._bodyForStatus("SCANNED",{qrUrl:r,timerHtml:this._formatTimer()})),this._bindBodyEvents();break}case"APPROVED":{this._clearTimers(),this._updateBody(this._bodyForStatus("APPROVED")),this.emit("qrauth:verified",{sessionId:e?.sessionId,user:e?.user,signature:e?.signature});break}case"DENIED":this._clearTimers(),this._updateBody(this._bodyForStatus("DENIED")),this._bindBodyEvents(),this.emit("qrauth:denied");break;case"EXPIRED":this._clearTimers(),this._status="EXPIRED",this._updateBody(this._bodyForStatus("EXPIRED")),this._bindBodyEvents(),this.emit("qrauth:expired");break}}_startCountdown(){this._timerInterval=setInterval(()=>{let t=this.shadow.getElementById("timer");if(!t||!this._expiresAt)return;let e=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));t.textContent=e>0?this._formatTimer(e):"",e<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,(this._status==="pending"||this._status==="SCANNED")&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(t){let e=t??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(e/60),r=e%60;return`Expires in ${i}:${r<10?"0":""}${r}`}_updateBody(t){let e=this.shadow.getElementById("body");e&&(e.innerHTML=t)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-2fa",x);export{p as QRAuthElement,m as QRAuthEphemeral,g as QRAuthLogin,x as QRAuthTwoFA};
1304
1304
  //# sourceMappingURL=qrauth-components.esm.js.map
@@ -1,5 +1,5 @@
1
- /* @qrauth/web-components v0.3.0 — https://qrauth.io */
2
- "use strict";var QRAuthComponents=(()=>{var v=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var E=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var S=(o,a,t)=>a in o?v(o,a,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[a]=t;var A=(o,a)=>{for(var t in a)v(o,t,{get:a[t],enumerable:!0})},T=(o,a,t,e)=>{if(a&&typeof a=="object"||typeof a=="function")for(let i of E(a))!I.call(o,i)&&i!==t&&v(o,i,{get:()=>a[i],enumerable:!(e=k(a,i))||e.enumerable});return o};var C=o=>T(v({},"__esModule",{value:!0}),o);var n=(o,a,t)=>S(o,typeof a!="symbol"?a+"":a,t);var $={};A($,{QRAuthElement:()=>p,QRAuthEphemeral:()=>b,QRAuthLogin:()=>g,QRAuthTwoFA:()=>m});var p=class extends HTMLElement{constructor(){super();n(this,"shadow");n(this,"_sseConnection",null);n(this,"_pollInterval",null);this.shadow=this.attachShadow({mode:"open"})}static get observedAttributes(){return["tenant","theme","base-url","force-mode","mobile-fallback-only"]}get tenant(){return this.getAttribute("tenant")||""}get theme(){return this.getAttribute("theme")||"light"}get baseUrl(){let t=this.getAttribute("base-url");return t!==null?t:"https://qrauth.io"}connectedCallback(){this.render()}disconnectedCallback(){this.cleanup()}attributeChangedCallback(t,e,i){e!==i&&this.render()}isMobileLike(){if(typeof window>"u")return!1;let t=this.getAttribute("force-mode");if(t==="mobile")return!0;if(t==="desktop"||this.hasAttribute("mobile-fallback-only"))return!1;if(typeof window.matchMedia=="function"){let e=window.matchMedia("(pointer: coarse)").matches,i=window.matchMedia("(hover: none)").matches;if(e&&i)return!0}return/Android|iPhone|iPad|iPod|Mobile|Tablet|BlackBerry|Opera Mini/i.test(navigator.userAgent)}connectSSE(t){this.disconnectSSE(),this._sseConnection=new EventSource(t),this._sseConnection.onerror=()=>{this.disconnectSSE()}}onSSEEvent(t,e){this._sseConnection&&this._sseConnection.addEventListener(t,i=>{let s=i;try{e(JSON.parse(s.data))}catch{}})}disconnectSSE(){this._sseConnection&&(this._sseConnection.close(),this._sseConnection=null)}startPolling(t,e,i,s){this.stopPolling();let r=async()=>{try{let d=await fetch(t,{headers:s});d.ok&&i(await d.json())}catch{}};r(),this._pollInterval=setInterval(r,e)}stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}cleanup(){this.disconnectSSE(),this.stopPolling()}emit(t,e){this.dispatchEvent(new CustomEvent(t,{bubbles:!0,composed:!0,detail:e}))}async generateCodeVerifier(){let t=new Uint8Array(32);return crypto.getRandomValues(t),this._base64url(t)}async computeCodeChallenge(t){let i=new TextEncoder().encode(t),s=await crypto.subtle.digest("SHA-256",i);return this._base64url(new Uint8Array(s))}_base64url(t){let e=Array.from(t,i=>String.fromCodePoint(i)).join("");return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}getBaseStyles(){return`
1
+ /* @qrauth/web-components v0.4.0 — https://qrauth.io */
2
+ "use strict";var QRAuthComponents=(()=>{var v=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var E=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var S=(o,a,t)=>a in o?v(o,a,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[a]=t;var A=(o,a)=>{for(var t in a)v(o,t,{get:a[t],enumerable:!0})},T=(o,a,t,e)=>{if(a&&typeof a=="object"||typeof a=="function")for(let i of E(a))!I.call(o,i)&&i!==t&&v(o,i,{get:()=>a[i],enumerable:!(e=k(a,i))||e.enumerable});return o};var C=o=>T(v({},"__esModule",{value:!0}),o);var n=(o,a,t)=>S(o,typeof a!="symbol"?a+"":a,t);var $={};A($,{QRAuthElement:()=>p,QRAuthEphemeral:()=>b,QRAuthLogin:()=>g,QRAuthTwoFA:()=>m});var p=class extends HTMLElement{constructor(){super();n(this,"shadow");n(this,"_sseConnection",null);n(this,"_pollInterval",null);this.shadow=this.attachShadow({mode:"open"})}static get observedAttributes(){return["tenant","theme","base-url","force-mode","mobile-fallback-only"]}get tenant(){return this.getAttribute("tenant")||""}get theme(){return this.getAttribute("theme")||"light"}get baseUrl(){let t=this.getAttribute("base-url");return t!==null?t:"https://qrauth.io"}connectedCallback(){this.render()}disconnectedCallback(){this.cleanup()}attributeChangedCallback(t,e,i){e!==i&&this.render()}isMobileLike(){if(typeof window>"u")return!1;let t=this.getAttribute("force-mode");if(t==="mobile")return!0;if(t==="desktop"||this.hasAttribute("mobile-fallback-only"))return!1;if(typeof window.matchMedia=="function"){let e=window.matchMedia("(pointer: coarse)").matches,i=window.matchMedia("(hover: none)").matches;if(e&&i)return!0}return/Android|iPhone|iPad|iPod|Mobile|Tablet|BlackBerry|Opera Mini/i.test(navigator.userAgent)}connectSSE(t){this.disconnectSSE(),this._sseConnection=new EventSource(t),this._sseConnection.onerror=()=>{this.disconnectSSE()}}onSSEEvent(t,e){this._sseConnection&&this._sseConnection.addEventListener(t,i=>{let r=i;try{e(JSON.parse(r.data))}catch{}})}disconnectSSE(){this._sseConnection&&(this._sseConnection.close(),this._sseConnection=null)}startPolling(t,e,i,r){this.stopPolling();let s=async()=>{try{let d=await fetch(t,{headers:r});d.ok&&i(await d.json())}catch{}};s(),this._pollInterval=setInterval(s,e)}stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}cleanup(){this.disconnectSSE(),this.stopPolling()}emit(t,e){this.dispatchEvent(new CustomEvent(t,{bubbles:!0,composed:!0,detail:e}))}async generateCodeVerifier(){let t=new Uint8Array(32);return crypto.getRandomValues(t),this._base64url(t)}async computeCodeChallenge(t){let i=new TextEncoder().encode(t),r=await crypto.subtle.digest("SHA-256",i);return this._base64url(new Uint8Array(r))}_base64url(t){let e=Array.from(t,i=>String.fromCodePoint(i)).join("");return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}getBaseStyles(){return`
3
3
  :host {
4
4
  display: inline-block;
5
5
  font-family: var(--qrauth-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
@@ -52,7 +52,7 @@
52
52
  </svg>`,B=`<svg class="spinner" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
53
53
  <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2.5"
54
54
  stroke-dasharray="31.416" stroke-dashoffset="10" stroke-linecap="round"/>
55
- </svg>`,g=class extends p{constructor(){super(...arguments);n(this,"_sessionId",null);n(this,"_sessionToken",null);n(this,"_codeVerifier",null);n(this,"_status","idle");n(this,"_expiresAt",null);n(this,"_timerInterval",null);n(this,"_pollTimeout",null)}static get observedAttributes(){return[...super.observedAttributes,"scopes","redirect-url","on-auth","display","animated"]}get scopes(){return this.getAttribute("scopes")||"identity"}get redirectUrl(){return this.getAttribute("redirect-url")}get onAuth(){return this.getAttribute("on-auth")}get display(){return this.getAttribute("display")||"button"}get animated(){return this.hasAttribute("animated")}get forceMode(){let t=this.getAttribute("force-mode");return t==="mobile"||t==="desktop"?t:"auto"}get mobileFallbackOnly(){return this.hasAttribute("mobile-fallback-only")}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}render(){this.display==="inline"?this._renderInline():this._renderButton()}_renderButton(){this.shadow.innerHTML=`
55
+ </svg>`,g=class extends p{constructor(){super(...arguments);n(this,"_sessionId",null);n(this,"_sessionToken",null);n(this,"_codeVerifier",null);n(this,"_status","idle");n(this,"_expiresAt",null);n(this,"_timerInterval",null);n(this,"_pollTimeout",null)}static get observedAttributes(){return[...super.observedAttributes,"scopes","redirect-url","on-auth","display","animated","redirect-uri"]}get scopes(){return this.getAttribute("scopes")||"identity"}get redirectUrl(){return this.getAttribute("redirect-url")}get onAuth(){return this.getAttribute("on-auth")}get display(){return this.getAttribute("display")||"button"}get animated(){return this.hasAttribute("animated")}get redirectUri(){return this.getAttribute("redirect-uri")}get forceMode(){let t=this.getAttribute("force-mode");return t==="mobile"||t==="desktop"?t:"auto"}get mobileFallbackOnly(){return this.hasAttribute("mobile-fallback-only")}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}render(){this.display==="inline"?this._renderInline():this._renderButton()}_renderButton(){this.shadow.innerHTML=`
56
56
  <style>
57
57
  ${this.getBaseStyles()}
58
58
  :host { display: inline-block; }
@@ -506,7 +506,7 @@
506
506
  }
507
507
  .footer a { color: var(--_disabled); text-decoration: none; }
508
508
  .footer a:hover { text-decoration: underline; }
509
- `}_bindButtonEvents(){this.shadow.getElementById("open-btn")?.addEventListener("click",()=>{this._openModal(),this._startAuth()});let e=this.shadow.getElementById("overlay");e?.addEventListener("click",i=>{i.target===e&&this._closeModal()}),this._bindModalButtons()}_bindInlineEvents(){this.shadow.getElementById("start-btn")?.addEventListener("click",()=>{this._startAuth()})}_bindModalButtons(){this.shadow.getElementById("close-btn")?.addEventListener("click",()=>this._closeModal()),this.shadow.getElementById("retry-btn")?.addEventListener("click",()=>this._retry()),this.shadow.getElementById("mobile-cta")?.addEventListener("click",()=>this._openApprovalPage())}_openApprovalPage(){if(!this._sessionToken)return;let t=`${this.baseUrl}/a/${this._sessionToken}`;window.open(t,"_blank","noopener")||(window.location.href=t)}_openModal(){this.shadow.getElementById("overlay")?.classList.remove("hidden"),this._updateBody(this._bodyForStatus("idle"))}_closeModal(){this.shadow.getElementById("overlay")?.classList.add("hidden"),this._clearTimers(),this._sessionId=null,this._sessionToken=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null}async _startAuth(){this._status="loading",this._updateBody(this._bodyForStatus("loading"));try{this._codeVerifier=await this.generateCodeVerifier();let t=await this.computeCodeChallenge(this._codeVerifier),e={"Content-Type":"application/json","X-Client-Id":this.tenant},i={scopes:this.scopes.split(/\s+/).filter(Boolean),codeChallenge:t,codeChallengeMethod:"S256"},s=await fetch(`${this.baseUrl}/api/v1/auth-sessions`,{method:"POST",headers:e,body:JSON.stringify(i)});if(!s.ok){let d=await s.json().catch(()=>({message:"Failed to create session"}));throw new Error(d.message??"Failed to create session")}let r=await s.json();this._sessionId=r.sessionId,this._sessionToken=r.token||(r.qrUrl?r.qrUrl.split("/").pop()??null:null),this._expiresAt=new Date(r.expiresAt).getTime(),this._status="pending",this._updateBody(this._bodyForStatus("pending",{qrUrl:r.qrUrl})),this._bindModalButtons(),this._startCountdown(),this._beginPolling(r.sessionId)}catch(t){let e=t instanceof Error?t.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:e})),this._bindModalButtons(),this.emit("qrauth:error",{message:e})}}_retry(){if(this._clearTimers(),this._sessionId=null,this._sessionToken=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null,this.display==="inline"){let t=this.shadow.getElementById("inline-body");t&&(t.innerHTML=this._inlineIdleContent(),this._bindInlineEvents())}else this._startAuth()}_beginPolling(t){let e="PENDING",i=2e3,s=5e3,r=0,d=20,h=async()=>{if(!this._sessionId)return;let l=`${this.baseUrl}/api/v1/auth-sessions/${t}`;this._codeVerifier&&(l+=`?code_verifier=${encodeURIComponent(this._codeVerifier)}`);try{let c=await(await fetch(l,{headers:{"X-Client-Id":this.tenant}})).json();if(r=0,i=2e3,c.status!==e&&(e=c.status,this._handleStatusChange(c.status,c),["APPROVED","DENIED","EXPIRED"].includes(c.status)))return;this._pollTimeout=setTimeout(h,i)}catch{if(r++,r>=d){this._handleStatusChange("EXPIRED",void 0);return}i=Math.min(i*1.5,s),this._pollTimeout=setTimeout(h,i)}};this._pollTimeout=setTimeout(h,i)}_handleStatusChange(t,e){switch(this._status=t,t){case"SCANNED":{let i=this.shadow.querySelector(".qr-frame img")?.src,s=i?decodeURIComponent(i.replace(/.*[?&]data=/,"").split("&")[0]):"";this._updateBody(this._bodyForStatus("SCANNED",{qrUrl:s,timerHtml:this._formatTimer()})),this._bindModalButtons(),this.emit("qrauth:scanned",e);break}case"APPROVED":{this._clearTimers(),this._updateBody(this._bodyForStatus("APPROVED")),this._bindModalButtons(),this.emit("qrauth:authenticated",{sessionId:e?.sessionId,user:e?.user,signature:e?.signature});let i=this.onAuth;if(i){let s=window[i];typeof s=="function"&&s(e)}this.redirectUrl?setTimeout(()=>{window.location.href=this.redirectUrl},1500):this.display==="button"&&setTimeout(()=>this._closeModal(),2200);break}case"DENIED":this._clearTimers(),this._updateBody(this._bodyForStatus("DENIED")),this._bindModalButtons(),this.emit("qrauth:denied");break;case"EXPIRED":this._clearTimers(),this._status="EXPIRED",this._updateBody(this._bodyForStatus("EXPIRED")),this._bindModalButtons(),this.emit("qrauth:expired");break}}_startCountdown(){this._timerInterval=setInterval(()=>{let t=this.shadow.getElementById("timer");if(!t||!this._expiresAt)return;let e=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));t.textContent=e>0?this._formatTimer(e):"",e<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,(this._status==="pending"||this._status==="SCANNED")&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(t){let e=t??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(e/60),s=e%60;return`Expires in ${i}:${s<10?"0":""}${s}`}_updateBody(t){let e=this.display==="inline"?this.shadow.getElementById("inline-body"):this.shadow.getElementById("modal-body");e&&(e.innerHTML=t)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-login",g);var x=`<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
509
+ `}_bindButtonEvents(){this.shadow.getElementById("open-btn")?.addEventListener("click",()=>{this._openModal(),this._startAuth()});let e=this.shadow.getElementById("overlay");e?.addEventListener("click",i=>{i.target===e&&this._closeModal()}),this._bindModalButtons()}_bindInlineEvents(){this.shadow.getElementById("start-btn")?.addEventListener("click",()=>{this._startAuth()})}_bindModalButtons(){this.shadow.getElementById("close-btn")?.addEventListener("click",()=>this._closeModal()),this.shadow.getElementById("retry-btn")?.addEventListener("click",()=>this._retry()),this.shadow.getElementById("mobile-cta")?.addEventListener("click",()=>this._openApprovalPage())}_openApprovalPage(){if(!this._sessionToken)return;let t=`${this.baseUrl}/a/${this._sessionToken}`;window.open(t,"_blank","noopener")||(window.location.href=t)}_openModal(){this.shadow.getElementById("overlay")?.classList.remove("hidden"),this._updateBody(this._bodyForStatus("idle"))}_closeModal(){this.shadow.getElementById("overlay")?.classList.add("hidden"),this._clearTimers(),this._sessionId=null,this._sessionToken=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null}async _startAuth(){this._status="loading",this._updateBody(this._bodyForStatus("loading"));try{this._codeVerifier=await this.generateCodeVerifier();let t=await this.computeCodeChallenge(this._codeVerifier),e={"Content-Type":"application/json","X-Client-Id":this.tenant},i={scopes:this.scopes.split(/\s+/).filter(Boolean),codeChallenge:t,codeChallengeMethod:"S256"};this.redirectUri&&(i.redirectUrl=this.redirectUri);let r=await fetch(`${this.baseUrl}/api/v1/auth-sessions`,{method:"POST",headers:e,body:JSON.stringify(i)});if(!r.ok){let d=await r.json().catch(()=>({message:"Failed to create session"}));throw new Error(d.message??"Failed to create session")}let s=await r.json();this._sessionId=s.sessionId,this._sessionToken=s.token||(s.qrUrl?s.qrUrl.split("/").pop()??null:null),this._expiresAt=new Date(s.expiresAt).getTime(),this._status="pending",this._updateBody(this._bodyForStatus("pending",{qrUrl:s.qrUrl})),this._bindModalButtons(),this._startCountdown(),this._beginPolling(s.sessionId)}catch(t){let e=t instanceof Error?t.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:e})),this._bindModalButtons(),this.emit("qrauth:error",{message:e})}}_retry(){if(this._clearTimers(),this._sessionId=null,this._sessionToken=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null,this.display==="inline"){let t=this.shadow.getElementById("inline-body");t&&(t.innerHTML=this._inlineIdleContent(),this._bindInlineEvents())}else this._startAuth()}_beginPolling(t){let e="PENDING",i=2e3,r=5e3,s=0,d=20,h=async()=>{if(!this._sessionId)return;let l=`${this.baseUrl}/api/v1/auth-sessions/${t}`;this._codeVerifier&&(l+=`?code_verifier=${encodeURIComponent(this._codeVerifier)}`);try{let c=await(await fetch(l,{headers:{"X-Client-Id":this.tenant}})).json();if(s=0,i=2e3,c.status!==e&&(e=c.status,this._handleStatusChange(c.status,c),["APPROVED","DENIED","EXPIRED"].includes(c.status)))return;this._pollTimeout=setTimeout(h,i)}catch{if(s++,s>=d){this._handleStatusChange("EXPIRED",void 0);return}i=Math.min(i*1.5,r),this._pollTimeout=setTimeout(h,i)}};this._pollTimeout=setTimeout(h,i)}_handleStatusChange(t,e){switch(this._status=t,t){case"SCANNED":{let i=this.shadow.querySelector(".qr-frame img")?.src,r=i?decodeURIComponent(i.replace(/.*[?&]data=/,"").split("&")[0]):"";this._updateBody(this._bodyForStatus("SCANNED",{qrUrl:r,timerHtml:this._formatTimer()})),this._bindModalButtons(),this.emit("qrauth:scanned",e);break}case"APPROVED":{this._clearTimers(),this._updateBody(this._bodyForStatus("APPROVED")),this._bindModalButtons(),this.emit("qrauth:authenticated",{sessionId:e?.sessionId,user:e?.user,signature:e?.signature});let i=this.onAuth;if(i){let r=window[i];typeof r=="function"&&r(e)}this.redirectUrl?setTimeout(()=>{window.location.href=this.redirectUrl},1500):this.display==="button"&&setTimeout(()=>this._closeModal(),2200);break}case"DENIED":this._clearTimers(),this._updateBody(this._bodyForStatus("DENIED")),this._bindModalButtons(),this.emit("qrauth:denied");break;case"EXPIRED":this._clearTimers(),this._status="EXPIRED",this._updateBody(this._bodyForStatus("EXPIRED")),this._bindModalButtons(),this.emit("qrauth:expired");break}}_startCountdown(){this._timerInterval=setInterval(()=>{let t=this.shadow.getElementById("timer");if(!t||!this._expiresAt)return;let e=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));t.textContent=e>0?this._formatTimer(e):"",e<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,(this._status==="pending"||this._status==="SCANNED")&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(t){let e=t??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(e/60),r=e%60;return`Expires in ${i}:${r<10?"0":""}${r}`}_updateBody(t){let e=this.display==="inline"?this.shadow.getElementById("inline-body"):this.shadow.getElementById("modal-body");e&&(e.innerHTML=t)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-login",g);var x=`<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
510
510
  <path d="M48 24 H152 Q166 24 166 38 V108 Q166 156 100 182 Q34 156 34 108 V38 Q34 24 48 24 Z"
511
511
  fill="none" stroke="#fff" stroke-width="5"/>
512
512
  <g fill="#fff" opacity="0.2">
@@ -672,7 +672,7 @@
672
672
  <div class="spinner-wrap">${M}</div>
673
673
  <p class="status-text">Generating access QR...</p>
674
674
  </div>
675
- `;case"active":{let i=e?.claimUrl?`https://api.qrserver.com/v1/create-qr-code/?size=440x440&data=${encodeURIComponent(e.claimUrl)}`:"",s=e?.useCount??0,r=e?.maxUses??1,h=r>1?`<div class="use-badge">${s}/${r} claimed</div>`:"";return`
675
+ `;case"active":{let i=e?.claimUrl?`https://api.qrserver.com/v1/create-qr-code/?size=440x440&data=${encodeURIComponent(e.claimUrl)}`:"",r=e?.useCount??0,s=e?.maxUses??1,h=s>1?`<div class="use-badge">${r}/${s} claimed</div>`:"";return`
676
676
  <h3>Scan to Access</h3>
677
677
  <p class="subtitle">Present this QR code to gain entry</p>
678
678
  <div class="qr-frame">
@@ -890,7 +890,7 @@
890
890
  }
891
891
  .footer a { color: var(--_disabled); text-decoration: none; }
892
892
  .footer a:hover { text-decoration: underline; }
893
- `}_bindButtonEvents(){this.shadow.getElementById("open-btn")?.addEventListener("click",()=>{this._openModal(),this._startSession()});let e=this.shadow.getElementById("overlay");e?.addEventListener("click",i=>{i.target===e&&this._closeModal()}),this._bindModalButtons()}_bindInlineEvents(){this.shadow.getElementById("start-btn")?.addEventListener("click",()=>{this._startSession()})}_bindModalButtons(){this.shadow.getElementById("close-btn")?.addEventListener("click",()=>this._closeModal()),this.shadow.getElementById("retry-btn")?.addEventListener("click",()=>this._retry())}_openModal(){this.shadow.getElementById("overlay")?.classList.remove("hidden"),this._updateBody(this._bodyForStatus("idle"))}_closeModal(){this.shadow.getElementById("overlay")?.classList.add("hidden"),this._clearTimers(),this._sessionId=null,this._claimUrl=null,this._status="idle",this._expiresAt=null}async _startSession(){this._status="loading",this._updateBody(this._bodyForStatus("loading"));try{let t=await fetch(`${this.baseUrl}/api/v1/ephemeral`,{method:"POST",headers:{"Content-Type":"application/json","X-Client-Id":this.tenant},body:JSON.stringify({scopes:this.scopes.split(/\s+/).filter(Boolean),ttl:this.ttl,maxUses:this.maxUses,deviceBinding:this.deviceBinding})});if(!t.ok){let i=await t.json().catch(()=>({message:"Failed to create session"}));throw new Error(i.message??"Failed to create session")}let e=await t.json();this._sessionId=e.sessionId,this._claimUrl=e.claimUrl,this._expiresAt=new Date(e.expiresAt).getTime(),this._status="active",this._updateBody(this._bodyForStatus("active",{claimUrl:e.claimUrl,timerHtml:this._formatTimer(),useCount:0,maxUses:e.maxUses})),this._bindModalButtons(),this._startCountdown(),this._beginPolling(e.sessionId)}catch(t){let e=t instanceof Error?t.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:e})),this._bindModalButtons(),this.emit("qrauth:error",{message:e})}}_retry(){if(this._clearTimers(),this._sessionId=null,this._claimUrl=null,this._status="idle",this._expiresAt=null,this.display==="inline"){let t=this.shadow.getElementById("inline-body");t&&(t.innerHTML=this._inlineIdleContent(),this._bindInlineEvents())}else this._startSession()}_beginPolling(t){let e=2e3,i=5e3,s=0,r=20,d=async()=>{if(this._sessionId)try{let l=await(await fetch(`${this.baseUrl}/api/v1/ephemeral/${t}`,{headers:{"X-Client-Id":this.tenant}})).json();if(s=0,e=2e3,this._handleStatusChange(l.status,l),["CLAIMED","EXPIRED","REVOKED"].includes(l.status)){if(l.status==="CLAIMED"&&l.useCount<l.maxUses){this._updateBody(this._bodyForStatus("active",{claimUrl:this._claimUrl??void 0,timerHtml:this._formatTimer(),useCount:l.useCount,maxUses:l.maxUses})),this._bindModalButtons(),this._pollTimeout=setTimeout(d,e);return}return}this._pollTimeout=setTimeout(d,e)}catch{if(s++,s>=r){this._handleStatusChange("EXPIRED",void 0);return}e=Math.min(e*1.5,i),this._pollTimeout=setTimeout(d,e)}};this._pollTimeout=setTimeout(d,e)}_handleStatusChange(t,e){switch(t){case"CLAIMED":{this._clearTimers(),this._status="CLAIMED",this._updateBody(this._bodyForStatus("CLAIMED")),this._bindModalButtons(),this.emit("qrauth:claimed",{sessionId:e?.sessionId,status:e?.status,scopes:e?.scopes,metadata:e?.metadata,useCount:e?.useCount,maxUses:e?.maxUses}),this.display==="button"&&setTimeout(()=>this._closeModal(),2200);break}case"EXPIRED":this._clearTimers(),this._status="EXPIRED",this._updateBody(this._bodyForStatus("EXPIRED")),this._bindModalButtons(),this.emit("qrauth:expired");break;case"REVOKED":this._clearTimers(),this._status="REVOKED",this._updateBody(this._bodyForStatus("REVOKED")),this._bindModalButtons(),this.emit("qrauth:revoked");break}}_startCountdown(){this._timerInterval=setInterval(()=>{let t=this.shadow.getElementById("timer");if(!t||!this._expiresAt)return;let e=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));t.textContent=e>0?this._formatTimer(e):"",e<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,this._status==="active"&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(t){let e=t??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(e/60),s=e%60;return`Expires in ${i}:${s<10?"0":""}${s}`}_updateBody(t){let e=this.display==="inline"?this.shadow.getElementById("inline-body"):this.shadow.getElementById("modal-body");e&&(e.innerHTML=t)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-ephemeral",b);var f=`<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
893
+ `}_bindButtonEvents(){this.shadow.getElementById("open-btn")?.addEventListener("click",()=>{this._openModal(),this._startSession()});let e=this.shadow.getElementById("overlay");e?.addEventListener("click",i=>{i.target===e&&this._closeModal()}),this._bindModalButtons()}_bindInlineEvents(){this.shadow.getElementById("start-btn")?.addEventListener("click",()=>{this._startSession()})}_bindModalButtons(){this.shadow.getElementById("close-btn")?.addEventListener("click",()=>this._closeModal()),this.shadow.getElementById("retry-btn")?.addEventListener("click",()=>this._retry())}_openModal(){this.shadow.getElementById("overlay")?.classList.remove("hidden"),this._updateBody(this._bodyForStatus("idle"))}_closeModal(){this.shadow.getElementById("overlay")?.classList.add("hidden"),this._clearTimers(),this._sessionId=null,this._claimUrl=null,this._status="idle",this._expiresAt=null}async _startSession(){this._status="loading",this._updateBody(this._bodyForStatus("loading"));try{let t=await fetch(`${this.baseUrl}/api/v1/ephemeral`,{method:"POST",headers:{"Content-Type":"application/json","X-Client-Id":this.tenant},body:JSON.stringify({scopes:this.scopes.split(/\s+/).filter(Boolean),ttl:this.ttl,maxUses:this.maxUses,deviceBinding:this.deviceBinding})});if(!t.ok){let i=await t.json().catch(()=>({message:"Failed to create session"}));throw new Error(i.message??"Failed to create session")}let e=await t.json();this._sessionId=e.sessionId,this._claimUrl=e.claimUrl,this._expiresAt=new Date(e.expiresAt).getTime(),this._status="active",this._updateBody(this._bodyForStatus("active",{claimUrl:e.claimUrl,timerHtml:this._formatTimer(),useCount:0,maxUses:e.maxUses})),this._bindModalButtons(),this._startCountdown(),this._beginPolling(e.sessionId)}catch(t){let e=t instanceof Error?t.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:e})),this._bindModalButtons(),this.emit("qrauth:error",{message:e})}}_retry(){if(this._clearTimers(),this._sessionId=null,this._claimUrl=null,this._status="idle",this._expiresAt=null,this.display==="inline"){let t=this.shadow.getElementById("inline-body");t&&(t.innerHTML=this._inlineIdleContent(),this._bindInlineEvents())}else this._startSession()}_beginPolling(t){let e=2e3,i=5e3,r=0,s=20,d=async()=>{if(this._sessionId)try{let l=await(await fetch(`${this.baseUrl}/api/v1/ephemeral/${t}`,{headers:{"X-Client-Id":this.tenant}})).json();if(r=0,e=2e3,this._handleStatusChange(l.status,l),["CLAIMED","EXPIRED","REVOKED"].includes(l.status)){if(l.status==="CLAIMED"&&l.useCount<l.maxUses){this._updateBody(this._bodyForStatus("active",{claimUrl:this._claimUrl??void 0,timerHtml:this._formatTimer(),useCount:l.useCount,maxUses:l.maxUses})),this._bindModalButtons(),this._pollTimeout=setTimeout(d,e);return}return}this._pollTimeout=setTimeout(d,e)}catch{if(r++,r>=s){this._handleStatusChange("EXPIRED",void 0);return}e=Math.min(e*1.5,i),this._pollTimeout=setTimeout(d,e)}};this._pollTimeout=setTimeout(d,e)}_handleStatusChange(t,e){switch(t){case"CLAIMED":{this._clearTimers(),this._status="CLAIMED",this._updateBody(this._bodyForStatus("CLAIMED")),this._bindModalButtons(),this.emit("qrauth:claimed",{sessionId:e?.sessionId,status:e?.status,scopes:e?.scopes,metadata:e?.metadata,useCount:e?.useCount,maxUses:e?.maxUses}),this.display==="button"&&setTimeout(()=>this._closeModal(),2200);break}case"EXPIRED":this._clearTimers(),this._status="EXPIRED",this._updateBody(this._bodyForStatus("EXPIRED")),this._bindModalButtons(),this.emit("qrauth:expired");break;case"REVOKED":this._clearTimers(),this._status="REVOKED",this._updateBody(this._bodyForStatus("REVOKED")),this._bindModalButtons(),this.emit("qrauth:revoked");break}}_startCountdown(){this._timerInterval=setInterval(()=>{let t=this.shadow.getElementById("timer");if(!t||!this._expiresAt)return;let e=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));t.textContent=e>0?this._formatTimer(e):"",e<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,this._status==="active"&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(t){let e=t??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(e/60),r=e%60;return`Expires in ${i}:${r<10?"0":""}${r}`}_updateBody(t){let e=this.display==="inline"?this.shadow.getElementById("inline-body"):this.shadow.getElementById("modal-body");e&&(e.innerHTML=t)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-ephemeral",b);var f=`<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
894
894
  <path d="M48 24 H152 Q166 24 166 38 V108 Q166 156 100 182 Q34 156 34 108 V38 Q34 24 48 24 Z"
895
895
  fill="none" stroke="#fff" stroke-width="5"/>
896
896
  <g fill="#fff" opacity="0.2">
@@ -912,7 +912,7 @@
912
912
  </svg>`,q=`<svg class="spinner" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
913
913
  <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2.5"
914
914
  stroke-dasharray="31.416" stroke-dashoffset="10" stroke-linecap="round"/>
915
- </svg>`,m=class extends p{constructor(){super(...arguments);n(this,"_sessionId",null);n(this,"_sessionToken",null);n(this,"_codeVerifier",null);n(this,"_status","idle");n(this,"_expiresAt",null);n(this,"_timerInterval",null);n(this,"_pollTimeout",null)}static get observedAttributes(){return[...super.observedAttributes,"session-token","scopes","auto-start"]}get sessionToken(){return this.getAttribute("session-token")}get scopes(){return this.getAttribute("scopes")||"identity"}get autoStart(){return this.hasAttribute("auto-start")}get forceMode(){let t=this.getAttribute("force-mode");return t==="mobile"||t==="desktop"?t:"auto"}get mobileFallbackOnly(){return this.hasAttribute("mobile-fallback-only")}connectedCallback(){super.connectedCallback(),this.autoStart&&this._startAuth()}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}render(){this.shadow.innerHTML=`
915
+ </svg>`,m=class extends p{constructor(){super(...arguments);n(this,"_sessionId",null);n(this,"_sessionToken",null);n(this,"_codeVerifier",null);n(this,"_status","idle");n(this,"_expiresAt",null);n(this,"_timerInterval",null);n(this,"_pollTimeout",null)}static get observedAttributes(){return[...super.observedAttributes,"session-token","scopes","auto-start","redirect-uri"]}get sessionToken(){return this.getAttribute("session-token")}get scopes(){return this.getAttribute("scopes")||"identity"}get autoStart(){return this.hasAttribute("auto-start")}get redirectUri(){return this.getAttribute("redirect-uri")}get forceMode(){let t=this.getAttribute("force-mode");return t==="mobile"||t==="desktop"?t:"auto"}get mobileFallbackOnly(){return this.hasAttribute("mobile-fallback-only")}connectedCallback(){super.connectedCallback(),this.autoStart&&this._startAuth()}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}render(){this.shadow.innerHTML=`
916
916
  <style>
917
917
  ${this.getBaseStyles()}
918
918
  :host { display: block; }
@@ -1009,13 +1009,13 @@
1009
1009
  <div class="spinner-wrap">${q}</div>
1010
1010
  <p class="status-text">Generating QR code...</p>
1011
1011
  </div>
1012
- `;case"pending":case"SCANNED":{if(this.isMobileLike())return this._mobilePendingBody(t,e,i);let s=t==="SCANNED";return`
1012
+ `;case"pending":case"SCANNED":{if(this.isMobileLike())return this._mobilePendingBody(t,e,i);let r=t==="SCANNED";return`
1013
1013
  ${i}
1014
1014
  <h3>Scan to verify</h3>
1015
1015
  <p class="subtitle">Use your phone camera to complete verification</p>
1016
- ${this._qrFrameHtml(e?.qrUrl??"",s)}
1017
- <div class="status-text ${s?"status-scanned":""}">
1018
- ${s?"&#10003; Scanned &mdash; waiting for approval...":"Waiting for scan..."}
1016
+ ${this._qrFrameHtml(e?.qrUrl??"",r)}
1017
+ <div class="status-text ${r?"status-scanned":""}">
1018
+ ${r?"&#10003; Scanned &mdash; waiting for approval...":"Waiting for scan..."}
1019
1019
  </div>
1020
1020
  <div class="timer" id="timer">${e?.timerHtml??""}</div>
1021
1021
  <div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
@@ -1052,7 +1052,7 @@
1052
1052
  <div class="qr-badge">${f}</div>
1053
1053
  ${e?'<div class="scanned-overlay"><span class="scanned-check">'+y+"</span></div>":""}
1054
1054
  </div>
1055
- `}_mobilePendingBody(t,e,i=""){let s=t==="SCANNED";return`
1055
+ `}_mobilePendingBody(t,e,i=""){let r=t==="SCANNED";return`
1056
1056
  ${i}
1057
1057
  <h3>Verify your identity</h3>
1058
1058
  <p class="subtitle">Tap continue to verify on this device.</p>
@@ -1060,13 +1060,13 @@
1060
1060
  <span class="btn-icon">${f}</span>
1061
1061
  Continue with QRAuth
1062
1062
  </button>
1063
- <div class="status-text ${s?"status-scanned":""}">
1064
- ${s?"&#10003; Scanned &mdash; waiting for approval...":"Waiting for approval..."}
1063
+ <div class="status-text ${r?"status-scanned":""}">
1064
+ ${r?"&#10003; Scanned &mdash; waiting for approval...":"Waiting for approval..."}
1065
1065
  </div>
1066
1066
  <div class="timer" id="timer">${e?.timerHtml??""}</div>
1067
1067
  <details class="alt-device">
1068
1068
  <summary>Use another device</summary>
1069
- ${this._qrFrameHtml(e?.qrUrl??"",s)}
1069
+ ${this._qrFrameHtml(e?.qrUrl??"",r)}
1070
1070
  </details>
1071
1071
  <div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
1072
1072
  `}_sharedStyles(){return`
@@ -1300,5 +1300,5 @@
1300
1300
  }
1301
1301
  .footer a { color: var(--_disabled); text-decoration: none; }
1302
1302
  .footer a:hover { text-decoration: underline; }
1303
- `}_bindBodyEvents(){this.shadow.getElementById("start-btn")?.addEventListener("click",()=>this._startAuth()),this.shadow.getElementById("retry-btn")?.addEventListener("click",()=>this._retry()),this.shadow.getElementById("mobile-cta")?.addEventListener("click",()=>this._openApprovalPage())}_openApprovalPage(){if(!this._sessionToken)return;let t=`${this.baseUrl}/a/${this._sessionToken}`;window.open(t,"_blank","noopener")||(window.location.href=t)}async _startAuth(){this._status="loading",this._updateBody(this._bodyForStatus("loading"));try{this._codeVerifier=await this.generateCodeVerifier();let t=await this.computeCodeChallenge(this._codeVerifier),e={"Content-Type":"application/json","X-Client-Id":this.tenant},i={scopes:this.scopes.split(/\s+/).filter(Boolean),codeChallenge:t,codeChallengeMethod:"S256"},s=await fetch(`${this.baseUrl}/api/v1/auth-sessions`,{method:"POST",headers:e,body:JSON.stringify(i)});if(!s.ok){let d=await s.json().catch(()=>({message:"Failed to create session"}));throw new Error(d.message??"Failed to create session")}let r=await s.json();this._sessionId=r.sessionId,this._sessionToken=r.token||(r.qrUrl?r.qrUrl.split("/").pop()??null:null),this._expiresAt=new Date(r.expiresAt).getTime(),this._status="pending",this._updateBody(this._bodyForStatus("pending",{qrUrl:r.qrUrl})),this._bindBodyEvents(),this._startCountdown(),this._beginPolling(r.sessionId)}catch(t){let e=t instanceof Error?t.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:e})),this._bindBodyEvents(),this.emit("qrauth:error",{message:e})}}_retry(){this._clearTimers(),this._sessionId=null,this._sessionToken=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null,this._updateBody(this._bodyForStatus("idle")),this._bindBodyEvents()}_beginPolling(t){let e="PENDING",i=2e3,s=5e3,r=0,d=20,h=async()=>{if(!this._sessionId)return;let l=`${this.baseUrl}/api/v1/auth-sessions/${t}`;this._codeVerifier&&(l+=`?code_verifier=${encodeURIComponent(this._codeVerifier)}`);try{let c=await(await fetch(l,{headers:{"X-Client-Id":this.tenant}})).json();if(r=0,i=2e3,c.status!==e&&(e=c.status,this._handleStatusChange(c.status,c),["APPROVED","DENIED","EXPIRED"].includes(c.status)))return;this._pollTimeout=setTimeout(h,i)}catch{if(r++,r>=d){this._handleStatusChange("EXPIRED",void 0);return}i=Math.min(i*1.5,s),this._pollTimeout=setTimeout(h,i)}};this._pollTimeout=setTimeout(h,i)}_handleStatusChange(t,e){switch(this._status=t,t){case"SCANNED":{let i=this.shadow.querySelector(".qr-frame img")?.src,s=i?decodeURIComponent(i.replace(/.*[?&]data=/,"").split("&")[0]):"";this._updateBody(this._bodyForStatus("SCANNED",{qrUrl:s,timerHtml:this._formatTimer()})),this._bindBodyEvents();break}case"APPROVED":{this._clearTimers(),this._updateBody(this._bodyForStatus("APPROVED")),this.emit("qrauth:verified",{sessionId:e?.sessionId,user:e?.user,signature:e?.signature});break}case"DENIED":this._clearTimers(),this._updateBody(this._bodyForStatus("DENIED")),this._bindBodyEvents(),this.emit("qrauth:denied");break;case"EXPIRED":this._clearTimers(),this._status="EXPIRED",this._updateBody(this._bodyForStatus("EXPIRED")),this._bindBodyEvents(),this.emit("qrauth:expired");break}}_startCountdown(){this._timerInterval=setInterval(()=>{let t=this.shadow.getElementById("timer");if(!t||!this._expiresAt)return;let e=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));t.textContent=e>0?this._formatTimer(e):"",e<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,(this._status==="pending"||this._status==="SCANNED")&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(t){let e=t??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(e/60),s=e%60;return`Expires in ${i}:${s<10?"0":""}${s}`}_updateBody(t){let e=this.shadow.getElementById("body");e&&(e.innerHTML=t)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-2fa",m);return C($);})();
1303
+ `}_bindBodyEvents(){this.shadow.getElementById("start-btn")?.addEventListener("click",()=>this._startAuth()),this.shadow.getElementById("retry-btn")?.addEventListener("click",()=>this._retry()),this.shadow.getElementById("mobile-cta")?.addEventListener("click",()=>this._openApprovalPage())}_openApprovalPage(){if(!this._sessionToken)return;let t=`${this.baseUrl}/a/${this._sessionToken}`;window.open(t,"_blank","noopener")||(window.location.href=t)}async _startAuth(){this._status="loading",this._updateBody(this._bodyForStatus("loading"));try{this._codeVerifier=await this.generateCodeVerifier();let t=await this.computeCodeChallenge(this._codeVerifier),e={"Content-Type":"application/json","X-Client-Id":this.tenant},i={scopes:this.scopes.split(/\s+/).filter(Boolean),codeChallenge:t,codeChallengeMethod:"S256"};this.redirectUri&&(i.redirectUrl=this.redirectUri);let r=await fetch(`${this.baseUrl}/api/v1/auth-sessions`,{method:"POST",headers:e,body:JSON.stringify(i)});if(!r.ok){let d=await r.json().catch(()=>({message:"Failed to create session"}));throw new Error(d.message??"Failed to create session")}let s=await r.json();this._sessionId=s.sessionId,this._sessionToken=s.token||(s.qrUrl?s.qrUrl.split("/").pop()??null:null),this._expiresAt=new Date(s.expiresAt).getTime(),this._status="pending",this._updateBody(this._bodyForStatus("pending",{qrUrl:s.qrUrl})),this._bindBodyEvents(),this._startCountdown(),this._beginPolling(s.sessionId)}catch(t){let e=t instanceof Error?t.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:e})),this._bindBodyEvents(),this.emit("qrauth:error",{message:e})}}_retry(){this._clearTimers(),this._sessionId=null,this._sessionToken=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null,this._updateBody(this._bodyForStatus("idle")),this._bindBodyEvents()}_beginPolling(t){let e="PENDING",i=2e3,r=5e3,s=0,d=20,h=async()=>{if(!this._sessionId)return;let l=`${this.baseUrl}/api/v1/auth-sessions/${t}`;this._codeVerifier&&(l+=`?code_verifier=${encodeURIComponent(this._codeVerifier)}`);try{let c=await(await fetch(l,{headers:{"X-Client-Id":this.tenant}})).json();if(s=0,i=2e3,c.status!==e&&(e=c.status,this._handleStatusChange(c.status,c),["APPROVED","DENIED","EXPIRED"].includes(c.status)))return;this._pollTimeout=setTimeout(h,i)}catch{if(s++,s>=d){this._handleStatusChange("EXPIRED",void 0);return}i=Math.min(i*1.5,r),this._pollTimeout=setTimeout(h,i)}};this._pollTimeout=setTimeout(h,i)}_handleStatusChange(t,e){switch(this._status=t,t){case"SCANNED":{let i=this.shadow.querySelector(".qr-frame img")?.src,r=i?decodeURIComponent(i.replace(/.*[?&]data=/,"").split("&")[0]):"";this._updateBody(this._bodyForStatus("SCANNED",{qrUrl:r,timerHtml:this._formatTimer()})),this._bindBodyEvents();break}case"APPROVED":{this._clearTimers(),this._updateBody(this._bodyForStatus("APPROVED")),this.emit("qrauth:verified",{sessionId:e?.sessionId,user:e?.user,signature:e?.signature});break}case"DENIED":this._clearTimers(),this._updateBody(this._bodyForStatus("DENIED")),this._bindBodyEvents(),this.emit("qrauth:denied");break;case"EXPIRED":this._clearTimers(),this._status="EXPIRED",this._updateBody(this._bodyForStatus("EXPIRED")),this._bindBodyEvents(),this.emit("qrauth:expired");break}}_startCountdown(){this._timerInterval=setInterval(()=>{let t=this.shadow.getElementById("timer");if(!t||!this._expiresAt)return;let e=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));t.textContent=e>0?this._formatTimer(e):"",e<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,(this._status==="pending"||this._status==="SCANNED")&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(t){let e=t??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(e/60),r=e%60;return`Expires in ${i}:${r<10?"0":""}${r}`}_updateBody(t){let e=this.shadow.getElementById("body");e&&(e.innerHTML=t)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-2fa",m);return C($);})();
1304
1304
  //# sourceMappingURL=qrauth-components.js.map
package/dist/twofa.d.ts CHANGED
@@ -23,6 +23,12 @@ import { QRAuthElement } from './base.js';
23
23
  * alternative same-device verification path and the
24
24
  * component is only there as the cross-device scan
25
25
  * option.
26
+ * redirect-uri — Where the QRAuth hosted approval page should send
27
+ * the user after they tap Approve on mobile. Must
28
+ * match one of the app's registered `redirectUrls`.
29
+ * Without this, the mobile-CTA new tab ends on a
30
+ * "you can close this page" dead end. See
31
+ * <qrauth-login> JSDoc for full semantics.
26
32
  *
27
33
  * Events (bubbles + composed, cross shadow-DOM):
28
34
  * qrauth:verified — detail: { sessionId, user, signature }
@@ -46,6 +52,8 @@ export declare class QRAuthTwoFA extends QRAuthElement {
46
52
  get sessionToken(): string | null;
47
53
  get scopes(): string;
48
54
  get autoStart(): boolean;
55
+ /** See <qrauth-login>.redirectUri for semantics. */
56
+ get redirectUri(): string | null;
49
57
  get forceMode(): 'mobile' | 'desktop' | 'auto';
50
58
  get mobileFallbackOnly(): boolean;
51
59
  connectedCallback(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qrauth/web-components",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "QRAuth Web Components — drop-in authentication elements",
5
5
  "type": "module",
6
6
  "main": "dist/qrauth-components.js",