@aifabrix/builder 2.1.4 → 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.
@@ -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 the specified file or default location
35
- * Supports both user secrets (~/.aifabrix/secrets.yaml) and project overrides
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 cannot be loaded or parsed
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
- const resolvedPath = resolveSecretsPath(secretsPath);
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
- if (!fs.existsSync(resolvedPath)) {
51
- throw new Error(`Secrets file not found: ${resolvedPath}`);
52
- }
90
+ const content = fs.readFileSync(resolvedPath, 'utf8');
91
+ const secrets = yaml.load(content);
53
92
 
54
- const content = fs.readFileSync(resolvedPath, 'utf8');
55
- const secrets = yaml.load(content);
93
+ if (!secrets || typeof secrets !== 'object') {
94
+ throw new Error(`Invalid secrets file format: ${resolvedPath}`);
95
+ }
56
96
 
57
- if (!secrets || typeof secrets !== 'object') {
58
- throw new Error(`Invalid secrets file format: ${resolvedPath}`);
97
+ return secrets;
59
98
  }
60
99
 
61
- return secrets;
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
- * Resolves secrets file path (same logic as loadSecrets)
66
- * Also checks common locations if path is not provided
67
- * @function resolveSecretsPath
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;
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
- // If none found, use default location
93
- if (!resolvedPath) {
94
- resolvedPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
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
- return resolvedPath;
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 = resolveSecretsPath(secretsPath);
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 });
package/lib/templates.js CHANGED
@@ -128,7 +128,10 @@ function buildDatabaseEnv(config) {
128
128
  'DB_PORT': '5432',
129
129
  'DB_NAME': dbName,
130
130
  'DB_USER': `${dbName}_user`,
131
- 'DB_PASSWORD': `kv://databases-${appName}-0-passwordKeyVault`
131
+ 'DB_PASSWORD': `kv://databases-${appName}-0-passwordKeyVault`,
132
+ // Also include DB_0_PASSWORD for compatibility with compose generator
133
+ // (compose generator expects DB_0_PASSWORD when databases array is present)
134
+ 'DB_0_PASSWORD': `kv://databases-${appName}-0-passwordKeyVault`
132
135
  };
133
136
  }
134
137
 
@@ -21,16 +21,32 @@ handlebars.registerHelper('pgQuote', (identifier) => {
21
21
  return '';
22
22
  }
23
23
  // Always quote identifiers to handle hyphens and special characters
24
- return `"${String(identifier).replace(/"/g, '""')}"`;
24
+ // Return SafeString to prevent HTML escaping
25
+ return new handlebars.SafeString(`"${String(identifier).replace(/"/g, '""')}"`);
25
26
  });
26
27
 
27
28
  // Helper to generate quoted PostgreSQL user name from database name
29
+ // User names must use underscores (not hyphens) for PostgreSQL compatibility
28
30
  handlebars.registerHelper('pgUser', (dbName) => {
29
31
  if (!dbName) {
30
32
  return '';
31
33
  }
34
+ // Replace hyphens with underscores in user name (database names can have hyphens, but user names should not)
35
+ const userName = `${String(dbName).replace(/-/g, '_')}_user`;
36
+ // Return SafeString to prevent HTML escaping
37
+ return new handlebars.SafeString(`"${userName.replace(/"/g, '""')}"`);
38
+ });
39
+
40
+ // Helper to generate old user name format (for migration - drops old users with hyphens)
41
+ // This is used to drop legacy users that were created with hyphens before the fix
42
+ handlebars.registerHelper('pgUserOld', (dbName) => {
43
+ if (!dbName) {
44
+ return '';
45
+ }
46
+ // Old format: database name + _user (preserving hyphens)
32
47
  const userName = `${String(dbName)}_user`;
33
- return `"${userName.replace(/"/g, '""')}"`;
48
+ // Return SafeString to prevent HTML escaping
49
+ return new handlebars.SafeString(`"${userName.replace(/"/g, '""')}"`);
34
50
  });
35
51
 
36
52
  /**
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  # Keycloak Identity and Access Management with Custom Themes
2
- # This Dockerfile extends Keycloak 24.0 with custom themes
2
+ # This Dockerfile extends Keycloak 26.4 with custom themes
3
3
 
4
- FROM quay.io/keycloak/keycloak:24.0
4
+ FROM quay.io/keycloak/keycloak:26.4
5
5
 
6
6
  # Set working directory
7
7
  WORKDIR /opt/keycloak
@@ -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
- RUN /opt/keycloak/bin/kc.sh build --health-enabled=true
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