@aifabrix/builder 2.31.0 → 2.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/integration/hubspot/README.md +2 -2
- package/integration/hubspot/hubspot-deploy-company.json +17 -14
- package/integration/hubspot/hubspot-deploy-contact.json +19 -16
- package/integration/hubspot/hubspot-deploy-deal.json +21 -18
- package/lib/api/types/datasources.types.js +31 -5
- package/lib/api/types/wizard.types.js +142 -0
- package/lib/api/wizard.api.js +177 -0
- package/lib/{app-config.js → app/config.js} +4 -4
- package/lib/{app-deploy.js → app/deploy.js} +8 -8
- package/lib/app/display.js +90 -0
- package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
- package/lib/{app-down.js → app/down.js} +4 -4
- package/lib/app/helpers.js +218 -0
- package/lib/app/index.js +298 -0
- package/lib/{app-list.js → app/list.js} +6 -6
- package/lib/{app-push.js → app/push.js} +4 -4
- package/lib/{app-readme.js → app/readme.js} +34 -13
- package/lib/{app-register.js → app/register.js} +9 -9
- package/lib/{app-rotate-secret.js → app/rotate-secret.js} +123 -37
- package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
- package/lib/{app-run.js → app/run.js} +6 -6
- package/lib/{build.js → build/index.js} +59 -32
- package/lib/build/package.json +7 -0
- package/lib/cli.js +245 -179
- package/lib/commands/app.js +3 -3
- package/lib/commands/datasource.js +4 -4
- package/lib/commands/login-credentials.js +209 -0
- package/lib/commands/login-device.js +254 -0
- package/lib/commands/login.js +67 -378
- package/lib/commands/logout.js +1 -1
- package/lib/commands/secrets-set.js +1 -1
- package/lib/commands/secure.js +2 -2
- package/lib/commands/wizard.js +498 -0
- package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
- package/lib/{config.js → core/config.js} +28 -26
- package/lib/{diff.js → core/diff.js} +157 -72
- package/lib/{secrets.js → core/secrets.js} +86 -49
- package/lib/{templates.js → core/templates-env.js} +14 -222
- package/lib/core/templates.js +279 -0
- package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
- package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
- package/lib/datasource/list.js +223 -0
- package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
- package/lib/{deployer.js → deployment/deployer.js} +48 -18
- package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
- package/lib/{push.js → deployment/push.js} +1 -1
- package/lib/external-system/deploy-helpers.js +145 -0
- package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
- package/lib/external-system/download-helpers.js +114 -0
- package/lib/{external-system-download.js → external-system/download.js} +92 -135
- package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
- package/lib/external-system/test-auth.js +40 -0
- package/lib/external-system/test-execution.js +84 -0
- package/lib/external-system/test-helpers.js +109 -0
- package/lib/{external-system-test.js → external-system/test.js} +174 -192
- package/lib/{generator-builders.js → generator/builders.js} +87 -10
- package/lib/{generator-external.js → generator/external.js} +115 -52
- package/lib/{github-generator.js → generator/github.js} +116 -15
- package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
- package/lib/{generator.js → generator/index.js} +49 -22
- package/lib/{generator-split.js → generator/split.js} +108 -55
- package/lib/generator/wizard-prompts.js +357 -0
- package/lib/generator/wizard.js +490 -0
- package/lib/{infra.js → infrastructure/index.js} +49 -22
- package/lib/schema/external-datasource.schema.json +145 -133
- package/lib/schema/external-system.schema.json +42 -0
- package/lib/utils/api.js +9 -5
- package/lib/utils/app-register-api.js +60 -32
- package/lib/utils/app-register-auth.js +172 -47
- package/lib/utils/app-register-config.js +130 -59
- package/lib/utils/app-run-containers.js +29 -8
- package/lib/utils/build-helpers.js +1 -1
- package/lib/utils/cli-utils.js +78 -30
- package/lib/utils/compose-generator.js +145 -65
- package/lib/utils/config-paths.js +2 -0
- package/lib/utils/deployment-errors.js +1 -1
- package/lib/utils/device-code.js +99 -41
- package/lib/utils/env-config-loader.js +1 -1
- package/lib/utils/env-copy.js +21 -18
- package/lib/utils/env-endpoints.js +115 -67
- package/lib/utils/env-map.js +13 -14
- package/lib/utils/env-ports.js +45 -25
- package/lib/utils/env-template.js +84 -42
- package/lib/utils/error-formatter.js +26 -9
- package/lib/utils/error-formatters/error-parser.js +90 -4
- package/lib/utils/error-formatters/http-status-errors.js +54 -17
- package/lib/utils/error-formatters/network-errors.js +103 -26
- package/lib/utils/external-system-display.js +184 -90
- package/lib/utils/external-system-validators.js +164 -42
- package/lib/utils/file-upload.js +109 -0
- package/lib/utils/health-check.js +199 -83
- package/lib/utils/infra-containers.js +1 -1
- package/lib/utils/infra-status.js +66 -15
- package/lib/utils/local-secrets.js +45 -25
- package/lib/utils/paths.js +45 -33
- package/lib/utils/schema-loader.js +42 -25
- package/lib/utils/schema-resolver.js +123 -74
- package/lib/utils/secrets-encryption.js +62 -25
- package/lib/utils/secrets-helpers.js +126 -63
- package/lib/utils/secrets-path.js +1 -1
- package/lib/utils/secrets-url.js +1 -1
- package/lib/utils/token-manager-refresh.js +181 -0
- package/lib/utils/token-manager.js +76 -123
- package/lib/utils/variable-transformer.js +154 -77
- package/lib/utils/yaml-preserve.js +41 -47
- package/lib/{template-validator.js → validation/template.js} +54 -23
- package/lib/{validate.js → validation/validate.js} +205 -125
- package/lib/{validator.js → validation/validator.js} +58 -39
- package/package.json +34 -3
- package/scripts/install-local.js +210 -0
- package/templates/external-system/deploy.ps1.hbs +34 -0
- package/templates/external-system/deploy.sh.hbs +34 -0
- package/templates/external-system/external-datasource.json.hbs +31 -12
- package/lib/app.js +0 -467
- package/lib/datasource-list.js +0 -141
- /package/lib/{app-prompts.js → app/prompts.js} +0 -0
- /package/lib/{env-reader.js → core/env-reader.js} +0 -0
- /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
|
-
*
|
|
297
|
-
*
|
|
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
|
-
* @
|
|
305
|
-
* @throws {Error} If no authentication method is available
|
|
194
|
+
* @throws {Error} If validation fails
|
|
306
195
|
*/
|
|
307
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
30
|
-
* @function
|
|
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}
|
|
33
|
+
* @returns {Object} Base result object
|
|
34
34
|
*/
|
|
35
|
-
function
|
|
36
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
188
|
-
* @function
|
|
189
|
-
* @param {Object}
|
|
190
|
-
* @
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
240
|
+
function transformAuthentication(authentication) {
|
|
241
|
+
const auth = {
|
|
242
|
+
...authentication,
|
|
243
|
+
enableSSO: authentication.enableSSO !== undefined ? authentication.enableSSO : true
|
|
244
|
+
};
|
|
197
245
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
if (!result.deploymentKey) {
|
|
298
|
-
result.deploymentKey = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
371
|
+
if (isFlat) {
|
|
372
|
+
return transformFlatStructure(variables, appName);
|
|
299
373
|
}
|
|
300
374
|
|
|
301
|
-
|
|
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
|
-
|
|
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 (
|
|
200
|
-
|
|
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:
|
|
242
|
-
total:
|
|
235
|
+
encrypted: stats.encrypted,
|
|
236
|
+
total: stats.total
|
|
243
237
|
};
|
|
244
238
|
}
|
|
245
239
|
|