@aifabrix/builder 2.1.7 → 2.3.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.
Files changed (43) hide show
  1. package/lib/app-deploy.js +73 -29
  2. package/lib/app-list.js +132 -0
  3. package/lib/app-readme.js +11 -4
  4. package/lib/app-register.js +435 -0
  5. package/lib/app-rotate-secret.js +164 -0
  6. package/lib/app-run.js +98 -84
  7. package/lib/app.js +13 -0
  8. package/lib/audit-logger.js +195 -15
  9. package/lib/build.js +155 -42
  10. package/lib/cli.js +104 -8
  11. package/lib/commands/app.js +8 -391
  12. package/lib/commands/login.js +130 -36
  13. package/lib/commands/secure.js +260 -0
  14. package/lib/config.js +315 -4
  15. package/lib/deployer.js +221 -183
  16. package/lib/infra.js +177 -112
  17. package/lib/push.js +34 -7
  18. package/lib/secrets.js +89 -23
  19. package/lib/templates.js +1 -1
  20. package/lib/utils/api-error-handler.js +465 -0
  21. package/lib/utils/api.js +165 -16
  22. package/lib/utils/auth-headers.js +84 -0
  23. package/lib/utils/build-copy.js +162 -0
  24. package/lib/utils/cli-utils.js +49 -3
  25. package/lib/utils/compose-generator.js +57 -16
  26. package/lib/utils/deployment-errors.js +90 -0
  27. package/lib/utils/deployment-validation.js +60 -0
  28. package/lib/utils/dev-config.js +83 -0
  29. package/lib/utils/docker-build.js +24 -0
  30. package/lib/utils/env-template.js +30 -10
  31. package/lib/utils/health-check.js +18 -1
  32. package/lib/utils/infra-containers.js +101 -0
  33. package/lib/utils/local-secrets.js +0 -2
  34. package/lib/utils/secrets-encryption.js +203 -0
  35. package/lib/utils/secrets-path.js +22 -3
  36. package/lib/utils/token-manager.js +381 -0
  37. package/package.json +2 -2
  38. package/templates/applications/README.md.hbs +155 -23
  39. package/templates/applications/miso-controller/Dockerfile +7 -119
  40. package/templates/infra/compose.yaml.hbs +93 -0
  41. package/templates/python/docker-compose.hbs +25 -17
  42. package/templates/typescript/docker-compose.hbs +25 -17
  43. package/test-output.txt +0 -5431
package/lib/deployer.js CHANGED
@@ -13,110 +13,151 @@ const axios = require('axios');
13
13
  const chalk = require('chalk');
14
14
  const auditLogger = require('./audit-logger');
15
15
  const logger = require('./utils/logger');
16
+ const { createAuthHeaders } = require('./utils/auth-headers');
17
+ const { validateControllerUrl, validateEnvironmentKey } = require('./utils/deployment-validation');
18
+ const { handleDeploymentError, handleDeploymentErrors } = require('./utils/deployment-errors');
16
19
 
17
20
  /**
18
- * Validates and sanitizes controller URL
19
- * Enforces HTTPS-only communication for security
20
- *
21
- * @param {string} url - Controller URL to validate
22
- * @throws {Error} If URL is invalid or uses HTTP
21
+ * Handles deployment request retry logic
22
+ * @async
23
+ * @function handleDeploymentRetry
24
+ * @param {string} endpoint - API endpoint
25
+ * @param {Object} manifest - Deployment manifest
26
+ * @param {Object} requestConfig - Request configuration
27
+ * @param {number} maxRetries - Maximum retry attempts
28
+ * @returns {Promise<Object>} Deployment result
29
+ * @throws {Error} If deployment fails after retries
23
30
  */
