@qrauth/web-components 0.2.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,15 @@
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
+
8
+ ## 0.3.0
9
+
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.
11
+ - `<qrauth-ephemeral>`: adds `force-mode` and `mobile-fallback-only` attributes for forward compatibility. No default mobile UI change — the ephemeral flow is a QR meant to be scanned by another device, so QR-first remains correct on all viewports.
12
+
3
13
  ## 0.2.0 — 2026-04-18
4
14
 
5
15
  ### Minor
@@ -4,14 +4,25 @@ import { QRAuthElement } from './base.js';
4
4
  * and claim status updates.
5
5
  *
6
6
  * Attributes:
7
- * tenant — Your QRAuth App client ID (required)
8
- * theme — "light" (default) | "dark"
9
- * base-url — API base URL (default: https://qrauth.io)
10
- * scopes — Space-separated scope strings (default: "access")
11
- * ttl — TTL string like "30m", "6h", "1d" (default: "30m")
12
- * max-uses — Maximum number of claims (default: "1")
13
- * device-binding — Boolean attribute; present = device binding enabled
14
- * display — "button" | "inline" (default: "inline")
7
+ * tenant — Your QRAuth App client ID (required)
8
+ * theme — "light" (default) | "dark"
9
+ * base-url — API base URL (default: https://qrauth.io)
10
+ * scopes — Space-separated scope strings (default: "access")
11
+ * ttl — TTL string like "30m", "6h", "1d" (default: "30m")
12
+ * max-uses — Maximum number of claims (default: "1")
13
+ * device-binding — Boolean attribute; present = device binding enabled
14
+ * display — "button" | "inline" (default: "inline")
15
+ * force-mode — Reserved for future mobile-variant UI; no default
16
+ * behavior change on mobile viewports as of 0.3.0.
17
+ * The ephemeral flow is inherently "generate QR for
18
+ * another device to scan", so QR-first is correct
19
+ * regardless of form factor. Accepted values:
20
+ * "mobile" | "desktop" | "auto" (default "auto").
21
+ * mobile-fallback-only — Reserved for forward compatibility with the
22
+ * pattern used by <qrauth-login> and <qrauth-2fa>.
23
+ * No effect in 0.3.0 — included so consumers can
24
+ * start passing it without breakage when a future
25
+ * mobile-variant UI is added.
15
26
  *
16
27
  * Events (bubbles + composed, cross shadow-DOM):
17
28
  * qrauth:claimed — detail: { sessionId, status, scopes, metadata, useCount, maxUses }
@@ -36,6 +47,12 @@ export declare class QRAuthEphemeral extends QRAuthElement {
36
47
  get maxUses(): number;
37
48
  get deviceBinding(): boolean;
38
49
  get display(): string;
50
+ /**
51
+ * Reserved attribute — no effect in 0.3.0. See JSDoc header above for why
52
+ * the ephemeral flow intentionally keeps the QR-first body on mobile.
53
+ */
54
+ get forceMode(): 'mobile' | 'desktop' | 'auto';
55
+ get mobileFallbackOnly(): boolean;
39
56
  disconnectedCallback(): void;
40
57
  protected render(): void;
41
58
  private _renderButton;
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.2.0 — https://qrauth.io */
2
- var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configurable:!0,writable:!0,value:e}):h[d]=e;var n=(h,d,e)=>E(h,typeof d!="symbol"?d+"":d,e);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 e=this.getAttribute("base-url");return e!==null?e:"https://qrauth.io"}connectedCallback(){this.render()}disconnectedCallback(){this.cleanup()}attributeChangedCallback(e,t,i){t!==i&&this.render()}isMobileLike(){if(typeof window>"u")return!1;let e=this.getAttribute("force-mode");if(e==="mobile")return!0;if(e==="desktop"||this.hasAttribute("mobile-fallback-only"))return!1;if(typeof window.matchMedia=="function"){let t=window.matchMedia("(pointer: coarse)").matches,i=window.matchMedia("(hover: none)").matches;if(t&&i)return!0}return/Android|iPhone|iPad|iPod|Mobile|Tablet|BlackBerry|Opera Mini/i.test(navigator.userAgent)}connectSSE(e){this.disconnectSSE(),this._sseConnection=new EventSource(e),this._sseConnection.onerror=()=>{this.disconnectSSE()}}onSSEEvent(e,t){this._sseConnection&&this._sseConnection.addEventListener(e,i=>{let s=i;try{t(JSON.parse(s.data))}catch{}})}disconnectSSE(){this._sseConnection&&(this._sseConnection.close(),this._sseConnection=null)}startPolling(e,t,i,s){this.stopPolling();let r=async()=>{try{let a=await fetch(e,{headers:s});a.ok&&i(await a.json())}catch{}};r(),this._pollInterval=setInterval(r,t)}stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}cleanup(){this.disconnectSSE(),this.stopPolling()}emit(e,t){this.dispatchEvent(new CustomEvent(e,{bubbles:!0,composed:!0,detail:t}))}async generateCodeVerifier(){let e=new Uint8Array(32);return crypto.getRandomValues(e),this._base64url(e)}async computeCodeChallenge(e){let i=new TextEncoder().encode(e),s=await crypto.subtle.digest("SHA-256",i);return this._base64url(new Uint8Array(s))}_base64url(e){let t=Array.from(e,i=>String.fromCodePoint(i)).join("");return btoa(t).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);
@@ -49,10 +49,10 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
49
49
  stroke-linecap="round" stroke-linejoin="round"/>
50
50
  </svg>`,f=`<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
51
51
  <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" fill="currentColor"/>
52
- </svg>`,k=`<svg class="spinner" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
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 e=this.getAttribute("force-mode");return e==="mobile"||e==="desktop"?e:"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; }
@@ -185,7 +185,7 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
185
185
  <div id="modal-body">
186
186
  ${this._bodyForStatus("idle")}
187
187
  </div>
188
- `}_bodyForStatus(e,t){switch(e){case"idle":return`
188
+ `}_bodyForStatus(t,e){switch(t){case"idle":return`
189
189
  <div class="state-header">
190
190
  <div class="brand-icon">${u}</div>
191
191
  <h3>Sign in with QRAuth</h3>
@@ -193,17 +193,17 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
193
193
  </div>
194
194
  `;case"loading":return`
195
195
  <div class="state-loading">
196
- <div class="spinner-wrap">${k}</div>
196
+ <div class="spinner-wrap">${E}</div>
197
197
  <p class="status-text">Generating QR code...</p>
198
198
  </div>
199
- `;case"pending":case"SCANNED":{if(this.isMobileLike())return this._mobilePendingBody(e,t);let i=e==="SCANNED";return`
199
+ `;case"pending":case"SCANNED":{if(this.isMobileLike())return this._mobilePendingBody(t,e);let i=t==="SCANNED";return`
200
200
  <h3>Sign in with QRAuth</h3>
201
201
  <p class="subtitle">Scan this QR code with your phone camera</p>
