@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,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/base.ts", "../src/login.ts", "../src/ephemeral.ts", "../src/twofa.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * QRAuthElement \u2014 base class for all QRAuth Web Components.\n * Provides: Shadow DOM setup, CSS custom property theming, SSE connection management,\n * lifecycle hooks, and event dispatching.\n */\nexport abstract class QRAuthElement extends HTMLElement {\n protected shadow: ShadowRoot;\n private _sseConnection: EventSource | null = null;\n private _pollInterval: ReturnType<typeof setInterval> | null = null;\n\n static get observedAttributes(): string[] {\n return ['tenant', 'theme', 'base-url'];\n }\n\n constructor() {\n super();\n this.shadow = this.attachShadow({ mode: 'open' });\n }\n\n // Getters for common attributes\n get tenant(): string { return this.getAttribute('tenant') || ''; }\n get theme(): string { return this.getAttribute('theme') || 'light'; }\n get baseUrl(): string {\n const val = this.getAttribute('base-url');\n // Allow empty string for local dev (relative URLs), fallback to production\n return val !== null ? val : 'https://qrauth.io';\n }\n\n connectedCallback(): void {\n this.render();\n }\n\n disconnectedCallback(): void {\n this.cleanup();\n }\n\n attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue !== newValue) {\n this.render();\n }\n }\n\n // Subclasses implement this\n protected abstract render(): void;\n\n // --- SSE Management ---\n protected connectSSE(url: string): void {\n this.disconnectSSE();\n // EventSource doesn't support custom headers natively.\n // For public (unauthenticated) SSE endpoints this works directly.\n this._sseConnection = new EventSource(url);\n\n this._sseConnection.onerror = () => {\n // Fall back to polling on SSE failure \u2014 caller should initiate polling\n this.disconnectSSE();\n };\n }\n\n protected onSSEEvent(event: string, callback: (data: unknown) => void): void {\n if (this._sseConnection) {\n this._sseConnection.addEventListener(event, (e: Event) => {\n const msg = e as MessageEvent;\n try { callback(JSON.parse(msg.data as string)); } catch { /* invalid JSON */ }\n });\n }\n }\n\n protected disconnectSSE(): void {\n if (this._sseConnection) {\n this._sseConnection.close();\n this._sseConnection = null;\n }\n }\n\n // --- Polling (fallback / primary for authenticated endpoints) ---\n protected startPolling(\n url: string,\n intervalMs: number,\n callback: (data: unknown) => void,\n headers?: Record<string, string>,\n ): void {\n this.stopPolling();\n const poll = async () => {\n try {\n const res = await fetch(url, { headers });\n if (res.ok) callback(await res.json());\n } catch { /* network error \u2014 retry next interval */ }\n };\n poll(); // immediate first poll\n this._pollInterval = setInterval(poll, intervalMs);\n }\n\n protected stopPolling(): void {\n if (this._pollInterval) {\n clearInterval(this._pollInterval);\n this._pollInterval = null;\n }\n }\n\n // --- Cleanup ---\n protected cleanup(): void {\n this.disconnectSSE();\n this.stopPolling();\n }\n\n // --- Event dispatch ---\n protected emit(name: string, detail?: unknown): void {\n this.dispatchEvent(new CustomEvent(name, {\n bubbles: true,\n composed: true, // crosses shadow DOM boundary\n detail,\n }));\n }\n\n // --- PKCE helpers ---\n protected async generateCodeVerifier(): Promise<string> {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return this._base64url(bytes);\n }\n\n protected async computeCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest('SHA-256', data);\n return this._base64url(new Uint8Array(hash));\n }\n\n private _base64url(bytes: Uint8Array): string {\n const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join('');\n return btoa(binString).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n }\n\n // --- Shared base styles (host-level theming via CSS custom properties) ---\n protected getBaseStyles(): string {\n return `\n :host {\n display: inline-block;\n font-family: var(--qrauth-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n --_primary: var(--qrauth-primary, #00a76f);\n --_primary-dark: var(--qrauth-primary-dark, #007a52);\n --_text: var(--qrauth-text, #1a1a2e);\n --_text-muted: var(--qrauth-text-muted, #637381);\n --_bg: var(--qrauth-bg, #ffffff);\n --_surface: var(--qrauth-surface, #f9fafb);\n --_border: var(--qrauth-border, #e0e0e0);\n --_radius: var(--qrauth-radius, 12px);\n --_shadow: var(--qrauth-shadow, 0 24px 48px rgba(0,0,0,0.15));\n --_btn-bg: var(--qrauth-btn-bg, #1b2a4a);\n --_btn-hover: var(--qrauth-btn-hover, #263b66);\n --_success: #00a76f;\n --_error: #ff5630;\n --_warning: #ffab00;\n --_disabled: #919eab;\n }\n :host([theme=\"dark\"]) {\n --_text: var(--qrauth-text, #f0f0f0);\n --_text-muted: var(--qrauth-text-muted, #919eab);\n --_bg: var(--qrauth-bg, #1a1a2e);\n --_surface: var(--qrauth-surface, #242436);\n --_border: var(--qrauth-border, rgba(255,255,255,0.12));\n --_shadow: var(--qrauth-shadow, 0 24px 48px rgba(0,0,0,0.5));\n --_btn-bg: var(--qrauth-btn-bg, #263b66);\n --_btn-hover: var(--qrauth-btn-hover, #2e4578);\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n `;\n }\n}\n", "import { QRAuthElement } from './base.js';\n\n// Same shield SVG as the browser SDK\nconst SHIELD_SVG = `<svg viewBox=\"0 0 200 200\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M48 24 H152 Q166 24 166 38 V108 Q166 156 100 182 Q34 156 34 108 V38 Q34 24 48 24 Z\"\n fill=\"none\" stroke=\"#fff\" stroke-width=\"5\"/>\n <g fill=\"#fff\" opacity=\"0.2\">\n <rect x=\"52\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"78\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"130\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"52\" y=\"66\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"104\" y=\"66\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"78\" y=\"92\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"130\" y=\"92\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"52\" y=\"118\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"104\" y=\"118\" width=\"18\" height=\"18\" rx=\"3\"/>\n </g>\n <path d=\"M62 104 L88 134 L144 64\"\n fill=\"none\" stroke=\"#fff\" stroke-width=\"15\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>`;\n\n// Check icon for success state\nconst CHECK_SVG = `<svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"currentColor\"/>\n</svg>`;\n\n// Spinner SVG\nconst SPINNER_SVG = `<svg class=\"spinner\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-dasharray=\"31.416\" stroke-dashoffset=\"10\" stroke-linecap=\"round\"/>\n</svg>`;\n\ntype AuthStatus =\n | 'idle'\n | 'loading'\n | 'pending'\n | 'SCANNED'\n | 'APPROVED'\n | 'DENIED'\n | 'EXPIRED'\n | 'error';\n\ninterface AuthSession {\n sessionId: string;\n qrUrl: string;\n expiresAt: string;\n status: AuthStatus;\n}\n\ninterface PollResponse {\n sessionId: string;\n status: AuthStatus;\n user?: Record<string, unknown>;\n signature?: string;\n}\n\n/**\n * <qrauth-login> \u2014 Drop-in QR authentication button / inline widget.\n *\n * Attributes:\n * tenant \u2014 Your QRAuth App client ID (required)\n * theme \u2014 \"light\" (default) | \"dark\"\n * base-url \u2014 API base URL (default: https://qrauth.io)\n * scopes \u2014 Space-separated scopes (default: \"identity\")\n * redirect-url \u2014 Optional redirect after successful auth\n * on-auth \u2014 Name of a global callback function to call on success\n * display \u2014 \"button\" (default) | \"inline\"\n * animated \u2014 If present, enables subtle QR pulse animation\n *\n * Events (bubbles + composed, cross shadow-DOM):\n * qrauth:authenticated \u2014 detail: { sessionId, user, signature }\n * qrauth:expired \u2014 fired when session times out\n * qrauth:error \u2014 detail: { message }\n * qrauth:scanned \u2014 fired when QR is scanned (before approval)\n *\n * @example\n * <qrauth-login tenant=\"qrauth_app_xxx\"></qrauth-login>\n * <qrauth-login tenant=\"qrauth_app_xxx\" display=\"inline\" theme=\"dark\"></qrauth-login>\n */\nexport class QRAuthLogin extends QRAuthElement {\n static override get observedAttributes(): string[] {\n return [...super.observedAttributes, 'scopes', 'redirect-url', 'on-auth', 'display', 'animated'];\n }\n\n private _sessionId: string | null = null;\n private _codeVerifier: string | null = null;\n private _status: AuthStatus = 'idle';\n private _expiresAt: number | null = null;\n private _timerInterval: ReturnType<typeof setInterval> | null = null;\n private _pollTimeout: ReturnType<typeof setTimeout> | null = null;\n\n get scopes(): string { return this.getAttribute('scopes') || 'identity'; }\n get redirectUrl(): string | null { return this.getAttribute('redirect-url'); }\n get onAuth(): string | null { return this.getAttribute('on-auth'); }\n get display(): string { return this.getAttribute('display') || 'button'; }\n get animated(): boolean { return this.hasAttribute('animated'); }\n\n // ---- Lifecycle ---------------------------------------------------------\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this._clearTimers();\n }\n\n // ---- Render ------------------------------------------------------------\n\n protected render(): void {\n if (this.display === 'inline') {\n this._renderInline();\n } else {\n this._renderButton();\n }\n }\n\n private _renderButton(): void {\n this.shadow.innerHTML = `\n <style>\n ${this.getBaseStyles()}\n :host { display: inline-block; }\n\n .btn {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 12px 24px;\n background: var(--_btn-bg);\n color: #fff;\n border: none;\n border-radius: var(--_radius);\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.2s, transform 0.15s, box-shadow 0.2s;\n user-select: none;\n line-height: 1.4;\n font-family: inherit;\n }\n .btn:hover:not(:disabled) {\n background: var(--_btn-hover);\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(27,42,74,0.3);\n }\n .btn:active:not(:disabled) { transform: translateY(0); }\n .btn:disabled { opacity: 0.6; cursor: not-allowed; }\n .btn-icon { width: 22px; height: 22px; flex-shrink: 0; }\n\n /* Modal overlay */\n .overlay {\n position: fixed;\n inset: 0;\n background: rgba(0,0,0,0.55);\n z-index: 99999;\n display: flex;\n align-items: center;\n justify-content: center;\n animation: fade-in 0.2s ease;\n }\n .overlay.hidden { display: none; }\n\n @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }\n @keyframes slide-up {\n from { transform: translateY(24px); opacity: 0; }\n to { transform: translateY(0); opacity: 1; }\n }\n\n .modal {\n background: var(--_bg);\n border-radius: 16px;\n padding: 32px 28px 24px;\n max-width: 380px;\n width: 90vw;\n box-shadow: var(--_shadow);\n text-align: center;\n animation: slide-up 0.3s ease;\n position: relative;\n }\n\n ${this._sharedModalStyles()}\n </style>\n\n <button class=\"btn\" id=\"open-btn\" aria-label=\"Sign in with QRAuth\">\n <span class=\"btn-icon\">${SHIELD_SVG}</span>\n Sign in with QRAuth\n </button>\n\n <div class=\"overlay hidden\" id=\"overlay\" role=\"dialog\" aria-modal=\"true\" aria-label=\"QRAuth sign-in\">\n <div class=\"modal\">\n ${this._modalContent()}\n </div>\n </div>\n `;\n\n this._bindButtonEvents();\n }\n\n private _renderInline(): void {\n this.shadow.innerHTML = `\n <style>\n ${this.getBaseStyles()}\n :host { display: block; }\n\n .inline-container {\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: 16px;\n padding: 28px 24px 20px;\n text-align: center;\n max-width: 360px;\n margin: 0 auto;\n }\n\n /* Idle state: show start button */\n .start-btn {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 12px 24px;\n background: var(--_btn-bg);\n color: #fff;\n border: none;\n border-radius: var(--_radius);\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.2s, transform 0.15s, box-shadow 0.2s;\n font-family: inherit;\n width: 100%;\n justify-content: center;\n }\n .start-btn:hover:not(:disabled) {\n background: var(--_btn-hover);\n transform: translateY(-1px);\n }\n .start-btn:disabled { opacity: 0.6; cursor: not-allowed; }\n .btn-icon { width: 22px; height: 22px; flex-shrink: 0; }\n\n ${this._sharedModalStyles()}\n </style>\n\n <div class=\"inline-container\">\n <div id=\"inline-body\">\n ${this._inlineIdleContent()}\n </div>\n </div>\n `;\n\n this._bindInlineEvents();\n }\n\n private _inlineIdleContent(): string {\n return `\n <button class=\"start-btn\" id=\"start-btn\" aria-label=\"Sign in with QRAuth\">\n <span class=\"btn-icon\">${SHIELD_SVG}</span>\n Sign in with QRAuth\n </button>\n `;\n }\n\n // ---- Shared modal HTML -------------------------------------------------\n\n private _modalContent(): string {\n return `\n <button class=\"close-btn\" id=\"close-btn\" aria-label=\"Close\">×</button>\n <div id=\"modal-body\">\n ${this._bodyForStatus('idle')}\n </div>\n `;\n }\n\n private _bodyForStatus(status: AuthStatus, data?: {\n qrUrl?: string;\n sessionId?: string;\n timerHtml?: string;\n errorMessage?: string;\n }): string {\n switch (status) {\n case 'idle':\n return `\n <div class=\"state-header\">\n <div class=\"brand-icon\">${SHIELD_SVG}</div>\n <h3>Sign in with QRAuth</h3>\n <p>Secure, passwordless authentication</p>\n </div>\n `;\n\n case 'loading':\n return `\n <div class=\"state-loading\">\n <div class=\"spinner-wrap\">${SPINNER_SVG}</div>\n <p class=\"status-text\">Generating QR code...</p>\n </div>\n `;\n\n case 'pending':\n case 'SCANNED': {\n const qrImageUrl = data?.qrUrl\n ? `https://api.qrserver.com/v1/create-qr-code/?size=440x440&data=${encodeURIComponent(data.qrUrl)}`\n : '';\n\n const isScanned = status === 'SCANNED';\n const scanClass = isScanned ? 'scanned' : '';\n\n return `\n <h3>Sign in with QRAuth</h3>\n <p class=\"subtitle\">Scan this QR code with your phone camera</p>\n <div class=\"qr-frame ${scanClass}${this.animated ? ' animated' : ''}\">\n <img src=\"${qrImageUrl}\" alt=\"QR Code for authentication\" width=\"220\" height=\"220\" loading=\"lazy\"/>\n <div class=\"qr-badge\">${SHIELD_SVG}</div>\n ${isScanned ? '<div class=\"scanned-overlay\"><span class=\"scanned-check\">' + CHECK_SVG + '</span></div>' : ''}\n </div>\n <div class=\"status-text ${isScanned ? 'status-scanned' : ''}\">\n ${isScanned\n ? '✓ QR code scanned — waiting for approval...'\n : 'Waiting for scan...'}\n </div>\n <div class=\"timer\" id=\"timer\">${data?.timerHtml ?? ''}</div>\n <div class=\"footer\">Secured by <a href=\"https://qrauth.io\" target=\"_blank\" rel=\"noopener\">QRAuth</a></div>\n `;\n }\n\n case 'APPROVED':\n return `\n <div class=\"state-approved\">\n <div class=\"success-icon\">${CHECK_SVG}</div>\n <h3>Identity verified!</h3>\n <p class=\"subtitle\">You are now signed in</p>\n </div>\n `;\n\n case 'DENIED':\n return `\n <div class=\"state-denied\">\n <div class=\"error-icon\">✗</div>\n <h3>Authentication denied</h3>\n <p class=\"subtitle\">The request was declined on your device.</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Try again</button>\n </div>\n `;\n\n case 'EXPIRED':\n return `\n <div class=\"state-expired\">\n <div class=\"expired-icon\">⌛</div>\n <h3>Session expired</h3>\n <p class=\"subtitle\">The QR code has timed out.</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Try again</button>\n </div>\n `;\n\n case 'error':\n return `\n <div class=\"state-error\">\n <div class=\"error-icon\">⚠</div>\n <h3>Something went wrong</h3>\n <p class=\"subtitle\">${data?.errorMessage ?? 'Failed to start authentication.'}</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Try again</button>\n </div>\n `;\n\n default:\n return '';\n }\n }\n\n // ---- Shared CSS for modal content --------------------------------------\n\n private _sharedModalStyles(): string {\n return `\n /* Typography */\n h3 {\n font-size: 18px;\n font-weight: 700;\n color: var(--_text);\n margin: 0 0 6px;\n }\n .subtitle {\n font-size: 13px;\n color: var(--_text-muted);\n margin: 0 0 20px;\n line-height: 1.5;\n }\n\n /* Close button */\n .close-btn {\n position: absolute;\n top: 12px;\n right: 14px;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--_text-muted);\n font-size: 22px;\n line-height: 1;\n padding: 4px 6px;\n border-radius: 6px;\n transition: color 0.15s, background 0.15s;\n font-family: inherit;\n }\n .close-btn:hover { color: var(--_text); background: var(--_surface); }\n\n /* Brand icon in idle state */\n .state-header { padding: 8px 0 4px; }\n .brand-icon {\n width: 56px;\n height: 56px;\n background: var(--_btn-bg);\n border-radius: 14px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 14px;\n padding: 10px;\n }\n .brand-icon svg { width: 36px; height: 36px; }\n\n /* Loading state */\n .state-loading {\n padding: 32px 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n }\n .spinner-wrap { color: var(--_primary); }\n .spinner {\n width: 40px;\n height: 40px;\n animation: spin 0.8s linear infinite;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n\n /* QR frame */\n .qr-frame {\n display: inline-block;\n padding: 14px;\n background: #fff;\n border: 2px solid var(--_border);\n border-radius: var(--_radius);\n margin-bottom: 14px;\n position: relative;\n transition: border-color 0.3s;\n }\n .qr-frame.scanned { border-color: var(--_success); }\n .qr-frame.animated { animation: qr-pulse 2s ease-in-out infinite; }\n @keyframes qr-pulse {\n 0%, 100% { box-shadow: 0 0 0 0 rgba(0,167,111,0); }\n 50% { box-shadow: 0 0 0 8px rgba(0,167,111,0.15); }\n }\n .qr-frame img { display: block; width: 220px; height: 220px; border-radius: 4px; }\n .qr-badge {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 52px;\n height: 52px;\n background: #1b2a4a;\n border-radius: 10px;\n padding: 6px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .qr-badge svg { width: 100%; height: 100%; }\n\n /* Scanned overlay on QR */\n .scanned-overlay {\n position: absolute;\n inset: 0;\n background: rgba(0,167,111,0.08);\n border-radius: calc(var(--_radius) - 2px);\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .scanned-check {\n width: 48px;\n height: 48px;\n background: var(--_success);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n opacity: 0;\n animation: pop-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.1s forwards;\n }\n .scanned-check svg { width: 28px; height: 28px; }\n @keyframes pop-in {\n from { transform: scale(0.5); opacity: 0; }\n to { transform: scale(1); opacity: 1; }\n }\n\n /* Status text */\n .status-text {\n font-size: 13px;\n color: var(--_text-muted);\n min-height: 20px;\n }\n .status-scanned {\n color: var(--_success);\n font-weight: 600;\n }\n\n /* Timer */\n .timer {\n font-size: 12px;\n color: var(--_disabled);\n margin-top: 8px;\n font-variant-numeric: tabular-nums;\n min-height: 18px;\n }\n\n /* Success state */\n .state-approved {\n padding: 24px 0 12px;\n animation: fade-scale-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);\n }\n @keyframes fade-scale-in {\n from { transform: scale(0.85); opacity: 0; }\n to { transform: scale(1); opacity: 1; }\n }\n .success-icon {\n width: 64px;\n height: 64px;\n background: var(--_success);\n border-radius: 50%;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n margin-bottom: 16px;\n }\n .success-icon svg { width: 36px; height: 36px; }\n\n /* Denied / expired / error states */\n .state-denied, .state-expired, .state-error {\n padding: 20px 0 8px;\n }\n .error-icon, .expired-icon {\n font-size: 48px;\n margin-bottom: 12px;\n line-height: 1;\n display: block;\n }\n .state-denied .error-icon { color: var(--_error); }\n .state-expired .expired-icon { color: var(--_disabled); }\n .state-error .error-icon { color: var(--_warning); }\n\n /* Retry button */\n .retry-btn {\n margin-top: 14px;\n padding: 9px 22px;\n background: var(--_surface);\n border: 1px solid var(--_border);\n border-radius: 8px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n color: var(--_text-muted);\n font-family: inherit;\n transition: background 0.15s, border-color 0.15s;\n }\n .retry-btn:hover { background: var(--_border); color: var(--_text); }\n\n /* Footer */\n .footer {\n margin-top: 18px;\n font-size: 10px;\n color: #c4cdd5;\n }\n .footer a { color: var(--_disabled); text-decoration: none; }\n .footer a:hover { text-decoration: underline; }\n `;\n }\n\n // ---- Event binding -----------------------------------------------------\n\n private _bindButtonEvents(): void {\n const openBtn = this.shadow.getElementById('open-btn');\n openBtn?.addEventListener('click', () => {\n this._openModal();\n this._startAuth();\n });\n\n // Close on overlay click\n const overlay = this.shadow.getElementById('overlay');\n overlay?.addEventListener('click', (e) => {\n if (e.target === overlay) this._closeModal();\n });\n\n this._bindModalButtons();\n }\n\n private _bindInlineEvents(): void {\n const startBtn = this.shadow.getElementById('start-btn');\n startBtn?.addEventListener('click', () => {\n this._startAuth();\n });\n }\n\n private _bindModalButtons(): void {\n this.shadow.getElementById('close-btn')?.addEventListener('click', () => this._closeModal());\n this.shadow.getElementById('retry-btn')?.addEventListener('click', () => this._retry());\n }\n\n // ---- Modal open / close (button display mode) --------------------------\n\n private _openModal(): void {\n const overlay = this.shadow.getElementById('overlay');\n overlay?.classList.remove('hidden');\n this._updateBody(this._bodyForStatus('idle'));\n }\n\n private _closeModal(): void {\n const overlay = this.shadow.getElementById('overlay');\n overlay?.classList.add('hidden');\n this._clearTimers();\n this._sessionId = null;\n this._codeVerifier = null;\n this._status = 'idle';\n this._expiresAt = null;\n }\n\n // ---- Auth flow ---------------------------------------------------------\n\n private async _startAuth(): Promise<void> {\n this._status = 'loading';\n this._updateBody(this._bodyForStatus('loading'));\n\n try {\n this._codeVerifier = await this.generateCodeVerifier();\n const challenge = await this.computeCodeChallenge(this._codeVerifier);\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-Client-Id': this.tenant,\n };\n\n const body: Record<string, unknown> = {\n scopes: this.scopes.split(/\\s+/).filter(Boolean),\n codeChallenge: challenge,\n codeChallengeMethod: 'S256',\n };\n\n const res = await fetch(`${this.baseUrl}/api/v1/auth-sessions`, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const err = await res.json().catch(() => ({ message: 'Failed to create session' }));\n throw new Error((err as { message?: string }).message ?? 'Failed to create session');\n }\n\n const session = await res.json() as AuthSession;\n this._sessionId = session.sessionId;\n this._expiresAt = new Date(session.expiresAt).getTime();\n this._status = 'pending';\n\n this._updateBody(this._bodyForStatus('pending', { qrUrl: session.qrUrl }));\n this._bindModalButtons();\n this._startCountdown();\n this._beginPolling(session.sessionId);\n\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unexpected error';\n this._status = 'error';\n this._updateBody(this._bodyForStatus('error', { errorMessage: message }));\n this._bindModalButtons();\n this.emit('qrauth:error', { message });\n }\n }\n\n private _retry(): void {\n this._clearTimers();\n this._sessionId = null;\n this._codeVerifier = null;\n this._status = 'idle';\n this._expiresAt = null;\n\n if (this.display === 'inline') {\n // Reset inline to show the start button again\n const inlineBody = this.shadow.getElementById('inline-body');\n if (inlineBody) {\n inlineBody.innerHTML = this._inlineIdleContent();\n this._bindInlineEvents();\n }\n } else {\n this._startAuth();\n }\n }\n\n // ---- Polling -----------------------------------------------------------\n\n private _beginPolling(sessionId: string): void {\n let lastStatus: string = 'PENDING';\n let interval = 2000;\n const maxInterval = 5000;\n let errorCount = 0;\n const maxErrors = 20;\n\n const poll = async () => {\n if (!this._sessionId) return; // component was reset\n\n let url = `${this.baseUrl}/api/v1/auth-sessions/${sessionId}`;\n if (this._codeVerifier) {\n url += `?code_verifier=${encodeURIComponent(this._codeVerifier)}`;\n }\n\n try {\n const res = await fetch(url, {\n headers: { 'X-Client-Id': this.tenant },\n });\n const data = await res.json() as PollResponse;\n errorCount = 0;\n interval = 2000;\n\n if (data.status !== lastStatus) {\n lastStatus = data.status;\n this._handleStatusChange(data.status, data);\n\n if (['APPROVED', 'DENIED', 'EXPIRED'].includes(data.status)) {\n return; // stop polling\n }\n }\n\n this._pollTimeout = setTimeout(poll, interval);\n\n } catch {\n errorCount++;\n if (errorCount >= maxErrors) {\n this._handleStatusChange('EXPIRED', undefined);\n return;\n }\n interval = Math.min(interval * 1.5, maxInterval);\n this._pollTimeout = setTimeout(poll, interval);\n }\n };\n\n this._pollTimeout = setTimeout(poll, interval);\n }\n\n // ---- Status handling ---------------------------------------------------\n\n private _handleStatusChange(status: AuthStatus, data?: PollResponse): void {\n this._status = status;\n\n switch (status) {\n case 'SCANNED': {\n // Keep QR visible but show scanned overlay\n const currentQrUrl = this.shadow.querySelector<HTMLImageElement>('.qr-frame img')?.src;\n // Extract the data param from the QR image URL\n const rawQrUrl = currentQrUrl\n ? decodeURIComponent(currentQrUrl.replace(/.*[?&]data=/, '').split('&')[0])\n : '';\n\n this._updateBody(this._bodyForStatus('SCANNED', {\n qrUrl: rawQrUrl,\n timerHtml: this._formatTimer(),\n }));\n this._bindModalButtons();\n this.emit('qrauth:scanned', data);\n break;\n }\n\n case 'APPROVED': {\n this._clearTimers();\n this._updateBody(this._bodyForStatus('APPROVED'));\n this._bindModalButtons();\n this.emit('qrauth:authenticated', {\n sessionId: data?.sessionId,\n user: data?.user,\n signature: data?.signature,\n });\n\n // Call the on-auth global callback if set\n const callbackName = this.onAuth;\n if (callbackName) {\n const globalCallback = (window as unknown as Record<string, unknown>)[callbackName];\n if (typeof globalCallback === 'function') {\n (globalCallback as (d: unknown) => void)(data);\n }\n }\n\n // Redirect if configured\n if (this.redirectUrl) {\n setTimeout(() => { window.location.href = this.redirectUrl!; }, 1500);\n } else if (this.display === 'button') {\n // Auto-close modal after success animation\n setTimeout(() => this._closeModal(), 2200);\n }\n break;\n }\n\n case 'DENIED':\n this._clearTimers();\n this._updateBody(this._bodyForStatus('DENIED'));\n this._bindModalButtons();\n this.emit('qrauth:denied');\n break;\n\n case 'EXPIRED':\n this._clearTimers();\n this._status = 'EXPIRED';\n this._updateBody(this._bodyForStatus('EXPIRED'));\n this._bindModalButtons();\n this.emit('qrauth:expired');\n break;\n }\n }\n\n // ---- Countdown timer ---------------------------------------------------\n\n private _startCountdown(): void {\n this._timerInterval = setInterval(() => {\n const timerEl = this.shadow.getElementById('timer');\n if (!timerEl || !this._expiresAt) return;\n\n const remaining = Math.max(0, Math.floor((this._expiresAt - Date.now()) / 1000));\n timerEl.textContent = remaining > 0 ? this._formatTimer(remaining) : '';\n\n if (remaining <= 0) {\n clearInterval(this._timerInterval!);\n this._timerInterval = null;\n if (this._status === 'pending' || this._status === 'SCANNED') {\n this._handleStatusChange('EXPIRED', undefined);\n }\n }\n }, 1000);\n }\n\n private _formatTimer(seconds?: number): string {\n const s = seconds ?? (this._expiresAt\n ? Math.max(0, Math.floor((this._expiresAt - Date.now()) / 1000))\n : 0);\n const mins = Math.floor(s / 60);\n const secs = s % 60;\n return `Expires in ${mins}:${secs < 10 ? '0' : ''}${secs}`;\n }\n\n // ---- DOM helpers -------------------------------------------------------\n\n /** Update just the inner body content without re-rendering the whole component */\n private _updateBody(html: string): void {\n const bodyEl = this.display === 'inline'\n ? this.shadow.getElementById('inline-body')\n : this.shadow.getElementById('modal-body');\n\n if (bodyEl) bodyEl.innerHTML = html;\n }\n\n private _clearTimers(): void {\n if (this._timerInterval) {\n clearInterval(this._timerInterval);\n this._timerInterval = null;\n }\n if (this._pollTimeout) {\n clearTimeout(this._pollTimeout);\n this._pollTimeout = null;\n }\n }\n}\n\ncustomElements.define('qrauth-login', QRAuthLogin);\n", "import { QRAuthElement } from './base.js';\n\n// Same SVGs as login.ts\nconst SHIELD_SVG = `<svg viewBox=\"0 0 200 200\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M48 24 H152 Q166 24 166 38 V108 Q166 156 100 182 Q34 156 34 108 V38 Q34 24 48 24 Z\"\n fill=\"none\" stroke=\"#fff\" stroke-width=\"5\"/>\n <g fill=\"#fff\" opacity=\"0.2\">\n <rect x=\"52\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"78\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"130\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"52\" y=\"66\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"104\" y=\"66\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"78\" y=\"92\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"130\" y=\"92\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"52\" y=\"118\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"104\" y=\"118\" width=\"18\" height=\"18\" rx=\"3\"/>\n </g>\n <path d=\"M62 104 L88 134 L144 64\"\n fill=\"none\" stroke=\"#fff\" stroke-width=\"15\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>`;\n\n// Check icon for success state\nconst CHECK_SVG = `<svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"currentColor\"/>\n</svg>`;\n\n// Spinner SVG\nconst SPINNER_SVG = `<svg class=\"spinner\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-dasharray=\"31.416\" stroke-dashoffset=\"10\" stroke-linecap=\"round\"/>\n</svg>`;\n\ntype EphemeralStatus =\n | 'idle'\n | 'loading'\n | 'active'\n | 'CLAIMED'\n | 'EXPIRED'\n | 'REVOKED'\n | 'error';\n\ninterface EphemeralSession {\n sessionId: string;\n token: string;\n claimUrl: string;\n expiresAt: string;\n scopes: string[];\n ttlSeconds: number;\n maxUses: number;\n}\n\ninterface PollResponse {\n sessionId: string;\n token: string;\n status: string;\n scopes: string[];\n ttlSeconds: number;\n maxUses: number;\n useCount: number;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * <qrauth-ephemeral> \u2014 Displays an ephemeral access QR code with TTL countdown\n * and claim status updates.\n *\n * Attributes:\n * tenant \u2014 Your QRAuth App client ID (required)\n * theme \u2014 \"light\" (default) | \"dark\"\n * base-url \u2014 API base URL (default: https://qrauth.io)\n * scopes \u2014 Space-separated scope strings (default: \"access\")\n * ttl \u2014 TTL string like \"30m\", \"6h\", \"1d\" (default: \"30m\")\n * max-uses \u2014 Maximum number of claims (default: \"1\")\n * device-binding \u2014 Boolean attribute; present = device binding enabled\n * display \u2014 \"button\" | \"inline\" (default: \"inline\")\n *\n * Events (bubbles + composed, cross shadow-DOM):\n * qrauth:claimed \u2014 detail: { sessionId, status, scopes, metadata, useCount, maxUses }\n * qrauth:expired \u2014 fired when session times out\n * qrauth:revoked \u2014 fired when session is revoked\n * qrauth:error \u2014 detail: { message }\n *\n * @example\n * <qrauth-ephemeral tenant=\"qrauth_app_xxx\" scopes=\"read:menu write:order\" ttl=\"30m\">\n * </qrauth-ephemeral>\n */\nexport class QRAuthEphemeral extends QRAuthElement {\n static override get observedAttributes(): string[] {\n return [...super.observedAttributes, 'scopes', 'ttl', 'max-uses', 'device-binding', 'display'];\n }\n\n private _sessionId: string | null = null;\n private _claimUrl: string | null = null;\n private _status: EphemeralStatus = 'idle';\n private _expiresAt: number | null = null;\n private _timerInterval: ReturnType<typeof setInterval> | null = null;\n private _pollTimeout: ReturnType<typeof setTimeout> | null = null;\n\n get scopes(): string { return this.getAttribute('scopes') || 'access'; }\n get ttl(): string { return this.getAttribute('ttl') || '30m'; }\n get maxUses(): number { return parseInt(this.getAttribute('max-uses') || '1', 10); }\n get deviceBinding(): boolean { return this.hasAttribute('device-binding'); }\n get display(): string { return this.getAttribute('display') || 'inline'; }\n\n // ---- Lifecycle -----------------------------------------------------------\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this._clearTimers();\n }\n\n // ---- Render --------------------------------------------------------------\n\n protected render(): void {\n if (this.display === 'button') {\n this._renderButton();\n } else {\n this._renderInline();\n }\n }\n\n private _renderButton(): void {\n this.shadow.innerHTML = `\n <style>\n ${this.getBaseStyles()}\n :host { display: inline-block; }\n\n .btn {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 12px 24px;\n background: var(--_btn-bg);\n color: #fff;\n border: none;\n border-radius: var(--_radius);\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.2s, transform 0.15s, box-shadow 0.2s;\n user-select: none;\n line-height: 1.4;\n font-family: inherit;\n }\n .btn:hover:not(:disabled) {\n background: var(--_btn-hover);\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(27,42,74,0.3);\n }\n .btn:active:not(:disabled) { transform: translateY(0); }\n .btn:disabled { opacity: 0.6; cursor: not-allowed; }\n .btn-icon { width: 22px; height: 22px; flex-shrink: 0; }\n\n /* Modal overlay */\n .overlay {\n position: fixed;\n inset: 0;\n background: rgba(0,0,0,0.55);\n z-index: 99999;\n display: flex;\n align-items: center;\n justify-content: center;\n animation: fade-in 0.2s ease;\n }\n .overlay.hidden { display: none; }\n\n @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }\n @keyframes slide-up {\n from { transform: translateY(24px); opacity: 0; }\n to { transform: translateY(0); opacity: 1; }\n }\n\n .modal {\n background: var(--_bg);\n border-radius: 16px;\n padding: 32px 28px 24px;\n max-width: 380px;\n width: 90vw;\n box-shadow: var(--_shadow);\n text-align: center;\n animation: slide-up 0.3s ease;\n position: relative;\n }\n\n ${this._sharedModalStyles()}\n </style>\n\n <button class=\"btn\" id=\"open-btn\" aria-label=\"Get access QR code\">\n <span class=\"btn-icon\">${SHIELD_SVG}</span>\n Get Access QR\n </button>\n\n <div class=\"overlay hidden\" id=\"overlay\" role=\"dialog\" aria-modal=\"true\" aria-label=\"QRAuth ephemeral access\">\n <div class=\"modal\">\n ${this._modalContent()}\n </div>\n </div>\n `;\n\n this._bindButtonEvents();\n }\n\n private _renderInline(): void {\n this.shadow.innerHTML = `\n <style>\n ${this.getBaseStyles()}\n :host { display: block; }\n\n .inline-container {\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: 16px;\n padding: 28px 24px 20px;\n text-align: center;\n max-width: 360px;\n margin: 0 auto;\n }\n\n /* Idle state: show start button */\n .start-btn {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 12px 24px;\n background: var(--_btn-bg);\n color: #fff;\n border: none;\n border-radius: var(--_radius);\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.2s, transform 0.15s, box-shadow 0.2s;\n font-family: inherit;\n width: 100%;\n justify-content: center;\n }\n .start-btn:hover:not(:disabled) {\n background: var(--_btn-hover);\n transform: translateY(-1px);\n }\n .start-btn:disabled { opacity: 0.6; cursor: not-allowed; }\n .btn-icon { width: 22px; height: 22px; flex-shrink: 0; }\n\n ${this._sharedModalStyles()}\n </style>\n\n <div class=\"inline-container\">\n <div id=\"inline-body\">\n ${this._inlineIdleContent()}\n </div>\n </div>\n `;\n\n this._bindInlineEvents();\n }\n\n private _inlineIdleContent(): string {\n return `\n <button class=\"start-btn\" id=\"start-btn\" aria-label=\"Get access QR code\">\n <span class=\"btn-icon\">${SHIELD_SVG}</span>\n Get Access QR\n </button>\n `;\n }\n\n // ---- Shared modal HTML ---------------------------------------------------\n\n private _modalContent(): string {\n return `\n <button class=\"close-btn\" id=\"close-btn\" aria-label=\"Close\">×</button>\n <div id=\"modal-body\">\n ${this._bodyForStatus('idle')}\n </div>\n `;\n }\n\n private _bodyForStatus(status: EphemeralStatus, data?: {\n claimUrl?: string;\n timerHtml?: string;\n errorMessage?: string;\n useCount?: number;\n maxUses?: number;\n }): string {\n switch (status) {\n case 'idle':\n return `\n <div class=\"state-header\">\n <div class=\"brand-icon\">${SHIELD_SVG}</div>\n <h3>Ephemeral Access</h3>\n <p>Scan to claim temporary access</p>\n </div>\n `;\n\n case 'loading':\n return `\n <div class=\"state-loading\">\n <div class=\"spinner-wrap\">${SPINNER_SVG}</div>\n <p class=\"status-text\">Generating access QR...</p>\n </div>\n `;\n\n case 'active': {\n const qrImageUrl = data?.claimUrl\n ? `https://api.qrserver.com/v1/create-qr-code/?size=440x440&data=${encodeURIComponent(data.claimUrl)}`\n : '';\n\n const useCount = data?.useCount ?? 0;\n const maxUses = data?.maxUses ?? 1;\n const isMultiUse = maxUses > 1;\n const useBadge = isMultiUse\n ? `<div class=\"use-badge\">${useCount}/${maxUses} claimed</div>`\n : '';\n\n return `\n <h3>Scan to Access</h3>\n <p class=\"subtitle\">Present this QR code to gain entry</p>\n <div class=\"qr-frame\">\n <img src=\"${qrImageUrl}\" alt=\"Ephemeral access QR code\" width=\"220\" height=\"220\" loading=\"lazy\"/>\n <div class=\"qr-badge\">${SHIELD_SVG}</div>\n </div>\n ${useBadge}\n <div class=\"timer\" id=\"timer\">${data?.timerHtml ?? ''}</div>\n <div class=\"footer\">Secured by <a href=\"https://qrauth.io\" target=\"_blank\" rel=\"noopener\">QRAuth</a></div>\n `;\n }\n\n case 'CLAIMED':\n return `\n <div class=\"state-claimed\">\n <div class=\"success-icon\">${CHECK_SVG}</div>\n <h3>Access granted!</h3>\n <p class=\"subtitle\">The QR code has been claimed successfully</p>\n </div>\n `;\n\n case 'EXPIRED':\n return `\n <div class=\"state-expired\">\n <div class=\"expired-icon\">⌛</div>\n <h3>Access expired</h3>\n <p class=\"subtitle\">This QR code has timed out.</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Generate new QR</button>\n </div>\n `;\n\n case 'REVOKED':\n return `\n <div class=\"state-revoked\">\n <div class=\"error-icon\">🚫</div>\n <h3>Access revoked</h3>\n <p class=\"subtitle\">This ephemeral session has been revoked.</p>\n </div>\n `;\n\n case 'error':\n return `\n <div class=\"state-error\">\n <div class=\"error-icon\">⚠</div>\n <h3>Something went wrong</h3>\n <p class=\"subtitle\">${data?.errorMessage ?? 'Failed to create ephemeral session.'}</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Try again</button>\n </div>\n `;\n\n default:\n return '';\n }\n }\n\n // ---- Shared CSS for modal content ----------------------------------------\n\n private _sharedModalStyles(): string {\n return `\n /* Typography */\n h3 {\n font-size: 18px;\n font-weight: 700;\n color: var(--_text);\n margin: 0 0 6px;\n }\n .subtitle {\n font-size: 13px;\n color: var(--_text-muted);\n margin: 0 0 20px;\n line-height: 1.5;\n }\n\n /* Close button */\n .close-btn {\n position: absolute;\n top: 12px;\n right: 14px;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--_text-muted);\n font-size: 22px;\n line-height: 1;\n padding: 4px 6px;\n border-radius: 6px;\n transition: color 0.15s, background 0.15s;\n font-family: inherit;\n }\n .close-btn:hover { color: var(--_text); background: var(--_surface); }\n\n /* Brand icon in idle state */\n .state-header { padding: 8px 0 4px; }\n .brand-icon {\n width: 56px;\n height: 56px;\n background: var(--_btn-bg);\n border-radius: 14px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 14px;\n padding: 10px;\n }\n .brand-icon svg { width: 36px; height: 36px; }\n\n /* Loading state */\n .state-loading {\n padding: 32px 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n }\n .spinner-wrap { color: var(--_primary); }\n .spinner {\n width: 40px;\n height: 40px;\n animation: spin 0.8s linear infinite;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n\n /* QR frame */\n .qr-frame {\n display: inline-block;\n padding: 14px;\n background: #fff;\n border: 2px solid var(--_border);\n border-radius: var(--_radius);\n margin-bottom: 14px;\n position: relative;\n transition: border-color 0.3s;\n }\n .qr-frame img { display: block; width: 220px; height: 220px; border-radius: 4px; }\n .qr-badge {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 52px;\n height: 52px;\n background: #1b2a4a;\n border-radius: 10px;\n padding: 6px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .qr-badge svg { width: 100%; height: 100%; }\n\n /* Multi-use badge */\n .use-badge {\n display: inline-block;\n background: var(--_surface);\n border: 1px solid var(--_border);\n border-radius: 20px;\n padding: 4px 12px;\n font-size: 12px;\n font-weight: 600;\n color: var(--_text-muted);\n margin-bottom: 8px;\n }\n\n /* Status text */\n .status-text {\n font-size: 13px;\n color: var(--_text-muted);\n min-height: 20px;\n }\n\n /* Timer */\n .timer {\n font-size: 12px;\n color: var(--_disabled);\n margin-top: 8px;\n font-variant-numeric: tabular-nums;\n min-height: 18px;\n }\n\n /* Claimed / success state */\n .state-claimed {\n padding: 24px 0 12px;\n animation: fade-scale-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);\n }\n @keyframes fade-scale-in {\n from { transform: scale(0.85); opacity: 0; }\n to { transform: scale(1); opacity: 1; }\n }\n .success-icon {\n width: 64px;\n height: 64px;\n background: var(--_success);\n border-radius: 50%;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n margin-bottom: 16px;\n }\n .success-icon svg { width: 36px; height: 36px; }\n\n /* Expired / revoked / error states */\n .state-expired, .state-revoked, .state-error {\n padding: 20px 0 8px;\n }\n .error-icon, .expired-icon {\n font-size: 48px;\n margin-bottom: 12px;\n line-height: 1;\n display: block;\n }\n .state-revoked .error-icon { color: var(--_error); }\n .state-expired .expired-icon { color: var(--_disabled); }\n .state-error .error-icon { color: var(--_warning); }\n\n /* Retry button */\n .retry-btn {\n margin-top: 14px;\n padding: 9px 22px;\n background: var(--_surface);\n border: 1px solid var(--_border);\n border-radius: 8px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n color: var(--_text-muted);\n font-family: inherit;\n transition: background 0.15s, border-color 0.15s;\n }\n .retry-btn:hover { background: var(--_border); color: var(--_text); }\n\n /* Footer */\n .footer {\n margin-top: 18px;\n font-size: 10px;\n color: #c4cdd5;\n }\n .footer a { color: var(--_disabled); text-decoration: none; }\n .footer a:hover { text-decoration: underline; }\n `;\n }\n\n // ---- Event binding -------------------------------------------------------\n\n private _bindButtonEvents(): void {\n const openBtn = this.shadow.getElementById('open-btn');\n openBtn?.addEventListener('click', () => {\n this._openModal();\n this._startSession();\n });\n\n // Close on overlay click\n const overlay = this.shadow.getElementById('overlay');\n overlay?.addEventListener('click', (e) => {\n if (e.target === overlay) this._closeModal();\n });\n\n this._bindModalButtons();\n }\n\n private _bindInlineEvents(): void {\n const startBtn = this.shadow.getElementById('start-btn');\n startBtn?.addEventListener('click', () => {\n this._startSession();\n });\n }\n\n private _bindModalButtons(): void {\n this.shadow.getElementById('close-btn')?.addEventListener('click', () => this._closeModal());\n this.shadow.getElementById('retry-btn')?.addEventListener('click', () => this._retry());\n }\n\n // ---- Modal open / close (button display mode) ----------------------------\n\n private _openModal(): void {\n const overlay = this.shadow.getElementById('overlay');\n overlay?.classList.remove('hidden');\n this._updateBody(this._bodyForStatus('idle'));\n }\n\n private _closeModal(): void {\n const overlay = this.shadow.getElementById('overlay');\n overlay?.classList.add('hidden');\n this._clearTimers();\n this._sessionId = null;\n this._claimUrl = null;\n this._status = 'idle';\n this._expiresAt = null;\n }\n\n // ---- Session flow --------------------------------------------------------\n\n private async _startSession(): Promise<void> {\n this._status = 'loading';\n this._updateBody(this._bodyForStatus('loading'));\n\n try {\n const res = await fetch(`${this.baseUrl}/api/v1/ephemeral`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Client-Id': this.tenant,\n },\n body: JSON.stringify({\n scopes: this.scopes.split(/\\s+/).filter(Boolean),\n ttl: this.ttl,\n maxUses: this.maxUses,\n deviceBinding: this.deviceBinding,\n }),\n });\n\n if (!res.ok) {\n const err = await res.json().catch(() => ({ message: 'Failed to create session' }));\n throw new Error((err as { message?: string }).message ?? 'Failed to create session');\n }\n\n const session = await res.json() as EphemeralSession;\n this._sessionId = session.sessionId;\n this._claimUrl = session.claimUrl;\n this._expiresAt = new Date(session.expiresAt).getTime();\n this._status = 'active';\n\n this._updateBody(this._bodyForStatus('active', {\n claimUrl: session.claimUrl,\n timerHtml: this._formatTimer(),\n useCount: 0,\n maxUses: session.maxUses,\n }));\n this._bindModalButtons();\n this._startCountdown();\n this._beginPolling(session.sessionId);\n\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unexpected error';\n this._status = 'error';\n this._updateBody(this._bodyForStatus('error', { errorMessage: message }));\n this._bindModalButtons();\n this.emit('qrauth:error', { message });\n }\n }\n\n private _retry(): void {\n this._clearTimers();\n this._sessionId = null;\n this._claimUrl = null;\n this._status = 'idle';\n this._expiresAt = null;\n\n if (this.display === 'inline') {\n // Reset inline to show the start button again\n const inlineBody = this.shadow.getElementById('inline-body');\n if (inlineBody) {\n inlineBody.innerHTML = this._inlineIdleContent();\n this._bindInlineEvents();\n }\n } else {\n this._startSession();\n }\n }\n\n // ---- Polling -------------------------------------------------------------\n\n private _beginPolling(sessionId: string): void {\n let interval = 2000;\n const maxInterval = 5000;\n let errorCount = 0;\n const maxErrors = 20;\n\n const poll = async () => {\n if (!this._sessionId) return; // component was reset\n\n try {\n const res = await fetch(`${this.baseUrl}/api/v1/ephemeral/${sessionId}`, {\n headers: { 'X-Client-Id': this.tenant },\n });\n const data = await res.json() as PollResponse;\n errorCount = 0;\n interval = 2000;\n\n this._handleStatusChange(data.status, data);\n\n if (['CLAIMED', 'EXPIRED', 'REVOKED'].includes(data.status)) {\n // For multi-use sessions that are only partially claimed, stay active\n if (data.status === 'CLAIMED' && data.useCount < data.maxUses) {\n // Partial claim \u2014 update badge and keep polling\n this._updateBody(this._bodyForStatus('active', {\n claimUrl: this._claimUrl ?? undefined,\n timerHtml: this._formatTimer(),\n useCount: data.useCount,\n maxUses: data.maxUses,\n }));\n this._bindModalButtons();\n this._pollTimeout = setTimeout(poll, interval);\n return;\n }\n return; // stop polling for terminal states\n }\n\n this._pollTimeout = setTimeout(poll, interval);\n\n } catch {\n errorCount++;\n if (errorCount >= maxErrors) {\n this._handleStatusChange('EXPIRED', undefined);\n return;\n }\n interval = Math.min(interval * 1.5, maxInterval);\n this._pollTimeout = setTimeout(poll, interval);\n }\n };\n\n this._pollTimeout = setTimeout(poll, interval);\n }\n\n // ---- Status handling -----------------------------------------------------\n\n private _handleStatusChange(status: string, data?: PollResponse): void {\n switch (status) {\n case 'CLAIMED': {\n // Full claim (useCount >= maxUses)\n this._clearTimers();\n this._status = 'CLAIMED';\n this._updateBody(this._bodyForStatus('CLAIMED'));\n this._bindModalButtons();\n this.emit('qrauth:claimed', {\n sessionId: data?.sessionId,\n status: data?.status,\n scopes: data?.scopes,\n metadata: data?.metadata,\n useCount: data?.useCount,\n maxUses: data?.maxUses,\n });\n\n if (this.display === 'button') {\n setTimeout(() => this._closeModal(), 2200);\n }\n break;\n }\n\n case 'EXPIRED':\n this._clearTimers();\n this._status = 'EXPIRED';\n this._updateBody(this._bodyForStatus('EXPIRED'));\n this._bindModalButtons();\n this.emit('qrauth:expired');\n break;\n\n case 'REVOKED':\n this._clearTimers();\n this._status = 'REVOKED';\n this._updateBody(this._bodyForStatus('REVOKED'));\n this._bindModalButtons();\n this.emit('qrauth:revoked');\n break;\n }\n }\n\n // ---- Countdown timer -----------------------------------------------------\n\n private _startCountdown(): void {\n this._timerInterval = setInterval(() => {\n const timerEl = this.shadow.getElementById('timer');\n if (!timerEl || !this._expiresAt) return;\n\n const remaining = Math.max(0, Math.floor((this._expiresAt - Date.now()) / 1000));\n timerEl.textContent = remaining > 0 ? this._formatTimer(remaining) : '';\n\n if (remaining <= 0) {\n clearInterval(this._timerInterval!);\n this._timerInterval = null;\n if (this._status === 'active') {\n this._handleStatusChange('EXPIRED', undefined);\n }\n }\n }, 1000);\n }\n\n private _formatTimer(seconds?: number): string {\n const s = seconds ?? (this._expiresAt\n ? Math.max(0, Math.floor((this._expiresAt - Date.now()) / 1000))\n : 0);\n const mins = Math.floor(s / 60);\n const secs = s % 60;\n return `Expires in ${mins}:${secs < 10 ? '0' : ''}${secs}`;\n }\n\n // ---- DOM helpers ---------------------------------------------------------\n\n /** Update just the inner body content without re-rendering the whole component */\n private _updateBody(html: string): void {\n const bodyEl = this.display === 'inline'\n ? this.shadow.getElementById('inline-body')\n : this.shadow.getElementById('modal-body');\n\n if (bodyEl) bodyEl.innerHTML = html;\n }\n\n private _clearTimers(): void {\n if (this._timerInterval) {\n clearInterval(this._timerInterval);\n this._timerInterval = null;\n }\n if (this._pollTimeout) {\n clearTimeout(this._pollTimeout);\n this._pollTimeout = null;\n }\n }\n}\n\ncustomElements.define('qrauth-ephemeral', QRAuthEphemeral);\n", "import { QRAuthElement } from './base.js';\n\n// Same shield SVG as login component\nconst SHIELD_SVG = `<svg viewBox=\"0 0 200 200\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M48 24 H152 Q166 24 166 38 V108 Q166 156 100 182 Q34 156 34 108 V38 Q34 24 48 24 Z\"\n fill=\"none\" stroke=\"#fff\" stroke-width=\"5\"/>\n <g fill=\"#fff\" opacity=\"0.2\">\n <rect x=\"52\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"78\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"130\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"52\" y=\"66\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"104\" y=\"66\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"78\" y=\"92\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"130\" y=\"92\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"52\" y=\"118\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"104\" y=\"118\" width=\"18\" height=\"18\" rx=\"3\"/>\n </g>\n <path d=\"M62 104 L88 134 L144 64\"\n fill=\"none\" stroke=\"#fff\" stroke-width=\"15\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>`;\n\n// Check icon for success state\nconst CHECK_SVG = `<svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"currentColor\"/>\n</svg>`;\n\n// Spinner SVG\nconst SPINNER_SVG = `<svg class=\"spinner\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-dasharray=\"31.416\" stroke-dashoffset=\"10\" stroke-linecap=\"round\"/>\n</svg>`;\n\ntype TwoFAStatus =\n | 'idle'\n | 'loading'\n | 'pending'\n | 'SCANNED'\n | 'APPROVED'\n | 'DENIED'\n | 'EXPIRED'\n | 'error';\n\ninterface AuthSession {\n sessionId: string;\n qrUrl: string;\n expiresAt: string;\n status: TwoFAStatus;\n}\n\ninterface PollResponse {\n sessionId: string;\n status: TwoFAStatus;\n user?: Record<string, unknown>;\n signature?: string;\n}\n\n/**\n * <qrauth-2fa> \u2014 Drop-in second-factor authentication component.\n *\n * After the user completes primary login (e.g. password), render this component\n * to perform QR-based step-up verification. Always displays inline \u2014 no button/modal mode.\n *\n * Attributes:\n * tenant \u2014 Your QRAuth App client ID (required)\n * theme \u2014 \"light\" (default) | \"dark\"\n * base-url \u2014 API base URL (default: https://qrauth.io)\n * session-token \u2014 JWT from the primary authentication (required for context)\n * scopes \u2014 Space-separated scopes (default: \"identity\")\n * auto-start \u2014 Boolean attribute; if present, starts 2FA immediately on connect\n *\n * Events (bubbles + composed, cross shadow-DOM):\n * qrauth:verified \u2014 detail: { sessionId, user, signature }\n * qrauth:denied \u2014 fired when the user denies on their device\n * qrauth:expired \u2014 fired when the session times out\n * qrauth:error \u2014 detail: { message }\n *\n * @example\n * <qrauth-2fa tenant=\"qrauth_app_xxx\" session-token=\"eyJ...\" auto-start></qrauth-2fa>\n * <qrauth-2fa tenant=\"qrauth_app_xxx\" scopes=\"identity payments\"></qrauth-2fa>\n */\nexport class QRAuthTwoFA extends QRAuthElement {\n static override get observedAttributes(): string[] {\n return [...super.observedAttributes, 'session-token', 'scopes', 'auto-start'];\n }\n\n private _sessionId: string | null = null;\n private _codeVerifier: string | null = null;\n private _status: TwoFAStatus = 'idle';\n private _expiresAt: number | null = null;\n private _timerInterval: ReturnType<typeof setInterval> | null = null;\n private _pollTimeout: ReturnType<typeof setTimeout> | null = null;\n\n get sessionToken(): string | null { return this.getAttribute('session-token'); }\n get scopes(): string { return this.getAttribute('scopes') || 'identity'; }\n get autoStart(): boolean { return this.hasAttribute('auto-start'); }\n\n // ---- Lifecycle -----------------------------------------------------------\n\n override connectedCallback(): void {\n super.connectedCallback(); // calls render()\n if (this.autoStart) {\n this._startAuth();\n }\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this._clearTimers();\n }\n\n // ---- Render --------------------------------------------------------------\n\n protected render(): void {\n this.shadow.innerHTML = `\n <style>\n ${this.getBaseStyles()}\n :host { display: block; }\n\n .container {\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: 16px;\n padding: 24px 20px 18px;\n text-align: center;\n max-width: 320px;\n margin: 0 auto;\n box-shadow: 0 2px 12px rgba(0,0,0,0.06);\n }\n\n /* Step indicator */\n .step-label {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--_primary);\n background: rgba(0,167,111,0.08);\n border-radius: 20px;\n padding: 4px 10px;\n margin-bottom: 14px;\n }\n .step-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--_primary);\n flex-shrink: 0;\n }\n\n /* Idle start button */\n .start-btn {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 11px 22px;\n background: var(--_btn-bg);\n color: #fff;\n border: none;\n border-radius: var(--_radius);\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.2s, transform 0.15s, box-shadow 0.2s;\n font-family: inherit;\n width: 100%;\n justify-content: center;\n margin-top: 4px;\n }\n .start-btn:hover:not(:disabled) {\n background: var(--_btn-hover);\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(27,42,74,0.3);\n }\n .start-btn:active:not(:disabled) { transform: translateY(0); }\n .start-btn:disabled { opacity: 0.6; cursor: not-allowed; }\n .btn-icon { width: 20px; height: 20px; flex-shrink: 0; }\n\n ${this._sharedStyles()}\n </style>\n\n <div class=\"container\" role=\"region\" aria-label=\"Second-factor authentication\">\n <div id=\"body\">\n ${this._bodyForStatus('idle')}\n </div>\n </div>\n `;\n\n // Re-bind events after full render (attributeChangedCallback re-renders)\n this._bindBodyEvents();\n }\n\n // ---- Body HTML per status ------------------------------------------------\n\n private _bodyForStatus(status: TwoFAStatus, data?: {\n qrUrl?: string;\n timerHtml?: string;\n errorMessage?: string;\n }): string {\n const stepLabel = `\n <div class=\"step-label\">\n <span class=\"step-dot\"></span>\n Step 2 of 2 — Verify identity\n </div>\n `;\n\n switch (status) {\n case 'idle':\n return `\n ${stepLabel}\n <div class=\"state-header\">\n <div class=\"brand-icon\">${SHIELD_SVG}</div>\n <h3>Verify your identity</h3>\n <p class=\"subtitle\">Scan the QR code with your phone to confirm it's you</p>\n </div>\n <button class=\"start-btn\" id=\"start-btn\" aria-label=\"Start 2FA verification\">\n <span class=\"btn-icon\">${SHIELD_SVG}</span>\n Verify your identity\n </button>\n `;\n\n case 'loading':\n return `\n ${stepLabel}\n <div class=\"state-loading\">\n <div class=\"spinner-wrap\">${SPINNER_SVG}</div>\n <p class=\"status-text\">Generating QR code...</p>\n </div>\n `;\n\n case 'pending':\n case 'SCANNED': {\n const qrImageUrl = data?.qrUrl\n ? `https://api.qrserver.com/v1/create-qr-code/?size=320x320&data=${encodeURIComponent(data.qrUrl)}`\n : '';\n\n const isScanned = status === 'SCANNED';\n const scanClass = isScanned ? 'scanned' : '';\n\n return `\n ${stepLabel}\n <h3>Scan to verify</h3>\n <p class=\"subtitle\">Use your phone camera to complete verification</p>\n <div class=\"qr-frame ${scanClass}\">\n <img src=\"${qrImageUrl}\" alt=\"QR Code for 2FA verification\" width=\"160\" height=\"160\" loading=\"lazy\"/>\n <div class=\"qr-badge\">${SHIELD_SVG}</div>\n ${isScanned ? '<div class=\"scanned-overlay\"><span class=\"scanned-check\">' + CHECK_SVG + '</span></div>' : ''}\n </div>\n <div class=\"status-text ${isScanned ? 'status-scanned' : ''}\">\n ${isScanned\n ? '✓ Scanned — waiting for approval...'\n : 'Waiting for scan...'}\n </div>\n <div class=\"timer\" id=\"timer\">${data?.timerHtml ?? ''}</div>\n <div class=\"footer\">Secured by <a href=\"https://qrauth.io\" target=\"_blank\" rel=\"noopener\">QRAuth</a></div>\n `;\n }\n\n case 'APPROVED':\n return `\n <div class=\"state-approved\">\n <div class=\"success-icon\">${CHECK_SVG}</div>\n <h3>Identity verified!</h3>\n <p class=\"subtitle\">Second factor confirmed</p>\n </div>\n `;\n\n case 'DENIED':\n return `\n <div class=\"state-denied\">\n <div class=\"error-icon\">✗</div>\n <h3>Verification denied</h3>\n <p class=\"subtitle\">The request was declined on your device.</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Try again</button>\n </div>\n `;\n\n case 'EXPIRED':\n return `\n <div class=\"state-expired\">\n <div class=\"expired-icon\">⌛</div>\n <h3>Session expired</h3>\n <p class=\"subtitle\">The QR code has timed out.</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Try again</button>\n </div>\n `;\n\n case 'error':\n return `\n <div class=\"state-error\">\n <div class=\"error-icon\">⚠</div>\n <h3>Something went wrong</h3>\n <p class=\"subtitle\">${data?.errorMessage ?? 'Failed to start verification.'}</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Try again</button>\n </div>\n `;\n\n default:\n return '';\n }\n }\n\n // ---- Shared CSS ----------------------------------------------------------\n\n private _sharedStyles(): string {\n return `\n /* Typography */\n h3 {\n font-size: 16px;\n font-weight: 700;\n color: var(--_text);\n margin: 0 0 5px;\n }\n .subtitle {\n font-size: 12px;\n color: var(--_text-muted);\n margin: 0 0 16px;\n line-height: 1.5;\n }\n\n /* Brand icon in idle state */\n .state-header { padding: 4px 0 2px; }\n .brand-icon {\n width: 48px;\n height: 48px;\n background: var(--_btn-bg);\n border-radius: 12px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 12px;\n padding: 8px;\n }\n .brand-icon svg { width: 32px; height: 32px; }\n\n /* Loading state */\n .state-loading {\n padding: 24px 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 14px;\n }\n .spinner-wrap { color: var(--_primary); }\n .spinner {\n width: 36px;\n height: 36px;\n animation: spin 0.8s linear infinite;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n\n /* QR frame \u2014 compact 160x160 */\n .qr-frame {\n display: inline-block;\n padding: 10px;\n background: #fff;\n border: 2px solid var(--_border);\n border-radius: var(--_radius);\n margin-bottom: 12px;\n position: relative;\n transition: border-color 0.3s;\n }\n .qr-frame.scanned { border-color: var(--_success); }\n .qr-frame img { display: block; width: 160px; height: 160px; border-radius: 4px; }\n\n /* Smaller badge for compact QR */\n .qr-badge {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 40px;\n height: 40px;\n background: #1b2a4a;\n border-radius: 8px;\n padding: 5px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .qr-badge svg { width: 100%; height: 100%; }\n\n /* Scanned overlay on QR */\n .scanned-overlay {\n position: absolute;\n inset: 0;\n background: rgba(0,167,111,0.08);\n border-radius: calc(var(--_radius) - 2px);\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .scanned-check {\n width: 40px;\n height: 40px;\n background: var(--_success);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n opacity: 0;\n animation: pop-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.1s forwards;\n }\n .scanned-check svg { width: 22px; height: 22px; }\n @keyframes pop-in {\n from { transform: scale(0.5); opacity: 0; }\n to { transform: scale(1); opacity: 1; }\n }\n\n /* Status text */\n .status-text {\n font-size: 12px;\n color: var(--_text-muted);\n min-height: 18px;\n }\n .status-scanned {\n color: var(--_success);\n font-weight: 600;\n }\n\n /* Timer */\n .timer {\n font-size: 11px;\n color: var(--_disabled);\n margin-top: 6px;\n font-variant-numeric: tabular-nums;\n min-height: 16px;\n }\n\n /* Success state */\n .state-approved {\n padding: 16px 0 8px;\n animation: fade-scale-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);\n }\n @keyframes fade-scale-in {\n from { transform: scale(0.85); opacity: 0; }\n to { transform: scale(1); opacity: 1; }\n }\n .success-icon {\n width: 56px;\n height: 56px;\n background: var(--_success);\n border-radius: 50%;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n margin-bottom: 14px;\n }\n .success-icon svg { width: 30px; height: 30px; }\n\n /* Denied / expired / error states */\n .state-denied, .state-expired, .state-error {\n padding: 16px 0 6px;\n }\n .error-icon, .expired-icon {\n font-size: 40px;\n margin-bottom: 10px;\n line-height: 1;\n display: block;\n }\n .state-denied .error-icon { color: var(--_error); }\n .state-expired .expired-icon { color: var(--_disabled); }\n .state-error .error-icon { color: var(--_warning); }\n\n /* Retry button */\n .retry-btn {\n margin-top: 12px;\n padding: 8px 20px;\n background: var(--_surface);\n border: 1px solid var(--_border);\n border-radius: 8px;\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n color: var(--_text-muted);\n font-family: inherit;\n transition: background 0.15s, border-color 0.15s;\n }\n .retry-btn:hover { background: var(--_border); color: var(--_text); }\n\n /* Footer */\n .footer {\n margin-top: 14px;\n font-size: 10px;\n color: #c4cdd5;\n }\n .footer a { color: var(--_disabled); text-decoration: none; }\n .footer a:hover { text-decoration: underline; }\n `;\n }\n\n // ---- Event binding -------------------------------------------------------\n\n private _bindBodyEvents(): void {\n this.shadow.getElementById('start-btn')?.addEventListener('click', () => this._startAuth());\n this.shadow.getElementById('retry-btn')?.addEventListener('click', () => this._retry());\n }\n\n // ---- Auth flow -----------------------------------------------------------\n\n private async _startAuth(): Promise<void> {\n this._status = 'loading';\n this._updateBody(this._bodyForStatus('loading'));\n\n try {\n this._codeVerifier = await this.generateCodeVerifier();\n const challenge = await this.computeCodeChallenge(this._codeVerifier);\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-Client-Id': this.tenant,\n };\n\n const body: Record<string, unknown> = {\n scopes: this.scopes.split(/\\s+/).filter(Boolean),\n codeChallenge: challenge,\n codeChallengeMethod: 'S256',\n };\n\n const res = await fetch(`${this.baseUrl}/api/v1/auth-sessions`, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const err = await res.json().catch(() => ({ message: 'Failed to create session' }));\n throw new Error((err as { message?: string }).message ?? 'Failed to create session');\n }\n\n const session = await res.json() as AuthSession;\n this._sessionId = session.sessionId;\n this._expiresAt = new Date(session.expiresAt).getTime();\n this._status = 'pending';\n\n this._updateBody(this._bodyForStatus('pending', { qrUrl: session.qrUrl }));\n this._bindBodyEvents();\n this._startCountdown();\n this._beginPolling(session.sessionId);\n\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unexpected error';\n this._status = 'error';\n this._updateBody(this._bodyForStatus('error', { errorMessage: message }));\n this._bindBodyEvents();\n this.emit('qrauth:error', { message });\n }\n }\n\n private _retry(): void {\n this._clearTimers();\n this._sessionId = null;\n this._codeVerifier = null;\n this._status = 'idle';\n this._expiresAt = null;\n\n this._updateBody(this._bodyForStatus('idle'));\n this._bindBodyEvents();\n }\n\n // ---- Polling -------------------------------------------------------------\n\n private _beginPolling(sessionId: string): void {\n let lastStatus: string = 'PENDING';\n let interval = 2000;\n const maxInterval = 5000;\n let errorCount = 0;\n const maxErrors = 20;\n\n const poll = async () => {\n if (!this._sessionId) return; // component was reset\n\n let url = `${this.baseUrl}/api/v1/auth-sessions/${sessionId}`;\n if (this._codeVerifier) {\n url += `?code_verifier=${encodeURIComponent(this._codeVerifier)}`;\n }\n\n try {\n const res = await fetch(url, {\n headers: { 'X-Client-Id': this.tenant },\n });\n const data = await res.json() as PollResponse;\n errorCount = 0;\n interval = 2000;\n\n if (data.status !== lastStatus) {\n lastStatus = data.status;\n this._handleStatusChange(data.status, data);\n\n if (['APPROVED', 'DENIED', 'EXPIRED'].includes(data.status)) {\n return; // stop polling\n }\n }\n\n this._pollTimeout = setTimeout(poll, interval);\n\n } catch {\n errorCount++;\n if (errorCount >= maxErrors) {\n this._handleStatusChange('EXPIRED', undefined);\n return;\n }\n interval = Math.min(interval * 1.5, maxInterval);\n this._pollTimeout = setTimeout(poll, interval);\n }\n };\n\n this._pollTimeout = setTimeout(poll, interval);\n }\n\n // ---- Status handling -----------------------------------------------------\n\n private _handleStatusChange(status: TwoFAStatus, data?: PollResponse): void {\n this._status = status;\n\n switch (status) {\n case 'SCANNED': {\n // Preserve current QR URL from the existing img element\n const currentQrUrl = this.shadow.querySelector<HTMLImageElement>('.qr-frame img')?.src;\n const rawQrUrl = currentQrUrl\n ? decodeURIComponent(currentQrUrl.replace(/.*[?&]data=/, '').split('&')[0])\n : '';\n\n this._updateBody(this._bodyForStatus('SCANNED', {\n qrUrl: rawQrUrl,\n timerHtml: this._formatTimer(),\n }));\n this._bindBodyEvents();\n break;\n }\n\n case 'APPROVED': {\n this._clearTimers();\n this._updateBody(this._bodyForStatus('APPROVED'));\n this.emit('qrauth:verified', {\n sessionId: data?.sessionId,\n user: data?.user,\n signature: data?.signature,\n });\n break;\n }\n\n case 'DENIED':\n this._clearTimers();\n this._updateBody(this._bodyForStatus('DENIED'));\n this._bindBodyEvents();\n this.emit('qrauth:denied');\n break;\n\n case 'EXPIRED':\n this._clearTimers();\n this._status = 'EXPIRED';\n this._updateBody(this._bodyForStatus('EXPIRED'));\n this._bindBodyEvents();\n this.emit('qrauth:expired');\n break;\n }\n }\n\n // ---- Countdown timer -----------------------------------------------------\n\n private _startCountdown(): void {\n this._timerInterval = setInterval(() => {\n const timerEl = this.shadow.getElementById('timer');\n if (!timerEl || !this._expiresAt) return;\n\n const remaining = Math.max(0, Math.floor((this._expiresAt - Date.now()) / 1000));\n timerEl.textContent = remaining > 0 ? this._formatTimer(remaining) : '';\n\n if (remaining <= 0) {\n clearInterval(this._timerInterval!);\n this._timerInterval = null;\n if (this._status === 'pending' || this._status === 'SCANNED') {\n this._handleStatusChange('EXPIRED', undefined);\n }\n }\n }, 1000);\n }\n\n private _formatTimer(seconds?: number): string {\n const s = seconds ?? (this._expiresAt\n ? Math.max(0, Math.floor((this._expiresAt - Date.now()) / 1000))\n : 0);\n const mins = Math.floor(s / 60);\n const secs = s % 60;\n return `Expires in ${mins}:${secs < 10 ? '0' : ''}${secs}`;\n }\n\n // ---- DOM helpers ---------------------------------------------------------\n\n private _updateBody(html: string): void {\n const bodyEl = this.shadow.getElementById('body');\n if (bodyEl) bodyEl.innerHTML = html;\n }\n\n private _clearTimers(): void {\n if (this._timerInterval) {\n clearInterval(this._timerInterval);\n this._timerInterval = null;\n }\n if (this._pollTimeout) {\n clearTimeout(this._pollTimeout);\n this._pollTimeout = null;\n }\n }\n}\n\ncustomElements.define('qrauth-2fa', QRAuthTwoFA);\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["QRAuthElement", "__publicField", "val", "_name", "oldValue", "newValue", "url", "event", "callback", "e", "msg", "intervalMs", "headers", "poll", "res", "name", "detail", "bytes", "verifier", "data", "hash", "binString", "byte", "SHIELD_SVG", "CHECK_SVG", "SPINNER_SVG", "QRAuthLogin", "QRAuthElement", "__publicField", "status", "data", "qrImageUrl", "isScanned", "overlay", "e", "challenge", "headers", "body", "res", "err", "session", "message", "inlineBody", "sessionId", "lastStatus", "interval", "maxInterval", "errorCount", "maxErrors", "poll", "url", "currentQrUrl", "rawQrUrl", "callbackName", "globalCallback", "timerEl", "remaining", "seconds", "s", "mins", "secs", "html", "bodyEl", "SHIELD_SVG", "CHECK_SVG", "SPINNER_SVG", "QRAuthEphemeral", "QRAuthElement", "__publicField", "status", "data", "qrImageUrl", "useCount", "maxUses", "useBadge", "overlay", "e", "res", "err", "session", "message", "inlineBody", "sessionId", "interval", "maxInterval", "errorCount", "maxErrors", "poll", "timerEl", "remaining", "seconds", "s", "mins", "secs", "html", "bodyEl", "SHIELD_SVG", "CHECK_SVG", "SPINNER_SVG", "QRAuthTwoFA", "QRAuthElement", "__publicField", "status", "data", "stepLabel", "qrImageUrl", "isScanned", "challenge", "headers", "body", "res", "err", "session", "message", "sessionId", "lastStatus", "interval", "maxInterval", "errorCount", "maxErrors", "poll", "url", "currentQrUrl", "rawQrUrl", "timerEl", "remaining", "seconds", "s", "mins", "secs", "html", "bodyEl"]
|
|
7
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/index.ts", "../src/base.ts", "../src/login.ts", "../src/ephemeral.ts", "../src/twofa.ts"],
|
|
4
|
-
"sourcesContent": ["export { QRAuthElement } from './base.js';\nexport { QRAuthLogin } from './login.js';\nexport { QRAuthEphemeral } from './ephemeral.js';\nexport { QRAuthTwoFA } from './twofa.js';\n", "/**\n * QRAuthElement \u2014 base class for all QRAuth Web Components.\n * Provides: Shadow DOM setup, CSS custom property theming, SSE connection management,\n * lifecycle hooks, and event dispatching.\n */\nexport abstract class QRAuthElement extends HTMLElement {\n protected shadow: ShadowRoot;\n private _sseConnection: EventSource | null = null;\n private _pollInterval: ReturnType<typeof setInterval> | null = null;\n\n static get observedAttributes(): string[] {\n return ['tenant', 'theme', 'base-url'];\n }\n\n constructor() {\n super();\n this.shadow = this.attachShadow({ mode: 'open' });\n }\n\n // Getters for common attributes\n get tenant(): string { return this.getAttribute('tenant') || ''; }\n get theme(): string { return this.getAttribute('theme') || 'light'; }\n get baseUrl(): string {\n const val = this.getAttribute('base-url');\n // Allow empty string for local dev (relative URLs), fallback to production\n return val !== null ? val : 'https://qrauth.io';\n }\n\n connectedCallback(): void {\n this.render();\n }\n\n disconnectedCallback(): void {\n this.cleanup();\n }\n\n attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue !== newValue) {\n this.render();\n }\n }\n\n // Subclasses implement this\n protected abstract render(): void;\n\n // --- SSE Management ---\n protected connectSSE(url: string): void {\n this.disconnectSSE();\n // EventSource doesn't support custom headers natively.\n // For public (unauthenticated) SSE endpoints this works directly.\n this._sseConnection = new EventSource(url);\n\n this._sseConnection.onerror = () => {\n // Fall back to polling on SSE failure \u2014 caller should initiate polling\n this.disconnectSSE();\n };\n }\n\n protected onSSEEvent(event: string, callback: (data: unknown) => void): void {\n if (this._sseConnection) {\n this._sseConnection.addEventListener(event, (e: Event) => {\n const msg = e as MessageEvent;\n try { callback(JSON.parse(msg.data as string)); } catch { /* invalid JSON */ }\n });\n }\n }\n\n protected disconnectSSE(): void {\n if (this._sseConnection) {\n this._sseConnection.close();\n this._sseConnection = null;\n }\n }\n\n // --- Polling (fallback / primary for authenticated endpoints) ---\n protected startPolling(\n url: string,\n intervalMs: number,\n callback: (data: unknown) => void,\n headers?: Record<string, string>,\n ): void {\n this.stopPolling();\n const poll = async () => {\n try {\n const res = await fetch(url, { headers });\n if (res.ok) callback(await res.json());\n } catch { /* network error \u2014 retry next interval */ }\n };\n poll(); // immediate first poll\n this._pollInterval = setInterval(poll, intervalMs);\n }\n\n protected stopPolling(): void {\n if (this._pollInterval) {\n clearInterval(this._pollInterval);\n this._pollInterval = null;\n }\n }\n\n // --- Cleanup ---\n protected cleanup(): void {\n this.disconnectSSE();\n this.stopPolling();\n }\n\n // --- Event dispatch ---\n protected emit(name: string, detail?: unknown): void {\n this.dispatchEvent(new CustomEvent(name, {\n bubbles: true,\n composed: true, // crosses shadow DOM boundary\n detail,\n }));\n }\n\n // --- PKCE helpers ---\n protected async generateCodeVerifier(): Promise<string> {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return this._base64url(bytes);\n }\n\n protected async computeCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest('SHA-256', data);\n return this._base64url(new Uint8Array(hash));\n }\n\n private _base64url(bytes: Uint8Array): string {\n const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join('');\n return btoa(binString).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n }\n\n // --- Shared base styles (host-level theming via CSS custom properties) ---\n protected getBaseStyles(): string {\n return `\n :host {\n display: inline-block;\n font-family: var(--qrauth-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n --_primary: var(--qrauth-primary, #00a76f);\n --_primary-dark: var(--qrauth-primary-dark, #007a52);\n --_text: var(--qrauth-text, #1a1a2e);\n --_text-muted: var(--qrauth-text-muted, #637381);\n --_bg: var(--qrauth-bg, #ffffff);\n --_surface: var(--qrauth-surface, #f9fafb);\n --_border: var(--qrauth-border, #e0e0e0);\n --_radius: var(--qrauth-radius, 12px);\n --_shadow: var(--qrauth-shadow, 0 24px 48px rgba(0,0,0,0.15));\n --_btn-bg: var(--qrauth-btn-bg, #1b2a4a);\n --_btn-hover: var(--qrauth-btn-hover, #263b66);\n --_success: #00a76f;\n --_error: #ff5630;\n --_warning: #ffab00;\n --_disabled: #919eab;\n }\n :host([theme=\"dark\"]) {\n --_text: var(--qrauth-text, #f0f0f0);\n --_text-muted: var(--qrauth-text-muted, #919eab);\n --_bg: var(--qrauth-bg, #1a1a2e);\n --_surface: var(--qrauth-surface, #242436);\n --_border: var(--qrauth-border, rgba(255,255,255,0.12));\n --_shadow: var(--qrauth-shadow, 0 24px 48px rgba(0,0,0,0.5));\n --_btn-bg: var(--qrauth-btn-bg, #263b66);\n --_btn-hover: var(--qrauth-btn-hover, #2e4578);\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n `;\n }\n}\n", "import { QRAuthElement } from './base.js';\n\n// Same shield SVG as the browser SDK\nconst SHIELD_SVG = `<svg viewBox=\"0 0 200 200\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M48 24 H152 Q166 24 166 38 V108 Q166 156 100 182 Q34 156 34 108 V38 Q34 24 48 24 Z\"\n fill=\"none\" stroke=\"#fff\" stroke-width=\"5\"/>\n <g fill=\"#fff\" opacity=\"0.2\">\n <rect x=\"52\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"78\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"130\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"52\" y=\"66\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"104\" y=\"66\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"78\" y=\"92\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"130\" y=\"92\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"52\" y=\"118\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"104\" y=\"118\" width=\"18\" height=\"18\" rx=\"3\"/>\n </g>\n <path d=\"M62 104 L88 134 L144 64\"\n fill=\"none\" stroke=\"#fff\" stroke-width=\"15\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>`;\n\n// Check icon for success state\nconst CHECK_SVG = `<svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"currentColor\"/>\n</svg>`;\n\n// Spinner SVG\nconst SPINNER_SVG = `<svg class=\"spinner\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-dasharray=\"31.416\" stroke-dashoffset=\"10\" stroke-linecap=\"round\"/>\n</svg>`;\n\ntype AuthStatus =\n | 'idle'\n | 'loading'\n | 'pending'\n | 'SCANNED'\n | 'APPROVED'\n | 'DENIED'\n | 'EXPIRED'\n | 'error';\n\ninterface AuthSession {\n sessionId: string;\n qrUrl: string;\n expiresAt: string;\n status: AuthStatus;\n}\n\ninterface PollResponse {\n sessionId: string;\n status: AuthStatus;\n user?: Record<string, unknown>;\n signature?: string;\n}\n\n/**\n * <qrauth-login> \u2014 Drop-in QR authentication button / inline widget.\n *\n * Attributes:\n * tenant \u2014 Your QRAuth App client ID (required)\n * theme \u2014 \"light\" (default) | \"dark\"\n * base-url \u2014 API base URL (default: https://qrauth.io)\n * scopes \u2014 Space-separated scopes (default: \"identity\")\n * redirect-url \u2014 Optional redirect after successful auth\n * on-auth \u2014 Name of a global callback function to call on success\n * display \u2014 \"button\" (default) | \"inline\"\n * animated \u2014 If present, enables subtle QR pulse animation\n *\n * Events (bubbles + composed, cross shadow-DOM):\n * qrauth:authenticated \u2014 detail: { sessionId, user, signature }\n * qrauth:expired \u2014 fired when session times out\n * qrauth:error \u2014 detail: { message }\n * qrauth:scanned \u2014 fired when QR is scanned (before approval)\n *\n * @example\n * <qrauth-login tenant=\"qrauth_app_xxx\"></qrauth-login>\n * <qrauth-login tenant=\"qrauth_app_xxx\" display=\"inline\" theme=\"dark\"></qrauth-login>\n */\nexport class QRAuthLogin extends QRAuthElement {\n static override get observedAttributes(): string[] {\n return [...super.observedAttributes, 'scopes', 'redirect-url', 'on-auth', 'display', 'animated'];\n }\n\n private _sessionId: string | null = null;\n private _codeVerifier: string | null = null;\n private _status: AuthStatus = 'idle';\n private _expiresAt: number | null = null;\n private _timerInterval: ReturnType<typeof setInterval> | null = null;\n private _pollTimeout: ReturnType<typeof setTimeout> | null = null;\n\n get scopes(): string { return this.getAttribute('scopes') || 'identity'; }\n get redirectUrl(): string | null { return this.getAttribute('redirect-url'); }\n get onAuth(): string | null { return this.getAttribute('on-auth'); }\n get display(): string { return this.getAttribute('display') || 'button'; }\n get animated(): boolean { return this.hasAttribute('animated'); }\n\n // ---- Lifecycle ---------------------------------------------------------\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this._clearTimers();\n }\n\n // ---- Render ------------------------------------------------------------\n\n protected render(): void {\n if (this.display === 'inline') {\n this._renderInline();\n } else {\n this._renderButton();\n }\n }\n\n private _renderButton(): void {\n this.shadow.innerHTML = `\n <style>\n ${this.getBaseStyles()}\n :host { display: inline-block; }\n\n .btn {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 12px 24px;\n background: var(--_btn-bg);\n color: #fff;\n border: none;\n border-radius: var(--_radius);\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.2s, transform 0.15s, box-shadow 0.2s;\n user-select: none;\n line-height: 1.4;\n font-family: inherit;\n }\n .btn:hover:not(:disabled) {\n background: var(--_btn-hover);\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(27,42,74,0.3);\n }\n .btn:active:not(:disabled) { transform: translateY(0); }\n .btn:disabled { opacity: 0.6; cursor: not-allowed; }\n .btn-icon { width: 22px; height: 22px; flex-shrink: 0; }\n\n /* Modal overlay */\n .overlay {\n position: fixed;\n inset: 0;\n background: rgba(0,0,0,0.55);\n z-index: 99999;\n display: flex;\n align-items: center;\n justify-content: center;\n animation: fade-in 0.2s ease;\n }\n .overlay.hidden { display: none; }\n\n @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }\n @keyframes slide-up {\n from { transform: translateY(24px); opacity: 0; }\n to { transform: translateY(0); opacity: 1; }\n }\n\n .modal {\n background: var(--_bg);\n border-radius: 16px;\n padding: 32px 28px 24px;\n max-width: 380px;\n width: 90vw;\n box-shadow: var(--_shadow);\n text-align: center;\n animation: slide-up 0.3s ease;\n position: relative;\n }\n\n ${this._sharedModalStyles()}\n </style>\n\n <button class=\"btn\" id=\"open-btn\" aria-label=\"Sign in with QRAuth\">\n <span class=\"btn-icon\">${SHIELD_SVG}</span>\n Sign in with QRAuth\n </button>\n\n <div class=\"overlay hidden\" id=\"overlay\" role=\"dialog\" aria-modal=\"true\" aria-label=\"QRAuth sign-in\">\n <div class=\"modal\">\n ${this._modalContent()}\n </div>\n </div>\n `;\n\n this._bindButtonEvents();\n }\n\n private _renderInline(): void {\n this.shadow.innerHTML = `\n <style>\n ${this.getBaseStyles()}\n :host { display: block; }\n\n .inline-container {\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: 16px;\n padding: 28px 24px 20px;\n text-align: center;\n max-width: 360px;\n margin: 0 auto;\n }\n\n /* Idle state: show start button */\n .start-btn {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 12px 24px;\n background: var(--_btn-bg);\n color: #fff;\n border: none;\n border-radius: var(--_radius);\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.2s, transform 0.15s, box-shadow 0.2s;\n font-family: inherit;\n width: 100%;\n justify-content: center;\n }\n .start-btn:hover:not(:disabled) {\n background: var(--_btn-hover);\n transform: translateY(-1px);\n }\n .start-btn:disabled { opacity: 0.6; cursor: not-allowed; }\n .btn-icon { width: 22px; height: 22px; flex-shrink: 0; }\n\n ${this._sharedModalStyles()}\n </style>\n\n <div class=\"inline-container\">\n <div id=\"inline-body\">\n ${this._inlineIdleContent()}\n </div>\n </div>\n `;\n\n this._bindInlineEvents();\n }\n\n private _inlineIdleContent(): string {\n return `\n <button class=\"start-btn\" id=\"start-btn\" aria-label=\"Sign in with QRAuth\">\n <span class=\"btn-icon\">${SHIELD_SVG}</span>\n Sign in with QRAuth\n </button>\n `;\n }\n\n // ---- Shared modal HTML -------------------------------------------------\n\n private _modalContent(): string {\n return `\n <button class=\"close-btn\" id=\"close-btn\" aria-label=\"Close\">×</button>\n <div id=\"modal-body\">\n ${this._bodyForStatus('idle')}\n </div>\n `;\n }\n\n private _bodyForStatus(status: AuthStatus, data?: {\n qrUrl?: string;\n sessionId?: string;\n timerHtml?: string;\n errorMessage?: string;\n }): string {\n switch (status) {\n case 'idle':\n return `\n <div class=\"state-header\">\n <div class=\"brand-icon\">${SHIELD_SVG}</div>\n <h3>Sign in with QRAuth</h3>\n <p>Secure, passwordless authentication</p>\n </div>\n `;\n\n case 'loading':\n return `\n <div class=\"state-loading\">\n <div class=\"spinner-wrap\">${SPINNER_SVG}</div>\n <p class=\"status-text\">Generating QR code...</p>\n </div>\n `;\n\n case 'pending':\n case 'SCANNED': {\n const qrImageUrl = data?.qrUrl\n ? `https://api.qrserver.com/v1/create-qr-code/?size=440x440&data=${encodeURIComponent(data.qrUrl)}`\n : '';\n\n const isScanned = status === 'SCANNED';\n const scanClass = isScanned ? 'scanned' : '';\n\n return `\n <h3>Sign in with QRAuth</h3>\n <p class=\"subtitle\">Scan this QR code with your phone camera</p>\n <div class=\"qr-frame ${scanClass}${this.animated ? ' animated' : ''}\">\n <img src=\"${qrImageUrl}\" alt=\"QR Code for authentication\" width=\"220\" height=\"220\" loading=\"lazy\"/>\n <div class=\"qr-badge\">${SHIELD_SVG}</div>\n ${isScanned ? '<div class=\"scanned-overlay\"><span class=\"scanned-check\">' + CHECK_SVG + '</span></div>' : ''}\n </div>\n <div class=\"status-text ${isScanned ? 'status-scanned' : ''}\">\n ${isScanned\n ? '✓ QR code scanned — waiting for approval...'\n : 'Waiting for scan...'}\n </div>\n <div class=\"timer\" id=\"timer\">${data?.timerHtml ?? ''}</div>\n <div class=\"footer\">Secured by <a href=\"https://qrauth.io\" target=\"_blank\" rel=\"noopener\">QRAuth</a></div>\n `;\n }\n\n case 'APPROVED':\n return `\n <div class=\"state-approved\">\n <div class=\"success-icon\">${CHECK_SVG}</div>\n <h3>Identity verified!</h3>\n <p class=\"subtitle\">You are now signed in</p>\n </div>\n `;\n\n case 'DENIED':\n return `\n <div class=\"state-denied\">\n <div class=\"error-icon\">✗</div>\n <h3>Authentication denied</h3>\n <p class=\"subtitle\">The request was declined on your device.</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Try again</button>\n </div>\n `;\n\n case 'EXPIRED':\n return `\n <div class=\"state-expired\">\n <div class=\"expired-icon\">⌛</div>\n <h3>Session expired</h3>\n <p class=\"subtitle\">The QR code has timed out.</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Try again</button>\n </div>\n `;\n\n case 'error':\n return `\n <div class=\"state-error\">\n <div class=\"error-icon\">⚠</div>\n <h3>Something went wrong</h3>\n <p class=\"subtitle\">${data?.errorMessage ?? 'Failed to start authentication.'}</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Try again</button>\n </div>\n `;\n\n default:\n return '';\n }\n }\n\n // ---- Shared CSS for modal content --------------------------------------\n\n private _sharedModalStyles(): string {\n return `\n /* Typography */\n h3 {\n font-size: 18px;\n font-weight: 700;\n color: var(--_text);\n margin: 0 0 6px;\n }\n .subtitle {\n font-size: 13px;\n color: var(--_text-muted);\n margin: 0 0 20px;\n line-height: 1.5;\n }\n\n /* Close button */\n .close-btn {\n position: absolute;\n top: 12px;\n right: 14px;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--_text-muted);\n font-size: 22px;\n line-height: 1;\n padding: 4px 6px;\n border-radius: 6px;\n transition: color 0.15s, background 0.15s;\n font-family: inherit;\n }\n .close-btn:hover { color: var(--_text); background: var(--_surface); }\n\n /* Brand icon in idle state */\n .state-header { padding: 8px 0 4px; }\n .brand-icon {\n width: 56px;\n height: 56px;\n background: var(--_btn-bg);\n border-radius: 14px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 14px;\n padding: 10px;\n }\n .brand-icon svg { width: 36px; height: 36px; }\n\n /* Loading state */\n .state-loading {\n padding: 32px 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n }\n .spinner-wrap { color: var(--_primary); }\n .spinner {\n width: 40px;\n height: 40px;\n animation: spin 0.8s linear infinite;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n\n /* QR frame */\n .qr-frame {\n display: inline-block;\n padding: 14px;\n background: #fff;\n border: 2px solid var(--_border);\n border-radius: var(--_radius);\n margin-bottom: 14px;\n position: relative;\n transition: border-color 0.3s;\n }\n .qr-frame.scanned { border-color: var(--_success); }\n .qr-frame.animated { animation: qr-pulse 2s ease-in-out infinite; }\n @keyframes qr-pulse {\n 0%, 100% { box-shadow: 0 0 0 0 rgba(0,167,111,0); }\n 50% { box-shadow: 0 0 0 8px rgba(0,167,111,0.15); }\n }\n .qr-frame img { display: block; width: 220px; height: 220px; border-radius: 4px; }\n .qr-badge {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 52px;\n height: 52px;\n background: #1b2a4a;\n border-radius: 10px;\n padding: 6px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .qr-badge svg { width: 100%; height: 100%; }\n\n /* Scanned overlay on QR */\n .scanned-overlay {\n position: absolute;\n inset: 0;\n background: rgba(0,167,111,0.08);\n border-radius: calc(var(--_radius) - 2px);\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .scanned-check {\n width: 48px;\n height: 48px;\n background: var(--_success);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n opacity: 0;\n animation: pop-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.1s forwards;\n }\n .scanned-check svg { width: 28px; height: 28px; }\n @keyframes pop-in {\n from { transform: scale(0.5); opacity: 0; }\n to { transform: scale(1); opacity: 1; }\n }\n\n /* Status text */\n .status-text {\n font-size: 13px;\n color: var(--_text-muted);\n min-height: 20px;\n }\n .status-scanned {\n color: var(--_success);\n font-weight: 600;\n }\n\n /* Timer */\n .timer {\n font-size: 12px;\n color: var(--_disabled);\n margin-top: 8px;\n font-variant-numeric: tabular-nums;\n min-height: 18px;\n }\n\n /* Success state */\n .state-approved {\n padding: 24px 0 12px;\n animation: fade-scale-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);\n }\n @keyframes fade-scale-in {\n from { transform: scale(0.85); opacity: 0; }\n to { transform: scale(1); opacity: 1; }\n }\n .success-icon {\n width: 64px;\n height: 64px;\n background: var(--_success);\n border-radius: 50%;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n margin-bottom: 16px;\n }\n .success-icon svg { width: 36px; height: 36px; }\n\n /* Denied / expired / error states */\n .state-denied, .state-expired, .state-error {\n padding: 20px 0 8px;\n }\n .error-icon, .expired-icon {\n font-size: 48px;\n margin-bottom: 12px;\n line-height: 1;\n display: block;\n }\n .state-denied .error-icon { color: var(--_error); }\n .state-expired .expired-icon { color: var(--_disabled); }\n .state-error .error-icon { color: var(--_warning); }\n\n /* Retry button */\n .retry-btn {\n margin-top: 14px;\n padding: 9px 22px;\n background: var(--_surface);\n border: 1px solid var(--_border);\n border-radius: 8px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n color: var(--_text-muted);\n font-family: inherit;\n transition: background 0.15s, border-color 0.15s;\n }\n .retry-btn:hover { background: var(--_border); color: var(--_text); }\n\n /* Footer */\n .footer {\n margin-top: 18px;\n font-size: 10px;\n color: #c4cdd5;\n }\n .footer a { color: var(--_disabled); text-decoration: none; }\n .footer a:hover { text-decoration: underline; }\n `;\n }\n\n // ---- Event binding -----------------------------------------------------\n\n private _bindButtonEvents(): void {\n const openBtn = this.shadow.getElementById('open-btn');\n openBtn?.addEventListener('click', () => {\n this._openModal();\n this._startAuth();\n });\n\n // Close on overlay click\n const overlay = this.shadow.getElementById('overlay');\n overlay?.addEventListener('click', (e) => {\n if (e.target === overlay) this._closeModal();\n });\n\n this._bindModalButtons();\n }\n\n private _bindInlineEvents(): void {\n const startBtn = this.shadow.getElementById('start-btn');\n startBtn?.addEventListener('click', () => {\n this._startAuth();\n });\n }\n\n private _bindModalButtons(): void {\n this.shadow.getElementById('close-btn')?.addEventListener('click', () => this._closeModal());\n this.shadow.getElementById('retry-btn')?.addEventListener('click', () => this._retry());\n }\n\n // ---- Modal open / close (button display mode) --------------------------\n\n private _openModal(): void {\n const overlay = this.shadow.getElementById('overlay');\n overlay?.classList.remove('hidden');\n this._updateBody(this._bodyForStatus('idle'));\n }\n\n private _closeModal(): void {\n const overlay = this.shadow.getElementById('overlay');\n overlay?.classList.add('hidden');\n this._clearTimers();\n this._sessionId = null;\n this._codeVerifier = null;\n this._status = 'idle';\n this._expiresAt = null;\n }\n\n // ---- Auth flow ---------------------------------------------------------\n\n private async _startAuth(): Promise<void> {\n this._status = 'loading';\n this._updateBody(this._bodyForStatus('loading'));\n\n try {\n this._codeVerifier = await this.generateCodeVerifier();\n const challenge = await this.computeCodeChallenge(this._codeVerifier);\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-Client-Id': this.tenant,\n };\n\n const body: Record<string, unknown> = {\n scopes: this.scopes.split(/\\s+/).filter(Boolean),\n codeChallenge: challenge,\n codeChallengeMethod: 'S256',\n };\n\n const res = await fetch(`${this.baseUrl}/api/v1/auth-sessions`, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const err = await res.json().catch(() => ({ message: 'Failed to create session' }));\n throw new Error((err as { message?: string }).message ?? 'Failed to create session');\n }\n\n const session = await res.json() as AuthSession;\n this._sessionId = session.sessionId;\n this._expiresAt = new Date(session.expiresAt).getTime();\n this._status = 'pending';\n\n this._updateBody(this._bodyForStatus('pending', { qrUrl: session.qrUrl }));\n this._bindModalButtons();\n this._startCountdown();\n this._beginPolling(session.sessionId);\n\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unexpected error';\n this._status = 'error';\n this._updateBody(this._bodyForStatus('error', { errorMessage: message }));\n this._bindModalButtons();\n this.emit('qrauth:error', { message });\n }\n }\n\n private _retry(): void {\n this._clearTimers();\n this._sessionId = null;\n this._codeVerifier = null;\n this._status = 'idle';\n this._expiresAt = null;\n\n if (this.display === 'inline') {\n // Reset inline to show the start button again\n const inlineBody = this.shadow.getElementById('inline-body');\n if (inlineBody) {\n inlineBody.innerHTML = this._inlineIdleContent();\n this._bindInlineEvents();\n }\n } else {\n this._startAuth();\n }\n }\n\n // ---- Polling -----------------------------------------------------------\n\n private _beginPolling(sessionId: string): void {\n let lastStatus: string = 'PENDING';\n let interval = 2000;\n const maxInterval = 5000;\n let errorCount = 0;\n const maxErrors = 20;\n\n const poll = async () => {\n if (!this._sessionId) return; // component was reset\n\n let url = `${this.baseUrl}/api/v1/auth-sessions/${sessionId}`;\n if (this._codeVerifier) {\n url += `?code_verifier=${encodeURIComponent(this._codeVerifier)}`;\n }\n\n try {\n const res = await fetch(url, {\n headers: { 'X-Client-Id': this.tenant },\n });\n const data = await res.json() as PollResponse;\n errorCount = 0;\n interval = 2000;\n\n if (data.status !== lastStatus) {\n lastStatus = data.status;\n this._handleStatusChange(data.status, data);\n\n if (['APPROVED', 'DENIED', 'EXPIRED'].includes(data.status)) {\n return; // stop polling\n }\n }\n\n this._pollTimeout = setTimeout(poll, interval);\n\n } catch {\n errorCount++;\n if (errorCount >= maxErrors) {\n this._handleStatusChange('EXPIRED', undefined);\n return;\n }\n interval = Math.min(interval * 1.5, maxInterval);\n this._pollTimeout = setTimeout(poll, interval);\n }\n };\n\n this._pollTimeout = setTimeout(poll, interval);\n }\n\n // ---- Status handling ---------------------------------------------------\n\n private _handleStatusChange(status: AuthStatus, data?: PollResponse): void {\n this._status = status;\n\n switch (status) {\n case 'SCANNED': {\n // Keep QR visible but show scanned overlay\n const currentQrUrl = this.shadow.querySelector<HTMLImageElement>('.qr-frame img')?.src;\n // Extract the data param from the QR image URL\n const rawQrUrl = currentQrUrl\n ? decodeURIComponent(currentQrUrl.replace(/.*[?&]data=/, '').split('&')[0])\n : '';\n\n this._updateBody(this._bodyForStatus('SCANNED', {\n qrUrl: rawQrUrl,\n timerHtml: this._formatTimer(),\n }));\n this._bindModalButtons();\n this.emit('qrauth:scanned', data);\n break;\n }\n\n case 'APPROVED': {\n this._clearTimers();\n this._updateBody(this._bodyForStatus('APPROVED'));\n this._bindModalButtons();\n this.emit('qrauth:authenticated', {\n sessionId: data?.sessionId,\n user: data?.user,\n signature: data?.signature,\n });\n\n // Call the on-auth global callback if set\n const callbackName = this.onAuth;\n if (callbackName) {\n const globalCallback = (window as unknown as Record<string, unknown>)[callbackName];\n if (typeof globalCallback === 'function') {\n (globalCallback as (d: unknown) => void)(data);\n }\n }\n\n // Redirect if configured\n if (this.redirectUrl) {\n setTimeout(() => { window.location.href = this.redirectUrl!; }, 1500);\n } else if (this.display === 'button') {\n // Auto-close modal after success animation\n setTimeout(() => this._closeModal(), 2200);\n }\n break;\n }\n\n case 'DENIED':\n this._clearTimers();\n this._updateBody(this._bodyForStatus('DENIED'));\n this._bindModalButtons();\n this.emit('qrauth:denied');\n break;\n\n case 'EXPIRED':\n this._clearTimers();\n this._status = 'EXPIRED';\n this._updateBody(this._bodyForStatus('EXPIRED'));\n this._bindModalButtons();\n this.emit('qrauth:expired');\n break;\n }\n }\n\n // ---- Countdown timer ---------------------------------------------------\n\n private _startCountdown(): void {\n this._timerInterval = setInterval(() => {\n const timerEl = this.shadow.getElementById('timer');\n if (!timerEl || !this._expiresAt) return;\n\n const remaining = Math.max(0, Math.floor((this._expiresAt - Date.now()) / 1000));\n timerEl.textContent = remaining > 0 ? this._formatTimer(remaining) : '';\n\n if (remaining <= 0) {\n clearInterval(this._timerInterval!);\n this._timerInterval = null;\n if (this._status === 'pending' || this._status === 'SCANNED') {\n this._handleStatusChange('EXPIRED', undefined);\n }\n }\n }, 1000);\n }\n\n private _formatTimer(seconds?: number): string {\n const s = seconds ?? (this._expiresAt\n ? Math.max(0, Math.floor((this._expiresAt - Date.now()) / 1000))\n : 0);\n const mins = Math.floor(s / 60);\n const secs = s % 60;\n return `Expires in ${mins}:${secs < 10 ? '0' : ''}${secs}`;\n }\n\n // ---- DOM helpers -------------------------------------------------------\n\n /** Update just the inner body content without re-rendering the whole component */\n private _updateBody(html: string): void {\n const bodyEl = this.display === 'inline'\n ? this.shadow.getElementById('inline-body')\n : this.shadow.getElementById('modal-body');\n\n if (bodyEl) bodyEl.innerHTML = html;\n }\n\n private _clearTimers(): void {\n if (this._timerInterval) {\n clearInterval(this._timerInterval);\n this._timerInterval = null;\n }\n if (this._pollTimeout) {\n clearTimeout(this._pollTimeout);\n this._pollTimeout = null;\n }\n }\n}\n\ncustomElements.define('qrauth-login', QRAuthLogin);\n", "import { QRAuthElement } from './base.js';\n\n// Same SVGs as login.ts\nconst SHIELD_SVG = `<svg viewBox=\"0 0 200 200\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M48 24 H152 Q166 24 166 38 V108 Q166 156 100 182 Q34 156 34 108 V38 Q34 24 48 24 Z\"\n fill=\"none\" stroke=\"#fff\" stroke-width=\"5\"/>\n <g fill=\"#fff\" opacity=\"0.2\">\n <rect x=\"52\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"78\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"130\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"52\" y=\"66\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"104\" y=\"66\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"78\" y=\"92\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"130\" y=\"92\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"52\" y=\"118\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"104\" y=\"118\" width=\"18\" height=\"18\" rx=\"3\"/>\n </g>\n <path d=\"M62 104 L88 134 L144 64\"\n fill=\"none\" stroke=\"#fff\" stroke-width=\"15\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>`;\n\n// Check icon for success state\nconst CHECK_SVG = `<svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"currentColor\"/>\n</svg>`;\n\n// Spinner SVG\nconst SPINNER_SVG = `<svg class=\"spinner\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-dasharray=\"31.416\" stroke-dashoffset=\"10\" stroke-linecap=\"round\"/>\n</svg>`;\n\ntype EphemeralStatus =\n | 'idle'\n | 'loading'\n | 'active'\n | 'CLAIMED'\n | 'EXPIRED'\n | 'REVOKED'\n | 'error';\n\ninterface EphemeralSession {\n sessionId: string;\n token: string;\n claimUrl: string;\n expiresAt: string;\n scopes: string[];\n ttlSeconds: number;\n maxUses: number;\n}\n\ninterface PollResponse {\n sessionId: string;\n token: string;\n status: string;\n scopes: string[];\n ttlSeconds: number;\n maxUses: number;\n useCount: number;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * <qrauth-ephemeral> \u2014 Displays an ephemeral access QR code with TTL countdown\n * and claim status updates.\n *\n * Attributes:\n * tenant \u2014 Your QRAuth App client ID (required)\n * theme \u2014 \"light\" (default) | \"dark\"\n * base-url \u2014 API base URL (default: https://qrauth.io)\n * scopes \u2014 Space-separated scope strings (default: \"access\")\n * ttl \u2014 TTL string like \"30m\", \"6h\", \"1d\" (default: \"30m\")\n * max-uses \u2014 Maximum number of claims (default: \"1\")\n * device-binding \u2014 Boolean attribute; present = device binding enabled\n * display \u2014 \"button\" | \"inline\" (default: \"inline\")\n *\n * Events (bubbles + composed, cross shadow-DOM):\n * qrauth:claimed \u2014 detail: { sessionId, status, scopes, metadata, useCount, maxUses }\n * qrauth:expired \u2014 fired when session times out\n * qrauth:revoked \u2014 fired when session is revoked\n * qrauth:error \u2014 detail: { message }\n *\n * @example\n * <qrauth-ephemeral tenant=\"qrauth_app_xxx\" scopes=\"read:menu write:order\" ttl=\"30m\">\n * </qrauth-ephemeral>\n */\nexport class QRAuthEphemeral extends QRAuthElement {\n static override get observedAttributes(): string[] {\n return [...super.observedAttributes, 'scopes', 'ttl', 'max-uses', 'device-binding', 'display'];\n }\n\n private _sessionId: string | null = null;\n private _claimUrl: string | null = null;\n private _status: EphemeralStatus = 'idle';\n private _expiresAt: number | null = null;\n private _timerInterval: ReturnType<typeof setInterval> | null = null;\n private _pollTimeout: ReturnType<typeof setTimeout> | null = null;\n\n get scopes(): string { return this.getAttribute('scopes') || 'access'; }\n get ttl(): string { return this.getAttribute('ttl') || '30m'; }\n get maxUses(): number { return parseInt(this.getAttribute('max-uses') || '1', 10); }\n get deviceBinding(): boolean { return this.hasAttribute('device-binding'); }\n get display(): string { return this.getAttribute('display') || 'inline'; }\n\n // ---- Lifecycle -----------------------------------------------------------\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this._clearTimers();\n }\n\n // ---- Render --------------------------------------------------------------\n\n protected render(): void {\n if (this.display === 'button') {\n this._renderButton();\n } else {\n this._renderInline();\n }\n }\n\n private _renderButton(): void {\n this.shadow.innerHTML = `\n <style>\n ${this.getBaseStyles()}\n :host { display: inline-block; }\n\n .btn {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 12px 24px;\n background: var(--_btn-bg);\n color: #fff;\n border: none;\n border-radius: var(--_radius);\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.2s, transform 0.15s, box-shadow 0.2s;\n user-select: none;\n line-height: 1.4;\n font-family: inherit;\n }\n .btn:hover:not(:disabled) {\n background: var(--_btn-hover);\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(27,42,74,0.3);\n }\n .btn:active:not(:disabled) { transform: translateY(0); }\n .btn:disabled { opacity: 0.6; cursor: not-allowed; }\n .btn-icon { width: 22px; height: 22px; flex-shrink: 0; }\n\n /* Modal overlay */\n .overlay {\n position: fixed;\n inset: 0;\n background: rgba(0,0,0,0.55);\n z-index: 99999;\n display: flex;\n align-items: center;\n justify-content: center;\n animation: fade-in 0.2s ease;\n }\n .overlay.hidden { display: none; }\n\n @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }\n @keyframes slide-up {\n from { transform: translateY(24px); opacity: 0; }\n to { transform: translateY(0); opacity: 1; }\n }\n\n .modal {\n background: var(--_bg);\n border-radius: 16px;\n padding: 32px 28px 24px;\n max-width: 380px;\n width: 90vw;\n box-shadow: var(--_shadow);\n text-align: center;\n animation: slide-up 0.3s ease;\n position: relative;\n }\n\n ${this._sharedModalStyles()}\n </style>\n\n <button class=\"btn\" id=\"open-btn\" aria-label=\"Get access QR code\">\n <span class=\"btn-icon\">${SHIELD_SVG}</span>\n Get Access QR\n </button>\n\n <div class=\"overlay hidden\" id=\"overlay\" role=\"dialog\" aria-modal=\"true\" aria-label=\"QRAuth ephemeral access\">\n <div class=\"modal\">\n ${this._modalContent()}\n </div>\n </div>\n `;\n\n this._bindButtonEvents();\n }\n\n private _renderInline(): void {\n this.shadow.innerHTML = `\n <style>\n ${this.getBaseStyles()}\n :host { display: block; }\n\n .inline-container {\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: 16px;\n padding: 28px 24px 20px;\n text-align: center;\n max-width: 360px;\n margin: 0 auto;\n }\n\n /* Idle state: show start button */\n .start-btn {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 12px 24px;\n background: var(--_btn-bg);\n color: #fff;\n border: none;\n border-radius: var(--_radius);\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.2s, transform 0.15s, box-shadow 0.2s;\n font-family: inherit;\n width: 100%;\n justify-content: center;\n }\n .start-btn:hover:not(:disabled) {\n background: var(--_btn-hover);\n transform: translateY(-1px);\n }\n .start-btn:disabled { opacity: 0.6; cursor: not-allowed; }\n .btn-icon { width: 22px; height: 22px; flex-shrink: 0; }\n\n ${this._sharedModalStyles()}\n </style>\n\n <div class=\"inline-container\">\n <div id=\"inline-body\">\n ${this._inlineIdleContent()}\n </div>\n </div>\n `;\n\n this._bindInlineEvents();\n }\n\n private _inlineIdleContent(): string {\n return `\n <button class=\"start-btn\" id=\"start-btn\" aria-label=\"Get access QR code\">\n <span class=\"btn-icon\">${SHIELD_SVG}</span>\n Get Access QR\n </button>\n `;\n }\n\n // ---- Shared modal HTML ---------------------------------------------------\n\n private _modalContent(): string {\n return `\n <button class=\"close-btn\" id=\"close-btn\" aria-label=\"Close\">×</button>\n <div id=\"modal-body\">\n ${this._bodyForStatus('idle')}\n </div>\n `;\n }\n\n private _bodyForStatus(status: EphemeralStatus, data?: {\n claimUrl?: string;\n timerHtml?: string;\n errorMessage?: string;\n useCount?: number;\n maxUses?: number;\n }): string {\n switch (status) {\n case 'idle':\n return `\n <div class=\"state-header\">\n <div class=\"brand-icon\">${SHIELD_SVG}</div>\n <h3>Ephemeral Access</h3>\n <p>Scan to claim temporary access</p>\n </div>\n `;\n\n case 'loading':\n return `\n <div class=\"state-loading\">\n <div class=\"spinner-wrap\">${SPINNER_SVG}</div>\n <p class=\"status-text\">Generating access QR...</p>\n </div>\n `;\n\n case 'active': {\n const qrImageUrl = data?.claimUrl\n ? `https://api.qrserver.com/v1/create-qr-code/?size=440x440&data=${encodeURIComponent(data.claimUrl)}`\n : '';\n\n const useCount = data?.useCount ?? 0;\n const maxUses = data?.maxUses ?? 1;\n const isMultiUse = maxUses > 1;\n const useBadge = isMultiUse\n ? `<div class=\"use-badge\">${useCount}/${maxUses} claimed</div>`\n : '';\n\n return `\n <h3>Scan to Access</h3>\n <p class=\"subtitle\">Present this QR code to gain entry</p>\n <div class=\"qr-frame\">\n <img src=\"${qrImageUrl}\" alt=\"Ephemeral access QR code\" width=\"220\" height=\"220\" loading=\"lazy\"/>\n <div class=\"qr-badge\">${SHIELD_SVG}</div>\n </div>\n ${useBadge}\n <div class=\"timer\" id=\"timer\">${data?.timerHtml ?? ''}</div>\n <div class=\"footer\">Secured by <a href=\"https://qrauth.io\" target=\"_blank\" rel=\"noopener\">QRAuth</a></div>\n `;\n }\n\n case 'CLAIMED':\n return `\n <div class=\"state-claimed\">\n <div class=\"success-icon\">${CHECK_SVG}</div>\n <h3>Access granted!</h3>\n <p class=\"subtitle\">The QR code has been claimed successfully</p>\n </div>\n `;\n\n case 'EXPIRED':\n return `\n <div class=\"state-expired\">\n <div class=\"expired-icon\">⌛</div>\n <h3>Access expired</h3>\n <p class=\"subtitle\">This QR code has timed out.</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Generate new QR</button>\n </div>\n `;\n\n case 'REVOKED':\n return `\n <div class=\"state-revoked\">\n <div class=\"error-icon\">🚫</div>\n <h3>Access revoked</h3>\n <p class=\"subtitle\">This ephemeral session has been revoked.</p>\n </div>\n `;\n\n case 'error':\n return `\n <div class=\"state-error\">\n <div class=\"error-icon\">⚠</div>\n <h3>Something went wrong</h3>\n <p class=\"subtitle\">${data?.errorMessage ?? 'Failed to create ephemeral session.'}</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Try again</button>\n </div>\n `;\n\n default:\n return '';\n }\n }\n\n // ---- Shared CSS for modal content ----------------------------------------\n\n private _sharedModalStyles(): string {\n return `\n /* Typography */\n h3 {\n font-size: 18px;\n font-weight: 700;\n color: var(--_text);\n margin: 0 0 6px;\n }\n .subtitle {\n font-size: 13px;\n color: var(--_text-muted);\n margin: 0 0 20px;\n line-height: 1.5;\n }\n\n /* Close button */\n .close-btn {\n position: absolute;\n top: 12px;\n right: 14px;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--_text-muted);\n font-size: 22px;\n line-height: 1;\n padding: 4px 6px;\n border-radius: 6px;\n transition: color 0.15s, background 0.15s;\n font-family: inherit;\n }\n .close-btn:hover { color: var(--_text); background: var(--_surface); }\n\n /* Brand icon in idle state */\n .state-header { padding: 8px 0 4px; }\n .brand-icon {\n width: 56px;\n height: 56px;\n background: var(--_btn-bg);\n border-radius: 14px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 14px;\n padding: 10px;\n }\n .brand-icon svg { width: 36px; height: 36px; }\n\n /* Loading state */\n .state-loading {\n padding: 32px 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n }\n .spinner-wrap { color: var(--_primary); }\n .spinner {\n width: 40px;\n height: 40px;\n animation: spin 0.8s linear infinite;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n\n /* QR frame */\n .qr-frame {\n display: inline-block;\n padding: 14px;\n background: #fff;\n border: 2px solid var(--_border);\n border-radius: var(--_radius);\n margin-bottom: 14px;\n position: relative;\n transition: border-color 0.3s;\n }\n .qr-frame img { display: block; width: 220px; height: 220px; border-radius: 4px; }\n .qr-badge {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 52px;\n height: 52px;\n background: #1b2a4a;\n border-radius: 10px;\n padding: 6px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .qr-badge svg { width: 100%; height: 100%; }\n\n /* Multi-use badge */\n .use-badge {\n display: inline-block;\n background: var(--_surface);\n border: 1px solid var(--_border);\n border-radius: 20px;\n padding: 4px 12px;\n font-size: 12px;\n font-weight: 600;\n color: var(--_text-muted);\n margin-bottom: 8px;\n }\n\n /* Status text */\n .status-text {\n font-size: 13px;\n color: var(--_text-muted);\n min-height: 20px;\n }\n\n /* Timer */\n .timer {\n font-size: 12px;\n color: var(--_disabled);\n margin-top: 8px;\n font-variant-numeric: tabular-nums;\n min-height: 18px;\n }\n\n /* Claimed / success state */\n .state-claimed {\n padding: 24px 0 12px;\n animation: fade-scale-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);\n }\n @keyframes fade-scale-in {\n from { transform: scale(0.85); opacity: 0; }\n to { transform: scale(1); opacity: 1; }\n }\n .success-icon {\n width: 64px;\n height: 64px;\n background: var(--_success);\n border-radius: 50%;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n margin-bottom: 16px;\n }\n .success-icon svg { width: 36px; height: 36px; }\n\n /* Expired / revoked / error states */\n .state-expired, .state-revoked, .state-error {\n padding: 20px 0 8px;\n }\n .error-icon, .expired-icon {\n font-size: 48px;\n margin-bottom: 12px;\n line-height: 1;\n display: block;\n }\n .state-revoked .error-icon { color: var(--_error); }\n .state-expired .expired-icon { color: var(--_disabled); }\n .state-error .error-icon { color: var(--_warning); }\n\n /* Retry button */\n .retry-btn {\n margin-top: 14px;\n padding: 9px 22px;\n background: var(--_surface);\n border: 1px solid var(--_border);\n border-radius: 8px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n color: var(--_text-muted);\n font-family: inherit;\n transition: background 0.15s, border-color 0.15s;\n }\n .retry-btn:hover { background: var(--_border); color: var(--_text); }\n\n /* Footer */\n .footer {\n margin-top: 18px;\n font-size: 10px;\n color: #c4cdd5;\n }\n .footer a { color: var(--_disabled); text-decoration: none; }\n .footer a:hover { text-decoration: underline; }\n `;\n }\n\n // ---- Event binding -------------------------------------------------------\n\n private _bindButtonEvents(): void {\n const openBtn = this.shadow.getElementById('open-btn');\n openBtn?.addEventListener('click', () => {\n this._openModal();\n this._startSession();\n });\n\n // Close on overlay click\n const overlay = this.shadow.getElementById('overlay');\n overlay?.addEventListener('click', (e) => {\n if (e.target === overlay) this._closeModal();\n });\n\n this._bindModalButtons();\n }\n\n private _bindInlineEvents(): void {\n const startBtn = this.shadow.getElementById('start-btn');\n startBtn?.addEventListener('click', () => {\n this._startSession();\n });\n }\n\n private _bindModalButtons(): void {\n this.shadow.getElementById('close-btn')?.addEventListener('click', () => this._closeModal());\n this.shadow.getElementById('retry-btn')?.addEventListener('click', () => this._retry());\n }\n\n // ---- Modal open / close (button display mode) ----------------------------\n\n private _openModal(): void {\n const overlay = this.shadow.getElementById('overlay');\n overlay?.classList.remove('hidden');\n this._updateBody(this._bodyForStatus('idle'));\n }\n\n private _closeModal(): void {\n const overlay = this.shadow.getElementById('overlay');\n overlay?.classList.add('hidden');\n this._clearTimers();\n this._sessionId = null;\n this._claimUrl = null;\n this._status = 'idle';\n this._expiresAt = null;\n }\n\n // ---- Session flow --------------------------------------------------------\n\n private async _startSession(): Promise<void> {\n this._status = 'loading';\n this._updateBody(this._bodyForStatus('loading'));\n\n try {\n const res = await fetch(`${this.baseUrl}/api/v1/ephemeral`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Client-Id': this.tenant,\n },\n body: JSON.stringify({\n scopes: this.scopes.split(/\\s+/).filter(Boolean),\n ttl: this.ttl,\n maxUses: this.maxUses,\n deviceBinding: this.deviceBinding,\n }),\n });\n\n if (!res.ok) {\n const err = await res.json().catch(() => ({ message: 'Failed to create session' }));\n throw new Error((err as { message?: string }).message ?? 'Failed to create session');\n }\n\n const session = await res.json() as EphemeralSession;\n this._sessionId = session.sessionId;\n this._claimUrl = session.claimUrl;\n this._expiresAt = new Date(session.expiresAt).getTime();\n this._status = 'active';\n\n this._updateBody(this._bodyForStatus('active', {\n claimUrl: session.claimUrl,\n timerHtml: this._formatTimer(),\n useCount: 0,\n maxUses: session.maxUses,\n }));\n this._bindModalButtons();\n this._startCountdown();\n this._beginPolling(session.sessionId);\n\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unexpected error';\n this._status = 'error';\n this._updateBody(this._bodyForStatus('error', { errorMessage: message }));\n this._bindModalButtons();\n this.emit('qrauth:error', { message });\n }\n }\n\n private _retry(): void {\n this._clearTimers();\n this._sessionId = null;\n this._claimUrl = null;\n this._status = 'idle';\n this._expiresAt = null;\n\n if (this.display === 'inline') {\n // Reset inline to show the start button again\n const inlineBody = this.shadow.getElementById('inline-body');\n if (inlineBody) {\n inlineBody.innerHTML = this._inlineIdleContent();\n this._bindInlineEvents();\n }\n } else {\n this._startSession();\n }\n }\n\n // ---- Polling -------------------------------------------------------------\n\n private _beginPolling(sessionId: string): void {\n let interval = 2000;\n const maxInterval = 5000;\n let errorCount = 0;\n const maxErrors = 20;\n\n const poll = async () => {\n if (!this._sessionId) return; // component was reset\n\n try {\n const res = await fetch(`${this.baseUrl}/api/v1/ephemeral/${sessionId}`, {\n headers: { 'X-Client-Id': this.tenant },\n });\n const data = await res.json() as PollResponse;\n errorCount = 0;\n interval = 2000;\n\n this._handleStatusChange(data.status, data);\n\n if (['CLAIMED', 'EXPIRED', 'REVOKED'].includes(data.status)) {\n // For multi-use sessions that are only partially claimed, stay active\n if (data.status === 'CLAIMED' && data.useCount < data.maxUses) {\n // Partial claim \u2014 update badge and keep polling\n this._updateBody(this._bodyForStatus('active', {\n claimUrl: this._claimUrl ?? undefined,\n timerHtml: this._formatTimer(),\n useCount: data.useCount,\n maxUses: data.maxUses,\n }));\n this._bindModalButtons();\n this._pollTimeout = setTimeout(poll, interval);\n return;\n }\n return; // stop polling for terminal states\n }\n\n this._pollTimeout = setTimeout(poll, interval);\n\n } catch {\n errorCount++;\n if (errorCount >= maxErrors) {\n this._handleStatusChange('EXPIRED', undefined);\n return;\n }\n interval = Math.min(interval * 1.5, maxInterval);\n this._pollTimeout = setTimeout(poll, interval);\n }\n };\n\n this._pollTimeout = setTimeout(poll, interval);\n }\n\n // ---- Status handling -----------------------------------------------------\n\n private _handleStatusChange(status: string, data?: PollResponse): void {\n switch (status) {\n case 'CLAIMED': {\n // Full claim (useCount >= maxUses)\n this._clearTimers();\n this._status = 'CLAIMED';\n this._updateBody(this._bodyForStatus('CLAIMED'));\n this._bindModalButtons();\n this.emit('qrauth:claimed', {\n sessionId: data?.sessionId,\n status: data?.status,\n scopes: data?.scopes,\n metadata: data?.metadata,\n useCount: data?.useCount,\n maxUses: data?.maxUses,\n });\n\n if (this.display === 'button') {\n setTimeout(() => this._closeModal(), 2200);\n }\n break;\n }\n\n case 'EXPIRED':\n this._clearTimers();\n this._status = 'EXPIRED';\n this._updateBody(this._bodyForStatus('EXPIRED'));\n this._bindModalButtons();\n this.emit('qrauth:expired');\n break;\n\n case 'REVOKED':\n this._clearTimers();\n this._status = 'REVOKED';\n this._updateBody(this._bodyForStatus('REVOKED'));\n this._bindModalButtons();\n this.emit('qrauth:revoked');\n break;\n }\n }\n\n // ---- Countdown timer -----------------------------------------------------\n\n private _startCountdown(): void {\n this._timerInterval = setInterval(() => {\n const timerEl = this.shadow.getElementById('timer');\n if (!timerEl || !this._expiresAt) return;\n\n const remaining = Math.max(0, Math.floor((this._expiresAt - Date.now()) / 1000));\n timerEl.textContent = remaining > 0 ? this._formatTimer(remaining) : '';\n\n if (remaining <= 0) {\n clearInterval(this._timerInterval!);\n this._timerInterval = null;\n if (this._status === 'active') {\n this._handleStatusChange('EXPIRED', undefined);\n }\n }\n }, 1000);\n }\n\n private _formatTimer(seconds?: number): string {\n const s = seconds ?? (this._expiresAt\n ? Math.max(0, Math.floor((this._expiresAt - Date.now()) / 1000))\n : 0);\n const mins = Math.floor(s / 60);\n const secs = s % 60;\n return `Expires in ${mins}:${secs < 10 ? '0' : ''}${secs}`;\n }\n\n // ---- DOM helpers ---------------------------------------------------------\n\n /** Update just the inner body content without re-rendering the whole component */\n private _updateBody(html: string): void {\n const bodyEl = this.display === 'inline'\n ? this.shadow.getElementById('inline-body')\n : this.shadow.getElementById('modal-body');\n\n if (bodyEl) bodyEl.innerHTML = html;\n }\n\n private _clearTimers(): void {\n if (this._timerInterval) {\n clearInterval(this._timerInterval);\n this._timerInterval = null;\n }\n if (this._pollTimeout) {\n clearTimeout(this._pollTimeout);\n this._pollTimeout = null;\n }\n }\n}\n\ncustomElements.define('qrauth-ephemeral', QRAuthEphemeral);\n", "import { QRAuthElement } from './base.js';\n\n// Same shield SVG as login component\nconst SHIELD_SVG = `<svg viewBox=\"0 0 200 200\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M48 24 H152 Q166 24 166 38 V108 Q166 156 100 182 Q34 156 34 108 V38 Q34 24 48 24 Z\"\n fill=\"none\" stroke=\"#fff\" stroke-width=\"5\"/>\n <g fill=\"#fff\" opacity=\"0.2\">\n <rect x=\"52\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"78\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"130\" y=\"40\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"52\" y=\"66\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"104\" y=\"66\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"78\" y=\"92\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"130\" y=\"92\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"52\" y=\"118\" width=\"18\" height=\"18\" rx=\"3\"/>\n <rect x=\"104\" y=\"118\" width=\"18\" height=\"18\" rx=\"3\"/>\n </g>\n <path d=\"M62 104 L88 134 L144 64\"\n fill=\"none\" stroke=\"#fff\" stroke-width=\"15\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>`;\n\n// Check icon for success state\nconst CHECK_SVG = `<svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"currentColor\"/>\n</svg>`;\n\n// Spinner SVG\nconst SPINNER_SVG = `<svg class=\"spinner\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-dasharray=\"31.416\" stroke-dashoffset=\"10\" stroke-linecap=\"round\"/>\n</svg>`;\n\ntype TwoFAStatus =\n | 'idle'\n | 'loading'\n | 'pending'\n | 'SCANNED'\n | 'APPROVED'\n | 'DENIED'\n | 'EXPIRED'\n | 'error';\n\ninterface AuthSession {\n sessionId: string;\n qrUrl: string;\n expiresAt: string;\n status: TwoFAStatus;\n}\n\ninterface PollResponse {\n sessionId: string;\n status: TwoFAStatus;\n user?: Record<string, unknown>;\n signature?: string;\n}\n\n/**\n * <qrauth-2fa> \u2014 Drop-in second-factor authentication component.\n *\n * After the user completes primary login (e.g. password), render this component\n * to perform QR-based step-up verification. Always displays inline \u2014 no button/modal mode.\n *\n * Attributes:\n * tenant \u2014 Your QRAuth App client ID (required)\n * theme \u2014 \"light\" (default) | \"dark\"\n * base-url \u2014 API base URL (default: https://qrauth.io)\n * session-token \u2014 JWT from the primary authentication (required for context)\n * scopes \u2014 Space-separated scopes (default: \"identity\")\n * auto-start \u2014 Boolean attribute; if present, starts 2FA immediately on connect\n *\n * Events (bubbles + composed, cross shadow-DOM):\n * qrauth:verified \u2014 detail: { sessionId, user, signature }\n * qrauth:denied \u2014 fired when the user denies on their device\n * qrauth:expired \u2014 fired when the session times out\n * qrauth:error \u2014 detail: { message }\n *\n * @example\n * <qrauth-2fa tenant=\"qrauth_app_xxx\" session-token=\"eyJ...\" auto-start></qrauth-2fa>\n * <qrauth-2fa tenant=\"qrauth_app_xxx\" scopes=\"identity payments\"></qrauth-2fa>\n */\nexport class QRAuthTwoFA extends QRAuthElement {\n static override get observedAttributes(): string[] {\n return [...super.observedAttributes, 'session-token', 'scopes', 'auto-start'];\n }\n\n private _sessionId: string | null = null;\n private _codeVerifier: string | null = null;\n private _status: TwoFAStatus = 'idle';\n private _expiresAt: number | null = null;\n private _timerInterval: ReturnType<typeof setInterval> | null = null;\n private _pollTimeout: ReturnType<typeof setTimeout> | null = null;\n\n get sessionToken(): string | null { return this.getAttribute('session-token'); }\n get scopes(): string { return this.getAttribute('scopes') || 'identity'; }\n get autoStart(): boolean { return this.hasAttribute('auto-start'); }\n\n // ---- Lifecycle -----------------------------------------------------------\n\n override connectedCallback(): void {\n super.connectedCallback(); // calls render()\n if (this.autoStart) {\n this._startAuth();\n }\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this._clearTimers();\n }\n\n // ---- Render --------------------------------------------------------------\n\n protected render(): void {\n this.shadow.innerHTML = `\n <style>\n ${this.getBaseStyles()}\n :host { display: block; }\n\n .container {\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: 16px;\n padding: 24px 20px 18px;\n text-align: center;\n max-width: 320px;\n margin: 0 auto;\n box-shadow: 0 2px 12px rgba(0,0,0,0.06);\n }\n\n /* Step indicator */\n .step-label {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--_primary);\n background: rgba(0,167,111,0.08);\n border-radius: 20px;\n padding: 4px 10px;\n margin-bottom: 14px;\n }\n .step-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--_primary);\n flex-shrink: 0;\n }\n\n /* Idle start button */\n .start-btn {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 11px 22px;\n background: var(--_btn-bg);\n color: #fff;\n border: none;\n border-radius: var(--_radius);\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.2s, transform 0.15s, box-shadow 0.2s;\n font-family: inherit;\n width: 100%;\n justify-content: center;\n margin-top: 4px;\n }\n .start-btn:hover:not(:disabled) {\n background: var(--_btn-hover);\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(27,42,74,0.3);\n }\n .start-btn:active:not(:disabled) { transform: translateY(0); }\n .start-btn:disabled { opacity: 0.6; cursor: not-allowed; }\n .btn-icon { width: 20px; height: 20px; flex-shrink: 0; }\n\n ${this._sharedStyles()}\n </style>\n\n <div class=\"container\" role=\"region\" aria-label=\"Second-factor authentication\">\n <div id=\"body\">\n ${this._bodyForStatus('idle')}\n </div>\n </div>\n `;\n\n // Re-bind events after full render (attributeChangedCallback re-renders)\n this._bindBodyEvents();\n }\n\n // ---- Body HTML per status ------------------------------------------------\n\n private _bodyForStatus(status: TwoFAStatus, data?: {\n qrUrl?: string;\n timerHtml?: string;\n errorMessage?: string;\n }): string {\n const stepLabel = `\n <div class=\"step-label\">\n <span class=\"step-dot\"></span>\n Step 2 of 2 — Verify identity\n </div>\n `;\n\n switch (status) {\n case 'idle':\n return `\n ${stepLabel}\n <div class=\"state-header\">\n <div class=\"brand-icon\">${SHIELD_SVG}</div>\n <h3>Verify your identity</h3>\n <p class=\"subtitle\">Scan the QR code with your phone to confirm it's you</p>\n </div>\n <button class=\"start-btn\" id=\"start-btn\" aria-label=\"Start 2FA verification\">\n <span class=\"btn-icon\">${SHIELD_SVG}</span>\n Verify your identity\n </button>\n `;\n\n case 'loading':\n return `\n ${stepLabel}\n <div class=\"state-loading\">\n <div class=\"spinner-wrap\">${SPINNER_SVG}</div>\n <p class=\"status-text\">Generating QR code...</p>\n </div>\n `;\n\n case 'pending':\n case 'SCANNED': {\n const qrImageUrl = data?.qrUrl\n ? `https://api.qrserver.com/v1/create-qr-code/?size=320x320&data=${encodeURIComponent(data.qrUrl)}`\n : '';\n\n const isScanned = status === 'SCANNED';\n const scanClass = isScanned ? 'scanned' : '';\n\n return `\n ${stepLabel}\n <h3>Scan to verify</h3>\n <p class=\"subtitle\">Use your phone camera to complete verification</p>\n <div class=\"qr-frame ${scanClass}\">\n <img src=\"${qrImageUrl}\" alt=\"QR Code for 2FA verification\" width=\"160\" height=\"160\" loading=\"lazy\"/>\n <div class=\"qr-badge\">${SHIELD_SVG}</div>\n ${isScanned ? '<div class=\"scanned-overlay\"><span class=\"scanned-check\">' + CHECK_SVG + '</span></div>' : ''}\n </div>\n <div class=\"status-text ${isScanned ? 'status-scanned' : ''}\">\n ${isScanned\n ? '✓ Scanned — waiting for approval...'\n : 'Waiting for scan...'}\n </div>\n <div class=\"timer\" id=\"timer\">${data?.timerHtml ?? ''}</div>\n <div class=\"footer\">Secured by <a href=\"https://qrauth.io\" target=\"_blank\" rel=\"noopener\">QRAuth</a></div>\n `;\n }\n\n case 'APPROVED':\n return `\n <div class=\"state-approved\">\n <div class=\"success-icon\">${CHECK_SVG}</div>\n <h3>Identity verified!</h3>\n <p class=\"subtitle\">Second factor confirmed</p>\n </div>\n `;\n\n case 'DENIED':\n return `\n <div class=\"state-denied\">\n <div class=\"error-icon\">✗</div>\n <h3>Verification denied</h3>\n <p class=\"subtitle\">The request was declined on your device.</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Try again</button>\n </div>\n `;\n\n case 'EXPIRED':\n return `\n <div class=\"state-expired\">\n <div class=\"expired-icon\">⌛</div>\n <h3>Session expired</h3>\n <p class=\"subtitle\">The QR code has timed out.</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Try again</button>\n </div>\n `;\n\n case 'error':\n return `\n <div class=\"state-error\">\n <div class=\"error-icon\">⚠</div>\n <h3>Something went wrong</h3>\n <p class=\"subtitle\">${data?.errorMessage ?? 'Failed to start verification.'}</p>\n <button class=\"retry-btn\" id=\"retry-btn\">Try again</button>\n </div>\n `;\n\n default:\n return '';\n }\n }\n\n // ---- Shared CSS ----------------------------------------------------------\n\n private _sharedStyles(): string {\n return `\n /* Typography */\n h3 {\n font-size: 16px;\n font-weight: 700;\n color: var(--_text);\n margin: 0 0 5px;\n }\n .subtitle {\n font-size: 12px;\n color: var(--_text-muted);\n margin: 0 0 16px;\n line-height: 1.5;\n }\n\n /* Brand icon in idle state */\n .state-header { padding: 4px 0 2px; }\n .brand-icon {\n width: 48px;\n height: 48px;\n background: var(--_btn-bg);\n border-radius: 12px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 12px;\n padding: 8px;\n }\n .brand-icon svg { width: 32px; height: 32px; }\n\n /* Loading state */\n .state-loading {\n padding: 24px 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 14px;\n }\n .spinner-wrap { color: var(--_primary); }\n .spinner {\n width: 36px;\n height: 36px;\n animation: spin 0.8s linear infinite;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n\n /* QR frame \u2014 compact 160x160 */\n .qr-frame {\n display: inline-block;\n padding: 10px;\n background: #fff;\n border: 2px solid var(--_border);\n border-radius: var(--_radius);\n margin-bottom: 12px;\n position: relative;\n transition: border-color 0.3s;\n }\n .qr-frame.scanned { border-color: var(--_success); }\n .qr-frame img { display: block; width: 160px; height: 160px; border-radius: 4px; }\n\n /* Smaller badge for compact QR */\n .qr-badge {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 40px;\n height: 40px;\n background: #1b2a4a;\n border-radius: 8px;\n padding: 5px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .qr-badge svg { width: 100%; height: 100%; }\n\n /* Scanned overlay on QR */\n .scanned-overlay {\n position: absolute;\n inset: 0;\n background: rgba(0,167,111,0.08);\n border-radius: calc(var(--_radius) - 2px);\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .scanned-check {\n width: 40px;\n height: 40px;\n background: var(--_success);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n opacity: 0;\n animation: pop-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.1s forwards;\n }\n .scanned-check svg { width: 22px; height: 22px; }\n @keyframes pop-in {\n from { transform: scale(0.5); opacity: 0; }\n to { transform: scale(1); opacity: 1; }\n }\n\n /* Status text */\n .status-text {\n font-size: 12px;\n color: var(--_text-muted);\n min-height: 18px;\n }\n .status-scanned {\n color: var(--_success);\n font-weight: 600;\n }\n\n /* Timer */\n .timer {\n font-size: 11px;\n color: var(--_disabled);\n margin-top: 6px;\n font-variant-numeric: tabular-nums;\n min-height: 16px;\n }\n\n /* Success state */\n .state-approved {\n padding: 16px 0 8px;\n animation: fade-scale-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);\n }\n @keyframes fade-scale-in {\n from { transform: scale(0.85); opacity: 0; }\n to { transform: scale(1); opacity: 1; }\n }\n .success-icon {\n width: 56px;\n height: 56px;\n background: var(--_success);\n border-radius: 50%;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n margin-bottom: 14px;\n }\n .success-icon svg { width: 30px; height: 30px; }\n\n /* Denied / expired / error states */\n .state-denied, .state-expired, .state-error {\n padding: 16px 0 6px;\n }\n .error-icon, .expired-icon {\n font-size: 40px;\n margin-bottom: 10px;\n line-height: 1;\n display: block;\n }\n .state-denied .error-icon { color: var(--_error); }\n .state-expired .expired-icon { color: var(--_disabled); }\n .state-error .error-icon { color: var(--_warning); }\n\n /* Retry button */\n .retry-btn {\n margin-top: 12px;\n padding: 8px 20px;\n background: var(--_surface);\n border: 1px solid var(--_border);\n border-radius: 8px;\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n color: var(--_text-muted);\n font-family: inherit;\n transition: background 0.15s, border-color 0.15s;\n }\n .retry-btn:hover { background: var(--_border); color: var(--_text); }\n\n /* Footer */\n .footer {\n margin-top: 14px;\n font-size: 10px;\n color: #c4cdd5;\n }\n .footer a { color: var(--_disabled); text-decoration: none; }\n .footer a:hover { text-decoration: underline; }\n `;\n }\n\n // ---- Event binding -------------------------------------------------------\n\n private _bindBodyEvents(): void {\n this.shadow.getElementById('start-btn')?.addEventListener('click', () => this._startAuth());\n this.shadow.getElementById('retry-btn')?.addEventListener('click', () => this._retry());\n }\n\n // ---- Auth flow -----------------------------------------------------------\n\n private async _startAuth(): Promise<void> {\n this._status = 'loading';\n this._updateBody(this._bodyForStatus('loading'));\n\n try {\n this._codeVerifier = await this.generateCodeVerifier();\n const challenge = await this.computeCodeChallenge(this._codeVerifier);\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-Client-Id': this.tenant,\n };\n\n const body: Record<string, unknown> = {\n scopes: this.scopes.split(/\\s+/).filter(Boolean),\n codeChallenge: challenge,\n codeChallengeMethod: 'S256',\n };\n\n const res = await fetch(`${this.baseUrl}/api/v1/auth-sessions`, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const err = await res.json().catch(() => ({ message: 'Failed to create session' }));\n throw new Error((err as { message?: string }).message ?? 'Failed to create session');\n }\n\n const session = await res.json() as AuthSession;\n this._sessionId = session.sessionId;\n this._expiresAt = new Date(session.expiresAt).getTime();\n this._status = 'pending';\n\n this._updateBody(this._bodyForStatus('pending', { qrUrl: session.qrUrl }));\n this._bindBodyEvents();\n this._startCountdown();\n this._beginPolling(session.sessionId);\n\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unexpected error';\n this._status = 'error';\n this._updateBody(this._bodyForStatus('error', { errorMessage: message }));\n this._bindBodyEvents();\n this.emit('qrauth:error', { message });\n }\n }\n\n private _retry(): void {\n this._clearTimers();\n this._sessionId = null;\n this._codeVerifier = null;\n this._status = 'idle';\n this._expiresAt = null;\n\n this._updateBody(this._bodyForStatus('idle'));\n this._bindBodyEvents();\n }\n\n // ---- Polling -------------------------------------------------------------\n\n private _beginPolling(sessionId: string): void {\n let lastStatus: string = 'PENDING';\n let interval = 2000;\n const maxInterval = 5000;\n let errorCount = 0;\n const maxErrors = 20;\n\n const poll = async () => {\n if (!this._sessionId) return; // component was reset\n\n let url = `${this.baseUrl}/api/v1/auth-sessions/${sessionId}`;\n if (this._codeVerifier) {\n url += `?code_verifier=${encodeURIComponent(this._codeVerifier)}`;\n }\n\n try {\n const res = await fetch(url, {\n headers: { 'X-Client-Id': this.tenant },\n });\n const data = await res.json() as PollResponse;\n errorCount = 0;\n interval = 2000;\n\n if (data.status !== lastStatus) {\n lastStatus = data.status;\n this._handleStatusChange(data.status, data);\n\n if (['APPROVED', 'DENIED', 'EXPIRED'].includes(data.status)) {\n return; // stop polling\n }\n }\n\n this._pollTimeout = setTimeout(poll, interval);\n\n } catch {\n errorCount++;\n if (errorCount >= maxErrors) {\n this._handleStatusChange('EXPIRED', undefined);\n return;\n }\n interval = Math.min(interval * 1.5, maxInterval);\n this._pollTimeout = setTimeout(poll, interval);\n }\n };\n\n this._pollTimeout = setTimeout(poll, interval);\n }\n\n // ---- Status handling -----------------------------------------------------\n\n private _handleStatusChange(status: TwoFAStatus, data?: PollResponse): void {\n this._status = status;\n\n switch (status) {\n case 'SCANNED': {\n // Preserve current QR URL from the existing img element\n const currentQrUrl = this.shadow.querySelector<HTMLImageElement>('.qr-frame img')?.src;\n const rawQrUrl = currentQrUrl\n ? decodeURIComponent(currentQrUrl.replace(/.*[?&]data=/, '').split('&')[0])\n : '';\n\n this._updateBody(this._bodyForStatus('SCANNED', {\n qrUrl: rawQrUrl,\n timerHtml: this._formatTimer(),\n }));\n this._bindBodyEvents();\n break;\n }\n\n case 'APPROVED': {\n this._clearTimers();\n this._updateBody(this._bodyForStatus('APPROVED'));\n this.emit('qrauth:verified', {\n sessionId: data?.sessionId,\n user: data?.user,\n signature: data?.signature,\n });\n break;\n }\n\n case 'DENIED':\n this._clearTimers();\n this._updateBody(this._bodyForStatus('DENIED'));\n this._bindBodyEvents();\n this.emit('qrauth:denied');\n break;\n\n case 'EXPIRED':\n this._clearTimers();\n this._status = 'EXPIRED';\n this._updateBody(this._bodyForStatus('EXPIRED'));\n this._bindBodyEvents();\n this.emit('qrauth:expired');\n break;\n }\n }\n\n // ---- Countdown timer -----------------------------------------------------\n\n private _startCountdown(): void {\n this._timerInterval = setInterval(() => {\n const timerEl = this.shadow.getElementById('timer');\n if (!timerEl || !this._expiresAt) return;\n\n const remaining = Math.max(0, Math.floor((this._expiresAt - Date.now()) / 1000));\n timerEl.textContent = remaining > 0 ? this._formatTimer(remaining) : '';\n\n if (remaining <= 0) {\n clearInterval(this._timerInterval!);\n this._timerInterval = null;\n if (this._status === 'pending' || this._status === 'SCANNED') {\n this._handleStatusChange('EXPIRED', undefined);\n }\n }\n }, 1000);\n }\n\n private _formatTimer(seconds?: number): string {\n const s = seconds ?? (this._expiresAt\n ? Math.max(0, Math.floor((this._expiresAt - Date.now()) / 1000))\n : 0);\n const mins = Math.floor(s / 60);\n const secs = s % 60;\n return `Expires in ${mins}:${secs < 10 ? '0' : ''}${secs}`;\n }\n\n // ---- DOM helpers ---------------------------------------------------------\n\n private _updateBody(html: string): void {\n const bodyEl = this.shadow.getElementById('body');\n if (bodyEl) bodyEl.innerHTML = html;\n }\n\n private _clearTimers(): void {\n if (this._timerInterval) {\n clearInterval(this._timerInterval);\n this._timerInterval = null;\n }\n if (this._pollTimeout) {\n clearTimeout(this._pollTimeout);\n this._pollTimeout = null;\n }\n }\n}\n\ncustomElements.define('qrauth-2fa', QRAuthTwoFA);\n"],
|
|
5
|
-
"mappings": ";4kBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,mBAAAE,EAAA,oBAAAC,EAAA,gBAAAC,EAAA,gBAAAC,ICKO,IAAeC,EAAf,cAAqC,WAAY,CAStD,aAAc,CACZ,MAAM,EATRC,EAAA,KAAU,UACVA,EAAA,KAAQ,iBAAqC,MAC7CA,EAAA,KAAQ,gBAAuD,MAQ7D,KAAK,OAAS,KAAK,aAAa,CAAE,KAAM,MAAO,CAAC,CAClD,CAPA,WAAW,oBAA+B,CACxC,MAAO,CAAC,SAAU,QAAS,UAAU,CACvC,CAQA,IAAI,QAAiB,CAAE,OAAO,KAAK,aAAa,QAAQ,GAAK,EAAI,CACjE,IAAI,OAAgB,CAAE,OAAO,KAAK,aAAa,OAAO,GAAK,OAAS,CACpE,IAAI,SAAkB,CACpB,IAAMC,EAAM,KAAK,aAAa,UAAU,EAExC,OAAOA,IAAQ,KAAOA,EAAM,mBAC9B,CAEA,mBAA0B,CACxB,KAAK,OAAO,CACd,CAEA,sBAA6B,CAC3B,KAAK,QAAQ,CACf,CAEA,yBAAyBC,EAAeC,EAAyBC,EAA+B,CAC1FD,IAAaC,GACf,KAAK,OAAO,CAEhB,CAMU,WAAWC,EAAmB,CACtC,KAAK,cAAc,EAGnB,KAAK,eAAiB,IAAI,YAAYA,CAAG,EAEzC,KAAK,eAAe,QAAU,IAAM,CAElC,KAAK,cAAc,CACrB,CACF,CAEU,WAAWC,EAAeC,EAAyC,CACvE,KAAK,gBACP,KAAK,eAAe,iBAAiBD,EAAQE,GAAa,CACxD,IAAMC,EAAMD,EACZ,GAAI,CAAED,EAAS,KAAK,MAAME,EAAI,IAAc,CAAC,CAAG,MAAQ,CAAqB,CAC/E,CAAC,CAEL,CAEU,eAAsB,CAC1B,KAAK,iBACP,KAAK,eAAe,MAAM,EAC1B,KAAK,eAAiB,KAE1B,CAGU,aACRJ,EACAK,EACAH,EACAI,EACM,CACN,KAAK,YAAY,EACjB,IAAMC,EAAO,SAAY,CACvB,GAAI,CACF,IAAMC,EAAM,MAAM,MAAMR,EAAK,CAAE,QAAAM,CAAQ,CAAC,EACpCE,EAAI,IAAIN,EAAS,MAAMM,EAAI,KAAK,CAAC,CACvC,MAAQ,CAA4C,CACtD,EACAD,EAAK,EACL,KAAK,cAAgB,YAAYA,EAAMF,CAAU,CACnD,CAEU,aAAoB,CACxB,KAAK,gBACP,cAAc,KAAK,aAAa,EAChC,KAAK,cAAgB,KAEzB,CAGU,SAAgB,CACxB,KAAK,cAAc,EACnB,KAAK,YAAY,CACnB,CAGU,KAAKI,EAAcC,EAAwB,CACnD,KAAK,cAAc,IAAI,YAAYD,EAAM,CACvC,QAAS,GACT,SAAU,GACV,OAAAC,CACF,CAAC,CAAC,CACJ,CAGA,MAAgB,sBAAwC,CACtD,IAAMC,EAAQ,IAAI,WAAW,EAAE,EAC/B,cAAO,gBAAgBA,CAAK,EACrB,KAAK,WAAWA,CAAK,CAC9B,CAEA,MAAgB,qBAAqBC,EAAmC,CAEtE,IAAMC,EADU,IAAI,YAAY,EACX,OAAOD,CAAQ,EAC9BE,EAAO,MAAM,OAAO,OAAO,OAAO,UAAWD,CAAI,EACvD,OAAO,KAAK,WAAW,IAAI,WAAWC,CAAI,CAAC,CAC7C,CAEQ,WAAWH,EAA2B,CAC5C,IAAMI,EAAY,MAAM,KAAKJ,EAAQK,GAAS,OAAO,cAAcA,CAAI,CAAC,EAAE,KAAK,EAAE,EACjF,OAAO,KAAKD,CAAS,EAAE,QAAQ,MAAO,GAAG,EAAE,QAAQ,MAAO,GAAG,EAAE,QAAQ,MAAO,EAAE,CAClF,CAGU,eAAwB,CAChC,MAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAgCT,CACF,ECrKA,IAAME,EAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAoBbC,EAAY;AAAA;AAAA,QAKZC,EAAc;AAAA;AAAA;AAAA,QAoDPC,EAAN,cAA0BC,CAAc,CAAxC,kCAKLC,EAAA,KAAQ,aAA4B,MACpCA,EAAA,KAAQ,gBAA+B,MACvCA,EAAA,KAAQ,UAAsB,QAC9BA,EAAA,KAAQ,aAA4B,MACpCA,EAAA,KAAQ,iBAAwD,MAChEA,EAAA,KAAQ,eAAqD,MAT7D,WAAoB,oBAA+B,CACjD,MAAO,CAAC,GAAG,MAAM,mBAAoB,SAAU,eAAgB,UAAW,UAAW,UAAU,CACjG,CASA,IAAI,QAAiB,CAAE,OAAO,KAAK,aAAa,QAAQ,GAAK,UAAY,CACzE,IAAI,aAA6B,CAAE,OAAO,KAAK,aAAa,cAAc,CAAG,CAC7E,IAAI,QAAwB,CAAE,OAAO,KAAK,aAAa,SAAS,CAAG,CACnE,IAAI,SAAkB,CAAE,OAAO,KAAK,aAAa,SAAS,GAAK,QAAU,CACzE,IAAI,UAAoB,CAAE,OAAO,KAAK,aAAa,UAAU,CAAG,CAIvD,sBAA6B,CACpC,MAAM,qBAAqB,EAC3B,KAAK,aAAa,CACpB,CAIU,QAAe,CACnB,KAAK,UAAY,SACnB,KAAK,cAAc,EAEnB,KAAK,cAAc,CAEvB,CAEQ,eAAsB,CAC5B,KAAK,OAAO,UAAY;AAAA;AAAA,UAElB,KAAK,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UA4DpB,KAAK,mBAAmB,CAAC;AAAA;AAAA;AAAA;AAAA,iCAIFL,CAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAM/B,KAAK,cAAc,CAAC;AAAA;AAAA;AAAA,MAK5B,KAAK,kBAAkB,CACzB,CAEQ,eAAsB,CAC5B,KAAK,OAAO,UAAY;AAAA;AAAA,UAElB,KAAK,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAsCpB,KAAK,mBAAmB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,YAKvB,KAAK,mBAAmB,CAAC;AAAA;AAAA;AAAA,MAKjC,KAAK,kBAAkB,CACzB,CAEQ,oBAA6B,CACnC,MAAO;AAAA;AAAA,iCAEsBA,CAAU;AAAA;AAAA;AAAA,KAIzC,CAIQ,eAAwB,CAC9B,MAAO;AAAA;AAAA;AAAA,UAGD,KAAK,eAAe,MAAM,CAAC;AAAA;AAAA,KAGnC,CAEQ,eAAeM,EAAoBC,EAKhC,CACT,OAAQD,EAAQ,CACd,IAAK,OACH,MAAO;AAAA;AAAA,sCAEuBN,CAAU;AAAA;AAAA;AAAA;AAAA,UAM1C,IAAK,UACH,MAAO;AAAA;AAAA,wCAEyBE,CAAW;AAAA;AAAA;AAAA,UAK7C,IAAK,UACL,IAAK,UAAW,CACd,IAAMM,EAAaD,GAAM,MACrB,iEAAiE,mBAAmBA,EAAK,KAAK,CAAC,GAC/F,GAEEE,EAAYH,IAAW,UAG7B,MAAO;AAAA;AAAA;AAAA,iCAFWG,EAAY,UAAY,EAKR,GAAG,KAAK,SAAW,YAAc,EAAE;AAAA,wBACrDD,CAAU;AAAA,oCACER,CAAU;AAAA,cAChCS,EAAY,4DAA8DR,EAAY,gBAAkB,EAAE;AAAA;AAAA,oCAEpFQ,EAAY,iBAAmB,EAAE;AAAA,cACvDA,EACE,2DACA,qBAAqB;AAAA;AAAA,0CAEKF,GAAM,WAAa,EAAE;AAAA;AAAA,SAGzD,CAEA,IAAK,WACH,MAAO;AAAA;AAAA,wCAEyBN,CAAS;AAAA;AAAA;AAAA;AAAA,UAM3C,IAAK,SACH,MAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAST,IAAK,UACH,MAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAST,IAAK,QACH,MAAO;AAAA;AAAA;AAAA;AAAA,kCAImBM,GAAM,cAAgB,iCAAiC;AAAA;AAAA;AAAA,UAKnF,QACE,MAAO,EACX,CACF,CAIQ,oBAA6B,CACnC,MAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAgNT,CAIQ,mBAA0B,CAChB,KAAK,OAAO,eAAe,UAAU,GAC5C,iBAAiB,QAAS,IAAM,CACvC,KAAK,WAAW,EAChB,KAAK,WAAW,CAClB,CAAC,EAGD,IAAMG,EAAU,KAAK,OAAO,eAAe,SAAS,EACpDA,GAAS,iBAAiB,QAAUC,GAAM,CACpCA,EAAE,SAAWD,GAAS,KAAK,YAAY,CAC7C,CAAC,EAED,KAAK,kBAAkB,CACzB,CAEQ,mBAA0B,CACf,KAAK,OAAO,eAAe,WAAW,GAC7C,iBAAiB,QAAS,IAAM,CACxC,KAAK,WAAW,CAClB,CAAC,CACH,CAEQ,mBAA0B,CAChC,KAAK,OAAO,eAAe,WAAW,GAAG,iBAAiB,QAAS,IAAM,KAAK,YAAY,CAAC,EAC3F,KAAK,OAAO,eAAe,WAAW,GAAG,iBAAiB,QAAS,IAAM,KAAK,OAAO,CAAC,CACxF,CAIQ,YAAmB,CACT,KAAK,OAAO,eAAe,SAAS,GAC3C,UAAU,OAAO,QAAQ,EAClC,KAAK,YAAY,KAAK,eAAe,MAAM,CAAC,CAC9C,CAEQ,aAAoB,CACV,KAAK,OAAO,eAAe,SAAS,GAC3C,UAAU,IAAI,QAAQ,EAC/B,KAAK,aAAa,EAClB,KAAK,WAAa,KAClB,KAAK,cAAgB,KACrB,KAAK,QAAU,OACf,KAAK,WAAa,IACpB,CAIA,MAAc,YAA4B,CACxC,KAAK,QAAU,UACf,KAAK,YAAY,KAAK,eAAe,SAAS,CAAC,EAE/C,GAAI,CACF,KAAK,cAAgB,MAAM,KAAK,qBAAqB,EACrD,IAAME,EAAY,MAAM,KAAK,qBAAqB,KAAK,aAAa,EAE9DC,EAAkC,CACtC,eAAgB,mBAChB,cAAe,KAAK,MACtB,EAEMC,EAAgC,CACpC,OAAQ,KAAK,OAAO,MAAM,KAAK,EAAE,OAAO,OAAO,EAC/C,cAAeF,EACf,oBAAqB,MACvB,EAEMG,EAAM,MAAM,MAAM,GAAG,KAAK,OAAO,wBAAyB,CAC9D,OAAQ,OACR,QAAAF,EACA,KAAM,KAAK,UAAUC,CAAI,CAC3B,CAAC,EAED,GAAI,CAACC,EAAI,GAAI,CACX,IAAMC,EAAM,MAAMD,EAAI,KAAK,EAAE,MAAM,KAAO,CAAE,QAAS,0BAA2B,EAAE,EAClF,MAAM,IAAI,MAAOC,EAA6B,SAAW,0BAA0B,CACrF,CAEA,IAAMC,EAAU,MAAMF,EAAI,KAAK,EAC/B,KAAK,WAAaE,EAAQ,UAC1B,KAAK,WAAa,IAAI,KAAKA,EAAQ,SAAS,EAAE,QAAQ,EACtD,KAAK,QAAU,UAEf,KAAK,YAAY,KAAK,eAAe,UAAW,CAAE,MAAOA,EAAQ,KAAM,CAAC,CAAC,EACzE,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,cAAcA,EAAQ,SAAS,CAEtC,OAASD,EAAK,CACZ,IAAME,EAAUF,aAAe,MAAQA,EAAI,QAAU,mBACrD,KAAK,QAAU,QACf,KAAK,YAAY,KAAK,eAAe,QAAS,CAAE,aAAcE,CAAQ,CAAC,CAAC,EACxE,KAAK,kBAAkB,EACvB,KAAK,KAAK,eAAgB,CAAE,QAAAA,CAAQ,CAAC,CACvC,CACF,CAEQ,QAAe,CAOrB,GANA,KAAK,aAAa,EAClB,KAAK,WAAa,KAClB,KAAK,cAAgB,KACrB,KAAK,QAAU,OACf,KAAK,WAAa,KAEd,KAAK,UAAY,SAAU,CAE7B,IAAMC,EAAa,KAAK,OAAO,eAAe,aAAa,EACvDA,IACFA,EAAW,UAAY,KAAK,mBAAmB,EAC/C,KAAK,kBAAkB,EAE3B,MACE,KAAK,WAAW,CAEpB,CAIQ,cAAcC,EAAyB,CAC7C,IAAIC,EAAqB,UACrBC,EAAW,IACTC,EAAc,IAChBC,EAAa,EACXC,EAAY,GAEZC,EAAO,SAAY,CACvB,GAAI,CAAC,KAAK,WAAY,OAEtB,IAAIC,EAAM,GAAG,KAAK,OAAO,yBAAyBP,CAAS,GACvD,KAAK,gBACPO,GAAO,kBAAkB,mBAAmB,KAAK,aAAa,CAAC,IAGjE,GAAI,CAIF,IAAMpB,EAAO,MAHD,MAAM,MAAMoB,EAAK,CAC3B,QAAS,CAAE,cAAe,KAAK,MAAO,CACxC,CAAC,GACsB,KAAK,EAI5B,GAHAH,EAAa,EACbF,EAAW,IAEPf,EAAK,SAAWc,IAClBA,EAAad,EAAK,OAClB,KAAK,oBAAoBA,EAAK,OAAQA,CAAI,EAEtC,CAAC,WAAY,SAAU,SAAS,EAAE,SAASA,EAAK,MAAM,GACxD,OAIJ,KAAK,aAAe,WAAWmB,EAAMJ,CAAQ,CAE/C,MAAQ,CAEN,GADAE,IACIA,GAAcC,EAAW,CAC3B,KAAK,oBAAoB,UAAW,MAAS,EAC7C,MACF,CACAH,EAAW,KAAK,IAAIA,EAAW,IAAKC,CAAW,EAC/C,KAAK,aAAe,WAAWG,EAAMJ,CAAQ,CAC/C,CACF,EAEA,KAAK,aAAe,WAAWI,EAAMJ,CAAQ,CAC/C,CAIQ,oBAAoBhB,EAAoBC,EAA2B,CAGzE,OAFA,KAAK,QAAUD,EAEPA,EAAQ,CACd,IAAK,UAAW,CAEd,IAAMsB,EAAe,KAAK,OAAO,cAAgC,eAAe,GAAG,IAE7EC,EAAWD,EACb,mBAAmBA,EAAa,QAAQ,cAAe,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC,CAAC,EACxE,GAEJ,KAAK,YAAY,KAAK,eAAe,UAAW,CAC9C,MAAOC,EACP,UAAW,KAAK,aAAa,CAC/B,CAAC,CAAC,EACF,KAAK,kBAAkB,EACvB,KAAK,KAAK,iBAAkBtB,CAAI,EAChC,KACF,CAEA,IAAK,WAAY,CACf,KAAK,aAAa,EAClB,KAAK,YAAY,KAAK,eAAe,UAAU,CAAC,EAChD,KAAK,kBAAkB,EACvB,KAAK,KAAK,uBAAwB,CAChC,UAAWA,GAAM,UACjB,KAAMA,GAAM,KACZ,UAAWA,GAAM,SACnB,CAAC,EAGD,IAAMuB,EAAe,KAAK,OAC1B,GAAIA,EAAc,CAChB,IAAMC,EAAkB,OAA8CD,CAAY,EAC9E,OAAOC,GAAmB,YAC3BA,EAAwCxB,CAAI,CAEjD,CAGI,KAAK,YACP,WAAW,IAAM,CAAE,OAAO,SAAS,KAAO,KAAK,WAAc,EAAG,IAAI,EAC3D,KAAK,UAAY,UAE1B,WAAW,IAAM,KAAK,YAAY,EAAG,IAAI,EAE3C,KACF,CAEA,IAAK,SACH,KAAK,aAAa,EAClB,KAAK,YAAY,KAAK,eAAe,QAAQ,CAAC,EAC9C,KAAK,kBAAkB,EACvB,KAAK,KAAK,eAAe,EACzB,MAEF,IAAK,UACH,KAAK,aAAa,EAClB,KAAK,QAAU,UACf,KAAK,YAAY,KAAK,eAAe,SAAS,CAAC,EAC/C,KAAK,kBAAkB,EACvB,KAAK,KAAK,gBAAgB,EAC1B,KACJ,CACF,CAIQ,iBAAwB,CAC9B,KAAK,eAAiB,YAAY,IAAM,CACtC,IAAMyB,EAAU,KAAK,OAAO,eAAe,OAAO,EAClD,GAAI,CAACA,GAAW,CAAC,KAAK,WAAY,OAElC,IAAMC,EAAY,KAAK,IAAI,EAAG,KAAK,OAAO,KAAK,WAAa,KAAK,IAAI,GAAK,GAAI,CAAC,EAC/ED,EAAQ,YAAcC,EAAY,EAAI,KAAK,aAAaA,CAAS,EAAI,GAEjEA,GAAa,IACf,cAAc,KAAK,cAAe,EAClC,KAAK,eAAiB,MAClB,KAAK,UAAY,WAAa,KAAK,UAAY,YACjD,KAAK,oBAAoB,UAAW,MAAS,EAGnD,EAAG,GAAI,CACT,CAEQ,aAAaC,EAA0B,CAC7C,IAAMC,EAAID,IAAY,KAAK,WACvB,KAAK,IAAI,EAAG,KAAK,OAAO,KAAK,WAAa,KAAK,IAAI,GAAK,GAAI,CAAC,EAC7D,GACEE,EAAO,KAAK,MAAMD,EAAI,EAAE,EACxBE,EAAOF,EAAI,GACjB,MAAO,cAAcC,CAAI,IAAIC,EAAO,GAAK,IAAM,EAAE,GAAGA,CAAI,EAC1D,CAKQ,YAAYC,EAAoB,CACtC,IAAMC,EAAS,KAAK,UAAY,SAC5B,KAAK,OAAO,eAAe,aAAa,EACxC,KAAK,OAAO,eAAe,YAAY,EAEvCA,IAAQA,EAAO,UAAYD,EACjC,CAEQ,cAAqB,CACvB,KAAK,iBACP,cAAc,KAAK,cAAc,EACjC,KAAK,eAAiB,MAEpB,KAAK,eACP,aAAa,KAAK,YAAY,EAC9B,KAAK,aAAe,KAExB,CACF,EAEA,eAAe,OAAO,eAAgBnC,CAAW,ECh2BjD,IAAMqC,EAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAoBbC,EAAY;AAAA;AAAA,QAKZC,EAAc;AAAA;AAAA;AAAA,QA2DPC,EAAN,cAA8BC,CAAc,CAA5C,kCAKLC,EAAA,KAAQ,aAA4B,MACpCA,EAAA,KAAQ,YAA2B,MACnCA,EAAA,KAAQ,UAA2B,QACnCA,EAAA,KAAQ,aAA4B,MACpCA,EAAA,KAAQ,iBAAwD,MAChEA,EAAA,KAAQ,eAAqD,MAT7D,WAAoB,oBAA+B,CACjD,MAAO,CAAC,GAAG,MAAM,mBAAoB,SAAU,MAAO,WAAY,iBAAkB,SAAS,CAC/F,CASA,IAAI,QAAiB,CAAE,OAAO,KAAK,aAAa,QAAQ,GAAK,QAAU,CACvE,IAAI,KAAc,CAAE,OAAO,KAAK,aAAa,KAAK,GAAK,KAAO,CAC9D,IAAI,SAAkB,CAAE,OAAO,SAAS,KAAK,aAAa,UAAU,GAAK,IAAK,EAAE,CAAG,CACnF,IAAI,eAAyB,CAAE,OAAO,KAAK,aAAa,gBAAgB,CAAG,CAC3E,IAAI,SAAkB,CAAE,OAAO,KAAK,aAAa,SAAS,GAAK,QAAU,CAIhE,sBAA6B,CACpC,MAAM,qBAAqB,EAC3B,KAAK,aAAa,CACpB,CAIU,QAAe,CACnB,KAAK,UAAY,SACnB,KAAK,cAAc,EAEnB,KAAK,cAAc,CAEvB,CAEQ,eAAsB,CAC5B,KAAK,OAAO,UAAY;AAAA;AAAA,UAElB,KAAK,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UA4DpB,KAAK,mBAAmB,CAAC;AAAA;AAAA;AAAA;AAAA,iCAIFL,CAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAM/B,KAAK,cAAc,CAAC;AAAA;AAAA;AAAA,MAK5B,KAAK,kBAAkB,CACzB,CAEQ,eAAsB,CAC5B,KAAK,OAAO,UAAY;AAAA;AAAA,UAElB,KAAK,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAsCpB,KAAK,mBAAmB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,YAKvB,KAAK,mBAAmB,CAAC;AAAA;AAAA;AAAA,MAKjC,KAAK,kBAAkB,CACzB,CAEQ,oBAA6B,CACnC,MAAO;AAAA;AAAA,iCAEsBA,CAAU;AAAA;AAAA;AAAA,KAIzC,CAIQ,eAAwB,CAC9B,MAAO;AAAA;AAAA;AAAA,UAGD,KAAK,eAAe,MAAM,CAAC;AAAA;AAAA,KAGnC,CAEQ,eAAeM,EAAyBC,EAMrC,CACT,OAAQD,EAAQ,CACd,IAAK,OACH,MAAO;AAAA;AAAA,sCAEuBN,CAAU;AAAA;AAAA;AAAA;AAAA,UAM1C,IAAK,UACH,MAAO;AAAA;AAAA,wCAEyBE,CAAW;AAAA;AAAA;AAAA,UAK7C,IAAK,SAAU,CACb,IAAMM,EAAaD,GAAM,SACrB,iEAAiE,mBAAmBA,EAAK,QAAQ,CAAC,GAClG,GAEEE,EAAWF,GAAM,UAAY,EAC7BG,EAAUH,GAAM,SAAW,EAE3BI,EADaD,EAAU,EAEzB,0BAA0BD,CAAQ,IAAIC,CAAO,iBAC7C,GAEJ,MAAO;AAAA;AAAA;AAAA;AAAA,wBAISF,CAAU;AAAA,oCACER,CAAU;AAAA;AAAA,YAElCW,CAAQ;AAAA,0CACsBJ,GAAM,WAAa,EAAE;AAAA;AAAA,SAGzD,CAEA,IAAK,UACH,MAAO;AAAA;AAAA,wCAEyBN,CAAS;AAAA;AAAA;AAAA;AAAA,UAM3C,IAAK,UACH,MAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAST,IAAK,UACH,MAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAQT,IAAK,QACH,MAAO;AAAA;AAAA;AAAA;AAAA,kCAImBM,GAAM,cAAgB,qCAAqC;AAAA;AAAA;AAAA,UAKvF,QACE,MAAO,EACX,CACF,CAIQ,oBAA6B,CACnC,MAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAuLT,CAIQ,mBAA0B,CAChB,KAAK,OAAO,eAAe,UAAU,GAC5C,iBAAiB,QAAS,IAAM,CACvC,KAAK,WAAW,EAChB,KAAK,cAAc,CACrB,CAAC,EAGD,IAAMK,EAAU,KAAK,OAAO,eAAe,SAAS,EACpDA,GAAS,iBAAiB,QAAUC,GAAM,CACpCA,EAAE,SAAWD,GAAS,KAAK,YAAY,CAC7C,CAAC,EAED,KAAK,kBAAkB,CACzB,CAEQ,mBAA0B,CACf,KAAK,OAAO,eAAe,WAAW,GAC7C,iBAAiB,QAAS,IAAM,CACxC,KAAK,cAAc,CACrB,CAAC,CACH,CAEQ,mBAA0B,CAChC,KAAK,OAAO,eAAe,WAAW,GAAG,iBAAiB,QAAS,IAAM,KAAK,YAAY,CAAC,EAC3F,KAAK,OAAO,eAAe,WAAW,GAAG,iBAAiB,QAAS,IAAM,KAAK,OAAO,CAAC,CACxF,CAIQ,YAAmB,CACT,KAAK,OAAO,eAAe,SAAS,GAC3C,UAAU,OAAO,QAAQ,EAClC,KAAK,YAAY,KAAK,eAAe,MAAM,CAAC,CAC9C,CAEQ,aAAoB,CACV,KAAK,OAAO,eAAe,SAAS,GAC3C,UAAU,IAAI,QAAQ,EAC/B,KAAK,aAAa,EAClB,KAAK,WAAa,KAClB,KAAK,UAAY,KACjB,KAAK,QAAU,OACf,KAAK,WAAa,IACpB,CAIA,MAAc,eAA+B,CAC3C,KAAK,QAAU,UACf,KAAK,YAAY,KAAK,eAAe,SAAS,CAAC,EAE/C,GAAI,CACF,IAAME,EAAM,MAAM,MAAM,GAAG,KAAK,OAAO,oBAAqB,CAC1D,OAAQ,OACR,QAAS,CACP,eAAgB,mBAChB,cAAe,KAAK,MACtB,EACA,KAAM,KAAK,UAAU,CACnB,OAAQ,KAAK,OAAO,MAAM,KAAK,EAAE,OAAO,OAAO,EAC/C,IAAK,KAAK,IACV,QAAS,KAAK,QACd,cAAe,KAAK,aACtB,CAAC,CACH,CAAC,EAED,GAAI,CAACA,EAAI,GAAI,CACX,IAAMC,EAAM,MAAMD,EAAI,KAAK,EAAE,MAAM,KAAO,CAAE,QAAS,0BAA2B,EAAE,EAClF,MAAM,IAAI,MAAOC,EAA6B,SAAW,0BAA0B,CACrF,CAEA,IAAMC,EAAU,MAAMF,EAAI,KAAK,EAC/B,KAAK,WAAaE,EAAQ,UAC1B,KAAK,UAAYA,EAAQ,SACzB,KAAK,WAAa,IAAI,KAAKA,EAAQ,SAAS,EAAE,QAAQ,EACtD,KAAK,QAAU,SAEf,KAAK,YAAY,KAAK,eAAe,SAAU,CAC7C,SAAUA,EAAQ,SAClB,UAAW,KAAK,aAAa,EAC7B,SAAU,EACV,QAASA,EAAQ,OACnB,CAAC,CAAC,EACF,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,cAAcA,EAAQ,SAAS,CAEtC,OAASD,EAAK,CACZ,IAAME,EAAUF,aAAe,MAAQA,EAAI,QAAU,mBACrD,KAAK,QAAU,QACf,KAAK,YAAY,KAAK,eAAe,QAAS,CAAE,aAAcE,CAAQ,CAAC,CAAC,EACxE,KAAK,kBAAkB,EACvB,KAAK,KAAK,eAAgB,CAAE,QAAAA,CAAQ,CAAC,CACvC,CACF,CAEQ,QAAe,CAOrB,GANA,KAAK,aAAa,EAClB,KAAK,WAAa,KAClB,KAAK,UAAY,KACjB,KAAK,QAAU,OACf,KAAK,WAAa,KAEd,KAAK,UAAY,SAAU,CAE7B,IAAMC,EAAa,KAAK,OAAO,eAAe,aAAa,EACvDA,IACFA,EAAW,UAAY,KAAK,mBAAmB,EAC/C,KAAK,kBAAkB,EAE3B,MACE,KAAK,cAAc,CAEvB,CAIQ,cAAcC,EAAyB,CAC7C,IAAIC,EAAW,IACTC,EAAc,IAChBC,EAAa,EACXC,EAAY,GAEZC,EAAO,SAAY,CACvB,GAAK,KAAK,WAEV,GAAI,CAIF,IAAMjB,EAAO,MAHD,MAAM,MAAM,GAAG,KAAK,OAAO,qBAAqBY,CAAS,GAAI,CACvE,QAAS,CAAE,cAAe,KAAK,MAAO,CACxC,CAAC,GACsB,KAAK,EAM5B,GALAG,EAAa,EACbF,EAAW,IAEX,KAAK,oBAAoBb,EAAK,OAAQA,CAAI,EAEtC,CAAC,UAAW,UAAW,SAAS,EAAE,SAASA,EAAK,MAAM,EAAG,CAE3D,GAAIA,EAAK,SAAW,WAAaA,EAAK,SAAWA,EAAK,QAAS,CAE7D,KAAK,YAAY,KAAK,eAAe,SAAU,CAC7C,SAAU,KAAK,WAAa,OAC5B,UAAW,KAAK,aAAa,EAC7B,SAAUA,EAAK,SACf,QAASA,EAAK,OAChB,CAAC,CAAC,EACF,KAAK,kBAAkB,EACvB,KAAK,aAAe,WAAWiB,EAAMJ,CAAQ,EAC7C,MACF,CACA,MACF,CAEA,KAAK,aAAe,WAAWI,EAAMJ,CAAQ,CAE/C,MAAQ,CAEN,GADAE,IACIA,GAAcC,EAAW,CAC3B,KAAK,oBAAoB,UAAW,MAAS,EAC7C,MACF,CACAH,EAAW,KAAK,IAAIA,EAAW,IAAKC,CAAW,EAC/C,KAAK,aAAe,WAAWG,EAAMJ,CAAQ,CAC/C,CACF,EAEA,KAAK,aAAe,WAAWI,EAAMJ,CAAQ,CAC/C,CAIQ,oBAAoBd,EAAgBC,EAA2B,CACrE,OAAQD,EAAQ,CACd,IAAK,UAAW,CAEd,KAAK,aAAa,EAClB,KAAK,QAAU,UACf,KAAK,YAAY,KAAK,eAAe,SAAS,CAAC,EAC/C,KAAK,kBAAkB,EACvB,KAAK,KAAK,iBAAkB,CAC1B,UAAWC,GAAM,UACjB,OAAQA,GAAM,OACd,OAAQA,GAAM,OACd,SAAUA,GAAM,SAChB,SAAUA,GAAM,SAChB,QAASA,GAAM,OACjB,CAAC,EAEG,KAAK,UAAY,UACnB,WAAW,IAAM,KAAK,YAAY,EAAG,IAAI,EAE3C,KACF,CAEA,IAAK,UACH,KAAK,aAAa,EAClB,KAAK,QAAU,UACf,KAAK,YAAY,KAAK,eAAe,SAAS,CAAC,EAC/C,KAAK,kBAAkB,EACvB,KAAK,KAAK,gBAAgB,EAC1B,MAEF,IAAK,UACH,KAAK,aAAa,EAClB,KAAK,QAAU,UACf,KAAK,YAAY,KAAK,eAAe,SAAS,CAAC,EAC/C,KAAK,kBAAkB,EACvB,KAAK,KAAK,gBAAgB,EAC1B,KACJ,CACF,CAIQ,iBAAwB,CAC9B,KAAK,eAAiB,YAAY,IAAM,CACtC,IAAMkB,EAAU,KAAK,OAAO,eAAe,OAAO,EAClD,GAAI,CAACA,GAAW,CAAC,KAAK,WAAY,OAElC,IAAMC,EAAY,KAAK,IAAI,EAAG,KAAK,OAAO,KAAK,WAAa,KAAK,IAAI,GAAK,GAAI,CAAC,EAC/ED,EAAQ,YAAcC,EAAY,EAAI,KAAK,aAAaA,CAAS,EAAI,GAEjEA,GAAa,IACf,cAAc,KAAK,cAAe,EAClC,KAAK,eAAiB,KAClB,KAAK,UAAY,UACnB,KAAK,oBAAoB,UAAW,MAAS,EAGnD,EAAG,GAAI,CACT,CAEQ,aAAaC,EAA0B,CAC7C,IAAMC,EAAID,IAAY,KAAK,WACvB,KAAK,IAAI,EAAG,KAAK,OAAO,KAAK,WAAa,KAAK,IAAI,GAAK,GAAI,CAAC,EAC7D,GACEE,EAAO,KAAK,MAAMD,EAAI,EAAE,EACxBE,EAAOF,EAAI,GACjB,MAAO,cAAcC,CAAI,IAAIC,EAAO,GAAK,IAAM,EAAE,GAAGA,CAAI,EAC1D,CAKQ,YAAYC,EAAoB,CACtC,IAAMC,EAAS,KAAK,UAAY,SAC5B,KAAK,OAAO,eAAe,aAAa,EACxC,KAAK,OAAO,eAAe,YAAY,EAEvCA,IAAQA,EAAO,UAAYD,EACjC,CAEQ,cAAqB,CACvB,KAAK,iBACP,cAAc,KAAK,cAAc,EACjC,KAAK,eAAiB,MAEpB,KAAK,eACP,aAAa,KAAK,YAAY,EAC9B,KAAK,aAAe,KAExB,CACF,EAEA,eAAe,OAAO,mBAAoB5B,CAAe,ECtzBzD,IAAM8B,EAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAoBbC,EAAY;AAAA;AAAA,QAKZC,EAAc;AAAA;AAAA;AAAA,QAqDPC,EAAN,cAA0BC,CAAc,CAAxC,kCAKLC,EAAA,KAAQ,aAA4B,MACpCA,EAAA,KAAQ,gBAA+B,MACvCA,EAAA,KAAQ,UAAuB,QAC/BA,EAAA,KAAQ,aAA4B,MACpCA,EAAA,KAAQ,iBAAwD,MAChEA,EAAA,KAAQ,eAAqD,MAT7D,WAAoB,oBAA+B,CACjD,MAAO,CAAC,GAAG,MAAM,mBAAoB,gBAAiB,SAAU,YAAY,CAC9E,CASA,IAAI,cAA8B,CAAE,OAAO,KAAK,aAAa,eAAe,CAAG,CAC/E,IAAI,QAAiB,CAAE,OAAO,KAAK,aAAa,QAAQ,GAAK,UAAY,CACzE,IAAI,WAAqB,CAAE,OAAO,KAAK,aAAa,YAAY,CAAG,CAI1D,mBAA0B,CACjC,MAAM,kBAAkB,EACpB,KAAK,WACP,KAAK,WAAW,CAEpB,CAES,sBAA6B,CACpC,MAAM,qBAAqB,EAC3B,KAAK,aAAa,CACpB,CAIU,QAAe,CACvB,KAAK,OAAO,UAAY;AAAA;AAAA,UAElB,KAAK,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAiEpB,KAAK,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,YAKlB,KAAK,eAAe,MAAM,CAAC;AAAA;AAAA;AAAA,MAMnC,KAAK,gBAAgB,CACvB,CAIQ,eAAeC,EAAqBC,EAIjC,CACT,IAAMC,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA,MAOlB,OAAQF,EAAQ,CACd,IAAK,OACH,MAAO;AAAA,YACHE,CAAS;AAAA;AAAA,sCAEiBR,CAAU;AAAA;AAAA;AAAA;AAAA;AAAA,qCAKXA,CAAU;AAAA;AAAA;AAAA,UAKzC,IAAK,UACH,MAAO;AAAA,YACHQ,CAAS;AAAA;AAAA,wCAEmBN,CAAW;AAAA;AAAA;AAAA,UAK7C,IAAK,UACL,IAAK,UAAW,CACd,IAAMO,EAAaF,GAAM,MACrB,iEAAiE,mBAAmBA,EAAK,KAAK,CAAC,GAC/F,GAEEG,EAAYJ,IAAW,UAG7B,MAAO;AAAA,YACHE,CAAS;AAAA;AAAA;AAAA,iCAHKE,EAAY,UAAY,EAMR;AAAA,wBAClBD,CAAU;AAAA,oCACET,CAAU;AAAA,cAChCU,EAAY,4DAA8DT,EAAY,gBAAkB,EAAE;AAAA;AAAA,oCAEpFS,EAAY,iBAAmB,EAAE;AAAA,cACvDA,EACE,mDACA,qBAAqB;AAAA;AAAA,0CAEKH,GAAM,WAAa,EAAE;AAAA;AAAA,SAGzD,CAEA,IAAK,WACH,MAAO;AAAA;AAAA,wCAEyBN,CAAS;AAAA;AAAA;AAAA;AAAA,UAM3C,IAAK,SACH,MAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAST,IAAK,UACH,MAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAST,IAAK,QACH,MAAO;AAAA;AAAA;AAAA;AAAA,kCAImBM,GAAM,cAAgB,+BAA+B;AAAA;AAAA;AAAA,UAKjF,QACE,MAAO,EACX,CACF,CAIQ,eAAwB,CAC9B,MAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KA2LT,CAIQ,iBAAwB,CAC9B,KAAK,OAAO,eAAe,WAAW,GAAG,iBAAiB,QAAS,IAAM,KAAK,WAAW,CAAC,EAC1F,KAAK,OAAO,eAAe,WAAW,GAAG,iBAAiB,QAAS,IAAM,KAAK,OAAO,CAAC,CACxF,CAIA,MAAc,YAA4B,CACxC,KAAK,QAAU,UACf,KAAK,YAAY,KAAK,eAAe,SAAS,CAAC,EAE/C,GAAI,CACF,KAAK,cAAgB,MAAM,KAAK,qBAAqB,EACrD,IAAMI,EAAY,MAAM,KAAK,qBAAqB,KAAK,aAAa,EAE9DC,EAAkC,CACtC,eAAgB,mBAChB,cAAe,KAAK,MACtB,EAEMC,EAAgC,CACpC,OAAQ,KAAK,OAAO,MAAM,KAAK,EAAE,OAAO,OAAO,EAC/C,cAAeF,EACf,oBAAqB,MACvB,EAEMG,EAAM,MAAM,MAAM,GAAG,KAAK,OAAO,wBAAyB,CAC9D,OAAQ,OACR,QAAAF,EACA,KAAM,KAAK,UAAUC,CAAI,CAC3B,CAAC,EAED,GAAI,CAACC,EAAI,GAAI,CACX,IAAMC,EAAM,MAAMD,EAAI,KAAK,EAAE,MAAM,KAAO,CAAE,QAAS,0BAA2B,EAAE,EAClF,MAAM,IAAI,MAAOC,EAA6B,SAAW,0BAA0B,CACrF,CAEA,IAAMC,EAAU,MAAMF,EAAI,KAAK,EAC/B,KAAK,WAAaE,EAAQ,UAC1B,KAAK,WAAa,IAAI,KAAKA,EAAQ,SAAS,EAAE,QAAQ,EACtD,KAAK,QAAU,UAEf,KAAK,YAAY,KAAK,eAAe,UAAW,CAAE,MAAOA,EAAQ,KAAM,CAAC,CAAC,EACzE,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,cAAcA,EAAQ,SAAS,CAEtC,OAASD,EAAK,CACZ,IAAME,EAAUF,aAAe,MAAQA,EAAI,QAAU,mBACrD,KAAK,QAAU,QACf,KAAK,YAAY,KAAK,eAAe,QAAS,CAAE,aAAcE,CAAQ,CAAC,CAAC,EACxE,KAAK,gBAAgB,EACrB,KAAK,KAAK,eAAgB,CAAE,QAAAA,CAAQ,CAAC,CACvC,CACF,CAEQ,QAAe,CACrB,KAAK,aAAa,EAClB,KAAK,WAAa,KAClB,KAAK,cAAgB,KACrB,KAAK,QAAU,OACf,KAAK,WAAa,KAElB,KAAK,YAAY,KAAK,eAAe,MAAM,CAAC,EAC5C,KAAK,gBAAgB,CACvB,CAIQ,cAAcC,EAAyB,CAC7C,IAAIC,EAAqB,UACrBC,EAAW,IACTC,EAAc,IAChBC,EAAa,EACXC,EAAY,GAEZC,EAAO,SAAY,CACvB,GAAI,CAAC,KAAK,WAAY,OAEtB,IAAIC,EAAM,GAAG,KAAK,OAAO,yBAAyBP,CAAS,GACvD,KAAK,gBACPO,GAAO,kBAAkB,mBAAmB,KAAK,aAAa,CAAC,IAGjE,GAAI,CAIF,IAAMlB,EAAO,MAHD,MAAM,MAAMkB,EAAK,CAC3B,QAAS,CAAE,cAAe,KAAK,MAAO,CACxC,CAAC,GACsB,KAAK,EAI5B,GAHAH,EAAa,EACbF,EAAW,IAEPb,EAAK,SAAWY,IAClBA,EAAaZ,EAAK,OAClB,KAAK,oBAAoBA,EAAK,OAAQA,CAAI,EAEtC,CAAC,WAAY,SAAU,SAAS,EAAE,SAASA,EAAK,MAAM,GACxD,OAIJ,KAAK,aAAe,WAAWiB,EAAMJ,CAAQ,CAE/C,MAAQ,CAEN,GADAE,IACIA,GAAcC,EAAW,CAC3B,KAAK,oBAAoB,UAAW,MAAS,EAC7C,MACF,CACAH,EAAW,KAAK,IAAIA,EAAW,IAAKC,CAAW,EAC/C,KAAK,aAAe,WAAWG,EAAMJ,CAAQ,CAC/C,CACF,EAEA,KAAK,aAAe,WAAWI,EAAMJ,CAAQ,CAC/C,CAIQ,oBAAoBd,EAAqBC,EAA2B,CAG1E,OAFA,KAAK,QAAUD,EAEPA,EAAQ,CACd,IAAK,UAAW,CAEd,IAAMoB,EAAe,KAAK,OAAO,cAAgC,eAAe,GAAG,IAC7EC,EAAWD,EACb,mBAAmBA,EAAa,QAAQ,cAAe,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC,CAAC,EACxE,GAEJ,KAAK,YAAY,KAAK,eAAe,UAAW,CAC9C,MAAOC,EACP,UAAW,KAAK,aAAa,CAC/B,CAAC,CAAC,EACF,KAAK,gBAAgB,EACrB,KACF,CAEA,IAAK,WAAY,CACf,KAAK,aAAa,EAClB,KAAK,YAAY,KAAK,eAAe,UAAU,CAAC,EAChD,KAAK,KAAK,kBAAmB,CAC3B,UAAWpB,GAAM,UACjB,KAAMA,GAAM,KACZ,UAAWA,GAAM,SACnB,CAAC,EACD,KACF,CAEA,IAAK,SACH,KAAK,aAAa,EAClB,KAAK,YAAY,KAAK,eAAe,QAAQ,CAAC,EAC9C,KAAK,gBAAgB,EACrB,KAAK,KAAK,eAAe,EACzB,MAEF,IAAK,UACH,KAAK,aAAa,EAClB,KAAK,QAAU,UACf,KAAK,YAAY,KAAK,eAAe,SAAS,CAAC,EAC/C,KAAK,gBAAgB,EACrB,KAAK,KAAK,gBAAgB,EAC1B,KACJ,CACF,CAIQ,iBAAwB,CAC9B,KAAK,eAAiB,YAAY,IAAM,CACtC,IAAMqB,EAAU,KAAK,OAAO,eAAe,OAAO,EAClD,GAAI,CAACA,GAAW,CAAC,KAAK,WAAY,OAElC,IAAMC,EAAY,KAAK,IAAI,EAAG,KAAK,OAAO,KAAK,WAAa,KAAK,IAAI,GAAK,GAAI,CAAC,EAC/ED,EAAQ,YAAcC,EAAY,EAAI,KAAK,aAAaA,CAAS,EAAI,GAEjEA,GAAa,IACf,cAAc,KAAK,cAAe,EAClC,KAAK,eAAiB,MAClB,KAAK,UAAY,WAAa,KAAK,UAAY,YACjD,KAAK,oBAAoB,UAAW,MAAS,EAGnD,EAAG,GAAI,CACT,CAEQ,aAAaC,EAA0B,CAC7C,IAAMC,EAAID,IAAY,KAAK,WACvB,KAAK,IAAI,EAAG,KAAK,OAAO,KAAK,WAAa,KAAK,IAAI,GAAK,GAAI,CAAC,EAC7D,GACEE,EAAO,KAAK,MAAMD,EAAI,EAAE,EACxBE,EAAOF,EAAI,GACjB,MAAO,cAAcC,CAAI,IAAIC,EAAO,GAAK,IAAM,EAAE,GAAGA,CAAI,EAC1D,CAIQ,YAAYC,EAAoB,CACtC,IAAMC,EAAS,KAAK,OAAO,eAAe,MAAM,EAC5CA,IAAQA,EAAO,UAAYD,EACjC,CAEQ,cAAqB,CACvB,KAAK,iBACP,cAAc,KAAK,cAAc,EACjC,KAAK,eAAiB,MAEpB,KAAK,eACP,aAAa,KAAK,YAAY,EAC9B,KAAK,aAAe,KAExB,CACF,EAEA,eAAe,OAAO,aAAc/B,CAAW",
|
|
6
|
-
"names": ["index_exports", "__export", "QRAuthElement", "QRAuthEphemeral", "QRAuthLogin", "QRAuthTwoFA", "QRAuthElement", "__publicField", "val", "_name", "oldValue", "newValue", "url", "event", "callback", "e", "msg", "intervalMs", "headers", "poll", "res", "name", "detail", "bytes", "verifier", "data", "hash", "binString", "byte", "SHIELD_SVG", "CHECK_SVG", "SPINNER_SVG", "QRAuthLogin", "QRAuthElement", "__publicField", "status", "data", "qrImageUrl", "isScanned", "overlay", "e", "challenge", "headers", "body", "res", "err", "session", "message", "inlineBody", "sessionId", "lastStatus", "interval", "maxInterval", "errorCount", "maxErrors", "poll", "url", "currentQrUrl", "rawQrUrl", "callbackName", "globalCallback", "timerEl", "remaining", "seconds", "s", "mins", "secs", "html", "bodyEl", "SHIELD_SVG", "CHECK_SVG", "SPINNER_SVG", "QRAuthEphemeral", "QRAuthElement", "__publicField", "status", "data", "qrImageUrl", "useCount", "maxUses", "useBadge", "overlay", "e", "res", "err", "session", "message", "inlineBody", "sessionId", "interval", "maxInterval", "errorCount", "maxErrors", "poll", "timerEl", "remaining", "seconds", "s", "mins", "secs", "html", "bodyEl", "SHIELD_SVG", "CHECK_SVG", "SPINNER_SVG", "QRAuthTwoFA", "QRAuthElement", "__publicField", "status", "data", "stepLabel", "qrImageUrl", "isScanned", "challenge", "headers", "body", "res", "err", "session", "message", "sessionId", "lastStatus", "interval", "maxInterval", "errorCount", "maxErrors", "poll", "url", "currentQrUrl", "rawQrUrl", "timerEl", "remaining", "seconds", "s", "mins", "secs", "html", "bodyEl"]
|
|
7
|
-
}
|