@alter-ai/connect 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Alter Connect SDK
2
2
 
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.
3
+ A lightweight JavaScript SDK for embedding OAuth integrations into an application. The SDK opens Alter Connect in a desktop popup, uses full-page navigation on mobile-classified devices, and validates completion data before invoking typed callbacks.
4
4
 
5
- **~10KB minified | Zero dependencies | TypeScript included**
5
+ **Typed OAuth callbacks | Zod-validated payloads | TypeScript included**
6
6
 
7
7
  ## Quick Start
8
8
 
@@ -12,18 +12,19 @@ A lightweight JavaScript SDK for embedding OAuth integrations into your applicat
12
12
  npm install @alter-ai/connect
13
13
  ```
14
14
 
15
- Or use via CDN:
15
+ The package metadata declares Node.js 20.19 or later for npm tooling. Browser
16
+ execution does not require Node.js. Or use the UMD bundle via CDN:
16
17
 
17
18
  ```html
18
19
  <script src="https://cdn.jsdelivr.net/npm/@alter-ai/connect@latest/dist/alter-connect.umd.js"></script>
19
20
  ```
20
21
 
21
- ### 2. Get a Session Token from Your Backend
22
+ ### 2. Get a Session Token from the Application Backend
22
23
 
23
- Your backend creates a short-lived session token using the [Alter SDK](https://www.npmjs.com/package/@alter-ai/alter-sdk):
24
+ The application backend creates a short-lived session token using the [Alter SDK](https://www.npmjs.com/package/@alter-ai/alter-sdk):
24
25
 
25
26
  ```typescript
26
- // YOUR backend (Node.js example using @alter-ai/alter-sdk)
27
+ // Application backend (Node.js example using @alter-ai/alter-sdk)
27
28
  import { App, CallerType } from "@alter-ai/alter-sdk";
28
29
 
