@qrauth/web-components 0.1.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/dist/base.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * QRAuthElement — base class for all QRAuth Web Components.
3
+ * Provides: Shadow DOM setup, CSS custom property theming, SSE connection management,
4
+ * lifecycle hooks, and event dispatching.
5
+ */
6
+ export declare abstract class QRAuthElement extends HTMLElement {
7
+ protected shadow: ShadowRoot;
8
+ private _sseConnection;
9
+ private _pollInterval;
10
+ static get observedAttributes(): string[];
11
+ constructor();
12
+ get tenant(): string;
13
+ get theme(): string;
14
+ get baseUrl(): string;
15
+ connectedCallback(): void;
16
+ disconnectedCallback(): void;
17
+ attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void;
18
+ protected abstract render(): void;
19
+ protected connectSSE(url: string): void;
20
+ protected onSSEEvent(event: string, callback: (data: unknown) => void): void;
21
+ protected disconnectSSE(): void;
22
+ protected startPolling(url: string, intervalMs: number, callback: (data: unknown) => void, headers?: Record<string, string>): void;
23
+ protected stopPolling(): void;
24
+ protected cleanup(): void;
25
+ protected emit(name: string, detail?: unknown): void;
26
+ protected generateCodeVerifier(): Promise<string>;
27
+ protected computeCodeChallenge(verifier: string): Promise<string>;
28
+ private _base64url;
29
+ protected getBaseStyles(): string;
30
+ }
package/dist/base.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * QRAuthElement — base class for all QRAuth Web Components.
3
+ * Provides: Shadow DOM setup, CSS custom property theming, SSE connection management,
4
+ * lifecycle hooks, and event dispatching.
5
+ */
6
+ export class QRAuthElement extends HTMLElement {
7
+ shadow;
8
+ _sseConnection = null;
9
+ _pollInterval = null;
10
+ static get observedAttributes() {
11
+ return ['tenant', 'theme', 'base-url'];
12
+ }
13
+ constructor() {
14
+ super();
15
+ this.shadow = this.attachShadow({ mode: 'open' });
16
+ }
17
+ // Getters for common attributes
18
+ get tenant() { return this.getAttribute('tenant') || ''; }
19
+ get theme() { return this.getAttribute('theme') || 'light'; }
20
+ get baseUrl() {
21
+ const val = this.getAttribute('base-url');
22
+ // Allow empty string for local dev (relative URLs), fallback to production
23
+ return val !== null ? val : 'https://qrauth.io';
24
+ }
25
+ connectedCallback() {
26
+ this.render();
27
+ }
28
+ disconnectedCallback() {
29
+ this.cleanup();
30
+ }
31
+ attributeChangedCallback(_name, oldValue, newValue) {
32
+ if (oldValue !== newValue) {
33
+ this.render();
34
+ }
35
+ }
36
+ // --- SSE Management ---
37
+ connectSSE(url) {
38
+ this.disconnectSSE();
39
+ // EventSource doesn't support custom headers natively.
40
+ // For public (unauthenticated) SSE endpoints this works directly.
41
+ this._sseConnection = new EventSource(url);
42
+ this._sseConnection.onerror = () => {
43
+ // Fall back to polling on SSE failure — caller should initiate polling
44
+ this.disconnectSSE();
45
+ };
46
+ }
47
+ onSSEEvent(event, callback) {
48
+ if (this._sseConnection) {
49
+ this._sseConnection.addEventListener(event, (e) => {
50
+ const msg = e;
51
+ try {
52
+ callback(JSON.parse(msg.data));
53
+ }
54
+ catch { /* invalid JSON */ }
55
+ });
56
+ }
57
+ }
58
+ disconnectSSE() {
59
+ if (this._sseConnection) {
60
+ this._sseConnection.close();
61
+ this._sseConnection = null;
62
+ }
63
+ }
64
+ // --- Polling (fallback / primary for authenticated endpoints) ---
65
+ startPolling(url, intervalMs, callback, headers) {
66
+ this.stopPolling();
67
+ const poll = async () => {
68
+ try {
69
+ const res = await fetch(url, { headers });
70
+ if (res.ok)
71
+ callback(await res.json());
72
+ }
73
+ catch { /* network error — retry next interval */ }
74
+ };
75
+ poll(); // immediate first poll
76
+ this._pollInterval = setInterval(poll, intervalMs);
77
+ }
78
+ stopPolling() {
79
+ if (this._pollInterval) {
80
+ clearInterval(this._pollInterval);
81
+ this._pollInterval = null;
82
+ }
83
+ }
84
+ // --- Cleanup ---
85
+ cleanup() {
86
+ this.disconnectSSE();
87
+ this.stopPolling();
88
+ }
89
+ // --- Event dispatch ---
90
+ emit(name, detail) {
91
+ this.dispatchEvent(new CustomEvent(name, {
92
+ bubbles: true,
93
+ composed: true, // crosses shadow DOM boundary
94
+ detail,
95
+ }));
96
+ }
97
+ // --- PKCE helpers ---
98
+ async generateCodeVerifier() {
99
+ const bytes = new Uint8Array(32);
100
+ crypto.getRandomValues(bytes);
101
+ return this._base64url(bytes);
102
+ }
103
+ async computeCodeChallenge(verifier) {
104
+ const encoder = new TextEncoder();
105
+ const data = encoder.encode(verifier);
106
+ const hash = await crypto.subtle.digest('SHA-256', data);
107
+ return this._base64url(new Uint8Array(hash));
108
+ }
109
+ _base64url(bytes) {
110
+ const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join('');
111
+ return btoa(binString).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
112
+ }
113
+ // --- Shared base styles (host-level theming via CSS custom properties) ---
114
+ getBaseStyles() {
115
+ return `
116
+ :host {
117
+ display: inline-block;
118
+ font-family: var(--qrauth-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
119
+ --_primary: var(--qrauth-primary, #00a76f);
120
+ --_primary-dark: var(--qrauth-primary-dark, #007a52);
121
+ --_text: var(--qrauth-text, #1a1a2e);
122
+ --_text-muted: var(--qrauth-text-muted, #637381);
123
+ --_bg: var(--qrauth-bg, #ffffff);
124
+ --_surface: var(--qrauth-surface, #f9fafb);
125
+ --_border: var(--qrauth-border, #e0e0e0);
126
+ --_radius: var(--qrauth-radius, 12px);
127
+ --_shadow: var(--qrauth-shadow, 0 24px 48px rgba(0,0,0,0.15));
128
+ --_btn-bg: var(--qrauth-btn-bg, #1b2a4a);
129
+ --_btn-hover: var(--qrauth-btn-hover, #263b66);
130
+ --_success: #00a76f;
131
+ --_error: #ff5630;
132
+ --_warning: #ffab00;
133
+ --_disabled: #919eab;
134
+ }
135
+ :host([theme="dark"]) {
136
+ --_text: var(--qrauth-text, #f0f0f0);
137
+ --_text-muted: var(--qrauth-text-muted, #919eab);
138
+ --_bg: var(--qrauth-bg, #1a1a2e);
139
+ --_surface: var(--qrauth-surface, #242436);
140
+ --_border: var(--qrauth-border, rgba(255,255,255,0.12));
141
+ --_shadow: var(--qrauth-shadow, 0 24px 48px rgba(0,0,0,0.5));
142
+ --_btn-bg: var(--qrauth-btn-bg, #263b66);
143
+ --_btn-hover: var(--qrauth-btn-hover, #2e4578);
144
+ }
145
+ * { box-sizing: border-box; margin: 0; padding: 0; }
146
+ `;
147
+ }
148
+ }
@@ -0,0 +1,61 @@
1
+ import { QRAuthElement } from './base.js';
2
+ /**
3
+ * <qrauth-ephemeral> — Displays an ephemeral access QR code with TTL countdown
4
+ * and claim status updates.
5
+ *
6
+ * Attributes:
7
+ * tenant — Your QRAuth App client ID (required)
8
+ * theme — "light" (default) | "dark"
9
+ * base-url — API base URL (default: https://qrauth.io)
10
+ * scopes — Space-separated scope strings (default: "access")
11
+ * ttl — TTL string like "30m", "6h", "1d" (default: "30m")
12
+ * max-uses — Maximum number of claims (default: "1")
13
+ * device-binding — Boolean attribute; present = device binding enabled
14
+ * display — "button" | "inline" (default: "inline")
15
+ *
16
+ * Events (bubbles + composed, cross shadow-DOM):
17
+ * qrauth:claimed — detail: { sessionId, status, scopes, metadata, useCount, maxUses }
18
+ * qrauth:expired — fired when session times out
19
+ * qrauth:revoked — fired when session is revoked
20
+ * qrauth:error — detail: { message }
21
+ *
22
+ * @example
23
+ * <qrauth-ephemeral tenant="qrauth_app_xxx" scopes="read:menu write:order" ttl="30m">
24
+ * </qrauth-ephemeral>
25
+ */
26
+ export declare class QRAuthEphemeral extends QRAuthElement {
27
+ static get observedAttributes(): string[];
28
+ private _sessionId;
29
+ private _claimUrl;
30
+ private _status;
31
+ private _expiresAt;
32
+ private _timerInterval;
33
+ private _pollTimeout;
34
+ get scopes(): string;
35
+ get ttl(): string;
36
+ get maxUses(): number;
37
+ get deviceBinding(): boolean;
38
+ get display(): string;
39
+ disconnectedCallback(): void;
40
+ protected render(): void;
41
+ private _renderButton;
42
+ private _renderInline;
43
+ private _inlineIdleContent;
44
+ private _modalContent;
45
+ private _bodyForStatus;
46
+ private _sharedModalStyles;
47
+ private _bindButtonEvents;
48
+ private _bindInlineEvents;
49
+ private _bindModalButtons;
50
+ private _openModal;
51
+ private _closeModal;
52
+ private _startSession;
53
+ private _retry;
54
+ private _beginPolling;
55
+ private _handleStatusChange;
56
+ private _startCountdown;
57
+ private _formatTimer;
58
+ /** Update just the inner body content without re-rendering the whole component */
59
+ private _updateBody;
60
+ private _clearTimers;
61
+ }