@qrauth/web-components 0.1.0 → 0.2.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 +27 -0
- package/README.md +105 -0
- package/dist/base.d.ts +21 -0
- package/dist/login.d.ts +41 -8
- package/dist/qrauth-components.esm.js +82 -19
- package/dist/qrauth-components.js +86 -23
- package/package.json +12 -4
- package/dist/base.js +0 -148
- package/dist/ephemeral.js +0 -727
- package/dist/index.js +0 -4
- package/dist/login.js +0 -769
- package/dist/qrauth-components.esm.js.map +0 -7
- package/dist/qrauth-components.js.map +0 -7
- package/dist/twofa.js +0 -625
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/* @qrauth/web-components v0.
|
|
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
|
|
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`
|
|
3
3
|
:host {
|
|
4
4
|
display: inline-block;
|
|
5
5
|
font-family: var(--qrauth-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
--_btn-hover: var(--qrauth-btn-hover, #2e4578);
|
|
31
31
|
}
|
|
32
32
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
33
|
-
`}};var
|
|
33
|
+
`}};var u=`<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
34
34
|
<path d="M48 24 H152 Q166 24 166 38 V108 Q166 156 100 182 Q34 156 34 108 V38 Q34 24 48 24 Z"
|
|
35
35
|
fill="none" stroke="#fff" stroke-width="5"/>
|
|
36
36
|
<g fill="#fff" opacity="0.2">
|
|
@@ -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>`,
|
|
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=`
|
|
56
56
|
<style>
|
|
57
57
|
${this.getBaseStyles()}
|
|
58
58
|
:host { display: inline-block; }
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
</style>
|
|
119
119
|
|
|
120
120
|
<button class="btn" id="open-btn" aria-label="Sign in with QRAuth">
|
|
121
|
-
<span class="btn-icon">${
|
|
121
|
+
<span class="btn-icon">${u}</span>
|
|
122
122
|
Sign in with QRAuth
|
|
123
123
|
</button>
|
|
124
124
|
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
</div>
|
|
178
178
|
`,this._bindInlineEvents()}_inlineIdleContent(){return`
|
|
179
179
|
<button class="start-btn" id="start-btn" aria-label="Sign in with QRAuth">
|
|
180
|
-
<span class="btn-icon">${
|
|
180
|
+
<span class="btn-icon">${u}</span>
|
|
181
181
|
Sign in with QRAuth
|
|
182
182
|
</button>
|
|
183
183
|
`}_modalContent(){return`
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
</div>
|
|
188
188
|
`}_bodyForStatus(e,t){switch(e){case"idle":return`
|
|
189
189
|
<div class="state-header">
|
|
190
|
-
<div class="brand-icon">${
|
|
190
|
+
<div class="brand-icon">${u}</div>
|
|
191
191
|
<h3>Sign in with QRAuth</h3>
|
|
192
192
|
<p>Secure, passwordless authentication</p>
|
|
193
193
|
</div>
|
|
@@ -196,16 +196,12 @@
|
|
|
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":{
|
|
199
|
+
`;case"pending":case"SCANNED":{if(this.isMobileLike())return this._mobilePendingBody(e,t);let i=e==="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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
${s?'<div class="scanned-overlay"><span class="scanned-check">'+_+"</span></div>":""}
|
|
206
|
-
</div>
|
|
207
|
-
<div class="status-text ${s?"status-scanned":""}">
|
|
208
|
-
${s?"✓ QR code scanned — waiting for approval...":"Waiting for scan..."}
|
|
202
|
+
${this._qrFrameHtml(t?.qrUrl??"",i)}
|
|
203
|
+
<div class="status-text ${i?"status-scanned":""}">
|
|
204
|
+
${i?"✓ QR code scanned — waiting for approval...":"Waiting for scan..."}
|
|
209
205
|
</div>
|
|
210
206
|
<div class="timer" id="timer">${t?.timerHtml??""}</div>
|
|
211
207
|
<div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
|
|
@@ -236,7 +232,29 @@
|
|
|
236
232
|
<p class="subtitle">${t?.errorMessage??"Failed to start authentication."}</p>
|
|
237
233
|
<button class="retry-btn" id="retry-btn">Try again</button>
|
|
238
234
|
</div>
|
|
239
|
-
`;default:return""}}
|
|
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":""}">
|
|
237
|
+
<img src="${i}" alt="QR Code for authentication" width="220" height="220" loading="lazy"/>
|
|
238
|
+
<div class="qr-badge">${u}</div>
|
|
239
|
+
${t?'<div class="scanned-overlay"><span class="scanned-check">'+_+"</span></div>":""}
|
|
240
|
+
</div>
|
|
241
|
+
`}_mobilePendingBody(e,t){let i=e==="SCANNED";return`
|
|
242
|
+
<h3>Sign in with QRAuth</h3>
|
|
243
|
+
<p class="subtitle">Tap continue to verify on this device.</p>
|
|
244
|
+
<button class="mobile-cta" id="mobile-cta" type="button">
|
|
245
|
+
<span class="btn-icon">${u}</span>
|
|
246
|
+
Continue with QRAuth
|
|
247
|
+
</button>
|
|
248
|
+
<div class="status-text ${i?"status-scanned":""}">
|
|
249
|
+
${i?"✓ Scanned — waiting for approval...":"Waiting for approval..."}
|
|
250
|
+
</div>
|
|
251
|
+
<div class="timer" id="timer">${t?.timerHtml??""}</div>
|
|
252
|
+
<details class="alt-device">
|
|
253
|
+
<summary>Use another device</summary>
|
|
254
|
+
${this._qrFrameHtml(t?.qrUrl??"",i)}
|
|
255
|
+
</details>
|
|
256
|
+
<div class="footer">Secured by <a href="https://qrauth.io" target="_blank" rel="noopener">QRAuth</a></div>
|
|
257
|
+
`}_sharedModalStyles(){return`
|
|
240
258
|
/* Typography */
|
|
241
259
|
h3 {
|
|
242
260
|
font-size: 18px;
|
|
@@ -435,6 +453,51 @@
|
|
|
435
453
|
}
|
|
436
454
|
.retry-btn:hover { background: var(--_border); color: var(--_text); }
|
|
437
455
|
|
|
456
|
+
/* Mobile-aware pending state: primary CTA + QR expander */
|
|
457
|
+
.mobile-cta {
|
|
458
|
+
display: flex;
|
|
459
|
+
align-items: center;
|
|
460
|
+
justify-content: center;
|
|
461
|
+
gap: 10px;
|
|
462
|
+
width: 100%;
|
|
463
|
+
padding: 14px 20px;
|
|
464
|
+
background: var(--_btn-bg);
|
|
465
|
+
color: #fff;
|
|
466
|
+
border: none;
|
|
467
|
+
border-radius: var(--_radius);
|
|
468
|
+
font-size: 15px;
|
|
469
|
+
font-weight: 600;
|
|
470
|
+
cursor: pointer;
|
|
471
|
+
font-family: inherit;
|
|
472
|
+
margin-bottom: 16px;
|
|
473
|
+
}
|
|
474
|
+
.mobile-cta:hover { background: var(--_btn-hover); }
|
|
475
|
+
.mobile-cta .btn-icon { width: 20px; height: 20px; }
|
|
476
|
+
|
|
477
|
+
details.alt-device {
|
|
478
|
+
margin-top: 14px;
|
|
479
|
+
text-align: left;
|
|
480
|
+
border-top: 1px solid var(--_border);
|
|
481
|
+
padding-top: 12px;
|
|
482
|
+
}
|
|
483
|
+
details.alt-device summary {
|
|
484
|
+
cursor: pointer;
|
|
485
|
+
font-size: 13px;
|
|
486
|
+
color: var(--_text-muted);
|
|
487
|
+
user-select: none;
|
|
488
|
+
padding: 6px 0;
|
|
489
|
+
text-align: center;
|
|
490
|
+
list-style: none;
|
|
491
|
+
}
|
|
492
|
+
details.alt-device summary::-webkit-details-marker { display: none; }
|
|
493
|
+
details.alt-device summary::before {
|
|
494
|
+
content: '\u25B8 ';
|
|
495
|
+
display: inline-block;
|
|
496
|
+
transition: transform 0.2s;
|
|
497
|
+
}
|
|
498
|
+
details.alt-device[open] summary::before { transform: rotate(90deg); }
|
|
499
|
+
details.alt-device > .qr-frame { margin-top: 10px; }
|
|
500
|
+
|
|
438
501
|
/* Footer */
|
|
439
502
|
.footer {
|
|
440
503
|
margin-top: 18px;
|
|
@@ -443,7 +506,7 @@
|
|
|
443
506
|
}
|
|
444
507
|
.footer a { color: var(--_disabled); text-decoration: none; }
|
|
445
508
|
.footer a:hover { text-decoration: underline; }
|
|
446
|
-
`}_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())}_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._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._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._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",
|
|
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">
|
|
447
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"
|
|
448
511
|
fill="none" stroke="#fff" stroke-width="5"/>
|
|
449
512
|
<g fill="#fff" opacity="0.2">
|
|
@@ -465,7 +528,7 @@
|
|
|
465
528
|
</svg>`,M=`<svg class="spinner" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
466
529
|
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2.5"
|
|
467
530
|
stroke-dasharray="31.416" stroke-dashoffset="10" stroke-linecap="round"/>
|
|
468
|
-
</svg>`,
|
|
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=`
|
|
469
532
|
<style>
|
|
470
533
|
${this.getBaseStyles()}
|
|
471
534
|
:host { display: inline-block; }
|
|
@@ -827,7 +890,7 @@
|
|
|
827
890
|
}
|
|
828
891
|
.footer a { color: var(--_disabled); text-decoration: none; }
|
|
829
892
|
.footer a:hover { text-decoration: underline; }
|
|
830
|
-
`}_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",
|
|
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">
|
|
831
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"
|
|
832
895
|
fill="none" stroke="#fff" stroke-width="5"/>
|
|
833
896
|
<g fill="#fff" opacity="0.2">
|
|
@@ -846,10 +909,10 @@
|
|
|
846
909
|
stroke-linecap="round" stroke-linejoin="round"/>
|
|
847
910
|
</svg>`,y=`<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
848
911
|
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" fill="currentColor"/>
|
|
849
|
-
</svg>`,
|
|
912
|
+
</svg>`,q=`<svg class="spinner" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
850
913
|
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2.5"
|
|
851
914
|
stroke-dasharray="31.416" stroke-dashoffset="10" stroke-linecap="round"/>
|
|
852
|
-
</svg>`,
|
|
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=`
|
|
853
916
|
<style>
|
|
854
917
|
${this.getBaseStyles()}
|
|
855
918
|
:host { display: block; }
|
|
@@ -943,7 +1006,7 @@
|
|
|
943
1006
|
`;case"loading":return`
|
|
944
1007
|
${i}
|
|
945
1008
|
<div class="state-loading">
|
|
946
|
-
<div class="spinner-wrap">${
|
|
1009
|
+
<div class="spinner-wrap">${q}</div>
|
|
947
1010
|
<p class="status-text">Generating QR code...</p>
|
|
948
1011
|
</div>
|
|
949
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`
|
|
@@ -1173,5 +1236,5 @@
|
|
|
1173
1236
|
}
|
|
1174
1237
|
.footer a { color: var(--_disabled); text-decoration: none; }
|
|
1175
1238
|
.footer a:hover { text-decoration: underline; }
|
|
1176
|
-
`}_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",
|
|
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);})();
|
|
1177
1240
|
//# sourceMappingURL=qrauth-components.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qrauth/web-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "QRAuth Web Components — drop-in authentication elements",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/qrauth-components.js",
|
|
@@ -14,11 +14,17 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
-
"dist/"
|
|
17
|
+
"dist/qrauth-components.js",
|
|
18
|
+
"dist/qrauth-components.esm.js",
|
|
19
|
+
"dist/*.d.ts",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md"
|
|
18
22
|
],
|
|
19
23
|
"scripts": {
|
|
20
24
|
"build": "tsc && node scripts/bundle.js",
|
|
21
25
|
"dev": "tsc --watch",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest",
|
|
22
28
|
"prepublishOnly": "npm run build"
|
|
23
29
|
},
|
|
24
30
|
"keywords": [
|
|
@@ -41,10 +47,12 @@
|
|
|
41
47
|
},
|
|
42
48
|
"repository": {
|
|
43
49
|
"type": "git",
|
|
44
|
-
"url": "https://github.com/qrauth-io/qrauth"
|
|
50
|
+
"url": "git+https://github.com/qrauth-io/qrauth.git"
|
|
45
51
|
},
|
|
46
52
|
"devDependencies": {
|
|
47
53
|
"esbuild": "^0.24.0",
|
|
48
|
-
"
|
|
54
|
+
"happy-dom": "^15.11.7",
|
|
55
|
+
"typescript": "^5.7.0",
|
|
56
|
+
"vitest": "^2.1.9"
|
|
49
57
|
}
|
|
50
58
|
}
|
package/dist/base.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QRAuthElement — base class for all QRAuth Web Components.
|
|
3
|
-
* Provides: Shadow DOM setup, CSS custom property theming, SSE connection management,
|
|
4
|
-
* lifecycle hooks, and event dispatching.
|
|
5
|
-
*/
|
|
6
|
-
export class QRAuthElement extends HTMLElement {
|
|
7
|
-
shadow;
|
|
8
|
-
_sseConnection = null;
|
|
9
|
-
_pollInterval = null;
|
|
10
|
-
static get observedAttributes() {
|
|
11
|
-
return ['tenant', 'theme', 'base-url'];
|
|
12
|
-
}
|
|
13
|
-
constructor() {
|
|
14
|
-
super();
|
|
15
|
-
this.shadow = this.attachShadow({ mode: 'open' });
|
|
16
|
-
}
|
|
17
|
-
// Getters for common attributes
|
|
18
|
-
get tenant() { return this.getAttribute('tenant') || ''; }
|
|
19
|
-
get theme() { return this.getAttribute('theme') || 'light'; }
|
|
20
|
-
get baseUrl() {
|
|
21
|
-
const val = this.getAttribute('base-url');
|
|
22
|
-
// Allow empty string for local dev (relative URLs), fallback to production
|
|
23
|
-
return val !== null ? val : 'https://qrauth.io';
|
|
24
|
-
}
|
|
25
|
-
connectedCallback() {
|
|
26
|
-
this.render();
|
|
27
|
-
}
|
|
28
|
-
disconnectedCallback() {
|
|
29
|
-
this.cleanup();
|
|
30
|
-
}
|
|
31
|
-
attributeChangedCallback(_name, oldValue, newValue) {
|
|
32
|
-
if (oldValue !== newValue) {
|
|
33
|
-
this.render();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
// --- SSE Management ---
|
|
37
|
-
connectSSE(url) {
|
|
38
|
-
this.disconnectSSE();
|
|
39
|
-
// EventSource doesn't support custom headers natively.
|
|
40
|
-
// For public (unauthenticated) SSE endpoints this works directly.
|
|
41
|
-
this._sseConnection = new EventSource(url);
|
|
42
|
-
this._sseConnection.onerror = () => {
|
|
43
|
-
// Fall back to polling on SSE failure — caller should initiate polling
|
|
44
|
-
this.disconnectSSE();
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
onSSEEvent(event, callback) {
|
|
48
|
-
if (this._sseConnection) {
|
|
49
|
-
this._sseConnection.addEventListener(event, (e) => {
|
|
50
|
-
const msg = e;
|
|
51
|
-
try {
|
|
52
|
-
callback(JSON.parse(msg.data));
|
|
53
|
-
}
|
|
54
|
-
catch { /* invalid JSON */ }
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
disconnectSSE() {
|
|
59
|
-
if (this._sseConnection) {
|
|
60
|
-
this._sseConnection.close();
|
|
61
|
-
this._sseConnection = null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
// --- Polling (fallback / primary for authenticated endpoints) ---
|
|
65
|
-
startPolling(url, intervalMs, callback, headers) {
|
|
66
|
-
this.stopPolling();
|
|
67
|
-
const poll = async () => {
|
|
68
|
-
try {
|
|
69
|
-
const res = await fetch(url, { headers });
|
|
70
|
-
if (res.ok)
|
|
71
|
-
callback(await res.json());
|
|
72
|
-
}
|
|
73
|
-
catch { /* network error — retry next interval */ }
|
|
74
|
-
};
|
|
75
|
-
poll(); // immediate first poll
|
|
76
|
-
this._pollInterval = setInterval(poll, intervalMs);
|
|
77
|
-
}
|
|
78
|
-
stopPolling() {
|
|
79
|
-
if (this._pollInterval) {
|
|
80
|
-
clearInterval(this._pollInterval);
|
|
81
|
-
this._pollInterval = null;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
// --- Cleanup ---
|
|
85
|
-
cleanup() {
|
|
86
|
-
this.disconnectSSE();
|
|
87
|
-
this.stopPolling();
|
|
88
|
-
}
|
|
89
|
-
// --- Event dispatch ---
|
|
90
|
-
emit(name, detail) {
|
|
91
|
-
this.dispatchEvent(new CustomEvent(name, {
|
|
92
|
-
bubbles: true,
|
|
93
|
-
composed: true, // crosses shadow DOM boundary
|
|
94
|
-
detail,
|
|
95
|
-
}));
|
|
96
|
-
}
|
|
97
|
-
// --- PKCE helpers ---
|
|
98
|
-
async generateCodeVerifier() {
|
|
99
|
-
const bytes = new Uint8Array(32);
|
|
100
|
-
crypto.getRandomValues(bytes);
|
|
101
|
-
return this._base64url(bytes);
|
|
102
|
-
}
|
|
103
|
-
async computeCodeChallenge(verifier) {
|
|
104
|
-
const encoder = new TextEncoder();
|
|
105
|
-
const data = encoder.encode(verifier);
|
|
106
|
-
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
107
|
-
return this._base64url(new Uint8Array(hash));
|
|
108
|
-
}
|
|
109
|
-
_base64url(bytes) {
|
|
110
|
-
const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join('');
|
|
111
|
-
return btoa(binString).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
112
|
-
}
|
|
113
|
-
// --- Shared base styles (host-level theming via CSS custom properties) ---
|
|
114
|
-
getBaseStyles() {
|
|
115
|
-
return `
|
|
116
|
-
:host {
|
|
117
|
-
display: inline-block;
|
|
118
|
-
font-family: var(--qrauth-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
119
|
-
--_primary: var(--qrauth-primary, #00a76f);
|
|
120
|
-
--_primary-dark: var(--qrauth-primary-dark, #007a52);
|
|
121
|
-
--_text: var(--qrauth-text, #1a1a2e);
|
|
122
|
-
--_text-muted: var(--qrauth-text-muted, #637381);
|
|
123
|
-
--_bg: var(--qrauth-bg, #ffffff);
|
|
124
|
-
--_surface: var(--qrauth-surface, #f9fafb);
|
|
125
|
-
--_border: var(--qrauth-border, #e0e0e0);
|
|
126
|
-
--_radius: var(--qrauth-radius, 12px);
|
|
127
|
-
--_shadow: var(--qrauth-shadow, 0 24px 48px rgba(0,0,0,0.15));
|
|
128
|
-
--_btn-bg: var(--qrauth-btn-bg, #1b2a4a);
|
|
129
|
-
--_btn-hover: var(--qrauth-btn-hover, #263b66);
|
|
130
|
-
--_success: #00a76f;
|
|
131
|
-
--_error: #ff5630;
|
|
132
|
-
--_warning: #ffab00;
|
|
133
|
-
--_disabled: #919eab;
|
|
134
|
-
}
|
|
135
|
-
:host([theme="dark"]) {
|
|
136
|
-
--_text: var(--qrauth-text, #f0f0f0);
|
|
137
|
-
--_text-muted: var(--qrauth-text-muted, #919eab);
|
|
138
|
-
--_bg: var(--qrauth-bg, #1a1a2e);
|
|
139
|
-
--_surface: var(--qrauth-surface, #242436);
|
|
140
|
-
--_border: var(--qrauth-border, rgba(255,255,255,0.12));
|
|
141
|
-
--_shadow: var(--qrauth-shadow, 0 24px 48px rgba(0,0,0,0.5));
|
|
142
|
-
--_btn-bg: var(--qrauth-btn-bg, #263b66);
|
|
143
|
-
--_btn-hover: var(--qrauth-btn-hover, #2e4578);
|
|
144
|
-
}
|
|
145
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
146
|
-
`;
|
|
147
|
-
}
|
|
148
|
-
}
|