| author | |
| committer | |
| log | 70857ce299941ac18a4509b10c725aba7e15a6b5 |
| tree | 58ba72b018f37f6eb006a037b19efc9d87df45fa |
| parent | 5e0ec383838ee95a60d40df8f700c1cc4f740b74 |
- 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 @@ |
| 1 | import { describe, expect, it } from "vitest"; | |
| 2 | ||
| 3 | import { parseBase64DataUrl } from "./imageMime.ts"; | |
| 4 | ||
| 5 | describe("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 | 32 | export function parseBase64DataUrl( |
| 33 | 33 | dataUrl: string, |
| 34 | 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 | 36 | if (!match) return null; |
| 37 | 37 | |
| 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 | 51 | const base64 = match[2]?.replace(/\s+/g, ""); |
| 40 | 52 | if (!mimeType || !base64) return null; |
| 41 | 53 |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+1-2| ... | ... | @@ -421,7 +421,6 @@ const make = Effect.gen(function* () { |
| 421 | 421 | { threadId: input.threadId, cwd, oldBranch, cause: Cause.pretty(cause) }, |
| 422 | 422 | ), |
| 423 | 423 | ), |
| 424 | Effect.forkScoped, | |
| 425 | 424 | ); |
| 426 | 425 | }); |
| 427 | 426 | |
| ... | ... | @@ -464,7 +463,7 @@ const make = Effect.gen(function* () { |
| 464 | 463 | messageId: message.id, |
| 465 | 464 | messageText: message.text, |
| 466 | 465 | ...(message.attachments !== undefined ? { attachments: branchGenerationAttachments } : {}), |
| 467 | }); | |
| 466 | }).pipe(Effect.forkScoped); | |
| 468 | 467 | |
| 469 | 468 | yield* sendTurnForThread({ |
| 470 | 469 | threadId: event.payload.threadId, |