@flexbe/sdk 0.2.28 → 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.
@@ -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 REFRESH_THRESHOLD = 0.8; // Refresh when 80% of token lifetime has passed
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
- initializeFromStorage() {
25
- if (typeof window === 'undefined')
26
- return;
27
- const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY);
28
- if (!storedToken)
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
- this.token = JSON.parse(storedToken);
32
- if (this.token.expiresAt > Date.now()) {
33
- this.logTokenStatus('Token loaded from storage');
34
- this.startRefreshInterval();
35
- }
36
- else {
37
- this.clearToken();
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 parse stored token:', error);
42
- this.clearToken();
52
+ console.error('Failed to revoke token:', error);
43
53
  }
44
54
  }
45
- setupStorageListener() {
55
+ getStoredToken() {
46
56
  if (typeof window === 'undefined')
47
- return;
48
- window.addEventListener('storage', (event) => {
49
- if (event.key !== TOKEN_STORAGE_KEY)
50
- return;
51
- if (!event.newValue) {
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 [, payload] = token.split('.');
84
- const decodedPayload = JSON.parse(atob(payload));
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 expiration:', error);
89
- return Date.now() + (4 * 60 * 1000); // Default to 4 minutes if parsing fails
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
- this.token = {
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.removeItem(TOKEN_STORAGE_KEY);
117
+ localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(token));
227
118
  }
228
119
  }
229
- async revokeToken() {
230
- const token = this.token;
231
- this.clearToken();
232
- if (!token)
233
- return;
120
+ getExpirationFromToken(token) {
234
121
  try {
235
- const controller = new AbortController();
236
- const timeoutId = setTimeout(() => controller.abort(), 30000);
237
- await fetch('/oauth/revoke', {
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 revoke token:', error);
251
- // Even if revocation fails, we still want to clear the local token
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;
@@ -7,50 +7,62 @@ var FlexbeAuthType;
7
7
  FlexbeAuthType["BEARER"] = "bearer";
8
8
  })(FlexbeAuthType || (exports.FlexbeAuthType = FlexbeAuthType = {}));
9
9
  class NotFoundException extends Error {
10
- constructor(message) {
10
+ constructor(message, error, errors) {
11
11
  super(Array.isArray(message) ? message.join(', ') : message);
12
12
  this.statusCode = 404;
13
13
  this.name = 'NotFoundException';
14
+ this.error = error || 'not_found';
15
+ this.errors = errors;
14
16
  }
15
17
  }
16
18
  exports.NotFoundException = NotFoundException;
17
19
  class ForbiddenException extends Error {
18
- constructor(message) {
20
+ constructor(message, error, errors) {
19
21
  super(Array.isArray(message) ? message.join(', ') : message);
20
22
  this.statusCode = 403;
21
23
  this.name = 'ForbiddenException';
24
+ this.error = error || 'forbidden';
25
+ this.errors = errors;
22
26
  }
23
27
  }
24
28
  exports.ForbiddenException = ForbiddenException;
25
29
  class BadRequestException extends Error {
26
- constructor(message) {
30
+ constructor(message, error, errors) {
27
31
  super(Array.isArray(message) ? message.join(', ') : message);
28
32
  this.statusCode = 400;
29
33
  this.name = 'BadRequestException';
34
+ this.error = error || 'bad_request';
35
+ this.errors = errors;
30
36
  }
31
37
  }
32
38
  exports.BadRequestException = BadRequestException;
33
39
  class UnauthorizedException extends Error {
34
- constructor(message) {
40
+ constructor(message, error, errors) {
35
41
  super(Array.isArray(message) ? message.join(', ') : message);
36
42
  this.statusCode = 401;
37
43
  this.name = 'UnauthorizedException';
44
+ this.error = error || 'unauthorized';
45
+ this.errors = errors;
38
46
  }
39
47
  }
40
48
  exports.UnauthorizedException = UnauthorizedException;
41
49
  class ServerException extends Error {
42
- constructor(message, statusCode = 500) {
50
+ constructor(message, error, statusCode = 500, errors) {
43
51
  super(Array.isArray(message) ? message.join(', ') : message);
44
52
  this.name = 'ServerException';
53
+ this.error = error || 'server_error';
45
54
  this.statusCode = statusCode;
55
+ this.errors = errors;
46
56
  }
47
57
  }
48
58
  exports.ServerException = ServerException;
49
59
  class TimeoutException extends Error {
50
- constructor(message) {
60
+ constructor(message, error, errors) {
51
61
  super(Array.isArray(message) ? message.join(', ') : message);
52
62
  this.statusCode = 408;
53
63
  this.name = 'TimeoutException';
64
+ this.error = error || 'timeout';
65
+ this.errors = errors;
54
66
  }
55
67
  }
56
68
  exports.TimeoutException = TimeoutException;
@@ -1,9 +1,27 @@
1
- import { NotFoundException, ForbiddenException, BadRequestException, UnauthorizedException, ServerException, TimeoutException } from '../types';
2
- import { FlexbeAuth } from './auth';
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.auth = new FlexbeAuth(config);
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
- ...(await this.auth.getAuthHeaders()),
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, {
@@ -41,23 +59,24 @@ export class ApiClient {
41
59
  const errorData = await response.json().catch(() => defaultError);
42
60
  switch (errorData.statusCode) {
43
61
  case 400:
44
- throw new BadRequestException(errorData.message);
62
+ throw new BadRequestException(errorData.message, errorData.error, errorData.errors);
45
63
  case 401:
46
- throw new UnauthorizedException(errorData.message);
64
+ throw new UnauthorizedException(errorData.message, errorData.error, errorData.errors);
47
65
  case 403:
48
- throw new ForbiddenException(errorData.message);
66
+ throw new ForbiddenException(errorData.message, errorData.error, errorData.errors);
49
67
  case 404:
50
- throw new NotFoundException(errorData.message);
68
+ throw new NotFoundException(errorData.message, errorData.error, errorData.errors);
51
69
  case 500:
52
70
  case 502:
53
71
  case 503:
54
72
  case 504:
55
- throw new ServerException(errorData.message, errorData.statusCode);
73
+ throw new ServerException(errorData.message, errorData.error, errorData.statusCode, errorData.errors);
56
74
  default:
57
75
  throw {
58
76
  message: errorData.message,
59
77
  error: errorData.error,
60
- statusCode: errorData.statusCode
78
+ statusCode: errorData.statusCode,
79
+ errors: errorData.errors
61
80
  };
62
81
  }
63
82
  }
@@ -77,6 +96,9 @@ export class ApiClient {
77
96
  };
78
97
  }
79
98
  catch (error) {
99
+ if (error instanceof UnauthorizedException) {
100
+ this.config.hooks?.onUnauthorized?.();
101
+ }
80
102
  if (error instanceof Error && error.name === 'AbortError') {
81
103
  throw new TimeoutException('Request timeout');
82
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
- baseUrl: config?.baseUrl || getEnvVar('FLEXBE_API_URL') || 'https://api.flexbe.com',
17
- timeout: config?.timeout || 30000,
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.');