@alter-ai/connect 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/README.md ADDED
@@ -0,0 +1,577 @@
1
+ # Alter Connect SDK
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.
4
+
5
+ ## 🚀 Quick Start
6
+
7
+ ### 1. Install
8
+
9
+ ```bash
10
+ npm install @alter-vault/connect
11
+ ```
12
+
13
+ Or use via CDN:
14
+
15
+ ```html
16
+ <script src="https://cdn.jsdelivr.net/npm/@alter-vault/connect@latest/dist/alter-connect.umd.js"></script>
17
+ ```
18
+
19
+ ### 2. Get a Session Token from Your Backend
20
+
21
+ Your backend creates a short-lived session token using your API key:
22
+
23
+ ```javascript
24
+ // YOUR backend endpoint
25
+ const response = await fetch('https://api.alterai.dev/oauth/connect/session', {
26
+ method: 'POST',
27
+ headers: {
28
+ 'x-api-key': process.env.ALTER_API_KEY, // Never expose this!
29
+ 'Content-Type': 'application/json'
30
+ },
31
+ body: JSON.stringify({
32
+ end_user: {
33
+ id: 'user_123',
34
+ email: 'user@example.com'
35
+ },
36
+ allowed_providers: ['google', 'slack', 'microsoft']
37
+ })
38
+ });
39
+
40
+ const { session_token } = await response.json();
41
+ ```
42
+
43
+ ### 3. Open the Connect UI
44
+
45
+ ```javascript
46
+ import AlterConnect from '@alter-vault/connect';
47
+
48
+ // Initialize SDK
49
+ const alterConnect = AlterConnect.create();
50
+
51
+ // Get session token from YOUR backend
52
+ const { session_token } = await fetch('/api/alter/session').then(r => r.json());
53
+
54
+ // Open Connect UI
55
+ await alterConnect.open({
56
+ token: session_token,
57
+ onSuccess: (connection) => {
58
+ console.log('Connected!', connection);
59
+ // Save connection.connection_id to your database
60
+ },
61
+ onError: (error) => {
62
+ console.error('Failed:', error);
63
+ }
64
+ });
65
+ ```
66
+
67
+ That's it! The SDK handles the OAuth flow, popup windows, and all security.
68
+
69
+ ## 📖 Framework Examples
70
+
71
+ ### React
72
+
73
+ ```jsx
74
+ import { useState } from 'react';
75
+ import AlterConnect from '@alter-vault/connect';
76
+
77
+ function ConnectButton() {
78
+ const [alterConnect] = useState(() => AlterConnect.create());
79
+
80
+ const handleConnect = async () => {
81
+ const { session_token } = await fetch('/api/alter/session')
82
+ .then(r => r.json());
83
+
84
+ await alterConnect.open({
85
+ token: session_token,
86
+ onSuccess: (connection) => {
87
+ console.log('Connected!', connection);
88
+ }
89
+ });
90
+ };
91
+
92
+ return <button onClick={handleConnect}>Connect Account</button>;
93
+ }
94
+ ```
95
+
96
+ ### Vue
97
+
98
+ ```vue
99
+ <template>
100
+ <button @click="handleConnect">Connect Account</button>
101
+ </template>
102
+
103
+ <script setup>
104
+ import { ref, onMounted } from 'vue';
105
+ import AlterConnect from '@alter-vault/connect';
106
+
107
+ const alterConnect = ref(null);
108
+
109
+ onMounted(() => {
110
+ alterConnect.value = AlterConnect.create();
111
+ });
112
+
113
+ async function handleConnect() {
114
+ const { session_token } = await fetch('/api/alter/session')
115
+ .then(r => r.json());
116
+
117
+ await alterConnect.value.open({
118
+ token: session_token,
119
+ onSuccess: (connection) => console.log('Connected!', connection)
120
+ });
121
+ }
122
+ </script>
123
+ ```
124
+
125
+ ### Vanilla JavaScript (CDN)
126
+
127
+ ```html
128
+ <button id="connect-btn">Connect Account</button>
129
+
130
+ <script src="https://cdn.jsdelivr.net/npm/@alter-vault/connect@latest/dist/alter-connect.umd.js"></script>
131
+ <script>
132
+ const alterConnect = AlterConnect.create();
133
+
134
+ document.getElementById('connect-btn').addEventListener('click', async () => {
135
+ const { session_token } = await fetch('/api/alter/session')
136
+ .then(r => r.json());
137
+
138
+ await alterConnect.open({
139
+ token: session_token,
140
+ onSuccess: (connection) => console.log('Connected!', connection)
141
+ });
142
+ });
143
+ </script>
144
+ ```
145
+
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
240
+
241
+ ### `AlterConnect.create(config?)`
242
+
243
+ Creates a new SDK instance.
244
+
245
+ ```javascript
246
+ const alterConnect = AlterConnect.create({
247
+ debug: false
248
+ });
249
+ ```
250
+
251
+ **Config Options:**
252
+
253
+ | Option | Type | Description | Default |
254
+ |--------|------|-------------|---------|
255
+ | `debug` | `boolean` | Enable debug logging | `false` |
256
+
257
+ **Note:** Branding (colors, fonts, logo) is now managed through the Developer Portal and fetched automatically at runtime.
258
+
259
+ ---
260
+
261
+ ### `alterConnect.open(options)`
262
+
263
+ Opens the Connect UI modal.
264
+
265
+ ```javascript
266
+ await alterConnect.open({
267
+ token: 'sess_abc123...',
268
+ onSuccess: (connection) => { /* ... */ },
269
+ onError: (error) => { /* ... */ },
270
+ onExit: () => { /* ... */ }
271
+ });
272
+ ```
273
+
274
+ **Options:**
275
+
276
+ | Parameter | Type | Required | Description |
277
+ |-----------|------|----------|-------------|
278
+ | `token` | `string` | ✅ Yes | Session token from your backend |
279
+ | `baseURL` | `string` | No | Alter API URL (usually auto-detected) |
280
+ | `onSuccess` | `function` | ✅ Yes | Called when connection succeeds |
281
+ | `onError` | `function` | No | Called when connection fails |
282
+ | `onExit` | `function` | No | Called when user closes modal |
283
+ | `onEvent` | `function` | No | Called for analytics events |
284
+
285
+ **Connection Object (onSuccess):**
286
+
287
+ ```typescript
288
+ {
289
+ connection_id: string; // Unique ID - store this!
290
+ provider: string; // e.g., 'google', 'slack'
291
+ provider_name: string; // e.g., 'Google', 'Slack'
292
+ account_identifier: string; // e.g., 'user@gmail.com'
293
+ timestamp: string; // ISO 8601 timestamp
294
+ operation: 'creation' | 'reauth';
295
+ scopes: string[]; // Granted OAuth scopes
296
+ status: 'active' | 'pending' | 'error';
297
+ metadata?: object; // Additional provider data
298
+ }
299
+ ```
300
+
301
+ **Error Object (onError):**
302
+
303
+ ```typescript
304
+ {
305
+ code: string; // e.g., 'invalid_token', 'popup_blocked'
306
+ message: string; // Human-readable message
307
+ details?: object; // Additional error context
308
+ }
309
+ ```
310
+
311
+ ---
312
+
313
+ ### `alterConnect.close()`
314
+
315
+ Manually closes the Connect UI.
316
+
317
+ ```javascript
318
+ alterConnect.close();
319
+ ```
320
+
321
+ ---
322
+
323
+ ### `alterConnect.destroy()`
324
+
325
+ Destroys the SDK instance and cleans up resources.
326
+
327
+ ```javascript
328
+ alterConnect.destroy();
329
+ ```
330
+
331
+ ---
332
+
333
+ ### `alterConnect.isOpen()`
334
+
335
+ Checks if the Connect UI is currently open.
336
+
337
+ ```javascript
338
+ if (alterConnect.isOpen()) {
339
+ console.log('Modal is open');
340
+ }
341
+ ```
342
+
343
+ **Returns:** `boolean`
344
+
345
+ ---
346
+
347
+ ### `alterConnect.getVersion()`
348
+
349
+ Gets the SDK version.
350
+
351
+ ```javascript
352
+ console.log(alterConnect.getVersion()); // "0.1.0"
353
+ ```
354
+
355
+ **Returns:** `string`
356
+
357
+ ## 🔒 Security Architecture
358
+
359
+ The SDK uses a **Plaid-style token architecture** for security:
360
+
361
+ ```
362
+ Frontend (Public) Backend (Secure) Alter API
363
+ │ │ │
364
+ │ 1. User clicks │ │
365
+ │ "Connect" │ │
366
+ │─────────────────────────>│ │
367
+ │ │ │
368
+ │ │ 2. Create session │
369
+ │ │ (with API key) │
370
+ │ │───────────────────────>│
371
+ │ │ │
372
+ │ │ 3. { session_token } │
373
+ │ │<───────────────────────│
374
+ │ │ │
375
+ │ 4. { session_token } │ │
376
+ │<─────────────────────────│ │
377
+ │ │ │
378
+ │ 5. SDK.open({ token }) │ │
379
+ │──────────────────────────┼───────────────────────>│
380
+ │ │ 6. OAuth flow │
381
+ │<─────────────────────────┴────────────────────────│
382
+ │ 7. onSuccess(connection)│ │
383
+ ```
384
+
385
+ **Key Security Features:**
386
+
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
392
+
393
+ ## 💡 Smart UX
394
+
395
+ ### Single Provider Flow
396
+
397
+ When only 1 provider is allowed, the SDK **skips the selection UI** and opens OAuth directly:
398
+
399
+ ```javascript
400
+ // Backend
401
+ allowed_providers: ['google']
402
+
403
+ // SDK behavior: Opens Google OAuth immediately (no modal)
404
+ ```
405
+
406
+ ### Multiple Provider Flow
407
+
408
+ When 2+ providers are allowed, the SDK shows a **provider selection modal**:
409
+
410
+ ```javascript
411
+ // Backend
412
+ allowed_providers: ['google', 'slack', 'microsoft']
413
+
414
+ // SDK behavior: Shows modal with provider grid
415
+ ```
416
+
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
496
+
497
+ Full TypeScript definitions included:
498
+
499
+ ```typescript
500
+ import AlterConnect, { Connection, AlterError } from '@alter-vault/connect';
501
+
502
+ const alterConnect = AlterConnect.create({
503
+ customization: { colors: { primary: '#6366f1' } }
504
+ });
505
+
506
+ await alterConnect.open({
507
+ token: sessionToken,
508
+ onSuccess: (connection: Connection) => {
509
+ // TypeScript knows all properties
510
+ console.log(connection.connection_id);
511
+ console.log(connection.provider);
512
+ },
513
+ onError: (error: AlterError) => {
514
+ console.error(error.code, error.message);
515
+ }
516
+ });
517
+ ```
518
+
519
+ ## 🌐 Browser Support
520
+
521
+ - Chrome/Edge 90+
522
+ - Firefox 88+
523
+ - Safari 14+
524
+ - Mobile browsers (iOS Safari 14+, Chrome Mobile)
525
+
526
+ ## 📊 Bundle Size
527
+
528
+ - **UMD:** 21KB minified
529
+ - **ESM:** 21KB minified
530
+ - **CJS:** 21KB minified
531
+ - **Zero runtime dependencies**
532
+
533
+ ## 🐛 Troubleshooting
534
+
535
+ ### Popup Blocked
536
+
537
+ **Problem:** Browser blocks the OAuth popup
538
+
539
+ **Solution:** Ensure `alterConnect.open()` is called directly from a user interaction (click event):
540
+
541
+ ```javascript
542
+ // ❌ Bad - may be blocked
543
+ button.addEventListener('click', async () => {
544
+ const token = await fetchToken(); // Async delay
545
+ alterConnect.open({ token }); // May be blocked
546
+ });
547
+
548
+ // ✅ Good - no async delay before open()
549
+ button.addEventListener('click', () => {
550
+ fetchToken().then(token => {
551
+ alterConnect.open({ token }); // Called synchronously
552
+ });
553
+ });
554
+ ```
555
+
556
+ ### Session Token Expired
557
+
558
+ **Problem:** `session_expired` error
559
+
560
+ **Solution:** Session tokens expire after 10 minutes. Create a new session token.
561
+
562
+ ### CORS Errors
563
+
564
+ **Problem:** CORS error when calling Alter API
565
+
566
+ **Solution:** Session tokens should be created from your **backend**, not frontend. The SDK handles all frontend API calls.
567
+
568
+ ## 🤝 Support
569
+
570
+ - **Documentation:** [https://docs.alterai.dev](https://docs.alterai.dev)
571
+ - **API Reference:** [https://api.alterai.dev/docs](https://api.alterai.dev/docs)
572
+ - **Issues:** [GitHub Issues](https://github.com/alter-ai/alter-vault/issues)
573
+ - **Email:** support@alterai.dev
574
+
575
+ ## 📄 License
576
+
577
+ MIT License - See [LICENSE](LICENSE) file for details
@@ -0,0 +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;
2
+ //# sourceMappingURL=alter-connect.cjs.js.map