authorgravatar for mohammedshazeb10@gmail.commohammed shazeb <mohammedshazeb10@gmail.com> 2026-08-15 17:31:26+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-15 12:01:26+00:00
log34a12bc33f5fba02b0b398e7dff35ca041a87dc7
treea85b50df50b71d33436b37acef554815dd2e254f
parent7afa184a99b266d466cc9517c147a75c3d839ad7
signature Signed by PGP key B5690EEEBB952194

fix(web): reject unsupported composer image types at attach time (#6574)


5 files changed, 39 insertions(+), 0 deletions(-)

apps/mobile/src/features/sharing/incoming-share-model.ts+8
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import {1import {
2 isProviderSendTurnSupportedImageMimeType,
2 PROVIDER_SEND_TURN_MAX_ATTACHMENTS,3 PROVIDER_SEND_TURN_MAX_ATTACHMENTS,
3 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,4 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
4} from "@t3tools/contracts";5} from "@t3tools/contracts";
...@@ -162,6 +163,13 @@ export async function buildIncomingShareDraft(input: {...@@ -162,6 +163,13 @@ export async function buildIncomingShareDraft(input: {
162 await releaseOwnedFiles(input.fileReader, [uri, payload.value]);163 await releaseOwnedFiles(input.fileReader, [uri, payload.value]);
163 continue;164 continue;
164 }165 }
166 if (!isProviderSendTurnSupportedImageMimeType(mimeType)) {
167 warnings.push(
168 `'${resolved?.originalName ?? fallbackName(uri, index, mimeType)}' is not a supported image type.`,
169 );
170 await releaseOwnedFiles(input.fileReader, [uri, payload.value]);
171 continue;
172 }
165 if (173 if (
166 resolved?.contentSize !== null &&174 resolved?.contentSize !== null &&
167 resolved?.contentSize !== undefined &&175 resolved?.contentSize !== undefined &&
apps/mobile/src/lib/composerImages.ts+5
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import {1import {
2 isProviderSendTurnSupportedImageMimeType,
2 PROVIDER_SEND_TURN_MAX_ATTACHMENTS,3 PROVIDER_SEND_TURN_MAX_ATTACHMENTS,
3 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,4 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
4 type UploadChatImageAttachment,5 type UploadChatImageAttachment,
...@@ -98,6 +99,10 @@ export async function pickComposerImages(input: { readonly existingCount: number...@@ -98,6 +99,10 @@ export async function pickComposerImages(input: { readonly existingCount: number
98 error = `Unsupported file type for '${asset.fileName ?? "image"}'.`;99 error = `Unsupported file type for '${asset.fileName ?? "image"}'.`;
99 continue;100 continue;
100 }101 }
102 if (!isProviderSendTurnSupportedImageMimeType(mimeType)) {
103 error = `'${asset.fileName ?? "image"}' is not a supported image type. Attach GIF, JPEG, PNG, or WebP images.`;
104 continue;
105 }
101106
102 const base64 = asset.base64;107 const base64 = asset.base64;
103 if (!base64) {108 if (!base64) {
apps/web/src/components/chat/ChatComposer.tsx+5
...@@ -12,6 +12,7 @@ import type {...@@ -12,6 +12,7 @@ import type {
12 ThreadId,12 ThreadId,
13} from "@t3tools/contracts";13} from "@t3tools/contracts";
14import {14import {
15 isProviderSendTurnSupportedImageMimeType,
15 ProviderDriverKind,16 ProviderDriverKind,
16 ProviderInstanceId,17 ProviderInstanceId,
17 PROVIDER_SEND_TURN_MAX_ATTACHMENTS,18 PROVIDER_SEND_TURN_MAX_ATTACHMENTS,
...@@ -2305,6 +2306,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2305,6 +2306,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2305 error = `Unsupported file type for '${file.name}'. Please attach image files only.`;2306 error = `Unsupported file type for '${file.name}'. Please attach image files only.`;
2306 continue;2307 continue;
2307 }2308 }
2309 if (!isProviderSendTurnSupportedImageMimeType(file.type)) {
2310 error = `'${file.name}' is not a supported image type. Attach GIF, JPEG, PNG, or WebP images.`;
2311 continue;
2312 }
2308 if (reservedCount >= PROVIDER_SEND_TURN_MAX_ATTACHMENTS) {2313 if (reservedCount >= PROVIDER_SEND_TURN_MAX_ATTACHMENTS) {
2309 error = `You can attach up to ${PROVIDER_SEND_TURN_MAX_ATTACHMENTS} images per message.`;2314 error = `You can attach up to ${PROVIDER_SEND_TURN_MAX_ATTACHMENTS} images per message.`;
2310 break;2315 break;
packages/contracts/src/orchestration.test.ts+7
...@@ -23,6 +23,7 @@ import {...@@ -23,6 +23,7 @@ import {
23 ThreadCreatedPayload,23 ThreadCreatedPayload,
24 ThreadTurnDiff,24 ThreadTurnDiff,
25 ThreadTurnStartRequestedPayload,25 ThreadTurnStartRequestedPayload,
26 isProviderSendTurnSupportedImageMimeType,
26} from "./orchestration.ts";27} from "./orchestration.ts";
27import { ProviderInstanceId } from "./providerInstance.ts";28import { ProviderInstanceId } from "./providerInstance.ts";
2829
...@@ -935,3 +936,9 @@ it.effect("project favicon overrides accept only supported image files", () =>...@@ -935,3 +936,9 @@ it.effect("project favicon overrides accept only supported image files", () =>
935 assert.strictEqual(invalid._tag, "Failure");936 assert.strictEqual(invalid._tag, "Failure");
936 }),937 }),
937);938);
939
940it("isProviderSendTurnSupportedImageMimeType accepts raster formats and rejects svg", () => {
941 assert.strictEqual(isProviderSendTurnSupportedImageMimeType("image/png"), true);
942 assert.strictEqual(isProviderSendTurnSupportedImageMimeType("IMAGE/JPEG"), true);
943 assert.strictEqual(isProviderSendTurnSupportedImageMimeType("image/svg+xml"), false);
944});
packages/contracts/src/orchestration.ts+14
...@@ -144,6 +144,20 @@ export type ProviderUserInputAnswers = typeof ProviderUserInputAnswers.Type;...@@ -144,6 +144,20 @@ export type ProviderUserInputAnswers = typeof ProviderUserInputAnswers.Type;
144export const PROVIDER_SEND_TURN_MAX_INPUT_CHARS = 120_000;144export const PROVIDER_SEND_TURN_MAX_INPUT_CHARS = 120_000;
145export const PROVIDER_SEND_TURN_MAX_ATTACHMENTS = 8;145export const PROVIDER_SEND_TURN_MAX_ATTACHMENTS = 8;
146export const PROVIDER_SEND_TURN_MAX_IMAGE_BYTES = 10 * 1024 * 1024;146export const PROVIDER_SEND_TURN_MAX_IMAGE_BYTES = 10 * 1024 * 1024;
147export const PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPES = [
148 "image/gif",
149 "image/jpeg",
150 "image/png",
151 "image/webp",
152] as const;
153const PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPE_SET = new Set<string>(
154 PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPES,
155);
156
157/** Whether a pasted or picked image mime type can be sent on a provider turn. */
158export function isProviderSendTurnSupportedImageMimeType(mimeType: string): boolean {
159 return PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPE_SET.has(mimeType.toLowerCase());
160}
147const PROVIDER_SEND_TURN_MAX_IMAGE_DATA_URL_CHARS = 14_000_000;161const PROVIDER_SEND_TURN_MAX_IMAGE_DATA_URL_CHARS = 14_000_000;
148const CHAT_ATTACHMENT_ID_MAX_CHARS = 128;162const CHAT_ATTACHMENT_ID_MAX_CHARS = 128;
149// Correlation id is command id by design in this model.163// Correlation id is command id by design in this model.