24
- function validateControllerUrl(url) {
25
- if (!url || typeof url !== 'string') {
26
- throw new Error('Controller URL is required and must be a string');
27
- }
31
+ async function handleDeploymentRetry(endpoint, manifest, requestConfig, maxRetries) {
32
+ let lastError;
28
33
 
29
- // Must use HTTPS for security (allow http://localhost for local development)
30
- if (!url.startsWith('https://') && !url.startsWith('http://localhost')) {
31
- throw new Error('Controller URL must use HTTPS (https://) or http://localhost');
34
+ // Validate manifest before sending
35
+ if (!manifest || typeof manifest !== 'object') {
36
+ throw new Error('Deployment manifest is required and must be an object');
32
37
  }
33
38
 
34
- // Basic URL format validation
35
- 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])?)*(localhost)?(:[0-9]+)?(\/.*)?$/;
36
- if (!urlPattern.test(url)) {
37
- throw new Error('Invalid controller URL format');
38
- }
39
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
40
+ try {
41
+ // Axios automatically serializes objects to JSON when Content-Type is application/json
42
+ const response = await axios.post(endpoint, manifest, requestConfig);
39
43
 
40
- // Remove trailing slash if present
41
- return url.replace(/\/$/, '');
42
- }
44
+ if (response.status >= 400) {
45
+ const error = new Error(`Controller returned error: ${response.status} ${response.statusText}`);
46
+ error.status = response.status;
47
+ error.response = {
48
+ status: response.status,
49
+ statusText: response.statusText,
50
+ data: response.data
51
+ };
52
+ error.data = response.data;
53
+ throw error;
54
+ }
43
55
 
44
- /**
45
- * Creates authentication headers for Client Credentials flow
46
- *
47
- * @param {string} clientId - Application client ID
48
- * @param {string} clientSecret - Application client secret
49
- * @returns {Object} Headers object with authentication
50
- * @throws {Error} If credentials are missing
51
- */
52
- function createClientCredentialsHeaders(clientId, clientSecret) {
53
- if (!clientId || !clientSecret) {
54
- throw new Error('Client ID and Client Secret are required for authentication');
55
- }
56
- return {
57
- 'x-client-id': clientId,
58
- 'x-client-secret': clientSecret
59
- };
60
- }
56
+ // OpenAPI spec: Response 202 structure { success: boolean, deploymentId: string, status: string, deploymentUrl?: string, healthCheckUrl?: string, message?: string }
57
+ // Handle both OpenAPI format and legacy format for backward compatibility
58
+ const responseData = response.data;
59
+ if (responseData && typeof responseData === 'object') {
60
+ // OpenAPI format: { success, deploymentId, status, deploymentUrl, healthCheckUrl, message }
61
+ return responseData;
62
+ }
61
63
 
62
- /**
63
- * Validates environment key
64
- * @param {string} envKey - Environment key to validate
65
- * @throws {Error} If environment key is invalid
66
- */
67
- function validateEnvironmentKey(envKey) {
68
- if (!envKey || typeof envKey !== 'string') {
69
- throw new Error('Environment key is required and must be a string');
70
- }
64
+ return response.data;
65
+ } catch (error) {
66
+ lastError = error;
71
67
 
72
- const validEnvironments = ['miso', 'dev', 'tst', 'pro'];
73
- if (!validEnvironments.includes(envKey.toLowerCase())) {
74
- throw new Error(`Invalid environment key: ${envKey}. Must be one of: ${validEnvironments.join(', ')}`);
68
+ if (attempt < maxRetries) {
69
+ const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
70
+ logger.log(chalk.yellow(`āš ļø Deployment attempt ${attempt} failed, retrying in ${delay}ms...`));
71
+ await new Promise(resolve => setTimeout(resolve, delay));
72
+ }
73
+ }
75
74
  }
76
75
 
77
- return envKey.toLowerCase();
76
+ // Preserve formatted error if available
77
+ const errorMessage = lastError.formatted || lastError.message;
78
+ const error = new Error(`Deployment failed after ${maxRetries} attempts: ${errorMessage}`);
79
+ if (lastError.formatted) {
80
+ error.formatted = lastError.formatted;
81
+ }
82
+ if (lastError.status) {
83
+ error.status = lastError.status;
84
+ }
85
+ if (lastError.data) {
86
+ error.data = lastError.data;
87
+ }
88
+ throw error;
78
89
  }
79
90
 
