@aifabrix/builder 2.0.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/secrets.js ADDED
@@ -0,0 +1,282 @@
1
+ /**
2
+ * AI Fabrix Builder Secrets Management
3
+ *
4
+ * This module handles secret resolution and environment file generation.
5
+ * Resolves kv:// references from secrets files and generates .env files.
6
+ *
7
+ * @fileoverview Secret resolution and environment management for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const yaml = require('js-yaml');
15
+ const os = require('os');
16
+
17
+ /**
18
+ * Loads environment configuration for docker/local context
19
+ * @returns {Object} Environment configuration
20
+ */
21
+ function loadEnvConfig() {
22
+ const envConfigPath = path.join(__dirname, 'schema', 'env-config.yaml');
23
+ const content = fs.readFileSync(envConfigPath, 'utf8');
24
+ return yaml.load(content);
25
+ }
26
+
27
+ /**
28
+ * Loads secrets from the specified file or default location
29
+ * Supports both user secrets (~/.aifabrix/secrets.yaml) and project overrides
30
+ *
31
+ * @async
32
+ * @function loadSecrets
33
+ * @param {string} [secretsPath] - Path to secrets file (optional)
34
+ * @returns {Promise<Object>} Loaded secrets object
35
+ * @throws {Error} If secrets file cannot be loaded or parsed
36
+ *
37
+ * @example
38
+ * const secrets = await loadSecrets('../../secrets.local.yaml');
39
+ * // Returns: { 'postgres-passwordKeyVault': 'admin123', ... }
40
+ */
41
+ async function loadSecrets(secretsPath) {
42
+ let resolvedPath = secretsPath;
43
+
44
+ if (!resolvedPath) {
45
+ resolvedPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
46
+ } else if (secretsPath.startsWith('..')) {
47
+ resolvedPath = path.resolve(process.cwd(), secretsPath);
48
+ }
49
+
50
+ if (!fs.existsSync(resolvedPath)) {
51
+ throw new Error(`Secrets file not found: ${resolvedPath}`);
52
+ }
53
+
54
+ const content = fs.readFileSync(resolvedPath, 'utf8');
55
+ const secrets = yaml.load(content);
56
+
57
+ if (!secrets || typeof secrets !== 'object') {
58
+ throw new Error(`Invalid secrets file format: ${resolvedPath}`);
59
+ }
60
+
61
+ return secrets;
62
+ }
63
+
64
+ /**
65
+ * Resolves kv:// references in environment template
66
+ * Replaces kv://keyName with actual values from secrets
67
+ *
68
+ * @async
69
+ * @function resolveKvReferences
70
+ * @param {string} envTemplate - Environment template content
71
+ * @param {Object} secrets - Secrets object from loadSecrets()
72
+ * @param {string} [environment='local'] - Environment context (docker/local)
73
+ * @returns {Promise<string>} Resolved environment content
74
+ * @throws {Error} If kv:// reference cannot be resolved
75
+ *
76
+ * @example
77
+ * const resolved = await resolveKvReferences(template, secrets, 'local');
78
+ * // Returns: 'DATABASE_URL=postgresql://user:pass@localhost:5432/db'
79
+ */
80
+ async function resolveKvReferences(envTemplate, secrets, environment = 'local') {
81
+ const envConfig = loadEnvConfig();
82
+ const envVars = envConfig.environments[environment] || envConfig.environments.local;
83
+
84
+ let resolved = envTemplate;
85
+ const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
86
+ const missingSecrets = [];
87
+
88
+ let match;
89
+ while ((match = kvPattern.exec(envTemplate)) !== null) {
90
+ const secretKey = match[1];
91
+ if (!(secretKey in secrets)) {
92
+ missingSecrets.push(`kv://${secretKey}`);
93
+ }
94
+ }
95
+
96
+ if (missingSecrets.length > 0) {
97
+ throw new Error(`Missing secrets: ${missingSecrets.join(', ')}`);
98
+ }
99
+
100
+ resolved = resolved.replace(kvPattern, (match, secretKey) => {
101
+ let value = secrets[secretKey];
102
+ if (typeof value === 'string') {
103
+ value = value.replace(/\$\{([A-Z_]+)\}/g, (m, envVar) => {
104
+ return envVars[envVar] || m;
105
+ });
106
+ }
107
+ return value;
108
+ });
109
+
110
+ return resolved;
111
+ }
112
+
113
+ /**
114
+ * Generates .env file from template and secrets
115
+ * Creates environment file for local development
116
+ *
117
+ * @async
118
+ * @function generateEnvFile
119
+ * @param {string} appName - Name of the application
120
+ * @param {string} [secretsPath] - Path to secrets file (optional)
121
+ * @param {string} [environment='local'] - Environment context
122
+ * @returns {Promise<string>} Path to generated .env file
123
+ * @throws {Error} If generation fails
124
+ *
125
+ * @example
126
+ * const envPath = await generateEnvFile('myapp', '../../secrets.local.yaml');
127
+ * // Returns: './builder/myapp/.env'
128
+ */
129
+ async function generateEnvFile(appName, secretsPath, environment = 'local') {
130
+ const builderPath = path.join(process.cwd(), 'builder', appName);
131
+ const templatePath = path.join(builderPath, 'env.template');
132
+ const variablesPath = path.join(builderPath, 'variables.yaml');
133
+ const envPath = path.join(builderPath, '.env');
134
+
135
+ if (!fs.existsSync(templatePath)) {
136
+ throw new Error(`env.template not found: ${templatePath}`);
137
+ }
138
+
139
+ const template = fs.readFileSync(templatePath, 'utf8');
140
+ const secrets = await loadSecrets(secretsPath);
141
+ const resolved = await resolveKvReferences(template, secrets, environment);
142
+
143
+ fs.writeFileSync(envPath, resolved, { mode: 0o600 });
144
+
145
+ if (fs.existsSync(variablesPath)) {
146
+ const variablesContent = fs.readFileSync(variablesPath, 'utf8');
147
+ const variables = yaml.load(variablesContent);
148
+
149
+ if (variables?.build?.envOutputPath) {
150
+ const outputPath = path.resolve(builderPath, variables.build.envOutputPath);
151
+ const outputDir = path.dirname(outputPath);
152
+
153
+ if (!fs.existsSync(outputDir)) {
154
+ fs.mkdirSync(outputDir, { recursive: true });
155
+ }
156
+
157
+ fs.copyFileSync(envPath, outputPath);
158
+ }
159
+ }
160
+
161
+ return envPath;
162
+ }
163
+
164
+ /**
165
+ * Generates admin secrets for infrastructure
166
+ * Creates ~/.aifabrix/admin-secrets.env with Postgres and Redis credentials
167
+ *
168
+ * @async
169
+ * @function generateAdminSecretsEnv
170
+ * @param {string} [secretsPath] - Path to secrets file (optional)
171
+ * @returns {Promise<string>} Path to generated admin-secrets.env file
172
+ * @throws {Error} If generation fails
173
+ *
174
+ * @example
175
+ * const adminEnvPath = await generateAdminSecretsEnv('../../secrets.local.yaml');
176
+ * // Returns: '~/.aifabrix/admin-secrets.env'
177
+ */
178
+ async function generateAdminSecretsEnv(secretsPath) {
179
+ const secrets = await loadSecrets(secretsPath);
180
+ const aifabrixDir = path.join(os.homedir(), '.aifabrix');
181
+ const adminEnvPath = path.join(aifabrixDir, 'admin-secrets.env');
182
+
183
+ if (!fs.existsSync(aifabrixDir)) {
184
+ fs.mkdirSync(aifabrixDir, { recursive: true, mode: 0o700 });
185
+ }
186
+
187
+ const postgresPassword = secrets['postgres-passwordKeyVault'] || '';
188
+
189
+ const adminSecrets = `# Infrastructure Admin Credentials
190
+ POSTGRES_PASSWORD=${postgresPassword}
191
+ PGADMIN_DEFAULT_EMAIL=admin@aifabrix.ai
192
+ PGADMIN_DEFAULT_PASSWORD=${postgresPassword}
193
+ REDIS_HOST=local:localhost:6379
194
+ REDIS_COMMANDER_USER=admin
195
+ REDIS_COMMANDER_PASSWORD=${postgresPassword}
196
+ `;
197
+
198
+ fs.writeFileSync(adminEnvPath, adminSecrets, { mode: 0o600 });
199
+ return adminEnvPath;
200
+ }
201
+
202
+ /**
203
+ * Validates that all required secrets are present
204
+ * Checks for missing kv:// references and provides helpful error messages
205
+ *
206
+ * @function validateSecrets
207
+ * @param {string} envTemplate - Environment template content
208
+ * @param {Object} secrets - Available secrets
209
+ * @returns {Object} Validation result with missing secrets
210
+ *
211
+ * @example
212
+ * const validation = validateSecrets(template, secrets);
213
+ * // Returns: { valid: false, missing: ['kv://missing-secret'] }
214
+ */
215
+ function validateSecrets(envTemplate, secrets) {
216
+ const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
217
+ const missing = [];
218
+
219
+ let match;
220
+ while ((match = kvPattern.exec(envTemplate)) !== null) {
221
+ const secretKey = match[1];
222
+ if (!(secretKey in secrets)) {
223
+ missing.push(`kv://${secretKey}`);
224
+ }
225
+ }
226
+
227
+ return {
228
+ valid: missing.length === 0,
229
+ missing
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Creates default secrets file if it doesn't exist
235
+ * Generates template with common secrets for local development
236
+ *
237
+ * @async
238
+ * @function createDefaultSecrets
239
+ * @param {string} secretsPath - Path where to create secrets file
240
+ * @returns {Promise<void>} Resolves when file is created
241
+ * @throws {Error} If file creation fails
242
+ *
243
+ * @example
244
+ * await createDefaultSecrets('~/.aifabrix/secrets.yaml');
245
+ * // Default secrets file is created
246
+ */
247
+ async function createDefaultSecrets(secretsPath) {
248
+ const resolvedPath = secretsPath.startsWith('~')
249
+ ? path.join(os.homedir(), secretsPath.slice(1))
250
+ : secretsPath;
251
+
252
+ const dir = path.dirname(resolvedPath);
253
+ if (!fs.existsSync(dir)) {
254
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
255
+ }
256
+
257
+ const defaultSecrets = `# Local Development Secrets
258
+ # Production uses Azure KeyVault
259
+
260
+ # Database Secrets
261
+ postgres-passwordKeyVault: "admin123"
262
+
263
+ # Redis Secrets
264
+ redis-passwordKeyVault: ""
265
+ redis-urlKeyVault: "redis://\${REDIS_HOST}:6379"
266
+
267
+ # Keycloak Secrets
268
+ keycloak-admin-passwordKeyVault: "admin123"
269
+ keycloak-auth-server-urlKeyVault: "http://\${KEYCLOAK_HOST}:8082"
270
+ `;
271
+
272
+ fs.writeFileSync(resolvedPath, defaultSecrets, { mode: 0o600 });
273
+ }
274
+
275
+ module.exports = {
276
+ loadSecrets,
277
+ resolveKvReferences,
278
+ generateEnvFile,
279
+ generateAdminSecretsEnv,
280
+ validateSecrets,
281
+ createDefaultSecrets
282
+ };
@@ -0,0 +1,301 @@
1
+ /**
2
+ * YAML Template Generation Module
3
+ *
4
+ * Generates configuration files for AI Fabrix applications
5
+ * following ISO 27001 security standards
6
+ */
7
+
8
+ const yaml = require('js-yaml');
9
+
10
+ /**
11
+ * Generate variables.yaml content for an application
12
+ * @param {string} appName - Application name
13
+ * @param {Object} config - Configuration options
14
+ * @returns {string} YAML content
15
+ */
16
+ function generateVariablesYaml(appName, config) {
17
+ const variables = {
18
+ app: {
19
+ key: appName,
20
+ name: appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
21
+ description: `${appName.replace(/-/g, ' ')} application`,
22
+ version: '1.0.0'
23
+ },
24
+ build: {
25
+ language: config.language || 'typescript',
26
+ port: parseInt(config.port, 10) || 3000,
27
+ environment: 'development'
28
+ },
29
+ services: {
30
+ database: config.database || false,
31
+ redis: config.redis || false,
32
+ storage: config.storage || false,
33
+ authentication: config.authentication || false
34
+ },
35
+ security: {
36
+ enableRBAC: config.authentication || false,
37
+ requireAuth: config.authentication || false,
38
+ auditLogging: true
39
+ },
40
+ monitoring: {
41
+ healthCheck: true,
42
+ metrics: true,
43
+ logging: true
44
+ }
45
+ };
46
+
47
+ return yaml.dump(variables, {
48
+ indent: 2,
49
+ lineWidth: 120,
50
+ noRefs: true,
51
+ sortKeys: false
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Generate env.template content with conditional variables
57
+ * @param {Object} config - Configuration options
58
+ * @param {Object} existingEnv - Existing environment variables
59
+ * @returns {string} Environment template content
60
+ */
61
+ function generateEnvTemplate(config, existingEnv = {}) {
62
+ const envVars = {
63
+ // Core application settings
64
+ 'NODE_ENV': 'development',
65
+ 'PORT': config.port || 3000,
66
+ 'APP_NAME': config.appName || 'myapp',
67
+ 'LOG_LEVEL': 'info'
68
+ };
69
+
70
+ // Add database variables if enabled
71
+ if (config.database) {
72
+ envVars['DATABASE_URL'] = 'kv://database-url';
73
+ envVars['DB_HOST'] = 'localhost';
74
+ envVars['DB_PORT'] = '5432';
75
+ envVars['DB_NAME'] = config.appName || 'myapp';
76
+ envVars['DB_USER'] = 'kv://database-user';
77
+ envVars['DB_PASSWORD'] = 'kv://database-password';
78
+ }
79
+
80
+ // Add Redis variables if enabled
81
+ if (config.redis) {
82
+ envVars['REDIS_URL'] = 'kv://redis-url';
83
+ envVars['REDIS_HOST'] = 'localhost';
84
+ envVars['REDIS_PORT'] = '6379';
85
+ envVars['REDIS_PASSWORD'] = 'kv://redis-password';
86
+ }
87
+
88
+ // Add storage variables if enabled
89
+ if (config.storage) {
90
+ envVars['STORAGE_TYPE'] = 'local';
91
+ envVars['STORAGE_PATH'] = '/app/storage';
92
+ envVars['STORAGE_URL'] = 'kv://storage-url';
93
+ envVars['STORAGE_KEY'] = 'kv://storage-key';
94
+ envVars['STORAGE_SECRET'] = 'kv://storage-secret';
95
+ }
96
+
97
+ // Add authentication variables if enabled
98
+ if (config.authentication) {
99
+ envVars['JWT_SECRET'] = 'kv://jwt-secret';
100
+ envVars['JWT_EXPIRES_IN'] = '24h';
101
+ envVars['AUTH_PROVIDER'] = 'local';
102
+ envVars['SESSION_SECRET'] = 'kv://session-secret';
103
+ }
104
+
105
+ // Merge with existing environment variables
106
+ Object.assign(envVars, existingEnv);
107
+
108
+ // Generate template content
109
+ const lines = [
110
+ '# AI Fabrix Environment Template',
111
+ '# Copy this file to .env and fill in the actual values',
112
+ '# Values marked with kv:// are secrets stored in Azure Key Vault',
113
+ '',
114
+ '# Core Application Settings',
115
+ ''
116
+ ];
117
+
118
+ // Add core variables
119
+ Object.entries(envVars).forEach(([key, value]) => {
120
+ if (key.startsWith('NODE_ENV') || key.startsWith('PORT') ||
121
+ key.startsWith('APP_NAME') || key.startsWith('LOG_LEVEL')) {
122
+ lines.push(`${key}=${value}`);
123
+ }
124
+ });
125
+
126
+ // Add service-specific sections
127
+ if (config.database) {
128
+ lines.push('', '# Database Configuration', '');
129
+ Object.entries(envVars).forEach(([key, value]) => {
130
+ if (key.startsWith('DB_') || key.startsWith('DATABASE_')) {
131
+ lines.push(`${key}=${value}`);
132
+ }
133
+ });
134
+ }
135
+
136
+ if (config.redis) {
137
+ lines.push('', '# Redis Configuration', '');
138
+ Object.entries(envVars).forEach(([key, value]) => {
139
+ if (key.startsWith('REDIS_')) {
140
+ lines.push(`${key}=${value}`);
141
+ }
142
+ });
143
+ }
144
+
145
+ if (config.storage) {
146
+ lines.push('', '# Storage Configuration', '');
147
+ Object.entries(envVars).forEach(([key, value]) => {
148
+ if (key.startsWith('STORAGE_')) {
149
+ lines.push(`${key}=${value}`);
150
+ }
151
+ });
152
+ }
153
+
154
+ if (config.authentication) {
155
+ lines.push('', '# Authentication Configuration', '');
156
+ Object.entries(envVars).forEach(([key, value]) => {
157
+ if (key.startsWith('JWT_') || key.startsWith('AUTH_') || key.startsWith('SESSION_')) {
158
+ lines.push(`${key}=${value}`);
159
+ }
160
+ });
161
+ }
162
+
163
+ return lines.join('\n');
164
+ }
165
+
166
+ /**
167
+ * Generate rbac.yaml content for RBAC configuration
168
+ * @param {string} appName - Application name
169
+ * @param {Object} config - Configuration options
170
+ * @returns {string} RBAC YAML content
171
+ */
172
+ function generateRbacYaml(appName, config) {
173
+ if (!config.authentication) {
174
+ return null;
175
+ }
176
+
177
+ const rbac = {
178
+ apiVersion: 'v1',
179
+ kind: 'RBACConfig',
180
+ metadata: {
181
+ name: `${appName}-rbac`,
182
+ namespace: 'default'
183
+ },
184
+ spec: {
185
+ roles: [
186
+ {
187
+ name: 'admin',
188
+ description: 'Full administrative access',
189
+ permissions: ['*']
190
+ },
191
+ {
192
+ name: 'user',
193
+ description: 'Standard user access',
194
+ permissions: ['read', 'write']
195
+ },
196
+ {
197
+ name: 'viewer',
198
+ description: 'Read-only access',
199
+ permissions: ['read']
200
+ }
201
+ ],
202
+ policies: [
203
+ {
204
+ name: 'admin-policy',
205
+ role: 'admin',
206
+ resources: ['*'],
207
+ actions: ['*']
208
+ },
209
+ {
210
+ name: 'user-policy',
211
+ role: 'user',
212
+ resources: ['data', 'profile'],
213
+ actions: ['read', 'write']
214
+ },
215
+ {
216
+ name: 'viewer-policy',
217
+ role: 'viewer',
218
+ resources: ['data'],
219
+ actions: ['read']
220
+ }
221
+ ],
222
+ bindings: [
223
+ {
224
+ name: 'admin-binding',
225
+ role: 'admin',
226
+ subjects: [
227
+ {
228
+ kind: 'User',
229
+ name: 'admin@example.com'
230
+ }
231
+ ]
232
+ }
233
+ ]
234
+ }
235
+ };
236
+
237
+ return yaml.dump(rbac, {
238
+ indent: 2,
239
+ lineWidth: 120,
240
+ noRefs: true,
241
+ sortKeys: false
242
+ });
243
+ }
244
+
245
+ /**
246
+ * Generate secrets.yaml content for sensitive values
247
+ * @param {Object} config - Configuration options
248
+ * @param {Object} existingSecrets - Existing secrets from .env
249
+ * @returns {string} Secrets YAML content
250
+ */
251
+ function generateSecretsYaml(config, existingSecrets = {}) {
252
+ const secrets = {
253
+ apiVersion: 'v1',
254
+ kind: 'Secret',
255
+ metadata: {
256
+ name: 'app-secrets',
257
+ namespace: 'default'
258
+ },
259
+ type: 'Opaque',
260
+ data: {}
261
+ };
262
+
263
+ // Add secrets based on enabled services
264
+ if (config.database) {
265
+ secrets.data['database-password'] = 'base64-encoded-password';
266
+ secrets.data['database-user'] = 'base64-encoded-user';
267
+ }
268
+
269
+ if (config.redis) {
270
+ secrets.data['redis-password'] = 'base64-encoded-redis-password';
271
+ }
272
+
273
+ if (config.storage) {
274
+ secrets.data['storage-key'] = 'base64-encoded-storage-key';
275
+ secrets.data['storage-secret'] = 'base64-encoded-storage-secret';
276
+ }
277
+
278
+ if (config.authentication) {
279
+ secrets.data['jwt-secret'] = 'base64-encoded-jwt-secret';
280
+ secrets.data['session-secret'] = 'base64-encoded-session-secret';
281
+ }
282
+
283
+ // Add existing secrets
284
+ Object.entries(existingSecrets).forEach(([key, value]) => {
285
+ secrets.data[key] = Buffer.from(value).toString('base64');
286
+ });
287
+
288
+ return yaml.dump(secrets, {
289
+ indent: 2,
290
+ lineWidth: 120,
291
+ noRefs: true,
292
+ sortKeys: false
293
+ });
294
+ }
295
+
296
+ module.exports = {
297
+ generateVariablesYaml,
298
+ generateEnvTemplate,
299
+ generateRbacYaml,
300
+ generateSecretsYaml
301
+ };