@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 +128 -245
- package/dist/alter-connect.cjs.js +1 -1
- package/dist/alter-connect.cjs.js.map +1 -1
- package/dist/alter-connect.esm.js +1 -1
- package/dist/alter-connect.esm.js.map +1 -1
- package/dist/alter-connect.umd.js +1 -1
- package/dist/alter-connect.umd.js.map +1 -1
- package/dist/types/core/alter-connect.d.ts +3 -9
- package/dist/types/core/alter-connect.d.ts.map +1 -1
- package/dist/types/core/config.d.ts +1 -11
- package/dist/types/core/config.d.ts.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/oauth/handler.d.ts +6 -0
- package/dist/types/oauth/handler.d.ts.map +1 -1
- package/dist/types/state/manager.d.ts +2 -7
- package/dist/types/state/manager.d.ts.map +1 -1
- package/dist/types/types/index.d.ts +0 -9
- package/dist/types/types/index.d.ts.map +1 -1
- package/dist/types/utils/mobile.d.ts +6 -0
- package/dist/types/utils/mobile.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/types/api/client.d.ts +0 -15
- package/dist/types/api/client.d.ts.map +0 -1
- package/dist/types/ui/modal.d.ts +0 -22
- package/dist/types/ui/modal.d.ts.map +0 -1
- package/dist/types/ui/provider-list.d.ts +0 -16
- package/dist/types/ui/provider-list.d.ts.map +0 -1
- package/dist/types/ui/search-bar.d.ts +0 -15
- package/dist/types/ui/search-bar.d.ts.map +0 -1
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
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
##
|
|
80
|
+
## Framework Examples
|
|
70
81
|
|
|
71
82
|
### React
|
|
72
83
|
|
|
73
84
|
```jsx
|
|
74
85
|
import { useState } from 'react';
|
|
75
|
-
import AlterConnect from '@alter-
|
|
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-
|
|
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-
|
|
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
|
-
##
|
|
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:**
|
|
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
|
|
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` |
|
|
194
|
+
| `token` | `string` | Yes | Session token from your backend |
|
|
279
195
|
| `baseURL` | `string` | No | Alter API URL (usually auto-detected) |
|
|
280
|
-
| `onSuccess` | `function` |
|
|
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
|
|
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[];
|
|
211
|
+
scopes: string[]; // Granted OAuth scopes
|
|
296
212
|
status: 'active' | 'pending' | 'error';
|
|
297
|
-
metadata?:
|
|
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
|
|
285
|
+
console.log(alterConnect.getVersion()); // "0.2.1"
|
|
353
286
|
```
|
|
354
287
|
|
|
355
|
-
|
|
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
|
-
##
|
|
314
|
+
## Security Architecture
|
|
358
315
|
|
|
359
|
-
The SDK uses a **Plaid-style token architecture
|
|
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
|
|
379
|
-
|
|
380
|
-
│ │
|
|
381
|
-
|
|
382
|
-
│
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
##
|
|
353
|
+
## Smart UX
|
|
394
354
|
|
|
395
355
|
### Single Provider Flow
|
|
396
356
|
|
|
397
|
-
When only 1 provider is allowed, the
|
|
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
|
-
|
|
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
|
|
366
|
+
When 2+ providers are allowed, the Connect UI shows a **provider selection screen**:
|
|
409
367
|
|
|
410
368
|
```javascript
|
|
411
|
-
// Backend
|
|
412
|
-
|
|
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
|
-
##
|
|
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, {
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
##
|
|
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/
|
|
455
|
+
- **Issues:** [GitHub Issues](https://github.com/AlterAIDev/Alter-Vault/issues)
|
|
573
456
|
- **Email:** support@alterai.dev
|
|
574
457
|
|
|
575
|
-
##
|
|
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="×",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
|