202
- ${this._qrFrameHtml(t?.qrUrl??"",i)}
202
+ ${this._qrFrameHtml(e?.qrUrl??"",i)}
203
203
  <div class="status-text ${i?"status-scanned":""}">
204
204
  ${i?"&#10003; QR code scanned &mdash; waiting for approval...":"Waiting for scan..."}
205
205
  </div>
206
- <div class="timer" id="timer">${t?.timerHtml??""}</div>
206
+ <div class="timer" id="timer">${e?.timerHtml??""}</div>
207
207
  <div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
208
208
  `}case"APPROVED":return`
209
209
  <div class="state-approved">
@@ -229,16 +229,16 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
229
229
  <div class="state-error">
230
230
  <div class="error-icon">&#9888;</div>
231
231
  <h3>Something went wrong</h3>
232
- <p class="subtitle">${t?.errorMessage??"Failed to start authentication."}</p>
232
+ <p class="subtitle">${e?.errorMessage??"Failed to start authentication."}</p>
233
233
  <button class="retry-btn" id="retry-btn">Try again</button>
234
234
  </div>
235
- `;default:return""}}_qrFrameHtml(e,t){let i=e?`https://api.qrserver.com/v1/create-qr-code/?size=440x440&data=${encodeURIComponent(e)}`:"";return`
236
- <div class="qr-frame ${t?"scanned":""}${this.animated?" animated":""}">
235
+ `;default:return""}}_qrFrameHtml(t,e){let i=t?`https://api.qrserver.com/v1/create-qr-code/?size=440x440&data=${encodeURIComponent(t)}`:"";return`
236
+ <div class="qr-frame ${e?"scanned":""}${this.animated?" animated":""}">
237
237
  <img src="${i}" alt="QR Code for authentication" width="220" height="220" loading="lazy"/>
238
238
  <div class="qr-badge">${u}</div>
239
- ${t?'<div class="scanned-overlay"><span class="scanned-check">'+f+"</span></div>":""}
239
+ ${e?'<div class="scanned-overlay"><span class="scanned-check">'+f+"</span></div>":""}
240
240
  </div>
241
- `}_mobilePendingBody(e,t){let i=e==="SCANNED";return`
241
+ `}_mobilePendingBody(t,e){let i=t==="SCANNED";return`
242
242
  <h3>Sign in with QRAuth</h3>
243
243
  <p class="subtitle">Tap continue to verify on this device.</p>
244
244
  <button class="mobile-cta" id="mobile-cta" type="button">
@@ -248,10 +248,10 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
248
248
  <div class="status-text ${i?"status-scanned":""}">
249
249
  ${i?"&#10003; Scanned &mdash; waiting for approval...":"Waiting for approval..."}
250
250
  </div>
251
- <div class="timer" id="timer">${t?.timerHtml??""}</div>
251
+ <div class="timer" id="timer">${e?.timerHtml??""}</div>
252
252
  <details class="alt-device">
253
253
  <summary>Use another device</summary>
254
- ${this._qrFrameHtml(t?.qrUrl??"",i)}
254
+ ${this._qrFrameHtml(e?.qrUrl??"",i)}
255
255
  </details>
256
256
  <div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
257
257
  `}_sharedModalStyles(){return`
