@alter-ai/connect 0.2.0 → 0.3.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 CHANGED
@@ -1,8 +1,10 @@
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
 
@@ -18,26 +20,25 @@ Or use via CDN:
18
20
 
19
21
  ### 2. Get a Session Token from Your Backend
20
22
 
21
- Your backend creates a short-lived session token using your API key:
23
+ Your backend creates a short-lived session token using the [Alter SDK](https://www.npmjs.com/package/@alter-ai/alter-sdk):
22
24
 
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
- })
25
+ ```typescript
26
+ // YOUR backend (Node.js example using @alter-ai/alter-sdk)
27
+ import { AlterVault, ActorType } from "@alter-ai/alter-sdk";
28
+
29
+ const vault = new AlterVault({
30
+ apiKey: process.env.ALTER_API_KEY!,
31
+ actorType: ActorType.BACKEND_SERVICE,
32
+ actorIdentifier: "my-backend",
33
+ });
34
+
35
+ const session = await vault.createConnectSession({
36
+ endUser: { id: "user_123" },
37
+ allowedProviders: ["google", "slack", "github"],
38
+ returnUrl: "https://yourapp.com/callback",
38
39
  });
39
40
 
40
- const { session_token } = await response.json();
41
+ const session_token = session.sessionToken;
41
42
  ```
42
43
 
43
44
  ### 3. Open the Connect UI
@@ -45,7 +46,7 @@ const { session_token } = await response.json();
45
46
  ```javascript
46
47
  import AlterConnect from '@alter-ai/connect';
47
48
 
48
- // Initialize SDK
49
+ // Initialize SDK (no API key needed!)
49
50
  const alterConnect = AlterConnect.create();
50
51
 
51
52
  // Get session token from YOUR backend
@@ -54,19 +55,23 @@ const { session_token } = await fetch('/api/alter/session').then(r => r.json());
54
55
  // Open Connect UI
55
56
  await alterConnect.open({
56
57
  token: session_token,
57
- onSuccess: (connection) => {
58
- console.log('Connected!', connection);
59
- // Save connection.connection_id to your database
58
+ onSuccess: (connections) => {
59
+ console.log('Connected!', connections);
60
+ // Save each connection.connection_id to your database
61
+ connections.forEach(conn => console.log(conn.provider, conn.connection_id));
60
62
  },
61
63
  onError: (error) => {
62
64
  console.error('Failed:', error);
65
+ },
66
+ onExit: () => {
67
+ console.log('User closed the window');
63
68
  }
64
69
  });
65
70
  ```
66
71
 
67
- That's it! The SDK handles the OAuth flow, popup windows, and all security.
72
+ That's it! The SDK handles the OAuth flow, popup windows, mobile redirects, and all security.
68
73
 
69
- ## 📖 Framework Examples
74
+ ## Framework Examples
70
75
 
71
76
  ### React
72
77
 
@@ -83,8 +88,8 @@ function ConnectButton() {
83
88
 
84
89
  await alterConnect.open({
85
90
  token: session_token,
86
- onSuccess: (connection) => {
87
- console.log('Connected!', connection);
91
+ onSuccess: (connections) => {
92
+ console.log('Connected!', connections);
88
93
  }
89
94
  });
90
95
  };
@@ -116,7 +121,7 @@ async function handleConnect() {
116
121
 
117
122
  await alterConnect.value.open({
118
123
  token: session_token,
119
- onSuccess: (connection) => console.log('Connected!', connection)
124
+ onSuccess: (connections) => console.log('Connected!', connections)
120
125
  });
121
126
  }
122
127
  </script>
@@ -137,106 +142,13 @@ async function handleConnect() {
137
142
 
138
143
  await alterConnect.open({
139
144
  token: session_token,
140
- onSuccess: (connection) => console.log('Connected!', connection)
145
+ onSuccess: (connections) => console.log('Connected!', connections)
141
146
  });
142
147
  });
143
148
  </script>
