@tamyla/clodo-framework 4.0.13 → 4.0.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +7 -0
  3. package/dist/cli/commands/create.js +2 -1
  4. package/dist/index.js +5 -0
  5. package/dist/middleware/Composer.js +38 -0
  6. package/dist/middleware/Registry.js +14 -0
  7. package/dist/middleware/index.js +3 -0
  8. package/dist/middleware/shared/basicAuth.js +21 -0
  9. package/dist/middleware/shared/cors.js +28 -0
  10. package/dist/middleware/shared/index.js +3 -0
  11. package/dist/middleware/shared/logging.js +14 -0
  12. package/dist/monitoring/HealthChecker.js +286 -0
  13. package/dist/orchestration/modules/StateManager.js +11 -3
  14. package/dist/security/ConfigurationValidator.js +216 -0
  15. package/dist/service-management/GenerationEngine.js +13 -2
  16. package/dist/service-management/ServiceOrchestrator.js +6 -2
  17. package/dist/service-management/generators/BaseGenerator.js +31 -6
  18. package/dist/service-management/generators/code/ServiceMiddlewareGenerator.js +156 -10
  19. package/dist/service-management/generators/code/WorkerIndexGenerator.js +75 -9
  20. package/dist/service-management/generators/utils/FileWriter.js +13 -1
  21. package/dist/services/ServiceClient.js +239 -0
  22. package/dist/simple-api.js +32 -1
  23. package/dist/utils/CircuitBreaker.js +192 -0
  24. package/dist/utils/EnvironmentValidator.js +147 -0
  25. package/dist/utils/TemplateRuntime.js +291 -0
  26. package/dist/utils/deployment/secret-generator.js +37 -26
  27. package/dist/version/FrameworkInfo.js +104 -0
  28. package/dist/worker/integration.js +13 -1
  29. package/docs/MIDDLEWARE_MIGRATION_SUMMARY.md +121 -0
  30. package/package.json +8 -1
  31. package/scripts/DEPLOY_COMMAND_NEW.js +128 -0
  32. package/scripts/README-automated-testing-suite.md +356 -0
  33. package/scripts/README-test-clodo-deployment.md +157 -0
  34. package/scripts/README.md +50 -0
  35. package/scripts/analyze-imports.ps1 +104 -0
  36. package/scripts/analyze-mixed-code.js +163 -0
  37. package/scripts/analyze-mixed-rationale.js +149 -0
  38. package/scripts/automated-testing-suite.js +776 -0
  39. package/scripts/check-templates.js +105 -0
  40. package/scripts/debug-generate-worker.js +58 -0
  41. package/scripts/deployment/README.md +31 -0
  42. package/scripts/deployment/deploy-domain.ps1 +449 -0
  43. package/scripts/deployment/deploy-staging.js +120 -0
  44. package/scripts/deployment/validate-staging.js +166 -0
  45. package/scripts/diagnose-imports.js +362 -0
  46. package/scripts/framework-diagnostic.js +368 -0
  47. package/scripts/migration/migrate-middleware-legacy-to-contract.js +64 -0
  48. package/scripts/post-publish-test.js +663 -0
  49. package/scripts/scan-worker-issues.js +52 -0
  50. package/scripts/service-management/README.md +27 -0
  51. package/scripts/service-management/setup-interactive.ps1 +693 -0
  52. package/scripts/test-clodo-deployment.js +588 -0
  53. package/scripts/test-downstream-install.js +237 -0
  54. package/scripts/test-local-package.ps1 +126 -0
  55. package/scripts/test-local-package.sh +166 -0
  56. package/scripts/test-package.js +339 -0
  57. package/scripts/testing/README.md +49 -0
  58. package/scripts/testing/test-first.ps1 +0 -0
  59. package/scripts/testing/test-first50.ps1 +0 -0
  60. package/scripts/testing/test.ps1 +0 -0
  61. package/scripts/utilities/README.md +61 -0
  62. package/scripts/utilities/check-bin.js +8 -0
  63. package/scripts/utilities/check-bundle.js +23 -0
  64. package/scripts/utilities/check-dist-imports.js +65 -0
  65. package/scripts/utilities/check-import-paths.js +191 -0
  66. package/scripts/utilities/cleanup-cli.js +159 -0
  67. package/scripts/utilities/deployment-helpers.ps1 +199 -0
  68. package/scripts/utilities/fix-dist-imports.js +135 -0
  69. package/scripts/utilities/generate-secrets.js +159 -0
  70. package/scripts/utilities/safe-push.ps1 +51 -0
  71. package/scripts/utilities/setup-helpers.ps1 +206 -0
  72. package/scripts/utilities/test-packaged-artifact.js +92 -0
  73. package/scripts/utilities/validate-dist-imports.js +189 -0
  74. package/scripts/utilities/validate-schema.js +102 -0
  75. package/scripts/verify-exports.js +193 -0
  76. package/scripts/verify-persistence-config.js +45 -0
  77. package/scripts/verify-worker-safety.js +73 -0
  78. package/templates/generic/src/config/.config-is-sample +1 -0
  79. package/templates/static-site/.env.example +1 -1
  80. package/templates/static-site/src/config/.config-is-sample +1 -0
  81. package/templates/static-site/src/config/domains.js +3 -0
  82. package/types/middleware.d.ts +1 -0
