@aifabrix/builder 2.6.1 → 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/cli.js +6 -2
- package/lib/config.js +25 -36
- package/lib/utils/api.js +19 -3
- package/lib/utils/token-manager.js +57 -21
- package/package.json +1 -1
package/lib/cli.js
CHANGED
|
@@ -437,8 +437,12 @@ function setupCommands(program) {
|
|
|
437
437
|
});
|
|
438
438
|
|
|
439
439
|
// Secrets management commands
|
|
440
|
-
program
|
|
441
|
-
.command('secrets
|
|
440
|
+
const secretsCmd = program
|
|
441
|
+
.command('secrets')
|
|
442
|
+
.description('Manage secrets in secrets files');
|
|
443
|
+
|
|
444
|
+
secretsCmd
|
|
445
|
+
.command('set <key> <value>')
|
|
442
446
|
.description('Set a secret value in secrets file')
|
|
443
447
|
.option('--shared', 'Save to general secrets file (from config.yaml aifabrix-secrets) instead of user secrets')
|
|
444
448
|
.action(async(key, value, options) => {
|
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
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
208
|
+
try {
|
|
209
|
+
// Call API refresh endpoint
|
|
210
|
+
const tokenResponse = await apiRefreshDeviceToken(controllerUrl, refreshToken);
|
|
199
211
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
|
|
217
|
+
// Save new token and refresh token to config
|
|
218
|
+
await config.saveDeviceToken(controllerUrl, token, newRefreshToken, expiresAt);
|
|
207
219
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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
|
|
252
|
-
|
|
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,
|