@umituz/web-cloudflare 1.4.3 → 1.4.5

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 (44) hide show
  1. package/README.md +24 -29
  2. package/package.json +6 -5
  3. package/src/config/patterns.ts +43 -24
  4. package/src/domain/entities/analytics.entity.ts +18 -35
  5. package/src/domain/entities/d1.entity.ts +27 -0
  6. package/src/domain/entities/image.entity.ts +27 -27
  7. package/src/domain/entities/kv.entity.ts +20 -17
  8. package/src/domain/entities/r2.entity.ts +49 -0
  9. package/src/domain/entities/worker.entity.ts +35 -0
  10. package/src/domains/analytics/entities/index.ts +47 -0
  11. package/src/domains/analytics/index.ts +13 -0
  12. package/src/{infrastructure/services/analytics → domains/analytics/services}/analytics.service.ts +1 -1
  13. package/src/{infrastructure/services/analytics → domains/analytics/services}/index.ts +1 -0
  14. package/src/domains/analytics/types/index.ts +5 -0
  15. package/src/domains/analytics/types/service.interface.ts +12 -0
  16. package/src/domains/images/entities/index.ts +48 -0
  17. package/src/domains/images/index.ts +13 -0
  18. package/src/{infrastructure/services/images → domains/images/services}/images.service.ts +3 -3
  19. package/src/{infrastructure/services/images → domains/images/services}/index.ts +1 -0
  20. package/src/domains/images/types/index.ts +5 -0
  21. package/src/domains/images/types/service.interface.ts +13 -0
  22. package/src/domains/kv/entities/index.ts +34 -0
  23. package/src/domains/kv/index.ts +13 -0
  24. package/src/{infrastructure/services/kv → domains/kv/services}/index.ts +1 -0
  25. package/src/{infrastructure/services/kv → domains/kv/services}/kv.service.ts +2 -2
  26. package/src/domains/kv/types/index.ts +5 -0
  27. package/src/domains/kv/types/service.interface.ts +13 -0
  28. package/src/domains/workers/entities/index.ts +1 -1
  29. package/src/domains/workflows/entities/index.ts +60 -0
  30. package/src/domains/workflows/index.ts +10 -0
  31. package/src/domains/workflows/services/index.ts +6 -0
  32. package/src/{infrastructure/services/workflows/index.ts → domains/workflows/services/workflows.service.ts} +1 -1
  33. package/src/domains/wrangler/entities/index.ts +2 -2
  34. package/src/domains/wrangler/services/wrangler.service.ts +16 -8
  35. package/src/domains/wrangler/types/service.interface.ts +2 -2
  36. package/src/index.ts +4 -4
  37. package/src/infrastructure/middleware/auth.ts +118 -0
  38. package/src/infrastructure/middleware/cache.ts +95 -0
  39. package/src/infrastructure/middleware/cors.ts +95 -0
  40. package/src/infrastructure/middleware/index.ts +20 -3
  41. package/src/infrastructure/middleware/rate-limit.ts +105 -0
  42. package/src/infrastructure/router/index.ts +26 -4
  43. package/src/infrastructure/utils/helpers.ts +25 -11
  44. package/src/infrastructure/utils/utils.util.ts +3 -2
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Cache Middleware
3
+ * @description Caching middleware for Cloudflare Workers
4
+ */
5
+
6
+ export interface CacheConfig {
7
+ enabled: boolean;
8
+ defaultTTL: number;
9
+ paths?: Record<string, number>;
10
+ prefix?: string;
11
+ bypassPaths?: string[];
12
+ respectHeaders?: boolean;
13
+ }
14
+
15
+ const cacheStore = new Map<string, { response: Response; expires: number }>();
16
+
17
+ /**
18
+ * Cache middleware
19
+ */
20
+ export async function cache(
21
+ request: Request,
22
+ config: CacheConfig
23
+ ): Promise<Response | null> {
24
+ if (!config.enabled) {
25
+ return null;
26
+ }
27
+
28
+ const url = new URL(request.url);
29
+ const cacheKey = `${config.prefix || 'cache'}:${url.pathname}${url.search}`;
30
+
31
+ // Check if path should bypass cache
32
+ if (config.bypassPaths?.some((path) => url.pathname.startsWith(path))) {
33
+ return null;
34
+ }
35
+
36
+ // Check cache
37
+ const cached = cacheStore.get(cacheKey);
38
+ if (cached && cached.expires > Date.now()) {
39
+ return cached.response;
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * Set cache
47
+ */
48
+ export function setCache(
49
+ request: Request,
50
+ response: Response,
51
+ config: CacheConfig
52
+ ): void {
53
+ if (!config.enabled) {
54
+ return;
55
+ }
56
+
57
+ const url = new URL(request.url);
58
+ const cacheKey = `${config.prefix || 'cache'}:${url.pathname}${url.search}`;
59
+
60
+ // Determine TTL
61
+ let ttl = config.defaultTTL;
62
+ for (const [path, pathTTL] of Object.entries(config.paths || {})) {
63
+ if (url.pathname.startsWith(path)) {
64
+ ttl = pathTTL;
65
+ break;
66
+ }
67
+ }
68
+
69
+ // Don't cache if TTL is 0
70
+ if (ttl === 0) {
71
+ return;
72
+ }
73
+
74
+ // Cache the response
75
+ cacheStore.set(cacheKey, {
76
+ response: response.clone(),
77
+ expires: Date.now() + ttl * 1000,
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Invalidate cache
83
+ */
84
+ export function invalidateCache(pattern?: string): void {
85
+ if (!pattern) {
86
+ cacheStore.clear();
87
+ return;
88
+ }
89
+
90
+ for (const key of cacheStore.keys()) {
91
+ if (key.includes(pattern)) {
92
+ cacheStore.delete(key);
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * CORS Middleware
3
+ * @description Cross-Origin Resource Sharing middleware for Cloudflare Workers
4
+ */
5
+
6
+ export interface CORSConfig {
7
+ enabled: boolean;
8
+ allowedOrigins: string[];
9
+ allowedMethods: string[];
10
+ allowedHeaders: string[];
11
+ exposedHeaders?: string[];
12
+ allowCredentials?: boolean;
13
+ maxAge?: number;
14
+ }
15
+
16
+ /**
17
+ * Add CORS headers to response
18
+ */
19
+ export function addCorsHeaders(
20
+ request: Request,
21
+ response: Response,
22
+ config: CORSConfig
23
+ ): Response {
24
+ if (!config.enabled) {
25
+ return response;
26
+ }
27
+
28
+ const headers = new Headers(response.headers);
29
+ const origin = request.headers.get('Origin');
30
+
31
+ // Check if origin is allowed
32
+ const allowedOrigin = config.allowedOrigins.includes('*')
33
+ ? '*'
34
+ : config.allowedOrigins.includes(origin || '')
35
+ ? origin
36
+ : config.allowedOrigins[0];
37
+
38
+ headers.set('Access-Control-Allow-Origin', allowedOrigin);
39
+ headers.set('Access-Control-Allow-Methods', config.allowedMethods.join(', '));
40
+ headers.set('Access-Control-Allow-Headers', config.allowedHeaders.join(', '));
41
+
42
+ if (config.exposedHeaders) {
43
+ headers.set('Access-Control-Expose-Headers', config.exposedHeaders.join(', '));
44
+ }
45
+
46
+ if (config.allowCredentials) {
47
+ headers.set('Access-Control-Allow-Credentials', 'true');
48
+ }
49
+
50
+ if (config.maxAge) {
51
+ headers.set('Access-Control-Max-Age', config.maxAge.toString());
52
+ }
53
+
54
+ return new Response(response.body, {
55
+ status: response.status,
56
+ statusText: response.statusText,
57
+ headers,
58
+ });
59
+ }
60
+
61
+ /**
62
+ * CORS middleware
63
+ */
64
+ export async function cors(
65
+ request: Request,
66
+ config: CORSConfig
67
+ ): Promise<Response | null> {
68
+ if (!config.enabled) {
69
+ return null;
70
+ }
71
+
72
+ // Handle preflight request
73
+ if (request.method === 'OPTIONS') {
74
+ const headers = new Headers();
75
+ const origin = request.headers.get('Origin');
76
+
77
+ const allowedOrigin = config.allowedOrigins.includes('*')
78
+ ? '*'
79
+ : config.allowedOrigins.includes(origin || '')
80
+ ? origin
81
+ : config.allowedOrigins[0];
82
+
83
+ headers.set('Access-Control-Allow-Origin', allowedOrigin);
84
+ headers.set('Access-Control-Allow-Methods', config.allowedMethods.join(', '));
85
+ headers.set('Access-Control-Allow-Headers', config.allowedHeaders.join(', '));
86
+
87
+ if (config.maxAge) {
88
+ headers.set('Access-Control-Max-Age', config.maxAge.toString());
89
+ }
90
+
91
+ return new Response(null, { headers });
92
+ }
93
+
94
+ return null;
95
+ }
@@ -3,6 +3,23 @@
3
3
  * @description Comprehensive middleware for Cloudflare Workers
4
4
  */
5
5
 
6
+ // ============================================================
7
+ // Environment Types
8
+ // ============================================================
9
+
10
+ export interface CloudflareMiddlewareEnv {
11
+ KV?: KVNamespace;
12
+ R2?: R2Bucket;
13
+ D1?: D1Database;
14
+ DO?: Record<string, DurableObjectNamespace>;
15
+ QUEUE?: Record<string, Queue>;
16
+ AI?: any;
17
+ vars?: Record<string, string>;
18
+ }
19
+
20
+ // Type alias for backwards compatibility
21
+ export type Env = CloudflareMiddlewareEnv;
22
+
6
23
  // Re-export existing middleware
7
24
  export { cors, addCorsHeaders } from './cors';
8
25
  export { cache, setCache, invalidateCache } from './cache';
@@ -300,7 +317,7 @@ export interface HealthCheckConfig {
300
317
  }
301
318
 
302
319
  export async function healthCheck(
303
- env: Env,
320
+ env: CloudflareMiddlewareEnv,
304
321
  config?: HealthCheckConfig
305
322
  ): Promise<Response> {
306
323
  const checks: Record<string, boolean | string> = {
@@ -357,9 +374,9 @@ export function handleMiddlewareError(
357
374
  }
358
375
 
359
376
  /**
360
- * Conditional Middleware
377
+ * Conditional Middleware (Chain)
361
378
  */
362
- export function conditionalMiddleware(
379
+ export function conditionalChainMiddleware(
363
380
  condition: (request: Request) => boolean,
364
381
  middleware: (request: Request) => Response | null
365
382
  ): (request: Request) => Response | null {
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Rate Limit Middleware
3
+ * @description Rate limiting middleware for Cloudflare Workers
4
+ */
5
+
6
+ export interface RateLimitConfig {
7
+ enabled: boolean;
8
+ maxRequests: number;
9
+ window: number;
10
+ by?: 'ip' | 'user' | 'both';
11
+ customKeys?: string[];
12
+ whitelist?: string[];
13
+ response?: {
14
+ status: number;
15
+ message: string;
16
+ retryAfter?: number;
17
+ };
18
+ }
19
+
20
+ interface RateLimitEntry {
21
+ count: number;
22
+ resetTime: number;
23
+ }
24
+
25
+ const rateLimitStore = new Map<string, RateLimitEntry>();
26
+
27
+ /**
28
+ * Get rate limit key
29
+ */
30
+ function getRateLimitKey(request: Request, config: RateLimitConfig): string {
31
+ const parts: string[] = [];
32
+
33
+ if (config.by === 'ip' || config.by === 'both') {
34
+ parts.push(request.headers.get('CF-Connecting-IP') || 'unknown');
35
+ }
36
+
37
+ if (config.by === 'user' || config.by === 'both') {
38
+ const auth = request.headers.get('Authorization');
39
+ if (auth) {
40
+ parts.push(auth.substring(0, 20));
41
+ }
42
+ }
43
+
44
+ if (config.customKeys) {
45
+ for (const key of config.customKeys) {
46
+ parts.push(request.headers.get(key) || '');
47
+ }
48
+ }
49
+
50
+ return parts.join(':') || 'default';
51
+ }
52
+
53
+ /**
54
+ * Check rate limit
55
+ */
56
+ export async function checkRateLimit(
57
+ request: Request,
58
+ config: RateLimitConfig
59
+ ): Promise<Response | null> {
60
+ if (!config.enabled) {
61
+ return null;
62
+ }
63
+
64
+ const key = getRateLimitKey(request, config);
65
+
66
+ // Check whitelist
67
+ if (config.whitelist?.includes(key)) {
68
+ return null;
69
+ }
70
+
71
+ const now = Date.now();
72
+ const entry = rateLimitStore.get(key);
73
+
74
+ // Reset if window expired
75
+ if (!entry || now > entry.resetTime) {
76
+ rateLimitStore.set(key, {
77
+ count: 1,
78
+ resetTime: now + config.window * 1000,
79
+ });
80
+ return null;
81
+ }
82
+
83
+ // Increment count
84
+ entry.count++;
85
+
86
+ // Check if exceeded
87
+ if (entry.count > config.maxRequests) {
88
+ const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
89
+ return new Response(
90
+ JSON.stringify({
91
+ error: config.response?.message || 'Rate limit exceeded',
92
+ retryAfter,
93
+ }),
94
+ {
95
+ status: config.response?.status || 429,
96
+ headers: {
97
+ 'Content-Type': 'application/json',
98
+ 'Retry-After': retryAfter.toString(),
99
+ },
100
+ }
101
+ );
102
+ }
103
+
104
+ return null;
105
+ }
@@ -5,6 +5,23 @@
5
5
 
6
6
  import { json, notFound, badRequest } from '../utils/helpers';
7
7
 
8
+ // ============================================================
9
+ // Environment Types
10
+ // ============================================================
11
+
12
+ export interface CloudflareEnv {
13
+ KV?: KVNamespace;
14
+ R2?: R2Bucket;
15
+ D1?: D1Database;
16
+ DO?: Record<string, DurableObjectNamespace>;
17
+ QUEUE?: Record<string, Queue>;
18
+ AI?: any;
19
+ vars?: Record<string, string>;
20
+ }
21
+
22
+ // Type alias for backwards compatibility
23
+ export type Env = CloudflareEnv;
24
+
8
25
  // ============================================================
9
26
  // Route Handler Types
10
27
  // ============================================================
@@ -12,13 +29,13 @@ import { json, notFound, badRequest } from '../utils/helpers';
12
29
  export type RouteHandler = (
13
30
  request: Request,
14
31
  params?: Record<string, string>,
15
- env?: Env,
32
+ env?: CloudflareEnv,
16
33
  ctx?: ExecutionContext
17
34
  ) => Promise<Response> | Response;
18
35
 
19
36
  export type Middleware = (
20
37
  request: Request,
21
- env?: Env,
38
+ env?: CloudflareEnv,
22
39
  ctx?: ExecutionContext
23
40
  ) => Promise<Response | null> | Response | null;
24
41
 
@@ -147,7 +164,7 @@ export class Router {
147
164
  */
148
165
  async handle(
149
166
  request: Request,
150
- env?: Env,
167
+ env?: CloudflareEnv,
151
168
  ctx?: ExecutionContext
152
169
  ): Promise<Response> {
153
170
  const url = new URL(request.url);
@@ -499,7 +516,12 @@ export async function body<T = unknown>(request: Request): Promise<T> {
499
516
  */
500
517
  export function query(request: Request): Record<string, string> {
501
518
  const url = new URL(request.url);
502
- return Object.fromEntries(url.searchParams.entries());
519
+ const result: Record<string, string> = {};
520
+ // URLSearchParams.keys() is not available in Workers runtime
521
+ url.searchParams.forEach((value, key) => {
522
+ result[key] = value;
523
+ });
524
+ return result;
503
525
  }
504
526
 
505
527
  /**
@@ -19,7 +19,17 @@ export async function parseBody<T = unknown>(request: Request): Promise<T> {
19
19
 
20
20
  if (contentType.includes('application/x-www-form-urlencoded')) {
21
21
  const formData = await request.formData();
22
- return Object.fromEntries(formData) as T;
22
+ const result: Record<string, unknown> = {};
23
+ // FormData.keys() is not available in Workers runtime
24
+ // Use alternative approach with for...of
25
+ const keys: string[] = [];
26
+ formData.forEach((value, key) => {
27
+ if (!keys.includes(key)) {
28
+ keys.push(key);
29
+ }
30
+ result[key] = value;
31
+ });
32
+ return result as T;
23
33
  }
24
34
 
25
35
  if (contentType.includes('text/')) {
@@ -297,16 +307,15 @@ export function generateCacheKey(request: Request, prefix?: string): string {
297
307
  const parts = [prefix || 'cache', url.pathname];
298
308
 
299
309
  // Add query params (sorted for consistency)
300
- const sortedParams = Array.from(url.searchParams.entries()).sort(
301
- ([a], [b]) => a.localeCompare(b)
302
- );
310
+ const params: string[] = [];
311
+ // URLSearchParams.keys() is not available in Workers runtime
312
+ url.searchParams.forEach((value, key) => {
313
+ params.push(`${key}=${value}`);
314
+ });
315
+ params.sort();
303
316
 
304
- if (sortedParams.length > 0) {
305
- parts.push(
306
- sortedParams
307
- .map(([key, value]) => `${key}=${value}`)
308
- .join('&')
309
- );
317
+ if (params.length > 0) {
318
+ parts.push(params.join('&'));
310
319
  }
311
320
 
312
321
  // Add auth header if present (for user-specific caching)
@@ -479,7 +488,12 @@ export function buildURL(base: string, params: Record<string, string | number |
479
488
  */
480
489
  export function parseQueryParams(url: string): Record<string, string> {
481
490
  const params = new URL(url).searchParams;
482
- return Object.fromEntries(params.entries());
491
+ const result: Record<string, string> = {};
492
+ // URLSearchParams.keys() is not available in Workers runtime
493
+ params.forEach((value, key) => {
494
+ result[key] = value;
495
+ });
496
+ return result;
483
497
  }
484
498
 
485
499
  /**
@@ -115,12 +115,13 @@ export const transformUtils = {
115
115
  */
116
116
  async streamToBlob(stream: ReadableStream): Promise<Blob> {
117
117
  const reader = stream.getReader();
118
- const chunks: Uint8Array[] = [];
118
+ const chunks: BlobPart[] = [];
119
119
 
120
120
  while (true) {
121
121
  const { done, value } = await reader.read();
122
122
  if (done) break;
123
- chunks.push(value);
123
+ // Convert Uint8Array to BlobPart
124
+ chunks.push(value as BlobPart);
124
125
  }
125
126
 
126
127
  return new Blob(chunks);