@aifabrix/builder 2.40.0 → 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.
- package/README.md +7 -5
- package/integration/hubspot/test.js +1 -1
- package/jest.config.manual.js +29 -0
- package/lib/api/credential.api.js +40 -0
- package/lib/api/dev.api.js +423 -0
- package/lib/api/types/credential.types.js +23 -0
- package/lib/api/types/dev.types.js +140 -0
- package/lib/app/config.js +21 -0
- package/lib/app/down.js +2 -1
- package/lib/app/index.js +9 -0
- package/lib/app/push.js +36 -12
- package/lib/app/readme.js +1 -3
- package/lib/app/run-env-compose.js +201 -0
- package/lib/app/run-helpers.js +121 -118
- package/lib/app/run.js +148 -28
- package/lib/app/show.js +5 -2
- package/lib/build/index.js +11 -3
- package/lib/cli/setup-app.js +140 -14
- package/lib/cli/setup-auth.js +1 -0
- package/lib/cli/setup-dev.js +180 -17
- package/lib/cli/setup-environment.js +4 -2
- package/lib/cli/setup-external-system.js +71 -21
- package/lib/cli/setup-infra.js +29 -2
- package/lib/cli/setup-secrets.js +52 -5
- package/lib/cli/setup-utility.js +19 -4
- package/lib/commands/app-install.js +172 -0
- package/lib/commands/app-shell.js +75 -0
- package/lib/commands/app-test.js +282 -0
- package/lib/commands/app.js +1 -1
- package/lib/commands/auth-status.js +36 -3
- package/lib/commands/dev-cli-handlers.js +141 -0
- package/lib/commands/dev-down.js +114 -0
- package/lib/commands/dev-init.js +309 -0
- package/lib/commands/secrets-list.js +118 -0
- package/lib/commands/secrets-remove.js +97 -0
- package/lib/commands/secrets-set.js +30 -17
- package/lib/commands/secrets-validate.js +50 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/commands/up-miso.js +0 -25
- package/lib/commands/upload.js +26 -1
- package/lib/core/admin-secrets.js +96 -0
- package/lib/core/secrets-ensure.js +378 -0
- package/lib/core/secrets-env-write.js +157 -0
- package/lib/core/secrets.js +147 -81
- package/lib/datasource/field-reference-validator.js +91 -0
- package/lib/datasource/validate.js +21 -3
- package/lib/deployment/environment-config.js +137 -0
- package/lib/deployment/environment.js +21 -98
- package/lib/deployment/push.js +32 -2
- package/lib/external-system/download.js +7 -0
- package/lib/external-system/test-auth.js +7 -3
- package/lib/external-system/test.js +5 -1
- package/lib/generator/index.js +174 -25
- package/lib/generator/wizard.js +13 -1
- package/lib/infrastructure/helpers.js +103 -20
- package/lib/infrastructure/index.js +88 -10
- package/lib/infrastructure/services.js +70 -15
- package/lib/schema/application-schema.json +24 -3
- package/lib/schema/external-system.schema.json +435 -413
- package/lib/utils/api.js +3 -3
- package/lib/utils/app-register-auth.js +25 -3
- package/lib/utils/cli-utils.js +20 -0
- package/lib/utils/compose-generator.js +76 -75
- package/lib/utils/compose-handlebars-helpers.js +43 -0
- package/lib/utils/compose-vector-helper.js +18 -0
- package/lib/utils/config-paths.js +127 -2
- package/lib/utils/credential-secrets-env.js +267 -0
- package/lib/utils/dev-cert-helper.js +122 -0
- package/lib/utils/device-code-helpers.js +224 -0
- package/lib/utils/device-code.js +37 -336
- package/lib/utils/docker-build.js +40 -8
- package/lib/utils/env-copy.js +83 -13
- package/lib/utils/env-map.js +35 -5
- package/lib/utils/env-template.js +6 -5
- package/lib/utils/error-formatters/http-status-errors.js +20 -1
- package/lib/utils/help-builder.js +15 -2
- package/lib/utils/infra-status.js +30 -1
- package/lib/utils/local-secrets.js +7 -52
- package/lib/utils/mutagen-install.js +195 -0
- package/lib/utils/mutagen.js +146 -0
- package/lib/utils/paths.js +49 -33
- package/lib/utils/port-resolver.js +28 -16
- package/lib/utils/remote-dev-auth.js +38 -0
- package/lib/utils/remote-docker-env.js +43 -0
- package/lib/utils/remote-secrets-loader.js +60 -0
- package/lib/utils/secrets-generator.js +94 -6
- package/lib/utils/secrets-helpers.js +33 -25
- package/lib/utils/secrets-path.js +2 -2
- package/lib/utils/secrets-utils.js +52 -1
- package/lib/utils/secrets-validation.js +84 -0
- package/lib/utils/ssh-key-helper.js +116 -0
- package/lib/utils/token-manager-messages.js +90 -0
- package/lib/utils/token-manager.js +5 -4
- package/lib/utils/variable-transformer.js +3 -3
- package/lib/validation/validate.js +1 -1
- package/lib/validation/validator.js +65 -0
- package/package.json +4 -2
- package/scripts/install-local.js +34 -15
- package/templates/README.md +0 -1
- package/templates/applications/README.md.hbs +4 -4
- package/templates/applications/dataplane/application.yaml +5 -4
- package/templates/applications/dataplane/env.template +12 -7
- package/templates/applications/keycloak/env.template +2 -0
- package/templates/applications/miso-controller/application.yaml +1 -0
- package/templates/applications/miso-controller/env.template +11 -9
- package/templates/external-system/external-system.json.hbs +1 -16
- package/templates/python/docker-compose.hbs +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
|
@@ -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
|
|
21
|
-
* Sets a secret value in either user secrets or
|
|
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
|
}
|
package/lib/commands/up-miso.js
CHANGED
|
@@ -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.'));
|
package/lib/commands/upload.js
CHANGED
|
@@ -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
|
+
};
|