@aifabrix/builder 2.4.0 → 2.5.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.
package/lib/utils/api.js CHANGED
@@ -15,24 +15,151 @@ const auditLogger = require('../audit-logger');
15
15
 
16
16
  /**
17
17
  * Logs API request performance metrics and errors to audit log
18
- * @param {string} url - API endpoint URL
19
- * @param {Object} options - Fetch options
20
- * @param {number} statusCode - HTTP status code
21
- * @param {number} duration - Request duration in milliseconds
22
- * @param {boolean} success - Whether the request was successful
23
- * @param {Object} errorInfo - Error information (if failed)
18
+ * @param {Object} params - Performance logging parameters
19
+ * @param {string} params.url - API endpoint URL
20
+ * @param {Object} params.options - Fetch options
21
+ * @param {number} params.statusCode - HTTP status code
22
+ * @param {number} params.duration - Request duration in milliseconds
23
+ * @param {boolean} params.success - Whether the request was successful
24
+ * @param {Object} [params.errorInfo] - Error information (if failed)
24
25
  */
25
- async function logApiPerformance(url, options, statusCode, duration, success, errorInfo = {}) {
26
+ async function logApiPerformance(params) {
26
27
  // Log all API calls (both success and failure) to audit log for troubleshooting
27
28
  // This helps track what API calls were made when errors occur
28
29
  try {
29
- await auditLogger.logApiCall(url, options, statusCode, duration, success, errorInfo);
30
+ await auditLogger.logApiCall(
31
+ params.url,
32
+ params.options,
33
+ params.statusCode,
34
+ params.duration,
35
+ params.success,
36
+ params.errorInfo || {}
37
+ );
30
38
  } catch (logError) {
31
39
  // Don't fail the API call if audit logging fails
32
40
  // Silently continue - audit logging should never break functionality
33
41
  }
34
42
  }
35
43
 
44
+ /**
45
+ * Parses error response text into error data
46
+ * @param {string} errorText - Error response text
47
+ * @param {number} status - HTTP status code
48
+ * @param {string} statusText - HTTP status text
49
+ * @returns {Object|string} Parsed error data
50
+ */
51
+ function parseErrorText(errorText, status, statusText) {
52
+ try {
53
+ return JSON.parse(errorText);
54
+ } catch {
55
+ return errorText || `HTTP ${status}: ${statusText}`;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Handles error response from API
61
+ * @async
62
+ * @function handleErrorResponse
63
+ * @param {Object} response - Fetch response object
64
+ * @param {string} url - API endpoint URL
65
+ * @param {Object} options - Fetch options
66
+ * @param {number} duration - Request duration
67
+ * @returns {Promise<Object>} Error response object
68
+ */
69
+ async function handleErrorResponse(response, url, options, duration) {
70
+ const errorText = await response.text();
71
+ const errorData = parseErrorText(errorText, response.status, response.statusText);
72
+ const parsedError = parseErrorResponse(errorData, response.status, false);
73
+
74
+ await logApiPerformance({
75
+ url,
76
+ options,
77
+ statusCode: response.status,
78
+ duration,
79
+ success: false,
80
+ errorInfo: {
81
+ errorType: parsedError.type,
82
+ errorMessage: parsedError.message,
83
+ errorData: parsedError.data,
84
+ correlationId: parsedError.data?.correlationId
85
+ }
86
+ });
87
+
88
+ return {
89
+ success: false,
90
+ error: parsedError.message,
91
+ errorData: parsedError.data,
92
+ errorType: parsedError.type,
93
+ formattedError: parsedError.formatted,
94
+ status: response.status
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Handles successful response from API
100
+ * @async
101
+ * @function handleSuccessResponse
102
+ * @param {Object} response - Fetch response object
103
+ * @param {string} url - API endpoint URL
104
+ * @param {Object} options - Fetch options
105
+ * @param {number} duration - Request duration
106
+ * @returns {Promise<Object>} Success response object
107
+ */
108
+ async function handleSuccessResponse(response, url, options, duration) {
109
+ await logApiPerformance({
110
+ url,
111
+ options,
112
+ statusCode: response.status,
113
+ duration,
114
+ success: true
115
+ });
116
+
117
+ const contentType = response.headers.get('content-type');
118
+ if (contentType && contentType.includes('application/json')) {
119
+ const data = await response.json();
120
+ return { success: true, data, status: response.status };
121
+ }
122
+
123
+ const text = await response.text();
124
+ return { success: true, data: text, status: response.status };
125
+ }
126
+
127
+ /**
128
+ * Handles network error from API call
129
+ * @async
130
+ * @function handleNetworkError
131
+ * @param {Error} error - Network error
132
+ * @param {string} url - API endpoint URL
133
+ * @param {Object} options - Fetch options
134
+ * @param {number} duration - Request duration
135
+ * @returns {Promise<Object>} Error response object
136
+ */
137
+ async function handleNetworkError(error, url, options, duration) {
138
+ const parsedError = parseErrorResponse(error.message, 0, true);
139
+
140
+ await logApiPerformance({
141
+ url,
142
+ options,
143
+ statusCode: 0,
144
+ duration,
145
+ success: false,
146
+ errorInfo: {
147
+ errorType: parsedError.type,
148
+ errorMessage: parsedError.message,
149
+ network: true
150
+ }
151
+ });
152
+
153
+ return {
154
+ success: false,
155
+ error: parsedError.message,
156
+ errorData: parsedError.data,
157
+ errorType: parsedError.type,
158
+ formattedError: parsedError.formatted,
159
+ network: true
160
+ };
161
+ }
162
+
36
163
  /**
37
164
  * Make an API call with proper error handling
38
165
  * @param {string} url - API endpoint URL
@@ -47,76 +174,13 @@ async function makeApiCall(url, options = {}) {
47
174
  const duration = Date.now() - startTime;
48
175
 
49
176
  if (!response.ok) {
50
- const errorText = await response.text();
51
- let errorData;
52
- try {
53
- errorData = JSON.parse(errorText);
54
- } catch {
55
- errorData = errorText || `HTTP ${response.status}: ${response.statusText}`;
56
- }
57
-
58
- // Parse error using error handler
59
- const parsedError = parseErrorResponse(errorData, response.status, false);
60
-
61
- // Log error to audit log
62
- await logApiPerformance(url, options, response.status, duration, false, {
63
- errorType: parsedError.type,
64
- errorMessage: parsedError.message,
65
- errorData: parsedError.data,
66
- correlationId: parsedError.data?.correlationId
67
- });
68
-
69
- return {
70
- success: false,
71
- error: parsedError.message,
72
- errorData: parsedError.data,
73
- errorType: parsedError.type,
74
- formattedError: parsedError.formatted,
75
- status: response.status
76
- };
77
- }
78
-
79
- // Log successful API call to audit log
80
- await logApiPerformance(url, options, response.status, duration, true);
81
-
82
- const contentType = response.headers.get('content-type');
83
- if (contentType && contentType.includes('application/json')) {
84
- const data = await response.json();
85
- return {
86
- success: true,
87
- data,
88
- status: response.status
89
- };
177
+ return await handleErrorResponse(response, url, options, duration);
90
178
  }
91
179
 
92
- const text = await response.text();
93
- return {
94
- success: true,
95
- data: text,
96
- status: response.status
97
- };
98
-
180
+ return await handleSuccessResponse(response, url, options, duration);
99
181
  } catch (error) {
100
182
  const duration = Date.now() - startTime;
101
-
102
- // Parse network error using error handler
103
- const parsedError = parseErrorResponse(error.message, 0, true);
104
-
105
- // Log network error to audit log
106
- await logApiPerformance(url, options, 0, duration, false, {
107
- errorType: parsedError.type,
108
- errorMessage: parsedError.message,
109
- network: true
110
- });
111
-
112
- return {
113
- success: false,
114
- error: parsedError.message,
115
- errorData: parsedError.data,
116
- errorType: parsedError.type,
117
- formattedError: parsedError.formatted,
118
- network: true
119
- };
183
+ return await handleNetworkError(error, url, options, duration);
120
184
  }
121
185
  }
122
186
 
@@ -186,286 +250,12 @@ async function authenticatedApiCall(url, options = {}, token) {
186
250
  return response;
187
251
  }
188
252
 
189
- /**
190
- * Parses device code response from API
191
- * Matches OpenAPI DeviceCodeResponse schema (camelCase)
192
- * @function parseDeviceCodeResponse
193
- * @param {Object} response - API response object
194
- * @returns {Object} Parsed device code response
195
- * @throws {Error} If response is invalid
196
- */
197
- function parseDeviceCodeResponse(response) {
198
- // OpenAPI spec: { success: boolean, data: DeviceCodeResponse, timestamp: string }
199
- const apiResponse = response.data;
200
- const responseData = apiResponse.data || apiResponse;
201
-
202
- // OpenAPI spec uses camelCase: deviceCode, userCode, verificationUri, expiresIn, interval
203
- const deviceCode = responseData.deviceCode;
204
- const userCode = responseData.userCode;
205
- const verificationUri = responseData.verificationUri;
206
- const expiresIn = responseData.expiresIn || 600;
207
- const interval = responseData.interval || 5;
208
-
209
- if (!deviceCode || !userCode || !verificationUri) {
210
- throw new Error('Invalid device code response: missing required fields');
211
- }
212
-
213
- // Return in snake_case for internal consistency (used by existing code)
214
- return {
215
- device_code: deviceCode,
216
- user_code: userCode,
217
- verification_uri: verificationUri,
218
- expires_in: expiresIn,
219
- interval: interval
220
- };
221
- }
222
-
223
- /**
224
- * Initiates OAuth2 Device Code Flow
225
- * Calls the device code endpoint to get device_code and user_code
226
- *
227
- * @async
228
- * @function initiateDeviceCodeFlow
229
- * @param {string} controllerUrl - Base URL of the controller
230
- * @param {string} environment - Environment key (e.g., 'miso', 'dev', 'tst', 'pro')
231
- * @returns {Promise<Object>} Device code response with device_code, user_code, verification_uri, expires_in, interval
232
- * @throws {Error} If initiation fails
233
- */
234
- async function initiateDeviceCodeFlow(controllerUrl, environment) {
235
- if (!environment || typeof environment !== 'string') {
236
- throw new Error('Environment key is required');
237
- }
238
-
239
- const url = `${controllerUrl}/api/v1/auth/login?environment=${encodeURIComponent(environment)}`;
240
- const response = await makeApiCall(url, {
241
- method: 'POST',
242
- headers: {
243
- 'Content-Type': 'application/json'
244
- }
245
- });
246
-
247
- if (!response.success) {
248
- throw new Error(`Device code initiation failed: ${response.error || 'Unknown error'}`);
249
- }
250
-
251
- return parseDeviceCodeResponse(response);
252
- }
253
-
254
- /**
255
- * Checks if token has expired based on elapsed time
256
- * @function checkTokenExpiration
257
- * @param {number} startTime - Start time in milliseconds
258
- * @param {number} expiresIn - Expiration time in seconds
259
- * @throws {Error} If token has expired
260
- */
261
- function checkTokenExpiration(startTime, expiresIn) {
262
- const maxWaitTime = (expiresIn + 30) * 1000;
263
- if (Date.now() - startTime > maxWaitTime) {
264
- throw new Error('Device code expired: Maximum polling time exceeded');
265
- }
266
- }
267
-
268
- /**
269
- * Parses token response from API
270
- * Matches OpenAPI DeviceCodeTokenResponse schema (camelCase)
271
- * @function parseTokenResponse
272
- * @param {Object} response - API response object
273
- * @returns {Object|null} Parsed token response or null if pending
274
- */
275
- function parseTokenResponse(response) {
276
- // OpenAPI spec: { success: boolean, data: DeviceCodeTokenResponse, timestamp: string }
277
- const apiResponse = response.data;
278
- const responseData = apiResponse.data || apiResponse;
279
-
280
- const error = responseData.error || apiResponse.error;
281
- if (error === 'authorization_pending' || error === 'slow_down') {
282
- return null;
283
- }
284
-
285
- // OpenAPI spec uses camelCase: accessToken, refreshToken, expiresIn
286
- const accessToken = responseData.accessToken;
287
- const refreshToken = responseData.refreshToken;
288
- const expiresIn = responseData.expiresIn || 3600;
289
-
290
- if (!accessToken) {
291
- throw new Error('Invalid token response: missing accessToken');
292
- }
293
-
294
- // Return in snake_case for internal consistency (used by existing code)
295
- return {
296
- access_token: accessToken,
297
- refresh_token: refreshToken,
298
- expires_in: expiresIn
299
- };
300
- }
301
-
302
- /**
303
- * Handles polling errors
304
- * @function handlePollingErrors
305
- * @param {string} error - Error code
306
- * @param {number} status - HTTP status code
307
- * @throws {Error} For fatal errors
308
- * @returns {boolean} True if should continue polling
309
- */
310
- function handlePollingErrors(error, status) {
311
- if (error === 'authorization_pending' || status === 202) {
312
- return true;
313
- }
314
-
315
- // Check error field first, then status code
316
- if (error === 'authorization_declined') {
317
- throw new Error('Authorization declined: User denied the request');
318
- }
319
-
320
- if (error === 'expired_token' || status === 410) {
321
- throw new Error('Device code expired: Please restart the authentication process');
322
- }
323
-
324
- if (error === 'slow_down') {
325
- return true;
326
- }
327
-
328
- throw new Error(`Token polling failed: ${error}`);
329
- }
330
-
331
- /**
332
- * Waits for next polling interval
333
- * @async
334
- * @function waitForNextPoll
335
- * @param {number} interval - Polling interval in seconds
336
- * @param {boolean} slowDown - Whether to slow down
337
- */
338
- async function waitForNextPoll(interval, slowDown) {
339
- const waitInterval = slowDown ? interval * 2 : interval;
340
- await new Promise(resolve => setTimeout(resolve, waitInterval * 1000));
341
- }
342
-
343
- /**
344
- * Polls for token during Device Code Flow
345
- * Continuously polls the token endpoint until user approves or flow expires
346
- *
347
- * @async
348
- * @function pollDeviceCodeToken
349
- * @param {string} controllerUrl - Base URL of the controller
350
- * @param {string} deviceCode - Device code from initiation
351
- * @param {number} interval - Polling interval in seconds
352
- * @param {number} expiresIn - Expiration time in seconds
353
- * @param {Function} [onPoll] - Optional callback called on each poll attempt
354
- * @returns {Promise<Object>} Token response with access_token, refresh_token, expires_in
355
- * @throws {Error} If polling fails or token is expired/declined
356
- */
357
- async function pollDeviceCodeToken(controllerUrl, deviceCode, interval, expiresIn, onPoll) {
358
- if (!deviceCode || typeof deviceCode !== 'string') {
359
- throw new Error('Device code is required');
360
- }
361
-
362
- const url = `${controllerUrl}/api/v1/auth/login/device/token`;
363
- const startTime = Date.now();
364
-
365
- // eslint-disable-next-line no-constant-condition
366
- while (true) {
367
- checkTokenExpiration(startTime, expiresIn);
368
-
369
- if (onPoll) {
370
- onPoll();
371
- }
372
-
373
- const response = await makeApiCall(url, {
374
- method: 'POST',
375
- headers: {
376
- 'Content-Type': 'application/json'
377
- },
378
- body: JSON.stringify({
379
- deviceCode: deviceCode
380
- })
381
- });
382
-
383
- if (response.success) {
384
- const tokenResponse = parseTokenResponse(response);
385
- if (tokenResponse) {
386
- return tokenResponse;
387
- }
388
-
389
- const apiResponse = response.data;
390
- const responseData = apiResponse.data || apiResponse;
391
- const error = responseData.error || apiResponse.error;
392
- const slowDown = error === 'slow_down';
393
- await waitForNextPoll(interval, slowDown);
394
- continue;
395
- }
396
-
397
- const apiResponse = response.data || {};
398
- const errorData = typeof apiResponse === 'object' ? apiResponse : {};
399
- const error = errorData.error || response.error || 'Unknown error';
400
- const shouldContinue = handlePollingErrors(error, response.status);
401
-
402
- if (shouldContinue) {
403
- const slowDown = error === 'slow_down';
404
- await waitForNextPoll(interval, slowDown);
405
- continue;
406
- }
407
- }
408
- }
409
-
410
- /**
411
- * Displays device code information to the user
412
- * Formats user code and verification URL for easy reading
413
- *
414
- * @function displayDeviceCodeInfo
415
- * @param {string} userCode - User code to display
416
- * @param {string} verificationUri - Verification URL
417
- * @param {Object} logger - Logger instance with log method
418
- * @param {Object} chalk - Chalk instance for colored output
419
- */
420
- function displayDeviceCodeInfo(userCode, verificationUri, logger, chalk) {
421
- logger.log(chalk.cyan('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
422
- logger.log(chalk.cyan(' Device Code Flow Authentication'));
423
- logger.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
424
- logger.log(chalk.yellow('To complete authentication:'));
425
- logger.log(chalk.gray(' 1. Visit: ') + chalk.blue.underline(verificationUri));
426
- logger.log(chalk.gray(' 2. Enter code: ') + chalk.bold.cyan(userCode));
427
- logger.log(chalk.gray(' 3. Approve the request\n'));
428
- logger.log(chalk.gray('Waiting for approval...'));
429
- }
430
-
431
- /**
432
- * Refresh device code access token using refresh token
433
- * Uses OpenAPI /api/v1/auth/login/device/refresh endpoint
434
- *
435
- * @async
436
- * @function refreshDeviceToken
437
- * @param {string} controllerUrl - Base URL of the controller
438
- * @param {string} refreshToken - Refresh token from previous authentication
439
- * @returns {Promise<Object>} Token response with access_token, refresh_token, expires_in
440
- * @throws {Error} If refresh fails or refresh token is invalid/expired
441
- */
442
- async function refreshDeviceToken(controllerUrl, refreshToken) {
443
- if (!refreshToken || typeof refreshToken !== 'string') {
444
- throw new Error('Refresh token is required');
445
- }
446
-
447
- const url = `${controllerUrl}/api/v1/auth/login/device/refresh`;
448
- const response = await makeApiCall(url, {
449
- method: 'POST',
450
- headers: {
451
- 'Content-Type': 'application/json'
452
- },
453
- body: JSON.stringify({ refreshToken })
454
- });
455
-
456
- if (!response.success) {
457
- const errorMsg = response.error || 'Unknown error';
458
- throw new Error(`Failed to refresh token: ${errorMsg}`);
459
- }
460
-
461
- // Parse response using existing parseTokenResponse function
462
- const tokenResponse = parseTokenResponse(response);
463
- if (!tokenResponse) {
464
- throw new Error('Invalid refresh token response');
465
- }
466
-
467
- return tokenResponse;
468
- }
253
+ const {
254
+ initiateDeviceCodeFlow,
255
+ pollDeviceCodeToken,
256
+ displayDeviceCodeInfo,
257
+ refreshDeviceToken
258
+ } = require('./device-code');
469
259
 
470
260
  module.exports = {
471
261
  makeApiCall,
@@ -28,7 +28,7 @@ const paths = require('./paths');
28
28
  *
29
29
  * @example
30
30
  * const devPath = await copyBuilderToDevDirectory('myapp', 1);
31
- * // Returns: '~/.aifabrix/applications-dev-1/myapp-dev-1'
31
+ * // Returns: '~/.aifabrix/applications-dev-1'
32
32
  */
33
33
  async function copyBuilderToDevDirectory(appName, developerId) {
34
34
  const builderPath = path.join(process.cwd(), 'builder', appName);
@@ -38,9 +38,8 @@ async function copyBuilderToDevDirectory(appName, developerId) {
38
38
  throw new Error(`Builder directory not found: ${builderPath}\nRun 'aifabrix create ${appName}' first`);
39
39
  }
40
40
 
41
- // Get base directory (applications or applications-dev-{id})
42
- const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
43
- const baseDir = paths.getApplicationsBaseDir(idNum);
41
+ // Get base directory (applications or applications-dev-{id}) using raw developerId text
42
+ const baseDir = paths.getApplicationsBaseDir(developerId);
44
43
 
45
44
  // Clear base directory before copying (delete all files)
46
45
  if (fsSync.existsSync(baseDir)) {
@@ -56,20 +55,14 @@ async function copyBuilderToDevDirectory(appName, developerId) {
56
55
  }
57
56
  }
58
57
 
59
- // Get target directory using getDevDirectory()
58
+ // Get target directory using getDevDirectory() (root of applications folder)
60
59
  const devDir = getDevDirectory(appName, developerId);
61
60
 
62
61
  // Create target directory
63
62
  await fs.mkdir(devDir, { recursive: true });
64
63
 
65
- // Copy files based on developer ID
66
- if (idNum === 0) {
67
- // Dev 0: Copy contents from builder/{appName}/ directly to applications/
68
- await copyDirectory(builderPath, devDir);
69
- } else {
70
- // Dev > 0: Copy builder/{appName}/ to applications-dev-{id}/{appName}-dev-{id}/
71
- await copyDirectory(builderPath, devDir);
72
- }
64
+ // Copy files to root of applications folder (same behavior for all dev IDs)
65
+ await copyDirectory(builderPath, devDir);
73
66
 
74
67
  return devDir;
75
68
  }
@@ -349,7 +349,8 @@ async function generateDockerCompose(appName, appConfig, options) {
349
349
  ...networksConfig,
350
350
  envFile: envFileAbsolutePath,
351
351
  databasePasswords: databasePasswords,
352
- devId: devId,
352
+ // IMPORTANT: pass numeric devId to templates so (eq devId 0) works correctly
353
+ devId: idNum,
353
354
  networkName: networkName,
354
355
  containerName: containerName
355
356
  };