@flexbe/sdk 0.2.29 → 0.2.30
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 +66 -179
- package/dist/cjs/client/api-client.js +25 -4
- package/dist/cjs/client/client.js +8 -4
- package/dist/cjs/client/token-manager.js +61 -178
- package/dist/esm/client/api-client.js +26 -5
- package/dist/esm/client/client.js +8 -4
- package/dist/esm/client/token-manager.js +61 -178
- 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 -3
|
@@ -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,19 +7,12 @@ 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 { UnauthorizedException } from '../types';
|
|
10
11
|
const TOKEN_STORAGE_KEY = 'flexbe_jwt_token';
|
|
11
|
-
const
|
|
12
|
-
const REFRESH_CHECK_INTERVAL = 30 * 1000; // Check every 30 seconds
|
|
13
|
-
const MAX_REFRESH_DELAY = 10000; // Maximum random delay of 10 seconds
|
|
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) {
|
|
@@ -27,85 +20,65 @@ export class TokenManager {
|
|
|
27
20
|
}
|
|
28
21
|
return TokenManager.instance;
|
|
29
22
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
this.clearToken();
|
|
23
|
+
getToken() {
|
|
24
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25
|
+
var _a;
|
|
26
|
+
const token = this.getStoredToken();
|
|
27
|
+
if (token && token.expiresAt > Date.now()) {
|
|
28
|
+
// TODO check if token expire less that 1 minute
|
|
29
|
+
// if so, retrieve a new token
|
|
30
|
+
if (token.expiresAt - Date.now() < TOKEN_REFRESH_THRESHOLD) {
|
|
31
|
+
void this.retrieveToken();
|
|
32
|
+
}
|
|
33
|
+
return token.accessToken;
|
|
44
34
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
35
|
+
yield this.retrieveToken();
|
|
36
|
+
const retrievedToken = this.getStoredToken();
|
|
37
|
+
return (_a = retrievedToken === null || retrievedToken === void 0 ? void 0 : retrievedToken.accessToken) !== null && _a !== void 0 ? _a : null;
|
|
38
|
+
});
|
|
50
39
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
return;
|
|
57
|
-
if (!event.newValue) {
|
|
58
|
-
this.clearToken();
|
|
59
|
-
// void this.retrieveToken();
|
|
40
|
+
revokeToken() {
|
|
41
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
42
|
+
const token = this.getStoredToken();
|
|
43
|
+
this.clearToken();
|
|
44
|
+
if (!token)
|
|
60
45
|
return;
|
|
61
|
-
}
|
|
62
46
|
try {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
this.clearToken();
|
|
77
|
-
void this.retrieveToken();
|
|
78
|
-
}
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
49
|
+
yield fetch('/oauth/revoke', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
'Authorization': `Bearer ${token.accessToken}`
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({ token: token.accessToken }),
|
|
56
|
+
credentials: 'include',
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
});
|
|
59
|
+
clearTimeout(timeoutId);
|
|
79
60
|
}
|
|
80
61
|
catch (error) {
|
|
81
|
-
console.error('Failed to
|
|
82
|
-
this.clearToken();
|
|
83
|
-
void this.retrieveToken();
|
|
62
|
+
console.error('Failed to revoke token:', error);
|
|
84
63
|
}
|
|
85
64
|
});
|
|
86
65
|
}
|
|
87
|
-
|
|
66
|
+
getStoredToken() {
|
|
67
|
+
if (typeof window === 'undefined')
|
|
68
|
+
return null;
|
|
69
|
+
const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY);
|
|
70
|
+
if (!storedToken) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
88
73
|
try {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
return decodedPayload.exp * 1000; // Convert to milliseconds
|
|
74
|
+
const token = JSON.parse(storedToken);
|
|
75
|
+
return token;
|
|
92
76
|
}
|
|
93
77
|
catch (error) {
|
|
94
|
-
console.error('Failed to parse token
|
|
95
|
-
return
|
|
78
|
+
console.error('getStoredToken: Failed to parse stored token:', error);
|
|
79
|
+
return null;
|
|
96
80
|
}
|
|
97
81
|
}
|
|
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
82
|
retrieveToken() {
|
|
110
83
|
return __awaiter(this, void 0, void 0, function* () {
|
|
111
84
|
if (this.tokenPromise) {
|
|
@@ -136,130 +109,44 @@ export class TokenManager {
|
|
|
136
109
|
clearTimeout(timeoutId);
|
|
137
110
|
if (!response.ok) {
|
|
138
111
|
const errorData = yield response.json().catch(() => ({ message: response.statusText }));
|
|
112
|
+
if (response.status === 401) {
|
|
113
|
+
throw new UnauthorizedException(errorData.message || response.statusText);
|
|
114
|
+
}
|
|
139
115
|
throw new Error(errorData.message || response.statusText);
|
|
140
116
|
}
|
|
141
117
|
const data = yield response.json();
|
|
142
118
|
this.setToken(data);
|
|
143
119
|
}
|
|
144
120
|
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
121
|
throw error;
|
|
152
122
|
}
|
|
153
123
|
});
|
|
154
124
|
}
|
|
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
125
|
setToken(tokenResponse) {
|
|
218
126
|
const expiresAt = this.getExpirationFromToken(tokenResponse.accessToken);
|
|
219
|
-
|
|
127
|
+
const token = {
|
|
220
128
|
accessToken: tokenResponse.accessToken,
|
|
221
129
|
expiresAt,
|
|
222
130
|
};
|
|
223
|
-
this.logTokenStatus('Token set', {
|
|
224
|
-
expiresAt: new Date(expiresAt).toISOString(),
|
|
225
|
-
});
|
|
226
131
|
if (typeof window !== 'undefined') {
|
|
227
|
-
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(
|
|
132
|
+
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(token));
|
|
228
133
|
}
|
|
229
|
-
this.startRefreshInterval();
|
|
230
134
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
135
|
+
getExpirationFromToken(token) {
|
|
136
|
+
try {
|
|
137
|
+
const [, payload] = token.split('.');
|
|
138
|
+
const decodedPayload = JSON.parse(atob(payload));
|
|
139
|
+
return decodedPayload.exp * 1000; // Convert to milliseconds
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
console.error('getExpirationFromToken: Failed to parse token expiration:', error);
|
|
143
|
+
return Date.now() + (4 * 60 * 1000); // Default to 4 minutes if parsing fails
|
|
236
144
|
}
|
|
237
145
|
}
|
|
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
|
-
});
|
|
146
|
+
clearToken() {
|
|
147
|
+
if (typeof window === 'undefined') {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
264
151
|
}
|
|
265
152
|
}
|
|
@@ -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,19 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TokenManager = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
4
5
|
const TOKEN_STORAGE_KEY = 'flexbe_jwt_token';
|
|
5
|
-
const
|
|
6
|
-
const REFRESH_CHECK_INTERVAL = 30 * 1000; // Check every 30 seconds
|
|
7
|
-
const MAX_REFRESH_DELAY = 10000; // Maximum random delay of 10 seconds
|
|
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) {
|
|
@@ -21,81 +14,59 @@ class TokenManager {
|
|
|
21
14
|
}
|
|
22
15
|
return TokenManager.instance;
|
|
23
16
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
async getToken() {
|
|
18
|
+
const token = this.getStoredToken();
|
|
19
|
+
if (token && token.expiresAt > Date.now()) {
|
|
20
|
+
// TODO check if token expire less that 1 minute
|
|
21
|
+
// if so, retrieve a new token
|
|
22
|
+
if (token.expiresAt - Date.now() < TOKEN_REFRESH_THRESHOLD) {
|
|
23
|
+
void this.retrieveToken();
|
|
24
|
+
}
|
|
25
|
+
return token.accessToken;
|
|
26
|
+
}
|
|
27
|
+
await this.retrieveToken();
|
|
28
|
+
const retrievedToken = this.getStoredToken();
|
|
29
|
+
return retrievedToken?.accessToken ?? null;
|
|
30
|
+
}
|
|
31
|
+
async revokeToken() {
|
|
32
|
+
const token = this.getStoredToken();
|
|
33
|
+
this.clearToken();
|
|
34
|
+
if (!token)
|
|
29
35
|
return;
|
|
30
36
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
39
|
+
await fetch('/oauth/revoke', {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
'Authorization': `Bearer ${token.accessToken}`
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({ token: token.accessToken }),
|
|
46
|
+
credentials: 'include',
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
});
|
|
49
|
+
clearTimeout(timeoutId);
|
|
39
50
|
}
|
|
40
51
|
catch (error) {
|
|
41
|
-
console.error('Failed to
|
|
42
|
-
this.clearToken();
|
|
52
|
+
console.error('Failed to revoke token:', error);
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
|
-
|
|
55
|
+
getStoredToken() {
|
|
46
56
|
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) {
|
|
57
|
+
return null;
|
|
58
|
+
const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY);
|
|
59
|
+
if (!storedToken) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
82
62
|
try {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
return decodedPayload.exp * 1000; // Convert to milliseconds
|
|
63
|
+
const token = JSON.parse(storedToken);
|
|
64
|
+
return token;
|
|
86
65
|
}
|
|
87
66
|
catch (error) {
|
|
88
|
-
console.error('Failed to parse token
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
async getToken() {
|
|
93
|
-
const token = this.token;
|
|
94
|
-
if (token && token.expiresAt && token.expiresAt > Date.now()) {
|
|
95
|
-
return token.accessToken;
|
|
67
|
+
console.error('getStoredToken: Failed to parse stored token:', error);
|
|
68
|
+
return null;
|
|
96
69
|
}
|
|
97
|
-
await this.retrieveToken();
|
|
98
|
-
return this.token?.accessToken ?? null;
|
|
99
70
|
}
|
|
100
71
|
async retrieveToken() {
|
|
101
72
|
if (this.tokenPromise) {
|
|
@@ -124,132 +95,44 @@ class TokenManager {
|
|
|
124
95
|
clearTimeout(timeoutId);
|
|
125
96
|
if (!response.ok) {
|
|
126
97
|
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
|
98
|
+
if (response.status === 401) {
|
|
99
|
+
throw new types_1.UnauthorizedException(errorData.message || response.statusText);
|
|
100
|
+
}
|
|
127
101
|
throw new Error(errorData.message || response.statusText);
|
|
128
102
|
}
|
|
129
103
|
const data = await response.json();
|
|
130
104
|
this.setToken(data);
|
|
131
105
|
}
|
|
132
106
|
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
107
|
throw error;
|
|
140
108
|
}
|
|
141
109
|
}
|
|
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
110
|
setToken(tokenResponse) {
|
|
209
111
|
const expiresAt = this.getExpirationFromToken(tokenResponse.accessToken);
|
|
210
|
-
|
|
112
|
+
const token = {
|
|
211
113
|
accessToken: tokenResponse.accessToken,
|
|
212
114
|
expiresAt,
|
|
213
115
|
};
|
|
214
|
-
this.logTokenStatus('Token set', {
|
|
215
|
-
expiresAt: new Date(expiresAt).toISOString(),
|
|
216
|
-
});
|
|
217
|
-
if (typeof window !== 'undefined') {
|
|
218
|
-
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(this.token));
|
|
219
|
-
}
|
|
220
|
-
this.startRefreshInterval();
|
|
221
|
-
}
|
|
222
|
-
clearToken() {
|
|
223
|
-
this.token = null;
|
|
224
|
-
this.clearRefreshTimers();
|
|
225
116
|
if (typeof window !== 'undefined') {
|
|
226
|
-
localStorage.
|
|
117
|
+
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(token));
|
|
227
118
|
}
|
|
228
119
|
}
|
|
229
|
-
|
|
230
|
-
const token = this.token;
|
|
231
|
-
this.clearToken();
|
|
232
|
-
if (!token)
|
|
233
|
-
return;
|
|
120
|
+
getExpirationFromToken(token) {
|
|
234
121
|
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);
|
|
122
|
+
const [, payload] = token.split('.');
|
|
123
|
+
const decodedPayload = JSON.parse(atob(payload));
|
|
124
|
+
return decodedPayload.exp * 1000; // Convert to milliseconds
|
|
248
125
|
}
|
|
249
126
|
catch (error) {
|
|
250
|
-
console.error('Failed to
|
|
251
|
-
|
|
127
|
+
console.error('getExpirationFromToken: Failed to parse token expiration:', error);
|
|
128
|
+
return Date.now() + (4 * 60 * 1000); // Default to 4 minutes if parsing fails
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
clearToken() {
|
|
132
|
+
if (typeof window === 'undefined') {
|
|
133
|
+
return;
|
|
252
134
|
}
|
|
135
|
+
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
253
136
|
}
|
|
254
137
|
}
|
|
255
138
|
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,16 +1,9 @@
|
|
|
1
|
+
import { UnauthorizedException } from '../types';
|
|
1
2
|
const TOKEN_STORAGE_KEY = 'flexbe_jwt_token';
|
|
2
|
-
const
|
|
3
|
-
const REFRESH_CHECK_INTERVAL = 30 * 1000; // Check every 30 seconds
|
|
4
|
-
const MAX_REFRESH_DELAY = 10000; // Maximum random delay of 10 seconds
|
|
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) {
|
|
@@ -18,81 +11,59 @@ export class TokenManager {
|
|
|
18
11
|
}
|
|
19
12
|
return TokenManager.instance;
|
|
20
13
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
async getToken() {
|
|
15
|
+
const token = this.getStoredToken();
|
|
16
|
+
if (token && token.expiresAt > Date.now()) {
|
|
17
|
+
// TODO check if token expire less that 1 minute
|
|
18
|
+
// if so, retrieve a new token
|
|
19
|
+
if (token.expiresAt - Date.now() < TOKEN_REFRESH_THRESHOLD) {
|
|
20
|
+
void this.retrieveToken();
|
|
21
|
+
}
|
|
22
|
+
return token.accessToken;
|
|
23
|
+
}
|
|
24
|
+
await this.retrieveToken();
|
|
25
|
+
const retrievedToken = this.getStoredToken();
|
|
26
|
+
return retrievedToken?.accessToken ?? null;
|
|
27
|
+
}
|
|
28
|
+
async revokeToken() {
|
|
29
|
+
const token = this.getStoredToken();
|
|
30
|
+
this.clearToken();
|
|
31
|
+
if (!token)
|
|
26
32
|
return;
|
|
27
33
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
36
|
+
await fetch('/oauth/revoke', {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
'Authorization': `Bearer ${token.accessToken}`
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({ token: token.accessToken }),
|
|
43
|
+
credentials: 'include',
|
|
44
|
+
signal: controller.signal,
|
|
45
|
+
});
|
|
46
|
+
clearTimeout(timeoutId);
|
|
36
47
|
}
|
|
37
48
|
catch (error) {
|
|
38
|
-
console.error('Failed to
|
|
39
|
-
this.clearToken();
|
|
49
|
+
console.error('Failed to revoke token:', error);
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
|
-
|
|
52
|
+
getStoredToken() {
|
|
43
53
|
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) {
|
|
54
|
+
return null;
|
|
55
|
+
const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY);
|
|
56
|
+
if (!storedToken) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
79
59
|
try {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
return decodedPayload.exp * 1000; // Convert to milliseconds
|
|
60
|
+
const token = JSON.parse(storedToken);
|
|
61
|
+
return token;
|
|
83
62
|
}
|
|
84
63
|
catch (error) {
|
|
85
|
-
console.error('Failed to parse token
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
async getToken() {
|
|
90
|
-
const token = this.token;
|
|
91
|
-
if (token && token.expiresAt && token.expiresAt > Date.now()) {
|
|
92
|
-
return token.accessToken;
|
|
64
|
+
console.error('getStoredToken: Failed to parse stored token:', error);
|
|
65
|
+
return null;
|
|
93
66
|
}
|
|
94
|
-
await this.retrieveToken();
|
|
95
|
-
return this.token?.accessToken ?? null;
|
|
96
67
|
}
|
|
97
68
|
async retrieveToken() {
|
|
98
69
|
if (this.tokenPromise) {
|
|
@@ -121,131 +92,43 @@ export class TokenManager {
|
|
|
121
92
|
clearTimeout(timeoutId);
|
|
122
93
|
if (!response.ok) {
|
|
123
94
|
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
|
95
|
+
if (response.status === 401) {
|
|
96
|
+
throw new UnauthorizedException(errorData.message || response.statusText);
|
|
97
|
+
}
|
|
124
98
|
throw new Error(errorData.message || response.statusText);
|
|
125
99
|
}
|
|
126
100
|
const data = await response.json();
|
|
127
101
|
this.setToken(data);
|
|
128
102
|
}
|
|
129
103
|
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
104
|
throw error;
|
|
137
105
|
}
|
|
138
106
|
}
|
|
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
107
|
setToken(tokenResponse) {
|
|
206
108
|
const expiresAt = this.getExpirationFromToken(tokenResponse.accessToken);
|
|
207
|
-
|
|
109
|
+
const token = {
|
|
208
110
|
accessToken: tokenResponse.accessToken,
|
|
209
111
|
expiresAt,
|
|
210
112
|
};
|
|
211
|
-
this.logTokenStatus('Token set', {
|
|
212
|
-
expiresAt: new Date(expiresAt).toISOString(),
|
|
213
|
-
});
|
|
214
|
-
if (typeof window !== 'undefined') {
|
|
215
|
-
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(this.token));
|
|
216
|
-
}
|
|
217
|
-
this.startRefreshInterval();
|
|
218
|
-
}
|
|
219
|
-
clearToken() {
|
|
220
|
-
this.token = null;
|
|
221
|
-
this.clearRefreshTimers();
|
|
222
113
|
if (typeof window !== 'undefined') {
|
|
223
|
-
localStorage.
|
|
114
|
+
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(token));
|
|
224
115
|
}
|
|
225
116
|
}
|
|
226
|
-
|
|
227
|
-
const token = this.token;
|
|
228
|
-
this.clearToken();
|
|
229
|
-
if (!token)
|
|
230
|
-
return;
|
|
117
|
+
getExpirationFromToken(token) {
|
|
231
118
|
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);
|
|
119
|
+
const [, payload] = token.split('.');
|
|
120
|
+
const decodedPayload = JSON.parse(atob(payload));
|
|
121
|
+
return decodedPayload.exp * 1000; // Convert to milliseconds
|
|
245
122
|
}
|
|
246
123
|
catch (error) {
|
|
247
|
-
console.error('Failed to
|
|
248
|
-
|
|
124
|
+
console.error('getExpirationFromToken: Failed to parse token expiration:', error);
|
|
125
|
+
return Date.now() + (4 * 60 * 1000); // Default to 4 minutes if parsing fails
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
clearToken() {
|
|
129
|
+
if (typeof window === 'undefined') {
|
|
130
|
+
return;
|
|
249
131
|
}
|
|
132
|
+
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
250
133
|
}
|
|
251
134
|
}
|
|
@@ -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.30",
|
|
4
4
|
"description": "TypeScript SDK for Flexbe API",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
25
25
|
"prepare": "npm run build",
|
|
26
26
|
"prepublishOnly": "npm test && npm run lint",
|
|
27
|
-
"version": "npm version"
|
|
28
|
-
"publish": "npm publish --access public"
|
|
27
|
+
"version": "npm version"
|
|
29
28
|
},
|
|
30
29
|
"keywords": [
|
|
31
30
|
"flexbe",
|