@alter-ai/connect 0.1.1 → 0.2.1

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/README.md CHANGED
@@ -1,19 +1,21 @@
1
1
  # Alter Connect SDK
2
2
 
3
- A lightweight JavaScript SDK for embedding OAuth integrations into your application. Connect your users to Google, Slack, Microsoft, and more with just a few lines of code.
3
+ A lightweight JavaScript SDK for embedding OAuth integrations into your application. The SDK opens a backend-served Connect UI in a popup — the backend handles auth, provider selection, branding, and OAuth, then sends results back via postMessage.
4
4
 
5
- ## 🚀 Quick Start
5
+ **~10KB minified | Zero dependencies | TypeScript included**
6
+
7
+ ## Quick Start
6
8
 
7
9
  ### 1. Install
8
10
 
9
11
  ```bash
10
- npm install @alter-vault/connect
12
+ npm install @alter-ai/connect
11
13
  ```
12
14
 
13
15
  Or use via CDN:
14
16
 
15
17
  ```html
16
- <script src="https://cdn.jsdelivr.net/npm/@alter-vault/connect@latest/dist/alter-connect.umd.js"></script>
18
+ <script src="https://cdn.jsdelivr.net/npm/@alter-ai/connect@latest/dist/alter-connect.umd.js"></script>
17
19
  ```
18
20
 
19
21
  ### 2. Get a Session Token from Your Backend
@@ -31,9 +33,15 @@ const response = await fetch('https://api.alterai.dev/oauth/connect/session', {
31
33
  body: JSON.stringify({
32
34
  end_user: {
33
35
  id: 'user_123',
34
- email: 'user@example.com'
36
+ email: 'user@example.com',
37
+ name: 'John Doe'
38
+ },
39
+ attributes: {
40
+ user_id: 'user_123',
41
+ org_id: 'org_456'
35
42
  },
36
- allowed_providers: ['google', 'slack', 'microsoft']
43
+ allowed_providers: ['google', 'slack', 'microsoft'],
44
+ allowed_origin: 'https://yourapp.com'
37
45
  })
38
46
  });
39
47
 
@@ -43,9 +51,9 @@ const { session_token } = await response.json();
43
51
  ### 3. Open the Connect UI
44
52
 
45
53
  ```javascript
46
- import AlterConnect from '@alter-vault/connect';
54
+ import AlterConnect from '@alter-ai/connect';
47
55
 
48
- // Initialize SDK
56
+ // Initialize SDK (no API key needed!)
49
57
  const alterConnect = AlterConnect.create();
50
58
 
51
59
  // Get session token from YOUR backend
@@ -60,19 +68,22 @@ await alterConnect.open({
60
68
  },
61
69
  onError: (error) => {
62
70
  console.error('Failed:', error);
71
+ },
72
+ onExit: () => {
73
+ console.log('User closed the window');
63
74
  }
64
75
  });
65
76
  ```
66
77
 
67
- That's it! The SDK handles the OAuth flow, popup windows, and all security.
78
+ That's it! The SDK handles the OAuth flow, popup windows, mobile redirects, and all security.
68
79
 
69
- ## 📖 Framework Examples
80
+ ## Framework Examples
70
81
 
71
82
  ### React
72
83
 
73
84
  ```jsx
74
85
  import { useState } from 'react';
75
- import AlterConnect from '@alter-vault/connect';
86
+ import AlterConnect from '@alter-ai/connect';
76
87
 
77
88
  function ConnectButton() {
78
89
  const [alterConnect] = useState(() => AlterConnect.create());
@@ -102,7 +113,7 @@ function ConnectButton() {
102
113
 
103
114
  <script setup>
104
115
  import { ref, onMounted } from 'vue';
105
- import AlterConnect from '@alter-vault/connect';
116
+ import AlterConnect from '@alter-ai/connect';
106
117
 
107
118
  const alterConnect = ref(null);
108
119
 
@@ -127,7 +138,7 @@ async function handleConnect() {
127
138
  ```html
128
139
  <button id="connect-btn">Connect Account</button>
129
140
 
130
- <script src="https://cdn.jsdelivr.net/npm/@alter-vault/connect@latest/dist/alter-connect.umd.js"></script>
141
+ <script src="https://cdn.jsdelivr.net/npm/@alter-ai/connect@latest/dist/alter-connect.umd.js"></script>
131
142
  <script>
132
143
  const alterConnect = AlterConnect.create();
133
144
 
