@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.
Files changed (133) hide show
  1. package/README.md +1 -1
  2. package/bin/aifabrix.js +1 -1
  3. package/integration/hubspot-test/README.md +126 -0
  4. package/integration/{hubspot → hubspot-test}/application.json +6 -6
  5. package/integration/{hubspot → hubspot-test}/create-hubspot.js +5 -5
  6. package/integration/hubspot-test/env.template +4 -0
  7. package/integration/{hubspot/hubspot-datasource-company.json → hubspot-test/hubspot-test-datasource-company.json} +3 -2
  8. package/integration/{hubspot/hubspot-datasource-contact.json → hubspot-test/hubspot-test-datasource-contact.json} +3 -2
  9. package/integration/{hubspot/hubspot-datasource-deal.json → hubspot-test/hubspot-test-datasource-deal.json} +3 -2
  10. package/integration/{hubspot/hubspot-datasource-users.json → hubspot-test/hubspot-test-datasource-users.json} +3 -2
  11. package/integration/{hubspot/hubspot-deploy.json → hubspot-test/hubspot-test-deploy.json} +198 -21
  12. package/integration/{hubspot/hubspot-system.json → hubspot-test/hubspot-test-system.json} +8 -7
  13. package/integration/hubspot-test/rbac.json +166 -0
  14. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-credential-real.yaml +3 -3
  15. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-env-vars.yaml +2 -2
  16. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-add-datasource.yaml +1 -1
  17. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-create.yaml +1 -1
  18. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-select.yaml +1 -1
  19. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-known-platform.yaml +1 -1
  20. package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  21. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-mode.yaml +1 -1
  22. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-file.yaml +1 -1
  23. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-url.yaml +1 -1
  24. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-source.yaml +1 -1
  25. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-array-test.yaml +1 -1
  26. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  27. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  28. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-test.yaml +1 -1
  29. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-test.yaml +1 -1
  30. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +1 -1
  31. package/integration/{hubspot → hubspot-test}/test.js +102 -59
  32. package/integration/{hubspot → hubspot-test}/wizard-hubspot-e2e.yaml +2 -2
  33. package/integration/{hubspot → hubspot-test}/wizard-hubspot-platform.yaml +1 -1
  34. package/lib/api/external-test.api.js +1 -1
  35. package/lib/api/service-users.api.js +111 -2
  36. package/lib/api/types/service-users.types.js +41 -0
  37. package/lib/api/wizard.api.js +2 -1
  38. package/lib/app/index.js +2 -2
  39. package/lib/app/prompts.js +2 -2
  40. package/lib/app/readme.js +3 -1
  41. package/lib/app/register.js +3 -1
  42. package/lib/app/rotate-secret.js +3 -0
  43. package/lib/cli/setup-app.js +5 -5
  44. package/lib/cli/setup-auth.js +19 -11
  45. package/lib/cli/setup-dev.js +62 -32
  46. package/lib/cli/setup-environment.js +6 -21
  47. package/lib/cli/setup-infra.js +13 -0
  48. package/lib/cli/setup-secrets.js +45 -6
  49. package/lib/cli/setup-service-user.js +146 -20
  50. package/lib/cli/setup-utility.js +12 -0
  51. package/lib/commands/auth-config.js +25 -19
  52. package/lib/commands/datasource.js +46 -1
  53. package/lib/commands/dev-init.js +1 -1
  54. package/lib/commands/repair-env-template.js +14 -8
  55. package/lib/commands/repair-rbac.js +25 -19
  56. package/lib/commands/repair.js +108 -31
  57. package/lib/commands/secrets-remove.js +1 -1
  58. package/lib/commands/secrets-set.js +6 -0
  59. package/lib/commands/secrets-validate.js +17 -4
  60. package/lib/commands/service-user.js +231 -2
  61. package/lib/commands/up-common.js +25 -0
  62. package/lib/commands/up-dataplane.js +91 -7
  63. package/lib/commands/wizard-core-helpers.js +5 -2
  64. package/lib/commands/wizard-core.js +2 -1
  65. package/lib/commands/wizard-headless.js +6 -1
  66. package/lib/commands/wizard.js +13 -6
  67. package/lib/core/admin-secrets.js +2 -0
  68. package/lib/core/config.js +7 -5
  69. package/lib/core/ensure-encryption-key.js +1 -3
  70. package/lib/core/secrets.js +32 -9
  71. package/lib/core/templates.js +1 -1
  72. package/lib/datasource/abac-validator.js +157 -0
  73. package/lib/datasource/field-reference-validator.js +74 -36
  74. package/lib/datasource/log-viewer.js +221 -0
  75. package/lib/datasource/resolve-app.js +109 -0
  76. package/lib/datasource/test-e2e.js +11 -20
  77. package/lib/datasource/test-integration.js +42 -22
  78. package/lib/datasource/validate.js +5 -2
  79. package/lib/external-system/download-helpers.js +3 -1
  80. package/lib/external-system/generator.js +12 -8
  81. package/lib/external-system/test-system-level.js +1 -1
  82. package/lib/generator/external-controller-manifest.js +3 -3
  83. package/lib/generator/external-schema-utils.js +3 -1
  84. package/lib/generator/external.js +7 -7
  85. package/lib/generator/helpers.js +13 -9
  86. package/lib/generator/index.js +4 -4
  87. package/lib/generator/split.js +45 -10
  88. package/lib/generator/wizard-prompts-secondary.js +39 -7
  89. package/lib/generator/wizard-readme.js +4 -1
  90. package/lib/generator/wizard.js +68 -53
  91. package/lib/infrastructure/helpers.js +50 -35
  92. package/lib/infrastructure/index.js +39 -23
  93. package/lib/schema/env-config.yaml +19 -2
  94. package/lib/schema/external-datasource.schema.json +11 -1
  95. package/lib/schema/wizard-config.schema.json +7 -1
  96. package/lib/utils/app-config-resolver.js +23 -1
  97. package/lib/utils/config-paths.js +48 -4
  98. package/lib/utils/credential-secrets-env.js +16 -1
  99. package/lib/utils/env-map.js +7 -3
  100. package/lib/utils/error-formatter.js +37 -0
  101. package/lib/utils/external-env-template.js +180 -0
  102. package/lib/utils/external-readme.js +33 -1
  103. package/lib/utils/external-system-display.js +43 -0
  104. package/lib/utils/external-system-validators.js +2 -2
  105. package/lib/utils/help-builder.js +3 -5
  106. package/lib/utils/local-secrets.js +26 -3
  107. package/lib/utils/paths.js +2 -1
  108. package/lib/utils/secrets-generator.js +2 -2
  109. package/lib/utils/secrets-utils.js +4 -0
  110. package/lib/utils/secure-file-permissions.js +91 -0
  111. package/lib/utils/token-manager.js +36 -3
  112. package/lib/utils/yaml-preserve.js +59 -1
  113. package/lib/validation/env-template-auth.js +50 -2
  114. package/lib/validation/external-manifest-validator.js +8 -0
  115. package/lib/validation/validate.js +8 -0
  116. package/lib/validation/validator.js +10 -13
  117. package/package.json +6 -2
  118. package/templates/applications/dataplane/env.template +5 -1
  119. package/templates/applications/miso-controller/application.yaml +1 -1
  120. package/templates/applications/miso-controller/env.template +13 -2
  121. package/templates/external-system/README.md.hbs +18 -5
  122. package/templates/external-system/env.template.hbs +22 -0
  123. package/integration/hubspot/README.md +0 -100
  124. package/integration/hubspot/env.template +0 -4
  125. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
  126. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
  127. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
  128. /package/integration/{hubspot → hubspot-test}/companies.json +0 -0
  129. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-app-name.yaml +0 -0
  130. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-missing-app.yaml +0 -0
  131. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-helpers.js +0 -0
  132. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-tests.js +0 -0
  133. /package/integration/{hubspot → hubspot-test}/test-dataplane-down.js +0 -0
