@aifabrix/builder 2.5.3 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -14,7 +14,6 @@ const app = require('./app');
14
14
  const secrets = require('./secrets');
15
15
  const generator = require('./generator');
16
16
  const validator = require('./validator');
17
- const keyGenerator = require('./key-generator');
18
17
  const config = require('./config');
19
18
  const devConfig = require('./utils/dev-config');
20
19
  const chalk = require('chalk');
@@ -306,9 +305,24 @@ function setupCommands(program) {
306
305
  .description('Generate deployment key')
307
306
  .action(async(appName) => {
308
307
  try {
309
- const key = await keyGenerator.generateDeploymentKey(appName);
308
+ // Generate JSON first, then extract key from it
309
+ const jsonPath = await generator.generateDeployJson(appName);
310
+
311
+ // Read the generated JSON file
312
+ const fs = require('fs');
313
+ const jsonContent = fs.readFileSync(jsonPath, 'utf8');
314
+ const deployment = JSON.parse(jsonContent);
315
+
316
+ // Extract deploymentKey from JSON
317
+ const key = deployment.deploymentKey;
318
+
319
+ if (!key) {
320
+ throw new Error('deploymentKey not found in generated JSON');
321
+ }
322
+
310
323
  logger.log(`\nDeployment key for ${appName}:`);
311
324
  logger.log(key);
325
+ logger.log(chalk.gray(`\nGenerated from: ${jsonPath}`));
312
326
  } catch (error) {
313
327
  handleCommandError(error, 'genkey');
314
328
  process.exit(1);
package/lib/generator.js CHANGED
@@ -16,6 +16,22 @@ const _secrets = require('./secrets');
16
16
  const _keyGenerator = require('./key-generator');
17
17
  const _validator = require('./validator');
18
18
 
19
+ /**
20
+ * Sanitizes authentication type - map keycloak to azure (schema allows: azure, local, none)
21
+ * @function sanitizeAuthType
22
+ * @param {string} authType - Authentication type
23
+ * @returns {string} Sanitized authentication type
24
+ */
25
+ function sanitizeAuthType(authType) {
26
+ if (authType === 'keycloak') {
27
+ return 'azure';
28
+ }
29
+ if (authType && !['azure', 'local', 'none'].includes(authType)) {
30
+ return 'azure'; // Default to azure if invalid type
31
+ }
32
+ return authType;
33
+ }
34
+
19
35
  /**
20
36
  * Loads variables.yaml file
21
37
  * @param {string} variablesPath - Path to variables.yaml
@@ -129,11 +145,12 @@ function buildAuthenticationConfig(variables, rbac) {
129
145
 
130
146
  // When enableSSO is false, default type to 'none' and requiredRoles to []
131
147
  // When enableSSO is true, require type and requiredRoles
148
+ // Sanitize auth type (e.g., map keycloak to azure)
132
149
  if (auth.enableSSO === false) {
133
- auth.type = variables.authentication.type || 'none';
150
+ auth.type = sanitizeAuthType(variables.authentication.type || 'none');
134
151
  auth.requiredRoles = variables.authentication.requiredRoles || [];
135
152
  } else {
136
- auth.type = variables.authentication.type || 'azure';
153
+ auth.type = sanitizeAuthType(variables.authentication.type || 'azure');
137
154
  auth.requiredRoles = variables.authentication.requiredRoles || [];
138
155
  }
139
156
 
@@ -313,18 +330,21 @@ async function generateDeployJson(appName) {
313
330
  const jsonPath = path.join(builderPath, 'aifabrix-deploy.json');
314
331
 
315
332
  // Load configuration files
316
- const { content: variablesContent, parsed: variables } = loadVariables(variablesPath);
333
+ const { parsed: variables } = loadVariables(variablesPath);
317
334
  const envTemplate = loadEnvTemplate(templatePath);
318
335
  const rbac = loadRbac(rbacPath);
319
336
 
320
- // Generate deployment key
321
- const deploymentKey = _keyGenerator.generateDeploymentKeyFromContent(variablesContent);
322
-
323
337
  // Parse environment variables from template
324
338
  const configuration = parseEnvironmentVariables(envTemplate);
325
339
 
326
- // Build deployment manifest
327
- const deployment = buildManifestStructure(appName, variables, deploymentKey, configuration, rbac);
340
+ // Build deployment manifest WITHOUT deploymentKey initially
341
+ const deployment = buildManifestStructure(appName, variables, null, configuration, rbac);
342
+
343
+ // Generate deploymentKey from the manifest object (excluding deploymentKey field)
344
+ const deploymentKey = _keyGenerator.generateDeploymentKeyFromJson(deployment);
345
+
346
+ // Add deploymentKey to manifest
347
+ deployment.deploymentKey = deploymentKey;
328
348
 
329
349
  // Validate deployment JSON against schema
330
350
  const validation = _validator.validateDeploymentJson(deployment);
@@ -64,6 +64,63 @@ function generateDeploymentKeyFromContent(content) {
64
64
  return hash.digest('hex');
65
65
  }
66
66
 
67
+ /**
68
+ * Recursively sorts object keys for deterministic JSON stringification
69
+ * @param {*} obj - Object to sort
70
+ * @returns {*} Object with sorted keys
71
+ */
72
+ function sortObjectKeys(obj) {
73
+ if (obj === null || typeof obj !== 'object') {
74
+ return obj;
75
+ }
76
+
77
+ if (Array.isArray(obj)) {
78
+ return obj.map(item => sortObjectKeys(item));
79
+ }
80
+
81
+ const sorted = {};
82
+ const keys = Object.keys(obj).sort();
83
+ for (const key of keys) {
84
+ sorted[key] = sortObjectKeys(obj[key]);
85
+ }
86
+ return sorted;
87
+ }
88
+
89
+ /**
90
+ * Generates deployment key from deployment manifest object
91
+ * Creates SHA256 hash of manifest (excluding deploymentKey field) for integrity verification
92
+ * Uses deterministic JSON stringification (sorted keys, no whitespace)
93
+ *
94
+ * @function generateDeploymentKeyFromJson
95
+ * @param {Object} deploymentObject - Deployment manifest object
96
+ * @returns {string} SHA256 hash of manifest (64-character hex string)
97
+ * @throws {Error} If deploymentObject is invalid
98
+ *
99
+ * @example
100
+ * const key = generateDeploymentKeyFromJson(deploymentManifest);
101
+ * // Returns: 'a1b2c3d4e5f6...' (64-character SHA256 hash)
102
+ */
103
+ function generateDeploymentKeyFromJson(deploymentObject) {
104
+ if (!deploymentObject || typeof deploymentObject !== 'object') {
105
+ throw new Error('Deployment object is required and must be an object');
106
+ }
107
+
108
+ // Create a copy and remove deploymentKey field if present
109
+ const manifestCopy = { ...deploymentObject };
110
+ delete manifestCopy.deploymentKey;
111
+
112
+ // Sort all keys recursively for deterministic JSON stringification
113
+ const sortedManifest = sortObjectKeys(manifestCopy);
114
+
115
+ // Deterministic JSON stringification: sorted keys, no whitespace
116
+ const jsonString = JSON.stringify(sortedManifest);
117
+
118
+ // Generate SHA256 hash
119
+ const hash = crypto.createHash('sha256');
120
+ hash.update(jsonString, 'utf8');
121
+ return hash.digest('hex');
122
+ }
123
+
67
124
  /**
68
125
  * Validates deployment key format
69
126
  * Ensures key is a valid SHA256 hash
@@ -89,5 +146,6 @@ function validateDeploymentKey(key) {
89
146
  module.exports = {
90
147
  generateDeploymentKey,
91
148
  generateDeploymentKeyFromContent,
149
+ generateDeploymentKeyFromJson,
92
150
  validateDeploymentKey
93
151
  };
@@ -34,7 +34,7 @@
34
34
  ]
35
35
  },
36
36
  "type": "object",
37
- "required": ["key", "displayName", "description", "type", "image", "registryMode", "port"],
37
+ "required": ["key", "displayName", "description", "type", "image", "registryMode", "port", "deploymentKey"],
38
38
  "properties": {
39
39
  "key": {
40
40
  "type": "string",
@@ -76,6 +76,11 @@
76
76
  "minimum": 1,
77
77
  "maximum": 65535
78
78
  },
79
+ "deploymentKey": {
80
+ "type": "string",
81
+ "description": "SHA256 hash of deployment manifest (excluding deploymentKey field)",
82
+ "pattern": "^[a-f0-9]{64}$"
83
+ },
79
84
  "requiresDatabase": {
80
85
  "type": "boolean",
81
86
  "description": "Whether application requires database"
package/lib/secrets.js CHANGED
@@ -169,10 +169,21 @@ async function loadSecrets(secretsPath, _appName) {
169
169
  */
170
170
  async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null, appName = null) {
171
171
  const os = require('os');
172
- let envVars = await buildEnvVarMap(environment, os);
172
+
173
+ // Get developer-id for local environment to adjust port variables
174
+ let developerId = null;
175
+ if (environment === 'local') {
176
+ try {
177
+ developerId = await config.getDeveloperId();
178
+ } catch {
179
+ // ignore, will use null (buildEnvVarMap will fetch it)
180
+ }
181
+ }
182
+
183
+ let envVars = await buildEnvVarMap(environment, os, developerId);
173
184
  if (!envVars || Object.keys(envVars).length === 0) {
174
185
  // Fallback to local environment variables if requested environment does not exist
175
- envVars = await buildEnvVarMap('local', os);
186
+ envVars = await buildEnvVarMap('local', os, developerId);
176
187
  }
177
188
  const resolved = interpolateEnvVars(envTemplate, envVars);
178
189
  const missing = collectMissingSecrets(resolved, secrets);
@@ -282,6 +293,7 @@ async function applyEnvironmentTransformations(resolved, environment, variablesP
282
293
  resolved = await rewriteInfraEndpoints(resolved, 'docker');
283
294
  resolved = await updatePortForDocker(resolved, variablesPath);
284
295
  } else if (environment === 'local') {
296
+ // adjustLocalEnvPortsInContent handles both PORT and infra endpoints
285
297
  resolved = await adjustLocalEnvPortsInContent(resolved, variablesPath);
286
298
  }
287
299
  return resolved;
@@ -48,6 +48,9 @@ function formatError(error) {
48
48
  // Check for specific error patterns first (most specific to least specific)
49
49
  if (errorMsg.includes('Configuration not found')) {
50
50
  messages.push(` ${errorMsg}`);
51
+ } else if (errorMsg.includes('does not match schema') || errorMsg.includes('Validation failed') || errorMsg.includes('Field "') || errorMsg.includes('Invalid format')) {
52
+ // Schema validation errors - show the actual error message
53
+ messages.push(` ${errorMsg}`);
51
54
  } else if (errorMsg.includes('not found locally') || (errorMsg.includes('Docker image') && errorMsg.includes('not found'))) {
52
55
  messages.push(' Docker image not found.');
53
56
  messages.push(' Run: aifabrix build <app> first');
@@ -57,7 +60,8 @@ function formatError(error) {
57
60
  } else if (errorMsg.includes('port')) {
58
61
  messages.push(' Port conflict detected.');
59
62
  messages.push(' Run "aifabrix doctor" to check which ports are in use.');
60
- } else if (errorMsg.includes('permission')) {
63
+ } else if ((errorMsg.includes('permission denied') || errorMsg.includes('EACCES') || errorMsg.includes('Permission denied')) && !errorMsg.includes('permissions/') && !errorMsg.includes('Field "permissions')) {
64
+ // Only match actual permission denied errors, not validation errors about permissions fields
61
65
  messages.push(' Permission denied.');
62
66
  messages.push(' Make sure you have the necessary permissions to run Docker commands.');
63
67
  } else if (errorMsg.includes('Azure CLI is not installed') || errorMsg.includes('az --version failed') || (errorMsg.includes('az') && errorMsg.includes('failed'))) {
@@ -28,83 +28,76 @@ async function getEnvHosts(context) {
28
28
  }
29
29
 
30
30
  /**
31
- * Rewrites infra endpoints (REDIS_URL/REDIS_HOST/REDIS_PORT, DB_HOST/DB_PORT, etc.) based on env-config and context
32
- * Uses getEnvHosts() to get all service values dynamically, avoiding hardcoded values
33
- * @async
34
- * @function rewriteInfraEndpoints
35
- * @param {string} envContent - .env file content
36
- * @param {'docker'|'local'} context - Environment context
37
- * @param {{redis:number, postgres:number}} [devPorts] - Ports object with developer-id adjusted ports (optional)
38
- * @returns {Promise<string>} Updated content
31
+ * Split host:port value into host and port
32
+ * @function splitHost
33
+ * @param {string|number} value - Host:port string or plain value
34
+ * @returns {{host: string|undefined, port: number|undefined}} Split host and port
39
35
  */
40
- async function rewriteInfraEndpoints(envContent, context, devPorts) {
41
- // Get all service values from config system (includes env-config.yaml + user env-config file)
42
- let hosts = await getEnvHosts(context);
36
+ function splitHost(value) {
37
+ if (typeof value !== 'string') return { host: undefined, port: undefined };
38
+ const m = value.match(/^([^:]+):(\d+)$/);
39
+ if (m) return { host: m[1], port: parseInt(m[2], 10) };
40
+ return { host: value, port: undefined };
41
+ }
43
42
 
44
- // Apply config.yaml → environments.{context} override (if exists)
43
+ /**
44
+ * Get aifabrix-localhost override from config.yaml for local context
45
+ * @function getLocalhostOverride
46
+ * @param {'docker'|'local'} context - Environment context
47
+ * @returns {string|null} Localhost override value or null
48
+ */
49
+ function getLocalhostOverride(context) {
50
+ if (context !== 'local') return null;
45
51
  try {
46
52
  const os = require('os');
47
53
  const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
48
54
  if (fs.existsSync(cfgPath)) {
49
55
  const cfgContent = fs.readFileSync(cfgPath, 'utf8');
50
56
  const cfg = yaml.load(cfgContent) || {};
51
- if (cfg && cfg.environments && cfg.environments[context]) {
52
- hosts = { ...hosts, ...cfg.environments[context] };
57
+ if (typeof cfg['aifabrix-localhost'] === 'string' && cfg['aifabrix-localhost'].trim().length > 0) {
58
+ return cfg['aifabrix-localhost'].trim();
53
59
  }
54
60
  }
55
61
  } catch {
56
- // Ignore config.yaml read errors, continue with env-config values
62
+ // ignore override errors
57
63
  }
64
+ return null;
65
+ }
58
66
 
59
- // Helper to split host:port values
60
- const splitHost = (value) => {
61
- if (typeof value !== 'string') return { host: undefined, port: undefined };
62
- const m = value.match(/^([^:]+):(\d+)$/);
63
- if (m) return { host: m[1], port: parseInt(m[2], 10) };
64
- return { host: value, port: undefined };
65
- };
66
-
67
- // Get aifabrix-localhost override for local context
68
- let localhostOverride = null;
69
- if (context === 'local') {
70
- try {
71
- const os = require('os');
72
- const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
73
- if (fs.existsSync(cfgPath)) {
74
- const cfgContent = fs.readFileSync(cfgPath, 'utf8');
75
- const cfg = yaml.load(cfgContent) || {};
76
- if (typeof cfg['aifabrix-localhost'] === 'string' && cfg['aifabrix-localhost'].trim().length > 0) {
77
- localhostOverride = cfg['aifabrix-localhost'].trim();
78
- }
79
- }
80
- } catch {
81
- // ignore override errors
82
- }
67
+ /**
68
+ * Get service port with developer-id adjustment (only for local context)
69
+ * @async
70
+ * @function getServicePort
71
+ * @param {string} portKey - Port key (e.g., 'REDIS_PORT', 'DB_PORT')
72
+ * @param {string} serviceName - Service name (e.g., 'redis', 'postgres')
73
+ * @param {Object} hosts - Hosts configuration object
74
+ * @param {'docker'|'local'} context - Environment context
75
+ * @param {Object} [devPorts] - Optional devPorts object with pre-adjusted ports
76
+ * @returns {Promise<number>} Service port with developer-id adjustment (for local context only)
77
+ */
78
+ async function getServicePort(portKey, serviceName, hosts, context, devPorts) {
79
+ // If devPorts provided, use it (already has developer-id adjustment)
80
+ if (devPorts && typeof devPorts[serviceName] === 'number') {
81
+ return devPorts[serviceName];
83
82
  }
84
83
 
85
- // Helper to get port value from config or devPorts, with developer-id adjustment
86
- const getServicePort = async(portKey, serviceName) => {
87
- // If devPorts provided, use it (already has developer-id adjustment)
88
- if (devPorts && typeof devPorts[serviceName] === 'number') {
89
- return devPorts[serviceName];
90
- }
91
-
92
- // Get base port from config
93
- let basePort = null;
94
- if (hosts[portKey] !== undefined && hosts[portKey] !== null) {
95
- const portVal = typeof hosts[portKey] === 'number' ? hosts[portKey] : parseInt(hosts[portKey], 10);
96
- if (!Number.isNaN(portVal)) {
97
- basePort = portVal;
98
- }
84
+ // Get base port from config
85
+ let basePort = null;
86
+ if (hosts[portKey] !== undefined && hosts[portKey] !== null) {
87
+ const portVal = typeof hosts[portKey] === 'number' ? hosts[portKey] : parseInt(hosts[portKey], 10);
88
+ if (!Number.isNaN(portVal)) {
89
+ basePort = portVal;
99
90
  }
91
+ }
100
92
 
101
- // Last resort fallback to devConfig (only if not in config)
102
- if (basePort === null || basePort === undefined) {
103
- const basePorts = devConfig.getBasePorts();
104
- basePort = basePorts[serviceName];
105
- }
93
+ // Last resort fallback to devConfig (only if not in config)
94
+ if (basePort === null || basePort === undefined) {
95
+ const basePorts = devConfig.getBasePorts();
96
+ basePort = basePorts[serviceName];
97
+ }
106
98
 
107
- // Apply developer-id adjustment
99
+ // Apply developer-id adjustment only for local context
100
+ if (context === 'local') {
108
101
  try {
109
102
  const devId = await config.getDeveloperId();
110
103
  let devIdNum = 0;
@@ -118,29 +111,40 @@ async function rewriteInfraEndpoints(envContent, context, devPorts) {
118
111
  } catch {
119
112
  return basePort;
120
113
  }
121
- };
122
-
123
- // Get Redis configuration
124
- const redisParts = splitHost(hosts.REDIS_HOST);
125
- let redisHost = redisParts.host || hosts.REDIS_HOST;
126
- // Fallback to default if not in config
127
- if (!redisHost) {
128
- redisHost = context === 'docker' ? 'redis' : 'localhost';
129
- }
130
- if (context === 'local' && localhostOverride && redisHost === 'localhost') {
131
- redisHost = localhostOverride;
132
114
  }
133
- const redisPort = await getServicePort('REDIS_PORT', 'redis');
134
115
 
135
- // Get DB configuration
136
- let dbHost = hosts.DB_HOST;
137
- // Fallback to default if not in config
138
- if (!dbHost) {
139
- dbHost = context === 'docker' ? 'postgres' : 'localhost';
116
+ // For docker context, return base port without adjustment
117
+ return basePort;
118
+ }
119
+
120
+ /**
121
+ * Get service host with localhost override applied
122
+ * @function getServiceHost
123
+ * @param {string|undefined} host - Host value from config
124
+ * @param {'docker'|'local'} context - Environment context
125
+ * @param {string} defaultHost - Default host if not in config
126
+ * @param {string|null} localhostOverride - Localhost override value
127
+ * @returns {string} Final host value
128
+ */
129
+ function getServiceHost(host, context, defaultHost, localhostOverride) {
130
+ const finalHost = host || defaultHost;
131
+ if (context === 'local' && localhostOverride && finalHost === 'localhost') {
132
+ return localhostOverride;
140
133
  }
141
- const finalDbHost = (context === 'local' && localhostOverride && dbHost === 'localhost') ? localhostOverride : dbHost;
142
- const dbPort = await getServicePort('DB_PORT', 'postgres');
134
+ return finalHost;
135
+ }
143
136
 
137
+ /**
138
+ * Update endpoint variables in env content
139
+ * @function updateEndpointVariables
140
+ * @param {string} envContent - Environment content
141
+ * @param {string} redisHost - Redis host
142
+ * @param {number} redisPort - Redis port
143
+ * @param {string} dbHost - Database host
144
+ * @param {number} dbPort - Database port
145
+ * @returns {string} Updated content
146
+ */
147
+ function updateEndpointVariables(envContent, redisHost, redisPort, dbHost, dbPort) {
144
148
  let updated = envContent;
145
149
 
146
150
  // Update REDIS_URL if present
@@ -180,7 +184,7 @@ async function rewriteInfraEndpoints(envContent, context, devPorts) {
180
184
 
181
185
  // Update DB_HOST if present
182
186
  if (/^DB_HOST\s*=.*$/m.test(updated)) {
183
- updated = updated.replace(/^DB_HOST\s*=\s*.*$/m, `DB_HOST=${finalDbHost}`);
187
+ updated = updated.replace(/^DB_HOST\s*=\s*.*$/m, `DB_HOST=${dbHost}`);
184
188
  }
185
189
 
186
190
  // Update DB_PORT if present
@@ -202,8 +206,59 @@ async function rewriteInfraEndpoints(envContent, context, devPorts) {
202
206
  return updated;
203
207
  }
204
208
 
209
+ /**
210
+ * Rewrites infra endpoints (REDIS_URL/REDIS_HOST/REDIS_PORT, DB_HOST/DB_PORT, etc.) based on env-config and context
211
+ * Uses getEnvHosts() to get all service values dynamically, avoiding hardcoded values
212
+ * @async
213
+ * @function rewriteInfraEndpoints
214
+ * @param {string} envContent - .env file content
215
+ * @param {'docker'|'local'} context - Environment context
216
+ * @param {{redis:number, postgres:number}} [devPorts] - Ports object with developer-id adjusted ports (optional)
217
+ * @param {Object} [adjustedHosts] - Optional adjusted hosts object (with developer-id adjusted ports) to use instead of loading from config
218
+ * @returns {Promise<string>} Updated content
219
+ */
220
+ async function rewriteInfraEndpoints(envContent, context, devPorts, adjustedHosts) {
221
+ // Get all service values from config system (includes env-config.yaml + user env-config file)
222
+ // Use adjustedHosts if provided, otherwise load from config
223
+ let hosts = adjustedHosts || await getEnvHosts(context);
224
+
225
+ // Apply config.yaml → environments.{context} override (if exists)
226
+ try {
227
+ const os = require('os');
228
+ const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
229
+ if (fs.existsSync(cfgPath)) {
230
+ const cfgContent = fs.readFileSync(cfgPath, 'utf8');
231
+ const cfg = yaml.load(cfgContent) || {};
232
+ if (cfg && cfg.environments && cfg.environments[context]) {
233
+ hosts = { ...hosts, ...cfg.environments[context] };
234
+ }
235
+ }
236
+ } catch {
237
+ // Ignore config.yaml read errors, continue with env-config values
238
+ }
239
+
240
+ // Get localhost override for local context
241
+ const localhostOverride = getLocalhostOverride(context);
242
+
243
+ // Get Redis configuration
244
+ const redisParts = splitHost(hosts.REDIS_HOST);
245
+ const redisHost = getServiceHost(redisParts.host || hosts.REDIS_HOST, context, context === 'docker' ? 'redis' : 'localhost', localhostOverride);
246
+ const redisPort = await getServicePort('REDIS_PORT', 'redis', hosts, context, devPorts);
247
+
248
+ // Get DB configuration
249
+ const dbHost = getServiceHost(hosts.DB_HOST, context, context === 'docker' ? 'postgres' : 'localhost', localhostOverride);
250
+ const dbPort = await getServicePort('DB_PORT', 'postgres', hosts, context, devPorts);
251
+
252
+ // Update endpoint variables
253
+ return updateEndpointVariables(envContent, redisHost, redisPort, dbHost, dbPort);
254
+ }
255
+
205
256
  module.exports = {
206
257
  rewriteInfraEndpoints,
207
- getEnvHosts
258
+ getEnvHosts,
259
+ splitHost,
260
+ getServicePort,
261
+ getServiceHost,
262
+ updateEndpointVariables
208
263
  };
209
264
 
@@ -12,19 +12,22 @@ const fs = require('fs');
12
12
  const path = require('path');
13
13
  const yaml = require('js-yaml');
14
14
  const { loadEnvConfig } = require('./env-config-loader');
15
+ const config = require('../config');
15
16
 
16
17
  /**
17
18
  * Build environment variable map for interpolation based on env-config.yaml
18
19
  * - Supports values like "host:port" by splitting into *_HOST (host) and *_PORT (port)
19
20
  * - Merges overrides from ~/.aifabrix/config.yaml under environments.{env}
20
21
  * - Applies aifabrix-localhost override for local context if configured
22
+ * - Applies developer-id adjustment to port variables for local context
21
23
  * @async
22
24
  * @function buildEnvVarMap
23
25
  * @param {'docker'|'local'} context - Environment context
24
26
  * @param {Object} [osModule] - Optional os module (for testing). If not provided, requires 'os'
27
+ * @param {number|null} [developerId] - Optional developer ID for port adjustment. If not provided, will be fetched from config for local context.
25
28
  * @returns {Promise<Object>} Map of variables for interpolation
26
29
  */
27
- async function buildEnvVarMap(context, osModule = null) {
30
+ async function buildEnvVarMap(context, osModule = null, developerId = null) {
28
31
  // Load env-config (base + user override if configured)
29
32
  let baseVars = {};
30
33
  try {
@@ -107,6 +110,50 @@ async function buildEnvVarMap(context, osModule = null) {
107
110
  result[key] = val;
108
111
  }
109
112
  }
113
+
114
+ // Apply developer-id adjustment to port variables for local context
115
+ if (context === 'local') {
116
+ let devIdNum = 0;
117
+ if (developerId !== null && developerId !== undefined) {
118
+ const parsed = typeof developerId === 'number' ? developerId : parseInt(developerId, 10);
119
+ if (!Number.isNaN(parsed)) {
120
+ devIdNum = parsed;
121
+ }
122
+ } else {
123
+ // Get developer-id from config if not provided
124
+ try {
125
+ const devId = await config.getDeveloperId();
126
+ if (devId !== null && devId !== undefined) {
127
+ const parsed = parseInt(devId, 10);
128
+ if (!Number.isNaN(parsed)) {
129
+ devIdNum = parsed;
130
+ }
131
+ }
132
+ } catch {
133
+ // ignore, will use 0
134
+ }
135
+ }
136
+
137
+ // Apply adjustment to all *_PORT variables
138
+ if (devIdNum !== 0) {
139
+ for (const [key, value] of Object.entries(result)) {
140
+ if (/_PORT$/.test(key)) {
141
+ let portVal;
142
+ if (typeof value === 'string') {
143
+ portVal = parseInt(value, 10);
144
+ } else if (typeof value === 'number') {
145
+ portVal = value;
146
+ } else {
147
+ continue;
148
+ }
149
+ if (!Number.isNaN(portVal)) {
150
+ result[key] = String(portVal + (devIdNum * 100));
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+
110
157
  return result;
111
158
  }
112
159
 
@@ -13,7 +13,6 @@ const path = require('path');
13
13
  const yaml = require('js-yaml');
14
14
  const config = require('../config');
15
15
  const { buildHostnameToServiceMap, resolveUrlPort } = require('./secrets-utils');
16
- const devConfig = require('../utils/dev-config');
17
16
  const { rewriteInfraEndpoints, getEnvHosts } = require('./env-endpoints');
18
17
  const { loadEnvConfig } = require('./env-config-loader');
19
18
  const { processEnvVariables } = require('./env-copy');
@@ -132,45 +131,18 @@ function loadEnvTemplate(templatePath) {
132
131
  }
133
132
 
134
133
  /**
135
- * Adjust infra-related ports in resolved .env content for local environment
136
- * Follows flow: getEnvHosts() → config.yaml override → variables.yaml overridedeveloper-id adjustment
134
+ * Calculate application port following override chain and developer-id adjustment
135
+ * Override chain: env-config.yaml → config.yaml → variables.yaml build.localPortvariables.yaml port
137
136
  * @async
138
- * @function adjustLocalEnvPortsInContent
139
- * @param {string} envContent - Resolved .env content
140
- * @param {string} [variablesPath] - Path to variables.yaml (to read build.localPort)
141
- * @returns {Promise<string>} Updated content with local ports
137
+ * @function calculateAppPort
138
+ * @param {string} [variablesPath] - Path to variables.yaml
139
+ * @param {Object} localEnv - Local environment config from env-config.yaml and config.yaml
140
+ * @param {string} envContent - Environment content for fallback
141
+ * @param {number} devIdNum - Developer ID number
142
+ * @returns {Promise<number>} Final application port with developer-id adjustment
142
143
  */
143
- async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
144
- // Get developer-id for port adjustment
145
- const devId = await config.getDeveloperId();
146
- let devIdNum = 0;
147
- if (devId !== null && devId !== undefined) {
148
- const parsed = parseInt(devId, 10);
149
- if (!Number.isNaN(parsed)) {
150
- devIdNum = parsed;
151
- }
152
- }
153
-
154
- // Step 1: Get base config from env-config.yaml (includes user env-config file if configured)
155
- let localEnv = await getEnvHosts('local');
156
-
157
- // Step 2: Apply config.yaml → environments.local override (if exists)
158
- try {
159
- const os = require('os');
160
- const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
161
- if (fs.existsSync(cfgPath)) {
162
- const cfgContent = fs.readFileSync(cfgPath, 'utf8');
163
- const cfg = yaml.load(cfgContent) || {};
164
- if (cfg && cfg.environments && cfg.environments.local) {
165
- localEnv = { ...localEnv, ...cfg.environments.local };
166
- }
167
- }
168
- } catch {
169
- // Ignore config.yaml read errors, continue with env-config values
170
- }
171
-
172
- // Step 3: Get PORT value following override chain
173
- // Start with env-config value, override with variables.yaml build.localPort, then port
144
+ async function calculateAppPort(variablesPath, localEnv, envContent, devIdNum) {
145
+ // Start with env-config value
174
146
  let baseAppPort = null;
175
147
  if (localEnv.PORT !== undefined && localEnv.PORT !== null) {
176
148
  const portVal = typeof localEnv.PORT === 'number' ? localEnv.PORT : parseInt(localEnv.PORT, 10);
@@ -179,7 +151,7 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
179
151
  }
180
152
  }
181
153
 
182
- // Override with variables.yaml → build.localPort (if exists)
154
+ // Override with variables.yaml → build.localPort (strongest)
183
155
  if (variablesPath && fs.existsSync(variablesPath)) {
184
156
  try {
185
157
  const variablesContent = fs.readFileSync(variablesPath, 'utf8');
@@ -188,7 +160,7 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
188
160
  if (typeof localPort === 'number' && localPort > 0) {
189
161
  baseAppPort = localPort;
190
162
  } else if (baseAppPort === null || baseAppPort === undefined) {
191
- // Fallback to variables.yaml → port if baseAppPort still not set
163
+ // Fallback to variables.yaml → port
192
164
  baseAppPort = variables?.port || 3000;
193
165
  }
194
166
  } catch {
@@ -206,29 +178,75 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
206
178
  }
207
179
  }
208
180
 
209
- // Step 4: Apply developer-id adjustment: finalPort = basePort + (developerId * 100)
210
- const appPort = devIdNum === 0 ? baseAppPort : (baseAppPort + (devIdNum * 100));
181
+ // Apply developer-id adjustment
182
+ return devIdNum === 0 ? baseAppPort : (baseAppPort + (devIdNum * 100));
183
+ }
184
+
185
+ /**
186
+ * Update localhost URLs that point to base app port to use developer-specific app port
187
+ * @function updateLocalhostUrls
188
+ * @param {string} content - Environment content
189
+ * @param {number} baseAppPort - Base application port
190
+ * @param {number} appPort - Developer-specific application port
191
+ * @returns {string} Updated content with adjusted localhost URLs
192
+ */
193
+ function updateLocalhostUrls(content, baseAppPort, appPort) {
194
+ const localhostUrlPattern = /(https?:\/\/localhost:)(\d+)(\b[^ \n]*)?/g;
195
+ return content.replace(localhostUrlPattern, (match, prefix, portNum, rest = '') => {
196
+ const num = parseInt(portNum, 10);
197
+ if (num === baseAppPort) {
198
+ return `${prefix}${appPort}${rest || ''}`;
199
+ }
200
+ return match;
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Adjust infra-related ports in resolved .env content for local environment
206
+ * Only handles PORT variable (other ports handled by interpolation)
207
+ * Follows flow: getEnvHosts() → config.yaml override → variables.yaml override → developer-id adjustment
208
+ * @async
209
+ * @function adjustLocalEnvPortsInContent
210
+ * @param {string} envContent - Resolved .env content
211
+ * @param {string} [variablesPath] - Path to variables.yaml (to read build.localPort)
212
+ * @returns {Promise<string>} Updated content with local ports
213
+ */
214
+ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
215
+ // Get developer-id for port adjustment
216
+ const devId = await config.getDeveloperId();
217
+ let devIdNum = 0;
218
+ if (devId !== null && devId !== undefined) {
219
+ const parsed = parseInt(devId, 10);
220
+ if (!Number.isNaN(parsed)) {
221
+ devIdNum = parsed;
222
+ }
223
+ }
224
+
225
+ // Get base config from env-config.yaml (includes user env-config file if configured)
226
+ let localEnv = await getEnvHosts('local');
211
227
 
212
- // Step 5: Get infra service ports from config and apply developer-id adjustment
213
- // All infra ports (REDIS_PORT, DB_PORT, etc.) come from localEnv and get developer-id adjustment
214
- const getInfraPort = (portKey, defaultValue) => {
215
- let port = defaultValue;
216
- if (localEnv[portKey] !== undefined && localEnv[portKey] !== null) {
217
- const portVal = typeof localEnv[portKey] === 'number' ? localEnv[portKey] : parseInt(localEnv[portKey], 10);
218
- if (!Number.isNaN(portVal)) {
219
- port = portVal;
228
+ // Apply config.yaml environments.local override (if exists)
229
+ try {
230
+ const os = require('os');
231
+ const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
232
+ if (fs.existsSync(cfgPath)) {
233
+ const cfgContent = fs.readFileSync(cfgPath, 'utf8');
234
+ const cfg = yaml.load(cfgContent) || {};
235
+ if (cfg && cfg.environments && cfg.environments.local) {
236
+ localEnv = { ...localEnv, ...cfg.environments.local };
220
237
  }
221
238
  }
222
- // Apply developer-id adjustment (infra ports are similar to docker)
223
- return devIdNum === 0 ? port : (port + (devIdNum * 100));
224
- };
239
+ } catch {
240
+ // Ignore config.yaml read errors, continue with env-config values
241
+ }
225
242
 
226
- // Get default ports from devConfig as last resort fallback
227
- const basePorts = devConfig.getBasePorts();
228
- const redisPort = getInfraPort('REDIS_PORT', basePorts.redis);
229
- const dbPort = getInfraPort('DB_PORT', basePorts.postgres);
243
+ // Calculate base port (without developer-id adjustment) for URL matching
244
+ const baseAppPort = await calculateAppPort(variablesPath, localEnv, envContent, 0);
245
+ // Calculate final port with developer-id adjustment
246
+ const appPort = await calculateAppPort(variablesPath, localEnv, envContent, devIdNum);
230
247
 
231
- // Update .env content
248
+ // Update .env content - only handle PORT variable
249
+ // Other port variables (DB_PORT, REDIS_PORT, etc.) are handled by interpolation
232
250
  let updated = envContent;
233
251
 
234
252
  // Update PORT
@@ -238,24 +256,11 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
238
256
  updated = `${updated}\nPORT=${appPort}\n`;
239
257
  }
240
258
 
241
- // Update DATABASE_PORT
242
- if (/^DATABASE_PORT\s*=.*$/m.test(updated)) {
243
- updated = updated.replace(/^DATABASE_PORT\s*=\s*.*$/m, `DATABASE_PORT=${dbPort}`);
244
- }
245
-
246
- // Update localhost URLs that point to the base app port to the dev-specific app port
247
- const localhostUrlPattern = /(https?:\/\/localhost:)(\d+)(\b[^ \n]*)?/g;
248
- updated = updated.replace(localhostUrlPattern, (match, prefix, portNum, rest = '') => {
249
- const num = parseInt(portNum, 10);
250
- if (num === baseAppPort) {
251
- return `${prefix}${appPort}${rest || ''}`;
252
- }
253
- return match;
254
- });
259
+ // Update localhost URLs
260
+ updated = updateLocalhostUrls(updated, baseAppPort, appPort);
255
261
 
256
- // Rewrite infra endpoints using env-config mapping for local context
257
- // This handles REDIS_HOST, REDIS_PORT, REDIS_URL, DB_HOST, etc.
258
- updated = await rewriteInfraEndpoints(updated, 'local', { redis: redisPort, postgres: dbPort });
262
+ // Update infra endpoints with developer-id adjusted ports for local context
263
+ updated = await rewriteInfraEndpoints(updated, 'local');
259
264
 
260
265
  return updated;
261
266
  }
@@ -73,6 +73,12 @@ function transformFlatStructure(variables, appName) {
73
73
  result.authentication = auth;
74
74
  }
75
75
 
76
+ // Add placeholder deploymentKey for validation (will be generated from JSON later)
77
+ // This is a 64-character hex string matching the SHA256 pattern
78
+ if (!result.deploymentKey) {
79
+ result.deploymentKey = '0000000000000000000000000000000000000000000000000000000000000000';
80
+ }
81
+
76
82
  return result;
77
83
  }
78
84
 
@@ -284,7 +290,15 @@ function transformVariablesForValidation(variables, appName) {
284
290
  databases: requires.databases || (requires.database ? [{ name: variables.app?.key || appName }] : [])
285
291
  };
286
292
 
287
- return transformOptionalFields(variables, transformed);
293
+ const result = transformOptionalFields(variables, transformed);
294
+
295
+ // Add placeholder deploymentKey for validation (will be generated from JSON later)
296
+ // This is a 64-character hex string matching the SHA256 pattern
297
+ if (!result.deploymentKey) {
298
+ result.deploymentKey = '0000000000000000000000000000000000000000000000000000000000000000';
299
+ }
300
+
301
+ return result;
288
302
  }
289
303
 
290
304
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.5.3",
3
+ "version": "2.6.0",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -44,9 +44,3 @@ build:
44
44
  localPort: 8082 # Port for local development (different from Docker port)
45
45
  containerPort: 8080 # Container port (different from local port)
46
46
  language: typescript # Runtime language for template selection
47
- secrets: # Path to secrets file (optional)
48
-
49
- # Docker Compose
50
- compose:
51
- file: docker-compose.yaml
52
- service: keycloak
@@ -71,7 +71,7 @@ REDIS_PERMISSIONS_TTL=900
71
71
  # Connects to external keycloak from aifabrix-setup
72
72
 
73
73
  KEYCLOAK_REALM=aifabrix
74
- KEYCLOAK_AUTH_SERVER_URL=kv://keycloak-auth-server-urlKeyVault
74
+ KEYCLOAK_SERVER_URL=kv://keycloak-server-urlKeyVault
75
75
  KEYCLOAK_CLIENT_ID=miso-controller
76
76
  KEYCLOAK_CLIENT_SECRET=kv://keycloak-client-secretKeyVault
77
77
  KEYCLOAK_ADMIN_USERNAME=admin
@@ -107,7 +107,7 @@ MOCK=true
107
107
  ENCRYPTION_KEY=kv://secrets-encryptionKeyVault
108
108
 
109
109
  # JWT Configuration (for client token generation)
110
- JWT_SECRET=kv://miso-controller-jwt-secretKeyVaultKeyVault
110
+ JWT_SECRET=kv://miso-controller-jwt-secretKeyVault
111
111
 
112
112
  # When API_KEY is set, a matching Bearer token bypasses OAuth2 validation
113
113
  API_KEY=kv://miso-controller-api-key-secretKeyVault
@@ -149,6 +149,4 @@ LOG_FILE_PATH=/mnt/data/logs
149
149
  # =============================================================================
150
150
 
151
151
  # Mount Volume Configuration
152
- MOUNT_VOLUME=C:/git/esystemsdev/aifabrix-miso/mount
153
-
154
-
152
+ MOUNT_VOLUME=/mnt/data/
@@ -134,19 +134,19 @@ permissions:
134
134
  description: "Delete environments"
135
135
 
136
136
  # Environment Applications
137
- - name: "environments_applications:create"
137
+ - name: "environments-applications:create"
138
138
  roles: ["aifabrix-platform-admin", "aifabrix-deployment-admin", "aifabrix-developer"]
139
139
  description: "Create applications within environments"
140
140
 
141
- - name: "environments_applications:read"
141
+ - name: "environments-applications:read"
142
142
  roles: ["aifabrix-platform-admin", "aifabrix-deployment-admin", "aifabrix-developer", "aifabrix-observer"]
143
143
  description: "View applications within environments"
144
144
 
145
- - name: "environments_applications:update"
145
+ - name: "environments-applications:update"
146
146
  roles: ["aifabrix-platform-admin", "aifabrix-deployment-admin", "aifabrix-developer"]
147
147
  description: "Update applications within environments"
148
148
 
149
- - name: "environments_applications:delete"
149
+ - name: "environments-applications:delete"
150
150
  roles: ["aifabrix-platform-admin", "aifabrix-deployment-admin"]
151
151
  description: "Remove applications from environments"
152
152
 
@@ -207,11 +207,11 @@ permissions:
207
207
  description: "Administrative export access to all data"
208
208
 
209
209
  # Admin Operations
210
- - name: "admin.sync"
210
+ - name: "admin:sync"
211
211
  roles: ["aifabrix-platform-admin", "aifabrix-infrastructure-admin"]
212
212
  description: "Full system synchronization operations"
213
213
 
214
- - name: "admin.keycloak"
214
+ - name: "admin:keycloak"
215
215
  roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
216
216
  description: "Keycloak administration and configuration"
217
217
 
@@ -34,7 +34,7 @@ healthCheck:
34
34
 
35
35
  # Authentication
36
36
  authentication:
37
- type: keycloak
37
+ type: local
38
38
  enableSSO: true
39
39
  requiredRoles:
40
40
  - aifabrix-user
@@ -48,10 +48,4 @@ build:
48
48
  envOutputPath: # Copy .env to repo root for local dev (relative to builder/) (if null, no .env file is copied) (if empty, .env file is copied to repo root)
49
49
  localPort: 3010 # Port for local development (different from Docker port)
50
50
  language: typescript # Runtime language for template selection (typescript or python)
51
- secrets: # Path to secrets file
52
- envFilePath: .env # Generated in builder/
53
51
 
54
- # Docker Compose
55
- compose:
56
- file: docker-compose.yaml
57
- service: miso-controller