@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.
- package/lib/app-deploy.js +73 -29
- package/lib/app-list.js +132 -0
- package/lib/app-readme.js +11 -4
- package/lib/app-register.js +435 -0
- package/lib/app-rotate-secret.js +164 -0
- package/lib/app-run.js +98 -84
- package/lib/app.js +13 -0
- package/lib/audit-logger.js +195 -15
- package/lib/build.js +57 -37
- package/lib/cli.js +90 -8
- package/lib/commands/app.js +8 -391
- package/lib/commands/login.js +130 -36
- package/lib/config.js +257 -4
- package/lib/deployer.js +221 -183
- package/lib/infra.js +177 -112
- package/lib/secrets.js +85 -99
- package/lib/utils/api-error-handler.js +465 -0
- package/lib/utils/api.js +165 -16
- package/lib/utils/auth-headers.js +84 -0
- package/lib/utils/build-copy.js +144 -0
- package/lib/utils/cli-utils.js +21 -0
- package/lib/utils/compose-generator.js +43 -14
- package/lib/utils/deployment-errors.js +90 -0
- package/lib/utils/deployment-validation.js +60 -0
- package/lib/utils/dev-config.js +83 -0
- package/lib/utils/env-template.js +30 -10
- package/lib/utils/health-check.js +18 -1
- package/lib/utils/infra-containers.js +101 -0
- package/lib/utils/local-secrets.js +0 -2
- package/lib/utils/secrets-path.js +18 -21
- package/lib/utils/secrets-utils.js +206 -0
- package/lib/utils/token-manager.js +381 -0
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +155 -23
- package/templates/applications/miso-controller/Dockerfile +7 -119
- package/templates/infra/compose.yaml.hbs +93 -0
- package/templates/python/docker-compose.hbs +25 -17
- package/templates/typescript/docker-compose.hbs +25 -17
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
|
-
|
|
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() ===
|
|
76
|
+
const isRunning = stdout.trim() === containerName;
|
|
73
77
|
if (debug) {
|
|
74
|
-
logger.log(chalk.gray(`[DEBUG] Container
|
|
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
|
|
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
|
|
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
|
-
|
|
102
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
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}
|
|
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,
|
|
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
|
-
|
|
276
|
-
|
|
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(
|
|
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
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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=${
|
|
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,
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
//
|
|
434
|
-
//
|
|
435
|
-
const
|
|
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]
|
|
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(
|
|
454
|
+
const portAvailable = await checkPortAvailable(hostPort);
|
|
440
455
|
if (debug) {
|
|
441
|
-
logger.log(chalk.gray(`[DEBUG] Port ${
|
|
456
|
+
logger.log(chalk.gray(`[DEBUG] Port ${hostPort} available: ${portAvailable}`));
|
|
442
457
|
}
|
|
443
458
|
if (!portAvailable) {
|
|
444
|
-
throw new Error(`Port ${
|
|
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,
|
|
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,
|
|
470
|
+
await startContainer(appName, tempComposePath, hostPort, appConfig, debug);
|
|
456
471
|
|
|
457
472
|
// Display success message
|
|
458
|
-
displayRunStatus(appName,
|
|
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
|
}
|
package/lib/audit-logger.js
CHANGED
|
@@ -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
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|