29
30
  const alterApp = new App({
@@ -33,11 +34,19 @@ const alterApp = new App({
33
34
  });
34
35
 
35
36
  const session = await alterApp.createConnectSession({
36
- allowedProviders: ["google", "slack", "github"],
37
- returnUrl: "https://yourapp.com/callback",
37
+ allowedProviders: ["provider-id"],
38
+ allowedOrigin: "https://app.example.com",
39
+ // Optional. When the user approves access, the Connect UI also lets them set
40
+ // their own usage limits on the connection being created — deny rules,
41
+ // human approval, time windows, request quotas, and operation/parameter
42
+ // rules. They can only narrow what they are granting, never widen it, and
43
+ // they can change or remove the limits later in their wallet.
44
+ //
45
+ // This step is enabled by default. To hide it, pass:
46
+ // allowUserPolicyRules: false,
38
47
  });
39
48
 
40
- const session_token = session.sessionToken;
49
+ const sessionToken = session.sessionToken;
41
50
  ```
42
51
 
43
52
  ### 3. Open the Connect UI
@@ -48,104 +57,60 @@ import AlterConnect from '@alter-ai/connect';
48
57
  // Initialize SDK (no API key needed!)
49
58
  const alterConnect = AlterConnect.create();
50
59
 
51
- // Get session token from YOUR backend
52
- const { session_token } = await fetch('/api/alter/session').then(r => r.json());
53
-
54
- // Open Connect UI
55
- await alterConnect.open({
56
- token: session_token,
57
- onSuccess: (connections) => {
58
- console.log('Connected!', connections);
59
- // Save each connection.grant_id to your database
60
- connections.forEach(conn => console.log(conn.provider, conn.grant_id));
61
- },
62
- onError: (error) => {
63
- console.error('Failed:', error);
64
- },
65
- onExit: () => {
66
- console.log('User closed the window');
67
- }
68
- });
69
- ```
70
-
71
- That's it! The SDK handles the OAuth flow, popup windows, mobile redirects, and all security.
72
-
73
- ## Framework Examples
74
-
75
- ### React
76
-
77
- ```jsx
78
- import { useState } from 'react';
79
- import AlterConnect from '@alter-ai/connect';
80
-
81
- function ConnectButton() {
82
- const [alterConnect] = useState(() => AlterConnect.create());
83
-
84
- const handleConnect = async () => {
85
- const { session_token } = await fetch('/api/alter/session')
86
- .then(r => r.json());
87
-
88
- await alterConnect.open({
89
- token: session_token,
90
- onSuccess: (connections) => {
91
- console.log('Connected!', connections);
92
- }
93
- });
94
- };
95
-
96
- return <button onClick={handleConnect}>Connect Account</button>;
60
+ const connectButton = document.querySelector('#connect-button');
61
+ if (!(connectButton instanceof HTMLButtonElement)) {
62
+ throw new Error('Connect button is missing');
97
63
  }
98
- ```
99
-
100
- ### Vue
101
-
102
- ```vue
103
- <template>
104
- <button @click="handleConnect">Connect Account</button>
105
- </template>
106
-
107
- <script setup>
108
- import { ref, onMounted } from 'vue';
109
- import AlterConnect from '@alter-ai/connect';
110
-
111
- const alterConnect = ref(null);
112
-
113
- onMounted(() => {
114
- alterConnect.value = AlterConnect.create();
115
- });
116
-
117
- async function handleConnect() {
118
- const { session_token } = await fetch('/api/alter/session')
119
- .then(r => r.json());
64
+ connectButton.disabled = true;
65
+
66
+ // Prefetch before the user interacts. Browsers may block a popup opened only
67
+ // after an awaited network request has detached it from the click gesture.
68
+ let sessionToken = null;
69
+ fetch('/api/alter/session')
70
+ .then(response => response.json())
71
+ .then(({ session_token }) => {
72
+ sessionToken = session_token;
73
+ connectButton.disabled = false;
74
+ })
75
+ .catch(() => {
76
+ console.error('Unable to prepare the Connect session');
77
+ });
120
78
 
121
- await alterConnect.value.open({
122
- token: session_token,
123
- onSuccess: (connections) => console.log('Connected!', connections)
79
+ connectButton.addEventListener('click', () => {
80
+ if (!sessionToken) return;
81
+ void alterConnect.open({
82
+ token: sessionToken,
83
+ onSuccess: (grants, completion) => {
84
+ console.log('Connected!', grants);
85
+ grants.forEach(grant => console.log(grant.provider, grant.grant_id));
86
+ completion.failedGrants.forEach(failure => {
87
+ console.warn(failure.providerId, failure.reason, failure.message);
88
+ });
89
+ },
90
+ onError: (error) => {
91
+ console.error('Failed:', error);
92
+ },
93
+ onExit: () => {
94
+ console.log('User closed the window');
95
+ }
124
96
  });
125
- }
126
- </script>
97
+ });
127
98
  ```
128
99
 
129
- ### Vanilla JavaScript (CDN)
100
+ The package launches the hosted OAuth Connect UI and validates desktop popup results before callbacks receive them.
130
101
 
131
- ```html
132
- <button id="connect-btn">Connect Account</button>
102
+ ## Framework Examples
133
103
 
134
- <script src="https://cdn.jsdelivr.net/npm/@alter-ai/connect@latest/dist/alter-connect.umd.js"></script>
135
- <script>
136
- const alterConnect = AlterConnect.create();
104
+ React, Vue, Angular, Svelte, and vanilla applications use the same pattern:
137
105
 
138
- document.getElementById('connect-btn').addEventListener('click', async () => {
139
- const { session_token } = await fetch('/api/alter/session')
140
- .then(r => r.json());
106
+ 1. Create one reusable `AlterConnect` instance.
107
+ 2. Fetch a session token before enabling the Connect button.
108
+ 3. Call `open()` directly in the click handler so popup activation remains attached to the user gesture.
109
+ 4. Destroy the instance when the owning component unmounts.
141
110
 
142
- await alterConnect.open({
143
- token: session_token,
144
- onSuccess: (connections) => console.log('Connected!', connections)
145
- });
146
- });
147
- </script>
148
- ```
111
+ The CDN bundle exposes the same class as `window.AlterConnect`. CommonJS
112
+ consumers read the default export with
113
+ `require('@alter-ai/connect').default`.
149
114
 
150
115
  ## API Reference
151
116
 
@@ -162,19 +127,20 @@ const alterConnect = AlterConnect.create({
162
127
  | Option | Type | Description | Default |
163
128
  |--------|------|-------------|---------|
164
129
  | `debug` | `boolean` | Enable debug logging | `false` |
130
+ | `baseURL` | `string` | Reserved and unsupported. Passing any value throws. | — |
165
131
 
166
- **Note:** Visual customization (colors, fonts, logo) is configured via the Developer Portal branding settings. The backend-served Connect UI applies your branding automatically.
132
+ **Note:** Visual customization (colors, fonts, logo) is configured via the Developer Portal branding settings. Alter Connect applies the configured branding automatically.
167
133
 
168
134
  ---
169
135
 
170
136
  ### `alterConnect.open(options)`
171
137
 
172
- Opens the Connect UI. On desktop, opens a centered popup window (500x700px). On mobile, uses a full-page redirect flow.
138
+ Attempts to open Connect. Desktop uses a centered popup window (500×700 px). Mobile-classified devices use full-page navigation, with the limitation documented under [Mobile behavior](#mobile-behavior).
173
139
 
174
140
  ```javascript
175
141
  await alterConnect.open({
176
142
  token: 'sess_abc123...',
177
- onSuccess: (connections) => { /* ... */ },
143
+ onSuccess: (grants, completion) => { /* ... */ },
178
144
  onError: (error) => { /* ... */ },
179
145
  onExit: () => { /* ... */ },
180
146
  onEvent: (eventName, metadata) => { /* ... */ }
@@ -183,51 +149,65 @@ await alterConnect.open({
183
149
 
184
150
  | Parameter | Type | Required | Description |
185
151
  |-----------|------|----------|-------------|
186
- | `token` | `string` | Yes | Session token from your backend |
187
- | `onSuccess` | `function` | Yes | Called with array of connections on success |
188
- | `onError` | `function` | No | Called when connection fails |
189
- | `onExit` | `function` | No | Called when user closes popup |
190
- | `onEvent` | `function` | No | Called for analytics events |
152
+ | `token` | `string` | Yes | Short-lived session token created by the application backend with an Alter SDK |
153
+ | `onSuccess` | `(grants, completion) => void` | Yes | Called with the legacy grants array and a typed completion object. `completion.failedGrants` identifies partial failures. |
154
+ | `onError` | `(error) => void` | No | Called for failures delivered to the browser SDK, such as a blocked popup, a malformed result, or a total usage-limit application failure. Hosted-page-only failures require backend polling. |
155
+ | `onExit` | `function` | No | Called when the user closes the desktop popup; this does not mark the backend session denied |
156
+ | `onEvent` | `(eventName, metadata) => void` | No | Called once with `connect_opened` after each launch attempt, including a blocked popup |
157
+
158
+ The promise resolves after the launch attempt, not after user authorization. Missing or invalid `token` or `onSuccess` values reject the returned promise with `code: "invalid_options"`. `baseURL` is not an `OpenOptions` field: TypeScript rejects it, while plain JavaScript ignores it.
191
159
 
192
- **Upgrading from 0.8.x:** `open()` no longer accepts a `baseURL` option the widget always opens against the production Alter host. A `baseURL` override is reserved for a future release but not currently supported; contact support if you need to target a non-production deployment.
160
+ `onSuccess`, `onError`, and `onExit` run through the event emitter's exception
161
+ guard. If one throws, the SDK logs the exception and does not perform that
162
+ callback's final state/listener cleanup; callback code should not throw.
163
+ `onEvent` is invoked directly, so an exception from it rejects `open()` after
164
+ the launch attempt.
193
165
 
194
- **Grants Array (onSuccess):**
166
+ **Completion data (`onSuccess`):**
195
167
 
196
- `onSuccess` receives an array of `Grant` objects (multi-provider flow):
168
+ For compatibility, the first argument remains an array of `Grant` objects. The
169
+ second argument makes partial completion explicit:
197
170
 
198
171
  ```typescript
199
- // Each connection in the array:
200
- {
201
- grant_id: string; // Unique ID - store this!
202
- provider: string; // e.g., 'google', 'slack'
203
- provider_name: string; // e.g., 'Google', 'Slack'
204
- account_identifier: string; // e.g., 'user@gmail.com'
205
- timestamp: string; // ISO 8601 timestamp
206
- operation: 'creation' | 'reauth';
207
- scopes: string[]; // Granted OAuth scopes
208
- status: 'active' | 'pending' | 'error';
209
- metadata?: {
210
- account_display_name?: string;
211
- account_email?: string;
212
- };
172
+ interface ConnectCompletion {
173
+ grants: Grant[];
174
+ failedGrants: Array<{
175
+ providerId: string;
176
+ reason: string;
177
+ message: string;
178
+ }>;
213
179
  }
214
180
  ```
215
181
 
182
+ `failedGrants` is empty for full success. When at least one provider succeeds,
183
+ it identifies any providers whose grants were revoked because the selected
184
+ usage limits could not be applied. Unknown `reason` strings are preserved.
185
+
216
186
  **Error Object (onError):**
217
187
 
218
188
  ```typescript
219
189
  {
220
- code: string; // e.g., 'invalid_token', 'popup_blocked'
221
- message: string; // Human-readable message
222
- details?: object; // Additional error context
190
+ code: string;
191
+ message: string;
192
+ details?: Record<string, unknown>;
193
+ failedGrants?: Array<{
194
+ providerId: string;
195
+ reason: string;
196
+ message: string;
197
+ }>;
223
198
  }
224
199
  ```
225
200
 
201
+ A completion with no successful grants and one or more failed grants invokes
202
+ `onError` with `code: "grant_policy_application_failed"` instead of reporting
203
+ bare success.
204
+
226
205
  ---
227
206
 
228
207
  ### `alterConnect.close()`
229
208
 
230
- Manually closes the Connect UI.
209
+ Closes an active popup if one exists, stops the current flow handler, removes
210
+ the current per-open callbacks, resets `isOpen()`, and emits `close`.
231
211
 
232
212
  ```javascript
233
213
  alterConnect.close();
@@ -250,24 +230,41 @@ alterConnect.destroy();
250
230
  Register an event listener. Returns an unsubscribe function.
251
231
 
252
232
  ```javascript
253
- const unsubscribe = alterConnect.on('success', (connection) => {
254
- console.log('Connected:', connection);
233
+ const unsubscribe = alterConnect.on('success', (grants, completion) => {
234
+ console.log('Connected:', grants);
235
+ console.log('Providers not kept:', completion.failedGrants);
255
236
  });
256
237
 
257
238
  // Later: unsubscribe();
258
239
  ```
259
240
 
260
- **Events:** `success`, `error`, `exit`, `close`, `event`
241
+ **Emitted bus events:** `success`, `error`, `exit`, `close`. The `success` handler receives `(grants, completion)`. Analytics does not emit an `event` bus event; use the per-open `onEvent` callback.
242
+
243
+ ---
244
+
245
+ ### `alterConnect.off(event, handler)`
246
+
247
+ Removes a previously registered event handler. Pass the same handler reference
248
+ that was supplied to `on()`.
249
+
250
+ ```javascript
251
+ const handleError = error => console.error(error);
252
+ alterConnect.on('error', handleError);
253
+ alterConnect.off('error', handleError);
254
+ ```
261
255
 
262
256
  ---
263
257
 
264
258
  ### `alterConnect.isOpen()`
265
259
 
266
- Checks if the Connect UI is currently open.
260
+ Returns the SDK's open-state flag. It becomes `true` when a launch starts and
261
+ returns to `false` when `close()` runs. A manually closed popup resets it
262
+ automatically only when the supplied `onExit` callback returns normally; if
263
+ `onExit` is omitted or throws, call `close()` explicitly before opening again.
267
264
 
268
265
  ```javascript
269
266
  if (alterConnect.isOpen()) {
270
- console.log('Modal is open');
267
+ console.log('Connect is open');
271
268
  }
272
269
  ```
273
270
 
@@ -278,41 +275,54 @@ if (alterConnect.isOpen()) {
278
275
  Gets the SDK version.
279
276
 
280
277
  ```javascript
281
- console.log(alterConnect.getVersion()); // e.g., "0.3.0"
278
+ console.log(alterConnect.getVersion()); // "0.2.0" in package 0.11.0
282
279
  ```
283
280
 
284
- ## Mobile Support
281
+ `getVersion()` reads a runtime constant embedded in the bundle. It does not currently match the package metadata version.
282
+
283
+ ## Mobile behavior
285
284
 
286
- The SDK automatically detects mobile devices and switches to an optimized flow:
285
+ The SDK chooses the flow using user-agent, touch, viewport, and orientation checks:
287
286
 
288
287
  | Device | Flow | How It Works |
289
288
  |--------|------|-------------|
290
- | Desktop | Popup | Opens centered popup (500x700px), communicates via postMessage |
291
- | Phone (<=480px) | Redirect | Full-page redirect, returns via URL params |
292
- | Tablet (portrait) | Redirect | Full-page redirect for better UX |
293
- | Tablet (landscape) | Popup | Uses popup flow like desktop |
289
+ | Desktop | Popup | Opens a centered popup (500x700px) and reports completion through callbacks |
290
+ | Mobile-classified viewport ≤480 px | Redirect | Navigates the full page to Connect |
291
+ | Mobile-classified viewport 481–1024 px in portrait | Redirect | Navigates the full page to Connect |
292
+ | Other devices | Popup | Uses the desktop callback flow |
294
293
 
295
- No code changes needed the SDK handles device detection automatically.
294
+ **Current limitation:** OAuth Connect does not navigate back to the session's `returnUrl`. Full-page navigation also destroys the callbacks and listeners registered by the original page. Do not rely on browser callbacks for mobile completion; use `createConnectSession()` plus `pollConnectSession()` on the application backend.
296
295
 
297
- For mobile redirect flow, include a `return_url` when creating the session:
298
-
299
- ```javascript
300
- // Backend session creation with mobile support
301
- body: JSON.stringify({
302
- allowed_providers: ['google', 'slack'],
303
- allowed_origin: 'https://yourapp.com', // For desktop popup (postMessage)
304
- return_url: 'https://yourapp.com/' // For mobile redirect (return destination)
305
- })
306
- ```
296
+ Closing the desktop popup invokes `onExit` locally but sends no denial
297
+ transition to the backend. A server-side poll remains pending until its timeout
298
+ or the session expires.
307
299
 
308
300
  ## Security
309
301
 
310
- - **API keys stay on the backend.** The frontend only ever holds a short-lived, single-use, scope-restricted session token minted by the application backend.
311
- - **No secrets in the browser.** The SDK is a popup launcher + callback listener; it does not handle credentials directly.
302
+ - **API keys stay on the backend.** The frontend only receives a short-lived session token minted by the application backend.
303
+ - **No API keys or provider credentials in the browser.** The frontend receives only the short-lived Connect session token.
312
304
 
313
305
  ## Provider selection
314
306
 
315
- When `allowedProviders` lists a single provider, the Connect UI skips the selection screen and opens that provider's OAuth flow directly. With two or more, the UI shows a provider picker.
307
+ The hosted UI renders the providers permitted by the session. When that provider is not already connected, a one-provider allowlist still renders one available provider. A verified user with an existing healthy connection can use the consent fast path; `requestedGrant`, when present, controls the sibling grant requested on that connection. Set `switchAccount: true` while creating the session to force full provider authorization.
308
+
309
+ ## Session-controlled behavior
310
+
311
+ Provider allowlists, per-provider requested scopes, origin binding, user identity, requested sibling grants, delegated agents, onward-delegation permission, delegation scope constraints, account switching, grant-expiry bounds, and policy authoring are session-creation concerns. They are not browser `OpenOptions`. Grant-expiry bounds add a duration picker to the hosted confirmation screen.
312
+
313
+ A recovery session from the server SDKs' `createConnectSessionForError()` is
314
+ opened like any other session by passing its `sessionToken` to `open()`. This
315
+ browser package exports none of the server-side exception classes, including
316
+ `ReAuthRequiredError`, `NoDelegatedGrantError`, `GrantNotFoundError`,
317
+ `CredentialRevokedError`, or the headless-flow `ConnectFlowError` family
318
+ (`ConnectDeniedError`, `ConnectConfigError`, `ConnectTimeoutError`).
319
+
320
+ With `allowUserPolicyRules` enabled, the hosted UI can author narrowing deny,
321
+ human-approval, time-window, quota, and operation/parameter content rules.
322
+ Human approval occurs on later API calls; Connect only authors the rule. If
323
+ selected rules fail to apply for some providers,
324
+ `completion.failedGrants` reports the partial failure. If no grant remains,
325
+ `onError` receives `grant_policy_application_failed`.
316
326
 
317
327
  ## TypeScript Support
318
328
 
@@ -320,21 +330,28 @@ Full TypeScript definitions included:
320
330
 
321
331
  ```typescript
322
332
  import AlterConnect, {
323
- AlterConnectConfig,
324
- Grant,
325
- AlterError
333
+ type AlterConnectConfig,
334
+ type OpenOptions,
335
+ type ConnectCompletion,
336
+ type ConnectFailedGrant,
337
+ type Provider,
338
+ type Grant,
339
+ type AlterError
326
340
  } from '@alter-ai/connect';
327
341
 
328
342
  const alterConnect = AlterConnect.create({ debug: true });
329
343
 
330
344
  await alterConnect.open({
331
345
  token: sessionToken,
332
- onSuccess: (grants: Grant[]) => {
346
+ onSuccess: (grants: Grant[], completion: ConnectCompletion) => {
333
347
  for (const grant of grants) {
334
- console.log(grant.grant_id); // Store this in your DB!
348
+ console.log(grant.grant_id); // Store this in the application database
335
349
  console.log(grant.provider);
336
350
  console.log(grant.scopes);
337
351
  }
352
+ for (const failure of completion.failedGrants) {
353
+ console.warn(failure.providerId, failure.message);
354
+ }
338
355
  },
339
356
  onError: (error: AlterError) => {
340
357
  console.error(error.code, error.message);
@@ -342,22 +359,11 @@ await alterConnect.open({
342
359
  });
343
360
  ```
344
361
 
345
- ## Bundle Size
346
-
347
- | Format | Size | Gzipped |
348
- |--------|------|---------|
349
- | **CJS** | ~10KB | ~3.5KB |
350
- | **ESM** | ~10KB | ~3.4KB |
351
- | **UMD** | ~11KB | ~3.5KB |
352
-
353
- Zero runtime dependencies.
362
+ The package has one runtime dependency: Zod 4 validates complete cross-window result payloads before callback data is consumed.
354
363
 
355
364
  ## Browser Support
356
365
 
357
- - Chrome/Edge 90+
358
- - Firefox 88+
359
- - Safari 14+
360
- - Mobile browsers (iOS Safari 14+, Chrome Mobile)
366
+ The CJS, ESM, and UMD bundles target ES2020 and use modern browser APIs including popup or full-page navigation, `postMessage`, URL parsing, and `sessionStorage`. Importing the package is side-effect free, but `AlterConnect.create()` must run in a browser because it checks redirect state during construction. The repository's product E2E suite exercises Chromium; it does not establish a cross-browser version matrix. The package metadata declares Node.js 20.19 or later for npm tooling; CDN browser use does not require Node.js.
361
367
 
362
368
  ## Troubleshooting
363
369
 
@@ -368,31 +374,32 @@ Zero runtime dependencies.
368
374
  **Solution:** Ensure `alterConnect.open()` is called directly from a user interaction (click event):
369
375
 
370
376
  ```javascript
371
- // Bad - may be blocked
377
+ // May be blocked: the network request runs after the click.
372
378
  button.addEventListener('click', async () => {
373
- const token = await fetchToken(); // Async delay
374
- alterConnect.open({ token }); // May be blocked
379
+ const token = await fetchToken();
380
+ void alterConnect.open({ token, onSuccess, onError });
375
381
  });
376
382
 
377
- // Good - no async delay before open()
383
+ // Reliable: fetch before enabling the button.
384
+ const token = await fetchToken();
385
+ button.disabled = false;
386
+
378
387
  button.addEventListener('click', () => {
379
- fetchToken().then(token => {
380
- alterConnect.open({ token }); // Called synchronously
381
- });
388
+ void alterConnect.open({ token, onSuccess, onError });
382
389
  });
383
390
  ```
384
391
 
385
392
  ### Session Token Expired
386
393
 
387
- **Problem:** `session_expired` error
394
+ **Problem:** The hosted Connect page reports that the session expired or is invalid
388
395
 
389
- **Solution:** Session tokens expire after 10 minutes. Create a new session token.
396
+ **Solution:** Session tokens have a 10-minute default lifetime. Create a new session and open its token. An expiry detected while binding or loading the hosted page is rendered there; it is not a reliable browser `onError` callback.
390
397
 
391
398
  ### CORS Errors
392
399
 
393
- **Problem:** CORS error when calling Alter API
400
+ **Problem:** Session creation was attempted from browser code
394
401
 
395
- **Solution:** Session tokens should be created from your **backend**, not frontend. The SDK handles all frontend API calls.
402
+ **Solution:** Create sessions on the application backend with an Alter server SDK, then return only the short-lived session token to the browser.
396
403
 
397
404
  ## Support
398
405
 
@@ -402,4 +409,4 @@ button.addEventListener('click', () => {
402
409
 
403
410
  ## License
404
411
 
405
- MIT License - See [LICENSE](LICENSE) file for details
412
+ MIT