@aifabrix/builder 2.0.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,256 @@
1
+ /**
2
+ * AI Fabrix Builder Deployment Module
3
+ *
4
+ * Handles deployment to Miso Controller API with ISO 27001 security measures.
5
+ * Manages authentication, validation, and API communication for deployments.
6
+ *
7
+ * @fileoverview Deployment orchestration and API communication
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const axios = require('axios');
13
+ const chalk = require('chalk');
14
+ const auditLogger = require('./audit-logger');
15
+
16
+ /**
17
+ * Validates and sanitizes controller URL
18
+ * Enforces HTTPS-only communication for security
19
+ *
20
+ * @param {string} url - Controller URL to validate
21
+ * @throws {Error} If URL is invalid or uses HTTP
22
+ */
23
+ function validateControllerUrl(url) {
24
+ if (!url || typeof url !== 'string') {
25
+ throw new Error('Controller URL is required and must be a string');
26
+ }
27
+
28
+ // Must use HTTPS for security
29
+ if (!url.startsWith('https://')) {
30
+ throw new Error('Controller URL must use HTTPS (https://)');
31
+ }
32
+
33
+ // Basic URL format validation
34
+ const urlPattern = /^https:\/\/[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(\/.*)?$/;
35
+ if (!urlPattern.test(url)) {
36
+ throw new Error('Invalid controller URL format');
37
+ }
38
+
39
+ // Remove trailing slash if present
40
+ return url.replace(/\/$/, '');
41
+ }
42
+
43
+ /**
44
+ * Sends deployment manifest to Miso Controller
45
+ *
46
+ * @async
47
+ * @param {string} url - Controller URL
48
+ * @param {Object} manifest - Deployment manifest
49
+ * @param {Object} options - Deployment options (timeout, retries, etc.)
50
+ * @returns {Promise<Object>} Deployment result from controller
51
+ * @throws {Error} If deployment fails
52
+ */
53
+ async function sendDeploymentRequest(url, manifest, options = {}) {
54
+ const endpoint = `${url}/api/pipeline/deploy`;
55
+ const timeout = options.timeout || 30000;
56
+ const maxRetries = options.maxRetries || 3;
57
+
58
+ const requestConfig = {
59
+ headers: {
60
+ 'Content-Type': 'application/json',
61
+ 'User-Agent': 'aifabrix-builder/2.0.0'
62
+ },
63
+ timeout,
64
+ validateStatus: (status) => status < 500 // Don't throw on 4xx errors
65
+ };
66
+
67
+ let lastError;
68
+
69
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
70
+ try {
71
+ const response = await axios.post(endpoint, manifest, requestConfig);
72
+
73
+ // Check for HTTP errors
74
+ if (response.status >= 400) {
75
+ const error = new Error(`Controller returned error: ${response.status} ${response.statusText}`);
76
+ error.status = response.status;
77
+ error.response = {
78
+ status: response.status,
79
+ statusText: response.statusText,
80
+ data: response.data
81
+ };
82
+ error.data = response.data;
83
+ throw error;
84
+ }
85
+
86
+ return response.data;
87
+
88
+ } catch (error) {
89
+ lastError = error;
90
+
91
+ // Log retry attempt
92
+ if (attempt < maxRetries) {
93
+ const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); // Exponential backoff, max 5s
94
+ console.log(chalk.yellow(`⚠️ Deployment attempt ${attempt} failed, retrying in ${delay}ms...`));
95
+ await new Promise(resolve => setTimeout(resolve, delay));
96
+ }
97
+ }
98
+ }
99
+
100
+ // All retries failed
101
+ throw new Error(`Deployment failed after ${maxRetries} attempts: ${lastError.message}`);
102
+ }
103
+
104
+ /**
105
+ * Polls deployment status from controller
106
+ *
107
+ * @async
108
+ * @param {string} deploymentId - Deployment ID to poll
109
+ * @param {string} controllerUrl - Controller URL
110
+ * @param {Object} options - Polling options (interval, maxAttempts, etc.)
111
+ * @returns {Promise<Object>} Deployment status
112
+ */
113
+ async function pollDeploymentStatus(deploymentId, controllerUrl, options = {}) {
114
+ const interval = options.interval || 5000;
115
+ const maxAttempts = options.maxAttempts || 60; // 5 minutes max
116
+
117
+ const statusEndpoint = `${controllerUrl}/api/pipeline/status/${deploymentId}`;
118
+
119
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
120
+ try {
121
+ const response = await axios.get(statusEndpoint, {
122
+ timeout: 10000,
123
+ validateStatus: (status) => status < 500
124
+ });
125
+
126
+ if (response.status === 200) {
127
+ const status = response.data.status;
128
+
129
+ // Terminal states
130
+ if (status === 'completed' || status === 'failed' || status === 'cancelled') {
131
+ return response.data;
132
+ }
133
+
134
+ // Log progress
135
+ console.log(chalk.blue(` Status: ${status} (attempt ${attempt + 1}/${maxAttempts})`));
136
+
137
+ // Wait before next poll
138
+ if (attempt < maxAttempts - 1) {
139
+ await new Promise(resolve => setTimeout(resolve, interval));
140
+ }
141
+ } else {
142
+ throw new Error(`Status check failed: ${response.status}`);
143
+ }
144
+ } catch (error) {
145
+ if (error.response && error.response.status === 404) {
146
+ throw new Error(`Deployment ${deploymentId} not found`);
147
+ }
148
+ throw error;
149
+ }
150
+ }
151
+
152
+ throw new Error('Deployment timeout: Maximum polling attempts reached');
153
+ }
154
+
155
+ /**
156
+ * Handles deployment errors with security-aware messages
157
+ *
158
+ * @param {Error} error - Error to handle
159
+ * @returns {Object} Structured error information
160
+ */
161
+ function handleDeploymentError(error) {
162
+ const safeError = {
163
+ message: error.message,
164
+ code: error.code || 'UNKNOWN',
165
+ timeout: error.code === 'ECONNABORTED',
166
+ status: error.status || error.response?.status,
167
+ data: error.data || error.response?.data
168
+ };
169
+
170
+ // Mask sensitive information in error messages
171
+ safeError.message = auditLogger.maskSensitiveData(safeError.message);
172
+
173
+ return safeError;
174
+ }
175
+
176
+ /**
177
+ * Deploys application to Miso Controller
178
+ * Main orchestrator for the deployment process
179
+ *
180
+ * @async
181
+ * @param {Object} manifest - Deployment manifest
182
+ * @param {string} controllerUrl - Controller URL
183
+ * @param {Object} options - Deployment options
184
+ * @returns {Promise<Object>} Deployment result
185
+ * @throws {Error} If deployment fails
186
+ */
187
+ async function deployToController(manifest, controllerUrl, options = {}) {
188
+ // Validate and sanitize controller URL
189
+ const url = validateControllerUrl(controllerUrl);
190
+
191
+ // Log deployment attempt for audit
192
+ auditLogger.logDeploymentAttempt(manifest.key, url, options);
193
+
194
+ try {
195
+ // Send deployment request
196
+ console.log(chalk.blue(`📤 Sending deployment request to ${url}...`));
197
+ const result = await sendDeploymentRequest(url, manifest, {
198
+ timeout: options.timeout || 30000,
199
+ maxRetries: options.maxRetries || 3
200
+ });
201
+
202
+ // Log success
203
+ if (result.deploymentId) {
204
+ auditLogger.logDeploymentSuccess(manifest.key, result.deploymentId, url);
205
+ }
206
+
207
+ // Poll for deployment status if enabled
208
+ if (options.poll && result.deploymentId) {
209
+ console.log(chalk.blue(`\n⏳ Polling deployment status (${options.pollInterval || 5000}ms intervals)...`));
210
+ const status = await pollDeploymentStatus(
211
+ result.deploymentId,
212
+ url,
213
+ {
214
+ interval: options.pollInterval || 5000,
215
+ maxAttempts: options.pollMaxAttempts || 60
216
+ }
217
+ );
218
+ result.status = status;
219
+ }
220
+
221
+ return result;
222
+
223
+ } catch (error) {
224
+ // Log failure for audit
225
+ auditLogger.logDeploymentFailure(manifest.key, url, error);
226
+
227
+ // Handle and re-throw with safe error
228
+ const safeError = handleDeploymentError(error);
229
+
230
+ // Provide user-friendly error messages
231
+ if (safeError.status === 401 || safeError.status === 403) {
232
+ throw new Error('Authentication failed. Check your deployment key.');
233
+ } else if (safeError.status === 400) {
234
+ throw new Error('Invalid deployment manifest. Please check your configuration.');
235
+ } else if (safeError.status === 404) {
236
+ throw new Error('Controller endpoint not found. Check the controller URL.');
237
+ } else if (safeError.code === 'ECONNREFUSED') {
238
+ throw new Error('Cannot connect to controller. Check if the controller is running.');
239
+ } else if (safeError.code === 'ENOTFOUND') {
240
+ throw new Error('Controller hostname not found. Check your controller URL.');
241
+ } else if (safeError.timeout) {
242
+ throw new Error('Request timed out. The controller may be overloaded.');
243
+ }
244
+
245
+ throw new Error(safeError.message);
246
+ }
247
+ }
248
+
249
+ module.exports = {
250
+ deployToController,
251
+ sendDeploymentRequest,
252
+ pollDeploymentStatus,
253
+ validateControllerUrl,
254
+ handleDeploymentError
255
+ };
256
+
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Environment File Reader and Converter Module
3
+ *
4
+ * Handles reading existing .env files and converting them to templates
5
+ * following ISO 27001 security standards for sensitive data handling
6
+ */
7
+
8
+ const fs = require('fs').promises;
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Read existing .env file from application folder
13
+ * @param {string} appPath - Path to application directory
14
+ * @returns {Promise<Object|null>} Parsed environment variables or null if not found
15
+ */
16
+ async function readExistingEnv(appPath) {
17
+ const envPath = path.join(appPath, '.env');
18
+
19
+ try {
20
+ await fs.access(envPath);
21
+ const content = await fs.readFile(envPath, 'utf8');
22
+ return parseEnvContent(content);
23
+ } catch (error) {
24
+ if (error.code === 'ENOENT') {
25
+ return null;
26
+ }
27
+ throw new Error(`Failed to read .env file: ${error.message}`);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Parse .env file content into key-value pairs
33
+ * @param {string} content - Raw .env file content
34
+ * @returns {Object} Parsed environment variables
35
+ */
36
+ function parseEnvContent(content) {
37
+ const envVars = {};
38
+ const lines = content.split('\n');
39
+
40
+ for (const line of lines) {
41
+ const trimmedLine = line.trim();
42
+
43
+ // Skip empty lines and comments
44
+ if (!trimmedLine || trimmedLine.startsWith('#')) {
45
+ continue;
46
+ }
47
+
48
+ // Parse key=value pairs
49
+ const equalIndex = trimmedLine.indexOf('=');
50
+ if (equalIndex === -1) {
51
+ continue;
52
+ }
53
+
54
+ const key = trimmedLine.substring(0, equalIndex).trim();
55
+ let value = trimmedLine.substring(equalIndex + 1).trim();
56
+
57
+ // Remove quotes if present
58
+ if ((value.startsWith('"') && value.endsWith('"')) ||
59
+ (value.startsWith('\'') && value.endsWith('\''))) {
60
+ value = value.slice(1, -1);
61
+ }
62
+
63
+ envVars[key] = value;
64
+ }
65
+
66
+ return envVars;
67
+ }
68
+
69
+ /**
70
+ * Determine if a value should be converted to kv:// reference
71
+ * @param {string} key - Environment variable key
72
+ * @param {string} value - Environment variable value
73
+ * @returns {boolean} True if value should be treated as sensitive
74
+ */
75
+ function detectSensitiveValue(key, value) {
76
+ // Check key patterns for sensitive data
77
+ const sensitiveKeyPatterns = [
78
+ /password/i,
79
+ /secret/i,
80
+ /key/i,
81
+ /token/i,
82
+ /api[_-]?key/i,
83
+ /private/i,
84
+ /auth/i,
85
+ /credential/i,
86
+ /passwd/i,
87
+ /pwd/i
88
+ ];
89
+
90
+ for (const pattern of sensitiveKeyPatterns) {
91
+ if (pattern.test(key)) {
92
+ return true;
93
+ }
94
+ }
95
+
96
+ // Check value patterns for sensitive data
97
+ const sensitiveValuePatterns = [
98
+ // UUIDs (8-4-4-4-12 format)
99
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
100
+ // Long random strings (>32 characters)
101
+ /^[a-zA-Z0-9+/=]{32,}$/,
102
+ // JWT tokens (three base64 parts separated by dots)
103
+ /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/,
104
+ // Hex strings (>16 characters)
105
+ /^[0-9a-f]{16,}$/i
106
+ ];
107
+
108
+ for (const pattern of sensitiveValuePatterns) {
109
+ if (pattern.test(value)) {
110
+ return true;
111
+ }
112
+ }
113
+
114
+ return false;
115
+ }
116
+
117
+ /**
118
+ * Convert existing .env variables to env.template format
119
+ * @param {Object} existingEnv - Existing environment variables
120
+ * @param {Object} requiredVars - Required variables for the application
121
+ * @returns {Object} Merged environment variables with sensitive values converted
122
+ */
123
+ function convertToEnvTemplate(existingEnv, requiredVars) {
124
+ const convertedEnv = { ...requiredVars };
125
+
126
+ // Process existing environment variables
127
+ Object.entries(existingEnv).forEach(([key, value]) => {
128
+ if (detectSensitiveValue(key, value)) {
129
+ // Convert sensitive values to kv:// references
130
+ convertedEnv[key] = `kv://${key.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
131
+ } else {
132
+ // Keep non-sensitive values as-is
133
+ convertedEnv[key] = value;
134
+ }
135
+ });
136
+
137
+ return convertedEnv;
138
+ }
139
+
140
+ /**
141
+ * Extract sensitive values for secrets.yaml generation
142
+ * @param {Object} envVars - Environment variables
143
+ * @returns {Object} Object suitable for secrets.yaml
144
+ */
145
+ function generateSecretsFromEnv(envVars) {
146
+ const secrets = {};
147
+
148
+ Object.entries(envVars).forEach(([key, value]) => {
149
+ if (detectSensitiveValue(key, value)) {
150
+ // Convert key to secret name format
151
+ const secretName = key.toLowerCase().replace(/[^a-z0-9]/g, '-');
152
+ secrets[secretName] = value;
153
+ }
154
+ });
155
+
156
+ return secrets;
157
+ }
158
+
159
+ /**
160
+ * Validate environment variable names
161
+ * @param {string} key - Environment variable key
162
+ * @returns {boolean} True if key is valid
163
+ */
164
+ function validateEnvKey(key) {
165
+ // Environment variable names should be uppercase letters, numbers, and underscores
166
+ return /^[A-Z][A-Z0-9_]*$/.test(key);
167
+ }
168
+
169
+ /**
170
+ * Sanitize environment variable value
171
+ * @param {string} value - Environment variable value
172
+ * @returns {string} Sanitized value
173
+ */
174
+ function sanitizeEnvValue(value) {
175
+ // Remove any potential injection characters
176
+ return value.replace(/[;\r\n]/g, '');
177
+ }
178
+
179
+ /**
180
+ * Generate environment template with security considerations
181
+ * @param {Object} config - Application configuration
182
+ * @param {Object} existingEnv - Existing environment variables
183
+ * @returns {Promise<Object>} Template generation result
184
+ */
185
+ async function generateEnvTemplate(config, existingEnv = {}) {
186
+ const result = {
187
+ template: '',
188
+ secrets: {},
189
+ warnings: []
190
+ };
191
+
192
+ try {
193
+ // Convert existing environment variables
194
+ const convertedEnv = convertToEnvTemplate(existingEnv, {});
195
+
196
+ // Extract secrets for secrets.yaml
197
+ result.secrets = generateSecretsFromEnv(existingEnv);
198
+
199
+ // Validate environment variables
200
+ Object.entries(existingEnv).forEach(([key, value]) => {
201
+ if (!validateEnvKey(key)) {
202
+ result.warnings.push(`Invalid environment variable name: ${key}`);
203
+ }
204
+
205
+ const sanitizedValue = sanitizeEnvValue(value);
206
+ if (sanitizedValue !== value) {
207
+ result.warnings.push(`Sanitized value for ${key} (removed special characters)`);
208
+ }
209
+ });
210
+
211
+ // Generate template content
212
+ const { generateEnvTemplate: generateTemplate } = require('./templates');
213
+ const baseTemplate = generateTemplate(config);
214
+
215
+ // Add existing environment variables to the template
216
+ const existingEnvSection = [];
217
+ Object.entries(convertedEnv).forEach(([key, value]) => {
218
+ if (!key.startsWith('NODE_ENV') && !key.startsWith('PORT') &&
219
+ !key.startsWith('APP_NAME') && !key.startsWith('LOG_LEVEL') &&
220
+ !key.startsWith('DB_') && !key.startsWith('DATABASE_') &&
221
+ !key.startsWith('REDIS_') && !key.startsWith('STORAGE_') &&
222
+ !key.startsWith('JWT_') && !key.startsWith('AUTH_') &&
223
+ !key.startsWith('SESSION_')) {
224
+ existingEnvSection.push(`${key}=${value}`);
225
+ }
226
+ });
227
+
228
+ if (existingEnvSection.length > 0) {
229
+ result.template = baseTemplate + '\n\n# Existing Environment Variables\n' + existingEnvSection.join('\n');
230
+ } else {
231
+ result.template = baseTemplate;
232
+ }
233
+
234
+ } catch (error) {
235
+ throw new Error(`Failed to generate environment template: ${error.message}`);
236
+ }
237
+
238
+ return result;
239
+ }
240
+
241
+ module.exports = {
242
+ readExistingEnv,
243
+ parseEnvContent,
244
+ detectSensitiveValue,
245
+ convertToEnvTemplate,
246
+ generateSecretsFromEnv,
247
+ validateEnvKey,
248
+ sanitizeEnvValue,
249
+ generateEnvTemplate
250
+ };