@vulog/aima-client 1.0.11 → 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/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,7 +19,7 @@ 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 = {
@@ -23,7 +31,6 @@ type ClientError = {
23
31
  originalError: any;
24
32
  };
25
33
  type Client = AxiosInstance & {
26
- refreshToken?: string;
27
34
  signInWithPassword: (username: string, password: string) => Promise<void>;
28
35
  clientOptions: ClientOptions;
29
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,7 +19,7 @@ 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 = {
@@ -23,7 +31,6 @@ type ClientError = {
23
31
  originalError: any;
24
32
  };
25
33
  type Client = AxiosInstance & {
26
- refreshToken?: string;
27
34
  signInWithPassword: (username: string, password: string) => Promise<void>;
28
35
  clientOptions: ClientOptions;
29
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,6 +114,23 @@ 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
135
  if (error instanceof import_axios.AxiosError) {
119
136
  return {
@@ -133,12 +150,12 @@ var formatError = (error) => {
133
150
  var getClient = (options) => {
134
151
  if (clientCache.has(options.name ?? options.fleetId)) {
135
152
  const { options: cachedOptions, client: client2 } = clientCache.get(options.fleetId);
136
- if (import_lodash.default.isEqual(cachedOptions, options)) {
153
+ if ((0, import_es_toolkit.isEqual)(cachedOptions, options)) {
137
154
  return client2;
138
155
  }
139
156
  }
140
157
  const client = import_axios.default.create({
141
- baseURL: import_lodash.default.trimEnd(options.baseUrl, "/"),
158
+ baseURL: (0, import_es_toolkit.trimEnd)(options.baseUrl, "/"),
142
159
  timeout: 3e4,
143
160
  headers: {
144
161
  "Cache-Control": "no-cache",
@@ -155,7 +172,7 @@ var getClient = (options) => {
155
172
  params.append("securityOptions", "SSL_OP_NO_SSLv3");
156
173
  params.append("grant_type", "client_credentials");
157
174
  const { data: token } = await import_axios.default.post(
158
- `${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`,
159
176
  params,
160
177
  {
161
178
  timeout: 3e4,
@@ -165,11 +182,17 @@ var getClient = (options) => {
165
182
  withCredentials: false
166
183
  }
167
184
  );
168
- 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
+ });
169
190
  return token.access_token;
170
191
  };
171
192
  const refreshTokenAuthentification = async () => {
172
- if (!client.refreshToken) {
193
+ const store = options.store ?? getMemoryStore(options);
194
+ const oldToken = await store.getToken();
195
+ if (!oldToken?.refreshToken) {
173
196
  throw new Error("No refresh token available");
174
197
  }
175
198
  const params = new URLSearchParams();
@@ -177,9 +200,9 @@ var getClient = (options) => {
177
200
  params.append("client_secret", options.clientSecret);
178
201
  params.append("securityOptions", "SSL_OP_NO_SSLv3");
179
202
  params.append("grant_type", "refresh_token");
180
- params.append("refresh_token", client.refreshToken);
203
+ params.append("refresh_token", oldToken.refreshToken);
181
204
  const { data: token } = await import_axios.default.post(
182
- `${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`,
183
206
  params,
184
207
  {
185
208
  timeout: 3e4,
@@ -189,9 +212,10 @@ var getClient = (options) => {
189
212
  withCredentials: false
190
213
  }
191
214
  );
192
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
193
- client.refreshToken = token.refresh_token;
194
- options.onRefreshToken?.(token.refresh_token);
215
+ await store.setToken({
216
+ accessToken: token.access_token,
217
+ refreshToken: token.refresh_token
218
+ });
195
219
  return token.access_token;
196
220
  };
197
221
  client.signInWithPassword = async (username, password) => {
@@ -206,7 +230,7 @@ var getClient = (options) => {
206
230
  params.append("username", username);
207
231
  params.append("password", password);
208
232
  const { data: token } = await import_axios.default.post(
209
- `${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`,
210
234
  params,
211
235
  {
212
236
  timeout: 3e4,
@@ -216,17 +240,39 @@ var getClient = (options) => {
216
240
  withCredentials: false
217
241
  }
218
242
  );
219
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
220
- 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
+ });
221
248
  };
222
- if (options.logCurl) {
223
- client.interceptors.request.use((request) => {
224
- 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();
225
258
  if (options.onLog) options.onLog({ curl, message: "getClient > Curl command" });
226
259
  else console.log({ curl, message: "getClient > Curl command" });
227
- 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
+ });
228
274
  });
229
- }
275
+ };
230
276
  client.interceptors.response.use(
231
277
  (response) => {
232
278
  if (options.logResponse) {
@@ -247,12 +293,32 @@ var getClient = (options) => {
247
293
  return Promise.reject(formatError(error));
248
294
  }
249
295
  if (status === 401) {
250
- return new Promise((resolve, reject) => {
251
- (options.secure ? refreshTokenAuthentification() : clientCredentialsAuthentification()).then((token) => {
252
- originalRequest.headers.Authorization = `Bearer ${token}`;
253
- resolve(client.request(originalRequest));
254
- }).catch(() => reject(formatError(error)));
255
- });
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);
256
322
  }
257
323
  return Promise.reject(formatError(error));
258
324
  }
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/getClient.ts
2
2
  import axios, { AxiosError } from "axios";
3
- import _ from "lodash";
3
+ import { isEqual, trimEnd } from "es-toolkit";
4
4
  import { LRUCache } from "lru-cache";
5
5
 
6
6
  // src/CurlHelper.ts
@@ -78,6 +78,23 @@ 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
99
  if (error instanceof AxiosError) {
83
100
  return {
@@ -97,12 +114,12 @@ var formatError = (error) => {
97
114
  var getClient = (options) => {
98
115
  if (clientCache.has(options.name ?? options.fleetId)) {
99
116
  const { options: cachedOptions, client: client2 } = clientCache.get(options.fleetId);
100
- if (_.isEqual(cachedOptions, options)) {
117
+ if (isEqual(cachedOptions, options)) {
101
118
  return client2;
102
119
  }
103
120
  }
104
121
  const client = axios.create({
105
- baseURL: _.trimEnd(options.baseUrl, "/"),
122
+ baseURL: trimEnd(options.baseUrl, "/"),
106
123
  timeout: 3e4,
107
124
  headers: {
108
125
  "Cache-Control": "no-cache",
@@ -119,7 +136,7 @@ var getClient = (options) => {
119
136
  params.append("securityOptions", "SSL_OP_NO_SSLv3");
120
137
  params.append("grant_type", "client_credentials");
121
138
  const { data: token } = await axios.post(
122
- `${_.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`,
123
140
  params,
124
141
  {
125
142
  timeout: 3e4,
@@ -129,11 +146,17 @@ var getClient = (options) => {
129
146
  withCredentials: false
130
147
  }
131
148
  );
132
- 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
+ });
133
154
  return token.access_token;
134
155
  };
135
156
  const refreshTokenAuthentification = async () => {
136
- if (!client.refreshToken) {
157
+ const store = options.store ?? getMemoryStore(options);
158
+ const oldToken = await store.getToken();
159
+ if (!oldToken?.refreshToken) {
137
160
  throw new Error("No refresh token available");
138
161
  }
139
162
  const params = new URLSearchParams();
@@ -141,9 +164,9 @@ var getClient = (options) => {
141
164
  params.append("client_secret", options.clientSecret);
142
165
  params.append("securityOptions", "SSL_OP_NO_SSLv3");
143
166
  params.append("grant_type", "refresh_token");
144
- params.append("refresh_token", client.refreshToken);
167
+ params.append("refresh_token", oldToken.refreshToken);
145
168
  const { data: token } = await axios.post(
146
- `${_.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`,
147
170
  params,
148
171
  {
149
172
  timeout: 3e4,
@@ -153,9 +176,10 @@ var getClient = (options) => {
153
176
  withCredentials: false
154
177
  }
155
178
  );
156
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
157
- client.refreshToken = token.refresh_token;
158
- options.onRefreshToken?.(token.refresh_token);
179
+ await store.setToken({
180
+ accessToken: token.access_token,
181
+ refreshToken: token.refresh_token
182
+ });
159
183
  return token.access_token;
160
184
  };
161
185
  client.signInWithPassword = async (username, password) => {
@@ -170,7 +194,7 @@ var getClient = (options) => {
170
194
  params.append("username", username);
171
195
  params.append("password", password);
172
196
  const { data: token } = await axios.post(
173
- `${_.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`,
174
198
  params,
175
199
  {
176
200
  timeout: 3e4,
@@ -180,17 +204,39 @@ var getClient = (options) => {
180
204
  withCredentials: false
181
205
  }
182
206
  );
183
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
184
- 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
+ });
185
212
  };
186
- if (options.logCurl) {
187
- client.interceptors.request.use((request) => {
188
- 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();
189
222
  if (options.onLog) options.onLog({ curl, message: "getClient > Curl command" });
190
223
  else console.log({ curl, message: "getClient > Curl command" });
191
- 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
+ });
192
238
  });
193
- }
239
+ };
194
240
  client.interceptors.response.use(
195
241
  (response) => {
196
242
  if (options.logResponse) {
@@ -211,12 +257,32 @@ var getClient = (options) => {
211
257
  return Promise.reject(formatError(error));
212
258
  }
213
259
  if (status === 401) {
214
- return new Promise((resolve, reject) => {
215
- (options.secure ? refreshTokenAuthentification() : clientCredentialsAuthentification()).then((token) => {
216
- originalRequest.headers.Authorization = `Bearer ${token}`;
217
- resolve(client.request(originalRequest));
218
- }).catch(() => reject(formatError(error)));
219
- });
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);
220
286
  }
221
287
  return Promise.reject(formatError(error));
222
288
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vulog/aima-client",
3
- "version": "1.0.11",
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
1
  import axios, { AxiosError } from 'axios';
2
- import _ from 'lodash';
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,6 +15,28 @@ 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
41
  if (error instanceof AxiosError) {
18
42
  return {
@@ -34,13 +58,13 @@ const formatError = (error: any): ClientError => {
34
58
  const getClient = (options: ClientOptions): Client => {
35
59
  if (clientCache.has(options.name ?? options.fleetId)) {
36
60
  const { options: cachedOptions, client } = clientCache.get(options.fleetId)!;
37
- if (_.isEqual(cachedOptions, options)) {
61
+ if (isEqual(cachedOptions, options)) {
38
62
  return client;
39
63
  }
40
64
  }
41
65
 
42
66
  const client = axios.create({
43
- baseURL: _.trimEnd(options.baseUrl, '/'),
67
+ baseURL: trimEnd(options.baseUrl, '/'),
44
68
  timeout: 30000,
45
69
  headers: {
46
70
  'Cache-Control': 'no-cache',
@@ -59,7 +83,7 @@ const getClient = (options: ClientOptions): Client => {
59
83
  params.append('grant_type', 'client_credentials');
60
84
 
61
85
  const { data: token } = await axios.post(
62
- `${_.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`,
63
87
  params,
64
88
  {
65
89
  timeout: 30000,
@@ -69,12 +93,18 @@ const getClient = (options: ClientOptions): Client => {
69
93
  withCredentials: false,
70
94
  }
71
95
  );
72
- 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
+ });
73
101
  return token.access_token;
74
102
  };
75
103
 
76
104
  const refreshTokenAuthentification = async (): Promise<string> => {
77
- if (!client.refreshToken) {
105
+ const store = options.store ?? getMemoryStore(options);
106
+ const oldToken = await store.getToken();
107
+ if (!oldToken?.refreshToken) {
78
108
  throw new Error('No refresh token available');
79
109
  }
80
110
  const params = new URLSearchParams();
@@ -82,10 +112,10 @@ const getClient = (options: ClientOptions): Client => {
82
112
  params.append('client_secret', options.clientSecret);
83
113
  params.append('securityOptions', 'SSL_OP_NO_SSLv3');
84
114
  params.append('grant_type', 'refresh_token');
85
- params.append('refresh_token', client.refreshToken);
115
+ params.append('refresh_token', oldToken.refreshToken);
86
116
 
87
117
  const { data: token } = await axios.post(
88
- `${_.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`,
89
119
  params,
90
120
  {
91
121
  timeout: 30000,
@@ -95,9 +125,10 @@ const getClient = (options: ClientOptions): Client => {
95
125
  withCredentials: false,
96
126
  }
97
127
  );
98
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
99
- client.refreshToken = token.refresh_token;
100
- options.onRefreshToken?.(token.refresh_token);
128
+ await store.setToken({
129
+ accessToken: token.access_token,
130
+ refreshToken: token.refresh_token,
131
+ });
101
132
  return token.access_token;
102
133
  };
103
134
 
@@ -114,7 +145,7 @@ const getClient = (options: ClientOptions): Client => {
114
145
  params.append('password', password);
115
146
 
116
147
  const { data: token } = await axios.post(
117
- `${_.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`,
118
149
  params,
119
150
  {
120
151
  timeout: 30000,
@@ -124,19 +155,44 @@ const getClient = (options: ClientOptions): Client => {
124
155
  withCredentials: false,
125
156
  }
126
157
  );
127
- client.defaults.headers.Authorization = `Bearer ${token.access_token}`;
128
- 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
+ });
129
163
  };
130
164
 
131
- if (options.logCurl) {
132
- client.interceptors.request.use((request) => {
133
- 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();
134
174
  if (options.onLog) options.onLog({ curl, message: 'getClient > Curl command' });
135
175
  // eslint-disable-next-line no-console
136
176
  else console.log({ curl, message: 'getClient > Curl command' });
137
- 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
+ });
138
194
  });
139
- }
195
+ };
140
196
 
141
197
  client.interceptors.response.use(
142
198
  (response) => {
@@ -162,14 +218,38 @@ const getClient = (options: ClientOptions): Client => {
162
218
  }
163
219
 
164
220
  if (status === 401) {
165
- return new Promise((resolve, reject) => {
166
- (options.secure ? refreshTokenAuthentification() : clientCredentialsAuthentification())
167
- .then((token) => {
168
- originalRequest.headers.Authorization = `Bearer ${token}`;
169
- 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));
170
242
  })
171
- .catch(() => reject(formatError(error)));
172
- });
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);
173
253
  }
174
254
 
175
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,7 +21,7 @@ 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
 
@@ -25,7 +35,6 @@ export type ClientError = {
25
35
  };
26
36
 
27
37
  export type Client = AxiosInstance & {
28
- refreshToken?: string;
29
38
  signInWithPassword: (username: string, password: string) => Promise<void>;
30
39
  clientOptions: ClientOptions;
31
40
  };