@aifabrix/builder 2.2.0 → 2.3.2
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/lib/app-run-helpers.js +381 -0
- package/lib/app-run.js +17 -392
- package/lib/build.js +102 -8
- package/lib/cli.js +27 -10
- package/lib/commands/secure.js +242 -0
- package/lib/config.js +106 -19
- package/lib/infra.js +14 -7
- package/lib/push.js +34 -7
- package/lib/secrets.js +75 -24
- package/lib/templates.js +1 -1
- package/lib/utils/build-copy.js +25 -5
- package/lib/utils/cli-utils.js +28 -3
- package/lib/utils/compose-generator.js +17 -4
- package/lib/utils/dev-config.js +8 -7
- package/lib/utils/docker-build.js +24 -0
- package/lib/utils/infra-containers.js +3 -2
- package/lib/utils/secrets-encryption.js +203 -0
- package/lib/utils/secrets-path.js +22 -3
- package/lib/utils/yaml-preserve.js +214 -0
- package/package.json +2 -2
- package/test-output.txt +0 -5431
package/lib/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ const chalk = require('chalk');
|
|
|
21
21
|
const logger = require('./utils/logger');
|
|
22
22
|
const { validateCommand, handleCommandError } = require('./utils/cli-utils');
|
|
23
23
|
const { handleLogin } = require('./commands/login');
|
|
24
|
+
const { handleSecure } = require('./commands/secure');
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Sets up all CLI commands on the Commander program instance
|
|
@@ -335,18 +336,19 @@ function setupCommands(program) {
|
|
|
335
336
|
// Commander.js converts --set-id to setId in options object
|
|
336
337
|
const setIdValue = options.setId || options['set-id'];
|
|
337
338
|
if (setIdValue) {
|
|
338
|
-
const
|
|
339
|
-
if (
|
|
340
|
-
throw new Error('Developer ID must be a non-negative
|
|
339
|
+
const digitsOnly = /^[0-9]+$/.test(setIdValue);
|
|
340
|
+
if (!digitsOnly) {
|
|
341
|
+
throw new Error('Developer ID must be a non-negative digit string (0 = default infra, > 0 = developer-specific)');
|
|
341
342
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
343
|
+
// Convert to number for setDeveloperId and getDevPorts (they expect numbers)
|
|
344
|
+
const devIdNum = parseInt(setIdValue, 10);
|
|
345
|
+
await config.setDeveloperId(devIdNum);
|
|
346
|
+
process.env.AIFABRIX_DEVELOPERID = setIdValue;
|
|
347
|
+
logger.log(chalk.green(`✓ Developer ID set to ${setIdValue}`));
|
|
345
348
|
// Use the ID we just set instead of reading from file to avoid race conditions
|
|
346
|
-
const
|
|
347
|
-
const ports = devConfig.getDevPorts(devId);
|
|
349
|
+
const ports = devConfig.getDevPorts(devIdNum);
|
|
348
350
|
logger.log('\n🔧 Developer Configuration\n');
|
|
349
|
-
logger.log(`Developer ID: ${
|
|
351
|
+
logger.log(`Developer ID: ${setIdValue}`);
|
|
350
352
|
logger.log('\nPorts:');
|
|
351
353
|
logger.log(` App: ${ports.app}`);
|
|
352
354
|
logger.log(` Postgres: ${ports.postgres}`);
|
|
@@ -358,7 +360,9 @@ function setupCommands(program) {
|
|
|
358
360
|
}
|
|
359
361
|
|
|
360
362
|
const devId = await config.getDeveloperId();
|
|
361
|
-
|
|
363
|
+
// Convert string developer ID to number for getDevPorts
|
|
364
|
+
const devIdNum = parseInt(devId, 10);
|
|
365
|
+
const ports = devConfig.getDevPorts(devIdNum);
|
|
362
366
|
logger.log('\n🔧 Developer Configuration\n');
|
|
363
367
|
logger.log(`Developer ID: ${devId}`);
|
|
364
368
|
logger.log('\nPorts:');
|
|
@@ -373,6 +377,19 @@ function setupCommands(program) {
|
|
|
373
377
|
process.exit(1);
|
|
374
378
|
}
|
|
375
379
|
});
|
|
380
|
+
|
|
381
|
+
// Security command
|
|
382
|
+
program.command('secure')
|
|
383
|
+
.description('Encrypt secrets in secrets.local.yaml files for ISO 27001 compliance')
|
|
384
|
+
.option('--secrets-encryption <key>', 'Encryption key (32 bytes, hex or base64)')
|
|
385
|
+
.action(async(options) => {
|
|
386
|
+
try {
|
|
387
|
+
await handleSecure(options);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
handleCommandError(error, 'secure');
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
376
393
|
}
|
|
377
394
|
|
|
378
395
|
module.exports = {
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder - Secure Command
|
|
3
|
+
*
|
|
4
|
+
* Handles encryption of secrets in secrets.local.yaml files
|
|
5
|
+
* Sets encryption key in config.yaml and encrypts all secret values
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Secure command implementation for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const yaml = require('js-yaml');
|
|
16
|
+
const inquirer = require('inquirer');
|
|
17
|
+
const chalk = require('chalk');
|
|
18
|
+
const logger = require('../utils/logger');
|
|
19
|
+
const { setSecretsEncryptionKey, getSecretsEncryptionKey } = require('../config');
|
|
20
|
+
const { validateEncryptionKey } = require('../utils/secrets-encryption');
|
|
21
|
+
const { encryptYamlValues } = require('../utils/yaml-preserve');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Finds all secrets.local.yaml files to encrypt
|
|
25
|
+
* Includes user secrets file and build secrets from all apps
|
|
26
|
+
*
|
|
27
|
+
* @async
|
|
28
|
+
* @function findSecretsFiles
|
|
29
|
+
* @returns {Promise<Array<{path: string, type: string}>>} Array of secrets file paths
|
|
30
|
+
*/
|
|
31
|
+
async function findSecretsFiles() {
|
|
32
|
+
const files = [];
|
|
33
|
+
|
|
34
|
+
// User's secrets file
|
|
35
|
+
const userSecretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
|
|
36
|
+
if (fs.existsSync(userSecretsPath)) {
|
|
37
|
+
files.push({ path: userSecretsPath, type: 'user' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Find all apps and check for build.secrets
|
|
41
|
+
// Scan builder directory for apps
|
|
42
|
+
try {
|
|
43
|
+
const builderDir = path.join(process.cwd(), 'builder');
|
|
44
|
+
if (fs.existsSync(builderDir)) {
|
|
45
|
+
const entries = fs.readdirSync(builderDir, { withFileTypes: true });
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
const appName = entry.name;
|
|
49
|
+
const variablesPath = path.join(builderDir, appName, 'variables.yaml');
|
|
50
|
+
if (fs.existsSync(variablesPath)) {
|
|
51
|
+
try {
|
|
52
|
+
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
53
|
+
const variables = yaml.load(variablesContent);
|
|
54
|
+
|
|
55
|
+
if (variables?.build?.secrets) {
|
|
56
|
+
const buildSecretsPath = path.resolve(
|
|
57
|
+
path.dirname(variablesPath),
|
|
58
|
+
variables.build.secrets
|
|
59
|
+
);
|
|
60
|
+
if (fs.existsSync(buildSecretsPath)) {
|
|
61
|
+
files.push({ path: buildSecretsPath, type: `app:${appName}` });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Ignore errors, continue
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// Ignore errors, continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check config.yaml for general secrets-path
|
|
76
|
+
try {
|
|
77
|
+
const { getSecretsPath } = require('../config');
|
|
78
|
+
const generalSecretsPath = await getSecretsPath();
|
|
79
|
+
if (generalSecretsPath) {
|
|
80
|
+
const resolvedPath = path.isAbsolute(generalSecretsPath)
|
|
81
|
+
? generalSecretsPath
|
|
82
|
+
: path.resolve(process.cwd(), generalSecretsPath);
|
|
83
|
+
if (fs.existsSync(resolvedPath) && !files.some(f => f.path === resolvedPath)) {
|
|
84
|
+
files.push({ path: resolvedPath, type: 'general' });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// Ignore errors, continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return files;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Encrypts all non-encrypted values in a secrets file
|
|
96
|
+
* Preserves YAML structure, comments, and formatting
|
|
97
|
+
* Skips URLs (http:// and https://) as they are not secrets
|
|
98
|
+
*
|
|
99
|
+
* @async
|
|
100
|
+
* @function encryptSecretsFile
|
|
101
|
+
* @param {string} filePath - Path to secrets file
|
|
102
|
+
* @param {string} encryptionKey - Encryption key
|
|
103
|
+
* @returns {Promise<{encrypted: number, total: number}>} Count of encrypted values
|
|
104
|
+
*/
|
|
105
|
+
async function encryptSecretsFile(filePath, encryptionKey) {
|
|
106
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
107
|
+
|
|
108
|
+
// Validate that file contains valid YAML structure (optional check)
|
|
109
|
+
try {
|
|
110
|
+
const secrets = yaml.load(content);
|
|
111
|
+
if (!secrets || typeof secrets !== 'object') {
|
|
112
|
+
throw new Error(`Invalid secrets file format: ${filePath}`);
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// If YAML parsing fails, still try to encrypt (might have syntax issues but could be fixable)
|
|
116
|
+
// The line-by-line parser will handle it gracefully
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Use line-by-line encryption to preserve comments and formatting
|
|
120
|
+
const result = encryptYamlValues(content, encryptionKey);
|
|
121
|
+
|
|
122
|
+
// Write back to file preserving all formatting
|
|
123
|
+
fs.writeFileSync(filePath, result.content, { mode: 0o600 });
|
|
124
|
+
|
|
125
|
+
return { encrypted: result.encrypted, total: result.total };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Prompt for encryption key if not provided
|
|
130
|
+
*
|
|
131
|
+
* @async
|
|
132
|
+
* @function promptForEncryptionKey
|
|
133
|
+
* @returns {Promise<string>} Encryption key
|
|
134
|
+
*/
|
|
135
|
+
async function promptForEncryptionKey() {
|
|
136
|
+
const answer = await inquirer.prompt([{
|
|
137
|
+
type: 'password',
|
|
138
|
+
name: 'key',
|
|
139
|
+
message: 'Enter encryption key (32 bytes, hex or base64):',
|
|
140
|
+
mask: '*',
|
|
141
|
+
validate: (input) => {
|
|
142
|
+
if (!input || input.trim().length === 0) {
|
|
143
|
+
return 'Encryption key is required';
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
validateEncryptionKey(input.trim());
|
|
147
|
+
return true;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return error.message;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}]);
|
|
153
|
+
|
|
154
|
+
return answer.key.trim();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Handle secure command action
|
|
159
|
+
* Sets encryption key and encrypts all secrets files
|
|
160
|
+
*
|
|
161
|
+
* @async
|
|
162
|
+
* @function handleSecure
|
|
163
|
+
* @param {Object} options - Command options
|
|
164
|
+
* @param {string} [options.secretsEncryption] - Encryption key (optional, will prompt if not provided)
|
|
165
|
+
* @returns {Promise<void>} Resolves when encryption completes
|
|
166
|
+
* @throws {Error} If encryption fails
|
|
167
|
+
*/
|
|
168
|
+
async function handleSecure(options) {
|
|
169
|
+
logger.log(chalk.blue('\n🔐 Securing secrets files...\n'));
|
|
170
|
+
|
|
171
|
+
// Get or prompt for encryption key
|
|
172
|
+
let encryptionKey = options.secretsEncryption || options['secrets-encryption'];
|
|
173
|
+
if (!encryptionKey) {
|
|
174
|
+
// Check if key already exists in config
|
|
175
|
+
const existingKey = await getSecretsEncryptionKey();
|
|
176
|
+
if (existingKey) {
|
|
177
|
+
logger.log(chalk.yellow('⚠️ Encryption key already configured in config.yaml'));
|
|
178
|
+
const useExisting = await inquirer.prompt([{
|
|
179
|
+
type: 'confirm',
|
|
180
|
+
name: 'use',
|
|
181
|
+
message: 'Use existing encryption key?',
|
|
182
|
+
default: true
|
|
183
|
+
}]);
|
|
184
|
+
if (useExisting.use) {
|
|
185
|
+
encryptionKey = existingKey;
|
|
186
|
+
} else {
|
|
187
|
+
encryptionKey = await promptForEncryptionKey();
|
|
188
|
+
await setSecretsEncryptionKey(encryptionKey);
|
|
189
|
+
logger.log(chalk.green('✓ Encryption key saved to config.yaml'));
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
encryptionKey = await promptForEncryptionKey();
|
|
193
|
+
await setSecretsEncryptionKey(encryptionKey);
|
|
194
|
+
logger.log(chalk.green('✓ Encryption key saved to config.yaml'));
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
// Validate and save the provided key
|
|
198
|
+
validateEncryptionKey(encryptionKey);
|
|
199
|
+
await setSecretsEncryptionKey(encryptionKey);
|
|
200
|
+
logger.log(chalk.green('✓ Encryption key saved to config.yaml'));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Find all secrets files
|
|
204
|
+
const secretsFiles = await findSecretsFiles();
|
|
205
|
+
|
|
206
|
+
if (secretsFiles.length === 0) {
|
|
207
|
+
logger.log(chalk.yellow('⚠️ No secrets files found to encrypt'));
|
|
208
|
+
logger.log(chalk.gray(' Create ~/.aifabrix/secrets.local.yaml or configure build.secrets in variables.yaml'));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
logger.log(chalk.gray(`Found ${secretsFiles.length} secrets file(s) to process:\n`));
|
|
213
|
+
|
|
214
|
+
// Encrypt each file
|
|
215
|
+
let totalEncrypted = 0;
|
|
216
|
+
let totalValues = 0;
|
|
217
|
+
|
|
218
|
+
for (const file of secretsFiles) {
|
|
219
|
+
try {
|
|
220
|
+
logger.log(chalk.gray(`Processing: ${file.path} (${file.type})`));
|
|
221
|
+
const result = await encryptSecretsFile(file.path, encryptionKey);
|
|
222
|
+
totalEncrypted += result.encrypted;
|
|
223
|
+
totalValues += result.total;
|
|
224
|
+
|
|
225
|
+
if (result.encrypted > 0) {
|
|
226
|
+
logger.log(chalk.green(` ✓ Encrypted ${result.encrypted} of ${result.total} values`));
|
|
227
|
+
} else {
|
|
228
|
+
logger.log(chalk.gray(` - All values already encrypted (${result.total} total)`));
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
logger.log(chalk.red(` ✗ Error: ${error.message}`));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
logger.log(chalk.green('\n✅ Encryption complete!'));
|
|
236
|
+
logger.log(chalk.gray(` Files processed: ${secretsFiles.length}`));
|
|
237
|
+
logger.log(chalk.gray(` Values encrypted: ${totalEncrypted} of ${totalValues} total`));
|
|
238
|
+
logger.log(chalk.gray(' Encryption key stored in: ~/.aifabrix/config.yaml\n'));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = { handleSecure };
|
|
242
|
+
|
package/lib/config.js
CHANGED
|
@@ -35,10 +35,24 @@ async function getConfig() {
|
|
|
35
35
|
config = {};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// Ensure
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
// Ensure developer-id exists as a digit-only string (default "0") and validate
|
|
39
|
+
const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
|
|
40
|
+
if (typeof config['developer-id'] === 'undefined' || config['developer-id'] === null) {
|
|
41
|
+
config['developer-id'] = '0';
|
|
42
|
+
} else if (typeof config['developer-id'] === 'number') {
|
|
43
|
+
// Convert numeric to string to preserve type consistency
|
|
44
|
+
if (config['developer-id'] < 0 || !Number.isFinite(config['developer-id'])) {
|
|
45
|
+
throw new Error(`Invalid developer-id value: "${config['developer-id']}". Must be a non-negative digit string.`);
|
|
46
|
+
}
|
|
47
|
+
config['developer-id'] = String(config['developer-id']);
|
|
48
|
+
} else if (typeof config['developer-id'] === 'string') {
|
|
49
|
+
if (!DEV_ID_DIGITS_REGEX.test(config['developer-id'])) {
|
|
50
|
+
throw new Error(`Invalid developer-id value: "${config['developer-id']}". Must contain only digits 0-9.`);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
throw new Error(`Invalid developer-id value type: ${typeof config['developer-id']}. Must be a non-negative digit string.`);
|
|
41
54
|
}
|
|
55
|
+
|
|
42
56
|
// Ensure environment defaults to 'dev' if not set
|
|
43
57
|
if (typeof config.environment === 'undefined') {
|
|
44
58
|
config.environment = 'dev';
|
|
@@ -51,15 +65,15 @@ async function getConfig() {
|
|
|
51
65
|
if (typeof config.device !== 'object' || config.device === null) {
|
|
52
66
|
config.device = {};
|
|
53
67
|
}
|
|
54
|
-
// Cache developer ID as property for easy access (
|
|
55
|
-
cachedDeveloperId = config['developer-id'] !== undefined ? config['developer-id'] : 0;
|
|
68
|
+
// Cache developer ID as property for easy access (string, default "0")
|
|
69
|
+
cachedDeveloperId = config['developer-id'] !== undefined ? config['developer-id'] : '0';
|
|
56
70
|
return config;
|
|
57
71
|
} catch (error) {
|
|
58
72
|
if (error.code === 'ENOENT') {
|
|
59
|
-
// Default developer ID is 0, default environment is 'dev'
|
|
60
|
-
cachedDeveloperId = 0;
|
|
73
|
+
// Default developer ID is "0", default environment is 'dev'
|
|
74
|
+
cachedDeveloperId = '0';
|
|
61
75
|
return {
|
|
62
|
-
'developer-id': 0,
|
|
76
|
+
'developer-id': '0',
|
|
63
77
|
environment: 'dev',
|
|
64
78
|
environments: {},
|
|
65
79
|
device: {}
|
|
@@ -80,7 +94,8 @@ async function saveConfig(data) {
|
|
|
80
94
|
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
81
95
|
|
|
82
96
|
// Set secure permissions
|
|
83
|
-
|
|
97
|
+
// Force quotes to ensure numeric-like strings (e.g., "01") remain strings in YAML
|
|
98
|
+
const configContent = yaml.dump(data, { forceQuotes: true });
|
|
84
99
|
// Write file first
|
|
85
100
|
await fs.writeFile(CONFIG_FILE, configContent, {
|
|
86
101
|
mode: 0o600,
|
|
@@ -117,7 +132,7 @@ async function clearConfig() {
|
|
|
117
132
|
* Get developer ID from configuration
|
|
118
133
|
* Loads config if not already cached, then returns cached developer ID
|
|
119
134
|
* Developer ID: 0 = default infra, > 0 = developer-specific
|
|
120
|
-
* @returns {Promise<
|
|
135
|
+
* @returns {Promise<string>} Developer ID as string (defaults to "0")
|
|
121
136
|
*/
|
|
122
137
|
async function getDeveloperId() {
|
|
123
138
|
// Always reload from file to ensure we have the latest value
|
|
@@ -130,21 +145,33 @@ async function getDeveloperId() {
|
|
|
130
145
|
|
|
131
146
|
/**
|
|
132
147
|
* Set developer ID in configuration
|
|
133
|
-
* @param {number} developerId - Developer ID to set (0 = default infra, > 0 = developer-specific
|
|
148
|
+
* @param {number|string} developerId - Developer ID to set (digit-only string preserved, or number). "0" = default infra, > "0" = developer-specific
|
|
134
149
|
* @returns {Promise<void>}
|
|
135
150
|
*/
|
|
136
151
|
async function setDeveloperId(developerId) {
|
|
137
|
-
|
|
138
|
-
|
|
152
|
+
const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
|
|
153
|
+
let devIdString;
|
|
154
|
+
if (typeof developerId === 'number') {
|
|
155
|
+
if (!Number.isFinite(developerId) || developerId < 0) {
|
|
156
|
+
throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
|
|
157
|
+
}
|
|
158
|
+
devIdString = String(developerId);
|
|
159
|
+
} else if (typeof developerId === 'string') {
|
|
160
|
+
if (!DEV_ID_DIGITS_REGEX.test(developerId)) {
|
|
161
|
+
throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
|
|
162
|
+
}
|
|
163
|
+
devIdString = developerId;
|
|
164
|
+
} else {
|
|
165
|
+
throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
|
|
139
166
|
}
|
|
140
167
|
// Clear cache first to ensure we get fresh data from file
|
|
141
168
|
cachedDeveloperId = null;
|
|
142
169
|
// Read file directly to avoid any caching issues
|
|
143
170
|
const config = await getConfig();
|
|
144
171
|
// Update developer ID
|
|
145
|
-
config['developer-id'] =
|
|
172
|
+
config['developer-id'] = devIdString;
|
|
146
173
|
// Update cache before saving
|
|
147
|
-
cachedDeveloperId =
|
|
174
|
+
cachedDeveloperId = devIdString;
|
|
148
175
|
// Save the entire config object to ensure all fields are preserved
|
|
149
176
|
await saveConfig(config);
|
|
150
177
|
// Verify the file was saved correctly by reading it back
|
|
@@ -154,8 +181,10 @@ async function setDeveloperId(developerId) {
|
|
|
154
181
|
// Read file again with fresh file handle to avoid OS caching
|
|
155
182
|
const savedContent = await fs.readFile(CONFIG_FILE, 'utf8');
|
|
156
183
|
const savedConfig = yaml.load(savedContent);
|
|
157
|
-
|
|
158
|
-
|
|
184
|
+
// YAML may parse numbers as numbers, so convert to string for comparison
|
|
185
|
+
const savedDevIdString = String(savedConfig['developer-id']);
|
|
186
|
+
if (savedDevIdString !== devIdString) {
|
|
187
|
+
throw new Error(`Failed to save developer ID: expected ${devIdString}, got ${savedDevIdString}. File content: ${savedContent.substring(0, 200)}`);
|
|
159
188
|
}
|
|
160
189
|
// Clear the cache to force reload from file on next getDeveloperId() call
|
|
161
190
|
// This ensures we get the value that was actually saved to disk
|
|
@@ -288,7 +317,7 @@ async function saveClientToken(environment, appName, controllerUrl, token, expir
|
|
|
288
317
|
/**
|
|
289
318
|
* Initialize and load developer ID
|
|
290
319
|
* Call this to ensure developerId is loaded and cached
|
|
291
|
-
* @returns {Promise<
|
|
320
|
+
* @returns {Promise<string>} Developer ID (string)
|
|
292
321
|
*/
|
|
293
322
|
async function loadDeveloperId() {
|
|
294
323
|
if (cachedDeveloperId === null) {
|
|
@@ -297,6 +326,60 @@ async function loadDeveloperId() {
|
|
|
297
326
|
return cachedDeveloperId;
|
|
298
327
|
}
|
|
299
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Get secrets encryption key from configuration
|
|
331
|
+
* @returns {Promise<string|null>} Encryption key or null if not set
|
|
332
|
+
*/
|
|
333
|
+
async function getSecretsEncryptionKey() {
|
|
334
|
+
const config = await getConfig();
|
|
335
|
+
return config['secrets-encryption'] || null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Set secrets encryption key in configuration
|
|
340
|
+
* @param {string} key - Encryption key (32 bytes, hex or base64)
|
|
341
|
+
* @returns {Promise<void>}
|
|
342
|
+
* @throws {Error} If key format is invalid
|
|
343
|
+
*/
|
|
344
|
+
async function setSecretsEncryptionKey(key) {
|
|
345
|
+
if (!key || typeof key !== 'string') {
|
|
346
|
+
throw new Error('Encryption key is required and must be a string');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Validate key format using encryption utilities
|
|
350
|
+
const { validateEncryptionKey } = require('./utils/secrets-encryption');
|
|
351
|
+
validateEncryptionKey(key);
|
|
352
|
+
|
|
353
|
+
const config = await getConfig();
|
|
354
|
+
config['secrets-encryption'] = key;
|
|
355
|
+
await saveConfig(config);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get general secrets path from configuration
|
|
360
|
+
* Used as fallback when build.secrets is not set in variables.yaml
|
|
361
|
+
* @returns {Promise<string|null>} Secrets path or null if not set
|
|
362
|
+
*/
|
|
363
|
+
async function getSecretsPath() {
|
|
364
|
+
const config = await getConfig();
|
|
365
|
+
return config['secrets-path'] || null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Set general secrets path in configuration
|
|
370
|
+
* @param {string} secretsPath - Path to general secrets file
|
|
371
|
+
* @returns {Promise<void>}
|
|
372
|
+
*/
|
|
373
|
+
async function setSecretsPath(secretsPath) {
|
|
374
|
+
if (!secretsPath || typeof secretsPath !== 'string') {
|
|
375
|
+
throw new Error('Secrets path is required and must be a string');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const config = await getConfig();
|
|
379
|
+
config['secrets-path'] = secretsPath;
|
|
380
|
+
await saveConfig(config);
|
|
381
|
+
}
|
|
382
|
+
|
|
300
383
|
// Create exports object
|
|
301
384
|
const exportsObj = {
|
|
302
385
|
getConfig,
|
|
@@ -312,6 +395,10 @@ const exportsObj = {
|
|
|
312
395
|
getClientToken,
|
|
313
396
|
saveDeviceToken,
|
|
314
397
|
saveClientToken,
|
|
398
|
+
getSecretsEncryptionKey,
|
|
399
|
+
setSecretsEncryptionKey,
|
|
400
|
+
getSecretsPath,
|
|
401
|
+
setSecretsPath,
|
|
315
402
|
CONFIG_DIR,
|
|
316
403
|
CONFIG_FILE
|
|
317
404
|
};
|
|
@@ -321,7 +408,7 @@ const exportsObj = {
|
|
|
321
408
|
// Developer ID: 0 = default infra, > 0 = developer-specific
|
|
322
409
|
Object.defineProperty(exportsObj, 'developerId', {
|
|
323
410
|
get() {
|
|
324
|
-
return cachedDeveloperId !== null ? cachedDeveloperId : 0; // Default to 0 if not loaded yet
|
|
411
|
+
return cachedDeveloperId !== null ? cachedDeveloperId : '0'; // Default to "0" if not loaded yet
|
|
325
412
|
},
|
|
326
413
|
enumerable: true,
|
|
327
414
|
configurable: true
|
package/lib/infra.js
CHANGED
|
@@ -28,21 +28,23 @@ const execAsync = promisify(exec);
|
|
|
28
28
|
/**
|
|
29
29
|
* Gets infrastructure directory name based on developer ID
|
|
30
30
|
* Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
|
|
31
|
-
* @param {number} devId - Developer ID
|
|
31
|
+
* @param {number|string} devId - Developer ID
|
|
32
32
|
* @returns {string} Infrastructure directory name
|
|
33
33
|
*/
|
|
34
34
|
function getInfraDirName(devId) {
|
|
35
|
-
|
|
35
|
+
const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
36
|
+
return idNum === 0 ? 'infra' : `infra-dev${devId}`;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Gets Docker Compose project name based on developer ID
|
|
40
41
|
* Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
|
|
41
|
-
* @param {number} devId - Developer ID
|
|
42
|
+
* @param {number|string} devId - Developer ID
|
|
42
43
|
* @returns {string} Docker Compose project name
|
|
43
44
|
*/
|
|
44
45
|
function getInfraProjectName(devId) {
|
|
45
|
-
|
|
46
|
+
const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
47
|
+
return idNum === 0 ? 'infra' : `infra-dev${devId}`;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
// Wrapper to support cwd option
|
|
@@ -96,7 +98,10 @@ async function startInfra(developerId = null) {
|
|
|
96
98
|
|
|
97
99
|
// Get developer ID from parameter or config
|
|
98
100
|
const devId = developerId || await config.getDeveloperId();
|
|
99
|
-
|
|
101
|
+
// Convert to number for getDevPorts (it expects numbers)
|
|
102
|
+
const devIdNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
103
|
+
const ports = devConfig.getDevPorts(devIdNum);
|
|
104
|
+
const idNum = devIdNum;
|
|
100
105
|
|
|
101
106
|
// Load compose template (Handlebars)
|
|
102
107
|
const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml.hbs');
|
|
@@ -116,7 +121,7 @@ async function startInfra(developerId = null) {
|
|
|
116
121
|
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
117
122
|
const template = handlebars.compile(templateContent);
|
|
118
123
|
// Dev 0: infra-aifabrix-network, Dev > 0: infra-dev{id}-aifabrix-network
|
|
119
|
-
const networkName =
|
|
124
|
+
const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
|
|
120
125
|
const composeContent = template({
|
|
121
126
|
devId: devId,
|
|
122
127
|
postgresPort: ports.postgres,
|
|
@@ -261,7 +266,9 @@ async function checkInfraHealth(devId = null) {
|
|
|
261
266
|
*/
|
|
262
267
|
async function getInfraStatus() {
|
|
263
268
|
const devId = await config.getDeveloperId();
|
|
264
|
-
|
|
269
|
+
// Convert string developer ID to number for getDevPorts
|
|
270
|
+
const devIdNum = parseInt(devId, 10);
|
|
271
|
+
const ports = devConfig.getDevPorts(devIdNum);
|
|
265
272
|
const services = {
|
|
266
273
|
postgres: { port: ports.postgres, url: `localhost:${ports.postgres}` },
|
|
267
274
|
redis: { port: ports.redis, url: `localhost:${ports.redis}` },
|
package/lib/push.js
CHANGED
|
@@ -21,12 +21,35 @@ const execAsync = promisify(exec);
|
|
|
21
21
|
* @returns {Promise<boolean>} True if Azure CLI is available
|
|
22
22
|
*/
|
|
23
23
|
async function checkAzureCLIInstalled() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
// On Windows, use shell option to ensure proper command resolution
|
|
25
|
+
const options = process.platform === 'win32' ? { shell: true } : {};
|
|
26
|
+
|
|
27
|
+
// Try multiple methods to detect Azure CLI (commands that don't require authentication)
|
|
28
|
+
const commands = process.platform === 'win32'
|
|
29
|
+
? ['az --version', 'az.cmd --version']
|
|
30
|
+
: ['az --version'];
|
|
31
|
+
|
|
32
|
+
for (const command of commands) {
|
|
33
|
+
try {
|
|
34
|
+
// Use a timeout to avoid hanging if command doesn't exist
|
|
35
|
+
await execAsync(command, { ...options, timeout: 5000 });
|
|
36
|
+
return true;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// Log the error for debugging (only in development)
|
|
39
|
+
if (process.env.DEBUG) {
|
|
40
|
+
logger.log(chalk.gray(`[DEBUG] Command '${command}' failed: ${error.message}`));
|
|
41
|
+
}
|
|
42
|
+
// Continue to next command if this one fails
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If all commands failed, Azure CLI is not available
|
|
48
|
+
// Log for debugging if enabled
|
|
49
|
+
if (process.env.DEBUG) {
|
|
50
|
+
logger.log(chalk.gray('[DEBUG] All Azure CLI detection methods failed'));
|
|
29
51
|
}
|
|
52
|
+
return false;
|
|
30
53
|
}
|
|
31
54
|
|
|
32
55
|
/**
|
|
@@ -103,7 +126,9 @@ function validateRegistryURL(registryUrl) {
|
|
|
103
126
|
async function checkACRAuthentication(registry) {
|
|
104
127
|
try {
|
|
105
128
|
const registryName = extractRegistryName(registry);
|
|
106
|
-
|
|
129
|
+
// On Windows, use shell option to ensure proper command resolution
|
|
130
|
+
const options = process.platform === 'win32' ? { shell: true } : {};
|
|
131
|
+
await execAsync(`az acr show --name ${registryName}`, options);
|
|
107
132
|
return true;
|
|
108
133
|
} catch (error) {
|
|
109
134
|
return false;
|
|
@@ -119,7 +144,9 @@ async function authenticateACR(registry) {
|
|
|
119
144
|
try {
|
|
120
145
|
const registryName = extractRegistryName(registry);
|
|
121
146
|
logger.log(chalk.blue(`Authenticating with ${registry}...`));
|
|
122
|
-
|
|
147
|
+
// On Windows, use shell option to ensure proper command resolution
|
|
148
|
+
const options = process.platform === 'win32' ? { shell: true } : {};
|
|
149
|
+
await execAsync(`az acr login --name ${registryName}`, options);
|
|
123
150
|
logger.log(chalk.green(`✓ Authenticated with ${registry}`));
|
|
124
151
|
} catch (error) {
|
|
125
152
|
throw new Error(`Failed to authenticate with Azure Container Registry: ${error.message}`);
|