@aifabrix/builder 2.1.6 → 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/secrets.js CHANGED
@@ -23,6 +23,13 @@ const {
23
23
  resolveSecretsPath,
24
24
  getActualSecretsPath
25
25
  } = require('./utils/secrets-path');
26
+ const {
27
+ loadUserSecrets,
28
+ loadBuildSecrets,
29
+ loadDefaultSecrets,
30
+ buildHostnameToServiceMap,
31
+ resolveUrlPort
32
+ } = require('./utils/secrets-utils');
26
33
 
27
34
  /**
28
35
  * Loads environment configuration for docker/local context
@@ -34,35 +41,6 @@ function loadEnvConfig() {
34
41
  return yaml.load(content);
35
42
  }
36
43
 
37
- /**
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
44
  /**
67
45
  * Loads secrets with cascading lookup
68
46
  * Supports both user secrets (~/.aifabrix/secrets.local.yaml) and project overrides
@@ -98,76 +76,16 @@ async function loadSecrets(secretsPath, appName) {
98
76
  }
99
77
 
100
78
  // 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
- }
79
+ let mergedSecrets = loadUserSecrets();
122
80
 
123
81
  // Then check build.secrets from variables.yaml if appName provided
124
82
  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}`);
148
- }
149
- }
83
+ mergedSecrets = await loadBuildSecrets(mergedSecrets, appName);
150
84
  }
151
85
 
152
86
  // If still no secrets found, try default location
153
87
  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
- }
170
- }
88
+ mergedSecrets = loadDefaultSecrets();
171
89
  }
172
90
 
173
91
  // If still empty, throw error
@@ -187,7 +105,9 @@ async function loadSecrets(secretsPath, appName) {
187
105
  * @param {string} envTemplate - Environment template content
188
106
  * @param {Object} secrets - Secrets object from loadSecrets()
189
107
  * @param {string} [environment='local'] - Environment context (docker/local)
190
- * @param {string} [secretsFilePath] - Path to secrets file (for error messages)
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)
191
111
  * @returns {Promise<string>} Resolved environment content
192
112
  * @throws {Error} If kv:// reference cannot be resolved
193
113
  *
@@ -195,7 +115,7 @@ async function loadSecrets(secretsPath, appName) {
195
115
  * const resolved = await resolveKvReferences(template, secrets, 'local');
196
116
  * // Returns: 'DATABASE_URL=postgresql://user:pass@localhost:5432/db'
197
117
  */
198
- async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePath = null) {
118
+ async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null) {
199
119
  const envConfig = loadEnvConfig();
200
120
  const envVars = envConfig.environments[environment] || envConfig.environments.local;
201
121
 
@@ -216,7 +136,20 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
216
136
  }
217
137
 
218
138
  if (missingSecrets.length > 0) {
219
- const fileInfo = secretsFilePath ? `\n\nSecrets file location: ${secretsFilePath}` : '';
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
+ }
220
153
  throw new Error(`Missing secrets: ${missingSecrets.join(', ')}${fileInfo}`);
221
154
  }
222
155
 
@@ -235,6 +168,36 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
235
168
  return resolved;
236
169
  }
237
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
+
238
201
  /**
239
202
  * Loads environment template from file
240
203
  * @function loadEnvTemplate
@@ -324,15 +287,21 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
324
287
 
325
288
  const template = loadEnvTemplate(templatePath);
326
289
 
327
- // Resolve secrets path to show in error messages (use actual path that loadSecrets would use)
328
- const resolvedSecretsPath = getActualSecretsPath(secretsPath, appName);
290
+ // Resolve secrets paths to show in error messages (use actual paths that loadSecrets would use)
291
+ const secretsPaths = getActualSecretsPath(secretsPath, appName);
329
292
 
330
293
  if (force) {
331
- await generateMissingSecrets(template, resolvedSecretsPath);
294
+ // Use userPath for generating missing secrets (priority file)
295
+ await generateMissingSecrets(template, secretsPaths.userPath);
332
296
  }
333
297
 
334
298
  const secrets = await loadSecrets(secretsPath, appName);
335
- const resolved = await resolveKvReferences(template, secrets, environment, resolvedSecretsPath);
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
+ }
336
305
 
337
306
  fs.writeFileSync(envPath, resolved, { mode: 0o600 });
338
307
 
@@ -54,26 +54,30 @@ function resolveSecretsPath(secretsPath) {
54
54
  }
55
55
 
56
56
  /**
57
- * Determines the actual secrets file path that loadSecrets would use
57
+ * Determines the actual secrets file paths that loadSecrets would use
58
58
  * Mirrors the cascading lookup logic from loadSecrets
59
59
  * @function getActualSecretsPath
60
60
  * @param {string} [secretsPath] - Path to secrets file (optional)
61
61
  * @param {string} [appName] - Application name (optional, for variables.yaml lookup)
62
- * @returns {string} Actual secrets file path that would be used
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)
63
65
  */