144
149
  ```
145
150
 
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
151
+ ## API Reference
240
152
 
241
153
  ### `AlterConnect.create(config?)`
242
154
 
@@ -244,57 +156,61 @@ Creates a new SDK instance.
244
156
 
245
157
  ```javascript
246
158
  const alterConnect = AlterConnect.create({
247
- debug: false
159
+ debug: true // Enable console logging (default: false)
248
160
  });
249
161
  ```
250
162
 
251
- **Config Options:**
252
-
253
163
  | Option | Type | Description | Default |
254
164
  |--------|------|-------------|---------|
255
165
  | `debug` | `boolean` | Enable debug logging | `false` |
256
166
 
257
- **Note:** Branding (colors, fonts, logo) is now managed through the Developer Portal and fetched automatically at runtime.
167
+ **Note:** Visual customization (colors, fonts, logo) is configured via the Developer Portal branding settings. The backend-served Connect UI applies your branding automatically.
258
168
 
259
169
  ---
260
170
 
261
171
  ### `alterConnect.open(options)`
262
172
 
263
- Opens the Connect UI modal.
173
+ Opens the Connect UI. On desktop, opens a centered popup window (500x700px). On mobile, uses a full-page redirect flow.
264
174
 
265
175
  ```javascript
266
176
  await alterConnect.open({
267
177
  token: 'sess_abc123...',
268
- onSuccess: (connection) => { /* ... */ },
178
+ // baseURL is auto-detected; override only for custom deployments
179
+ onSuccess: (connections) => { /* ... */ },
269
180
  onError: (error) => { /* ... */ },
270
- onExit: () => { /* ... */ }
181
+ onExit: () => { /* ... */ },
182
+ onEvent: (eventName, metadata) => { /* ... */ }
271
183
  });
272
184
  ```
273
185
 
274
- **Options:**
275
-
276
186
  | Parameter | Type | Required | Description |
277
187
  |-----------|------|----------|-------------|
278
- | `token` | `string` | Yes | Session token from your backend |
188
+ | `token` | `string` | Yes | Session token from your backend |
279
189
  | `baseURL` | `string` | No | Alter API URL (usually auto-detected) |
280
- | `onSuccess` | `function` | Yes | Called when connection succeeds |
190
+ | `onSuccess` | `function` | Yes | Called with array of connections on success |
281
191
  | `onError` | `function` | No | Called when connection fails |
282
- | `onExit` | `function` | No | Called when user closes modal |
192
+ | `onExit` | `function` | No | Called when user closes popup |
283
193
  | `onEvent` | `function` | No | Called for analytics events |
284
194
 
285
- **Connection Object (onSuccess):**
195
+ **Connections Array (onSuccess):**
196
+
197
+ `onSuccess` receives an array of `Connection` objects (multi-provider flow):
286
198
 
287
199
  ```typescript
200
+ // Each connection in the array:
288
201
  {
289
202
  connection_id: string; // Unique ID - store this!
290
203
  provider: string; // e.g., 'google', 'slack'
291
204
  provider_name: string; // e.g., 'Google', 'Slack'
292
205
  account_identifier: string; // e.g., 'user@gmail.com'
293
206
  timestamp: string; // ISO 8601 timestamp
294
- operation: 'creation' | 'reauth';
295
- scopes: string[]; // Granted OAuth scopes
207
+ operation: 'creation' | 'reauth' | 'grant';
208
+ scopes: string[]; // Granted OAuth scopes
296
209
  status: 'active' | 'pending' | 'error';
297
- metadata?: object; // Additional provider data
210
+ metadata?: {
211
+ account_display_name?: string;
212
+ account_email?: string;
213
+ };
298
214
  }
299
215
  ```
300
216
 
@@ -330,6 +246,22 @@ alterConnect.destroy();
330
246
 
331
247
  ---
332
248
 
249
+ ### `alterConnect.on(event, handler)`
250
+
251
+ Register an event listener. Returns an unsubscribe function.
252
+
253
+ ```javascript
254
+ const unsubscribe = alterConnect.on('success', (connection) => {
255
+ console.log('Connected:', connection);
256
+ });
257
+
258
+ // Later: unsubscribe();
259
+ ```
260
+
261
+ **Events:** `success`, `error`, `exit`, `close`, `event`
262
+
263
+ ---
264
+
333
265
  ### `alterConnect.isOpen()`
334
266
 
335
267
  Checks if the Connect UI is currently open.
@@ -340,8 +272,6 @@ if (alterConnect.isOpen()) {
340
272
  }
341
273
  ```
342
274
 
343
- **Returns:** `boolean`
344
-
345
275
  ---
346
276
 
347
277
  ### `alterConnect.getVersion()`
@@ -349,14 +279,37 @@ if (alterConnect.isOpen()) {
349
279
  Gets the SDK version.
350
280
 
351
281
  ```javascript
352
- console.log(alterConnect.getVersion()); // "0.1.0"
282
+ console.log(alterConnect.getVersion()); // "0.2.0"
353
283
  ```
354
284
 
355
- **Returns:** `string`
285
+ ## Mobile Support
356
286
 
357
- ## 🔒 Security Architecture
287
+ The SDK automatically detects mobile devices and switches to an optimized flow:
358
288
 
359
- The SDK uses a **Plaid-style token architecture** for security:
289
+ | Device | Flow | How It Works |
290
+ |--------|------|-------------|
291
+ | Desktop | Popup | Opens centered popup (500x700px), communicates via postMessage |
292
+ | Phone (<=480px) | Redirect | Full-page redirect, returns via URL params |
293
+ | Tablet (portrait) | Redirect | Full-page redirect for better UX |
294
+ | Tablet (landscape) | Popup | Uses popup flow like desktop |
295
+
296
+ No code changes needed — the SDK handles device detection automatically.
297
+
298
+ For mobile redirect flow, include a `return_url` when creating the session:
299
+
300
+ ```javascript
301
+ // Backend session creation with mobile support
302
+ body: JSON.stringify({
303
+ end_user: { id: 'user_123', email: 'user@example.com' },
304
+ allowed_providers: ['google', 'slack'],
305
+ allowed_origin: 'https://yourapp.com', // For desktop popup (postMessage)
306
+ return_url: 'https://yourapp.com/' // For mobile redirect (return destination)
307
+ })
308
+ ```
309
+
310
+ ## Security Architecture
311
+
312
+ The SDK uses a **Plaid-style token architecture**:
360
313
 
361
314
  ```
362
315
  Frontend (Public) Backend (Secure) Alter API
@@ -375,140 +328,63 @@ Frontend (Public) Backend (Secure) Alter API
375
328
  │ 4. { session_token } │ │
376
329
  │<─────────────────────────│ │
377
330
  │ │ │
378
- │ 5. SDK.open({ token })
379
- │──────────────────────────┼───────────────────────>│
380
- │ │ 6. OAuth flow
381
- │<─────────────────────────┴────────────────────────│
382
- 7. onSuccess(connection)│ │
331
+ │ 5. SDK opens popup to backend Connect UI
332
+ │───────────────────────────────────────────────────>│
333
+ │ │
334
+ │ 6. Backend handles auth + OAuth │
335
+ │ │
336
+ │ 7. postMessage(connection) │
337
+ │<──────────────────────────────────────────────────│
338
+ │ 8. onSuccess(connections) │ │
383
339
  ```
384
340
 
385
341
  **Key Security Features:**
386
342
 
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
343
+ - **API keys never touch frontend** - Only backend has API key
344
+ - **Session tokens are short-lived** - Expire after 10 minutes
345
+ - **Session tokens are single-use** - Can only create one connection
346
+ - **Session tokens are scoped** - Locked to specific user & providers
347
+ - **No secrets in browser** - SDK is purely a popup launcher + callback listener
392
348
 
393
- ## 💡 Smart UX
349
+ ## Smart UX
394
350
 
395
351
  ### Single Provider Flow
396
352
 
397
- When only 1 provider is allowed, the SDK **skips the selection UI** and opens OAuth directly:
353
+ When only 1 provider is allowed, the Connect UI **skips the selection screen** and opens OAuth directly:
398
354
 
399
355
  ```javascript
400
- // Backend
401
- allowed_providers: ['google']
402
-
403
- // SDK behavior: Opens Google OAuth immediately (no modal)
356
+ // Backend: allowed_providers: ['google']
357
+ // Result: Opens Google OAuth immediately (no selection screen)
404
358
  ```
405
359
 
406
360
  ### Multiple Provider Flow
407
361
 
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
362
+ When 2+ providers are allowed, the Connect UI shows a **provider selection screen**:
473
363
 
474
364
  ```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
- });
365
+ // Backend: allowed_providers: ['google', 'slack', 'github']
366
+ // Result: Shows provider selection, user picks one
493
367
  ```
494
368
 
495
- ## 📦 TypeScript Support
369
+ ## TypeScript Support
496
370
 
497
371
  Full TypeScript definitions included:
498
372
 
499
373
  ```typescript
500
- import AlterConnect, { Connection, AlterError } from '@alter-ai/connect';
374
+ import AlterConnect, {
375
+ AlterConnectConfig,
376
+ Connection,
377
+ AlterError
378
+ } from '@alter-ai/connect';
501
379
 
502
- const alterConnect = AlterConnect.create({
503
- customization: { colors: { primary: '#6366f1' } }
504
- });
380
+ const alterConnect = AlterConnect.create({ debug: true });
505
381
 
506
382
  await alterConnect.open({
507
383
  token: sessionToken,
508
- onSuccess: (connection: Connection) => {
509
- // TypeScript knows all properties
384
+ onSuccess: (connections: Connection[]) => {
510
385
  console.log(connection.connection_id);
511
386
  console.log(connection.provider);
387
+ console.log(connection.scopes);
512
388
  },
513
389
  onError: (error: AlterError) => {
514
390
  console.error(error.code, error.message);
@@ -516,21 +392,24 @@ await alterConnect.open({
516
392
  });
517
393
  ```
518
394
 
519
- ## 🌐 Browser Support
395
+ ## Bundle Size
396
+
397
+ | Format | Size | Gzipped |
398
+ |--------|------|---------|
399
+ | **CJS** | ~10KB | ~3.5KB |
400
+ | **ESM** | ~10KB | ~3.4KB |
401
+ | **UMD** | ~11KB | ~3.5KB |
402
+
403
+ Zero runtime dependencies.
404
+
405
+ ## Browser Support
520
406
 
521
407
  - Chrome/Edge 90+
522
408
  - Firefox 88+
523
409
  - Safari 14+
524
410
  - Mobile browsers (iOS Safari 14+, Chrome Mobile)
525
411
 
526
- ## 📊 Bundle Size
527
-
528
- - **UMD:** 21KB minified
529
- - **ESM:** 21KB minified
530
- - **CJS:** 21KB minified
531
- - **Zero runtime dependencies**
532
-
533
- ## 🐛 Troubleshooting
412
+ ## Troubleshooting
534
413
 
535
414
  ### Popup Blocked
536
415
 
@@ -539,13 +418,13 @@ await alterConnect.open({
539
418
  **Solution:** Ensure `alterConnect.open()` is called directly from a user interaction (click event):
540
419
 
541
420
  ```javascript
542
- // Bad - may be blocked
421
+ // Bad - may be blocked
543
422
  button.addEventListener('click', async () => {
544
423
  const token = await fetchToken(); // Async delay
545
424
  alterConnect.open({ token }); // May be blocked
546
425
  });
547
426
 
548
- // Good - no async delay before open()
427
+ // Good - no async delay before open()
549
428
  button.addEventListener('click', () => {
550
429
  fetchToken().then(token => {
551
430
  alterConnect.open({ token }); // Called synchronously
@@ -565,13 +444,12 @@ button.addEventListener('click', () => {
565
444
 
566
445
  **Solution:** Session tokens should be created from your **backend**, not frontend. The SDK handles all frontend API calls.
567
446
 
568
- ## 🤝 Support
447
+ ## Support
569
448
 
570
449
  - **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)
450
+ - **Issues:** [GitHub Issues](https://github.com/AlterAIDev/Alter-Vault/issues)
573
451
  - **Email:** support@alterai.dev
574
452
 
575
- ## 📄 License
453
+ ## License
576
454
 
577
455
  MIT License - See [LICENSE](LICENSE) file for details
@@ -1,2 +1,2 @@
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;
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 i=this.events.get(e);i&&i.delete(t)}emit(e,...t){const i=this.events.get(e);i&&i.forEach(i=>{try{i(...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 i{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 s(){const e=navigator.userAgent||navigator.vendor||window.opera,t=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(e),i="ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0,s=window.innerWidth<=768;return t||i&&s}function n(){return s()&&window.innerWidth<=480?"phone":s()&&window.innerWidth>480&&window.innerWidth<=1024?"tablet":"desktop"}function o(e){return"reauth"===e?"reauth":"grant"===e?"grant":"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,expectedOrigin:e.expectedOrigin||""};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){if(!this.options.expectedOrigin)try{const t=new URL(e);this.options.expectedOrigin=t.origin,this.log("Derived expected origin:",this.options.expectedOrigin)}catch{return this.log("Failed to parse OAuth URL for origin validation:",e),void this.options.onError({code:"invalid_oauth_url",message:"Failed to determine origin from OAuth URL. Cannot proceed securely."})}!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 i=sessionStorage.getItem("alter_oauth_state");if(!i)return!1;try{const s=JSON.parse(i);if(Date.now()-s.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 i=n.get("connection_id"),c=n.get("provider"),h=n.get("account_identifier");if(!i||!c||!h)return a(s.returnUrl),t({code:"invalid_response",message:"OAuth redirect returned incomplete connection data"}),!0;const l={connection_id:i,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(s.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(s.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,i=window.screenY+(window.outerHeight-this.options.popupHeight)/2,s=[`width=${this.options.popupWidth}`,`height=${this.options.popupHeight}`,`left=${t}`,`top=${i}`,"resizable=yes","scrollbars=yes","status=yes"].join(",");this.log("Opening OAuth popup:",e),this.popup=window.open(e,"alter_oauth_popup",s),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){if(this.log("OAuth success"),Array.isArray(t.connections)){this.log("Multi-provider success:",t.connections.length,"connections");const e=[];for(const i of t.connections)i.connection_id&&i.provider?e.push({connection_id:i.connection_id,provider:i.provider,provider_name:i.provider_name||i.provider,account_identifier:i.account_identifier||"",timestamp:i.timestamp||(new Date).toISOString(),operation:o(i.operation),scopes:Array.isArray(i.scopes)?i.scopes:[],status:r(i.status),metadata:i.metadata}):this.log("Skipping invalid connection item:",i);return 0===e.length?(this.settled=!0,this.close(),void this.options.onError({code:"invalid_response",message:"Server returned empty connections array"})):(this.settled=!0,this.close(),void this.options.onSuccess(e))}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 i={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([i])}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(s={}){this._oauthHandler=null,this._baseURL="https://api.alterai.dev",this._perOpenCleanups=[],this.config=function(e){return{debug:e.debug??!1}}(s),this.eventEmitter=new t,this.stateManager=new i,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 /sdk/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 i=`${this._baseURL}/sdk/oauth/connect#session=${encodeURIComponent(t.token)}`;e(this.config,"Connect URL:",i),this._oauthHandler.startOAuth(i),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,i)=>{e.onEvent(t,i)}))}createError(e,t,i){const s=new Error(t);return s.code=e,s.details=i,s}}exports.default=l;
2
2
  //# sourceMappingURL=alter-connect.cjs.js.map