@umituz/web-cloudflare 1.0.1
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/LICENSE +21 -0
- package/README.md +621 -0
- package/package.json +87 -0
- package/src/config/patterns.ts +469 -0
- package/src/config/types.ts +648 -0
- package/src/domain/entities/analytics.entity.ts +47 -0
- package/src/domain/entities/d1.entity.ts +37 -0
- package/src/domain/entities/image.entity.ts +48 -0
- package/src/domain/entities/index.ts +11 -0
- package/src/domain/entities/kv.entity.ts +34 -0
- package/src/domain/entities/r2.entity.ts +55 -0
- package/src/domain/entities/worker.entity.ts +35 -0
- package/src/domain/index.ts +7 -0
- package/src/domain/interfaces/index.ts +6 -0
- package/src/domain/interfaces/services.interface.ts +82 -0
- package/src/index.ts +53 -0
- package/src/infrastructure/constants/index.ts +13 -0
- package/src/infrastructure/domain/ai-gateway.entity.ts +169 -0
- package/src/infrastructure/domain/workflows.entity.ts +108 -0
- package/src/infrastructure/middleware/index.ts +405 -0
- package/src/infrastructure/router/index.ts +549 -0
- package/src/infrastructure/services/ai-gateway/index.ts +416 -0
- package/src/infrastructure/services/analytics/analytics.service.ts +189 -0
- package/src/infrastructure/services/analytics/index.ts +7 -0
- package/src/infrastructure/services/d1/d1.service.ts +191 -0
- package/src/infrastructure/services/d1/index.ts +7 -0
- package/src/infrastructure/services/images/images.service.ts +227 -0
- package/src/infrastructure/services/images/index.ts +7 -0
- package/src/infrastructure/services/kv/index.ts +7 -0
- package/src/infrastructure/services/kv/kv.service.ts +116 -0
- package/src/infrastructure/services/r2/index.ts +7 -0
- package/src/infrastructure/services/r2/r2.service.ts +164 -0
- package/src/infrastructure/services/workers/index.ts +7 -0
- package/src/infrastructure/services/workers/workers.service.ts +164 -0
- package/src/infrastructure/services/workflows/index.ts +437 -0
- package/src/infrastructure/utils/helpers.ts +732 -0
- package/src/infrastructure/utils/index.ts +6 -0
- package/src/infrastructure/utils/utils.util.ts +150 -0
- package/src/presentation/hooks/cloudflare.hooks.ts +314 -0
- package/src/presentation/hooks/index.ts +6 -0
- package/src/worker.example.ts +41 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Middleware Collection
|
|
3
|
+
* @description Comprehensive middleware for Cloudflare Workers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Re-export existing middleware
|
|
7
|
+
export { cors, addCorsHeaders } from './cors';
|
|
8
|
+
export { cache, setCache, invalidateCache } from './cache';
|
|
9
|
+
export { checkRateLimit } from './rate-limit';
|
|
10
|
+
export { requireAuth, addUserContext } from './auth';
|
|
11
|
+
|
|
12
|
+
// ============================================================
|
|
13
|
+
// New Middleware
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compression Middleware
|
|
18
|
+
*/
|
|
19
|
+
export async function compression(
|
|
20
|
+
request: Request,
|
|
21
|
+
response: Response
|
|
22
|
+
): Promise<Response> {
|
|
23
|
+
const acceptEncoding = request.headers.get('Accept-Encoding') || '';
|
|
24
|
+
const contentType = response.headers.get('Content-Type') || '';
|
|
25
|
+
|
|
26
|
+
// Check if client accepts compression
|
|
27
|
+
if (!acceptEncoding.includes('gzip') && !acceptEncoding.includes('br')) {
|
|
28
|
+
return response;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check if content type is compressible
|
|
32
|
+
const compressibleTypes = [
|
|
33
|
+
'text/',
|
|
34
|
+
'application/json',
|
|
35
|
+
'application/javascript',
|
|
36
|
+
'application/xml',
|
|
37
|
+
'application/xhtml+xml',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const isCompressible = compressibleTypes.some((type) =>
|
|
41
|
+
contentType.includes(type)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (!isCompressible) {
|
|
45
|
+
return response;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Return original response (Cloudflare handles compression automatically)
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Security Headers Middleware
|
|
54
|
+
*/
|
|
55
|
+
export interface SecurityHeadersConfig {
|
|
56
|
+
frameGuard?: boolean;
|
|
57
|
+
contentTypeNosniff?: boolean;
|
|
58
|
+
xssProtection?: boolean;
|
|
59
|
+
strictTransportSecurity?: boolean;
|
|
60
|
+
referrerPolicy?: string;
|
|
61
|
+
contentSecurityPolicy?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function addSecurityHeaders(
|
|
65
|
+
response: Response,
|
|
66
|
+
config: SecurityHeadersConfig = {}
|
|
67
|
+
): Response {
|
|
68
|
+
const headers = new Headers(response.headers);
|
|
69
|
+
|
|
70
|
+
if (config.frameGuard !== false) {
|
|
71
|
+
headers.set('X-Frame-Options', 'DENY');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (config.contentTypeNosniff !== false) {
|
|
75
|
+
headers.set('X-Content-Type-Options', 'nosniff');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (config.xssProtection !== false) {
|
|
79
|
+
headers.set('X-XSS-Protection', '1; mode=block');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (config.strictTransportSecurity) {
|
|
83
|
+
headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (config.referrerPolicy) {
|
|
87
|
+
headers.set('Referrer-Policy', config.referrerPolicy);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (config.contentSecurityPolicy) {
|
|
91
|
+
headers.set('Content-Security-Policy', config.contentSecurityPolicy);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return new Response(response.body, {
|
|
95
|
+
status: response.status,
|
|
96
|
+
statusText: response.statusText,
|
|
97
|
+
headers,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Bot Detection Middleware
|
|
103
|
+
*/
|
|
104
|
+
export async function detectBot(request: Request): Promise<{
|
|
105
|
+
isBot: boolean;
|
|
106
|
+
botType?: string;
|
|
107
|
+
}> {
|
|
108
|
+
const userAgent = request.headers.get('User-Agent') || '';
|
|
109
|
+
const botPatterns = [
|
|
110
|
+
/googlebot/i,
|
|
111
|
+
/bingbot/i,
|
|
112
|
+
/slurp/i,
|
|
113
|
+
/duckduckbot/i,
|
|
114
|
+
/baiduspider/i,
|
|
115
|
+
/yandexbot/i,
|
|
116
|
+
/facebookexternalhit/i,
|
|
117
|
+
/twitterbot/i,
|
|
118
|
+
/linkedinbot/i,
|
|
119
|
+
/whatsapp/i,
|
|
120
|
+
/telegrambot/i,
|
|
121
|
+
/applebot/i,
|
|
122
|
+
/semrushbot/i,
|
|
123
|
+
/ahrefsbot/i,
|
|
124
|
+
/mj12bot/i,
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
for (const pattern of botPatterns) {
|
|
128
|
+
if (pattern.test(userAgent)) {
|
|
129
|
+
return {
|
|
130
|
+
isBot: true,
|
|
131
|
+
botType: pattern.source.replace('/i', '').replace('/', ''),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { isBot: false };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Request Logging Middleware
|
|
141
|
+
*/
|
|
142
|
+
export interface LogConfig {
|
|
143
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
144
|
+
includeHeaders?: boolean;
|
|
145
|
+
includeBody?: boolean;
|
|
146
|
+
sampleRate?: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function logRequest(
|
|
150
|
+
request: Request,
|
|
151
|
+
config: LogConfig = { level: 'info' }
|
|
152
|
+
): Promise<void> {
|
|
153
|
+
// Sample rate check
|
|
154
|
+
if (config.sampleRate && Math.random() > config.sampleRate) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const url = new URL(request.url);
|
|
159
|
+
const logData: Record<string, unknown> = {
|
|
160
|
+
method: request.method,
|
|
161
|
+
url: url.pathname,
|
|
162
|
+
timestamp: Date.now(),
|
|
163
|
+
userAgent: request.headers.get('User-Agent'),
|
|
164
|
+
ip: request.headers.get('CF-Connecting-IP'),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (config.includeHeaders) {
|
|
168
|
+
const headers: Record<string, string> = {};
|
|
169
|
+
request.headers.forEach((value, key) => {
|
|
170
|
+
headers[key] = value;
|
|
171
|
+
});
|
|
172
|
+
logData.headers = headers;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (config.includeBody && request.method !== 'GET') {
|
|
176
|
+
try {
|
|
177
|
+
const clone = request.clone();
|
|
178
|
+
logData.body = await clone.json();
|
|
179
|
+
} catch {
|
|
180
|
+
// Body not JSON or already consumed
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
switch (config.level) {
|
|
185
|
+
case 'debug':
|
|
186
|
+
console.debug('[Request]', JSON.stringify(logData));
|
|
187
|
+
break;
|
|
188
|
+
case 'info':
|
|
189
|
+
console.info('[Request]', JSON.stringify(logData));
|
|
190
|
+
break;
|
|
191
|
+
case 'warn':
|
|
192
|
+
console.warn('[Request]', JSON.stringify(logData));
|
|
193
|
+
break;
|
|
194
|
+
case 'error':
|
|
195
|
+
console.error('[Request]', JSON.stringify(logData));
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Response Time Tracking Middleware
|
|
202
|
+
*/
|
|
203
|
+
export async function trackResponseTime(
|
|
204
|
+
handler: () => Promise<Response>
|
|
205
|
+
): Promise<{ response: Response; duration: number }> {
|
|
206
|
+
const start = Date.now();
|
|
207
|
+
const response = await handler();
|
|
208
|
+
const duration = Date.now() - start;
|
|
209
|
+
|
|
210
|
+
// Add timing header
|
|
211
|
+
response.headers.set('X-Response-Time', `${duration}ms`);
|
|
212
|
+
|
|
213
|
+
return { response, duration };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* IP Filter Middleware
|
|
218
|
+
*/
|
|
219
|
+
export interface IPFilterConfig {
|
|
220
|
+
mode: 'whitelist' | 'blacklist';
|
|
221
|
+
ips: string[];
|
|
222
|
+
cidrs?: string[];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function checkIPFilter(
|
|
226
|
+
request: Request,
|
|
227
|
+
config: IPFilterConfig
|
|
228
|
+
): Response | null {
|
|
229
|
+
const ip = request.headers.get('CF-Connecting-IP');
|
|
230
|
+
|
|
231
|
+
if (!ip) {
|
|
232
|
+
return new Response('IP not found', { status: 400 });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const isAllowed = config.ips.includes(ip);
|
|
236
|
+
|
|
237
|
+
if (config.mode === 'whitelist' && !isAllowed) {
|
|
238
|
+
return new Response('IP not allowed', { status: 403 });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (config.mode === 'blacklist' && isAllowed) {
|
|
242
|
+
return new Response('IP blocked', { status: 403 });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Method Override Middleware
|
|
250
|
+
*/
|
|
251
|
+
export function methodOverride(request: Request): Request {
|
|
252
|
+
const method = request.headers.get('X-HTTP-Method-Override');
|
|
253
|
+
const url = new URL(request.url);
|
|
254
|
+
const bodyMethod = url.searchParams.get('_method');
|
|
255
|
+
|
|
256
|
+
const overrideMethod = method || bodyMethod;
|
|
257
|
+
|
|
258
|
+
if (overrideMethod && ['PUT', 'PATCH', 'DELETE'].includes(overrideMethod.toUpperCase())) {
|
|
259
|
+
// Return modified request
|
|
260
|
+
return new Request(request.url, {
|
|
261
|
+
method: overrideMethod.toUpperCase(),
|
|
262
|
+
headers: request.headers,
|
|
263
|
+
body: request.body,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return request;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Request ID Middleware
|
|
272
|
+
*/
|
|
273
|
+
export function addRequestID(request: Request): string {
|
|
274
|
+
const existingID = request.headers.get('X-Request-ID');
|
|
275
|
+
|
|
276
|
+
if (existingID) {
|
|
277
|
+
return existingID;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const requestID = crypto.randomUUID();
|
|
281
|
+
return requestID;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Response Time Middleware
|
|
286
|
+
*/
|
|
287
|
+
export function responseTime(): Response {
|
|
288
|
+
const response = new Response(JSON.stringify({ time: Date.now() }), {
|
|
289
|
+
headers: { 'Content-Type': 'application/json' },
|
|
290
|
+
});
|
|
291
|
+
return response;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Health Check Middleware
|
|
296
|
+
*/
|
|
297
|
+
export interface HealthCheckConfig {
|
|
298
|
+
uptime: number;
|
|
299
|
+
checks: Record<string, () => Promise<boolean>>;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export async function healthCheck(
|
|
303
|
+
env: Env,
|
|
304
|
+
config?: HealthCheckConfig
|
|
305
|
+
): Promise<Response> {
|
|
306
|
+
const checks: Record<string, boolean | string> = {
|
|
307
|
+
healthy: true,
|
|
308
|
+
timestamp: new Date().toISOString(),
|
|
309
|
+
uptime: config?.uptime || process.uptime?.() || 0,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
if (config?.checks) {
|
|
313
|
+
for (const [name, check] of Object.entries(config.checks)) {
|
|
314
|
+
try {
|
|
315
|
+
checks[name] = await check();
|
|
316
|
+
} catch (error) {
|
|
317
|
+
checks[name] = error instanceof Error ? error.message : String(error);
|
|
318
|
+
checks.healthy = false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const status = checks.healthy ? 200 : 503;
|
|
324
|
+
return Response.json(checks, { status });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Error Handling Middleware
|
|
329
|
+
*/
|
|
330
|
+
export interface ErrorHandlerConfig {
|
|
331
|
+
debug: boolean;
|
|
332
|
+
logger?: (error: Error) => void;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function handleMiddlewareError(
|
|
336
|
+
error: Error,
|
|
337
|
+
config: ErrorHandlerConfig = { debug: false }
|
|
338
|
+
): Response {
|
|
339
|
+
if (config.logger) {
|
|
340
|
+
config.logger(error);
|
|
341
|
+
} else {
|
|
342
|
+
console.error('[Middleware Error]', error);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const status = 500;
|
|
346
|
+
const body: Record<string, unknown> = {
|
|
347
|
+
error: 'Internal Server Error',
|
|
348
|
+
status,
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
if (config.debug) {
|
|
352
|
+
body.message = error.message;
|
|
353
|
+
body.stack = error.stack;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return Response.json(body, { status });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Conditional Middleware
|
|
361
|
+
*/
|
|
362
|
+
export function conditionalMiddleware(
|
|
363
|
+
condition: (request: Request) => boolean,
|
|
364
|
+
middleware: (request: Request) => Response | null
|
|
365
|
+
): (request: Request) => Response | null {
|
|
366
|
+
return (request: Request) => {
|
|
367
|
+
if (condition(request)) {
|
|
368
|
+
return middleware(request);
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Chain Middleware
|
|
376
|
+
*/
|
|
377
|
+
export function chainMiddleware(
|
|
378
|
+
...middlewares: Array<(request: Request) => Response | null>
|
|
379
|
+
): (request: Request) => Response | null {
|
|
380
|
+
return (request: Request) => {
|
|
381
|
+
for (const middleware of middlewares) {
|
|
382
|
+
const response = middleware(request);
|
|
383
|
+
if (response) {
|
|
384
|
+
return response;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Async Middleware Chain
|
|
393
|
+
*/
|
|
394
|
+
export async function chainAsyncMiddleware(
|
|
395
|
+
request: Request,
|
|
396
|
+
...middlewares: Array<(request: Request) => Promise<Response | null>>
|
|
397
|
+
): Promise<Response | null> {
|
|
398
|
+
for (const middleware of middlewares) {
|
|
399
|
+
const response = await middleware(request);
|
|
400
|
+
if (response) {
|
|
401
|
+
return response;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
}
|