@umituz/web-cloudflare 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +621 -0
  3. package/package.json +87 -0
  4. package/src/config/patterns.ts +469 -0
  5. package/src/config/types.ts +648 -0
  6. package/src/domain/entities/analytics.entity.ts +47 -0
  7. package/src/domain/entities/d1.entity.ts +37 -0
  8. package/src/domain/entities/image.entity.ts +48 -0
  9. package/src/domain/entities/index.ts +11 -0
  10. package/src/domain/entities/kv.entity.ts +34 -0
  11. package/src/domain/entities/r2.entity.ts +55 -0
  12. package/src/domain/entities/worker.entity.ts +35 -0
  13. package/src/domain/index.ts +7 -0
  14. package/src/domain/interfaces/index.ts +6 -0
  15. package/src/domain/interfaces/services.interface.ts +82 -0
  16. package/src/index.ts +53 -0
  17. package/src/infrastructure/constants/index.ts +13 -0
  18. package/src/infrastructure/domain/ai-gateway.entity.ts +169 -0
  19. package/src/infrastructure/domain/workflows.entity.ts +108 -0
  20. package/src/infrastructure/middleware/index.ts +405 -0
  21. package/src/infrastructure/router/index.ts +549 -0
  22. package/src/infrastructure/services/ai-gateway/index.ts +416 -0
  23. package/src/infrastructure/services/analytics/analytics.service.ts +189 -0
  24. package/src/infrastructure/services/analytics/index.ts +7 -0
  25. package/src/infrastructure/services/d1/d1.service.ts +191 -0
  26. package/src/infrastructure/services/d1/index.ts +7 -0
  27. package/src/infrastructure/services/images/images.service.ts +227 -0
  28. package/src/infrastructure/services/images/index.ts +7 -0
  29. package/src/infrastructure/services/kv/index.ts +7 -0
  30. package/src/infrastructure/services/kv/kv.service.ts +116 -0
  31. package/src/infrastructure/services/r2/index.ts +7 -0
  32. package/src/infrastructure/services/r2/r2.service.ts +164 -0
  33. package/src/infrastructure/services/workers/index.ts +7 -0
  34. package/src/infrastructure/services/workers/workers.service.ts +164 -0
  35. package/src/infrastructure/services/workflows/index.ts +437 -0
  36. package/src/infrastructure/utils/helpers.ts +732 -0
  37. package/src/infrastructure/utils/index.ts +6 -0
  38. package/src/infrastructure/utils/utils.util.ts +150 -0
  39. package/src/presentation/hooks/cloudflare.hooks.ts +314 -0
  40. package/src/presentation/hooks/index.ts +6 -0
  41. package/src/worker.example.ts +41 -0
