@umituz/react-native-ai-generation-content 1.70.2 → 1.70.4

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/react-native-ai-generation-content",
3
- "version": "1.70.2",
3
+ "version": "1.70.4",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -104,7 +104,7 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
104
104
  </AtomicText>
105
105
  {examplePrompts.slice(0, 4).map((example, index) => (
106
106
  <AtomicButton
107
- key={index}
107
+ key={`${example}-${index}`}
108
108
  variant="outline"
109
109
  size="sm"
110
110
  onPress={() => handleExampleSelect(example)}
@@ -54,7 +54,7 @@ export const ImageSelectionGrid: React.FC<ImageSelectionGridProps> = ({
54
54
  >
55
55
  {images.map((uri, index) => (
56
56
  <GridImageItem
57
- key={index}
57
+ key={uri}
58
58
  styles={styles}
59
59
  uri={uri}
60
60
  index={index}
@@ -30,7 +30,7 @@ export interface UseTextToVideoFormReturn {
30
30
  setSoundEnabled: (enabled: boolean) => void;
31
31
  setProfessionalMode: (enabled: boolean) => void;
32
32
  setFrames: (frames: FrameData[]) => void;
33
- handleFrameChange: (index: number) => void;
33
+ handleFrameChange: (fromIndex: number, toIndex: number) => void;
34
34
  handleFrameDelete: (index: number) => void;
35
35
  selectExamplePrompt: (prompt: string) => void;
36
36
  reset: () => void;
@@ -88,19 +88,38 @@ export function useTextToVideoForm(
88
88
  setState((prev) => ({ ...prev, professionalMode }));
89
89
  }, []);
90
90
 
91
- const handleFrameChange = useCallback((index: number) => {
92
- if (__DEV__) {
93
-
94
- console.log("[TextToVideoForm] Frame change requested:", index);
95
- }
96
- }, []);
91
+ const handleFrameChange = useCallback(
92
+ (fromIndex: number, toIndex: number) => {
93
+ if (fromIndex < 0 || fromIndex >= frames.length || toIndex < 0 || toIndex >= frames.length) {
94
+ if (__DEV__) {
95
+ console.warn("[TextToVideoForm] Invalid frame indices:", { fromIndex, toIndex, length: frames.length });
96
+ }
97
+ return;
98
+ }
99
+
100
+ if (fromIndex === toIndex) return;
101
+
102
+ setFrames((prevFrames) => {
103
+ const newFrames = [...prevFrames];
104
+ const [movedFrame] = newFrames.splice(fromIndex, 1);
105
+ newFrames.splice(toIndex, 0, movedFrame);
106
+ return newFrames;
107
+ });
108
+ },
109
+ [frames.length],
110
+ );
97
111
 
98
112
  const handleFrameDelete = useCallback(
99
113
  (index: number) => {
100
- const newFrames = frames.filter((f) => f.order !== index);
101
- setFrames(newFrames);
114
+ if (index < 0 || index >= frames.length) {
115
+ if (__DEV__) {
116
+ console.warn("[TextToVideoForm] Invalid frame index:", index);
117
+ }
118
+ return;
119
+ }
120
+ setFrames((prevFrames) => prevFrames.filter((_, i) => i !== index));
102
121
  },
103
- [frames],
122
+ [frames.length],
104
123
  );
105
124
 
106
125
  const selectExamplePrompt = useCallback(
@@ -7,15 +7,27 @@ import { HTTP_CONTENT_TYPE } from "./http-methods.constants";
7
7
 
8
8
  /**
9
9
  * Parses response based on content type
10
+ * Note: Type T is not validated at runtime - caller must ensure type safety
10
11
  */
11
12
  export async function parseResponse<T>(response: Response): Promise<T> {
12
13
  const contentType = response.headers.get("content-type");
13
14
 
14
15
  if (contentType?.includes(HTTP_CONTENT_TYPE.JSON)) {
15
- return response.json();
16
+ try {
17
+ return await response.json();
18
+ } catch (error) {
19
+ throw new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : 'Unknown error'}`);
20
+ }
16
21
  }
17
22
 
18
- return response.text() as T;
23
+ // For non-JSON responses, return text
24
+ // WARNING: Type T is not validated - caller is responsible for type safety
25
+ try {
26
+ const text = await response.text();
27
+ return text as T;
28
+ } catch (error) {
29
+ throw new Error(`Failed to parse text response: ${error instanceof Error ? error.message : 'Unknown error'}`);
30
+ }
19
31
  }
20
32
 
21
33
  /**
@@ -45,9 +45,9 @@ class GenerationOrchestratorService {
45
45
 
46
46
  if (typeof __DEV__ !== "undefined" && __DEV__) {
47
47
  console.log("[Orchestrator] Generate started:", {
48
- model: request.model,
49
- capability: request.capability,
50
- provider: provider.providerId,
48
+ hasModel: !!request.model,
49
+ hasCapability: !!request.capability,
50
+ timestamp: new Date().toISOString(),
51
51
  });
52
52
  }
53
53
 
@@ -56,8 +56,8 @@ class GenerationOrchestratorService {
56
56
 
57
57
  if (typeof __DEV__ !== "undefined" && __DEV__) {
58
58
  console.log("[Orchestrator] Job submitted:", {
59
- requestId: submission.requestId,
60
- provider: provider.providerId,
59
+ hasRequestId: !!submission.requestId,
60
+ timestamp: new Date().toISOString(),
61
61
  });
62
62
  }
63
63
 
@@ -91,9 +91,9 @@ class GenerationOrchestratorService {
91
91
 
92
92
  if (typeof __DEV__ !== "undefined" && __DEV__) {
93
93
  console.log("[Orchestrator] Generate completed:", {
94
- requestId: submission.requestId,
95
94
  duration: `${duration}ms`,
96
95
  success: true,
96
+ timestamp: new Date().toISOString(),
97
97
  });
98
98
  }
99
99
 
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { extractErrorMessage, validateProvider, prepareImageInputData } from "../utils";
8
8
  import { extractImageResult } from "../utils/url-extractor";
9
+ import { validateURL } from "../validation/base-validator";
9
10
  import type { ImageResultExtractor } from "../utils/url-extractor";
10
11
  import type { ImageFeatureType } from "../../domain/interfaces";
11
12
 
@@ -62,6 +63,15 @@ export async function executeImageFeature(
62
63
  return { success: false, error: "No image in response" };
63
64
  }
64
65
 
66
+ // Validate the extracted URL for security (prevent SSRF, invalid protocols, etc.)
67
+ const urlValidation = validateURL(imageUrl);
68
+ if (!urlValidation.isValid) {
69
+ return {
70
+ success: false,
71
+ error: `Invalid image URL received: ${urlValidation.errors.join(", ")}`
72
+ };
73
+ }
74
+
65
75
  return {
66
76
  success: true,
67
77
  imageUrl,
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { validateProvider } from "../utils/provider-validator.util";
8
8
  import { formatBase64 } from "../utils/base64.util";
9
+ import { validateURL } from "../validation/base-validator";
9
10
  import { env } from "../config/env.config";
10
11
 
11
12
  declare const __DEV__: boolean;
@@ -59,7 +60,9 @@ export async function executeMultiImageGeneration(
59
60
  if (typeof __DEV__ !== "undefined" && __DEV__) {
60
61
  console.log("[MultiImageExecutor] Generation started", {
61
62
  imageCount: imageUrls.length,
62
- model: input.model,
63
+ hasModel: !!input.model,
64
+ hasPrompt: !!input.prompt,
65
+ timestamp: new Date().toISOString(),
63
66
  });
64
67
  }
65
68
 
@@ -77,12 +80,31 @@ export async function executeMultiImageGeneration(
77
80
  });
78
81
 
79
82
  const rawResult = result as Record<string, unknown>;
80
- const data = (rawResult?.data ?? rawResult) as { images?: Array<{ url: string }> };
81
- const imageUrl = data?.images?.[0]?.url;
83
+ const data = rawResult?.data ?? rawResult;
82
84
 
83
- return imageUrl
84
- ? { success: true, imageUrl }
85
- : { success: false, error: "No image generated" };
85
+ // Type-safe extraction of image URL
86
+ if (data && typeof data === "object" && "images" in data) {
87
+ const images = (data as { images?: unknown }).images;
88
+ if (Array.isArray(images) && images.length > 0) {
89
+ const firstImage = images[0];
90
+ if (firstImage && typeof firstImage === "object" && "url" in firstImage) {
91
+ const imageUrl = (firstImage as { url?: unknown }).url;
92
+ if (typeof imageUrl === "string" && imageUrl.length > 0) {
93
+ // Validate URL for security
94
+ const urlValidation = validateURL(imageUrl);
95
+ if (!urlValidation.isValid) {
96
+ return {
97
+ success: false,
98
+ error: `Invalid image URL received: ${urlValidation.errors.join(", ")}`
99
+ };
100
+ }
101
+ return { success: true, imageUrl };
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ return { success: false, error: "No image generated" };
86
108
  } catch (error) {
87
109
  const message = error instanceof Error ? error.message : "Generation failed";
88
110
  if (typeof __DEV__ !== "undefined" && __DEV__) {
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { extractErrorMessage, validateProvider, prepareVideoInputData } from "../utils";
8
8
  import { extractVideoResult } from "../utils/url-extractor";
9
+ import { validateURL } from "../validation/base-validator";
9
10
  import { DEFAULT_MAX_POLL_TIME_MS } from "../constants";
10
11
  import type { VideoFeatureType } from "../../domain/interfaces";
11
12
  import type { ExecuteVideoFeatureOptions, VideoFeatureResult, VideoFeatureRequest } from "./video-feature-executor.types";
@@ -48,6 +49,15 @@ export async function executeVideoFeature(
48
49
  return { success: false, error: "No video in response" };
49
50
  }
50
51
 
52
+ // Validate URL for security (prevent SSRF, invalid protocols, etc.)
53
+ const urlValidation = validateURL(videoUrl);
54
+ if (!urlValidation.isValid) {
55
+ return {
56
+ success: false,
57
+ error: `Invalid video URL received: ${urlValidation.errors.join(", ")}`
58
+ };
59
+ }
60
+
51
61
  return {
52
62
  success: true,
53
63
  videoUrl,
@@ -4,20 +4,21 @@
4
4
 
5
5
  /**
6
6
  * Safely extracts error message from unknown error type
7
+ * @param error - The error to extract message from
8
+ * @param prefix - Optional prefix to prepend to error message
9
+ * @returns The extracted error message with optional prefix
7
10
  */
8
- export function getErrorMessage(error: unknown): string {
9
- if (error instanceof Error) {
10
- return error.message;
11
- }
11
+ export function getErrorMessage(error: unknown, prefix?: string): string {
12
+ let message = "An unknown error occurred";
12
13
 
13
- if (typeof error === "string") {
14
- return error;
15
- }
16
-
17
- if (error && typeof error === "object" && "message" in error) {
18
- return String(error.message);
14
+ if (error instanceof Error) {
15
+ message = error.message;
16
+ } else if (typeof error === "string") {
17
+ message = error;
18
+ } else if (error && typeof error === "object" && "message" in error) {
19
+ message = String(error.message);
19
20
  }
20
21
 
21
- return "An unknown error occurred";
22
+ return prefix ? `${prefix}: ${message}` : message;
22
23
  }
23
24
 
@@ -4,6 +4,27 @@
4
4
 
5
5
  import { getErrorMessage } from "./error-extractors";
6
6
 
7
+ /**
8
+ * Sanitizes error message to prevent information disclosure
9
+ * Removes sensitive data like file paths, API keys, stack traces
10
+ */
11
+ function sanitizeErrorMessage(message: string): string {
12
+ if (!message) return "An error occurred";
13
+
14
+ // Remove file paths (Unix and Windows style)
15
+ let sanitized = message.replace(/\/[\w\/\-.]+/g, "[PATH]");
16
+ sanitized = sanitized.replace(/[A-Z]:\\[\w\\\-.]+/g, "[PATH]");
17
+
18
+ // Remove potential API keys or tokens (common patterns)
19
+ sanitized = sanitized.replace(/[a-z0-9]{32,}/gi, "[TOKEN]");
20
+
21
+ // Remove stack trace lines
22
+ sanitized = sanitized.split("\n")[0];
23
+
24
+ // Limit length
25
+ return sanitized.slice(0, 500);
26
+ }
27
+
7
28
  /**
8
29
  * Wraps an async function with error handling
9
30
  */
@@ -15,9 +36,19 @@ export async function withErrorHandling<T>(
15
36
  const data = await operation();
16
37
  return { data };
17
38
  } catch (error) {
18
- const appError = error instanceof Error ? error : new Error(getErrorMessage(error));
39
+ const errorMessage = getErrorMessage(error);
40
+ const sanitizedMessage = sanitizeErrorMessage(errorMessage);
41
+ const appError = new Error(sanitizedMessage);
42
+
43
+ // In production, don't expose original error details
44
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
45
+ console.error("[withErrorHandling] Original error:", error);
46
+ }
47
+
19
48
  onError?.(appError);
20
49
  return { error: appError };
21
50
  }
22
51
  }
23
52
 
53
+ declare const __DEV__: boolean;
54
+
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { readFileAsBase64 } from "@umituz/react-native-design-system";
7
7
  import { getAuthService, getCreditService, getPaywallService, isAppServicesConfigured } from "../config/app-services.config";
8
+ import { env } from "../config/env.config";
8
9
 
9
10
  declare const __DEV__: boolean;
10
11
 
@@ -37,6 +38,15 @@ export async function prepareImage(uri: string): Promise<string> {
37
38
  throw new Error("[prepareImage] Failed to convert image to base64");
38
39
  }
39
40
 
41
+ // Validate base64 size to prevent memory exhaustion
42
+ const maxSizeBytes = env.validationMaxBase64SizeMb * 1024 * 1024;
43
+ if (base64.length > maxSizeBytes) {
44
+ throw new Error(
45
+ `[prepareImage] Image exceeds maximum size of ${env.validationMaxBase64SizeMb}MB. ` +
46
+ `Current size: ${(base64.length / (1024 * 1024)).toFixed(2)}MB`
47
+ );
48
+ }
49
+
40
50
  return base64;
41
51
  } catch (error) {
42
52
  const message = error instanceof Error ? error.message : String(error);
@@ -41,8 +41,11 @@ export function extractOutputUrl(
41
41
  const topMedia =
42
42
  (resultObj.image as Record<string, unknown>) ||
43
43
  (resultObj.video as Record<string, unknown>);
44
- if (topMedia && typeof topMedia === "object" && typeof topMedia.url === "string") {
45
- return topMedia.url;
44
+ if (topMedia && typeof topMedia === "object") {
45
+ const url = topMedia.url;
46
+ if (typeof url === "string" && url.length > 0) {
47
+ return url;
48
+ }
46
49
  }
47
50
 
48
51
  // Check nested data/output objects
@@ -29,21 +29,35 @@ export function validateImageData(input: unknown): ValidationResult {
29
29
  return { isValid: false, errors: ["Image data must be a string"] };
30
30
  }
31
31
 
32
- if (input.startsWith("http://") || input.startsWith("https://")) {
33
- return validateURL(input);
32
+ const trimmed = input.trim();
33
+
34
+ // Validate HTTP/HTTPS URLs
35
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
36
+ return validateURL(trimmed);
34
37
  }
35
38
 
36
- if (input.startsWith("data:image/")) {
37
- const base64Part = input.split(",")[1];
38
- if (!base64Part) {
39
+ // Validate data URI format
40
+ if (trimmed.startsWith("data:image/")) {
41
+ const parts = trimmed.split(",");
42
+ if (parts.length !== 2) {
39
43
  return { isValid: false, errors: ["Invalid data URI format"] };
40
44
  }
45
+ const base64Part = parts[1];
46
+ if (!base64Part || base64Part.length === 0) {
47
+ return { isValid: false, errors: ["Invalid data URI: missing base64 data"] };
48
+ }
41
49
  return validateBase64(base64Part);
42
50
  }
43
51
 
52
+ // Validate standalone base64 string (fallback)
53
+ const base64Result = validateBase64(trimmed);
54
+ if (base64Result.isValid) {
55
+ return base64Result;
56
+ }
57
+
44
58
  return {
45
59
  isValid: false,
46
- errors: ["Image data must be a URL or base64 data URI"],
60
+ errors: ["Image data must be a URL, base64 data URI, or valid base64 string"],
47
61
  };
48
62
  }
49
63
 
@@ -11,17 +11,30 @@ export function sanitizeString(input: unknown): string {
11
11
  return "";
12
12
  }
13
13
 
14
- return input
15
- .trim()
14
+ let sanitized = input.trim();
15
+
16
+ // Remove dangerous HTML tags and protocols
17
+ sanitized = sanitized
16
18
  .replace(/[<>]/g, "")
17
19
  .replace(/javascript:/gi, "")
18
- .replace(/data:/gi, "")
20
+ .replace(/data:(?!image\/)/gi, "") // Allow data:image/ for valid use cases
19
21
  .replace(/vbscript:/gi, "")
20
- .replace(/on\w+\s*=/gi, "")
22
+ .replace(/file:/gi, "")
23
+ .replace(/on\w+\s*=/gi, "");
24
+
25
+ // Remove SQL injection patterns
26
+ sanitized = sanitized
21
27
  .replace(/--/g, "")
22
28
  .replace(/;\s*drop\s+/gi, "")
23
- .replace(/['"\\]/g, "")
24
- .slice(0, 10000);
29
+ .replace(/;\s*delete\s+/gi, "")
30
+ .replace(/;\s*insert\s+/gi, "")
31
+ .replace(/;\s*update\s+/gi, "");
32
+
33
+ // Remove Unicode escape sequences that could bypass filters
34
+ sanitized = sanitized.replace(/\\u[\da-fA-F]{4}/g, "");
35
+
36
+ // Limit length to prevent DoS
37
+ return sanitized.slice(0, 10000);
25
38
  }
26
39
 
27
40
  /**
@@ -36,7 +36,7 @@ export const AIGenerationConfig: React.FC<AIGenerationConfigProps> = ({
36
36
  {images.length > 0 && (
37
37
  <View style={styles.imagePreviewContainer}>
38
38
  {images.map((img, index) => (
39
- <View key={index} style={styles.imageWrapper}>
39
+ <View key={img.uri || `image-${index}`} style={styles.imageWrapper}>
40
40
  <Image
41
41
  source={{ uri: img.previewUrl || img.uri }}
42
42
  style={styles.previewImage}
@@ -52,7 +52,7 @@ export function GridSelector<T>({
52
52
  const isSelected = selectedValue === option.value;
53
53
  return (
54
54
  <TouchableOpacity
55
- key={index}
55
+ key={`${option.label}-${index}`}
56
56
  style={[
57
57
  styles.card,
58
58
  {
@@ -52,10 +52,9 @@ export async function handleModeration<TInput, TResult>(
52
52
  isGeneratingRef.current = false;
53
53
  if (isMountedRef.current) resetState();
54
54
  },
55
- async () => {
56
- try {
57
- await executeGeneration(input);
58
- } catch (err) {
55
+ () => {
56
+ // Return the promise to allow proper error handling chain
57
+ return executeGeneration(input).catch((err) => {
59
58
  const error = parseError(err);
60
59
  if (isMountedRef.current) {
61
60
  setState({ status: "error", isGenerating: false, result: null, error });
@@ -63,9 +62,10 @@ export async function handleModeration<TInput, TResult>(
63
62
  showError("Error", getAlertMessage(error, alertMessages));
64
63
  onError?.(error);
65
64
  handleLifecycleComplete("error", undefined, error);
66
- } finally {
65
+ throw error; // Re-throw to allow caller to handle
66
+ }).finally(() => {
67
67
  isGeneratingRef.current = false;
68
- }
68
+ });
69
69
  },
70
70
  );
71
71
  return undefined;