@tamyla/clodo-framework 3.1.14 → 3.1.15

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.
@@ -0,0 +1,596 @@
1
+ /**
2
+ * Domain Router
3
+ * CLI wrapper layer for domain routing, auto-detection, and multi-domain deployments
4
+ *
5
+ * REFACTORED (Task 3.1): Now delegates to MultiDomainOrchestrator for actual deployment logic
6
+ *
7
+ * Features:
8
+ * - Auto-detect available domains from configuration
9
+ * - Smart domain selection based on environment
10
+ * - Environment-specific routing strategies
11
+ * - Delegates multi-domain deployments to MultiDomainOrchestrator
12
+ * - CLI-friendly interface for domain selection and config loading
13
+ */
14
+
15
+ import { existsSync, readFileSync } from 'fs';
16
+ import { resolve } from 'path';
17
+ import { MultiDomainOrchestrator } from "../../../orchestration/multi-domain-orchestrator.js";
18
+ export class DomainRouter {
19
+ constructor(options = {}) {
20
+ this.configPath = options.configPath || './config/domains.json';
21
+ this.cloudflareAPI = options.cloudflareAPI || null;
22
+ this.verbose = options.verbose || false;
23
+ this.environment = options.environment || 'development';
24
+ this.domains = [];
25
+ this.routes = {};
26
+ this.failoverStrategies = {};
27
+ this.loadedConfig = null;
28
+
29
+ // Initialize MultiDomainOrchestrator for delegation
30
+ // Only creates if orchestrator options provided
31
+ this.orchestrator = null;
32
+ this.disableOrchestrator = options.disableOrchestrator || false; // For testing
33
+ this.orchestratorOptions = options.orchestratorOptions || {
34
+ dryRun: options.dryRun || false,
35
+ skipTests: options.skipTests || false,
36
+ parallelDeployments: options.parallelDeployments || 3,
37
+ servicePath: options.servicePath || process.cwd(),
38
+ cloudflareToken: options.cloudflareToken || null,
39
+ cloudflareAccountId: options.cloudflareAccountId || null,
40
+ enablePersistence: options.enablePersistence !== false,
41
+ rollbackEnabled: options.rollbackEnabled !== false
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Initialize the orchestrator for multi-domain deployments
47
+ * REFACTORED (Task 3.1): Delegates to MultiDomainOrchestrator
48
+ * @param {Object} options - Orchestrator initialization options
49
+ * @returns {Promise<void>}
50
+ */
51
+ async initializeOrchestrator(options = {}) {
52
+ if (this.orchestrator) {
53
+ return; // Already initialized
54
+ }
55
+ const orchestratorConfig = {
56
+ ...this.orchestratorOptions,
57
+ ...options,
58
+ domains: this.domains,
59
+ environment: this.environment
60
+ };
61
+ try {
62
+ this.orchestrator = new MultiDomainOrchestrator(orchestratorConfig);
63
+ await this.orchestrator.initialize();
64
+ if (this.verbose) {
65
+ console.log('✅ Multi-Domain Orchestrator initialized');
66
+ }
67
+ } catch (error) {
68
+ throw new Error(`Failed to initialize orchestrator: ${error.message}`);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Load domain configuration from file or API
74
+ * @param {Object} options - Loading options
75
+ * @returns {Promise<Object>} Loaded configuration
76
+ */
77
+ async loadConfiguration(options = {}) {
78
+ try {
79
+ // Try loading from file first
80
+ if (options.configPath && existsSync(options.configPath)) {
81
+ const content = readFileSync(options.configPath, 'utf-8');
82
+ this.loadedConfig = JSON.parse(content);
83
+ if (this.verbose) {
84
+ console.log(`📋 Loaded domain config from: ${options.configPath}`);
85
+ }
86
+ return this.loadedConfig;
87
+ }
88
+
89
+ // If no file, try Cloudflare API
90
+ if (this.cloudflareAPI && options.useCloudflareAPI) {
91
+ this.loadedConfig = await this.cloudflareAPI.getDomainsConfiguration();
92
+ if (this.verbose) {
93
+ console.log('📋 Loaded domain config from Cloudflare API');
94
+ }
95
+ return this.loadedConfig;
96
+ }
97
+ return null;
98
+ } catch (error) {
99
+ throw new Error(`Failed to load domain configuration: ${error.message}`);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Detect available domains from configuration or environment
105
+ * @param {Object} options - Detection options
106
+ * @returns {Promise<Array<string>>} Array of discovered domains
107
+ */
108
+ async detectDomains(options = {}) {
109
+ const config = options.config || this.loadedConfig || {};
110
+ const domains = [];
111
+
112
+ // Extract from config
113
+ if (config.domains) {
114
+ if (Array.isArray(config.domains)) {
115
+ domains.push(...config.domains);
116
+ } else if (typeof config.domains === 'object') {
117
+ // Handle environment-keyed domains: { production: 'api.example.com', staging: 'staging-api.example.com' }
118
+ for (const [env, domainList] of Object.entries(config.domains)) {
119
+ if (Array.isArray(domainList)) {
120
+ domains.push(...domainList);
121
+ } else if (typeof domainList === 'string') {
122
+ domains.push(domainList);
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ // Extract from environment variable
129
+ const envDomains = process.env.CLODO_DOMAINS;
130
+ if (envDomains) {
131
+ domains.push(...envDomains.split(',').map(d => d.trim()));
132
+ }
133
+
134
+ // Deduplicate and sort
135
+ const uniqueDomains = [...new Set(domains)].sort();
136
+ if (this.verbose) {
137
+ console.log(`🔍 Detected ${uniqueDomains.length} domains: ${uniqueDomains.join(', ')}`);
138
+ }
139
+ this.domains = uniqueDomains;
140
+ return uniqueDomains;
141
+ }
142
+
143
+ /**
144
+ * Select the appropriate domain for deployment based on environment and options
145
+ * @param {Object} options - Selection criteria
146
+ * @returns {string|Array<string>} Selected domain(s)
147
+ */
148
+ selectDomain(options = {}) {
149
+ const {
150
+ specificDomain,
151
+ environment = this.environment,
152
+ selectAll = false,
153
+ environmentMap = {}
154
+ } = options;
155
+
156
+ // If specific domain requested, validate and return it
157
+ if (specificDomain) {
158
+ if (this.domains.includes(specificDomain)) {
159
+ if (this.verbose) {
160
+ console.log(`✓ Selected domain: ${specificDomain}`);
161
+ }
162
+ return specificDomain;
163
+ } else {
164
+ throw new Error(`Domain '${specificDomain}' not found in available domains: ${this.domains.join(', ')}`);
165
+ }
166
+ }
167
+
168
+ // If returning all domains
169
+ if (selectAll) {
170
+ if (this.verbose) {
171
+ console.log(`✓ Selected all ${this.domains.length} domains`);
172
+ }
173
+ return this.domains;
174
+ }
175
+
176
+ // Select based on environment mapping
177
+ const envMap = environmentMap[environment];
178
+ if (envMap) {
179
+ const selectedDomain = Array.isArray(envMap) ? envMap[0] : envMap;
180
+ if (this.domains.includes(selectedDomain)) {
181
+ if (this.verbose) {
182
+ console.log(`✓ Selected domain for ${environment}: ${selectedDomain}`);
183
+ }
184
+ return selectedDomain;
185
+ }
186
+ }
187
+
188
+ // Default to first available domain
189
+ if (this.domains.length > 0) {
190
+ if (this.verbose) {
191
+ console.log(`✓ Selected default domain: ${this.domains[0]}`);
192
+ }
193
+ return this.domains[0];
194
+ }
195
+ throw new Error('No domains available for selection');
196
+ }
197
+
198
+ /**
199
+ * Get environment-specific routing configuration
200
+ * @param {string} domain - Domain name
201
+ * @param {string} environment - Environment (development, staging, production)
202
+ * @returns {Object} Routing configuration
203
+ */
204
+ getEnvironmentRouting(domain, environment = this.environment) {
205
+ const config = this.loadedConfig || {};
206
+ const domainConfig = config[domain] || config.routing || {};
207
+ const environmentRouting = {
208
+ domain,
209
+ environment,
210
+ endpoints: [],
211
+ strategies: [],
212
+ rateLimit: 1000,
213
+ timeout: 30000,
214
+ retries: 3,
215
+ cacheTTL: 3600,
216
+ corsEnabled: false,
217
+ customHeaders: {},
218
+ ...domainConfig[environment] // Allow environment-specific overrides
219
+ };
220
+
221
+ // Add default endpoints for environment
222
+ if (environment === 'production') {
223
+ environmentRouting.rateLimit = 10000;
224
+ environmentRouting.cacheTTL = 86400; // 24 hours
225
+ environmentRouting.strategies = ['load-balance', 'geo-route'];
226
+ } else if (environment === 'staging') {
227
+ environmentRouting.rateLimit = 5000;
228
+ environmentRouting.cacheTTL = 3600; // 1 hour
229
+ environmentRouting.strategies = ['round-robin'];
230
+ } else {
231
+ // development
232
+ environmentRouting.rateLimit = 100;
233
+ environmentRouting.cacheTTL = 300; // 5 minutes
234
+ environmentRouting.strategies = ['direct'];
235
+ }
236
+ if (this.verbose) {
237
+ console.log(`🛣️ Environment routing for ${domain} (${environment}): ${environmentRouting.strategies.join(', ')}`);
238
+ }
239
+ return environmentRouting;
240
+ }
241
+
242
+ /**
243
+ * Get failover strategy for domain
244
+ * @param {string} domain - Domain name
245
+ * @returns {Object} Failover configuration
246
+ */
247
+ getFailoverStrategy(domain) {
248
+ if (this.failoverStrategies[domain]) {
249
+ return this.failoverStrategies[domain];
250
+ }
251
+ const config = this.loadedConfig || {};
252
+ const domainConfig = config[domain] || {};
253
+ const strategy = {
254
+ domain,
255
+ primaryEndpoint: domainConfig.primaryEndpoint || null,
256
+ secondaryEndpoints: domainConfig.secondaryEndpoints || [],
257
+ healthCheckInterval: domainConfig.healthCheckInterval || 30000,
258
+ healthCheckPath: domainConfig.healthCheckPath || '/health',
259
+ failoverThreshold: domainConfig.failoverThreshold || 3,
260
+ autoFailover: domainConfig.autoFailover !== false,
261
+ maxRetries: domainConfig.maxRetries || 5,
262
+ rollbackOnFailure: domainConfig.rollbackOnFailure !== false,
263
+ notifications: domainConfig.notifications || []
264
+ };
265
+ this.failoverStrategies[domain] = strategy;
266
+ if (this.verbose) {
267
+ console.log(`⚡ Failover strategy for ${domain}: ${strategy.autoFailover ? 'auto' : 'manual'} with ${strategy.secondaryEndpoints.length} backups`);
268
+ }
269
+ return strategy;
270
+ }
271
+
272
+ /**
273
+ * Validate domain configuration
274
+ * @param {Object} config - Configuration to validate
275
+ * @returns {Object} Validation result { valid, errors }
276
+ */
277
+ validateConfiguration(config) {
278
+ const result = {
279
+ valid: true,
280
+ errors: [],
281
+ warnings: []
282
+ };
283
+ if (!config) {
284
+ result.valid = false;
285
+ result.errors.push('Configuration cannot be empty');
286
+ return result;
287
+ }
288
+ if (!config.domains || Array.isArray(config.domains) && config.domains.length === 0) {
289
+ result.valid = false;
290
+ result.errors.push('At least one domain must be specified');
291
+ }
292
+
293
+ // Validate each domain has required fields
294
+ if (Array.isArray(config.domains)) {
295
+ config.domains.forEach(domain => {
296
+ if (!domain || typeof domain !== 'string') {
297
+ result.errors.push('All domains must be non-empty strings');
298
+ result.valid = false;
299
+ }
300
+ });
301
+ }
302
+
303
+ // Check for environment-specific configurations
304
+ if (config.environments) {
305
+ const validEnvs = ['development', 'staging', 'production'];
306
+ for (const env of Object.keys(config.environments)) {
307
+ if (!validEnvs.includes(env)) {
308
+ result.warnings.push(`Unknown environment: ${env}`);
309
+ }
310
+ }
311
+ }
312
+ if (this.verbose && result.errors.length > 0) {
313
+ console.log(`❌ Configuration validation failed: ${result.errors.join(', ')}`);
314
+ }
315
+ return result;
316
+ }
317
+
318
+ /**
319
+ * Plan multi-domain deployment
320
+ * REFACTORED (Task 3.1): Delegates to MultiDomainOrchestrator
321
+ * @param {Array<string>} domains - Domains to deploy
322
+ * @param {Object} options - Deployment options
323
+ * @returns {Object} Deployment plan
324
+ */
325
+ planMultiDomainDeployment(domains, options = {}) {
326
+ if (!this.orchestrator) {
327
+ // Fallback to basic planning if orchestrator not initialized
328
+ const {
329
+ parallelDeployments = 3,
330
+ environment = this.environment,
331
+ validateBeforeDeploy = true,
332
+ rollbackOnError = true
333
+ } = options;
334
+
335
+ // Validate all domains
336
+ const invalidDomains = domains.filter(d => !this.domains.includes(d));
337
+ if (invalidDomains.length > 0) {
338
+ throw new Error(`Invalid domains: ${invalidDomains.join(', ')}`);
339
+ }
340
+
341
+ // Create deployment batches
342
+ const batches = [];
343
+ for (let i = 0; i < domains.length; i += parallelDeployments) {
344
+ batches.push(domains.slice(i, i + parallelDeployments));
345
+ }
346
+ return {
347
+ totalDomains: domains.length,
348
+ batches,
349
+ parallelDeployments,
350
+ environment,
351
+ phases: [{
352
+ phase: 'validation',
353
+ domains
354
+ }, {
355
+ phase: 'preparation',
356
+ domains
357
+ }, {
358
+ phase: 'deployment',
359
+ domains,
360
+ batches
361
+ }, {
362
+ phase: 'verification',
363
+ domains
364
+ }, {
365
+ phase: 'rollback',
366
+ domains,
367
+ enabled: rollbackOnError
368
+ }],
369
+ estimatedDuration: domains.length * 5 * 60 * 1000,
370
+ rollbackOnError,
371
+ validateBeforeDeploy
372
+ };
373
+ }
374
+
375
+ // Delegate to orchestrator's batch creation
376
+ const batches = this.orchestrator.createDeploymentBatches();
377
+ if (this.verbose) {
378
+ console.log(`📊 Deployment plan created: ${batches.length} batches for ${domains.length} domains`);
379
+ }
380
+ return {
381
+ totalDomains: domains.length,
382
+ batches,
383
+ parallelDeployments: this.orchestratorOptions.parallelDeployments,
384
+ environment: this.environment,
385
+ orchestratorManaged: true
386
+ };
387
+ }
388
+
389
+ /**
390
+ * Execute multi-domain deployment with coordination
391
+ * REFACTORED (Task 3.1): Delegates to MultiDomainOrchestrator for actual deployment
392
+ * @param {Array<string>} domains - Domains to deploy
393
+ * @param {Function} deployFn - Async function to deploy a single domain (legacy support)
394
+ * @param {Object} options - Deployment options
395
+ * @returns {Promise<Object>} Deployment results
396
+ */
397
+ async deployAcrossDomains(domains, deployFn, options = {}) {
398
+ // For tests or when explicitly disabled, use legacy mode without orchestrator
399
+ if (this.disableOrchestrator) {
400
+ return this._deployAcrossDomainsLegacy(domains, deployFn, options);
401
+ }
402
+
403
+ // Ensure orchestrator is initialized
404
+ if (!this.orchestrator) {
405
+ await this.initializeOrchestrator();
406
+ }
407
+ try {
408
+ // If deployFn provided, use orchestrator's delegated deployment with custom handler
409
+ if (deployFn && typeof deployFn === 'function') {
410
+ if (this.verbose) {
411
+ console.log('🚀 Using custom deployment function with orchestrator coordination');
412
+ }
413
+
414
+ // Validate domains
415
+ const invalidDomains = domains.filter(d => !this.domains.includes(d));
416
+ if (invalidDomains.length > 0) {
417
+ throw new Error(`Invalid domains: ${invalidDomains.join(', ')}`);
418
+ }
419
+
420
+ // Execute with plan
421
+ const plan = this.planMultiDomainDeployment(domains, options);
422
+ const results = {
423
+ successful: [],
424
+ failed: [],
425
+ skipped: [],
426
+ duration: 0,
427
+ startTime: new Date(),
428
+ orchestratorManaged: true
429
+ };
430
+
431
+ // Phase 1: Validation
432
+ if (plan.validateBeforeDeploy !== false) {
433
+ for (const domain of domains) {
434
+ const validation = this.validateConfiguration({
435
+ domains: [domain]
436
+ });
437
+ if (!validation.valid) {
438
+ results.failed.push({
439
+ domain,
440
+ error: validation.errors.join(', ')
441
+ });
442
+ }
443
+ }
444
+ if (results.failed.length > 0 && options.rollbackOnError !== false) {
445
+ throw new Error(`Validation failed for ${results.failed.length} domain(s)`);
446
+ }
447
+ }
448
+
449
+ // Phase 2: Batch deployment via orchestrator
450
+ for (const batch of plan.batches) {
451
+ const deployPromises = batch.map(domain => this.orchestrator.deploySingleDomain(domain, options).then(result => {
452
+ results.successful.push({
453
+ domain,
454
+ ...result
455
+ });
456
+ }).catch(error => {
457
+ results.failed.push({
458
+ domain,
459
+ error: error.message
460
+ });
461
+ if (options.rollbackOnError !== false) {
462
+ throw error;
463
+ }
464
+ }));
465
+ try {
466
+ await Promise.all(deployPromises);
467
+ } catch (error) {
468
+ if (options.rollbackOnError !== false) {
469
+ results.duration = Date.now() - results.startTime.getTime();
470
+ throw new Error(`Deployment failed at batch. Completed: ${results.successful.length}, Failed: ${results.failed.length}`);
471
+ }
472
+ }
473
+ }
474
+ results.duration = Date.now() - results.startTime.getTime();
475
+ if (this.verbose) {
476
+ console.log(`✅ Multi-domain deployment complete: ${results.successful.length} successful, ${results.failed.length} failed in ${Math.ceil(results.duration / 1000)}s`);
477
+ }
478
+ return results;
479
+ } else {
480
+ // No custom function provided - use orchestrator's portfolio deployment
481
+ if (this.verbose) {
482
+ console.log('🚀 Using orchestrator portfolio deployment');
483
+ }
484
+ const startTime = Date.now();
485
+ const results = await this.orchestrator.deployPortfolio();
486
+ const duration = Date.now() - startTime;
487
+ return {
488
+ ...results,
489
+ duration,
490
+ orchestratorManaged: true
491
+ };
492
+ }
493
+ } catch (error) {
494
+ if (this.verbose) {
495
+ console.log(`❌ Deployment failed: ${error.message}`);
496
+ }
497
+ throw error;
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Legacy deployment logic for testing or when orchestrator is disabled
503
+ * @private
504
+ * @param {Array<string>} domains - Domains to deploy
505
+ * @param {Function} deployFn - Async function to deploy a single domain
506
+ * @param {Object} options - Deployment options
507
+ * @returns {Promise<Object>} Deployment results
508
+ */
509
+ async _deployAcrossDomainsLegacy(domains, deployFn, options = {}) {
510
+ const plan = this.planMultiDomainDeployment(domains, options);
511
+ const results = {
512
+ successful: [],
513
+ failed: [],
514
+ skipped: [],
515
+ duration: 0,
516
+ startTime: new Date()
517
+ };
518
+ try {
519
+ // Phase 1: Validation
520
+ if (plan.validateBeforeDeploy) {
521
+ for (const domain of domains) {
522
+ const routing = this.getEnvironmentRouting(domain, options.environment || this.environment);
523
+ const validation = this.validateConfiguration({
524
+ domains: [domain],
525
+ ...routing
526
+ });
527
+ if (!validation.valid) {
528
+ results.failed.push({
529
+ domain,
530
+ error: validation.errors.join(', ')
531
+ });
532
+ }
533
+ }
534
+ if (results.failed.length > 0 && plan.rollbackOnError) {
535
+ throw new Error(`Validation failed for ${results.failed.length} domain(s)`);
536
+ }
537
+ }
538
+
539
+ // Phase 2: Batch deployment
540
+ for (const batch of plan.batches) {
541
+ const deployPromises = batch.map(domain => deployFn(domain, options).then(result => {
542
+ results.successful.push({
543
+ domain,
544
+ ...result
545
+ });
546
+ }).catch(error => {
547
+ results.failed.push({
548
+ domain,
549
+ error: error.message
550
+ });
551
+ if (plan.rollbackOnError) {
552
+ throw error;
553
+ }
554
+ }));
555
+ try {
556
+ await Promise.all(deployPromises);
557
+ } catch (error) {
558
+ if (plan.rollbackOnError) {
559
+ results.duration = Date.now() - results.startTime.getTime();
560
+ throw new Error(`Deployment failed at batch. Completed: ${results.successful.length}, Failed: ${results.failed.length}`);
561
+ }
562
+ }
563
+ }
564
+ results.duration = Date.now() - results.startTime.getTime();
565
+ if (this.verbose) {
566
+ console.log(`✅ Deployment complete: ${results.successful.length} successful, ${results.failed.length} failed in ${Math.ceil(results.duration / 1000)}s`);
567
+ }
568
+ return results;
569
+ } catch (error) {
570
+ results.duration = Date.now() - results.startTime.getTime();
571
+ if (this.verbose) {
572
+ console.log(`❌ Deployment failed: ${error.message}`);
573
+ }
574
+ throw error;
575
+ }
576
+ }
577
+
578
+ /**
579
+ * Get routing summary for debugging
580
+ * @returns {Object} Routing summary
581
+ */
582
+ getSummary() {
583
+ const summary = {
584
+ totalDomains: this.domains.length,
585
+ domains: this.domains,
586
+ environment: this.environment,
587
+ routing: {},
588
+ failover: {}
589
+ };
590
+ for (const domain of this.domains) {
591
+ summary.routing[domain] = this.getEnvironmentRouting(domain);
592
+ summary.failover[domain] = this.getFailoverStrategy(domain);
593
+ }
594
+ return summary;
595
+ }
596
+ }