@tamyla/clodo-framework 1.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/CHANGELOG.md +564 -0
- package/LICENSE +21 -0
- package/README.md +1393 -0
- package/bin/README.md +71 -0
- package/bin/clodo-service.js +416 -0
- package/bin/security/security-cli.js +96 -0
- package/bin/service-management/README.md +74 -0
- package/bin/service-management/create-service.js +129 -0
- package/bin/service-management/init-service.js +102 -0
- package/bin/service-management/init-service.js.backup +889 -0
- package/bin/shared/config/customer-cli.js +293 -0
- package/dist/config/ConfigurationManager.js +159 -0
- package/dist/config/CustomerConfigCLI.js +220 -0
- package/dist/config/FeatureManager.js +426 -0
- package/dist/config/customers.js +441 -0
- package/dist/config/domains.js +180 -0
- package/dist/config/features.js +225 -0
- package/dist/config/index.js +6 -0
- package/dist/database/database-orchestrator.js +730 -0
- package/dist/database/index.js +4 -0
- package/dist/deployment/auditor.js +971 -0
- package/dist/deployment/index.js +10 -0
- package/dist/deployment/rollback-manager.js +523 -0
- package/dist/deployment/testers/api-tester.js +80 -0
- package/dist/deployment/testers/auth-tester.js +129 -0
- package/dist/deployment/testers/core.js +217 -0
- package/dist/deployment/testers/database-tester.js +105 -0
- package/dist/deployment/testers/index.js +74 -0
- package/dist/deployment/testers/load-tester.js +120 -0
- package/dist/deployment/testers/performance-tester.js +105 -0
- package/dist/deployment/validator.js +558 -0
- package/dist/deployment/wrangler-deployer.js +574 -0
- package/dist/handlers/GenericRouteHandler.js +532 -0
- package/dist/index.js +39 -0
- package/dist/migration/MigrationAdapters.js +562 -0
- package/dist/modules/ModuleManager.js +668 -0
- package/dist/modules/security.js +98 -0
- package/dist/orchestration/cross-domain-coordinator.js +1083 -0
- package/dist/orchestration/index.js +5 -0
- package/dist/orchestration/modules/DeploymentCoordinator.js +258 -0
- package/dist/orchestration/modules/DomainResolver.js +196 -0
- package/dist/orchestration/modules/StateManager.js +332 -0
- package/dist/orchestration/multi-domain-orchestrator.js +255 -0
- package/dist/routing/EnhancedRouter.js +158 -0
- package/dist/schema/SchemaManager.js +778 -0
- package/dist/security/ConfigurationValidator.js +490 -0
- package/dist/security/DeploymentManager.js +208 -0
- package/dist/security/SecretGenerator.js +142 -0
- package/dist/security/SecurityCLI.js +228 -0
- package/dist/security/index.js +51 -0
- package/dist/security/patterns/environment-rules.js +66 -0
- package/dist/security/patterns/insecure-patterns.js +21 -0
- package/dist/service-management/ConfirmationEngine.js +411 -0
- package/dist/service-management/ErrorTracker.js +294 -0
- package/dist/service-management/GenerationEngine.js +3109 -0
- package/dist/service-management/InputCollector.js +237 -0
- package/dist/service-management/ServiceCreator.js +229 -0
- package/dist/service-management/ServiceInitializer.js +448 -0
- package/dist/service-management/ServiceOrchestrator.js +638 -0
- package/dist/service-management/handlers/ConfigMutator.js +130 -0
- package/dist/service-management/handlers/ConfirmationHandler.js +71 -0
- package/dist/service-management/handlers/GenerationHandler.js +80 -0
- package/dist/service-management/handlers/InputHandler.js +59 -0
- package/dist/service-management/handlers/ValidationHandler.js +203 -0
- package/dist/service-management/index.js +7 -0
- package/dist/services/GenericDataService.js +488 -0
- package/dist/shared/cloudflare/domain-discovery.js +562 -0
- package/dist/shared/cloudflare/domain-manager.js +912 -0
- package/dist/shared/cloudflare/index.js +8 -0
- package/dist/shared/cloudflare/ops.js +387 -0
- package/dist/shared/config/cache.js +1167 -0
- package/dist/shared/config/command-config-manager.js +174 -0
- package/dist/shared/config/customer-cli.js +258 -0
- package/dist/shared/config/index.js +9 -0
- package/dist/shared/config/manager.js +289 -0
- package/dist/shared/database/connection-manager.js +338 -0
- package/dist/shared/database/index.js +7 -0
- package/dist/shared/database/orchestrator.js +632 -0
- package/dist/shared/deployment/auditor.js +971 -0
- package/dist/shared/deployment/index.js +10 -0
- package/dist/shared/deployment/rollback-manager.js +523 -0
- package/dist/shared/deployment/validator.js +558 -0
- package/dist/shared/index.js +32 -0
- package/dist/shared/monitoring/health-checker.js +250 -0
- package/dist/shared/monitoring/index.js +8 -0
- package/dist/shared/monitoring/memory-manager.js +382 -0
- package/dist/shared/monitoring/production-monitor.js +390 -0
- package/dist/shared/production-tester/api-tester.js +80 -0
- package/dist/shared/production-tester/auth-tester.js +129 -0
- package/dist/shared/production-tester/core.js +217 -0
- package/dist/shared/production-tester/database-tester.js +105 -0
- package/dist/shared/production-tester/index.js +74 -0
- package/dist/shared/production-tester/load-tester.js +120 -0
- package/dist/shared/production-tester/performance-tester.js +105 -0
- package/dist/shared/security/api-token-manager.js +296 -0
- package/dist/shared/security/index.js +8 -0
- package/dist/shared/security/secret-generator.js +918 -0
- package/dist/shared/security/secure-token-manager.js +379 -0
- package/dist/shared/utils/error-recovery.js +240 -0
- package/dist/shared/utils/graceful-shutdown-manager.js +380 -0
- package/dist/shared/utils/index.js +9 -0
- package/dist/shared/utils/interactive-prompts.js +134 -0
- package/dist/shared/utils/rate-limiter.js +249 -0
- package/dist/utils/ErrorHandler.js +173 -0
- package/dist/utils/deployment/config-cache.js +1160 -0
- package/dist/utils/deployment/index.js +6 -0
- package/dist/utils/deployment/interactive-prompts.js +97 -0
- package/dist/utils/deployment/secret-generator.js +896 -0
- package/dist/utils/dirname-helper.js +35 -0
- package/dist/utils/domain-config.js +159 -0
- package/dist/utils/error-recovery.js +240 -0
- package/dist/utils/esm-helper.js +52 -0
- package/dist/utils/framework-config.js +481 -0
- package/dist/utils/graceful-shutdown-manager.js +379 -0
- package/dist/utils/health-checker.js +114 -0
- package/dist/utils/index.js +36 -0
- package/dist/utils/prompt-handler.js +98 -0
- package/dist/utils/usage-tracker.js +252 -0
- package/dist/utils/validation.js +112 -0
- package/dist/version/VersionDetector.js +723 -0
- package/dist/worker/index.js +4 -0
- package/dist/worker/integration.js +332 -0
- package/docs/FRAMEWORK-ARCHITECTURE-OVERVIEW.md +206 -0
- package/docs/INTEGRATION_GUIDE.md +2045 -0
- package/docs/README.md +82 -0
- package/docs/SECURITY.md +242 -0
- package/docs/deployment/deployment-guide.md +540 -0
- package/docs/overview.md +280 -0
- package/package.json +176 -0
- package/types/index.d.ts +575 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
// eslint-disable-next-line no-unused-vars
|
|
3
|
+
import { resolve, join } from 'path';
|
|
4
|
+
import { createDomainConfigSchema, validateDomainConfig, createDomainRegistry } from './domains.js';
|
|
5
|
+
import { createLogger } from '../utils/index.js';
|
|
6
|
+
import { getDirname } from '../utils/esm-helper.js';
|
|
7
|
+
const __dirname = getDirname(import.meta.url, 'src/config');
|
|
8
|
+
const logger = createLogger('CustomerConfig');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Customer Configuration Manager
|
|
12
|
+
* Manages multi-environment, multi-customer configuration structure
|
|
13
|
+
* Integrates with existing domain and feature flag systems
|
|
14
|
+
*/
|
|
15
|
+
export class CustomerConfigurationManager {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.configDir = options.configDir || resolve(__dirname, '..', '..', 'config');
|
|
18
|
+
this.environments = options.environments || ['development', 'staging', 'production'];
|
|
19
|
+
this.domainRegistry = createDomainRegistry({});
|
|
20
|
+
this.customers = new Map();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a new customer configuration from template
|
|
25
|
+
* @param {string} customerName - Customer identifier
|
|
26
|
+
* @param {string} domain - Customer domain (optional)
|
|
27
|
+
* @param {Object} options - Additional customer options
|
|
28
|
+
*/
|
|
29
|
+
async createCustomer(customerName, domain = null, options = {}) {
|
|
30
|
+
logger.info(`Creating customer configuration: ${customerName}`);
|
|
31
|
+
const customerDir = resolve(this.configDir, 'customers', customerName);
|
|
32
|
+
|
|
33
|
+
// Create customer directory structure
|
|
34
|
+
if (!existsSync(customerDir)) {
|
|
35
|
+
mkdirSync(customerDir, {
|
|
36
|
+
recursive: true
|
|
37
|
+
});
|
|
38
|
+
logger.info(`Created customer directory: ${customerDir}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Create domain configuration for customer
|
|
42
|
+
const domainConfig = this.createCustomerDomainConfig(customerName, domain, options);
|
|
43
|
+
this.domainRegistry.add(customerName, domainConfig);
|
|
44
|
+
|
|
45
|
+
// Create environment-specific configs from templates
|
|
46
|
+
for (const env of this.environments) {
|
|
47
|
+
await this.createCustomerEnvironment(customerName, env, domain, options);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Store customer metadata
|
|
51
|
+
this.customers.set(customerName, {
|
|
52
|
+
name: customerName,
|
|
53
|
+
domain: domain,
|
|
54
|
+
createdAt: new Date().toISOString(),
|
|
55
|
+
environments: this.environments,
|
|
56
|
+
...options
|
|
57
|
+
});
|
|
58
|
+
logger.info(`Customer ${customerName} configuration created successfully`);
|
|
59
|
+
return this.getCustomerInfo(customerName);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create domain configuration for customer
|
|
64
|
+
*/
|
|
65
|
+
createCustomerDomainConfig(customerName, domain, options) {
|
|
66
|
+
const baseConfig = createDomainConfigSchema();
|
|
67
|
+
|
|
68
|
+
// Generate customer-specific domain config
|
|
69
|
+
const domainConfig = {
|
|
70
|
+
...baseConfig,
|
|
71
|
+
name: customerName,
|
|
72
|
+
displayName: options.displayName || this.capitalizeFirst(customerName),
|
|
73
|
+
accountId: options.accountId || '00000000000000000000000000000000',
|
|
74
|
+
// Placeholder for onboarding
|
|
75
|
+
zoneId: options.zoneId || '00000000000000000000000000000000',
|
|
76
|
+
// Placeholder for onboarding
|
|
77
|
+
|
|
78
|
+
domains: {
|
|
79
|
+
production: domain || `${customerName}.com`,
|
|
80
|
+
staging: `staging.${domain || `${customerName}.com`}`,
|
|
81
|
+
development: `dev.${domain || `${customerName}.com`}`
|
|
82
|
+
},
|
|
83
|
+
features: {
|
|
84
|
+
// Customer-specific feature flags
|
|
85
|
+
customerIsolation: true,
|
|
86
|
+
customDomain: !!domain,
|
|
87
|
+
multiEnvironment: true,
|
|
88
|
+
...options.features
|
|
89
|
+
},
|
|
90
|
+
settings: {
|
|
91
|
+
...baseConfig.settings,
|
|
92
|
+
customerName: customerName,
|
|
93
|
+
customerDomain: domain,
|
|
94
|
+
...options.settings
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Skip validation during initial creation if using placeholders
|
|
99
|
+
if (options.skipValidation || !options.accountId || !options.zoneId) {
|
|
100
|
+
logger.info(`Created customer domain config for ${customerName} (framework mode - placeholders used)`);
|
|
101
|
+
return domainConfig;
|
|
102
|
+
}
|
|
103
|
+
validateDomainConfig(domainConfig);
|
|
104
|
+
return domainConfig;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create customer environment config from template
|
|
109
|
+
*/
|
|
110
|
+
// eslint-disable-next-line no-unused-vars
|
|
111
|
+
async createCustomerEnvironment(customerName, environment, domain, options) {
|
|
112
|
+
const templatePath = resolve(this.configDir, 'customers', 'template', `${environment}.env.template`);
|
|
113
|
+
const outputPath = resolve(this.configDir, 'customers', customerName, `${environment}.env`);
|
|
114
|
+
|
|
115
|
+
// Check if template exists
|
|
116
|
+
if (!existsSync(templatePath)) {
|
|
117
|
+
logger.warn(`Template not found: ${templatePath}, skipping ${environment} config`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Skip if config already exists
|
|
122
|
+
if (existsSync(outputPath)) {
|
|
123
|
+
logger.info(`Config already exists: ${outputPath}, skipping`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
// Read template and replace placeholders
|
|
128
|
+
let template = readFileSync(templatePath, 'utf8');
|
|
129
|
+
|
|
130
|
+
// Replace customer-specific placeholders
|
|
131
|
+
template = template.replace(/\{\{CUSTOMER_NAME\}\}/g, customerName);
|
|
132
|
+
template = template.replace(/\{\{CUSTOMER_DOMAIN\}\}/g, domain || `${customerName}.com`);
|
|
133
|
+
template = template.replace(/\{\{ENVIRONMENT\}\}/g, environment);
|
|
134
|
+
|
|
135
|
+
// Environment-specific domain replacements
|
|
136
|
+
const envDomains = this.getEnvironmentDomains(customerName, domain);
|
|
137
|
+
template = template.replace(/\{\{DOMAIN\}\}/g, envDomains[environment]);
|
|
138
|
+
|
|
139
|
+
// Write customer config
|
|
140
|
+
writeFileSync(outputPath, template);
|
|
141
|
+
logger.info(`Created customer config: ${environment}.env for ${customerName}`);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
logger.error(`Failed to create ${environment} config for ${customerName}:`, error.message);
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get environment-specific domains for customer
|
|
150
|
+
*/
|
|
151
|
+
getEnvironmentDomains(customerName, domain) {
|
|
152
|
+
const baseDomain = domain || `${customerName}.com`;
|
|
153
|
+
return {
|
|
154
|
+
production: baseDomain,
|
|
155
|
+
staging: `staging.${baseDomain}`,
|
|
156
|
+
development: `dev.${baseDomain}`
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Validate customer configurations
|
|
162
|
+
*/
|
|
163
|
+
async validateConfigs() {
|
|
164
|
+
logger.info('Validating customer configuration structure...');
|
|
165
|
+
let valid = true;
|
|
166
|
+
const errors = [];
|
|
167
|
+
|
|
168
|
+
// Validate base configuration files
|
|
169
|
+
const baseFiles = ['wrangler.base.toml', 'variables.base.env'];
|
|
170
|
+
for (const file of baseFiles) {
|
|
171
|
+
const path = resolve(this.configDir, 'base', file);
|
|
172
|
+
if (!existsSync(path)) {
|
|
173
|
+
errors.push(`Missing base config: ${file}`);
|
|
174
|
+
valid = false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Validate environment configs
|
|
179
|
+
for (const env of this.environments) {
|
|
180
|
+
const envFile = `${env}.toml`;
|
|
181
|
+
const path = resolve(this.configDir, 'environments', envFile);
|
|
182
|
+
if (!existsSync(path)) {
|
|
183
|
+
errors.push(`Missing environment config: ${envFile}`);
|
|
184
|
+
valid = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Validate customer configs
|
|
189
|
+
const customersDir = resolve(this.configDir, 'customers');
|
|
190
|
+
if (existsSync(customersDir)) {
|
|
191
|
+
const customerDirs = this.getCustomerDirectories();
|
|
192
|
+
for (const customerName of customerDirs) {
|
|
193
|
+
const customerValid = await this.validateCustomerConfig(customerName);
|
|
194
|
+
if (!customerValid) {
|
|
195
|
+
valid = false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (errors.length > 0) {
|
|
200
|
+
logger.error('Configuration validation errors:', errors);
|
|
201
|
+
}
|
|
202
|
+
logger.info(`Configuration validation ${valid ? 'passed' : 'failed'}`);
|
|
203
|
+
return {
|
|
204
|
+
valid,
|
|
205
|
+
errors
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Validate individual customer configuration
|
|
211
|
+
*/
|
|
212
|
+
async validateCustomerConfig(customerName) {
|
|
213
|
+
const customerDir = resolve(this.configDir, 'customers', customerName);
|
|
214
|
+
let valid = true;
|
|
215
|
+
|
|
216
|
+
// Check environment files exist
|
|
217
|
+
for (const env of this.environments) {
|
|
218
|
+
const envFile = resolve(customerDir, `${env}.env`);
|
|
219
|
+
if (!existsSync(envFile)) {
|
|
220
|
+
logger.error(`Missing ${env}.env for customer ${customerName}`);
|
|
221
|
+
valid = false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Validate domain configuration (skip in framework mode)
|
|
226
|
+
try {
|
|
227
|
+
const domainConfig = this.domainRegistry.get(customerName);
|
|
228
|
+
if (!domainConfig.settings?.isFrameworkMode) {
|
|
229
|
+
validateDomainConfig(domainConfig);
|
|
230
|
+
} else {
|
|
231
|
+
logger.info(`Skipping domain validation for ${customerName} (framework mode)`);
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
logger.error(`Invalid domain config for ${customerName}:`, error.message);
|
|
235
|
+
valid = false;
|
|
236
|
+
}
|
|
237
|
+
return valid;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Show effective configuration for customer/environment
|
|
242
|
+
*/
|
|
243
|
+
showConfig(customerName, environment) {
|
|
244
|
+
logger.info(`Showing effective configuration: ${customerName}/${environment}`);
|
|
245
|
+
|
|
246
|
+
// Get domain config from registry
|
|
247
|
+
let domainConfig;
|
|
248
|
+
try {
|
|
249
|
+
domainConfig = this.domainRegistry.get(customerName);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
// If not in registry, try to find customer metadata
|
|
252
|
+
const customerMeta = this.customers.get(customerName);
|
|
253
|
+
if (customerMeta) {
|
|
254
|
+
// Recreate domain config from metadata
|
|
255
|
+
domainConfig = this.createCustomerDomainConfig(customerName, customerMeta.domain, customerMeta);
|
|
256
|
+
} else {
|
|
257
|
+
throw new Error(`Customer not found: ${customerName}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const config = {
|
|
261
|
+
customer: customerName,
|
|
262
|
+
environment: environment,
|
|
263
|
+
domain: domainConfig,
|
|
264
|
+
variables: {},
|
|
265
|
+
features: {}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// Load base variables
|
|
269
|
+
const baseVarsPath = resolve(this.configDir, 'base', 'variables.base.env');
|
|
270
|
+
if (existsSync(baseVarsPath)) {
|
|
271
|
+
config.variables.base = this.parseEnvFile(baseVarsPath);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Load customer environment variables
|
|
275
|
+
const customerConfigPath = resolve(this.configDir, 'customers', customerName, `${environment}.env`);
|
|
276
|
+
if (existsSync(customerConfigPath)) {
|
|
277
|
+
config.variables.customer = this.parseEnvFile(customerConfigPath);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Get domain features
|
|
281
|
+
config.features = domainConfig.features || {};
|
|
282
|
+
return config;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Generate deployment command for customer/environment
|
|
287
|
+
*/
|
|
288
|
+
getDeployCommand(customerName, environment) {
|
|
289
|
+
const commands = {
|
|
290
|
+
development: `wrangler dev --config config/environments/development.toml`,
|
|
291
|
+
staging: `wrangler deploy --config config/environments/staging.toml --env staging`,
|
|
292
|
+
production: `wrangler deploy --env production`
|
|
293
|
+
};
|
|
294
|
+
const command = commands[environment];
|
|
295
|
+
if (!command) {
|
|
296
|
+
throw new Error(`Unknown environment: ${environment}`);
|
|
297
|
+
}
|
|
298
|
+
logger.info(`Generated deploy command for ${customerName}/${environment}`);
|
|
299
|
+
return {
|
|
300
|
+
command: command,
|
|
301
|
+
customer: customerName,
|
|
302
|
+
environment: environment,
|
|
303
|
+
configPath: `config/customers/${customerName}/${environment}.env`
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Get customer information
|
|
309
|
+
*/
|
|
310
|
+
getCustomerInfo(customerName) {
|
|
311
|
+
const domainConfig = this.domainRegistry.get(customerName);
|
|
312
|
+
const customerMeta = this.customers.get(customerName);
|
|
313
|
+
return {
|
|
314
|
+
...customerMeta,
|
|
315
|
+
domainConfig: domainConfig,
|
|
316
|
+
configPath: resolve(this.configDir, 'customers', customerName)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* List all customers
|
|
322
|
+
*/
|
|
323
|
+
listCustomers() {
|
|
324
|
+
return Array.from(this.customers.keys()).map(name => ({
|
|
325
|
+
name: name,
|
|
326
|
+
...this.getCustomerInfo(name)
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Load existing customers from filesystem
|
|
332
|
+
*/
|
|
333
|
+
async loadExistingCustomers() {
|
|
334
|
+
const customersDir = resolve(this.configDir, 'customers');
|
|
335
|
+
if (!existsSync(customersDir)) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
// Read customer directories
|
|
340
|
+
const customerDirs = this.getCustomerDirectories();
|
|
341
|
+
for (const customerName of customerDirs) {
|
|
342
|
+
// Skip template directory
|
|
343
|
+
if (customerName === 'template') continue;
|
|
344
|
+
const customerDir = resolve(customersDir, customerName);
|
|
345
|
+
|
|
346
|
+
// Try to load customer metadata from a metadata file or infer from configs
|
|
347
|
+
const metadata = {
|
|
348
|
+
name: customerName,
|
|
349
|
+
createdAt: new Date().toISOString(),
|
|
350
|
+
// Placeholder
|
|
351
|
+
environments: this.environments
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Try to infer domain from production config
|
|
355
|
+
const prodConfigPath = resolve(customerDir, 'production.env');
|
|
356
|
+
if (existsSync(prodConfigPath)) {
|
|
357
|
+
try {
|
|
358
|
+
const prodConfig = this.parseEnvFile(prodConfigPath);
|
|
359
|
+
if (prodConfig.DOMAIN) {
|
|
360
|
+
metadata.domain = prodConfig.DOMAIN.replace(/^https?:\/\//, '');
|
|
361
|
+
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
logger.warn(`Could not parse production config for ${customerName}:`, error.message);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Register customer
|
|
368
|
+
this.customers.set(customerName, metadata);
|
|
369
|
+
|
|
370
|
+
// Try to register domain config
|
|
371
|
+
try {
|
|
372
|
+
const domainConfig = this.createCustomerDomainConfig(customerName, metadata.domain, {
|
|
373
|
+
skipValidation: true,
|
|
374
|
+
isFrameworkMode: true
|
|
375
|
+
});
|
|
376
|
+
this.domainRegistry.add(customerName, domainConfig);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
logger.warn(`Could not register domain for existing customer ${customerName}:`, error.message);
|
|
379
|
+
}
|
|
380
|
+
logger.info(`Loaded existing customer: ${customerName}`);
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
logger.error('Error loading existing customers:', error.message);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Get customer directories from filesystem
|
|
389
|
+
*/
|
|
390
|
+
getCustomerDirectories() {
|
|
391
|
+
const customersDir = resolve(this.configDir, 'customers');
|
|
392
|
+
if (!existsSync(customersDir)) {
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
// Read directory contents
|
|
397
|
+
const items = readdirSync(customersDir);
|
|
398
|
+
return items.filter(item => {
|
|
399
|
+
const itemPath = resolve(customersDir, item);
|
|
400
|
+
return statSync(itemPath).isDirectory() && item !== 'template';
|
|
401
|
+
});
|
|
402
|
+
} catch (error) {
|
|
403
|
+
logger.error('Error reading customer directories:', error.message);
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Parse environment file
|
|
410
|
+
*/
|
|
411
|
+
parseEnvFile(filePath) {
|
|
412
|
+
const content = readFileSync(filePath, 'utf8');
|
|
413
|
+
const variables = {};
|
|
414
|
+
content.split('\n').forEach(line => {
|
|
415
|
+
const trimmed = line.trim();
|
|
416
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
417
|
+
const [key, ...valueParts] = trimmed.split('=');
|
|
418
|
+
if (key && valueParts.length > 0) {
|
|
419
|
+
variables[key.trim()] = valueParts.join('=').trim();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
return variables;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Capitalize first letter
|
|
428
|
+
*/
|
|
429
|
+
capitalizeFirst(str) {
|
|
430
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Default instance
|
|
435
|
+
export const customerConfigManager = new CustomerConfigurationManager();
|
|
436
|
+
|
|
437
|
+
// Convenience functions
|
|
438
|
+
export const createCustomer = (name, domain, options) => customerConfigManager.createCustomer(name, domain, options);
|
|
439
|
+
export const validateCustomerConfigs = () => customerConfigManager.validateConfigs();
|
|
440
|
+
export const showCustomerConfig = (customer, env) => customerConfigManager.showConfig(customer, env);
|
|
441
|
+
export const getCustomerDeployCommand = (customer, env) => customerConfigManager.getDeployCommand(customer, env);
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { deepMerge, validateRequired, createLogger } from '../utils/index.js';
|
|
2
|
+
const logger = createLogger('DomainConfig');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a base domain configuration schema
|
|
6
|
+
* @returns {Object} Base domain configuration template
|
|
7
|
+
*/
|
|
8
|
+
export const createDomainConfigSchema = () => ({
|
|
9
|
+
name: '',
|
|
10
|
+
displayName: '',
|
|
11
|
+
accountId: '',
|
|
12
|
+
zoneId: '',
|
|
13
|
+
domains: {
|
|
14
|
+
production: '',
|
|
15
|
+
staging: '',
|
|
16
|
+
development: ''
|
|
17
|
+
},
|
|
18
|
+
services: {},
|
|
19
|
+
databases: {},
|
|
20
|
+
features: {},
|
|
21
|
+
settings: {
|
|
22
|
+
environment: 'development',
|
|
23
|
+
logLevel: 'info',
|
|
24
|
+
corsOrigins: ['*']
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validates a domain configuration object
|
|
30
|
+
* @param {Object} config - Domain configuration to validate
|
|
31
|
+
* @throws {Error} If validation fails
|
|
32
|
+
*/
|
|
33
|
+
export const validateDomainConfig = config => {
|
|
34
|
+
try {
|
|
35
|
+
// Required top-level fields
|
|
36
|
+
validateRequired(config, ['name', 'accountId', 'zoneId']);
|
|
37
|
+
|
|
38
|
+
// Validate Cloudflare IDs format (basic check)
|
|
39
|
+
if (!/^[a-f0-9]{32}$/.test(config.accountId)) {
|
|
40
|
+
throw new Error('accountId must be a valid Cloudflare account ID (32 hex characters)');
|
|
41
|
+
}
|
|
42
|
+
if (!/^[a-f0-9]{32}$/.test(config.zoneId)) {
|
|
43
|
+
throw new Error('zoneId must be a valid Cloudflare zone ID (32 hex characters)');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Validate domains structure
|
|
47
|
+
if (!config.domains || typeof config.domains !== 'object') {
|
|
48
|
+
throw new Error('domains must be an object with production/staging/development keys');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// At least one domain should be configured
|
|
52
|
+
const hasDomain = Object.values(config.domains).some(domain => domain && domain.trim());
|
|
53
|
+
if (!hasDomain) {
|
|
54
|
+
throw new Error('At least one domain (production, staging, or development) must be configured');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Validate domain formats (basic URL validation)
|
|
58
|
+
Object.entries(config.domains).forEach(([env, domain]) => {
|
|
59
|
+
if (domain && domain.trim()) {
|
|
60
|
+
try {
|
|
61
|
+
new URL(`https://${domain}`);
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Error(`Invalid domain format for ${env}: ${domain}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
logger.info(`Domain configuration validated: ${config.name}`);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
logger.error(`Domain configuration validation failed: ${error.message}`);
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Merges base domain config with service-specific extensions
|
|
76
|
+
* @param {Object} baseConfig - Base domain configuration
|
|
77
|
+
* @param {Object} serviceConfig - Service-specific configuration extensions
|
|
78
|
+
* @returns {Object} Merged configuration
|
|
79
|
+
*/
|
|
80
|
+
export const mergeDomainConfigs = (baseConfig, serviceConfig) => {
|
|
81
|
+
if (!baseConfig || !serviceConfig) {
|
|
82
|
+
throw new Error('Both baseConfig and serviceConfig are required for merging');
|
|
83
|
+
}
|
|
84
|
+
const merged = deepMerge(baseConfig, serviceConfig);
|
|
85
|
+
|
|
86
|
+
// Validate the merged configuration
|
|
87
|
+
validateDomainConfig(merged);
|
|
88
|
+
logger.info(`Domain configurations merged: ${baseConfig.name}`);
|
|
89
|
+
return merged;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Creates a domain configuration registry
|
|
94
|
+
* @param {Object} domainConfigs - Object containing domain configurations
|
|
95
|
+
* @returns {Object} Domain registry with lookup methods
|
|
96
|
+
*/
|
|
97
|
+
export const createDomainRegistry = domainConfigs => {
|
|
98
|
+
const registry = {
|
|
99
|
+
...domainConfigs
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
get: domainName => {
|
|
103
|
+
const config = registry[domainName];
|
|
104
|
+
if (!config) {
|
|
105
|
+
throw new Error(`Domain not found: ${domainName}`);
|
|
106
|
+
}
|
|
107
|
+
return config;
|
|
108
|
+
},
|
|
109
|
+
list: () => Object.keys(registry),
|
|
110
|
+
validateAll: () => {
|
|
111
|
+
Object.values(registry).forEach(validateDomainConfig);
|
|
112
|
+
logger.info(`All ${Object.keys(registry).length} domain configurations validated`);
|
|
113
|
+
},
|
|
114
|
+
add: (domainName, config) => {
|
|
115
|
+
validateDomainConfig(config);
|
|
116
|
+
registry[domainName] = config;
|
|
117
|
+
logger.info(`Domain added to registry: ${domainName}`);
|
|
118
|
+
},
|
|
119
|
+
remove: domainName => {
|
|
120
|
+
if (!registry[domainName]) {
|
|
121
|
+
throw new Error(`Domain not found: ${domainName}`);
|
|
122
|
+
}
|
|
123
|
+
delete registry[domainName];
|
|
124
|
+
logger.info(`Domain removed from registry: ${domainName}`);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Gets domain configuration from environment variables
|
|
131
|
+
* @param {Object} env - Environment variables
|
|
132
|
+
* @param {Object} domainConfigs - Available domain configurations
|
|
133
|
+
* @returns {Object} Domain configuration for current environment
|
|
134
|
+
*/
|
|
135
|
+
export const getDomainFromEnv = (env, domainConfigs) => {
|
|
136
|
+
const domainName = env.DOMAIN_NAME || env.CF_DOMAIN_NAME || 'default';
|
|
137
|
+
if (!domainConfigs[domainName]) {
|
|
138
|
+
logger.warn(`Domain not found: ${domainName}, using default`);
|
|
139
|
+
return domainConfigs.default || Object.values(domainConfigs)[0];
|
|
140
|
+
}
|
|
141
|
+
return domainConfigs[domainName];
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Creates environment-specific configuration overrides
|
|
146
|
+
* @param {Object} baseConfig - Base domain configuration
|
|
147
|
+
* @param {string} environment - Target environment (production, staging, development)
|
|
148
|
+
* @returns {Object} Environment-specific configuration
|
|
149
|
+
*/
|
|
150
|
+
export const createEnvironmentConfig = (baseConfig, environment = 'development') => {
|
|
151
|
+
const envConfig = {
|
|
152
|
+
...baseConfig
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Set environment-specific settings
|
|
156
|
+
envConfig.settings = {
|
|
157
|
+
...envConfig.settings,
|
|
158
|
+
environment,
|
|
159
|
+
isProduction: environment === 'production',
|
|
160
|
+
isStaging: environment === 'staging',
|
|
161
|
+
isDevelopment: environment === 'development'
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Environment-specific domain selection
|
|
165
|
+
if (envConfig.domains[environment]) {
|
|
166
|
+
envConfig.currentDomain = envConfig.domains[environment];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Environment-specific feature overrides
|
|
170
|
+
if (environment === 'production') {
|
|
171
|
+
// Ensure critical features are enabled in production
|
|
172
|
+
envConfig.features = {
|
|
173
|
+
...envConfig.features,
|
|
174
|
+
errorReporting: true,
|
|
175
|
+
monitoring: true
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
logger.info(`Environment config created for ${baseConfig.name} (${environment})`);
|
|
179
|
+
return envConfig;
|
|
180
|
+
};
|