@aifabrix/builder 2.31.1 → 2.32.2

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 (118) hide show
  1. package/README.md +9 -9
  2. package/integration/hubspot/README.md +2 -2
  3. package/integration/hubspot/hubspot-deploy-company.json +17 -14
  4. package/integration/hubspot/hubspot-deploy-contact.json +19 -16
  5. package/integration/hubspot/hubspot-deploy-deal.json +21 -18
  6. package/lib/api/types/datasources.types.js +31 -5
  7. package/lib/api/types/wizard.types.js +142 -0
  8. package/lib/api/wizard.api.js +177 -0
  9. package/lib/{app-config.js → app/config.js} +4 -4
  10. package/lib/{app-deploy.js → app/deploy.js} +8 -8
  11. package/lib/app/display.js +90 -0
  12. package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
  13. package/lib/{app-down.js → app/down.js} +4 -4
  14. package/lib/app/helpers.js +218 -0
  15. package/lib/app/index.js +298 -0
  16. package/lib/{app-list.js → app/list.js} +6 -6
  17. package/lib/{app-push.js → app/push.js} +4 -4
  18. package/lib/{app-readme.js → app/readme.js} +34 -13
  19. package/lib/{app-register.js → app/register.js} +9 -9
  20. package/lib/{app-rotate-secret.js → app/rotate-secret.js} +10 -10
  21. package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
  22. package/lib/{app-run.js → app/run.js} +6 -6
  23. package/lib/{build.js → build/index.js} +59 -32
  24. package/lib/build/package.json +7 -0
  25. package/lib/cli.js +245 -179
  26. package/lib/commands/app.js +3 -3
  27. package/lib/commands/datasource.js +4 -4
  28. package/lib/commands/login-credentials.js +209 -0
  29. package/lib/commands/login-device.js +254 -0
  30. package/lib/commands/login.js +67 -378
  31. package/lib/commands/logout.js +1 -1
  32. package/lib/commands/secrets-set.js +1 -1
  33. package/lib/commands/secure.js +2 -2
  34. package/lib/commands/wizard.js +498 -0
  35. package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
  36. package/lib/{config.js → core/config.js} +28 -26
  37. package/lib/{diff.js → core/diff.js} +157 -72
  38. package/lib/{secrets.js → core/secrets.js} +86 -49
  39. package/lib/{templates.js → core/templates-env.js} +14 -222
  40. package/lib/core/templates.js +279 -0
  41. package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
  42. package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
  43. package/lib/datasource/list.js +223 -0
  44. package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
  45. package/lib/{deployer.js → deployment/deployer.js} +48 -18
  46. package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
  47. package/lib/{push.js → deployment/push.js} +1 -1
  48. package/lib/external-system/deploy-helpers.js +145 -0
  49. package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
  50. package/lib/external-system/download-helpers.js +114 -0
  51. package/lib/{external-system-download.js → external-system/download.js} +92 -135
  52. package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
  53. package/lib/external-system/test-auth.js +40 -0
  54. package/lib/external-system/test-execution.js +84 -0
  55. package/lib/external-system/test-helpers.js +109 -0
  56. package/lib/{external-system-test.js → external-system/test.js} +174 -192
  57. package/lib/{generator-builders.js → generator/builders.js} +87 -10
  58. package/lib/{generator-external.js → generator/external.js} +115 -52
  59. package/lib/{github-generator.js → generator/github.js} +116 -15
  60. package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
  61. package/lib/{generator.js → generator/index.js} +49 -22
  62. package/lib/{generator-split.js → generator/split.js} +108 -55
  63. package/lib/generator/wizard-prompts.js +357 -0
  64. package/lib/generator/wizard.js +490 -0
  65. package/lib/{infra.js → infrastructure/index.js} +49 -22
  66. package/lib/schema/external-datasource.schema.json +158 -136
  67. package/lib/schema/external-system.schema.json +43 -1
  68. package/lib/utils/api.js +9 -5
  69. package/lib/utils/app-register-api.js +60 -32
  70. package/lib/utils/app-register-auth.js +172 -47
  71. package/lib/utils/app-register-config.js +130 -59
  72. package/lib/utils/app-run-containers.js +29 -8
  73. package/lib/utils/build-helpers.js +1 -1
  74. package/lib/utils/cli-utils.js +78 -30
  75. package/lib/utils/compose-generator.js +145 -65
  76. package/lib/utils/config-paths.js +2 -0
  77. package/lib/utils/deployment-errors.js +1 -1
  78. package/lib/utils/device-code.js +99 -41
  79. package/lib/utils/env-config-loader.js +1 -1
  80. package/lib/utils/env-copy.js +21 -18
  81. package/lib/utils/env-endpoints.js +115 -67
  82. package/lib/utils/env-map.js +13 -14
  83. package/lib/utils/env-ports.js +45 -25
  84. package/lib/utils/env-template.js +84 -42
  85. package/lib/utils/error-formatter.js +26 -9
  86. package/lib/utils/error-formatters/error-parser.js +90 -4
  87. package/lib/utils/error-formatters/http-status-errors.js +54 -17
  88. package/lib/utils/error-formatters/network-errors.js +103 -26
  89. package/lib/utils/external-system-display.js +184 -90
  90. package/lib/utils/external-system-validators.js +164 -42
  91. package/lib/utils/file-upload.js +109 -0
  92. package/lib/utils/health-check.js +199 -83
  93. package/lib/utils/infra-containers.js +1 -1
  94. package/lib/utils/infra-status.js +66 -15
  95. package/lib/utils/local-secrets.js +45 -25
  96. package/lib/utils/paths.js +45 -33
  97. package/lib/utils/schema-loader.js +42 -25
  98. package/lib/utils/schema-resolver.js +123 -74
  99. package/lib/utils/secrets-encryption.js +62 -25
  100. package/lib/utils/secrets-helpers.js +126 -63
  101. package/lib/utils/secrets-path.js +1 -1
  102. package/lib/utils/secrets-url.js +1 -1
  103. package/lib/utils/token-manager-refresh.js +181 -0
  104. package/lib/utils/token-manager.js +76 -123
  105. package/lib/utils/variable-transformer.js +154 -77
  106. package/lib/utils/yaml-preserve.js +41 -47
  107. package/lib/{template-validator.js → validation/template.js} +54 -23
  108. package/lib/{validate.js → validation/validate.js} +205 -125
  109. package/lib/{validator.js → validation/validator.js} +58 -39
  110. package/package.json +31 -2
  111. package/templates/external-system/deploy.ps1.hbs +34 -0
  112. package/templates/external-system/deploy.sh.hbs +34 -0
  113. package/templates/external-system/external-datasource.json.hbs +31 -12
  114. package/lib/app.js +0 -467
  115. package/lib/datasource-list.js +0 -141
  116. /package/lib/{app-prompts.js → app/prompts.js} +0 -0
  117. /package/lib/{env-reader.js → core/env-reader.js} +0 -0
  118. /package/lib/{key-generator.js → core/key-generator.js} +0 -0
