@aifabrix/builder 2.1.6 → 2.2.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.
Files changed (38) hide show
  1. package/lib/app-deploy.js +73 -29
  2. package/lib/app-list.js +132 -0
  3. package/lib/app-readme.js +11 -4
  4. package/lib/app-register.js +435 -0
  5. package/lib/app-rotate-secret.js +164 -0
  6. package/lib/app-run.js +98 -84
  7. package/lib/app.js +13 -0
  8. package/lib/audit-logger.js +195 -15
  9. package/lib/build.js +57 -37
  10. package/lib/cli.js +90 -8
  11. package/lib/commands/app.js +8 -391
  12. package/lib/commands/login.js +130 -36
  13. package/lib/config.js +257 -4
  14. package/lib/deployer.js +221 -183
  15. package/lib/infra.js +177 -112
  16. package/lib/secrets.js +85 -99
  17. package/lib/utils/api-error-handler.js +465 -0
  18. package/lib/utils/api.js +165 -16
  19. package/lib/utils/auth-headers.js +84 -0
  20. package/lib/utils/build-copy.js +144 -0
  21. package/lib/utils/cli-utils.js +21 -0
  22. package/lib/utils/compose-generator.js +43 -14
  23. package/lib/utils/deployment-errors.js +90 -0
  24. package/lib/utils/deployment-validation.js +60 -0
  25. package/lib/utils/dev-config.js +83 -0
  26. package/lib/utils/env-template.js +30 -10
  27. package/lib/utils/health-check.js +18 -1
  28. package/lib/utils/infra-containers.js +101 -0
  29. package/lib/utils/local-secrets.js +0 -2
  30. package/lib/utils/secrets-path.js +18 -21
  31. package/lib/utils/secrets-utils.js +206 -0
  32. package/lib/utils/token-manager.js +381 -0
  33. package/package.json +1 -1
  34. package/templates/applications/README.md.hbs +155 -23
  35. package/templates/applications/miso-controller/Dockerfile +7 -119
  36. package/templates/infra/compose.yaml.hbs +93 -0
  37. package/templates/python/docker-compose.hbs +25 -17
  38. package/templates/typescript/docker-compose.hbs +25 -17
package/lib/secrets.js CHANGED
@@ -15,6 +15,8 @@ const yaml = require('js-yaml');
15
15
  const os = require('os');
16
16
  const chalk = require('chalk');
17
17
  const logger = require('./utils/logger');
18
+ const config = require('./config');
19
+ const devConfig = require('./utils/dev-config');
18
20
  const {
19
21
  generateMissingSecrets,
20
22
  createDefaultSecrets
@@ -23,6 +25,13 @@ const {
23
25
  resolveSecretsPath,
24
26
  getActualSecretsPath
25
27
  } = require('./utils/secrets-path');
28
+ const {
29
+ loadUserSecrets,
30
+ loadBuildSecrets,
31
+ loadDefaultSecrets,
32
+ buildHostnameToServiceMap,
33
+ resolveUrlPort
34
+ } = require('./utils/secrets-utils');
26
35
 
27
36
  /**
28
37
  * Loads environment configuration for docker/local context
@@ -34,35 +43,6 @@ function loadEnvConfig() {
34
43
  return yaml.load(content);
35
44
  }
36
45
 
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
46
  /**
67
47
  * Loads secrets with cascading lookup
68
48
  * Supports both user secrets (~/.aifabrix/secrets.local.yaml) and project overrides
@@ -98,76 +78,16 @@ async function loadSecrets(secretsPath, appName) {
98
78
  }
99
79
 
100
80
  // 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
- }
81
+ let mergedSecrets = loadUserSecrets();
122
82
 
123
83
  // Then check build.secrets from variables.yaml if appName provided
124
84
  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
- }
85
+ mergedSecrets = await loadBuildSecrets(mergedSecrets, appName);
150
86
  }
151
87
 
152
88
  // If still no secrets found, try default location
153
89
  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
- }
90
+ mergedSecrets = loadDefaultSecrets();
171
91
  }
172
92
 
173
93
  // If still empty, throw error
@@ -187,7 +107,9 @@ async function loadSecrets(secretsPath, appName) {
187
107
  * @param {string} envTemplate - Environment template content
188
108
  * @param {Object} secrets - Secrets object from loadSecrets()
189
109
  * @param {string} [environment='local'] - Environment context (docker/local)
190
- * @param {string} [secretsFilePath] - Path to secrets file (for error messages)
110
+ * @param {Object|string|null} [secretsFilePaths] - Paths object with userPath and buildPath, or string path (for backward compatibility)
111
+ * @param {string} [secretsFilePaths.userPath] - User's secrets file path
112
+ * @param {string|null} [secretsFilePaths.buildPath] - App's build.secrets file path (if configured)
191
113
  * @returns {Promise<string>} Resolved environment content
192
114
  * @throws {Error} If kv:// reference cannot be resolved
193
115
  *
@@ -195,7 +117,7 @@ async function loadSecrets(secretsPath, appName) {
195
117
  * const resolved = await resolveKvReferences(template, secrets, 'local');
196
118
  * // Returns: 'DATABASE_URL=postgresql://user:pass@localhost:5432/db'
197
119
  */
198
- async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePath = null) {
120
+ async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null) {
199
121
  const envConfig = loadEnvConfig();
200
122
  const envVars = envConfig.environments[environment] || envConfig.environments.local;
201
123
 
@@ -216,7 +138,20 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
216
138
  }
