@tamyla/clodo-framework 4.4.0 → 4.5.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +2 -1844
  2. package/README.md +44 -18
  3. package/dist/cli/commands/add.js +325 -0
  4. package/dist/config/service-schema-config.js +98 -5
  5. package/dist/index.js +22 -3
  6. package/dist/middleware/Composer.js +2 -1
  7. package/dist/middleware/factories.js +445 -0
  8. package/dist/middleware/index.js +4 -1
  9. package/dist/modules/ModuleManager.js +6 -2
  10. package/dist/routing/EnhancedRouter.js +248 -44
  11. package/dist/routing/RequestContext.js +393 -0
  12. package/dist/schema/SchemaManager.js +6 -2
  13. package/dist/service-management/generators/code/ServiceMiddlewareGenerator.js +79 -223
  14. package/dist/service-management/generators/code/WorkerIndexGenerator.js +241 -98
  15. package/dist/service-management/generators/config/WranglerTomlGenerator.js +130 -89
  16. package/dist/simple-api.js +4 -4
  17. package/dist/utilities/index.js +134 -1
  18. package/dist/utils/config/environment-var-normalizer.js +233 -0
  19. package/dist/validation/environmentGuard.js +172 -0
  20. package/docs/CHANGELOG.md +1877 -0
  21. package/docs/api-reference.md +153 -0
  22. package/package.json +4 -1
  23. package/scripts/repro-clodo.js +123 -0
  24. package/templates/ai-worker/package.json +19 -0
  25. package/templates/ai-worker/src/index.js +160 -0
  26. package/templates/cron-worker/package.json +19 -0
  27. package/templates/cron-worker/src/index.js +211 -0
  28. package/templates/edge-proxy/package.json +18 -0
  29. package/templates/edge-proxy/src/index.js +150 -0
  30. package/templates/minimal/package.json +17 -0
  31. package/templates/minimal/src/index.js +40 -0
  32. package/templates/queue-processor/package.json +19 -0
  33. package/templates/queue-processor/src/index.js +213 -0
  34. package/templates/rest-api/.dev.vars +2 -0
  35. package/templates/rest-api/package.json +19 -0
  36. package/templates/rest-api/src/index.js +124 -0
