@lumiapassport/ui-kit 1.10.0 → 1.11.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.
@@ -0,0 +1,160 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>X (Twitter) Login - Lumia Passport</title>
7
+
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
17
+ background: linear-gradient(135deg, #1DA1F2 0%, #14171A 100%);
18
+ color: #333;
19
+ min-height: 100vh;
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ padding: 2rem;
24
+ }
25
+
26
+ .container {
27
+ background: white;
28
+ border-radius: 16px;
29
+ padding: 3rem 2rem;
30
+ max-width: 450px;
31
+ width: 100%;
32
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
33
+ text-align: center;
34
+ }
35
+
36
+ .logo {
37
+ width: 80px;
38
+ height: 80px;
39
+ margin: 0 auto 1.5rem;
40
+ background: linear-gradient(135deg, #1DA1F2 0%, #14171A 100%);
41
+ border-radius: 50%;
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ font-size: 2.5rem;
46
+ color: white;
47
+ font-weight: bold;
48
+ }
49
+
50
+ h1 {
51
+ font-size: 1.75rem;
52
+ margin-bottom: 0.5rem;
53
+ color: #333;
54
+ }
55
+
56
+ p {
57
+ color: #666;
58
+ margin-bottom: 2rem;
59
+ line-height: 1.5;
60
+ }
61
+
62
+ #content {
63
+ min-height: 120px;
64
+ display: flex;
65
+ flex-direction: column;
66
+ justify-content: center;
67
+ align-items: center;
68
+ gap: 1rem;
69
+ margin: 2rem 0;
70
+ }
71
+
72
+ .loading {
73
+ display: flex;
74
+ flex-direction: column;
75
+ align-items: center;
76
+ gap: 1rem;
77
+ }
78
+
79
+ .spinner {
80
+ width: 40px;
81
+ height: 40px;
82
+ border: 4px solid rgba(29, 161, 242, 0.3);
83
+ border-top-color: #1DA1F2;
84
+ border-radius: 50%;
85
+ animation: spin 1s linear infinite;
86
+ }
87
+
88
+ @keyframes spin {
89
+ to { transform: rotate(360deg); }
90
+ }
91
+
92
+ .error {
93
+ background: #fee;
94
+ border: 1px solid #fcc;
95
+ border-radius: 8px;
96
+ padding: 1rem;
97
+ color: #c33;
98
+ margin-top: 1rem;
99
+ }
100
+
101
+ .success {
102
+ background: #d1fae5;
103
+ border: 1px solid #6ee7b7;
104
+ border-radius: 8px;
105
+ padding: 1rem;
106
+ color: #065f46;
107
+ margin-top: 1rem;
108
+ }
109
+
110
+ .footer {
111
+ margin-top: 2rem;
112
+ padding-top: 1.5rem;
113
+ border-top: 1px solid #e0e0e0;
114
+ font-size: 0.875rem;
115
+ color: #999;
116
+ }
117
+
118
+ .retry-button {
119
+ background: #1DA1F2;
120
+ color: white;
121
+ border: none;
122
+ padding: 0.75rem 1.5rem;
123
+ border-radius: 8px;
124
+ font-size: 1rem;
125
+ cursor: pointer;
126
+ margin-top: 1rem;
127
+ transition: background 0.2s;
128
+ }
129
+
130
+ .retry-button:hover {
131
+ background: #1991DB;
132
+ }
133
+
134
+ .retry-button:disabled {
135
+ background: #ccc;
136
+ cursor: not-allowed;
137
+ }
138
+ </style>
139
+ </head>
140
+ <body>
141
+ <div class="container">
142
+ <div class="logo">𝕏</div>
143
+ <h1>Sign in with X</h1>
144
+ <p>Redirecting to X (Twitter) for authentication...</p>
145
+
146
+ <div id="content">
147
+ <div class="loading">
148
+ <div class="spinner"></div>
149
+ <p>Initializing OAuth flow...</p>
150
+ </div>
151
+ </div>
152
+
153
+ <div class="footer">
154
+ <p>You can close this window after authentication</p>
155
+ </div>
156
+ </div>
157
+
158
+ <script src="./x.js"></script>
159
+ </body>
160
+ </html>
@@ -0,0 +1,378 @@
1
+ /**
2
+ * X (Twitter) OAuth Popup Script
3
+ * Handles OAuth 2.0 with PKCE flow for X authentication
4
+ */
5
+
6
+ // Get configuration from URL parameters
7
+ const urlParams = new URLSearchParams(window.location.search);
8
+
9
+ // TSS_URL and PROJECT_ID are build-time constants, passed via URL on initial load
10
+ // After backend redirect, we use stored values from localStorage
11
+ const TSS_URL = urlParams.get('tssUrl') || localStorage.getItem('x_oauth_tssUrl') || (typeof __LUMIA_TSS_URL__ !== 'undefined' ? __LUMIA_TSS_URL__ : null);
12
+ const MODE = urlParams.get('mode') || localStorage.getItem('x_oauth_mode') || 'login';
13
+ const PROJECT_ID = urlParams.get('projectId') || localStorage.getItem('x_oauth_projectId') || (typeof window !== 'undefined' && window.__LUMIA_PROJECT_ID__) || null;
14
+
15
+ const STATE = urlParams.get('state');
16
+ const CODE = urlParams.get('code');
17
+ const ERROR_PARAM = urlParams.get('error');
18
+ const SUCCESS = urlParams.get('success'); // Backend redirected with success=true
19
+
20
+ console.log('[X OAuth] Initializing with:', { TSS_URL, MODE, PROJECT_ID, STATE, CODE, ERROR_PARAM, SUCCESS });
21
+
22
+ const contentEl = document.getElementById('content');
23
+
24
+ // Track if we've successfully sent auth result to parent
25
+ let authResultSent = false;
26
+ // Track if we're redirecting to X OAuth (to prevent cancellation on redirect)
27
+ let isRedirectingToProvider = false;
28
+
29
+ function showLoading(message) {
30
+ if (contentEl) {
31
+ contentEl.innerHTML = `
32
+ <div class="loading">
33
+ <div class="spinner"></div>
34
+ <p>${message}</p>
35
+ </div>
36
+ `;
37
+ }
38
+ }
39
+
40
+ function showError(message, allowRetry = true) {
41
+ console.error('[X OAuth] Error:', message);
42
+
43
+ if (contentEl) {
44
+ contentEl.innerHTML = `
45
+ <div class="error">
46
+ <strong>Error:</strong> ${message}
47
+ </div>
48
+ ${allowRetry ? '<button class="retry-button" onclick="window.location.reload()">Retry</button>' : ''}
49
+ `;
50
+ }
51
+
52
+ // Send error to opener
53
+ if (window.opener) {
54
+ window.opener.postMessage({
55
+ type: 'X_AUTH_ERROR',
56
+ provider: 'x',
57
+ error: message
58
+ }, '*');
59
+ }
60
+ }
61
+
62
+ function showSuccess(message) {
63
+ if (contentEl) {
64
+ contentEl.innerHTML = `
65
+ <div class="success">
66
+ ✓ ${message}<br>
67
+ <small style="color: #666;">Closing window...</small>
68
+ </div>
69
+ `;
70
+ }
71
+ }
72
+
73
+ async function startOAuthFlow() {
74
+ if (!TSS_URL) {
75
+ showError('TSS URL not configured. Missing tssUrl parameter.', false);
76
+ return;
77
+ }
78
+
79
+ try {
80
+ showLoading('Starting X OAuth flow...');
81
+
82
+ // Determine the correct endpoint based on mode
83
+ const endpoint = MODE === 'link'
84
+ ? `${TSS_URL}/api/auth/link/x/start`
85
+ : `${TSS_URL}/api/auth/x/start`;
86
+
87
+ // Add projectId to endpoint if available
88
+ const fullEndpoint = PROJECT_ID
89
+ ? `${endpoint}?projectId=${encodeURIComponent(PROJECT_ID)}`
90
+ : endpoint;
91
+
92
+ console.log('[X OAuth] Starting flow with config:', { TSS_URL, MODE, PROJECT_ID, endpoint: fullEndpoint });
93
+ console.log('[X OAuth] Making request to:', fullEndpoint);
94
+
95
+ // Call backend to get authorization URL
96
+ const response = await fetch(fullEndpoint, {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Content-Type': 'application/json',
100
+ ...(MODE === 'link' ? {
101
+ 'Authorization': `Bearer ${urlParams.get('token')}`
102
+ } : {})
103
+ },
104
+ credentials: 'include'
105
+ });
106
+
107
+ if (!response.ok) {
108
+ const errorText = await response.text().catch(() => '');
109
+ let errorData;
110
+ try {
111
+ errorData = JSON.parse(errorText);
112
+ } catch {
113
+ errorData = { message: errorText };
114
+ }
115
+ console.error('[X OAuth] Backend error response:', { status: response.status, errorData, errorText });
116
+ throw new Error(errorData.message || errorText || `Failed to start OAuth: ${response.statusText}`);
117
+ }
118
+
119
+ const data = await response.json();
120
+ console.log('[X OAuth] Backend response:', data);
121
+
122
+ // Backend may return 'url' or 'authorizationUrl'
123
+ const authUrl = data.authorizationUrl || data.url;
124
+ console.log('[X OAuth] Authorization URL:', authUrl);
125
+
126
+ if (!authUrl) {
127
+ throw new Error('No authorization URL returned from server');
128
+ }
129
+
130
+ // Store state and config in localStorage (persists across redirects)
131
+ if (data.state) {
132
+ localStorage.setItem('x_oauth_state', data.state);
133
+ }
134
+ localStorage.setItem('x_oauth_mode', MODE);
135
+ localStorage.setItem('x_oauth_tssUrl', TSS_URL);
136
+ if (PROJECT_ID) {
137
+ localStorage.setItem('x_oauth_projectId', PROJECT_ID);
138
+ }
139
+
140
+ // Redirect to X OAuth page
141
+ showLoading('Redirecting to X...');
142
+ isRedirectingToProvider = true; // Prevent cancellation message on redirect
143
+ window.location.href = authUrl;
144
+
145
+ } catch (error) {
146
+ console.error('[X OAuth] Start flow error:', error);
147
+ const errorMessage = error.message || 'Failed to start OAuth flow';
148
+ console.error('[X OAuth] Error details:', { error, MODE, TSS_URL, PROJECT_ID });
149
+ showError(errorMessage);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Handle successful OAuth after backend redirect
155
+ * Backend processes callback and redirects back with success=true
156
+ */
157
+ async function handleBackendSuccess() {
158
+ try {
159
+ showLoading('Completing authentication...');
160
+
161
+ console.log('[X OAuth] Backend redirected with success, verifying session...');
162
+ console.log('[X OAuth] Using config:', { TSS_URL, PROJECT_ID, MODE });
163
+
164
+ // Validate required parameters
165
+ if (!TSS_URL) {
166
+ throw new Error('Missing TSS URL. Check build-time configuration.');
167
+ }
168
+
169
+ // Verify the session was created by checking auth endpoint
170
+ const verifyEndpoint = PROJECT_ID
171
+ ? `${TSS_URL}/api/auth/verify?projectId=${encodeURIComponent(PROJECT_ID)}`
172
+ : `${TSS_URL}/api/auth/verify`;
173
+
174
+ const verifyResponse = await fetch(verifyEndpoint, {
175
+ method: 'GET',
176
+ credentials: 'include',
177
+ });
178
+
179
+ if (!verifyResponse.ok) {
180
+ console.error('[X OAuth] Verify failed:', verifyResponse.status);
181
+ throw new Error('Failed to verify authentication. Session may not be created.');
182
+ }
183
+
184
+ const userData = await verifyResponse.json();
185
+ console.log('[X OAuth] Authentication verified:', userData);
186
+
187
+ // Send success to opener
188
+ if (window.opener) {
189
+ window.opener.postMessage({
190
+ type: 'X_AUTH_SUCCESS',
191
+ provider: 'x',
192
+ user: userData,
193
+ mode: MODE
194
+ }, '*');
195
+
196
+ // Mark that we've sent the auth result
197
+ authResultSent = true;
198
+
199
+ showSuccess(MODE === 'link' ? 'Account linked successfully!' : 'Authentication successful!');
200
+
201
+ // Clean up localStorage
202
+ localStorage.removeItem('x_oauth_state');
203
+ localStorage.removeItem('x_oauth_mode');
204
+ localStorage.removeItem('x_oauth_tssUrl');
205
+ localStorage.removeItem('x_oauth_projectId');
206
+
207
+ setTimeout(() => {
208
+ window.close();
209
+ }, 1500);
210
+ } else {
211
+ console.error('[X OAuth] No opener window found');
212
+ showError('No opener window found. Please close this window manually.');
213
+ }
214
+
215
+ } catch (error) {
216
+ console.error('[X OAuth] Backend success handler error:', error);
217
+ showError(error.message || 'Failed to complete authentication');
218
+
219
+ // Clean up localStorage on error
220
+ localStorage.removeItem('x_oauth_state');
221
+ localStorage.removeItem('x_oauth_mode');
222
+ localStorage.removeItem('x_oauth_tssUrl');
223
+ localStorage.removeItem('x_oauth_projectId');
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Handle OAuth callback (legacy, may not be used if backend handles redirect_uri)
229
+ */
230
+ async function handleCallback() {
231
+ try {
232
+ showLoading('Completing authentication...');
233
+
234
+ // Verify state matches
235
+ const savedState = localStorage.getItem('x_oauth_state');
236
+ const savedMode = localStorage.getItem('x_oauth_mode');
237
+
238
+ if (savedState && STATE !== savedState) {
239
+ throw new Error('Invalid state parameter. Possible CSRF attack.');
240
+ }
241
+
242
+ if (ERROR_PARAM === 'access_denied') {
243
+ console.warn('[X OAuth] User denied authorization during legacy callback');
244
+ redirectWithAccessDenied();
245
+ return;
246
+ }
247
+
248
+ if (ERROR_PARAM) {
249
+ throw new Error(`OAuth error: ${ERROR_PARAM}`);
250
+ }
251
+
252
+ if (!CODE) {
253
+ throw new Error('No authorization code received');
254
+ }
255
+
256
+ console.log('[X OAuth] Callback successful, mode:', savedMode);
257
+
258
+ // For login mode, the backend callback endpoint handles everything
259
+ // and redirects back with tokens. For link mode, we need to verify.
260
+ if (savedMode === 'login') {
261
+ // The callback endpoint should have set cookies/tokens
262
+ // Get user info to verify
263
+ const response = await fetch(`${TSS_URL}/api/auth/verify`, {
264
+ credentials: 'include'
265
+ });
266
+
267
+ if (!response.ok) {
268
+ throw new Error('Failed to verify authentication');
269
+ }
270
+
271
+ const userData = await response.json();
272
+ console.log('[X OAuth] User data:', userData);
273
+
274
+ // Send success to opener
275
+ if (window.opener) {
276
+ window.opener.postMessage({
277
+ type: 'X_AUTH_SUCCESS',
278
+ provider: 'x',
279
+ user: userData,
280
+ mode: 'login'
281
+ }, '*');
282
+
283
+ showSuccess('Authentication successful!');
284
+
285
+ setTimeout(() => {
286
+ window.close();
287
+ }, 1500);
288
+ } else {
289
+ showError('No opener window found');
290
+ }
291
+ } else {
292
+ // Link mode - backend has already linked the account
293
+ if (window.opener) {
294
+ window.opener.postMessage({
295
+ type: 'X_AUTH_SUCCESS',
296
+ provider: 'x',
297
+ mode: 'link'
298
+ }, '*');
299
+
300
+ showSuccess('Account linked successfully!');
301
+
302
+ setTimeout(() => {
303
+ window.close();
304
+ }, 1500);
305
+ } else {
306
+ showError('No opener window found');
307
+ }
308
+ }
309
+
310
+ } catch (error) {
311
+ console.error('[X OAuth] Callback error:', error);
312
+ showError(error.message || 'Failed to complete authentication');
313
+ }
314
+ }
315
+
316
+ // Determine if this is a callback or initial load
317
+ if (ERROR_PARAM) {
318
+ // Backend redirected with error
319
+ const errorMessage = decodeURIComponent(ERROR_PARAM);
320
+ console.error('[X OAuth] Backend returned error:', errorMessage);
321
+ showError(errorMessage);
322
+
323
+ // Send error to opener
324
+ if (window.opener) {
325
+ window.opener.postMessage({
326
+ type: 'X_AUTH_ERROR',
327
+ provider: 'x',
328
+ error: errorMessage
329
+ }, '*');
330
+ authResultSent = true; // Mark that we've sent result
331
+ }
332
+ } else if (SUCCESS === 'true') {
333
+ // Backend redirected back after successful OAuth
334
+ handleBackendSuccess();
335
+ } else if (CODE) {
336
+ // This is a callback from X (should not happen with backend redirect_uri)
337
+ console.warn('[X OAuth] Received code parameter, but backend should handle this');
338
+ handleCallback();
339
+ } else {
340
+ // This is initial load, start OAuth flow
341
+ if (document.readyState === 'loading') {
342
+ document.addEventListener('DOMContentLoaded', startOAuthFlow);
343
+ } else {
344
+ startOAuthFlow();
345
+ }
346
+ }
347
+
348
+ // Handle popup closing without authentication
349
+ window.addEventListener('beforeunload', () => {
350
+ // Only send cancellation if we haven't sent success/error AND not redirecting to provider
351
+ if (!authResultSent && !isRedirectingToProvider && window.opener) {
352
+ console.log('[X OAuth] Window closing without auth result, sending cancellation');
353
+ window.opener.postMessage({
354
+ type: 'X_AUTH_CANCELLED',
355
+ provider: 'x'
356
+ }, '*');
357
+ }
358
+ });
359
+ function redirectWithAccessDenied() {
360
+ const storedState = localStorage.getItem('x_oauth_state');
361
+ const storedMode = localStorage.getItem('x_oauth_mode') || MODE || 'login';
362
+
363
+ localStorage.removeItem('x_oauth_state');
364
+ localStorage.removeItem('x_oauth_mode');
365
+ localStorage.removeItem('x_oauth_tssUrl');
366
+ localStorage.removeItem('x_oauth_projectId');
367
+
368
+ const params = new URLSearchParams();
369
+ params.set('success', 'false');
370
+ params.set('error', 'ACCESS_DENIED');
371
+ params.set('mode', storedMode);
372
+ if (storedState) {
373
+ params.set('state', storedState);
374
+ }
375
+
376
+ const redirectUrl = `${window.location.origin}${window.location.pathname}?${params.toString()}`;
377
+ window.location.replace(redirectUrl);
378
+ }