64
66
  function getActualSecretsPath(secretsPath, appName) {
65
67
  // If explicit path provided, use it (backward compatibility)
66
68
  if (secretsPath) {
67
- return resolveSecretsPath(secretsPath);
69
+ const resolvedPath = resolveSecretsPath(secretsPath);
70
+ return {
71
+ userPath: resolvedPath,
72
+ buildPath: null
73
+ };
68
74
  }
69
75
 
70
76
  // Cascading lookup: user's file first
71
77
  const userSecretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
72
- if (fs.existsSync(userSecretsPath)) {
73
- return userSecretsPath;
74
- }
75
78
 
76
- // Then check build.secrets from variables.yaml if appName provided
79
+ // Check build.secrets from variables.yaml if appName provided
80
+ let buildSecretsPath = null;
77
81
  if (appName) {
78
82
  const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
79
83
  if (fs.existsSync(variablesPath)) {
@@ -82,29 +86,22 @@ function getActualSecretsPath(secretsPath, appName) {
82
86
  const variables = yaml.load(variablesContent);
83
87
 
84
88
  if (variables?.build?.secrets) {
85
- const buildSecretsPath = path.resolve(
89
+ buildSecretsPath = path.resolve(
86
90
  path.dirname(variablesPath),
87
91
  variables.build.secrets
88
92
  );
89
-
90
- if (fs.existsSync(buildSecretsPath)) {
91
- return buildSecretsPath;
92
- }
93
93
  }
94
94
  } catch (error) {
95
- // Ignore errors, continue to next check
95
+ // Ignore errors, continue
96
96
  }
97
97
  }
98
98
  }
99
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;
100
+ // Return both paths (even if files don't exist) for error messages
101
+ return {
102
+ userPath: userSecretsPath,
103
+ buildPath: buildSecretsPath
104
+ };
108
105
  }
109
106
 
110
107
  module.exports = {
@@ -0,0 +1,206 @@
1
+ /**
2
+ * AI Fabrix Builder Secrets Utilities
3
+ *
4
+ * This module provides utility functions for loading and processing secrets.
5
+ * Helper functions for secrets.js module.
6
+ *
7
+ * @fileoverview Secrets utility functions 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 yaml = require('js-yaml');
15
+ const os = require('os');
16
+ const logger = require('./logger');
17
+
18
+ /**
19
+ * Loads secrets from file with cascading lookup support
20
+ * First checks ~/.aifabrix/secrets.local.yaml, then build.secrets from variables.yaml
21
+ *
22
+ * @async
23
+ * @function loadSecretsFromFile
24
+ * @param {string} filePath - Path to secrets file
25
+ * @returns {Promise<Object>} Loaded secrets object or empty object if file doesn't exist
26
+ */
27
+ async function loadSecretsFromFile(filePath) {
28
+ if (!fs.existsSync(filePath)) {
29
+ return {};
30
+ }
31
+
32
+ try {
33
+ const content = fs.readFileSync(filePath, 'utf8');
34
+ const secrets = yaml.load(content);
35
+
36
+ if (!secrets || typeof secrets !== 'object') {
37
+ return {};
38
+ }
39
+
40
+ return secrets;
41
+ } catch (error) {
42
+ logger.warn(`Warning: Could not read secrets file ${filePath}: ${error.message}`);
43
+ return {};
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Loads user secrets from ~/.aifabrix/secrets.local.yaml
49
+ * @function loadUserSecrets
50
+ * @returns {Object} Loaded secrets object or empty object
51
+ */
52
+ function loadUserSecrets() {
53
+ const userSecretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
54
+ if (!fs.existsSync(userSecretsPath)) {
55
+ return {};
56
+ }
57
+
58
+ try {
59
+ const content = fs.readFileSync(userSecretsPath, 'utf8');
60
+ const secrets = yaml.load(content);
61
+ if (!secrets || typeof secrets !== 'object') {
62
+ throw new Error(`Invalid secrets file format: ${userSecretsPath}`);
63
+ }
64
+ return secrets;
65
+ } catch (error) {
66
+ if (error.message.includes('Invalid secrets file format')) {
67
+ throw error;
68
+ }
69
+ logger.warn(`Warning: Could not read secrets file ${userSecretsPath}: ${error.message}`);
70
+ return {};
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Loads build secrets from variables.yaml and merges with existing secrets
76
+ * @async
77
+ * @function loadBuildSecrets
78
+ * @param {Object} mergedSecrets - Existing secrets to merge with
79
+ * @param {string} appName - Application name
80
+ * @returns {Promise<Object>} Merged secrets object
81
+ */
82
+ async function loadBuildSecrets(mergedSecrets, appName) {
83
+ const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
84
+ if (!fs.existsSync(variablesPath)) {
85
+ return mergedSecrets;
86
+ }
87
+
88
+ try {
89
+ const variablesContent = fs.readFileSync(variablesPath, 'utf8');
90
+ const variables = yaml.load(variablesContent);
91
+
92
+ if (variables?.build?.secrets) {
93
+ const buildSecretsPath = path.resolve(
94
+ path.dirname(variablesPath),
95
+ variables.build.secrets
96
+ );
97
+
98
+ const buildSecrets = await loadSecretsFromFile(buildSecretsPath);
99
+
100
+ // Merge: user's file takes priority, but use build.secrets for missing/empty values
101
+ for (const [key, value] of Object.entries(buildSecrets)) {
102
+ if (!(key in mergedSecrets) || !mergedSecrets[key] || mergedSecrets[key] === '') {
103
+ mergedSecrets[key] = value;
104
+ }
105
+ }
106
+ }
107
+ } catch (error) {
108
+ logger.warn(`Warning: Could not load build.secrets from variables.yaml: ${error.message}`);
109
+ }
110
+
111
+ return mergedSecrets;
112
+ }
113
+
114
+ /**
115
+ * Loads default secrets from ~/.aifabrix/secrets.yaml
116
+ * @function loadDefaultSecrets
117
+ * @returns {Object} Loaded secrets object or empty object
118
+ */
119
+ function loadDefaultSecrets() {
120
+ const defaultPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
121
+ if (!fs.existsSync(defaultPath)) {
122
+ return {};
123
+ }
124
+
125
+ try {
126
+ const content = fs.readFileSync(defaultPath, 'utf8');
127
+ const secrets = yaml.load(content);
128
+ if (!secrets || typeof secrets !== 'object') {
129
+ throw new Error(`Invalid secrets file format: ${defaultPath}`);
130
+ }
131
+ return secrets;
132
+ } catch (error) {
133
+ if (error.message.includes('Invalid secrets file format')) {
134
+ throw error;
135
+ }
136
+ logger.warn(`Warning: Could not read secrets file ${defaultPath}: ${error.message}`);
137
+ return {};
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Builds a map of hostname to service name from environment config
143
+ * @function buildHostnameToServiceMap
144
+ * @param {Object} dockerHosts - Docker environment hosts configuration
145
+ * @returns {Object} Map of hostname to service name
146
+ */
147
+ function buildHostnameToServiceMap(dockerHosts) {
148
+ const hostnameToService = {};
149
+ for (const [key, hostname] of Object.entries(dockerHosts)) {
150
+ if (key.endsWith('_HOST')) {
151
+ // Use hostname directly as service name (e.g., 'keycloak', 'miso-controller')
152
+ hostnameToService[hostname] = hostname;
153
+ }
154
+ }
155
+ return hostnameToService;
156
+ }
157
+
158
+ /**
159
+ * Resolves port for a single URL by looking up service's variables.yaml
160
+ * @function resolveUrlPort
161
+ * @param {string} protocol - URL protocol (http:// or https://)
162
+ * @param {string} hostname - Service hostname
163
+ * @param {string} port - Current port
164
+ * @param {string} urlPath - URL path and query string
165
+ * @param {Object} hostnameToService - Map of hostname to service name
166
+ * @returns {string} URL with resolved port
167
+ */
168
+ function resolveUrlPort(protocol, hostname, port, urlPath, hostnameToService) {
169
+ const serviceName = hostnameToService[hostname];
170
+ if (!serviceName) {
171
+ // Not a service hostname, keep original
172
+ return `${protocol}${hostname}:${port}${urlPath}`;
173
+ }
174
+
175
+ // Try to load service's variables.yaml
176
+ const serviceVariablesPath = path.join(process.cwd(), 'builder', serviceName, 'variables.yaml');
177
+ if (!fs.existsSync(serviceVariablesPath)) {
178
+ // Service variables.yaml not found, keep original port
179
+ return `${protocol}${hostname}:${port}${urlPath}`;
180
+ }
181
+
182
+ try {
183
+ const variablesContent = fs.readFileSync(serviceVariablesPath, 'utf8');
184
+ const variables = yaml.load(variablesContent);
185
+
186
+ // Get containerPort or fall back to port
187
+ const containerPort = variables?.build?.containerPort || variables?.port || port;
188
+
189
+ // Replace port in URL
190
+ return `${protocol}${hostname}:${containerPort}${urlPath}`;
191
+ } catch (error) {
192
+ // Error loading variables.yaml, keep original port
193
+ logger.warn(`Warning: Could not load variables.yaml for service ${serviceName}: ${error.message}`);
194
+ return `${protocol}${hostname}:${port}${urlPath}`;
195
+ }
196
+ }
197
+
198
+ module.exports = {
199
+ loadSecretsFromFile,
200
+ loadUserSecrets,
201
+ loadBuildSecrets,
202
+ loadDefaultSecrets,
203
+ buildHostnameToServiceMap,
204
+ resolveUrlPort
205
+ };
206
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.1.6",
3
+ "version": "2.1.7",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {