@umituz/web-cloudflare 1.4.6 → 1.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/package.json +1 -1
  2. package/src/config/patterns.ts +11 -8
  3. package/src/config/types.ts +9 -1
  4. package/src/domains/ai-gateway/services/index.ts +52 -25
  5. package/src/domains/analytics/services/analytics.service.ts +16 -92
  6. package/src/domains/analytics/types/service.interface.ts +1 -1
  7. package/src/domains/d1/services/d1.service.ts +19 -22
  8. package/src/domains/images/services/images.service.ts +58 -21
  9. package/src/domains/kv/services/kv.service.ts +14 -5
  10. package/src/domains/middleware/entities/index.ts +4 -4
  11. package/src/domains/middleware/services/auth.service.ts +6 -2
  12. package/src/domains/middleware/services/cache.service.ts +7 -3
  13. package/src/domains/middleware/services/cors.service.ts +8 -5
  14. package/src/domains/middleware/services/rate-limit.service.ts +7 -3
  15. package/src/domains/middleware/types/service.interface.ts +17 -11
  16. package/src/domains/r2/services/r2.service.ts +25 -13
  17. package/src/domains/workers/entities/index.ts +17 -1
  18. package/src/domains/workers/examples/worker.example.ts +11 -8
  19. package/src/domains/workers/services/workers.service.ts +9 -4
  20. package/src/domains/workflows/entities/index.ts +14 -1
  21. package/src/domains/workflows/services/workflows.service.ts +43 -10
  22. package/src/domains/wrangler/services/wrangler.service.ts +150 -443
  23. package/src/index.ts +44 -12
  24. package/src/infrastructure/middleware/index.ts +23 -18
  25. package/src/infrastructure/router/index.ts +2 -1
  26. package/src/infrastructure/utils/helpers.ts +29 -7
  27. package/src/presentation/hooks/cloudflare.hooks.ts +7 -309
  28. package/src/presentation/hooks/index.ts +4 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-cloudflare",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
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",
@@ -246,7 +246,7 @@ export const minimalConfig: Partial<WorkerConfig> = {
246
246
  export function mergeConfigs<T extends Record<string, any>>(
247
247
  base: T,
248
248
  ...overrides: Array<Partial<Record<string, any>>>
249
- ): T {
249
+ ): Record<string, any> {
250
250
  return overrides.reduce((acc, override) => {
251
251
  return deepMerge(acc, override);
252
252
  }, base);
@@ -255,24 +255,27 @@ export function mergeConfigs<T extends Record<string, any>>(
255
255
  function deepMerge<T extends Record<string, any>>(
256
256
  target: T,
257
257
  source: Partial<Record<string, any>>
258
- ): T {
259
- const output = { ...target };
258
+ ): Record<string, any> {
259
+ const output: Record<string, unknown> = { ...target };
260
260
 
261
261
  if (isObject(target) && isObject(source)) {
262
262
  Object.keys(source).forEach((key) => {
263
263
  const sourceValue = source[key];
264
- const targetValue = target[key as keyof T];
264
+ const targetValue = (target as Record<string, unknown>)[key];
265
265
 
266
266
  if (isObject(sourceValue)) {
267
267
  if (!(key in target)) {
268
- (output as any)[key] = sourceValue;
268
+ output[key] = sourceValue;
269
269
  } else if (isObject(targetValue)) {
270
- (output as any)[key] = deepMerge(targetValue, sourceValue);
270
+ output[key] = deepMerge(
271
+ targetValue as Record<string, any>,
272
+ sourceValue
273
+ );
271
274
  } else {
272
- (output as any)[key] = sourceValue;
275
+ output[key] = sourceValue;
273
276
  }
274
277
  } else {
275
- (output as any)[key] = sourceValue;
278
+ output[key] = sourceValue;
276
279
  }
277
280
  });
278
281
  }
@@ -629,7 +629,7 @@ export interface EnvConfig {
629
629
  /**
630
630
  * AI bindings
631
631
  */
632
- AI?: any;
632
+ AI?: WorkersAIBinding;
633
633
 
634
634
  /**
635
635
  * Custom environment variables
@@ -637,6 +637,14 @@ export interface EnvConfig {
637
637
  vars?: Record<string, string>;
638
638
  }
639
639
 
640
+ /**
641
+ * Workers AI Binding
642
+ * @description Cloudflare Workers AI runtime binding
643
+ */
644
+ export interface WorkersAIBinding {
645
+ run: <T = unknown>(model: string, inputs: Record<string, unknown>) => Promise<T>;
646
+ }
647
+
640
648
  // ============================================================
641
649
  // Configuration Merging Types
642
650
  // ============================================================
@@ -10,6 +10,7 @@ import type {
10
10
  AIProvider,
11
11
  AIAnalytics,
12
12
  } from '../entities';
13
+ import type { WorkersAIBinding } from '../../../config/types';
13
14
 
14
15
  export class AIGatewayService {
15
16
  private config: AIGatewayConfig;
@@ -127,17 +128,27 @@ export class AIGatewayService {
127
128
  throw new Error(`Provider error: ${response.statusText}`);
128
129
  }
129
130
 
130
- const data = await response.json();
131
+ const data = await response.json() as {
132
+ id?: string;
133
+ content?: string;
134
+ text?: string;
135
+ output?: string;
136
+ usage?: {
137
+ promptTokens?: number;
138
+ completionTokens?: number;
139
+ totalTokens?: number;
140
+ };
141
+ };
131
142
 
132
143
  return {
133
144
  id: data.id || this.generateId(),
134
145
  provider: provider.id,
135
146
  model: request.model,
136
- content: data.content || data.text || data.output,
137
- usage: data.usage || {
138
- promptTokens: 0,
139
- completionTokens: 0,
140
- totalTokens: 0,
147
+ content: data.content || data.text || data.output || '',
148
+ usage: {
149
+ promptTokens: data.usage?.promptTokens || 0,
150
+ completionTokens: data.usage?.completionTokens || 0,
151
+ totalTokens: data.usage?.totalTokens || 0,
141
152
  },
142
153
  cached: false,
143
154
  timestamp: Date.now(),
@@ -215,16 +226,23 @@ import type {
215
226
 
216
227
  export class WorkersAIService {
217
228
  private env: {
218
- AI?: AiTextGeneration;
229
+ AI?: WorkersAIBinding;
219
230
  bindings?: {
220
- AI?: Ai;
231
+ AI?: WorkersAIBinding;
221
232
  };
222
233
  };
223
234
 
224
- constructor(env: { AI?: any; bindings?: any }) {
235
+ constructor(env: { AI?: WorkersAIBinding; bindings?: { AI?: WorkersAIBinding } }) {
225
236
  this.env = env;
226
237
  }
227
238
 
239
+ /**
240
+ * Workers AI Binding type
241
+ */
242
+ private getAI(): WorkersAIBinding | null {
243
+ return this.env.bindings?.AI || this.env.AI || null;
244
+ }
245
+
228
246
  /**
229
247
  * Run text generation model
230
248
  */
@@ -233,19 +251,24 @@ export class WorkersAIService {
233
251
  inputs: WorkersAIInputs['text_generation']
234
252
  ): Promise<WorkersAIResponse> {
235
253
  try {
236
- // @ts-ignore - Workers AI runtime binding
237
- const ai = this.env.bindings?.AI || this.env.AI;
254
+ const ai = this.getAI();
238
255
 
239
256
  if (!ai) {
240
257
  throw new Error('Workers AI binding not configured');
241
258
  }
242
259
 
243
- const response = await ai.run(model, inputs);
260
+ if (!inputs) {
261
+ throw new Error('Inputs are required for text generation');
262
+ }
263
+
264
+ const response = await ai.run(model, inputs as Record<string, unknown>);
244
265
 
245
266
  return {
246
267
  success: true,
247
268
  data: {
248
- output: response.response || response.output || response.text,
269
+ output: ((response as Record<string, unknown>).response as string | string[] | undefined) ||
270
+ ((response as Record<string, unknown>).output as string | string[] | undefined) ||
271
+ ((response as Record<string, unknown>).text as string | string[] | undefined),
249
272
  },
250
273
  model,
251
274
  };
@@ -319,19 +342,23 @@ Generate the script:`;
319
342
  inputs: WorkersAIInputs['image_generation']
320
343
  ): Promise<WorkersAIResponse> {
321
344
  try {
322
- // @ts-ignore - Workers AI runtime binding
323
- const ai = this.env.bindings?.AI || this.env.AI;
345
+ const ai = this.getAI();
324
346
 
325
347
  if (!ai) {
326
348
  throw new Error('Workers AI binding not configured');
327
349
  }
328
350
 
329
- const response = await ai.run(model, inputs);
351
+ if (!inputs) {
352
+ throw new Error('Inputs are required for image generation');
353
+ }
354
+
355
+ const response = await ai.run(model, inputs as Record<string, unknown>);
330
356
 
331
357
  return {
332
358
  success: true,
333
359
  data: {
334
- image: response.image || response.output,
360
+ image: (response as Record<string, unknown>).image as string | undefined ||
361
+ (response as Record<string, unknown>).output as string | undefined,
335
362
  },
336
363
  model,
337
364
  };
@@ -349,20 +376,19 @@ Generate the script:`;
349
376
  */
350
377
  async generateEmbedding(text: string): Promise<WorkersAIResponse> {
351
378
  try {
352
- // @ts-ignore - Workers AI runtime binding
353
- const ai = this.env.bindings?.AI || this.env.AI;
379
+ const ai = this.getAI();
354
380
 
355
381
  if (!ai) {
356
382
  throw new Error('Workers AI binding not configured');
357
383
  }
358
384
 
359
385
  const model = '@cf/openai/clip-vit-base-patch32';
360
- const response = await ai.run(model, { text });
386
+ const response = await ai.run(model, { text }) as Record<string, unknown>;
361
387
 
362
388
  return {
363
389
  success: true,
364
390
  data: {
365
- embedding: response.embedding || response.output,
391
+ embedding: response.embedding as number[] | undefined || response.output as number[] | undefined,
366
392
  },
367
393
  model,
368
394
  };
@@ -384,8 +410,7 @@ Generate the script:`;
384
410
  targetLang: string
385
411
  ): Promise<WorkersAIResponse> {
386
412
  try {
387
- // @ts-ignore - Workers AI runtime binding
388
- const ai = this.env.bindings?.AI || this.env.AI;
413
+ const ai = this.getAI();
389
414
 
390
415
  if (!ai) {
391
416
  throw new Error('Workers AI binding not configured');
@@ -396,12 +421,14 @@ Generate the script:`;
396
421
  text,
397
422
  source_lang: sourceLang,
398
423
  target_lang: targetLang,
399
- });
424
+ }) as Record<string, unknown>;
400
425
 
401
426
  return {
402
427
  success: true,
403
428
  data: {
404
- output: response.translated_text || response.output || response.text,
429
+ output: response.translated_text as string | string[] | undefined ||
430
+ response.output as string | string[] | undefined ||
431
+ response.text as string | string[] | undefined,
405
432
  },
406
433
  model: '@cf/meta/m2m100-1.2b',
407
434
  };
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Analytics Service
3
- * @description Cloudflare Web Analytics operations
3
+ * @description Cloudflare Web Analytics operations for Workers runtime
4
4
  */
5
5
 
6
- import type { AnalyticsEvent, AnalyticsPageviewEvent, AnalyticsCustomEvent, AnalyticsData } from "../entities";
6
+ import type { AnalyticsEvent, AnalyticsPageviewEvent, AnalyticsCustomEvent } from "../entities";
7
+ import type { AnalyticsData } from "../../../domain/entities/analytics.entity";
7
8
  import type { IAnalyticsService } from "../../../domain/interfaces/services.interface";
8
9
 
9
10
  export interface AnalyticsClientOptions {
@@ -29,13 +30,9 @@ class AnalyticsService implements IAnalyticsService {
29
30
 
30
31
  async trackEvent(event: AnalyticsEvent): Promise<void> {
31
32
  this.ensureInitialized();
32
-
33
33
  this.eventQueue.push(event);
34
-
35
- // In a browser environment, send to Cloudflare Analytics
36
- if (typeof window !== "undefined" && (window as any)._cfAnalytics) {
37
- (window as any)._cfAnalytics.track(event);
38
- }
34
+ // In Workers runtime, events are queued for batch processing
35
+ // The actual sending would be done via Cloudflare Analytics API
39
36
  }
40
37
 
41
38
  async trackPageview(url: string, title: string, referrer?: string): Promise<void> {
@@ -50,12 +47,10 @@ class AnalyticsService implements IAnalyticsService {
50
47
  await this.trackEvent(event);
51
48
  }
52
49
 
53
- async trackCustom(eventName: string, data?: Record<string, unknown>): Promise<void> {
54
- if (typeof window === "undefined") return;
55
-
50
+ async trackCustom(eventName: string, data?: Record<string, unknown>, url?: string): Promise<void> {
56
51
  const event: AnalyticsCustomEvent = {
57
52
  timestamp: Date.now(),
58
- url: window.location.href,
53
+ url: url || '/workers',
59
54
  eventType: "custom",
60
55
  eventName,
61
56
  eventData: data,
@@ -65,11 +60,9 @@ class AnalyticsService implements IAnalyticsService {
65
60
  }
66
61
 
67
62
  async trackOutboundLink(url: string, linkType?: string): Promise<void> {
68
- if (typeof window === "undefined") return;
69
-
70
63
  const event: AnalyticsCustomEvent = {
71
64
  timestamp: Date.now(),
72
- url: window.location.href,
65
+ url: '/workers',
73
66
  eventType: "custom",
74
67
  eventName: "outbound-link",
75
68
  eventData: {
@@ -82,11 +75,9 @@ class AnalyticsService implements IAnalyticsService {
82
75
  }
83
76
 
84
77
  async trackTiming(name: string, value: number, label?: string): Promise<void> {
85
- if (typeof window === "undefined") return;
86
-
87
78
  const event: AnalyticsEvent = {
88
79
  timestamp: Date.now(),
89
- url: window.location.href,
80
+ url: '/workers',
90
81
  eventType: "timing",
91
82
  eventData: {
92
83
  name,
@@ -101,89 +92,22 @@ class AnalyticsService implements IAnalyticsService {
101
92
  async getAnalytics(): Promise<AnalyticsData> {
102
93
  this.ensureInitialized();
103
94
 
104
- // In a real implementation, this would fetch from Cloudflare Analytics API
105
- // For now, return queued events
106
-
107
95
  return {
108
96
  siteId: this.siteId!,
109
- events: this.eventQueue,
97
+ events: [...this.eventQueue],
110
98
  metrics: {
111
- pageviews: this.eventQueue.filter((e) => e.eventType === "pageview").length,
112
- uniqueVisitors: new Set(this.eventQueue.map((e) => e.url)).size,
99
+ pageviews: this.eventQueue.filter(e => e.eventType === "pageview").length,
100
+ uniqueVisitors: 0, // Would be calculated from stored data
113
101
  },
114
102
  };
115
103
  }
116
104
 
117
- /**
118
- * Get analytics script tag
119
- */
120
- getScriptTag(): string {
121
- this.ensureInitialized();
122
-
123
- const scriptUrl = this.scriptUrl || `https://static.cloudflareinsights.com/beacon.min.js`;
124
-
125
- return `
126
- <script defer src='${scriptUrl}' data-cf-beacon='{"token": "${this.siteId}"}'></script>
127
- `.trim();
128
- }
129
-
130
- /**
131
- * Clear queued events
132
- */
133
- clearEvents(): void {
105
+ async flush(): Promise<void> {
106
+ // In Workers, this would send events to Cloudflare Analytics API
134
107
  this.eventQueue = [];
135
108
  }
136
-
137
- /**
138
- * Get queued events
139
- */
140
- getQueuedEvents(): readonly AnalyticsEvent[] {
141
- return this.eventQueue;
142
- }
143
-
144
- /**
145
- * E-commerce helpers
146
- */
147
- async trackPurchase(transactionId: string, revenue: number, items: readonly { id: string; name: string; price: number; quantity: number }[]): Promise<void> {
148
- await this.trackCustom("purchase", {
149
- transactionId,
150
- revenue,
151
- items,
152
- });
153
- }
154
-
155
- async trackAddToCart(itemId: string, price: number, quantity: number): Promise<void> {
156
- await this.trackCustom("add-to-cart", {
157
- itemId,
158
- price,
159
- quantity,
160
- });
161
- }
162
-
163
- async trackRemoveFromCart(itemId: string, quantity: number): Promise<void> {
164
- await this.trackCustom("remove-from-cart", {
165
- itemId,
166
- quantity,
167
- });
168
- }
169
-
170
- /**
171
- * Engagement helpers
172
- */
173
- async trackScrollDepth(depth: number): Promise<void> {
174
- await this.trackCustom("scroll-depth", { depth });
175
- }
176
-
177
- async trackTimeOnPage(seconds: number): Promise<void> {
178
- await this.trackCustom("time-on-page", { seconds });
179
- }
180
-
181
- async trackEngagement(action: string, target?: string): Promise<void> {
182
- await this.trackCustom("engagement", {
183
- action,
184
- target,
185
- });
186
- }
187
109
  }
188
110
 
111
+ // Export class and singleton instance
112
+ export { AnalyticsService };
189
113
  export const analyticsService = new AnalyticsService();
@@ -3,7 +3,7 @@
3
3
  * @description Abstract interface for Analytics operations
4
4
  */
5
5
 
6
- import type { AnalyticsEvent, AnalyticsData } from '../entities';
6
+ import type { AnalyticsEvent, AnalyticsData } from '../../../domain/entities/analytics.entity';
7
7
 
8
8
  export interface IAnalyticsService {
9
9
  trackEvent(event: AnalyticsEvent): Promise<void>;
@@ -3,7 +3,7 @@
3
3
  * @description Cloudflare D1 database operations
4
4
  */
5
5
 
6
- import type { D1QueryResult, D1BatchResult } from "../entities";
6
+ import type { D1QueryResult, D1BatchResult } from "../../../domain/entities/d1.entity";
7
7
  import type { ID1Service } from "../../../domain/interfaces/services.interface";
8
8
 
9
9
  export interface D1ExecOptions {
@@ -33,14 +33,10 @@ class D1Service implements ID1Service {
33
33
  const result = params ? await stmt.bind(...params).all() : await stmt.all();
34
34
 
35
35
  return {
36
- results: result.results as T[],
37
- success: result.success,
36
+ rows: result.results as T[],
38
37
  meta: result.meta
39
38
  ? {
40
39
  duration: result.meta.duration,
41
- rows_read: result.meta.rows_read,
42
- rows_written: result.meta.rows_written,
43
- last_row_id: result.meta.last_row_id,
44
40
  changes: result.meta.changes,
45
41
  }
46
42
  : undefined,
@@ -48,20 +44,20 @@ class D1Service implements ID1Service {
48
44
  }
49
45
 
50
46
  async batch(
51
- statements: readonly { sql: string; params?: readonly unknown[]; binding?: string }[]
47
+ sqlStatements: readonly { sql: string; params?: readonly unknown[]; binding?: string }[]
52
48
  ): Promise<D1BatchResult> {
53
49
  // Group by binding
54
50
  const grouped = new Map<string, D1PreparedStatement[]>();
55
51
 
56
- for (const stmt of statements) {
57
- const binding = stmt.binding || "default";
58
- const database = this.getDatabase(binding);
52
+ for (const stmt of sqlStatements) {
53
+ const bindingName = stmt.binding || "default";
54
+ const database = this.getDatabase(bindingName);
59
55
 
60
- if (!grouped.has(binding)) {
61
- grouped.set(binding, []);
56
+ if (!grouped.has(bindingName)) {
57
+ grouped.set(bindingName, []);
62
58
  }
63
59
 
64
- grouped.get(binding)!.push(database.prepare(stmt.sql));
60
+ grouped.get(bindingName)!.push(database.prepare(stmt.sql));
65
61
  }
66
62
 
67
63
  // Note: D1 batch requires all statements to be from the same database
@@ -69,18 +65,17 @@ class D1Service implements ID1Service {
69
65
  throw new Error("Batch operations must be from the same database binding");
70
66
  }
71
67
 
72
- const [binding, statements] = Array.from(grouped.entries())[0];
73
- const database = this.getDatabase(binding);
68
+ const [bindingName, preparedStatements] = Array.from(grouped.entries())[0];
69
+ const database = this.getDatabase(bindingName);
74
70
 
75
- const results = await database.batch(statements as D1PreparedStatement[]);
71
+ const results = await database.batch(preparedStatements as D1PreparedStatement[]);
76
72
 
77
73
  return {
78
74
  success: results.every((r) => r.success),
79
75
  results: results.map((r) => ({
80
- results: r.results,
81
- success: r.success,
82
- meta: r.meta,
83
- })),
76
+ rows: r.results,
77
+ meta: r.meta ? { duration: r.meta.duration, changes: r.meta.changes } : undefined,
78
+ })) as D1QueryResult[],
84
79
  };
85
80
  }
86
81
 
@@ -94,7 +89,7 @@ class D1Service implements ID1Service {
94
89
  async findOne<T>(sql: string, params?: readonly unknown[], binding?: string): Promise<T | null> {
95
90
  const result = await this.query<T>(sql, params, binding);
96
91
 
97
- return (result.results[0] as T) ?? null;
92
+ return (result.rows[0] as T) ?? null;
98
93
  }
99
94
 
100
95
  async insert<T>(
@@ -165,7 +160,7 @@ class D1Service implements ID1Service {
165
160
 
166
161
  const result = await this.query<{ name: string }>(sql, [table], binding);
167
162
 
168
- return result.results.length > 0;
163
+ return result.rows.length > 0;
169
164
  }
170
165
 
171
166
  /**
@@ -188,4 +183,6 @@ interface D1Transaction {
188
183
  query<T>(sql: string, params?: readonly unknown[]): Promise<D1QueryResult<T>>;
189
184
  }
190
185
 
186
+ // Export class and singleton instance
187
+ export { D1Service };
191
188
  export const d1Service = new D1Service();
@@ -3,7 +3,7 @@
3
3
  * @description Cloudflare Images operations
4
4
  */
5
5
 
6
- import type { ImageUploadResult, ImageUploadOptions, ImageTransformation, SignedURL } from "../entities";
6
+ import type { ImageUploadResult, ImageUploadOptions, ImageTransformation, SignedURL } from "../../../domain/entities/image.entity";
7
7
  import type { IImageService } from "../../../domain/interfaces/services.interface";
8
8
  import { validationUtils, transformUtils } from "../../../infrastructure/utils";
9
9
  import { MAX_IMAGE_SIZE, ALLOWED_IMAGE_TYPES } from "../../../infrastructure/constants";
@@ -59,14 +59,13 @@ class ImagesService implements IImageService {
59
59
  const formData = new FormData();
60
60
  formData.append("file", file);
61
61
 
62
- if (options?.metadata) {
63
- for (const [key, value] of Object.entries(options.metadata)) {
64
- formData.append(`metadata[${key}]`, value);
65
- }
66
- }
67
-
68
- if (options?.requireSignedURLs !== undefined) {
69
- formData.append("requireSignedURLs", options.requireSignedURLs.toString());
62
+ // Apply transformations if specified in options
63
+ if (options?.width || options?.height || options?.format || options?.quality) {
64
+ const variants: Array<{ width?: number; height?: number; format?: string; quality?: number }> = [];
65
+ if (options.width) variants.push({ width: options.width });
66
+ if (options.height) variants.push({ height: options.height });
67
+ if (options.format) variants.push({ format: options.format });
68
+ if (options.quality) variants.push({ quality: options.quality });
70
69
  }
71
70
 
72
71
  // Upload
@@ -81,19 +80,31 @@ class ImagesService implements IImageService {
81
80
  throw new Error(`Upload failed: ${error}`);
82
81
  }
83
82
 
84
- const data = await response.json();
83
+ const data = await response.json() as {
84
+ result: {
85
+ id: string;
86
+ filename: string;
87
+ uploaded: string;
88
+ variants: string[];
89
+ requireSignedURLs: boolean;
90
+ };
91
+ };
85
92
 
93
+ // Map API response to old entity type
86
94
  return {
87
95
  id: data.result.id,
88
- filename: data.result.filename,
89
- uploaded: new Date(data.result.uploaded),
90
- variants: data.result.variants,
91
- requireSignedURLs: data.result.requireSignedURLs,
96
+ url: data.result.variants[0] || '',
97
+ variants: data.result.variants.map((v) => ({
98
+ width: 0,
99
+ height: 0,
100
+ format: (options?.format || 'jpeg') as 'jpeg' | 'png' | 'webp' | 'avif',
101
+ url: v,
102
+ })),
92
103
  };
93
104
  }
94
105
 
95
106
  async getSignedURL(imageId: string, expiresIn = 3600): Promise<SignedURL> {
96
- const expiresAt = new Date(Date.now() + expiresIn * 1000);
107
+ const expires = Date.now() + expiresIn * 1000;
97
108
 
98
109
  const response = await fetch(`${this.getAPIBaseURL()}/${imageId}`, {
99
110
  method: "POST",
@@ -102,7 +113,7 @@ class ImagesService implements IImageService {
102
113
  "Content-Type": "application/json",
103
114
  },
104
115
  body: JSON.stringify({
105
- expiry: expiresAt.toISOString(),
116
+ expiry: new Date(expires).toISOString(),
106
117
  }),
107
118
  });
108
119
 
@@ -111,11 +122,16 @@ class ImagesService implements IImageService {
111
122
  throw new Error(`Failed to get signed URL: ${error}`);
112
123
  }
113
124
 
114
- const data = await response.json();
125
+ const data = await response.json() as {
126
+ result: {
127
+ signedURLs?: string[];
128
+ variants?: string[];
129
+ };
130
+ };
115
131
 
116
132
  return {
117
- url: data.result.signedURLs?.[0] || data.result.variants?.[0],
118
- expiresAt,
133
+ url: data.result.signedURLs?.[0] || data.result.variants?.[0] || '',
134
+ expires,
119
135
  };
120
136
  }
121
137
 
@@ -164,10 +180,29 @@ class ImagesService implements IImageService {
164
180
  throw new Error(`List failed: ${error}`);
165
181
  }
166
182
 
167
- const data = await response.json();
183
+ const data = await response.json() as {
184
+ result: {
185
+ images: Array<{
186
+ id: string;
187
+ filename: string;
188
+ uploaded: string;
189
+ variants: string[];
190
+ }>;
191
+ totalCount: number;
192
+ };
193
+ };
168
194
 
169
195
  return {
170
- images: data.result.images,
196
+ images: data.result.images.map((img) => ({
197
+ id: img.id,
198
+ url: img.variants[0] || '',
199
+ variants: img.variants.map((v) => ({
200
+ width: 0,
201
+ height: 0,
202
+ format: 'jpeg' as const,
203
+ url: v,
204
+ })),
205
+ })),
171
206
  totalCount: data.result.totalCount,
172
207
  };
173
208
  }
@@ -224,4 +259,6 @@ class ImagesService implements IImageService {
224
259
  }
225
260
  }
226
261
 
262
+ // Export class and singleton instance
263
+ export { ImagesService };
227
264
  export const imagesService = new ImagesService();