@strivacity/sdk-core 3.0.0-rc.0 → 3.0.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/CHANGELOG.md +33 -0
- package/README.md +384 -79
- package/dist/flows/EmbeddedFlow.cjs +1 -1
- package/dist/flows/EmbeddedFlow.cjs.map +1 -1
- package/dist/flows/EmbeddedFlow.d.ts +2 -1
- package/dist/flows/EmbeddedFlow.mjs +1 -1
- package/dist/flows/EmbeddedFlow.mjs.map +1 -1
- package/dist/flows/NativeFlow.cjs +1 -1
- package/dist/flows/NativeFlow.cjs.map +1 -1
- package/dist/flows/NativeFlow.d.ts +1 -1
- package/dist/flows/NativeFlow.mjs +1 -1
- package/dist/flows/NativeFlow.mjs.map +1 -1
- package/dist/handlers/BaseFlowHandler.cjs +2 -0
- package/dist/handlers/BaseFlowHandler.cjs.map +1 -0
- package/dist/{utils/NativeFlowHandler.d.ts → handlers/BaseFlowHandler.d.ts} +11 -12
- package/dist/handlers/BaseFlowHandler.mjs +2 -0
- package/dist/handlers/BaseFlowHandler.mjs.map +1 -0
- package/dist/handlers/EmbeddedFlowHandler.cjs +2 -0
- package/dist/handlers/EmbeddedFlowHandler.cjs.map +1 -0
- package/dist/{utils → handlers}/EmbeddedFlowHandler.d.ts +3 -35
- package/dist/handlers/EmbeddedFlowHandler.mjs +2 -0
- package/dist/handlers/EmbeddedFlowHandler.mjs.map +1 -0
- package/dist/handlers/NativeFlowHandler.cjs +2 -0
- package/dist/handlers/NativeFlowHandler.cjs.map +1 -0
- package/dist/handlers/NativeFlowHandler.d.ts +30 -0
- package/dist/handlers/NativeFlowHandler.mjs +2 -0
- package/dist/handlers/NativeFlowHandler.mjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.ts +48 -56
- package/dist/types.mjs.map +1 -1
- package/package.json +7 -1
- package/dist/utils/EmbeddedFlowHandler.cjs +0 -2
- package/dist/utils/EmbeddedFlowHandler.cjs.map +0 -1
- package/dist/utils/EmbeddedFlowHandler.mjs +0 -2
- package/dist/utils/EmbeddedFlowHandler.mjs.map +0 -1
- package/dist/utils/NativeFlowHandler.cjs +0 -2
- package/dist/utils/NativeFlowHandler.cjs.map +0 -1
- package/dist/utils/NativeFlowHandler.mjs +0 -2
- package/dist/utils/NativeFlowHandler.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
## 3.0.1 (2026-04-20)
|
|
2
|
+
|
|
3
|
+
### 🩹 Fixes
|
|
4
|
+
|
|
5
|
+
- handle language parameter from response body url correctly ([e42294b](https://github.com/Strivacity/sdk-js/commit/e42294b))
|
|
6
|
+
|
|
7
|
+
### 🧱 Updated Dependencies
|
|
8
|
+
|
|
9
|
+
- Updated testing to 3.0.1
|
|
10
|
+
|
|
11
|
+
# 3.0.0 (2026-04-09)
|
|
12
|
+
|
|
13
|
+
### 🚀 Features
|
|
14
|
+
|
|
15
|
+
- custom sdk mode added ([17812c6](https://github.com/Strivacity/sdk-js/commit/17812c6))
|
|
16
|
+
- ⚠️ EmbeddedFlow implemented ([a0e3ad8](https://github.com/Strivacity/sdk-js/commit/a0e3ad8))
|
|
17
|
+
- ⚠️ NativeFlow entry function now returns an object instead of a string ([9a8942d](https://github.com/Strivacity/sdk-js/commit/9a8942d))
|
|
18
|
+
- popupUrlHandler refactored ([886f6ea](https://github.com/Strivacity/sdk-js/commit/886f6ea))
|
|
19
|
+
|
|
20
|
+
### 🩹 Fixes
|
|
21
|
+
|
|
22
|
+
- select and multiselect default value handling fixed ([25ecacd](https://github.com/Strivacity/sdk-js/commit/25ecacd))
|
|
23
|
+
- widget type definitions fixed ([c2e9b12](https://github.com/Strivacity/sdk-js/commit/c2e9b12))
|
|
24
|
+
|
|
25
|
+
### ⚠️ Breaking Changes
|
|
26
|
+
|
|
27
|
+
- EmbeddedFlow implemented ([a0e3ad8](https://github.com/Strivacity/sdk-js/commit/a0e3ad8))
|
|
28
|
+
- NativeFlow entry function now returns an object instead of a string ([9a8942d](https://github.com/Strivacity/sdk-js/commit/9a8942d))
|
|
29
|
+
|
|
30
|
+
### 🧱 Updated Dependencies
|
|
31
|
+
|
|
32
|
+
- Updated testing to 3.0.0
|
|
33
|
+
|
|
1
34
|
## 3.0.0-rc.0 (2026-02-18)
|
|
2
35
|
|
|
3
36
|
### 🚀 Features
|
package/README.md
CHANGED
|
@@ -1,65 +1,90 @@
|
|
|
1
1
|
# @strivacity/sdk-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A framework-agnostic JavaScript/TypeScript client that integrates Strivacity's policy-driven authentication journeys into any application using the OAuth 2.0 PKCE flow. Supports `redirect`, `popup`, `native`, and `embedded` modes.
|
|
4
|
+
|
|
5
|
+
See our [Developer Portal](https://www.strivacity.com/learn-support/developer-hub) to get started with developing with the Strivacity product.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
This SDK provides the core authentication primitives for Strivacity's policy-driven journeys. It is a standalone JavaScript/TypeScript library with no framework dependencies that can be used directly or as the foundation for framework-specific SDKs (React, Vue, Angular, etc.). The SDK uses the OAuth 2.0 PKCE flow to authenticate with Strivacity.
|
|
10
|
+
|
|
11
|
+
## Demo Application
|
|
4
12
|
|
|
5
13
|
- [Example app](https://github.com/Strivacity/sdk-js/tree/main/apps/web-component)
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
## Install
|
|
8
16
|
|
|
9
17
|
```bash
|
|
10
18
|
npm install @strivacity/sdk-core
|
|
11
19
|
```
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Initialization
|
|
24
|
+
|
|
25
|
+
Create a flow instance using the `initFlow` factory function:
|
|
14
26
|
|
|
15
27
|
```js
|
|
16
28
|
import { initFlow } from '@strivacity/sdk-core';
|
|
17
29
|
|
|
18
30
|
const sdk = initFlow({
|
|
19
|
-
mode: 'redirect', // or 'popup'
|
|
31
|
+
mode: 'redirect', // or 'popup', 'native', 'embedded'
|
|
20
32
|
issuer: 'https://<YOUR_DOMAIN>',
|
|
21
33
|
scopes: ['openid', 'profile'],
|
|
22
34
|
clientId: '<YOUR_CLIENT_ID>',
|
|
23
35
|
redirectUri: '<YOUR_REDIRECT_URI>',
|
|
24
36
|
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Redirect / Popup mode
|
|
25
40
|
|
|
41
|
+
In `redirect` mode the user is taken to the identity provider in the same window; in `popup` mode authentication happens in a popup. Both are initiated the same way from code.
|
|
42
|
+
|
|
43
|
+
#### Login
|
|
44
|
+
|
|
45
|
+
```js
|
|
26
46
|
await sdk.login();
|
|
27
47
|
```
|
|
28
48
|
|
|
29
|
-
|
|
49
|
+
#### Callback handling
|
|
30
50
|
|
|
31
|
-
|
|
51
|
+
Process the authorization callback after the identity provider redirects back to your application:
|
|
32
52
|
|
|
33
|
-
|
|
53
|
+
```js
|
|
54
|
+
await sdk.handleCallback();
|
|
55
|
+
```
|
|
34
56
|
|
|
35
|
-
|
|
57
|
+
#### Logout
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
The `postLogoutRedirectUri` parameter is optional and specifies where users are redirected after logout. This URI must be configured in the Admin Console as an allowed post-logout redirect URI.
|
|
38
60
|
|
|
39
|
-
|
|
61
|
+
```js
|
|
62
|
+
await sdk.logout({ postLogoutRedirectUri: location.origin });
|
|
63
|
+
```
|
|
40
64
|
|
|
41
|
-
|
|
42
|
-
- `issuer` (string): The issuer URL of the OpenID Provider.
|
|
43
|
-
- `clientId` (string): The client identifier for the application.
|
|
44
|
-
- `redirectUri` (string): The URI to redirect to after authentication.
|
|
45
|
-
- `scopes` (Array<string>): The scopes to request during authentication.
|
|
46
|
-
- `responseType` (ResponseType): The response type requested from the OpenID Provider.
|
|
47
|
-
- `responseMode` (ResponseMode): The response mode to use.
|
|
48
|
-
- `storageTokenName` (string): The name of the token in storage.
|
|
49
|
-
- `storage` (SDKStorageType): A custom storage implementation.
|
|
65
|
+
#### Token refresh
|
|
50
66
|
|
|
51
|
-
|
|
52
|
-
- `popup`: Uses a popup window for authentication. Returns an instance of `PopupFlow`.
|
|
53
|
-
- `redirect`: Uses a full-page redirect for authentication. Returns an instance of `RedirectFlow`.
|
|
54
|
-
- `native`: Uses a native flow for authentication. Returns an instance of `NativeFlow`.
|
|
67
|
+
Refreshes the access token using the stored refresh token, keeping the session alive without requiring the user to log in again.
|
|
55
68
|
|
|
56
|
-
|
|
69
|
+
```js
|
|
70
|
+
await sdk.refresh();
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### Token revocation
|
|
74
|
+
|
|
75
|
+
Revokes the current access and refresh tokens on the server, invalidating the session immediately.
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
await sdk.revoke();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Native mode
|
|
57
82
|
|
|
58
|
-
|
|
83
|
+
In `native` mode, `login()` returns a `NativeFlowHandler` instance that drives the authentication UI step by step.
|
|
59
84
|
|
|
60
|
-
|
|
85
|
+
#### Login page example
|
|
61
86
|
|
|
62
|
-
|
|
87
|
+
The login page extracts `session_id` and optionally `language` from the URL on load and passes them to `startSession()` to resume an existing flow or start a new one. When a `language` parameter is present it overrides `uiLocales` to display the authentication UI in the specified language:
|
|
63
88
|
|
|
64
89
|
```js
|
|
65
90
|
import { initFlow } from '@strivacity/sdk-core';
|
|
@@ -72,60 +97,243 @@ const sdk = initFlow({
|
|
|
72
97
|
redirectUri: '<YOUR_REDIRECT_URI>',
|
|
73
98
|
});
|
|
74
99
|
|
|
75
|
-
//
|
|
100
|
+
// Returns a NativeFlowHandler instance
|
|
76
101
|
const handler = await sdk.login();
|
|
77
102
|
|
|
103
|
+
// Extract session_id from URL if present
|
|
104
|
+
const url = new URL(window.location.href);
|
|
105
|
+
const sessionId = url.searchParams.get('session_id');
|
|
106
|
+
|
|
78
107
|
// Start a new session or resume an existing one
|
|
79
|
-
const state = await handler.startSession();
|
|
108
|
+
const state = await handler.startSession(sessionId);
|
|
80
109
|
|
|
81
110
|
if (state.finalizeUrl) {
|
|
82
|
-
//
|
|
111
|
+
// Finalize the session when the flow is complete
|
|
83
112
|
await handler.finalizeSession(state.finalizeUrl);
|
|
84
113
|
} else {
|
|
85
|
-
//
|
|
86
|
-
console.log('
|
|
114
|
+
// Render the form from state.widgets and handle user input
|
|
115
|
+
console.log('Current form state:', state);
|
|
87
116
|
}
|
|
117
|
+
```
|
|
88
118
|
|
|
89
|
-
|
|
119
|
+
#### Submitting a form
|
|
120
|
+
|
|
121
|
+
```js
|
|
90
122
|
const formState = await handler.submitForm('formId', {
|
|
91
|
-
|
|
123
|
+
email: 'user@example.com',
|
|
124
|
+
password: 'secret',
|
|
92
125
|
});
|
|
93
126
|
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
127
|
+
if (formState.finalizeUrl) {
|
|
128
|
+
await handler.finalizeSession(formState.finalizeUrl);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### Callback page example
|
|
133
|
+
|
|
134
|
+
When a `session_id` is present in the URL the native flow is resumed by forwarding it to the login page. Otherwise the standard `handleCallback()` path is used:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
const url = new URL(location.href);
|
|
138
|
+
const sessionId = url.searchParams.get('session_id');
|
|
139
|
+
|
|
140
|
+
if (sessionId) {
|
|
141
|
+
// Forward to login page with session_id to resume the native flow
|
|
142
|
+
window.location.href = `/login?session_id=${sessionId}`;
|
|
97
143
|
} else {
|
|
98
|
-
|
|
99
|
-
|
|
144
|
+
try {
|
|
145
|
+
await sdk.handleCallback();
|
|
146
|
+
window.location.href = '/profile';
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error('Error during callback handling:', error);
|
|
149
|
+
}
|
|
100
150
|
}
|
|
101
151
|
```
|
|
102
152
|
|
|
103
|
-
|
|
153
|
+
#### Entry page example
|
|
154
|
+
|
|
155
|
+
The entry page processes flows started by an external process (e.g. password reset) by calling `entry()` to extract the necessary parameters to resume the flow:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
try {
|
|
159
|
+
const data = await sdk.entry();
|
|
160
|
+
|
|
161
|
+
if (data && Object.keys(data).length > 0) {
|
|
162
|
+
window.location.href = `/callback?${new URLSearchParams(data).toString()}`;
|
|
163
|
+
} else {
|
|
164
|
+
window.location.href = '/';
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error('Entry failed:', error);
|
|
168
|
+
window.location.href = '/';
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Custom mode
|
|
173
|
+
|
|
174
|
+
Custom mode lets you replace the built-in flow implementations with your own classes. This is useful when you need to proxy authentication through a backend server (BFF pattern) rather than communicating with the identity provider directly from the browser.
|
|
175
|
+
|
|
176
|
+
Pass `mode: 'custom'` together with a `customFlow` class that extends one of the built-in flow base classes (`NativeFlow`, `EmbeddedFlow`, etc.):
|
|
104
177
|
|
|
105
|
-
|
|
178
|
+
```ts
|
|
179
|
+
import { initFlow } from '@strivacity/sdk-core';
|
|
180
|
+
import { CustomNativeFlow } from './sdk/CustomNativeFlow';
|
|
106
181
|
|
|
107
|
-
|
|
182
|
+
const sdk = initFlow({
|
|
183
|
+
mode: 'custom',
|
|
184
|
+
issuer: 'https://<YOUR_DOMAIN>',
|
|
185
|
+
scopes: ['openid', 'profile'],
|
|
186
|
+
clientId: '<YOUR_CLIENT_ID>',
|
|
187
|
+
redirectUri: '<YOUR_REDIRECT_URI>',
|
|
188
|
+
customFlow: CustomNativeFlow,
|
|
189
|
+
});
|
|
190
|
+
```
|
|
108
191
|
|
|
109
|
-
|
|
192
|
+
#### Creating a custom flow class
|
|
110
193
|
|
|
111
|
-
|
|
194
|
+
Extend `NativeFlow` (or `EmbeddedFlow`) and override the methods you need to change. The example below delegates session management to a backend API instead of calling the identity provider directly:
|
|
112
195
|
|
|
113
|
-
|
|
196
|
+
```ts
|
|
197
|
+
import type { NativeParams } from '@strivacity/sdk-core';
|
|
198
|
+
import { NativeFlow } from '@strivacity/sdk-core/flows/NativeFlow';
|
|
199
|
+
import { Session } from '@strivacity/sdk-core/utils/Session';
|
|
200
|
+
import { CustomNativeFlowHandler } from './CustomNativeFlowHandler';
|
|
114
201
|
|
|
115
|
-
|
|
202
|
+
export class CustomNativeFlow extends NativeFlow {
|
|
203
|
+
override login(params: NativeParams = {}): CustomNativeFlowHandler {
|
|
204
|
+
this.dispatchEvent('loginInitiated', []);
|
|
205
|
+
return new CustomNativeFlowHandler(this, params);
|
|
206
|
+
}
|
|
116
207
|
|
|
117
|
-
|
|
208
|
+
override register(params: NativeParams = {}): CustomNativeFlowHandler {
|
|
209
|
+
params.prompt = 'create';
|
|
210
|
+
return this.login(params);
|
|
211
|
+
}
|
|
118
212
|
|
|
119
|
-
|
|
213
|
+
async fetchSessionData(): Promise<void> {
|
|
214
|
+
const response = await this.httpClient.request(new URL('/api/session/info', location.origin).toString(), { method: 'GET', credentials: 'include' });
|
|
215
|
+
|
|
216
|
+
this.session = Object.assign(new Session(), await response.json());
|
|
217
|
+
|
|
218
|
+
if (!this.accessTokenExpired) {
|
|
219
|
+
this.dispatchEvent('loggedIn', [{ claims: this.idTokenClaims! }]);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
override async refresh(): Promise<void> {
|
|
224
|
+
const response = await this.httpClient.request(new URL('/api/session/refresh', location.origin).toString(), { method: 'POST', credentials: 'include' });
|
|
120
225
|
|
|
121
|
-
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
this.session = null;
|
|
228
|
+
this.dispatchEvent('tokenRefreshFailed', [{}]);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
122
231
|
|
|
123
|
-
|
|
232
|
+
this.session = Object.assign(new Session(), await response.json());
|
|
233
|
+
this.dispatchEvent('tokenRefreshed', [{ claims: this.idTokenClaims! }]);
|
|
234
|
+
}
|
|
124
235
|
|
|
125
|
-
|
|
236
|
+
override async logout(): Promise<void> {
|
|
237
|
+
this.dispatchEvent('logoutInitiated', []);
|
|
238
|
+
|
|
239
|
+
await this.httpClient.request(new URL('/api/session/logout', location.origin).toString(), { method: 'POST', credentials: 'include' });
|
|
240
|
+
|
|
241
|
+
this.session = null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
#### Creating a custom flow handler
|
|
247
|
+
|
|
248
|
+
Extend `NativeFlowHandler` and override `startSession` and `finalizeSession` to communicate with your backend:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
import type { LoginFlowState } from '@strivacity/sdk-core';
|
|
252
|
+
import { NativeFlowHandler } from '@strivacity/sdk-core/handlers/NativeFlowHandler';
|
|
253
|
+
import { CustomNativeFlow } from './CustomNativeFlow';
|
|
254
|
+
|
|
255
|
+
export class CustomNativeFlowHandler extends NativeFlowHandler {
|
|
256
|
+
declare sdk: CustomNativeFlow;
|
|
257
|
+
|
|
258
|
+
override async startSession(sessionId?: string | null): Promise<LoginFlowState | void> {
|
|
259
|
+
if (sessionId) {
|
|
260
|
+
this.sessionId = sessionId;
|
|
261
|
+
return this.submitForm();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const response = await this.sdk.httpClient.request(new URL('/api/session/start', location.origin).toString(), {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
credentials: 'include',
|
|
267
|
+
headers: { 'Content-Type': 'application/json' },
|
|
268
|
+
body: JSON.stringify(this.params),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const data = await response.json();
|
|
272
|
+
this.sessionId = data.session_id;
|
|
273
|
+
|
|
274
|
+
return this.submitForm();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
override async finalizeSession(finalizeUrl: string): Promise<void> {
|
|
278
|
+
const finalizeResponse = await this.sdk.httpClient.request(finalizeUrl, {
|
|
279
|
+
method: 'GET',
|
|
280
|
+
headers: { Authorization: `Bearer ${this.sessionId}` },
|
|
281
|
+
credentials: 'include',
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const redirectUri = new URL(await finalizeResponse.text());
|
|
285
|
+
|
|
286
|
+
await this.sdk.httpClient.request(new URL('/api/session/finalize', location.origin).toString(), {
|
|
287
|
+
method: 'POST',
|
|
288
|
+
headers: {
|
|
289
|
+
'Content-Type': 'application/json',
|
|
290
|
+
Authorization: `Bearer ${this.sessionId}`,
|
|
291
|
+
},
|
|
292
|
+
body: JSON.stringify(Object.fromEntries(redirectUri.searchParams)),
|
|
293
|
+
credentials: 'include',
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await this.sdk.fetchSessionData();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Event subscription
|
|
302
|
+
|
|
303
|
+
The SDK provides an event system for listening to authentication lifecycle events:
|
|
126
304
|
|
|
127
305
|
```js
|
|
128
|
-
|
|
306
|
+
const { dispose } = sdk.subscribeToEvent('loggedIn', (eventData) => {
|
|
307
|
+
console.log('User logged in:', eventData);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Later: unsubscribe
|
|
311
|
+
dispose();
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Available events:
|
|
315
|
+
|
|
316
|
+
- **`init`**: Fired when the SDK is initialized.
|
|
317
|
+
- **`loggedIn`**: Fired when a user successfully logs in.
|
|
318
|
+
- **`loginInitiated`**: Fired when a login flow is initiated.
|
|
319
|
+
- **`sessionLoaded`**: Fired when an existing session is loaded.
|
|
320
|
+
- **`accessTokenExpired`**: Fired when the access token expires.
|
|
321
|
+
- **`tokenRefreshed`**: Fired when access tokens are successfully refreshed.
|
|
322
|
+
- **`tokenRefreshFailed`**: Fired when token refresh fails.
|
|
323
|
+
- **`logoutInitiated`**: Fired when logout is initiated.
|
|
324
|
+
- **`tokenRevoked`**: Fired when tokens are successfully revoked.
|
|
325
|
+
- **`tokenRevokeFailed`**: Fired when token revocation fails.
|
|
326
|
+
|
|
327
|
+
## Logging
|
|
328
|
+
|
|
329
|
+
The SDK supports optional logging to help you debug authentication flows and monitor SDK behavior. You can enable the built-in console logger or provide your own custom logger implementation.
|
|
330
|
+
|
|
331
|
+
### Using the Default Logger
|
|
332
|
+
|
|
333
|
+
Enable the default console logger by adding the `logging` option when calling `initFlow`:
|
|
334
|
+
|
|
335
|
+
```js
|
|
336
|
+
import { initFlow, DefaultLogging } from '@strivacity/sdk-core';
|
|
129
337
|
|
|
130
338
|
const sdk = initFlow({
|
|
131
339
|
mode: 'redirect',
|
|
@@ -133,48 +341,145 @@ const sdk = initFlow({
|
|
|
133
341
|
scopes: ['openid', 'profile'],
|
|
134
342
|
clientId: '<YOUR_CLIENT_ID>',
|
|
135
343
|
redirectUri: '<YOUR_REDIRECT_URI>',
|
|
344
|
+
logging: DefaultLogging,
|
|
136
345
|
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The default logger writes to the browser console and automatically prefixes messages with a correlation ID when available (via the `xEventId` property).
|
|
349
|
+
|
|
350
|
+
### Creating a Custom Logger
|
|
351
|
+
|
|
352
|
+
Implement the `SDKLogging` interface and pass your class to the `logging` option:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import type { SDKLogging } from '@strivacity/sdk-core';
|
|
137
356
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
357
|
+
export class MyLogger implements SDKLogging {
|
|
358
|
+
xEventId?: string;
|
|
359
|
+
|
|
360
|
+
debug(message: string): void {
|
|
361
|
+
console.debug(this.xEventId ? `[${this.xEventId}] ${message}` : message);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
info(message: string): void {
|
|
365
|
+
console.info(this.xEventId ? `[${this.xEventId}] ${message}` : message);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
warn(message: string): void {
|
|
369
|
+
console.warn(this.xEventId ? `[${this.xEventId}] ${message}` : message);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
error(message: string, error: Error): void {
|
|
373
|
+
console.error(this.xEventId ? `[${this.xEventId}] ${message}` : message, error);
|
|
374
|
+
}
|
|
142
375
|
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
```js
|
|
379
|
+
import { initFlow } from '@strivacity/sdk-core';
|
|
380
|
+
import { MyLogger } from './logging/MyLogger';
|
|
381
|
+
|
|
382
|
+
const sdk = initFlow({
|
|
383
|
+
// ...other options
|
|
384
|
+
logging: MyLogger,
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
The `SDKLogging` interface requires `debug`, `info`, `warn`, and `error` methods. The optional `xEventId` property, when set by the SDK, provides a correlation ID to trace related log messages across the authentication flow.
|
|
389
|
+
|
|
390
|
+
## API Documentation
|
|
391
|
+
|
|
392
|
+
### `initFlow(options)`
|
|
143
393
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
sdk.subscribeToEvent('sessionLoaded', updateSession);
|
|
147
|
-
sdk.subscribeToEvent('tokenRefreshed', updateSession);
|
|
148
|
-
sdk.subscribeToEvent('tokenRefreshFailed', updateSession);
|
|
149
|
-
sdk.subscribeToEvent('logoutInitiated', updateSession);
|
|
150
|
-
sdk.subscribeToEvent('tokenRevoked', updateSession);
|
|
151
|
-
sdk.subscribeToEvent('tokenRevokeFailed', updateSession);
|
|
394
|
+
```typescript
|
|
395
|
+
initFlow(options: SDKOptions): PopupFlow | RedirectFlow | NativeFlow | EmbeddedFlow
|
|
152
396
|
```
|
|
153
397
|
|
|
154
|
-
|
|
398
|
+
The `initFlow` function creates and returns a flow instance based on the `mode` specified in `options`.
|
|
399
|
+
|
|
400
|
+
**Parameters:**
|
|
401
|
+
|
|
402
|
+
| Name | Type | Required | Description |
|
|
403
|
+
| ------------------ | ------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------ |
|
|
404
|
+
| `issuer` | `string` | ✅ | The issuer URL of the OpenID Provider. |
|
|
405
|
+
| `clientId` | `string` | ✅ | The client identifier for the application. |
|
|
406
|
+
| `redirectUri` | `string` | ✅ | The URI to redirect to after authentication. |
|
|
407
|
+
| `mode` | `'redirect' \| 'popup' \| 'native' \| 'embedded' \| 'custom'` | ⬜ | The authentication flow mode. Defaults to `'redirect'`. |
|
|
408
|
+
| `scopes` | `string[]` | ⬜ | The scopes to request. Defaults to `['openid']`. |
|
|
409
|
+
| `responseType` | `'code' \| 'id_token'` | ⬜ | The response type. |
|
|
410
|
+
| `responseMode` | `'query' \| 'fragment'` | ⬜ | The response mode. |
|
|
411
|
+
| `storageTokenName` | `string` | ⬜ | Custom name for the session storage key. Defaults to `'sty.session'`. |
|
|
412
|
+
| `storage` | `SDKStorageType` | ⬜ | Custom storage implementation. Defaults to `LocalStorage`. |
|
|
413
|
+
| `httpClient` | `SDKHttpClientType` | ⬜ | Custom HTTP client implementation. |
|
|
414
|
+
| `logging` | `SDKLoggingType` | ⬜ | Logging implementation class. |
|
|
415
|
+
| `customFlow` | `FlowType` | ⬜ | Custom flow class. Required when `mode` is `'custom'`. Must extend `BaseFlow`. |
|
|
416
|
+
|
|
417
|
+
**Returns:** A flow instance (`PopupFlow`, `RedirectFlow`, `NativeFlow`, `EmbeddedFlow`, or your custom flow class) depending on the configured `mode`.
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
### Flow instance methods
|
|
422
|
+
|
|
423
|
+
All flow instances share the following methods:
|
|
155
424
|
|
|
156
|
-
- **`
|
|
157
|
-
- **`
|
|
158
|
-
- **`
|
|
159
|
-
- **`
|
|
160
|
-
- **`
|
|
161
|
-
- **`
|
|
162
|
-
- **`
|
|
163
|
-
- **`
|
|
425
|
+
- **`login(options?: LoginOptions): Promise<void | NativeFlowHandler>`**: Initiates login. In `native` mode returns a `NativeFlowHandler`.
|
|
426
|
+
- **`register(options?: RegisterOptions): Promise<void | NativeFlowHandler>`**: Initiates registration.
|
|
427
|
+
- **`entry(url?: string): Promise<Record<string, string>>`**: Processes an externally-initiated flow URL and returns parameters needed to resume the flow.
|
|
428
|
+
- **`handleCallback(url?: string): Promise<void>`**: Processes the authorization callback.
|
|
429
|
+
- **`refresh(): Promise<void>`**: Refreshes the session tokens.
|
|
430
|
+
- **`revoke(): Promise<void>`**: Revokes the current session tokens.
|
|
431
|
+
- **`logout(options?: LogoutOptions): Promise<void>`**: Logs the user out.
|
|
432
|
+
- **`subscribeToEvent(eventType: string, callback: Function): { dispose(): void }`**: Subscribes to an authentication lifecycle event. Returns an object with a `dispose()` method to unsubscribe.
|
|
164
433
|
|
|
165
|
-
|
|
434
|
+
**Session state getters:**
|
|
166
435
|
|
|
167
|
-
|
|
436
|
+
- **`isAuthenticated: Promise<boolean>`**: Resolves to `true` when a valid session exists.
|
|
437
|
+
- **`isAuthenticatedSync: boolean`**: Synchronous check for an active session.
|
|
438
|
+
- **`idTokenClaims: IdTokenClaims | null`**: Claims from the ID token.
|
|
439
|
+
- **`accessToken: string | null`**: The current access token.
|
|
440
|
+
- **`refreshToken: string | null`**: The current refresh token.
|
|
441
|
+
- **`accessTokenExpired: boolean`**: `true` when the access token has expired.
|
|
442
|
+
- **`accessTokenExpirationDate: number | null`**: Expiration timestamp (Unix seconds) of the access token.
|
|
168
443
|
|
|
169
|
-
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
### `NativeFlowHandler`
|
|
447
|
+
|
|
448
|
+
Returned by `NativeFlow.login()` and `NativeFlow.register()` to drive the step-by-step native authentication UI.
|
|
449
|
+
|
|
450
|
+
- **`startSession(sessionId?: string | null): Promise<LoginFlowState | void>`**: Starts a new authentication session or resumes an existing one when `sessionId` is provided.
|
|
451
|
+
- **`submitForm(formId?: string, body?: Record<string, unknown>): Promise<LoginFlowState>`**: Submits a form step in the authentication flow.
|
|
452
|
+
- **`finalizeSession(finalizeUrl: string): Promise<void>`**: Finalizes the session using the URL returned in `LoginFlowState.finalizeUrl`.
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
### `subscribeToEvent(eventType, callback)`
|
|
457
|
+
|
|
458
|
+
Subscribe to SDK lifecycle events.
|
|
170
459
|
|
|
171
460
|
**Parameters:**
|
|
172
461
|
|
|
173
|
-
|
|
174
|
-
|
|
462
|
+
| Name | Type | Required | Description |
|
|
463
|
+
| ----------- | -------------------------- | -------- | ---------------------------------------- |
|
|
464
|
+
| `eventType` | `string` | ✅ | The event type to listen for. |
|
|
465
|
+
| `callback` | `(eventData: any) => void` | ✅ | The handler called when the event fires. |
|
|
466
|
+
|
|
467
|
+
**Returns:** `{ dispose(): void }` — call `dispose()` to unsubscribe.
|
|
468
|
+
|
|
469
|
+
## Vulnerability Reporting
|
|
470
|
+
|
|
471
|
+
The [Guidelines for responsible disclosure](https://www.strivacity.com/report-a-security-issue) details the procedure for disclosing security issues. Please do not report security vulnerabilities on the public issue tracker.
|
|
472
|
+
|
|
473
|
+
## License
|
|
474
|
+
|
|
475
|
+
@strivacity/sdk-core is available under the MIT License. See the [LICENSE](https://github.com/Strivacity/sdk-js/blob/main/LICENSE) file for more info.
|
|
476
|
+
|
|
477
|
+
## Contributing
|
|
478
|
+
|
|
479
|
+
Please see our [contributing guide](https://github.com/Strivacity/sdk-js/blob/main/CONTRIBUTING.md).
|
|
175
480
|
|
|
176
481
|
## Migrating to v3.0
|
|
177
482
|
|
|
178
483
|
### Entry API Major Changes
|
|
179
484
|
|
|
180
|
-
Strivacity SDK's `entry()` API now returns a structured object instead of a plain string.
|
|
485
|
+
Strivacity SDK's `entry()` API now returns a structured object instead of a plain string. Check the example above in the usage section for more details.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("../utils/handlers.cjs"),
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("../utils/handlers.cjs"),g=require("../handlers/EmbeddedFlowHandler.cjs"),u=require("./BaseFlow.cjs");require("../utils/errors.cjs");require("../handlers/BaseFlowHandler.cjs");require("../utils/State.cjs");require("../utils/crypto.cjs");require("../utils/base64Url.cjs");require("../utils/date.cjs");require("../utils/jwt.cjs");require("../utils/Metadata.cjs");require("../utils/Session.cjs");class p extends u.BaseFlow{constructor(r,e,s,i){r.urlHandler||(r.urlHandler=c.redirectUrlHandler),r.callbackHandler||(r.callbackHandler=c.redirectCallbackHandler),super(r,e,s,i),globalThis.sty||(globalThis.sty={}),globalThis.sty.oidcService=this}login(r={}){return this.dispatchEvent("loginInitiated",[]),new g.EmbeddedFlowHandler(this,r)}register(r={}){return r.prompt="create",this.login(r)}async entry(r){r||(r=globalThis.window?.location.href);const e=new URL(r);e.searchParams.append("sdk","web-embedded"),e.searchParams.append("client_id",this.options.clientId),e.searchParams.append("redirect_uri",this.options.redirectUri);const s=await this.httpClient.request(`${this.options.issuer}/provider/flow/entry?${e.searchParams.toString()}`,{headers:{"Accept-Language":"*"}});if(!s.ok){if(s.status===400){const o=await s.json();let n="Entry request failed with status 400";typeof o=="object"&&(o.error?n=`${o.error}: ${o.error_description}`:o.errorKey&&(n=o.errorKey));const d=new Error(n);throw this.logging?.error("Entry request error",d),d}const t=new Error(`Entry request failed with status ${s.status}`);throw this.logging?.error("Entry request error",t),t}let i;try{i=new URL(await s.text())}catch{i=new URL(s.url)}const a=i.searchParams.get("short_app_id"),l=i.searchParams.get("session_id"),h=i.searchParams.get("language")||navigator.language;if(!a){const t=new Error('"short_app_id" is missing from the response');throw this.logging?.error("Entry response error",t),t}if(!l){const t=new Error('"session_id" is missing from the response');throw this.logging?.error("Entry response error",t),t}return{session_id:l,short_app_id:a,language:h}}async handleCallback(r){if(typeof this.options.callbackHandler!="function"){const e=new Error("Missing option: callbackHandler");throw this.logging?.error("Required option missing",e),e}r||(r=globalThis.window?.location.href),await this.tokenExchange(await this.options.callbackHandler(r,this.options.responseMode||"fragment"))}}exports.EmbeddedFlow=p;
|
|
2
2
|
//# sourceMappingURL=EmbeddedFlow.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmbeddedFlow.cjs","sources":["../../src/flows/EmbeddedFlow.ts"],"sourcesContent":["import type { SDKOptions, SDKStorage, SDKHttpClient, SDKLogging, ExtraRequestArgs } from '../types';\nimport { redirectUrlHandler, redirectCallbackHandler } from '../utils/handlers';\nimport { EmbeddedFlowHandler } from '../
|
|
1
|
+
{"version":3,"file":"EmbeddedFlow.cjs","sources":["../../src/flows/EmbeddedFlow.ts"],"sourcesContent":["import type { SDKOptions, SDKStorage, SDKHttpClient, SDKLogging, ExtraRequestArgs } from '../types';\nimport { redirectUrlHandler, redirectCallbackHandler } from '../utils/handlers';\nimport { EmbeddedFlowHandler } from '../handlers/EmbeddedFlowHandler';\nimport { BaseFlow } from './BaseFlow';\n\nexport class EmbeddedFlow extends BaseFlow<SDKOptions, ExtraRequestArgs> {\n\tconstructor(options: SDKOptions, storage: SDKStorage, httpClient: SDKHttpClient, logging?: SDKLogging) {\n\t\tif (!options.urlHandler) {\n\t\t\toptions.urlHandler = redirectUrlHandler;\n\t\t}\n\t\tif (!options.callbackHandler) {\n\t\t\toptions.callbackHandler = redirectCallbackHandler;\n\t\t}\n\n\t\tsuper(options, storage, httpClient, logging);\n\n\t\tif (!globalThis.sty) {\n\t\t\tglobalThis.sty = {};\n\t\t}\n\n\t\t// NOTE: Register the OIDC service instance globally for use in the login component\n\t\tglobalThis.sty.oidcService = this;\n\t}\n\n\t/**\n\t * Initiates the login process via embedded UI.\n\t * @param {ExtraRequestArgs} [params={}] Optional parameters for the login request.\n\t * @returns {EmbeddedFlowHandler} Returns with an embedded login handler.\n\t */\n\toverride login(params: ExtraRequestArgs = {}): EmbeddedFlowHandler {\n\t\tthis.dispatchEvent('loginInitiated', []);\n\n\t\treturn new EmbeddedFlowHandler(this, params);\n\t}\n\n\toverride register(params: ExtraRequestArgs = {}) {\n\t\tparams.prompt = 'create';\n\n\t\treturn this.login(params);\n\t}\n\n\toverride async entry(url?: string) {\n\t\tif (!url) {\n\t\t\turl = globalThis.window?.location.href;\n\t\t}\n\n\t\tconst entryUrl = new URL(url);\n\t\tentryUrl.searchParams.append('sdk', 'web-embedded');\n\t\tentryUrl.searchParams.append('client_id', this.options.clientId);\n\t\tentryUrl.searchParams.append('redirect_uri', this.options.redirectUri);\n\n\t\tconst response = await this.httpClient.request<string | Record<string, string>>(\n\t\t\t`${this.options.issuer}/provider/flow/entry?${entryUrl.searchParams.toString()}`,\n\t\t\t{\n\t\t\t\theaders: {\n\t\t\t\t\t'Accept-Language': '*',\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\n\t\tif (!response.ok) {\n\t\t\tif (response.status === 400) {\n\t\t\t\tconst data = await response.json();\n\t\t\t\tlet message = 'Entry request failed with status 400';\n\n\t\t\t\tif (typeof data === 'object') {\n\t\t\t\t\tif (data.error) {\n\t\t\t\t\t\tmessage = `${data.error}: ${data.error_description}`;\n\t\t\t\t\t} else if (data.errorKey) {\n\t\t\t\t\t\tmessage = data.errorKey;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst error = new Error(message);\n\t\t\t\tthis.logging?.error('Entry request error', error);\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\tconst error = new Error(`Entry request failed with status ${response.status}`);\n\t\t\tthis.logging?.error('Entry request error', error);\n\t\t\tthrow error;\n\t\t}\n\n\t\tlet uri: URL;\n\n\t\ttry {\n\t\t\turi = new URL(await response.text());\n\t\t} catch {\n\t\t\turi = new URL(response.url);\n\t\t}\n\n\t\tconst shortAppId = uri.searchParams.get('short_app_id');\n\t\tconst sessionId = uri.searchParams.get('session_id');\n\t\tconst language = uri.searchParams.get('language') || navigator.language;\n\n\t\tif (!shortAppId) {\n\t\t\tconst error = new Error('\"short_app_id\" is missing from the response');\n\t\t\tthis.logging?.error('Entry response error', error);\n\t\t\tthrow error;\n\t\t}\n\t\tif (!sessionId) {\n\t\t\tconst error = new Error('\"session_id\" is missing from the response');\n\t\t\tthis.logging?.error('Entry response error', error);\n\t\t\tthrow error;\n\t\t}\n\n\t\treturn { session_id: sessionId, short_app_id: shortAppId, language: language };\n\t}\n\n\t/**\n\t * Handles the callback after login or registration via a redirect.\n\t * @param {string} [url] The URL to handle the callback from. Defaults to the current window location.\n\t * @returns {Promise<void>} A promise that resolves when the callback is handled.\n\t *\n\t * @throws {Error} Throws an error if callback handler is not defined.\n\t */\n\tasync handleCallback(url?: string): Promise<void> {\n\t\tif (typeof this.options.callbackHandler !== 'function') {\n\t\t\tconst error = new Error('Missing option: callbackHandler');\n\t\t\tthis.logging?.error('Required option missing', error);\n\t\t\tthrow error;\n\t\t}\n\n\t\tif (!url) {\n\t\t\turl = globalThis.window?.location.href;\n\t\t}\n\n\t\tawait this.tokenExchange((await this.options.callbackHandler(url, this.options.responseMode || 'fragment')) as Record<string, string>);\n\t}\n}\n"],"names":["EmbeddedFlow","BaseFlow","options","storage","httpClient","logging","redirectUrlHandler","redirectCallbackHandler","params","EmbeddedFlowHandler","url","entryUrl","response","data","message","error","uri","shortAppId","sessionId","language"],"mappings":"yeAKO,MAAMA,UAAqBC,EAAAA,QAAuC,CACxE,YAAYC,EAAqBC,EAAqBC,EAA2BC,EAAsB,CACjGH,EAAQ,aACZA,EAAQ,WAAaI,EAAAA,oBAEjBJ,EAAQ,kBACZA,EAAQ,gBAAkBK,EAAAA,yBAG3B,MAAML,EAASC,EAASC,EAAYC,CAAO,EAEtC,WAAW,MACf,WAAW,IAAM,CAAA,GAIlB,WAAW,IAAI,YAAc,IAC9B,CAOS,MAAMG,EAA2B,GAAyB,CAClE,YAAK,cAAc,iBAAkB,EAAE,EAEhC,IAAIC,EAAAA,oBAAoB,KAAMD,CAAM,CAC5C,CAES,SAASA,EAA2B,GAAI,CAChD,OAAAA,EAAO,OAAS,SAET,KAAK,MAAMA,CAAM,CACzB,CAEA,MAAe,MAAME,EAAc,CAC7BA,IACJA,EAAM,WAAW,QAAQ,SAAS,MAGnC,MAAMC,EAAW,IAAI,IAAID,CAAG,EAC5BC,EAAS,aAAa,OAAO,MAAO,cAAc,EAClDA,EAAS,aAAa,OAAO,YAAa,KAAK,QAAQ,QAAQ,EAC/DA,EAAS,aAAa,OAAO,eAAgB,KAAK,QAAQ,WAAW,EAErE,MAAMC,EAAW,MAAM,KAAK,WAAW,QACtC,GAAG,KAAK,QAAQ,MAAM,wBAAwBD,EAAS,aAAa,UAAU,GAC9E,CACC,QAAS,CACR,kBAAmB,GAAA,CACpB,CACD,EAGD,GAAI,CAACC,EAAS,GAAI,CACjB,GAAIA,EAAS,SAAW,IAAK,CAC5B,MAAMC,EAAO,MAAMD,EAAS,KAAA,EAC5B,IAAIE,EAAU,uCAEV,OAAOD,GAAS,WACfA,EAAK,MACRC,EAAU,GAAGD,EAAK,KAAK,KAAKA,EAAK,iBAAiB,GACxCA,EAAK,WACfC,EAAUD,EAAK,WAIjB,MAAME,EAAQ,IAAI,MAAMD,CAAO,EAC/B,WAAK,SAAS,MAAM,sBAAuBC,CAAK,EAC1CA,CACP,CAEA,MAAMA,EAAQ,IAAI,MAAM,oCAAoCH,EAAS,MAAM,EAAE,EAC7E,WAAK,SAAS,MAAM,sBAAuBG,CAAK,EAC1CA,CACP,CAEA,IAAIC,EAEJ,GAAI,CACHA,EAAM,IAAI,IAAI,MAAMJ,EAAS,MAAM,CACpC,MAAQ,CACPI,EAAM,IAAI,IAAIJ,EAAS,GAAG,CAC3B,CAEA,MAAMK,EAAaD,EAAI,aAAa,IAAI,cAAc,EAChDE,EAAYF,EAAI,aAAa,IAAI,YAAY,EAC7CG,EAAWH,EAAI,aAAa,IAAI,UAAU,GAAK,UAAU,SAE/D,GAAI,CAACC,EAAY,CAChB,MAAMF,EAAQ,IAAI,MAAM,6CAA6C,EACrE,WAAK,SAAS,MAAM,uBAAwBA,CAAK,EAC3CA,CACP,CACA,GAAI,CAACG,EAAW,CACf,MAAMH,EAAQ,IAAI,MAAM,2CAA2C,EACnE,WAAK,SAAS,MAAM,uBAAwBA,CAAK,EAC3CA,CACP,CAEA,MAAO,CAAE,WAAYG,EAAW,aAAcD,EAAY,SAAAE,CAAA,CAC3D,CASA,MAAM,eAAeT,EAA6B,CACjD,GAAI,OAAO,KAAK,QAAQ,iBAAoB,WAAY,CACvD,MAAMK,EAAQ,IAAI,MAAM,iCAAiC,EACzD,WAAK,SAAS,MAAM,0BAA2BA,CAAK,EAC9CA,CACP,CAEKL,IACJA,EAAM,WAAW,QAAQ,SAAS,MAGnC,MAAM,KAAK,cAAe,MAAM,KAAK,QAAQ,gBAAgBA,EAAK,KAAK,QAAQ,cAAgB,UAAU,CAA4B,CACtI,CACD"}
|