@aifabrix/builder 2.10.0 → 2.11.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.
@@ -0,0 +1,205 @@
1
+ /**
2
+ * AI Fabrix Builder - App Register Configuration Utilities
3
+ *
4
+ * Configuration extraction and loading for application registration
5
+ *
6
+ * @fileoverview Configuration utilities for app registration
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const fs = require('fs').promises;
12
+ const path = require('path');
13
+ const chalk = require('chalk');
14
+ const yaml = require('js-yaml');
15
+ const logger = require('./logger');
16
+ const { detectAppType } = require('./paths');
17
+
18
+ // Import createApp to auto-generate config if missing
19
+ let createApp;
20
+ try {
21
+ createApp = require('../app').createApp;
22
+ } catch {
23
+ createApp = null;
24
+ }
25
+
26
+ /**
27
+ * Load variables.yaml file for an application
28
+ * @async
29
+ * @param {string} appKey - Application key
30
+ * @returns {Promise<{variables: Object, created: boolean}>} Variables and creation flag
31
+ */
32
+ async function loadVariablesYaml(appKey) {
33
+ // Detect app type and get correct path (integration or builder)
34
+ const { appPath } = await detectAppType(appKey);
35
+ const variablesPath = path.join(appPath, 'variables.yaml');
36
+
37
+ try {
38
+ const variablesContent = await fs.readFile(variablesPath, 'utf-8');
39
+ return { variables: yaml.load(variablesContent), created: false };
40
+ } catch (error) {
41
+ if (error.code === 'ENOENT') {
42
+ logger.log(chalk.yellow(`⚠️ variables.yaml not found for ${appKey}`));
43
+ logger.log(chalk.yellow('📝 Creating minimal configuration...\n'));
44
+ return { variables: null, created: true };
45
+ }
46
+ throw new Error(`Failed to read variables.yaml: ${error.message}`);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Create minimal application configuration if needed
52
+ * @async
53
+ * @param {string} appKey - Application key
54
+ * @param {Object} options - Registration options
55
+ * @returns {Promise<Object>} Variables after creation
56
+ */
57
+ async function createMinimalAppIfNeeded(appKey, options) {
58
+ if (!createApp) {
59
+ throw new Error('Cannot auto-create application: createApp function not available');
60
+ }
61
+
62
+ await createApp(appKey, {
63
+ port: options.port,
64
+ language: 'typescript',
65
+ database: false,
66
+ redis: false,
67
+ storage: false,
68
+ authentication: false
69
+ });
70
+
71
+ // Detect app type and get correct path (integration or builder)
72
+ const { appPath } = await detectAppType(appKey);
73
+ const variablesPath = path.join(appPath, 'variables.yaml');
74
+ const variablesContent = await fs.readFile(variablesPath, 'utf-8');
75
+ return yaml.load(variablesContent);
76
+ }
77
+
78
+ /**
79
+ * Builds image reference string from variables
80
+ * Format: repository:tag (e.g., aifabrix/miso-controller:latest or myregistry.azurecr.io/miso-controller:v1.0.0)
81
+ * @param {Object} variables - Variables from YAML file
82
+ * @param {string} appKey - Application key (fallback)
83
+ * @returns {string} Image reference string
84
+ */
85
+ function buildImageReference(variables, appKey) {
86
+ const imageName = variables.image?.name || variables.app?.key || appKey;
87
+ const registry = variables.image?.registry;
88
+ const tag = variables.image?.tag || 'latest';
89
+ return registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}`;
90
+ }
91
+
92
+ /**
93
+ * Extract URL from external system JSON file for registration
94
+ * @async
95
+ * @param {string} appKey - Application key
96
+ * @param {Object} externalIntegration - External integration config from variables.yaml
97
+ * @returns {Promise<{url: string, apiKey?: string}>} URL and optional API key
98
+ */
99
+ async function extractExternalIntegrationUrl(appKey, externalIntegration) {
100
+ if (!externalIntegration || !externalIntegration.systems || externalIntegration.systems.length === 0) {
101
+ throw new Error('externalIntegration.systems is required for external type applications');
102
+ }
103
+
104
+ // Detect app type and get correct path (integration or builder)
105
+ const { appPath } = await detectAppType(appKey);
106
+ const schemaBasePath = externalIntegration.schemaBasePath || './';
107
+ const systemFileName = externalIntegration.systems[0];
108
+
109
+ // Resolve system file path (handle both relative and absolute paths)
110
+ const systemFilePath = path.isAbsolute(schemaBasePath)
111
+ ? path.join(schemaBasePath, systemFileName)
112
+ : path.join(appPath, schemaBasePath, systemFileName);
113
+
114
+ try {
115
+ const systemContent = await fs.readFile(systemFilePath, 'utf-8');
116
+ const systemJson = JSON.parse(systemContent);
117
+
118
+ // Extract URL from environment.baseUrl
119
+ const url = systemJson.environment?.baseUrl;
120
+ if (!url) {
121
+ throw new Error(`Missing environment.baseUrl in ${systemFileName}`);
122
+ }
123
+
124
+ // Extract optional API key from authentication if present
125
+ let apiKey;
126
+ if (systemJson.authentication?.apikey?.key) {
127
+ // If it's a kv:// reference, we can't resolve it here, so leave it undefined
128
+ // The API will handle kv:// references
129
+ const keyValue = systemJson.authentication.apikey.key;
130
+ if (!keyValue.startsWith('kv://') && !keyValue.startsWith('{{')) {
131
+ apiKey = keyValue;
132
+ }
133
+ }
134
+
135
+ return { url, apiKey };
136
+ } catch (error) {
137
+ if (error.code === 'ENOENT') {
138
+ throw new Error(`External system file not found: ${systemFilePath}`);
139
+ }
140
+ throw new Error(`Failed to read external system file: ${error.message}`);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Extract application configuration from variables.yaml
146
+ * @async
147
+ * @param {Object} variables - Variables from YAML file
148
+ * @param {string} appKey - Application key
149
+ * @param {Object} options - Registration options
150
+ * @returns {Promise<Object>} Extracted configuration
151
+ */
152
+ async function extractAppConfiguration(variables, appKey, options) {
153
+ const appKeyFromFile = variables.app?.key || appKey;
154
+ const displayName = variables.app?.name || options.name || appKey;
155
+ const description = variables.app?.description || '';
156
+
157
+ // Handle external type
158
+ if (variables.app?.type === 'external') {
159
+ // Extract URL from external system JSON file
160
+ const { url, apiKey } = await extractExternalIntegrationUrl(appKey, variables.externalIntegration);
161
+
162
+ // Build simplified externalIntegration object for registration API
163
+ const externalIntegration = { url };
164
+ if (apiKey) {
165
+ externalIntegration.apiKey = apiKey;
166
+ }
167
+
168
+ return {
169
+ appKey: appKeyFromFile,
170
+ displayName,
171
+ description,
172
+ appType: 'external',
173
+ externalIntegration,
174
+ port: null, // External systems don't need ports
175
+ image: null, // External systems don't need images
176
+ language: null // External systems don't need language
177
+ };
178
+ }
179
+
180
+ const appType = variables.build?.language === 'typescript' ? 'webapp' : 'service';
181
+ const registryMode = variables.image?.registryMode || 'external';
182
+ const port = variables.build?.port || options.port || 3000;
183
+ const language = variables.build?.language || 'typescript';
184
+ const image = buildImageReference(variables, appKeyFromFile);
185
+
186
+ return {
187
+ appKey: appKeyFromFile,
188
+ displayName,
189
+ description,
190
+ appType,
191
+ registryMode,
192
+ port,
193
+ image,
194
+ language
195
+ };
196
+ }
197
+
198
+ module.exports = {
199
+ loadVariablesYaml,
200
+ createMinimalAppIfNeeded,
201
+ buildImageReference,
202
+ extractAppConfiguration,
203
+ extractExternalIntegrationUrl
204
+ };
205
+
@@ -0,0 +1,69 @@
1
+ /**
2
+ * AI Fabrix Builder - App Register Display Utilities
3
+ *
4
+ * Display and formatting utilities for application registration results
5
+ *
6
+ * @fileoverview Display utilities for app registration
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const chalk = require('chalk');
12
+ const logger = require('./logger');
13
+
14
+ /**
15
+ * Get environment prefix for GitHub Secrets
16
+ * @param {string} environment - Environment key (e.g., 'dev', 'tst', 'pro', 'miso')
17
+ * @returns {string} Uppercase prefix (e.g., 'DEV', 'TST', 'PRO', 'MISO')
18
+ */
19
+ function getEnvironmentPrefix(environment) {
20
+ if (!environment) {
21
+ return 'DEV';
22
+ }
23
+ // Convert to uppercase and handle common variations
24
+ const env = environment.toLowerCase();
25
+ if (env === 'dev' || env === 'development') {
26
+ return 'DEV';
27
+ }
28
+ if (env === 'tst' || env === 'test' || env === 'staging') {
29
+ return 'TST';
30
+ }
31
+ if (env === 'pro' || env === 'prod' || env === 'production') {
32
+ return 'PRO';
33
+ }
34
+ // For other environments (e.g., 'miso'), uppercase the entire string
35
+ // Use full string if 4 characters or less, otherwise use first 4 characters
36
+ const upper = environment.toUpperCase();
37
+ return upper.length <= 4 ? upper : upper.substring(0, 4);
38
+ }
39
+
40
+ /**
41
+ * Display registration success and credentials
42
+ * @param {Object} data - Registration response data
43
+ * @param {string} apiUrl - API URL
44
+ * @param {string} environment - Environment key
45
+ */
46
+ function displayRegistrationResults(data, apiUrl, environment) {
47
+ logger.log(chalk.green('✅ Application registered successfully!\n'));
48
+ logger.log(chalk.bold('📋 Application Details:'));
49
+ logger.log(` ID: ${data.application.id}`);
50
+ logger.log(` Key: ${data.application.key}`);
51
+ logger.log(` Display Name: ${data.application.displayName}\n`);
52
+
53
+ logger.log(chalk.bold.yellow('🔑 CREDENTIALS (save these immediately):'));
54
+ logger.log(chalk.yellow(` Client ID: ${data.credentials.clientId}`));
55
+ logger.log(chalk.yellow(` Client Secret: ${data.credentials.clientSecret}\n`));
56
+
57
+ logger.log(chalk.red('⚠️ IMPORTANT: Client Secret will not be shown again!\n'));
58
+
59
+ const envPrefix = getEnvironmentPrefix(environment);
60
+ logger.log(chalk.bold('📝 Add to GitHub Secrets:'));
61
+ logger.log(chalk.cyan(' Repository level:'));
62
+ logger.log(chalk.cyan(` MISO_CONTROLLER_URL = ${apiUrl}`));
63
+ logger.log(chalk.cyan(`\n Environment level (${environment}):`));
64
+ logger.log(chalk.cyan(` ${envPrefix}_MISO_CLIENTID = ${data.credentials.clientId}`));
65
+ logger.log(chalk.cyan(` ${envPrefix}_MISO_CLIENTSECRET = ${data.credentials.clientSecret}\n`));
66
+ }
67
+
68
+ module.exports = { displayRegistrationResults, getEnvironmentPrefix };
69
+
@@ -0,0 +1,143 @@
1
+ /**
2
+ * AI Fabrix Builder - App Register Validation Utilities
3
+ *
4
+ * Validation logic for application registration
5
+ *
6
+ * @fileoverview Validation utilities for app registration
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const path = require('path');
12
+ const chalk = require('chalk');
13
+ const logger = require('./logger');
14
+ const { detectAppType } = require('./paths');
15
+ const applicationSchema = require('../schema/application-schema.json');
16
+
17
+ // Extract valid enum values from application schema
18
+ const validTypes = applicationSchema.properties.type.enum || [];
19
+ const validRegistryModes = applicationSchema.properties.registryMode.enum || [];
20
+ const portConstraints = {
21
+ minimum: applicationSchema.properties.port?.minimum || 1,
22
+ maximum: applicationSchema.properties.port?.maximum || 65535
23
+ };
24
+ const imagePattern = applicationSchema.properties.image?.pattern || /^[a-zA-Z0-9._/-]+:[a-zA-Z0-9._-]+$/;
25
+
26
+ /**
27
+ * Validation schema for application registration
28
+ * Validates according to application-schema.json
29
+ */
30
+ const registerApplicationSchema = {
31
+ environmentId: (val) => {
32
+ if (!val || val.length < 1) {
33
+ throw new Error('Invalid environment ID format');
34
+ }
35
+ return val;
36
+ },
37
+ key: (val) => {
38
+ if (!val || val.length < 1) {
39
+ throw new Error('Application key is required');
40
+ }
41
+ const keyPattern = applicationSchema.properties.key.pattern;
42
+ const keyMaxLength = applicationSchema.properties.key.maxLength || 50;
43
+ if (val.length > keyMaxLength) {
44
+ throw new Error(`Application key must be at most ${keyMaxLength} characters`);
45
+ }
46
+ if (keyPattern && !new RegExp(keyPattern).test(val)) {
47
+ throw new Error('Application key must contain only lowercase letters, numbers, and hyphens');
48
+ }
49
+ return val;
50
+ },
51
+ displayName: (val) => {
52
+ if (!val || val.length < 1) {
53
+ throw new Error('Display name is required');
54
+ }
55
+ const displayNameMaxLength = applicationSchema.properties.displayName.maxLength || 100;
56
+ if (val.length > displayNameMaxLength) {
57
+ throw new Error(`Display name must be at most ${displayNameMaxLength} characters`);
58
+ }
59
+ return val;
60
+ },
61
+ description: (val) => val || undefined,
62
+ image: (val, appType) => {
63
+ // Image is required for non-external types
64
+ if (appType !== 'external') {
65
+ if (!val || typeof val !== 'string') {
66
+ throw new Error('Image is required for non-external application types');
67
+ }
68
+ // Validate image format: repository:tag
69
+ const pattern = typeof imagePattern === 'string' ? new RegExp(imagePattern) : imagePattern;
70
+ if (!pattern.test(val)) {
71
+ throw new Error('Image must be in format repository:tag (e.g., aifabrix/miso-controller:latest or myregistry.azurecr.io/miso-controller:v1.0.0)');
72
+ }
73
+ }
74
+ return val || undefined;
75
+ },
76
+ configuration: (val) => {
77
+ if (!val || !val.type || !validTypes.includes(val.type)) {
78
+ throw new Error(`Configuration type must be one of: ${validTypes.join(', ')}`);
79
+ }
80
+
81
+ // For external type: require externalIntegration, skip registryMode/port validation
82
+ if (val.type === 'external') {
83
+ if (!val.externalIntegration) {
84
+ throw new Error('externalIntegration is required for external application type');
85
+ }
86
+ // External type should not have registryMode, port, or image
87
+ return val;
88
+ }
89
+
90
+ // For non-external types: require registryMode, port, image
91
+ if (!val.registryMode || !validRegistryModes.includes(val.registryMode)) {
92
+ throw new Error(`Registry mode must be one of: ${validRegistryModes.join(', ')}`);
93
+ }
94
+ if (val.port === undefined || val.port === null) {
95
+ throw new Error('Port is required for non-external application types');
96
+ }
97
+ if (!Number.isInteger(val.port) || val.port < portConstraints.minimum || val.port > portConstraints.maximum) {
98
+ throw new Error(`Port must be an integer between ${portConstraints.minimum} and ${portConstraints.maximum}`);
99
+ }
100
+ return val;
101
+ }
102
+ };
103
+
104
+ /**
105
+ * Validate application registration data
106
+ * @async
107
+ * @param {Object} config - Application configuration
108
+ * @param {string} originalAppKey - Original app key for error messages
109
+ * @throws {Error} If validation fails
110
+ */
111
+ async function validateAppRegistrationData(config, originalAppKey) {
112
+ const missingFields = [];
113
+ if (!config.appKey) missingFields.push('app.key');
114
+ if (!config.displayName) missingFields.push('app.name');
115
+
116
+ if (missingFields.length > 0) {
117
+ logger.error(chalk.red('❌ Missing required fields in variables.yaml:'));
118
+ missingFields.forEach(field => logger.error(chalk.red(` - ${field}`)));
119
+ // Detect app type to show correct path
120
+ const { appPath } = await detectAppType(originalAppKey);
121
+ const relativePath = path.relative(process.cwd(), appPath);
122
+ logger.error(chalk.red(`\n Please update ${relativePath}/variables.yaml and try again.`));
123
+ process.exit(1);
124
+ }
125
+
126
+ try {
127
+ registerApplicationSchema.key(config.appKey);
128
+ registerApplicationSchema.displayName(config.displayName);
129
+ registerApplicationSchema.image(config.image, config.appType);
130
+ registerApplicationSchema.configuration({
131
+ type: config.appType,
132
+ registryMode: config.registryMode,
133
+ port: config.port,
134
+ externalIntegration: config.externalIntegration
135
+ });
136
+ } catch (error) {
137
+ logger.error(chalk.red(`❌ Invalid configuration: ${error.message}`));
138
+ process.exit(1);
139
+ }
140
+ }
141
+
142
+ module.exports = { registerApplicationSchema, validateAppRegistrationData };
143
+
@@ -388,7 +388,7 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
388
388
  }
389
389
 
390
390
  const url = `${controllerUrl}/api/v1/auth/login/device/refresh`;
391
- const response = await makeApiCall(url, {
391
+ const response = await getMakeApiCall()(url, {
392
392
  method: 'POST',
393
393
  headers: {
394
394
  'Content-Type': 'application/json'
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Error Parser Utilities
3
+ *
4
+ * Parses error responses and determines error types
5
+ *
6
+ * @fileoverview Error parsing utilities
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const { formatPermissionError } = require('./permission-errors');
12
+ const { formatValidationError } = require('./validation-errors');
13
+ const {
14
+ formatAuthenticationError,
15
+ formatServerError,
16
+ formatConflictError,
17
+ formatNotFoundError,
18
+ formatGenericError
19
+ } = require('./http-status-errors');
20
+ const { formatNetworkError } = require('./network-errors');
21
+
22
+ /**
23
+ * Parses error response into error data object
24
+ * @param {string|Object} errorResponse - Error response (string or parsed JSON)
25
+ * @returns {Object} Parsed error data object
26
+ */
27
+ function parseErrorData(errorResponse) {
28
+ if (errorResponse === undefined || errorResponse === null) {
29
+ return { message: 'Unknown error occurred' };
30
+ }
31
+ if (typeof errorResponse === 'string') {
32
+ try {
33
+ return JSON.parse(errorResponse);
34
+ } catch {
35
+ return { message: errorResponse };
36
+ }
37
+ }
38
+ return typeof errorResponse === 'object' ? errorResponse : { message: String(errorResponse) };
39
+ }
40
+
41
+ /**
42
+ * Creates error result object
43
+ * @param {string} type - Error type
44
+ * @param {string} message - Error message
45
+ * @param {string} formatted - Formatted error message
46
+ * @param {Object} data - Error data
47
+ * @returns {Object} Error result object
48
+ */
49
+ function createErrorResult(type, message, formatted, data) {
50
+ return { type, message, formatted, data };
51
+ }
52
+
53
+ /**
54
+ * Gets error message from error data
55
+ * @param {Object} errorData - Error data object
56
+ * @param {string} defaultMessage - Default message if not found
57
+ * @returns {string} Error message
58
+ */
59
+ function getErrorMessage(errorData, defaultMessage) {
60
+ return errorData.detail || errorData.title || errorData.errorDescription ||
61
+ errorData.message || errorData.error || defaultMessage;
62
+ }
63
+
64
+ /**
65
+ * Handles 400 validation error
66
+ * @param {Object} errorData - Error data object
67
+ * @returns {Object} Error result object
68
+ */
69
+ function handleValidationError(errorData) {
70
+ const errorMessage = getErrorMessage(errorData, 'Validation error');
71
+ return createErrorResult('validation', errorMessage, formatValidationError(errorData), errorData);
72
+ }
73
+
74
+ /**
75
+ * Handles specific 4xx client error codes
76
+ * @param {number} statusCode - HTTP status code
77
+ * @param {Object} errorData - Error data object
78
+ * @returns {Object|null} Error result object or null if not handled
79
+ */
80
+ function handleSpecificClientErrors(statusCode, errorData) {
81
+ switch (statusCode) {
82
+ case 403:
83
+ return createErrorResult('permission', 'Permission denied', formatPermissionError(errorData), errorData);
84
+ case 401:
85
+ return createErrorResult('authentication', 'Authentication failed', formatAuthenticationError(errorData), errorData);
86
+ case 400:
87
+ case 422:
88
+ return handleValidationError(errorData);
89
+ case 404:
90
+ return createErrorResult('notfound', getErrorMessage(errorData, 'Not found'), formatNotFoundError(errorData), errorData);
91
+ case 409:
92
+ return createErrorResult('conflict', getErrorMessage(errorData, 'Conflict'), formatConflictError(errorData), errorData);
93
+ default:
94
+ return null;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Handles HTTP status code errors
100
+ * @param {number} statusCode - HTTP status code
101
+ * @param {Object} errorData - Error data object
102
+ * @returns {Object|null} Error result object or null if not handled
103
+ */
104
+ function handleStatusCodeError(statusCode, errorData) {
105
+ // Handle 4xx client errors
106
+ if (statusCode >= 400 && statusCode < 500) {
107
+ return handleSpecificClientErrors(statusCode, errorData);
108
+ }
109
+ // Handle 5xx server errors
110
+ if (statusCode >= 500) {
111
+ return createErrorResult('server', 'Server error', formatServerError(errorData), errorData);
112
+ }
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * Parses error response and determines error type
118
+ * @param {string|Object} errorResponse - Error response (string or parsed JSON)
119
+ * @param {number} statusCode - HTTP status code
120
+ * @param {boolean} isNetworkError - Whether this is a network error
121
+ * @returns {Object} Parsed error object with type, message, and formatted output
122
+ */
123
+ function parseErrorResponse(errorResponse, statusCode, isNetworkError) {
124
+ let errorData = parseErrorData(errorResponse);
125
+
126
+ // Handle nested response structure (some APIs wrap errors in data field)
127
+ if (errorData.data && typeof errorData.data === 'object') {
128
+ errorData = errorData.data;
129
+ }
130
+
131
+ // Handle network errors
132
+ if (isNetworkError) {
133
+ const errorMessage = errorData.message || errorResponse || 'Network error';
134
+ return createErrorResult('network', errorMessage, formatNetworkError(errorMessage, errorData), errorData);
135
+ }
136
+
137
+ // Handle HTTP status codes
138
+ const statusError = handleStatusCodeError(statusCode, errorData);
139
+ if (statusError) {
140
+ return statusError;
141
+ }
142
+
143
+ // Generic error
144
+ return createErrorResult('generic', errorData.message || errorData.error || 'Unknown error', formatGenericError(errorData, statusCode), errorData);
145
+ }
146
+
147
+ module.exports = {
148
+ parseErrorResponse
149
+ };
150
+