@@ -12,7 +12,7 @@ const fs = require('fs');
12
12
  const path = require('path');
13
13
  const Ajv = require('ajv');
14
14
  const { detectAppType, getDeployJsonPath } = require('../utils/paths');
15
- const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
15
+ const { resolveApplicationConfigPath, resolveRbacPath } = require('../utils/app-config-resolver');
16
16
  const { loadConfigFile } = require('../utils/config-format');
17
17
  const { loadVariables, loadRbac } = require('./helpers');
18
18
  const {
@@ -117,9 +117,9 @@ async function generateExternalSystemDeployJson(appName, appPath) {
117
117
  const systemFilePath = resolveSystemFilePath(variables, appPath, appName);
118
118
  const systemJson = await loadSystemFileContent(systemFilePath);
119
119
 
120
- // Load rbac.yaml from app directory (similar to regular apps)
121
- const rbacPath = path.join(appPath, 'rbac.yaml');
122
- const rbac = loadRbac(rbacPath);
120
+ // Load RBAC from app directory (rbac.yaml, rbac.yml, or rbac.json)
121
+ const rbacPath = resolveRbacPath(appPath);
122
+ const rbac = rbacPath ? loadRbac(rbacPath) : null;
123
123
  mergeRbacIntoSystemJson(systemJson, rbac);
124
124
 
125
125
  // Write it as <app-name>-deploy.json (consistent naming)
@@ -153,9 +153,9 @@ async function loadSystemFile(appPath, schemaBasePath, systemFileName) {
153
153
 
154
154
  const systemJson = loadConfigFile(systemFilePath);
155
155
 
156
- // Load rbac.yaml from app directory and merge if present
157
- const rbacPath = path.join(appPath, 'rbac.yaml');
158
- const rbac = loadRbac(rbacPath);
156
+ // Load RBAC from app directory (rbac.yaml, rbac.yml, or rbac.json) and merge if present
157
+ const rbacPath = resolveRbacPath(appPath);
158
+ const rbac = rbacPath ? loadRbac(rbacPath) : null;
159
159
  if (rbac) {
160
160
  if (rbac.roles && (!systemJson.roles || systemJson.roles.length === 0)) {
161
161
  systemJson.roles = rbac.roles;
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  const fs = require('fs');
12
- const yaml = require('js-yaml');
12
+ const path = require('path');
13
13
  const { loadConfigFile } = require('../utils/config-format');
14
14
 
15
15
  /**
@@ -37,21 +37,25 @@ function loadEnvTemplate(templatePath) {
37
37
  }
38
38
 
39
39
  /**
40
- * Loads rbac.yaml file if it exists
41
- * @param {string} rbacPath - Path to rbac.yaml
42
- * @returns {Object|null} Parsed RBAC configuration or null
43
- * @throws {Error} If file exists but has invalid YAML
40
+ * Loads RBAC config file (rbac.yaml, rbac.yml, or rbac.json) if it exists.
41
+ * Uses loadConfigFile so format is inferred from extension.
42
+ *
43
+ * @param {string} rbacPath - Path to RBAC file (e.g. from resolveRbacPath)
44
+ * @returns {Object|null} Parsed RBAC configuration or null if path is falsy or file does not exist
45
+ * @throws {Error} If file exists but has invalid syntax (message references actual filename, e.g. rbac.json)
44
46
  */
45
47
  function loadRbac(rbacPath) {
48
+ if (!rbacPath || typeof rbacPath !== 'string') {
49
+ return null;
50
+ }
46
51
  if (!fs.existsSync(rbacPath)) {
47
52
  return null;
48
53
  }
49
-
50
- const rbacContent = fs.readFileSync(rbacPath, 'utf8');
51
54
  try {
52
- return yaml.load(rbacContent);
55
+ return loadConfigFile(rbacPath);
53
56
  } catch (error) {
54
- throw new Error(`Invalid YAML syntax in rbac.yaml: ${error.message}`);
57
+ const basename = path.basename(rbacPath);
58
+ throw new Error(`Invalid syntax in ${basename}: ${error.message}`);
55
59
  }
56
60
  }
57
61
 
@@ -13,7 +13,7 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const _validator = require('../validation/validator');
15
15
  const builders = require('./builders');
16
- const { detectAppType, getDeployJsonPath, resolveApplicationConfigPath } = require('../utils/paths');
16
+ const { detectAppType, getDeployJsonPath, resolveApplicationConfigPath, resolveRbacPath } = require('../utils/paths');
17
17
  const { logOfflinePathWhenType } = require('../utils/cli-utils');
18
18
  const splitFunctions = require('./split');
19
19
  const { loadVariables, loadEnvTemplate, loadRbac, parseEnvironmentVariables } = require('./helpers');
@@ -39,7 +39,7 @@ const { buildEnvVarMap } = require('../utils/env-map');
39
39
  *
40
40
  * @example
41
41
  * const jsonPath = await generateDeployJson('myapp');
42
- * // Returns: './builder/myapp/myapp-deploy.json' or './integration/hubspot/application-schema.json'
42
+ * // Returns: './builder/myapp/myapp-deploy.json' or './integration/hubspot-test/application-schema.json'
43
43
  */
44
44
  /**
45
45
  * Loads configuration files for deployment generation
@@ -51,12 +51,12 @@ const { buildEnvVarMap } = require('../utils/env-map');
51
51
  function loadDeploymentConfigFiles(appPath, appType, appName) {
52
52
  const variablesPath = resolveApplicationConfigPath(appPath);
53
53
  const templatePath = path.join(appPath, 'env.template');
54
- const rbacPath = path.join(appPath, 'rbac.yaml');
54
+ const rbacPath = resolveRbacPath(appPath);
55
55
  const jsonPath = getDeployJsonPath(appName, appType, true); // Use new naming
56
56
 
57
57
  const { parsed: variables } = loadVariables(variablesPath);
58
58
  const envTemplate = loadEnvTemplate(templatePath);
59
- const rbac = loadRbac(rbacPath);
59
+ const rbac = rbacPath ? loadRbac(rbacPath) : null;
60
60
 
61
61
  return { variables, envTemplate, rbac, jsonPath };
62
62
  }
@@ -14,6 +14,7 @@ const yaml = require('js-yaml');
14
14
  const { parseImageReference } = require('./parse-image');
15
15
  const { generateReadmeFromDeployJson } = require('./split-readme');
16
16
  const { extractVariablesYaml, getExternalDatasourceFileName } = require('./split-variables');
17
+ const { generateExternalEnvTemplateContent } = require('../utils/external-env-template');
17
18
 
18
19
  /**
19
20
  * Converts configuration array back to env.template format
@@ -207,24 +208,49 @@ function mergeEnvTemplateWithExisting(existingContent, expectedByKey) {
207
208
  return updatedLines.join('\n') + (updatedLines.length > 0 ? '\n' : '');
208
209
  }
209
210
 
211
+ /**
212
+ * Builds key -> line map from env.template content (for merge when using external system template).
213
+ * @param {string} content - Full env.template content
214
+ * @returns {Map<string, string>} Key to full KEY=value line
215
+ */
216
+ function buildExpectedByKeyFromEnvContent(content) {
217
+ const expectedByKey = new Map();
218
+ if (!content || typeof content !== 'string') return expectedByKey;
219
+ const lines = content.split(/\r?\n/);
220
+ for (const line of lines) {
221
+ const trimmed = line.trim();
222
+ if (!trimmed || trimmed.startsWith('#')) continue;
223
+ const eq = trimmed.indexOf('=');
224
+ if (eq > 0) {
225
+ const key = trimmed.substring(0, eq).trim();
226
+ expectedByKey.set(key, trimmed);
227
+ }
228
+ }
229
+ return expectedByKey;
230
+ }
231
+
210
232
  /**
211
233
  * Writes env.template (merge or overwrite).
212
234
  * @param {string} outputDir - Output directory
213
235
  * @param {string} envTemplate - Default env.template content
214
- * @param {Object} options - Options (mergeEnvTemplate, configuration)
236
+ * @param {Object} options - Options (mergeEnvTemplate, configuration, expectedByKey for external)
215
237
  * @returns {Promise<string>} Path to env.template
216
238
  */
217
239
  async function writeEnvTemplateToDir(outputDir, envTemplate, options) {
218
240
  const envTemplatePath = path.join(outputDir, 'env.template');
219
241
  const fsSync = require('fs');
220
- if (options.mergeEnvTemplate && options.configuration && fsSync.existsSync(envTemplatePath)) {
221
- const expectedByKey = buildEnvTemplateExpectedByKey(options.configuration);
222
- const existingContent = await fs.readFile(envTemplatePath, 'utf8');
223
- const merged = mergeEnvTemplateWithExisting(existingContent, expectedByKey);
224
- await writeComponentFile(envTemplatePath, merged);
225
- } else {
226
- await writeComponentFile(envTemplatePath, envTemplate);
242
+ const useMerge = options.mergeEnvTemplate && fsSync.existsSync(envTemplatePath);
243
+ if (useMerge) {
244
+ const expectedByKey = options.expectedByKey ||
245
+ (options.configuration ? buildEnvTemplateExpectedByKey(options.configuration) : new Map());
246
+ if (expectedByKey.size > 0) {
247
+ const existingContent = await fs.readFile(envTemplatePath, 'utf8');
248
+ const merged = mergeEnvTemplateWithExisting(existingContent, expectedByKey);
249
+ await writeComponentFile(envTemplatePath, merged);
250
+ return envTemplatePath;
251
+ }
227
252
  }
253
+ await writeComponentFile(envTemplatePath, envTemplate);
228
254
  return envTemplatePath;
229
255
  }
230
256
 
@@ -401,12 +427,21 @@ async function splitDeployJson(deployJsonPath, outputDir = null, splitOptions =
401
427
  normalizeDeploymentForSplit(deployment);
402
428
 
403
429
  const configArray = deployment.configuration || [];
404
- const envTemplate = extractEnvTemplate(configArray);
430
+ let envTemplate;
431
+ const writeOptions = buildSplitWriteOptions(splitOptions, configArray);
432
+ if (deployment.system && typeof deployment.system === 'object') {
433
+ envTemplate = generateExternalEnvTemplateContent(deployment.system);
434
+ if (writeOptions.mergeEnvTemplate) {
435
+ writeOptions.expectedByKey = buildExpectedByKeyFromEnvContent(envTemplate);
436
+ }
437
+ } else {
438
+ envTemplate = extractEnvTemplate(configArray);
439
+ }
440
+
405
441
  const variables = extractVariablesYaml(deployment);
406
442
  const rbac = extractRbacYaml(deployment);
407
443
  const readme = generateReadmeFromDeployJson(deployment);
408
444
 
409
- const writeOptions = buildSplitWriteOptions(splitOptions, configArray);
410
445
  const result = await writeComponentFiles(finalOutputDir, envTemplate, variables, rbac, readme, writeOptions);
411
446
  await applyExternalSystemFilesToResult(finalOutputDir, deployment, result);
412
447
  return result;
@@ -107,6 +107,16 @@ async function promptForEntitySelection(entities = []) {
107
107
  /** @param {*} o - Value to stringify */
108
108
  const _s = (o) => (o === null || o === undefined || o === '' ? '—' : String(o));
109
109
 
110
+ /**
111
+ * Humanize app key for display name (must stay in sync with prepareWizardContext in lib/generator/wizard.js).
112
+ * @param {string} appKey - Application key (e.g. hubspot-demo)
113
+ * @returns {string} Display name (e.g. Hubspot Demo)
114
+ */
115
+ function humanizeAppKey(appKey) {
116
+ if (!appKey || typeof appKey !== 'string') return appKey || '';
117
+ return appKey.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
118
+ }
119
+
110
120
  /** @param {Object} sys - systemSummary */
111
121
  function _formatSystem(sys) {
112
122
  return [
@@ -149,8 +159,11 @@ function _formatFieldMappings(fm) {
149
159
  /**
150
160
  * Derive a preview summary from systemConfig and datasourceConfigs when the dataplane
151
161
  * preview API does not return summaries. Ensures a compact summary is always shown.
162
+ * When appKey is provided, system key/displayName and datasource keys are overridden
163
+ * to match what prepareWizardContext will write (so the preview matches saved files).
152
164
  * @param {Object} systemConfig - System configuration
153
165
  * @param {Object[]} datasourceConfigs - Array of datasource configurations
166
+ * @param {string} [appKey] - Optional app key; when set, overrides system key/displayName and rewrites datasource keys
154
167
  * @returns {Object} Preview object compatible with formatPreviewSummary
155
168
  */
156
169
  function _buildDatasourceSummary(ds) {
@@ -171,8 +184,9 @@ function _buildDatasourceSummary(ds) {
171
184
  }
172
185
 
173
186
  function _buildSystemSummary(sys) {
174
- const baseUrl = sys.openapi?.servers?.[0]?.url ?? sys.baseUrl ?? sys.openapi?.baseUrl ?? null;
175
- const authType = sys.authentication?.type ?? sys.authenticationType ?? null;
187
+ const baseUrl = sys.openapi?.servers?.[0]?.url ?? sys.baseUrl ?? sys.openapi?.baseUrl ??
188
+ sys.authentication?.variables?.baseUrl ?? null;
189
+ const authType = sys.authentication?.type ?? sys.authentication?.method ?? sys.authenticationType ?? null;
176
190
  const endpointCount = sys.openapi?.endpoints?.length ??
177
191
  (sys.openapi?.operations ? Object.keys(sys.openapi.operations || {}).length : null);
178
192
  return {
@@ -191,7 +205,7 @@ function _buildFieldMappingsSummary(ds0) {
191
205
  return mappedFields.length > 0 ? { mappingCount: mappedFields.length, mappedFields, unmappedFields: [] } : null;
192
206
  }
193
207
 
194
- function derivePreviewFromConfig(systemConfig, datasourceConfigs) {
208
+ function derivePreviewFromConfig(systemConfig, datasourceConfigs, appKey) {
195
209
  const sys = systemConfig || {};
196
210
  const dsList = Array.isArray(datasourceConfigs) ? datasourceConfigs : (datasourceConfigs ? [datasourceConfigs] : []);
197
211
 
@@ -199,7 +213,23 @@ function derivePreviewFromConfig(systemConfig, datasourceConfigs) {
199
213
  systemSummary: _buildSystemSummary(sys),
200
214
  fieldMappingsSummary: _buildFieldMappingsSummary(dsList[0] || {})
201
215
  };
202
- const datasourceSummaries = dsList.map(_buildDatasourceSummary);
216
+ let datasourceSummaries = dsList.map(_buildDatasourceSummary);
217
+
218
+ if (appKey && typeof appKey === 'string') {
219
+ result.systemSummary.key = appKey;
220
+ result.systemSummary.displayName = humanizeAppKey(appKey);
221
+ const originalSystemKey = sys.key || appKey;
222
+ const originalPrefix = `${originalSystemKey}-`;
223
+ datasourceSummaries = datasourceSummaries.map((summary, i) => {
224
+ const ds = dsList[i] || {};
225
+ const dsKey = ds.key || '';
226
+ const newKey = dsKey && dsKey.startsWith(originalPrefix)
227
+ ? `${appKey}-${dsKey.substring(originalPrefix.length)}`
228
+ : `${appKey}-${ds.entityType || ds.entityKey || (dsKey && dsKey.split('-').pop()) || 'default'}`;
229
+ return { ...summary, key: newKey };
230
+ });
231
+ }
232
+
203
233
  if (datasourceSummaries.length === 1) {
204
234
  result.datasourceSummary = datasourceSummaries[0];
205
235
  } else if (datasourceSummaries.length > 1) {
@@ -247,11 +277,12 @@ function formatPreviewSummary(preview) {
247
277
  * @param {Object|null} [opts.preview] - Preview data from getPreview (null = fallback to YAML)
248
278
  * @param {Object} opts.systemConfig - System configuration
249
279
  * @param {Object[]} opts.datasourceConfigs - Array of datasource configurations
280
+ * @param {string} [opts.appKey] - App key; when set and using fallback preview, overrides system key/displayName and datasource keys
250
281
  * @returns {Promise<Object>} Object with action ('accept'|'cancel') and optionally edited configs
251
282
  */
252
- async function promptForConfigReview({ preview, systemConfig, datasourceConfigs }) {
283
+ async function promptForConfigReview({ preview, systemConfig, datasourceConfigs, appKey }) {
253
284
  const hasSummary = preview && (preview.systemSummary || preview.datasourceSummary || (preview.datasourceSummaries && preview.datasourceSummaries.length > 0));
254
- const summaryToShow = hasSummary ? preview : derivePreviewFromConfig(systemConfig, datasourceConfigs);
285
+ const summaryToShow = hasSummary ? preview : derivePreviewFromConfig(systemConfig, datasourceConfigs, appKey);
255
286
  const canShowSummary = summaryToShow.systemSummary || summaryToShow.datasourceSummary ||
256
287
  (summaryToShow.datasourceSummaries && summaryToShow.datasourceSummaries.length > 0);
257
288
 
@@ -290,5 +321,6 @@ module.exports = {
290
321
  promptForEntitySelection,
291
322
  promptForConfigReview,
292
323
  derivePreviewFromConfig,
293
- formatPreviewSummary
324
+ formatPreviewSummary,
325
+ humanizeAppKey
294
326
  };
@@ -68,6 +68,8 @@ async function generateReadme(options) {
68
68
  };
69
69
  });
70
70
 
71
+ const auth = systemConfig.authentication || systemConfig.auth || {};
72
+ const authType = auth.type || auth.method || auth.authType;
71
73
  const readmeContent = generateExternalReadmeContent({
72
74
  appName,
73
75
  systemKey,
@@ -75,7 +77,8 @@ async function generateReadme(options) {
75
77
  displayName: systemConfig.displayName,
76
78
  description: systemConfig.description,
77
79
  fileExt: ext,
78
- datasources
80
+ datasources,
81
+ authType
79
82
  });
80
83
 
81
84
  await fs.writeFile(readmePath, readmeContent, 'utf8');
@@ -15,7 +15,8 @@ const chalk = require('chalk');
15
15
  const logger = require('../utils/logger');
16
16
  const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
17
17
  const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
18
- const { systemKeyToKvPrefix } = require('../utils/credential-secrets-env');
18
+ const { systemKeyToKvPrefix, securityKeyToVar, isValidKvPath } = require('../utils/credential-secrets-env');
19
+ const { generateExternalEnvTemplateContent } = require('../utils/external-env-template');
19
20
  const { generateReadme } = require('./wizard-readme');
20
21
 
21
22
  /**
@@ -130,10 +131,12 @@ async function generateConfigFilesForWizard(params) {
130
131
  format: format || 'yaml'
131
132
  });
132
133
 
133
- // Generate env.template with KV_* authentication variables
134
- await generateEnvTemplate(appPath, systemConfig, finalSystemKey);
135
-
134
+ // Generate env.template with Authentication and Configuration sections (Handlebars)
136
135
  const envTemplatePath = path.join(appPath, 'env.template');
136
+ const envTemplateContent = generateExternalEnvTemplateContent(systemConfig);
137
+ await fs.writeFile(envTemplatePath, envTemplateContent, 'utf8');
138
+ logger.log(chalk.green('✓ Generated env.template'));
139
+
137
140
  try {
138
141
  const secretsEnsure = require('../core/secrets-ensure');
139
142
  await secretsEnsure.ensureSecretsFromEnvTemplate(envTemplatePath, { emptyValuesForCredentials: true });
@@ -297,72 +300,84 @@ async function generateOrUpdateVariablesYaml(params) {
297
300
  }
298
301
  }
299
302
 
300
- /**
301
- * Adds API key authentication lines with KV_* convention
302
- * @param {Array<string>} lines - Lines array to append to
303
- * @param {string} prefix - KV prefix (e.g. HUBSPOT)
304
- */
305
- function addApiKeyAuthLines(lines, prefix) {
306
- lines.push('# API Key Authentication');
307
- lines.push(`KV_${prefix}_APIKEY=`);
308
- lines.push('');
303
+ /** Path-style kv value: kv://systemKey/key (camelCase key). */
304
+ function toPathStyleKv(systemKey, key) {
305
+ return `kv://${systemKey}/${key}`;
309
306
  }
310
307
 
311
308
  /**
312
- * Adds OAuth2 authentication lines with KV_* convention
309
+ * Adds env.template lines from authentication.security (path-style kv:// values).
313
310
  * @param {Array<string>} lines - Lines array to append to
314
- * @param {Object} auth - Authentication configuration
315
- * @param {string} prefix - KV prefix
311
+ * @param {Object} security - authentication.security object
312
+ * @param {string} systemKey - System key for path namespace
313
+ * @param {string} prefix - KV prefix (e.g. HUBSPOT)
316
314
  */
317
- function addOAuth2AuthLines(lines, auth, prefix) {
318
- lines.push('# OAuth2 Authentication');
319
- lines.push(`KV_${prefix}_CLIENTID=`);
320
- lines.push(`KV_${prefix}_CLIENTSECRET=`);
321
- if (auth.scope) lines.push(`SCOPE=${auth.scope}`);
322
- lines.push('');
315
+ function addLinesFromSecurity(lines, security, systemKey, prefix) {
316
+ if (!security || typeof security !== 'object') return;
317
+ for (const key of Object.keys(security)) {
318
+ const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
319
+ let value = security[key];
320
+ if (typeof value === 'string' && value.trim().startsWith('kv://') && isValidKvPath(value.trim())) {
321
+ value = value.trim();
322
+ } else {
323
+ value = toPathStyleKv(systemKey, key);
324
+ }
325
+ lines.push(`${envName}=${value}`);
326
+ }
327
+ if (Object.keys(security).length > 0) lines.push('');
323
328
  }
324
329
 
325
- /**
326
- * Adds bearer token authentication lines with KV_* convention
327
- * @param {Array<string>} lines - Lines array to append to
328
- * @param {string} prefix - KV prefix
329
- */
330
- function addBearerTokenAuthLines(lines, prefix) {
331
- lines.push('# Bearer Token Authentication');
332
- lines.push(`KV_${prefix}_BEARERTOKEN=`);
333
- lines.push('');
334
- }
330
+ /** Fallback security keys by auth method (camelCase) for path-style kv://. */
331
+ const FALLBACK_SECURITY_BY_AUTH = {
332
+ oauth2: ['clientId', 'clientSecret'],
333
+ oauth: ['clientId', 'clientSecret'],
334
+ aad: ['clientId', 'clientSecret'],
335
+ apikey: ['apiKey'],
336
+ apiKey: ['apiKey'],
337
+ basic: ['username', 'password'],
338
+ queryParam: ['paramValue'],
339
+ oidc: [],
340
+ hmac: ['signingSecret'],
341
+ bearer: ['bearerToken'],
342
+ token: ['bearerToken'],
343
+ none: []
344
+ };
335
345
 
336
346
  /**
337
- * Adds basic authentication lines with KV_* convention
347
+ * Adds auth lines by type with path-style kv:// values (fallback when security is absent).
338
348
  * @param {Array<string>} lines - Lines array to append to
349
+ * @param {string} authType - Authentication type
350
+ * @param {string} systemKey - System key
339
351
  * @param {string} prefix - KV prefix
340
352
  */
341
- function addBasicAuthLines(lines, prefix) {
342
- lines.push('# Basic Authentication');
343
- lines.push(`KV_${prefix}_USERNAME=`);
344
- lines.push(`KV_${prefix}_PASSWORD=`);
353
+ function addFallbackAuthLines(lines, authType, systemKey, prefix) {
354
+ const keys = FALLBACK_SECURITY_BY_AUTH[authType] || FALLBACK_SECURITY_BY_AUTH.apikey;
355
+ if (keys.length === 0) return;
356
+ const labels = { oauth2: 'OAuth2', aad: 'Azure AD', apikey: 'API Key', basic: 'Basic', queryParam: 'Query Param', hmac: 'HMAC', bearer: 'Bearer Token' };
357
+ const label = labels[authType] || authType;
358
+ lines.push(`# ${label} Authentication`);
359
+ for (const key of keys) {
360
+ lines.push(`KV_${prefix}_${securityKeyToVar(key)}=${toPathStyleKv(systemKey, key)}`);
361
+ }
345
362
  lines.push('');
346
363
  }
347
364
 
348
365
  /**
349
- * Adds authentication lines based on auth type. Uses KV_<APPKEY>_<VAR> convention.
366
+ * Adds authentication lines: from authentication.security when present, else by auth type with path-style kv://.
350
367
  * @param {Array<string>} lines - Lines array to append to
351
- * @param {Object} auth - Authentication configuration
352
- * @param {string} authType - Authentication type
353
- * @param {string} systemKey - System key (e.g. hubspot) for KV_ prefix
368
+ * @param {Object} auth - Authentication configuration (may have security, type/method)
369
+ * @param {string} authType - Normalized auth type
370
+ * @param {string} systemKey - System key for KV path namespace
354
371
  */
355
372
  function addAuthenticationLines(lines, auth, authType, systemKey) {
356
373
  const prefix = systemKeyToKvPrefix(systemKey);
357
374
  if (!prefix) return;
358
- if (authType === 'apikey' || authType === 'apiKey') {
359
- addApiKeyAuthLines(lines, prefix);
360
- } else if (authType === 'oauth2' || authType === 'oauth') {
361
- addOAuth2AuthLines(lines, auth || {}, prefix);
362
- } else if (authType === 'bearer' || authType === 'token') {
363
- addBearerTokenAuthLines(lines, prefix);
364
- } else if (authType === 'basic') {
365
- addBasicAuthLines(lines, prefix);
375
+ const security = auth?.security;
376
+ if (security && typeof security === 'object' && Object.keys(security).length > 0) {
377
+ lines.push('# Authentication (from security)');
378
+ addLinesFromSecurity(lines, security, systemKey, prefix);
379
+ } else {
380
+ addFallbackAuthLines(lines, authType, systemKey, prefix);
366
381
  }
367
382
  }
368
383
 
@@ -381,22 +396,22 @@ function addBaseUrlLines(lines, systemConfig) {
381
396
  }
382
397
 
383
398
  /**
384
- * Generate env.template with KV_* authentication variables
399
+ * Generate env.template with KV_* authentication variables (legacy; prefer generateExternalEnvTemplateContent).
385
400
  * @async
386
- * @function generateEnvTemplate
401
+ * @function _generateEnvTemplate
387
402
  * @param {string} appPath - Application directory path
388
403
  * @param {Object} systemConfig - System configuration (must have key for systemKey)
389
404
  * @param {string} [finalSystemKey] - Final system key for KV_ prefix (default: systemConfig.key)
390
405
  * @throws {Error} If generation fails
391
406
  */
392
- async function generateEnvTemplate(appPath, systemConfig, finalSystemKey) {
407
+ async function _generateEnvTemplate(appPath, systemConfig, finalSystemKey) {
393
408
  try {
394
409
  const envTemplatePath = path.join(appPath, 'env.template');
395
410
  const systemKey = finalSystemKey || systemConfig?.key;
396
411
  const lines = ['# Environment variables for external system integration', '# Use KV_* for credential push (aifabrix credential push)', ''];
397
412
 
398
413
  const auth = systemConfig?.authentication || systemConfig?.auth || {};
399
- const authType = auth.type || auth.authType || 'apikey';
414
+ const authType = (auth.type || auth.method || auth.authType || 'apikey').toLowerCase();
400
415
 
401
416
  addAuthenticationLines(lines, auth, authType, systemKey);
402
417
  addBaseUrlLines(lines, systemConfig);
@@ -14,6 +14,7 @@ const fs = require('fs');
14
14
  const chalk = require('chalk');
15
15
  const handlebars = require('handlebars');
16
16
  const secrets = require('../core/secrets');
17
+ const adminSecrets = require('../core/admin-secrets');
17
18
  const logger = require('../utils/logger');
18
19
  const dockerUtils = require('../utils/docker');
19
20
  const paths = require('../utils/paths');
@@ -69,19 +70,6 @@ function logVolumeResetHint(infraDir) {
69
70
  ));
70
71
  }
71
72
 
72
- /**
73
- * Apply password to admin-secrets file content (all three password keys).
74
- * @param {string} content - Current file content
75
- * @param {string} password - Password to set
76
- * @returns {string} Updated content
77
- */
78
- function applyPasswordToAdminSecretsContent(content, password) {
79
- return content
80
- .replace(/^POSTGRES_PASSWORD=.*$/m, `POSTGRES_PASSWORD=${password}`)
81
- .replace(/^PGADMIN_DEFAULT_PASSWORD=.*$/m, `PGADMIN_DEFAULT_PASSWORD=${password}`)
82
- .replace(/^REDIS_COMMANDER_PASSWORD=.*$/m, `REDIS_COMMANDER_PASSWORD=${password}`);
83
- }
84
-
85
73
  /**
86
74
  * Sync postgres-passwordKeyVault to the main secrets store (file or remote).
87
75
  * @param {string} password - Password to store
@@ -94,10 +82,42 @@ async function syncPostgresPasswordToStore(password) {
94
82
  }
95
83
  }
96
84
 
85
+ /** Default admin env keys and values when missing. */
86
+ const DEFAULT_ADMIN_OBJ = {
87
+ PGADMIN_DEFAULT_EMAIL: 'admin@aifabrix.dev',
88
+ REDIS_HOST: 'local:redis:6379:0:',
89
+ REDIS_COMMANDER_USER: 'admin'
90
+ };
91
+
92
+ /**
93
+ * Writes merged admin secrets to disk and logs/syncs as needed.
94
+ * @async
95
+ * @param {string} adminSecretsPath - Path to admin-secrets.env
96
+ * @param {Object} adminObj - Decrypted admin secrets object
97
+ * @param {string} passwordToUse - Password to set for Postgres, pgAdmin, Redis Commander
98
+ * @param {boolean} shouldOverwriteWithAdminPwd - Whether this was an explicit admin password update
99
+ */
100
+ async function applyAdminSecretsUpdate(adminSecretsPath, adminObj, passwordToUse, shouldOverwriteWithAdminPwd) {
101
+ const merged = { ...DEFAULT_ADMIN_OBJ, ...adminObj };
102
+ merged.POSTGRES_PASSWORD = passwordToUse;
103
+ merged.PGADMIN_DEFAULT_PASSWORD = passwordToUse;
104
+ merged.REDIS_COMMANDER_PASSWORD = passwordToUse;
105
+ const content = await secrets.formatAdminSecretsContent(merged);
106
+ fs.writeFileSync(adminSecretsPath, content, { mode: 0o600 });
107
+ if (shouldOverwriteWithAdminPwd) {
108
+ logger.log('Updated admin password in admin-secrets.env.');
109
+ await syncPostgresPasswordToStore(passwordToUse);
110
+ logVolumeResetHint(path.join(paths.getAifabrixHome(), getInfraDirName(0)));
111
+ } else {
112
+ logger.log('Set default admin password in admin-secrets.env for local use.');
113
+ }
114
+ }
115
+
97
116
  /**
98
117
  * Ensure admin secrets file exists and set admin password.
99
118
  * When adminPwd is provided, update POSTGRES_PASSWORD, PGADMIN_DEFAULT_PASSWORD, REDIS_COMMANDER_PASSWORD
100
119
  * in admin-secrets.env (overwrites existing values). Otherwise only backfill empty fields.
120
+ * Reads and writes using decrypted values; writes encrypted when secrets-encryption key is set.
101
121
  *
102
122
  * @async
103
123
  * @param {Object} [options] - Options
@@ -109,29 +129,25 @@ async function ensureAdminSecrets(options = {}) {
109
129
  ? options.adminPwd.trim()
110
130
  : null;
111
131
  const passwordToUse = adminPwdOverride || DEFAULT_ADMIN_PASSWORD;
112
-
113
132
  const adminSecretsPath = path.join(paths.getAifabrixHome(), 'admin-secrets.env');
133
+
114
134
  if (!fs.existsSync(adminSecretsPath)) {
115
135
  logger.log('Generating admin-secrets.env...');
116
136
  await secrets.generateAdminSecretsEnv(undefined);
137
+ return adminSecretsPath;
117
138
  }
118
- let content = fs.readFileSync(adminSecretsPath, 'utf8');
119
- const needsBackfill = /^POSTGRES_PASSWORD=\s*$/m.test(content) ||
120
- /^PGADMIN_DEFAULT_PASSWORD=\s*$/m.test(content) ||
121
- /^REDIS_COMMANDER_PASSWORD=\s*$/m.test(content);
139
+
140
+ const adminObj = await adminSecrets.readAndDecryptAdminSecrets(adminSecretsPath);
141
+ const needsBackfill = !(adminObj.POSTGRES_PASSWORD && adminObj.POSTGRES_PASSWORD.trim()) ||
142
+ !(adminObj.PGADMIN_DEFAULT_PASSWORD && adminObj.PGADMIN_DEFAULT_PASSWORD.trim()) ||
143
+ !(adminObj.REDIS_COMMANDER_PASSWORD && adminObj.REDIS_COMMANDER_PASSWORD.trim());
122
144
  const shouldOverwriteWithAdminPwd = adminPwdOverride !== null;
123
145
 
124
- if (shouldOverwriteWithAdminPwd) {
125
- content = applyPasswordToAdminSecretsContent(content, passwordToUse);
126
- fs.writeFileSync(adminSecretsPath, content, { mode: 0o600 });
127
- logger.log('Updated admin password in admin-secrets.env.');
128
- await syncPostgresPasswordToStore(passwordToUse);
129
- logVolumeResetHint(path.join(paths.getAifabrixHome(), getInfraDirName(0)));
130
- } else if (needsBackfill) {
131
- content = applyPasswordToAdminSecretsContent(content, passwordToUse);
132
- fs.writeFileSync(adminSecretsPath, content, { mode: 0o600 });
133
- logger.log('Set default admin password in admin-secrets.env for local use.');
146
+ if (!shouldOverwriteWithAdminPwd && !needsBackfill) {
147
+ return adminSecretsPath;
134
148
  }
149
+
150
+ await applyAdminSecretsUpdate(adminSecretsPath, adminObj, passwordToUse, shouldOverwriteWithAdminPwd);
135
151
  return adminSecretsPath;
136
152
  }
137
153
 
@@ -243,12 +259,13 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "miso" -c "GRANT AL
243
259
  }
244
260
 
245
261
  /**
246
- * Prepare infrastructure directory and extract postgres password
262
+ * Prepare infrastructure directory and extract postgres password from decrypted admin secrets.
263
+ * @async
247
264
  * @param {string} devId - Developer ID
248
265
  * @param {string} adminSecretsPath - Path to admin secrets file
249
- * @returns {Object} Object with infraDir and postgresPassword
266
+ * @returns {Promise<Object>} Object with infraDir and postgresPassword
250
267
  */
251
- function prepareInfraDirectory(devId, adminSecretsPath) {
268
+ async function prepareInfraDirectory(devId, adminSecretsPath) {
252
269
  const aifabrixDir = paths.getAifabrixHome();
253
270
  const infraDirName = getInfraDirName(devId);
254
271
  const infraDir = path.join(aifabrixDir, infraDirName);
@@ -265,10 +282,8 @@ function prepareInfraDirectory(devId, adminSecretsPath) {
265
282
  }
266
283
  }
267
284
 
268
- const adminSecretsContent = fs.readFileSync(adminSecretsPath, 'utf8');
269
- const postgresPasswordMatch = adminSecretsContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
270
- const raw = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
271
- const postgresPassword = (raw && raw.trim()) || DEFAULT_ADMIN_PASSWORD;
285
+ const adminObj = await adminSecrets.readAndDecryptAdminSecrets(adminSecretsPath);
286
+ const postgresPassword = (adminObj.POSTGRES_PASSWORD && adminObj.POSTGRES_PASSWORD.trim()) || DEFAULT_ADMIN_PASSWORD;
272
287
  generatePgAdminConfig(infraDir, postgresPassword);
273
288
 
274
289
  return { infraDir, postgresPassword };