@@ -12,10 +12,13 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const yaml = require('js-yaml');
15
- const config = require('../config');
16
- const { makeApiCall, refreshDeviceToken: apiRefreshDeviceToken } = require('./api');
15
+ const config = require('../core/config');
17
16
  const logger = require('./logger');
18
17
  const pathsUtil = require('./paths');
18
+ const {
19
+ refreshClientToken,
20
+ refreshDeviceToken
21
+ } = require('./token-manager-refresh');
19
22
 
20
23
  function getSecretsFilePath() {
21
24
  return path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
@@ -100,69 +103,6 @@ function shouldRefreshToken(expiresAt) {
100
103
  return config.shouldRefreshToken(expiresAt);
101
104
  }
102
105
 
103
- /**
104
- * Refresh client token using credentials from secrets.local.yaml
105
- * Gets new token from API and saves it to config.yaml
106
- * @param {string} environment - Environment key
107
- * @param {string} appName - Application name
108
- * @param {string} controllerUrl - Controller URL
109
- * @param {string} [clientId] - Optional client ID (if not provided, loads from secrets.local.yaml)
110
- * @param {string} [clientSecret] - Optional client secret (if not provided, loads from secrets.local.yaml)
111
- * @returns {Promise<{token: string, expiresAt: string}>} New token and expiration
112
- * @throws {Error} If credentials are missing or token refresh fails
113
- */
114
- async function refreshClientToken(environment, appName, controllerUrl, clientId, clientSecret) {
115
- if (!environment || typeof environment !== 'string') {
116
- throw new Error('Environment is required and must be a string');
117
- }
118
- if (!appName || typeof appName !== 'string') {
119
- throw new Error('App name is required and must be a string');
120
- }
121
- if (!controllerUrl || typeof controllerUrl !== 'string') {
122
- throw new Error('Controller URL is required and must be a string');
123
- }
124
-
125
- // Load credentials if not provided
126
- let credentials = null;
127
- if (clientId && clientSecret) {
128
- credentials = { clientId, clientSecret };
129
- } else {
130
- credentials = await loadClientCredentials(appName);
131
- if (!credentials) {
132
- throw new Error(`Client credentials not found for app '${appName}'. Add them to ~/.aifabrix/secrets.local.yaml as '${appName}-client-idKeyVault' and '${appName}-client-secretKeyVault'`);
133
- }
134
- }
135
-
136
- // Call login API to get new token
137
- const response = await makeApiCall(`${controllerUrl}/api/v1/auth/token`, {
138
- method: 'POST',
139
- headers: {
140
- 'Content-Type': 'application/json',
141
- 'x-client-id': credentials.clientId,
142
- 'x-client-secret': credentials.clientSecret
143
- }
144
- });
145
-
146
- if (!response.success) {
147
- throw new Error(`Failed to refresh token: ${response.error || 'Unknown error'}`);
148
- }
149
-
150
- const responseData = response.data;
151
- if (!responseData || !responseData.token) {
152
- throw new Error('Invalid response: missing token');
153
- }
154
-
155
- const token = responseData.token;
156
- // Calculate expiration (default to 24 hours if not provided)
157
- const expiresIn = responseData.expiresIn || 86400;
158
- const expiresAt = responseData.expiresAt || new Date(Date.now() + expiresIn * 1000).toISOString();
159
-
160
- // Save token to config.yaml (NEVER save credentials)
161
- await config.saveClientToken(environment, appName, controllerUrl, token, expiresAt);
162
-
163
- return { token, expiresAt };
164
- }
165
-
166
106
  /**
167
107
  * Get or refresh client token for environment and app
168
108
  * Checks if token exists and is valid, refreshes if expired
@@ -192,53 +132,6 @@ async function getOrRefreshClientToken(environment, appName, controllerUrl) {
192
132
  };
193
133
  }
194
134
 
195
- /**
196
- * Refresh device token using refresh token
197
- * Calls API refresh endpoint and saves new token to config
198
- * @param {string} controllerUrl - Controller URL
199
- * @param {string} refreshToken - Refresh token
200
- * @returns {Promise<{token: string, refreshToken: string, expiresAt: string}>} New token info
201
- * @throws {Error} If refresh fails or refresh token is expired/invalid
202
- */
203
- async function refreshDeviceToken(controllerUrl, refreshToken) {
204
- if (!controllerUrl || typeof controllerUrl !== 'string') {
205
- throw new Error('Controller URL is required');
206
- }
207
- if (!refreshToken || typeof refreshToken !== 'string') {
208
- throw new Error('Refresh token is required');
209
- }
210
-
211
- try {
212
- // Call API refresh endpoint
213
- const tokenResponse = await apiRefreshDeviceToken(controllerUrl, refreshToken);
214
-
215
- const token = tokenResponse.access_token;
216
- const newRefreshToken = tokenResponse.refresh_token || refreshToken; // Use new refresh token if provided, otherwise keep old one
217
- const expiresIn = tokenResponse.expires_in || 3600;
218
- const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
219
-
220
- // Save new token and refresh token to config
221
- await config.saveDeviceToken(controllerUrl, token, newRefreshToken, expiresAt);
222
-
223
- return {
224
- token,
225
- refreshToken: newRefreshToken,
226
- expiresAt
227
- };
228
- } catch (error) {
229
- // Check if error indicates refresh token expiry (case-insensitive)
230
- const errorMessage = (error.message || String(error)).toLowerCase();
231
- if (errorMessage.includes('expired') ||
232
- errorMessage.includes('invalid') ||
233
- errorMessage.includes('401') ||
234
- errorMessage.includes('unauthorized')) {
235
- throw new Error('Refresh token has expired. Please login again using: aifabrix login');
236
- }
237
- // Re-throw other errors as-is
238
- throw error;
239
- }
240
- }
241
-
242
135
  /**
243
136
  * Get or refresh device token for controller
244
137
  * Checks if token exists and is valid, refreshes proactively if within 15 minutes of expiry
@@ -293,18 +186,14 @@ async function getOrRefreshDeviceToken(controllerUrl) {
293
186
  }
294
187
 
295
188
  /**
296
- * Get deployment authentication configuration with priority:
297
- * 1. Device token (Bearer) - for user-level audit tracking (preferred)
298
- * 2. Client token (Bearer) - for application-level authentication
299
- * 3. Client credentials (x-client-id/x-client-secret) - direct credential authentication
300
- *
189
+ * Validates deployment auth parameters
190
+ * @function validateDeploymentAuthParams
301
191
  * @param {string} controllerUrl - Controller URL
302
192
  * @param {string} environment - Environment key
303
193
  * @param {string} appName - Application name
304
- * @returns {Promise<{type: 'bearer'|'client-credentials', token?: string, clientId?: string, clientSecret?: string, controller: string}>} Auth configuration
305
- * @throws {Error} If no authentication method is available
194
+ * @throws {Error} If validation fails
306
195
  */
307
- async function getDeploymentAuth(controllerUrl, environment, appName) {
196
+ function validateDeploymentAuthParams(controllerUrl, environment, appName) {
308
197
  if (!controllerUrl || typeof controllerUrl !== 'string') {
309
198
  throw new Error('Controller URL is required');
310
199
  }
@@ -314,8 +203,16 @@ async function getDeploymentAuth(controllerUrl, environment, appName) {
314
203
  if (!appName || typeof appName !== 'string') {
315
204
  throw new Error('App name is required');
316
205
  }
206
+ }
317
207
 
318
- // Priority 1: Try device token (for user-level audit)
208
+ /**
209
+ * Tries to get device token for deployment auth
210
+ * @async
211
+ * @function tryDeviceTokenAuth
212
+ * @param {string} controllerUrl - Controller URL
213
+ * @returns {Promise<Object|null>} Auth config with device token or null
214
+ */
215
+ async function tryDeviceTokenAuth(controllerUrl) {
319
216
  const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
320
217
  if (deviceToken && deviceToken.token) {
321
218
  return {
@@ -324,8 +221,19 @@ async function getDeploymentAuth(controllerUrl, environment, appName) {
324
221
  controller: deviceToken.controller
325
222
  };
326
223
  }
224
+ return null;
225
+ }
327
226
 
328
- // Priority 2: Try client token (application-level)
227
+ /**
228
+ * Tries to get client token for deployment auth
229
+ * @async
230
+ * @function tryClientTokenAuth
231
+ * @param {string} environment - Environment key
232
+ * @param {string} appName - Application name
233
+ * @param {string} controllerUrl - Controller URL
234
+ * @returns {Promise<Object|null>} Auth config with client token or null
235
+ */
236
+ async function tryClientTokenAuth(environment, appName, controllerUrl) {
329
237
  try {
330
238
  const clientToken = await getOrRefreshClientToken(environment, appName, controllerUrl);
331
239
  if (clientToken && clientToken.token) {
@@ -339,8 +247,18 @@ async function getDeploymentAuth(controllerUrl, environment, appName) {
339
247
  // Client token unavailable, continue to credentials
340
248
  logger.warn(`Client token unavailable: ${error.message}`);
341
249
  }
250
+ return null;
251
+ }
342
252
 
343
- // Priority 3: Use client credentials directly
253
+ /**
254
+ * Tries to get client credentials for deployment auth
255
+ * @async
256
+ * @function tryClientCredentialsAuth
257
+ * @param {string} appName - Application name
258
+ * @param {string} controllerUrl - Controller URL
259
+ * @returns {Promise<Object|null>} Auth config with client credentials or null
260
+ */
261
+ async function tryClientCredentialsAuth(appName, controllerUrl) {
344
262
  const credentials = await loadClientCredentials(appName);
345
263
  if (credentials && credentials.clientId && credentials.clientSecret) {
346
264
  return {
@@ -350,6 +268,41 @@ async function getDeploymentAuth(controllerUrl, environment, appName) {
350
268
  controller: controllerUrl
351
269
  };
352
270
  }
271
+ return null;
272
+ }
273
+
274
+ /**
275
+ * Get deployment authentication configuration with priority:
276
+ * 1. Device token (Bearer) - for user-level audit tracking (preferred)
277
+ * 2. Client token (Bearer) - for application-level authentication
278
+ * 3. Client credentials (x-client-id/x-client-secret) - direct credential authentication
279
+ *
280
+ * @param {string} controllerUrl - Controller URL
281
+ * @param {string} environment - Environment key
282
+ * @param {string} appName - Application name
283
+ * @returns {Promise<{type: 'bearer'|'client-credentials', token?: string, clientId?: string, clientSecret?: string, controller: string}>} Auth configuration
284
+ * @throws {Error} If no authentication method is available
285
+ */
286
+ async function getDeploymentAuth(controllerUrl, environment, appName) {
287
+ validateDeploymentAuthParams(controllerUrl, environment, appName);
288
+
289
+ // Priority 1: Try device token (for user-level audit)
290
+ const deviceAuth = await tryDeviceTokenAuth(controllerUrl);
291
+ if (deviceAuth) {
292
+ return deviceAuth;
293
+ }
294
+
295
+ // Priority 2: Try client token (application-level)
296
+ const clientTokenAuth = await tryClientTokenAuth(environment, appName, controllerUrl);
297
+ if (clientTokenAuth) {
298
+ return clientTokenAuth;
299
+ }
300
+
301
+ // Priority 3: Use client credentials directly
302
+ const credentialsAuth = await tryClientCredentialsAuth(appName, controllerUrl);
303
+ if (credentialsAuth) {
304
+ return credentialsAuth;
305
+ }
353
306
 
354
307
  throw new Error(`No authentication method available. Run 'aifabrix login' for device token, or add credentials to ~/.aifabrix/secrets.local.yaml as '${appName}-client-idKeyVault' and '${appName}-client-secretKeyVault'`);
355
308
  }
@@ -26,14 +26,14 @@ function sanitizeAuthType(authType) {
26
26
  }
27
27
 
28
28
  /**
29
- * Transforms flat structure to schema-compatible format
30
- * @function transformFlatStructure
29
+ * Builds base result object from variables
30
+ * @function buildBaseResult
31
31
  * @param {Object} variables - Raw variables from YAML
32
32
  * @param {string} appName - Application name (fallback)
33
- * @returns {Object} Transformed variables matching schema
33
+ * @returns {Object} Base result object
34
34
  */
35
- function transformFlatStructure(variables, appName) {
36
- const result = {
35
+ function buildBaseResult(variables, appName) {
36
+ return {
37
37
  key: variables.key || appName,
38
38
  displayName: variables.displayName || appName,
39
39
  description: variables.description || '',
@@ -47,37 +47,85 @@ function transformFlatStructure(variables, appName) {
47
47
  databases: variables.databases || [],
48
48
  ...variables
49
49
  };
50
+ }
50
51
 
51
- // Sanitize authentication if present
52
- if (result.authentication && result.authentication.type) {
53
- result.authentication = {
54
- ...result.authentication,
55
- type: sanitizeAuthType(result.authentication.type)
56
- };
57
- }
58
- // Handle partial authentication objects (when only enableSSO is provided)
59
- if (result.authentication && result.authentication.enableSSO !== undefined) {
60
- const auth = {
61
- ...result.authentication,
62
- enableSSO: result.authentication.enableSSO
63
- };
64
- // When enableSSO is false, default type to 'none' and requiredRoles to []
65
- // When enableSSO is true, default type to 'azure' if not provided
66
- if (auth.enableSSO === false) {
67
- auth.type = sanitizeAuthType(result.authentication.type || 'none');
68
- auth.requiredRoles = result.authentication.requiredRoles || [];
69
- } else {
70
- auth.type = sanitizeAuthType(result.authentication.type || 'azure');
71
- auth.requiredRoles = result.authentication.requiredRoles || [];
72
- }
73
- result.authentication = auth;
74
- }
75
-
76
- // Add placeholder deploymentKey for validation (will be generated from JSON later)
77
- // This is a 64-character hex string matching the SHA256 pattern
52
+ /**
53
+ * Handles authentication type sanitization
54
+ * @function sanitizeAuthenticationType
55
+ * @param {Object} authentication - Authentication object
56
+ * @returns {Object} Sanitized authentication object
57
+ */
58
+ function sanitizeAuthenticationType(authentication) {
59
+ if (!authentication || !authentication.type) {
60
+ return authentication;
61
+ }
62
+ return {
63
+ ...authentication,
64
+ type: sanitizeAuthType(authentication.type)
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Handles partial authentication objects with enableSSO
70
+ * @function handlePartialAuthentication
71
+ * @param {Object} authentication - Authentication object
72
+ * @returns {Object} Processed authentication object
73
+ */
74
+ function handlePartialAuthentication(authentication) {
75
+ if (!authentication || authentication.enableSSO === undefined) {
76
+ return authentication;
77
+ }
78
+
79
+ const auth = {
80
+ ...authentication,
81
+ enableSSO: authentication.enableSSO
82
+ };
83
+
84
+ // When enableSSO is false, default type to 'none' and requiredRoles to []
85
+ // When enableSSO is true, default type to 'azure' if not provided
86
+ if (auth.enableSSO === false) {
87
+ auth.type = sanitizeAuthType(authentication.type || 'none');
88
+ auth.requiredRoles = authentication.requiredRoles || [];
89
+ } else {
90
+ auth.type = sanitizeAuthType(authentication.type || 'azure');
91
+ auth.requiredRoles = authentication.requiredRoles || [];
92
+ }
93
+
94
+ return auth;
95
+ }
96
+
97
+ /**
98
+ * Adds placeholder deployment key if missing
99
+ * @function addPlaceholderDeploymentKey
100
+ * @param {Object} result - Result object
101
+ * @returns {Object} Result object with deployment key
102
+ */
103
+ function addPlaceholderDeploymentKey(result) {
78
104
  if (!result.deploymentKey) {
105
+ // This is a 64-character hex string matching the SHA256 pattern
79
106
  result.deploymentKey = '0000000000000000000000000000000000000000000000000000000000000000';
80
107
  }
108
+ return result;
109
+ }
110
+
111
+ /**
112
+ * Transforms flat structure to schema-compatible format
113
+ * @function transformFlatStructure
114
+ * @param {Object} variables - Raw variables from YAML
115
+ * @param {string} appName - Application name (fallback)
116
+ * @returns {Object} Transformed variables matching schema
117
+ */
118
+ function transformFlatStructure(variables, appName) {
119
+ const result = buildBaseResult(variables, appName);
120
+
121
+ // Sanitize authentication if present
122
+ if (result.authentication) {
123
+ result.authentication = sanitizeAuthenticationType(result.authentication);
124
+ result.authentication = handlePartialAuthentication(result.authentication);
125
+ }
126
+
127
+ // Add placeholder deploymentKey for validation
128
+ addPlaceholderDeploymentKey(result);
81
129
 
82
130
  return result;
83
131
  }
@@ -184,38 +232,37 @@ function validateDeploymentConfig(deployment) {
184
232
  }
185
233
 
186
234
  /**
187
- * Transforms optional fields from variables
188
- * @function transformOptionalFields
189
- * @param {Object} variables - Raw variables from YAML
190
- * @param {Object} transformed - Base transformed object
191
- * @returns {Object} Transformed object with optional fields added
235
+ * Transforms authentication configuration
236
+ * @function transformAuthentication
237
+ * @param {Object} authentication - Authentication configuration
238
+ * @returns {Object} Transformed authentication object
192
239
  */
193
- function transformOptionalFields(variables, transformed) {
194
- if (variables.healthCheck) {
195
- transformed.healthCheck = variables.healthCheck;
196
- }
240
+ function transformAuthentication(authentication) {
241
+ const auth = {
242
+ ...authentication,
243
+ enableSSO: authentication.enableSSO !== undefined ? authentication.enableSSO : true
244
+ };
197
245
 
198
- if (variables.authentication) {
199
- // Ensure authentication object has enableSSO at minimum
200
- // Default type and requiredRoles based on enableSSO value
201
- const auth = {
202
- ...variables.authentication,
203
- enableSSO: variables.authentication.enableSSO !== undefined ? variables.authentication.enableSSO : true
204
- };
205
-
206
- // When enableSSO is false, default type to 'none' and requiredRoles to []
207
- // When enableSSO is true, default type to 'azure' if not provided
208
- if (auth.enableSSO === false) {
209
- auth.type = sanitizeAuthType(variables.authentication.type || 'none');
210
- auth.requiredRoles = variables.authentication.requiredRoles || [];
211
- } else {
212
- auth.type = sanitizeAuthType(variables.authentication.type || 'azure');
213
- auth.requiredRoles = variables.authentication.requiredRoles || [];
214
- }
215
-
216
- transformed.authentication = auth;
246
+ // When enableSSO is false, default type to 'none' and requiredRoles to []
247
+ // When enableSSO is true, default type to 'azure' if not provided
248
+ if (auth.enableSSO === false) {
249
+ auth.type = sanitizeAuthType(authentication.type || 'none');
250
+ auth.requiredRoles = authentication.requiredRoles || [];
251
+ } else {
252
+ auth.type = sanitizeAuthType(authentication.type || 'azure');
253
+ auth.requiredRoles = authentication.requiredRoles || [];
217
254
  }
218
255
 
256
+ return auth;
257
+ }
258
+
259
+ /**
260
+ * Transforms configuration sections (repository, build, deployment)
261
+ * @function transformConfigSections
262
+ * @param {Object} variables - Raw variables from YAML
263
+ * @param {Object} transformed - Base transformed object
264
+ */
265
+ function transformConfigSections(variables, transformed) {
219
266
  const repository = validateRepositoryConfig(variables.repository);
220
267
  if (repository) {
221
268
  transformed.repository = repository;
@@ -230,7 +277,15 @@ function transformOptionalFields(variables, transformed) {
230
277
  if (deployment) {
231
278
  transformed.deployment = deployment;
232
279
  }
280
+ }
233
281
 
282
+ /**
283
+ * Transforms simple optional fields
284
+ * @function transformSimpleOptionalFields
285
+ * @param {Object} variables - Raw variables from YAML
286
+ * @param {Object} transformed - Base transformed object
287
+ */
288
+ function transformSimpleOptionalFields(variables, transformed) {
234
289
  if (variables.startupCommand) {
235
290
  transformed.startupCommand = variables.startupCommand;
236
291
  }
@@ -249,6 +304,26 @@ function transformOptionalFields(variables, transformed) {
249
304
  if (variables.permissions) {
250
305
  transformed.permissions = variables.permissions;
251
306
  }
307
+ }
308
+
309
+ /**
310
+ * Transforms optional fields from variables
311
+ * @function transformOptionalFields
312
+ * @param {Object} variables - Raw variables from YAML
313
+ * @param {Object} transformed - Base transformed object
314
+ * @returns {Object} Transformed object with optional fields added
315
+ */
316
+ function transformOptionalFields(variables, transformed) {
317
+ if (variables.healthCheck) {
318
+ transformed.healthCheck = variables.healthCheck;
319
+ }
320
+
321
+ if (variables.authentication) {
322
+ transformed.authentication = transformAuthentication(variables.authentication);
323
+ }
324
+
325
+ transformConfigSections(variables, transformed);
326
+ transformSimpleOptionalFields(variables, transformed);
252
327
 
253
328
  return transformed;
254
329
  }
@@ -263,20 +338,18 @@ function transformOptionalFields(variables, transformed) {
263
338
  * @param {string} appName - Application name (fallback)
264
339
  * @returns {Object} Transformed variables matching schema
265
340
  */
266
- function transformVariablesForValidation(variables, appName) {
267
- // Check if structure is already flat (has key, displayName, image as string)
268
- const isFlat = variables.key && variables.image && typeof variables.image === 'string';
269
-
270
- if (isFlat) {
271
- return transformFlatStructure(variables, appName);
272
- }
273
-
274
- // Nested structure - transform it
341
+ /**
342
+ * Builds base transformed structure from nested variables
343
+ * @function buildBaseTransformedStructure
344
+ * @param {Object} variables - Variables object
345
+ * @param {string} appName - Application name
346
+ * @returns {Object} Base transformed structure
347
+ */
348
+ function buildBaseTransformedStructure(variables, appName) {
275
349
  const requires = variables.requires || {};
276
350
  const imageRef = buildImageReference(variables, appName);
277
351
 
278
- // Transform to flat schema structure
279
- const transformed = {
352
+ return {
280
353
  key: variables.app?.key || appName,
281
354
  displayName: variables.app?.displayName || appName,
282
355
  description: variables.app?.description || '',
@@ -289,16 +362,20 @@ function transformVariablesForValidation(variables, appName) {
289
362
  requiresStorage: requires.storage || false,
290
363
  databases: requires.databases || (requires.database ? [{ name: variables.app?.key || appName }] : [])
291
364
  };
365
+ }
292
366
 
293
- const result = transformOptionalFields(variables, transformed);
367
+ function transformVariablesForValidation(variables, appName) {
368
+ // Check if structure is already flat
369
+ const isFlat = variables.key && variables.image && typeof variables.image === 'string';
294
370
 
295
- // Add placeholder deploymentKey for validation (will be generated from JSON later)
296
- // This is a 64-character hex string matching the SHA256 pattern
297
- if (!result.deploymentKey) {
298
- result.deploymentKey = '0000000000000000000000000000000000000000000000000000000000000000';
371
+ if (isFlat) {
372
+ return transformFlatStructure(variables, appName);
299
373
  }
300
374
 
301
- return result;
375
+ // Nested structure - transform it
376
+ const transformed = buildBaseTransformedStructure(variables, appName);
377
+ const result = transformOptionalFields(variables, transformed);
378
+ return addPlaceholderDeploymentKey(result);
302
379
  }
303
380
 
304
381
  module.exports = {
@@ -185,61 +185,55 @@ function formatValue(value, quoted, quoteChar) {
185
185
  * const result = encryptYamlValues(yamlContent, encryptionKey);
186
186
  * // Returns: { content: '...', encrypted: 5, total: 10 }
187
187
  */
188
+ /**
189
+ * Processes a single line for encryption
190
+ * @function processLineForEncryption
191
+ * @param {string} line - Line to process
192
+ * @param {string} encryptionKey - Encryption key
193
+ * @param {Object} stats - Statistics object
194
+ * @returns {string} Processed line
195
+ */
196
+ function processLineForEncryption(line, encryptionKey, stats) {
197
+ const trimmed = line.trim();
198
+
199
+ // Preserve empty lines and comment-only lines
200
+ if (trimmed === '' || trimmed.startsWith('#')) {
201
+ return line;
202
+ }
203
+
204
+ const kvPattern = /^(\s*)([^#:\n]+?):\s*(.+?)(\s*)(#.*)?$/;
205
+ const match = line.match(kvPattern);
206
+ if (!match) {
207
+ return line;
208
+ }
209
+
210
+ stats.total++;
211
+ const [, indent, key, valuePart, trailingWhitespace, comment] = match;
212
+ const { value, quoted, quoteChar } = extractValue(valuePart);
213
+
214
+ if (shouldEncryptValue(value)) {
215
+ const encryptedValue = encryptSecret(value, encryptionKey);
216
+ const formattedValue = formatValue(encryptedValue, quoted, quoteChar);
217
+ stats.encrypted++;
218
+ return `${indent}${key}: ${formattedValue}${trailingWhitespace}${comment || ''}`;
219
+ }
220
+
221
+ return line;
222
+ }
223
+
188
224
  function encryptYamlValues(content, encryptionKey) {
189
225
  const lines = content.split(/\r?\n/);
190
226
  const encryptedLines = [];
191
- let encryptedCount = 0;
192
- let totalCount = 0;
193
-
194
- // Pattern to match key-value pairs with optional comments
195
- // Matches: indentation, key, colon, value, optional whitespace, optional comment
196
- // Handles: key: value, key: "value", key: value # comment, etc.
197
- const kvPattern = /^(\s*)([^#:\n]+?):\s*(.+?)(\s*)(#.*)?$/;
227
+ const stats = { encrypted: 0, total: 0 };
198
228
 
199
- for (let i = 0; i < lines.length; i++) {
200
- const line = lines[i];
201
- const trimmed = line.trim();
202
-
203
- // Preserve empty lines and comment-only lines
204
- if (trimmed === '' || trimmed.startsWith('#')) {
205
- encryptedLines.push(line);
206
- continue;
207
- }
208
-
209
- // Try to match key-value pattern
210
- const match = line.match(kvPattern);
211
- if (match) {
212
- totalCount++;
213
- const [, indent, key, valuePart, trailingWhitespace, comment] = match;
214
-
215
- // Extract value (handle quotes)
216
- const { value, quoted, quoteChar } = extractValue(valuePart);
217
-
218
- // Check if value should be encrypted
219
- if (shouldEncryptValue(value)) {
220
- // Encrypt the value
221
- const encryptedValue = encryptSecret(value, encryptionKey);
222
- const formattedValue = formatValue(encryptedValue, quoted, quoteChar);
223
-
224
- // Reconstruct line with encrypted value
225
- const encryptedLine = `${indent}${key}: ${formattedValue}${trailingWhitespace}${comment || ''}`;
226
- encryptedLines.push(encryptedLine);
227
- encryptedCount++;
228
- } else {
229
- // Keep original line (already encrypted, URL, or non-string)
230
- encryptedLines.push(line);
231
- }
232
- } else {
233
- // Line doesn't match pattern (multiline value, complex structure, etc.)
234
- // Preserve as-is
235
- encryptedLines.push(line);
236
- }
229
+ for (const line of lines) {
230
+ encryptedLines.push(processLineForEncryption(line, encryptionKey, stats));
237
231
  }
238
232
 
239
233
  return {
240
234
  content: encryptedLines.join('\n'),
241
- encrypted: encryptedCount,
242
- total: totalCount
235
+ encrypted: stats.encrypted,
236
+ total: stats.total
243
237
  };
244
238
  }
245
239