@@ -0,0 +1,732 @@
1
+ /**
2
+ * Cloudflare Utility Functions
3
+ * @description Helper functions for Cloudflare Workers
4
+ */
5
+
6
+ // ============================================================
7
+ // Request Utilities
8
+ // ============================================================
9
+
10
+ /**
11
+ * Parse request body
12
+ */
13
+ export async function parseBody<T = unknown>(request: Request): Promise<T> {
14
+ const contentType = request.headers.get('Content-Type') || '';
15
+
16
+ if (contentType.includes('application/json')) {
17
+ return request.json() as Promise<T>;
18
+ }
19
+
20
+ if (contentType.includes('application/x-www-form-urlencoded')) {
21
+ const formData = await request.formData();
22
+ return Object.fromEntries(formData) as T;
23
+ }
24
+
25
+ if (contentType.includes('text/')) {
26
+ return request.text() as unknown as T;
27
+ }
28
+
29
+ throw new Error(`Unsupported content type: ${contentType}`);
30
+ }
31
+
32
+ /**
33
+ * Get client IP
34
+ */
35
+ export function getClientIP(request: Request): string {
36
+ return (
37
+ request.headers.get('CF-Connecting-IP') ||
38
+ request.headers.get('X-Forwarded-For') ||
39
+ 'unknown'
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Get client country
45
+ */
46
+ export function getClientCountry(request: Request): string | null {
47
+ return request.headers.get('CF-IPCountry');
48
+ }
49
+
50
+ /**
51
+ * Get request timestamp
52
+ */
53
+ export function getRequestTimestamp(request: Request): number {
54
+ const cfDate = request.headers.get('CF-Ray');
55
+ if (cfDate) {
56
+ // Extract timestamp from CF-Ray if available
57
+ return Date.now();
58
+ }
59
+ return Date.now();
60
+ }
61
+
62
+ /**
63
+ * Check if request is HTTPS
64
+ */
65
+ export function isHTTPS(request: Request): boolean {
66
+ const url = new URL(request.url);
67
+ return url.protocol === 'https:';
68
+ }
69
+
70
+ /**
71
+ * Check if request is from specific origin
72
+ */
73
+ export function isFromOrigin(request: Request, origin: string): boolean {
74
+ const requestOrigin = request.headers.get('Origin');
75
+ return requestOrigin === origin;
76
+ }
77
+
78
+ /**
79
+ * Get user agent
80
+ */
81
+ export function getUserAgent(request: Request): string {
82
+ return request.headers.get('User-Agent') || 'unknown';
83
+ }
84
+
85
+ /**
86
+ * Parse accept language
87
+ */
88
+ export function parseAcceptLanguage(request: Request): string[] {
89
+ const acceptLanguage = request.headers.get('Accept-Language') || '';
90
+ return acceptLanguage
91
+ .split(',')
92
+ .map((lang) => lang.split(';')[0].trim());
93
+ }
94
+
95
+ // ============================================================
96
+ // Response Utilities
97
+ // ============================================================
98
+
99
+ /**
100
+ * Create JSON response
101
+ */
102
+ export function json<T>(data: T, status: number = 200): Response {
103
+ return Response.json(data, { status });
104
+ }
105
+
106
+ /**
107
+ * Create error response
108
+ */
109
+ export function error(
110
+ message: string,
111
+ status: number = 500,
112
+ details?: Record<string, unknown>
113
+ ): Response {
114
+ return Response.json(
115
+ {
116
+ error: message,
117
+ status,
118
+ ...(details && { details }),
119
+ },
120
+ { status }
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Create not found response
126
+ */
127
+ export function notFound(message: string = 'Not Found'): Response {
128
+ return error(message, 404);
129
+ }
130
+
131
+ /**
132
+ * Create unauthorized response
133
+ */
134
+ export function unauthorized(message: string = 'Unauthorized'): Response {
135
+ return error(message, 401);
136
+ }
137
+
138
+ /**
139
+ * Create forbidden response
140
+ */
141
+ export function forbidden(message: string = 'Forbidden'): Response {
142
+ return error(message, 403);
143
+ }
144
+
145
+ /**
146
+ * Create bad request response
147
+ */
148
+ export function badRequest(message: string = 'Bad Request', details?: Record<string, unknown>): Response {
149
+ return error(message, 400, details);
150
+ }
151
+
152
+ /**
153
+ * Create redirect response
154
+ */
155
+ export function redirect(url: string, status: number = 302): Response {
156
+ return Response.redirect(url, status);
157
+ }
158
+
159
+ /**
160
+ * Create no-content response
161
+ */
162
+ export function noContent(): Response {
163
+ return new Response(null, { status: 204 });
164
+ }
165
+
166
+ /**
167
+ * Create HTML response
168
+ */
169
+ export function html(content: string, status: number = 200): Response {
170
+ return new Response(content, {
171
+ status,
172
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
173
+ });
174
+ }
175
+
176
+ /**
177
+ * Create text response
178
+ */
179
+ export function text(content: string, status: number = 200): Response {
180
+ return new Response(content, {
181
+ status,
182
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Create file response
188
+ */
189
+ export function file(
190
+ content: ArrayBuffer,
191
+ contentType: string,
192
+ filename?: string
193
+ ): Response {
194
+ const headers = new Headers({ 'Content-Type': contentType });
195
+
196
+ if (filename) {
197
+ headers.set('Content-Disposition', `attachment; filename="${filename}"`);
198
+ }
199
+
200
+ return new Response(content, { headers });
201
+ }
202
+
203
+ /**
204
+ * Stream response
205
+ */
206
+ export function stream(
207
+ readableStream: ReadableStream,
208
+ contentType: string = 'application/octet-stream'
209
+ ): Response {
210
+ return new Response(readableStream, {
211
+ headers: { 'Content-Type': contentType },
212
+ });
213
+ }
214
+
215
+ // ============================================================
216
+ // Validation Utilities
217
+ // ============================================================
218
+
219
+ /**
220
+ * Validate email
221
+ */
222
+ export function isValidEmail(email: string): boolean {
223
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
224
+ return emailRegex.test(email);
225
+ }
226
+
227
+ /**
228
+ * Validate URL
229
+ */
230
+ export function isValidURL(url: string): boolean {
231
+ try {
232
+ new URL(url);
233
+ return true;
234
+ } catch {
235
+ return false;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Validate UUID
241
+ */
242
+ export function isValidUUID(uuid: string): boolean {
243
+ const uuidRegex =
244
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
245
+ return uuidRegex.test(uuid);
246
+ }
247
+
248
+ /**
249
+ * Validate phone number (basic)
250
+ */
251
+ export function isValidPhone(phone: string): boolean {
252
+ const phoneRegex = /^\+?[\d\s\-()]+$/;
253
+ return phoneRegex.test(phone) && phone.replace(/\D/g, '').length >= 10;
254
+ }
255
+
256
+ /**
257
+ * Sanitize string input
258
+ */
259
+ export function sanitize(input: string): string {
260
+ return input
261
+ .replace(/</g, '&lt;')
262
+ .replace(/>/g, '&gt;')
263
+ .replace(/"/g, '&quot;')
264
+ .replace(/'/g, '&#x27;')
265
+ .replace(/\//g, '&#x2F;');
266
+ }
267
+
268
+ /**
269
+ * Truncate text
270
+ */
271
+ export function truncate(text: string, length: number, suffix: string = '...'): string {
272
+ if (text.length <= length) return text;
273
+ return text.substring(0, length - suffix.length) + suffix;
274
+ }
275
+
276
+ /**
277
+ * Slugify text
278
+ */
279
+ export function slugify(text: string): string {
280
+ return text
281
+ .toLowerCase()
282
+ .trim()
283
+ .replace(/[^\w\s-]/g, '')
284
+ .replace(/[\s_-]+/g, '-')
285
+ .replace(/^-+|-+$/g, '');
286
+ }
287
+
288
+ // ============================================================
289
+ // Cache Utilities
290
+ // ============================================================
291
+
292
+ /**
293
+ * Generate cache key
294
+ */
295
+ export function generateCacheKey(request: Request, prefix?: string): string {
296
+ const url = new URL(request.url);
297
+ const parts = [prefix || 'cache', url.pathname];
298
+
299
+ // Add query params (sorted for consistency)
300
+ const sortedParams = Array.from(url.searchParams.entries()).sort(
301
+ ([a], [b]) => a.localeCompare(b)
302
+ );
303
+
304
+ if (sortedParams.length > 0) {
305
+ parts.push(
306
+ sortedParams
307
+ .map(([key, value]) => `${key}=${value}`)
308
+ .join('&')
309
+ );
310
+ }
311
+
312
+ // Add auth header if present (for user-specific caching)
313
+ const auth = request.headers.get('Authorization');
314
+ if (auth) {
315
+ parts.push(auth.substring(0, 20)); // First 20 chars of auth
316
+ }
317
+
318
+ return parts.join(':');
319
+ }
320
+
321
+ /**
322
+ * Generate hash
323
+ */
324
+ export async function hash(input: string): Promise<string> {
325
+ const encoder = new TextEncoder();
326
+ const data = encoder.encode(input);
327
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
328
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
329
+ return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
330
+ }
331
+
332
+ /**
333
+ * Parse cache control header
334
+ */
335
+ export function parseCacheControl(header: string): {
336
+ maxAge?: number;
337
+ noCache?: boolean;
338
+ noStore?: boolean;
339
+ mustRevalidate?: boolean;
340
+ } {
341
+ const directives = header.split(',').map((d) => d.trim());
342
+ const result: Record<string, boolean | number> = {};
343
+
344
+ for (const directive of directives) {
345
+ const [key, value] = directive.split('=');
346
+
347
+ switch (key.toLowerCase()) {
348
+ case 'max-age':
349
+ result.maxAge = parseInt(value, 10);
350
+ break;
351
+ case 'no-cache':
352
+ result.noCache = true;
353
+ break;
354
+ case 'no-store':
355
+ result.noStore = true;
356
+ break;
357
+ case 'must-revalidate':
358
+ result.mustRevalidate = true;
359
+ break;
360
+ }
361
+ }
362
+
363
+ return result as {
364
+ maxAge?: number;
365
+ noCache?: boolean;
366
+ noStore?: boolean;
367
+ mustRevalidate?: boolean;
368
+ };
369
+ }
370
+
371
+ // ============================================================
372
+ // Time Utilities
373
+ // ============================================================
374
+
375
+ /**
376
+ * Parse duration string to seconds
377
+ */
378
+ export function parseDuration(duration: string): number {
379
+ const match = duration.match(/^(\d+)(s|m|h|d)$/);
380
+
381
+ if (!match) {
382
+ throw new Error(`Invalid duration format: ${duration}`);
383
+ }
384
+
385
+ const value = parseInt(match[1], 10);
386
+ const unit = match[2];
387
+
388
+ switch (unit) {
389
+ case 's':
390
+ return value;
391
+ case 'm':
392
+ return value * 60;
393
+ case 'h':
394
+ return value * 3600;
395
+ case 'd':
396
+ return value * 86400;
397
+ default:
398
+ throw new Error(`Invalid duration unit: ${unit}`);
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Format duration
404
+ */
405
+ export function formatDuration(seconds: number): string {
406
+ if (seconds < 60) return `${seconds}s`;
407
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
408
+ if (seconds < 86400)
409
+ return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
410
+ return `${Math.floor(seconds / 86400)}d ${Math.floor((seconds % 86400) / 3600)}h`;
411
+ }
412
+
413
+ /**
414
+ * Sleep utility
415
+ */
416
+ export function sleep(ms: number): Promise<void> {
417
+ return new Promise((resolve) => setTimeout(resolve, ms));
418
+ }
419
+
420
+ /**
421
+ * Retry with exponential backoff
422
+ */
423
+ export async function retry<T>(
424
+ fn: () => Promise<T>,
425
+ options: {
426
+ maxAttempts?: number;
427
+ initialDelay?: number;
428
+ maxDelay?: number;
429
+ backoffMultiplier?: number;
430
+ } = {}
431
+ ): Promise<T> {
432
+ const {
433
+ maxAttempts = 3,
434
+ initialDelay = 1000,
435
+ maxDelay = 10000,
436
+ backoffMultiplier = 2,
437
+ } = options;
438
+
439
+ let lastError: Error | undefined;
440
+
441
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
442
+ try {
443
+ return await fn();
444
+ } catch (error) {
445
+ lastError = error as Error;
446
+
447
+ if (attempt < maxAttempts - 1) {
448
+ const delay = Math.min(
449
+ initialDelay * Math.pow(backoffMultiplier, attempt),
450
+ maxDelay
451
+ );
452
+ await sleep(delay);
453
+ }
454
+ }
455
+ }
456
+
457
+ throw lastError;
458
+ }
459
+
460
+ // ============================================================
461
+ // URL Utilities
462
+ // ============================================================
463
+
464
+ /**
465
+ * Build URL with query params
466
+ */
467
+ export function buildURL(base: string, params: Record<string, string | number | boolean | undefined>): string {
468
+ const url = new URL(base);
469
+ Object.entries(params).forEach(([key, value]) => {
470
+ if (value !== undefined) {
471
+ url.searchParams.set(key, String(value));
472
+ }
473
+ });
474
+ return url.toString();
475
+ }
476
+
477
+ /**
478
+ * Parse query params
479
+ */
480
+ export function parseQueryParams(url: string): Record<string, string> {
481
+ const params = new URL(url).searchParams;
482
+ return Object.fromEntries(params.entries());
483
+ }
484
+
485
+ /**
486
+ * Join URL paths
487
+ */
488
+ export function joinPath(...parts: string[]): string {
489
+ return parts
490
+ .map((part) => part.replace(/^\/+|\/+$/g, ''))
491
+ .filter(Boolean)
492
+ .join('/');
493
+ }
494
+
495
+ // ============================================================
496
+ // Encoding Utilities
497
+ // ============================================================
498
+
499
+ /**
500
+ * Base64 encode
501
+ */
502
+ export function base64Encode(input: string): string {
503
+ return btoa(input);
504
+ }
505
+
506
+ /**
507
+ * Base64 decode
508
+ */
509
+ export function base64Decode(input: string): string {
510
+ return atob(input);
511
+ }
512
+
513
+ /**
514
+ * Base64 URL encode
515
+ */
516
+ export function base64URLEncode(input: string): string {
517
+ return base64Encode(input)
518
+ .replace(/\+/g, '-')
519
+ .replace(/\//g, '_')
520
+ .replace(/=/g, '');
521
+ }
522
+
523
+ /**
524
+ * Base64 URL decode
525
+ */
526
+ export function base64URLDecode(input: string): string {
527
+ let base64 = input.replace(/-/g, '+').replace(/_/g, '/');
528
+
529
+ while (base64.length % 4) {
530
+ base64 += '=';
531
+ }
532
+
533
+ return base64Decode(base64);
534
+ }
535
+
536
+ /**
537
+ * URL safe encode
538
+ */
539
+ export function urlSafeEncode(input: string): string {
540
+ return encodeURIComponent(input);
541
+ }
542
+
543
+ /**
544
+ * URL safe decode
545
+ */
546
+ export function urlSafeDecode(input: string): string {
547
+ return decodeURIComponent(input);
548
+ }
549
+
550
+ // ============================================================
551
+ // Array Utilities
552
+ // ============================================================
553
+
554
+ /**
555
+ * Chunk array
556
+ */
557
+ export function chunk<T>(array: T[], size: number): T[][] {
558
+ const chunks: T[][] = [];
559
+ for (let i = 0; i < array.length; i += size) {
560
+ chunks.push(array.slice(i, i + size));
561
+ }
562
+ return chunks;
563
+ }
564
+
565
+ /**
566
+ * Shuffle array
567
+ */
568
+ export function shuffle<T>(array: T[]): T[] {
569
+ const result = [...array];
570
+ for (let i = result.length - 1; i > 0; i--) {
571
+ const j = Math.floor(Math.random() * (i + 1));
572
+ [result[i], result[j]] = [result[j], result[i]];
573
+ }
574
+ return result;
575
+ }
576
+
577
+ /**
578
+ * Unique array
579
+ */
580
+ export function unique<T>(array: T[]): T[] {
581
+ return Array.from(new Set(array));
582
+ }
583
+
584
+ /**
585
+ * Group array by key
586
+ */
587
+ export function groupBy<T>(
588
+ array: T[],
589
+ key: keyof T
590
+ ): Record<string, T[]> {
591
+ return array.reduce((result, item) => {
592
+ const group = String(item[key]);
593
+ if (!result[group]) {
594
+ result[group] = [];
595
+ }
596
+ result[group].push(item);
597
+ return result;
598
+ }, {} as Record<string, T[]>);
599
+ }
600
+
601
+ // ============================================================
602
+ // Object Utilities
603
+ // ============================================================
604
+
605
+ /**
606
+ * Deep clone object
607
+ */
608
+ export function deepClone<T>(obj: T): T {
609
+ return JSON.parse(JSON.stringify(obj));
610
+ }
611
+
612
+ /**
613
+ * Deep merge objects
614
+ */
615
+ export function deepMerge<T extends Record<string, any>>(target: T, ...sources: Partial<T>[]): T {
616
+ if (!sources.length) return target;
617
+ const source = sources.shift();
618
+
619
+ if (isObject(target) && isObject(source)) {
620
+ for (const key in source) {
621
+ if (isObject(source[key])) {
622
+ if (!target[key]) Object.assign(target, { [key]: {} });
623
+ deepMerge(target[key], source[key]);
624
+ } else {
625
+ Object.assign(target, { [key]: source[key] });
626
+ }
627
+ }
628
+ }
629
+
630
+ return deepMerge(target, ...sources);
631
+ }
632
+
633
+ function isObject(item: unknown): item is Record<string, unknown> {
634
+ return Boolean(item && typeof item === 'object' && !Array.isArray(item));
635
+ }
636
+
637
+ /**
638
+ * Pick properties from object
639
+ */
640
+ export function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
641
+ const result = {} as Pick<T, K>;
642
+ keys.forEach((key) => {
643
+ if (key in obj) {
644
+ result[key] = obj[key];
645
+ }
646
+ });
647
+ return result;
648
+ }
649
+
650
+ /**
651
+ * Omit properties from object
652
+ */
653
+ export function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
654
+ const result = { ...obj };
655
+ keys.forEach((key) => {
656
+ delete result[key];
657
+ });
658
+ return result as Omit<T, K>;
659
+ }
660
+
661
+ // ============================================================
662
+ // Random Utilities
663
+ // ============================================================
664
+
665
+ /**
666
+ * Generate random string
667
+ */
668
+ export function randomString(length: number = 16): string {
669
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
670
+ let result = '';
671
+ for (let i = 0; i < length; i++) {
672
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
673
+ }
674
+ return result;
675
+ }
676
+
677
+ /**
678
+ * Generate random ID
679
+ */
680
+ export function randomID(prefix: string = ''): string {
681
+ const timestamp = Date.now().toString(36);
682
+ const random = Math.random().toString(36).substring(2, 9);
683
+ return prefix ? `${prefix}_${timestamp}${random}` : `${timestamp}${random}`;
684
+ }
685
+
686
+ /**
687
+ * Random item from array
688
+ */
689
+ export function randomItem<T>(array: T[]): T {
690
+ return array[Math.floor(Math.random() * array.length)];
691
+ }
692
+
693
+ /**
694
+ * Random number in range
695
+ */
696
+ export function randomInRange(min: number, max: number): number {
697
+ return Math.floor(Math.random() * (max - min + 1)) + min;
698
+ }
699
+
700
+ // ============================================================
701
+ // Type Guards
702
+ // ============================================================
703
+
704
+ /**
705
+ * Check if value is defined
706
+ */
707
+ export function isDefined<T>(value: T | null | undefined): value is T {
708
+ return value !== null && value !== undefined;
709
+ }
710
+
711
+ /**
712
+ * Check if value is empty
713
+ */
714
+ export function isEmpty(value: unknown): boolean {
715
+ if (value === null || value === undefined) return true;
716
+ if (typeof value === 'string') return value.trim().length === 0;
717
+ if (Array.isArray(value)) return value.length === 0;
718
+ if (typeof value === 'object') return Object.keys(value).length === 0;
719
+ return false;
720
+ }
721
+
722
+ /**
723
+ * Check if value is a plain object
724
+ */
725
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
726
+ return (
727
+ typeof value === 'object' &&
728
+ value !== null &&
729
+ !Array.isArray(value) &&
730
+ Object.prototype.toString.call(value) === '[object Object]'
731
+ );
732
+ }