@aifabrix/builder 2.40.2 → 2.42.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 (198) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +7 -5
  3. package/integration/hubspot/README.md +8 -4
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/integration/hubspot/test.js +1 -1
  16. package/jest.config.manual.js +2 -1
  17. package/lib/api/credential.api.js +40 -0
  18. package/lib/api/dev.api.js +423 -0
  19. package/lib/api/external-test.api.js +111 -0
  20. package/lib/api/index.js +42 -19
  21. package/lib/api/pipeline.api.js +66 -120
  22. package/lib/api/types/credential.types.js +23 -0
  23. package/lib/api/types/dev.types.js +140 -0
  24. package/lib/api/types/pipeline.types.js +37 -0
  25. package/lib/api/wizard-platform.api.js +61 -0
  26. package/lib/api/wizard.api.js +34 -1
  27. package/lib/app/config.js +44 -11
  28. package/lib/app/down.js +2 -1
  29. package/lib/app/index.js +12 -1
  30. package/lib/app/prompts.js +44 -29
  31. package/lib/app/push.js +36 -12
  32. package/lib/app/readme.js +9 -6
  33. package/lib/app/run-env-compose.js +264 -0
  34. package/lib/app/run-helpers.js +121 -118
  35. package/lib/app/run.js +148 -28
  36. package/lib/app/show-display.js +1 -1
  37. package/lib/app/show.js +5 -2
  38. package/lib/build/index.js +11 -3
  39. package/lib/cli/setup-app.js +172 -15
  40. package/lib/cli/setup-credential-deployment.js +31 -6
  41. package/lib/cli/setup-dev.js +206 -16
  42. package/lib/cli/setup-environment.js +16 -6
  43. package/lib/cli/setup-external-system.js +89 -24
  44. package/lib/cli/setup-infra.js +82 -15
  45. package/lib/cli/setup-secrets.js +52 -5
  46. package/lib/cli/setup-utility.js +129 -24
  47. package/lib/commands/app-install.js +172 -0
  48. package/lib/commands/app-shell.js +75 -0
  49. package/lib/commands/app-test.js +282 -0
  50. package/lib/commands/app.js +1 -1
  51. package/lib/commands/credential-env.js +162 -0
  52. package/lib/commands/credential-list.js +17 -22
  53. package/lib/commands/credential-push.js +96 -0
  54. package/lib/commands/datasource.js +77 -6
  55. package/lib/commands/dev-cli-handlers.js +141 -0
  56. package/lib/commands/dev-down.js +114 -0
  57. package/lib/commands/dev-init.js +347 -0
  58. package/lib/commands/repair-auth-config.js +99 -0
  59. package/lib/commands/repair-datasource-keys.js +208 -0
  60. package/lib/commands/repair-datasource.js +235 -0
  61. package/lib/commands/repair-env-template.js +348 -0
  62. package/lib/commands/repair-internal.js +85 -0
  63. package/lib/commands/repair-rbac.js +158 -0
  64. package/lib/commands/repair.js +507 -0
  65. package/lib/commands/secrets-list.js +118 -0
  66. package/lib/commands/secrets-remove.js +97 -0
  67. package/lib/commands/secrets-set.js +30 -17
  68. package/lib/commands/secrets-validate.js +50 -0
  69. package/lib/commands/test-e2e-external.js +165 -0
  70. package/lib/commands/up-dataplane.js +2 -2
  71. package/lib/commands/up-miso.js +0 -25
  72. package/lib/commands/upload.js +96 -40
  73. package/lib/commands/wizard-core-helpers.js +226 -4
  74. package/lib/commands/wizard-core.js +67 -29
  75. package/lib/commands/wizard-dataplane.js +1 -1
  76. package/lib/commands/wizard-entity-selection.js +43 -0
  77. package/lib/commands/wizard-headless.js +44 -5
  78. package/lib/commands/wizard-helpers.js +7 -3
  79. package/lib/commands/wizard.js +86 -64
  80. package/lib/core/admin-secrets.js +96 -0
  81. package/lib/core/config.js +7 -1
  82. package/lib/core/secrets-ensure.js +378 -0
  83. package/lib/core/secrets-env-write.js +157 -0
  84. package/lib/core/secrets.js +176 -89
  85. package/lib/datasource/deploy.js +12 -3
  86. package/lib/datasource/field-reference-validator.js +91 -0
  87. package/lib/datasource/test-e2e.js +219 -0
  88. package/lib/datasource/test-integration.js +154 -0
  89. package/lib/datasource/validate.js +21 -3
  90. package/lib/deployment/deployer.js +7 -5
  91. package/lib/deployment/environment-config.js +137 -0
  92. package/lib/deployment/environment.js +21 -98
  93. package/lib/deployment/push.js +32 -2
  94. package/lib/external-system/download.js +188 -203
  95. package/lib/external-system/generator.js +204 -56
  96. package/lib/external-system/test-auth.js +7 -3
  97. package/lib/external-system/test-execution.js +2 -1
  98. package/lib/external-system/test-system-level.js +73 -0
  99. package/lib/external-system/test.js +56 -19
  100. package/lib/generator/external-controller-manifest.js +29 -2
  101. package/lib/generator/external-schema-utils.js +1 -1
  102. package/lib/generator/external.js +10 -3
  103. package/lib/generator/index.js +177 -25
  104. package/lib/generator/split-readme.js +1 -0
  105. package/lib/generator/split-variables.js +7 -1
  106. package/lib/generator/split.js +194 -54
  107. package/lib/generator/wizard-prompts-secondary.js +294 -0
  108. package/lib/generator/wizard-prompts.js +105 -106
  109. package/lib/generator/wizard-readme.js +88 -0
  110. package/lib/generator/wizard.js +155 -158
  111. package/lib/infrastructure/compose.js +11 -1
  112. package/lib/infrastructure/helpers.js +103 -20
  113. package/lib/infrastructure/index.js +98 -12
  114. package/lib/infrastructure/services.js +88 -22
  115. package/lib/schema/application-schema.json +32 -8
  116. package/lib/schema/external-datasource.schema.json +49 -26
  117. package/lib/schema/external-system.schema.json +509 -411
  118. package/lib/schema/wizard-config.schema.json +16 -0
  119. package/lib/utils/api.js +41 -13
  120. package/lib/utils/app-register-auth.js +25 -3
  121. package/lib/utils/auth-headers.js +8 -7
  122. package/lib/utils/cli-utils.js +20 -0
  123. package/lib/utils/compose-generator.js +77 -76
  124. package/lib/utils/compose-handlebars-helpers.js +54 -0
  125. package/lib/utils/compose-vector-helper.js +18 -0
  126. package/lib/utils/config-format-preference.js +51 -0
  127. package/lib/utils/config-format.js +36 -0
  128. package/lib/utils/config-paths.js +127 -2
  129. package/lib/utils/configuration-env-resolver.js +179 -0
  130. package/lib/utils/credential-display.js +83 -0
  131. package/lib/utils/credential-secrets-env.js +357 -0
  132. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  133. package/lib/utils/deployment-validation-helpers.js +4 -4
  134. package/lib/utils/dev-ca-install.js +139 -0
  135. package/lib/utils/dev-cert-helper.js +122 -0
  136. package/lib/utils/device-code-helpers.js +224 -0
  137. package/lib/utils/device-code.js +37 -336
  138. package/lib/utils/docker-build.js +40 -8
  139. package/lib/utils/env-copy.js +103 -13
  140. package/lib/utils/env-map.js +35 -5
  141. package/lib/utils/env-template.js +6 -5
  142. package/lib/utils/error-formatters/http-status-errors.js +20 -2
  143. package/lib/utils/error-formatters/permission-errors.js +0 -1
  144. package/lib/utils/error-formatters/validation-errors.js +0 -1
  145. package/lib/utils/external-readme.js +56 -29
  146. package/lib/utils/external-system-display.js +59 -1
  147. package/lib/utils/external-system-test-helpers.js +21 -8
  148. package/lib/utils/external-system-validators.js +3 -0
  149. package/lib/utils/file-upload.js +20 -50
  150. package/lib/utils/help-builder.js +16 -2
  151. package/lib/utils/infra-status.js +80 -45
  152. package/lib/utils/local-secrets.js +7 -52
  153. package/lib/utils/mutagen-install.js +195 -0
  154. package/lib/utils/mutagen.js +146 -0
  155. package/lib/utils/paths.js +128 -37
  156. package/lib/utils/port-resolver.js +28 -16
  157. package/lib/utils/remote-dev-auth.js +38 -0
  158. package/lib/utils/remote-docker-env.js +43 -0
  159. package/lib/utils/remote-secrets-loader.js +60 -0
  160. package/lib/utils/secrets-canonical.js +93 -0
  161. package/lib/utils/secrets-generator.js +114 -6
  162. package/lib/utils/secrets-helpers.js +108 -114
  163. package/lib/utils/secrets-path.js +2 -2
  164. package/lib/utils/secrets-utils.js +52 -1
  165. package/lib/utils/secrets-validation.js +84 -0
  166. package/lib/utils/ssh-key-helper.js +116 -0
  167. package/lib/utils/test-log-writer.js +56 -0
  168. package/lib/utils/token-manager-messages.js +90 -0
  169. package/lib/utils/token-manager.js +29 -36
  170. package/lib/utils/variable-transformer.js +3 -3
  171. package/lib/validation/env-template-auth.js +157 -0
  172. package/lib/validation/env-template-kv.js +41 -0
  173. package/lib/validation/external-manifest-validator.js +25 -0
  174. package/lib/validation/external-system-auth-rules.js +86 -0
  175. package/lib/validation/validate-batch.js +149 -0
  176. package/lib/validation/validate-datasource-keys-api.js +33 -0
  177. package/lib/validation/validate-display.js +94 -16
  178. package/lib/validation/validate.js +25 -12
  179. package/lib/validation/validator.js +72 -9
  180. package/lib/validation/wizard-datasource-validation.js +50 -0
  181. package/package.json +8 -3
  182. package/scripts/install-local.js +34 -15
  183. package/templates/README.md +0 -1
  184. package/templates/applications/README.md.hbs +4 -4
  185. package/templates/applications/dataplane/application.yaml +6 -5
  186. package/templates/applications/dataplane/env.template +15 -10
  187. package/templates/applications/dataplane/rbac.yaml +2 -2
  188. package/templates/applications/keycloak/env.template +2 -0
  189. package/templates/applications/miso-controller/application.yaml +1 -0
  190. package/templates/applications/miso-controller/env.template +12 -10
  191. package/templates/external-system/README.md.hbs +65 -25
  192. package/templates/external-system/deploy.js.hbs +4 -2
  193. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  194. package/templates/external-system/external-system.json.hbs +1 -18
  195. package/templates/infra/compose.yaml.hbs +6 -0
  196. package/templates/python/docker-compose.hbs +49 -23
  197. package/templates/typescript/docker-compose.hbs +48 -22
  198. package/integration/hubspot/application.yaml +0 -37
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @fileoverview aifabrix secret list – list secret keys and values (user file, shared file, or remote API)
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const yaml = require('js-yaml');
10
+ const chalk = require('chalk');
11
+ const logger = require('../utils/logger');
12
+ const { getAifabrixSecretsPath } = require('../core/config');
13
+ const pathsUtil = require('../utils/paths');
14
+ const { isRemoteSecretsUrl, getRemoteDevAuth } = require('../utils/remote-dev-auth');
15
+ const devApi = require('../api/dev.api');
16
+
17
+ const REMOTE_NOT_CONFIGURED_MSG = 'Remote server is not configured. Set remote-server and run "aifabrix dev init" first.';
18
+
19
+ /**
20
+ * List secret keys and values from a YAML file.
21
+ * @param {string} filePath - Absolute path to secrets file
22
+ * @returns {Array<{ key: string, value: string }>} Key-value pairs (value stringified)
23
+ */
24
+ function listKeysAndValuesFromFile(filePath) {
25
+ if (!fs.existsSync(filePath)) {
26
+ return [];
27
+ }
28
+ try {
29
+ const content = fs.readFileSync(filePath, 'utf8');
30
+ const data = yaml.load(content) || {};
31
+ if (typeof data !== 'object' || Array.isArray(data)) return [];
32
+ return Object.entries(data).map(([key, val]) => ({
33
+ key,
34
+ value: (val !== null && val !== undefined) ? String(val) : ''
35
+ }));
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+
41
+ const KEY_COL_WIDTH = 45;
42
+ const TABLE_SEPARATOR_LENGTH = 120;
43
+
44
+ /**
45
+ * Log a list of secret keys and values as a table (header, column headers, separator, rows).
46
+ * Keys are sorted alphabetically. Matches datasource list style.
47
+ * @param {string} emptyMessage - Message when items.length === 0
48
+ * @param {string} title - Table title (e.g. "User secrets")
49
+ * @param {Array<{ key: string, value: string }>} items - Key-value pairs
50
+ */
51
+ function logKeyValueList(emptyMessage, title, items) {
52
+ if (items.length === 0) {
53
+ logger.log(chalk.gray(emptyMessage));
54
+ return;
55
+ }
56
+ logger.log(chalk.bold(`\n📋 ${title}:\n`));
57
+ logger.log(chalk.gray('Key'.padEnd(KEY_COL_WIDTH) + 'Value'));
58
+ logger.log(chalk.gray('-'.repeat(TABLE_SEPARATOR_LENGTH)));
59
+ const sorted = [...items].sort((a, b) => a.key.localeCompare(b.key, undefined, { sensitivity: 'base' }));
60
+ sorted.forEach(({ key, value }) => {
61
+ const keyCol = key.padEnd(KEY_COL_WIDTH);
62
+ logger.log(`${keyCol}${value}`);
63
+ });
64
+ logger.log('');
65
+ }
66
+
67
+ /**
68
+ * List shared secrets (remote API or file) and log key and value.
69
+ * @param {string} generalSecretsPath - Path or URL for shared secrets
70
+ * @returns {Promise<void>}
71
+ */
72
+ async function listSharedSecrets(generalSecretsPath) {
73
+ if (isRemoteSecretsUrl(generalSecretsPath)) {
74
+ const auth = await getRemoteDevAuth();
75
+ if (!auth) {
76
+ throw new Error(REMOTE_NOT_CONFIGURED_MSG);
77
+ }
78
+ const items = await devApi.listSecrets(auth.serverUrl, auth.clientCertPem);
79
+ const keyValues = items.map(i => ({ key: i.name || i.key || '', value: (i.value !== null && i.value !== undefined) ? String(i.value) : '' }));
80
+ logKeyValueList('No shared secrets (remote).', 'Shared secrets (remote)', keyValues);
81
+ return;
82
+ }
83
+ const resolvedPath = path.isAbsolute(generalSecretsPath)
84
+ ? generalSecretsPath
85
+ : path.resolve(process.cwd(), generalSecretsPath);
86
+ const keyValues = listKeysAndValuesFromFile(resolvedPath);
87
+ const fileTitle = `Shared secrets (file: ${resolvedPath})`;
88
+ logKeyValueList('No shared secrets in file.', fileTitle, keyValues);
89
+ }
90
+
91
+ /** List user secrets and log key and value. */
92
+ function listUserSecrets() {
93
+ const userSecretsPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
94
+ const keyValues = listKeysAndValuesFromFile(userSecretsPath);
95
+ logKeyValueList('No user secrets.', 'User secrets', keyValues);
96
+ }
97
+
98
+ /**
99
+ * Handle secret list command. Lists key and value for each secret.
100
+ * @param {Object} options - Command options
101
+ * @param {boolean} [options.shared] - If true, list shared secrets (file or remote API)
102
+ * @returns {Promise<void>}
103
+ */
104
+ async function handleSecretsList(options) {
105
+ const isShared = options.shared || options['shared'] || false;
106
+
107
+ if (isShared) {
108
+ const generalSecretsPath = await getAifabrixSecretsPath();
109
+ if (!generalSecretsPath) {
110
+ throw new Error('Shared secrets not configured. Set aifabrix-secrets in config.yaml.');
111
+ }
112
+ await listSharedSecrets(generalSecretsPath);
113
+ } else {
114
+ listUserSecrets();
115
+ }
116
+ }
117
+
118
+ module.exports = { handleSecretsList };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @fileoverview aifabrix secret remove – remove a secret (user file, shared file, or remote API)
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const yaml = require('js-yaml');
10
+ const chalk = require('chalk');
11
+ const logger = require('../utils/logger');
12
+ const { getAifabrixSecretsPath } = require('../core/config');
13
+ const pathsUtil = require('../utils/paths');
14
+ const { isRemoteSecretsUrl, getRemoteDevAuth } = require('../utils/remote-dev-auth');
15
+ const devApi = require('../api/dev.api');
16
+
17
+ /**
18
+ * Remove a key from a YAML secrets file.
19
+ * @param {string} key - Secret key
20
+ * @param {string} filePath - Absolute path to secrets file
21
+ * @throws {Error} If file cannot be read or written
22
+ */
23
+ function removeKeyFromFile(key, filePath) {
24
+ let data = {};
25
+ if (fs.existsSync(filePath)) {
26
+ const content = fs.readFileSync(filePath, 'utf8');
27
+ data = yaml.load(content) || {};
28
+ if (typeof data !== 'object' || Array.isArray(data)) {
29
+ data = {};
30
+ }
31
+ }
32
+ if (!Object.prototype.hasOwnProperty.call(data, key)) {
33
+ throw new Error(`Secret '${key}' not found.`);
34
+ }
35
+ delete data[key];
36
+ const yamlContent = yaml.dump(data, { indent: 2, lineWidth: 120, noRefs: true, sortKeys: false });
37
+ fs.writeFileSync(filePath, yamlContent, { mode: 0o600 });
38
+ }
39
+
40
+ /**
41
+ * Remove secret from shared store (remote API or file).
42
+ * @param {string} key - Secret key
43
+ * @param {string} generalSecretsPath - Path or URL for shared secrets
44
+ * @returns {Promise<void>}
45
+ */
46
+ async function removeSharedSecret(key, generalSecretsPath) {
47
+ if (isRemoteSecretsUrl(generalSecretsPath)) {
48
+ const auth = await getRemoteDevAuth();
49
+ if (!auth) {
50
+ throw new Error('Remote server not configured or certificate missing. Run "aifabrix dev init" first.');
51
+ }
52
+ try {
53
+ await devApi.deleteSecret(auth.serverUrl, auth.clientCertPem, key);
54
+ } catch (err) {
55
+ if (err.status === 404) {
56
+ throw new Error(`Secret '${key}' not found.`);
57
+ }
58
+ throw err;
59
+ }
60
+ logger.log(chalk.green(`✓ Secret '${key}' removed from remote shared secrets.`));
61
+ return;
62
+ }
63
+ const resolvedPath = path.isAbsolute(generalSecretsPath)
64
+ ? generalSecretsPath
65
+ : path.resolve(process.cwd(), generalSecretsPath);
66
+ removeKeyFromFile(key, resolvedPath);
67
+ logger.log(chalk.green(`✓ Secret '${key}' removed from shared secrets file.`));
68
+ }
69
+
70
+ /**
71
+ * Handle secret remove command.
72
+ * @param {string} key - Secret key to remove
73
+ * @param {Object} options - Command options
74
+ * @param {boolean} [options.shared] - If true, remove from shared secrets (file or remote API)
75
+ * @returns {Promise<void>}
76
+ */
77
+ async function handleSecretsRemove(key, options) {
78
+ if (!key || typeof key !== 'string') {
79
+ throw new Error('Secret key is required.');
80
+ }
81
+
82
+ const isShared = options.shared || options['shared'] || false;
83
+
84
+ if (isShared) {
85
+ const generalSecretsPath = await getAifabrixSecretsPath();
86
+ if (!generalSecretsPath) {
87
+ throw new Error('Shared secrets not configured. Set aifabrix-secrets in config.yaml.');
88
+ }
89
+ await removeSharedSecret(key, generalSecretsPath);
90
+ } else {
91
+ const userSecretsPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
92
+ removeKeyFromFile(key, userSecretsPath);
93
+ logger.log(chalk.green(`✓ Secret '${key}' removed from user secrets.`));
94
+ }
95
+ }
96
+
97
+ module.exports = { handleSecretsRemove };
@@ -15,24 +15,46 @@ const logger = require('../utils/logger');
15
15
  const { getAifabrixSecretsPath } = require('../core/config');
16
16
  const { saveLocalSecret, saveSecret } = require('../utils/local-secrets');
17
17
  const pathsUtil = require('../utils/paths');
18
+ const { isRemoteSecretsUrl, getRemoteDevAuth } = require('../utils/remote-dev-auth');
19
+ const devApi = require('../api/dev.api');
18
20
 
19
21
  /**
20
- * Handle secrets set command action
21
- * Sets a secret value in either user secrets or general secrets file
22
+ * Handle secret set command action
23
+ * Sets a secret value in either user secrets, general secrets file, or remote API (when aifabrix-secrets is http(s) URL).
22
24
  *
23
25
  * @async
24
26
  * @function handleSecretsSet
25
27
  * @param {string} key - Secret key name
26
28
  * @param {string} value - Secret value (supports full URLs or environment variable interpolation)
27
29
  * @param {Object} options - Command options
28
- * @param {boolean} [options.shared] - If true, save to general secrets file
30
+ * @param {boolean} [options.shared] - If true, save to general secrets file or remote API
29
31
  * @returns {Promise<void>} Resolves when secret is saved
30
32
  * @throws {Error} If save fails or validation fails
31
- *
32
- * @example
33
- * await handleSecretsSet('keycloak-public-server-urlKeyVault', 'https://mydomain.com/keycloak', { shared: false });
34
- * await handleSecretsSet('keycloak-public-server-urlKeyVault', 'https://${VAR}:8182', { shared: true });
35
33
  */
34
+ /**
35
+ * Save secret to shared store (remote API or file).
36
+ * @param {string} key - Secret key
37
+ * @param {string} value - Secret value
38
+ * @param {string} generalSecretsPath - Path or URL for shared secrets
39
+ * @returns {Promise<void>}
40
+ */
41
+ async function setSharedSecret(key, value, generalSecretsPath) {
42
+ if (isRemoteSecretsUrl(generalSecretsPath)) {
43
+ const auth = await getRemoteDevAuth();
44
+ if (!auth) {
45
+ throw new Error('Remote server not configured or certificate missing. Run "aifabrix dev init" first.');
46
+ }
47
+ await devApi.addSecret(auth.serverUrl, auth.clientCertPem, { key, value });
48
+ logger.log(chalk.green(`✓ Secret '${key}' saved to remote secrets (shared).`));
49
+ return;
50
+ }
51
+ const resolvedPath = path.isAbsolute(generalSecretsPath)
52
+ ? generalSecretsPath
53
+ : path.resolve(process.cwd(), generalSecretsPath);
54
+ await saveSecret(key, value, resolvedPath);
55
+ logger.log(chalk.green(`✓ Secret '${key}' saved to general secrets file: ${resolvedPath}`));
56
+ }
57
+
36
58
  async function handleSecretsSet(key, value, options) {
37
59
  if (!key || typeof key !== 'string') {
38
60
  throw new Error('Secret key is required and must be a string');
@@ -45,21 +67,12 @@ async function handleSecretsSet(key, value, options) {
45
67
  const isShared = options.shared || options['shared'] || false;
46
68
 
47
69
  if (isShared) {
48
- // Save to general secrets file
49
70
  const generalSecretsPath = await getAifabrixSecretsPath();
50
71
  if (!generalSecretsPath) {
51
72
  throw new Error('General secrets file not configured. Set aifabrix-secrets in config.yaml or use without --shared flag for user secrets.');
52
73
  }
53
-
54
- // Resolve path (handle absolute vs relative)
55
- const resolvedPath = path.isAbsolute(generalSecretsPath)
56
- ? generalSecretsPath
57
- : path.resolve(process.cwd(), generalSecretsPath);
58
-
59
- await saveSecret(key, value, resolvedPath);
60
- logger.log(chalk.green(`✓ Secret '${key}' saved to general secrets file: ${resolvedPath}`));
74
+ await setSharedSecret(key, value, generalSecretsPath);
61
75
  } else {
62
- // Save to user secrets file
63
76
  await saveLocalSecret(key, value);
64
77
  const userSecretsPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
65
78
  logger.log(chalk.green(`✓ Secret '${key}' saved to user secrets file: ${userSecretsPath}`));
@@ -0,0 +1,50 @@
1
+ /**
2
+ * AI Fabrix Builder – Secrets validate command
3
+ *
4
+ * Validates a secrets file (structure and optional naming convention).
5
+ *
6
+ * @fileoverview Secrets validate command implementation
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const chalk = require('chalk');
12
+ const path = require('path');
13
+ const logger = require('../utils/logger');
14
+ const { validateSecretsFile } = require('../utils/secrets-validation');
15
+ const pathsUtil = require('../utils/paths');
16
+ const secretsEnsure = require('../core/secrets-ensure');
17
+
18
+ /**
19
+ * Handle secret validate command action.
20
+ * Validates secrets file at given path or at resolved write target from config.
21
+ *
22
+ * @async
23
+ * @function handleSecretsValidate
24
+ * @param {string} [pathArg] - Optional path to secrets file
25
+ * @param {Object} options - Command options
26
+ * @param {boolean} [options.naming] - Check key names against *KeyVault convention
27
+ * @returns {Promise<{ valid: boolean, errors: string[] }>}
28
+ */
29
+ async function handleSecretsValidate(pathArg, options = {}) {
30
+ let filePath = pathArg;
31
+ if (!filePath) {
32
+ const target = await secretsEnsure.resolveWriteTarget();
33
+ if (target.type === 'file' && target.filePath) {
34
+ filePath = target.filePath;
35
+ } else {
36
+ filePath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
37
+ }
38
+ }
39
+
40
+ const result = validateSecretsFile(filePath, { checkNaming: Boolean(options.naming) });
41
+ if (result.valid) {
42
+ logger.log(chalk.green(`✓ Secrets file is valid: ${result.path}`));
43
+ return { valid: true, errors: [] };
44
+ }
45
+ logger.log(chalk.red(`✗ Validation failed: ${result.path}`));
46
+ result.errors.forEach((err) => logger.log(chalk.yellow(` • ${err}`)));
47
+ return { valid: false, errors: result.errors };
48
+ }
49
+
50
+ module.exports = { handleSecretsValidate };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Run E2E tests for all datasources of an external system.
3
+ *
4
+ * @fileoverview test-e2e <external system> – run E2E for every datasource
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const chalk = require('chalk');
14
+ const logger = require('../utils/logger');
15
+ const { getIntegrationPath } = require('../utils/paths');
16
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
17
+ const { loadConfigFile } = require('../utils/config-format');
18
+ const { discoverIntegrationFiles, buildEffectiveDatasourceFiles } = require('./repair-internal');
19
+ const { runDatasourceTestE2E } = require('../datasource/test-e2e');
20
+
21
+ /**
22
+ * Derives datasource key from filename when file has no key (same logic as repair).
23
+ * @param {string} fileName - Datasource file name
24
+ * @param {string} systemKey - System key
25
+ * @returns {string}
26
+ */
27
+ function deriveDatasourceKeyFromFileName(fileName, systemKey) {
28
+ const base = path.basename(fileName, path.extname(fileName));
29
+ if (/^datasource-/.test(base)) {
30
+ const suffix = base.slice('datasource-'.length);
31
+ return systemKey && typeof systemKey === 'string' ? `${systemKey}-${suffix}` : base;
32
+ }
33
+ return base.replace(/-datasource-/, '-');
34
+ }
35
+
36
+ /* eslint-disable max-statements -- Key resolution from system or files */
37
+ /**
38
+ * Resolves the list of datasource keys for an external system (from system file or discovered files).
39
+ * @param {string} appPath - Integration app path
40
+ * @param {string} configPath - Application config path
41
+ * @param {Object} variables - Loaded application variables (externalIntegration.dataSources = filenames)
42
+ * @param {string} systemKey - System key from system file
43
+ * @param {Object} systemParsed - Parsed system config (may have dataSources array of keys)
44
+ * @param {string[]} datasourceFiles - Discovered datasource filenames
45
+ * @returns {string[]} Sorted list of datasource keys
46
+ */
47
+ function getDatasourceKeys(appPath, configPath, variables, systemKey, systemParsed, datasourceFiles) {
48
+ const fromSystem = Array.isArray(systemParsed.dataSources) && systemParsed.dataSources.length > 0
49
+ ? systemParsed.dataSources
50
+ : null;
51
+ const keys = [];
52
+ const seen = new Set();
53
+ if (fromSystem) {
54
+ fromSystem.forEach(k => {
55
+ if (k && typeof k === 'string' && !seen.has(k)) {
56
+ keys.push(k.trim());
57
+ seen.add(k.trim());
58
+ }
59
+ });
60
+ keys.sort();
61
+ return keys;
62
+ }
63
+ for (const fileName of datasourceFiles) {
64
+ const filePath = path.join(appPath, fileName);
65
+ if (!fs.existsSync(filePath)) continue;
66
+ try {
67
+ const parsed = loadConfigFile(filePath);
68
+ const key = parsed && typeof parsed.key === 'string' && parsed.key.trim()
69
+ ? parsed.key.trim()
70
+ : deriveDatasourceKeyFromFileName(fileName, systemKey);
71
+ if (key && !seen.has(key)) {
72
+ keys.push(key);
73
+ seen.add(key);
74
+ }
75
+ } catch {
76
+ const key = deriveDatasourceKeyFromFileName(fileName, systemKey);
77
+ if (key && !seen.has(key)) {
78
+ keys.push(key);
79
+ seen.add(key);
80
+ }
81
+ }
82
+ }
83
+ keys.sort();
84
+ return keys;
85
+ }
86
+
87
+ /* eslint-disable max-lines-per-function, max-statements -- Load context, then loop over keys */
88
+ /**
89
+ * Runs E2E for all datasources of an external system. Uses each datasource's payloadTemplate (no extra params required).
90
+ *
91
+ * @async
92
+ * @param {string} externalSystem - System key (e.g. hubspot-demo)
93
+ * @param {Object} options - Options passed to each runDatasourceTestE2E
94
+ * @param {string} [options.env] - Environment (dev, tst, pro)
95
+ * @param {boolean} [options.debug] - Include debug, write log
96
+ * @param {boolean} [options.verbose] - Verbose output
97
+ * @param {boolean} [options.async] - If false, sync mode (default true)
98
+ * @returns {Promise<{ success: boolean, results: Array<{ key: string, success: boolean, error?: string }> }>}
99
+ */
100
+ async function runTestE2EForExternalSystem(externalSystem, options = {}) {
101
+ if (!externalSystem || typeof externalSystem !== 'string') {
102
+ throw new Error('External system name is required');
103
+ }
104
+ const appPath = getIntegrationPath(externalSystem);
105
+ if (!fs.existsSync(appPath)) {
106
+ throw new Error(`Integration path not found: ${appPath}`);
107
+ }
108
+ const configPath = resolveApplicationConfigPath(appPath);
109
+ let variables = {};
110
+ if (fs.existsSync(configPath)) {
111
+ variables = loadConfigFile(configPath);
112
+ }
113
+ const { systemFiles, datasourceFiles: discovered } = discoverIntegrationFiles(appPath);
114
+ if (systemFiles.length === 0) {
115
+ throw new Error(`No system file found in ${appPath}. Expected *-system.yaml or *-system.json`);
116
+ }
117
+ const datasourceFiles = buildEffectiveDatasourceFiles(
118
+ appPath,
119
+ discovered,
120
+ variables.externalIntegration?.dataSources
121
+ );
122
+ const systemPath = path.join(appPath, systemFiles[0]);
123
+ const systemParsed = loadConfigFile(systemPath);
124
+ const systemKey = systemParsed.key ||
125
+ path.basename(systemFiles[0], path.extname(systemFiles[0])).replace(/-system$/, '');
126
+
127
+ const keys = getDatasourceKeys(
128
+ appPath,
129
+ configPath,
130
+ variables,
131
+ systemKey,
132
+ systemParsed,
133
+ datasourceFiles
134
+ );
135
+ if (keys.length === 0) {
136
+ logger.log(chalk.yellow(`No datasources found for ${externalSystem}. Add datasource files and run aifabrix repair.`));
137
+ return { success: true, results: [] };
138
+ }
139
+
140
+ const results = [];
141
+ const opts = {
142
+ app: externalSystem,
143
+ environment: options.env,
144
+ debug: options.debug,
145
+ verbose: options.verbose,
146
+ async: options.async !== false
147
+ };
148
+ for (const key of keys) {
149
+ try {
150
+ const data = await runDatasourceTestE2E(key, opts);
151
+ const steps = data.steps || data.completedActions || [];
152
+ const failed = data.success === false || steps.some(s => s.success === false || s.error);
153
+ results.push({ key, success: !failed });
154
+ } catch (err) {
155
+ results.push({ key, success: false, error: err.message });
156
+ }
157
+ }
158
+ const success = results.every(r => r.success);
159
+ return { success, results };
160
+ }
161
+
162
+ module.exports = {
163
+ runTestE2EForExternalSystem,
164
+ getDatasourceKeys
165
+ };
@@ -86,7 +86,7 @@ async function handleUpDataplane(options = {}) {
86
86
  logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
87
87
 
88
88
  const [controllerUrl, environmentKey] = await Promise.all([resolveControllerUrl(), resolveEnvironment()]);
89
- const authConfig = await checkAuthentication(controllerUrl, environmentKey);
89
+ const authConfig = await checkAuthentication(controllerUrl, environmentKey, { throwOnFailure: true });
90
90
 
91
91
  const cfg = await config.getConfig();
92
92
  const environment = (cfg && cfg.environment) ? cfg.environment : 'dev';
@@ -108,7 +108,7 @@ async function handleUpDataplane(options = {}) {
108
108
 
109
109
  await app.deployApp('dataplane', deployOpts);
110
110
  logger.log('');
111
- await app.runApp('dataplane', {});
111
+ await app.runApp('dataplane', { skipEnvOutputPath: true });
112
112
 
113
113
  logger.log(chalk.green('\n✓ up-dataplane complete. Dataplane is registered, deployed in dev, and running locally.'));
114
114
  }
@@ -14,16 +14,10 @@ const pathsUtil = require('../utils/paths');
14
14
  const { loadConfigFile } = require('../utils/config-format');
15
15
  const logger = require('../utils/logger');
16
16
  const config = require('../core/config');
17
- const secrets = require('../core/secrets');
18
17
  const infra = require('../infrastructure');
19
18
  const app = require('../app');
20
- const { saveLocalSecret } = require('../utils/local-secrets');
21
19
  const { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly, validateEnvOutputPathFolderOrNull } = require('./up-common');
22
20
 
23
- /** Keycloak base port (from templates/applications/keycloak application config) */
24
- const KEYCLOAK_BASE_PORT = 8082;
25
- /** Miso controller base port (dev-config app base) */
26
- const MISO_BASE_PORT = 3000;
27
21
  /**
28
22
  * Parse --image options array into map { keycloak?: string, 'miso-controller'?: string }
29
23
  * @param {string[]|string} imageOpts - Option value(s) e.g. ['keycloak=reg/k:v1', 'miso-controller=reg/m:v1']
@@ -64,22 +58,6 @@ function buildImageRefFromRegistry(appName, registry) {
64
58
  }
65
59
  }
66
60
 
67
- /**
68
- * Set URL secrets and resolve keycloak + miso-controller (no force; existing .env preserved)
69
- * @async
70
- * @param {number} devIdNum - Developer ID number
71
- */
72
- async function setMisoSecretsAndResolve(devIdNum) {
73
- const keycloakPort = KEYCLOAK_BASE_PORT + (devIdNum === 0 ? 0 : devIdNum * 100);
74
- const misoPort = MISO_BASE_PORT + (devIdNum === 0 ? 0 : devIdNum * 100);
75
- await saveLocalSecret('keycloak-public-server-urlKeyVault', `http://localhost:${keycloakPort}`);
76
- await saveLocalSecret('miso-controller-web-server-url', `http://localhost:${misoPort}`);
77
- logger.log(chalk.green('✓ Set keycloak and miso-controller URL secrets'));
78
- await secrets.generateEnvFile('keycloak', undefined, 'docker', false, true);
79
- await secrets.generateEnvFile('miso-controller', undefined, 'docker', false, true);
80
- logger.log(chalk.green('✓ Resolved keycloak and miso-controller'));
81
- }
82
-
83
61
  /**
84
62
  * Build run options and run keycloak, then miso-controller
85
63
  * @async
@@ -130,9 +108,6 @@ async function handleUpMiso(options = {}) {
130
108
  // Deploy-only: do not copy .env to repo paths; patch variables so envOutputPath is null
131
109
  patchEnvOutputPathForDeployOnly('keycloak');
132
110
  patchEnvOutputPathForDeployOnly('miso-controller');
133
- const developerId = await config.getDeveloperId();
134
- const devIdNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
135
- await setMisoSecretsAndResolve(devIdNum);
136
111
  await runMisoApps(options);
137
112
  logger.log(chalk.green('\n✓ up-miso complete. Keycloak and miso-controller are running.') +
138
113
  chalk.gray('\n Run onboarding and register Keycloak from the miso-controller repo if needed. Use \'aifabrix up-dataplane\' for dataplane.'));