@umituz/web-cloudflare 1.4.4 → 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.
- package/package.json +2 -1
- package/src/config/patterns.ts +43 -24
- package/src/domain/entities/analytics.entity.ts +30 -0
- package/src/domain/entities/d1.entity.ts +27 -0
- package/src/domain/entities/image.entity.ts +48 -0
- package/src/domain/entities/kv.entity.ts +37 -0
- package/src/domain/entities/r2.entity.ts +49 -0
- package/src/domain/entities/worker.entity.ts +35 -0
- package/src/domains/analytics/entities/index.ts +2 -2
- package/src/domains/workers/entities/index.ts +1 -1
- package/src/domains/workflows/entities/index.ts +60 -0
- package/src/domains/wrangler/entities/index.ts +2 -2
- package/src/domains/wrangler/services/wrangler.service.ts +16 -8
- package/src/domains/wrangler/types/service.interface.ts +2 -2
- package/src/infrastructure/middleware/auth.ts +118 -0
- package/src/infrastructure/middleware/cache.ts +95 -0
- package/src/infrastructure/middleware/cors.ts +95 -0
- package/src/infrastructure/middleware/index.ts +20 -3
- package/src/infrastructure/middleware/rate-limit.ts +105 -0
- package/src/infrastructure/router/index.ts +26 -4
- package/src/infrastructure/utils/helpers.ts +25 -11
- package/src/infrastructure/utils/utils.util.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-cloudflare",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.5",
|
|
4
4
|
"description": "Comprehensive Cloudflare Workers integration with config-based patterns, middleware, router, workflows, and AI (Patch-only versioning: only z in x.y.z increments)",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"typescript": ">=5.0.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
+
"@cloudflare/workers-types": "^4.20260317.1",
|
|
78
79
|
"@types/node": "~22.13.10",
|
|
79
80
|
"typescript": "~5.9.2"
|
|
80
81
|
},
|
package/src/config/patterns.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* @description Reusable configuration patterns for different use cases
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { AIGatewayConfig } from '../
|
|
7
|
-
import type { WorkflowDefinition } from '../
|
|
6
|
+
import type { AIGatewayConfig } from '../domains/ai-gateway/entities';
|
|
7
|
+
import type { WorkflowDefinition } from '../domains/workflows/entities';
|
|
8
8
|
import type { WorkerConfig } from './types';
|
|
9
9
|
|
|
10
10
|
// ============================================================
|
|
@@ -106,6 +106,8 @@ export const saasConfig: Partial<WorkerConfig> = {
|
|
|
106
106
|
},
|
|
107
107
|
workflows: {
|
|
108
108
|
enabled: true,
|
|
109
|
+
maxExecutionTime: 300,
|
|
110
|
+
defaultRetries: 2,
|
|
109
111
|
},
|
|
110
112
|
};
|
|
111
113
|
|
|
@@ -149,6 +151,8 @@ export const cdnConfig: Partial<WorkerConfig> = {
|
|
|
149
151
|
},
|
|
150
152
|
rateLimit: {
|
|
151
153
|
enabled: false,
|
|
154
|
+
maxRequests: 0,
|
|
155
|
+
window: 0,
|
|
152
156
|
},
|
|
153
157
|
compression: {
|
|
154
158
|
enabled: true,
|
|
@@ -194,7 +198,7 @@ export const aiFirstConfig: Partial<WorkerConfig> = {
|
|
|
194
198
|
baseURL: 'https://api.openai.com/v1',
|
|
195
199
|
apiKey: '',
|
|
196
200
|
models: ['gpt-4', 'gpt-3.5-turbo'],
|
|
197
|
-
|
|
201
|
+
fallbackProvider: 'workers-ai',
|
|
198
202
|
weight: 1,
|
|
199
203
|
},
|
|
200
204
|
],
|
|
@@ -206,6 +210,8 @@ export const aiFirstConfig: Partial<WorkerConfig> = {
|
|
|
206
210
|
},
|
|
207
211
|
workflows: {
|
|
208
212
|
enabled: true,
|
|
213
|
+
maxExecutionTime: 600,
|
|
214
|
+
defaultRetries: 3,
|
|
209
215
|
},
|
|
210
216
|
};
|
|
211
217
|
|
|
@@ -215,13 +221,18 @@ export const aiFirstConfig: Partial<WorkerConfig> = {
|
|
|
215
221
|
export const minimalConfig: Partial<WorkerConfig> = {
|
|
216
222
|
cache: {
|
|
217
223
|
enabled: false,
|
|
224
|
+
defaultTTL: 0,
|
|
218
225
|
},
|
|
219
226
|
rateLimit: {
|
|
220
227
|
enabled: false,
|
|
228
|
+
maxRequests: 0,
|
|
229
|
+
window: 0,
|
|
221
230
|
},
|
|
222
231
|
cors: {
|
|
223
232
|
enabled: true,
|
|
224
233
|
allowedOrigins: ['*'],
|
|
234
|
+
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
235
|
+
allowedHeaders: ['*'],
|
|
225
236
|
},
|
|
226
237
|
};
|
|
227
238
|
|
|
@@ -234,26 +245,34 @@ export const minimalConfig: Partial<WorkerConfig> = {
|
|
|
234
245
|
*/
|
|
235
246
|
export function mergeConfigs<T extends Record<string, any>>(
|
|
236
247
|
base: T,
|
|
237
|
-
...overrides: Partial<
|
|
248
|
+
...overrides: Array<Partial<Record<string, any>>>
|
|
238
249
|
): T {
|
|
239
250
|
return overrides.reduce((acc, override) => {
|
|
240
251
|
return deepMerge(acc, override);
|
|
241
252
|
}, base);
|
|
242
253
|
}
|
|
243
254
|
|
|
244
|
-
function deepMerge<T
|
|
245
|
-
|
|
255
|
+
function deepMerge<T extends Record<string, any>>(
|
|
256
|
+
target: T,
|
|
257
|
+
source: Partial<Record<string, any>>
|
|
258
|
+
): T {
|
|
259
|
+
const output = { ...target };
|
|
246
260
|
|
|
247
261
|
if (isObject(target) && isObject(source)) {
|
|
248
262
|
Object.keys(source).forEach((key) => {
|
|
249
|
-
|
|
263
|
+
const sourceValue = source[key];
|
|
264
|
+
const targetValue = target[key as keyof T];
|
|
265
|
+
|
|
266
|
+
if (isObject(sourceValue)) {
|
|
250
267
|
if (!(key in target)) {
|
|
251
|
-
|
|
268
|
+
(output as any)[key] = sourceValue;
|
|
269
|
+
} else if (isObject(targetValue)) {
|
|
270
|
+
(output as any)[key] = deepMerge(targetValue, sourceValue);
|
|
252
271
|
} else {
|
|
253
|
-
(output as any)[key] =
|
|
272
|
+
(output as any)[key] = sourceValue;
|
|
254
273
|
}
|
|
255
274
|
} else {
|
|
256
|
-
|
|
275
|
+
(output as any)[key] = sourceValue;
|
|
257
276
|
}
|
|
258
277
|
});
|
|
259
278
|
}
|
|
@@ -261,7 +280,7 @@ function deepMerge<T>(target: T, source: Partial<T>): T {
|
|
|
261
280
|
return output;
|
|
262
281
|
}
|
|
263
282
|
|
|
264
|
-
function isObject(item: unknown): item is Record<string,
|
|
283
|
+
function isObject(item: unknown): item is Record<string, any> {
|
|
265
284
|
return Boolean(item && typeof item === 'object' && !Array.isArray(item));
|
|
266
285
|
}
|
|
267
286
|
|
|
@@ -316,52 +335,52 @@ export class ConfigBuilder {
|
|
|
316
335
|
}
|
|
317
336
|
|
|
318
337
|
withCache(config: Partial<NonNullable<WorkerConfig['cache']>>): ConfigBuilder {
|
|
319
|
-
this.config.cache = { ...this.config.cache, ...config }
|
|
338
|
+
this.config.cache = this.config.cache ? { ...this.config.cache, ...config } : config as NonNullable<WorkerConfig['cache']>;
|
|
320
339
|
return this;
|
|
321
340
|
}
|
|
322
341
|
|
|
323
342
|
withRateLimit(config: Partial<NonNullable<WorkerConfig['rateLimit']>>): ConfigBuilder {
|
|
324
|
-
this.config.rateLimit = { ...this.config.rateLimit, ...config }
|
|
343
|
+
this.config.rateLimit = this.config.rateLimit ? { ...this.config.rateLimit, ...config } : config as NonNullable<WorkerConfig['rateLimit']>;
|
|
325
344
|
return this;
|
|
326
345
|
}
|
|
327
346
|
|
|
328
347
|
withAI(config: Partial<NonNullable<WorkerConfig['ai']>>): ConfigBuilder {
|
|
329
|
-
this.config.ai = { ...this.config.ai, ...config }
|
|
348
|
+
this.config.ai = this.config.ai ? { ...this.config.ai, ...config } : config as NonNullable<WorkerConfig['ai']>;
|
|
330
349
|
return this;
|
|
331
350
|
}
|
|
332
351
|
|
|
333
352
|
withWorkflows(config: Partial<NonNullable<WorkerConfig['workflows']>>): ConfigBuilder {
|
|
334
|
-
this.config.workflows = { ...this.config.workflows, ...config }
|
|
353
|
+
this.config.workflows = this.config.workflows ? { ...this.config.workflows, ...config } : config as NonNullable<WorkerConfig['workflows']>;
|
|
335
354
|
return this;
|
|
336
355
|
}
|
|
337
356
|
|
|
338
357
|
withCORS(config: Partial<NonNullable<WorkerConfig['cors']>>): ConfigBuilder {
|
|
339
|
-
this.config.cors = { ...this.config.cors, ...config }
|
|
358
|
+
this.config.cors = this.config.cors ? { ...this.config.cors, ...config } : config as NonNullable<WorkerConfig['cors']>;
|
|
340
359
|
return this;
|
|
341
360
|
}
|
|
342
361
|
|
|
343
362
|
withAnalytics(config: Partial<NonNullable<WorkerConfig['analytics']>>): ConfigBuilder {
|
|
344
|
-
this.config.analytics = { ...this.config.analytics, ...config }
|
|
363
|
+
this.config.analytics = this.config.analytics ? { ...this.config.analytics, ...config } : config as NonNullable<WorkerConfig['analytics']>;
|
|
345
364
|
return this;
|
|
346
365
|
}
|
|
347
366
|
|
|
348
367
|
withCompression(config: Partial<NonNullable<WorkerConfig['compression']>>): ConfigBuilder {
|
|
349
|
-
this.config.compression = { ...this.config.compression, ...config }
|
|
368
|
+
this.config.compression = this.config.compression ? { ...this.config.compression, ...config } : config as NonNullable<WorkerConfig['compression']>;
|
|
350
369
|
return this;
|
|
351
370
|
}
|
|
352
371
|
|
|
353
372
|
withImageOptimization(config: Partial<NonNullable<WorkerConfig['imageOptimization']>>): ConfigBuilder {
|
|
354
|
-
this.config.imageOptimization = { ...this.config.imageOptimization, ...config }
|
|
373
|
+
this.config.imageOptimization = this.config.imageOptimization ? { ...this.config.imageOptimization, ...config } : config as NonNullable<WorkerConfig['imageOptimization']>;
|
|
355
374
|
return this;
|
|
356
375
|
}
|
|
357
376
|
|
|
358
377
|
withQueues(config: Partial<NonNullable<WorkerConfig['queues']>>): ConfigBuilder {
|
|
359
|
-
this.config.queues = { ...this.config.queues, ...config }
|
|
378
|
+
this.config.queues = this.config.queues ? { ...this.config.queues, ...config } : config as NonNullable<WorkerConfig['queues']>;
|
|
360
379
|
return this;
|
|
361
380
|
}
|
|
362
381
|
|
|
363
382
|
withScheduledTasks(config: Partial<NonNullable<WorkerConfig['scheduledTasks']>>): ConfigBuilder {
|
|
364
|
-
this.config.scheduledTasks = { ...this.config.scheduledTasks, ...config }
|
|
383
|
+
this.config.scheduledTasks = this.config.scheduledTasks ? { ...this.config.scheduledTasks, ...config } : config as NonNullable<WorkerConfig['scheduledTasks']>;
|
|
365
384
|
return this;
|
|
366
385
|
}
|
|
367
386
|
|
|
@@ -449,9 +468,9 @@ export function getEnvironmentConfig(
|
|
|
449
468
|
switch (environment) {
|
|
450
469
|
case 'development':
|
|
451
470
|
return mergeConfigs(minimalConfig, {
|
|
452
|
-
cache: { enabled: false },
|
|
453
|
-
rateLimit: { enabled: false },
|
|
454
|
-
cors: { allowedOrigins: ['*'] },
|
|
471
|
+
cache: { enabled: false, defaultTTL: 0 },
|
|
472
|
+
rateLimit: { enabled: false, maxRequests: 0, window: 0 },
|
|
473
|
+
cors: { enabled: true, allowedOrigins: ['*'], allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['*'] },
|
|
455
474
|
});
|
|
456
475
|
|
|
457
476
|
case 'staging':
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Entity
|
|
3
|
+
* @description Basic Analytics entity placeholder
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface AnalyticsEntity {
|
|
7
|
+
siteId: string;
|
|
8
|
+
eventCount: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AnalyticsConfig {
|
|
12
|
+
siteId: string;
|
|
13
|
+
scriptUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AnalyticsEvent {
|
|
17
|
+
timestamp: number;
|
|
18
|
+
url: string;
|
|
19
|
+
eventType: string;
|
|
20
|
+
data?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AnalyticsData {
|
|
24
|
+
siteId: string;
|
|
25
|
+
events: AnalyticsEvent[];
|
|
26
|
+
metrics?: {
|
|
27
|
+
pageviews: number;
|
|
28
|
+
uniqueVisitors: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D1 Entity
|
|
3
|
+
* @description Basic D1 entity placeholder
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface D1Entity {
|
|
7
|
+
databaseId: string;
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface D1DatabaseConfig {
|
|
12
|
+
name: string;
|
|
13
|
+
migrations?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface D1QueryResult<T = unknown> {
|
|
17
|
+
rows: T[];
|
|
18
|
+
meta?: {
|
|
19
|
+
duration: number;
|
|
20
|
+
changes?: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface D1BatchResult<T = unknown> {
|
|
25
|
+
success: boolean;
|
|
26
|
+
results?: D1QueryResult<T>[];
|
|
27
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Entity
|
|
3
|
+
* @description Basic Image entity placeholder
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ImageEntity {
|
|
7
|
+
id: string;
|
|
8
|
+
url: string;
|
|
9
|
+
variant?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ImageConfig {
|
|
13
|
+
formats?: Array<'webp' | 'avif' | 'jpeg' | 'png'>;
|
|
14
|
+
quality?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ImageVariant {
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
format: string;
|
|
21
|
+
url: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ImageUploadResult {
|
|
25
|
+
id: string;
|
|
26
|
+
url: string;
|
|
27
|
+
variants: ImageVariant[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ImageUploadOptions {
|
|
31
|
+
format?: 'webp' | 'avif' | 'jpeg' | 'png';
|
|
32
|
+
quality?: number;
|
|
33
|
+
width?: number;
|
|
34
|
+
height?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ImageTransformation {
|
|
38
|
+
width?: number;
|
|
39
|
+
height?: number;
|
|
40
|
+
fit?: 'contain' | 'cover' | 'fill';
|
|
41
|
+
format?: 'webp' | 'avif' | 'jpeg' | 'png';
|
|
42
|
+
quality?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SignedURL {
|
|
46
|
+
url: string;
|
|
47
|
+
expires: number;
|
|
48
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KV Entity
|
|
3
|
+
* @description Basic KV entity placeholder
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface KVEntity {
|
|
7
|
+
namespaceId: string;
|
|
8
|
+
key: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface KVNamespaceConfig {
|
|
13
|
+
id: string;
|
|
14
|
+
ttl?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface KVEntry {
|
|
18
|
+
key: string;
|
|
19
|
+
value: string;
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface KVListOptions {
|
|
24
|
+
limit?: number;
|
|
25
|
+
cursor?: string;
|
|
26
|
+
prefix?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface KVListResult {
|
|
30
|
+
keys: KVEntry[];
|
|
31
|
+
cursor?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface KVKey {
|
|
35
|
+
name: string;
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R2 Entity
|
|
3
|
+
* @description Basic R2 entity placeholder
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface R2Entity {
|
|
7
|
+
bucketName: string;
|
|
8
|
+
key: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface R2BucketConfig {
|
|
12
|
+
name: string;
|
|
13
|
+
location?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface R2Object {
|
|
17
|
+
key: string;
|
|
18
|
+
size: number;
|
|
19
|
+
uploaded: Date;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface R2UploadResult {
|
|
23
|
+
key: string;
|
|
24
|
+
etag?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface R2ListOptions {
|
|
28
|
+
limit?: number;
|
|
29
|
+
cursor?: string;
|
|
30
|
+
prefix?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface R2ListResult {
|
|
34
|
+
objects: R2Object[];
|
|
35
|
+
cursor?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface R2PutOptions {
|
|
39
|
+
customMetadata?: Record<string, string>;
|
|
40
|
+
httpMetadata?: {
|
|
41
|
+
contentType?: string;
|
|
42
|
+
cacheControl?: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface R2PresignedURL {
|
|
47
|
+
url: string;
|
|
48
|
+
expires: number;
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Entity
|
|
3
|
+
* @description Basic Worker entity placeholder
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface WorkerEntity {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WorkerConfig {
|
|
12
|
+
name: string;
|
|
13
|
+
routes?: string[];
|
|
14
|
+
schedule?: string;
|
|
15
|
+
bindings?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WorkerResponse {
|
|
19
|
+
status: number;
|
|
20
|
+
body?: BodyInit | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IncomingRequestCfProperties {
|
|
24
|
+
colo?: string;
|
|
25
|
+
country?: string;
|
|
26
|
+
httpProtocol?: string;
|
|
27
|
+
tlsVersion?: string;
|
|
28
|
+
tlsCipher?: string;
|
|
29
|
+
asn?: number;
|
|
30
|
+
requestPriority?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type { WorkerRequest as _WorkerRequest } from '../../domains/workers/entities';
|
|
34
|
+
// Re-export WorkerRequest from workers domain
|
|
35
|
+
export { WorkerRequest } from '../../domains/workers/entities';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @description Cloudflare Web Analytics configuration and types
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export interface
|
|
6
|
+
export interface WebAnalyticsConfig {
|
|
7
7
|
readonly siteId: string;
|
|
8
8
|
readonly scriptUrl?: string;
|
|
9
9
|
}
|
|
@@ -33,7 +33,7 @@ export interface AnalyticsTimingEvent extends AnalyticsEvent {
|
|
|
33
33
|
readonly label?: string;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export interface
|
|
36
|
+
export interface WebAnalyticsData {
|
|
37
37
|
readonly siteId: string;
|
|
38
38
|
readonly events: readonly AnalyticsEvent[];
|
|
39
39
|
readonly metrics?: AnalyticsMetrics;
|
|
@@ -11,7 +11,7 @@ export interface WorkerResponse extends Response {
|
|
|
11
11
|
waitUntil?: (promise: Promise<unknown>) => void;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export interface
|
|
14
|
+
export interface CloudflareWorkerConfig {
|
|
15
15
|
readonly name: string;
|
|
16
16
|
readonly routes?: string[];
|
|
17
17
|
readonly schedule?: string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflows Domain Entities
|
|
3
|
+
* @description Workflow orchestration entities for Cloudflare Workers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Workflow step definition
|
|
8
|
+
*/
|
|
9
|
+
export interface WorkflowStep {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
handler: string;
|
|
13
|
+
timeout?: number;
|
|
14
|
+
retryPolicy?: {
|
|
15
|
+
maxAttempts: number;
|
|
16
|
+
backoffMultiplier: number;
|
|
17
|
+
initialDelay: number;
|
|
18
|
+
maxDelay: number;
|
|
19
|
+
};
|
|
20
|
+
dependencies?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Workflow definition
|
|
25
|
+
*/
|
|
26
|
+
export interface WorkflowDefinition {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
steps: WorkflowStep[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Workflow execution state
|
|
35
|
+
*/
|
|
36
|
+
export interface WorkflowExecution {
|
|
37
|
+
id: string;
|
|
38
|
+
workflowId: string;
|
|
39
|
+
status: 'pending' | 'running' | 'completed' | 'failed';
|
|
40
|
+
currentStep?: string;
|
|
41
|
+
startedAt: number;
|
|
42
|
+
completedAt?: number;
|
|
43
|
+
input: unknown;
|
|
44
|
+
output?: unknown;
|
|
45
|
+
error?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Workflow config
|
|
50
|
+
*/
|
|
51
|
+
export interface CloudflareWorkflowConfig {
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
maxExecutionTime: number;
|
|
54
|
+
defaultRetries: number;
|
|
55
|
+
workflows?: Record<string, WorkflowDefinition>;
|
|
56
|
+
storage?: 'kv' | 'd1';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Type alias for compatibility
|
|
60
|
+
export type WorkflowConfig = CloudflareWorkflowConfig;
|
|
@@ -132,9 +132,9 @@ export interface WorkerVersionInfo {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
|
-
*
|
|
135
|
+
* Wrangler analytics data
|
|
136
136
|
*/
|
|
137
|
-
export interface
|
|
137
|
+
export interface WranglerAnalyticsData {
|
|
138
138
|
requests?: number;
|
|
139
139
|
errors?: number;
|
|
140
140
|
statusCodes?: Record<string, number>;
|
|
@@ -118,6 +118,14 @@ export class WranglerService implements IWranglerService {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Convert string result to void result
|
|
123
|
+
*/
|
|
124
|
+
private asVoidResult(result: WranglerResult<string>): WranglerResult<void> {
|
|
125
|
+
const { data, ...rest } = result;
|
|
126
|
+
return rest;
|
|
127
|
+
}
|
|
128
|
+
|
|
121
129
|
// ==================== Authentication ====================
|
|
122
130
|
|
|
123
131
|
async login(options?: WranglerCommandOptions): Promise<WranglerResult<AuthInfo>> {
|
|
@@ -132,7 +140,7 @@ export class WranglerService implements IWranglerService {
|
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
async logout(options?: WranglerCommandOptions): Promise<WranglerResult<void>> {
|
|
135
|
-
return this.execute(['logout'], options);
|
|
143
|
+
return this.asVoidResult(await this.execute(['logout'], options));
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
async whoami(options?: WranglerCommandOptions): Promise<WranglerResult<AuthInfo>> {
|
|
@@ -158,7 +166,7 @@ export class WranglerService implements IWranglerService {
|
|
|
158
166
|
if (template) {
|
|
159
167
|
args.push('--template', template);
|
|
160
168
|
}
|
|
161
|
-
return this.execute(args, options);
|
|
169
|
+
return this.asVoidResult(await this.execute(args, options));
|
|
162
170
|
}
|
|
163
171
|
|
|
164
172
|
async dev(
|
|
@@ -237,7 +245,7 @@ export class WranglerService implements IWranglerService {
|
|
|
237
245
|
value: string,
|
|
238
246
|
options?: WranglerCommandOptions
|
|
239
247
|
): Promise<WranglerResult<void>> {
|
|
240
|
-
return this.execute(['kv:key', 'put', '--namespace-id', namespaceId, key, value], options);
|
|
248
|
+
return this.asVoidResult(await this.execute(['kv:key', 'put', '--namespace-id', namespaceId, key, value], options));
|
|
241
249
|
}
|
|
242
250
|
|
|
243
251
|
async kvKeyGet(
|
|
@@ -253,7 +261,7 @@ export class WranglerService implements IWranglerService {
|
|
|
253
261
|
key: string,
|
|
254
262
|
options?: WranglerCommandOptions
|
|
255
263
|
): Promise<WranglerResult<void>> {
|
|
256
|
-
return this.execute(['kv:key', 'delete', '--namespace-id', namespaceId, key], options);
|
|
264
|
+
return this.asVoidResult(await this.execute(['kv:key', 'delete', '--namespace-id', namespaceId, key], options));
|
|
257
265
|
}
|
|
258
266
|
|
|
259
267
|
// ==================== R2 Operations ====================
|
|
@@ -289,7 +297,7 @@ export class WranglerService implements IWranglerService {
|
|
|
289
297
|
bucketName: string,
|
|
290
298
|
options?: WranglerCommandOptions
|
|
291
299
|
): Promise<WranglerResult<void>> {
|
|
292
|
-
return this.execute(['r2', 'bucket', 'delete', bucketName], options);
|
|
300
|
+
return this.asVoidResult(await this.execute(['r2', 'bucket', 'delete', bucketName], options));
|
|
293
301
|
}
|
|
294
302
|
|
|
295
303
|
async r2ObjectPut(
|
|
@@ -298,7 +306,7 @@ export class WranglerService implements IWranglerService {
|
|
|
298
306
|
file: string,
|
|
299
307
|
options?: WranglerCommandOptions
|
|
300
308
|
): Promise<WranglerResult<void>> {
|
|
301
|
-
return this.execute(['r2', 'object', 'put', bucketName, key, '--file', file], options);
|
|
309
|
+
return this.asVoidResult(await this.execute(['r2', 'object', 'put', bucketName, key, '--file', file], options));
|
|
302
310
|
}
|
|
303
311
|
|
|
304
312
|
// ==================== D1 Operations ====================
|
|
@@ -412,7 +420,7 @@ export class WranglerService implements IWranglerService {
|
|
|
412
420
|
secretName: string,
|
|
413
421
|
options?: WranglerCommandOptions
|
|
414
422
|
): Promise<WranglerResult<void>> {
|
|
415
|
-
return this.execute(['secret', 'delete', secretName], options);
|
|
423
|
+
return this.asVoidResult(await this.execute(['secret', 'delete', secretName], options));
|
|
416
424
|
}
|
|
417
425
|
|
|
418
426
|
// ==================== Monitoring ====================
|
|
@@ -446,7 +454,7 @@ export class WranglerService implements IWranglerService {
|
|
|
446
454
|
versionId: string,
|
|
447
455
|
options?: WranglerCommandOptions
|
|
448
456
|
): Promise<WranglerResult<void>> {
|
|
449
|
-
return this.execute(['versions', 'rollback', '--version-id', versionId], options);
|
|
457
|
+
return this.asVoidResult(await this.execute(['versions', 'rollback', '--version-id', versionId], options));
|
|
450
458
|
}
|
|
451
459
|
|
|
452
460
|
// ==================== Generic Command Execution ====================
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Middleware
|
|
3
|
+
* @description Authentication middleware for Cloudflare Workers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface AuthConfig {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
type: 'bearer' | 'apikey' | 'basic';
|
|
9
|
+
token?: string;
|
|
10
|
+
apiKeyHeader?: string;
|
|
11
|
+
apiKeyValue?: string;
|
|
12
|
+
username?: string;
|
|
13
|
+
password?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Require authentication
|
|
18
|
+
*/
|
|
19
|
+
export async function requireAuth(
|
|
20
|
+
request: Request,
|
|
21
|
+
config: AuthConfig
|
|
22
|
+
): Promise<Response | null> {
|
|
23
|
+
if (!config.enabled) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const authHeader = request.headers.get('Authorization');
|
|
28
|
+
|
|
29
|
+
if (!authHeader) {
|
|
30
|
+
return new Response(
|
|
31
|
+
JSON.stringify({ error: 'Missing authorization header' }),
|
|
32
|
+
{
|
|
33
|
+
status: 401,
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
switch (config.type) {
|
|
40
|
+
case 'bearer':
|
|
41
|
+
if (!authHeader.startsWith('Bearer ')) {
|
|
42
|
+
return new Response(
|
|
43
|
+
JSON.stringify({ error: 'Invalid authorization type' }),
|
|
44
|
+
{
|
|
45
|
+
status: 401,
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const token = authHeader.substring(7);
|
|
51
|
+
if (token !== config.token) {
|
|
52
|
+
return new Response(
|
|
53
|
+
JSON.stringify({ error: 'Invalid token' }),
|
|
54
|
+
{
|
|
55
|
+
status: 401,
|
|
56
|
+
headers: { 'Content-Type': 'application/json' },
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case 'apikey':
|
|
63
|
+
const apiKey = request.headers.get(config.apiKeyHeader || 'X-API-Key');
|
|
64
|
+
if (apiKey !== config.apiKeyValue) {
|
|
65
|
+
return new Response(
|
|
66
|
+
JSON.stringify({ error: 'Invalid API key' }),
|
|
67
|
+
{
|
|
68
|
+
status: 401,
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
|
|
75
|
+
case 'basic':
|
|
76
|
+
if (!authHeader.startsWith('Basic ')) {
|
|
77
|
+
return new Response(
|
|
78
|
+
JSON.stringify({ error: 'Invalid authorization type' }),
|
|
79
|
+
{
|
|
80
|
+
status: 401,
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const credentials = atob(authHeader.substring(6));
|
|
86
|
+
const [username, password] = credentials.split(':');
|
|
87
|
+
if (username !== config.username || password !== config.password) {
|
|
88
|
+
return new Response(
|
|
89
|
+
JSON.stringify({ error: 'Invalid credentials' }),
|
|
90
|
+
{
|
|
91
|
+
status: 401,
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Add user context to request
|
|
104
|
+
*/
|
|
105
|
+
export function addUserContext(request: Request, user: {
|
|
106
|
+
id: string;
|
|
107
|
+
[key: string]: unknown;
|
|
108
|
+
}): Request {
|
|
109
|
+
const headers = new Headers(request.headers);
|
|
110
|
+
headers.set('X-User-ID', user.id);
|
|
111
|
+
headers.set('X-User-Context', JSON.stringify(user));
|
|
112
|
+
|
|
113
|
+
return new Request(request.url, {
|
|
114
|
+
method: request.method,
|
|
115
|
+
headers,
|
|
116
|
+
body: request.body,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -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:
|
|
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
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
301
|
-
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
118
|
+
const chunks: BlobPart[] = [];
|
|
119
119
|
|
|
120
120
|
while (true) {
|
|
121
121
|
const { done, value } = await reader.read();
|
|
122
122
|
if (done) break;
|
|
123
|
-
|
|
123
|
+
// Convert Uint8Array to BlobPart
|
|
124
|
+
chunks.push(value as BlobPart);
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
return new Blob(chunks);
|