@aifabrix/builder 2.40.2 → 2.41.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 (103) hide show
  1. package/README.md +6 -4
  2. package/integration/hubspot/test.js +1 -1
  3. package/lib/api/credential.api.js +40 -0
  4. package/lib/api/dev.api.js +423 -0
  5. package/lib/api/types/credential.types.js +23 -0
  6. package/lib/api/types/dev.types.js +140 -0
  7. package/lib/app/config.js +21 -0
  8. package/lib/app/down.js +2 -1
  9. package/lib/app/index.js +9 -0
  10. package/lib/app/push.js +36 -12
  11. package/lib/app/readme.js +1 -3
  12. package/lib/app/run-env-compose.js +201 -0
  13. package/lib/app/run-helpers.js +121 -118
  14. package/lib/app/run.js +148 -28
  15. package/lib/app/show.js +5 -2
  16. package/lib/build/index.js +11 -3
  17. package/lib/cli/setup-app.js +140 -14
  18. package/lib/cli/setup-dev.js +180 -17
  19. package/lib/cli/setup-environment.js +4 -2
  20. package/lib/cli/setup-external-system.js +71 -21
  21. package/lib/cli/setup-infra.js +29 -2
  22. package/lib/cli/setup-secrets.js +52 -5
  23. package/lib/cli/setup-utility.js +12 -3
  24. package/lib/commands/app-install.js +172 -0
  25. package/lib/commands/app-shell.js +75 -0
  26. package/lib/commands/app-test.js +282 -0
  27. package/lib/commands/app.js +1 -1
  28. package/lib/commands/dev-cli-handlers.js +141 -0
  29. package/lib/commands/dev-down.js +114 -0
  30. package/lib/commands/dev-init.js +309 -0
  31. package/lib/commands/secrets-list.js +118 -0
  32. package/lib/commands/secrets-remove.js +97 -0
  33. package/lib/commands/secrets-set.js +30 -17
  34. package/lib/commands/secrets-validate.js +50 -0
  35. package/lib/commands/up-dataplane.js +2 -2
  36. package/lib/commands/up-miso.js +0 -25
  37. package/lib/commands/upload.js +26 -1
  38. package/lib/core/admin-secrets.js +96 -0
  39. package/lib/core/secrets-ensure.js +378 -0
  40. package/lib/core/secrets-env-write.js +157 -0
  41. package/lib/core/secrets.js +147 -81
  42. package/lib/datasource/field-reference-validator.js +91 -0
  43. package/lib/datasource/validate.js +21 -3
  44. package/lib/deployment/environment-config.js +137 -0
  45. package/lib/deployment/environment.js +21 -98
  46. package/lib/deployment/push.js +32 -2
  47. package/lib/external-system/download.js +7 -0
  48. package/lib/external-system/test-auth.js +7 -3
  49. package/lib/external-system/test.js +5 -1
  50. package/lib/generator/index.js +174 -25
  51. package/lib/generator/wizard.js +8 -0
  52. package/lib/infrastructure/helpers.js +103 -20
  53. package/lib/infrastructure/index.js +88 -10
  54. package/lib/infrastructure/services.js +70 -15
  55. package/lib/schema/application-schema.json +24 -3
  56. package/lib/schema/external-system.schema.json +435 -413
  57. package/lib/utils/api.js +3 -3
  58. package/lib/utils/app-register-auth.js +25 -3
  59. package/lib/utils/cli-utils.js +20 -0
  60. package/lib/utils/compose-generator.js +76 -75
  61. package/lib/utils/compose-handlebars-helpers.js +43 -0
  62. package/lib/utils/compose-vector-helper.js +18 -0
  63. package/lib/utils/config-paths.js +127 -2
  64. package/lib/utils/credential-secrets-env.js +267 -0
  65. package/lib/utils/dev-cert-helper.js +122 -0
  66. package/lib/utils/device-code-helpers.js +224 -0
  67. package/lib/utils/device-code.js +37 -336
  68. package/lib/utils/docker-build.js +40 -8
  69. package/lib/utils/env-copy.js +83 -13
  70. package/lib/utils/env-map.js +35 -5
  71. package/lib/utils/env-template.js +6 -5
  72. package/lib/utils/error-formatters/http-status-errors.js +20 -1
  73. package/lib/utils/help-builder.js +15 -2
  74. package/lib/utils/infra-status.js +30 -1
  75. package/lib/utils/local-secrets.js +7 -52
  76. package/lib/utils/mutagen-install.js +195 -0
  77. package/lib/utils/mutagen.js +146 -0
  78. package/lib/utils/paths.js +43 -33
  79. package/lib/utils/port-resolver.js +28 -16
  80. package/lib/utils/remote-dev-auth.js +38 -0
  81. package/lib/utils/remote-docker-env.js +43 -0
  82. package/lib/utils/remote-secrets-loader.js +60 -0
  83. package/lib/utils/secrets-generator.js +94 -6
  84. package/lib/utils/secrets-helpers.js +33 -25
  85. package/lib/utils/secrets-path.js +2 -2
  86. package/lib/utils/secrets-utils.js +52 -1
  87. package/lib/utils/secrets-validation.js +84 -0
  88. package/lib/utils/ssh-key-helper.js +116 -0
  89. package/lib/utils/token-manager-messages.js +90 -0
  90. package/lib/utils/token-manager.js +5 -4
  91. package/lib/utils/variable-transformer.js +3 -3
  92. package/lib/validation/validator.js +65 -0
  93. package/package.json +2 -2
  94. package/scripts/install-local.js +34 -15
  95. package/templates/README.md +0 -1
  96. package/templates/applications/README.md.hbs +4 -4
  97. package/templates/applications/dataplane/application.yaml +5 -4
  98. package/templates/applications/dataplane/env.template +12 -7
  99. package/templates/applications/keycloak/env.template +2 -0
  100. package/templates/applications/miso-controller/application.yaml +1 -0
  101. package/templates/applications/miso-controller/env.template +11 -9
  102. package/templates/python/docker-compose.hbs +49 -23
  103. package/templates/typescript/docker-compose.hbs +48 -22
