@webex/plugin-authorization-browser 3.8.1 → 3.9.0-multi-llms.2
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/BROWSER-OAUTH-FLOW-GUIDE.md +541 -0
- package/README.md +309 -22
- package/dist/authorization.js +1 -1
- package/package.json +14 -14
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
# Webex Browser OAuth Authorization Flow - Technical Guide
|
|
2
|
+
|
|
3
|
+
This document explains how the Webex SDK handles OAuth authorization in browser environments, covering the complete flow from initialization to token management.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Browser Webex Initialization](#1-browser-webex-initialization)
|
|
8
|
+
- [Browser OAuth Flow Trigger](#2-browser-oauth-flow-trigger)
|
|
9
|
+
- [Authorization Code Processing](#3-authorization-code-processing)
|
|
10
|
+
- [Token Generation and Exchange](#4-token-generation-and-exchange)
|
|
11
|
+
- [Refresh Token Management](#5-refresh-token-management)
|
|
12
|
+
- [Browser Ready and Authorization Events](#6-browser-ready-and-authorization-events)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. Browser Webex Initialization
|
|
17
|
+
|
|
18
|
+
### 1.1 Basic Browser Setup
|
|
19
|
+
|
|
20
|
+
Browser applications initialize the Webex SDK with OAuth parameters:
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
// Basic browser initialization
|
|
24
|
+
const webex = Webex.init({
|
|
25
|
+
credentials: {
|
|
26
|
+
client_id: 'your-client-id',
|
|
27
|
+
redirect_uri: 'https://your-app.com/callback',
|
|
28
|
+
scope: 'spark:all spark:kms',
|
|
29
|
+
clientType: 'public' // or 'confidential'
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Parameters:
|
|
35
|
+
|
|
36
|
+
- **client_id**: Your registered application ID
|
|
37
|
+
- **redirect_uri**: Where to send the user after authentication
|
|
38
|
+
- **scope**: Requested permissions (e.g., 'spark:all spark:kms')
|
|
39
|
+
- **clientType**: 'public' (default) or 'confidential'
|
|
40
|
+
|
|
41
|
+
**Reference**: See `docs/samples/browser-auth/app.js` for a working example
|
|
42
|
+
|
|
43
|
+
### 1.2 Browser Flow Type Determination
|
|
44
|
+
|
|
45
|
+
The browser plugin automatically selects OAuth flow based on `clientType`:
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
// Public client (default) - uses response_type=token
|
|
49
|
+
const webex = Webex.init({
|
|
50
|
+
credentials: {
|
|
51
|
+
client_id: 'your-client-id',
|
|
52
|
+
clientType: 'public' // or omit (defaults to public)
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// Calls initiateImplicitGrant() -> response_type=token
|
|
56
|
+
|
|
57
|
+
// Confidential client - uses response_type=code
|
|
58
|
+
const webex = Webex.init({
|
|
59
|
+
credentials: {
|
|
60
|
+
client_id: 'your-client-id',
|
|
61
|
+
clientType: 'confidential'
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// Calls initiateAuthorizationCodeGrant() -> response_type=code
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Flow selection:
|
|
68
|
+
|
|
69
|
+
- **Public Client** (`clientType: 'public'` or undefined): Uses **Implicit Grant** with `response_type=token`
|
|
70
|
+
- **Confidential Client** (`clientType: 'confidential'`): Uses **Authorization Code Grant** with `response_type=code`
|
|
71
|
+
|
|
72
|
+
**Reference**: `initiateLogin()` method in `packages/@webex/plugin-authorization-browser/src/authorization.js`
|
|
73
|
+
|
|
74
|
+
### 1.3 Webex ID Broker Grant Types and Browser SDK Implementation
|
|
75
|
+
|
|
76
|
+
Webex ID Broker supports several OAuth 2.0 grant types. The browser SDK implements them as follows:
|
|
77
|
+
|
|
78
|
+
#### 1. Authorization Code Grant (`clientType: 'confidential'`)
|
|
79
|
+
|
|
80
|
+
**Webex ID Broker**: Returns authorization code after user authentication
|
|
81
|
+
**Browser SDK**: Uses `response_type=code`, then exchanges code for tokens
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
// SDK automatically handles this flow
|
|
85
|
+
const webex = Webex.init({
|
|
86
|
+
credentials: { clientType: 'confidential', /* other config */ }
|
|
87
|
+
});
|
|
88
|
+
webex.authorization.initiateLogin(); // Uses response_type=code
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### 2. Implicit Grant (`clientType: 'public'` - default)
|
|
92
|
+
|
|
93
|
+
**Webex ID Broker**: Returns access token directly after user authentication
|
|
94
|
+
**Browser SDK**: Uses `response_type=token`, receives token in URL hash
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
// SDK automatically handles this flow
|
|
98
|
+
const webex = Webex.init({
|
|
99
|
+
credentials: { clientType: 'public', /* other config */ }
|
|
100
|
+
});
|
|
101
|
+
webex.authorization.initiateLogin(); // Uses response_type=token
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### 3. Refresh Token Grant (automatic)
|
|
105
|
+
|
|
106
|
+
**Webex ID Broker**: Exchanges refresh token for new access token
|
|
107
|
+
**Browser SDK**: Automatically uses `grant_type=refresh_token` when tokens expire
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
// SDK handles refresh automatically, or manually:
|
|
111
|
+
webex.credentials.supertoken.refresh(); // Uses grant_type=refresh_token
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### 4. Client Credentials Grant (Node.js only)
|
|
115
|
+
|
|
116
|
+
**Webex ID Broker**: Application-to-application authentication
|
|
117
|
+
**Browser SDK**: Not supported (requires client secret)
|
|
118
|
+
|
|
119
|
+
**Note**: Client credentials grant is only available in Node.js SDK since it requires a client secret that cannot be safely stored in browsers.
|
|
120
|
+
|
|
121
|
+
### 1.4 Browser Implementation Differences
|
|
122
|
+
|
|
123
|
+
#### Implicit Grant Flow (Public Clients)
|
|
124
|
+
|
|
125
|
+
- **Security**: Tokens exposed in browser URL (less secure)
|
|
126
|
+
- **Client Secret**: No client secret required
|
|
127
|
+
- **Token Location**: Access token in URL hash fragment
|
|
128
|
+
- **Redirect**: Single redirect with token
|
|
129
|
+
- **Best For**: Single-page applications, mobile apps
|
|
130
|
+
|
|
131
|
+
#### Authorization Code Grant Flow (Confidential Clients)
|
|
132
|
+
|
|
133
|
+
- **Security**: More secure, tokens never exposed to browser
|
|
134
|
+
- **Client Secret**: Requires client secret
|
|
135
|
+
- **Token Location**: Authorization code in URL, exchanged server-side
|
|
136
|
+
- **Redirect**: Two-step process (code → token exchange)
|
|
137
|
+
- **Best For**: Web applications with backend
|
|
138
|
+
|
|
139
|
+
### 1.5 Browser URL Parsing on Load
|
|
140
|
+
|
|
141
|
+
During browser initialization, the plugin automatically:
|
|
142
|
+
|
|
143
|
+
1. **Parses current URL** for OAuth tokens/codes
|
|
144
|
+
2. **Checks for OAuth errors** in URL parameters
|
|
145
|
+
3. **Validates CSRF tokens** for security
|
|
146
|
+
4. **Cleans the URL** by removing sensitive parameters
|
|
147
|
+
|
|
148
|
+
**Reference**: `initialize()` method in `packages/@webex/plugin-authorization-browser/src/authorization.js`
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 2. Browser OAuth Flow Trigger
|
|
153
|
+
|
|
154
|
+
### 2.1 Initiating Browser Login
|
|
155
|
+
|
|
156
|
+
The `initiateLogin()` method redirects users to Webex's identity broker for authentication. It does **NOT** accept email/password directly.
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
// Basic login - redirects to Webex login page
|
|
160
|
+
webex.authorization.initiateLogin()
|
|
161
|
+
|
|
162
|
+
// Login with custom state data
|
|
163
|
+
webex.authorization.initiateLogin({
|
|
164
|
+
state: {
|
|
165
|
+
returnUrl: '/dashboard',
|
|
166
|
+
userId: 'user123'
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Login in popup window (default dimensions: 600x800)
|
|
171
|
+
webex.authorization.initiateLogin({
|
|
172
|
+
separateWindow: true
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// Login in popup with custom dimensions
|
|
176
|
+
webex.authorization.initiateLogin({
|
|
177
|
+
separateWindow: {
|
|
178
|
+
width: 800,
|
|
179
|
+
height: 600
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Parameters:
|
|
185
|
+
|
|
186
|
+
- **options.state** (Object, optional): Custom data included in OAuth state
|
|
187
|
+
- **options.separateWindow** (Boolean|Object, optional): Open in popup window
|
|
188
|
+
|
|
189
|
+
Process:
|
|
190
|
+
|
|
191
|
+
1. **Generates CSRF token** for security
|
|
192
|
+
2. **Determines flow type** based on client configuration
|
|
193
|
+
3. **Builds OAuth URL** with required parameters
|
|
194
|
+
4. **Redirects user** to Webex identity broker
|
|
195
|
+
|
|
196
|
+
**Reference**: `initiateLogin()` method in browser authorization plugin
|
|
197
|
+
|
|
198
|
+
### 2.2 Browser CSRF Token Generation
|
|
199
|
+
|
|
200
|
+
Browser-specific CSRF protection:
|
|
201
|
+
|
|
202
|
+
1. **Generates UUID token** using `uuid.v4()`
|
|
203
|
+
2. **Stores in sessionStorage** for later verification
|
|
204
|
+
3. **Includes in state parameter** of OAuth request
|
|
205
|
+
|
|
206
|
+
**Reference**: `_generateSecurityToken()` method in browser plugin
|
|
207
|
+
|
|
208
|
+
### 2.3 Browser Redirection Options
|
|
209
|
+
|
|
210
|
+
Browser supports multiple redirection methods:
|
|
211
|
+
|
|
212
|
+
- **Same window**: Default behavior (full page redirect)
|
|
213
|
+
- **Popup window**: Optional separate window with configurable dimensions
|
|
214
|
+
|
|
215
|
+
**Reference**: `initiateImplicitGrant()` and `initiateAuthorizationCodeGrant()` methods
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 3. Authorization Code Processing
|
|
220
|
+
|
|
221
|
+
### 3.1 User Authentication at ID Broker
|
|
222
|
+
|
|
223
|
+
User experience at Webex identity broker:
|
|
224
|
+
|
|
225
|
+
1. **User enters credentials** (username/password, SSO, etc.)
|
|
226
|
+
2. **Identity verification** occurs
|
|
227
|
+
3. **Consent screen** may appear for scope approval
|
|
228
|
+
4. **Authorization decision** is made
|
|
229
|
+
|
|
230
|
+
### 3.2 Browser Redirect Back
|
|
231
|
+
|
|
232
|
+
After successful authentication:
|
|
233
|
+
|
|
234
|
+
- **Implicit Grant**: User redirected with access token in URL hash
|
|
235
|
+
- **Authorization Code Grant**: User redirected with authorization code in query parameters
|
|
236
|
+
|
|
237
|
+
### 3.3 Browser Code/Token Reception
|
|
238
|
+
|
|
239
|
+
Browser SDK processes the return URL automatically:
|
|
240
|
+
|
|
241
|
+
1. **URL parsing** during plugin initialization
|
|
242
|
+
2. **Error checking** for OAuth errors
|
|
243
|
+
3. **Token/code extraction** from URL parameters
|
|
244
|
+
4. **State validation** for CSRF protection
|
|
245
|
+
|
|
246
|
+
**Reference**: `initialize()` and `_parseHash()` methods in browser plugin
|
|
247
|
+
|
|
248
|
+
### 3.4 Browser CSRF Token Verification
|
|
249
|
+
|
|
250
|
+
Browser security validation:
|
|
251
|
+
|
|
252
|
+
1. **Extract state parameter** from redirect URL
|
|
253
|
+
2. **Decode Base64 state** object
|
|
254
|
+
3. **Compare CSRF tokens** (URL vs sessionStorage)
|
|
255
|
+
4. **Throw error** if tokens don't match
|
|
256
|
+
|
|
257
|
+
**Reference**: `_verifySecurityToken()` method in browser plugin
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 4. Token Generation and Exchange
|
|
262
|
+
|
|
263
|
+
### 4.1 Browser Implicit Grant Processing
|
|
264
|
+
|
|
265
|
+
For browser implicit grant, access token comes directly in URL hash:
|
|
266
|
+
|
|
267
|
+
- access_token
|
|
268
|
+
- token_type (Bearer)
|
|
269
|
+
- expires_in
|
|
270
|
+
- scope
|
|
271
|
+
|
|
272
|
+
> **Note:** Refresh tokens are generally not provided in implicit grant flows due to security reasons. Most OAuth providers, including Webex, do not issue refresh tokens for implicit grant to prevent long-lived tokens from being exposed in browser environments. Applications using implicit grant should expect to obtain new access tokens by re-authenticating the user when the current token expires.
|
|
273
|
+
|
|
274
|
+
**Reference**: Token parsing in `_parseHash()` method
|
|
275
|
+
|
|
276
|
+
### 4.2 Browser Authorization Code Exchange
|
|
277
|
+
|
|
278
|
+
For browser confidential clients, code exchange happens:
|
|
279
|
+
|
|
280
|
+
- **Client-side**: Code captured from URL
|
|
281
|
+
- **Server-side**: Code exchanged for tokens using client secret
|
|
282
|
+
|
|
283
|
+
### 4.3 Browser JWT Authentication
|
|
284
|
+
|
|
285
|
+
Browser JWT authentication for guest users:
|
|
286
|
+
|
|
287
|
+
```javascript
|
|
288
|
+
// Create JWT token
|
|
289
|
+
webex.authorization.createJwt({
|
|
290
|
+
issuer: 'your-guest-issuer-id',
|
|
291
|
+
secretId: 'your-base64-encoded-secret',
|
|
292
|
+
displayName: 'Guest User Name',
|
|
293
|
+
expiresIn: '12h'
|
|
294
|
+
}).then(({jwt}) => {
|
|
295
|
+
console.log('Created JWT:', jwt);
|
|
296
|
+
|
|
297
|
+
// Exchange JWT for access token
|
|
298
|
+
return webex.authorization.requestAccessTokenFromJwt({jwt});
|
|
299
|
+
}).then(() => {
|
|
300
|
+
console.log('Guest user authenticated');
|
|
301
|
+
}).catch(error => {
|
|
302
|
+
console.error('JWT authentication failed:', error);
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Process:
|
|
307
|
+
|
|
308
|
+
1. **Create JWT token** with issuer, secret, display name
|
|
309
|
+
2. **Exchange JWT for access token** via API call
|
|
310
|
+
|
|
311
|
+
**Reference**: `createJwt()` and `requestAccessTokenFromJwt()` methods in browser plugin
|
|
312
|
+
|
|
313
|
+
### 4.4 Browser Token Storage
|
|
314
|
+
|
|
315
|
+
Browser token storage:
|
|
316
|
+
|
|
317
|
+
1. **Stored in credentials object** via `webex.credentials.set()`
|
|
318
|
+
2. **Persisted in browser storage** (localStorage/sessionStorage)
|
|
319
|
+
3. **Monitored for expiration** by interceptors
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 5. Refresh Token Management
|
|
324
|
+
|
|
325
|
+
### 5.1 Browser Token Expiration Detection
|
|
326
|
+
|
|
327
|
+
Browser monitors token validity through:
|
|
328
|
+
|
|
329
|
+
```javascript
|
|
330
|
+
// Check if token is expired
|
|
331
|
+
if (webex.credentials.supertoken.isExpired) {
|
|
332
|
+
console.log('Token is expired');
|
|
333
|
+
// SDK will automatically refresh if refresh token available
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Check if token can be refreshed
|
|
337
|
+
if (webex.credentials.supertoken.canRefresh) {
|
|
338
|
+
console.log('Token can be refreshed');
|
|
339
|
+
} else {
|
|
340
|
+
console.log('User needs to re-authenticate');
|
|
341
|
+
webex.authorization.initiateLogin();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Manual token refresh
|
|
345
|
+
webex.credentials.supertoken.refresh().then((newToken) => {
|
|
346
|
+
console.log('Token refreshed successfully');
|
|
347
|
+
}).catch((error) => {
|
|
348
|
+
console.error('Token refresh failed:', error);
|
|
349
|
+
webex.authorization.initiateLogin();
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Detection mechanisms:
|
|
354
|
+
|
|
355
|
+
- **HTTP interceptors** checking responses
|
|
356
|
+
- **Automatic refresh triggers** before expiration
|
|
357
|
+
- **Error handling** for 401 Unauthorized responses
|
|
358
|
+
|
|
359
|
+
### 5.2 Browser Refresh Token Flow
|
|
360
|
+
|
|
361
|
+
Browser refresh token process using `refreshCallback`:
|
|
362
|
+
|
|
363
|
+
```javascript
|
|
364
|
+
// Configure refresh callback during initialization
|
|
365
|
+
const webex = Webex.init({
|
|
366
|
+
credentials: {
|
|
367
|
+
client_id: 'your-client-id',
|
|
368
|
+
redirect_uri: 'https://your-app.com/callback',
|
|
369
|
+
scope: 'spark:all spark:kms',
|
|
370
|
+
refreshCallback: (webex, token) => {
|
|
371
|
+
// Custom refresh logic for browser
|
|
372
|
+
return webex.request({
|
|
373
|
+
method: 'POST',
|
|
374
|
+
uri: 'https://webexapis.com/v1/access_token',
|
|
375
|
+
form: {
|
|
376
|
+
grant_type: 'refresh_token',
|
|
377
|
+
refresh_token: token.refresh_token,
|
|
378
|
+
client_id: token.config.client_id
|
|
379
|
+
}
|
|
380
|
+
}).then(response => response.body);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Refresh process:
|
|
387
|
+
|
|
388
|
+
1. **POST request** to `/access_token` endpoint
|
|
389
|
+
2. **grant_type**: "refresh_token"
|
|
390
|
+
3. **Current refresh token** as parameter
|
|
391
|
+
4. **Client ID** for identification
|
|
392
|
+
5. **New tokens returned** and stored automatically
|
|
393
|
+
|
|
394
|
+
### 5.3 Browser Automatic Refresh
|
|
395
|
+
|
|
396
|
+
SDK automatically handles token refresh:
|
|
397
|
+
|
|
398
|
+
```javascript
|
|
399
|
+
// SDK automatically refreshes tokens before API calls
|
|
400
|
+
webex.people.get('me').then(person => {
|
|
401
|
+
// Token was automatically refreshed if needed
|
|
402
|
+
console.log('Current user:', person.displayName);
|
|
403
|
+
}).catch(error => {
|
|
404
|
+
if (error.message.includes('unauthorized')) {
|
|
405
|
+
// Refresh failed, user needs to re-authenticate
|
|
406
|
+
webex.authorization.initiateLogin();
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Listen for token refresh events
|
|
411
|
+
webex.credentials.on('change:supertoken', () => {
|
|
412
|
+
console.log('Token was refreshed');
|
|
413
|
+
});
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### 5.4 Browser JWT Refresh
|
|
417
|
+
|
|
418
|
+
Browser JWT refresh using callback:
|
|
419
|
+
|
|
420
|
+
```javascript
|
|
421
|
+
// Configure JWT refresh callback
|
|
422
|
+
const webex = Webex.init({
|
|
423
|
+
credentials: {
|
|
424
|
+
jwtRefreshCallback: async (webex) => {
|
|
425
|
+
// Get new JWT from your backend
|
|
426
|
+
const response = await fetch('/api/jwt-refresh', {
|
|
427
|
+
method: 'POST',
|
|
428
|
+
credentials: 'include'
|
|
429
|
+
});
|
|
430
|
+
const { jwt } = await response.json();
|
|
431
|
+
return jwt;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// SDK automatically uses jwtRefreshCallback when JWT expires
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**Reference**: JWT refresh implementation in browser authorization plugin
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## 6. Browser Ready and Authorization Events
|
|
444
|
+
|
|
445
|
+
### 6.1 Browser SDK Initialization Lifecycle
|
|
446
|
+
|
|
447
|
+
Browser SDK initialization phases:
|
|
448
|
+
|
|
449
|
+
1. **Construction**: Basic object creation
|
|
450
|
+
2. **Plugin Loading**: Authorization and other plugins initialize
|
|
451
|
+
3. **Storage Loading**: Browser storage data retrieval
|
|
452
|
+
4. **Authentication Check**: Token validation and refresh
|
|
453
|
+
5. **Ready State**: All systems operational
|
|
454
|
+
|
|
455
|
+
### 6.2 Browser Authorization Events
|
|
456
|
+
|
|
457
|
+
Browser-specific events:
|
|
458
|
+
|
|
459
|
+
```javascript
|
|
460
|
+
// Listen for SDK ready event
|
|
461
|
+
webex.once('ready', () => {
|
|
462
|
+
console.log('Webex SDK is ready and authenticated');
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Listen for unauthorized event
|
|
466
|
+
webex.on('unauthorized', () => {
|
|
467
|
+
console.log('User authentication lost - redirect to login');
|
|
468
|
+
webex.authorization.initiateLogin();
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Listen for logout event
|
|
472
|
+
webex.on('client:logout', () => {
|
|
473
|
+
console.log('User has logged out');
|
|
474
|
+
});
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Events:
|
|
478
|
+
|
|
479
|
+
- **ready**: SDK fully initialized and authenticated
|
|
480
|
+
- **unauthorized**: User authentication lost
|
|
481
|
+
- **client:logout**: User has logged out
|
|
482
|
+
|
|
483
|
+
### 6.3 Browser Authentication Status
|
|
484
|
+
|
|
485
|
+
Check browser authentication status:
|
|
486
|
+
|
|
487
|
+
```javascript
|
|
488
|
+
// Check if user can make authenticated requests
|
|
489
|
+
if (webex.canAuthorize) {
|
|
490
|
+
console.log('User is authenticated');
|
|
491
|
+
// Make API calls
|
|
492
|
+
} else {
|
|
493
|
+
console.log('User needs to login');
|
|
494
|
+
webex.authorization.initiateLogin();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Check if authorization is in progress
|
|
498
|
+
if (webex.authorization.isAuthorizing) {
|
|
499
|
+
console.log('Authorization in progress...');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Check if SDK is fully ready
|
|
503
|
+
if (webex.ready) {
|
|
504
|
+
console.log('SDK is fully initialized');
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Status Properties:
|
|
509
|
+
|
|
510
|
+
- `webex.canAuthorize`: Boolean for authenticated requests capability
|
|
511
|
+
- `webex.authorization.isAuthorizing`: Boolean for authorization in progress
|
|
512
|
+
- `webex.ready`: Boolean for SDK fully initialized
|
|
513
|
+
|
|
514
|
+
### 6.4 Browser Ready State Dependencies
|
|
515
|
+
|
|
516
|
+
Browser ready state depends on:
|
|
517
|
+
|
|
518
|
+
- **Credentials loaded** from browser storage
|
|
519
|
+
- **All plugins initialized**
|
|
520
|
+
- **Services catalog loaded**
|
|
521
|
+
- **Authentication state established**
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
## Browser Code References
|
|
526
|
+
|
|
527
|
+
- **Main Browser Plugin**: `packages/@webex/plugin-authorization-browser/src/authorization.js`
|
|
528
|
+
- **Browser Auth Sample**: `docs/samples/browser-auth/app.js`
|
|
529
|
+
- **Browser Plugin Docs**: `packages/@webex/plugin-authorization-browser/README.md`
|
|
530
|
+
|
|
531
|
+
## Maintainers
|
|
532
|
+
|
|
533
|
+
This package is maintained by [Cisco Webex for Developers](https://developer.webex.com/).
|
|
534
|
+
|
|
535
|
+
## Contribute
|
|
536
|
+
|
|
537
|
+
Pull requests welcome. Please see [CONTRIBUTING.md](https://github.com/webex/webex-js-sdk/blob/master/CONTRIBUTING.md) for more details.
|
|
538
|
+
|
|
539
|
+
## License
|
|
540
|
+
|
|
541
|
+
© 2016-2025 Cisco and/or its affiliates. All Rights Reserved.
|
package/README.md
CHANGED
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
# @webex/plugin-authorization-browser
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> OAuth2 authorization plugin for browser environments in the Cisco Webex JS SDK. Handles OAuth2 flows including Implicit Grant and Authorization Code Grant for web applications.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Table of Contents
|
|
6
6
|
|
|
7
|
-
- [
|
|
8
|
-
- [
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
7
|
+
- [@webex/plugin-authorization-browser](#webexplugin-authorization-browser)
|
|
8
|
+
- [Table of Contents](#table-of-contents)
|
|
9
|
+
- [Install](#install)
|
|
10
|
+
- [What it Does](#what-it-does)
|
|
11
|
+
- [Usage](#usage)
|
|
12
|
+
- [Basic OAuth2 Login](#basic-oauth2-login)
|
|
13
|
+
- [Implicit Grant Flow](#implicit-grant-flow)
|
|
14
|
+
- [Authorization Code Grant Flow](#authorization-code-grant-flow)
|
|
15
|
+
- [Login with Popup Window](#login-with-popup-window)
|
|
16
|
+
- [JWT Authentication](#jwt-authentication)
|
|
17
|
+
- [Guest JWT Creation](#guest-jwt-creation)
|
|
18
|
+
- [Logout](#logout)
|
|
19
|
+
- [Checking Authentication Status](#checking-authentication-status)
|
|
20
|
+
- [Error Handling](#error-handling)
|
|
21
|
+
- [API Reference](#api-reference)
|
|
22
|
+
- [Methods](#methods)
|
|
23
|
+
- [`initiateLogin(options)`](#initiateloginoptions)
|
|
24
|
+
- [`initiateImplicitGrant(options)`](#initiateimplicitgrantoptions)
|
|
25
|
+
- [`initiateAuthorizationCodeGrant(options)`](#initiateauthorizationcodegrantoptions)
|
|
26
|
+
- [`requestAccessTokenFromJwt({ jwt })`](#requestaccesstokenfromjwt-jwt-)
|
|
27
|
+
- [`createJwt(options)`](#createjwtoptions)
|
|
28
|
+
- [`logout(options)`](#logoutoptions)
|
|
29
|
+
- [Properties](#properties)
|
|
30
|
+
- [`isAuthorizing` (boolean)](#isauthorizing-boolean)
|
|
31
|
+
- [`isAuthenticating` (boolean)](#isauthenticating-boolean)
|
|
32
|
+
- [`ready` (boolean)](#ready-boolean)
|
|
33
|
+
- [Maintainers](#maintainers)
|
|
34
|
+
- [Contribute](#contribute)
|
|
35
|
+
- [License](#license)
|
|
12
36
|
|
|
13
37
|
## Install
|
|
14
38
|
|
|
@@ -16,38 +40,301 @@
|
|
|
16
40
|
npm install --save @webex/plugin-authorization-browser
|
|
17
41
|
```
|
|
18
42
|
|
|
43
|
+
## What it Does
|
|
44
|
+
|
|
45
|
+
The `@webex/plugin-authorization-browser` plugin provides OAuth2 authentication capabilities specifically for browser environments. It:
|
|
46
|
+
|
|
47
|
+
- **Automatically handles OAuth2 flows**: Supports both Implicit Grant and Authorization Code Grant flows
|
|
48
|
+
- **Manages authentication state**: Tracks authorization status and handles token parsing from URL
|
|
49
|
+
- **Provides CSRF protection**: Generates and validates CSRF tokens for security
|
|
50
|
+
- **Supports popup authentication**: Can open login in a separate window
|
|
51
|
+
- **JWT token support**: Create and use JWT tokens for guest authentication
|
|
52
|
+
- **URL cleanup**: Automatically removes sensitive tokens from browser URL after authentication
|
|
53
|
+
- **Cross-browser compatibility**: Works across different browser environments
|
|
54
|
+
|
|
19
55
|
## Usage
|
|
20
56
|
|
|
21
|
-
|
|
57
|
+
### Basic OAuth2 Login
|
|
22
58
|
|
|
23
|
-
|
|
59
|
+
The simplest way to authenticate users:
|
|
24
60
|
|
|
25
|
-
```
|
|
26
|
-
|
|
61
|
+
```javascript
|
|
62
|
+
const Webex = require('webex');
|
|
63
|
+
|
|
64
|
+
// Initialize Webex SDK
|
|
65
|
+
const webex = Webex.init({
|
|
66
|
+
credentials: {
|
|
67
|
+
client_id: 'your-client-id',
|
|
68
|
+
redirect_uri: 'https://your-app.com/callback',
|
|
69
|
+
scope: 'spark:all'
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Start the login process
|
|
74
|
+
webex.authorization.initiateLogin()
|
|
75
|
+
.then(() => {
|
|
76
|
+
console.log('Login initiated');
|
|
77
|
+
// User will be redirected to Webex login page
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// After redirect, check if user is authenticated
|
|
81
|
+
if (webex.canAuthorize) {
|
|
82
|
+
console.log('User is authenticated');
|
|
83
|
+
// Make API calls
|
|
84
|
+
}
|
|
27
85
|
```
|
|
28
86
|
|
|
29
|
-
|
|
87
|
+
### Implicit Grant Flow
|
|
30
88
|
|
|
31
|
-
|
|
89
|
+
For public clients (single-page applications):
|
|
32
90
|
|
|
33
|
-
|
|
91
|
+
```javascript
|
|
92
|
+
const webex = Webex.init({
|
|
93
|
+
credentials: {
|
|
94
|
+
client_id: 'your-client-id',
|
|
95
|
+
redirect_uri: 'https://your-app.com/callback',
|
|
96
|
+
scope: 'spark:all'
|
|
97
|
+
// No client_secret for public clients
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Initiate implicit grant flow
|
|
102
|
+
webex.authorization.initiateImplicitGrant({
|
|
103
|
+
state: { customData: 'value' } // Optional state data
|
|
104
|
+
})
|
|
105
|
+
.then(() => {
|
|
106
|
+
console.log('Implicit grant flow started');
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Authorization Code Grant Flow
|
|
111
|
+
|
|
112
|
+
For confidential clients with client secret:
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
const webex = Webex.init({
|
|
116
|
+
credentials: {
|
|
117
|
+
client_id: 'your-client-id',
|
|
118
|
+
client_secret: 'your-client-secret',
|
|
119
|
+
redirect_uri: 'https://your-app.com/callback',
|
|
120
|
+
scope: 'spark:all',
|
|
121
|
+
clientType: 'confidential' // This triggers authorization code flow
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Initiate authorization code grant flow
|
|
126
|
+
webex.authorization.initiateAuthorizationCodeGrant({
|
|
127
|
+
state: { customData: 'value' }
|
|
128
|
+
})
|
|
129
|
+
.then(() => {
|
|
130
|
+
console.log('Authorization code flow started');
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Login with Popup Window
|
|
135
|
+
|
|
136
|
+
Open login in a separate popup window instead of redirecting:
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
// Basic popup with default dimensions (600x800)
|
|
140
|
+
webex.authorization.initiateLogin({
|
|
141
|
+
separateWindow: true
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Custom popup dimensions
|
|
145
|
+
webex.authorization.initiateLogin({
|
|
146
|
+
separateWindow: {
|
|
147
|
+
width: 800,
|
|
148
|
+
height: 600
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// With custom state and popup
|
|
153
|
+
webex.authorization.initiateLogin({
|
|
154
|
+
state: {
|
|
155
|
+
returnUrl: '/dashboard',
|
|
156
|
+
userId: 'user123'
|
|
157
|
+
},
|
|
158
|
+
separateWindow: {
|
|
159
|
+
width: 900,
|
|
160
|
+
height: 700
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### JWT Authentication
|
|
166
|
+
|
|
167
|
+
Authenticate using a JWT token (useful for guest users):
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
// Assuming you have a JWT from your backend
|
|
171
|
+
const jwtToken = '<YOUR_JWT_TOKEN_HERE>';
|
|
172
|
+
|
|
173
|
+
webex.authorization.requestAccessTokenFromJwt({
|
|
174
|
+
jwt: jwtToken
|
|
175
|
+
})
|
|
176
|
+
.then(() => {
|
|
177
|
+
console.log('Authenticated with JWT');
|
|
178
|
+
// User is now authenticated and can make API calls
|
|
179
|
+
})
|
|
180
|
+
.catch(error => {
|
|
181
|
+
console.error('JWT authentication failed:', error);
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Guest JWT Creation
|
|
186
|
+
|
|
187
|
+
Create JWT tokens for guest users:
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
// Create a guest JWT token
|
|
191
|
+
webex.authorization.createJwt({
|
|
192
|
+
issuer: 'your-guest-issuer-id',
|
|
193
|
+
secretId: 'your-base64-encoded-secret',
|
|
194
|
+
displayName: 'Guest User Name', // Optional
|
|
195
|
+
expiresIn: '12h' // Token expiration
|
|
196
|
+
})
|
|
197
|
+
.then(({ jwt }) => {
|
|
198
|
+
console.log('Created guest JWT:', jwt);
|
|
199
|
+
|
|
200
|
+
// Use the JWT to authenticate
|
|
201
|
+
return webex.authorization.requestAccessTokenFromJwt({ jwt });
|
|
202
|
+
})
|
|
203
|
+
.then(() => {
|
|
204
|
+
console.log('Guest user authenticated');
|
|
205
|
+
})
|
|
206
|
+
.catch(error => {
|
|
207
|
+
console.error('Guest JWT creation failed:', error);
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Logout
|
|
212
|
+
|
|
213
|
+
Log out the current user:
|
|
214
|
+
|
|
215
|
+
```javascript
|
|
216
|
+
// Logout and redirect to Webex logout page
|
|
217
|
+
webex.authorization.logout();
|
|
34
218
|
|
|
35
|
-
|
|
36
|
-
webex.authorization
|
|
37
|
-
.then((authorization-browser) => {
|
|
38
|
-
console.log(authorization-browser);
|
|
39
|
-
})
|
|
219
|
+
// Logout without redirect (clean up local session only)
|
|
220
|
+
webex.authorization.logout({ noRedirect: true });
|
|
40
221
|
|
|
222
|
+
// Logout with custom logout URL
|
|
223
|
+
webex.authorization.logout({
|
|
224
|
+
goto: 'https://your-app.com/goodbye'
|
|
225
|
+
});
|
|
41
226
|
```
|
|
42
227
|
|
|
228
|
+
### Checking Authentication Status
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
// Check if SDK can authorize (has valid token)
|
|
232
|
+
if (webex.canAuthorize) {
|
|
233
|
+
console.log('User is authenticated');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check if authorization is in progress
|
|
237
|
+
if (webex.authorization.isAuthorizing) {
|
|
238
|
+
console.log('Authorization in progress...');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Listen for authentication events
|
|
242
|
+
webex.on('ready', () => {
|
|
243
|
+
console.log('SDK is ready and authenticated');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
webex.on('unauthorized', () => {
|
|
247
|
+
console.log('User is not authenticated');
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Error Handling
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
// Handle authentication errors from URL
|
|
255
|
+
try {
|
|
256
|
+
const webex = Webex.init({
|
|
257
|
+
credentials: { /* your config */ }
|
|
258
|
+
});
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (error.name === 'OAuthError') {
|
|
261
|
+
console.error('OAuth error:', error.message);
|
|
262
|
+
// Handle specific OAuth errors like access_denied
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Handle JWT authentication errors
|
|
267
|
+
webex.authorization.requestAccessTokenFromJwt({ jwt: 'invalid-jwt' })
|
|
268
|
+
.catch(error => {
|
|
269
|
+
console.error('JWT authentication failed:', error);
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## API Reference
|
|
274
|
+
|
|
275
|
+
### Methods
|
|
276
|
+
|
|
277
|
+
#### `initiateLogin(options)`
|
|
278
|
+
|
|
279
|
+
Initiates the appropriate OAuth flow based on client configuration.
|
|
280
|
+
|
|
281
|
+
- `options.state` - Optional state object for custom data
|
|
282
|
+
- `options.separateWindow` - Boolean or object for popup window settings
|
|
283
|
+
|
|
284
|
+
#### `initiateImplicitGrant(options)`
|
|
285
|
+
|
|
286
|
+
Starts the Implicit Grant flow for public clients.
|
|
287
|
+
|
|
288
|
+
#### `initiateAuthorizationCodeGrant(options)`
|
|
289
|
+
|
|
290
|
+
Starts the Authorization Code Grant flow for confidential clients.
|
|
291
|
+
|
|
292
|
+
#### `requestAccessTokenFromJwt({ jwt })`
|
|
293
|
+
|
|
294
|
+
Exchanges a JWT for an access token.
|
|
295
|
+
|
|
296
|
+
#### `createJwt(options)`
|
|
297
|
+
|
|
298
|
+
Creates a JWT token for guest authentication.
|
|
299
|
+
|
|
300
|
+
- `options.issuer` - Guest issuer ID
|
|
301
|
+
- `options.secretId` - Base64 encoded secret
|
|
302
|
+
- `options.displayName` - Optional display name
|
|
303
|
+
- `options.expiresIn` - Token expiration time
|
|
304
|
+
|
|
305
|
+
#### `logout(options)`
|
|
306
|
+
|
|
307
|
+
Logs out the current user.
|
|
308
|
+
|
|
309
|
+
- `options.noRedirect` - Skip redirect to logout page
|
|
310
|
+
- `options.goto` - Custom redirect URL after logout
|
|
311
|
+
|
|
312
|
+
### Properties
|
|
313
|
+
|
|
314
|
+
#### `isAuthorizing` (boolean)
|
|
315
|
+
|
|
316
|
+
Indicates if an authorization flow is currently in progress.
|
|
317
|
+
|
|
318
|
+
#### `isAuthenticating` (boolean)
|
|
319
|
+
|
|
320
|
+
Alias for `isAuthorizing`.
|
|
321
|
+
|
|
322
|
+
#### `ready` (boolean)
|
|
323
|
+
|
|
324
|
+
Indicates if the authorization plugin has finished initialization.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
43
328
|
## Maintainers
|
|
44
329
|
|
|
45
|
-
This package is maintained by
|
|
330
|
+
This package is maintained by Cisco Webex for Developers.
|
|
46
331
|
|
|
47
332
|
## Contribute
|
|
48
333
|
|
|
49
|
-
Pull requests welcome. Please see
|
|
334
|
+
Pull requests welcome. Please see CONTRIBUTING.md for more details.
|
|
50
335
|
|
|
51
336
|
## License
|
|
52
337
|
|
|
53
|
-
|
|
338
|
+
This project is licensed under the Cisco General Terms - see the LICENSE for details.
|
|
339
|
+
|
|
340
|
+
© 2016-2025 Cisco and/or its affiliates. All Rights Reserved.
|
package/dist/authorization.js
CHANGED
|
@@ -421,7 +421,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
421
421
|
throw new Error("CSRF token ".concat(token, " does not match stored token ").concat(sessionToken));
|
|
422
422
|
}
|
|
423
423
|
},
|
|
424
|
-
version: "3.
|
|
424
|
+
version: "3.9.0-multi-llms.2"
|
|
425
425
|
}, ((0, _applyDecoratedDescriptor2.default)(_obj, "initiateImplicitGrant", [_dec], (0, _getOwnPropertyDescriptor.default)(_obj, "initiateImplicitGrant"), _obj), (0, _applyDecoratedDescriptor2.default)(_obj, "initiateAuthorizationCodeGrant", [_dec2], (0, _getOwnPropertyDescriptor.default)(_obj, "initiateAuthorizationCodeGrant"), _obj), (0, _applyDecoratedDescriptor2.default)(_obj, "requestAccessTokenFromJwt", [_common.oneFlight], (0, _getOwnPropertyDescriptor.default)(_obj, "requestAccessTokenFromJwt"), _obj)), _obj)));
|
|
426
426
|
var _default = exports.default = Authorization;
|
|
427
427
|
//# sourceMappingURL=authorization.js.map
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"directory": "packages/@webex/plugin-authorization-browser"
|
|
11
11
|
},
|
|
12
12
|
"engines": {
|
|
13
|
-
"node": ">=
|
|
13
|
+
"node": ">=18"
|
|
14
14
|
},
|
|
15
15
|
"browserify": {
|
|
16
16
|
"transform": [
|
|
@@ -24,23 +24,23 @@
|
|
|
24
24
|
"@webex/eslint-config-legacy": "0.0.0",
|
|
25
25
|
"@webex/jest-config-legacy": "0.0.0",
|
|
26
26
|
"@webex/legacy-tools": "0.0.0",
|
|
27
|
-
"@webex/test-helper-appid": "3.
|
|
28
|
-
"@webex/test-helper-automation": "3.
|
|
29
|
-
"@webex/test-helper-chai": "3.
|
|
30
|
-
"@webex/test-helper-mocha": "3.
|
|
31
|
-
"@webex/test-helper-mock-webex": "3.
|
|
32
|
-
"@webex/test-helper-test-users": "3.
|
|
27
|
+
"@webex/test-helper-appid": "3.9.0-multi-llms.0",
|
|
28
|
+
"@webex/test-helper-automation": "3.9.0-multi-llms.0",
|
|
29
|
+
"@webex/test-helper-chai": "3.9.0-multi-llms.0",
|
|
30
|
+
"@webex/test-helper-mocha": "3.9.0-multi-llms.0",
|
|
31
|
+
"@webex/test-helper-mock-webex": "3.9.0-multi-llms.0",
|
|
32
|
+
"@webex/test-helper-test-users": "3.9.0-multi-llms.0",
|
|
33
33
|
"eslint": "^8.24.0",
|
|
34
34
|
"prettier": "^2.7.1",
|
|
35
35
|
"sinon": "^9.2.4"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@webex/common": "3.
|
|
39
|
-
"@webex/internal-plugin-device": "3.
|
|
40
|
-
"@webex/plugin-authorization-node": "3.
|
|
41
|
-
"@webex/storage-adapter-local-storage": "3.
|
|
42
|
-
"@webex/storage-adapter-spec": "3.
|
|
43
|
-
"@webex/webex-core": "3.
|
|
38
|
+
"@webex/common": "3.9.0-multi-llms.0",
|
|
39
|
+
"@webex/internal-plugin-device": "3.9.0-multi-llms.2",
|
|
40
|
+
"@webex/plugin-authorization-node": "3.9.0-multi-llms.2",
|
|
41
|
+
"@webex/storage-adapter-local-storage": "3.9.0-multi-llms.2",
|
|
42
|
+
"@webex/storage-adapter-spec": "3.9.0-multi-llms.0",
|
|
43
|
+
"@webex/webex-core": "3.9.0-multi-llms.2",
|
|
44
44
|
"jsonwebtoken": "^9.0.2",
|
|
45
45
|
"lodash": "^4.17.21",
|
|
46
46
|
"uuid": "^3.3.2"
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"test:style": "eslint ./src/**/*.*",
|
|
55
55
|
"test:unit": "webex-legacy-tools test --unit --runner jest"
|
|
56
56
|
},
|
|
57
|
-
"version": "3.
|
|
57
|
+
"version": "3.9.0-multi-llms.2"
|
|
58
58
|
}
|