@aifabrix/builder 2.4.0 → 2.5.1

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.
Files changed (38) hide show
  1. package/README.md +3 -0
  2. package/lib/app-down.js +123 -0
  3. package/lib/app.js +4 -2
  4. package/lib/build.js +19 -13
  5. package/lib/cli.js +52 -9
  6. package/lib/commands/secure.js +5 -40
  7. package/lib/config.js +26 -4
  8. package/lib/env-reader.js +3 -2
  9. package/lib/generator.js +0 -9
  10. package/lib/infra.js +30 -3
  11. package/lib/schema/application-schema.json +0 -15
  12. package/lib/schema/env-config.yaml +8 -8
  13. package/lib/secrets.js +167 -253
  14. package/lib/templates.js +10 -18
  15. package/lib/utils/api-error-handler.js +182 -147
  16. package/lib/utils/api.js +144 -354
  17. package/lib/utils/build-copy.js +6 -13
  18. package/lib/utils/compose-generator.js +2 -1
  19. package/lib/utils/device-code.js +349 -0
  20. package/lib/utils/env-config-loader.js +102 -0
  21. package/lib/utils/env-copy.js +131 -0
  22. package/lib/utils/env-endpoints.js +209 -0
  23. package/lib/utils/env-map.js +116 -0
  24. package/lib/utils/env-ports.js +60 -0
  25. package/lib/utils/environment-checker.js +39 -6
  26. package/lib/utils/image-name.js +49 -0
  27. package/lib/utils/paths.js +22 -20
  28. package/lib/utils/secrets-generator.js +3 -3
  29. package/lib/utils/secrets-helpers.js +359 -0
  30. package/lib/utils/secrets-path.js +12 -36
  31. package/lib/utils/secrets-url.js +38 -0
  32. package/lib/utils/secrets-utils.js +1 -42
  33. package/lib/utils/variable-transformer.js +0 -9
  34. package/package.json +1 -1
  35. package/templates/applications/README.md.hbs +4 -2
  36. package/templates/applications/miso-controller/env.template +1 -1
  37. package/templates/infra/compose.yaml +4 -0
  38. package/templates/infra/compose.yaml.hbs +9 -4
package/lib/secrets.js CHANGED
@@ -12,10 +12,24 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const yaml = require('js-yaml');
15
- const chalk = require('chalk');
16
15
  const logger = require('./utils/logger');
17
16
  const config = require('./config');