@@ -460,6 +460,222 @@ export class ConfigurationValidator {
460
460
  return config;
461
461
  }
462
462
 
463
+ /**
464
+ * Validate service configuration consistency between manifest and wrangler config
465
+ * @param {string} manifestPath - Path to manifest.json
466
+ * @param {string} wranglerPath - Path to wrangler.toml
467
+ * @returns {Object} Validation result with issues array
468
+ */
469
+ static validateServiceConfig(manifestPath, wranglerPath) {
470
+ const issues = [];
471
+ try {
472
+ // Read manifest.json
473
+ const manifestContent = readFileSync(manifestPath, 'utf8');
474
+ const manifest = JSON.parse(manifestContent);
475
+
476
+ // Read wrangler.toml (basic parsing - could be enhanced with a TOML parser)
477
+ const wranglerContent = readFileSync(wranglerPath, 'utf8');
478
+ const wranglerConfig = this.parseWranglerToml(wranglerContent);
479
+
480
+ // Validate D1 database consistency
481
+ this.validateD1Consistency(manifest, wranglerConfig, issues);
482
+
483
+ // Validate KV namespace consistency
484
+ this.validateKVConsistency(manifest, wranglerConfig, issues);
485
+
486
+ // Validate R2 bucket consistency
487
+ this.validateR2Consistency(manifest, wranglerConfig, issues);
488
+
489
+ // Validate environment variables
490
+ this.validateEnvironmentConsistency(manifest, wranglerConfig, issues);
491
+ } catch (error) {
492
+ issues.push({
493
+ type: 'error',
494
+ message: `Configuration validation failed: ${error.message}`,
495
+ severity: 'critical'
496
+ });
497
+ }
498
+ return {
499
+ valid: issues.length === 0,
500
+ issues: issues
501
+ };
502
+ }
503
+
504
+ /**
505
+ * Parse wrangler.toml content (basic implementation)
506
+ * @param {string} content - TOML content
507
+ * @returns {Object} Parsed configuration
508
+ */
509
+ static parseWranglerToml(content) {
510
+ const config = {
511
+ d1_databases: [],
512
+ kv_namespaces: [],
513
+ r2_buckets: [],
514
+ vars: {}
515
+ };
516
+ if (!content || !content.trim()) {
517
+ return config;
518
+ }
519
+ const lines = content.split('\n');
520
+ let currentSection = null;
521
+ for (const line of lines) {
522
+ const trimmed = line.trim();
523
+
524
+ // Section headers
525
+ if (trimmed.startsWith('[[')) {
526
+ if (trimmed.includes('d1_databases')) {
527
+ currentSection = 'd1_databases';
528
+ config.d1_databases.push({});
529
+ } else if (trimmed.includes('kv_namespaces')) {
530
+ currentSection = 'kv_namespaces';
531
+ config.kv_namespaces.push({});
532
+ } else if (trimmed.includes('r2_buckets')) {
533
+ currentSection = 'r2_buckets';
534
+ config.r2_buckets.push({});
535
+ }
536
+ } else if (trimmed.startsWith('[') && !trimmed.startsWith('[[')) {
537
+ currentSection = trimmed.replace(/\[|\]/g, '');
538
+ // Support both table and array-of-tables syntaxes by creating a container
539
+ if (Array.isArray(config[currentSection]) && config[currentSection].length === 0) {
540
+ config[currentSection].push({});
541
+ }
542
+ }
543
+ // Key-value pairs
544
+ else if (trimmed.includes('=') && currentSection) {
545
+ const [key, ...valueParts] = trimmed.split('=');
546
+ const value = valueParts.join('=').trim().replace(/"/g, '');
547
+ if (currentSection === 'vars') {
548
+ config.vars[key.trim()] = value;
549
+ } else if (Array.isArray(config[currentSection])) {
550
+ const currentItem = config[currentSection][config[currentSection].length - 1];
551
+ if (currentItem) {
552
+ currentItem[key.trim()] = value;
553
+ }
554
+ }
555
+ }
556
+ }
557
+ return config;
558
+ }
559
+
560
+ /**
561
+ * Validate D1 database consistency
562
+ */
563
+ static validateD1Consistency(manifest, wranglerConfig, issues) {
564
+ const manifestD1 = manifest.d1 || false;
565
+ const wranglerD1 = wranglerConfig.d1_databases || [];
566
+ if (manifestD1 === true && wranglerD1.length === 0) {
567
+ issues.push({
568
+ type: 'mismatch',
569
+ message: 'Manifest declares D1=true but wrangler.toml has no [[d1_databases]] configured',
570
+ severity: 'critical',
571
+ manifest: {
572
+ d1: manifestD1
573
+ },
574
+ wrangler: {
575
+ d1_databases: wranglerD1
576
+ }
577
+ });
578
+ } else if (manifestD1 === false && wranglerD1.length > 0) {
579
+ issues.push({
580
+ type: 'mismatch',
581
+ message: 'Manifest declares D1=false but wrangler.toml has [[d1_databases]] configured',
582
+ severity: 'critical',
583
+ manifest: {
584
+ d1: manifestD1
585
+ },
586
+ wrangler: {
587
+ d1_databases: wranglerD1
588
+ }
589
+ });
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Validate KV namespace consistency
595
+ */
596
+ static validateKVConsistency(manifest, wranglerConfig, issues) {
597
+ const manifestKV = manifest.kv || false;
598
+ const wranglerKV = wranglerConfig.kv_namespaces || [];
599
+ if (manifestKV === true && wranglerKV.length === 0) {
600
+ issues.push({
601
+ type: 'mismatch',
602
+ message: 'Manifest declares KV=true but wrangler.toml has no [[kv_namespaces]] configured',
603
+ severity: 'critical',
604
+ manifest: {
605
+ kv: manifestKV
606
+ },
607
+ wrangler: {
608
+ kv_namespaces: wranglerKV
609
+ }
610
+ });
611
+ } else if (manifestKV === false && wranglerKV.length > 0) {
612
+ issues.push({
613
+ type: 'mismatch',
614
+ message: 'Manifest declares KV=false but wrangler.toml has [[kv_namespaces]] configured',
615
+ severity: 'critical',
616
+ manifest: {
617
+ kv: manifestKV
618
+ },
619
+ wrangler: {
620
+ kv_namespaces: wranglerKV
621
+ }
622
+ });
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Validate R2 bucket consistency
628
+ */
629
+ static validateR2Consistency(manifest, wranglerConfig, issues) {
630
+ const manifestR2 = manifest.r2 || false;
631
+ const wranglerR2 = wranglerConfig.r2_buckets || [];
632
+ if (manifestR2 === true && wranglerR2.length === 0) {
633
+ issues.push({
634
+ type: 'mismatch',
635
+ message: 'Manifest declares R2=true but wrangler.toml has no [[r2_buckets]] configured',
636
+ severity: 'critical',
637
+ manifest: {
638
+ r2: manifestR2
639
+ },
640
+ wrangler: {
641
+ r2_buckets: wranglerR2
642
+ }
643
+ });
644
+ } else if (manifestR2 === false && wranglerR2.length > 0) {
645
+ issues.push({
646
+ type: 'mismatch',
647
+ message: 'Manifest declares R2=false but wrangler.toml has [[r2_buckets]] configured',
648
+ severity: 'critical',
649
+ manifest: {
650
+ r2: manifestR2
651
+ },
652
+ wrangler: {
653
+ r2_buckets: wranglerR2
654
+ }
655
+ });
656
+ }
657
+ }
658
+
659
+ /**
660
+ * Validate environment variable consistency
661
+ */
662
+ static validateEnvironmentConsistency(manifest, wranglerConfig, issues) {
663
+ const manifestEnv = manifest.environment || {};
664
+ const wranglerEnv = wranglerConfig.vars || {};
665
+
666
+ // Check for required environment variables declared in manifest but missing in wrangler
667
+ for (const [key, value] of Object.entries(manifestEnv)) {
668
+ if (value === 'required' && !(key in wranglerEnv)) {
669
+ issues.push({
670
+ type: 'missing_env',
671
+ message: `Manifest declares ${key} as required but not found in wrangler.toml vars`,
672
+ severity: 'warning',
673
+ key: key
674
+ });
675
+ }
676
+ }
677
+ }
678
+
463
679
  /**
464
680
  * Log validation results
465
681
  */
@@ -52,6 +52,8 @@ export class GenerationEngine {
52
52
  this.templatesDir = options.templatesDir || join(__dirname, '..', '..', 'templates');
53
53
  this.outputDir = options.outputDir || process.cwd();
54
54
  this.force = options.force || false;
55
+ // Default middleware strategy: 'contract' (can be overridden per-generation)
56
+ this.middlewareStrategy = options.middlewareStrategy || 'contract';
55
57
 
56
58
  // Initialize generator registry for centralized management
57
59
  this.generatorRegistry = new GeneratorRegistry({
@@ -192,7 +194,7 @@ export class GenerationEngine {
192
194
  },
193
195
  generateServiceMiddleware: {
194
196
  generator: 'serviceMiddlewareGenerator',
195
- returnPath: 'src/middleware.js'
197
+ returnPath: 'src/middleware/service-middleware.js'
196
198
  },
197
199
  generateServiceUtils: {
198
200
  generator: 'serviceUtilsGenerator',
@@ -283,6 +285,8 @@ export class GenerationEngine {
283
285
  // Only create proxy method if it doesn't already exist
284
286
  if (!this[methodName]) {
285
287
  this[methodName] = async function (...args) {
288
+ // Allow per-call overrides (apply BEFORE building context so context picks it up)
289
+ if (args[0] && args[0].middlewareStrategy) this.middlewareStrategy = args[0].middlewareStrategy;
286
290
  const context = this.buildContext(methodName, ...args);
287
291
  const generator = this[config.generator];
288
292
  const result = await generator.generate(context);
@@ -315,10 +319,12 @@ export class GenerationEngine {
315
319
  }
316
320
 
317
321
  // For most methods: (coreInputs, confirmedValues, servicePath)
322
+ // Include middlewareStrategy so generators can adapt their output
318
323
  return {
319
324
  coreInputs: args[0],
320
325
  confirmedValues: args[1],
321
- servicePath: args[2]
326
+ servicePath: args[2],
327
+ middlewareStrategy: this.middlewareStrategy
322
328
  };
323
329
  }
324
330
 
@@ -334,6 +340,11 @@ export class GenerationEngine {
334
340
  outputPath: this.outputDir,
335
341
  ...options
336
342
  };
343
+
344
+ // Allow per-generation override of middleware strategy
345
+ if (config.middlewareStrategy) {
346
+ this.middlewareStrategy = config.middlewareStrategy;
347
+ }
337
348
  console.log('⚙️ Tier 3: Automated Generation');
338
349
  console.log('Generating 67+ configuration files and service components...\n');
339
350
  try {
@@ -25,6 +25,8 @@ export class ServiceOrchestrator {
25
25
  this.interactive = options.interactive !== false;
26
26
  this.outputPath = options.outputPath || '.';
27
27
  this.templatePath = options.templatePath || './templates';
28
+ // Middleware strategy for generation: 'contract' (default) or 'legacy'
29
+ this.middlewareStrategy = options.middlewareStrategy || 'contract';
28
30
 
29
31
  // Initialize modular handler components
30
32
  this.inputHandler = new InputHandler({
@@ -63,7 +65,8 @@ export class ServiceOrchestrator {
63
65
  console.log(chalk.yellow('⚙️ Tier 3: Automated Generation'));
64
66
  console.log(chalk.white('Generating 67 configuration files and service components...\n'));
65
67
  const generationResult = await this.generationHandler.generateService(coreInputs, confirmedValues, {
66
- outputPath: this.outputPath
68
+ outputPath: this.outputPath,
69
+ middlewareStrategy: this.middlewareStrategy
67
70
  });
68
71
 
69
72
  // Display results
@@ -87,7 +90,8 @@ export class ServiceOrchestrator {
87
90
 
88
91
  // Generate service using GenerationHandler
89
92
  const generationResult = await this.generationHandler.generateService(coreInputs, confirmedValues, {
90
- outputPath: this.outputPath
93
+ outputPath: this.outputPath,
94
+ middlewareStrategy: this.middlewareStrategy
91
95
  });
92
96
  console.log(chalk.green(`✓ Service "${coreInputs.serviceName}" created successfully`));
93
97
  } catch (error) {
@@ -180,6 +180,9 @@ export class BaseGenerator {
180
180
  const fullPath = path.join(this.servicePath, relativePath);
181
181
  const overwrite = options.overwrite !== false; // Default to true
182
182
 
183
+ // Debug: log the target file path for diagnosis
184
+ this.logger.info(`BaseGenerator: writing file -> ${fullPath}`);
185
+
183
186
  // Check if file exists and overwrite is disabled
184
187
  try {
185
188
  await fs.access(fullPath);
@@ -197,13 +200,35 @@ export class BaseGenerator {
197
200
  recursive: true
198
201
  });
199
202
 
200
- // Write file
201
- try {
202
- await fs.writeFile(fullPath, content, 'utf8');
203
- this.logger.info(`Generated: ${relativePath}`);
204
- } catch (error) {
205
- throw new Error(`Failed to write file '${relativePath}': ${error.message}`);
203
+ // Write file with retry logic for filesystem stability
204
+ const maxRetries = 3;
205
+ let lastError;
206
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
207
+ try {
208
+ this.logger.info(`BaseGenerator: attempt ${attempt} - writing to ${fullPath}`);
209
+ await fs.writeFile(fullPath, content, 'utf8');
210
+
211
+ // Add a small delay to let file system settle, especially on Windows
212
+ if (attempt === 1) {
213
+ await new Promise(resolve => setTimeout(resolve, 10));
214
+ }
215
+
216
+ // Verify the file was written successfully by checking it exists
217
+ await fs.access(fullPath, fs.constants.F_OK);
218
+ this.logger.info(`Generated: ${relativePath}`);
219
+ return; // Success
220
+ } catch (error) {
221
+ lastError = error;
222
+ this.logger.warn(`Write attempt ${attempt}/${maxRetries} failed for ${relativePath}: ${error.message}`);
223
+ if (attempt < maxRetries) {
224
+ // Wait before retry (exponential backoff)
225
+ await new Promise(resolve => setTimeout(resolve, 50 * attempt));
226
+ }
227
+ }
206
228
  }
229
+
230
+ // If all retries failed, throw the last error
231
+ throw new Error(`Failed to write and verify file '${relativePath}' after ${maxRetries} attempts: ${lastError.message}`);
207
232
  }
208
233
 
209
234
  /**
@@ -21,8 +21,13 @@ export class ServiceMiddlewareGenerator extends BaseGenerator {
21
21
  if (!this.shouldGenerate(context)) {
22
22
  return null;
23
23
  }
24
- const middlewareContent = `/**
25
- * ${confirmedValues.displayName} - Service Middleware
24
+
25
+ // Allow legacy strategy if requested by context
26
+ const strategy = context && context.middlewareStrategy ? context.middlewareStrategy : 'contract';
27
+ let middlewareContent;
28
+ if (strategy === 'legacy') {
29
+ middlewareContent = `/**
30
+ * ${confirmedValues.displayName} - Service Middleware (LEGACY)
26
31
  *
27
32
  * Generated by Clodo Framework GenerationEngine
28
33
  * Service Type: ${coreInputs.serviceType}
@@ -47,19 +52,19 @@ export function createServiceMiddleware(serviceConfig, env) {
47
52
  headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
48
53
  }
49
54
 
50
- ${confirmedValues.features.logging ? `
55
+ ${confirmedValues.features && confirmedValues.features.logging ? `
51
56
  // Request logging
52
- console.log(\`[\${new Date().toISOString()}] \${request.method} \${request.url}\`);` : ''}
57
+ console.log('[' + new Date().toISOString() + '] ' + request.method + ' ' + request.url);` : ''}
53
58
 
54
- ${confirmedValues.features.rateLimiting ? `
59
+ ${confirmedValues.features && confirmedValues.features.rateLimiting ? `
55
60
  // Rate limiting (placeholder - implement based on requirements)
56
61
  // This would typically check request frequency and block if over limit` : ''}
57
62
 
58
- ${confirmedValues.features.authentication ? `
63
+ ${confirmedValues.features && confirmedValues.features.authentication ? `
59
64
  // Authentication middleware (placeholder)
60
65
  // This would validate JWT tokens, API keys, etc.` : ''}
61
66
 
62
- ${confirmedValues.features.authorization ? `
67
+ ${confirmedValues.features && confirmedValues.features.authorization ? `
63
68
  // Authorization middleware (placeholder)
64
69
  // This would check user permissions and roles` : ''}
65
70
 
@@ -77,11 +82,11 @@ export function createServiceMiddleware(serviceConfig, env) {
77
82
  headers.set('X-Version', '${confirmedValues.version}');
78
83
  headers.set('X-Response-Time', Date.now().toString());
79
84
 
80
- ${confirmedValues.features.monitoring ? `
85
+ ${confirmedValues.features && confirmedValues.features.monitoring ? `
81
86
  // Response monitoring
82
- console.log(\`Response: \${response.status} (\${Date.now()}ms)\`);` : ''}
87
+ console.log('Response: ' + response.status + ' (' + Date.now() + 'ms)');` : ''}
83
88
 
84
- ${confirmedValues.features.caching ? `
89
+ ${confirmedValues.features && confirmedValues.features.caching ? `
85
90
  // Cache headers (placeholder - implement based on content type)
86
91
  if (response.status === 200) {
87
92
  headers.set('Cache-Control', 'public, max-age=300'); // 5 minutes
@@ -95,6 +100,35 @@ export function createServiceMiddleware(serviceConfig, env) {
95
100
  };
96
101
  }
97
102
  `;
103
+ } else {
104
+ middlewareContent = `/**
105
+ * ${confirmedValues.displayName} - Service Middleware
106
+ *
107
+ * Generated by Clodo Framework GenerationEngine
108
+ * Service Type: ${coreInputs.serviceType}
109
+ */
110
+
111
+ // Service middleware contract skeleton - minimal and opt-in
112
+ export default class ${confirmedValues.packageName || confirmedValues.serviceName ? confirmedValues.serviceName : 'Service'}Middleware {
113
+ // Implement only the hooks you need
114
+ async preprocess(request) { return null; }
115
+ async authenticate(request) { return null; }
116
+ async validate(request) { return null; }
117
+ async postprocess(response) { return response; }
118
+ }
119
+
120
+ // Optional registration helper - Worker generator will call this to register
121
+ export function registerMiddleware(registry, serviceName) {
122
+ if (!registry || typeof registry.register !== 'function') return;
123
+ try {
124
+ registry.register(serviceName || '${coreInputs.serviceName}', new (exports.default || ${confirmedValues.packageName || confirmedValues.serviceName ? confirmedValues.serviceName : 'Service'}Middleware)());
125
+ } catch (e) {
126
+ // Non-fatal - allow services to manually register if needed
127
+ }
128
+ }
129
+
130
+ `;
131
+ }
98
132
  const filePath = join(servicePath, 'src', 'middleware', 'service-middleware.js');
99
133
 
100
134
  // Ensure directory exists
@@ -104,6 +138,118 @@ export function createServiceMiddleware(serviceConfig, env) {
104
138
  });
105
139
  writeFileSync(filePath, middlewareContent, 'utf8');
106
140
  this.logger.info(`Generated: ${filePath}`);
141
+
142
+ // Also generate a lightweight runtime helper for middleware composition and registry
143
+ const runtimeContent = `// Lightweight middleware runtime for generated services
144
+ export const MiddlewareRegistry = (() => {
145
+ const map = new Map();
146
+ return {
147
+ register(serviceName, instance) { map.set(serviceName, instance); },
148
+ get(serviceName) { return map.get(serviceName) || null; },
149
+ clear() { map.clear(); }
150
+ };
151
+ })();
152
+
153
+ export const MiddlewareComposer = {
154
+ compose(...middlewares) {
155
+ const chain = middlewares.filter(Boolean);
156
+ return {
157
+ async execute(request, handler) {
158
+ let req = request;
159
+ for (const m of chain) {
160
+ if (typeof m.preprocess === 'function') {
161
+ const res = await m.preprocess(req);
162
+ if (res) return res;
163
+ }
164
+ if (typeof m.authenticate === 'function') {
165
+ const res = await m.authenticate(req);
166
+ if (res) return res;
167
+ }
168
+ if (typeof m.validate === 'function') {
169
+ const res = await m.validate(req);
170
+ if (res) return res;
171
+ }
172
+ }
173
+ let response = await handler(req);
174
+ for (const m of chain.slice().reverse()) {
175
+ if (typeof m.postprocess === 'function') {
176
+ const updated = await m.postprocess(response);
177
+ if (updated instanceof Response) response = updated;
178
+ }
179
+ }
180
+ return response;
181
+ }
182
+ };
183
+ }
184
+ };
185
+ `;
186
+ const runtimePath = join(servicePath, 'src', 'middleware', 'runtime.js');
187
+ writeFileSync(runtimePath, runtimeContent, 'utf8');
188
+ this.logger.info(`Generated: ${runtimePath}`);
189
+
190
+ // Minimal shared implementations (copied into generated service for self-containment)
191
+ const sharedDir = join(servicePath, 'src', 'middleware', 'shared');
192
+ mkdirSync(sharedDir, {
193
+ recursive: true
194
+ });
195
+ const corsContent = `export function cors(options = {}) {
196
+ const origin = options.origin || '*';
197
+ const allowMethods = options.methods || 'GET, POST, PUT, DELETE, OPTIONS';
198
+ const allowHeaders = options.headers || 'Content-Type, Authorization';
199
+
200
+ return {
201
+ preprocess(request) {
202
+ if (request.method === 'OPTIONS') {
203
+ const headers = new Headers();
204
+ headers.set('Access-Control-Allow-Origin', origin);
205
+ headers.set('Access-Control-Allow-Methods', allowMethods);
206
+ headers.set('Access-Control-Allow-Headers', allowHeaders);
207
+ return new Response(null, { status: 204, headers });
208
+ }
209
+ return null;
210
+ },
211
+ postprocess(response) {
212
+ const headers = new Headers(response.headers);
213
+ headers.set('Access-Control-Allow-Origin', origin);
214
+ return new Response(response.body, { ...response, headers });
215
+ }
216
+ };
217
+ }
218
+ `;
219
+ const loggingContent = `export function logging(options = {}) {
220
+ const level = options.level || 'info';
221
+ return {
222
+ preprocess(request) {
223
+ try {
224
+ const path = new URL(request.url).pathname;
225
+ console.log('[' + new Date().toISOString() + '] ' + level.toUpperCase() + ' ' + request.method + ' ' + path);
226
+ } catch (e) {}
227
+ return null;
228
+ }
229
+ };
230
+ }
231
+ `;
232
+ const basicAuthContent = `export function basicAuth({ realm = 'Restricted' } = {}) {
233
+ return {
234
+ authenticate(request) {
235
+ const auth = request.headers.get('Authorization');
236
+ if (!auth) {
237
+ return new Response(JSON.stringify({ error: 'Unauthorized' }), {
238
+ status: 401,
239
+ headers: { 'Content-Type': 'application/json', 'WWW-Authenticate': 'Basic realm="' + realm + '"' }
240
+ });
241
+ }
242
+ return null;
243
+ }
244
+ };
245
+ }
246
+ `;
247
+ writeFileSync(join(sharedDir, 'cors.js'), corsContent, 'utf8');
248
+ writeFileSync(join(sharedDir, 'logging.js'), loggingContent, 'utf8');
249
+ writeFileSync(join(sharedDir, 'basicAuth.js'), basicAuthContent, 'utf8');
250
+ const indexContent = `export { cors } from './cors.js';\nexport { logging } from './logging.js';\nexport { basicAuth } from './basicAuth.js';\n`;
251
+ writeFileSync(join(sharedDir, 'index.js'), indexContent, 'utf8');
252
+ this.logger.info(`Generated shared middleware in: ${sharedDir}`);
107
253
  return filePath;
108
254
  }
109
255
 
@@ -70,7 +70,8 @@ export class WorkerIndexGenerator extends BaseGenerator {
70
70
 
71
71
  import { domains } from '../config/domains.js';
72
72
  import { createServiceHandlers } from '../handlers/service-handlers.js';
73
- import { createServiceMiddleware } from '../middleware/service-middleware.js';
73
+ import { MiddlewareRegistry, MiddlewareComposer } from '../middleware/runtime.js';
74
+ import * as Shared from '../middleware/shared/index.js';
74
75
 
75
76
  export default {
76
77
  async fetch(request, env, ctx) {
@@ -78,16 +79,81 @@ export default {
78
79
  // Get service configuration
79
80
  const serviceConfig = domains['${coreInputs.serviceName}'];
80
81
 
81
- // Apply middleware
82
- const middleware = createServiceMiddleware(serviceConfig, env);
83
- const processedRequest = await middleware.processRequest(request);
82
+ // Build shared middleware instances
83
+ const sharedMiddlewares = [
84
+ Shared.cors({ origin: serviceConfig.corsPolicy || '*' }),
85
+ Shared.logging({ level: serviceConfig.logLevel || 'info' })
86
+ ];
84
87
 
85
- // Route to appropriate handler
86
- const handlers = createServiceHandlers(serviceConfig, env);
87
- const response = await handlers.handleRequest(processedRequest, ctx);
88
+ // Lazy-load service middleware and support legacy factory compatibility
89
+ let serviceMiddlewareInstance = null;
90
+ let legacyFactory = null;
88
91
 
89
- // Apply response middleware
90
- return await middleware.processResponse(response);
92
+ try {
93
+ const mod = await import('../middleware/service-middleware.js');
94
+
95
+ if (mod?.registerMiddleware) {
96
+ // New-style registration helper
97
+ mod.registerMiddleware(MiddlewareRegistry, serviceConfig.name);
98
+ serviceMiddlewareInstance = MiddlewareRegistry.get(serviceConfig.name);
99
+ } else if (mod?.default) {
100
+ const def = mod.default;
101
+ // If the default is a class (constructor), instantiate and register
102
+ if (typeof def === 'function' && def.prototype) {
103
+ try {
104
+ const instance = new def();
105
+ MiddlewareRegistry.register(serviceConfig.name, instance);
106
+ serviceMiddlewareInstance = instance;
107
+ } catch (e) {
108
+ // ignore instantiation errors
109
+ }
110
+ } else if (typeof def === 'function') {
111
+ // Legacy factory exported as default
112
+ legacyFactory = def;
113
+ }
114
+ } else if (mod?.createServiceMiddleware) {
115
+ legacyFactory = mod.createServiceMiddleware;
116
+ }
117
+ } catch (e) {
118
+ // No service-specific middleware found - continue with shared only
119
+ }
120
+
121
+ // Compose final middleware chain
122
+ let chain;
123
+
124
+ if (legacyFactory) {
125
+ const legacyInstance = legacyFactory(serviceConfig, env);
126
+ const adapter = {
127
+ preprocess: async (req) => {
128
+ if (legacyInstance && typeof legacyInstance.processRequest === 'function') {
129
+ const processed = await legacyInstance.processRequest(req);
130
+ if (processed instanceof Response) return processed; // short-circuit
131
+ return null; // continue (legacy returns a Request)
132
+ }
133
+ return null;
134
+ },
135
+ postprocess: async (res) => {
136
+ if (legacyInstance && typeof legacyInstance.processResponse === 'function') {
137
+ const r = await legacyInstance.processResponse(res);
138
+ return r instanceof Response ? r : res;
139
+ }
140
+ return res;
141
+ }
142
+ };
143
+
144
+ chain = MiddlewareComposer.compose(...sharedMiddlewares, adapter);
145
+ } else {
146
+ const svcMw = serviceMiddlewareInstance || MiddlewareRegistry.get(serviceConfig.name);
147
+ chain = MiddlewareComposer.compose(...sharedMiddlewares, svcMw);
148
+ }
149
+
150
+ // Execute middleware chain with final handler
151
+ const response = await chain.execute(request, async (req) => {
152
+ const handlers = createServiceHandlers(serviceConfig, env);
153
+ return handlers.handleRequest(req, ctx);
154
+ });
155
+
156
+ return response;
91
157
 
92
158
  } catch (error) {
93
159
  console.error('Worker error:', error);