@vulog/aima-client 1.0.10 → 1.0.13

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/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @vulog/aima-client
2
2
 
3
+ ```bash
4
+ npm i @vulog/aima-client
5
+ ```
6
+
3
7
  ```javascript
4
8
  import { getClient } from '@vulog/aima-client';
5
9
 
package/dist/index.d.mts CHANGED
@@ -1,5 +1,13 @@
1
1
  import { AxiosInstance } from 'axios';
2
2
 
3
+ type Token = {
4
+ accessToken: string;
5
+ refreshToken: string;
6
+ };
7
+ type Store = {
8
+ getToken: () => Promise<Token | undefined>;
9
+ setToken: (token: Token) => Promise<void>;
10
+ };
3
11
  type ClientOptions = {
4
12
  fleetId: string;
5
13
  name?: string;
@@ -11,18 +19,18 @@ type ClientOptions = {
11
19
  secure?: boolean;
12
20
  logCurl?: boolean;
13
21
  logResponse?: boolean;
14
- onRefreshToken?: (refreshToken: string) => void;
22
+ store?: Store;
15
23
  onLog?: (...args: any[]) => void;
16
24
  };
17
25
  type ClientError = {
18
26
  formattedError: {
19
- status: number;
20
- data: any;
27
+ status?: number;
28
+ data?: any;
29
+ message?: string;
21
30
  };
22
31
  originalError: any;
23
32
  };
24
33
  type Client = AxiosInstance & {
25
- refreshToken?: string;
26
34
  signInWithPassword: (username: string, password: string) => Promise<void>;
27
35
  clientOptions: ClientOptions;
28
36
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  import { AxiosInstance } from 'axios';
2
2
 
3
+ type Token = {
4
+ accessToken: string;
5
+ refreshToken: string;
6
+ };
7
+ type Store = {
8
+ getToken: () => Promise<Token | undefined>;
9
+ setToken: (token: Token) => Promise<void>;
10
+ };
3
11
  type ClientOptions = {
4
12
  fleetId: string;
5
13
  name?: string;
@@ -11,18 +19,18 @@ type ClientOptions = {
11
19
  secure?: boolean;
12
20
  logCurl?: boolean;
13
21
  logResponse?: boolean;
14
- onRefreshToken?: (refreshToken: string) => void;
22
+ store?: Store;
15
23
  onLog?: (...args: any[]) => void;
16
24
  };
17
25
  type ClientError = {
18
26
  formattedError: {
19
- status: number;
20
- data: any;
27
+ status?: number;
28
+ data?: any;
29
+ message?: string;
21
30
  };
22
31
  originalError: any;
23
32
  };
24
33
  type Client = AxiosInstance & {
25
- refreshToken?: string;
26
34
  signInWithPassword: (username: string, password: string) => Promise<void>;
27
35
  clientOptions: ClientOptions;
28
36
  };
package/dist/index.js CHANGED
@@ -36,7 +36,7 @@ module.exports = __toCommonJS(src_exports);
36
36
 
37
37
  // src/getClient.ts
38
38
  var import_axios = __toESM(require("axios"));
39
- var import_lodash = __toESM(require("lodash"));
39
+ var import_es_toolkit = require("es-toolkit");
40
40
  var import_lru_cache = require("lru-cache");
41
41
 
42
42
  // src/CurlHelper.ts
@@ -114,25 +114,48 @@ var CurlHelper = class {
114
114
 
115
115
  // src/getClient.ts
116
116
  var clientCache = new import_lru_cache.LRUCache({ max: 100 });
117
+ var tokenCache = new import_lru_cache.LRUCache({ max: 100 });
118
+ var getMemoryStore = (options) => ({
119
+ getToken: async () => {
120
+ const log = options.onLog ?? console.log;
121
+ log("getMemoryStore.getToken", options.name ?? options.fleetId);
122
+ if (tokenCache.has(options.name ?? options.fleetId)) {
123
+ log("getMemoryStore.getToken", tokenCache.get(options.name ?? options.fleetId));
124
+ return tokenCache.get(options.name ?? options.fleetId);
125
+ }
126
+ return void 0;
127
+ },
128
+ setToken: async (token) => {
129
+ const log = options.onLog ?? console.log;
130
+ log("getMemoryStore.setToken", options.name ?? options.fleetId, token);
131
+ tokenCache.set(options.name ?? options.fleetId, token);
132
+ }
133
+ });
117
134
  var formatError = (error) => {
118
- const { response: { status, data } = { status: 500 } } = error ?? {};
135
+ if (error instanceof import_axios.AxiosError) {
136
+ return {
137
+ originalError: error.toJSON(),
138
+ formattedError: {
139
+ status: error.response?.status ?? error.status,
140
+ data: error.response?.data,
141
+ message: error.message
142
+ }
143
+ };
144
+ }
119
145
  return {
120
- formattedError: {
121
- status,
122
- data
123
- },
124
- originalError: error.toJSON ? error.toJSON() : error
146
+ formattedError: {},
147
+ originalError: JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error)))
125
148
  };
126
149
  };
127
150
  var getClient = (options) => {
128
151
  if (clientCache.has(options.name ?? options.fleetId)) {
129
152
  const { options: cachedOptions, client: client2 } = clientCache.get(options.fleetId);
130
- if (import_lodash.default.isEqual(cachedOptions, options)) {
153
+ if ((0, import_es_toolkit.isEqual)(cachedOptions, options)) {
131
154
  return client2;
132
155
  }
133
156
  }
134
157
  const client = import_axios.default.create({
135
- baseURL: import_lodash.default.trimEnd(options.baseUrl, "/"),
158
+ baseURL: (0, import_es_toolkit.trimEnd)(options.baseUrl, "/"),
136
159
  timeout: 3e4,
137
160
  headers: {
138
161
  "Cache-Control": "no-cache",
@@ -149,7 +172,7 @@ var getClient = (options) => {
149
172
  params.append("securityOptions", "SSL_OP_NO_SSLv3");
150
173
  params.append("grant_type", "client_credentials");
151
174
  const { data: token } = await import_axios.default.post(
152
- `${import_lodash.default.trimEnd(options.baseUrl, "/")}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
175
+ `${(0, import_es_toolkit.trimEnd)(options.baseUrl, "/")}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
153
176
  params,
154
177
  {
155
178
  timeout: 3e4,
@@ -159,11 +182,17 @@ var getClient = (options) => {
159
182
  withCredentials: false
160
183
  }
161
184
  );
162
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
185
+ const store = options.store ?? getMemoryStore(options);
186
+ await store.setToken({
187
+ accessToken: token.access_token,
188
+ refreshToken: token.refresh_token
189
+ });
163
190
  return token.access_token;
164
191
  };
165
192
  const refreshTokenAuthentification = async () => {
166
- if (!client.refreshToken) {
193
+ const store = options.store ?? getMemoryStore(options);
194
+ const oldToken = await store.getToken();
195
+ if (!oldToken?.refreshToken) {
167
196
  throw new Error("No refresh token available");
168
197
  }
169
198
  const params = new URLSearchParams();
@@ -171,9 +200,9 @@ var getClient = (options) => {
171
200
  params.append("client_secret", options.clientSecret);
172
201
  params.append("securityOptions", "SSL_OP_NO_SSLv3");
173
202
  params.append("grant_type", "refresh_token");
174
- params.append("refresh_token", client.refreshToken);
203
+ params.append("refresh_token", oldToken.refreshToken);
175
204
  const { data: token } = await import_axios.default.post(
176
- `${import_lodash.default.trimEnd(options.baseUrl, "/")}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
205
+ `${(0, import_es_toolkit.trimEnd)(options.baseUrl, "/")}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
177
206
  params,
178
207
  {
179
208
  timeout: 3e4,
@@ -183,9 +212,10 @@ var getClient = (options) => {
183
212
  withCredentials: false
184
213
  }
185
214
  );
186
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
187
- client.refreshToken = token.refresh_token;
188
- options.onRefreshToken?.(token.refresh_token);
215
+ await store.setToken({
216
+ accessToken: token.access_token,
217
+ refreshToken: token.refresh_token
218
+ });
189
219
  return token.access_token;
190
220
  };
191
221
  client.signInWithPassword = async (username, password) => {
@@ -200,7 +230,7 @@ var getClient = (options) => {
200
230
  params.append("username", username);
201
231
  params.append("password", password);
202
232
  const { data: token } = await import_axios.default.post(
203
- `${import_lodash.default.trimEnd(options.baseUrl, "/")}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
233
+ `${(0, import_es_toolkit.trimEnd)(options.baseUrl, "/")}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
204
234
  params,
205
235
  {
206
236
  timeout: 3e4,
@@ -210,17 +240,39 @@ var getClient = (options) => {
210
240
  withCredentials: false
211
241
  }
212
242
  );
213
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
214
- client.refreshToken = token.refresh_token;
243
+ const store = options.store ?? getMemoryStore(options);
244
+ await store.setToken({
245
+ accessToken: token.access_token,
246
+ refreshToken: token.refresh_token
247
+ });
215
248
  };
216
- if (options.logCurl) {
217
- client.interceptors.request.use((request) => {
218
- const curl = new CurlHelper(request).generateCommand();
249
+ client.interceptors.request.use(async (request) => {
250
+ const newRequest = request;
251
+ const store = options.store ?? getMemoryStore(options);
252
+ const token = await store.getToken();
253
+ if (token?.accessToken) {
254
+ newRequest.headers.Authorization = `Bearer ${token.accessToken}`;
255
+ }
256
+ if (options.logCurl) {
257
+ const curl = new CurlHelper(newRequest).generateCommand();
219
258
  if (options.onLog) options.onLog({ curl, message: "getClient > Curl command" });
220
259
  else console.log({ curl, message: "getClient > Curl command" });
221
- return request;
260
+ }
261
+ return newRequest;
262
+ });
263
+ let isRefreshing = false;
264
+ let refreshSubscribers = [];
265
+ const executorRefresh = (config) => {
266
+ return new Promise((resolve, reject) => {
267
+ refreshSubscribers.push((token, error) => {
268
+ if (error) {
269
+ reject(formatError(error));
270
+ return;
271
+ }
272
+ resolve(client.request(config));
273
+ });
222
274
  });
223
- }
275
+ };
224
276
  client.interceptors.response.use(
225
277
  (response) => {
226
278
  if (options.logResponse) {
@@ -241,12 +293,32 @@ var getClient = (options) => {
241
293
  return Promise.reject(formatError(error));
242
294
  }
243
295
  if (status === 401) {
244
- return new Promise((resolve, reject) => {
245
- (options.secure ? refreshTokenAuthentification() : clientCredentialsAuthentification()).then((token) => {
246
- originalRequest.headers.Authorization = `Bearer ${token}`;
247
- resolve(client.request(originalRequest));
248
- }).catch(() => reject(formatError(error)));
249
- });
296
+ originalRequest.attemptCount += 1;
297
+ if (!isRefreshing) {
298
+ isRefreshing = true;
299
+ let authentification;
300
+ if (options.secure) {
301
+ authentification = refreshTokenAuthentification;
302
+ } else {
303
+ authentification = async () => {
304
+ const store = options.store ?? getMemoryStore(options);
305
+ const token = await store.getToken();
306
+ if (!token?.refreshToken) {
307
+ return clientCredentialsAuthentification();
308
+ }
309
+ return refreshTokenAuthentification();
310
+ };
311
+ }
312
+ authentification().then((accessToken) => {
313
+ refreshSubscribers.forEach((cb) => cb(accessToken));
314
+ }).catch((errorAuth) => {
315
+ refreshSubscribers.forEach((cb) => cb(void 0, errorAuth));
316
+ }).finally(() => {
317
+ isRefreshing = false;
318
+ refreshSubscribers = [];
319
+ });
320
+ }
321
+ return executorRefresh(originalRequest);
250
322
  }
251
323
  return Promise.reject(formatError(error));
252
324
  }
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/getClient.ts
2
- import axios from "axios";
3
- import _ from "lodash";
2
+ import axios, { AxiosError } from "axios";
3
+ import { isEqual, trimEnd } from "es-toolkit";
4
4
  import { LRUCache } from "lru-cache";
5
5
 
6
6
  // src/CurlHelper.ts
@@ -78,25 +78,48 @@ var CurlHelper = class {
78
78
 
79
79
  // src/getClient.ts
80
80
  var clientCache = new LRUCache({ max: 100 });
81
+ var tokenCache = new LRUCache({ max: 100 });
82
+ var getMemoryStore = (options) => ({
83
+ getToken: async () => {
84
+ const log = options.onLog ?? console.log;
85
+ log("getMemoryStore.getToken", options.name ?? options.fleetId);
86
+ if (tokenCache.has(options.name ?? options.fleetId)) {
87
+ log("getMemoryStore.getToken", tokenCache.get(options.name ?? options.fleetId));
88
+ return tokenCache.get(options.name ?? options.fleetId);
89
+ }
90
+ return void 0;
91
+ },
92
+ setToken: async (token) => {
93
+ const log = options.onLog ?? console.log;
94
+ log("getMemoryStore.setToken", options.name ?? options.fleetId, token);
95
+ tokenCache.set(options.name ?? options.fleetId, token);
96
+ }
97
+ });
81
98
  var formatError = (error) => {
82
- const { response: { status, data } = { status: 500 } } = error ?? {};
99
+ if (error instanceof AxiosError) {
100
+ return {
101
+ originalError: error.toJSON(),
102
+ formattedError: {
103
+ status: error.response?.status ?? error.status,
104
+ data: error.response?.data,
105
+ message: error.message
106
+ }
107
+ };
108
+ }
83
109
  return {
84
- formattedError: {
85
- status,
86
- data
87
- },
88
- originalError: error.toJSON ? error.toJSON() : error
110
+ formattedError: {},
111
+ originalError: JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error)))
89
112
  };
90
113
  };
91
114
  var getClient = (options) => {
92
115
  if (clientCache.has(options.name ?? options.fleetId)) {
93
116
  const { options: cachedOptions, client: client2 } = clientCache.get(options.fleetId);
94
- if (_.isEqual(cachedOptions, options)) {
117
+ if (isEqual(cachedOptions, options)) {
95
118
  return client2;
96
119
  }
97
120
  }
98
121
  const client = axios.create({
99
- baseURL: _.trimEnd(options.baseUrl, "/"),
122
+ baseURL: trimEnd(options.baseUrl, "/"),
100
123
  timeout: 3e4,
101
124
  headers: {
102
125
  "Cache-Control": "no-cache",
@@ -113,7 +136,7 @@ var getClient = (options) => {
113
136
  params.append("securityOptions", "SSL_OP_NO_SSLv3");
114
137
  params.append("grant_type", "client_credentials");
115
138
  const { data: token } = await axios.post(
116
- `${_.trimEnd(options.baseUrl, "/")}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
139
+ `${trimEnd(options.baseUrl, "/")}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
117
140
  params,
118
141
  {
119
142
  timeout: 3e4,
@@ -123,11 +146,17 @@ var getClient = (options) => {
123
146
  withCredentials: false
124
147
  }
125
148
  );
126
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
149
+ const store = options.store ?? getMemoryStore(options);
150
+ await store.setToken({
151
+ accessToken: token.access_token,
152
+ refreshToken: token.refresh_token
153
+ });
127
154
  return token.access_token;
128
155
  };
129
156
  const refreshTokenAuthentification = async () => {
130
- if (!client.refreshToken) {
157
+ const store = options.store ?? getMemoryStore(options);
158
+ const oldToken = await store.getToken();
159
+ if (!oldToken?.refreshToken) {
131
160
  throw new Error("No refresh token available");
132
161
  }
133
162
  const params = new URLSearchParams();
@@ -135,9 +164,9 @@ var getClient = (options) => {
135
164
  params.append("client_secret", options.clientSecret);
136
165
  params.append("securityOptions", "SSL_OP_NO_SSLv3");
137
166
  params.append("grant_type", "refresh_token");
138
- params.append("refresh_token", client.refreshToken);
167
+ params.append("refresh_token", oldToken.refreshToken);
139
168
  const { data: token } = await axios.post(
140
- `${_.trimEnd(options.baseUrl, "/")}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
169
+ `${trimEnd(options.baseUrl, "/")}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
141
170
  params,
142
171
  {
143
172
  timeout: 3e4,
@@ -147,9 +176,10 @@ var getClient = (options) => {
147
176
  withCredentials: false
148
177
  }
149
178
  );
150
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
151
- client.refreshToken = token.refresh_token;
152
- options.onRefreshToken?.(token.refresh_token);
179
+ await store.setToken({
180
+ accessToken: token.access_token,
181
+ refreshToken: token.refresh_token
182
+ });
153
183
  return token.access_token;
154
184
  };
155
185
  client.signInWithPassword = async (username, password) => {
@@ -164,7 +194,7 @@ var getClient = (options) => {
164
194
  params.append("username", username);
165
195
  params.append("password", password);
166
196
  const { data: token } = await axios.post(
167
- `${_.trimEnd(options.baseUrl, "/")}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
197
+ `${trimEnd(options.baseUrl, "/")}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
168
198
  params,
169
199
  {
170
200
  timeout: 3e4,
@@ -174,17 +204,39 @@ var getClient = (options) => {
174
204
  withCredentials: false
175
205
  }
176
206
  );
177
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
178
- client.refreshToken = token.refresh_token;
207
+ const store = options.store ?? getMemoryStore(options);
208
+ await store.setToken({
209
+ accessToken: token.access_token,
210
+ refreshToken: token.refresh_token
211
+ });
179
212
  };
180
- if (options.logCurl) {
181
- client.interceptors.request.use((request) => {
182
- const curl = new CurlHelper(request).generateCommand();
213
+ client.interceptors.request.use(async (request) => {
214
+ const newRequest = request;
215
+ const store = options.store ?? getMemoryStore(options);
216
+ const token = await store.getToken();
217
+ if (token?.accessToken) {
218
+ newRequest.headers.Authorization = `Bearer ${token.accessToken}`;
219
+ }
220
+ if (options.logCurl) {
221
+ const curl = new CurlHelper(newRequest).generateCommand();
183
222
  if (options.onLog) options.onLog({ curl, message: "getClient > Curl command" });
184
223
  else console.log({ curl, message: "getClient > Curl command" });
185
- return request;
224
+ }
225
+ return newRequest;
226
+ });
227
+ let isRefreshing = false;
228
+ let refreshSubscribers = [];
229
+ const executorRefresh = (config) => {
230
+ return new Promise((resolve, reject) => {
231
+ refreshSubscribers.push((token, error) => {
232
+ if (error) {
233
+ reject(formatError(error));
234
+ return;
235
+ }
236
+ resolve(client.request(config));
237
+ });
186
238
  });
187
- }
239
+ };
188
240
  client.interceptors.response.use(
189
241
  (response) => {
190
242
  if (options.logResponse) {
@@ -205,12 +257,32 @@ var getClient = (options) => {
205
257
  return Promise.reject(formatError(error));
206
258
  }
207
259
  if (status === 401) {
208
- return new Promise((resolve, reject) => {
209
- (options.secure ? refreshTokenAuthentification() : clientCredentialsAuthentification()).then((token) => {
210
- originalRequest.headers.Authorization = `Bearer ${token}`;
211
- resolve(client.request(originalRequest));
212
- }).catch(() => reject(formatError(error)));
213
- });
260
+ originalRequest.attemptCount += 1;
261
+ if (!isRefreshing) {
262
+ isRefreshing = true;
263
+ let authentification;
264
+ if (options.secure) {
265
+ authentification = refreshTokenAuthentification;
266
+ } else {
267
+ authentification = async () => {
268
+ const store = options.store ?? getMemoryStore(options);
269
+ const token = await store.getToken();
270
+ if (!token?.refreshToken) {
271
+ return clientCredentialsAuthentification();
272
+ }
273
+ return refreshTokenAuthentification();
274
+ };
275
+ }
276
+ authentification().then((accessToken) => {
277
+ refreshSubscribers.forEach((cb) => cb(accessToken));
278
+ }).catch((errorAuth) => {
279
+ refreshSubscribers.forEach((cb) => cb(void 0, errorAuth));
280
+ }).finally(() => {
281
+ isRefreshing = false;
282
+ refreshSubscribers = [];
283
+ });
284
+ }
285
+ return executorRefresh(originalRequest);
214
286
  }
215
287
  return Promise.reject(formatError(error));
216
288
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vulog/aima-client",
3
- "version": "1.0.10",
3
+ "version": "1.0.13",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -9,7 +9,8 @@
9
9
  "build": "tsup",
10
10
  "dev": "tsup --watch",
11
11
  "test": "vitest run",
12
- "test:watch": "vitest"
12
+ "test:watch": "vitest",
13
+ "lint": "eslint src/**/* --ext .ts"
13
14
  },
14
15
  "keywords": [
15
16
  "AIMA",
@@ -20,23 +21,22 @@
20
21
  "author": "Vulog",
21
22
  "license": "ISC",
22
23
  "devDependencies": {
23
- "@types/lodash": "^4.17.12",
24
- "@types/node": "^22.7.9",
24
+ "@types/node": "^22.10.1",
25
25
  "eslint-config-airbnb-base": "^15.0.0",
26
26
  "eslint-config-airbnb-typescript": "^18.0.0",
27
27
  "eslint-config-prettier": "^9.1.0",
28
28
  "eslint-plugin-import": "^2.31.0",
29
29
  "eslint-plugin-prettier": "^5.2.1",
30
30
  "eslint-plugin-unused-imports": "^4.1.4",
31
- "prettier": "^3.3.3",
32
- "tsup": "^8.3.0",
33
- "typescript": "^5.6.3",
34
- "vitest": "^2.1.3"
31
+ "prettier": "^3.4.1",
32
+ "tsup": "^8.3.5",
33
+ "typescript": "^5.7.2",
34
+ "vitest": "^2.1.8"
35
35
  },
36
36
  "peerDependencies": {
37
- "axios": "^1.7.7",
38
- "lodash": "^4.17.21",
39
- "lru-cache": "^11.0.1"
37
+ "axios": "^1.7.8",
38
+ "es-toolkit":"^1.29.0",
39
+ "lru-cache": "^11.0.2"
40
40
  },
41
41
  "description": ""
42
42
  }
package/src/getClient.ts CHANGED
@@ -1,9 +1,11 @@
1
- import axios from 'axios';
2
- import _ from 'lodash';
1
+ import axios, { AxiosError } from 'axios';
2
+ import { isEqual, trimEnd } from 'es-toolkit';
3
3
  import { LRUCache } from 'lru-cache';
4
4
 
5
5
  import { CurlHelper } from './CurlHelper';
6
- import { Client, ClientError, ClientOptions } from './types';
6
+ import { Client, ClientError, ClientOptions, Store, Token } from './types';
7
+
8
+ type RefreshSubscriber = (token?: string, error?: any) => void;
7
9
 
8
10
  const clientCache = new LRUCache<
9
11
  string,
@@ -13,28 +15,56 @@ const clientCache = new LRUCache<
13
15
  }
14
16
  >({ max: 100 });
15
17
 
18
+ const tokenCache = new LRUCache<string, Token>({ max: 100 });
19
+
20
+ const getMemoryStore = (options: ClientOptions): Store => ({
21
+ getToken: async (): Promise<Token | undefined> => {
22
+ // eslint-disable-next-line no-console
23
+ const log = options.onLog ?? console.log;
24
+ log('getMemoryStore.getToken', options.name ?? options.fleetId);
25
+ if (tokenCache.has(options.name ?? options.fleetId)) {
26
+ log('getMemoryStore.getToken', tokenCache.get(options.name ?? options.fleetId));
27
+ return tokenCache.get(options.name ?? options.fleetId)!;
28
+ }
29
+
30
+ return undefined;
31
+ },
32
+ setToken: async (token: Token): Promise<void> => {
33
+ // eslint-disable-next-line no-console
34
+ const log = options.onLog ?? console.log;
35
+ log('getMemoryStore.setToken', options.name ?? options.fleetId, token);
36
+ tokenCache.set(options.name ?? options.fleetId, token);
37
+ },
38
+ });
39
+
16
40
  const formatError = (error: any): ClientError => {
17
- const { response: { status, data } = { status: 500 } } = error ?? {};
41
+ if (error instanceof AxiosError) {
42
+ return {
43
+ originalError: error.toJSON(),
44
+ formattedError: {
45
+ status: error.response?.status ?? error.status,
46
+ data: error.response?.data,
47
+ message: error.message,
48
+ },
49
+ };
50
+ }
18
51
 
19
52
  return {
20
- formattedError: {
21
- status,
22
- data,
23
- },
24
- originalError: error.toJSON ? error.toJSON() : error,
53
+ formattedError: {},
54
+ originalError: JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error))),
25
55
  };
26
56
  };
27
57
 
28
58
  const getClient = (options: ClientOptions): Client => {
29
59
  if (clientCache.has(options.name ?? options.fleetId)) {
30
60
  const { options: cachedOptions, client } = clientCache.get(options.fleetId)!;
31
- if (_.isEqual(cachedOptions, options)) {
61
+ if (isEqual(cachedOptions, options)) {
32
62
  return client;
33
63
  }
34
64
  }
35
65
 
36
66
  const client = axios.create({
37
- baseURL: _.trimEnd(options.baseUrl, '/'),
67
+ baseURL: trimEnd(options.baseUrl, '/'),
38
68
  timeout: 30000,
39
69
  headers: {
40
70
  'Cache-Control': 'no-cache',
@@ -53,7 +83,7 @@ const getClient = (options: ClientOptions): Client => {
53
83
  params.append('grant_type', 'client_credentials');
54
84
 
55
85
  const { data: token } = await axios.post(
56
- `${_.trimEnd(options.baseUrl, '/')}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
86
+ `${trimEnd(options.baseUrl, '/')}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
57
87
  params,
58
88
  {
59
89
  timeout: 30000,
@@ -63,12 +93,18 @@ const getClient = (options: ClientOptions): Client => {
63
93
  withCredentials: false,
64
94
  }
65
95
  );
66
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
96
+ const store = options.store ?? getMemoryStore(options);
97
+ await store.setToken({
98
+ accessToken: token.access_token,
99
+ refreshToken: token.refresh_token,
100
+ });
67
101
  return token.access_token;
68
102
  };
69
103
 
70
104
  const refreshTokenAuthentification = async (): Promise<string> => {
71
- if (!client.refreshToken) {
105
+ const store = options.store ?? getMemoryStore(options);
106
+ const oldToken = await store.getToken();
107
+ if (!oldToken?.refreshToken) {
72
108
  throw new Error('No refresh token available');
73
109
  }
74
110
  const params = new URLSearchParams();
@@ -76,10 +112,10 @@ const getClient = (options: ClientOptions): Client => {
76
112
  params.append('client_secret', options.clientSecret);
77
113
  params.append('securityOptions', 'SSL_OP_NO_SSLv3');
78
114
  params.append('grant_type', 'refresh_token');
79
- params.append('refresh_token', client.refreshToken);
115
+ params.append('refresh_token', oldToken.refreshToken);
80
116
 
81
117
  const { data: token } = await axios.post(
82
- `${_.trimEnd(options.baseUrl, '/')}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
118
+ `${trimEnd(options.baseUrl, '/')}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
83
119
  params,
84
120
  {
85
121
  timeout: 30000,
@@ -89,9 +125,10 @@ const getClient = (options: ClientOptions): Client => {
89
125
  withCredentials: false,
90
126
  }
91
127
  );
92
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
93
- client.refreshToken = token.refresh_token;
94
- options.onRefreshToken?.(token.refresh_token);
128
+ await store.setToken({
129
+ accessToken: token.access_token,
130
+ refreshToken: token.refresh_token,
131
+ });
95
132
  return token.access_token;
96
133
  };
97
134
 
@@ -108,7 +145,7 @@ const getClient = (options: ClientOptions): Client => {
108
145
  params.append('password', password);
109
146
 
110
147
  const { data: token } = await axios.post(
111
- `${_.trimEnd(options.baseUrl, '/')}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
148
+ `${trimEnd(options.baseUrl, '/')}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`,
112
149
  params,
