@aifabrix/builder 2.1.7 → 2.3.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 (43) 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 +155 -42
  10. package/lib/cli.js +104 -8
  11. package/lib/commands/app.js +8 -391
  12. package/lib/commands/login.js +130 -36
  13. package/lib/commands/secure.js +260 -0
  14. package/lib/config.js +315 -4
  15. package/lib/deployer.js +221 -183
  16. package/lib/infra.js +177 -112
  17. package/lib/push.js +34 -7
  18. package/lib/secrets.js +89 -23
  19. package/lib/templates.js +1 -1
  20. package/lib/utils/api-error-handler.js +465 -0
  21. package/lib/utils/api.js +165 -16
  22. package/lib/utils/auth-headers.js +84 -0
  23. package/lib/utils/build-copy.js +162 -0
  24. package/lib/utils/cli-utils.js +49 -3
  25. package/lib/utils/compose-generator.js +57 -16
  26. package/lib/utils/deployment-errors.js +90 -0
  27. package/lib/utils/deployment-validation.js +60 -0
  28. package/lib/utils/dev-config.js +83 -0
  29. package/lib/utils/docker-build.js +24 -0
  30. package/lib/utils/env-template.js +30 -10
  31. package/lib/utils/health-check.js +18 -1
  32. package/lib/utils/infra-containers.js +101 -0
  33. package/lib/utils/local-secrets.js +0 -2
  34. package/lib/utils/secrets-encryption.js +203 -0
  35. package/lib/utils/secrets-path.js +22 -3
  36. package/lib/utils/token-manager.js +381 -0
  37. package/package.json +2 -2
  38. package/templates/applications/README.md.hbs +155 -23
  39. package/templates/applications/miso-controller/Dockerfile +7 -119
  40. package/templates/infra/compose.yaml.hbs +93 -0
  41. package/templates/python/docker-compose.hbs +25 -17
  42. package/templates/typescript/docker-compose.hbs +25 -17
  43. package/test-output.txt +0 -5431
package/lib/app-run.js CHANGED
@@ -12,7 +12,6 @@
12
12
  const fs = require('fs').promises;
13
13
  const fsSync = require('fs');
14
14
  const path = require('path');
15
- const net = require('net');
16
15
  const chalk = require('chalk');
17
16
  const yaml = require('js-yaml');
18
17
  const { exec } = require('child_process');
@@ -20,8 +19,10 @@ const { promisify } = require('util');
20
19
  const validator = require('./validator');
21
20
  const infra = require('./infra');
22
21
  const secrets = require('./secrets');
22
+ const config = require('./config');
23
+ const buildCopy = require('./utils/build-copy');
23
24
  const logger = require('./utils/logger');
24
- const { waitForHealthCheck } = require('./utils/health-check');
25
+ const { waitForHealthCheck, checkPortAvailable } = require('./utils/health-check');
25
26
  const composeGenerator = require('./utils/compose-generator');
26
27
 
27
28
  const execAsync = promisify(exec);
@@ -59,24 +60,27 @@ async function checkImageExists(imageName, tag = 'latest', debug = false) {
59
60
  /**
60
61
  * Checks if container is already running
61
62
  * @param {string} appName - Application name
63
+ * @param {number} developerId - Developer ID (0 = default infra, > 0 = developer-specific)
62
64
  * @param {boolean} [debug=false] - Enable debug logging
63
65
  * @returns {Promise<boolean>} True if container is running
64
66
  */
65
- async function checkContainerRunning(appName, debug = false) {
67
+ async function checkContainerRunning(appName, developerId, debug = false) {
66
68
  try {
67
- const cmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Names}}"`;
69
+ // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
70
+ const containerName = developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
71
+ const cmd = `docker ps --filter "name=${containerName}" --format "{{.Names}}"`;
68
72
  if (debug) {
69
73
  logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
70
74
  }
71
75
  const { stdout } = await execAsync(cmd);
72
- const isRunning = stdout.trim() === `aifabrix-${appName}`;
76
+ const isRunning = stdout.trim() === containerName;
73
77
  if (debug) {
74
- logger.log(chalk.gray(`[DEBUG] Container aifabrix-${appName} running: ${isRunning}`));
78
+ logger.log(chalk.gray(`[DEBUG] Container ${containerName} running: ${isRunning}`));
75
79
  if (isRunning) {
76
80
  // Get container status details
77
- const statusCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Status}}"`;
81
+ const statusCmd = `docker ps --filter "name=${containerName}" --format "{{.Status}}"`;
78
82
  const { stdout: status } = await execAsync(statusCmd);
79
- const portsCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Ports}}"`;
83
+ const portsCmd = `docker ps --filter "name=${containerName}" --format "{{.Ports}}"`;
80
84
  const { stdout: ports } = await execAsync(portsCmd);
