@aifabrix/builder 2.1.5 → 2.1.6
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 +127 -49
- package/lib/utils/env-template.js +70 -0
- package/lib/utils/local-secrets.js +98 -0
- package/lib/utils/secrets-path.js +114 -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,10 @@ const {
|
|
|
19
19
|
generateMissingSecrets,
|
|
20
20
|
createDefaultSecrets
|
|
21
21
|
} = require('./utils/secrets-generator');
|
|
22
|
+
const {
|
|
23
|
+
resolveSecretsPath,
|
|
24
|
+
getActualSecretsPath
|
|
25
|
+
} = require('./utils/secrets-path');
|
|
22
26
|
|
|
23
27
|
/**
|
|
24
28
|
* Loads environment configuration for docker/local context
|
|
@@ -31,73 +35,147 @@ function loadEnvConfig() {
|
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
/**
|
|
34
|
-
* Loads secrets from
|
|
35
|
-
*
|
|
38
|
+
* Loads secrets from file with cascading lookup support
|
|
39
|
+
* First checks ~/.aifabrix/secrets.local.yaml, then build.secrets from variables.yaml
|
|
40
|
+
*
|
|
41
|
+
* @async
|
|
42
|
+
* @function loadSecretsFromFile
|
|
43
|
+
* @param {string} filePath - Path to secrets file
|
|
44
|
+
* @returns {Promise<Object>} Loaded secrets object or empty object if file doesn't exist
|
|
45
|
+
*/
|
|
46
|
+
async function loadSecretsFromFile(filePath) {
|
|
47
|
+
if (!fs.existsSync(filePath)) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
53
|
+
const secrets = yaml.load(content);
|
|
54
|
+
|
|
55
|
+
if (!secrets || typeof secrets !== 'object') {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return secrets;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
logger.warn(`Warning: Could not read secrets file ${filePath}: ${error.message}`);
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Loads secrets with cascading lookup
|
|
68
|
+
* Supports both user secrets (~/.aifabrix/secrets.local.yaml) and project overrides
|
|
69
|
+
* User's file takes priority, then falls back to build.secrets from variables.yaml
|
|
36
70
|
*
|
|
37
71
|
* @async
|
|
38
72
|
* @function loadSecrets
|
|
39
|
-
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
73
|
+
* @param {string} [secretsPath] - Path to secrets file (optional, for explicit override)
|
|
74
|
+
* @param {string} [appName] - Application name (optional, for variables.yaml lookup)
|
|
40
75
|
* @returns {Promise<Object>} Loaded secrets object
|
|
41
|
-
* @throws {Error} If secrets file
|
|
76
|
+
* @throws {Error} If no secrets file found and no fallback available
|
|
42
77
|
*
|
|
43
78
|
* @example
|
|
44
79
|
* const secrets = await loadSecrets('../../secrets.local.yaml');
|
|
45
80
|
* // Returns: { 'postgres-passwordKeyVault': 'admin123', ... }
|
|
46
81
|
*/
|
|
47
|
-
async function loadSecrets(secretsPath) {
|
|
48
|
-
|
|
82
|
+
async function loadSecrets(secretsPath, appName) {
|
|
83
|
+
// If explicit path provided, use it (backward compatibility)
|
|
84
|
+
if (secretsPath) {
|
|
85
|
+
const resolvedPath = resolveSecretsPath(secretsPath);
|
|
86
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
87
|
+
throw new Error(`Secrets file not found: ${resolvedPath}`);
|
|
88
|
+
}
|
|
49
89
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
90
|
+
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
91
|
+
const secrets = yaml.load(content);
|
|
53
92
|
|
|
54
|
-
|
|
55
|
-
|
|
93
|
+
if (!secrets || typeof secrets !== 'object') {
|
|
94
|
+
throw new Error(`Invalid secrets file format: ${resolvedPath}`);
|
|
95
|
+
}
|
|
56
96
|
|
|
57
|
-
|
|
58
|
-
throw new Error(`Invalid secrets file format: ${resolvedPath}`);
|
|
97
|
+
return secrets;
|
|
59
98
|
}
|
|
60
99
|
|
|
61
|
-
|
|
62
|
-
|
|
100
|
+
// Cascading lookup: user's file first
|
|
101
|
+
const userSecretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
|
|
102
|
+
let mergedSecrets;
|
|
103
|
+
if (fs.existsSync(userSecretsPath)) {
|
|
104
|
+
try {
|
|
105
|
+
const content = fs.readFileSync(userSecretsPath, 'utf8');
|
|
106
|
+
const secrets = yaml.load(content);
|
|
107
|
+
if (!secrets || typeof secrets !== 'object') {
|
|
108
|
+
throw new Error(`Invalid secrets file format: ${userSecretsPath}`);
|
|
109
|
+
}
|
|
110
|
+
mergedSecrets = secrets;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
// If it's a format error, throw it; otherwise log warning and continue
|
|
113
|
+
if (error.message.includes('Invalid secrets file format')) {
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
logger.warn(`Warning: Could not read secrets file ${userSecretsPath}: ${error.message}`);
|
|
117
|
+
mergedSecrets = {};
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
mergedSecrets = {};
|
|
121
|
+
}
|
|
63
122
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
123
|
+
// Then check build.secrets from variables.yaml if appName provided
|
|
124
|
+
if (appName) {
|
|
125
|
+
const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
126
|
+
if (fs.existsSync(variablesPath)) {
|
|
127
|
+
try {
|
|
128
|
+
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
129
|
+
const variables = yaml.load(variablesContent);
|
|
130
|
+
|
|
131
|
+
if (variables?.build?.secrets) {
|
|
132
|
+
const buildSecretsPath = path.resolve(
|
|
133
|
+
path.dirname(variablesPath),
|
|
134
|
+
variables.build.secrets
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const buildSecrets = await loadSecretsFromFile(buildSecretsPath);
|
|
138
|
+
|
|
139
|
+
// Merge: user's file takes priority, but use build.secrets for missing/empty values
|
|
140
|
+
for (const [key, value] of Object.entries(buildSecrets)) {
|
|
141
|
+
if (!(key in mergedSecrets) || !mergedSecrets[key] || mergedSecrets[key] === '') {
|
|
142
|
+
mergedSecrets[key] = value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger.warn(`Warning: Could not load build.secrets from variables.yaml: ${error.message}`);
|
|
89
148
|
}
|
|
90
149
|
}
|
|
150
|
+
}
|
|
91
151
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
152
|
+
// If still no secrets found, try default location
|
|
153
|
+
if (Object.keys(mergedSecrets).length === 0) {
|
|
154
|
+
const defaultPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
|
|
155
|
+
if (fs.existsSync(defaultPath)) {
|
|
156
|
+
try {
|
|
157
|
+
const content = fs.readFileSync(defaultPath, 'utf8');
|
|
158
|
+
const secrets = yaml.load(content);
|
|
159
|
+
if (!secrets || typeof secrets !== 'object') {
|
|
160
|
+
throw new Error(`Invalid secrets file format: ${defaultPath}`);
|
|
161
|
+
}
|
|
162
|
+
mergedSecrets = secrets;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// If it's a format error, throw it; otherwise log warning
|
|
165
|
+
if (error.message.includes('Invalid secrets file format')) {
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
logger.warn(`Warning: Could not read secrets file ${defaultPath}: ${error.message}`);
|
|
169
|
+
}
|
|
95
170
|
}
|
|
96
|
-
} else if (secretsPath.startsWith('..')) {
|
|
97
|
-
resolvedPath = path.resolve(process.cwd(), secretsPath);
|
|
98
171
|
}
|
|
99
172
|
|
|
100
|
-
|
|
173
|
+
// If still empty, throw error
|
|
174
|
+
if (Object.keys(mergedSecrets).length === 0) {
|
|
175
|
+
throw new Error('No secrets file found. Please create ~/.aifabrix/secrets.local.yaml or configure build.secrets in variables.yaml');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return mergedSecrets;
|
|
101
179
|
}
|
|
102
180
|
|
|
103
181
|
/**
|
|
@@ -246,14 +324,14 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
|
|
|
246
324
|
|
|
247
325
|
const template = loadEnvTemplate(templatePath);
|
|
248
326
|
|
|
249
|
-
// Resolve secrets path to show in error messages
|
|
250
|
-
const resolvedSecretsPath =
|
|
327
|
+
// Resolve secrets path to show in error messages (use actual path that loadSecrets would use)
|
|
328
|
+
const resolvedSecretsPath = getActualSecretsPath(secretsPath, appName);
|
|
251
329
|
|
|
252
330
|
if (force) {
|
|
253
331
|
await generateMissingSecrets(template, resolvedSecretsPath);
|
|
254
332
|
}
|
|
255
333
|
|
|
256
|
-
const secrets = await loadSecrets(secretsPath);
|
|
334
|
+
const secrets = await loadSecrets(secretsPath, appName);
|
|
257
335
|
const resolved = await resolveKvReferences(template, secrets, environment, resolvedSecretsPath);
|
|
258
336
|
|
|
259
337
|
fs.writeFileSync(envPath, resolved, { mode: 0o600 });
|
|
@@ -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,114 @@
|
|
|
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 path 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 {string} Actual secrets file path that would be used
|
|
63
|
+
*/
|
|
64
|
+
function getActualSecretsPath(secretsPath, appName) {
|
|
65
|
+
// If explicit path provided, use it (backward compatibility)
|
|
66
|
+
if (secretsPath) {
|
|
67
|
+
return resolveSecretsPath(secretsPath);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Cascading lookup: user's file first
|
|
71
|
+
const userSecretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
|
|
72
|
+
if (fs.existsSync(userSecretsPath)) {
|
|
73
|
+
return userSecretsPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Then check build.secrets from variables.yaml if appName provided
|
|
77
|
+
if (appName) {
|
|
78
|
+
const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
79
|
+
if (fs.existsSync(variablesPath)) {
|
|
80
|
+
try {
|
|
81
|
+
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
82
|
+
const variables = yaml.load(variablesContent);
|
|
83
|
+
|
|
84
|
+
if (variables?.build?.secrets) {
|
|
85
|
+
const buildSecretsPath = path.resolve(
|
|
86
|
+
path.dirname(variablesPath),
|
|
87
|
+
variables.build.secrets
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (fs.existsSync(buildSecretsPath)) {
|
|
91
|
+
return buildSecretsPath;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// Ignore errors, continue to next check
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// If still no secrets found, try default location
|
|
101
|
+
const defaultPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
|
|
102
|
+
if (fs.existsSync(defaultPath)) {
|
|
103
|
+
return defaultPath;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Return user's file path as default (even if it doesn't exist) for error messages
|
|
107
|
+
return userSecretsPath;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
resolveSecretsPath,
|
|
112
|
+
getActualSecretsPath
|
|
113
|
+
};
|
|
114
|
+
|
package/package.json
CHANGED
|
@@ -21,9 +21,10 @@ RUN if [ -d "/tmp/build-context/themes" ] && [ "$(ls -A /tmp/build-context/theme
|
|
|
21
21
|
fi && \
|
|
22
22
|
rm -rf /tmp/build-context
|
|
23
23
|
|
|
24
|
-
# Build Keycloak with health checks enabled
|
|
24
|
+
# Build Keycloak with health checks enabled (similar to Keycloak 24.0)
|
|
25
25
|
# This enables the /health, /health/live, /health/ready, and /health/started endpoints
|
|
26
|
-
|
|
26
|
+
# Expose health endpoints on main HTTP port (like 24.0) instead of management port
|
|
27
|
+
RUN /opt/keycloak/bin/kc.sh build --health-enabled=true --http-management-health-enabled=false
|
|
27
28
|
|
|
28
29
|
# Switch back to keycloak user
|
|
29
30
|
USER keycloak
|
|
@@ -15,8 +15,13 @@ KC_HTTP_ENABLED=true
|
|
|
15
15
|
# HEALTH CHECK CONFIGURATION
|
|
16
16
|
# =============================================================================
|
|
17
17
|
# Enable health check endpoints: /health, /health/live, /health/ready, /health/started
|
|
18
|
+
# Keycloak 26.4.0+: Health endpoints are exposed on main HTTP(S) port (8080) by default
|
|
19
|
+
# Set http-management-health-enabled=false to expose on main port instead of management port
|
|
18
20
|
|
|
19
21
|
KC_HEALTH_ENABLED=true
|
|
22
|
+
# Expose health endpoints on main HTTP port (like Keycloak 24.0)
|
|
23
|
+
# Set to false to expose on main port instead of management port (9000)
|
|
24
|
+
KC_HTTP_MANAGEMENT_HEALTH_ENABLED=false
|
|
20
25
|
|
|
21
26
|
# =============================================================================
|
|
22
27
|
# DATABASE CONFIGURATION
|
|
@@ -25,9 +25,9 @@ requires:
|
|
|
25
25
|
|
|
26
26
|
# Health Check
|
|
27
27
|
healthCheck:
|
|
28
|
-
path: /health
|
|
28
|
+
path: /health/ready
|
|
29
29
|
interval: 30
|
|
30
|
-
probePath: /health
|
|
30
|
+
probePath: /health/ready
|
|
31
31
|
probeRequestType: GET
|
|
32
32
|
probeProtocol: Https
|
|
33
33
|
probeIntervalInSeconds: 120
|
|
@@ -49,4 +49,4 @@ build:
|
|
|
49
49
|
# Docker Compose
|
|
50
50
|
compose:
|
|
51
51
|
file: docker-compose.yaml
|
|
52
|
-
service: keycloak
|
|
52
|
+
service: keycloak
|
|
@@ -62,6 +62,9 @@ RUN pnpm exec tsc -p tsconfig.docker.json || true
|
|
|
62
62
|
# Copy sensitive-fields.config.json to dist folder
|
|
63
63
|
RUN mkdir -p dist/src/services/logging && \
|
|
64
64
|
cp src/services/logging/sensitive-fields.config.json dist/src/services/logging/ || true
|
|
65
|
+
# Copy generated Prisma logs client to dist folder (needed at runtime)
|
|
66
|
+
RUN mkdir -p dist/database/generated && \
|
|
67
|
+
cp -r src/database/generated/logs-client dist/database/generated/ || true
|
|
65
68
|
|
|
66
69
|
# Return to root to prune correctly (needed to keep workspace dependencies)
|
|
67
70
|
WORKDIR /app
|
|
@@ -121,14 +121,14 @@ MISO_CONTROLLER_URL=kv://miso-controller-url
|
|
|
121
121
|
|
|
122
122
|
# Web Server URL (for OpenAPI documentation server URLs)
|
|
123
123
|
# Used to generate correct server URLs in OpenAPI spec
|
|
124
|
-
WEB_SERVER_URL=kv://miso-web-server-url
|
|
124
|
+
WEB_SERVER_URL=kv://miso-controller-web-server-url
|
|
125
125
|
|
|
126
126
|
# MISO Environment Configuration (miso, dev, tst, pro)
|
|
127
127
|
MISO_ENVIRONMENT=miso
|
|
128
128
|
|
|
129
129
|
# MISO Application Client Credentials (per application)
|
|
130
|
-
MISO_CLIENTID=kv://miso-client-idKeyVault
|
|
131
|
-
MISO_CLIENTSECRET=kv://miso-client-secretKeyVault
|
|
130
|
+
MISO_CLIENTID=kv://miso-controller-client-idKeyVault
|
|
131
|
+
MISO_CLIENTSECRET=kv://miso-controller-client-secretKeyVault
|
|
132
132
|
|
|
133
133
|
# =============================================================================
|
|
134
134
|
# MORI SERVICE CONFIGURATION
|