@@ -506,7 +506,7 @@ var w=Object.defineProperty;var E=(h,d,e)=>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 t=this.shadow.getElementById("overlay");t?.addEventListener("click",i=>{i.target===t&&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 e=`${this.baseUrl}/a/${this._sessionToken}`;window.open(e,"_blank","noopener")||(window.location.href=e)}_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 e=await this.computeCodeChallenge(this._codeVerifier),t={"Content-Type":"application/json","X-Client-Id":this.tenant},i={scopes:this.scopes.split(/\s+/).filter(Boolean),codeChallenge:e,codeChallengeMethod:"S256"},s=await fetch(`${this.baseUrl}/api/v1/auth-sessions`,{method:"POST",headers:t,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(e){let t=e instanceof Error?e.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:t})),this._bindModalButtons(),this.emit("qrauth:error",{message:t})}}_retry(){if(this._clearTimers(),this._sessionId=null,this._sessionToken=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null,this.display==="inline"){let e=this.shadow.getElementById("inline-body");e&&(e.innerHTML=this._inlineIdleContent(),this._bindInlineEvents())}else this._startAuth()}_beginPolling(e){let t="PENDING",i=2e3,s=5e3,r=0,a=20,c=async()=>{if(!this._sessionId)return;let o=`${this.baseUrl}/api/v1/auth-sessions/${e}`;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!==t&&(t=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(e,t){switch(this._status=e,e){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",t);break}case"APPROVED":{this._clearTimers(),this._updateBody(this._bodyForStatus("APPROVED")),this._bindModalButtons(),this.emit("qrauth:authenticated",{sessionId:t?.sessionId,user:t?.user,signature:t?.signature});let i=this.onAuth;if(i){let s=window[i];typeof s=="function"&&s(t)}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 e=this.shadow.getElementById("timer");if(!e||!this._expiresAt)return;let t=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));e.textContent=t>0?this._formatTimer(t):"",t<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,(this._status==="pending"||this._status==="SCANNED")&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(e){let t=e??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(t/60),s=t%60;return`Expires in ${i}:${s<10?"0":""}${s}`}_updateBody(e){let t=this.display==="inline"?this.shadow.getElementById("inline-body"):this.shadow.getElementById("modal-body");t&&(t.innerHTML=e)}_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">
@@ -528,7 +528,7 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
528
528
  </svg>`,S=`<svg class="spinner" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
529
529
  <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2.5"
530
530
  stroke-dasharray="31.416" stroke-dashoffset="10" stroke-linecap="round"/>
531
- </svg>`,m=class extends p{constructor(){super(...arguments);n(this,"_sessionId",null);n(this,"_claimUrl",null);n(this,"_status","idle");n(this,"_expiresAt",null);n(this,"_timerInterval",null);n(this,"_pollTimeout",null)}static get observedAttributes(){return[...super.observedAttributes,"scopes","ttl","max-uses","device-binding","display"]}get scopes(){return this.getAttribute("scopes")||"access"}get ttl(){return this.getAttribute("ttl")||"30m"}get maxUses(){return parseInt(this.getAttribute("max-uses")||"1",10)}get deviceBinding(){return this.hasAttribute("device-binding")}get display(){return this.getAttribute("display")||"inline"}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}render(){this.display==="button"?this._renderButton():this._renderInline()}_renderButton(){this.shadow.innerHTML=`
531
+ </svg>`,m=class extends p{constructor(){super(...arguments);n(this,"_sessionId",null);n(this,"_claimUrl",null);n(this,"_status","idle");n(this,"_expiresAt",null);n(this,"_timerInterval",null);n(this,"_pollTimeout",null)}static get observedAttributes(){return[...super.observedAttributes,"scopes","ttl","max-uses","device-binding","display"]}get scopes(){return this.getAttribute("scopes")||"access"}get ttl(){return this.getAttribute("ttl")||"30m"}get maxUses(){return parseInt(this.getAttribute("max-uses")||"1",10)}get deviceBinding(){return this.hasAttribute("device-binding")}get display(){return this.getAttribute("display")||"inline"}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==="button"?this._renderButton():this._renderInline()}_renderButton(){this.shadow.innerHTML=`
532
532
  <style>
533
533
  ${this.getBaseStyles()}
534
534
  :host { display: inline-block; }
@@ -661,7 +661,7 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
661
661
  <div id="modal-body">
662
662
  ${this._bodyForStatus("idle")}
663
663
  </div>
664
- `}_bodyForStatus(e,t){switch(e){case"idle":return`
664
+ `}_bodyForStatus(t,e){switch(t){case"idle":return`
665
665
  <div class="state-header">
666
666
  <div class="brand-icon">${b}</div>
667
667
  <h3>Ephemeral Access</h3>
@@ -672,7 +672,7 @@ var w=Object.defineProperty;var E=(h,d,e)=>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=t?.claimUrl?`https://api.qrserver.com/v1/create-qr-code/?size=440x440&data=${encodeURIComponent(t.claimUrl)}`:"",s=t?.useCount??0,r=t?.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">
@@ -680,7 +680,7 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
680
680
  <div class="qr-badge">${b}</div>
681
681
  </div>
682
682
  ${c}
683
- <div class="timer" id="timer">${t?.timerHtml??""}</div>
683
+ <div class="timer" id="timer">${e?.timerHtml??""}</div>
684
684
  <div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
685
685
  `}case"CLAIMED":return`
686
686
  <div class="state-claimed">
@@ -705,7 +705,7 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
705
705
  <div class="state-error">
706
706
  <div class="error-icon">&#9888;</div>
707
707
  <h3>Something went wrong</h3>
708
- <p class="subtitle">${t?.errorMessage??"Failed to create ephemeral session."}</p>
708
+ <p class="subtitle">${e?.errorMessage??"Failed to create ephemeral session."}</p>
709
709
  <button class="retry-btn" id="retry-btn">Try again</button>
710
710
  </div>
711
711
  `;default:return""}}_sharedModalStyles(){return`
@@ -890,7 +890,7 @@ var w=Object.defineProperty;var E=(h,d,e)=>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 t=this.shadow.getElementById("overlay");t?.addEventListener("click",i=>{i.target===t&&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 e=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(!e.ok){let i=await e.json().catch(()=>({message:"Failed to create session"}));throw new Error(i.message??"Failed to create session")}let t=await e.json();this._sessionId=t.sessionId,this._claimUrl=t.claimUrl,this._expiresAt=new Date(t.expiresAt).getTime(),this._status="active",this._updateBody(this._bodyForStatus("active",{claimUrl:t.claimUrl,timerHtml:this._formatTimer(),useCount:0,maxUses:t.maxUses})),this._bindModalButtons(),this._startCountdown(),this._beginPolling(t.sessionId)}catch(e){let t=e instanceof Error?e.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:t})),this._bindModalButtons(),this.emit("qrauth:error",{message:t})}}_retry(){if(this._clearTimers(),this._sessionId=null,this._claimUrl=null,this._status="idle",this._expiresAt=null,this.display==="inline"){let e=this.shadow.getElementById("inline-body");e&&(e.innerHTML=this._inlineIdleContent(),this._bindInlineEvents())}else this._startSession()}_beginPolling(e){let t=2e3,i=5e3,s=0,r=20,a=async()=>{if(this._sessionId)try{let o=await(await fetch(`${this.baseUrl}/api/v1/ephemeral/${e}`,{headers:{"X-Client-Id":this.tenant}})).json();if(s=0,t=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,t);return}return}this._pollTimeout=setTimeout(a,t)}catch{if(s++,s>=r){this._handleStatusChange("EXPIRED",void 0);return}t=Math.min(t*1.5,i),this._pollTimeout=setTimeout(a,t)}};this._pollTimeout=setTimeout(a,t)}_handleStatusChange(e,t){switch(e){case"CLAIMED":{this._clearTimers(),this._status="CLAIMED",this._updateBody(this._bodyForStatus("CLAIMED")),this._bindModalButtons(),this.emit("qrauth:claimed",{sessionId:t?.sessionId,status:t?.status,scopes:t?.scopes,metadata:t?.metadata,useCount:t?.useCount,maxUses:t?.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 e=this.shadow.getElementById("timer");if(!e||!this._expiresAt)return;let t=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));e.textContent=t>0?this._formatTimer(t):"",t<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,this._status==="active"&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(e){let t=e??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(t/60),s=t%60;return`Expires in ${i}:${s<10?"0":""}${s}`}_updateBody(e){let t=this.display==="inline"?this.shadow.getElementById("inline-body"):this.shadow.getElementById("modal-body");t&&(t.innerHTML=e)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-ephemeral",m);var x=`<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 E=(h,d,e)=>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>`,v=class extends p{constructor(){super(...arguments);n(this,"_sessionId",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")}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; }
@@ -987,20 +987,20 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
987
987
  ${this._bodyForStatus("idle")}
988
988
  </div>
989
989
  </div>
990
- `,this._bindBodyEvents()}_bodyForStatus(e,t){let i=`
990
+ `,this._bindBodyEvents()}_bodyForStatus(t,e){let i=`
991
991
  <div class="step-label">
992
992
  <span class="step-dot"></span>
993
993
  Step 2 of 2 &mdash; Verify identity
994
994
  </div>
995
- `;switch(e){case"idle":return`
995
+ `;switch(t){case"idle":return`
996
996
  ${i}
997
997
  <div class="state-header">
998
- <div class="brand-icon">${x}</div>
998
+ <div class="brand-icon">${v}</div>
999
999
  <h3>Verify your identity</h3>
1000
1000
  <p class="subtitle">Scan the QR code with your phone to confirm it&apos;s you</p>
1001
1001
  </div>
1002
1002
  <button class="start-btn" id="start-btn" aria-label="Start 2FA verification">
1003
- <span class="btn-icon">${x}</span>
1003
+ <span class="btn-icon">${v}</span>
1004
1004
  Verify your identity
1005
1005
  </button>
1006
1006
  `;case"loading":return`
@@ -1009,19 +1009,15 @@ var w=Object.defineProperty;var E=(h,d,e)=>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":{let s=t?.qrUrl?`https://api.qrserver.com/v1/create-qr-code/?size=320x320&data=${encodeURIComponent(t.qrUrl)}`:"",r=e==="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
- <div class="qr-frame ${r?"scanned":""}">
1017
- <img src="${s}" alt="QR Code for 2FA verification" width="160" height="160" loading="lazy"/>
1018
- <div class="qr-badge">${x}</div>
1019
- ${r?'<div class="scanned-overlay"><span class="scanned-check">'+_+"</span></div>":""}
1020
- </div>
1016
+ ${this._qrFrameHtml(e?.qrUrl??"",r)}
1021
1017
  <div class="status-text ${r?"status-scanned":""}">
1022
1018
  ${r?"&#10003; Scanned &mdash; waiting for approval...":"Waiting for scan..."}
1023
1019
  </div>
1024
- <div class="timer" id="timer">${t?.timerHtml??""}</div>
1020
+ <div class="timer" id="timer">${e?.timerHtml??""}</div>
1025
1021
  <div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
1026
1022
  `}case"APPROVED":return`
1027
1023
  <div class="state-approved">
@@ -1047,10 +1043,33 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
1047
1043
  <div class="state-error">
1048
1044
  <div class="error-icon">&#9888;</div>
1049
1045
  <h3>Something went wrong</h3>
1050
- <p class="subtitle">${t?.errorMessage??"Failed to start verification."}</p>
1046
+ <p class="subtitle">${e?.errorMessage??"Failed to start verification."}</p>
1051
1047
  <button class="retry-btn" id="retry-btn">Try again</button>
1052
1048
  </div>
1053
- `;default:return""}}_sharedStyles(){return`
1049
+ `;default:return""}}_qrFrameHtml(t,e){let i=t?`https://api.qrserver.com/v1/create-qr-code/?size=320x320&data=${encodeURIComponent(t)}`:"";return`
1050
+ <div class="qr-frame ${e?"scanned":""}">
1051
+ <img src="${i}" alt="QR Code for 2FA verification" width="160" height="160" loading="lazy"/>
1052
+ <div class="qr-badge">${v}</div>
1053
+ ${e?'<div class="scanned-overlay"><span class="scanned-check">'+_+"</span></div>":""}
1054
+ </div>
1055
+ `}_mobilePendingBody(t,e,i=""){let r=t==="SCANNED";return`
1056
+ ${i}
1057
+ <h3>Verify your identity</h3>
1058
+ <p class="subtitle">Tap continue to verify on this device.</p>
1059
+ <button class="mobile-cta" id="mobile-cta" type="button">
1060
+ <span class="btn-icon">${v}</span>
1061
+ Continue with QRAuth
1062
+ </button>
1063
+ <div class="status-text ${r?"status-scanned":""}">
1064
+ ${r?"&#10003; Scanned &mdash; waiting for approval...":"Waiting for approval..."}
1065
+ </div>
1066
+ <div class="timer" id="timer">${e?.timerHtml??""}</div>
1067
+ <details class="alt-device">
1068
+ <summary>Use another device</summary>
1069
+ ${this._qrFrameHtml(e?.qrUrl??"",r)}
1070
+ </details>
1071
+ <div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
1072
+ `}_sharedStyles(){return`
1054
1073
  /* Typography */
1055
1074
  h3 {
1056
1075
  font-size: 16px;
@@ -1228,6 +1247,51 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
1228
1247
  }
1229
1248
  .retry-btn:hover { background: var(--_border); color: var(--_text); }
1230
1249
 
1250
+ /* Mobile-aware pending state: primary CTA + QR expander */
1251
+ .mobile-cta {
1252
+ display: flex;
1253
+ align-items: center;
1254
+ justify-content: center;
1255
+ gap: 10px;
1256
+ width: 100%;
1257
+ padding: 12px 18px;
1258
+ background: var(--_btn-bg);
1259
+ color: #fff;
1260
+ border: none;
1261
+ border-radius: var(--_radius);
1262
+ font-size: 14px;
1263
+ font-weight: 600;
1264
+ cursor: pointer;
1265
+ font-family: inherit;
1266
+ margin-bottom: 12px;
1267
+ }
1268
+ .mobile-cta:hover { background: var(--_btn-hover); }
1269
+ .mobile-cta .btn-icon { width: 18px; height: 18px; }
1270
+
1271
+ details.alt-device {
1272
+ margin-top: 12px;
1273
+ text-align: left;
1274
+ border-top: 1px solid var(--_border);
1275
+ padding-top: 10px;
1276
+ }
1277
+ details.alt-device summary {
1278
+ cursor: pointer;
1279
+ font-size: 12px;
1280
+ color: var(--_text-muted);
1281
+ user-select: none;
1282
+ padding: 4px 0;
1283
+ text-align: center;
1284
+ list-style: none;
1285
+ }
1286
+ details.alt-device summary::-webkit-details-marker { display: none; }
1287
+ details.alt-device summary::before {
1288
+ content: '\u25B8 ';
1289
+ display: inline-block;
1290
+ transition: transform 0.2s;
1291
+ }
1292
+ details.alt-device[open] summary::before { transform: rotate(90deg); }
1293
+ details.alt-device > .qr-frame { margin-top: 8px; }
1294
+
1231
1295
  /* Footer */
1232
1296
  .footer {
1233
1297
  margin-top: 14px;
@@ -1236,5 +1300,5 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
1236
1300
  }
1237
1301
  .footer a { color: var(--_disabled); text-decoration: none; }
1238
1302
  .footer a:hover { text-decoration: underline; }
1239
- `}_bindBodyEvents(){this.shadow.getElementById("start-btn")?.addEventListener("click",()=>this._startAuth()),this.shadow.getElementById("retry-btn")?.addEventListener("click",()=>this._retry())}async _startAuth(){this._status="loading",this._updateBody(this._bodyForStatus("loading"));try{this._codeVerifier=await this.generateCodeVerifier();let e=await this.computeCodeChallenge(this._codeVerifier),t={"Content-Type":"application/json","X-Client-Id":this.tenant},i={scopes:this.scopes.split(/\s+/).filter(Boolean),codeChallenge:e,codeChallengeMethod:"S256"},s=await fetch(`${this.baseUrl}/api/v1/auth-sessions`,{method:"POST",headers:t,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._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(e){let t=e instanceof Error?e.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:t})),this._bindBodyEvents(),this.emit("qrauth:error",{message:t})}}_retry(){this._clearTimers(),this._sessionId=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null,this._updateBody(this._bodyForStatus("idle")),this._bindBodyEvents()}_beginPolling(e){let t="PENDING",i=2e3,s=5e3,r=0,a=20,c=async()=>{if(!this._sessionId)return;let o=`${this.baseUrl}/api/v1/auth-sessions/${e}`;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!==t&&(t=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(e,t){switch(this._status=e,e){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:t?.sessionId,user:t?.user,signature:t?.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 e=this.shadow.getElementById("timer");if(!e||!this._expiresAt)return;let t=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));e.textContent=t>0?this._formatTimer(t):"",t<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,(this._status==="pending"||this._status==="SCANNED")&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(e){let t=e??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(t/60),s=t%60;return`Expires in ${i}:${s<10?"0":""}${s}`}_updateBody(e){let t=this.shadow.getElementById("body");t&&(t.innerHTML=e)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-2fa",v);export{p as QRAuthElement,m as QRAuthEphemeral,g as QRAuthLogin,v 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};
1240
1304
  //# sourceMappingURL=qrauth-components.esm.js.map
@@ -1,5 +1,5 @@
1
- /* @qrauth/web-components v0.2.0 — https://qrauth.io */
2
- "use strict";var QRAuthComponents=(()=>{var v=Object.defineProperty;var E=Object.getOwnPropertyDescriptor;var k=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var S=(o,a,e)=>a in o?v(o,a,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[a]=e;var A=(o,a)=>{for(var e in a)v(o,e,{get:a[e],enumerable:!0})},C=(o,a,e,t)=>{if(a&&typeof a=="object"||typeof a=="function")for(let i of k(a))!I.call(o,i)&&i!==e&&v(o,i,{get:()=>a[i],enumerable:!(t=E(a,i))||t.enumerable});return o};var T=o=>C(v({},"__esModule",{value:!0}),o);var n=(o,a,e)=>S(o,typeof a!="symbol"?a+"":a,e);var D={};A(D,{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 e=this.getAttribute("base-url");return e!==null?e:"https://qrauth.io"}connectedCallback(){this.render()}disconnectedCallback(){this.cleanup()}attributeChangedCallback(e,t,i){t!==i&&this.render()}isMobileLike(){if(typeof window>"u")return!1;let e=this.getAttribute("force-mode");if(e==="mobile")return!0;if(e==="desktop"||this.hasAttribute("mobile-fallback-only"))return!1;if(typeof window.matchMedia=="function"){let t=window.matchMedia("(pointer: coarse)").matches,i=window.matchMedia("(hover: none)").matches;if(t&&i)return!0}return/Android|iPhone|iPad|iPod|Mobile|Tablet|BlackBerry|Opera Mini/i.test(navigator.userAgent)}connectSSE(e){this.disconnectSSE(),this._sseConnection=new EventSource(e),this._sseConnection.onerror=()=>{this.disconnectSSE()}}onSSEEvent(e,t){this._sseConnection&&this._sseConnection.addEventListener(e,i=>{let s=i;try{t(JSON.parse(s.data))}catch{}})}disconnectSSE(){this._sseConnection&&(this._sseConnection.close(),this._sseConnection=null)}startPolling(e,t,i,s){this.stopPolling();let r=async()=>{try{let d=await fetch(e,{headers:s});d.ok&&i(await d.json())}catch{}};r(),this._pollInterval=setInterval(r,t)}stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}cleanup(){this.disconnectSSE(),this.stopPolling()}emit(e,t){this.dispatchEvent(new CustomEvent(e,{bubbles:!0,composed:!0,detail:t}))}async generateCodeVerifier(){let e=new Uint8Array(32);return crypto.getRandomValues(e),this._base64url(e)}async computeCodeChallenge(e){let i=new TextEncoder().encode(e),s=await crypto.subtle.digest("SHA-256",i);return this._base64url(new Uint8Array(s))}_base64url(e){let t=Array.from(e,i=>String.fromCodePoint(i)).join("");return btoa(t).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 e=this.getAttribute("force-mode");return e==="mobile"||e==="desktop"?e:"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; }
@@ -185,7 +185,7 @@
185
185
  <div id="modal-body">
186
186
  ${this._bodyForStatus("idle")}
187
187
  </div>
188
- `}_bodyForStatus(e,t){switch(e){case"idle":return`
188
+ `}_bodyForStatus(t,e){switch(t){case"idle":return`
189
189
  <div class="state-header">
190
190
  <div class="brand-icon">${u}</div>
191
191
  <h3>Sign in with QRAuth</h3>
@@ -196,14 +196,14 @@
196
196
  <div class="spinner-wrap">${B}</div>
197
197
  <p class="status-text">Generating QR code...</p>
198
198
  </div>
199
- `;case"pending":case"SCANNED":{if(this.isMobileLike())return this._mobilePendingBody(e,t);let i=e==="SCANNED";return`
199
+ `;case"pending":case"SCANNED":{if(this.isMobileLike())return this._mobilePendingBody(t,e);let i=t==="SCANNED";return`
200
200
  <h3>Sign in with QRAuth</h3>
201
201
  <p class="subtitle">Scan this QR code with your phone camera</p>
202
- ${this._qrFrameHtml(t?.qrUrl??"",i)}
202
+ ${this._qrFrameHtml(e?.qrUrl??"",i)}
203
203
  <div class="status-text ${i?"status-scanned":""}">
204
204
  ${i?"&#10003; QR code scanned &mdash; waiting for approval...":"Waiting for scan..."}
205
205
  </div>
206
- <div class="timer" id="timer">${t?.timerHtml??""}</div>
206
+ <div class="timer" id="timer">${e?.timerHtml??""}</div>
207
207
  <div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
208
208
  `}case"APPROVED":return`
209
209
  <div class="state-approved">
@@ -229,16 +229,16 @@
229
229
  <div class="state-error">
230
230
  <div class="error-icon">&#9888;</div>
231
231
  <h3>Something went wrong</h3>
232
- <p class="subtitle">${t?.errorMessage??"Failed to start authentication."}</p>
232
+ <p class="subtitle">${e?.errorMessage??"Failed to start authentication."}</p>
233
233
  <button class="retry-btn" id="retry-btn">Try again</button>
234
234
  </div>
235
- `;default:return""}}_qrFrameHtml(e,t){let i=e?`https://api.qrserver.com/v1/create-qr-code/?size=440x440&data=${encodeURIComponent(e)}`:"";return`
236
- <div class="qr-frame ${t?"scanned":""}${this.animated?" animated":""}">
235
+ `;default:return""}}_qrFrameHtml(t,e){let i=t?`https://api.qrserver.com/v1/create-qr-code/?size=440x440&data=${encodeURIComponent(t)}`:"";return`
236
+ <div class="qr-frame ${e?"scanned":""}${this.animated?" animated":""}">
237
237
  <img src="${i}" alt="QR Code for authentication" width="220" height="220" loading="lazy"/>
238
238
  <div class="qr-badge">${u}</div>
239
- ${t?'<div class="scanned-overlay"><span class="scanned-check">'+_+"</span></div>":""}
239
+ ${e?'<div class="scanned-overlay"><span class="scanned-check">'+_+"</span></div>":""}
240
240
  </div>
241
- `}_mobilePendingBody(e,t){let i=e==="SCANNED";return`
241
+ `}_mobilePendingBody(t,e){let i=t==="SCANNED";return`
242
242
  <h3>Sign in with QRAuth</h3>
243
243
  <p class="subtitle">Tap continue to verify on this device.</p>
244
244
  <button class="mobile-cta" id="mobile-cta" type="button">
@@ -248,10 +248,10 @@
248
248
  <div class="status-text ${i?"status-scanned":""}">
249
249
  ${i?"&#10003; Scanned &mdash; waiting for approval...":"Waiting for approval..."}
250
250
  </div>
251
- <div class="timer" id="timer">${t?.timerHtml??""}</div>
251
+ <div class="timer" id="timer">${e?.timerHtml??""}</div>
252
252
  <details class="alt-device">
253
253
  <summary>Use another device</summary>
254
- ${this._qrFrameHtml(t?.qrUrl??"",i)}
254
+ ${this._qrFrameHtml(e?.qrUrl??"",i)}
255
255
  </details>
256
256
  <div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
257
257
  `}_sharedModalStyles(){return`
@@ -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 t=this.shadow.getElementById("overlay");t?.addEventListener("click",i=>{i.target===t&&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 e=`${this.baseUrl}/a/${this._sessionToken}`;window.open(e,"_blank","noopener")||(window.location.href=e)}_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 e=await this.computeCodeChallenge(this._codeVerifier),t={"Content-Type":"application/json","X-Client-Id":this.tenant},i={scopes:this.scopes.split(/\s+/).filter(Boolean),codeChallenge:e,codeChallengeMethod:"S256"},s=await fetch(`${this.baseUrl}/api/v1/auth-sessions`,{method:"POST",headers:t,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(e){let t=e instanceof Error?e.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:t})),this._bindModalButtons(),this.emit("qrauth:error",{message:t})}}_retry(){if(this._clearTimers(),this._sessionId=null,this._sessionToken=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null,this.display==="inline"){let e=this.shadow.getElementById("inline-body");e&&(e.innerHTML=this._inlineIdleContent(),this._bindInlineEvents())}else this._startAuth()}_beginPolling(e){let t="PENDING",i=2e3,s=5e3,r=0,d=20,h=async()=>{if(!this._sessionId)return;let l=`${this.baseUrl}/api/v1/auth-sessions/${e}`;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!==t&&(t=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(e,t){switch(this._status=e,e){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",t);break}case"APPROVED":{this._clearTimers(),this._updateBody(this._bodyForStatus("APPROVED")),this._bindModalButtons(),this.emit("qrauth:authenticated",{sessionId:t?.sessionId,user:t?.user,signature:t?.signature});let i=this.onAuth;if(i){let s=window[i];typeof s=="function"&&s(t)}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 e=this.shadow.getElementById("timer");if(!e||!this._expiresAt)return;let t=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));e.textContent=t>0?this._formatTimer(t):"",t<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,(this._status==="pending"||this._status==="SCANNED")&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(e){let t=e??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(t/60),s=t%60;return`Expires in ${i}:${s<10?"0":""}${s}`}_updateBody(e){let t=this.display==="inline"?this.shadow.getElementById("inline-body"):this.shadow.getElementById("modal-body");t&&(t.innerHTML=e)}_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">
@@ -528,7 +528,7 @@
528
528
  </svg>`,M=`<svg class="spinner" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
529
529
  <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2.5"
530
530
  stroke-dasharray="31.416" stroke-dashoffset="10" stroke-linecap="round"/>
531
- </svg>`,b=class extends p{constructor(){super(...arguments);n(this,"_sessionId",null);n(this,"_claimUrl",null);n(this,"_status","idle");n(this,"_expiresAt",null);n(this,"_timerInterval",null);n(this,"_pollTimeout",null)}static get observedAttributes(){return[...super.observedAttributes,"scopes","ttl","max-uses","device-binding","display"]}get scopes(){return this.getAttribute("scopes")||"access"}get ttl(){return this.getAttribute("ttl")||"30m"}get maxUses(){return parseInt(this.getAttribute("max-uses")||"1",10)}get deviceBinding(){return this.hasAttribute("device-binding")}get display(){return this.getAttribute("display")||"inline"}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}render(){this.display==="button"?this._renderButton():this._renderInline()}_renderButton(){this.shadow.innerHTML=`
531
+ </svg>`,b=class extends p{constructor(){super(...arguments);n(this,"_sessionId",null);n(this,"_claimUrl",null);n(this,"_status","idle");n(this,"_expiresAt",null);n(this,"_timerInterval",null);n(this,"_pollTimeout",null)}static get observedAttributes(){return[...super.observedAttributes,"scopes","ttl","max-uses","device-binding","display"]}get scopes(){return this.getAttribute("scopes")||"access"}get ttl(){return this.getAttribute("ttl")||"30m"}get maxUses(){return parseInt(this.getAttribute("max-uses")||"1",10)}get deviceBinding(){return this.hasAttribute("device-binding")}get display(){return this.getAttribute("display")||"inline"}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==="button"?this._renderButton():this._renderInline()}_renderButton(){this.shadow.innerHTML=`
532
532
  <style>
533
533
  ${this.getBaseStyles()}
534
534
  :host { display: inline-block; }
@@ -661,7 +661,7 @@
661
661
  <div id="modal-body">
662
662
  ${this._bodyForStatus("idle")}
663
663
  </div>
664
- `}_bodyForStatus(e,t){switch(e){case"idle":return`
664
+ `}_bodyForStatus(t,e){switch(t){case"idle":return`
665
665
  <div class="state-header">
666
666
  <div class="brand-icon">${x}</div>
667
667
  <h3>Ephemeral Access</h3>
@@ -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=t?.claimUrl?`https://api.qrserver.com/v1/create-qr-code/?size=440x440&data=${encodeURIComponent(t.claimUrl)}`:"",s=t?.useCount??0,r=t?.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">
@@ -680,7 +680,7 @@
680
680
  <div class="qr-badge">${x}</div>
681
681
  </div>
682
682
  ${h}
683
- <div class="timer" id="timer">${t?.timerHtml??""}</div>
683
+ <div class="timer" id="timer">${e?.timerHtml??""}</div>
684
684
  <div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
685
685
  `}case"CLAIMED":return`
686
686
  <div class="state-claimed">
@@ -705,7 +705,7 @@
705
705
  <div class="state-error">
706
706
  <div class="error-icon">&#9888;</div>
707
707
  <h3>Something went wrong</h3>
708
- <p class="subtitle">${t?.errorMessage??"Failed to create ephemeral session."}</p>
708
+ <p class="subtitle">${e?.errorMessage??"Failed to create ephemeral session."}</p>
709
709
  <button class="retry-btn" id="retry-btn">Try again</button>
710
710
  </div>
711
711
  `;default:return""}}_sharedModalStyles(){return`
@@ -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 t=this.shadow.getElementById("overlay");t?.addEventListener("click",i=>{i.target===t&&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 e=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(!e.ok){let i=await e.json().catch(()=>({message:"Failed to create session"}));throw new Error(i.message??"Failed to create session")}let t=await e.json();this._sessionId=t.sessionId,this._claimUrl=t.claimUrl,this._expiresAt=new Date(t.expiresAt).getTime(),this._status="active",this._updateBody(this._bodyForStatus("active",{claimUrl:t.claimUrl,timerHtml:this._formatTimer(),useCount:0,maxUses:t.maxUses})),this._bindModalButtons(),this._startCountdown(),this._beginPolling(t.sessionId)}catch(e){let t=e instanceof Error?e.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:t})),this._bindModalButtons(),this.emit("qrauth:error",{message:t})}}_retry(){if(this._clearTimers(),this._sessionId=null,this._claimUrl=null,this._status="idle",this._expiresAt=null,this.display==="inline"){let e=this.shadow.getElementById("inline-body");e&&(e.innerHTML=this._inlineIdleContent(),this._bindInlineEvents())}else this._startSession()}_beginPolling(e){let t=2e3,i=5e3,s=0,r=20,d=async()=>{if(this._sessionId)try{let l=await(await fetch(`${this.baseUrl}/api/v1/ephemeral/${e}`,{headers:{"X-Client-Id":this.tenant}})).json();if(s=0,t=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,t);return}return}this._pollTimeout=setTimeout(d,t)}catch{if(s++,s>=r){this._handleStatusChange("EXPIRED",void 0);return}t=Math.min(t*1.5,i),this._pollTimeout=setTimeout(d,t)}};this._pollTimeout=setTimeout(d,t)}_handleStatusChange(e,t){switch(e){case"CLAIMED":{this._clearTimers(),this._status="CLAIMED",this._updateBody(this._bodyForStatus("CLAIMED")),this._bindModalButtons(),this.emit("qrauth:claimed",{sessionId:t?.sessionId,status:t?.status,scopes:t?.scopes,metadata:t?.metadata,useCount:t?.useCount,maxUses:t?.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 e=this.shadow.getElementById("timer");if(!e||!this._expiresAt)return;let t=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));e.textContent=t>0?this._formatTimer(t):"",t<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,this._status==="active"&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(e){let t=e??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(t/60),s=t%60;return`Expires in ${i}:${s<10?"0":""}${s}`}_updateBody(e){let t=this.display==="inline"?this.shadow.getElementById("inline-body"):this.shadow.getElementById("modal-body");t&&(t.innerHTML=e)}_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,"_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")}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; }
@@ -987,12 +987,12 @@
987
987
  ${this._bodyForStatus("idle")}
988
988
  </div>
989
989
  </div>
990
- `,this._bindBodyEvents()}_bodyForStatus(e,t){let i=`
990
+ `,this._bindBodyEvents()}_bodyForStatus(t,e){let i=`
991
991
  <div class="step-label">
992
992
  <span class="step-dot"></span>
993
993
  Step 2 of 2 &mdash; Verify identity
994
994
  </div>
995
- `;switch(e){case"idle":return`
995
+ `;switch(t){case"idle":return`
996
996
  ${i}
997
997
  <div class="state-header">
998
998
  <div class="brand-icon">${f}</div>
@@ -1009,19 +1009,15 @@
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":{let s=t?.qrUrl?`https://api.qrserver.com/v1/create-qr-code/?size=320x320&data=${encodeURIComponent(t.qrUrl)}`:"",r=e==="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
- <div class="qr-frame ${r?"scanned":""}">
1017
- <img src="${s}" alt="QR Code for 2FA verification" width="160" height="160" loading="lazy"/>
1018
- <div class="qr-badge">${f}</div>
1019
- ${r?'<div class="scanned-overlay"><span class="scanned-check">'+y+"</span></div>":""}
1020
- </div>
1016
+ ${this._qrFrameHtml(e?.qrUrl??"",r)}
1021
1017
  <div class="status-text ${r?"status-scanned":""}">
1022
1018
  ${r?"&#10003; Scanned &mdash; waiting for approval...":"Waiting for scan..."}
1023
1019
  </div>
1024
- <div class="timer" id="timer">${t?.timerHtml??""}</div>
1020
+ <div class="timer" id="timer">${e?.timerHtml??""}</div>
1025
1021
  <div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
1026
1022
  `}case"APPROVED":return`
1027
1023
  <div class="state-approved">
@@ -1047,10 +1043,33 @@
1047
1043
  <div class="state-error">
1048
1044
  <div class="error-icon">&#9888;</div>
1049
1045
  <h3>Something went wrong</h3>
1050
- <p class="subtitle">${t?.errorMessage??"Failed to start verification."}</p>
1046
+ <p class="subtitle">${e?.errorMessage??"Failed to start verification."}</p>
1051
1047
  <button class="retry-btn" id="retry-btn">Try again</button>
1052
1048
  </div>
1053
- `;default:return""}}_sharedStyles(){return`
1049
+ `;default:return""}}_qrFrameHtml(t,e){let i=t?`https://api.qrserver.com/v1/create-qr-code/?size=320x320&data=${encodeURIComponent(t)}`:"";return`
1050
+ <div class="qr-frame ${e?"scanned":""}">
1051
+ <img src="${i}" alt="QR Code for 2FA verification" width="160" height="160" loading="lazy"/>
1052
+ <div class="qr-badge">${f}</div>
1053
+ ${e?'<div class="scanned-overlay"><span class="scanned-check">'+y+"</span></div>":""}
1054
+ </div>
1055
+ `}_mobilePendingBody(t,e,i=""){let r=t==="SCANNED";return`
1056
+ ${i}
1057
+ <h3>Verify your identity</h3>
1058
+ <p class="subtitle">Tap continue to verify on this device.</p>
1059
+ <button class="mobile-cta" id="mobile-cta" type="button">
1060
+ <span class="btn-icon">${f}</span>
1061
+ Continue with QRAuth
1062
+ </button>
1063
+ <div class="status-text ${r?"status-scanned":""}">
1064
+ ${r?"&#10003; Scanned &mdash; waiting for approval...":"Waiting for approval..."}
1065
+ </div>
1066
+ <div class="timer" id="timer">${e?.timerHtml??""}</div>
1067
+ <details class="alt-device">
1068
+ <summary>Use another device</summary>
1069
+ ${this._qrFrameHtml(e?.qrUrl??"",r)}
1070
+ </details>
1071
+ <div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
1072
+ `}_sharedStyles(){return`
1054
1073
  /* Typography */
1055
1074
  h3 {
1056
1075
  font-size: 16px;
@@ -1228,6 +1247,51 @@
1228
1247
  }
1229
1248
  .retry-btn:hover { background: var(--_border); color: var(--_text); }
1230
1249
 
1250
+ /* Mobile-aware pending state: primary CTA + QR expander */
1251
+ .mobile-cta {
1252
+ display: flex;
1253
+ align-items: center;
1254
+ justify-content: center;
1255
+ gap: 10px;
1256
+ width: 100%;
1257
+ padding: 12px 18px;
1258
+ background: var(--_btn-bg);
1259
+ color: #fff;
1260
+ border: none;
1261
+ border-radius: var(--_radius);
1262
+ font-size: 14px;
1263
+ font-weight: 600;
1264
+ cursor: pointer;
1265
+ font-family: inherit;
1266
+ margin-bottom: 12px;
1267
+ }
1268
+ .mobile-cta:hover { background: var(--_btn-hover); }
1269
+ .mobile-cta .btn-icon { width: 18px; height: 18px; }
1270
+
1271
+ details.alt-device {
1272
+ margin-top: 12px;
1273
+ text-align: left;
1274
+ border-top: 1px solid var(--_border);
1275
+ padding-top: 10px;
1276
+ }
1277
+ details.alt-device summary {
1278
+ cursor: pointer;
1279
+ font-size: 12px;
1280
+ color: var(--_text-muted);
1281
+ user-select: none;
1282
+ padding: 4px 0;
1283
+ text-align: center;
1284
+ list-style: none;
1285
+ }
1286
+ details.alt-device summary::-webkit-details-marker { display: none; }
1287
+ details.alt-device summary::before {
1288
+ content: '\u25B8 ';
1289
+ display: inline-block;
1290
+ transition: transform 0.2s;
1291
+ }
1292
+ details.alt-device[open] summary::before { transform: rotate(90deg); }
1293
+ details.alt-device > .qr-frame { margin-top: 8px; }
1294
+
1231
1295
  /* Footer */
1232
1296
  .footer {
1233
1297
  margin-top: 14px;
@@ -1236,5 +1300,5 @@
1236
1300
  }
1237
1301
  .footer a { color: var(--_disabled); text-decoration: none; }
1238
1302
  .footer a:hover { text-decoration: underline; }
1239
- `}_bindBodyEvents(){this.shadow.getElementById("start-btn")?.addEventListener("click",()=>this._startAuth()),this.shadow.getElementById("retry-btn")?.addEventListener("click",()=>this._retry())}async _startAuth(){this._status="loading",this._updateBody(this._bodyForStatus("loading"));try{this._codeVerifier=await this.generateCodeVerifier();let e=await this.computeCodeChallenge(this._codeVerifier),t={"Content-Type":"application/json","X-Client-Id":this.tenant},i={scopes:this.scopes.split(/\s+/).filter(Boolean),codeChallenge:e,codeChallengeMethod:"S256"},s=await fetch(`${this.baseUrl}/api/v1/auth-sessions`,{method:"POST",headers:t,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._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(e){let t=e instanceof Error?e.message:"Unexpected error";this._status="error",this._updateBody(this._bodyForStatus("error",{errorMessage:t})),this._bindBodyEvents(),this.emit("qrauth:error",{message:t})}}_retry(){this._clearTimers(),this._sessionId=null,this._codeVerifier=null,this._status="idle",this._expiresAt=null,this._updateBody(this._bodyForStatus("idle")),this._bindBodyEvents()}_beginPolling(e){let t="PENDING",i=2e3,s=5e3,r=0,d=20,h=async()=>{if(!this._sessionId)return;let l=`${this.baseUrl}/api/v1/auth-sessions/${e}`;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!==t&&(t=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(e,t){switch(this._status=e,e){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:t?.sessionId,user:t?.user,signature:t?.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 e=this.shadow.getElementById("timer");if(!e||!this._expiresAt)return;let t=Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3));e.textContent=t>0?this._formatTimer(t):"",t<=0&&(clearInterval(this._timerInterval),this._timerInterval=null,(this._status==="pending"||this._status==="SCANNED")&&this._handleStatusChange("EXPIRED",void 0))},1e3)}_formatTimer(e){let t=e??(this._expiresAt?Math.max(0,Math.floor((this._expiresAt-Date.now())/1e3)):0),i=Math.floor(t/60),s=t%60;return`Expires in ${i}:${s<10?"0":""}${s}`}_updateBody(e){let t=this.shadow.getElementById("body");t&&(t.innerHTML=e)}_clearTimers(){this._timerInterval&&(clearInterval(this._timerInterval),this._timerInterval=null),this._pollTimeout&&(clearTimeout(this._pollTimeout),this._pollTimeout=null)}};customElements.define("qrauth-2fa",m);return T(D);})();
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($);})();
1240
1304
  //# sourceMappingURL=qrauth-components.js.map
package/dist/twofa.d.ts CHANGED
@@ -6,12 +6,29 @@ import { QRAuthElement } from './base.js';
6
6
  * to perform QR-based step-up verification. Always displays inline — no button/modal mode.
7
7
  *
8
8
  * Attributes:
9
- * tenant — Your QRAuth App client ID (required)
10
- * theme — "light" (default) | "dark"
11
- * base-url — API base URL (default: https://qrauth.io)
12
- * session-token — JWT from the primary authentication (required for context)
13
- * scopes — Space-separated scopes (default: "identity")
14
- * auto-start — Boolean attribute; if present, starts 2FA immediately on connect
9
+ * tenant — Your QRAuth App client ID (required)
10
+ * theme — "light" (default) | "dark"
11
+ * base-url — API base URL (default: https://qrauth.io)
12
+ * session-token — JWT from the primary authentication (required for context)
13
+ * scopes — Space-separated scopes (default: "identity")
14
+ * auto-start — Boolean attribute; if present, starts 2FA immediately on connect
15
+ * force-mode — "mobile" | "desktop" | "auto" (default "auto").
16
+ * Overrides the automatic mobile-like detection that
17
+ * swaps the QR-first body for a "Continue with
18
+ * QRAuth" CTA + QR expander on phones and tablets.
19
+ * Mirrors the 0.2.0 <qrauth-login> treatment.
20
+ * mobile-fallback-only — If present, disables the mobile-aware UI entirely
21
+ * and keeps the QR-first body on every device. Use
22
+ * this when the surrounding page already provides an
23
+ * alternative same-device verification path and the
24
+ * component is only there as the cross-device scan
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.
15
32
  *
16
33
  * Events (bubbles + composed, cross shadow-DOM):
17
34
  * qrauth:verified — detail: { sessionId, user, signature }
@@ -26,6 +43,7 @@ import { QRAuthElement } from './base.js';
26
43
  export declare class QRAuthTwoFA extends QRAuthElement {
27
44
  static get observedAttributes(): string[];
28
45
  private _sessionId;
46
+ private _sessionToken;
29
47
  private _codeVerifier;
30
48
  private _status;
31
49
  private _expiresAt;
@@ -34,12 +52,37 @@ export declare class QRAuthTwoFA extends QRAuthElement {
34
52
  get sessionToken(): string | null;
35
53
  get scopes(): string;
36
54
  get autoStart(): boolean;
55
+ /** See <qrauth-login>.redirectUri for semantics. */
56
+ get redirectUri(): string | null;
57
+ get forceMode(): 'mobile' | 'desktop' | 'auto';
58
+ get mobileFallbackOnly(): boolean;
37
59
  connectedCallback(): void;
38
60
  disconnectedCallback(): void;
39
61
  protected render(): void;
40
62
  private _bodyForStatus;
63
+ /**
64
+ * Shared QR frame markup used by both the desktop pending body and the
65
+ * mobile "Use another device" expander. Centralised so the two paths can't
66
+ * drift.
67
+ */
68
+ private _qrFrameHtml;
69
+ /**
70
+ * Mobile-aware pending body. Primary CTA opens the hosted approval page
71
+ * (/a/:token) in a new tab — a same-device path that short-circuits the
72
+ * cross-device QR scan that is a dead end on phones. The QR itself is
73
+ * demoted to a collapsed expander for the "I have a second device" case.
74
+ * Mirrors the 0.2.0 <qrauth-login> treatment.
75
+ */
76
+ private _mobilePendingBody;
41
77
  private _sharedStyles;
42
78
  private _bindBodyEvents;
79
+ /**
80
+ * Mobile same-device path: open the hosted approval page in a new tab.
81
+ * The existing polling loop in _beginPolling picks up SCANNED / APPROVED
82
+ * transitions from the server once the user completes the flow there, so
83
+ * qrauth:verified fires unchanged.
84
+ */
85
+ private _openApprovalPage;
43
86
  private _startAuth;
44
87
  private _retry;
45
88
  private _beginPolling;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qrauth/web-components",
3
- "version": "0.2.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",