@@ -143,100 +154,7 @@ async function handleConnect() {
143
154
  </script>
144
155
  ```
145
156
 
146
- ## 🎨 Branding & Customization
147
-
148
- **Branding is now managed through the Developer Portal** and automatically applied to the Connect UI at runtime.
149
-
150
- ### Managing Branding (Developer Portal)
151
-
152
- Configure your app's branding through the Developer Portal API:
153
-
154
- #### Initial Setup or Complete Rebrand (PUT)
155
-
156
- Use PUT for initial branding setup or to completely replace existing branding:
157
-
158
- ```javascript
159
- // Create or fully replace branding
160
- await fetch('https://api.alterai.dev/api/v1/developer/apps/{app_id}/branding', {
161
- method: 'PUT',
162
- headers: {
163
- 'Authorization': `Bearer ${DEVELOPER_TOKEN}`,
164
- 'Content-Type': 'application/json'
165
- },
166
- body: JSON.stringify({
167
- logo_url: 'https://your-domain.com/logo.png', // Required
168
- colors: { // Required
169
- primary: '#6366f1',
170
- text: '#1f2937',
171
- background: '#ffffff'
172
- },
173
- font_family: 'Inter, sans-serif' // Optional
174
- })
175
- });
176
- ```
177
-
178
- #### Partial Updates (PATCH)
179
-
180
- Use PATCH to update specific branding fields:
181
-
182
- ```javascript
183
- // Update only font (logo + colors preserved)
184
- await fetch('https://api.alterai.dev/api/v1/developer/apps/{app_id}/branding', {
185
- method: 'PATCH',
186
- headers: {
187
- 'Authorization': `Bearer ${DEVELOPER_TOKEN}`,
188
- 'Content-Type': 'application/json'
189
- },
190
- body: JSON.stringify({
191
- font_family: 'Roboto, sans-serif'
192
- })
193
- });
194
-
195
- // Update logo + colors (font preserved)
196
- await fetch('https://api.alterai.dev/api/v1/developer/apps/{app_id}/branding', {
197
- method: 'PATCH',
198
- headers: {
199
- 'Authorization': `Bearer ${DEVELOPER_TOKEN}`,
200
- 'Content-Type': 'application/json'
201
- },
202
- body: JSON.stringify({
203
- logo_url: 'https://your-domain.com/new-logo.png',
204
- colors: {
205
- primary: '#ff0000',
206
- text: '#000000',
207
- background: '#ffffff'
208
- }
209
- })
210
- });
211
- ```
212
-
213
- **Important Rules:**
214
- - **PUT:** Requires `logo_url` and `colors`. Font is optional. Replaces all existing branding.
215
- - **PATCH:** Requires existing branding (use PUT first). If updating logo or colors, both must be provided together. Font can be updated independently.
216
- - **DELETE:** Removes all branding and reverts to Alter defaults.
217
-
218
- ### How It Works
219
-
220
- 1. **Configure branding** in the Developer Portal
221
- 2. **SDK fetches branding** automatically when `alterConnect.open()` is called
222
- 3. **Branding is applied** instantly to the Connect UI
223
- 4. **No redeployment needed** - changes take effect immediately
224
-
225
- ### Fallback Behavior
226
-
227
- If no branding is configured or the API call fails, the SDK gracefully falls back to Alter's default branding.
228
-
229
- ### SDK Configuration Options
230
-
231
- For local development or debugging, you can still configure some options:
232
-
233
- ```javascript
234
- const alterConnect = AlterConnect.create({
235
- debug: true // Enable console logging
236
- });
237
- ```
238
-
239
- ## 📚 API Reference
157
+ ## API Reference
240
158
 
241
159
  ### `AlterConnect.create(config?)`
242
160
 
@@ -244,42 +162,40 @@ Creates a new SDK instance.
244
162
 
245
163
  ```javascript
246
164
  const alterConnect = AlterConnect.create({
247
- debug: false
165
+ debug: true // Enable console logging (default: false)
248
166
  });
249
167
  ```
250
168
 
251
- **Config Options:**
252
-
253
169
  | Option | Type | Description | Default |
254
170
  |--------|------|-------------|---------|
255
171
  | `debug` | `boolean` | Enable debug logging | `false` |
256
172
 
257
- **Note:** Branding (colors, fonts, logo) is now managed through the Developer Portal and fetched automatically at runtime.
173
+ **Note:** Visual customization (colors, fonts, logo) is configured via the Developer Portal branding settings. The backend-served Connect UI applies your branding automatically.
258
174
 
259
175
  ---
260
176
 
261
177
  ### `alterConnect.open(options)`
262
178
 
263
- Opens the Connect UI modal.
179
+ Opens the Connect UI. On desktop, opens a centered popup window (500x700px). On mobile, uses a full-page redirect flow.
264
180
 
265
181
  ```javascript
266
182
  await alterConnect.open({
267
183
  token: 'sess_abc123...',
184
+ baseURL: 'https://api.alterai.dev', // Optional
268
185
  onSuccess: (connection) => { /* ... */ },
269
186
  onError: (error) => { /* ... */ },
270
- onExit: () => { /* ... */ }
187
+ onExit: () => { /* ... */ },
188
+ onEvent: (eventName, metadata) => { /* ... */ }
271
189
  });
272
190
  ```
273
191
 
274
- **Options:**
275
-
276
192
  | Parameter | Type | Required | Description |
277
193
  |-----------|------|----------|-------------|
278
- | `token` | `string` | Yes | Session token from your backend |
194
+ | `token` | `string` | Yes | Session token from your backend |
279
195
  | `baseURL` | `string` | No | Alter API URL (usually auto-detected) |
280
- | `onSuccess` | `function` | Yes | Called when connection succeeds |
196
+ | `onSuccess` | `function` | Yes | Called when connection succeeds |
281
197
  | `onError` | `function` | No | Called when connection fails |
282
- | `onExit` | `function` | No | Called when user closes modal |
198
+ | `onExit` | `function` | No | Called when user closes popup |
283
199
  | `onEvent` | `function` | No | Called for analytics events |
284
200
 
285
201
  **Connection Object (onSuccess):**
@@ -292,9 +208,12 @@ await alterConnect.open({
292
208
  account_identifier: string; // e.g., 'user@gmail.com'
293
209
  timestamp: string; // ISO 8601 timestamp
294
210
  operation: 'creation' | 'reauth';
295
- scopes: string[]; // Granted OAuth scopes
211
+ scopes: string[]; // Granted OAuth scopes
296
212
  status: 'active' | 'pending' | 'error';
297
- metadata?: object; // Additional provider data
213
+ metadata?: {
214
+ account_display_name?: string;
215
+ account_email?: string;
216
+ };
298
217
  }
299
218
  ```
300
219
 
@@ -330,6 +249,22 @@ alterConnect.destroy();
330
249
 
331
250
  ---
332
251
 
252
+ ### `alterConnect.on(event, handler)`
253
+
254
+ Register an event listener. Returns an unsubscribe function.
255
+
256
+ ```javascript
257
+ const unsubscribe = alterConnect.on('success', (connection) => {
258
+ console.log('Connected:', connection);
259
+ });
260
+
261
+ // Later: unsubscribe();
262
+ ```
263
+
264
+ **Events:** `success`, `error`, `exit`, `close`, `event`
265
+
266
+ ---
267
+
333
268
  ### `alterConnect.isOpen()`
334
269
 
335
270
  Checks if the Connect UI is currently open.
@@ -340,8 +275,6 @@ if (alterConnect.isOpen()) {
340
275
  }
341
276
  ```
342
277
 
343
- **Returns:** `boolean`
344
-
345
278
  ---
346
279
 
347
280
  ### `alterConnect.getVersion()`
@@ -349,14 +282,38 @@ if (alterConnect.isOpen()) {
349
282
  Gets the SDK version.
350
283
 
351
284
  ```javascript
352
- console.log(alterConnect.getVersion()); // "0.1.0"
285
+ console.log(alterConnect.getVersion()); // "0.2.1"
353
286
  ```
354
287
 
355
- **Returns:** `string`
288
+ ## Mobile Support
289
+
290
+ The SDK automatically detects mobile devices and switches to an optimized flow:
291
+
292
+ | Device | Flow | How It Works |
293
+ |--------|------|-------------|
294
+ | Desktop | Popup | Opens centered popup (500x700px), communicates via postMessage |
295
+ | Phone (<=480px) | Redirect | Full-page redirect, returns via URL params |
296
+ | Tablet (portrait) | Redirect | Full-page redirect for better UX |
297
+ | Tablet (landscape) | Popup | Uses popup flow like desktop |
298
+
299
+ No code changes needed — the SDK handles device detection automatically.
300
+
301
+ For mobile redirect flow, include a `return_url` when creating the session:
302
+
303
+ ```javascript
304
+ // Backend session creation with mobile support
305
+ body: JSON.stringify({
306
+ end_user: { id: 'user_123', email: 'user@example.com' },
307
+ attributes: { user_id: 'user_123' },
308
+ allowed_providers: ['google', 'slack'],
309
+ allowed_origin: 'https://yourapp.com', // For desktop popup (postMessage)
310
+ return_url: 'https://yourapp.com/' // For mobile redirect (return destination)
311
+ })
312
+ ```
356
313
 
357
- ## 🔒 Security Architecture
314
+ ## Security Architecture
358
315
 
359
- The SDK uses a **Plaid-style token architecture** for security:
316
+ The SDK uses a **Plaid-style token architecture**:
360
317
 
361
318
  ```
362
319
  Frontend (Public) Backend (Secure) Alter API
@@ -375,140 +332,63 @@ Frontend (Public) Backend (Secure) Alter API
375
332
  │ 4. { session_token } │ │
376
333
  │<─────────────────────────│ │
377
334
  │ │ │
378
- │ 5. SDK.open({ token })
379
- │──────────────────────────┼───────────────────────>│
380
- │ │ 6. OAuth flow
381
- │<─────────────────────────┴────────────────────────│
382
- 7. onSuccess(connection)│ │
335
+ │ 5. SDK opens popup to backend Connect UI
336
+ │───────────────────────────────────────────────────>│
337
+ │ │
338
+ │ 6. Backend handles auth + OAuth │
339
+ │ │
340
+ │ 7. postMessage(connection) │
341
+ │<──────────────────────────────────────────────────│
342
+ │ 8. onSuccess(connection) │ │
383
343
  ```
384
344
 
385
345
  **Key Security Features:**
386
346
 
387
- **API keys never touch frontend** - Only backend has API key
388
- **Session tokens are short-lived** - Expire after 10 minutes
389
- **Session tokens are single-use** - Can only create one connection
390
- **Session tokens are scoped** - Locked to specific user & providers
391
- **No secrets in browser** - SDK is purely a UI component
347
+ - **API keys never touch frontend** - Only backend has API key
348
+ - **Session tokens are short-lived** - Expire after 10 minutes
349
+ - **Session tokens are single-use** - Can only create one connection
350
+ - **Session tokens are scoped** - Locked to specific user & providers
351
+ - **No secrets in browser** - SDK is purely a popup launcher + callback listener
392
352
 
393
- ## 💡 Smart UX
353
+ ## Smart UX
394
354
 
395
355
  ### Single Provider Flow
396
356
 
397
- When only 1 provider is allowed, the SDK **skips the selection UI** and opens OAuth directly:
357
+ When only 1 provider is allowed, the Connect UI **skips the selection screen** and opens OAuth directly:
398
358
 
399
359
  ```javascript
400
- // Backend
401
- allowed_providers: ['google']
402
-
403
- // SDK behavior: Opens Google OAuth immediately (no modal)
360
+ // Backend: allowed_providers: ['google']
361
+ // Result: Opens Google OAuth immediately (no selection screen)
404
362
  ```
405
363
 
406
364
  ### Multiple Provider Flow
407
365
 
408
- When 2+ providers are allowed, the SDK shows a **provider selection modal**:
366
+ When 2+ providers are allowed, the Connect UI shows a **provider selection screen**:
409
367
 
410
368
  ```javascript
411
- // Backend
412
- allowed_providers: ['google', 'slack', 'microsoft']
413
-
414
- // SDK behavior: Shows modal with provider grid
369
+ // Backend: allowed_providers: ['google', 'slack', 'microsoft']
370
+ // Result: Shows provider selection, user picks one
415
371
  ```
416
372
 
417
- ## 🎯 Common Patterns
418
-
419
- ### Save Connection to Your Database
420
-
421
- ```javascript
422
- await alterConnect.open({
423
- token: sessionToken,
424
- onSuccess: async (connection) => {
425
- // Save to YOUR database
426
- await fetch('/api/connections', {
427
- method: 'POST',
428
- body: JSON.stringify({
429
- connection_id: connection.connection_id,
430
- user_id: currentUserId,
431
- provider: connection.provider
432
- })
433
- });
434
- }
435
- });
436
- ```
437
-
438
- ### Validate Provider Type
439
-
440
- ```javascript
441
- await alterConnect.open({
442
- token: sessionToken,
443
- onSuccess: async (connection) => {
444
- // Ensure user connected the right provider
445
- if (connection.provider !== 'google') {
446
- alert('Please connect your Gmail account');
447
- return;
448
- }
449
-
450
- await saveConnection(connection);
451
- }
452
- });
453
- ```
454
-
455
- ### Validate Email Domain
456
-
457
- ```javascript
458
- await alterConnect.open({
459
- token: sessionToken,
460
- onSuccess: async (connection) => {
461
- // Ensure user connected a work email
462
- if (!connection.account_identifier.endsWith('@company.com')) {
463
- alert('Please connect your @company.com work email');
464
- return;
465
- }
466
-
467
- await saveConnection(connection);
468
- }
469
- });
470
- ```
471
-
472
- ### Handle Errors Gracefully
473
-
474
- ```javascript
475
- await alterConnect.open({
476
- token: sessionToken,
477
- onSuccess: (connection) => {
478
- console.log('Connected!', connection);
479
- },
480
- onError: (error) => {
481
- if (error.code === 'popup_blocked') {
482
- alert('Please allow popups for this site');
483
- } else if (error.code === 'session_expired') {
484
- alert('Session expired. Please try again.');
485
- } else {
486
- alert(`Connection failed: ${error.message}`);
487
- }
488
- },
489
- onExit: () => {
490
- console.log('User cancelled');
491
- }
492
- });
493
- ```
494
-
495
- ## 📦 TypeScript Support
373
+ ## TypeScript Support
496
374
 
497
375
  Full TypeScript definitions included:
498
376
 
499
377
  ```typescript
500
- import AlterConnect, { Connection, AlterError } from '@alter-vault/connect';
378
+ import AlterConnect, {
379
+ AlterConnectConfig,
380
+ Connection,
381
+ AlterError
382
+ } from '@alter-ai/connect';
501
383
 
502
- const alterConnect = AlterConnect.create({
503
- customization: { colors: { primary: '#6366f1' } }
504
- });
384
+ const alterConnect = AlterConnect.create({ debug: true });
505
385
 
506
386
  await alterConnect.open({
507
387
  token: sessionToken,
508
388
  onSuccess: (connection: Connection) => {
509
- // TypeScript knows all properties
510
389
  console.log(connection.connection_id);
511
390
  console.log(connection.provider);
391
+ console.log(connection.scopes);
512
392
  },
513
393
  onError: (error: AlterError) => {
514
394
  console.error(error.code, error.message);
@@ -516,21 +396,24 @@ await alterConnect.open({
516
396
  });
517
397
  ```
518
398
 
519
- ## 🌐 Browser Support
399
+ ## Bundle Size
400
+
401
+ | Format | Size | Gzipped |
402
+ |--------|------|---------|
403
+ | **CJS** | ~10KB | ~3.5KB |
404
+ | **ESM** | ~10KB | ~3.4KB |
405
+ | **UMD** | ~11KB | ~3.5KB |
406
+
407
+ Zero runtime dependencies.
408
+
409
+ ## Browser Support
520
410
 
521
411
  - Chrome/Edge 90+
522
412
  - Firefox 88+
523
413
  - Safari 14+
524
414
  - Mobile browsers (iOS Safari 14+, Chrome Mobile)
525
415
 
526
- ## 📊 Bundle Size
527
-
528
- - **UMD:** 21KB minified
529
- - **ESM:** 21KB minified
530
- - **CJS:** 21KB minified
531
- - **Zero runtime dependencies**
532
-
533
- ## 🐛 Troubleshooting
416
+ ## Troubleshooting
534
417
 
535
418
  ### Popup Blocked
536
419
 
@@ -539,13 +422,13 @@ await alterConnect.open({
539
422
  **Solution:** Ensure `alterConnect.open()` is called directly from a user interaction (click event):
540
423
 
541
424
  ```javascript
542
- // Bad - may be blocked
425
+ // Bad - may be blocked
543
426
  button.addEventListener('click', async () => {
544
427
  const token = await fetchToken(); // Async delay
545
428
  alterConnect.open({ token }); // May be blocked
546
429
  });
547
430
 
548
- // Good - no async delay before open()
431
+ // Good - no async delay before open()
549
432
  button.addEventListener('click', () => {
550
433
  fetchToken().then(token => {
551
434
  alterConnect.open({ token }); // Called synchronously
@@ -565,13 +448,13 @@ button.addEventListener('click', () => {
565
448
 
566
449
  **Solution:** Session tokens should be created from your **backend**, not frontend. The SDK handles all frontend API calls.
567
450
 
568
- ## 🤝 Support
451
+ ## Support
569
452
 
570
453
  - **Documentation:** [https://docs.alterai.dev](https://docs.alterai.dev)
571
454
  - **API Reference:** [https://api.alterai.dev/docs](https://api.alterai.dev/docs)
572
- - **Issues:** [GitHub Issues](https://github.com/alter-ai/alter-vault/issues)
455
+ - **Issues:** [GitHub Issues](https://github.com/AlterAIDev/Alter-Vault/issues)
573
456
  - **Email:** support@alterai.dev
574
457
 
575
- ## 📄 License
458
+ ## License
576
459
 
577
460
  MIT License - See [LICENSE](LICENSE) file for details
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});const t={colors:{primary:"#6366f1",text:"#1f2937",background:"#ffffff"},fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'},e="en",n=!1;function o(t){if(t.customization?.colors?.primary&&!r(t.customization.colors.primary))throw new Error(`Invalid primary color: ${t.customization.colors.primary}`);if(t.customization?.colors?.text&&!r(t.customization.colors.text))throw new Error(`Invalid text color: ${t.customization.colors.text}`);if(t.customization?.colors?.background&&!r(t.customization.colors.background))throw new Error(`Invalid background color: ${t.customization.colors.background}`)}function s(o){const s=t.colors,i=t.fontFamily;return{customization:{colors:{primary:o.customization?.colors?.primary||s.primary,text:o.customization?.colors?.text||s.text,background:o.customization?.colors?.background||s.background},fontFamily:o.customization?.fontFamily||i},locale:o.locale||e,debug:o.debug??n}}function i(t,...e){t.debug&&console.log("[Alter Connect]",...e)}function r(t){return!!/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(t)||(!!/^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[\d.]+\s*)?\)$/.test(t)||(!!/^hsla?\(\s*\d+\s*,\s*[\d.]+%\s*,\s*[\d.]+%\s*(,\s*[\d.]+\s*)?\)$/.test(t)||!!/^[a-z]+$/.test(t.toLowerCase())))}class a{constructor(){this.events=new Map}on(t,e){return this.events.has(t)||this.events.set(t,new Set),this.events.get(t).add(e),()=>this.off(t,e)}off(t,e){const n=this.events.get(t);n&&n.delete(e)}emit(t,...e){const n=this.events.get(t);n&&n.forEach(n=>{try{n(...e)}catch(e){console.error(`[Alter Connect] Error in event handler for '${t}':`,e)}})}removeAllListeners(t){t?this.events.delete(t):this.events.clear()}}class l{constructor(){this.state={isOpen:!1,isLoading:!1,error:null,sessionToken:null,providers:[],selectedProvider:null,searchQuery:"",categoryFilter:null},this.listeners=new Set}getState(){return{...this.state}}get(t){return this.state[t]}setState(t){this.state={...this.state,...t},this.notifyListeners()}subscribe(t){return this.listeners.add(t),()=>{this.listeners.delete(t)}}clearListeners(){this.listeners.clear()}notifyListeners(){const t=this.getState();this.listeners.forEach(e=>{try{e(t)}catch(t){console.error("[Alter Connect] Error in state listener:",t)}})}}class c{constructor(t=!1){this.TIMEOUT=3e4,this.DEFAULT_BASE_URL="https://api.alterai.dev",this.debug=t}async fetchProviders(t,e){this.log("Fetching providers with session token");const n=await this.fetch("/oauth/providers",{method:"GET",sessionToken:t,baseURL:e||this.DEFAULT_BASE_URL});return this.log(`Fetched ${n.providers.length} providers`),n.providers}async fetchBranding(t,e){this.log("Fetching branding with session token");const n=await this.fetch("/oauth/branding",{method:"GET",sessionToken:t,baseURL:e||this.DEFAULT_BASE_URL});return this.log("Fetched branding:",n),n}buildAuthorizationURL(t,e,n){const o=`${n||this.DEFAULT_BASE_URL}/oauth/connect/initiate?provider=${t}&session=${e}`;return this.log("Authorization URL:",o),o}async fetch(t,e){const n=`${e.baseURL}${t}`,o={"Content-Type":"application/json"};e.sessionToken&&(o["X-Session-Token"]=e.sessionToken);const s=new AbortController,i=setTimeout(()=>s.abort(),this.TIMEOUT);try{const t=await fetch(n,{method:e.method,headers:o,body:e.body,signal:s.signal});if(clearTimeout(i),!t.ok){const e=await t.json().catch(()=>({error:"unknown_error",message:`HTTP ${t.status}: ${t.statusText}`}));throw this.createError(e.error||"api_error",e.message||e.error_description||`Request failed with status ${t.status}`,e)}return await t.json()}catch(t){if(clearTimeout(i),"AbortError"===t.name)throw this.createError("timeout",`Request timed out after ${this.TIMEOUT}ms`,{timeout:this.TIMEOUT});if(t instanceof TypeError)throw this.createError("network_error","Network request failed. Please check your internet connection.",{originalError:t.message});if(this.isAlterError(t))throw t;throw this.createError("unknown_error",t.message||"An unexpected error occurred",{originalError:t})}}createError(t,e,n){return{code:t,message:e,details:n}}isAlterError(t){return t&&"object"==typeof t&&"code"in t&&"message"in t}log(...t){this.debug&&console.log("[Alter API Client]",...t)}}class h{constructor(t){this.overlay=null,this.container=null,this.contentEl=null,this.closeButton=null,this.onClose=null,this.handleClose=()=>{this.onClose&&this.onClose()},this.handleEscape=t=>{"Escape"===t.key&&this.handleClose()},this.config=t}open(t){this.onClose=t.onClose,this.overlay=document.createElement("div"),this.overlay.className="alter-connect-overlay",this.overlay.style.cssText="\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 999999;\n animation: alter-connect-fade-in 0.2s ease-out;\n ",this.container=document.createElement("div"),this.container.className="alter-connect-modal",this.container.style.cssText=`\n background: #ffffff;\n border-radius: 12px;\n box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);\n width: 90%;\n max-width: 480px;\n max-height: 90vh;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n font-family: ${this.config.customization.fontFamily};\n animation: alter-connect-slide-up 0.3s ease-out;\n color: #1a1a1a;\n `,this.closeButton=document.createElement("button"),this.closeButton.className="alter-connect-close",this.closeButton.innerHTML="&times;",this.closeButton.setAttribute("aria-label","Close"),this.closeButton.style.cssText="\n position: absolute;\n top: 16px;\n right: 16px;\n background: transparent;\n border: none;\n font-size: 28px;\n line-height: 1;\n color: #6b7280;\n cursor: pointer;\n padding: 0;\n width: 32px;\n height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: background-color 0.2s, color 0.2s;\n ",this.closeButton.addEventListener("click",()=>this.handleClose()),this.closeButton.addEventListener("mouseenter",()=>{this.closeButton.style.backgroundColor="#f3f4f6",this.closeButton.style.color="#111827"}),this.closeButton.addEventListener("mouseleave",()=>{this.closeButton.style.backgroundColor="transparent",this.closeButton.style.color="#6b7280"}),this.contentEl=document.createElement("div"),this.contentEl.className="alter-connect-content",this.contentEl.style.cssText="\n padding: 40px 32px 32px 32px;\n overflow-y: auto;\n flex: 1;\n position: relative;\n ",this.container.appendChild(this.closeButton),this.container.appendChild(this.contentEl),this.overlay.appendChild(this.container),document.body.appendChild(this.overlay),this.overlay.addEventListener("click",t=>{t.target===this.overlay&&this.handleClose()}),document.addEventListener("keydown",this.handleEscape),document.body.style.overflow="hidden"}setContent(t){this.contentEl&&(this.contentEl.innerHTML="",this.contentEl.appendChild(t))}close(){this.overlay&&this.overlay.parentNode&&this.overlay.parentNode.removeChild(this.overlay),this.overlay=null,this.container=null,this.contentEl=null,this.closeButton=null,this.onClose=null,document.removeEventListener("keydown",this.handleEscape),document.body.style.overflow=""}destroy(){this.close()}}class d{constructor(t){this.container=null,this.onProviderClick=null,this.config=t}render(t,e){return this.onProviderClick=e,this.container=document.createElement("div"),this.container.className="alter-connect-provider-list",this.container.style.cssText="\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n gap: 12px;\n margin-bottom: 20px;\n ",this.renderProviders(t),this.container}update(t){this.container&&(this.container.innerHTML="",this.renderProviders(t))}destroy(){this.container=null,this.onProviderClick=null}renderProviders(t){if(this.container){if(0===t.length){const t=document.createElement("div");return t.style.cssText="\n text-align: center;\n padding: 40px 20px;\n color: white;\n font-size: 14px;\n ",t.textContent="No providers found",void this.container.appendChild(t)}t.forEach(t=>{const e=this.createProviderButton(t);this.container.appendChild(e)})}}createProviderButton(t){const e=document.createElement("button");e.className="alter-connect-provider-button",e.setAttribute("data-provider-id",t.id),e.style.cssText=`\n display: flex;\n align-items: center;\n padding: 16px;\n background: white;\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n cursor: pointer;\n transition: all 0.15s;\n text-decoration: none;\n color: inherit;\n gap: 12px;\n font-family: ${this.config.customization.fontFamily};\n width: 100%;\n `;const n=document.createElement("span");n.textContent="→",n.className="connect-arrow",n.style.cssText="\n color: #9ca3af;\n font-size: 18px;\n transition: all 0.15s;\n flex-shrink: 0;\n ",e.addEventListener("mouseenter",()=>{e.style.borderColor="#d1d5db",e.style.boxShadow="0 2px 8px rgba(0, 0, 0, 0.08)",e.style.transform="translateY(-1px)",n.style.color="#000000",n.style.transform="translateX(2px)"}),e.addEventListener("mouseleave",()=>{e.style.borderColor="#e5e7eb",e.style.boxShadow="none",e.style.transform="translateY(0)",n.style.color="#9ca3af",n.style.transform="translateX(0)"});const o=document.createElement("div");if(o.style.cssText="\n width: 40px;\n height: 40px;\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: #f9fafb;\n border: 1px solid #e5e7eb;\n flex-shrink: 0;\n ",t.logo_url){const e=document.createElement("img");e.src=t.logo_url,e.alt=t.name,e.style.cssText="\n width: 28px;\n height: 28px;\n object-fit: contain;\n ",e.addEventListener("error",()=>{o.textContent=t.name.charAt(0).toUpperCase()}),o.appendChild(e)}else o.textContent=t.name.charAt(0).toUpperCase();const s=document.createElement("div");s.style.cssText="\n flex: 1;\n min-width: 0;\n text-align: left;\n ";const i=document.createElement("div");i.textContent=t.name,i.style.cssText="\n font-size: 14px;\n font-weight: 600;\n color: #1a1a1a;\n margin-bottom: 2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n ";const r=document.createElement("div");return r.textContent=t.category?t.category.charAt(0).toUpperCase()+t.category.slice(1):"Other",r.style.cssText="\n font-size: 12px;\n color: #6b7280;\n font-weight: 400;\n ",s.appendChild(i),s.appendChild(r),e.appendChild(o),e.appendChild(s),e.appendChild(n),e.addEventListener("click",()=>{this.onProviderClick&&this.onProviderClick(t)}),e}}class p{constructor(t){this.popup=null,this.pollInterval=null,this.messageListener=null,this.options={onSuccess:t.onSuccess,onError:t.onError,onCancel:t.onCancel,popupWidth:t.popupWidth||450,popupHeight:t.popupHeight||600,debug:t.debug||!1}}openPopup(t){const e=window.screenX+(window.outerWidth-this.options.popupWidth)/2,n=window.screenY+(window.outerHeight-this.options.popupHeight)/2,o=[`width=${this.options.popupWidth}`,`height=${this.options.popupHeight}`,`left=${e}`,`top=${n}`,"resizable=yes","scrollbars=yes","status=yes"].join(",");this.log("Opening OAuth popup:",t),this.popup=window.open(t,"alter_oauth_popup",o),this.popup?(this.startPolling(),this.setupMessageListener()):this.options.onError({code:"popup_blocked",message:"Popup was blocked by browser. Please allow popups for this site."})}close(){this.log("Closing OAuth handler"),this.popup&&!this.popup.closed&&this.popup.close(),this.popup=null,null!==this.pollInterval&&(clearInterval(this.pollInterval),this.pollInterval=null),this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null)}startPolling(){this.pollInterval=window.setInterval(()=>{this.popup&&!this.popup.closed||(this.log("Popup closed by user"),this.close(),this.options.onCancel())},500)}setupMessageListener(){this.messageListener=t=>{this.log("Received message:",t.data);const e=t.data;if(e&&"object"==typeof e)if("alter_connect_success"===e.type){this.log("OAuth success");const t={connection_id:e.connection_id,provider:e.provider,provider_name:e.provider_name||e.provider,account_identifier:e.account_identifier,timestamp:e.timestamp,operation:e.operation||"creation",scopes:e.scopes||[],status:e.status||"active",metadata:e.metadata};this.close(),this.options.onSuccess(t)}else if("alter_connect_error"===e.type){this.log("OAuth error");const t={code:e.error||"oauth_error",message:e.error_description||"OAuth authorization failed",details:e};this.close(),this.options.onError(t)}},window.addEventListener("message",this.messageListener)}log(...t){this.options.debug&&console.log("[OAuth Handler]",...t)}}const u="0.1.0";class g{constructor(t={}){this._searchBar=null,this._providerList=null,this._oauthHandler=null,this._baseURL="https://api.alterai.dev",o(t),this.config=s(t),this.eventEmitter=new a,this.stateManager=new l,this.apiClient=new c(this.config.debug),this._modal=new h(this.config),this._isInitialized=!0,function(){if(document.getElementById("alter-connect-styles"))return;const t=document.createElement("style");t.id="alter-connect-styles",t.textContent="\n @keyframes alter-connect-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n }\n\n @keyframes alter-connect-slide-up {\n from {\n opacity: 0;\n transform: translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n .alter-connect-modal * {\n box-sizing: border-box;\n }\n ",document.head.appendChild(t)}(),i(this.config,"Alter Connect SDK initialized",{version:u})}static create(t){return new g(t)}async open(t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call open() - SDK instance has been destroyed");if(i(this.config,"Opening Connect UI"),!t.token||"string"!=typeof t.token)throw this.createError("invalid_options","Session token is required. Create one from your backend using POST /oauth/connect/session");if(!t.onSuccess||"function"!=typeof t.onSuccess)throw this.createError("invalid_options","onSuccess callback is required");if(this.isOpen())i(this.config,"Connect UI is already open");else try{this._baseURL=t.baseURL||"https://api.alterai.dev",this.stateManager.setState({isOpen:!0,isLoading:!0,error:null}),this.stateManager.setState({sessionToken:t.token});const[e,n]=await Promise.all([this.apiClient.fetchProviders(t.token,this._baseURL),this.apiClient.fetchBranding(t.token,this._baseURL).catch(()=>null)]);if(n&&(i(this.config,"Applying branding from API"),n.colors&&(this.config.customization.colors={primary:n.colors.primary,text:n.colors.text,background:n.colors.background}),n.font_family&&(this.config.customization.fontFamily=n.font_family),this.config.customization.logoUrl=n.logo_url||void 0),this.stateManager.setState({providers:e,isLoading:!1}),this.registerEventHandlers(t),1===e.length)i(this.config,"Single provider detected - skipping selection UI"),this.handleProviderClick(e[0]),t.onEvent&&t.onEvent("single_provider_flow",{timestamp:(new Date).toISOString(),provider:e[0].id});else{i(this.config,"Multiple providers detected - showing selection UI");const n=this.buildUIContent(e);this._modal.open({onClose:()=>{this.eventEmitter.emit("exit")}}),this._modal.setContent(n),t.onEvent&&t.onEvent("modal_opened",{timestamp:(new Date).toISOString(),providerCount:e.length})}i(this.config,"Connect UI opened successfully")}catch(e){throw i(this.config,"Error opening Connect UI:",e),this.stateManager.setState({isLoading:!1,isOpen:!1,error:this.normalizeError(e)}),t.onError&&t.onError(this.normalizeError(e)),e}}close(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call close() - SDK instance has been destroyed");i(this.config,"Closing Connect UI"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this._searchBar&&(this._searchBar.destroy(),this._searchBar=null),this._providerList&&(this._providerList.destroy(),this._providerList=null),this._modal.close(),this.stateManager.setState({isOpen:!1,selectedProvider:null,searchQuery:"",categoryFilter:null}),this.eventEmitter.emit("close")}destroy(){this._isInitialized&&(i(this.config,"Destroying SDK instance"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this._searchBar&&(this._searchBar.destroy(),this._searchBar=null),this._providerList&&(this._providerList.destroy(),this._providerList=null),this._modal.destroy(),this.stateManager.get("isOpen")&&this.stateManager.setState({isOpen:!1,selectedProvider:null,searchQuery:"",categoryFilter:null}),this.eventEmitter.removeAllListeners(),this.stateManager.clearListeners(),this._isInitialized=!1)}updateConfig(t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call updateConfig() - SDK instance has been destroyed");i(this.config,"Updating config",t);const e={...this.config,...t};o(e),this.config=s(e),this.apiClient=new c(this.config.debug)}getConfig(){return{...this.config}}on(t,e){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call on() - SDK instance has been destroyed");return this.eventEmitter.on(t,e)}off(t,e){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call off() - SDK instance has been destroyed");this.eventEmitter.off(t,e)}isOpen(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call isOpen() - SDK instance has been destroyed");return this.stateManager.get("isOpen")}getVersion(){return u}buildUIContent(t){const e=document.createElement("div");e.className="alter-connect-ui-container";const n=document.createElement("div");n.style.cssText="\n background: #000000;\n border-bottom: 1px solid #1a1a1a;\n padding: 20px 24px;\n margin: -32px -32px 32px -32px;\n display: flex;\n align-items: center;\n justify-content: center;\n ";const o=document.createElement("img");this.config.customization.logoUrl?(o.src=this.config.customization.logoUrl,o.alt="App Logo"):(o.src=`${this._baseURL}/static/Alter_Primary_Logo_name.png`,o.alt="Alter"),o.style.cssText="\n height: 28px;\n width: auto;\n max-width: 200px;\n object-fit: contain;\n ",o.addEventListener("error",()=>{const t=document.createElement("div");t.textContent="alter",t.style.cssText='\n font-size: 18px;\n font-weight: 600;\n color: #ffffff;\n letter-spacing: -0.02em;\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;\n ',o.replaceWith(t)}),n.appendChild(o);const s=document.createElement("div");s.style.cssText="\n margin-bottom: 32px;\n text-align: center;\n ";const i=document.createElement("h1");i.textContent="Connect an Integration",i.style.cssText='\n font-size: 24px;\n font-weight: 600;\n color: #1a1a1a;\n margin-bottom: 8px;\n letter-spacing: -0.02em;\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;\n ';const r=document.createElement("p");r.textContent="Choose a service to connect to your account",r.style.cssText='\n margin: 0;\n font-size: 15px;\n color: #6b7280;\n font-weight: 400;\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;\n ',s.appendChild(i),s.appendChild(r),this._providerList=new d(this.config);const a=this._providerList.render(t,t=>{this.handleProviderClick(t)});return e.appendChild(n),e.appendChild(s),e.appendChild(a),e}handleProviderClick(t){i(this.config,"Provider clicked:",t.id),this.stateManager.setState({selectedProvider:t});const e=this.stateManager.get("sessionToken");if(!e){const t=this.createError("no_session","No active session");return void this.eventEmitter.emit("error",t)}const n=this.apiClient.buildAuthorizationURL(t.id,e,this._baseURL);this._oauthHandler=new p({onSuccess:t=>{i(this.config,"OAuth success:",t),this.handleOAuthSuccess(t)},onError:t=>{i(this.config,"OAuth error:",t),this.handleOAuthError(t)},onCancel:()=>{i(this.config,"OAuth cancelled"),this.handleOAuthCancel()},popupWidth:450,popupHeight:600,debug:this.config.debug}),this._oauthHandler.openPopup(n),i(this.config,"OAuth flow started for provider:",t.id)}handleOAuthSuccess(t){this.stateManager.setState({selectedProvider:null}),this.eventEmitter.emit("success",t),this._oauthHandler&&(this._oauthHandler=null)}handleOAuthError(t){this.stateManager.setState({selectedProvider:null,error:t}),this.eventEmitter.emit("error",t),this._oauthHandler&&(this._oauthHandler=null)}handleOAuthCancel(){this.stateManager.setState({selectedProvider:null}),this.eventEmitter.emit("exit"),this._oauthHandler&&(this._oauthHandler=null)}registerEventHandlers(t){t.onSuccess&&this.eventEmitter.on("success",e=>{t.onSuccess(e),this.close()}),t.onExit&&this.eventEmitter.on("exit",()=>{t.onExit(),this.close()}),t.onError&&this.eventEmitter.on("error",e=>{t.onError(e)}),t.onEvent&&this.eventEmitter.on("event",(e,n)=>{t.onEvent(e,n)})}createError(t,e,n){const o=new Error(e);return o.code=t,o.details=n,o}normalizeError(t){if(t&&"object"==typeof t&&"code"in t&&"message"in t)return t;const e=this.createError("unknown_error",t?.message||String(t)||"An unexpected error occurred",{originalError:t});return{code:e.code,message:e.message,details:e.details}}}exports.default=g;
1
+ "use strict";function e(e,...t){e.debug&&console.log("[Alter Connect]",...t)}Object.defineProperty(exports,"__esModule",{value:!0});class t{constructor(){this.events=new Map}on(e,t){return this.events.has(e)||this.events.set(e,new Set),this.events.get(e).add(t),()=>this.off(e,t)}off(e,t){const s=this.events.get(e);s&&s.delete(t)}emit(e,...t){const s=this.events.get(e);s&&s.forEach(s=>{try{s(...t)}catch(t){console.error(`[Alter Connect] Error in event handler for '${e}':`,t)}})}removeAllListeners(e){e?this.events.delete(e):this.events.clear()}}class s{constructor(){this.state={isOpen:!1,sessionToken:null,error:null},this.listeners=new Set}getState(){return{...this.state}}get(e){return this.state[e]}setState(e){this.state={...this.state,...e},this.notifyListeners()}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}clearListeners(){this.listeners.clear()}notifyListeners(){const e=this.getState();this.listeners.forEach(t=>{try{t(e)}catch(e){console.error("[Alter Connect] Error in state listener:",e)}})}}function i(){const e=navigator.userAgent||navigator.vendor||window.opera,t=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(e),s="ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0,i=window.innerWidth<=768;return t||s&&i}function n(){return i()&&window.innerWidth<=480?"phone":i()&&window.innerWidth>480&&window.innerWidth<=1024?"tablet":"desktop"}function o(e){return"reauth"===e?"reauth":"creation"}function r(e){return"pending"===e?"pending":"error"===e?"error":"active"}function a(e){sessionStorage.removeItem("alter_oauth_state");try{const t=new URL(e);window.history.replaceState({},document.title,t.pathname+t.search+t.hash)}catch{window.history.replaceState({},document.title,window.location.pathname)}}class c{constructor(e){this.popup=null,this.pollInterval=null,this.messageListener=null,this.settled=!1,this.options={baseURL:e.baseURL,onSuccess:e.onSuccess,onError:e.onError,onCancel:e.onCancel,popupWidth:e.popupWidth||500,popupHeight:e.popupHeight||700,debug:e.debug||!1};try{this.expectedOrigin=new URL(e.baseURL).origin}catch{throw new Error(`Invalid baseURL: "${e.baseURL}". Must be a full URL with protocol (e.g., "https://api.alterai.dev").`)}}startOAuth(e){!function(){const e=n();return"phone"===e||"tablet"===e&&window.innerHeight>window.innerWidth}()?(this.log("Using popup flow for desktop"),this.openPopup(e)):(this.log("Using redirect flow for mobile device"),this.startRedirectFlow(e))}startRedirectFlow(e){this.log("Starting redirect flow:",e);const t={timestamp:Date.now(),returnUrl:window.location.href};try{sessionStorage.setItem("alter_oauth_state",JSON.stringify(t))}catch(e){return this.log("Failed to save state:",e),void this.options.onError({code:"redirect_error",message:"Failed to start OAuth flow: could not save session state",details:{error:e}})}window.location.href=e}static checkOAuthReturn(e,t){const s=sessionStorage.getItem("alter_oauth_state");if(!s)return!1;try{const i=JSON.parse(s);if(Date.now()-i.timestamp>3e5)return sessionStorage.removeItem("alter_oauth_state"),!1;const n=new URLSearchParams(window.location.search),c=n.get("alter_connect_success"),h=n.get("alter_connect_error");if("true"===c){const s=n.get("connection_id"),c=n.get("provider"),h=n.get("account_identifier");if(!s||!c||!h)return a(i.returnUrl),t({code:"invalid_response",message:"OAuth redirect returned incomplete connection data"}),!0;const l={connection_id:s,provider:c,provider_name:n.get("provider_name")||c,account_identifier:h,timestamp:n.get("timestamp")||(new Date).toISOString(),operation:o(n.get("operation")),scopes:n.get("scopes")?.split(",")||[],status:r(n.get("status"))};return a(i.returnUrl),e(l),!0}if(h){const e={code:n.get("error_code")||"oauth_error",message:n.get("error_description")||"OAuth authorization failed"};return a(i.returnUrl),t(e),!0}return!1}catch(e){return console.error("[OAuth Handler] Failed to check OAuth return:",e),sessionStorage.removeItem("alter_oauth_state"),!1}}openPopup(e){const t=window.screenX+(window.outerWidth-this.options.popupWidth)/2,s=window.screenY+(window.outerHeight-this.options.popupHeight)/2,i=[`width=${this.options.popupWidth}`,`height=${this.options.popupHeight}`,`left=${t}`,`top=${s}`,"resizable=yes","scrollbars=yes","status=yes"].join(",");this.log("Opening OAuth popup:",e),this.popup=window.open(e,"alter_oauth_popup",i),this.popup?(this.startPolling(),this.setupMessageListener()):this.options.onError({code:"popup_blocked",message:"Popup was blocked by browser. Please allow popups for this site."})}close(){this.log("Closing OAuth handler"),this.popup&&!this.popup.closed&&this.popup.close(),this.popup=null,null!==this.pollInterval&&(clearInterval(this.pollInterval),this.pollInterval=null),this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null)}startPolling(){this.pollInterval=window.setInterval(()=>{this.settled||this.popup&&!this.popup.closed||(this.log("Popup closed by user"),this.settled=!0,this.close(),this.options.onCancel())},500)}setupMessageListener(){this.messageListener=e=>{if(this.settled)return;if(e.origin!==this.expectedOrigin)return void this.log("Rejected message from unexpected origin:",e.origin,"(expected:",this.expectedOrigin+")");this.log("Received message from",e.origin);const t=e.data;if(t&&"object"==typeof t)if("alter_connect_success"===t.type){this.log("OAuth success");const e=function(e){return e.connection_id&&"string"==typeof e.connection_id?e.provider&&"string"==typeof e.provider?e.account_identifier&&"string"==typeof e.account_identifier?e.timestamp&&"string"==typeof e.timestamp?null:"Missing or invalid timestamp":"Missing or invalid account_identifier":"Missing or invalid provider":"Missing or invalid connection_id"}(t);if(e)return this.log("Invalid connection payload:",e),this.settled=!0,this.close(),void this.options.onError({code:"invalid_response",message:`Server returned incomplete connection data: ${e}`});const s={connection_id:t.connection_id,provider:t.provider,provider_name:t.provider_name||t.provider,account_identifier:t.account_identifier,timestamp:t.timestamp,operation:o(t.operation),scopes:Array.isArray(t.scopes)?t.scopes:[],status:r(t.status),metadata:t.metadata};this.settled=!0,this.close(),this.options.onSuccess(s)}else if("alter_connect_error"===t.type){this.log("OAuth error");const e={code:t.error||"oauth_error",message:t.error_description||"OAuth authorization failed",details:t};this.settled=!0,this.close(),this.options.onError(e)}},window.addEventListener("message",this.messageListener)}log(...e){this.options.debug&&console.log("[OAuth Handler]",...e)}}const h="0.2.0";class l{constructor(i={}){this._oauthHandler=null,this._baseURL="https://api.alterai.dev",this._perOpenCleanups=[],this.config=function(e){return{debug:e.debug??!1}}(i),this.eventEmitter=new t,this.stateManager=new s,this._isInitialized=!0,e(this.config,"Alter Connect SDK initialized",{version:h}),this.checkRedirectReturn()}checkRedirectReturn(){c.checkOAuthReturn(t=>{e(this.config,"OAuth redirect return - success:",t),this.eventEmitter.emit("success",t)},t=>{e(this.config,"OAuth redirect return - error:",t),this.eventEmitter.emit("error",t)})&&e(this.config,"OAuth redirect return detected and handled")}static create(e){return new l(e)}async open(t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call open() - SDK instance has been destroyed");if(e(this.config,"Opening Connect UI"),!t.token||"string"!=typeof t.token)throw this.createError("invalid_options","Session token is required. Create one from your backend using POST /oauth/connect/session");if(!t.onSuccess||"function"!=typeof t.onSuccess)throw this.createError("invalid_options","onSuccess callback is required");if(this.isOpen())return void e(this.config,"Connect UI is already open");this._baseURL=t.baseURL||"https://api.alterai.dev",this._oauthHandler=new c({baseURL:this._baseURL,onSuccess:t=>{e(this.config,"OAuth success:",t),this.handleOAuthSuccess(t)},onError:t=>{e(this.config,"OAuth error:",t),this.handleOAuthError(t)},onCancel:()=>{e(this.config,"OAuth cancelled (popup closed)"),this.handleOAuthCancel()},popupWidth:500,popupHeight:700,debug:this.config.debug}),this.stateManager.setState({isOpen:!0,error:null,sessionToken:t.token}),this.registerEventHandlers(t);const s=`${this._baseURL}/oauth/connect#session=${encodeURIComponent(t.token)}`;e(this.config,"Connect URL:",s),this._oauthHandler.startOAuth(s),t.onEvent&&t.onEvent("connect_opened",{timestamp:(new Date).toISOString()}),e(this.config,"Connect UI opened successfully")}close(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call close() - SDK instance has been destroyed");e(this.config,"Closing Connect UI"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this._perOpenCleanups.forEach(e=>e()),this._perOpenCleanups=[],this.stateManager.setState({isOpen:!1}),this.eventEmitter.emit("close")}destroy(){this._isInitialized&&(e(this.config,"Destroying SDK instance"),this._oauthHandler&&(this._oauthHandler.close(),this._oauthHandler=null),this.stateManager.get("isOpen")&&this.stateManager.setState({isOpen:!1}),this.eventEmitter.removeAllListeners(),this.stateManager.clearListeners(),this._isInitialized=!1)}on(e,t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call on() - SDK instance has been destroyed");return this.eventEmitter.on(e,t)}off(e,t){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call off() - SDK instance has been destroyed");this.eventEmitter.off(e,t)}isOpen(){if(!this._isInitialized)throw this.createError("sdk_destroyed","Cannot call isOpen() - SDK instance has been destroyed");return this.stateManager.get("isOpen")}getVersion(){return h}cleanupHandler(){this._oauthHandler=null}handleOAuthSuccess(e){this.cleanupHandler(),this.eventEmitter.emit("success",e)}handleOAuthError(e){this.cleanupHandler(),this.stateManager.setState({error:e}),this.eventEmitter.emit("error",e)}handleOAuthCancel(){this.cleanupHandler(),this.eventEmitter.emit("exit")}registerEventHandlers(e){e.onSuccess&&this._perOpenCleanups.push(this.eventEmitter.on("success",t=>{e.onSuccess(t),this.close()})),e.onExit&&this._perOpenCleanups.push(this.eventEmitter.on("exit",()=>{e.onExit(),this.close()})),this._perOpenCleanups.push(this.eventEmitter.on("error",t=>{e.onError&&e.onError(t),this.close()})),e.onEvent&&this._perOpenCleanups.push(this.eventEmitter.on("event",(t,s)=>{e.onEvent(t,s)}))}createError(e,t,s){const i=new Error(t);return i.code=e,i.details=s,i}}exports.default=l;
2
2
  //# sourceMappingURL=alter-connect.cjs.js.map