@@ -0,0 +1,445 @@
1
+ /**
2
+ * Middleware Factories — High-level composable middleware for Cloudflare Workers
3
+ *
4
+ * These factory functions create middleware objects compatible with both
5
+ * the MiddlewareComposer lifecycle (preprocess/authenticate/validate/postprocess)
6
+ * and the EnhancedRouter's .use() method.
7
+ *
8
+ * @example
9
+ * import {
10
+ * createCorsMiddleware,
11
+ * createRateLimitGuard,
12
+ * createErrorHandler,
13
+ * createLogger,
14
+ * composeMiddleware
15
+ * } from '@tamyla/clodo-framework';
16
+ *
17
+ * const middleware = composeMiddleware(
18
+ * createCorsMiddleware({ origins: ['*'] }),
19
+ * createLogger({ level: 'info' }),
20
+ * createRateLimitGuard({ maxRequests: 100, windowMs: 60000 }),
21
+ * createErrorHandler({ includeStack: false })
22
+ * );
23
+ *
24
+ * @module @tamyla/clodo-framework/middleware/factories
25
+ */
26
+
27
+ // ─── CORS Middleware ───────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Create CORS middleware
31
+ * @param {Object} [options]
32
+ * @param {string|string[]} [options.origins='*'] - Allowed origins
33
+ * @param {string[]} [options.methods=['GET','POST','PUT','DELETE','PATCH','OPTIONS']] - Allowed methods
34
+ * @param {string[]} [options.headers=['Content-Type','Authorization']] - Allowed headers
35
+ * @param {boolean} [options.credentials=false] - Allow credentials
36
+ * @param {number} [options.maxAge=86400] - Preflight cache duration (seconds)
37
+ * @returns {Object} Middleware object with preprocess and postprocess
38
+ */
39
+ export function createCorsMiddleware(options = {}) {
40
+ const origins = options.origins || options.origin || '*';
41
+ const allowOrigin = Array.isArray(origins) ? origins : [origins];
42
+ const methods = (options.methods || ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']).join(', ');
43
+ const headers = (options.headers || ['Content-Type', 'Authorization']).join(', ');
44
+ const credentials = options.credentials || false;
45
+ const maxAge = String(options.maxAge || 86400);
46
+ function getOriginHeader(requestOrigin) {
47
+ if (allowOrigin.includes('*')) return '*';
48
+ if (allowOrigin.includes(requestOrigin)) return requestOrigin;
49
+ return null;
50
+ }
51
+ return {
52
+ preprocess(request) {
53
+ const requestOrigin = request.headers.get('Origin') || '';
54
+ const origin = getOriginHeader(requestOrigin);
55
+ if (request.method === 'OPTIONS') {
56
+ const h = new Headers();
57
+ if (origin) h.set('Access-Control-Allow-Origin', origin);
58
+ h.set('Access-Control-Allow-Methods', methods);
59
+ h.set('Access-Control-Allow-Headers', headers);
60
+ h.set('Access-Control-Max-Age', maxAge);
61
+ if (credentials) h.set('Access-Control-Allow-Credentials', 'true');
62
+ return new Response(null, {
63
+ status: 204,
64
+ headers: h
65
+ });
66
+ }
67
+ return null; // pass through to next middleware
68
+ },
69
+ postprocess(response) {
70
+ const h = new Headers(response.headers);
71
+ const origin = allowOrigin.includes('*') ? '*' : allowOrigin[0];
72
+ h.set('Access-Control-Allow-Origin', origin);
73
+ if (credentials) h.set('Access-Control-Allow-Credentials', 'true');
74
+ return new Response(response.body, {
75
+ status: response.status,
76
+ statusText: response.statusText,
77
+ headers: h
78
+ });
79
+ }
80
+ };
81
+ }
82
+
83
+ // ─── Error Handler Middleware ──────────────────────────────────────────
84
+
85
+ /**
86
+ * Create error handler middleware
87
+ * @param {Object} [options]
88
+ * @param {boolean} [options.includeStack=false] - Include stack trace in response
89
+ * @param {boolean} [options.logErrors=true] - Log errors to console
90
+ * @param {Function} [options.onError] - Custom error handler: (error, request) => Response | null
91
+ * @returns {Object} Middleware object
92
+ */
93
+ export function createErrorHandler(options = {}) {
94
+ const includeStack = options.includeStack || false;
95
+ const logErrors = options.logErrors !== false;
96
+ const onError = options.onError || null;
97
+ return {
98
+ /**
99
+ * Wraps the handler execution — this is used by the compose function
100
+ * to catch errors from the handler and any downstream middleware.
101
+ */
102
+ async wrapHandler(request, handler) {
103
+ try {
104
+ return await handler(request);
105
+ } catch (error) {
106
+ if (logErrors) {
107
+ console.error(`[ErrorHandler] ${error.message}`, error.stack);
108
+ }
109
+
110
+ // Custom error handler
111
+ if (onError) {
112
+ const custom = onError(error, request);
113
+ if (custom instanceof Response) return custom;
114
+ }
115
+ const status = error.status || error.statusCode || 500;
116
+ const body = {
117
+ error: error.message || 'Internal Server Error',
118
+ status
119
+ };
120
+ if (includeStack && error.stack) {
121
+ body.stack = error.stack;
122
+ }
123
+ return new Response(JSON.stringify(body), {
124
+ status,
125
+ headers: {
126
+ 'Content-Type': 'application/json'
127
+ }
128
+ });
129
+ }
130
+ }
131
+ };
132
+ }
133
+
134
+ // ─── Rate Limit Guard Middleware ──────────────────────────────────────
135
+
136
+ /**
137
+ * Create rate-limiting middleware using a simple in-memory token bucket.
138
+ * For production use with multiple Workers instances, back this with KV or Durable Objects.
139
+ *
140
+ * @param {Object} [options]
141
+ * @param {number} [options.maxRequests=100] - Max requests per window
142
+ * @param {number} [options.windowMs=60000] - Window duration in milliseconds
143
+ * @param {Function} [options.keyFn] - Function to extract rate-limit key from request (default: IP)
144
+ * @param {Object} [options.kvBinding] - Optional KV namespace for distributed rate limiting
145
+ * @returns {Object} Middleware object with preprocess
146
+ */
147
+ export function createRateLimitGuard(options = {}) {
148
+ const maxRequests = options.maxRequests || 100;
149
+ const windowMs = options.windowMs || 60000;
150
+ const keyFn = options.keyFn || (request => request.headers.get('CF-Connecting-IP') || 'unknown');
151
+ const buckets = new Map();
152
+ function getBucket(key) {
153
+ const now = Date.now();
154
+ let bucket = buckets.get(key);
155
+ if (!bucket || now - bucket.windowStart > windowMs) {
156
+ bucket = {
157
+ windowStart: now,
158
+ count: 0
159
+ };
160
+ buckets.set(key, bucket);
161
+ }
162
+ return bucket;
163
+ }
164
+
165
+ // Periodic cleanup to prevent memory leaks (every 10 windows)
166
+ let cleanupCounter = 0;
167
+ function maybeCleanup() {
168
+ if (++cleanupCounter % (maxRequests * 10) === 0) {
169
+ const now = Date.now();
170
+ for (const [key, bucket] of buckets.entries()) {
171
+ if (now - bucket.windowStart > windowMs * 2) {
172
+ buckets.delete(key);
173
+ }
174
+ }
175
+ }
176
+ }
177
+ return {
178
+ preprocess(request) {
179
+ const key = keyFn(request);
180
+ const bucket = getBucket(key);
181
+ maybeCleanup();
182
+ bucket.count++;
183
+ if (bucket.count > maxRequests) {
184
+ const retryAfter = Math.ceil((bucket.windowStart + windowMs - Date.now()) / 1000);
185
+ return new Response(JSON.stringify({
186
+ error: 'Too Many Requests',
187
+ retryAfter
188
+ }), {
189
+ status: 429,
190
+ headers: {
191
+ 'Content-Type': 'application/json',
192
+ 'Retry-After': String(Math.max(retryAfter, 1)),
193
+ 'X-RateLimit-Limit': String(maxRequests),
194
+ 'X-RateLimit-Remaining': '0',
195
+ 'X-RateLimit-Reset': String(Math.ceil((bucket.windowStart + windowMs) / 1000))
196
+ }
197
+ });
198
+ }
199
+
200
+ // Add rate limit headers to request for downstream use
201
+ request._rateLimitRemaining = maxRequests - bucket.count;
202
+ return null; // pass through
203
+ },
204
+ postprocess(response, request) {
205
+ // Add rate limit info headers to every response
206
+ const h = new Headers(response.headers);
207
+ h.set('X-RateLimit-Limit', String(maxRequests));
208
+ if (request?._rateLimitRemaining !== undefined) {
209
+ h.set('X-RateLimit-Remaining', String(request._rateLimitRemaining));
210
+ }
211
+ return new Response(response.body, {
212
+ status: response.status,
213
+ statusText: response.statusText,
214
+ headers: h
215
+ });
216
+ }
217
+ };
218
+ }
219
+
220
+ // ─── Logger Middleware ────────────────────────────────────────────────
221
+
222
+ /**
223
+ * Create a logging middleware
224
+ * @param {Object} [options]
225
+ * @param {string} [options.level='info'] - Log level: 'debug' | 'info' | 'warn' | 'error'
226
+ * @param {string} [options.prefix=''] - Log prefix
227
+ * @param {boolean} [options.includeHeaders=false] - Log request headers
228
+ * @param {boolean} [options.includeLatency=true] - Log response latency
229
+ * @param {Function} [options.logger=console] - Custom logger
230
+ * @returns {Object} Middleware object
231
+ */
232
+ export function createLogger(options = {}) {
233
+ const level = options.level || 'info';
234
+ const prefix = options.prefix ? `[${options.prefix}] ` : '';
235
+ const includeHeaders = options.includeHeaders || false;
236
+ const includeLatency = options.includeLatency !== false;
237
+ const logger = options.logger || console;
238
+ const levels = {
239
+ debug: 0,
240
+ info: 1,
241
+ warn: 2,
242
+ error: 3
243
+ };
244
+ const currentLevel = levels[level] ?? 1;
245
+ function log(lvl, ...args) {
246
+ if ((levels[lvl] ?? 1) >= currentLevel) {
247
+ (logger[lvl] || logger.log)(...args);
248
+ }
249
+ }
250
+ return {
251
+ preprocess(request) {
252
+ const url = new URL(request.url);
253
+ const logLine = `${prefix}→ ${request.method} ${url.pathname}${url.search}`;
254
+ log('info', logLine);
255
+ if (includeHeaders) {
256
+ const headers = Object.fromEntries(request.headers.entries());
257
+ log('debug', `${prefix} Headers:`, headers);
258
+ }
259
+
260
+ // Store start time for latency calculation
261
+ request._startTime = Date.now();
262
+ return null; // pass through
263
+ },
264
+ postprocess(response) {
265
+ if (includeLatency && response) {
266
+ const latency = Date.now() - (response._startTime || Date.now());
267
+ log('info', `${prefix}← ${response.status} (${latency}ms)`);
268
+ }
269
+ return response;
270
+ }
271
+ };
272
+ }
273
+
274
+ // ─── Bearer Auth Middleware ──────────────────────────────────────────
275
+
276
+ /**
277
+ * Create bearer token authentication middleware
278
+ * @param {Object} options
279
+ * @param {string|Function} options.token - Expected token string, or async (token, request) => boolean validator
280
+ * @param {string} [options.realm='API'] - WWW-Authenticate realm
281
+ * @param {string} [options.headerName='Authorization'] - Header to check
282
+ * @returns {Object} Middleware object with authenticate
283
+ */
284
+ export function createBearerAuth(options = {}) {
285
+ const tokenValidator = typeof options.token === 'function' ? options.token : t => t === options.token;
286
+ const realm = options.realm || 'API';
287
+ const headerName = options.headerName || 'Authorization';
288
+ return {
289
+ async authenticate(request) {
290
+ const header = request.headers.get(headerName);
291
+ if (!header || !header.startsWith('Bearer ')) {
292
+ return new Response(JSON.stringify({
293
+ error: 'Authentication required'
294
+ }), {
295
+ status: 401,
296
+ headers: {
297
+ 'Content-Type': 'application/json',
298
+ 'WWW-Authenticate': `Bearer realm="${realm}"`
299
+ }
300
+ });
301
+ }
302
+ const token = header.slice(7); // remove 'Bearer '
303
+ const valid = await tokenValidator(token, request);
304
+ if (!valid) {
305
+ return new Response(JSON.stringify({
306
+ error: 'Invalid token'
307
+ }), {
308
+ status: 403,
309
+ headers: {
310
+ 'Content-Type': 'application/json'
311
+ }
312
+ });
313
+ }
314
+ return null; // authenticated — pass through
315
+ }
316
+ };
317
+ }
318
+
319
+ // ─── API Key Middleware ──────────────────────────────────────────────
320
+
321
+ /**
322
+ * Create API key authentication middleware
323
+ * @param {Object} options
324
+ * @param {string|string[]|Function} options.keys - Valid API key(s) or async validator function
325
+ * @param {string} [options.headerName='X-API-Key'] - Header to check
326
+ * @param {string} [options.queryParam] - Optional query parameter name to check
327
+ * @returns {Object} Middleware object with authenticate
328
+ */
329
+ export function createApiKeyAuth(options = {}) {
330
+ const keys = Array.isArray(options.keys) ? options.keys : [options.keys];
331
+ const validator = typeof options.keys === 'function' ? options.keys : k => keys.includes(k);
332
+ const headerName = options.headerName || 'X-API-Key';
333
+ const queryParam = options.queryParam;
334
+ return {
335
+ async authenticate(request) {
336
+ let key = request.headers.get(headerName);
337
+
338
+ // Fallback to query parameter if configured
339
+ if (!key && queryParam) {
340
+ const url = new URL(request.url);
341
+ key = url.searchParams.get(queryParam);
342
+ }
343
+ if (!key) {
344
+ return new Response(JSON.stringify({
345
+ error: 'API key required'
346
+ }), {
347
+ status: 401,
348
+ headers: {
349
+ 'Content-Type': 'application/json'
350
+ }
351
+ });
352
+ }
353
+ const valid = await validator(key, request);
354
+ if (!valid) {
355
+ return new Response(JSON.stringify({
356
+ error: 'Invalid API key'
357
+ }), {
358
+ status: 403,
359
+ headers: {
360
+ 'Content-Type': 'application/json'
361
+ }
362
+ });
363
+ }
364
+ return null; // authenticated
365
+ }
366
+ };
367
+ }
368
+
369
+ // ─── Compose Middleware ──────────────────────────────────────────────
370
+
371
+ /**
372
+ * Compose multiple middleware into a single executable middleware chain.
373
+ * This is the recommended way to combine middleware for use with the router.
374
+ *
375
+ * Execution order:
376
+ * preprocess → authenticate → validate → handler → postprocess (reverse)
377
+ *
378
+ * Any phase returning a Response short-circuits the chain.
379
+ *
380
+ * @param {...Object} middlewares - Middleware objects
381
+ * @returns {Object} Composed middleware with execute(request, handler) method
382
+ *
383
+ * @example
384
+ * const composed = composeMiddleware(
385
+ * createCorsMiddleware({ origins: ['*'] }),
386
+ * createLogger({ prefix: 'api' }),
387
+ * createRateLimitGuard({ maxRequests: 100 }),
388
+ * createErrorHandler()
389
+ * );
390
+ *
391
+ * // Use with router
392
+ * router.use(composed);
393
+ *
394
+ * // Or use directly
395
+ * const response = await composed.execute(request, handler);
396
+ */
397
+ export function composeMiddleware(...middlewares) {
398
+ const chain = middlewares.filter(Boolean);
399
+ return {
400
+ async execute(request, handler) {
401
+ // Find error handler if one exists (it wraps the handler)
402
+ const errorHandler = chain.find(m => typeof m.wrapHandler === 'function');
403
+
404
+ // Pre-handler phases (in order)
405
+ for (const m of chain) {
406
+ if (typeof m.preprocess === 'function') {
407
+ const res = await m.preprocess(request);
408
+ if (res instanceof Response) return res;
409
+ }
410
+ }
411
+ for (const m of chain) {
412
+ if (typeof m.authenticate === 'function') {
413
+ const res = await m.authenticate(request);
414
+ if (res instanceof Response) return res;
415
+ }
416
+ }
417
+ for (const m of chain) {
418
+ if (typeof m.validate === 'function') {
419
+ const res = await m.validate(request);
420
+ if (res instanceof Response) return res;
421
+ }
422
+ }
423
+
424
+ // Execute handler (wrapped by error handler if present)
425
+ let response;
426
+ if (errorHandler) {
427
+ response = await errorHandler.wrapHandler(request, handler);
428
+ } else {
429
+ response = await handler(request);
430
+ }
431
+
432
+ // Post-handler phase (in reverse order)
433
+ for (const m of chain.slice().reverse()) {
434
+ if (typeof m.postprocess === 'function') {
435
+ const updated = await m.postprocess(response);
436
+ if (updated instanceof Response) response = updated;
437
+ }
438
+ }
439
+ return response;
440
+ },
441
+ // Also expose as a preprocess-only middleware for nesting
442
+ preprocess: chain.length === 1 && chain[0].preprocess ? chain[0].preprocess : undefined,
443
+ postprocess: chain.length === 1 && chain[0].postprocess ? chain[0].postprocess : undefined
444
+ };
445
+ }
@@ -1,3 +1,6 @@
1
1
  export { MiddlewareRegistry } from './Registry.js';
2
2
  export { MiddlewareComposer } from './Composer.js';
3
- export * as Shared from './shared/index.js';
3
+ export * as Shared from './shared/index.js';
4
+
5
+ // High-level middleware factories (recommended API)
6
+ export { createCorsMiddleware, createErrorHandler, createRateLimitGuard, createLogger, createBearerAuth, createApiKeyAuth, composeMiddleware } from './factories.js';
@@ -60,7 +60,9 @@ export class ModuleManager {
60
60
  });
61
61
  });
62
62
  }
63
- console.log(`✅ Registered module: ${moduleName}`);
63
+ if (typeof process !== 'undefined' && process.env?.DEBUG) {
64
+ console.log(`✅ Registered module: ${moduleName}`);
65
+ }
64
66
  }
65
67
 
66
68
  /**
@@ -665,4 +667,6 @@ moduleManager.registerModule('logging', {
665
667
  }
666
668
  }
667
669
  });
668
- console.log('✅ Module Manager initialized with core modules');
670
+
671
+ // Module Manager initialized with 3 default modules (auth, files, logging)
672
+ // Set DEBUG=true to see registration logs