authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-28 23:53:19-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-01 20:06:21-08:00
log70857ce299941ac18a4509b10c725aba7e15a6b5
tree58ba72b018f37f6eb006a037b19efc9d87df45fa
parent5e0ec383838ee95a60d40df8f700c1cc4f740b74

Handle parameterized base64 image data URLs

- accept `data:image/...;...;base64,...` headers while still rejecting invalid formats - add `imageMime` tests for mime params, non-base64 URLs, and missing mime types - fork branch-generation prompt effect at call site in `ProviderCommandReactor`

3 files changed, 42 insertions(+), 4 deletions(-)

apps/server/src/imageMime.test.ts created+27
...@@ -0,0 +1,27 @@
1import { describe, expect, it } from "vitest";
2
3import { parseBase64DataUrl } from "./imageMime.ts";
4
5describe("imageMime", () => {
6 it("parses base64 data URL with mime type", () => {
7 expect(parseBase64DataUrl("data:image/png;base64,SGVsbG8=")).toEqual({
8 mimeType: "image/png",
9 base64: "SGVsbG8=",
10 });
11 });
12
13 it("parses base64 data URL with mime parameters", () => {
14 expect(parseBase64DataUrl("data:image/png;charset=utf-8;base64,SGVsbG8=")).toEqual({
15 mimeType: "image/png",
16 base64: "SGVsbG8=",
17 });
18 });
19
20 it("rejects non-base64 data URL", () => {
21 expect(parseBase64DataUrl("data:image/png;charset=utf-8,hello")).toBeNull();
22 });
23
24 it("rejects missing mime type", () => {
25 expect(parseBase64DataUrl("data:;base64,SGVsbG8=")).toBeNull();
26 });
27});
apps/server/src/imageMime.ts+14-2
...@@ -32,10 +32,22 @@ export const SAFE_IMAGE_FILE_EXTENSIONS = new Set([...@@ -32,10 +32,22 @@ export const SAFE_IMAGE_FILE_EXTENSIONS = new Set([
32export function parseBase64DataUrl(32export function parseBase64DataUrl(
33 dataUrl: string,33 dataUrl: string,
34): { readonly mimeType: string; readonly base64: string } | null {34): { readonly mimeType: string; readonly base64: string } | null {
35 const match = /^data:([^;,]+);base64,([a-z0-9+/=\r\n]+)$/i.exec(dataUrl.trim());35 const match = /^data:([^,]+),([a-z0-9+/=\r\n]+)$/i.exec(dataUrl.trim());
36 if (!match) return null;36 if (!match) return null;
3737
38 const mimeType = match[1]?.trim().toLowerCase();38 const headerParts = (match[1] ?? "")
39 .split(";")
40 .map((part) => part.trim())
41 .filter((part) => part.length > 0);
42 if (headerParts.length < 2) {
43 return null;
44 }
45 const trailingToken = headerParts.at(-1)?.toLowerCase();
46 if (trailingToken !== "base64") {
47 return null;
48 }
49
50 const mimeType = headerParts[0]?.toLowerCase();
39 const base64 = match[2]?.replace(/\s+/g, "");51 const base64 = match[2]?.replace(/\s+/g, "");
40 if (!mimeType || !base64) return null;52 if (!mimeType || !base64) return null;
4153
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+1-2
...@@ -421,7 +421,6 @@ const make = Effect.gen(function* () {...@@ -421,7 +421,6 @@ const make = Effect.gen(function* () {
421 { threadId: input.threadId, cwd, oldBranch, cause: Cause.pretty(cause) },421 { threadId: input.threadId, cwd, oldBranch, cause: Cause.pretty(cause) },
422 ),422 ),
423 ),423 ),
424 Effect.forkScoped,
425 );424 );
426 });425 });
427426
...@@ -464,7 +463,7 @@ const make = Effect.gen(function* () {...@@ -464,7 +463,7 @@ const make = Effect.gen(function* () {
464 messageId: message.id,463 messageId: message.id,
465 messageText: message.text,464 messageText: message.text,
466 ...(message.attachments !== undefined ? { attachments: branchGenerationAttachments } : {}),465 ...(message.attachments !== undefined ? { attachments: branchGenerationAttachments } : {}),
467 });466 }).pipe(Effect.forkScoped);
468467
469 yield* sendTurnForThread({468 yield* sendTurnForThread({
470 threadId: event.payload.threadId,469 threadId: event.payload.threadId,