@aifabrix/builder 2.42.0 → 2.43.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.
- package/README.md +1 -1
- package/bin/aifabrix.js +1 -1
- package/integration/hubspot-test/README.md +126 -0
- package/integration/{hubspot → hubspot-test}/application.json +6 -6
- package/integration/{hubspot → hubspot-test}/create-hubspot.js +5 -5
- package/integration/hubspot-test/env.template +4 -0
- package/integration/{hubspot/hubspot-datasource-company.json → hubspot-test/hubspot-test-datasource-company.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-contact.json → hubspot-test/hubspot-test-datasource-contact.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-deal.json → hubspot-test/hubspot-test-datasource-deal.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-users.json → hubspot-test/hubspot-test-datasource-users.json} +3 -2
- package/integration/{hubspot/hubspot-deploy.json → hubspot-test/hubspot-test-deploy.json} +198 -21
- package/integration/{hubspot/hubspot-system.json → hubspot-test/hubspot-test-system.json} +8 -7
- package/integration/hubspot-test/rbac.json +166 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-credential-real.yaml +3 -3
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-env-vars.yaml +2 -2
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-add-datasource.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-create.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-select.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-known-platform.yaml +1 -1
- package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-mode.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-file.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-url.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-source.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-array-test.yaml +1 -1
- package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
- package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test.js +102 -59
- package/integration/{hubspot → hubspot-test}/wizard-hubspot-e2e.yaml +2 -2
- package/integration/{hubspot → hubspot-test}/wizard-hubspot-platform.yaml +1 -1
- package/lib/api/external-test.api.js +1 -1
- package/lib/api/service-users.api.js +111 -2
- package/lib/api/types/service-users.types.js +41 -0
- package/lib/api/wizard.api.js +2 -1
- package/lib/app/index.js +2 -2
- package/lib/app/prompts.js +2 -2
- package/lib/app/readme.js +3 -1
- package/lib/app/register.js +3 -1
- package/lib/app/rotate-secret.js +3 -0
- package/lib/cli/setup-app.js +5 -5
- package/lib/cli/setup-auth.js +19 -11
- package/lib/cli/setup-dev.js +62 -32
- package/lib/cli/setup-environment.js +6 -21
- package/lib/cli/setup-infra.js +13 -0
- package/lib/cli/setup-secrets.js +45 -6
- package/lib/cli/setup-service-user.js +146 -20
- package/lib/cli/setup-utility.js +12 -0
- package/lib/commands/auth-config.js +25 -19
- package/lib/commands/datasource.js +46 -1
- package/lib/commands/dev-init.js +1 -1
- package/lib/commands/repair-env-template.js +14 -8
- package/lib/commands/repair-rbac.js +25 -19
- package/lib/commands/repair.js +108 -31
- package/lib/commands/secrets-remove.js +1 -1
- package/lib/commands/secrets-set.js +6 -0
- package/lib/commands/secrets-validate.js +17 -4
- package/lib/commands/service-user.js +231 -2
- package/lib/commands/up-common.js +25 -0
- package/lib/commands/up-dataplane.js +91 -7
- package/lib/commands/wizard-core-helpers.js +5 -2
- package/lib/commands/wizard-core.js +2 -1
- package/lib/commands/wizard-headless.js +6 -1
- package/lib/commands/wizard.js +13 -6
- package/lib/core/admin-secrets.js +2 -0
- package/lib/core/config.js +7 -5
- package/lib/core/ensure-encryption-key.js +1 -3
- package/lib/core/secrets.js +32 -9
- package/lib/core/templates.js +1 -1
- package/lib/datasource/abac-validator.js +157 -0
- package/lib/datasource/field-reference-validator.js +74 -36
- package/lib/datasource/log-viewer.js +221 -0
- package/lib/datasource/resolve-app.js +109 -0
- package/lib/datasource/test-e2e.js +11 -20
- package/lib/datasource/test-integration.js +42 -22
- package/lib/datasource/validate.js +5 -2
- package/lib/external-system/download-helpers.js +3 -1
- package/lib/external-system/generator.js +12 -8
- package/lib/external-system/test-system-level.js +1 -1
- package/lib/generator/external-controller-manifest.js +3 -3
- package/lib/generator/external-schema-utils.js +3 -1
- package/lib/generator/external.js +7 -7
- package/lib/generator/helpers.js +13 -9
- package/lib/generator/index.js +4 -4
- package/lib/generator/split.js +45 -10
- package/lib/generator/wizard-prompts-secondary.js +39 -7
- package/lib/generator/wizard-readme.js +4 -1
- package/lib/generator/wizard.js +68 -53
- package/lib/infrastructure/helpers.js +50 -35
- package/lib/infrastructure/index.js +39 -23
- package/lib/schema/env-config.yaml +19 -2
- package/lib/schema/external-datasource.schema.json +11 -1
- package/lib/schema/wizard-config.schema.json +7 -1
- package/lib/utils/app-config-resolver.js +23 -1
- package/lib/utils/config-paths.js +48 -4
- package/lib/utils/credential-secrets-env.js +16 -1
- package/lib/utils/env-map.js +7 -3
- package/lib/utils/error-formatter.js +37 -0
- package/lib/utils/external-env-template.js +180 -0
- package/lib/utils/external-readme.js +33 -1
- package/lib/utils/external-system-display.js +43 -0
- package/lib/utils/external-system-validators.js +2 -2
- package/lib/utils/help-builder.js +3 -5
- package/lib/utils/local-secrets.js +26 -3
- package/lib/utils/paths.js +2 -1
- package/lib/utils/secrets-generator.js +2 -2
- package/lib/utils/secrets-utils.js +4 -0
- package/lib/utils/secure-file-permissions.js +91 -0
- package/lib/utils/token-manager.js +36 -3
- package/lib/utils/yaml-preserve.js +59 -1
- package/lib/validation/env-template-auth.js +50 -2
- package/lib/validation/external-manifest-validator.js +8 -0
- package/lib/validation/validate.js +8 -0
- package/lib/validation/validator.js +10 -13
- package/package.json +6 -2
- package/templates/applications/dataplane/env.template +5 -1
- package/templates/applications/miso-controller/application.yaml +1 -1
- package/templates/applications/miso-controller/env.template +13 -2
- package/templates/external-system/README.md.hbs +18 -5
- package/templates/external-system/env.template.hbs +22 -0
- package/integration/hubspot/README.md +0 -100
- package/integration/hubspot/env.template +0 -4
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
- /package/integration/{hubspot → hubspot-test}/companies.json +0 -0
- /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-app-name.yaml +0 -0
- /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-missing-app.yaml +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down-helpers.js +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down-tests.js +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down.js +0 -0
|
@@ -37,8 +37,34 @@ const {
|
|
|
37
37
|
startDockerServicesAndConfigure,
|
|
38
38
|
checkInfraHealth
|
|
39
39
|
} = require('./services');
|
|
40
|
+
const adminSecrets = require('../core/admin-secrets');
|
|
40
41
|
// Lazy require to avoid circular dependency: infra -> app/down -> run-helpers -> infra
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Runs a callback with a temporary .env.run file in infraDir (created from admin-secrets).
|
|
45
|
+
* Removes the file in a finally block.
|
|
46
|
+
* @async
|
|
47
|
+
* @param {string} infraDir - Infrastructure directory path
|
|
48
|
+
* @param {string} adminSecretsPath - Path to admin-secrets.env
|
|
49
|
+
* @param {function(string): Promise<void>} fn - Callback receiving runEnvPath
|
|
50
|
+
* @returns {Promise<void>}
|
|
51
|
+
*/
|
|
52
|
+
async function withRunEnv(infraDir, adminSecretsPath, fn) {
|
|
53
|
+
const runEnvPath = path.join(infraDir, '.env.run');
|
|
54
|
+
try {
|
|
55
|
+
const adminObj = await adminSecrets.readAndDecryptAdminSecrets(adminSecretsPath);
|
|
56
|
+
const content = adminSecrets.envObjectToContent(adminObj);
|
|
57
|
+
fs.writeFileSync(runEnvPath, content, { mode: 0o600 });
|
|
58
|
+
await fn(runEnvPath);
|
|
59
|
+
} finally {
|
|
60
|
+
try {
|
|
61
|
+
if (fs.existsSync(runEnvPath)) fs.unlinkSync(runEnvPath);
|
|
62
|
+
} catch {
|
|
63
|
+
// Ignore unlink errors
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
42
68
|
/**
|
|
43
69
|
* Prepares infrastructure environment
|
|
44
70
|
* Ensures infra secrets exist, then admin-secrets.env, then miso init script.
|
|
@@ -65,7 +91,7 @@ async function prepareInfrastructureEnvironment(developerId, options = {}) {
|
|
|
65
91
|
}
|
|
66
92
|
|
|
67
93
|
// Prepare infrastructure directory
|
|
68
|
-
const { infraDir } = prepareInfraDirectory(devId, adminSecretsPath);
|
|
94
|
+
const { infraDir } = await prepareInfraDirectory(devId, adminSecretsPath);
|
|
69
95
|
await ensureMisoInitScript(infraDir);
|
|
70
96
|
|
|
71
97
|
return { devId, idNum, ports, templatePath, infraDir, adminSecretsPath };
|
|
@@ -174,8 +200,7 @@ async function removeAppVolumes(appNames, devId) {
|
|
|
174
200
|
async function stopInfra() {
|
|
175
201
|
const devId = await config.getDeveloperId();
|
|
176
202
|
const aifabrixDir = paths.getAifabrixHome();
|
|
177
|
-
const
|
|
178
|
-
const infraDir = path.join(aifabrixDir, infraDirName);
|
|
203
|
+
const infraDir = path.join(aifabrixDir, getInfraDirName(devId));
|
|
179
204
|
const composePath = path.join(infraDir, 'compose.yaml');
|
|
180
205
|
const adminSecretsPath = path.join(aifabrixDir, 'admin-secrets.env');
|
|
181
206
|
|
|
@@ -184,17 +209,15 @@ async function stopInfra() {
|
|
|
184
209
|
return;
|
|
185
210
|
}
|
|
186
211
|
|
|
187
|
-
|
|
212
|
+
await withRunEnv(infraDir, adminSecretsPath, async(runEnvPath) => {
|
|
188
213
|
logger.log('Stopping application containers on the same network...');
|
|
189
214
|
await stopAllAppContainers(devId);
|
|
190
215
|
logger.log('Stopping infrastructure services...');
|
|
191
216
|
const projectName = getInfraProjectName(devId);
|
|
192
217
|
const composeCmd = await dockerUtils.getComposeCommand();
|
|
193
|
-
await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${
|
|
218
|
+
await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${runEnvPath}" down`, { cwd: infraDir });
|
|
194
219
|
logger.log('Infrastructure services stopped');
|
|
195
|
-
}
|
|
196
|
-
// Keep the compose file for future use
|
|
197
|
-
}
|
|
220
|
+
});
|
|
198
221
|
}
|
|
199
222
|
|
|
200
223
|
/**
|
|
@@ -240,8 +263,7 @@ async function stopAllAppContainersAndVolumes(devId) {
|
|
|
240
263
|
async function stopInfraWithVolumes() {
|
|
241
264
|
const devId = await config.getDeveloperId();
|
|
242
265
|
const aifabrixDir = paths.getAifabrixHome();
|
|
243
|
-
const
|
|
244
|
-
const infraDir = path.join(aifabrixDir, infraDirName);
|
|
266
|
+
const infraDir = path.join(aifabrixDir, getInfraDirName(devId));
|
|
245
267
|
const composePath = path.join(infraDir, 'compose.yaml');
|
|
246
268
|
const adminSecretsPath = path.join(aifabrixDir, 'admin-secrets.env');
|
|
247
269
|
|
|
@@ -250,17 +272,15 @@ async function stopInfraWithVolumes() {
|
|
|
250
272
|
return;
|
|
251
273
|
}
|
|
252
274
|
|
|
253
|
-
|
|
275
|
+
await withRunEnv(infraDir, adminSecretsPath, async(runEnvPath) => {
|
|
254
276
|
logger.log('Stopping application containers on the same network...');
|
|
255
277
|
await stopAllAppContainersAndVolumes(devId);
|
|
256
278
|
logger.log('Stopping infrastructure services and removing all data...');
|
|
257
279
|
const projectName = getInfraProjectName(devId);
|
|
258
280
|
const composeCmd = await dockerUtils.getComposeCommand();
|
|
259
|
-
await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${
|
|
281
|
+
await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${runEnvPath}" down -v`, { cwd: infraDir });
|
|
260
282
|
logger.log('Infrastructure services stopped and all data removed');
|
|
261
|
-
}
|
|
262
|
-
// Keep the compose file for future use
|
|
263
|
-
}
|
|
283
|
+
});
|
|
264
284
|
}
|
|
265
285
|
|
|
266
286
|
/**
|
|
@@ -281,7 +301,6 @@ async function restartService(serviceName) {
|
|
|
281
301
|
if (!serviceName || typeof serviceName !== 'string') {
|
|
282
302
|
throw new Error('Service name is required and must be a string');
|
|
283
303
|
}
|
|
284
|
-
|
|
285
304
|
const validServices = ['postgres', 'redis', 'pgadmin', 'redis-commander', 'traefik'];
|
|
286
305
|
if (!validServices.includes(serviceName)) {
|
|
287
306
|
throw new Error(`Invalid service name. Must be one of: ${validServices.join(', ')}`);
|
|
@@ -289,8 +308,7 @@ async function restartService(serviceName) {
|
|
|
289
308
|
|
|
290
309
|
const devId = await config.getDeveloperId();
|
|
291
310
|
const aifabrixDir = paths.getAifabrixHome();
|
|
292
|
-
const
|
|
293
|
-
const infraDir = path.join(aifabrixDir, infraDirName);
|
|
311
|
+
const infraDir = path.join(aifabrixDir, getInfraDirName(devId));
|
|
294
312
|
const composePath = path.join(infraDir, 'compose.yaml');
|
|
295
313
|
const adminSecretsPath = path.join(aifabrixDir, 'admin-secrets.env');
|
|
296
314
|
|
|
@@ -298,15 +316,13 @@ async function restartService(serviceName) {
|
|
|
298
316
|
throw new Error('Infrastructure not properly configured');
|
|
299
317
|
}
|
|
300
318
|
|
|
301
|
-
|
|
319
|
+
await withRunEnv(infraDir, adminSecretsPath, async(runEnvPath) => {
|
|
302
320
|
logger.log(`Restarting ${serviceName} service...`);
|
|
303
321
|
const projectName = getInfraProjectName(devId);
|
|
304
322
|
const composeCmd = await dockerUtils.getComposeCommand();
|
|
305
|
-
await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${
|
|
323
|
+
await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${runEnvPath}" restart ${serviceName}`, { cwd: infraDir });
|
|
306
324
|
logger.log(`${serviceName} service restarted successfully`);
|
|
307
|
-
}
|
|
308
|
-
// Keep the compose file for future use
|
|
309
|
-
}
|
|
325
|
+
});
|
|
310
326
|
}
|
|
311
327
|
|
|
312
328
|
// Re-export status helper functions
|
|
@@ -16,11 +16,19 @@ environments:
|
|
|
16
16
|
REDIS_PORT: 6379 # Internal port (container-to-container). REDIS_PUBLIC_PORT calculated automatically.
|
|
17
17
|
MISO_HOST: miso-controller
|
|
18
18
|
MISO_PORT: 3000 # Internal port (container-to-container). MISO_PUBLIC_PORT calculated automatically.
|
|
19
|
+
MISO_PUBLIC_PORT: 3000
|
|
19
20
|
KEYCLOAK_HOST: keycloak
|
|
20
|
-
KEYCLOAK_PORT:
|
|
21
|
+
KEYCLOAK_PORT: 8080 # Internal port (container-to-container). KEYCLOAK_PUBLIC_PORT calculated automatically.
|
|
21
22
|
KEYCLOAK_PUBLIC_PORT: 8082
|
|
23
|
+
MORI_HOST: mori-controller
|
|
24
|
+
MORI_PORT: 3004
|
|
25
|
+
OPENWEBUI_HOST: openwebui
|
|
26
|
+
OPENWEBUI_PORT: 3003
|
|
27
|
+
FLOWISE_HOST: flowise
|
|
28
|
+
FLOWISE_PORT: 3002
|
|
22
29
|
DATAPLANE_HOST: dataplane
|
|
23
|
-
DATAPLANE_PORT: 3001
|
|
30
|
+
DATAPLANE_PORT: 3001 # Internal port (container-to-container). DATAPLANE_PUBLIC_PORT calculated automatically.
|
|
31
|
+
DATAPLANE_PUBLIC_PORT: 3001
|
|
24
32
|
NODE_ENV: production
|
|
25
33
|
PYTHONUNBUFFERED: 1
|
|
26
34
|
PYTHONDONTWRITEBYTECODE: 1
|
|
@@ -33,10 +41,19 @@ environments:
|
|
|
33
41
|
REDIS_PORT: 6379
|
|
34
42
|
MISO_HOST: localhost
|
|
35
43
|
MISO_PORT: 3010
|
|
44
|
+
MISO_PUBLIC_PORT: 3010
|
|
36
45
|
KEYCLOAK_HOST: localhost
|
|
37
46
|
KEYCLOAK_PORT: 8082
|
|
47
|
+
KEYCLOAK_PUBLIC_PORT: 8082
|
|
48
|
+
MORI_HOST: localhost
|
|
49
|
+
MORI_PORT: 3014
|
|
50
|
+
OPENWEBUI_HOST: localhost
|
|
51
|
+
OPENWEBUI_PORT: 3013
|
|
52
|
+
FLOWISE_HOST: localhost
|
|
53
|
+
FLOWISE_PORT: 3012
|
|
38
54
|
DATAPLANE_HOST: localhost
|
|
39
55
|
DATAPLANE_PORT: 3011
|
|
56
|
+
DATAPLANE_PUBLIC_PORT: 3011
|
|
40
57
|
NODE_ENV: development
|
|
41
58
|
PYTHONUNBUFFERED: 1
|
|
42
59
|
PYTHONDONTWRITEBYTECODE: 1
|
|
@@ -544,7 +544,7 @@
|
|
|
544
544
|
"properties":{
|
|
545
545
|
"rejectIf":{
|
|
546
546
|
"type":"array",
|
|
547
|
-
"description":"List of conditions that cause a record to be rejected.",
|
|
547
|
+
"description":"List of conditions that cause a record to be rejected. For lessThan: missing field is treated as reject. For greaterThan: missing field is not rejected. See quality docs for operator semantics.",
|
|
548
548
|
"items":{
|
|
549
549
|
"type":"object",
|
|
550
550
|
"required":[
|
|
@@ -659,6 +659,11 @@
|
|
|
659
659
|
"default":true,
|
|
660
660
|
"description":"Enable two-phase sync pattern. When true: validates metadata first (quality rules, comparison with DocumentRecords), then fetches binaries via CIP for changed/new documents. When false: fetches binaries directly without metadata validation phase (single-phase sync). Note: Files are never synced back to external systems (one-way sync only: external → dataplane)."
|
|
661
661
|
},
|
|
662
|
+
"ingestAfterSync":{
|
|
663
|
+
"type":"boolean",
|
|
664
|
+
"default":false,
|
|
665
|
+
"description":"When true, chunk and embed each document after store during sync so vector search returns hits immediately. When false, ingestion runs later (e.g. Celery task or on approval). Set true for E2E tests that validate vector step."
|
|
666
|
+
},
|
|
662
667
|
"binaryOperationRef":{
|
|
663
668
|
"type":"string",
|
|
664
669
|
"default":"get",
|
|
@@ -700,6 +705,11 @@
|
|
|
700
705
|
},
|
|
701
706
|
"notifications":{
|
|
702
707
|
"type":"object"
|
|
708
|
+
},
|
|
709
|
+
"ingestAfterSync":{
|
|
710
|
+
"type":"boolean",
|
|
711
|
+
"default":false,
|
|
712
|
+
"description":"When true, chunk and embed each document after store during sync so vector search returns hits."
|
|
703
713
|
}
|
|
704
714
|
},
|
|
705
715
|
"additionalProperties":false
|
|
@@ -20,11 +20,17 @@
|
|
|
20
20
|
},
|
|
21
21
|
"systemIdOrKey": {
|
|
22
22
|
"type": "string",
|
|
23
|
-
"description": "
|
|
23
|
+
"description": "Application/system key (e.g. hubspot-demo), not a datasource or entity key. Required when mode='add-datasource'. Must be the system key from the dataplane, not an entity key (e.g. 'companies').",
|
|
24
24
|
"pattern": "^[a-z0-9-]+$",
|
|
25
25
|
"minLength": 1,
|
|
26
26
|
"maxLength": 50
|
|
27
27
|
},
|
|
28
|
+
"systemDisplayName": {
|
|
29
|
+
"type": ["string", "null"],
|
|
30
|
+
"title": "Systemdisplayname",
|
|
31
|
+
"description": "System-level display name for the credential (e.g. 'Hubspot Demo'). When the OpenAPI title is entity-specific (e.g. 'Companies'), pass the system name here so authentication.displayName is system-level.",
|
|
32
|
+
"maxLength": 200
|
|
33
|
+
},
|
|
28
34
|
"source": {
|
|
29
35
|
"type": "object",
|
|
30
36
|
"description": "Source configuration for the wizard",
|
|
@@ -49,4 +49,26 @@ function resolveApplicationConfigPath(appPath) {
|
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
const RBAC_NAMES = ['rbac.yaml', 'rbac.yml', 'rbac.json'];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolves path to RBAC config file (rbac.yaml, rbac.yml, or rbac.json).
|
|
56
|
+
* Returns the first path that exists; no renames or migrations.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} appPath - Absolute path to application directory
|
|
59
|
+
* @returns {string|null} Absolute path to RBAC file, or null if none exist
|
|
60
|
+
*/
|
|
61
|
+
function resolveRbacPath(appPath) {
|
|
62
|
+
if (!appPath || typeof appPath !== 'string') {
|
|
63
|
+
throw new Error('App path is required and must be a string');
|
|
64
|
+
}
|
|
65
|
+
for (const name of RBAC_NAMES) {
|
|
66
|
+
const candidate = path.join(appPath, name);
|
|
67
|
+
if (fs.existsSync(candidate)) {
|
|
68
|
+
return candidate;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { resolveApplicationConfigPath, resolveRbacPath };
|
|
@@ -56,30 +56,73 @@ async function setPathConfig(getConfigFn, saveConfigFn, key, value, errorMsg) {
|
|
|
56
56
|
await saveConfigFn(config);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Clear a path config key (set to undefined so getPathConfig returns null).
|
|
61
|
+
* @param {Function} getConfigFn - Function to get config
|
|
62
|
+
* @param {Function} saveConfigFn - Function to save config
|
|
63
|
+
* @param {string} key - Configuration key
|
|
64
|
+
* @returns {Promise<void>}
|
|
65
|
+
*/
|
|
66
|
+
async function clearPathConfig(getConfigFn, saveConfigFn, key) {
|
|
67
|
+
const config = await getConfigFn();
|
|
68
|
+
config[key] = undefined;
|
|
69
|
+
await saveConfigFn(config);
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
function createHomeAndSecretsPathFunctions(getConfigFn, saveConfigFn) {
|
|
60
73
|
return {
|
|
61
74
|
async getAifabrixHomeOverride() {
|
|
62
75
|
return getPathConfig(getConfigFn, 'aifabrix-home');
|
|
63
76
|
},
|
|
64
77
|
async setAifabrixHomeOverride(homePath) {
|
|
65
|
-
|
|
78
|
+
if (typeof homePath !== 'string') {
|
|
79
|
+
throw new Error('Home path is required and must be a string');
|
|
80
|
+
}
|
|
81
|
+
const trimmed = homePath.trim();
|
|
82
|
+
if (trimmed === '') {
|
|
83
|
+
await clearPathConfig(getConfigFn, saveConfigFn, 'aifabrix-home');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-home', trimmed, 'Home path must be a non-empty string');
|
|
66
87
|
},
|
|
67
88
|
async getAifabrixSecretsPath() {
|
|
68
89
|
return getPathConfig(getConfigFn, 'aifabrix-secrets');
|
|
69
90
|
},
|
|
70
91
|
async setAifabrixSecretsPath(secretsPath) {
|
|
71
|
-
|
|
92
|
+
if (typeof secretsPath !== 'string') {
|
|
93
|
+
throw new Error('Secrets path is required and must be a string');
|
|
94
|
+
}
|
|
95
|
+
const trimmed = secretsPath.trim();
|
|
96
|
+
if (trimmed === '') {
|
|
97
|
+
await clearPathConfig(getConfigFn, saveConfigFn, 'aifabrix-secrets');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-secrets', trimmed, 'Secrets path must be a non-empty string');
|
|
72
101
|
}
|
|
73
102
|
};
|
|
74
103
|
}
|
|
75
104
|
|
|
105
|
+
/** Default env-config path when aifabrix-env-config is not set (builder schema). */
|
|
106
|
+
function getDefaultEnvConfigPath() {
|
|
107
|
+
return path.join(__dirname, '..', 'schema', 'env-config.yaml');
|
|
108
|
+
}
|
|
109
|
+
|
|
76
110
|
function createEnvConfigPathFunctions(getConfigFn, saveConfigFn) {
|
|
77
111
|
return {
|
|
78
112
|
async getAifabrixEnvConfigPath() {
|
|
79
|
-
|
|
113
|
+
const value = await getPathConfig(getConfigFn, 'aifabrix-env-config');
|
|
114
|
+
return value || getDefaultEnvConfigPath();
|
|
80
115
|
},
|
|
81
116
|
async setAifabrixEnvConfigPath(envConfigPath) {
|
|
82
|
-
|
|
117
|
+
if (typeof envConfigPath !== 'string') {
|
|
118
|
+
throw new Error('Env config path is required and must be a string');
|
|
119
|
+
}
|
|
120
|
+
const trimmed = envConfigPath.trim();
|
|
121
|
+
if (trimmed === '') {
|
|
122
|
+
await clearPathConfig(getConfigFn, saveConfigFn, 'aifabrix-env-config');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-env-config', trimmed, 'Env config path must be a non-empty string');
|
|
83
126
|
},
|
|
84
127
|
async getAifabrixBuilderDir() {
|
|
85
128
|
const envConfigPath = await getPathConfig(getConfigFn, 'aifabrix-env-config');
|
|
@@ -214,6 +257,7 @@ module.exports = {
|
|
|
214
257
|
getPathConfig,
|
|
215
258
|
setPathConfig,
|
|
216
259
|
createPathConfigFunctions,
|
|
260
|
+
getDefaultEnvConfigPath,
|
|
217
261
|
SETTINGS_RESPONSE_KEYS
|
|
218
262
|
};
|
|
219
263
|
|
|
@@ -92,6 +92,17 @@ function kvPathInferred(segments) {
|
|
|
92
92
|
return (namespace && pathVar) ? `kv://${namespace}/${pathVar}` : null;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Returns the path segment used in kv://<systemKey>/<segment> for a given security key.
|
|
97
|
+
* Uses the same derivation as env key → path (securityKeyToVar + varSegmentsToCamelCase).
|
|
98
|
+
* @param {string} securityKey - Security key (e.g. 'apiKey', 'clientId', 'clientSecret')
|
|
99
|
+
* @returns {string} Canonical path segment (e.g. 'apiKey', 'clientId')
|
|
100
|
+
*/
|
|
101
|
+
function getKvPathSegmentForSecurityKey(securityKey) {
|
|
102
|
+
if (!securityKey || typeof securityKey !== 'string') return '';
|
|
103
|
+
return varSegmentsToCamelCase([securityKeyToVar(securityKey)]);
|
|
104
|
+
}
|
|
105
|
+
|
|
95
106
|
/**
|
|
96
107
|
* Converts KV_* env key to kv:// path in format kv://<system-key>/<variable>.
|
|
97
108
|
* System-key uses hyphens (e.g. microsoft-teams); variable is camelCase (e.g. clientId).
|
|
@@ -220,7 +231,10 @@ function buildItemsFromEnv(envFilePath, secrets, itemsByKey) {
|
|
|
220
231
|
const fromEnv = collectKvEnvVarsAsSecretItems(envMap);
|
|
221
232
|
for (const { key, value } of fromEnv) {
|
|
222
233
|
const resolved = resolveKvValue(secrets, value);
|
|
223
|
-
|
|
234
|
+
// Skip placeholder: value that equals the kv path (e.g. from env.template) must not be pushed as the secret
|
|
235
|
+
if (resolved !== null && resolved !== undefined && isValidKvPath(key) && resolved.trim() !== key.trim()) {
|
|
236
|
+
itemsByKey.set(key, resolved);
|
|
237
|
+
}
|
|
224
238
|
}
|
|
225
239
|
} catch {
|
|
226
240
|
// Best-effort: continue without .env items
|
|
@@ -349,6 +363,7 @@ module.exports = {
|
|
|
349
363
|
collectKvRefsFromPayload,
|
|
350
364
|
pushCredentialSecrets,
|
|
351
365
|
kvEnvKeyToPath,
|
|
366
|
+
getKvPathSegmentForSecurityKey,
|
|
352
367
|
systemKeyToKvPrefix,
|
|
353
368
|
securityKeyToVar,
|
|
354
369
|
isValidKvPath,
|
package/lib/utils/env-map.js
CHANGED
|
@@ -270,9 +270,13 @@ function calculateDockerPublicPorts(result, devIdNum, schemaBaseVars = {}) {
|
|
|
270
270
|
// Match any variable ending with _PORT (e.g., MISO_PORT, KEYCLOAK_PORT, DB_PORT)
|
|
271
271
|
if (/_PORT$/.test(key) && !/_PUBLIC_PORT$/.test(key)) {
|
|
272
272
|
const publicPortKey = key.replace(/_PORT$/, '_PUBLIC_PORT');
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
const
|
|
273
|
+
// Prefer schema *_PUBLIC_PORT (e.g. KEYCLOAK_PUBLIC_PORT: 8082) so public port is canonical;
|
|
274
|
+
// fall back to schema *_PORT (e.g. KEYCLOAK_PORT: 8080) then merged value
|
|
275
|
+
const schemaPublic = schemaBaseVars[publicPortKey];
|
|
276
|
+
const schemaInternal = schemaBaseVars[key];
|
|
277
|
+
const sourceVal = schemaPublic !== undefined && schemaPublic !== null
|
|
278
|
+
? schemaPublic
|
|
279
|
+
: (schemaInternal !== undefined && schemaInternal !== null ? schemaInternal : value);
|
|
276
280
|
let portVal;
|
|
277
281
|
if (typeof sourceVal === 'string') {
|
|
278
282
|
portVal = parseInt(sourceVal, 10);
|
|
@@ -19,6 +19,8 @@ const PATTERN_DESCRIPTIONS = {
|
|
|
19
19
|
'^[a-z-]+$': 'lowercase letters and hyphens only',
|
|
20
20
|
'^[A-Z_][A-Z0-9_]*$': 'uppercase letters, numbers, and underscores (must start with letter or underscore)',
|
|
21
21
|
'^[a-zA-Z0-9_-]+$': 'letters, numbers, hyphens, and underscores only',
|
|
22
|
+
'^[a-zA-Z0-9_]+$': 'letters, numbers, and underscores only',
|
|
23
|
+
'^[a-zA-Z0-9_.]+$': 'letters, numbers, underscores, and dots only',
|
|
22
24
|
'^(http|https)://.*$': 'valid HTTP or HTTPS URL',
|
|
23
25
|
'^/[a-z0-9/-]*$': 'URL path starting with / (lowercase letters, numbers, hyphens, slashes)'
|
|
24
26
|
};
|
|
@@ -132,6 +134,35 @@ function createKeywordFormatters(field, error) {
|
|
|
132
134
|
* @param {Object} error - Raw validation error from Ajv
|
|
133
135
|
* @returns {string} Formatted error message
|
|
134
136
|
*/
|
|
137
|
+
/**
|
|
138
|
+
* Formats oneOf/anyOf validation errors with actionable message
|
|
139
|
+
* @param {string} field - Field name
|
|
140
|
+
* @param {Object} error - AJV error (keyword oneOf or anyOf)
|
|
141
|
+
* @returns {string} Formatted error message
|
|
142
|
+
*/
|
|
143
|
+
function formatOneOfAnyOfError(field, error) {
|
|
144
|
+
const instancePath = (error.instancePath || '').replace(/^\//, '');
|
|
145
|
+
if (instancePath === 'capabilities') {
|
|
146
|
+
return `${field}: must be either an array of operation names (e.g. ["list","get"]) or an object with boolean flags (e.g. { "list": true }).`;
|
|
147
|
+
}
|
|
148
|
+
return `${field}: value does not match any allowed shape. Check type and required fields.`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Formats const validation errors
|
|
153
|
+
* @param {string} field - Field name
|
|
154
|
+
* @param {Object} error - AJV error (keyword const)
|
|
155
|
+
* @returns {string} Formatted error message
|
|
156
|
+
*/
|
|
157
|
+
function formatConstError(field, error) {
|
|
158
|
+
const allowed = error.params?.allowedValue;
|
|
159
|
+
if (allowed !== undefined) {
|
|
160
|
+
const display = typeof allowed === 'string' ? `"${allowed}"` : String(allowed);
|
|
161
|
+
return `${field}: must be exactly ${display}`;
|
|
162
|
+
}
|
|
163
|
+
return `${field}: invalid value (constraint violation)`;
|
|
164
|
+
}
|
|
165
|
+
|
|
135
166
|
function formatSingleError(error) {
|
|
136
167
|
const field = getFieldName(error);
|
|
137
168
|
|
|
@@ -142,6 +173,12 @@ function formatSingleError(error) {
|
|
|
142
173
|
if (error.keyword === 'additionalProperties') {
|
|
143
174
|
return formatAdditionalPropertiesError(field, error);
|
|
144
175
|
}
|
|
176
|
+
if (error.keyword === 'oneOf' || error.keyword === 'anyOf') {
|
|
177
|
+
return formatOneOfAnyOfError(field, error);
|
|
178
|
+
}
|
|
179
|
+
if (error.keyword === 'const') {
|
|
180
|
+
return formatConstError(field, error);
|
|
181
|
+
}
|
|
145
182
|
|
|
146
183
|
// Use object lookup for keyword-specific messages
|
|
147
184
|
const formatters = createKeywordFormatters(field, error);
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds Handlebars context and generates env.template content for external systems.
|
|
3
|
+
* Single source for create, download, split, and repair so env.template structure is consistent.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview External system env.template generation
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const Handlebars = require('handlebars');
|
|
15
|
+
const { systemKeyToKvPrefix, kvEnvKeyToPath, securityKeyToVar } = require('./credential-secrets-env');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Builds hint string from portalInput (options → enum, validation → min-max or pattern).
|
|
19
|
+
* @param {Object} portalInput - Portal input config (label, options, validation)
|
|
20
|
+
* @returns {string} Hint suffix for comment
|
|
21
|
+
*/
|
|
22
|
+
function buildPortalInputHint(portalInput) {
|
|
23
|
+
if (!portalInput || typeof portalInput !== 'object') return '';
|
|
24
|
+
const parts = [];
|
|
25
|
+
if (Array.isArray(portalInput.options) && portalInput.options.length > 0) {
|
|
26
|
+
parts.push(`enum ${portalInput.options.join(',')}`);
|
|
27
|
+
}
|
|
28
|
+
const v = portalInput.validation;
|
|
29
|
+
if (v && typeof v === 'object') {
|
|
30
|
+
if (typeof v.minLength === 'number' || typeof v.maxLength === 'number') {
|
|
31
|
+
parts.push('min-max');
|
|
32
|
+
} else if (typeof v.pattern === 'string' && v.pattern) {
|
|
33
|
+
parts.push('pattern');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return parts.length ? ` - ${parts.join(', ')}` : '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Fallback security keys by auth method when authentication.security is absent. */
|
|
40
|
+
const FALLBACK_SECURITY_BY_AUTH = {
|
|
41
|
+
oauth2: ['clientId', 'clientSecret'],
|
|
42
|
+
oauth: ['clientId', 'clientSecret'],
|
|
43
|
+
aad: ['clientId', 'clientSecret'],
|
|
44
|
+
apikey: ['apiKey'],
|
|
45
|
+
apiKey: ['apiKey'],
|
|
46
|
+
basic: ['username', 'password'],
|
|
47
|
+
queryParam: ['paramValue'],
|
|
48
|
+
oidc: [],
|
|
49
|
+
hmac: ['signingSecret'],
|
|
50
|
+
bearer: ['bearerToken'],
|
|
51
|
+
token: ['bearerToken'],
|
|
52
|
+
none: []
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Builds authSecureVars array from system authentication.security (or fallback by auth type).
|
|
57
|
+
* @param {Object} system - System object with key and authentication
|
|
58
|
+
* @returns {Array<{name: string, value: string}>}
|
|
59
|
+
*/
|
|
60
|
+
function buildAuthSecureVarsFromSystem(system) {
|
|
61
|
+
const authSecureVars = [];
|
|
62
|
+
const systemKey = system?.key || 'external-system';
|
|
63
|
+
const prefix = systemKeyToKvPrefix(systemKey);
|
|
64
|
+
if (!prefix) return authSecureVars;
|
|
65
|
+
const security = system?.authentication?.security || system?.auth?.security;
|
|
66
|
+
const authMethod = (system?.authentication?.method || system?.authentication?.type ||
|
|
67
|
+
system?.auth?.method || system?.auth?.type || 'apikey').toLowerCase();
|
|
68
|
+
if (security && typeof security === 'object' && Object.keys(security).length > 0) {
|
|
69
|
+
for (const key of Object.keys(security)) {
|
|
70
|
+
const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
|
|
71
|
+
const pathVal = kvEnvKeyToPath(envName, systemKey);
|
|
72
|
+
authSecureVars.push({ name: envName, value: pathVal || `kv://${systemKey}/${key}` });
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
const keys = FALLBACK_SECURITY_BY_AUTH[authMethod] || FALLBACK_SECURITY_BY_AUTH.apikey;
|
|
76
|
+
for (const key of keys) {
|
|
77
|
+
authSecureVars.push({
|
|
78
|
+
name: `KV_${prefix}_${securityKeyToVar(key)}`,
|
|
79
|
+
value: `kv://${systemKey}/${key}`
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return authSecureVars;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Builds configuration array with name, value, comment from system.configuration.
|
|
88
|
+
* @param {Object} system - System object with configuration array
|
|
89
|
+
* @returns {Array<{name: string, value: string, comment: string}>}
|
|
90
|
+
*/
|
|
91
|
+
function buildConfigurationEntries(system) {
|
|
92
|
+
const configuration = [];
|
|
93
|
+
const configList = Array.isArray(system?.configuration) ? system.configuration : [];
|
|
94
|
+
for (const entry of configList) {
|
|
95
|
+
if (!entry || !entry.name) continue;
|
|
96
|
+
const label = entry.portalInput?.label || entry.name;
|
|
97
|
+
const hint = buildPortalInputHint(entry.portalInput || {});
|
|
98
|
+
let value = entry.value !== undefined && entry.value !== null ? String(entry.value) : '';
|
|
99
|
+
if (entry.location === 'keyvault' && value && !value.startsWith('kv://')) value = `kv://${value}`;
|
|
100
|
+
configuration.push({ name: entry.name, value, comment: `${label}${hint}` });
|
|
101
|
+
}
|
|
102
|
+
return configuration;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Builds template context from system object for env.template.hbs.
|
|
107
|
+
* @param {Object} system - Full system object (e.g. deployment.system or parsed system file)
|
|
108
|
+
* @returns {{ authMethod: string, authSecureVars: Array<{name: string, value: string}>, authNonSecureVarNames: string[], configuration: Array<{name: string, value: string, comment: string}> }}
|
|
109
|
+
*/
|
|
110
|
+
function buildExternalEnvTemplateContext(system) {
|
|
111
|
+
const authMethod = (system?.authentication?.method ||
|
|
112
|
+
system?.authentication?.type ||
|
|
113
|
+
system?.auth?.method ||
|
|
114
|
+
system?.auth?.type ||
|
|
115
|
+
'apikey').toLowerCase();
|
|
116
|
+
const authSecureVars = buildAuthSecureVarsFromSystem(system);
|
|
117
|
+
const authVars = system?.authentication?.variables || system?.auth?.variables || {};
|
|
118
|
+
const authNonSecureVarNames = Object.keys(authVars);
|
|
119
|
+
const configuration = buildConfigurationEntries(system);
|
|
120
|
+
return {
|
|
121
|
+
authMethod,
|
|
122
|
+
authSecureVars,
|
|
123
|
+
authNonSecureVarNames,
|
|
124
|
+
configuration
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Inline fallback when env.template.hbs is missing or unreadable (e.g. CI path or bundled). */
|
|
129
|
+
const DEFAULT_ENV_TEMPLATE_HBS = `# Environment variables for external system integration
|
|
130
|
+
# Use kv:// (or aifabrix secret set) for sensitive values; plain values for non-sensitive configuration.
|
|
131
|
+
#
|
|
132
|
+
|
|
133
|
+
{{#if authMethod}}
|
|
134
|
+
# Authentication
|
|
135
|
+
# Type: {{authMethod}}
|
|
136
|
+
{{#each authSecureVars}}
|
|
137
|
+
{{name}}={{value}}
|
|
138
|
+
{{/each}}
|
|
139
|
+
{{#if authNonSecureVarNames}}
|
|
140
|
+
# Non-secure (e.g. URLs): {{#each authNonSecureVarNames}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}
|
|
141
|
+
{{/if}}
|
|
142
|
+
|
|
143
|
+
{{/if}}
|
|
144
|
+
{{#if configuration.length}}
|
|
145
|
+
# Configuration
|
|
146
|
+
{{#each configuration}}
|
|
147
|
+
# {{comment}}
|
|
148
|
+
{{name}}={{value}}
|
|
149
|
+
{{/each}}
|
|
150
|
+
{{/if}}
|
|
151
|
+
`;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Generates env.template content from system using the Handlebars template.
|
|
155
|
+
* @param {Object} system - Full system object (e.g. deployment.system or parsed system file)
|
|
156
|
+
* @returns {string} Rendered env.template content
|
|
157
|
+
*/
|
|
158
|
+
function generateExternalEnvTemplateContent(system) {
|
|
159
|
+
if (!system || typeof system !== 'object') {
|
|
160
|
+
return '# Environment variables for external system integration\n# Use kv:// (or aifabrix secret set) for sensitive values.\n\n';
|
|
161
|
+
}
|
|
162
|
+
let templateContent;
|
|
163
|
+
try {
|
|
164
|
+
const templatePath = path.join(__dirname, '..', '..', 'templates', 'external-system', 'env.template.hbs');
|
|
165
|
+
templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
166
|
+
} catch (_) {
|
|
167
|
+
templateContent = undefined;
|
|
168
|
+
}
|
|
169
|
+
if (typeof templateContent !== 'string' || !templateContent.trim()) {
|
|
170
|
+
templateContent = DEFAULT_ENV_TEMPLATE_HBS;
|
|
171
|
+
}
|
|
172
|
+
const template = Handlebars.compile(templateContent);
|
|
173
|
+
const context = buildExternalEnvTemplateContext(system);
|
|
174
|
+
return template(context);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
buildExternalEnvTemplateContext,
|
|
179
|
+
generateExternalEnvTemplateContent
|
|
180
|
+
};
|