@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @qrauth/web-components
|
|
2
|
+
|
|
3
|
+
## 0.2.0 — 2026-04-18
|
|
4
|
+
|
|
5
|
+
### Minor
|
|
6
|
+
|
|
7
|
+
- **`<qrauth-login>` — mobile-aware pending state.** 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:authenticated` on approval.
|
|
8
|
+
- **New attribute `force-mode="mobile" | "desktop" | "auto"`** — overrides the automatic detection. Useful for demos, screenshots, and host sites that know their audience's form factor.
|
|
9
|
+
- **New attribute `mobile-fallback-only`** — when present, disables the mobile-aware UI entirely and keeps the QR-first body on every device. Intended for host sites that already provide a same-device login path around the component; the QRAuth modal is then purely the cross-device scan option.
|
|
10
|
+
- **Shared `isMobileLike()` detector on `QRAuthElement`** — base-class helper so `<qrauth-2fa>` and `<qrauth-ephemeral>` can adopt the same branch without refactor when they grow analogous mobile-aware modes.
|
|
11
|
+
- **`MOBILE_MEDIA_QUERY` export** — `'(pointer: coarse) and (hover: none)'`, for documentation and test mocks.
|
|
12
|
+
|
|
13
|
+
### Internal
|
|
14
|
+
|
|
15
|
+
- Added `vitest` + `happy-dom` test harness (`npm test` runs the new `tests/login.mobile.test.ts`).
|
|
16
|
+
- Added Playwright spec `packages/e2e/tests/qrauth-login-mobile.spec.ts` under an iPhone 13 viewport (chromium-emulated).
|
|
17
|
+
- Bundle size: 10.4 KB gzipped IIFE / 10.2 KB gzipped ESM (target was <12 KB).
|
|
18
|
+
|
|
19
|
+
### Non-breaking
|
|
20
|
+
|
|
21
|
+
- Event surface (`qrauth:authenticated`, `qrauth:scanned`, `qrauth:expired`, `qrauth:denied`, `qrauth:error`) unchanged.
|
|
22
|
+
- Desktop pending body markup unchanged.
|
|
23
|
+
- Polling / SSE path unchanged — APPROVED transitions still fire `qrauth:authenticated` regardless of whether approval completed on the hosted page or via a scanned QR.
|
|
24
|
+
|
|
25
|
+
## 0.1.0
|
|
26
|
+
|
|
27
|
+
- Initial release: `<qrauth-login>`, `<qrauth-2fa>`, `<qrauth-ephemeral>` Shadow DOM custom elements.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# @qrauth/web-components
|
|
2
|
+
|
|
3
|
+
Drop-in Web Components for [QRAuth](https://qrauth.io) — passwordless QR authentication, 2FA, and ephemeral delegated access. Shadow DOM, framework-free, ~10 KB gzipped.
|
|
4
|
+
|
|
5
|
+
- `<qrauth-login>` — passwordless sign-in, button or inline. Mobile-aware pending state (tap-to-continue CTA with QR demoted under a "Use another device" expander).
|
|
6
|
+
- `<qrauth-2fa>` — drop-in second-factor step with session-token binding.
|
|
7
|
+
- `<qrauth-ephemeral>` — time-limited, device-bound access QRs.
|
|
8
|
+
|
|
9
|
+
All three components bubble `qrauth:*` events across the Shadow DOM boundary (`composed: true`) so any framework can listen with plain `addEventListener`.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @qrauth/web-components
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or load the CDN IIFE directly in any HTML page:
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<script
|
|
21
|
+
src="https://cdn.qrauth.io/v1/components.js"
|
|
22
|
+
integrity="sha384-..."
|
|
23
|
+
crossorigin="anonymous"
|
|
24
|
+
></script>
|
|
25
|
+
|
|
26
|
+
<qrauth-login tenant="your-app-id"></qrauth-login>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<qrauth-login
|
|
33
|
+
tenant="qrauth_app_xxx"
|
|
34
|
+
theme="light"
|
|
35
|
+
base-url="https://qrauth.io"
|
|
36
|
+
></qrauth-login>
|
|
37
|
+
|
|
38
|
+
<script>
|
|
39
|
+
document.querySelector('qrauth-login').addEventListener('qrauth:authenticated', (e) => {
|
|
40
|
+
const { sessionId, user, signature } = e.detail;
|
|
41
|
+
// Exchange sessionId + signature server-side for your own session.
|
|
42
|
+
});
|
|
43
|
+
</script>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Attributes (login)
|
|
47
|
+
|
|
48
|
+
| Attribute | Values | Default | Purpose |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
| `tenant` | string | — | Your QRAuth App client ID. Required. |
|
|
51
|
+
| `theme` | `light` \| `dark` | `light` | Colour scheme. |
|
|
52
|
+
| `base-url` | URL | `https://qrauth.io` | API host. Override for staging / local dev. |
|
|
53
|
+
| `scopes` | space-separated | `identity` | OAuth-style scopes requested. |
|
|
54
|
+
| `redirect-url` | URL | — | Auto-redirect after success. |
|
|
55
|
+
| `on-auth` | string | — | Name of a global function to invoke on success. |
|
|
56
|
+
| `display` | `button` \| `inline` | `button` | Modal trigger vs. embedded widget. |
|
|
57
|
+
| `animated` | boolean | off | Enables the pulse animation on the QR. |
|
|
58
|
+
| `force-mode` | `mobile` \| `desktop` \| `auto` | `auto` | Override automatic mobile detection. |
|
|
59
|
+
| `mobile-fallback-only` | boolean | off | Keep the QR-first body on every device; disables the mobile CTA path. |
|
|
60
|
+
|
|
61
|
+
## Events
|
|
62
|
+
|
|
63
|
+
All events bubble across the Shadow DOM (`composed: true`):
|
|
64
|
+
|
|
65
|
+
- `qrauth:authenticated` — `detail: { sessionId, user, signature }`
|
|
66
|
+
- `qrauth:scanned` — fired on the PENDING → SCANNED transition (pre-approval)
|
|
67
|
+
- `qrauth:expired` — session timed out
|
|
68
|
+
- `qrauth:denied` — user denied approval on their device
|
|
69
|
+
- `qrauth:error` — `detail: { message }`
|
|
70
|
+
|
|
71
|
+
## Theming
|
|
72
|
+
|
|
73
|
+
Override any of the following CSS custom properties on the host page or on the element itself:
|
|
74
|
+
|
|
75
|
+
```css
|
|
76
|
+
qrauth-login {
|
|
77
|
+
--qrauth-primary: #00a76f;
|
|
78
|
+
--qrauth-text: #1a1a2e;
|
|
79
|
+
--qrauth-bg: #ffffff;
|
|
80
|
+
--qrauth-surface: #f9fafb;
|
|
81
|
+
--qrauth-border: #e0e0e0;
|
|
82
|
+
--qrauth-radius: 12px;
|
|
83
|
+
--qrauth-btn-bg: #1b2a4a;
|
|
84
|
+
--qrauth-btn-hover: #263b66;
|
|
85
|
+
--qrauth-shadow: 0 24px 48px rgba(0, 0, 0, 0.15);
|
|
86
|
+
--qrauth-font: 'Inter', sans-serif;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Or flip the bundled dark palette via `theme="dark"`.
|
|
91
|
+
|
|
92
|
+
## Framework integration
|
|
93
|
+
|
|
94
|
+
The components are framework-free native Custom Elements. They work in React, Vue, Svelte, Angular, plain HTML — wherever the browser exposes `customElements`. For React, wrap event listeners in `useEffect` because React's synthetic event system doesn't observe custom events directly.
|
|
95
|
+
|
|
96
|
+
## Links
|
|
97
|
+
|
|
98
|
+
- [Homepage](https://qrauth.io)
|
|
99
|
+
- [Docs](https://docs.qrauth.io)
|
|
100
|
+
- [Signing Architecture](https://docs.qrauth.io/guide/signing-architecture) — cryptographic model behind every approved session
|
|
101
|
+
- [Issues](https://github.com/qrauth-io/qrauth/issues)
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT.
|
package/dist/base.d.ts
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* Provides: Shadow DOM setup, CSS custom property theming, SSE connection management,
|
|
4
4
|
* lifecycle hooks, and event dispatching.
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* Media query that identifies mobile-like devices (coarse pointer + no hover).
|
|
8
|
+
* Exported for documentation and test mocks.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MOBILE_MEDIA_QUERY = "(pointer: coarse) and (hover: none)";
|
|
6
11
|
export declare abstract class QRAuthElement extends HTMLElement {
|
|
7
12
|
protected shadow: ShadowRoot;
|
|
8
13
|
private _sseConnection;
|
|
@@ -16,6 +21,22 @@ export declare abstract class QRAuthElement extends HTMLElement {
|
|
|
16
21
|
disconnectedCallback(): void;
|
|
17
22
|
attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void;
|
|
18
23
|
protected abstract render(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Detect whether the component is running on a mobile-like device. Subclasses
|
|
26
|
+
* may use this to branch UX — e.g. `<qrauth-login>` swaps the big QR for a
|
|
27
|
+
* "Continue on this device" CTA when isMobileLike() returns true, since the
|
|
28
|
+
* scan-with-another-device flow is a dead end when the user already IS the
|
|
29
|
+
* phone. Resolution order:
|
|
30
|
+
*
|
|
31
|
+
* 1. `force-mode="mobile"` or `force-mode="desktop"` attribute — wins.
|
|
32
|
+
* 2. `mobile-fallback-only` attribute — always returns false (keeps the
|
|
33
|
+
* component's QR-only behaviour even on phones; consumers opt in here
|
|
34
|
+
* when they provide their own alternative flow around the component).
|
|
35
|
+
* 3. matchMedia on `(pointer: coarse)` + `(hover: none)` (both must match).
|
|
36
|
+
* 4. UA sniff fallback for environments without matchMedia (headless tests,
|
|
37
|
+
* old browsers).
|
|
38
|
+
*/
|
|
39
|
+
protected isMobileLike(): boolean;
|
|
19
40
|
protected connectSSE(url: string): void;
|
|
20
41
|
protected onSSEEvent(event: string, callback: (data: unknown) => void): void;
|
|
21
42
|
protected disconnectSSE(): void;
|
package/dist/login.d.ts
CHANGED
|
@@ -3,14 +3,24 @@ import { QRAuthElement } from './base.js';
|
|
|
3
3
|
* <qrauth-login> — Drop-in QR authentication button / inline widget.
|
|
4
4
|
*
|
|
5
5
|
* Attributes:
|
|
6
|
-
* tenant
|
|
7
|
-
* theme
|
|
8
|
-
* base-url
|
|
9
|
-
* scopes
|
|
10
|
-
* redirect-url
|
|
11
|
-
* on-auth
|
|
12
|
-
* display
|
|
13
|
-
* animated
|
|
6
|
+
* tenant — Your QRAuth App client ID (required)
|
|
7
|
+
* theme — "light" (default) | "dark"
|
|
8
|
+
* base-url — API base URL (default: https://qrauth.io)
|
|
9
|
+
* scopes — Space-separated scopes (default: "identity")
|
|
10
|
+
* redirect-url — Optional redirect after successful auth
|
|
11
|
+
* on-auth — Name of a global callback function to call on success
|
|
12
|
+
* display — "button" (default) | "inline"
|
|
13
|
+
* animated — If present, enables subtle QR pulse animation
|
|
14
|
+
* force-mode — "mobile" | "desktop" | "auto" (default "auto").
|
|
15
|
+
* Overrides the automatic mobile-like detection that
|
|
16
|
+
* swaps the QR-first body for a "Continue with
|
|
17
|
+
* QRAuth" CTA + QR expander on phones and tablets.
|
|
18
|
+
* mobile-fallback-only — If present, disables the mobile-aware UI entirely
|
|
19
|
+
* and keeps the QR-first body on every device. Use
|
|
20
|
+
* this when the surrounding page already provides an
|
|
21
|
+
* alternative same-device login path and the
|
|
22
|
+
* component is only there as the cross-device scan
|
|
23
|
+
* option.
|
|
14
24
|
*
|
|
15
25
|
* Events (bubbles + composed, cross shadow-DOM):
|
|
16
26
|
* qrauth:authenticated — detail: { sessionId, user, signature }
|
|
@@ -25,6 +35,7 @@ import { QRAuthElement } from './base.js';
|
|
|
25
35
|
export declare class QRAuthLogin extends QRAuthElement {
|
|
26
36
|
static get observedAttributes(): string[];
|
|
27
37
|
private _sessionId;
|
|
38
|
+
private _sessionToken;
|
|
28
39
|
private _codeVerifier;
|
|
29
40
|
private _status;
|
|
30
41
|
private _expiresAt;
|
|
@@ -35,6 +46,8 @@ export declare class QRAuthLogin extends QRAuthElement {
|
|
|
35
46
|
get onAuth(): string | null;
|
|
36
47
|
get display(): string;
|
|
37
48
|
get animated(): boolean;
|
|
49
|
+
get forceMode(): 'mobile' | 'desktop' | 'auto';
|
|
50
|
+
get mobileFallbackOnly(): boolean;
|
|
38
51
|
disconnectedCallback(): void;
|
|
39
52
|
protected render(): void;
|
|
40
53
|
private _renderButton;
|
|
@@ -42,10 +55,30 @@ export declare class QRAuthLogin extends QRAuthElement {
|
|
|
42
55
|
private _inlineIdleContent;
|
|
43
56
|
private _modalContent;
|
|
44
57
|
private _bodyForStatus;
|
|
58
|
+
/**
|
|
59
|
+
* Shared QR frame markup used by both the desktop pending body and the
|
|
60
|
+
* mobile "Use another device" expander. Centralised so the two paths can't
|
|
61
|
+
* drift.
|
|
62
|
+
*/
|
|
63
|
+
private _qrFrameHtml;
|
|
64
|
+
/**
|
|
65
|
+
* Mobile-aware pending body. Primary CTA opens the hosted approval page
|
|
66
|
+
* (/a/:token) in a new tab — this same-device path short-circuits the
|
|
67
|
+
* cross-device QR scan that is a dead end on phones. The QR itself is
|
|
68
|
+
* demoted to a collapsed expander for the "I have a second device" case.
|
|
69
|
+
*/
|
|
70
|
+
private _mobilePendingBody;
|
|
45
71
|
private _sharedModalStyles;
|
|
46
72
|
private _bindButtonEvents;
|
|
47
73
|
private _bindInlineEvents;
|
|
48
74
|
private _bindModalButtons;
|
|
75
|
+
/**
|
|
76
|
+
* Mobile same-device path: open the hosted approval page in a new tab.
|
|
77
|
+
* The existing polling loop in _beginPolling picks up SCANNED / APPROVED
|
|
78
|
+
* transitions from the server once the user completes the flow there,
|
|
79
|
+
* so qrauth:scanned and qrauth:authenticated fire unchanged.
|
|
80
|
+
*/
|
|
81
|
+
private _openApprovalPage;
|
|
49
82
|
private _openModal;
|
|
50
83
|
private _closeModal;
|
|
51
84
|
private _startAuth;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/* @qrauth/web-components v0.
|
|
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"]}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()}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.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`
|
|
3
3
|
:host {
|
|
4
4
|
display: inline-block;
|
|
5
5
|
font-family: var(--qrauth-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
@@ -52,7 +52,7 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
|
|
|
52
52
|
</svg>`,k=`<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,"_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")}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"]}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; }
|
|
@@ -196,16 +196,12 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
|
|
|
196
196
|
<div class="spinner-wrap">${k}</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">'+f+"</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 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
|
|
|
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">'+f+"</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 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
|
|
|
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 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
|
|
|
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 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._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,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 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">
|
|
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 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
|
|
|
465
528
|
</svg>`,S=`<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>`,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=`
|
|
469
532
|
<style>
|
|
470
533
|
${this.getBaseStyles()}
|
|
471
534
|
:host { display: inline-block; }
|
|
@@ -827,7 +890,7 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
|
|
|
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,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",
|
|
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">
|
|
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 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
|
|
|
846
909
|
stroke-linecap="round" stroke-linejoin="round"/>
|
|
847
910
|
</svg>`,_=`<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>`,A=`<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>`,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=`
|
|
853
916
|
<style>
|
|
854
917
|
${this.getBaseStyles()}
|
|
855
918
|
:host { display: block; }
|
|
@@ -943,7 +1006,7 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
|
|
|
943
1006
|
`;case"loading":return`
|
|
944
1007
|
${i}
|
|
945
1008
|
<div class="state-loading">
|
|
946
|
-
<div class="spinner-wrap">${
|
|
1009
|
+
<div class="spinner-wrap">${A}</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 @@ var w=Object.defineProperty;var E=(h,d,e)=>d in h?w(h,d,{enumerable:!0,configura
|
|
|
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 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",
|
|
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};
|
|
1177
1240
|
//# sourceMappingURL=qrauth-components.esm.js.map
|