@aifabrix/builder 2.1.5 → 2.1.7
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/commands/app.js +43 -0
- package/lib/secrets.js +104 -57
- package/lib/utils/env-template.js +70 -0
- package/lib/utils/local-secrets.js +98 -0
- package/lib/utils/secrets-path.js +111 -0
- package/lib/utils/secrets-utils.js +206 -0
- package/package.json +1 -1
- package/templates/applications/keycloak/Dockerfile +3 -2
- package/templates/applications/keycloak/env.template +5 -0
- package/templates/applications/keycloak/variables.yaml +3 -3
- package/templates/applications/miso-controller/Dockerfile +3 -0
- package/templates/applications/miso-controller/env.template +3 -3
- package/test-output.txt +5431 -0
package/lib/commands/app.js
CHANGED
|
@@ -16,6 +16,8 @@ const yaml = require('js-yaml');
|
|
|
16
16
|
const { getConfig } = require('../config');
|
|
17
17
|
const { authenticatedApiCall } = require('../utils/api');
|
|
18
18
|
const logger = require('../utils/logger');
|
|
19
|
+
const { saveLocalSecret, isLocalhost } = require('../utils/local-secrets');
|
|
20
|
+
const { updateEnvTemplate } = require('../utils/env-template');
|
|
19
21
|
|
|
20
22
|
// Import createApp to auto-generate config if missing
|
|
21
23
|
let createApp;
|
|
@@ -312,6 +314,26 @@ function setupAppCommands(program) {
|
|
|
312
314
|
registrationData
|
|
313
315
|
);
|
|
314
316
|
|
|
317
|
+
// Save credentials to local secrets if localhost
|
|
318
|
+
if (isLocalhost(config.apiUrl)) {
|
|
319
|
+
const appKey = responseData.application.key;
|
|
320
|
+
const clientIdKey = `${appKey}-client-idKeyVault`;
|
|
321
|
+
const clientSecretKey = `${appKey}-client-secretKeyVault`;
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
await saveLocalSecret(clientIdKey, responseData.credentials.clientId);
|
|
325
|
+
await saveLocalSecret(clientSecretKey, responseData.credentials.clientSecret);
|
|
326
|
+
|
|
327
|
+
// Update env.template
|
|
328
|
+
await updateEnvTemplate(appKey, clientIdKey, clientSecretKey);
|
|
329
|
+
|
|
330
|
+
logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
|
|
331
|
+
logger.log(chalk.green('✓ env.template updated with MISO_CLIENTID and MISO_CLIENTSECRET\n'));
|
|
332
|
+
} catch (error) {
|
|
333
|
+
logger.warn(chalk.yellow(`⚠️ Could not save credentials locally: ${error.message}`));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
315
337
|
// Display results
|
|
316
338
|
displayRegistrationResults(responseData, config.apiUrl);
|
|
317
339
|
|
|
@@ -403,6 +425,27 @@ function setupAppCommands(program) {
|
|
|
403
425
|
logger.log(chalk.yellow(` Client Secret: ${response.data.credentials.clientSecret}\n`));
|
|
404
426
|
logger.log(chalk.red('⚠️ Old secret is now invalid. Update GitHub Secrets!\n'));
|
|
405
427
|
|
|
428
|
+
// Save credentials to local secrets if localhost
|
|
429
|
+
if (isLocalhost(config.apiUrl)) {
|
|
430
|
+
const appKey = response.data.application?.key || options.app;
|
|
431
|
+
const clientIdKey = `${appKey}-client-idKeyVault`;
|
|
432
|
+
const clientSecretKey = `${appKey}-client-secretKeyVault`;
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
await saveLocalSecret(clientIdKey, response.data.credentials.clientId);
|
|
436
|
+
await saveLocalSecret(clientSecretKey, response.data.credentials.clientSecret);
|
|
437
|
+
|
|
438
|
+
// Update env.template
|
|
439
|
+
await updateEnvTemplate(appKey, clientIdKey, clientSecretKey);
|
|
440
|
+
|
|
441
|
+
logger.log(chalk.green('✓ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
|
|
442
|
+
logger.log(chalk.green('✓ env.template updated with MISO_CLIENTID and MISO_CLIENTSECRET'));
|
|
443
|
+
logger.log('');
|
|
444
|
+
} catch (error) {
|
|
445
|
+
logger.warn(chalk.yellow(`⚠️ Could not save credentials locally: ${error.message}`));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
406
449
|
} catch (error) {
|
|
407
450
|
logger.error(chalk.red('❌ Rotation failed:'), error.message);
|
|
408
451
|
process.exit(1);
|
package/lib/secrets.js
CHANGED
|
@@ -19,6 +19,17 @@ const {
|
|
|
19
19
|
generateMissingSecrets,
|
|
20
20
|
createDefaultSecrets
|
|
21
21
|
} = require('./utils/secrets-generator');
|
|
22
|
+
const {
|
|
23
|
+
resolveSecretsPath,
|
|
24
|
+
getActualSecretsPath
|
|
25
|
+
} = require('./utils/secrets-path');
|
|
26
|
+
const {
|
|
27
|
+
loadUserSecrets,
|
|
28
|
+
loadBuildSecrets,
|
|
29
|
+
loadDefaultSecrets,
|
|
30
|
+
buildHostnameToServiceMap,
|
|
31
|
+
resolveUrlPort
|
|
32
|
+
} = require('./utils/secrets-utils');
|
|
22
33
|
|
|
23
34
|
/**
|
|
24
35
|
* Loads environment configuration for docker/local context
|
|
@@ -31,73 +42,58 @@ function loadEnvConfig() {
|
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
/**
|
|
34
|
-
* Loads secrets
|
|
35
|
-
* Supports both user secrets (~/.aifabrix/secrets.yaml) and project overrides
|
|
45
|
+
* Loads secrets with cascading lookup
|
|
46
|
+
* Supports both user secrets (~/.aifabrix/secrets.local.yaml) and project overrides
|
|
47
|
+
* User's file takes priority, then falls back to build.secrets from variables.yaml
|
|
36
48
|
*
|
|
37
49
|
* @async
|
|
38
50
|
* @function loadSecrets
|
|
39
|
-
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
51
|
+
* @param {string} [secretsPath] - Path to secrets file (optional, for explicit override)
|
|
52
|
+
* @param {string} [appName] - Application name (optional, for variables.yaml lookup)
|
|
40
53
|
* @returns {Promise<Object>} Loaded secrets object
|
|
41
|
-
* @throws {Error} If secrets file
|
|
54
|
+
* @throws {Error} If no secrets file found and no fallback available
|
|
42
55
|
*
|
|
43
56
|
* @example
|
|
44
57
|
* const secrets = await loadSecrets('../../secrets.local.yaml');
|
|
45
58
|
* // Returns: { 'postgres-passwordKeyVault': 'admin123', ... }
|
|
46
59
|
*/
|
|
47
|
-
async function loadSecrets(secretsPath) {
|
|
48
|
-
|
|
60
|
+
async function loadSecrets(secretsPath, appName) {
|
|
61
|
+
// If explicit path provided, use it (backward compatibility)
|
|
62
|
+
if (secretsPath) {
|
|
63
|
+
const resolvedPath = resolveSecretsPath(secretsPath);
|
|
64
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
65
|
+
throw new Error(`Secrets file not found: ${resolvedPath}`);
|
|
66
|
+
}
|
|
49
67
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
68
|
+
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
69
|
+
const secrets = yaml.load(content);
|
|
53
70
|
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
if (!secrets || typeof secrets !== 'object') {
|
|
72
|
+
throw new Error(`Invalid secrets file format: ${resolvedPath}`);
|
|
73
|
+
}
|
|
56
74
|
|
|
57
|
-
|
|
58
|
-
throw new Error(`Invalid secrets file format: ${resolvedPath}`);
|
|
75
|
+
return secrets;
|
|
59
76
|
}
|
|
60
77
|
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
// Cascading lookup: user's file first
|
|
79
|
+
let mergedSecrets = loadUserSecrets();
|
|
63
80
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
69
|
-
* @returns {string} Resolved secrets file path
|
|
70
|
-
*/
|
|
71
|
-
function resolveSecretsPath(secretsPath) {
|
|
72
|
-
let resolvedPath = secretsPath;
|
|
73
|
-
|
|
74
|
-
if (!resolvedPath) {
|
|
75
|
-
// Check common locations for secrets.local.yaml
|
|
76
|
-
const commonLocations = [
|
|
77
|
-
path.join(process.cwd(), '..', 'aifabrix-setup', 'secrets.local.yaml'),
|
|
78
|
-
path.join(process.cwd(), '..', '..', 'aifabrix-setup', 'secrets.local.yaml'),
|
|
79
|
-
path.join(process.cwd(), 'secrets.local.yaml'),
|
|
80
|
-
path.join(process.cwd(), '..', 'secrets.local.yaml'),
|
|
81
|
-
path.join(os.homedir(), '.aifabrix', 'secrets.yaml')
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
// Find first existing file
|
|
85
|
-
for (const location of commonLocations) {
|
|
86
|
-
if (fs.existsSync(location)) {
|
|
87
|
-
resolvedPath = location;
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
81
|
+
// Then check build.secrets from variables.yaml if appName provided
|
|
82
|
+
if (appName) {
|
|
83
|
+
mergedSecrets = await loadBuildSecrets(mergedSecrets, appName);
|
|
84
|
+
}
|
|
91
85
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
86
|
+
// If still no secrets found, try default location
|
|
87
|
+
if (Object.keys(mergedSecrets).length === 0) {
|
|
88
|
+
mergedSecrets = loadDefaultSecrets();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// If still empty, throw error
|
|
92
|
+
if (Object.keys(mergedSecrets).length === 0) {
|
|
93
|
+
throw new Error('No secrets file found. Please create ~/.aifabrix/secrets.local.yaml or configure build.secrets in variables.yaml');
|
|
98
94
|
}
|
|
99
95
|
|
|
100
|
-
return
|
|
96
|
+
return mergedSecrets;
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
/**
|
|
@@ -109,7 +105,9 @@ function resolveSecretsPath(secretsPath) {
|
|
|
109
105
|
* @param {string} envTemplate - Environment template content
|
|
110
106
|
* @param {Object} secrets - Secrets object from loadSecrets()
|
|
111
107
|
* @param {string} [environment='local'] - Environment context (docker/local)
|
|
112
|
-
* @param {string} [
|
|
108
|
+
* @param {Object|string|null} [secretsFilePaths] - Paths object with userPath and buildPath, or string path (for backward compatibility)
|
|
109
|
+
* @param {string} [secretsFilePaths.userPath] - User's secrets file path
|
|
110
|
+
* @param {string|null} [secretsFilePaths.buildPath] - App's build.secrets file path (if configured)
|
|
113
111
|
* @returns {Promise<string>} Resolved environment content
|
|
114
112
|
* @throws {Error} If kv:// reference cannot be resolved
|
|
115
113
|
*
|
|
@@ -117,7 +115,7 @@ function resolveSecretsPath(secretsPath) {
|
|
|
117
115
|
* const resolved = await resolveKvReferences(template, secrets, 'local');
|
|
118
116
|
* // Returns: 'DATABASE_URL=postgresql://user:pass@localhost:5432/db'
|
|
119
117
|
*/
|
|
120
|
-
async function resolveKvReferences(envTemplate, secrets, environment = 'local',
|
|
118
|
+
async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null) {
|
|
121
119
|
const envConfig = loadEnvConfig();
|
|
122
120
|
const envVars = envConfig.environments[environment] || envConfig.environments.local;
|
|
123
121
|
|
|
@@ -138,7 +136,20 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
|
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
if (missingSecrets.length > 0) {
|
|
141
|
-
|
|
139
|
+
let fileInfo = '';
|
|
140
|
+
if (secretsFilePaths) {
|
|
141
|
+
// Handle backward compatibility: if it's a string, use it as-is
|
|
142
|
+
if (typeof secretsFilePaths === 'string') {
|
|
143
|
+
fileInfo = `\n\nSecrets file location: ${secretsFilePaths}`;
|
|
144
|
+
} else if (typeof secretsFilePaths === 'object' && secretsFilePaths.userPath) {
|
|
145
|
+
// New format: show both paths if buildPath is configured
|
|
146
|
+
const paths = [secretsFilePaths.userPath];
|
|
147
|
+
if (secretsFilePaths.buildPath) {
|
|
148
|
+
paths.push(secretsFilePaths.buildPath);
|
|
149
|
+
}
|
|
150
|
+
fileInfo = `\n\nSecrets file location: ${paths.join(' and ')}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
142
153
|
throw new Error(`Missing secrets: ${missingSecrets.join(', ')}${fileInfo}`);
|
|
143
154
|
}
|
|
144
155
|
|
|
@@ -157,6 +168,36 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
|
|
|
157
168
|
return resolved;
|
|
158
169
|
}
|
|
159
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Resolves service ports in URLs within .env content for Docker environment
|
|
173
|
+
* Replaces ports in URLs with containerPort from service's variables.yaml
|
|
174
|
+
*
|
|
175
|
+
* @function resolveServicePortsInEnvContent
|
|
176
|
+
* @param {string} envContent - Resolved .env file content
|
|
177
|
+
* @param {string} environment - Environment context (docker/local)
|
|
178
|
+
* @returns {string} Content with resolved ports
|
|
179
|
+
*/
|
|
180
|
+
function resolveServicePortsInEnvContent(envContent, environment) {
|
|
181
|
+
// Only process docker environment
|
|
182
|
+
if (environment !== 'docker') {
|
|
183
|
+
return envContent;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const envConfig = loadEnvConfig();
|
|
187
|
+
const dockerHosts = envConfig.environments.docker || {};
|
|
188
|
+
const hostnameToService = buildHostnameToServiceMap(dockerHosts);
|
|
189
|
+
|
|
190
|
+
// Pattern to match URLs: http://hostname:port or https://hostname:port
|
|
191
|
+
// Matches: protocol://hostname:port/path?query
|
|
192
|
+
// Captures: protocol, hostname, port, and optional path/query
|
|
193
|
+
// Note: [^\s\n]* matches any non-whitespace characters except newline (stops at end of line)
|
|
194
|
+
const urlPattern = /(https?:\/\/)([a-zA-Z0-9-]+):(\d+)([^\s\n]*)?/g;
|
|
195
|
+
|
|
196
|
+
return envContent.replace(urlPattern, (match, protocol, hostname, port, urlPath = '') => {
|
|
197
|
+
return resolveUrlPort(protocol, hostname, port, urlPath || '', hostnameToService);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
160
201
|
/**
|
|
161
202
|
* Loads environment template from file
|
|
162
203
|
* @function loadEnvTemplate
|
|
@@ -246,15 +287,21 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
|
|
|
246
287
|
|
|
247
288
|
const template = loadEnvTemplate(templatePath);
|
|
248
289
|
|
|
249
|
-
// Resolve secrets
|
|
250
|
-
const
|
|
290
|
+
// Resolve secrets paths to show in error messages (use actual paths that loadSecrets would use)
|
|
291
|
+
const secretsPaths = getActualSecretsPath(secretsPath, appName);
|
|
251
292
|
|
|
252
293
|
if (force) {
|
|
253
|
-
|
|
294
|
+
// Use userPath for generating missing secrets (priority file)
|
|
295
|
+
await generateMissingSecrets(template, secretsPaths.userPath);
|
|
254
296
|
}
|
|
255
297
|
|
|
256
|
-
const secrets = await loadSecrets(secretsPath);
|
|
257
|
-
|
|
298
|
+
const secrets = await loadSecrets(secretsPath, appName);
|
|
299
|
+
let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths);
|
|
300
|
+
|
|
301
|
+
// Resolve service ports in URLs for docker environment
|
|
302
|
+
if (environment === 'docker') {
|
|
303
|
+
resolved = resolveServicePortsInEnvContent(resolved, environment);
|
|
304
|
+
}
|
|
258
305
|
|
|
259
306
|
fs.writeFileSync(envPath, resolved, { mode: 0o600 });
|
|
260
307
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Template Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for updating env.template files
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Environment template update utilities
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const fsSync = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const chalk = require('chalk');
|
|
15
|
+
const logger = require('../utils/logger');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Updates env.template to add MISO_CLIENTID and MISO_CLIENTSECRET entries
|
|
19
|
+
* @async
|
|
20
|
+
* @param {string} appKey - Application key
|
|
21
|
+
* @param {string} clientIdKey - Secret key for client ID (e.g., 'myapp-client-idKeyVault')
|
|
22
|
+
* @param {string} clientSecretKey - Secret key for client secret (e.g., 'myapp-client-secretKeyVault')
|
|
23
|
+
* @returns {Promise<void>} Resolves when template is updated
|
|
24
|
+
*/
|
|
25
|
+
async function updateEnvTemplate(appKey, clientIdKey, clientSecretKey) {
|
|
26
|
+
const envTemplatePath = path.join(process.cwd(), 'builder', appKey, 'env.template');
|
|
27
|
+
|
|
28
|
+
if (!fsSync.existsSync(envTemplatePath)) {
|
|
29
|
+
logger.warn(chalk.yellow(`⚠️ env.template not found for ${appKey}, skipping update`));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
let content = await fs.readFile(envTemplatePath, 'utf8');
|
|
35
|
+
|
|
36
|
+
// Check if MISO_CLIENTID already exists
|
|
37
|
+
const hasClientId = /^MISO_CLIENTID\s*=/m.test(content);
|
|
38
|
+
const hasClientSecret = /^MISO_CLIENTSECRET\s*=/m.test(content);
|
|
39
|
+
|
|
40
|
+
if (hasClientId && hasClientSecret) {
|
|
41
|
+
// Update existing entries
|
|
42
|
+
content = content.replace(/^MISO_CLIENTID\s*=.*$/m, `MISO_CLIENTID=kv://${clientIdKey}`);
|
|
43
|
+
content = content.replace(/^MISO_CLIENTSECRET\s*=.*$/m, `MISO_CLIENTSECRET=kv://${clientSecretKey}`);
|
|
44
|
+
} else {
|
|
45
|
+
// Add new section if not present
|
|
46
|
+
const misoSection = `# MISO Application Client Credentials (per application)
|
|
47
|
+
MISO_CLIENTID=kv://${clientIdKey}
|
|
48
|
+
MISO_CLIENTSECRET=kv://${clientSecretKey}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
// Try to find a good place to insert (after last section or at end)
|
|
52
|
+
const lastSectionMatch = content.match(/# =+.*$/gm);
|
|
53
|
+
if (lastSectionMatch && lastSectionMatch.length > 0) {
|
|
54
|
+
const lastSectionIndex = content.lastIndexOf(lastSectionMatch[lastSectionMatch.length - 1]);
|
|
55
|
+
const insertIndex = content.indexOf('\n', lastSectionIndex) + 1;
|
|
56
|
+
content = content.slice(0, insertIndex) + '\n' + misoSection + content.slice(insertIndex);
|
|
57
|
+
} else {
|
|
58
|
+
content = content + '\n' + misoSection;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await fs.writeFile(envTemplatePath, content, 'utf8');
|
|
63
|
+
logger.log(chalk.green(`✓ Updated env.template for ${appKey}`));
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logger.warn(chalk.yellow(`⚠️ Could not update env.template: ${error.message}`));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { updateEnvTemplate };
|
|
70
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Secrets Management Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for managing local secrets in ~/.aifabrix/secrets.local.yaml
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Local secrets management utilities
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const yaml = require('js-yaml');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const chalk = require('chalk');
|
|
16
|
+
const logger = require('../utils/logger');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Saves a secret to ~/.aifabrix/secrets.local.yaml
|
|
20
|
+
* Merges with existing secrets without overwriting other keys
|
|
21
|
+
*
|
|
22
|
+
* @async
|
|
23
|
+
* @function saveLocalSecret
|
|
24
|
+
* @param {string} key - Secret key name
|
|
25
|
+
* @param {string} value - Secret value
|
|
26
|
+
* @returns {Promise<void>} Resolves when secret is saved
|
|
27
|
+
* @throws {Error} If save fails
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* await saveLocalSecret('myapp-client-idKeyVault', 'client-id-value');
|
|
31
|
+
*/
|
|
32
|
+
async function saveLocalSecret(key, value) {
|
|
33
|
+
if (!key || typeof key !== 'string') {
|
|
34
|
+
throw new Error('Secret key is required and must be a string');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (value === undefined || value === null) {
|
|
38
|
+
throw new Error('Secret value is required');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const secretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
|
|
42
|
+
const secretsDir = path.dirname(secretsPath);
|
|
43
|
+
|
|
44
|
+
// Create directory if needed
|
|
45
|
+
if (!fs.existsSync(secretsDir)) {
|
|
46
|
+
fs.mkdirSync(secretsDir, { recursive: true, mode: 0o700 });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Load existing secrets
|
|
50
|
+
let existingSecrets = {};
|
|
51
|
+
if (fs.existsSync(secretsPath)) {
|
|
52
|
+
try {
|
|
53
|
+
const content = fs.readFileSync(secretsPath, 'utf8');
|
|
54
|
+
existingSecrets = yaml.load(content) || {};
|
|
55
|
+
if (typeof existingSecrets !== 'object') {
|
|
56
|
+
existingSecrets = {};
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.warn(`Warning: Could not read existing secrets file: ${error.message}`);
|
|
60
|
+
existingSecrets = {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Merge with new secret
|
|
65
|
+
const updatedSecrets = {
|
|
66
|
+
...existingSecrets,
|
|
67
|
+
[key]: value
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Save to file
|
|
71
|
+
const yamlContent = yaml.dump(updatedSecrets, {
|
|
72
|
+
indent: 2,
|
|
73
|
+
lineWidth: 120,
|
|
74
|
+
noRefs: true,
|
|
75
|
+
sortKeys: false
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
fs.writeFileSync(secretsPath, yamlContent, { mode: 0o600 });
|
|
79
|
+
logger.log(chalk.green(`✓ Saved secret ${key} to ${secretsPath}`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Checks if a URL is localhost
|
|
84
|
+
* @function isLocalhost
|
|
85
|
+
* @param {string} url - URL to check
|
|
86
|
+
* @returns {boolean} True if URL is localhost
|
|
87
|
+
*/
|
|
88
|
+
function isLocalhost(url) {
|
|
89
|
+
if (!url || typeof url !== 'string') {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const urlLower = url.toLowerCase();
|
|
94
|
+
return urlLower.includes('localhost') || urlLower.includes('127.0.0.1');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { saveLocalSecret, isLocalhost };
|
|
98
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Secrets Path Resolution
|
|
3
|
+
*
|
|
4
|
+
* This module handles secrets file path resolution with cascading lookup support.
|
|
5
|
+
* Determines the actual path that would be used for loading secrets.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Secrets path resolution utilities 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
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolves secrets file path (backward compatibility)
|
|
19
|
+
* Checks common locations if path is not provided
|
|
20
|
+
* @function resolveSecretsPath
|
|
21
|
+
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
22
|
+
* @returns {string} Resolved secrets file path
|
|
23
|
+
*/
|
|
24
|
+
function resolveSecretsPath(secretsPath) {
|
|
25
|
+
let resolvedPath = secretsPath;
|
|
26
|
+
|
|
27
|
+
if (!resolvedPath) {
|
|
28
|
+
// Check common locations for secrets.local.yaml
|
|
29
|
+
const commonLocations = [
|
|
30
|
+
path.join(process.cwd(), '..', 'aifabrix-setup', 'secrets.local.yaml'),
|
|
31
|
+
path.join(process.cwd(), '..', '..', 'aifabrix-setup', 'secrets.local.yaml'),
|
|
32
|
+
path.join(process.cwd(), 'secrets.local.yaml'),
|
|
33
|
+
path.join(process.cwd(), '..', 'secrets.local.yaml'),
|
|
34
|
+
path.join(os.homedir(), '.aifabrix', 'secrets.yaml')
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// Find first existing file
|
|
38
|
+
for (const location of commonLocations) {
|
|
39
|
+
if (fs.existsSync(location)) {
|
|
40
|
+
resolvedPath = location;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// If none found, use default location
|
|
46
|
+
if (!resolvedPath) {
|
|
47
|
+
resolvedPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
|
|
48
|
+
}
|
|
49
|
+
} else if (secretsPath.startsWith('..')) {
|
|
50
|
+
resolvedPath = path.resolve(process.cwd(), secretsPath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return resolvedPath;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Determines the actual secrets file paths that loadSecrets would use
|
|
58
|
+
* Mirrors the cascading lookup logic from loadSecrets
|
|
59
|
+
* @function getActualSecretsPath
|
|
60
|
+
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
61
|
+
* @param {string} [appName] - Application name (optional, for variables.yaml lookup)
|
|
62
|
+
* @returns {Object} Object with userPath and buildPath (if configured)
|
|
63
|
+
* @returns {string} returns.userPath - User's secrets file path (~/.aifabrix/secrets.local.yaml)
|
|
64
|
+
* @returns {string|null} returns.buildPath - App's build.secrets file path (if configured in variables.yaml)
|
|
65
|
+
*/
|
|
66
|
+
function getActualSecretsPath(secretsPath, appName) {
|
|
67
|
+
// If explicit path provided, use it (backward compatibility)
|
|
68
|
+
if (secretsPath) {
|
|
69
|
+
const resolvedPath = resolveSecretsPath(secretsPath);
|
|
70
|
+
return {
|
|
71
|
+
userPath: resolvedPath,
|
|
72
|
+
buildPath: null
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Cascading lookup: user's file first
|
|
77
|
+
const userSecretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
|
|
78
|
+
|
|
79
|
+
// Check build.secrets from variables.yaml if appName provided
|
|
80
|
+
let buildSecretsPath = null;
|
|
81
|
+
if (appName) {
|
|
82
|
+
const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
83
|
+
if (fs.existsSync(variablesPath)) {
|
|
84
|
+
try {
|
|
85
|
+
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
86
|
+
const variables = yaml.load(variablesContent);
|
|
87
|
+
|
|
88
|
+
if (variables?.build?.secrets) {
|
|
89
|
+
buildSecretsPath = path.resolve(
|
|
90
|
+
path.dirname(variablesPath),
|
|
91
|
+
variables.build.secrets
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// Ignore errors, continue
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Return both paths (even if files don't exist) for error messages
|
|
101
|
+
return {
|
|
102
|
+
userPath: userSecretsPath,
|
|
103
|
+
buildPath: buildSecretsPath
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
resolveSecretsPath,
|
|
109
|
+
getActualSecretsPath
|
|
110
|
+
};
|
|
111
|
+
|