80
91
  /**
81
- * Creates deployment request configuration
82
- * @function createDeploymentRequestConfig
83
- * @param {string} clientId - Client ID
84
- * @param {string} clientSecret - Client Secret
85
- * @param {number} timeout - Request timeout
86
- * @returns {Object} Request configuration
92
+ * Validates deployment configuration via validate endpoint
93
+ * This is the first step in the deployment process
94
+ *
95
+ * @async
96
+ * @param {string} url - Controller URL
97
+ * @param {string} envKey - Environment key (miso, dev, tst, pro)
98
+ * @param {Object} manifest - Deployment manifest (applicationConfig)
99
+ * @param {Object} authConfig - Authentication configuration
100
+ * @param {Object} options - Validation options (repositoryUrl, timeout, retries, etc.)
101
+ * @returns {Promise<Object>} Validation result with validateToken
102
+ * @throws {Error} If validation fails
87
103
  */
88
- function createDeploymentRequestConfig(clientId, clientSecret, timeout) {
89
- return {
104
+ async function validateDeployment(url, envKey, manifest, authConfig, options = {}) {
105
+ const validatedEnvKey = validateEnvironmentKey(envKey);
106
+ const endpoint = `${url}/api/v1/pipeline/${validatedEnvKey}/validate`;
107
+ const timeout = options.timeout || 30000;
108
+ const maxRetries = options.maxRetries || 3;
109
+
110
+ // Extract clientId and clientSecret
111
+ const tokenManager = require('./utils/token-manager');
112
+ const { clientId, clientSecret } = await tokenManager.extractClientCredentials(
113
+ authConfig,
114
+ manifest.key,
115
+ validatedEnvKey,
116
+ options
117
+ );
118
+
119
+ // Build validation request
120
+ const repositoryUrl = options.repositoryUrl || `https://github.com/aifabrix/${manifest.key}`;
121
+ const validationRequest = {
122
+ clientId: clientId,
123
+ clientSecret: clientSecret,
124
+ repositoryUrl: repositoryUrl,
125
+ applicationConfig: manifest
126
+ };
127
+
128
+ // Create request config with client credentials headers
129
+ const requestConfig = {
90
130
  headers: {
91
131
  'Content-Type': 'application/json',
92
132
  'User-Agent': 'aifabrix-builder/2.0.0',
93
- ...createClientCredentialsHeaders(clientId, clientSecret)
133
+ 'x-client-id': clientId,
134
+ 'x-client-secret': clientSecret
94
135
  },
95
136
  timeout,
96
- validateStatus: (status) => status < 500
137
+ validateStatus: (status) => status < 600 // Don't throw on any status
97
138
  };
98
- }
99
139
 
100
- /**
101
- * Handles deployment request retry logic
102
- * @async
103
- * @function handleDeploymentRetry
104
- * @param {string} endpoint - API endpoint
105
- * @param {Object} manifest - Deployment manifest
106
- * @param {Object} requestConfig - Request configuration
107
- * @param {number} maxRetries - Maximum retry attempts
108
- * @returns {Promise<Object>} Deployment result
109
- * @throws {Error} If deployment fails after retries
110
- */
111
- async function handleDeploymentRetry(endpoint, manifest, requestConfig, maxRetries) {
112
140
  let lastError;
113
-
114
141
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
115
142
  try {
116
- const response = await axios.post(endpoint, manifest, requestConfig);
143
+ const response = await axios.post(endpoint, validationRequest, requestConfig);
144
+
145
+ // Handle successful validation (200 OK with valid: true)
146
+ if (response.status === 200 && response.data.valid === true) {
147
+ return {
148
+ success: true,
149
+ validateToken: response.data.validateToken,
150
+ draftDeploymentId: response.data.draftDeploymentId,
151
+ imageServer: response.data.imageServer,
152
+ imageUsername: response.data.imageUsername,
153
+ imagePassword: response.data.imagePassword,
154
+ expiresAt: response.data.expiresAt
155
+ };
156
+ }
117
157
 
158
+ // Handle validation errors
118
159
  if (response.status >= 400) {
119
- const error = new Error(`Controller returned error: ${response.status} ${response.statusText}`);
160
+ const error = new Error(`Validation request failed: ${response.status} ${response.statusText}`);
120
161
  error.status = response.status;
121
162
  error.response = {
122
163
  status: response.status,
@@ -126,43 +167,69 @@ async function handleDeploymentRetry(endpoint, manifest, requestConfig, maxRetri
126
167
  error.data = response.data;
127
168
  throw error;
128
169
  }
129
-
130
- return response.data;
131
170
  } catch (error) {
132
171
  lastError = error;
133
-
134
- if (attempt < maxRetries) {
172
+ const shouldRetry = attempt < maxRetries && error.response && error.response.status >= 500;
173
+ if (shouldRetry) {
135
174
  const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
136
- logger.log(chalk.yellow(`āš ļø Deployment attempt ${attempt} failed, retrying in ${delay}ms...`));
175
+ logger.log(chalk.yellow(`āš ļø Validation attempt ${attempt} failed, retrying in ${delay}ms...`));
137
176
  await new Promise(resolve => setTimeout(resolve, delay));
177
+ } else {
178
+ throw error;
138
179
  }
139
180
  }
140
181
  }
141
182
 
142
- throw new Error(`Deployment failed after ${maxRetries} attempts: ${lastError.message}`);
183
+ throw lastError;
143
184
  }
144
185
 
145
186
  /**
146
- * Sends deployment manifest to Miso Controller with Client Credentials authentication
187
+ * Sends deployment request using validateToken from validation step
188
+ * This is the second step in the deployment process
147
189
  *
148
190
  * @async
149
191
  * @param {string} url - Controller URL
150
- * @param {string} envKey - Environment key (dev, tst, pro)
151
- * @param {Object} manifest - Deployment manifest
152
- * @param {string} clientId - Client ID for authentication
153
- * @param {string} clientSecret - Client Secret for authentication
154
- * @param {Object} options - Deployment options (timeout, retries, etc.)
192
+ * @param {string} envKey - Environment key (miso, dev, tst, pro)
193
+ * @param {string} validateToken - Validation token from validate endpoint
194
+ * @param {Object} authConfig - Authentication configuration
195
+ * @param {Object} options - Deployment options (imageTag, timeout, retries, etc.)
155
196
  * @returns {Promise<Object>} Deployment result from controller
156
197
  * @throws {Error} If deployment fails
157
198
  */
158
- async function sendDeploymentRequest(url, envKey, manifest, clientId, clientSecret, options = {}) {
199
+ async function sendDeploymentRequest(url, envKey, validateToken, authConfig, options = {}) {
159
200
  const validatedEnvKey = validateEnvironmentKey(envKey);
160
201
  const endpoint = `${url}/api/v1/pipeline/${validatedEnvKey}/deploy`;
161
202
  const timeout = options.timeout || 30000;
162
203
  const maxRetries = options.maxRetries || 3;
163
- const requestConfig = createDeploymentRequestConfig(clientId, clientSecret, timeout);
164
204
 
165
- return handleDeploymentRetry(endpoint, manifest, requestConfig, maxRetries);
205
+ // Extract clientId and clientSecret for deploy endpoint
206
+ // These should have been loaded during validation and stored in authConfig
207
+ if (!authConfig.clientId || !authConfig.clientSecret) {
208
+ throw new Error('Client ID and Client Secret are required for deployment. These should have been loaded during validation.');
209
+ }
210
+ const clientId = authConfig.clientId;
211
+ const clientSecret = authConfig.clientSecret;
212
+
213
+ // Build deployment request
214
+ const imageTag = options.imageTag || 'latest';
215
+ const deployRequest = {
216
+ validateToken: validateToken,
217
+ imageTag: imageTag
218
+ };
219
+
220
+ // Create request config with client credentials headers
221
+ const requestConfig = {
222
+ headers: {
223
+ 'Content-Type': 'application/json',
224
+ 'User-Agent': 'aifabrix-builder/2.0.0',
225
+ 'x-client-id': clientId,
226
+ 'x-client-secret': clientSecret
227
+ },
228
+ timeout,
229
+ validateStatus: (status) => status < 500
230
+ };
231
+
232
+ return handleDeploymentRetry(endpoint, deployRequest, requestConfig, maxRetries);
166
233
  }
167
234
 
168
235
  /**
@@ -177,39 +244,46 @@ function isTerminalStatus(status) {
177
244
 
178
245
  /**
179
246
  * Polls deployment status from controller
247
+ * Uses pipeline endpoint for CI/CD monitoring with minimal deployment info
180
248
  *
181
249
  * @async
182
250
  * @param {string} deploymentId - Deployment ID to poll
183
251
  * @param {string} controllerUrl - Controller URL
184
252
  * @param {string} envKey - Environment key
185
- * @param {string} clientId - Client ID for authentication
186
- * @param {string} clientSecret - Client Secret for authentication
253
+ * @param {Object} authConfig - Authentication configuration
187
254
  * @param {Object} options - Polling options (interval, maxAttempts, etc.)
188
255
  * @returns {Promise<Object>} Deployment status
189
256
  */
190
- async function pollDeploymentStatus(deploymentId, controllerUrl, envKey, clientId, clientSecret, options = {}) {
257
+ async function pollDeploymentStatus(deploymentId, controllerUrl, envKey, authConfig, options = {}) {
191
258
  const interval = options.interval || 5000;
192
259
  const maxAttempts = options.maxAttempts || 60;
193
260
 
194
261
  const validatedEnvKey = validateEnvironmentKey(envKey);
195
- const statusEndpoint = `${controllerUrl}/api/v1/environments/${validatedEnvKey}/deployments/${deploymentId}`;
262
+ const statusEndpoint = `${controllerUrl}/api/v1/pipeline/${validatedEnvKey}/deployments/${deploymentId}`;
196
263
 
197
264
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
198
265
  try {
199
266
  const response = await axios.get(statusEndpoint, {
200
- headers: createClientCredentialsHeaders(clientId, clientSecret),
267
+ headers: {
268
+ 'Content-Type': 'application/json',
269
+ ...createAuthHeaders(authConfig)
270
+ },
201
271
  timeout: 10000,
202
272
  validateStatus: (status) => status < 500
203
273
  });
204
274
 
205
275
  if (response.status === 200) {
206
- const status = response.data.status;
276
+ // OpenAPI spec: Response structure { success: boolean, data: { status, progress, ... }, timestamp: string }
277
+ const responseData = response.data;
278
+ const deploymentData = responseData.data || responseData;
279
+ const status = deploymentData.status;
207
280
 
208
281
  if (isTerminalStatus(status)) {
209
- return response.data;
282
+ return deploymentData;
210
283
  }
211
284
 
212
- logger.log(chalk.blue(` Status: ${status} (attempt ${attempt + 1}/${maxAttempts})`));
285
+ const progress = deploymentData.progress || 0;
286
+ logger.log(chalk.blue(` Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxAttempts})`));
213
287
 
214
288
  if (attempt < maxAttempts - 1) {
215
289
  await new Promise(resolve => setTimeout(resolve, interval));
@@ -229,46 +303,45 @@ async function pollDeploymentStatus(deploymentId, controllerUrl, envKey, clientI
229
303
  }
230
304
 
231
305
  /**
232
- * Handles deployment errors with security-aware messages
233
- *
234
- * @param {Error} error - Error to handle
235
- * @returns {Object} Structured error information
236
- */
237
- function handleDeploymentError(error) {
238
- const safeError = {
239
- message: error.message,
240
- code: error.code || 'UNKNOWN',
241
- timeout: error.code === 'ECONNABORTED',
242
- status: error.status || error.response?.status,
243
- data: error.data || error.response?.data
244
- };
245
-
246
- // Mask sensitive information in error messages
247
- safeError.message = auditLogger.maskSensitiveData(safeError.message);
248
-
249
- return safeError;
250
- }
251
-
252
- /**
253
- * Sends deployment request to controller
306
+ * Validates and sends deployment request to controller
307
+ * Implements two-step process: validate then deploy
254
308
  * @async
255
309
  * @param {string} url - Controller URL
256
310
  * @param {string} validatedEnvKey - Validated environment key
257
311
  * @param {Object} manifest - Deployment manifest
258
- * @param {string} clientId - Client ID
259
- * @param {string} clientSecret - Client Secret
312
+ * @param {Object} authConfig - Authentication configuration
260
313
  * @param {Object} options - Deployment options
261
314
  * @returns {Promise<Object>} Deployment result
262
315
  */
263
- async function sendDeployment(url, validatedEnvKey, manifest, clientId, clientSecret, options) {
264
- logger.log(chalk.blue(`šŸ“¤ Sending deployment request to ${url}/api/v1/pipeline/${validatedEnvKey}/deploy...`));
265
- const result = await sendDeploymentRequest(url, validatedEnvKey, manifest, clientId, clientSecret, {
316
+ async function sendDeployment(url, validatedEnvKey, manifest, authConfig, options) {
317
+ // Step 1: Validate deployment
318
+ logger.log(chalk.blue('šŸ” Validating deployment configuration...'));
319
+ const validateResult = await validateDeployment(url, validatedEnvKey, manifest, authConfig, {
320
+ repositoryUrl: options.repositoryUrl,
321
+ controllerId: options.controllerId,
322
+ timeout: options.timeout || 30000,
323
+ maxRetries: options.maxRetries || 3
324
+ });
325
+
326
+ if (!validateResult.success || !validateResult.validateToken) {
327
+ throw new Error('Validation failed: validateToken not received');
328
+ }
329
+
330
+ logger.log(chalk.green('āœ“ Validation successful'));
331
+ if (validateResult.draftDeploymentId) {
332
+ logger.log(chalk.gray(` Draft Deployment ID: ${validateResult.draftDeploymentId}`));
333
+ }
334
+
335
+ // Step 2: Deploy using validateToken
336
+ logger.log(chalk.blue('\nšŸš€ Deploying application...'));
337
+ const result = await sendDeploymentRequest(url, validatedEnvKey, validateResult.validateToken, authConfig, {
338
+ imageTag: options.imageTag || 'latest',
266
339
  timeout: options.timeout || 30000,
267
340
  maxRetries: options.maxRetries || 3
268
341
  });
269
342
 
270
343
  if (result.deploymentId) {
271
- auditLogger.logDeploymentSuccess(manifest.key, result.deploymentId, url);
344
+ await auditLogger.logDeploymentSuccess(manifest.key, result.deploymentId, url);
272
345
  }
273
346
 
274
347
  return result;
@@ -280,12 +353,11 @@ async function sendDeployment(url, validatedEnvKey, manifest, clientId, clientSe
280
353
  * @param {Object} result - Deployment result
281
354
  * @param {string} url - Controller URL
282
355
  * @param {string} validatedEnvKey - Validated environment key
283
- * @param {string} clientId - Client ID
284
- * @param {string} clientSecret - Client Secret
356
+ * @param {Object} authConfig - Authentication configuration
285
357
  * @param {Object} options - Deployment options
286
358
  * @returns {Promise<Object>} Deployment result with status
287
359
  */
288
- async function pollDeployment(result, url, validatedEnvKey, clientId, clientSecret, options) {
360
+ async function pollDeployment(result, url, validatedEnvKey, authConfig, options) {
289
361
  if (!options.poll || !result.deploymentId) {
290
362
  return result;
291
363
  }
@@ -295,8 +367,7 @@ async function pollDeployment(result, url, validatedEnvKey, clientId, clientSecr
295
367
  result.deploymentId,
296
368
  url,
297
369
  validatedEnvKey,
298
- clientId,
299
- clientSecret,
370
+ authConfig,
300
371
  {
301
372
  interval: options.pollInterval || 5000,
302
373
  maxAttempts: options.pollMaxAttempts || 60
@@ -308,65 +379,30 @@ async function pollDeployment(result, url, validatedEnvKey, clientId, clientSecr
308
379
  }
309
380
 
310
381
  /**
311
- * Handles deployment errors and maps them to user-friendly messages
312
- * @param {Error} error - Error object
313
- * @param {string} manifestKey - Manifest key for audit logging
314
- * @param {string} url - Controller URL for audit logging
315
- * @throws {Error} User-friendly error message
316
- */
317
- function handleDeploymentErrors(error, manifestKey, url) {
318
- auditLogger.logDeploymentFailure(manifestKey, url, error);
319
-
320
- const safeError = handleDeploymentError(error);
321
-
322
- if (safeError.status === 401 || safeError.status === 403) {
323
- throw new Error('Authentication failed. Check your client ID and client secret.');
324
- }
325
-
326
- if (safeError.status === 400) {
327
- throw new Error('Invalid deployment manifest. Please check your configuration.');
328
- }
329
-
330
- if (safeError.status === 404) {
331
- throw new Error('Controller endpoint not found. Check the controller URL and environment.');
332
- }
333
-
334
- if (safeError.code === 'ECONNREFUSED') {
335
- throw new Error('Cannot connect to controller. Check if the controller is running.');
336
- }
337
-
338
- if (safeError.code === 'ENOTFOUND') {
339
- throw new Error('Controller hostname not found. Check your controller URL.');
340
- }
341
-
342
- if (safeError.timeout) {
343
- throw new Error('Request timed out. The controller may be overloaded.');
344
- }
345
-
346
- throw new Error(safeError.message);
347
- }
348
-
349
- /**
350
- * Deploys application to Miso Controller with Client Credentials authentication
382
+ * Deploys application to Miso Controller with authentication
383
+ * Supports both Bearer token and client credentials authentication
351
384
  * Main orchestrator for the deployment process
352
385
  *
353
386
  * @async
354
387
  * @param {Object} manifest - Deployment manifest
355
388
  * @param {string} controllerUrl - Controller URL
356
- * @param {string} envKey - Environment key (dev, tst, pro)
357
- * @param {string} clientId - Client ID for authentication
358
- * @param {string} clientSecret - Client Secret for authentication
389
+ * @param {string} envKey - Environment key (miso, dev, tst, pro)
390
+ * @param {Object} authConfig - Authentication configuration
391
+ * @param {string} authConfig.type - Auth type: 'bearer' or 'credentials'
392
+ * @param {string} [authConfig.token] - Bearer token (for type 'bearer')
393
+ * @param {string} [authConfig.clientId] - Client ID (for type 'credentials')
394
+ * @param {string} [authConfig.clientSecret] - Client secret (for type 'credentials')
359
395
  * @param {Object} options - Deployment options
360
396
  * @returns {Promise<Object>} Deployment result
361
397
  * @throws {Error} If deployment fails
362
398
  */
363
- async function deployToController(manifest, controllerUrl, envKey, clientId, clientSecret, options = {}) {
399
+ async function deployToController(manifest, controllerUrl, envKey, authConfig, options = {}) {
364
400
  // Validate inputs
365
401
  if (!envKey) {
366
402
  throw new Error('Environment key is required');
367
403
  }
368
- if (!clientId || !clientSecret) {
369
- throw new Error('Client ID and Client Secret are required for authentication');
404
+ if (!authConfig || !authConfig.type) {
405
+ throw new Error('Authentication configuration is required');
370
406
  }
371
407
 
372
408
  // Validate and sanitize controller URL
@@ -374,27 +410,29 @@ async function deployToController(manifest, controllerUrl, envKey, clientId, cli
374
410
  const validatedEnvKey = validateEnvironmentKey(envKey);
375
411
 
376
412
  // Log deployment attempt for audit
377
- auditLogger.logDeploymentAttempt(manifest.key, url, options);
413
+ await auditLogger.logDeploymentAttempt(manifest.key, url, options);
378
414
 
379
415
  try {
380
416
  // Send deployment request
381
- const result = await sendDeployment(url, validatedEnvKey, manifest, clientId, clientSecret, options);
417
+ const result = await sendDeployment(url, validatedEnvKey, manifest, authConfig, options);
382
418
 
383
419
  // Poll for deployment status if enabled
384
- return await pollDeployment(result, url, validatedEnvKey, clientId, clientSecret, options);
420
+ return await pollDeployment(result, url, validatedEnvKey, authConfig, options);
385
421
 
386
422
  } catch (error) {
387
- handleDeploymentErrors(error, manifest.key, url);
423
+ // Use unified error handler (already logged in deployToController, so pass alreadyLogged=true)
424
+ await handleDeploymentErrors(error, manifest.key, url, false);
388
425
  }
389
426
  }
390
427
 
391
428
  module.exports = {
392
429
  deployToController,
393
430
  sendDeploymentRequest,
431
+ validateDeployment,
432
+ handleDeploymentErrors,
394
433
  pollDeploymentStatus,
395
434
  validateControllerUrl,
396
435
  handleDeploymentError,
397
- createClientCredentialsHeaders,
398
436
  validateEnvironmentKey
399
437
  };
400
438