@aifabrix/builder 2.21.0 → 2.22.0

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.
@@ -10,62 +10,126 @@
10
10
 
11
11
  const chalk = require('chalk');
12
12
  const logger = require('./logger');
13
- const { getConfig } = require('../config');
13
+ const { getConfig, normalizeControllerUrl } = require('../config');
14
14
  const { getOrRefreshDeviceToken } = require('./token-manager');
15
+ const { formatAuthenticationError } = require('./error-formatters/http-status-errors');
16
+
17
+ /**
18
+ * Display authentication error and exit
19
+ * Uses the centralized error formatter for consistent error messages
20
+ * @param {Error|null} [error] - Optional error object
21
+ * @param {Object|string} [controllerUrlOrData] - Optional controller URL string or error data object
22
+ */
23
+ function displayAuthenticationError(error = null, controllerUrlOrData = null) {
24
+ // Build error data object for the formatter
25
+ let errorData;
26
+ if (typeof controllerUrlOrData === 'object' && controllerUrlOrData !== null) {
27
+ // If it's an object, use it directly (may contain attemptedUrls, etc.)
28
+ errorData = {
29
+ message: error ? error.message : controllerUrlOrData.message,
30
+ controllerUrl: controllerUrlOrData.controllerUrl || undefined,
31
+ attemptedUrls: controllerUrlOrData.attemptedUrls || undefined,
32
+ correlationId: controllerUrlOrData.correlationId || undefined
33
+ };
34
+ } else {
35
+ // If it's a string or null, treat as controllerUrl
36
+ errorData = {
37
+ message: error ? error.message : undefined,
38
+ controllerUrl: controllerUrlOrData || undefined,
39
+ correlationId: undefined
40
+ };
41
+ }
42
+
43
+ // Use centralized formatter (it will include controller URL in the command)
44
+ const formattedError = formatAuthenticationError(errorData);
45
+ logger.error(formattedError);
46
+
47
+ process.exit(1);
48
+ }
15
49
 
16
50
  /**
17
51
  * Check if user is authenticated and get token
18
52
  * @async
19
- * @param {string} [controllerUrl] - Optional controller URL from variables.yaml
53
+ * @param {string} [controllerUrl] - Optional controller URL from variables.yaml or --controller flag
20
54
  * @param {string} [environment] - Optional environment key
21
- * @returns {Promise<{apiUrl: string, token: string}>} Configuration with API URL and token
55
+ * @returns {Promise<{apiUrl: string, token: string, controllerUrl: string}>} Configuration with API URL, token, and controller URL
22
56
  */