18
- const devConfig = require('./utils/dev-config');
17
+ const {
18
+ interpolateEnvVars,
19
+ collectMissingSecrets,
20
+ formatMissingSecretsFileInfo,
21
+ replaceKvInContent,
22
+ loadEnvTemplate,
23
+ processEnvVariables,
24
+ adjustLocalEnvPortsInContent,
25
+ rewriteInfraEndpoints,
26
+ readYamlAtPath,
27
+ applyCanonicalSecretsOverride,
28
+ ensureNonEmptySecrets,
29
+ validateSecrets
30
+ } = require('./utils/secrets-helpers');
31
+ const { buildEnvVarMap } = require('./utils/env-map');
32
+ const { resolveServicePortsInEnvContent } = require('./utils/secrets-url');
19
33
  const {
20
34
  generateMissingSecrets,
21
35
  createDefaultSecrets
@@ -26,22 +40,28 @@ const {
26
40
  } = require('./utils/secrets-path');
27
41
  const {
28
42
  loadUserSecrets,
29
- loadBuildSecrets,
30
- loadDefaultSecrets,
31
- buildHostnameToServiceMap,
32
- resolveUrlPort
43
+ loadDefaultSecrets
33
44
  } = require('./utils/secrets-utils');
34
45
  const { decryptSecret, isEncrypted } = require('./utils/secrets-encryption');
35
46
  const pathsUtil = require('./utils/paths');
36
47
 
37
48
  /**
38
- * Loads environment configuration for docker/local context
39
- * @returns {Object} Environment configuration
49
+ * Generates a canonical secret name from an environment variable key.
50
+ * Converts to lowercase, replaces non-alphanumeric characters with hyphens,
51
+ * collapses consecutive hyphens, and trims leading/trailing hyphens.
52
+ *
53
+ * @function getCanonicalSecretName
54
+ * @param {string} key - Environment variable key (e.g., JWT_SECRET)
55
+ * @returns {string} Canonical secret name (e.g., jwt-secret)
40
56
  */
41
- function loadEnvConfig() {
42
- const envConfigPath = path.join(__dirname, 'schema', 'env-config.yaml');
43
- const content = fs.readFileSync(envConfigPath, 'utf8');
44
- return yaml.load(content);
57
+ function getCanonicalSecretName(key) {
58
+ if (!key || typeof key !== 'string') {
59
+ return '';
60
+ }
61
+ const lower = key.toLowerCase();
62
+ const hyphenated = lower.replace(/[^a-z0-9]/g, '-');
63
+ const collapsed = hyphenated.replace(/-+/g, '-');
64
+ return collapsed.replace(/^-+|-+$/g, '');
45
65
  }
46
66
 
47
67
  /**
@@ -89,13 +109,13 @@ async function decryptSecretsObject(secrets) {
89
109
  /**
90
110
  * Loads secrets with cascading lookup
91
111
  * Supports both user secrets (~/.aifabrix/secrets.local.yaml) and project overrides
92
- * User's file takes priority, then falls back to build.secrets from variables.yaml
112
+ * User's file takes priority, then falls back to aifabrix-secrets from config.yaml
93
113
  * Automatically decrypts values with secure:// prefix
94
114
  *
95
115
  * @async
96
116
  * @function loadSecrets
97
117
  * @param {string} [secretsPath] - Path to secrets file (optional, for explicit override)
98
- * @param {string} [appName] - Application name (optional, for variables.yaml lookup)
118
+ * @param {string} [appName] - Application name (optional, for backward compatibility)
99
119
  * @returns {Promise<Object>} Loaded secrets object with decrypted values
100
120
  * @throws {Error} If no secrets file found and no fallback available
101
121
  *
@@ -103,46 +123,28 @@ async function decryptSecretsObject(secrets) {
103
123
  * const secrets = await loadSecrets('../../secrets.local.yaml');
104
124
  * // Returns: { 'postgres-passwordKeyVault': 'admin123', ... }
105
125
  */
106
- async function loadSecrets(secretsPath, appName) {
107
- let secrets;
108
-
109
- // If explicit path provided, use it (backward compatibility)
126
+ async function loadSecrets(secretsPath, _appName) {
127
+ // Explicit path branch
110
128
  if (secretsPath) {
111
129
  const resolvedPath = resolveSecretsPath(secretsPath);
112
130
  if (!fs.existsSync(resolvedPath)) {
113
131
  throw new Error(`Secrets file not found: ${resolvedPath}`);
114
132
  }
115
-
116
- const content = fs.readFileSync(resolvedPath, 'utf8');
117
- secrets = yaml.load(content);
118
-
119
- if (!secrets || typeof secrets !== 'object') {
133
+ const explicitSecrets = readYamlAtPath(resolvedPath);
134
+ if (!explicitSecrets || typeof explicitSecrets !== 'object') {
120
135
  throw new Error(`Invalid secrets file format: ${resolvedPath}`);
121
136
  }
122
- } else {
123
- // Cascading lookup: user's file first
124
- let mergedSecrets = loadUserSecrets();
125
-
126
- // Then check build.secrets from variables.yaml if appName provided
127
- if (appName) {
128
- mergedSecrets = await loadBuildSecrets(mergedSecrets, appName);
129
- }
130
-
131
- // If still no secrets found, try default location
132
- if (Object.keys(mergedSecrets).length === 0) {
133
- mergedSecrets = loadDefaultSecrets();
134
- }
135
-
136
- // If still empty, throw error
137
- if (Object.keys(mergedSecrets).length === 0) {
138
- throw new Error('No secrets file found. Please create ~/.aifabrix/secrets.local.yaml or configure build.secrets in variables.yaml');
139
- }
140
-
141
- secrets = mergedSecrets;
137
+ return await decryptSecretsObject(explicitSecrets);
142
138
  }
143
139
 
144
- // Decrypt encrypted values
145
- return await decryptSecretsObject(secrets);
140
+ // Cascading lookup branch
141
+ let mergedSecrets = loadUserSecrets();
142
+ mergedSecrets = await applyCanonicalSecretsOverride(mergedSecrets);
143
+ if (Object.keys(mergedSecrets).length === 0) {
144
+ mergedSecrets = loadDefaultSecrets();
145
+ }
146
+ ensureNonEmptySecrets(mergedSecrets);
147
+ return await decryptSecretsObject(mergedSecrets);
146
148
  }
147
149
 
148
150
  /**
@@ -156,7 +158,7 @@ async function loadSecrets(secretsPath, appName) {
156
158
  * @param {string} [environment='local'] - Environment context (docker/local)
157
159
  * @param {Object|string|null} [secretsFilePaths] - Paths object with userPath and buildPath, or string path (for backward compatibility)
158
160
  * @param {string} [secretsFilePaths.userPath] - User's secrets file path
159
- * @param {string|null} [secretsFilePaths.buildPath] - App's build.secrets file path (if configured)
161
+ * @param {string|null} [secretsFilePaths.buildPath] - App's aifabrix-secrets file path (from config.yaml, if configured)
160
162
  * @param {string} [appName] - Application name (optional, for error messages)
161
163
  * @returns {Promise<string>} Resolved environment content
162
164
  * @throws {Error} If kv:// reference cannot be resolved
@@ -166,229 +168,168 @@ async function loadSecrets(secretsPath, appName) {
166
168
  * // Returns: 'DATABASE_URL=postgresql://user:pass@localhost:5432/db'
167
169
  */
168
170
  async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null, appName = null) {
169
- const envConfig = loadEnvConfig();
170
- const envVars = envConfig.environments[environment] || envConfig.environments.local;
171
-
172
- // First, replace ${VAR} references in the template itself (for variables like DB_HOST=${DB_HOST})
173
- let resolved = envTemplate.replace(/\$\{([A-Z_]+)\}/g, (match, envVar) => {
174
- return envVars[envVar] || match;
175
- });
176
-
177
- const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
178
- const missingSecrets = [];
179
-
180
- let match;
181
- while ((match = kvPattern.exec(resolved)) !== null) {
182
- const secretKey = match[1];
183
- if (!(secretKey in secrets)) {
184
- missingSecrets.push(`kv://${secretKey}`);
185
- }
171
+ const os = require('os');
172
+ let envVars = await buildEnvVarMap(environment, os);
173
+ if (!envVars || Object.keys(envVars).length === 0) {
174
+ // Fallback to local environment variables if requested environment does not exist
175
+ envVars = await buildEnvVarMap('local', os);
186
176
  }
187
-
188
- if (missingSecrets.length > 0) {
189
- let fileInfo = '';
190
- if (secretsFilePaths) {
191
- // Handle backward compatibility: if it's a string, use it as-is
192
- if (typeof secretsFilePaths === 'string') {
193
- fileInfo = `\n\nSecrets file location: ${secretsFilePaths}`;
194
- } else if (typeof secretsFilePaths === 'object' && secretsFilePaths.userPath) {
195
- // New format: show both paths if buildPath is configured
196
- const paths = [secretsFilePaths.userPath];
197
- if (secretsFilePaths.buildPath) {
198
- paths.push(secretsFilePaths.buildPath);
199
- }
200
- fileInfo = `\n\nSecrets file location: ${paths.join(' and ')}`;
201
- }
202
- }
177
+ const resolved = interpolateEnvVars(envTemplate, envVars);
178
+ const missing = collectMissingSecrets(resolved, secrets);
179
+ if (missing.length > 0) {
180
+ const fileInfo = formatMissingSecretsFileInfo(secretsFilePaths);
203
181
  const resolveCommand = appName ? `aifabrix resolve ${appName}` : 'aifabrix resolve <app-name>';
204
- throw new Error(`Missing secrets: ${missingSecrets.join(', ')}${fileInfo}\n\nRun "${resolveCommand}" to generate missing secrets.`);
182
+ throw new Error(`Missing secrets: ${missing.join(', ')}${fileInfo}\n\nRun "${resolveCommand}" to generate missing secrets.`);
205
183
  }
206
-
207
- // Now replace kv:// references, and handle ${VAR} inside the secret values
208
- resolved = resolved.replace(kvPattern, (match, secretKey) => {
209
- let value = secrets[secretKey];
210
- if (typeof value === 'string') {
211
- // Replace ${VAR} references inside the secret value
212
- value = value.replace(/\$\{([A-Z_]+)\}/g, (m, envVar) => {
213
- return envVars[envVar] || m;
214
- });
215
- }
216
- return value;
217
- });
218
-
219
- return resolved;
184
+ return replaceKvInContent(resolved, secrets, envVars);
220
185
  }
221
186
 
222
- /**
223
- * Resolves service ports in URLs within .env content for Docker environment
224
- * Replaces ports in URLs with containerPort from service's variables.yaml
225
- *
226
- * @function resolveServicePortsInEnvContent
227
- * @param {string} envContent - Resolved .env file content
228
- * @param {string} environment - Environment context (docker/local)
229
- * @returns {string} Content with resolved ports
230
- */
231
- function resolveServicePortsInEnvContent(envContent, environment) {
232
- // Only process docker environment
233
- if (environment !== 'docker') {
234
- return envContent;
235
- }
236
-
237
- const envConfig = loadEnvConfig();
238
- const dockerHosts = envConfig.environments.docker || {};
239
- const hostnameToService = buildHostnameToServiceMap(dockerHosts);
240
-
241
- // Pattern to match URLs: http://hostname:port or https://hostname:port
242
- // Matches: protocol://hostname:port/path?query
243
- // Captures: protocol, hostname, port, and optional path/query
244
- // Note: [^\s\n]* matches any non-whitespace characters except newline (stops at end of line)
245
- const urlPattern = /(https?:\/\/)([a-zA-Z0-9-]+):(\d+)([^\s\n]*)?/g;
246
-
247
- return envContent.replace(urlPattern, (match, protocol, hostname, port, urlPath = '') => {
248
- return resolveUrlPort(protocol, hostname, port, urlPath || '', hostnameToService);
249
- });
250
- }
187
+ // resolveServicePortsInEnvContent, loadEnvTemplate, and processEnvVariables
188
+ // are imported from ./utils/secrets-helpers above.
251
189
 
252
190
  /**
253
- * Loads environment template from file
254
- * @function loadEnvTemplate
255
- * @param {string} templatePath - Path to env.template
256
- * @returns {string} Template content
257
- * @throws {Error} If file not found
191
+ * Generates .env file from template and secrets
192
+ * Creates environment file for local development
193
+ *
194
+ * @async
195
+ * @function generateEnvFile
196
+ * @param {string} appName - Name of the application
197
+ * @param {string} [secretsPath] - Path to secrets file (optional)
198
+ * @param {string} [environment='local'] - Environment context
199
+ * @param {boolean} [force=false] - Generate missing secret keys in secrets file
200
+ * @param {boolean} [skipOutputPath=false] - Skip processing envOutputPath (to avoid recursion)
201
+ * @returns {Promise<string>} Path to generated .env file
202
+ * @throws {Error} If generation fails
203
+ *
204
+ * @example
205
+ * const envPath = await generateEnvFile('myapp', '../../secrets.local.yaml', 'local', true);
206
+ * // Returns: './builder/myapp/.env'
258
207
  */
259
- function loadEnvTemplate(templatePath) {
260
- if (!fs.existsSync(templatePath)) {
261
- throw new Error(`env.template not found: ${templatePath}`);
262
- }
263
- return fs.readFileSync(templatePath, 'utf8');
264
- }
265
-
266
208
  /**
267
- * Processes environment variables and copies to output path if needed
268
- * Updates PORT variable to use localPort if available (only when copying to envOutputPath)
269
- * When .env stays in builder folder, uses port (container port)
270
- * @function processEnvVariables
271
- * @param {string} envPath - Path to generated .env file
272
- * @param {string} variablesPath - Path to variables.yaml
273
- * @throws {Error} If processing fails
209
+ * Updates PORT in resolved content for docker environment
210
+ * Sets PORT to container port (build.containerPort or port from variables.yaml)
211
+ * NOT the host port (which includes developer-id offset)
212
+ * @async
213
+ * @function updatePortForDocker
214
+ * @param {string} resolved - Resolved environment content
215
+ * @param {string} variablesPath - Path to variables.yaml file
216
+ * @returns {Promise<string>} Updated content with PORT set
274
217
  */
275
- function processEnvVariables(envPath, variablesPath) {
276
- if (!fs.existsSync(variablesPath)) {
277
- return;
278
- }
279
-
280
- const variablesContent = fs.readFileSync(variablesPath, 'utf8');
281
- const variables = yaml.load(variablesContent);
218
+ async function updatePortForDocker(resolved, variablesPath) {
219
+ // Step 1: Get base config from env-config.yaml (includes user env-config file if configured)
220
+ const { getEnvHosts } = require('./utils/env-endpoints');
221
+ let dockerEnv = await getEnvHosts('docker');
282
222
 
283
- if (!variables?.build?.envOutputPath || variables.build.envOutputPath === null) {
284
- return;
285
- }
286
-
287
- let outputPath = path.resolve(process.cwd(), variables.build.envOutputPath);
288
- if (!outputPath.endsWith('.env')) {
289
- if (fs.existsSync(outputPath) && fs.statSync(outputPath).isDirectory()) {
290
- outputPath = path.join(outputPath, '.env');
291
- } else {
292
- outputPath = path.join(outputPath, '.env');
223
+ // Step 2: Apply config.yaml → environments.docker override (if exists)
224
+ try {
225
+ const os = require('os');
226
+ const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
227
+ if (fs.existsSync(cfgPath)) {
228
+ const cfgContent = fs.readFileSync(cfgPath, 'utf8');
229
+ const cfg = yaml.load(cfgContent) || {};
230
+ if (cfg && cfg.environments && cfg.environments.docker) {
231
+ dockerEnv = { ...dockerEnv, ...cfg.environments.docker };
232
+ }
293
233
  }
234
+ } catch {
235
+ // Ignore config.yaml read errors, continue with env-config values
294
236
  }
295
237
 
296
- const outputDir = path.dirname(outputPath);
297
- if (!fs.existsSync(outputDir)) {
298
- fs.mkdirSync(outputDir, { recursive: true });
238
+ // Step 3: Get PORT value for container (should be container port, NOT host port)
239
+ // For Docker containers, PORT should be the container port (build.containerPort or port)
240
+ // NOT the host port (which includes developer-id offset)
241
+ let containerPort = null;
242
+ if (variablesPath && fs.existsSync(variablesPath)) {
243
+ try {
244
+ const variablesContent = fs.readFileSync(variablesPath, 'utf8');
245
+ const variables = yaml.load(variablesContent);
246
+ // Use containerPort if specified, otherwise use base port (no developer-id offset)
247
+ containerPort = variables?.build?.containerPort || variables?.port || 3000;
248
+ } catch {
249
+ // Fallback to default
250
+ containerPort = 3000;
251
+ }
252
+ } else {
253
+ // Fallback: check dockerEnv.PORT (but this should be container port, not host port)
254
+ if (dockerEnv.PORT !== undefined && dockerEnv.PORT !== null) {
255
+ const portVal = typeof dockerEnv.PORT === 'number' ? dockerEnv.PORT : parseInt(dockerEnv.PORT, 10);
256
+ if (!Number.isNaN(portVal)) {
257
+ containerPort = portVal;
258
+ }
259
+ }
260
+ if (containerPort === null || containerPort === undefined) {
261
+ containerPort = 3000;
262
+ }
299
263
  }
300
264
 
301
- // Read the .env file content
302
- let envContent = fs.readFileSync(envPath, 'utf8');
303
-
304
- // Update PORT variable: use localPort ONLY when copying to envOutputPath (outside builder folder)
305
- // When .env stays in builder folder, it uses port (container port)
306
- const portToUse = variables.build?.localPort || variables.port || 3000;
307
-
308
- // Replace PORT line (handles PORT=value format, with or without spaces)
309
- envContent = envContent.replace(/^PORT\s*=\s*.*$/m, `PORT=${portToUse}`);
265
+ // PORT in container should be the container port (no developer-id adjustment)
266
+ // Docker will map container port to host port via port mapping
267
+ return resolved.replace(/^PORT\s*=\s*.*$/m, `PORT=${containerPort}`);
268
+ }
310
269
 
311
- // Write updated content to output path
312
- fs.writeFileSync(outputPath, envContent, { mode: 0o600 });
313
- logger.log(chalk.green(`✓ Copied .env to: ${variables.build.envOutputPath}`));
270
+ /**
271
+ * Applies environment-specific transformations to resolved content
272
+ * @async
273
+ * @function applyEnvironmentTransformations
274
+ * @param {string} resolved - Resolved environment content
275
+ * @param {string} environment - Environment context
276
+ * @param {string} variablesPath - Path to variables.yaml file
277
+ * @returns {Promise<string>} Transformed content
278
+ */
279
+ async function applyEnvironmentTransformations(resolved, environment, variablesPath) {
280
+ if (environment === 'docker') {
281
+ resolved = await resolveServicePortsInEnvContent(resolved, environment);
282
+ resolved = await rewriteInfraEndpoints(resolved, 'docker');
283
+ resolved = await updatePortForDocker(resolved, variablesPath);
284
+ } else if (environment === 'local') {
285
+ resolved = await adjustLocalEnvPortsInContent(resolved, variablesPath);
286
+ }
287
+ return resolved;
314
288
  }
315
289
 
316
290
  /**
317
- * Generates .env file from template and secrets
318
- * Creates environment file for local development
319
- *
291
+ * Generates .env file content from template and secrets (without writing to disk)
320
292
  * @async
321
- * @function generateEnvFile
293
+ * @function generateEnvContent
322
294
  * @param {string} appName - Name of the application
323
295
  * @param {string} [secretsPath] - Path to secrets file (optional)
324
296
  * @param {string} [environment='local'] - Environment context
325
297
  * @param {boolean} [force=false] - Generate missing secret keys in secrets file
326
- * @returns {Promise<string>} Path to generated .env file
298
+ * @returns {Promise<string>} Generated .env file content
327
299
  * @throws {Error} If generation fails
328
- *
329
- * @example
330
- * const envPath = await generateEnvFile('myapp', '../../secrets.local.yaml', 'local', true);
331
- * // Returns: './builder/myapp/.env'
332
300
  */
333
- async function generateEnvFile(appName, secretsPath, environment = 'local', force = false) {
301
+ async function generateEnvContent(appName, secretsPath, environment = 'local', force = false) {
334
302
  const builderPath = path.join(process.cwd(), 'builder', appName);
335
303
  const templatePath = path.join(builderPath, 'env.template');
336
304
  const variablesPath = path.join(builderPath, 'variables.yaml');
337
- const envPath = path.join(builderPath, '.env');
338
305
 
339
306
  const template = loadEnvTemplate(templatePath);
340
-
341
- // Resolve secrets paths to show in error messages (use actual paths that loadSecrets would use)
342
307
  const secretsPaths = await getActualSecretsPath(secretsPath, appName);
343
308
 
344
309
  if (force) {
345
- // Use userPath for generating missing secrets (priority file)
346
310
  await generateMissingSecrets(template, secretsPaths.userPath);
347
311
  }
348
312
 
349
313
  const secrets = await loadSecrets(secretsPath, appName);
350
314
  let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths, appName);
315
+ resolved = await applyEnvironmentTransformations(resolved, environment, variablesPath);
351
316
 
352
- // Resolve service ports in URLs for docker environment
353
- if (environment === 'docker') {
354
- resolved = resolveServicePortsInEnvContent(resolved, environment);
355
- }
356
-
357
- // For local environment, update infrastructure ports to use dev-specific ports
358
- if (environment === 'local') {
359
- const devId = await config.getDeveloperId();
360
- // Convert string developer ID to number for getDevPorts
361
- const devIdNum = parseInt(devId, 10);
362
- const ports = devConfig.getDevPorts(devIdNum);
363
-
364
- // Update DATABASE_PORT if present
365
- resolved = resolved.replace(/^DATABASE_PORT\s*=\s*.*$/m, `DATABASE_PORT=${ports.postgres}`);
317
+ return resolved;
318
+ }
366
319
 
367
- // Update REDIS_URL if present (format: redis://localhost:port)
368
- resolved = resolved.replace(/^REDIS_URL\s*=\s*redis:\/\/localhost:\d+/m, `REDIS_URL=redis://localhost:${ports.redis}`);
320
+ async function generateEnvFile(appName, secretsPath, environment = 'local', force = false, skipOutputPath = false) {
321
+ const builderPath = path.join(process.cwd(), 'builder', appName);
322
+ const variablesPath = path.join(builderPath, 'variables.yaml');
323
+ const envPath = path.join(builderPath, '.env');
369
324
 
370
- // Update REDIS_HOST if it contains a port
371
- resolved = resolved.replace(/^REDIS_HOST\s*=\s*localhost:\d+/m, `REDIS_HOST=localhost:${ports.redis}`);
372
- }
325
+ const resolved = await generateEnvContent(appName, secretsPath, environment, force);
373
326
 
374
327
  fs.writeFileSync(envPath, resolved, { mode: 0o600 });
375
328
 
376
- // Update PORT variable in container .env file to use port (from variables.yaml)
377
- // Note: containerPort is ONLY used for Docker Compose port mapping, NOT for PORT env variable
378
- // The application inside container listens on PORT env variable, which should be 'port' from variables.yaml
379
- if (fs.existsSync(variablesPath)) {
380
- const variablesContent = fs.readFileSync(variablesPath, 'utf8');
381
- const variables = yaml.load(variablesContent);
382
- const port = variables.port || 3000;
383
-
384
- // Update PORT in container .env file to use port (NOT containerPort, NOT localPort)
385
- let envContent = fs.readFileSync(envPath, 'utf8');
386
- envContent = envContent.replace(/^PORT\s*=\s*.*$/m, `PORT=${port}`);
387
- fs.writeFileSync(envPath, envContent, { mode: 0o600 });
388
- }
389
-
390
329
  // Process and copy to envOutputPath if configured (uses localPort for copied file)
391
- processEnvVariables(envPath, variablesPath);
330
+ if (!skipOutputPath) {
331
+ await processEnvVariables(envPath, variablesPath, appName, secretsPath);
332
+ }
392
333
 
393
334
  return envPath;
394
335
  }
@@ -447,43 +388,16 @@ REDIS_COMMANDER_PASSWORD=${postgresPassword}
447
388
  return adminEnvPath;
448
389
  }
449
390
 
450
- /**
451
- * Validates that all required secrets are present
452
- * Checks for missing kv:// references and provides helpful error messages
453
- *
454
- * @function validateSecrets
455
- * @param {string} envTemplate - Environment template content
456
- * @param {Object} secrets - Available secrets
457
- * @returns {Object} Validation result with missing secrets
458
- *
459
- * @example
460
- * const validation = validateSecrets(template, secrets);
461
- * // Returns: { valid: false, missing: ['kv://missing-secret'] }
462
- */
463
- function validateSecrets(envTemplate, secrets) {
464
- const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
465
- const missing = [];
466
-
467
- let match;
468
- while ((match = kvPattern.exec(envTemplate)) !== null) {
469
- const secretKey = match[1];
470
- if (!(secretKey in secrets)) {
471
- missing.push(`kv://${secretKey}`);
472
- }
473
- }
474
-
475
- return {
476
- valid: missing.length === 0,
477
- missing
478
- };
479
- }
391
+ // validateSecrets is imported from ./utils/secrets-helpers
480
392
 
481
393
  module.exports = {
482
394
  loadSecrets,
483
395
  resolveKvReferences,
484
396
  generateEnvFile,
397
+ generateEnvContent,
485
398
  generateMissingSecrets,
486
399
  generateAdminSecretsEnv,
487
400
  validateSecrets,
488
- createDefaultSecrets
401
+ createDefaultSecrets,
402
+ getCanonicalSecretName
489
403
  };
package/lib/templates.js CHANGED
@@ -50,8 +50,7 @@ function generateVariablesYaml(appName, config) {
50
50
  language: config.language || 'typescript',
51
51
  envOutputPath: null,
52
52
  context: null, // Defaults to dev directory in build process
53
- dockerfile: '',
54
- secrets: null
53
+ dockerfile: ''
55
54
  },
56
55
  repository: {
57
56
  enabled: false,
@@ -59,9 +58,7 @@ function generateVariablesYaml(appName, config) {
59
58
  },
60
59
  deployment: {
61
60
  controllerUrl: '',
62
- environment: 'dev',
63
- clientId: '',
64
- clientSecret: ''
61
+ environment: 'dev'
65
62
  }
66
63
  };
67
64
 
@@ -125,7 +122,7 @@ function buildDatabaseEnv(config) {
125
122
  return {
126
123
  'DATABASE_URL': `kv://databases-${appName}-0-urlKeyVault`,
127
124
  'DB_HOST': '${DB_HOST}',
128
- 'DB_PORT': '5432',
125
+ 'DB_PORT': '${DB_PORT}',
129
126
  'DB_NAME': dbName,
130
127
  'DB_USER': `${dbName}_user`,
131
128
  'DB_PASSWORD': `kv://databases-${appName}-0-passwordKeyVault`,
@@ -143,8 +140,8 @@ function buildRedisEnv(config) {
143
140
  return {
144
141
  'REDIS_URL': 'kv://redis-url',
145
142
  'REDIS_HOST': '${REDIS_HOST}',
146
- 'REDIS_PORT': '6379',
147
- 'REDIS_PASSWORD': 'kv://redis-password'
143
+ 'REDIS_PORT': '${REDIS_PORT}',
144
+ 'REDIS_PASSWORD': 'kv://redis-passwordKeyVault'
148
145
  };
149
146
  }
150
147
 
@@ -155,10 +152,7 @@ function buildStorageEnv(config) {
155
152
 
156
153
  return {
157
154
  'STORAGE_TYPE': 'local',
158
- 'STORAGE_PATH': '/app/storage',
159
- 'STORAGE_URL': 'kv://storage-url',
160
- 'STORAGE_KEY': 'kv://storage-key',
161
- 'STORAGE_SECRET': 'kv://storage-secret'
155
+ 'STORAGE_PATH': '/app/storage'
162
156
  };
163
157
  }
164
158
 
@@ -168,10 +162,9 @@ function buildAuthEnv(config) {
168
162
  }
169
163
 
170
164
  return {
171
- 'JWT_SECRET': 'kv://jwt-secret',
165
+ 'JWT_SECRET': 'kv://miso-controller-jwt-secretKeyVault',
172
166
  'JWT_EXPIRES_IN': '24h',
173
- 'AUTH_PROVIDER': 'local',
174
- 'SESSION_SECRET': 'kv://session-secret'
167
+ 'AUTH_PROVIDER': 'local'
175
168
  };
176
169
  }
177
170
 
@@ -415,15 +408,14 @@ function generateSecretsYaml(config, existingSecrets = {}) {
415
408
  secrets.data['database-user'] = 'base64-encoded-user';
416
409
  }
417
410
  if (config.redis) {
418
- secrets.data['redis-password'] = 'base64-encoded-redis-password';
411
+ secrets.data['redis-passwordKeyVault'] = 'base64-encoded-redis-password';
419
412
  }
420
413
  if (config.storage) {
421
414
  secrets.data['storage-key'] = 'base64-encoded-storage-key';
422
415
  secrets.data['storage-secret'] = 'base64-encoded-storage-secret';
423
416
  }
424
417
  if (config.authentication) {
425
- secrets.data['jwt-secret'] = 'base64-encoded-jwt-secret';
426
- secrets.data['session-secret'] = 'base64-encoded-session-secret';
418
+ secrets.data['miso-controller-jwt-secretKeyVault'] = 'base64-encoded-miso-controller-jwt-secretKeyVault';
427
419
  }
428
420
  Object.entries(existingSecrets).forEach(([key, value]) => {
429
421
  secrets.data[key] = Buffer.from(value).toString('base64');