217
139
 
218
140
  if (missingSecrets.length > 0) {
219
- const fileInfo = secretsFilePath ? `\n\nSecrets file location: ${secretsFilePath}` : '';
141
+ let fileInfo = '';
142
+ if (secretsFilePaths) {
143
+ // Handle backward compatibility: if it's a string, use it as-is
144
+ if (typeof secretsFilePaths === 'string') {
145
+ fileInfo = `\n\nSecrets file location: ${secretsFilePaths}`;
146
+ } else if (typeof secretsFilePaths === 'object' && secretsFilePaths.userPath) {
147
+ // New format: show both paths if buildPath is configured
148
+ const paths = [secretsFilePaths.userPath];
149
+ if (secretsFilePaths.buildPath) {
150
+ paths.push(secretsFilePaths.buildPath);
151
+ }
152
+ fileInfo = `\n\nSecrets file location: ${paths.join(' and ')}`;
153
+ }
154
+ }
220
155
  throw new Error(`Missing secrets: ${missingSecrets.join(', ')}${fileInfo}`);
221
156
  }
222
157
 
@@ -235,6 +170,36 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
235
170
  return resolved;
236
171
  }
237
172
 
173
+ /**
174
+ * Resolves service ports in URLs within .env content for Docker environment
175
+ * Replaces ports in URLs with containerPort from service's variables.yaml
176
+ *
177
+ * @function resolveServicePortsInEnvContent
178
+ * @param {string} envContent - Resolved .env file content
179
+ * @param {string} environment - Environment context (docker/local)
180
+ * @returns {string} Content with resolved ports
181
+ */
182
+ function resolveServicePortsInEnvContent(envContent, environment) {
183
+ // Only process docker environment
184
+ if (environment !== 'docker') {
185
+ return envContent;
186
+ }
187
+
188
+ const envConfig = loadEnvConfig();
189
+ const dockerHosts = envConfig.environments.docker || {};
190
+ const hostnameToService = buildHostnameToServiceMap(dockerHosts);
191
+
192
+ // Pattern to match URLs: http://hostname:port or https://hostname:port
193
+ // Matches: protocol://hostname:port/path?query
194
+ // Captures: protocol, hostname, port, and optional path/query
195
+ // Note: [^\s\n]* matches any non-whitespace characters except newline (stops at end of line)
196
+ const urlPattern = /(https?:\/\/)([a-zA-Z0-9-]+):(\d+)([^\s\n]*)?/g;
197
+
198
+ return envContent.replace(urlPattern, (match, protocol, hostname, port, urlPath = '') => {
199
+ return resolveUrlPort(protocol, hostname, port, urlPath || '', hostnameToService);
200
+ });
201
+ }
202
+
238
203
  /**
239
204
  * Loads environment template from file
240
205
  * @function loadEnvTemplate
@@ -324,15 +289,36 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
324
289
 
325
290
  const template = loadEnvTemplate(templatePath);
326
291
 
327
- // Resolve secrets path to show in error messages (use actual path that loadSecrets would use)
328
- const resolvedSecretsPath = getActualSecretsPath(secretsPath, appName);
292
+ // Resolve secrets paths to show in error messages (use actual paths that loadSecrets would use)
293
+ const secretsPaths = getActualSecretsPath(secretsPath, appName);
329
294
 
330
295
  if (force) {
331
- await generateMissingSecrets(template, resolvedSecretsPath);
296
+ // Use userPath for generating missing secrets (priority file)
297
+ await generateMissingSecrets(template, secretsPaths.userPath);
332
298
  }
333
299
 
334
300
  const secrets = await loadSecrets(secretsPath, appName);
335
- const resolved = await resolveKvReferences(template, secrets, environment, resolvedSecretsPath);
301
+ let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths);
302
+
303
+ // Resolve service ports in URLs for docker environment
304
+ if (environment === 'docker') {
305
+ resolved = resolveServicePortsInEnvContent(resolved, environment);
306
+ }
307
+
308
+ // For local environment, update infrastructure ports to use dev-specific ports
309
+ if (environment === 'local') {
310
+ const devId = await config.getDeveloperId();
311
+ const ports = devConfig.getDevPorts(devId);
312
+
313
+ // Update DATABASE_PORT if present
314
+ resolved = resolved.replace(/^DATABASE_PORT\s*=\s*.*$/m, `DATABASE_PORT=${ports.postgres}`);
315
+
316
+ // Update REDIS_URL if present (format: redis://localhost:port)
317
+ resolved = resolved.replace(/^REDIS_URL\s*=\s*redis:\/\/localhost:\d+/m, `REDIS_URL=redis://localhost:${ports.redis}`);
318
+
319
+ // Update REDIS_HOST if it contains a port
320
+ resolved = resolved.replace(/^REDIS_HOST\s*=\s*localhost:\d+/m, `REDIS_HOST=localhost:${ports.redis}`);
321
+ }
336
322
 
337
323
  fs.writeFileSync(envPath, resolved, { mode: 0o600 });
338
324