23
57
  async function checkAuthentication(controllerUrl, environment) {
24
- const config = await getConfig();
58
+ try {
59
+ const config = await getConfig();
25
60
 
26
- // Try to get controller URL from parameter, config, or device tokens
27
- let finalControllerUrl = controllerUrl;
28
- let token = null;
61
+ // Try to get controller URL from parameter, config, or device tokens
62
+ // Handle empty string as falsy (treat same as undefined/null)
63
+ const normalizedControllerUrl = (controllerUrl && controllerUrl.trim()) ? normalizeControllerUrl(controllerUrl) : null;
64
+ let finalControllerUrl = normalizedControllerUrl;
65
+ let token = null;
66
+ let lastError = null;
67
+ const attemptedUrls = []; // Track all attempted URLs
29
68
 
30
- // If controller URL provided, try to get device token
31
- if (finalControllerUrl) {
32
- const deviceToken = await getOrRefreshDeviceToken(finalControllerUrl);
33
- if (deviceToken && deviceToken.token) {
34
- token = deviceToken.token;
35
- finalControllerUrl = deviceToken.controller;
69
+ // If controller URL provided, try to get device token
70
+ if (finalControllerUrl) {
71
+ attemptedUrls.push(finalControllerUrl);
72
+ try {
73
+ const deviceToken = await getOrRefreshDeviceToken(finalControllerUrl);
74
+ if (deviceToken && deviceToken.token) {
75
+ token = deviceToken.token;
76
+ finalControllerUrl = deviceToken.controller || finalControllerUrl;
77
+ }
78
+ } catch (error) {
79
+ lastError = error;
80
+ logger.warn(chalk.yellow(`⚠️ Failed to get token for controller ${finalControllerUrl}: ${error.message}`));
81
+ }
36
82
  }
37
- }
38
83
 
39
- // If no token yet, try to find any device token in config
40
- if (!token && config.device) {
41
- const deviceUrls = Object.keys(config.device);
42
- if (deviceUrls.length > 0) {
43
- // Use first available device token
44
- finalControllerUrl = deviceUrls[0];
45
- const deviceToken = await getOrRefreshDeviceToken(finalControllerUrl);
46
- if (deviceToken && deviceToken.token) {
47
- token = deviceToken.token;
48
- finalControllerUrl = deviceToken.controller;
84
+ // If no token yet, try to find any device token in config
85
+ if (!token && config.device) {
86
+ const deviceUrls = Object.keys(config.device);
87
+ if (deviceUrls.length > 0) {
88
+ // Try each device token until we find a valid one
89
+ for (const storedUrl of deviceUrls) {
90
+ attemptedUrls.push(storedUrl);
91
+ try {
92
+ const normalizedStoredUrl = normalizeControllerUrl(storedUrl);
93
+ const deviceToken = await getOrRefreshDeviceToken(normalizedStoredUrl);
94
+ if (deviceToken && deviceToken.token) {
95
+ token = deviceToken.token;
96
+ finalControllerUrl = deviceToken.controller || normalizedStoredUrl;
97
+ break;
98
+ }
99
+ } catch (error) {
100
+ lastError = error;
101
+ // Continue to next URL
102
+ }
103
+ }
49
104
  }
50
105
  }
51
- }
52
106
 
53
- // If still no token, check for client token (requires environment and app)
54
- if (!token && environment) {
55
- // For app register, we don't have an app yet, so client tokens won't work
56
- // This is expected - device tokens should be used for registration
57
- }
107
+ // If still no token, check for client token (requires environment and app)
108
+ if (!token && environment) {
109
+ // For app register, we don't have an app yet, so client tokens won't work
110
+ // This is expected - device tokens should be used for registration
111
+ }
58
112
 
59
- if (!token || !finalControllerUrl) {
60
- logger.error(chalk.red('❌ Not logged in. Run: aifabrix login'));
61
- logger.error(chalk.gray(' Use device code flow: aifabrix login --method device --controller <url>'));
62
- process.exit(1);
63
- }
113
+ // If no token found, display error with attempted URLs
114
+ if (!token || !finalControllerUrl) {
115
+ const errorData = {
116
+ message: lastError ? lastError.message : 'No valid authentication found',
117
+ controllerUrl: controllerUrl || (attemptedUrls.length > 0 ? attemptedUrls[0] : undefined),
118
+ attemptedUrls: attemptedUrls.length > 1 ? attemptedUrls : undefined,
119
+ correlationId: undefined
120
+ };
121
+ displayAuthenticationError(lastError, errorData);
122
+ }
64
123
 
65
- return {
66
- apiUrl: finalControllerUrl,
67
- token: token
68
- };
124
+ return {
125
+ apiUrl: finalControllerUrl,
126
+ token: token,
127
+ controllerUrl: finalControllerUrl // Return the actual URL used
128
+ };
129
+ } catch (error) {
130
+ // Handle any unexpected errors during authentication check
131
+ displayAuthenticationError(error, { controllerUrl: controllerUrl });
132
+ }
69
133
  }
70
134
 
71
135
  module.exports = { checkAuthentication };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * AI Fabrix Builder - Configuration Path Utilities
3
+ *
4
+ * Helper functions for managing path configuration in config.yaml
5
+ *
6
+ * @fileoverview Path configuration utilities for config management
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ /**
12
+ * Get path configuration value
13
+ * @async
14
+ * @param {Function} getConfigFn - Function to get config
15
+ * @param {string} key - Configuration key
16
+ * @returns {Promise<string|null>} Path value or null
17
+ */
18
+ async function getPathConfig(getConfigFn, key) {
19
+ const config = await getConfigFn();
20
+ return config[key] || null;
21
+ }
22
+
23
+ /**
24
+ * Set path configuration value
25
+ * @async
26
+ * @param {Function} getConfigFn - Function to get config
27
+ * @param {Function} saveConfigFn - Function to save config
28
+ * @param {string} key - Configuration key
29
+ * @param {string} value - Path value
30
+ * @param {string} errorMsg - Error message if validation fails
31
+ * @returns {Promise<void>}
32
+ */
33
+ async function setPathConfig(getConfigFn, saveConfigFn, key, value, errorMsg) {
34
+ if (!value || typeof value !== 'string') {
35
+ throw new Error(errorMsg);
36
+ }
37
+ const config = await getConfigFn();
38
+ config[key] = value;
39
+ await saveConfigFn(config);
40
+ }
41
+
42
+ /**
43
+ * Create path configuration functions with config access
44
+ * @param {Function} getConfigFn - Function to get config
45
+ * @param {Function} saveConfigFn - Function to save config
46
+ * @returns {Object} Path configuration functions
47
+ */
48
+ function createPathConfigFunctions(getConfigFn, saveConfigFn) {
49
+ return {
50
+ /**
51
+ * Get aifabrix-home override path
52
+ * @async
53
+ * @returns {Promise<string|null>} Home path or null
54
+ */
55
+ async getAifabrixHomeOverride() {
56
+ return getPathConfig(getConfigFn, 'aifabrix-home');
57
+ },
58
+
59
+ /**
60
+ * Set aifabrix-home override path
61
+ * @async
62
+ * @param {string} homePath - Home path
63
+ * @returns {Promise<void>}
64
+ */
65
+ async setAifabrixHomeOverride(homePath) {
66
+ await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-home', homePath, 'Home path is required and must be a string');
67
+ },
68
+
69
+ /**
70
+ * Get aifabrix-secrets path
71
+ * @async
72
+ * @returns {Promise<string|null>} Secrets path or null
73
+ */
74
+ async getAifabrixSecretsPath() {
75
+ return getPathConfig(getConfigFn, 'aifabrix-secrets');
76
+ },
77
+
78
+ /**
79
+ * Set aifabrix-secrets path
80
+ * @async
81
+ * @param {string} secretsPath - Secrets path
82
+ * @returns {Promise<void>}
83
+ */
84
+ async setAifabrixSecretsPath(secretsPath) {
85
+ await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-secrets', secretsPath, 'Secrets path is required and must be a string');
86
+ },
87
+
88
+ /**
89
+ * Get aifabrix-env-config path
90
+ * @async
91
+ * @returns {Promise<string|null>} Env config path or null
92
+ */
93
+ async getAifabrixEnvConfigPath() {
94
+ return getPathConfig(getConfigFn, 'aifabrix-env-config');
95
+ },
96
+
97
+ /**
98
+ * Set aifabrix-env-config path
99
+ * @async
100
+ * @param {string} envConfigPath - Env config path
101
+ * @returns {Promise<void>}
102
+ */
103
+ async setAifabrixEnvConfigPath(envConfigPath) {
104
+ await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-env-config', envConfigPath, 'Env config path is required and must be a string');
105
+ }
106
+ };
107
+ }
108
+
109
+ module.exports = {
110
+ createPathConfigFunctions
111
+ };
112
+
@@ -0,0 +1,233 @@
1
+ /**
2
+ * AI Fabrix Builder - Configuration Token Management
3
+ *
4
+ * Token management functions for device and client tokens in config.yaml
5
+ *
6
+ * @fileoverview Token management utilities for config
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ /**
12
+ * Normalize controller URL for consistent storage and lookup
13
+ * Removes trailing slashes and normalizes the URL format
14
+ * @param {string} url - Controller URL to normalize
15
+ * @returns {string} Normalized controller URL
16
+ */
17
+ function normalizeControllerUrl(url) {
18
+ if (!url || typeof url !== 'string') {
19
+ return url;
20
+ }
21
+ // Remove trailing slashes
22
+ let normalized = url.trim().replace(/\/+$/, '');
23
+ // Ensure it starts with http:// or https://
24
+ if (!normalized.match(/^https?:\/\//)) {
25
+ // If it doesn't start with protocol, assume http://
26
+ normalized = `http://${normalized}`;
27
+ }
28
+ return normalized;
29
+ }
30
+
31
+ /**
32
+ * Create token management functions with config access
33
+ * @param {Function} getConfigFn - Function to get config
34
+ * @param {Function} saveConfigFn - Function to save config
35
+ * @param {Function} getSecretsEncryptionKeyFn - Function to get encryption key
36
+ * @param {Function} encryptTokenValueFn - Function to encrypt token
37
+ * @param {Function} decryptTokenValueFn - Function to decrypt token
38
+ * @param {Function} isTokenEncryptedFn - Function to check if token is encrypted
39
+ * @returns {Object} Token management functions
40
+ */
41
+ function createTokenManagementFunctions(
42
+ getConfigFn,
43
+ saveConfigFn,
44
+ getSecretsEncryptionKeyFn,
45
+ encryptTokenValueFn,
46
+ decryptTokenValueFn,
47
+ isTokenEncryptedFn
48
+ ) {
49
+ /**
50
+ * Extract device token information with encryption/decryption handling
51
+ * @param {Object} deviceToken - Device token object from config
52
+ * @param {string} controllerUrl - Controller URL
53
+ * @returns {Promise<{controller: string, token: string, refreshToken: string, expiresAt: string}>} Device token info
54
+ */
55
+ async function extractDeviceTokenInfo(deviceToken, controllerUrl) {
56
+ // Migration: If tokens are plain text and encryption key exists, encrypt them first
57
+ const encryptionKey = await getSecretsEncryptionKeyFn();
58
+ if (encryptionKey) {
59
+ let needsSave = false;
60
+
61
+ if (deviceToken.token && !isTokenEncryptedFn(deviceToken.token)) {
62
+ // Token is plain text, encrypt it
63
+ deviceToken.token = await encryptTokenValueFn(deviceToken.token);
64
+ needsSave = true;
65
+ }
66
+
67
+ if (deviceToken.refreshToken && !isTokenEncryptedFn(deviceToken.refreshToken)) {
68
+ // Refresh token is plain text, encrypt it
69
+ deviceToken.refreshToken = await encryptTokenValueFn(deviceToken.refreshToken);
70
+ needsSave = true;
71
+ }
72
+
73
+ if (needsSave) {
74
+ // Save encrypted tokens back to config
75
+ const config = await getConfigFn();
76
+ await saveConfigFn(config);
77
+ }
78
+ }
79
+ // Decrypt tokens if encrypted (for return value)
80
+ const token = deviceToken.token ? await decryptTokenValueFn(deviceToken.token) : undefined;
81
+ const refreshToken = deviceToken.refreshToken ? await decryptTokenValueFn(deviceToken.refreshToken) : null;
82
+
83
+ return {
84
+ controller: controllerUrl,
85
+ token: token,
86
+ refreshToken: refreshToken,
87
+ expiresAt: deviceToken.expiresAt
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Get device token for controller
93
+ * @param {string} controllerUrl - Controller URL
94
+ * @returns {Promise<{controller: string, token: string, refreshToken: string, expiresAt: string}|null>} Device token info or null
95
+ */
96
+ async function getDeviceToken(controllerUrl) {
97
+ const config = await getConfigFn();
98
+ if (!controllerUrl) return null;
99
+
100
+ // Normalize URL for consistent lookup
101
+ const normalizedUrl = normalizeControllerUrl(controllerUrl);
102
+
103
+ // Try exact match first
104
+ if (config.device && config.device[normalizedUrl]) {
105
+ const deviceToken = config.device[normalizedUrl];
106
+ return extractDeviceTokenInfo(deviceToken, normalizedUrl);
107
+ }
108
+
109
+ // Try to find matching URL by normalizing all stored URLs
110
+ if (config.device) {
111
+ for (const storedUrl of Object.keys(config.device)) {
112
+ if (normalizeControllerUrl(storedUrl) === normalizedUrl) {
113
+ const deviceToken = config.device[storedUrl];
114
+ // Migrate to normalized URL if different
115
+ if (storedUrl !== normalizedUrl) {
116
+ config.device[normalizedUrl] = deviceToken;
117
+ delete config.device[storedUrl];
118
+ await saveConfigFn(config);
119
+ }
120
+ return extractDeviceTokenInfo(deviceToken, normalizedUrl);
121
+ }
122
+ }
123
+ }
124
+
125
+ return null;
126
+ }
127
+
128
+ /**
129
+ * Save device token for controller (root level)
130
+ * @param {string} controllerUrl - Controller URL (used as key)
131
+ * @param {string} token - Device access token
132
+ * @param {string} refreshToken - Refresh token for token renewal
133
+ * @param {string} expiresAt - ISO timestamp string
134
+ * @returns {Promise<void>}
135
+ */
136
+ async function saveDeviceToken(controllerUrl, token, refreshToken, expiresAt) {
137
+ const config = await getConfigFn();
138
+ if (!config.device) config.device = {};
139
+
140
+ // Normalize URL for consistent storage
141
+ const normalizedUrl = normalizeControllerUrl(controllerUrl);
142
+
143
+ // If there's an existing entry with a different URL format, remove it
144
+ if (config.device) {
145
+ for (const storedUrl of Object.keys(config.device)) {
146
+ if (normalizeControllerUrl(storedUrl) === normalizedUrl && storedUrl !== normalizedUrl) {
147
+ delete config.device[storedUrl];
148
+ }
149
+ }
150
+ }
151
+
152
+ // Encrypt tokens before saving
153
+ const encryptedToken = await encryptTokenValueFn(token);
154
+ const encryptedRefreshToken = refreshToken ? await encryptTokenValueFn(refreshToken) : null;
155
+
156
+ config.device[normalizedUrl] = {
157
+ token: encryptedToken,
158
+ refreshToken: encryptedRefreshToken,
159
+ expiresAt
160
+ };
161
+ await saveConfigFn(config);
162
+ }
163
+
164
+ /**
165
+ * Get client token for environment and app
166
+ * @param {string} environment - Environment key
167
+ * @param {string} appName - Application name
168
+ * @returns {Promise<{controller: string, token: string, expiresAt: string}|null>} Client token info or null
169
+ */
170
+ async function getClientToken(environment, appName) {
171
+ const config = await getConfigFn();
172
+ if (!config.environments || !config.environments[environment]) return null;
173
+ if (!config.environments[environment].clients || !config.environments[environment].clients[appName]) return null;
174
+
175
+ const clientToken = config.environments[environment].clients[appName];
176
+
177
+ // Migration: If token is plain text and encryption key exists, encrypt it first
178
+ const encryptionKey = await getSecretsEncryptionKeyFn();
179
+ if (encryptionKey && clientToken.token && !isTokenEncryptedFn(clientToken.token)) {
180
+ // Token is plain text, encrypt it
181
+ clientToken.token = await encryptTokenValueFn(clientToken.token);
182
+ // Save encrypted token back to config
183
+ await saveConfigFn(config);
184
+ }
185
+
186
+ // Decrypt token if encrypted (for return value)
187
+ const token = await decryptTokenValueFn(clientToken.token);
188
+
189
+ return {
190
+ controller: clientToken.controller,
191
+ token: token,
192
+ expiresAt: clientToken.expiresAt
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Save client token for environment and app
198
+ * @param {string} environment - Environment key
199
+ * @param {string} appName - Application name
200
+ * @param {string} controllerUrl - Controller URL
201
+ * @param {string} token - Client token
202
+ * @param {string} expiresAt - ISO timestamp string
203
+ * @returns {Promise<void>}
204
+ */
205
+ async function saveClientToken(environment, appName, controllerUrl, token, expiresAt) {
206
+ const config = await getConfigFn();
207
+ if (!config.environments) config.environments = {};
208
+ if (!config.environments[environment]) config.environments[environment] = { clients: {} };
209
+ if (!config.environments[environment].clients) config.environments[environment].clients = {};
210
+
211
+ // Encrypt token before saving
212
+ const encryptedToken = await encryptTokenValueFn(token);
213
+
214
+ config.environments[environment].clients[appName] = {
215
+ controller: controllerUrl,
216
+ token: encryptedToken,
217
+ expiresAt
218
+ };
219
+ await saveConfigFn(config);
220
+ }
221
+
222
+ return {
223
+ getDeviceToken,
224
+ getClientToken,
225
+ saveDeviceToken,
226
+ saveClientToken
227
+ };
228
+ }
229
+
230
+ module.exports = {
231
+ createTokenManagementFunctions
232
+ };
233
+
@@ -213,7 +213,9 @@ function handlePollingErrors(error, status, response) {
213
213
  }
214
214
 
215
215
  // Handle validation errors with detailed message
216
- if (error === 'validation_error' || status === 400) {
216
+ // Check for validation_error, status 400, or specific validation error codes
217
+ if (error === 'validation_error' || status === 400 ||
218
+ error === 'INVALID_TOKEN' || error === 'INVALID_ACCESS_TOKEN') {
217
219
  throw createValidationError(response);
218
220
  }
219
221
 
@@ -245,14 +247,26 @@ function extractPollingError(response) {
245
247
  if (response.errorType === 'validation') {
246
248
  return 'validation_error';
247
249
  }
250
+ // Check if error code indicates validation error (e.g., INVALID_TOKEN)
251
+ const errorCode = errorData.error || errorData.code || response.error;
252
+ if (errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN') {
253
+ return 'validation_error';
254
+ }
248
255
  // Return the error message from structured error
249
- return errorData.detail || errorData.title || errorData.message || errorData.error || response.error || 'Unknown error';
256
+ return errorData.detail || errorData.title || errorData.message || errorCode || response.error || 'Unknown error';
250
257
  }
251
258
 
252
259
  // Fallback to original extraction logic
253
260
  const apiResponse = response.data || {};
254
261
  const errorData = typeof apiResponse === 'object' ? apiResponse : {};
255
- return errorData.error || response.error || 'Unknown error';
262
+ const errorCode = errorData.error || response.error || 'Unknown error';
263
+
264
+ // Check if error code indicates validation error (e.g., INVALID_TOKEN)
265
+ if (errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN') {
266
+ return 'validation_error';
267
+ }
268
+
269
+ return errorCode;
256
270
  }
257
271
 
258
272
  /**
@@ -278,14 +292,22 @@ function handleSuccessfulPoll(response) {
278
292
  */
279
293
  async function processPollingResponse(response, interval) {
280
294
  if (response.success) {
295
+ // Check if response contains an error code even though success is true
296
+ const apiResponse = response.data || {};
297
+ const responseData = apiResponse.data || apiResponse;
298
+ const errorCode = responseData.error || apiResponse.error || response.error;
299
+
300
+ // If there's an error code like INVALID_TOKEN, treat it as a validation error
301
+ if (errorCode && (errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN')) {
302
+ throw createValidationError(response);
303
+ }
304
+
281
305
  const tokenResponse = handleSuccessfulPoll(response);
282
306
  if (tokenResponse) {
283
307
  return tokenResponse;
284
308
  }
285
309
 
286
- const apiResponse = response.data;
287
- const responseData = apiResponse.data || apiResponse;
288
- const error = responseData.error || apiResponse.error;
310
+ const error = errorCode;
289
311
  const slowDown = error === 'slow_down';
290
312
  await waitForNextPoll(interval, slowDown);
291
313
  return null;