@@ -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 };
@@ -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.'));
@@ -6,11 +6,14 @@
6
6
  * @version 2.0.0
7
7
  */
8
8
 
9
+ const path = require('path');
9
10
  const chalk = require('chalk');
10
11
  const logger = require('../utils/logger');
11
12
  const { resolveControllerUrl } = require('../utils/controller-url');
12
13
  const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../utils/token-manager');
13
14
  const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
15
+ const { getIntegrationPath } = require('../utils/paths');
16
+ const { pushCredentialSecrets } = require('../utils/credential-secrets-env');
14
17
  const { validateExternalSystemComplete } = require('../validation/validate');
15
18
  const { displayValidationResults } = require('../validation/validate-display');
16
19
  const { generateControllerManifest } = require('../generator/external-controller-manifest');
@@ -115,6 +118,28 @@ function throwIfValidationFailed(validationResult) {
115
118
  }
116
119
  }
117
120
 
121
+ /**
122
+ * Pushes credential secrets from .env and payload to dataplane; logs result or warning.
123
+ * @param {string} dataplaneUrl - Dataplane URL
124
+ * @param {Object} authConfig - Auth config
125
+ * @param {string} systemKey - System key
126
+ * @param {Object} payload - Upload payload
127
+ */
128
+ async function pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey, payload) {
129
+ const envFilePath = path.join(getIntegrationPath(systemKey), '.env');
130
+ const pushResult = await pushCredentialSecrets(dataplaneUrl, authConfig, {
131
+ envFilePath,
132
+ appName: systemKey,
133
+ payload
134
+ });
135
+ if (pushResult.pushed > 0) {
136
+ logger.log(chalk.green(`Pushed ${pushResult.pushed} credential secret(s) to dataplane.`));
137
+ }
138
+ if (pushResult.warning) {
139
+ logger.log(chalk.yellow(`Warning: ${pushResult.warning}`));
140
+ }
141
+ }
142
+
118
143
  /**
119
144
  * Uploads external system to dataplane (upload → validate → publish). No controller deploy.
120
145
  * @param {string} systemKey - External system key (integration/<system-key>/)
@@ -126,7 +151,6 @@ function throwIfValidationFailed(validationResult) {
126
151
  */
