diff --git a/apps/desktop-client/src/main/adapters/media-image.test.ts b/apps/desktop-client/src/main/adapters/media-image.test.ts index 23376e2..374e261 100644 --- a/apps/desktop-client/src/main/adapters/media-image.test.ts +++ b/apps/desktop-client/src/main/adapters/media-image.test.ts @@ -18,6 +18,7 @@ vi.mock("electron", () => ({ })); vi.mock("../transport/api-client", () => ({ + fetchDesktopApiURL: vi.fn(), resolveDesktopApiURL: (path: string) => `http://127.0.0.1:8080${path}`, })); @@ -27,10 +28,12 @@ import { } from "./media-image"; describe("media image helpers", () => { - it("resolves API asset URLs through the desktop API base and requests png output", () => { - expect(buildAssetURLCandidates("/api/public/assets/12").at(0)).toBe( + it("resolves public asset URLs through public and authenticated desktop candidates", () => { + expect(buildAssetURLCandidates("/api/public/assets/12")).toEqual([ "http://127.0.0.1:8080/api/public/assets/12?format=png", - ); + "http://127.0.0.1:8080/api/desktop/content/assets/12?format=png", + "http://127.0.0.1:8080/api/public/assets/12", + ]); }); it("centers crop rectangles at the requested ratio", () => { diff --git a/apps/desktop-client/src/main/adapters/media-image.ts b/apps/desktop-client/src/main/adapters/media-image.ts index 50fdbec..c127443 100644 --- a/apps/desktop-client/src/main/adapters/media-image.ts +++ b/apps/desktop-client/src/main/adapters/media-image.ts @@ -2,7 +2,7 @@ import { Buffer } from "node:buffer"; import { nativeImage } from "electron"; -import { resolveDesktopApiURL } from "../transport/api-client"; +import { fetchDesktopApiURL, resolveDesktopApiURL } from "../transport/api-client"; export interface ImageAssetBlob { blob: Blob; @@ -20,6 +20,24 @@ export interface CropRect { const passthroughImageTypes = new Set(["image/png", "image/jpeg", "image/jpg", "image/gif"]); +function buildDesktopAssetFallbackURL(publicAssetUrl: URL): string | null { + if (!publicAssetUrl.pathname.startsWith("/api/public/assets/")) { + return null; + } + + const token = publicAssetUrl.pathname.slice("/api/public/assets/".length); + if (!token) { + return null; + } + + const fallbackURL = new URL(resolveDesktopApiURL(`/api/desktop/content/assets/${token}`)); + publicAssetUrl.searchParams.forEach((value, key) => { + fallbackURL.searchParams.set(key, value); + }); + fallbackURL.searchParams.set("format", "png"); + return fallbackURL.toString(); +} + export function buildAssetURLCandidates(sourceUrl: string): string[] { const trimmed = sourceUrl.trim(); if (!trimmed) { @@ -46,6 +64,11 @@ export function buildAssetURLCandidates(sourceUrl: string): string[] { apiAssetUrl.searchParams.set("format", "png"); } pushCandidate(apiAssetUrl.toString()); + + const desktopAssetFallbackURL = buildDesktopAssetFallbackURL(apiAssetUrl); + if (desktopAssetFallbackURL) { + pushCandidate(desktopAssetFallbackURL); + } } pushCandidate(parsed.toString()); @@ -56,9 +79,29 @@ export function buildAssetURLCandidates(sourceUrl: string): string[] { return candidates; } +function shouldUseDesktopAssetFetch(candidate: string): boolean { + try { + return new URL(candidate).pathname.startsWith("/api/desktop/content/assets/"); + } catch { + return false; + } +} + +function fileNameForImageType(sourceType: string): string { + if (sourceType === "image/gif") { + return "image.gif"; + } + if (sourceType === "image/jpeg" || sourceType === "image/jpg") { + return "image.jpg"; + } + return "image.png"; +} + export async function fetchImageAssetBlob(sourceUrl: string): Promise { for (const candidate of buildAssetURLCandidates(sourceUrl)) { - const response = await fetch(candidate).catch(() => null); + const response = await ( + shouldUseDesktopAssetFetch(candidate) ? fetchDesktopApiURL(candidate) : fetch(candidate) + ).catch(() => null); if (!response?.ok) { continue; } @@ -80,7 +123,7 @@ export async function fetchImageAssetBlob(sourceUrl: string): Promise { + const headers = new Headers(init.headers); + if (transportSession.clientToken && !headers.has("Authorization")) { + headers.set("Authorization", `Bearer ${transportSession.clientToken}`); + } + + const url = + input instanceof URL + ? input.toString() + : /^https?:\/\//i.test(input) + ? input + : resolveDesktopURL(input); + return fetch(url, { + ...init, + headers, + }); +} + function resolveObservedRequestURL(path: string | undefined, baseURL: string): string { if (!path) { return baseURL;