@aifabrix/builder 2.22.1 → 2.31.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/jest.config.coverage.js +37 -0
- package/lib/api/pipeline.api.js +10 -9
- package/lib/app-deploy.js +36 -14
- package/lib/app-list.js +191 -71
- package/lib/app-prompts.js +77 -26
- package/lib/app-readme.js +123 -5
- package/lib/app-rotate-secret.js +101 -57
- package/lib/app-run-helpers.js +200 -172
- package/lib/app-run.js +137 -68
- package/lib/audit-logger.js +8 -7
- package/lib/build.js +161 -250
- package/lib/cli.js +73 -65
- package/lib/commands/login.js +45 -31
- package/lib/commands/logout.js +181 -0
- package/lib/commands/secrets-set.js +2 -2
- package/lib/commands/secure.js +61 -26
- package/lib/config.js +79 -45
- package/lib/datasource-deploy.js +89 -29
- package/lib/deployer.js +164 -129
- package/lib/diff.js +63 -21
- package/lib/environment-deploy.js +36 -19
- package/lib/external-system-deploy.js +134 -66
- package/lib/external-system-download.js +244 -171
- package/lib/external-system-test.js +199 -164
- package/lib/generator-external.js +145 -72
- package/lib/generator-helpers.js +49 -17
- package/lib/generator-split.js +105 -58
- package/lib/infra.js +101 -131
- package/lib/schema/application-schema.json +895 -896
- package/lib/schema/env-config.yaml +11 -4
- package/lib/template-validator.js +13 -4
- package/lib/utils/api.js +8 -8
- package/lib/utils/app-register-auth.js +36 -18
- package/lib/utils/app-run-containers.js +140 -0
- package/lib/utils/auth-headers.js +6 -6
- package/lib/utils/build-copy.js +60 -2
- package/lib/utils/build-helpers.js +94 -0
- package/lib/utils/cli-utils.js +177 -76
- package/lib/utils/compose-generator.js +12 -2
- package/lib/utils/config-tokens.js +151 -9
- package/lib/utils/deployment-errors.js +137 -69
- package/lib/utils/deployment-validation-helpers.js +103 -0
- package/lib/utils/docker-build.js +57 -0
- package/lib/utils/dockerfile-utils.js +13 -3
- package/lib/utils/env-copy.js +163 -94
- package/lib/utils/env-map.js +226 -86
- package/lib/utils/environment-checker.js +2 -2
- package/lib/utils/error-formatters/network-errors.js +0 -1
- package/lib/utils/external-system-display.js +14 -19
- package/lib/utils/external-system-env-helpers.js +107 -0
- package/lib/utils/external-system-test-helpers.js +144 -0
- package/lib/utils/health-check.js +10 -8
- package/lib/utils/infra-status.js +123 -0
- package/lib/utils/local-secrets.js +3 -2
- package/lib/utils/paths.js +228 -49
- package/lib/utils/schema-loader.js +125 -57
- package/lib/utils/token-manager.js +10 -7
- package/lib/utils/yaml-preserve.js +55 -16
- package/lib/validate.js +87 -89
- package/package.json +4 -4
- package/scripts/ci-fix.sh +19 -0
- package/scripts/ci-simulate.sh +19 -0
- package/templates/applications/miso-controller/test.yaml +1 -0
- package/templates/python/Dockerfile.hbs +8 -45
- package/templates/typescript/Dockerfile.hbs +8 -42
package/lib/commands/login.js
CHANGED
|
@@ -320,6 +320,48 @@ function buildScope(offline, customScope) {
|
|
|
320
320
|
return defaultScope;
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Validate device code API response
|
|
325
|
+
* @param {Object} deviceCodeApiResponse - API response
|
|
326
|
+
* @throws {Error} If response is invalid
|
|
327
|
+
*/
|
|
328
|
+
function validateDeviceCodeResponse(deviceCodeApiResponse) {
|
|
329
|
+
if (!deviceCodeApiResponse) {
|
|
330
|
+
throw new Error('Device code flow initiation returned no response');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!deviceCodeApiResponse.success) {
|
|
334
|
+
const errorMessage = deviceCodeApiResponse.formattedError ||
|
|
335
|
+
deviceCodeApiResponse.error ||
|
|
336
|
+
'Device code flow initiation failed';
|
|
337
|
+
const error = new Error(errorMessage);
|
|
338
|
+
if (deviceCodeApiResponse.formattedError) {
|
|
339
|
+
error.formattedError = deviceCodeApiResponse.formattedError;
|
|
340
|
+
}
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!deviceCodeApiResponse.data) {
|
|
345
|
+
throw new Error('Device code flow initiation returned no data');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Convert API response to device code format
|
|
351
|
+
* @param {Object} apiResponse - API response data
|
|
352
|
+
* @returns {Object} Device code response in snake_case format
|
|
353
|
+
*/
|
|
354
|
+
function convertDeviceCodeResponse(apiResponse) {
|
|
355
|
+
const deviceCodeData = apiResponse.data || apiResponse;
|
|
356
|
+
return {
|
|
357
|
+
device_code: deviceCodeData.deviceCode || deviceCodeData.device_code,
|
|
358
|
+
user_code: deviceCodeData.userCode || deviceCodeData.user_code,
|
|
359
|
+
verification_uri: deviceCodeData.verificationUri || deviceCodeData.verification_uri,
|
|
360
|
+
expires_in: deviceCodeData.expiresIn || deviceCodeData.expires_in || 600,
|
|
361
|
+
interval: deviceCodeData.interval || 5
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
323
365
|
/**
|
|
324
366
|
* Handle device code flow login
|
|
325
367
|
* @async
|
|
@@ -343,39 +385,11 @@ async function handleDeviceCodeLogin(controllerUrl, environment, offline, scope)
|
|
|
343
385
|
const deviceCodeApiResponse = await initiateDeviceCodeFlow(controllerUrl, envKey, requestScope);
|
|
344
386
|
|
|
345
387
|
// Validate response structure
|
|
346
|
-
|
|
347
|
-
throw new Error('Device code flow initiation returned no response');
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Check if API call was successful
|
|
351
|
-
if (!deviceCodeApiResponse.success) {
|
|
352
|
-
const errorMessage = deviceCodeApiResponse.formattedError ||
|
|
353
|
-
deviceCodeApiResponse.error ||
|
|
354
|
-
'Device code flow initiation failed';
|
|
355
|
-
const error = new Error(errorMessage);
|
|
356
|
-
// Preserve formattedError for better error display
|
|
357
|
-
if (deviceCodeApiResponse.formattedError) {
|
|
358
|
-
error.formattedError = deviceCodeApiResponse.formattedError;
|
|
359
|
-
}
|
|
360
|
-
throw error;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Handle API response format: { success: boolean, data: DeviceCodeResponse }
|
|
364
|
-
if (!deviceCodeApiResponse.data) {
|
|
365
|
-
throw new Error('Device code flow initiation returned no data');
|
|
366
|
-
}
|
|
388
|
+
validateDeviceCodeResponse(deviceCodeApiResponse);
|
|
367
389
|
|
|
390
|
+
// Convert API response to device code format
|
|
368
391
|
const apiResponse = deviceCodeApiResponse.data;
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
// Convert camelCase from API to snake_case for compatibility with existing code
|
|
372
|
-
const deviceCodeResponse = {
|
|
373
|
-
device_code: deviceCodeData.deviceCode || deviceCodeData.device_code,
|
|
374
|
-
user_code: deviceCodeData.userCode || deviceCodeData.user_code,
|
|
375
|
-
verification_uri: deviceCodeData.verificationUri || deviceCodeData.verification_uri,
|
|
376
|
-
expires_in: deviceCodeData.expiresIn || deviceCodeData.expires_in || 600,
|
|
377
|
-
interval: deviceCodeData.interval || 5
|
|
378
|
-
};
|
|
392
|
+
const deviceCodeResponse = convertDeviceCodeResponse(apiResponse);
|
|
379
393
|
|
|
380
394
|
displayDeviceCodeInfo(deviceCodeResponse.user_code, deviceCodeResponse.verification_uri, logger, chalk);
|
|
381
395
|
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder - Logout Command
|
|
3
|
+
*
|
|
4
|
+
* Handles clearing authentication tokens from config.yaml
|
|
5
|
+
* Supports clearing all tokens or specific tokens based on options
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Logout command implementation for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const chalk = require('chalk');
|
|
13
|
+
const {
|
|
14
|
+
clearDeviceToken,
|
|
15
|
+
clearAllDeviceTokens,
|
|
16
|
+
clearClientToken,
|
|
17
|
+
clearAllClientTokens,
|
|
18
|
+
clearClientTokensForEnvironment,
|
|
19
|
+
normalizeControllerUrl
|
|
20
|
+
} = require('../config');
|
|
21
|
+
const logger = require('../utils/logger');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate environment key format
|
|
27
|
+
* @param {string} envKey - Environment key to validate
|
|
28
|
+
* @throws {Error} If environment key format is invalid
|
|
29
|
+
*/
|
|
30
|
+
function validateEnvironmentKey(envKey) {
|
|
31
|
+
if (!/^[a-z0-9-_]+$/i.test(envKey)) {
|
|
32
|
+
throw new Error('Environment key must contain only letters, numbers, hyphens, and underscores');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate controller URL format
|
|
38
|
+
* @param {string} url - Controller URL to validate
|
|
39
|
+
* @throws {Error} If URL format is invalid
|
|
40
|
+
*/
|
|
41
|
+
function validateControllerUrl(url) {
|
|
42
|
+
if (!url || typeof url !== 'string' || url.trim().length === 0) {
|
|
43
|
+
throw new Error('Controller URL is required and must be a non-empty string');
|
|
44
|
+
}
|
|
45
|
+
const normalized = normalizeControllerUrl(url);
|
|
46
|
+
// Check for valid URL format: http:// or https:// followed by valid hostname
|
|
47
|
+
// Hostname should be localhost or contain at least one dot (domain)
|
|
48
|
+
try {
|
|
49
|
+
const urlObj = new URL(normalized);
|
|
50
|
+
const hostname = urlObj.hostname;
|
|
51
|
+
// Allow localhost or hostnames with at least one dot (domain)
|
|
52
|
+
if (hostname !== 'localhost' && !hostname.includes('.')) {
|
|
53
|
+
throw new Error('Controller URL must be a valid HTTP or HTTPS URL');
|
|
54
|
+
}
|
|
55
|
+
} catch (urlError) {
|
|
56
|
+
throw new Error('Controller URL must be a valid HTTP or HTTPS URL');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Clear device tokens based on options
|
|
62
|
+
* @async
|
|
63
|
+
* @param {Object} options - Logout options
|
|
64
|
+
* @returns {Promise<number>} Number of device tokens cleared
|
|
65
|
+
*/
|
|
66
|
+
async function clearDeviceTokens(options) {
|
|
67
|
+
if (options.controller) {
|
|
68
|
+
// Clear specific controller device token
|
|
69
|
+
const cleared = await clearDeviceToken(options.controller);
|
|
70
|
+
if (cleared) {
|
|
71
|
+
logger.log(chalk.green(`✓ Cleared device token for controller: ${options.controller}`));
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
logger.log(chalk.gray(` No device token found for controller: ${options.controller}`));
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!options.environment && !options.app) {
|
|
79
|
+
// Clear all device tokens (only when no environment/app specified)
|
|
80
|
+
const cleared = await clearAllDeviceTokens();
|
|
81
|
+
if (cleared > 0) {
|
|
82
|
+
logger.log(chalk.green(`✓ Cleared ${cleared} device token(s)`));
|
|
83
|
+
} else {
|
|
84
|
+
logger.log(chalk.gray(' No device tokens found'));
|
|
85
|
+
}
|
|
86
|
+
return cleared;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Clear client tokens based on options
|
|
94
|
+
* @async
|
|
95
|
+
* @param {Object} options - Logout options
|
|
96
|
+
* @returns {Promise<number>} Number of client tokens cleared
|
|
97
|
+
*/
|
|
98
|
+
async function clearClientTokens(options) {
|
|
99
|
+
if (options.app && options.environment) {
|
|
100
|
+
// Clear specific app token in environment
|
|
101
|
+
const cleared = await clearClientToken(options.environment, options.app);
|
|
102
|
+
if (cleared) {
|
|
103
|
+
logger.log(chalk.green(`✓ Cleared client token for app '${options.app}' in environment '${options.environment}'`));
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
logger.log(chalk.gray(` No client token found for app '${options.app}' in environment '${options.environment}'`));
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (options.environment && !options.app) {
|
|
111
|
+
// Clear all client tokens for environment
|
|
112
|
+
const cleared = await clearClientTokensForEnvironment(options.environment);
|
|
113
|
+
if (cleared > 0) {
|
|
114
|
+
logger.log(chalk.green(`✓ Cleared ${cleared} client token(s) for environment '${options.environment}'`));
|
|
115
|
+
} else {
|
|
116
|
+
logger.log(chalk.gray(` No client tokens found for environment '${options.environment}'`));
|
|
117
|
+
}
|
|
118
|
+
return cleared;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!options.controller && !options.environment && !options.app) {
|
|
122
|
+
// Clear all client tokens (only when no specific options specified)
|
|
123
|
+
const cleared = await clearAllClientTokens();
|
|
124
|
+
if (cleared > 0) {
|
|
125
|
+
logger.log(chalk.green(`✓ Cleared ${cleared} client token(s)`));
|
|
126
|
+
} else {
|
|
127
|
+
logger.log(chalk.gray(' No client tokens found'));
|
|
128
|
+
}
|
|
129
|
+
return cleared;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Handle logout command action
|
|
137
|
+
* @async
|
|
138
|
+
* @function handleLogout
|
|
139
|
+
* @param {Object} options - Logout options
|
|
140
|
+
* @param {string} [options.controller] - Clear device tokens for specific controller
|
|
141
|
+
* @param {string} [options.environment] - Clear client tokens for specific environment
|
|
142
|
+
* @param {string} [options.app] - Clear client tokens for specific app (requires --environment)
|
|
143
|
+
* @returns {Promise<void>} Resolves when logout completes
|
|
144
|
+
* @throws {Error} If logout fails or options are invalid
|
|
145
|
+
*/
|
|
146
|
+
async function handleLogout(options) {
|
|
147
|
+
const configPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
|
|
148
|
+
logger.log(chalk.blue('\n🔓 Clearing authentication tokens...\n'));
|
|
149
|
+
|
|
150
|
+
// Validate options
|
|
151
|
+
if (options.app && !options.environment) {
|
|
152
|
+
throw new Error('--app requires --environment option');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if ('controller' in options && options.controller !== undefined && options.controller !== null) {
|
|
156
|
+
validateControllerUrl(options.controller);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if ('environment' in options && options.environment !== undefined && options.environment !== null) {
|
|
160
|
+
validateEnvironmentKey(options.environment);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Clear device tokens
|
|
164
|
+
const deviceTokensCleared = await clearDeviceTokens(options);
|
|
165
|
+
|
|
166
|
+
// Clear client tokens
|
|
167
|
+
const clientTokensCleared = await clearClientTokens(options);
|
|
168
|
+
|
|
169
|
+
// Summary
|
|
170
|
+
const totalCleared = deviceTokensCleared + clientTokensCleared;
|
|
171
|
+
if (totalCleared > 0) {
|
|
172
|
+
logger.log(chalk.green('\n✅ Successfully cleared tokens!'));
|
|
173
|
+
logger.log(chalk.gray(`Config file: ${configPath}\n`));
|
|
174
|
+
} else {
|
|
175
|
+
logger.log(chalk.yellow('\n⚠️ No tokens found to clear'));
|
|
176
|
+
logger.log(chalk.gray(`Config file: ${configPath}\n`));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = { handleLogout };
|
|
181
|
+
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const path = require('path');
|
|
13
|
-
const os = require('os');
|
|
14
13
|
const chalk = require('chalk');
|
|
15
14
|
const logger = require('../utils/logger');
|
|
16
15
|
const { getAifabrixSecretsPath } = require('../config');
|
|
17
16
|
const { saveLocalSecret, saveSecret } = require('../utils/local-secrets');
|
|
17
|
+
const pathsUtil = require('../utils/paths');
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Handle secrets set command action
|
|
@@ -61,7 +61,7 @@ async function handleSecretsSet(key, value, options) {
|
|
|
61
61
|
} else {
|
|
62
62
|
// Save to user secrets file
|
|
63
63
|
await saveLocalSecret(key, value);
|
|
64
|
-
const userSecretsPath = path.join(
|
|
64
|
+
const userSecretsPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
|
|
65
65
|
logger.log(chalk.green(`✓ Secret '${key}' saved to user secrets file: ${userSecretsPath}`));
|
|
66
66
|
}
|
|
67
67
|
}
|
package/lib/commands/secure.js
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
|
-
const os = require('os');
|
|
15
14
|
const yaml = require('js-yaml');
|
|
16
15
|
const inquirer = require('inquirer');
|
|
17
16
|
const chalk = require('chalk');
|
|
@@ -19,6 +18,7 @@ const logger = require('../utils/logger');
|
|
|
19
18
|
const { setSecretsEncryptionKey, getSecretsEncryptionKey } = require('../config');
|
|
20
19
|
const { validateEncryptionKey } = require('../utils/secrets-encryption');
|
|
21
20
|
const { encryptYamlValues } = require('../utils/yaml-preserve');
|
|
21
|
+
const pathsUtil = require('../utils/paths');
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Finds all secrets.local.yaml files to encrypt
|
|
@@ -32,7 +32,7 @@ async function findSecretsFiles() {
|
|
|
32
32
|
const files = [];
|
|
33
33
|
|
|
34
34
|
// User's secrets file
|
|
35
|
-
const userSecretsPath = path.join(
|
|
35
|
+
const userSecretsPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
|
|
36
36
|
if (fs.existsSync(userSecretsPath)) {
|
|
37
37
|
files.push({ path: userSecretsPath, type: 'user' });
|
|
38
38
|
}
|
|
@@ -120,20 +120,12 @@ async function promptForEncryptionKey() {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
*
|
|
124
|
-
* Sets encryption key and encrypts all secrets files
|
|
125
|
-
*
|
|
123
|
+
* Get encryption key from options or prompt user
|
|
126
124
|
* @async
|
|
127
|
-
* @function handleSecure
|
|
128
125
|
* @param {Object} options - Command options
|
|
129
|
-
* @
|
|
130
|
-
* @returns {Promise<void>} Resolves when encryption completes
|
|
131
|
-
* @throws {Error} If encryption fails
|
|
126
|
+
* @returns {Promise<string>} Encryption key
|
|
132
127
|
*/
|
|
133
|
-
async function
|
|
134
|
-
logger.log(chalk.blue('\n🔐 Securing secrets files...\n'));
|
|
135
|
-
|
|
136
|
-
// Get or prompt for encryption key
|
|
128
|
+
async function getEncryptionKey(options) {
|
|
137
129
|
let encryptionKey = options.secretsEncryption || options['secrets-encryption'];
|
|
138
130
|
if (!encryptionKey) {
|
|
139
131
|
// Check if key already exists in config
|
|
@@ -164,19 +156,17 @@ async function handleSecure(options) {
|
|
|
164
156
|
await setSecretsEncryptionKey(encryptionKey);
|
|
165
157
|
logger.log(chalk.green('✓ Encryption key saved to config.yaml'));
|
|
166
158
|
}
|
|
159
|
+
return encryptionKey;
|
|
160
|
+
}
|
|
167
161
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
logger.log(chalk.gray(`Found ${secretsFiles.length} secrets file(s) to process:\n`));
|
|
178
|
-
|
|
179
|
-
// Encrypt each file
|
|
162
|
+
/**
|
|
163
|
+
* Process and encrypt all secrets files
|
|
164
|
+
* @async
|
|
165
|
+
* @param {Array} secretsFiles - Array of secrets file objects
|
|
166
|
+
* @param {string} encryptionKey - Encryption key
|
|
167
|
+
* @returns {Promise<Object>} Object with totalEncrypted and totalValues
|
|
168
|
+
*/
|
|
169
|
+
async function processSecretsFiles(secretsFiles, encryptionKey) {
|
|
180
170
|
let totalEncrypted = 0;
|
|
181
171
|
let totalValues = 0;
|
|
182
172
|
|
|
@@ -197,11 +187,56 @@ async function handleSecure(options) {
|
|
|
197
187
|
}
|
|
198
188
|
}
|
|
199
189
|
|
|
190
|
+
return { totalEncrypted, totalValues };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Display encryption summary
|
|
195
|
+
* @param {number} filesCount - Number of files processed
|
|
196
|
+
* @param {number} totalEncrypted - Number of values encrypted
|
|
197
|
+
* @param {number} totalValues - Total number of values
|
|
198
|
+
*/
|
|
199
|
+
function displayEncryptionSummary(filesCount, totalEncrypted, totalValues) {
|
|
200
200
|
logger.log(chalk.green('\n✅ Encryption complete!'));
|
|
201
|
-
logger.log(chalk.gray(` Files processed: ${
|
|
201
|
+
logger.log(chalk.gray(` Files processed: ${filesCount}`));
|
|
202
202
|
logger.log(chalk.gray(` Values encrypted: ${totalEncrypted} of ${totalValues} total`));
|
|
203
203
|
logger.log(chalk.gray(' Encryption key stored in: ~/.aifabrix/config.yaml\n'));
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Handle secure command action
|
|
208
|
+
* Sets encryption key and encrypts all secrets files
|
|
209
|
+
*
|
|
210
|
+
* @async
|
|
211
|
+
* @function handleSecure
|
|
212
|
+
* @param {Object} options - Command options
|
|
213
|
+
* @param {string} [options.secretsEncryption] - Encryption key (optional, will prompt if not provided)
|
|
214
|
+
* @returns {Promise<void>} Resolves when encryption completes
|
|
215
|
+
* @throws {Error} If encryption fails
|
|
216
|
+
*/
|
|
217
|
+
async function handleSecure(options) {
|
|
218
|
+
logger.log(chalk.blue('\n🔐 Securing secrets files...\n'));
|
|
219
|
+
|
|
220
|
+
// Get or prompt for encryption key
|
|
221
|
+
const encryptionKey = await getEncryptionKey(options);
|
|
222
|
+
|
|
223
|
+
// Find all secrets files
|
|
224
|
+
const secretsFiles = await findSecretsFiles();
|
|
225
|
+
|
|
226
|
+
if (secretsFiles.length === 0) {
|
|
227
|
+
logger.log(chalk.yellow('⚠️ No secrets files found to encrypt'));
|
|
228
|
+
logger.log(chalk.gray(' Create ~/.aifabrix/secrets.local.yaml or configure aifabrix-secrets in config.yaml'));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
logger.log(chalk.gray(`Found ${secretsFiles.length} secrets file(s) to process:\n`));
|
|
233
|
+
|
|
234
|
+
// Process and encrypt all files
|
|
235
|
+
const { totalEncrypted, totalValues } = await processSecretsFiles(secretsFiles, encryptionKey);
|
|
236
|
+
|
|
237
|
+
// Display summary
|
|
238
|
+
displayEncryptionSummary(secretsFiles.length, totalEncrypted, totalValues);
|
|
239
|
+
}
|
|
240
|
+
|
|
206
241
|
module.exports = { handleSecure };
|
|
207
242
|
|
package/lib/config.js
CHANGED
|
@@ -47,6 +47,71 @@ function normalizeControllerUrl(url) {
|
|
|
47
47
|
return normalized;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Validate and normalize developer ID
|
|
52
|
+
* @param {*} developerId - Developer ID value (can be string, number, undefined, or null)
|
|
53
|
+
* @returns {string} Normalized developer ID as string
|
|
54
|
+
* @throws {Error} If developer ID is invalid
|
|
55
|
+
*/
|
|
56
|
+
function validateAndNormalizeDeveloperId(developerId) {
|
|
57
|
+
const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
|
|
58
|
+
|
|
59
|
+
if (typeof developerId === 'undefined' || developerId === null) {
|
|
60
|
+
return '0';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof developerId === 'number') {
|
|
64
|
+
if (developerId < 0 || !Number.isFinite(developerId)) {
|
|
65
|
+
throw new Error(`Invalid developer-id value: "${developerId}". Must be a non-negative digit string.`);
|
|
66
|
+
}
|
|
67
|
+
return String(developerId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (typeof developerId === 'string') {
|
|
71
|
+
if (!DEV_ID_DIGITS_REGEX.test(developerId)) {
|
|
72
|
+
throw new Error(`Invalid developer-id value: "${developerId}". Must contain only digits 0-9.`);
|
|
73
|
+
}
|
|
74
|
+
return developerId;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new Error(`Invalid developer-id value type: ${typeof developerId}. Must be a non-negative digit string.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Ensure default config values exist
|
|
82
|
+
* @param {Object} config - Configuration object
|
|
83
|
+
* @returns {Object} Config with defaults applied
|
|
84
|
+
*/
|
|
85
|
+
function applyConfigDefaults(config) {
|
|
86
|
+
// Ensure environment defaults to 'dev' if not set
|
|
87
|
+
if (typeof config.environment === 'undefined') {
|
|
88
|
+
config.environment = 'dev';
|
|
89
|
+
}
|
|
90
|
+
// Ensure environments object exists
|
|
91
|
+
if (typeof config.environments !== 'object' || config.environments === null) {
|
|
92
|
+
config.environments = {};
|
|
93
|
+
}
|
|
94
|
+
// Ensure device object exists at root level
|
|
95
|
+
if (typeof config.device !== 'object' || config.device === null) {
|
|
96
|
+
config.device = {};
|
|
97
|
+
}
|
|
98
|
+
return config;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get default config when file doesn't exist
|
|
103
|
+
* @returns {Object} Default configuration
|
|
104
|
+
*/
|
|
105
|
+
function getDefaultConfig() {
|
|
106
|
+
cachedDeveloperId = '0';
|
|
107
|
+
return {
|
|
108
|
+
'developer-id': '0',
|
|
109
|
+
environment: 'dev',
|
|
110
|
+
environments: {},
|
|
111
|
+
device: {}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
50
115
|
async function getConfig() {
|
|
51
116
|
try {
|
|
52
117
|
const configContent = await fs.readFile(RUNTIME_CONFIG_FILE, 'utf8');
|
|
@@ -57,49 +122,18 @@ async function getConfig() {
|
|
|
57
122
|
config = {};
|
|
58
123
|
}
|
|
59
124
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Convert numeric to string to preserve type consistency
|
|
66
|
-
if (config['developer-id'] < 0 || !Number.isFinite(config['developer-id'])) {
|
|
67
|
-
throw new Error(`Invalid developer-id value: "${config['developer-id']}". Must be a non-negative digit string.`);
|
|
68
|
-
}
|
|
69
|
-
config['developer-id'] = String(config['developer-id']);
|
|
70
|
-
} else if (typeof config['developer-id'] === 'string') {
|
|
71
|
-
if (!DEV_ID_DIGITS_REGEX.test(config['developer-id'])) {
|
|
72
|
-
throw new Error(`Invalid developer-id value: "${config['developer-id']}". Must contain only digits 0-9.`);
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
throw new Error(`Invalid developer-id value type: ${typeof config['developer-id']}. Must be a non-negative digit string.`);
|
|
76
|
-
}
|
|
125
|
+
// Validate and normalize developer ID
|
|
126
|
+
config['developer-id'] = validateAndNormalizeDeveloperId(config['developer-id']);
|
|
127
|
+
|
|
128
|
+
// Apply defaults
|
|
129
|
+
config = applyConfigDefaults(config);
|
|
77
130
|
|
|
78
|
-
// Ensure environment defaults to 'dev' if not set
|
|
79
|
-
if (typeof config.environment === 'undefined') {
|
|
80
|
-
config.environment = 'dev';
|
|
81
|
-
}
|
|
82
|
-
// Ensure environments object exists
|
|
83
|
-
if (typeof config.environments !== 'object' || config.environments === null) {
|
|
84
|
-
config.environments = {};
|
|
85
|
-
}
|
|
86
|
-
// Ensure device object exists at root level
|
|
87
|
-
if (typeof config.device !== 'object' || config.device === null) {
|
|
88
|
-
config.device = {};
|
|
89
|
-
}
|
|
90
131
|
// Cache developer ID as property for easy access (string, default "0")
|
|
91
132
|
cachedDeveloperId = config['developer-id'] !== undefined ? config['developer-id'] : '0';
|
|
92
133
|
return config;
|
|
93
134
|
} catch (error) {
|
|
94
135
|
if (error.code === 'ENOENT') {
|
|
95
|
-
|
|
96
|
-
cachedDeveloperId = '0';
|
|
97
|
-
return {
|
|
98
|
-
'developer-id': '0',
|
|
99
|
-
environment: 'dev',
|
|
100
|
-
environments: {},
|
|
101
|
-
device: {}
|
|
102
|
-
};
|
|
136
|
+
return getDefaultConfig();
|
|
103
137
|
}
|
|
104
138
|
throw new Error(`Failed to read config: ${error.message}`);
|
|
105
139
|
}
|
|
@@ -344,14 +378,14 @@ Object.defineProperty(exportsObj, 'developerId', {
|
|
|
344
378
|
|
|
345
379
|
// Token management functions - created after dependencies are defined
|
|
346
380
|
const { createTokenManagementFunctions } = require('./utils/config-tokens');
|
|
347
|
-
const tokenFunctions = createTokenManagementFunctions(
|
|
348
|
-
getConfig,
|
|
349
|
-
saveConfig,
|
|
350
|
-
getSecretsEncryptionKey,
|
|
351
|
-
encryptTokenValue,
|
|
352
|
-
decryptTokenValue,
|
|
353
|
-
require('./utils/token-encryption').isTokenEncrypted
|
|
354
|
-
);
|
|
381
|
+
const tokenFunctions = createTokenManagementFunctions({
|
|
382
|
+
getConfigFn: getConfig,
|
|
383
|
+
saveConfigFn: saveConfig,
|
|
384
|
+
getSecretsEncryptionKeyFn: getSecretsEncryptionKey,
|
|
385
|
+
encryptTokenValueFn: encryptTokenValue,
|
|
386
|
+
decryptTokenValueFn: decryptTokenValue,
|
|
387
|
+
isTokenEncryptedFn: require('./utils/token-encryption').isTokenEncrypted
|
|
388
|
+
});
|
|
355
389
|
Object.assign(exportsObj, tokenFunctions);
|
|
356
390
|
|
|
357
391
|
// Path configuration functions - created after getConfig/saveConfig are defined
|