127
152
  async function uploadExternalSystem(systemKey, options = {}) {
128
153
  validateSystemKeyFormat(systemKey);
129
-
130
154
  logger.log(chalk.blue(`\nUploading external system to dataplane: ${systemKey}`));
131
155
 
132
156
  const validationResult = await validateExternalSystemComplete(systemKey, { type: 'external' });
@@ -146,6 +170,7 @@ async function uploadExternalSystem(systemKey, options = {}) {
146
170
  requireBearerForDataplanePipeline(authConfig);
147
171
  logger.log(chalk.blue(`Dataplane: ${dataplaneUrl}`));
148
172
 
173
+ await pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey, payload);
149
174
  await runUploadValidatePublish(dataplaneUrl, authConfig, payload);
150
175
 
151
176
  logger.log(chalk.green('\nUpload validated and published to dataplane.'));
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Read and decrypt admin-secrets.env for use in Docker runs.
3
+ * Supports plain KEY=value and secure:// encrypted values; uses config secrets-encryption key.
4
+ * Decrypted content is for in-memory use only (e.g. to build a temporary .env for compose).
5
+ *
6
+ * @fileoverview Admin secrets read/decrypt for infra and application runs
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+ const config = require('./config');
16
+ const { decryptSecret, isEncrypted } = require('../utils/secrets-encryption');
17
+
18
+ /**
19
+ * Parse .env-style content into key-value map (excludes comments and empty lines).
20
+ * Values are trimmed; does not unescape quotes.
21
+ * @param {string} content - Raw file content
22
+ * @returns {Object.<string, string>} Map of variable name to value
23
+ */
24
+ function parseAdminEnvContent(content) {
25
+ const map = {};
26
+ if (!content || typeof content !== 'string') return map;
27
+ const lines = content.split(/\r?\n/);
28
+ for (const line of lines) {
29
+ const trimmed = line.trim();
30
+ if (!trimmed || trimmed.startsWith('#')) continue;
31
+ const eq = trimmed.indexOf('=');
32
+ if (eq > 0) {
33
+ const key = trimmed.substring(0, eq).trim();
34
+ let value = trimmed.substring(eq + 1);
35
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith('\'') && value.endsWith('\''))) {
36
+ value = value.slice(1, -1);
37
+ }
38
+ map[key] = value.trim();
39
+ }
40
+ }
41
+ return map;
42
+ }
43
+
44
+ /**
45
+ * Read admin-secrets.env from disk and return decrypted key-value object.
46
+ * Values with secure:// prefix are decrypted using config secrets-encryption key.
47
+ * Use the returned object only in memory (e.g. to build a temporary .env for docker compose).
48
+ *
49
+ * @async
50
+ * @param {string} [adminSecretsPath] - Path to admin-secrets.env; default: ~/.aifabrix/admin-secrets.env
51
+ * @returns {Promise<Object.<string, string>>} Plain object e.g. { POSTGRES_PASSWORD, PGADMIN_DEFAULT_EMAIL, ... }
52
+ * @throws {Error} If file missing, or encrypted value and decryption fails / no key configured
53
+ */
54
+ async function readAndDecryptAdminSecrets(adminSecretsPath) {
55
+ const pathsUtil = require('../utils/paths');
56
+ const resolvedPath = adminSecretsPath || path.join(pathsUtil.getAifabrixHome(), 'admin-secrets.env');
57
+ if (!fs.existsSync(resolvedPath)) {
58
+ throw new Error(`Admin secrets file not found: ${resolvedPath}. Run 'aifabrix up-infra' or ensure admin-secrets.env exists.`);
59
+ }
60
+ const content = fs.readFileSync(resolvedPath, 'utf8');
61
+ const raw = parseAdminEnvContent(content);
62
+ const encryptionKey = await config.getSecretsEncryptionKey();
63
+ const out = {};
64
+ for (const [key, value] of Object.entries(raw)) {
65
+ if (value && isEncrypted(value)) {
66
+ if (!encryptionKey) {
67
+ throw new Error('Admin secrets contain encrypted values but no secrets-encryption key is configured. Run "aifabrix secure --secrets-encryption <key>" to set the key.');
68
+ }
69
+ out[key] = decryptSecret(value, encryptionKey);
70
+ } else {
71
+ out[key] = value;
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+
77
+ /**
78
+ * Serialize a key-value object to .env file format (KEY=value, one per line).
79
+ * @param {Object.<string, string>} obj - Decrypted admin or merged env object
80
+ * @returns {string} .env file content
81
+ */
82
+ function envObjectToContent(obj) {
83
+ const lines = [];
84
+ for (const [key, value] of Object.entries(obj)) {
85
+ if (key === undefined || key === '') continue;
86
+ const safe = String(value ?? '').replace(/\n/g, ' ').trim();
87
+ lines.push(`${key}=${safe}`);
88
+ }
89
+ return lines.join('\n');
90
+ }
91
+
92
+ module.exports = {
93
+ readAndDecryptAdminSecrets,
94
+ parseAdminEnvContent,
95
+ envObjectToContent
96
+ };