@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 +144 -266
- 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/oauth/handler.d.ts +3 -2
- package/dist/types/oauth/handler.d.ts.map +1 -1
- package/dist/types/types/index.d.ts +2 -2
- package/dist/types/types/index.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
|
@@ -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
|
|
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
|
-
```
|
|
24
|
-
// YOUR backend
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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: (
|
|
58
|
-
console.log('Connected!',
|
|
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
|
-
##
|
|
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: (
|
|
87
|
-
console.log('Connected!',
|
|
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: (
|
|
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: (
|
|
145
|
+
onSuccess: (connections) => console.log('Connected!', connections)
|
|
141
146
|
});
|
|
142
147
|
});
|
|
143
148
|
</script>
|
|
144
149
|
```
|
|
145
150
|
|
|
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
|
|
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:**
|
|
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
|
|
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
|
-
|
|
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` |
|
|
188
|
+
| `token` | `string` | Yes | Session token from your backend |
|
|
279
189
|
| `baseURL` | `string` | No | Alter API URL (usually auto-detected) |
|
|
280
|
-
| `onSuccess` | `function` |
|
|
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
|
|
192
|
+
| `onExit` | `function` | No | Called when user closes popup |
|
|
283
193
|
| `onEvent` | `function` | No | Called for analytics events |
|
|
284
194
|
|
|
285
|
-
**
|
|
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[];
|
|
207
|
+
operation: 'creation' | 'reauth' | 'grant';
|
|
208
|
+
scopes: string[]; // Granted OAuth scopes
|
|
296
209
|
status: 'active' | 'pending' | 'error';
|
|
297
|
-
metadata?:
|
|
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.
|
|
282
|
+
console.log(alterConnect.getVersion()); // "0.2.0"
|
|
353
283
|
```
|
|
354
284
|
|
|
355
|
-
|
|
285
|
+
## Mobile Support
|
|
356
286
|
|
|
357
|
-
|
|
287
|
+
The SDK automatically detects mobile devices and switches to an optimized flow:
|
|
358
288
|
|
|
359
|
-
|
|
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
|
|
379
|
-
|
|
380
|
-
│ │
|
|
381
|
-
|
|
382
|
-
│
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
##
|
|
349
|
+
## Smart UX
|
|
394
350
|
|
|
395
351
|
### Single Provider Flow
|
|
396
352
|
|
|
397
|
-
When only 1 provider is allowed, the
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
##
|
|
369
|
+
## TypeScript Support
|
|
496
370
|
|
|
497
371
|
Full TypeScript definitions included:
|
|
498
372
|
|
|
499
373
|
```typescript
|
|
500
|
-
import AlterConnect, {
|
|
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: (
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
##
|
|
447
|
+
## Support
|
|
569
448
|
|
|
570
449
|
- **Documentation:** [https://docs.alterai.dev](https://docs.alterai.dev)
|
|
571
|
-
- **
|
|
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
|
-
##
|
|
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
|