81
85
  logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
82
86
  logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
@@ -94,56 +98,36 @@ async function checkContainerRunning(appName, debug = false) {
94
98
  /**
95
99
  * Stops and removes existing container
96
100
  * @param {string} appName - Application name
101
+ * @param {number} developerId - Developer ID (0 = default infra, > 0 = developer-specific)
97
102
  * @param {boolean} [debug=false] - Enable debug logging
98
103
  */
99
- async function stopAndRemoveContainer(appName, debug = false) {
104
+ async function stopAndRemoveContainer(appName, developerId, debug = false) {
100
105
  try {
101
- logger.log(chalk.yellow(`Stopping existing container aifabrix-${appName}...`));
102
- const stopCmd = `docker stop aifabrix-${appName}`;
106
+ // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
107
+ const containerName = developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
108
+ logger.log(chalk.yellow(`Stopping existing container ${containerName}...`));
109
+ const stopCmd = `docker stop ${containerName}`;
103
110
  if (debug) {
104
111
  logger.log(chalk.gray(`[DEBUG] Executing: ${stopCmd}`));
105
112
  }
106
113
  await execAsync(stopCmd);
107
- const rmCmd = `docker rm aifabrix-${appName}`;
114
+ const rmCmd = `docker rm ${containerName}`;
108
115
  if (debug) {
109
116
  logger.log(chalk.gray(`[DEBUG] Executing: ${rmCmd}`));
110
117
  }
111
118
  await execAsync(rmCmd);
112
- logger.log(chalk.green(`✓ Container aifabrix-${appName} stopped and removed`));
119
+ logger.log(chalk.green(`✓ Container ${containerName} stopped and removed`));
113
120
  } catch (error) {
114
121
  if (debug) {
115
122
  logger.log(chalk.gray(`[DEBUG] Stop/remove container error: ${error.message}`));
116
123
  }
117
124
  // Container might not exist, which is fine
118
- logger.log(chalk.gray(`Container aifabrix-${appName} was not running`));
125
+ // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
126
+ const containerName = developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
127
+ logger.log(chalk.gray(`Container ${containerName} was not running`));
119
128
  }
120
129
  }
121
130
 
122
- /**
123
- * Checks if port is available
124
- * @param {number} port - Port number to check
125
- * @returns {Promise<boolean>} True if port is available
126
- */
127
- async function checkPortAvailable(port) {
128
- return new Promise((resolve) => {
129
- const server = net.createServer();
130
- server.listen(port, () => {
131
- server.close(() => resolve(true));
132
- });
133
- server.on('error', () => resolve(false));
134
- });
135
- }
136
-
137
- /**
138
- * Extracts image name from configuration (same logic as build.js)
139
- * @param {Object} config - Application configuration
140
- * @param {string} appName - Application name (fallback)
141
- * @returns {string} Image name
142
- */
143
- function getImageName(config, appName) {
144
- return composeGenerator.getImageName(config, appName);
145
- }
146
-
147
131
  /**
148
132
  * Validates app name and loads configuration
149
133
  * @async
@@ -230,7 +214,7 @@ async function validateAppConfiguration(appName) {
230
214
  */
231
215
  async function checkPrerequisites(appName, config, debug = false) {
232
216
  // Extract image name from configuration (same logic as build process)
233
- const imageName = getImageName(config, appName);
217
+ const imageName = composeGenerator.getImageName(config, appName);
234
218
  const imageTag = config.image?.tag || 'latest';
235
219
  const fullImageName = `${imageName}:${imageTag}`;
236
220
 
@@ -261,19 +245,28 @@ async function checkPrerequisites(appName, config, debug = false) {
261
245
  }
262
246
  logger.log(chalk.green('✓ Infrastructure is running'));
263
247
  }
264
-
265
248
  /**
266
249
  * Prepares environment: ensures .env file and generates Docker Compose
267
250
  * @async
268
251
  * @param {string} appName - Application name
269
- * @param {Object} config - Application configuration
252
+ * @param {Object} appConfig - Application configuration
270
253
  * @param {Object} options - Run options
271
254
  * @returns {Promise<string>} Path to generated compose file
272
255
  */
273
- async function prepareEnvironment(appName, config, options) {
256
+ async function prepareEnvironment(appName, appConfig, options) {
257
+ // Get developer ID and dev-specific directory
258
+ const developerId = await config.getDeveloperId();
259
+ const devDir = buildCopy.getDevDirectory(appName, developerId);
260
+
261
+ // Ensure dev directory exists (should exist from build, but check anyway)
262
+ if (!fsSync.existsSync(devDir)) {
263
+ await buildCopy.copyBuilderToDevDirectory(appName, developerId);
264
+ }
265
+
274
266
  // Ensure .env file exists with 'docker' environment context (for running in Docker)
275
- const envPath = path.join(process.cwd(), 'builder', appName, '.env');
276
- if (!fsSync.existsSync(envPath)) {
267
+ // Generate in builder directory first, then copy to dev directory
268
+ const builderEnvPath = path.join(process.cwd(), 'builder', appName, '.env');
269
+ if (!fsSync.existsSync(builderEnvPath)) {
277
270
  logger.log(chalk.yellow('Generating .env file from template...'));
278
271
  await secrets.generateEnvFile(appName, null, 'docker');
279
272
  } else {
@@ -282,8 +275,14 @@ async function prepareEnvironment(appName, config, options) {
282
275
  await secrets.generateEnvFile(appName, null, 'docker');
283
276
  }
284
277
 
278
+ // Copy .env to dev directory
279
+ const devEnvPath = path.join(devDir, '.env');
280
+ if (fsSync.existsSync(builderEnvPath)) {
281
+ await fs.copyFile(builderEnvPath, devEnvPath);
282
+ }
283
+
285
284
  // Also ensure .env file in apps/ directory is updated (for Docker build context)
286
- const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
285
+ const variablesPath = path.join(devDir, 'variables.yaml');
287
286
  if (fsSync.existsSync(variablesPath)) {
288
287
  const variablesContent = fsSync.readFileSync(variablesPath, 'utf8');
289
288
  const variables = yaml.load(variablesContent);
@@ -292,14 +291,28 @@ async function prepareEnvironment(appName, config, options) {
292
291
  // The generateEnvFile already copies to apps/, but ensure it's using docker context
293
292
  logger.log(chalk.blue('Ensuring .env file in apps/ directory is updated for Docker...'));
294
293
  await secrets.generateEnvFile(appName, null, 'docker');
294
+ // Copy .env to dev directory again after regeneration
295
+ if (fsSync.existsSync(builderEnvPath)) {
296
+ await fs.copyFile(builderEnvPath, devEnvPath);
297
+ }
295
298
  }
296
299
  }
297
300
 
298
301
  // Generate Docker Compose configuration
299
302
  logger.log(chalk.blue('Generating Docker Compose configuration...'));
300
- const composeContent = await composeGenerator.generateDockerCompose(appName, config, options);
301
- // Write compose file to temporary location
302
- const tempComposePath = path.join(process.cwd(), 'builder', appName, 'docker-compose.yaml');
303
+ const composeOptions = { ...options };
304
+ if (!composeOptions.port) {
305
+ const basePort = appConfig.port || 3000;
306
+ composeOptions.port = developerId === 0 ? basePort : basePort + (developerId * 100);
307
+ }
308
+ const composeContent = await composeGenerator.generateDockerCompose(appName, appConfig, composeOptions);
309
+
310
+ // Write compose file to dev-specific directory (devDir already defined above)
311
+ // Ensure dev directory exists (should exist from build, but check anyway)
312
+ if (!fsSync.existsSync(devDir)) {
313
+ await buildCopy.copyBuilderToDevDirectory(appName, developerId);
314
+ }
315
+ const tempComposePath = path.join(devDir, 'docker-compose.yaml');
303
316
  await fs.writeFile(tempComposePath, composeContent);
304
317
 
305
318
  return tempComposePath;
@@ -346,48 +359,41 @@ async function startContainer(appName, composePath, port, config = null, debug =
346
359
  logger.log(chalk.gray(`[DEBUG] Compose file: ${composePath}`));
347
360
  }
348
361
  await execAsync(composeCmd, { env });
349
- logger.log(chalk.green(`✓ Container aifabrix-${appName} started`));
350
-
362
+ // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
363
+ const containerName = config.developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${config.developerId}-${appName}`;
364
+ logger.log(chalk.green(`✓ Container ${containerName} started`));
351
365
  if (debug) {
352
366
  // Get container status after start
353
- const statusCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Status}}"`;
367
+ const statusCmd = `docker ps --filter "name=${containerName}" --format "{{.Status}}"`;
354
368
  const { stdout: status } = await execAsync(statusCmd);
355
- const portsCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Ports}}"`;
369
+ const portsCmd = `docker ps --filter "name=${containerName}" --format "{{.Ports}}"`;
356
370
  const { stdout: ports } = await execAsync(portsCmd);
357
371
  logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
358
372
  logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
359
373
  }
360
374
 
361
- // Wait for health check using host port
362
- // Port is the host port (CLI --port or config.port, NOT localPort)
375
+ // Wait for health check using host port (CLI --port or dev-specific port, NOT localPort)
363
376
  const healthCheckPath = config?.healthCheck?.path || '/health';
364
377
  logger.log(chalk.blue(`Waiting for application to be healthy at http://localhost:${port}${healthCheckPath}...`));
365
378
  await waitForHealthCheck(appName, 90, port, config, debug);
366
379
  }
367
-
368
380
  /**
369
381
  * Displays run status after successful start
370
382
  * @param {string} appName - Application name
371
383
  * @param {number} port - Application port
372
- * @param {Object} config - Application configuration
384
+ * @param {Object} config - Application configuration (with developerId property)
373
385
  */
374
- function displayRunStatus(appName, port, config) {
386
+ async function displayRunStatus(appName, port, config) {
387
+ // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
388
+ const containerName = config.developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${config.developerId}-${appName}`;
375
389
  const healthCheckPath = config?.healthCheck?.path || '/health';
376
390
  const healthCheckUrl = `http://localhost:${port}${healthCheckPath}`;
377
391
 
378
392
  logger.log(chalk.green(`\n✓ App running at http://localhost:${port}`));
379
393
  logger.log(chalk.blue(`Health check: ${healthCheckUrl}`));
380
- logger.log(chalk.gray(`Container: aifabrix-${appName}`));
394
+ logger.log(chalk.gray(`Container: ${containerName}`));
381
395
  }
382
396
 
383
- /**
384
- * Waits for container health check to pass
385
- * @param {string} appName - Application name
386
- * @param {number} timeout - Timeout in seconds
387
- * @param {number} port - Application port (optional, will be detected if not provided)
388
- * @param {Object} config - Application configuration (optional)
389
- */
390
-
391
397
  /**
392
398
  * Runs the application locally using Docker
393
399
  * Starts container with proper port mapping and environment
@@ -415,47 +421,56 @@ async function runApp(appName, options = {}) {
415
421
 
416
422
  try {
417
423
  // Validate app name and load configuration
418
- const config = await validateAppConfiguration(appName);
424
+ const appConfig = await validateAppConfiguration(appName);
425
+
426
+ // Load developer ID once from config module - it's now cached and available as config.developerId
427
+ // Developer ID: 0 = default infra, > 0 = developer-specific
428
+ const developerId = await config.getDeveloperId(); // Load and cache developer ID
429
+ appConfig.developerId = developerId; // Use developer ID in config
430
+
419
431
  if (debug) {
420
- logger.log(chalk.gray(`[DEBUG] Configuration loaded: port=${config.port || 'default'}, healthCheck.path=${config.healthCheck?.path || '/health'}`));
432
+ logger.log(chalk.gray(`[DEBUG] Configuration loaded: port=${appConfig.port || 'default'}, healthCheck.path=${appConfig.healthCheck?.path || '/health'}, developerId=${appConfig.developerId}`));
421
433
  }
422
434
 
423
435
  // Check prerequisites: image and infrastructure
424
- await checkPrerequisites(appName, config, debug);
436
+ await checkPrerequisites(appName, appConfig, debug);
425
437
 
426
438
  // Check if container is already running
427
- const containerRunning = await checkContainerRunning(appName, debug);
439
+ const containerRunning = await checkContainerRunning(appName, appConfig.developerId, debug);
428
440
  if (containerRunning) {
429
- logger.log(chalk.yellow(`Container aifabrix-${appName} is already running`));
430
- await stopAndRemoveContainer(appName, debug);
441
+ // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
442
+ const containerName = appConfig.developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${appConfig.developerId}-${appName}`;
443
+ logger.log(chalk.yellow(`Container ${containerName} is already running`));
444
+ await stopAndRemoveContainer(appName, appConfig.developerId, debug);
431
445
  }
432
446
 
433
- // Check port availability
434
- // Host port: CLI --port if provided, otherwise port from variables.yaml (NOT localPort)
435
- const port = options.port || config.port || 3000;
447
+ // Calculate host port: use dev-specific port offset if not overridden
448
+ // IMPORTANT: Container port (for Dockerfile) stays unchanged from appConfig.port
449
+ const basePort = appConfig.port || 3000;
450
+ const hostPort = options.port || (appConfig.developerId === 0 ? basePort : basePort + (appConfig.developerId * 100));
436
451
  if (debug) {
437
- logger.log(chalk.gray(`[DEBUG] Port selection: ${port} (${options.port ? 'CLI override' : config.port ? 'config.port' : 'default'})`));
452
+ logger.log(chalk.gray(`[DEBUG] Host port: ${hostPort} (${options.port ? 'CLI override' : 'dev-specific'}), Container port: ${appConfig.build?.containerPort || appConfig.port || 3000} (unchanged)`));
438
453
  }
439
- const portAvailable = await checkPortAvailable(port);
454
+ const portAvailable = await checkPortAvailable(hostPort);
440
455
  if (debug) {
441
- logger.log(chalk.gray(`[DEBUG] Port ${port} available: ${portAvailable}`));
456
+ logger.log(chalk.gray(`[DEBUG] Port ${hostPort} available: ${portAvailable}`));
442
457
  }
443
458
  if (!portAvailable) {
444
- throw new Error(`Port ${port} is already in use. Try --port <alternative>`);
459
+ throw new Error(`Port ${hostPort} is already in use. Try --port <alternative>`);
445
460
  }
446
461
 
447
462
  // Prepare environment: ensure .env file and generate Docker Compose
448
- const tempComposePath = await prepareEnvironment(appName, config, options);
463
+ const tempComposePath = await prepareEnvironment(appName, appConfig, options);
449
464
  if (debug) {
450
465
  logger.log(chalk.gray(`[DEBUG] Compose file generated: ${tempComposePath}`));
451
466
  }
452
467
 
453
468
  try {
454
469
  // Start container and wait for health check
455
- await startContainer(appName, tempComposePath, port, config, debug);
470
+ await startContainer(appName, tempComposePath, hostPort, appConfig, debug);
456
471
 
457
472
  // Display success message
458
- displayRunStatus(appName, port, config);
473
+ await displayRunStatus(appName, hostPort, appConfig);
459
474
 
460
475
  } catch (error) {
461
476
  // Keep the compose file for debugging - don't delete on error
@@ -474,7 +489,6 @@ async function runApp(appName, options = {}) {
474
489
  throw new Error(`Failed to run application: ${error.message}`);
475
490
  }
476
491
  }
477
-
478
492
  module.exports = {
479
493
  runApp,
480
494
  checkImageExists,
package/lib/app.js CHANGED
@@ -22,6 +22,7 @@ const { validateAppName, pushApp } = require('./app-push');
22
22
  const { generateDockerfileForApp } = require('./app-dockerfile');
23
23
  const { loadTemplateVariables, updateTemplateVariables, mergeTemplateVariables } = require('./utils/template-helpers');
24
24
  const logger = require('./utils/logger');
25
+ const auditLogger = require('./audit-logger');
25
26
 
26
27
  /**
27
28
  * Displays success message after app creation
@@ -288,6 +289,18 @@ async function createApp(appName, options = {}) {
288
289
 
289
290
  await handleGitHubWorkflows(options, config);
290
291
  displaySuccessMessage(appName, config, envConversionMessage, options.app);
292
+
293
+ // Log application creation for audit trail
294
+ await auditLogger.logApplicationCreation(appName, {
295
+ language: config.language,
296
+ port: config.port,
297
+ database: config.database,
298
+ redis: config.redis,
299
+ storage: config.storage,
300
+ authentication: config.authentication,
301
+ template: options.template,
302
+ api: null // Local operation, no API involved
303
+ });
291
304
  } catch (error) {
292
305
  throw new Error(`Failed to create application: ${error.message}`);
293
306
  }
@@ -11,6 +11,46 @@
11
11
 
12
12
  /* eslint-disable no-console */
13
13
 
14
+ const fs = require('fs').promises;
15
+ const path = require('path');
16
+ const os = require('os');
17
+
18
+ // Audit log file path (in user's home directory for compliance)
19
+ let auditLogPath = null;
20
+
21
+ /**
22
+ * Gets the audit log file path
23
+ * Creates .aifabrix directory in user's home if it doesn't exist
24
+ * @returns {Promise<string>} Path to audit log file
25
+ */
26
+ async function getAuditLogPath() {
27
+ if (auditLogPath) {
28
+ return auditLogPath;
29
+ }
30
+
31
+ const homeDir = os.homedir();
32
+ const aifabrixDir = path.join(homeDir, '.aifabrix');
33
+
34
+ try {
35
+ await fs.mkdir(aifabrixDir, { recursive: true });
36
+ } catch (error) {
37
+ // If we can't create the directory, fall back to current directory
38
+ const fallbackDir = path.join(process.cwd(), '.aifabrix');
39
+ try {
40
+ await fs.mkdir(fallbackDir, { recursive: true });
41
+ auditLogPath = path.join(fallbackDir, 'audit.log');
42
+ return auditLogPath;
43
+ } catch {
44
+ // Last resort: use temp directory
45
+ auditLogPath = path.join(os.tmpdir(), 'aifabrix-audit.log');
46
+ return auditLogPath;
47
+ }
48
+ }
49
+
50
+ auditLogPath = path.join(aifabrixDir, 'audit.log');
51
+ return auditLogPath;
52
+ }
53
+
14
54
  /**
15
55
  * Masks sensitive data in strings
16
56
  * Prevents secrets, keys, and passwords from appearing in logs
@@ -62,25 +102,45 @@ function createAuditEntry(level, message, metadata = {}) {
62
102
  };
63
103
 
64
104
  // Mask sensitive metadata
105
+ // Only include non-null, non-undefined values to avoid cluttering logs
65
106
  for (const [key, value] of Object.entries(metadata)) {
66
- entry.metadata[key] = maskSensitiveData(
67
- typeof value === 'string' ? value : JSON.stringify(value)
68
- );
107
+ // Skip null and undefined values to keep logs clean
108
+ if (value !== null && value !== undefined) {
109
+ entry.metadata[key] = maskSensitiveData(
110
+ typeof value === 'string' ? value : JSON.stringify(value)
111
+ );
112
+ }
69
113
  }
70
114
 
71
115
  return entry;
72
116
  }
73
117
 
74
118
  /**
75
- * Logs audit entry to console (structured JSON format)
119
+ * Logs audit entry to file (structured JSON format)
120
+ * Only prints to console if AUDIT_LOG_CONSOLE environment variable is set
76
121
  *
77
122
  * @param {string} level - Log level
78
123
  * @param {string} message - Log message
79
124
  * @param {Object} metadata - Additional metadata
80
125
  */
81
- function auditLog(level, message, metadata) {
126
+ async function auditLog(level, message, metadata) {
82
127
  const entry = createAuditEntry(level, message, metadata);
83
- console.log(JSON.stringify(entry));
128
+ const logLine = JSON.stringify(entry) + '\n';
129
+
130
+ // Write to audit log file (for ISO 27001 compliance)
131
+ try {
132
+ const logPath = await getAuditLogPath();
133
+ await fs.appendFile(logPath, logLine, 'utf8');
134
+ } catch (writeError) {
135
+ // If file write fails, fall back to console.error (but don't show audit log)
136
+ // This ensures we don't lose audit trail even if file system fails
137
+ console.error(`[AUDIT LOG ERROR] Failed to write audit log: ${writeError.message}`);
138
+ }
139
+
140
+ // Only print to console if explicitly requested (for debugging/compliance review)
141
+ if (process.env.AUDIT_LOG_CONSOLE === 'true' || process.env.AUDIT_LOG_CONSOLE === '1') {
142
+ console.log(logLine.trim());
143
+ }
84
144
  }
85
145
 
86
146
  /**
@@ -90,14 +150,21 @@ function auditLog(level, message, metadata) {
90
150
  * @param {string} controllerUrl - Controller URL
91
151
  * @param {Object} options - Deployment options
92
152
  */
93
- function logDeploymentAttempt(appName, controllerUrl, options = {}) {
94
- auditLog('AUDIT', 'Deployment initiated', {
153
+ async function logDeploymentAttempt(appName, controllerUrl, options = {}) {
154
+ const metadata = {
95
155
  action: 'deploy',
96
156
  appName,
97
157
  controllerUrl,
98
158
  environment: options.environment || 'unknown',
99
159
  timestamp: Date.now()
100
- });
160
+ };
161
+
162
+ // Only include api field if it's provided and not null
163
+ if (options.api !== undefined && options.api !== null) {
164
+ metadata.api = options.api;
165
+ }
166
+
167
+ await auditLog('AUDIT', 'Deployment initiated', metadata);
101
168
  }
102
169
 
103
170
  /**
@@ -107,8 +174,8 @@ function logDeploymentAttempt(appName, controllerUrl, options = {}) {
107
174
  * @param {string} deploymentId - Deployment ID
108
175
  * @param {string} controllerUrl - Controller URL
109
176
  */
110
- function logDeploymentSuccess(appName, deploymentId, controllerUrl) {
111
- auditLog('AUDIT', 'Deployment succeeded', {
177
+ async function logDeploymentSuccess(appName, deploymentId, controllerUrl) {
178
+ await auditLog('AUDIT', 'Deployment succeeded', {
112
179
  action: 'deploy',
113
180
  appName,
114
181
  deploymentId,
@@ -125,8 +192,8 @@ function logDeploymentSuccess(appName, deploymentId, controllerUrl) {
125
192
  * @param {string} controllerUrl - Controller URL
126
193
  * @param {Error} error - Error that occurred
127
194
  */
128
- function logDeploymentFailure(appName, controllerUrl, error) {
129
- auditLog('ERROR', 'Deployment failed', {
195
+ async function logDeploymentFailure(appName, controllerUrl, error) {
196
+ await auditLog('ERROR', 'Deployment failed', {
130
197
  action: 'deploy',
131
198
  appName,
132
199
  controllerUrl,
@@ -143,8 +210,8 @@ function logDeploymentFailure(appName, controllerUrl, error) {
143
210
  * @param {string} event - Security event name
144
211
  * @param {Object} details - Event details
145
212
  */
146
- function logSecurityEvent(event, details = {}) {
147
- auditLog('AUDIT', `Security event: ${event}`, {
213
+ async function logSecurityEvent(event, details = {}) {
214
+ await auditLog('AUDIT', `Security event: ${event}`, {
148
215
  eventType: 'security',
149
216
  event,
150
217
  ...details,
@@ -152,12 +219,125 @@ function logSecurityEvent(event, details = {}) {
152
219
  });
153
220
  }
154
221
 
222
+ /**
223
+ * Logs application creation event
224
+ * Tracks when new applications are created for audit trail
225
+ *
226
+ * @param {string} appName - Application name
227
+ * @param {Object} options - Creation options
228
+ */
229
+ async function logApplicationCreation(appName, options = {}) {
230
+ const metadata = {
231
+ action: 'create',
232
+ appName,
233
+ language: options.language || 'unknown',
234
+ port: options.port || 'unknown',
235
+ hasDatabase: options.database || false,
236
+ hasRedis: options.redis || false,
237
+ hasStorage: options.storage || false,
238
+ hasAuthentication: options.authentication || false,
239
+ template: options.template || null,
240
+ timestamp: Date.now()
241
+ };
242
+
243
+ // Only include api field if it's provided and not null
244
+ // For local operations (create), api is typically null
245
+ if (options.api !== undefined && options.api !== null) {
246
+ metadata.api = options.api;
247
+ }
248
+
249
+ await auditLog('AUDIT', 'Application created', metadata);
250
+ }
251
+
252
+ /**
253
+ * Logs API call attempt with full details for audit trail
254
+ * Logs both successful and failed API calls for troubleshooting
255
+ *
256
+ * @param {string} url - API endpoint URL
257
+ * @param {Object} options - Fetch options (method, headers, etc.)
258
+ * @param {number} statusCode - HTTP status code
259
+ * @param {number} duration - Request duration in milliseconds
260
+ * @param {boolean} success - Whether the request was successful
261
+ * @param {Object} errorInfo - Error information (if failed)
262
+ */
263
+ async function logApiCall(url, options, statusCode, duration, success, errorInfo = {}) {
264
+ const method = options.method || 'GET';
265
+ const path = extractPathFromUrl(url);
266
+
267
+ // Extract controller URL from full URL
268
+ let controllerUrl = 'unknown';
269
+ try {
270
+ const urlObj = new URL(url);
271
+ controllerUrl = `${urlObj.protocol}//${urlObj.host}`;
272
+ } catch {
273
+ // If URL parsing fails, use the original URL
274
+ controllerUrl = url;
275
+ }
276
+
277
+ const metadata = {
278
+ action: 'api_call',
279
+ method,
280
+ path,
281
+ url: maskSensitiveData(url),
282
+ controllerUrl: maskSensitiveData(controllerUrl),
283
+ statusCode,
284
+ duration,
285
+ success,
286
+ timestamp: Date.now()
287
+ };
288
+
289
+ // Add error details if request failed
290
+ if (!success) {
291
+ metadata.errorType = errorInfo.errorType || 'unknown';
292
+ metadata.errorMessage = errorInfo.errorMessage || 'Unknown error';
293
+
294
+ // Include correlation ID if available
295
+ if (errorInfo.correlationId) {
296
+ metadata.correlationId = errorInfo.correlationId;
297
+ }
298
+
299
+ // Include error data (masked) if available
300
+ if (errorInfo.errorData) {
301
+ const errorDataStr = typeof errorInfo.errorData === 'string'
302
+ ? errorInfo.errorData
303
+ : JSON.stringify(errorInfo.errorData);
304
+ metadata.errorData = maskSensitiveData(errorDataStr);
305
+ }
306
+ }
307
+
308
+ // Log as ERROR for failed requests, INFO for successful ones
309
+ const level = success ? 'INFO' : 'ERROR';
310
+ const message = success
311
+ ? `API call succeeded: ${method} ${path}`
312
+ : `API call failed: ${method} ${path} (${statusCode})`;
313
+
314
+ await auditLog(level, message, metadata);
315
+ }
316
+
317
+ /**
318
+ * Extracts path and query string from full URL
319
+ * @param {string} url - Full URL
320
+ * @returns {string} Path with query string
321
+ */
322
+ function extractPathFromUrl(url) {
323
+ try {
324
+ const urlObj = new URL(url);
325
+ return urlObj.pathname + urlObj.search;
326
+ } catch {
327
+ // If URL parsing fails, try to extract path manually
328
+ const match = url.match(/https?:\/\/[^/]+(\/.*)/);
329
+ return match ? match[1] : url;
330
+ }
331
+ }
332
+
155
333
  module.exports = {
156
334
  auditLog,
157
335
  logDeploymentAttempt,
158
336
  logDeploymentSuccess,
159
337
  logDeploymentFailure,
160
338
  logSecurityEvent,
339
+ logApplicationCreation,
340
+ logApiCall,
161
341
  maskSensitiveData,
162
342
  createAuditEntry
163
343
  };