@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-cloudflare",
3
- "version": "1.4.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
  },
@@ -3,8 +3,8 @@
3
3
  * @description Reusable configuration patterns for different use cases
4
4
  */
5
5
 
6
- import type { AIGatewayConfig } from '../infrastructure/domain/ai-gateway.entity';
7
- import type { WorkflowDefinition } from '../infrastructure/domain/workflows.entity';
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
- fallback: 'workers-ai',
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<T>[]
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>(target: T, source: Partial<T>): T {
245
- const output = { ...target } as T;
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
- if (isObject(source[key as keyof T])) {
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
- Object.assign(output, { [key]: source[key as keyof T] });
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] = deepMerge(target[key as keyof T], source[key as keyof T]);
272
+ (output as any)[key] = sourceValue;
254
273
  }
255
274
  } else {
256
- Object.assign(output, { [key]: source[key as keyof T] });
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, unknown> {
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 AnalyticsConfig {
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 AnalyticsData {
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 WorkerConfig {
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
- * Analytics data
135
+ * Wrangler analytics data
136
136
  */
137
- export interface AnalyticsData {
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 ====================
@@ -12,8 +12,8 @@ import type {
12
12
  D1DatabaseInfo,
13
13
  SecretInfo,
14
14
  WorkerVersionInfo,
15
- AnalyticsData,
16
- } from '../entities/wrangler.entity';
15
+ WranglerAnalyticsData,
16
+ } from '../entities';
17
17
 
18
18
  export interface IWranglerService {
19
19
  // Authentication
@@ -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: 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);