113
150
  {
114
151
  timeout: 30000,
@@ -118,19 +155,44 @@ const getClient = (options: ClientOptions): Client => {
118
155
  withCredentials: false,
119
156
  }
120
157
  );
121
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
122
- client.refreshToken = token.refresh_token;
158
+ const store = options.store ?? getMemoryStore(options);
159
+ await store.setToken({
160
+ accessToken: token.access_token,
161
+ refreshToken: token.refresh_token,
162
+ });
123
163
  };
124
164
 
125
- if (options.logCurl) {
126
- client.interceptors.request.use((request) => {
127
- const curl = new CurlHelper(request).generateCommand();
165
+ client.interceptors.request.use(async (request) => {
166
+ const newRequest = request;
167
+ const store = options.store ?? getMemoryStore(options);
168
+ const token = await store.getToken();
169
+ if (token?.accessToken) {
170
+ newRequest.headers.Authorization = `Bearer ${token.accessToken}`;
171
+ }
172
+ if (options.logCurl) {
173
+ const curl = new CurlHelper(newRequest).generateCommand();
128
174
  if (options.onLog) options.onLog({ curl, message: 'getClient > Curl command' });
129
175
  // eslint-disable-next-line no-console
130
176
  else console.log({ curl, message: 'getClient > Curl command' });
131
- return request;
177
+ }
178
+
179
+ return newRequest;
180
+ });
181
+
182
+ let isRefreshing = false;
183
+ let refreshSubscribers: RefreshSubscriber[] = [];
184
+
185
+ const executorRefresh = (config: any) => {
186
+ return new Promise((resolve, reject) => {
187
+ refreshSubscribers.push((token, error) => {
188
+ if (error) {
189
+ reject(formatError(error));
190
+ return;
191
+ }
192
+ resolve(client.request(config));
193
+ });
132
194
  });
133
- }
195
+ };
134
196
 
135
197
  client.interceptors.response.use(
136
198
  (response) => {
@@ -156,14 +218,38 @@ const getClient = (options: ClientOptions): Client => {
156
218
  }
157
219
 
158
220
  if (status === 401) {
159
- return new Promise((resolve, reject) => {
160
- (options.secure ? refreshTokenAuthentification() : clientCredentialsAuthentification())
161
- .then((token) => {
162
- originalRequest.headers.Authorization = `Bearer ${token}`;
163
- resolve(client.request(originalRequest));
221
+ originalRequest.attemptCount += 1;
222
+ if (!isRefreshing) {
223
+ isRefreshing = true;
224
+
225
+ let authentification: () => Promise<string>;
226
+ if (options.secure) {
227
+ authentification = refreshTokenAuthentification;
228
+ } else {
229
+ authentification = async () => {
230
+ const store = options.store ?? getMemoryStore(options);
231
+ const token = await store.getToken();
232
+ if (!token?.refreshToken) {
233
+ return clientCredentialsAuthentification();
234
+ }
235
+ return refreshTokenAuthentification();
236
+ };
237
+ }
238
+
239
+ authentification()
240
+ .then((accessToken) => {
241
+ refreshSubscribers.forEach((cb) => cb(accessToken));
164
242
  })
165
- .catch(() => reject(formatError(error)));
166
- });
243
+ .catch((errorAuth) => {
244
+ refreshSubscribers.forEach((cb) => cb(undefined, errorAuth));
245
+ })
246
+ .finally(() => {
247
+ isRefreshing = false;
248
+ refreshSubscribers = [];
249
+ });
250
+ }
251
+
252
+ return executorRefresh(originalRequest);
167
253
  }
168
254
 
169
255
  return Promise.reject(formatError(error));
package/src/types.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  import { AxiosInstance } from 'axios';
2
2
 
3
+ export type Token = {
4
+ accessToken: string;
5
+ refreshToken: string;
6
+ };
7
+
8
+ export type Store = {
9
+ getToken: () => Promise<Token | undefined>;
10
+ setToken: (token: Token) => Promise<void>;
11
+ };
12
+
3
13
  export type ClientOptions = {
4
14
  fleetId: string;
5
15
  name?: string;
@@ -11,20 +21,20 @@ export type ClientOptions = {
11
21
  secure?: boolean;
12
22
  logCurl?: boolean;
13
23
  logResponse?: boolean;
14
- onRefreshToken?: (refreshToken: string) => void;
24
+ store?: Store;
15
25
  onLog?: (...args: any[]) => void;
16
26
  };
17
27
 
18
28
  export type ClientError = {
19
29
  formattedError: {
20
- status: number;
21
- data: any;
30
+ status?: number;
31
+ data?: any;
32
+ message?: string;
22
33
  };
23
34
  originalError: any;
24
35
  };
25
36
 
26
37
  export type Client = AxiosInstance & {
27
- refreshToken?: string;
28
38
  signInWithPassword: (username: string, password: string) => Promise<void>;
29
39
  clientOptions: ClientOptions;
30
40
  };