@flexbe/sdk 0.2.29 → 0.2.31
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/dist/browser/client/api-client.js +28 -5
- package/dist/browser/client/client.js +6 -5
- package/dist/browser/client/token-manager.js +73 -180
- package/dist/cjs/client/api-client.js +25 -4
- package/dist/cjs/client/client.js +8 -4
- package/dist/cjs/client/token-manager.js +68 -179
- package/dist/esm/client/api-client.js +26 -5
- package/dist/esm/client/client.js +8 -4
- package/dist/esm/client/token-manager.js +68 -179
- package/dist/types/client/api-client.d.ts +2 -1
- package/dist/types/client/token-manager.d.ts +5 -16
- package/dist/types/types/index.d.ts +3 -0
- package/package.json +2 -4
|
@@ -7,12 +7,32 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { NotFoundException, ForbiddenException, BadRequestException, UnauthorizedException, ServerException, TimeoutException } from '../types';
|
|
11
|
-
import {
|
|
10
|
+
import { NotFoundException, ForbiddenException, BadRequestException, UnauthorizedException, ServerException, TimeoutException, FlexbeAuthType } from '../types';
|
|
11
|
+
import { TokenManager } from './token-manager';
|
|
12
12
|
export class ApiClient {
|
|
13
13
|
constructor(config) {
|
|
14
14
|
this.config = config;
|
|
15
|
-
this.
|
|
15
|
+
this.tokenManager = TokenManager.getInstance();
|
|
16
|
+
if (this.config.authType === FlexbeAuthType.BEARER) {
|
|
17
|
+
// Start initialization but don't wait for it
|
|
18
|
+
void this.tokenManager.getToken(); // just warm up the token manager before any request
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
getAuthHeaders() {
|
|
22
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23
|
+
const headers = {};
|
|
24
|
+
if (this.config.authType === FlexbeAuthType.API_KEY) {
|
|
25
|
+
headers['x-api-key'] = this.config.apiKey;
|
|
26
|
+
}
|
|
27
|
+
else if (this.config.authType === FlexbeAuthType.BEARER) {
|
|
28
|
+
const token = yield this.tokenManager.getToken();
|
|
29
|
+
if (!token) {
|
|
30
|
+
throw new Error('No valid bearer token available');
|
|
31
|
+
}
|
|
32
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
33
|
+
}
|
|
34
|
+
return headers;
|
|
35
|
+
});
|
|
16
36
|
}
|
|
17
37
|
buildUrl(path, params) {
|
|
18
38
|
const searchParams = new URLSearchParams();
|
|
@@ -27,12 +47,12 @@ export class ApiClient {
|
|
|
27
47
|
}
|
|
28
48
|
request(config) {
|
|
29
49
|
return __awaiter(this, void 0, void 0, function* () {
|
|
50
|
+
var _a, _b;
|
|
30
51
|
try {
|
|
31
|
-
yield this.auth.ensureInitialized();
|
|
32
52
|
const controller = new AbortController();
|
|
33
53
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
34
54
|
const url = this.buildUrl(config.url, config.params);
|
|
35
|
-
const headers = Object.assign(Object.assign({}, (yield this.
|
|
55
|
+
const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json' }, (yield this.getAuthHeaders())), config.headers);
|
|
36
56
|
const response = yield fetch(this.config.baseUrl + url, Object.assign(Object.assign({}, config), { headers, signal: controller.signal }));
|
|
37
57
|
clearTimeout(timeoutId);
|
|
38
58
|
if (!response.ok) {
|
|
@@ -81,6 +101,9 @@ export class ApiClient {
|
|
|
81
101
|
};
|
|
82
102
|
}
|
|
83
103
|
catch (error) {
|
|
104
|
+
if (error instanceof UnauthorizedException) {
|
|
105
|
+
(_b = (_a = this.config.hooks) === null || _a === void 0 ? void 0 : _a.onUnauthorized) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
106
|
+
}
|
|
84
107
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
85
108
|
throw new TimeoutException('Request timeout');
|
|
86
109
|
}
|
|
@@ -21,12 +21,13 @@ export class FlexbeClient {
|
|
|
21
21
|
}
|
|
22
22
|
return undefined;
|
|
23
23
|
};
|
|
24
|
-
|
|
25
|
-
baseUrl:
|
|
26
|
-
timeout:
|
|
27
|
-
apiKey:
|
|
28
|
-
authType:
|
|
24
|
+
const defaultConfig = {
|
|
25
|
+
baseUrl: getEnvVar('FLEXBE_API_URL') || 'https://api.flexbe.com',
|
|
26
|
+
timeout: 30000,
|
|
27
|
+
apiKey: getEnvVar('FLEXBE_API_KEY') || '',
|
|
28
|
+
authType: FlexbeAuthType.API_KEY,
|
|
29
29
|
};
|
|
30
|
+
this.config = Object.assign(Object.assign({}, defaultConfig), config);
|
|
30
31
|
if (this.config.authType === 'apiKey' && !this.config.apiKey) {
|
|
31
32
|
throw new Error('API key is required when using apiKey authentication. Please provide it either through config or FLEXBE_API_KEY environment variable.');
|
|
32
33
|
}
|
|
@@ -7,105 +7,84 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const MAX_REFRESH_DELAY = 10000; // Maximum random delay of 10 seconds
|
|
10
|
+
import { UnauthorizedException } from '../types';
|
|
11
|
+
const TOKEN_STORAGE_KEY = 'flexbe_jwt';
|
|
12
|
+
const TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000; // update token 5 minutes before expiration
|
|
14
13
|
export class TokenManager {
|
|
15
14
|
constructor() {
|
|
16
|
-
this.token = null;
|
|
17
|
-
this.refreshInterval = null;
|
|
18
|
-
this.refreshTimeout = null;
|
|
19
15
|
this.tokenPromise = null;
|
|
20
|
-
this.debug = false;
|
|
21
|
-
this.initializeFromStorage();
|
|
22
|
-
this.setupStorageListener();
|
|
23
16
|
}
|
|
24
17
|
static getInstance() {
|
|
25
18
|
if (!TokenManager.instance) {
|
|
26
19
|
TokenManager.instance = new TokenManager();
|
|
27
20
|
}
|
|
21
|
+
if (typeof window !== 'undefined') {
|
|
22
|
+
// Clean up old token storage place
|
|
23
|
+
// TODO remove this after June 1, 2025
|
|
24
|
+
localStorage.removeItem('flexbe_jwt_token');
|
|
25
|
+
;
|
|
26
|
+
}
|
|
28
27
|
return TokenManager.instance;
|
|
29
28
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
this.clearToken();
|
|
29
|
+
getToken() {
|
|
30
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
31
|
+
var _a;
|
|
32
|
+
const token = this.getStoredToken();
|
|
33
|
+
if (token && token.expiresAt > Date.now()) {
|
|
34
|
+
// TODO check if token expire less that 1 minute
|
|
35
|
+
// if so, retrieve a new token
|
|
36
|
+
if (token.expiresAt - Date.now() < TOKEN_REFRESH_THRESHOLD) {
|
|
37
|
+
void this.retrieveToken();
|
|
38
|
+
}
|
|
39
|
+
return token.accessToken;
|
|
44
40
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
41
|
+
yield this.retrieveToken();
|
|
42
|
+
const retrievedToken = this.getStoredToken();
|
|
43
|
+
return (_a = retrievedToken === null || retrievedToken === void 0 ? void 0 : retrievedToken.accessToken) !== null && _a !== void 0 ? _a : null;
|
|
44
|
+
});
|
|
50
45
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
return;
|
|
57
|
-
if (!event.newValue) {
|
|
58
|
-
this.clearToken();
|
|
59
|
-
// void this.retrieveToken();
|
|
46
|
+
revokeToken() {
|
|
47
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
48
|
+
const token = this.getStoredToken();
|
|
49
|
+
this.clearToken();
|
|
50
|
+
if (!token)
|
|
60
51
|
return;
|
|
61
|
-
}
|
|
62
52
|
try {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
this.clearToken();
|
|
77
|
-
void this.retrieveToken();
|
|
78
|
-
}
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
55
|
+
yield fetch('/oauth/revoke', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
'Authorization': `Bearer ${token.accessToken}`
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({ token: token.accessToken }),
|
|
62
|
+
credentials: 'include',
|
|
63
|
+
signal: controller.signal,
|
|
64
|
+
});
|
|
65
|
+
clearTimeout(timeoutId);
|
|
79
66
|
}
|
|
80
67
|
catch (error) {
|
|
81
|
-
console.error('Failed to
|
|
82
|
-
this.clearToken();
|
|
83
|
-
void this.retrieveToken();
|
|
68
|
+
console.error('Failed to revoke token:', error);
|
|
84
69
|
}
|
|
85
70
|
});
|
|
86
71
|
}
|
|
87
|
-
|
|
72
|
+
getStoredToken() {
|
|
73
|
+
if (typeof window === 'undefined')
|
|
74
|
+
return null;
|
|
75
|
+
const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY);
|
|
76
|
+
if (!storedToken) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
88
79
|
try {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
return decodedPayload.exp * 1000; // Convert to milliseconds
|
|
80
|
+
const token = JSON.parse(storedToken);
|
|
81
|
+
return token;
|
|
92
82
|
}
|
|
93
83
|
catch (error) {
|
|
94
|
-
console.error('Failed to parse token
|
|
95
|
-
return
|
|
84
|
+
console.error('getStoredToken: Failed to parse stored token:', error);
|
|
85
|
+
return null;
|
|
96
86
|
}
|
|
97
87
|
}
|
|
98
|
-
getToken() {
|
|
99
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
100
|
-
var _a, _b;
|
|
101
|
-
const token = this.token;
|
|
102
|
-
if (token && token.expiresAt && token.expiresAt > Date.now()) {
|
|
103
|
-
return token.accessToken;
|
|
104
|
-
}
|
|
105
|
-
yield this.retrieveToken();
|
|
106
|
-
return (_b = (_a = this.token) === null || _a === void 0 ? void 0 : _a.accessToken) !== null && _b !== void 0 ? _b : null;
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
88
|
retrieveToken() {
|
|
110
89
|
return __awaiter(this, void 0, void 0, function* () {
|
|
111
90
|
if (this.tokenPromise) {
|
|
@@ -136,130 +115,44 @@ export class TokenManager {
|
|
|
136
115
|
clearTimeout(timeoutId);
|
|
137
116
|
if (!response.ok) {
|
|
138
117
|
const errorData = yield response.json().catch(() => ({ message: response.statusText }));
|
|
118
|
+
if (response.status === 401) {
|
|
119
|
+
throw new UnauthorizedException(errorData.message || response.statusText);
|
|
120
|
+
}
|
|
139
121
|
throw new Error(errorData.message || response.statusText);
|
|
140
122
|
}
|
|
141
123
|
const data = yield response.json();
|
|
142
124
|
this.setToken(data);
|
|
143
125
|
}
|
|
144
126
|
catch (error) {
|
|
145
|
-
console.error('Failed to retrieve token:', error);
|
|
146
|
-
this.clearToken();
|
|
147
|
-
// Schedule a retry after REFRESH_CHECK_INTERVAL
|
|
148
|
-
setTimeout(() => {
|
|
149
|
-
void this.retrieveToken();
|
|
150
|
-
}, REFRESH_CHECK_INTERVAL);
|
|
151
127
|
throw error;
|
|
152
128
|
}
|
|
153
129
|
});
|
|
154
130
|
}
|
|
155
|
-
startRefreshInterval() {
|
|
156
|
-
this.clearRefreshTimers();
|
|
157
|
-
if (!this.token)
|
|
158
|
-
return;
|
|
159
|
-
const tokenLifetime = this.token.expiresAt - Date.now();
|
|
160
|
-
const refreshThreshold = Math.round(tokenLifetime * REFRESH_THRESHOLD);
|
|
161
|
-
const timeUntilRefresh = refreshThreshold - (Math.random() * MAX_REFRESH_DELAY);
|
|
162
|
-
this.logTokenStatus('Starting refresh interval', {
|
|
163
|
-
tokenLifetime: `${Math.round(tokenLifetime / 1000)} seconds`,
|
|
164
|
-
refreshThreshold: `${Math.round(refreshThreshold / 1000)} seconds`,
|
|
165
|
-
timeUntilRefresh: `${Math.round(timeUntilRefresh / 1000)} seconds`,
|
|
166
|
-
});
|
|
167
|
-
this.scheduleRefresh(timeUntilRefresh);
|
|
168
|
-
this.refreshInterval = window.setInterval(() => {
|
|
169
|
-
if (!this.token)
|
|
170
|
-
return;
|
|
171
|
-
const timeUntilExpiry = this.token.expiresAt - Date.now();
|
|
172
|
-
if (timeUntilExpiry <= 0) {
|
|
173
|
-
this.logTokenStatus('Token expired');
|
|
174
|
-
this.clearToken();
|
|
175
|
-
void this.retrieveToken();
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
const refreshThreshold = Math.round(timeUntilExpiry * REFRESH_THRESHOLD);
|
|
179
|
-
if (timeUntilExpiry <= refreshThreshold) {
|
|
180
|
-
this.logTokenStatus('Refreshing token', {
|
|
181
|
-
timeUntilExpiry: `${Math.round(timeUntilExpiry / 1000)} seconds`,
|
|
182
|
-
refreshThreshold: `${Math.round(refreshThreshold / 1000)} seconds`,
|
|
183
|
-
});
|
|
184
|
-
this.scheduleRefresh(refreshThreshold - (Math.random() * MAX_REFRESH_DELAY));
|
|
185
|
-
}
|
|
186
|
-
}, REFRESH_CHECK_INTERVAL);
|
|
187
|
-
}
|
|
188
|
-
scheduleRefresh(delay) {
|
|
189
|
-
if (this.refreshTimeout) {
|
|
190
|
-
window.clearTimeout(this.refreshTimeout);
|
|
191
|
-
}
|
|
192
|
-
this.refreshTimeout = window.setTimeout(() => {
|
|
193
|
-
const token = this.token;
|
|
194
|
-
if (token && token.expiresAt - Date.now() <= token.expiresAt * REFRESH_THRESHOLD) {
|
|
195
|
-
void this.retrieveToken();
|
|
196
|
-
}
|
|
197
|
-
}, delay);
|
|
198
|
-
}
|
|
199
|
-
clearRefreshTimers() {
|
|
200
|
-
if (this.refreshInterval) {
|
|
201
|
-
clearInterval(this.refreshInterval);
|
|
202
|
-
this.refreshInterval = null;
|
|
203
|
-
}
|
|
204
|
-
if (this.refreshTimeout) {
|
|
205
|
-
window.clearTimeout(this.refreshTimeout);
|
|
206
|
-
this.refreshTimeout = null;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
logTokenStatus(message, additionalInfo = {}) {
|
|
210
|
-
if (!this.debug)
|
|
211
|
-
return;
|
|
212
|
-
const token = this.token;
|
|
213
|
-
if (!token)
|
|
214
|
-
return;
|
|
215
|
-
console.log(message, Object.assign({ expiresIn: `${Math.round((token.expiresAt - Date.now()) / 1000)} seconds`, expiresAt: new Date(token.expiresAt).toISOString() }, additionalInfo));
|
|
216
|
-
}
|
|
217
131
|
setToken(tokenResponse) {
|
|
218
132
|
const expiresAt = this.getExpirationFromToken(tokenResponse.accessToken);
|
|
219
|
-
|
|
133
|
+
const token = {
|
|
220
134
|
accessToken: tokenResponse.accessToken,
|
|
221
135
|
expiresAt,
|
|
222
136
|
};
|
|
223
|
-
this.logTokenStatus('Token set', {
|
|
224
|
-
expiresAt: new Date(expiresAt).toISOString(),
|
|
225
|
-
});
|
|
226
137
|
if (typeof window !== 'undefined') {
|
|
227
|
-
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(
|
|
138
|
+
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(token));
|
|
228
139
|
}
|
|
229
|
-
this.startRefreshInterval();
|
|
230
140
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
141
|
+
getExpirationFromToken(token) {
|
|
142
|
+
try {
|
|
143
|
+
const [, payload] = token.split('.');
|
|
144
|
+
const decodedPayload = JSON.parse(atob(payload));
|
|
145
|
+
return decodedPayload.exp * 1000; // Convert to milliseconds
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error('getExpirationFromToken: Failed to parse token expiration:', error);
|
|
149
|
+
return Date.now() + (4 * 60 * 1000); // Default to 4 minutes if parsing fails
|
|
236
150
|
}
|
|
237
151
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return;
|
|
244
|
-
try {
|
|
245
|
-
const controller = new AbortController();
|
|
246
|
-
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
247
|
-
yield fetch('/oauth/revoke', {
|
|
248
|
-
method: 'POST',
|
|
249
|
-
headers: {
|
|
250
|
-
'Content-Type': 'application/json',
|
|
251
|
-
'Authorization': `Bearer ${token.accessToken}`
|
|
252
|
-
},
|
|
253
|
-
body: JSON.stringify({ token: token.accessToken }),
|
|
254
|
-
credentials: 'include',
|
|
255
|
-
signal: controller.signal,
|
|
256
|
-
});
|
|
257
|
-
clearTimeout(timeoutId);
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
console.error('Failed to revoke token:', error);
|
|
261
|
-
// Even if revocation fails, we still want to clear the local token
|
|
262
|
-
}
|
|
263
|
-
});
|
|
152
|
+
clearToken() {
|
|
153
|
+
if (typeof window === 'undefined') {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
264
157
|
}
|
|
265
158
|
}
|
|
@@ -2,11 +2,29 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ApiClient = void 0;
|
|
4
4
|
const types_1 = require("../types");
|
|
5
|
-
const
|
|
5
|
+
const token_manager_1 = require("./token-manager");
|
|
6
6
|
class ApiClient {
|
|
7
7
|
constructor(config) {
|
|
8
8
|
this.config = config;
|
|
9
|
-
this.
|
|
9
|
+
this.tokenManager = token_manager_1.TokenManager.getInstance();
|
|
10
|
+
if (this.config.authType === types_1.FlexbeAuthType.BEARER) {
|
|
11
|
+
// Start initialization but don't wait for it
|
|
12
|
+
void this.tokenManager.getToken(); // just warm up the token manager before any request
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async getAuthHeaders() {
|
|
16
|
+
const headers = {};
|
|
17
|
+
if (this.config.authType === types_1.FlexbeAuthType.API_KEY) {
|
|
18
|
+
headers['x-api-key'] = this.config.apiKey;
|
|
19
|
+
}
|
|
20
|
+
else if (this.config.authType === types_1.FlexbeAuthType.BEARER) {
|
|
21
|
+
const token = await this.tokenManager.getToken();
|
|
22
|
+
if (!token) {
|
|
23
|
+
throw new Error('No valid bearer token available');
|
|
24
|
+
}
|
|
25
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
26
|
+
}
|
|
27
|
+
return headers;
|
|
10
28
|
}
|
|
11
29
|
buildUrl(path, params) {
|
|
12
30
|
const searchParams = new URLSearchParams();
|
|
@@ -21,12 +39,12 @@ class ApiClient {
|
|
|
21
39
|
}
|
|
22
40
|
async request(config) {
|
|
23
41
|
try {
|
|
24
|
-
await this.auth.ensureInitialized();
|
|
25
42
|
const controller = new AbortController();
|
|
26
43
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
27
44
|
const url = this.buildUrl(config.url, config.params);
|
|
28
45
|
const headers = {
|
|
29
|
-
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
...(await this.getAuthHeaders()),
|
|
30
48
|
...config.headers,
|
|
31
49
|
};
|
|
32
50
|
const response = await fetch(this.config.baseUrl + url, {
|
|
@@ -81,6 +99,9 @@ class ApiClient {
|
|
|
81
99
|
};
|
|
82
100
|
}
|
|
83
101
|
catch (error) {
|
|
102
|
+
if (error instanceof types_1.UnauthorizedException) {
|
|
103
|
+
this.config.hooks?.onUnauthorized?.();
|
|
104
|
+
}
|
|
84
105
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
85
106
|
throw new types_1.TimeoutException('Request timeout');
|
|
86
107
|
}
|
|
@@ -15,11 +15,15 @@ class FlexbeClient {
|
|
|
15
15
|
}
|
|
16
16
|
return undefined;
|
|
17
17
|
};
|
|
18
|
+
const defaultConfig = {
|
|
19
|
+
baseUrl: getEnvVar('FLEXBE_API_URL') || 'https://api.flexbe.com',
|
|
20
|
+
timeout: 30000,
|
|
21
|
+
apiKey: getEnvVar('FLEXBE_API_KEY') || '',
|
|
22
|
+
authType: types_1.FlexbeAuthType.API_KEY,
|
|
23
|
+
};
|
|
18
24
|
this.config = {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
apiKey: config?.apiKey || getEnvVar('FLEXBE_API_KEY') || '',
|
|
22
|
-
authType: config?.authType || types_1.FlexbeAuthType.API_KEY,
|
|
25
|
+
...defaultConfig,
|
|
26
|
+
...config,
|
|
23
27
|
};
|
|
24
28
|
if (this.config.authType === 'apiKey' && !this.config.apiKey) {
|
|
25
29
|
throw new Error('API key is required when using apiKey authentication. Please provide it either through config or FLEXBE_API_KEY environment variable.');
|
|
@@ -1,102 +1,79 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TokenManager = void 0;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const MAX_REFRESH_DELAY = 10000; // Maximum random delay of 10 seconds
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
const TOKEN_STORAGE_KEY = 'flexbe_jwt';
|
|
6
|
+
const TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000; // update token 5 minutes before expiration
|
|
8
7
|
class TokenManager {
|
|
9
8
|
constructor() {
|
|
10
|
-
this.token = null;
|
|
11
|
-
this.refreshInterval = null;
|
|
12
|
-
this.refreshTimeout = null;
|
|
13
9
|
this.tokenPromise = null;
|
|
14
|
-
this.debug = false;
|
|
15
|
-
this.initializeFromStorage();
|
|
16
|
-
this.setupStorageListener();
|
|
17
10
|
}
|
|
18
11
|
static getInstance() {
|
|
19
12
|
if (!TokenManager.instance) {
|
|
20
13
|
TokenManager.instance = new TokenManager();
|
|
21
14
|
}
|
|
15
|
+
if (typeof window !== 'undefined') {
|
|
16
|
+
// Clean up old token storage place
|
|
17
|
+
// TODO remove this after June 1, 2025
|
|
18
|
+
localStorage.removeItem('flexbe_jwt_token');
|
|
19
|
+
;
|
|
20
|
+
}
|
|
22
21
|
return TokenManager.instance;
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
async getToken() {
|
|
24
|
+
const token = this.getStoredToken();
|
|
25
|
+
if (token && token.expiresAt > Date.now()) {
|
|
26
|
+
// TODO check if token expire less that 1 minute
|
|
27
|
+
// if so, retrieve a new token
|
|
28
|
+
if (token.expiresAt - Date.now() < TOKEN_REFRESH_THRESHOLD) {
|
|
29
|
+
void this.retrieveToken();
|
|
30
|
+
}
|
|
31
|
+
return token.accessToken;
|
|
32
|
+
}
|
|
33
|
+
await this.retrieveToken();
|
|
34
|
+
const retrievedToken = this.getStoredToken();
|
|
35
|
+
return retrievedToken?.accessToken ?? null;
|
|
36
|
+
}
|
|
37
|
+
async revokeToken() {
|
|
38
|
+
const token = this.getStoredToken();
|
|
39
|
+
this.clearToken();
|
|
40
|
+
if (!token)
|
|
29
41
|
return;
|
|
30
42
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
45
|
+
await fetch('/oauth/revoke', {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
'Authorization': `Bearer ${token.accessToken}`
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify({ token: token.accessToken }),
|
|
52
|
+
credentials: 'include',
|
|
53
|
+
signal: controller.signal,
|
|
54
|
+
});
|
|
55
|
+
clearTimeout(timeoutId);
|
|
39
56
|
}
|
|
40
57
|
catch (error) {
|
|
41
|
-
console.error('Failed to
|
|
42
|
-
this.clearToken();
|
|
58
|
+
console.error('Failed to revoke token:', error);
|
|
43
59
|
}
|
|
44
60
|
}
|
|
45
|
-
|
|
61
|
+
getStoredToken() {
|
|
46
62
|
if (typeof window === 'undefined')
|
|
47
|
-
return;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.clearToken();
|
|
53
|
-
// void this.retrieveToken();
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
try {
|
|
57
|
-
const newToken = JSON.parse(event.newValue);
|
|
58
|
-
// Skip if the new token is exactly the same as current token
|
|
59
|
-
if (this.token &&
|
|
60
|
-
this.token.accessToken === newToken.accessToken &&
|
|
61
|
-
this.token.expiresAt === newToken.expiresAt) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
if (newToken.expiresAt > Date.now()) {
|
|
65
|
-
this.token = newToken;
|
|
66
|
-
this.logTokenStatus('Token updated from storage');
|
|
67
|
-
this.startRefreshInterval();
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
this.clearToken();
|
|
71
|
-
void this.retrieveToken();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
console.error('Failed to parse token from storage event:', error);
|
|
76
|
-
this.clearToken();
|
|
77
|
-
void this.retrieveToken();
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
getExpirationFromToken(token) {
|
|
63
|
+
return null;
|
|
64
|
+
const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY);
|
|
65
|
+
if (!storedToken) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
82
68
|
try {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
return decodedPayload.exp * 1000; // Convert to milliseconds
|
|
69
|
+
const token = JSON.parse(storedToken);
|
|
70
|
+
return token;
|
|
86
71
|
}
|
|
87
72
|
catch (error) {
|
|
88
|
-
console.error('Failed to parse token
|
|
89
|
-
return
|
|
73
|
+
console.error('getStoredToken: Failed to parse stored token:', error);
|
|
74
|
+
return null;
|
|
90
75
|
}
|
|
91
76
|
}
|
|
92
|
-
async getToken() {
|
|
93
|
-
const token = this.token;
|
|
94
|
-
if (token && token.expiresAt && token.expiresAt > Date.now()) {
|
|
95
|
-
return token.accessToken;
|
|
96
|
-
}
|
|
97
|
-
await this.retrieveToken();
|
|
98
|
-
return this.token?.accessToken ?? null;
|
|
99
|
-
}
|
|
100
77
|
async retrieveToken() {
|
|
101
78
|
if (this.tokenPromise) {
|
|
102
79
|
await this.tokenPromise;
|
|
@@ -124,132 +101,44 @@ class TokenManager {
|
|
|
124
101
|
clearTimeout(timeoutId);
|
|
125
102
|
if (!response.ok) {
|
|
126
103
|
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
|
104
|
+
if (response.status === 401) {
|
|
105
|
+
throw new types_1.UnauthorizedException(errorData.message || response.statusText);
|
|
106
|
+
}
|
|
127
107
|
throw new Error(errorData.message || response.statusText);
|
|
128
108
|
}
|
|
129
109
|
const data = await response.json();
|
|
130
110
|
this.setToken(data);
|
|
131
111
|
}
|
|
132
112
|
catch (error) {
|
|
133
|
-
console.error('Failed to retrieve token:', error);
|
|
134
|
-
this.clearToken();
|
|
135
|
-
// Schedule a retry after REFRESH_CHECK_INTERVAL
|
|
136
|
-
setTimeout(() => {
|
|
137
|
-
void this.retrieveToken();
|
|
138
|
-
}, REFRESH_CHECK_INTERVAL);
|
|
139
113
|
throw error;
|
|
140
114
|
}
|
|
141
115
|
}
|
|
142
|
-
startRefreshInterval() {
|
|
143
|
-
this.clearRefreshTimers();
|
|
144
|
-
if (!this.token)
|
|
145
|
-
return;
|
|
146
|
-
const tokenLifetime = this.token.expiresAt - Date.now();
|
|
147
|
-
const refreshThreshold = Math.round(tokenLifetime * REFRESH_THRESHOLD);
|
|
148
|
-
const timeUntilRefresh = refreshThreshold - (Math.random() * MAX_REFRESH_DELAY);
|
|
149
|
-
this.logTokenStatus('Starting refresh interval', {
|
|
150
|
-
tokenLifetime: `${Math.round(tokenLifetime / 1000)} seconds`,
|
|
151
|
-
refreshThreshold: `${Math.round(refreshThreshold / 1000)} seconds`,
|
|
152
|
-
timeUntilRefresh: `${Math.round(timeUntilRefresh / 1000)} seconds`,
|
|
153
|
-
});
|
|
154
|
-
this.scheduleRefresh(timeUntilRefresh);
|
|
155
|
-
this.refreshInterval = window.setInterval(() => {
|
|
156
|
-
if (!this.token)
|
|
157
|
-
return;
|
|
158
|
-
const timeUntilExpiry = this.token.expiresAt - Date.now();
|
|
159
|
-
if (timeUntilExpiry <= 0) {
|
|
160
|
-
this.logTokenStatus('Token expired');
|
|
161
|
-
this.clearToken();
|
|
162
|
-
void this.retrieveToken();
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
const refreshThreshold = Math.round(timeUntilExpiry * REFRESH_THRESHOLD);
|
|
166
|
-
if (timeUntilExpiry <= refreshThreshold) {
|
|
167
|
-
this.logTokenStatus('Refreshing token', {
|
|
168
|
-
timeUntilExpiry: `${Math.round(timeUntilExpiry / 1000)} seconds`,
|
|
169
|
-
refreshThreshold: `${Math.round(refreshThreshold / 1000)} seconds`,
|
|
170
|
-
});
|
|
171
|
-
this.scheduleRefresh(refreshThreshold - (Math.random() * MAX_REFRESH_DELAY));
|
|
172
|
-
}
|
|
173
|
-
}, REFRESH_CHECK_INTERVAL);
|
|
174
|
-
}
|
|
175
|
-
scheduleRefresh(delay) {
|
|
176
|
-
if (this.refreshTimeout) {
|
|
177
|
-
window.clearTimeout(this.refreshTimeout);
|
|
178
|
-
}
|
|
179
|
-
this.refreshTimeout = window.setTimeout(() => {
|
|
180
|
-
const token = this.token;
|
|
181
|
-
if (token && token.expiresAt - Date.now() <= token.expiresAt * REFRESH_THRESHOLD) {
|
|
182
|
-
void this.retrieveToken();
|
|
183
|
-
}
|
|
184
|
-
}, delay);
|
|
185
|
-
}
|
|
186
|
-
clearRefreshTimers() {
|
|
187
|
-
if (this.refreshInterval) {
|
|
188
|
-
clearInterval(this.refreshInterval);
|
|
189
|
-
this.refreshInterval = null;
|
|
190
|
-
}
|
|
191
|
-
if (this.refreshTimeout) {
|
|
192
|
-
window.clearTimeout(this.refreshTimeout);
|
|
193
|
-
this.refreshTimeout = null;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
logTokenStatus(message, additionalInfo = {}) {
|
|
197
|
-
if (!this.debug)
|
|
198
|
-
return;
|
|
199
|
-
const token = this.token;
|
|
200
|
-
if (!token)
|
|
201
|
-
return;
|
|
202
|
-
console.log(message, {
|
|
203
|
-
expiresIn: `${Math.round((token.expiresAt - Date.now()) / 1000)} seconds`,
|
|
204
|
-
expiresAt: new Date(token.expiresAt).toISOString(),
|
|
205
|
-
...additionalInfo,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
116
|
setToken(tokenResponse) {
|
|
209
117
|
const expiresAt = this.getExpirationFromToken(tokenResponse.accessToken);
|
|
210
|
-
|
|
118
|
+
const token = {
|
|
211
119
|
accessToken: tokenResponse.accessToken,
|
|
212
120
|
expiresAt,
|
|
213
121
|
};
|
|
214
|
-
this.logTokenStatus('Token set', {
|
|
215
|
-
expiresAt: new Date(expiresAt).toISOString(),
|
|
216
|
-
});
|
|
217
122
|
if (typeof window !== 'undefined') {
|
|
218
|
-
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(
|
|
123
|
+
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(token));
|
|
219
124
|
}
|
|
220
|
-
this.startRefreshInterval();
|
|
221
125
|
}
|
|
222
|
-
|
|
223
|
-
this.token = null;
|
|
224
|
-
this.clearRefreshTimers();
|
|
225
|
-
if (typeof window !== 'undefined') {
|
|
226
|
-
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
async revokeToken() {
|
|
230
|
-
const token = this.token;
|
|
231
|
-
this.clearToken();
|
|
232
|
-
if (!token)
|
|
233
|
-
return;
|
|
126
|
+
getExpirationFromToken(token) {
|
|
234
127
|
try {
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
method: 'POST',
|
|
239
|
-
headers: {
|
|
240
|
-
'Content-Type': 'application/json',
|
|
241
|
-
'Authorization': `Bearer ${token.accessToken}`
|
|
242
|
-
},
|
|
243
|
-
body: JSON.stringify({ token: token.accessToken }),
|
|
244
|
-
credentials: 'include',
|
|
245
|
-
signal: controller.signal,
|
|
246
|
-
});
|
|
247
|
-
clearTimeout(timeoutId);
|
|
128
|
+
const [, payload] = token.split('.');
|
|
129
|
+
const decodedPayload = JSON.parse(atob(payload));
|
|
130
|
+
return decodedPayload.exp * 1000; // Convert to milliseconds
|
|
248
131
|
}
|
|
249
132
|
catch (error) {
|
|
250
|
-
console.error('Failed to
|
|
251
|
-
|
|
133
|
+
console.error('getExpirationFromToken: Failed to parse token expiration:', error);
|
|
134
|
+
return Date.now() + (4 * 60 * 1000); // Default to 4 minutes if parsing fails
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
clearToken() {
|
|
138
|
+
if (typeof window === 'undefined') {
|
|
139
|
+
return;
|
|
252
140
|
}
|
|
141
|
+
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
253
142
|
}
|
|
254
143
|
}
|
|
255
144
|
exports.TokenManager = TokenManager;
|
|
@@ -1,9 +1,27 @@
|
|
|
1
|
-
import { NotFoundException, ForbiddenException, BadRequestException, UnauthorizedException, ServerException, TimeoutException } from '../types';
|
|
2
|
-
import {
|
|
1
|
+
import { NotFoundException, ForbiddenException, BadRequestException, UnauthorizedException, ServerException, TimeoutException, FlexbeAuthType } from '../types';
|
|
2
|
+
import { TokenManager } from './token-manager';
|
|
3
3
|
export class ApiClient {
|
|
4
4
|
constructor(config) {
|
|
5
5
|
this.config = config;
|
|
6
|
-
this.
|
|
6
|
+
this.tokenManager = TokenManager.getInstance();
|
|
7
|
+
if (this.config.authType === FlexbeAuthType.BEARER) {
|
|
8
|
+
// Start initialization but don't wait for it
|
|
9
|
+
void this.tokenManager.getToken(); // just warm up the token manager before any request
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async getAuthHeaders() {
|
|
13
|
+
const headers = {};
|
|
14
|
+
if (this.config.authType === FlexbeAuthType.API_KEY) {
|
|
15
|
+
headers['x-api-key'] = this.config.apiKey;
|
|
16
|
+
}
|
|
17
|
+
else if (this.config.authType === FlexbeAuthType.BEARER) {
|
|
18
|
+
const token = await this.tokenManager.getToken();
|
|
19
|
+
if (!token) {
|
|
20
|
+
throw new Error('No valid bearer token available');
|
|
21
|
+
}
|
|
22
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
23
|
+
}
|
|
24
|
+
return headers;
|
|
7
25
|
}
|
|
8
26
|
buildUrl(path, params) {
|
|
9
27
|
const searchParams = new URLSearchParams();
|
|
@@ -18,12 +36,12 @@ export class ApiClient {
|
|
|
18
36
|
}
|
|
19
37
|
async request(config) {
|
|
20
38
|
try {
|
|
21
|
-
await this.auth.ensureInitialized();
|
|
22
39
|
const controller = new AbortController();
|
|
23
40
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
24
41
|
const url = this.buildUrl(config.url, config.params);
|
|
25
42
|
const headers = {
|
|
26
|
-
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
...(await this.getAuthHeaders()),
|
|
27
45
|
...config.headers,
|
|
28
46
|
};
|
|
29
47
|
const response = await fetch(this.config.baseUrl + url, {
|
|
@@ -78,6 +96,9 @@ export class ApiClient {
|
|
|
78
96
|
};
|
|
79
97
|
}
|
|
80
98
|
catch (error) {
|
|
99
|
+
if (error instanceof UnauthorizedException) {
|
|
100
|
+
this.config.hooks?.onUnauthorized?.();
|
|
101
|
+
}
|
|
81
102
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
82
103
|
throw new TimeoutException('Request timeout');
|
|
83
104
|
}
|
|
@@ -12,11 +12,15 @@ export class FlexbeClient {
|
|
|
12
12
|
}
|
|
13
13
|
return undefined;
|
|
14
14
|
};
|
|
15
|
+
const defaultConfig = {
|
|
16
|
+
baseUrl: getEnvVar('FLEXBE_API_URL') || 'https://api.flexbe.com',
|
|
17
|
+
timeout: 30000,
|
|
18
|
+
apiKey: getEnvVar('FLEXBE_API_KEY') || '',
|
|
19
|
+
authType: FlexbeAuthType.API_KEY,
|
|
20
|
+
};
|
|
15
21
|
this.config = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
apiKey: config?.apiKey || getEnvVar('FLEXBE_API_KEY') || '',
|
|
19
|
-
authType: config?.authType || FlexbeAuthType.API_KEY,
|
|
22
|
+
...defaultConfig,
|
|
23
|
+
...config,
|
|
20
24
|
};
|
|
21
25
|
if (this.config.authType === 'apiKey' && !this.config.apiKey) {
|
|
22
26
|
throw new Error('API key is required when using apiKey authentication. Please provide it either through config or FLEXBE_API_KEY environment variable.');
|
|
@@ -1,99 +1,76 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const MAX_REFRESH_DELAY = 10000; // Maximum random delay of 10 seconds
|
|
1
|
+
import { UnauthorizedException } from '../types';
|
|
2
|
+
const TOKEN_STORAGE_KEY = 'flexbe_jwt';
|
|
3
|
+
const TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000; // update token 5 minutes before expiration
|
|
5
4
|
export class TokenManager {
|
|
6
5
|
constructor() {
|
|
7
|
-
this.token = null;
|
|
8
|
-
this.refreshInterval = null;
|
|
9
|
-
this.refreshTimeout = null;
|
|
10
6
|
this.tokenPromise = null;
|
|
11
|
-
this.debug = false;
|
|
12
|
-
this.initializeFromStorage();
|
|
13
|
-
this.setupStorageListener();
|
|
14
7
|
}
|
|
15
8
|
static getInstance() {
|
|
16
9
|
if (!TokenManager.instance) {
|
|
17
10
|
TokenManager.instance = new TokenManager();
|
|
18
11
|
}
|
|
12
|
+
if (typeof window !== 'undefined') {
|
|
13
|
+
// Clean up old token storage place
|
|
14
|
+
// TODO remove this after June 1, 2025
|
|
15
|
+
localStorage.removeItem('flexbe_jwt_token');
|
|
16
|
+
;
|
|
17
|
+
}
|
|
19
18
|
return TokenManager.instance;
|
|
20
19
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
async getToken() {
|
|
21
|
+
const token = this.getStoredToken();
|
|
22
|
+
if (token && token.expiresAt > Date.now()) {
|
|
23
|
+
// TODO check if token expire less that 1 minute
|
|
24
|
+
// if so, retrieve a new token
|
|
25
|
+
if (token.expiresAt - Date.now() < TOKEN_REFRESH_THRESHOLD) {
|
|
26
|
+
void this.retrieveToken();
|
|
27
|
+
}
|
|
28
|
+
return token.accessToken;
|
|
29
|
+
}
|
|
30
|
+
await this.retrieveToken();
|
|
31
|
+
const retrievedToken = this.getStoredToken();
|
|
32
|
+
return retrievedToken?.accessToken ?? null;
|
|
33
|
+
}
|
|
34
|
+
async revokeToken() {
|
|
35
|
+
const token = this.getStoredToken();
|
|
36
|
+
this.clearToken();
|
|
37
|
+
if (!token)
|
|
26
38
|
return;
|
|
27
39
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
42
|
+
await fetch('/oauth/revoke', {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
'Authorization': `Bearer ${token.accessToken}`
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({ token: token.accessToken }),
|
|
49
|
+
credentials: 'include',
|
|
50
|
+
signal: controller.signal,
|
|
51
|
+
});
|
|
52
|
+
clearTimeout(timeoutId);
|
|
36
53
|
}
|
|
37
54
|
catch (error) {
|
|
38
|
-
console.error('Failed to
|
|
39
|
-
this.clearToken();
|
|
55
|
+
console.error('Failed to revoke token:', error);
|
|
40
56
|
}
|
|
41
57
|
}
|
|
42
|
-
|
|
58
|
+
getStoredToken() {
|
|
43
59
|
if (typeof window === 'undefined')
|
|
44
|
-
return;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.clearToken();
|
|
50
|
-
// void this.retrieveToken();
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
try {
|
|
54
|
-
const newToken = JSON.parse(event.newValue);
|
|
55
|
-
// Skip if the new token is exactly the same as current token
|
|
56
|
-
if (this.token &&
|
|
57
|
-
this.token.accessToken === newToken.accessToken &&
|
|
58
|
-
this.token.expiresAt === newToken.expiresAt) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (newToken.expiresAt > Date.now()) {
|
|
62
|
-
this.token = newToken;
|
|
63
|
-
this.logTokenStatus('Token updated from storage');
|
|
64
|
-
this.startRefreshInterval();
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
this.clearToken();
|
|
68
|
-
void this.retrieveToken();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
console.error('Failed to parse token from storage event:', error);
|
|
73
|
-
this.clearToken();
|
|
74
|
-
void this.retrieveToken();
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
getExpirationFromToken(token) {
|
|
60
|
+
return null;
|
|
61
|
+
const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY);
|
|
62
|
+
if (!storedToken) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
79
65
|
try {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
return decodedPayload.exp * 1000; // Convert to milliseconds
|
|
66
|
+
const token = JSON.parse(storedToken);
|
|
67
|
+
return token;
|
|
83
68
|
}
|
|
84
69
|
catch (error) {
|
|
85
|
-
console.error('Failed to parse token
|
|
86
|
-
return
|
|
70
|
+
console.error('getStoredToken: Failed to parse stored token:', error);
|
|
71
|
+
return null;
|
|
87
72
|
}
|
|
88
73
|
}
|
|
89
|
-
async getToken() {
|
|
90
|
-
const token = this.token;
|
|
91
|
-
if (token && token.expiresAt && token.expiresAt > Date.now()) {
|
|
92
|
-
return token.accessToken;
|
|
93
|
-
}
|
|
94
|
-
await this.retrieveToken();
|
|
95
|
-
return this.token?.accessToken ?? null;
|
|
96
|
-
}
|
|
97
74
|
async retrieveToken() {
|
|
98
75
|
if (this.tokenPromise) {
|
|
99
76
|
await this.tokenPromise;
|
|
@@ -121,131 +98,43 @@ export class TokenManager {
|
|
|
121
98
|
clearTimeout(timeoutId);
|
|
122
99
|
if (!response.ok) {
|
|
123
100
|
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
|
101
|
+
if (response.status === 401) {
|
|
102
|
+
throw new UnauthorizedException(errorData.message || response.statusText);
|
|
103
|
+
}
|
|
124
104
|
throw new Error(errorData.message || response.statusText);
|
|
125
105
|
}
|
|
126
106
|
const data = await response.json();
|
|
127
107
|
this.setToken(data);
|
|
128
108
|
}
|
|
129
109
|
catch (error) {
|
|
130
|
-
console.error('Failed to retrieve token:', error);
|
|
131
|
-
this.clearToken();
|
|
132
|
-
// Schedule a retry after REFRESH_CHECK_INTERVAL
|
|
133
|
-
setTimeout(() => {
|
|
134
|
-
void this.retrieveToken();
|
|
135
|
-
}, REFRESH_CHECK_INTERVAL);
|
|
136
110
|
throw error;
|
|
137
111
|
}
|
|
138
112
|
}
|
|
139
|
-
startRefreshInterval() {
|
|
140
|
-
this.clearRefreshTimers();
|
|
141
|
-
if (!this.token)
|
|
142
|
-
return;
|
|
143
|
-
const tokenLifetime = this.token.expiresAt - Date.now();
|
|
144
|
-
const refreshThreshold = Math.round(tokenLifetime * REFRESH_THRESHOLD);
|
|
145
|
-
const timeUntilRefresh = refreshThreshold - (Math.random() * MAX_REFRESH_DELAY);
|
|
146
|
-
this.logTokenStatus('Starting refresh interval', {
|
|
147
|
-
tokenLifetime: `${Math.round(tokenLifetime / 1000)} seconds`,
|
|
148
|
-
refreshThreshold: `${Math.round(refreshThreshold / 1000)} seconds`,
|
|
149
|
-
timeUntilRefresh: `${Math.round(timeUntilRefresh / 1000)} seconds`,
|
|
150
|
-
});
|
|
151
|
-
this.scheduleRefresh(timeUntilRefresh);
|
|
152
|
-
this.refreshInterval = window.setInterval(() => {
|
|
153
|
-
if (!this.token)
|
|
154
|
-
return;
|
|
155
|
-
const timeUntilExpiry = this.token.expiresAt - Date.now();
|
|
156
|
-
if (timeUntilExpiry <= 0) {
|
|
157
|
-
this.logTokenStatus('Token expired');
|
|
158
|
-
this.clearToken();
|
|
159
|
-
void this.retrieveToken();
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const refreshThreshold = Math.round(timeUntilExpiry * REFRESH_THRESHOLD);
|
|
163
|
-
if (timeUntilExpiry <= refreshThreshold) {
|
|
164
|
-
this.logTokenStatus('Refreshing token', {
|
|
165
|
-
timeUntilExpiry: `${Math.round(timeUntilExpiry / 1000)} seconds`,
|
|
166
|
-
refreshThreshold: `${Math.round(refreshThreshold / 1000)} seconds`,
|
|
167
|
-
});
|
|
168
|
-
this.scheduleRefresh(refreshThreshold - (Math.random() * MAX_REFRESH_DELAY));
|
|
169
|
-
}
|
|
170
|
-
}, REFRESH_CHECK_INTERVAL);
|
|
171
|
-
}
|
|
172
|
-
scheduleRefresh(delay) {
|
|
173
|
-
if (this.refreshTimeout) {
|
|
174
|
-
window.clearTimeout(this.refreshTimeout);
|
|
175
|
-
}
|
|
176
|
-
this.refreshTimeout = window.setTimeout(() => {
|
|
177
|
-
const token = this.token;
|
|
178
|
-
if (token && token.expiresAt - Date.now() <= token.expiresAt * REFRESH_THRESHOLD) {
|
|
179
|
-
void this.retrieveToken();
|
|
180
|
-
}
|
|
181
|
-
}, delay);
|
|
182
|
-
}
|
|
183
|
-
clearRefreshTimers() {
|
|
184
|
-
if (this.refreshInterval) {
|
|
185
|
-
clearInterval(this.refreshInterval);
|
|
186
|
-
this.refreshInterval = null;
|
|
187
|
-
}
|
|
188
|
-
if (this.refreshTimeout) {
|
|
189
|
-
window.clearTimeout(this.refreshTimeout);
|
|
190
|
-
this.refreshTimeout = null;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
logTokenStatus(message, additionalInfo = {}) {
|
|
194
|
-
if (!this.debug)
|
|
195
|
-
return;
|
|
196
|
-
const token = this.token;
|
|
197
|
-
if (!token)
|
|
198
|
-
return;
|
|
199
|
-
console.log(message, {
|
|
200
|
-
expiresIn: `${Math.round((token.expiresAt - Date.now()) / 1000)} seconds`,
|
|
201
|
-
expiresAt: new Date(token.expiresAt).toISOString(),
|
|
202
|
-
...additionalInfo,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
113
|
setToken(tokenResponse) {
|
|
206
114
|
const expiresAt = this.getExpirationFromToken(tokenResponse.accessToken);
|
|
207
|
-
|
|
115
|
+
const token = {
|
|
208
116
|
accessToken: tokenResponse.accessToken,
|
|
209
117
|
expiresAt,
|
|
210
118
|
};
|
|
211
|
-
this.logTokenStatus('Token set', {
|
|
212
|
-
expiresAt: new Date(expiresAt).toISOString(),
|
|
213
|
-
});
|
|
214
119
|
if (typeof window !== 'undefined') {
|
|
215
|
-
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(
|
|
120
|
+
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(token));
|
|
216
121
|
}
|
|
217
|
-
this.startRefreshInterval();
|
|
218
122
|
}
|
|
219
|
-
|
|
220
|
-
this.token = null;
|
|
221
|
-
this.clearRefreshTimers();
|
|
222
|
-
if (typeof window !== 'undefined') {
|
|
223
|
-
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
async revokeToken() {
|
|
227
|
-
const token = this.token;
|
|
228
|
-
this.clearToken();
|
|
229
|
-
if (!token)
|
|
230
|
-
return;
|
|
123
|
+
getExpirationFromToken(token) {
|
|
231
124
|
try {
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
method: 'POST',
|
|
236
|
-
headers: {
|
|
237
|
-
'Content-Type': 'application/json',
|
|
238
|
-
'Authorization': `Bearer ${token.accessToken}`
|
|
239
|
-
},
|
|
240
|
-
body: JSON.stringify({ token: token.accessToken }),
|
|
241
|
-
credentials: 'include',
|
|
242
|
-
signal: controller.signal,
|
|
243
|
-
});
|
|
244
|
-
clearTimeout(timeoutId);
|
|
125
|
+
const [, payload] = token.split('.');
|
|
126
|
+
const decodedPayload = JSON.parse(atob(payload));
|
|
127
|
+
return decodedPayload.exp * 1000; // Convert to milliseconds
|
|
245
128
|
}
|
|
246
129
|
catch (error) {
|
|
247
|
-
console.error('Failed to
|
|
248
|
-
|
|
130
|
+
console.error('getExpirationFromToken: Failed to parse token expiration:', error);
|
|
131
|
+
return Date.now() + (4 * 60 * 1000); // Default to 4 minutes if parsing fails
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
clearToken() {
|
|
135
|
+
if (typeof window === 'undefined') {
|
|
136
|
+
return;
|
|
249
137
|
}
|
|
138
|
+
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
250
139
|
}
|
|
251
140
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { FlexbeConfig, FlexbeResponse } from '../types';
|
|
2
2
|
export declare class ApiClient {
|
|
3
3
|
private readonly config;
|
|
4
|
-
private readonly
|
|
4
|
+
private readonly tokenManager;
|
|
5
5
|
constructor(config: FlexbeConfig);
|
|
6
|
+
private getAuthHeaders;
|
|
6
7
|
private buildUrl;
|
|
7
8
|
private request;
|
|
8
9
|
get<T>(url: string, config?: RequestInit & {
|
|
@@ -1,24 +1,13 @@
|
|
|
1
|
-
import { TokenResponse } from '../types';
|
|
2
1
|
export declare class TokenManager {
|
|
3
2
|
private static instance;
|
|
4
|
-
private token;
|
|
5
|
-
private refreshInterval;
|
|
6
|
-
private refreshTimeout;
|
|
7
3
|
private tokenPromise;
|
|
8
|
-
private debug;
|
|
9
|
-
private constructor();
|
|
10
4
|
static getInstance(): TokenManager;
|
|
11
|
-
private initializeFromStorage;
|
|
12
|
-
private setupStorageListener;
|
|
13
|
-
private getExpirationFromToken;
|
|
14
5
|
getToken(): Promise<string | null>;
|
|
6
|
+
revokeToken(): Promise<void>;
|
|
7
|
+
private getStoredToken;
|
|
15
8
|
private retrieveToken;
|
|
16
9
|
private doRetrieveToken;
|
|
17
|
-
private
|
|
18
|
-
private
|
|
19
|
-
private
|
|
20
|
-
private logTokenStatus;
|
|
21
|
-
setToken(tokenResponse: TokenResponse): void;
|
|
22
|
-
clearToken(): void;
|
|
23
|
-
revokeToken(): Promise<void>;
|
|
10
|
+
private setToken;
|
|
11
|
+
private getExpirationFromToken;
|
|
12
|
+
private clearToken;
|
|
24
13
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flexbe/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.31",
|
|
4
4
|
"description": "TypeScript SDK for Flexbe API",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -23,9 +23,7 @@
|
|
|
23
23
|
"lint": "eslint src --ext .ts",
|
|
24
24
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
25
25
|
"prepare": "npm run build",
|
|
26
|
-
"prepublishOnly": "npm test && npm run lint"
|
|
27
|
-
"version": "npm version",
|
|
28
|
-
"publish": "npm publish --access public"
|
|
26
|
+
"prepublishOnly": "npm test && npm run lint"
|
|
29
27
|
},
|
|
30
28
|
"keywords": [
|
|
31
29
|
"flexbe",
|