From 34a12bc33f5fba02b0b398e7dff35ca041a87dc7 Mon Sep 17 00:00:00 2001 From: mohammed shazeb Date: Sat, 15 Aug 2026 17:31:26 +0530 Subject: [PATCH] fix(web): reject unsupported composer image types at attach time (#6574) --- .../src/features/sharing/incoming-share-model.ts | 8 ++++++++ apps/mobile/src/lib/composerImages.ts | 5 +++++ apps/web/src/components/chat/ChatComposer.tsx | 5 +++++ packages/contracts/src/orchestration.test.ts | 7 +++++++ packages/contracts/src/orchestration.ts | 14 ++++++++++++++ 5 files changed, 39 insertions(+) diff --git a/apps/mobile/src/features/sharing/incoming-share-model.ts b/apps/mobile/src/features/sharing/incoming-share-model.ts index 873574209fc52ed9357f5cae4542849b887f5c5c..d9985a7000517ca660eeb414a459c407f4b32fd1 100644 --- a/apps/mobile/src/features/sharing/incoming-share-model.ts +++ b/apps/mobile/src/features/sharing/incoming-share-model.ts @@ -1,4 +1,5 @@ import { + isProviderSendTurnSupportedImageMimeType, PROVIDER_SEND_TURN_MAX_ATTACHMENTS, PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, } from "@t3tools/contracts"; @@ -162,6 +163,13 @@ export async function buildIncomingShareDraft(input: { await releaseOwnedFiles(input.fileReader, [uri, payload.value]); continue; } + if (!isProviderSendTurnSupportedImageMimeType(mimeType)) { + warnings.push( + `'${resolved?.originalName ?? fallbackName(uri, index, mimeType)}' is not a supported image type.`, + ); + await releaseOwnedFiles(input.fileReader, [uri, payload.value]); + continue; + } if ( resolved?.contentSize !== null && resolved?.contentSize !== undefined && diff --git a/apps/mobile/src/lib/composerImages.ts b/apps/mobile/src/lib/composerImages.ts index e92bb0c6e08bb14464bc66e31c49719b52fe805e..747b7afd31bc75adc4af337e872dbfc26a7be11f 100644 --- a/apps/mobile/src/lib/composerImages.ts +++ b/apps/mobile/src/lib/composerImages.ts @@ -1,4 +1,5 @@ import { + isProviderSendTurnSupportedImageMimeType, PROVIDER_SEND_TURN_MAX_ATTACHMENTS, PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, type UploadChatImageAttachment, @@ -98,6 +99,10 @@ export async function pickComposerImages(input: { readonly existingCount: number error = `Unsupported file type for '${asset.fileName ?? "image"}'.`; continue; } + if (!isProviderSendTurnSupportedImageMimeType(mimeType)) { + error = `'${asset.fileName ?? "image"}' is not a supported image type. Attach GIF, JPEG, PNG, or WebP images.`; + continue; + } const base64 = asset.base64; if (!base64) { diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index afba1e086b8434419814291449c34998fbe838df..5a01c5c76435ca1397cbeee19b23263fb866ae69 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -12,6 +12,7 @@ import type { ThreadId, } from "@t3tools/contracts"; import { + isProviderSendTurnSupportedImageMimeType, ProviderDriverKind, ProviderInstanceId, PROVIDER_SEND_TURN_MAX_ATTACHMENTS, @@ -2305,6 +2306,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) error = `Unsupported file type for '${file.name}'. Please attach image files only.`; continue; } + if (!isProviderSendTurnSupportedImageMimeType(file.type)) { + error = `'${file.name}' is not a supported image type. Attach GIF, JPEG, PNG, or WebP images.`; + continue; + } if (reservedCount >= PROVIDER_SEND_TURN_MAX_ATTACHMENTS) { error = `You can attach up to ${PROVIDER_SEND_TURN_MAX_ATTACHMENTS} images per message.`; break; diff --git a/packages/contracts/src/orchestration.test.ts b/packages/contracts/src/orchestration.test.ts index eba1b4648b25d92764233a50d22567bc7b03d6fa..f403e6de26ccdcc28a9482ed1e666a1d50956dee 100644 --- a/packages/contracts/src/orchestration.test.ts +++ b/packages/contracts/src/orchestration.test.ts @@ -23,6 +23,7 @@ import { ThreadCreatedPayload, ThreadTurnDiff, ThreadTurnStartRequestedPayload, + isProviderSendTurnSupportedImageMimeType, } from "./orchestration.ts"; import { ProviderInstanceId } from "./providerInstance.ts"; @@ -935,3 +936,9 @@ it.effect("project favicon overrides accept only supported image files", () => assert.strictEqual(invalid._tag, "Failure"); }), ); + +it("isProviderSendTurnSupportedImageMimeType accepts raster formats and rejects svg", () => { + assert.strictEqual(isProviderSendTurnSupportedImageMimeType("image/png"), true); + assert.strictEqual(isProviderSendTurnSupportedImageMimeType("IMAGE/JPEG"), true); + assert.strictEqual(isProviderSendTurnSupportedImageMimeType("image/svg+xml"), false); +}); diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 35fef721efa7dda42f457179e5f2ec08f8a75454..cd9f3a74787647768b7bd2ff848531d5d68545c1 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -144,6 +144,20 @@ export type ProviderUserInputAnswers = typeof ProviderUserInputAnswers.Type; export const PROVIDER_SEND_TURN_MAX_INPUT_CHARS = 120_000; export const PROVIDER_SEND_TURN_MAX_ATTACHMENTS = 8; export const PROVIDER_SEND_TURN_MAX_IMAGE_BYTES = 10 * 1024 * 1024; +export const PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPES = [ + "image/gif", + "image/jpeg", + "image/png", + "image/webp", +] as const; +const PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPE_SET = new Set( + PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPES, +); + +/** Whether a pasted or picked image mime type can be sent on a provider turn. */ +export function isProviderSendTurnSupportedImageMimeType(mimeType: string): boolean { + return PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPE_SET.has(mimeType.toLowerCase()); +} const PROVIDER_SEND_TURN_MAX_IMAGE_DATA_URL_CHARS = 14_000_000; const CHAT_ATTACHMENT_ID_MAX_CHARS = 128; // Correlation id is command id by design in this model. -- 2.54.0