@aifabrix/builder 2.6.2 → 2.6.3

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/lib/config.js CHANGED
@@ -226,13 +226,23 @@ async function setCurrentEnvironment(environment) {
226
226
  * @returns {boolean} True if token is expired
227
227
  */
228
228
  function isTokenExpired(expiresAt) {
229
- if (!expiresAt) {
230
- return true;
231
- }
229
+ if (!expiresAt) return true;
232
230
  const expirationTime = new Date(expiresAt).getTime();
233
231
  const now = Date.now();
234
- // Add 5 minute buffer to refresh before actual expiration
235
- return now >= (expirationTime - 5 * 60 * 1000);
232
+ return now >= (expirationTime - 5 * 60 * 1000); // 5 minute buffer
233
+ }
234
+
235
+ /**
236
+ * Check if token should be refreshed proactively (within 15 minutes of expiry)
237
+ * Helps keep Keycloak sessions alive by refreshing before SSO Session Idle timeout (30 minutes)
238
+ * @param {string} expiresAt - ISO timestamp string
239
+ * @returns {boolean} True if token should be refreshed proactively
240
+ */
241
+ function shouldRefreshToken(expiresAt) {
242
+ if (!expiresAt) return true;
243
+ const expirationTime = new Date(expiresAt).getTime();
244
+ const now = Date.now();
245
+ return now >= (expirationTime - 15 * 60 * 1000); // 15 minutes buffer
236
246
  }
237
247
 
238
248
  /**
@@ -242,9 +252,7 @@ function isTokenExpired(expiresAt) {
242
252
  */
243
253
  async function getDeviceToken(controllerUrl) {
244
254
  const config = await getConfig();
245
- if (!config.device || !config.device[controllerUrl]) {
246
- return null;
247
- }
255
+ if (!config.device || !config.device[controllerUrl]) return null;
248
256
  const deviceToken = config.device[controllerUrl];
249
257
  return {
250
258
  controller: controllerUrl,
@@ -262,12 +270,8 @@ async function getDeviceToken(controllerUrl) {
262
270
  */
263
271
  async function getClientToken(environment, appName) {
264
272
  const config = await getConfig();
265
- if (!config.environments || !config.environments[environment]) {
266
- return null;
267
- }
268
- if (!config.environments[environment].clients || !config.environments[environment].clients[appName]) {
269
- return null;
270
- }
273
+ if (!config.environments || !config.environments[environment]) return null;
274
+ if (!config.environments[environment].clients || !config.environments[environment].clients[appName]) return null;
271
275
  return config.environments[environment].clients[appName];
272
276
  }
273
277
 
@@ -281,14 +285,8 @@ async function getClientToken(environment, appName) {
281
285
  */
282
286
  async function saveDeviceToken(controllerUrl, token, refreshToken, expiresAt) {
283
287
  const config = await getConfig();
284
- if (!config.device) {
285
- config.device = {};
286
- }
287
- config.device[controllerUrl] = {
288
- token: token,
289
- refreshToken: refreshToken,
290
- expiresAt: expiresAt
291
- };
288
+ if (!config.device) config.device = {};
289
+ config.device[controllerUrl] = { token, refreshToken, expiresAt };
292
290
  await saveConfig(config);
293
291
  }
294
292
 
@@ -303,20 +301,10 @@ async function saveDeviceToken(controllerUrl, token, refreshToken, expiresAt) {
303
301
  */
304
302
  async function saveClientToken(environment, appName, controllerUrl, token, expiresAt) {
305
303
  const config = await getConfig();
306
- if (!config.environments) {
307
- config.environments = {};
308
- }
309
- if (!config.environments[environment]) {
310
- config.environments[environment] = { clients: {} };
311
- }
312
- if (!config.environments[environment].clients) {
313
- config.environments[environment].clients = {};
314
- }
315
- config.environments[environment].clients[appName] = {
316
- controller: controllerUrl,
317
- token: token,
318
- expiresAt: expiresAt
319
- };
304
+ if (!config.environments) config.environments = {};
305
+ if (!config.environments[environment]) config.environments[environment] = { clients: {} };
306
+ if (!config.environments[environment].clients) config.environments[environment].clients = {};
307
+ config.environments[environment].clients[appName] = { controller: controllerUrl, token, expiresAt };
320
308
  await saveConfig(config);
321
309
  }
322
310
 
@@ -468,6 +456,7 @@ const exportsObj = {
468
456
  getCurrentEnvironment,
469
457
  setCurrentEnvironment,
470
458
  isTokenExpired,
459
+ shouldRefreshToken,
471
460
  getDeviceToken,
472
461
  getClientToken,
473
462
  saveDeviceToken,
package/lib/utils/api.js CHANGED
@@ -236,14 +236,30 @@ async function authenticatedApiCall(url, options = {}, token) {
236
236
  if (refreshedToken && refreshedToken.token) {
237
237
  // Retry request with new token
238
238
  headers['Authorization'] = `Bearer ${refreshedToken.token}`;
239
- return makeApiCall(url, {
239
+ const retryResponse = await makeApiCall(url, {
240
240
  ...options,
241
241
  headers
242
242
  });
243
+ return retryResponse;
244
+ }
245
+
246
+ // Token refresh failed or no refresh token available
247
+ // Return a more helpful error message
248
+ if (!refreshedToken) {
249
+ return {
250
+ ...response,
251
+ error: 'Authentication failed: Token expired and refresh failed. Please login again using: aifabrix login',
252
+ formattedError: 'Authentication failed: Token expired and refresh failed. Please login again using: aifabrix login'
253
+ };
243
254
  }
244
255
  } catch (refreshError) {
245
- // Refresh failed, return original 401 error
246
- // This allows the caller to handle the authentication error
256
+ // Refresh failed, return original 401 error with additional context
257
+ const errorMessage = refreshError.message || String(refreshError);
258
+ return {
259
+ ...response,
260
+ error: `Authentication failed: ${errorMessage}. Please login again using: aifabrix login`,
261
+ formattedError: `Authentication failed: ${errorMessage}. Please login again using: aifabrix login`
262
+ };
247
263
  }
248
264
  }
249
265
 
@@ -86,6 +86,17 @@ function isTokenExpired(expiresAt) {
86
86
  return config.isTokenExpired(expiresAt);
87
87
  }
88
88
 
89
+ /**
90
+ * Check if token should be refreshed proactively
91
+ * Returns true if token is within 15 minutes of expiry
92
+ * This helps keep Keycloak sessions alive by refreshing before the SSO Session Idle timeout (30 minutes)
93
+ * @param {string} expiresAt - ISO timestamp string
94
+ * @returns {boolean} True if token should be refreshed proactively
95
+ */
96
+ function shouldRefreshToken(expiresAt) {
97
+ return config.shouldRefreshToken(expiresAt);
98
+ }
99
+
89
100
  /**
90
101
  * Refresh client token using credentials from secrets.local.yaml
91
102
  * Gets new token from API and saves it to config.yaml
@@ -184,7 +195,7 @@ async function getOrRefreshClientToken(environment, appName, controllerUrl) {
184
195
  * @param {string} controllerUrl - Controller URL
185
196
  * @param {string} refreshToken - Refresh token
186
197
  * @returns {Promise<{token: string, refreshToken: string, expiresAt: string}>} New token info
187
- * @throws {Error} If refresh fails
198
+ * @throws {Error} If refresh fails or refresh token is expired/invalid
188
199
  */
189
200
  async function refreshDeviceToken(controllerUrl, refreshToken) {
190
201
  if (!controllerUrl || typeof controllerUrl !== 'string') {
@@ -194,27 +205,41 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
194
205
  throw new Error('Refresh token is required');
195
206
  }
196
207
 
197
- // Call API refresh endpoint
198
- const tokenResponse = await apiRefreshDeviceToken(controllerUrl, refreshToken);
208
+ try {
209
+ // Call API refresh endpoint
210
+ const tokenResponse = await apiRefreshDeviceToken(controllerUrl, refreshToken);
199
211
 
200
- const token = tokenResponse.access_token;
201
- const newRefreshToken = tokenResponse.refresh_token || refreshToken; // Use new refresh token if provided, otherwise keep old one
202
- const expiresIn = tokenResponse.expires_in || 3600;
203
- const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
212
+ const token = tokenResponse.access_token;
213
+ const newRefreshToken = tokenResponse.refresh_token || refreshToken; // Use new refresh token if provided, otherwise keep old one
214
+ const expiresIn = tokenResponse.expires_in || 3600;
215
+ const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
204
216
 
205
- // Save new token and refresh token to config
206
- await config.saveDeviceToken(controllerUrl, token, newRefreshToken, expiresAt);
217
+ // Save new token and refresh token to config
218
+ await config.saveDeviceToken(controllerUrl, token, newRefreshToken, expiresAt);
207
219
 
208
- return {
209
- token,
210
- refreshToken: newRefreshToken,
211
- expiresAt
212
- };
220
+ return {
221
+ token,
222
+ refreshToken: newRefreshToken,
223
+ expiresAt
224
+ };
225
+ } catch (error) {
226
+ // Check if error indicates refresh token expiry (case-insensitive)
227
+ const errorMessage = (error.message || String(error)).toLowerCase();
228
+ if (errorMessage.includes('expired') ||
229
+ errorMessage.includes('invalid') ||
230
+ errorMessage.includes('401') ||
231
+ errorMessage.includes('unauthorized')) {
232
+ throw new Error('Refresh token has expired. Please login again using: aifabrix login');
233
+ }
234
+ // Re-throw other errors as-is
235
+ throw error;
236
+ }
213
237
  }
214
238
 
215
239
  /**
216
240
  * Get or refresh device token for controller
217
- * Checks if token exists and is valid, refreshes if expired using refresh token
241
+ * Checks if token exists and is valid, refreshes proactively if within 15 minutes of expiry
242
+ * This helps keep Keycloak sessions alive by refreshing before the SSO Session Idle timeout (30 minutes)
218
243
  * @param {string} controllerUrl - Controller URL
219
244
  * @returns {Promise<{token: string, controller: string}|null>} Token and controller URL, or null if not available
220
245
  */
@@ -226,18 +251,23 @@ async function getOrRefreshDeviceToken(controllerUrl) {
226
251
  return null;
227
252
  }
228
253
 
229
- // Check if token is expired
230
- if (!isTokenExpired(tokenInfo.expiresAt)) {
231
- // Token is valid
254
+ // Check if token should be refreshed proactively (within 15 minutes of expiry)
255
+ // This ensures we refresh before Keycloak's SSO Session Idle timeout (30 minutes)
256
+ const needsRefresh = shouldRefreshToken(tokenInfo.expiresAt);
257
+
258
+ if (!needsRefresh) {
259
+ // Token is valid and doesn't need refresh yet
232
260
  return {
233
261
  token: tokenInfo.token,
234
262
  controller: tokenInfo.controller
235
263
  };
236
264
  }
237
265
 
238
- // Token is expired, try to refresh if refresh token exists
266
+ // Token needs refresh (expired or within 15 minutes of expiry)
267
+ // Try to refresh if refresh token exists
239
268
  if (!tokenInfo.refreshToken) {
240
269
  // No refresh token available
270
+ logger.warn('Access token expired and no refresh token available. Please login again using: aifabrix login');
241
271
  return null;
242
272
  }
243
273
 
@@ -248,8 +278,13 @@ async function getOrRefreshDeviceToken(controllerUrl) {
248
278
  controller: controllerUrl
249
279
  };
250
280
  } catch (error) {
251
- // Refresh failed, return null
252
- logger.warn(`Failed to refresh device token: ${error.message}`);
281
+ // Refresh failed - check if it's a refresh token expiry
282
+ const errorMessage = error.message || String(error);
283
+ if (errorMessage.includes('Refresh token has expired')) {
284
+ logger.warn(`Refresh token expired: ${errorMessage}`);
285
+ } else {
286
+ logger.warn(`Failed to refresh device token: ${errorMessage}`);
287
+ }
253
288
  return null;
254
289
  }
255
290
  }
@@ -370,6 +405,7 @@ module.exports = {
370
405
  getDeviceToken,
371
406
  getClientToken,
372
407
  isTokenExpired,
408
+ shouldRefreshToken,
373
409
  refreshClientToken,
374
410
  refreshDeviceToken,
375
411
  loadClientCredentials,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.6.2",
3
+ "version": "2.6.3",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {