@aifabrix/builder 2.2.0 → 2.3.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/lib/build.js +100 -7
- package/lib/cli.js +14 -0
- package/lib/commands/secure.js +260 -0
- package/lib/config.js +58 -0
- package/lib/push.js +34 -7
- package/lib/secrets.js +72 -23
- package/lib/templates.js +1 -1
- package/lib/utils/build-copy.js +18 -0
- package/lib/utils/cli-utils.js +28 -3
- package/lib/utils/compose-generator.js +14 -2
- package/lib/utils/docker-build.js +24 -0
- package/lib/utils/secrets-encryption.js +203 -0
- package/lib/utils/secrets-path.js +22 -3
- package/package.json +2 -2
- package/test-output.txt +0 -5431
package/lib/build.js
CHANGED
|
@@ -28,6 +28,52 @@ const buildCopy = require('./utils/build-copy');
|
|
|
28
28
|
|
|
29
29
|
const execAsync = promisify(exec);
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Copies application template files to dev directory
|
|
33
|
+
* Used when apps directory doesn't exist to ensure build can proceed
|
|
34
|
+
* @async
|
|
35
|
+
* @param {string} templatePath - Path to template directory
|
|
36
|
+
* @param {string} devDir - Target dev directory
|
|
37
|
+
* @param {string} _language - Language (typescript/python) - currently unused but kept for future use
|
|
38
|
+
* @throws {Error} If copying fails
|
|
39
|
+
*/
|
|
40
|
+
async function copyTemplateFilesToDevDir(templatePath, devDir, _language) {
|
|
41
|
+
if (!fsSync.existsSync(templatePath)) {
|
|
42
|
+
throw new Error(`Template path not found: ${templatePath}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const entries = await fs.readdir(templatePath);
|
|
46
|
+
|
|
47
|
+
// Copy only application files, skip Dockerfile and docker-compose templates
|
|
48
|
+
const appFiles = entries.filter(entry => {
|
|
49
|
+
const lowerEntry = entry.toLowerCase();
|
|
50
|
+
// Include .gitignore, exclude .hbs files and docker-related files
|
|
51
|
+
if (entry === '.gitignore') {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (lowerEntry.endsWith('.hbs')) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (lowerEntry.startsWith('dockerfile') || lowerEntry.includes('docker-compose')) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (entry.startsWith('.') && entry !== '.gitignore') {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
for (const entry of appFiles) {
|
|
67
|
+
const sourcePath = path.join(templatePath, entry);
|
|
68
|
+
const targetPath = path.join(devDir, entry);
|
|
69
|
+
|
|
70
|
+
const entryStats = await fs.stat(sourcePath);
|
|
71
|
+
if (entryStats.isFile()) {
|
|
72
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
31
77
|
/**
|
|
32
78
|
* Loads variables.yaml configuration for an application
|
|
33
79
|
* @param {string} appName - Application name
|
|
@@ -91,7 +137,6 @@ function detectLanguage(appPath) {
|
|
|
91
137
|
const packageJsonPath = path.join(appPath, 'package.json');
|
|
92
138
|
const requirementsPath = path.join(appPath, 'requirements.txt');
|
|
93
139
|
const pyprojectPath = path.join(appPath, 'pyproject.toml');
|
|
94
|
-
const dockerfilePath = path.join(appPath, 'Dockerfile');
|
|
95
140
|
|
|
96
141
|
// Check for package.json (TypeScript/Node.js)
|
|
97
142
|
if (fsSync.existsSync(packageJsonPath)) {
|
|
@@ -103,11 +148,6 @@ function detectLanguage(appPath) {
|
|
|
103
148
|
return 'python';
|
|
104
149
|
}
|
|
105
150
|
|
|
106
|
-
// Check for custom Dockerfile
|
|
107
|
-
if (fsSync.existsSync(dockerfilePath)) {
|
|
108
|
-
throw new Error('Custom Dockerfile found. Use --force-template to regenerate from template.');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
151
|
// Default to typescript if no indicators found
|
|
112
152
|
return 'typescript';
|
|
113
153
|
}
|
|
@@ -315,11 +355,43 @@ async function buildApp(appName, options = {}) {
|
|
|
315
355
|
const devDir = await buildCopy.copyBuilderToDevDirectory(appName, developerId);
|
|
316
356
|
logger.log(chalk.green(`✓ Files copied to: ${devDir}`));
|
|
317
357
|
|
|
358
|
+
// 2a. Check if application source files exist, if not copy from templates
|
|
359
|
+
const appsPath = path.join(process.cwd(), 'apps', appName);
|
|
360
|
+
if (fsSync.existsSync(appsPath)) {
|
|
361
|
+
// Copy app source files from apps directory
|
|
362
|
+
await buildCopy.copyAppSourceFiles(appsPath, devDir);
|
|
363
|
+
logger.log(chalk.green(`✓ Copied application source files from apps/${appName}`));
|
|
364
|
+
} else {
|
|
365
|
+
// No apps directory - check if we need to copy template files
|
|
366
|
+
const language = options.language || buildConfig.language || detectLanguage(devDir);
|
|
367
|
+
const packageJsonPath = path.join(devDir, 'package.json');
|
|
368
|
+
const requirementsPath = path.join(devDir, 'requirements.txt');
|
|
369
|
+
|
|
370
|
+
if (language === 'typescript' && !fsSync.existsSync(packageJsonPath)) {
|
|
371
|
+
// Copy TypeScript template files
|
|
372
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'typescript');
|
|
373
|
+
await copyTemplateFilesToDevDir(templatePath, devDir, language);
|
|
374
|
+
logger.log(chalk.green(`✓ Generated application files from ${language} template`));
|
|
375
|
+
} else if (language === 'python' && !fsSync.existsSync(requirementsPath)) {
|
|
376
|
+
// Copy Python template files
|
|
377
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'python');
|
|
378
|
+
await copyTemplateFilesToDevDir(templatePath, devDir, language);
|
|
379
|
+
logger.log(chalk.green(`✓ Generated application files from ${language} template`));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
318
383
|
// 3. Prepare build context (use dev-specific directory)
|
|
319
384
|
// If buildConfig.context is relative, resolve it relative to devDir
|
|
320
385
|
// If buildConfig.context is '../..' (apps flag), keep it as is (it's relative to devDir)
|
|
321
386
|
let contextPath;
|
|
322
|
-
|
|
387
|
+
|
|
388
|
+
// Check if context is using old format (../appName) - these are incompatible with dev directory structure
|
|
389
|
+
if (buildConfig.context && buildConfig.context.startsWith('../') && buildConfig.context !== '../..') {
|
|
390
|
+
// Old format detected - always use devDir instead
|
|
391
|
+
logger.log(chalk.yellow(`⚠️ Warning: Build context uses old format: ${buildConfig.context}`));
|
|
392
|
+
logger.log(chalk.yellow(` Using dev directory instead: ${devDir}`));
|
|
393
|
+
contextPath = devDir;
|
|
394
|
+
} else if (buildConfig.context && buildConfig.context !== '../..') {
|
|
323
395
|
// Resolve relative context path from dev directory
|
|
324
396
|
contextPath = path.resolve(devDir, buildConfig.context);
|
|
325
397
|
} else if (buildConfig.context === '../..') {
|
|
@@ -333,6 +405,22 @@ async function buildApp(appName, options = {}) {
|
|
|
333
405
|
contextPath = devDir;
|
|
334
406
|
}
|
|
335
407
|
|
|
408
|
+
// Ensure context path is absolute and normalized
|
|
409
|
+
contextPath = path.resolve(contextPath);
|
|
410
|
+
|
|
411
|
+
// Validate that context path exists (skip in test environments)
|
|
412
|
+
const isTestEnv = process.env.NODE_ENV === 'test' ||
|
|
413
|
+
process.env.JEST_WORKER_ID !== undefined ||
|
|
414
|
+
typeof jest !== 'undefined';
|
|
415
|
+
|
|
416
|
+
if (!isTestEnv && !fsSync.existsSync(contextPath)) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
`Build context path does not exist: ${contextPath}\n` +
|
|
419
|
+
`Expected dev directory: ${devDir}\n` +
|
|
420
|
+
'Please ensure files were copied correctly or update the context in variables.yaml.'
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
336
424
|
// 4. Check if Dockerfile exists in dev directory
|
|
337
425
|
const appDockerfilePath = path.join(devDir, 'Dockerfile');
|
|
338
426
|
const hasExistingDockerfile = fsSync.existsSync(appDockerfilePath) && !options.forceTemplate;
|
|
@@ -362,6 +450,11 @@ async function buildApp(appName, options = {}) {
|
|
|
362
450
|
|
|
363
451
|
// 6. Build Docker image
|
|
364
452
|
const tag = options.tag || 'latest';
|
|
453
|
+
|
|
454
|
+
// Log paths for debugging
|
|
455
|
+
logger.log(chalk.blue(`Using Dockerfile: ${dockerfilePath}`));
|
|
456
|
+
logger.log(chalk.blue(`Using build context: ${contextPath}`));
|
|
457
|
+
|
|
365
458
|
await executeBuild(imageName, dockerfilePath, contextPath, tag, options);
|
|
366
459
|
|
|
367
460
|
// 7. Post-build tasks
|
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
|
|
@@ -373,6 +374,19 @@ function setupCommands(program) {
|
|
|
373
374
|
process.exit(1);
|
|
374
375
|
}
|
|
375
376
|
});
|
|
377
|
+
|
|
378
|
+
// Security command
|
|
379
|
+
program.command('secure')
|
|
380
|
+
.description('Encrypt secrets in secrets.local.yaml files for ISO 27001 compliance')
|
|
381
|
+
.option('--secrets-encryption <key>', 'Encryption key (32 bytes, hex or base64)')
|
|
382
|
+
.action(async(options) => {
|
|
383
|
+
try {
|
|
384
|
+
await handleSecure(options);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
handleCommandError(error, 'secure');
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
376
390
|
}
|
|
377
391
|
|
|
378
392
|
module.exports = {
|
|
@@ -0,0 +1,260 @@
|
|
|
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 { encryptSecret, isEncrypted, validateEncryptionKey } = require('../utils/secrets-encryption');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Finds all secrets.local.yaml files to encrypt
|
|
24
|
+
* Includes user secrets file and build secrets from all apps
|
|
25
|
+
*
|
|
26
|
+
* @async
|
|
27
|
+
* @function findSecretsFiles
|
|
28
|
+
* @returns {Promise<Array<{path: string, type: string}>>} Array of secrets file paths
|
|
29
|
+
*/
|
|
30
|
+
async function findSecretsFiles() {
|
|
31
|
+
const files = [];
|
|
32
|
+
|
|
33
|
+
// User's secrets file
|
|
34
|
+
const userSecretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
|
|
35
|
+
if (fs.existsSync(userSecretsPath)) {
|
|
36
|
+
files.push({ path: userSecretsPath, type: 'user' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Find all apps and check for build.secrets
|
|
40
|
+
// Scan builder directory for apps
|
|
41
|
+
try {
|
|
42
|
+
const builderDir = path.join(process.cwd(), 'builder');
|
|
43
|
+
if (fs.existsSync(builderDir)) {
|
|
44
|
+
const entries = fs.readdirSync(builderDir, { withFileTypes: true });
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
const appName = entry.name;
|
|
48
|
+
const variablesPath = path.join(builderDir, appName, 'variables.yaml');
|
|
49
|
+
if (fs.existsSync(variablesPath)) {
|
|
50
|
+
try {
|
|
51
|
+
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
52
|
+
const variables = yaml.load(variablesContent);
|
|
53
|
+
|
|
54
|
+
if (variables?.build?.secrets) {
|
|
55
|
+
const buildSecretsPath = path.resolve(
|
|
56
|
+
path.dirname(variablesPath),
|
|
57
|
+
variables.build.secrets
|
|
58
|
+
);
|
|
59
|
+
if (fs.existsSync(buildSecretsPath)) {
|
|
60
|
+
files.push({ path: buildSecretsPath, type: `app:${appName}` });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// Ignore errors, continue
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Ignore errors, continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check config.yaml for general secrets-path
|
|
75
|
+
try {
|
|
76
|
+
const { getSecretsPath } = require('../config');
|
|
77
|
+
const generalSecretsPath = await getSecretsPath();
|
|
78
|
+
if (generalSecretsPath) {
|
|
79
|
+
const resolvedPath = path.isAbsolute(generalSecretsPath)
|
|
80
|
+
? generalSecretsPath
|
|
81
|
+
: path.resolve(process.cwd(), generalSecretsPath);
|
|
82
|
+
if (fs.existsSync(resolvedPath) && !files.some(f => f.path === resolvedPath)) {
|
|
83
|
+
files.push({ path: resolvedPath, type: 'general' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// Ignore errors, continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return files;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Encrypts all non-encrypted values in a secrets file
|
|
95
|
+
* Preserves YAML structure and comments
|
|
96
|
+
*
|
|
97
|
+
* @async
|
|
98
|
+
* @function encryptSecretsFile
|
|
99
|
+
* @param {string} filePath - Path to secrets file
|
|
100
|
+
* @param {string} encryptionKey - Encryption key
|
|
101
|
+
* @returns {Promise<{encrypted: number, total: number}>} Count of encrypted values
|
|
102
|
+
*/
|
|
103
|
+
async function encryptSecretsFile(filePath, encryptionKey) {
|
|
104
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
105
|
+
const secrets = yaml.load(content);
|
|
106
|
+
|
|
107
|
+
if (!secrets || typeof secrets !== 'object') {
|
|
108
|
+
throw new Error(`Invalid secrets file format: ${filePath}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let encryptedCount = 0;
|
|
112
|
+
let totalCount = 0;
|
|
113
|
+
const updatedSecrets = {};
|
|
114
|
+
|
|
115
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
116
|
+
totalCount++;
|
|
117
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
118
|
+
if (isEncrypted(value)) {
|
|
119
|
+
// Already encrypted, keep as-is
|
|
120
|
+
updatedSecrets[key] = value;
|
|
121
|
+
} else {
|
|
122
|
+
// Encrypt the value
|
|
123
|
+
updatedSecrets[key] = encryptSecret(value, encryptionKey);
|
|
124
|
+
encryptedCount++;
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
// Non-string or empty value, keep as-is
|
|
128
|
+
updatedSecrets[key] = value;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Write back to file with same formatting
|
|
133
|
+
// Use yaml.dump with appropriate options to preserve structure
|
|
134
|
+
const yamlContent = yaml.dump(updatedSecrets, {
|
|
135
|
+
indent: 2,
|
|
136
|
+
lineWidth: -1,
|
|
137
|
+
noRefs: true,
|
|
138
|
+
sortKeys: false
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
fs.writeFileSync(filePath, yamlContent, { mode: 0o600 });
|
|
142
|
+
|
|
143
|
+
return { encrypted: encryptedCount, total: totalCount };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Prompt for encryption key if not provided
|
|
148
|
+
*
|
|
149
|
+
* @async
|
|
150
|
+
* @function promptForEncryptionKey
|
|
151
|
+
* @returns {Promise<string>} Encryption key
|
|
152
|
+
*/
|
|
153
|
+
async function promptForEncryptionKey() {
|
|
154
|
+
const answer = await inquirer.prompt([{
|
|
155
|
+
type: 'password',
|
|
156
|
+
name: 'key',
|
|
157
|
+
message: 'Enter encryption key (32 bytes, hex or base64):',
|
|
158
|
+
mask: '*',
|
|
159
|
+
validate: (input) => {
|
|
160
|
+
if (!input || input.trim().length === 0) {
|
|
161
|
+
return 'Encryption key is required';
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
validateEncryptionKey(input.trim());
|
|
165
|
+
return true;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return error.message;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}]);
|
|
171
|
+
|
|
172
|
+
return answer.key.trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Handle secure command action
|
|
177
|
+
* Sets encryption key and encrypts all secrets files
|
|
178
|
+
*
|
|
179
|
+
* @async
|
|
180
|
+
* @function handleSecure
|
|
181
|
+
* @param {Object} options - Command options
|
|
182
|
+
* @param {string} [options.secretsEncryption] - Encryption key (optional, will prompt if not provided)
|
|
183
|
+
* @returns {Promise<void>} Resolves when encryption completes
|
|
184
|
+
* @throws {Error} If encryption fails
|
|
185
|
+
*/
|
|
186
|
+
async function handleSecure(options) {
|
|
187
|
+
logger.log(chalk.blue('\n🔐 Securing secrets files...\n'));
|
|
188
|
+
|
|
189
|
+
// Get or prompt for encryption key
|
|
190
|
+
let encryptionKey = options.secretsEncryption || options['secrets-encryption'];
|
|
191
|
+
if (!encryptionKey) {
|
|
192
|
+
// Check if key already exists in config
|
|
193
|
+
const existingKey = await getSecretsEncryptionKey();
|
|
194
|
+
if (existingKey) {
|
|
195
|
+
logger.log(chalk.yellow('⚠️ Encryption key already configured in config.yaml'));
|
|
196
|
+
const useExisting = await inquirer.prompt([{
|
|
197
|
+
type: 'confirm',
|
|
198
|
+
name: 'use',
|
|
199
|
+
message: 'Use existing encryption key?',
|
|
200
|
+
default: true
|
|
201
|
+
}]);
|
|
202
|
+
if (useExisting.use) {
|
|
203
|
+
encryptionKey = existingKey;
|
|
204
|
+
} else {
|
|
205
|
+
encryptionKey = await promptForEncryptionKey();
|
|
206
|
+
await setSecretsEncryptionKey(encryptionKey);
|
|
207
|
+
logger.log(chalk.green('✓ Encryption key saved to config.yaml'));
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
encryptionKey = await promptForEncryptionKey();
|
|
211
|
+
await setSecretsEncryptionKey(encryptionKey);
|
|
212
|
+
logger.log(chalk.green('✓ Encryption key saved to config.yaml'));
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
// Validate and save the provided key
|
|
216
|
+
validateEncryptionKey(encryptionKey);
|
|
217
|
+
await setSecretsEncryptionKey(encryptionKey);
|
|
218
|
+
logger.log(chalk.green('✓ Encryption key saved to config.yaml'));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Find all secrets files
|
|
222
|
+
const secretsFiles = await findSecretsFiles();
|
|
223
|
+
|
|
224
|
+
if (secretsFiles.length === 0) {
|
|
225
|
+
logger.log(chalk.yellow('⚠️ No secrets files found to encrypt'));
|
|
226
|
+
logger.log(chalk.gray(' Create ~/.aifabrix/secrets.local.yaml or configure build.secrets in variables.yaml'));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
logger.log(chalk.gray(`Found ${secretsFiles.length} secrets file(s) to process:\n`));
|
|
231
|
+
|
|
232
|
+
// Encrypt each file
|
|
233
|
+
let totalEncrypted = 0;
|
|
234
|
+
let totalValues = 0;
|
|
235
|
+
|
|
236
|
+
for (const file of secretsFiles) {
|
|
237
|
+
try {
|
|
238
|
+
logger.log(chalk.gray(`Processing: ${file.path} (${file.type})`));
|
|
239
|
+
const result = await encryptSecretsFile(file.path, encryptionKey);
|
|
240
|
+
totalEncrypted += result.encrypted;
|
|
241
|
+
totalValues += result.total;
|
|
242
|
+
|
|
243
|
+
if (result.encrypted > 0) {
|
|
244
|
+
logger.log(chalk.green(` ✓ Encrypted ${result.encrypted} of ${result.total} values`));
|
|
245
|
+
} else {
|
|
246
|
+
logger.log(chalk.gray(` - All values already encrypted (${result.total} total)`));
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
logger.log(chalk.red(` ✗ Error: ${error.message}`));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
logger.log(chalk.green('\n✅ Encryption complete!'));
|
|
254
|
+
logger.log(chalk.gray(` Files processed: ${secretsFiles.length}`));
|
|
255
|
+
logger.log(chalk.gray(` Values encrypted: ${totalEncrypted} of ${totalValues} total`));
|
|
256
|
+
logger.log(chalk.gray(' Encryption key stored in: ~/.aifabrix/config.yaml\n'));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = { handleSecure };
|
|
260
|
+
|
package/lib/config.js
CHANGED
|
@@ -297,6 +297,60 @@ async function loadDeveloperId() {
|
|
|
297
297
|
return cachedDeveloperId;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Get secrets encryption key from configuration
|
|
302
|
+
* @returns {Promise<string|null>} Encryption key or null if not set
|
|
303
|
+
*/
|
|
304
|
+
async function getSecretsEncryptionKey() {
|
|
305
|
+
const config = await getConfig();
|
|
306
|
+
return config['secrets-encryption'] || null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Set secrets encryption key in configuration
|
|
311
|
+
* @param {string} key - Encryption key (32 bytes, hex or base64)
|
|
312
|
+
* @returns {Promise<void>}
|
|
313
|
+
* @throws {Error} If key format is invalid
|
|
314
|
+
*/
|
|
315
|
+
async function setSecretsEncryptionKey(key) {
|
|
316
|
+
if (!key || typeof key !== 'string') {
|
|
317
|
+
throw new Error('Encryption key is required and must be a string');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Validate key format using encryption utilities
|
|
321
|
+
const { validateEncryptionKey } = require('./utils/secrets-encryption');
|
|
322
|
+
validateEncryptionKey(key);
|
|
323
|
+
|
|
324
|
+
const config = await getConfig();
|
|
325
|
+
config['secrets-encryption'] = key;
|
|
326
|
+
await saveConfig(config);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get general secrets path from configuration
|
|
331
|
+
* Used as fallback when build.secrets is not set in variables.yaml
|
|
332
|
+
* @returns {Promise<string|null>} Secrets path or null if not set
|
|
333
|
+
*/
|
|
334
|
+
async function getSecretsPath() {
|
|
335
|
+
const config = await getConfig();
|
|
336
|
+
return config['secrets-path'] || null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Set general secrets path in configuration
|
|
341
|
+
* @param {string} secretsPath - Path to general secrets file
|
|
342
|
+
* @returns {Promise<void>}
|
|
343
|
+
*/
|
|
344
|
+
async function setSecretsPath(secretsPath) {
|
|
345
|
+
if (!secretsPath || typeof secretsPath !== 'string') {
|
|
346
|
+
throw new Error('Secrets path is required and must be a string');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const config = await getConfig();
|
|
350
|
+
config['secrets-path'] = secretsPath;
|
|
351
|
+
await saveConfig(config);
|
|
352
|
+
}
|
|
353
|
+
|
|
300
354
|
// Create exports object
|
|
301
355
|
const exportsObj = {
|
|
302
356
|
getConfig,
|
|
@@ -312,6 +366,10 @@ const exportsObj = {
|
|
|
312
366
|
getClientToken,
|
|
313
367
|
saveDeviceToken,
|
|
314
368
|
saveClientToken,
|
|
369
|
+
getSecretsEncryptionKey,
|
|
370
|
+
setSecretsEncryptionKey,
|
|
371
|
+
getSecretsPath,
|
|
372
|
+
setSecretsPath,
|
|
315
373
|
CONFIG_DIR,
|
|
316
374
|
CONFIG_FILE
|
|
317
375
|
};
|
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}`);
|