feat(desktop-client): fall back to authenticated desktop asset endpoint
When a public asset URL fails (e.g. stale signed token), retry via the new /api/desktop/content/assets endpoint with the desktop Bearer token so editor images keep rendering across token rotations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ vi.mock("electron", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../transport/api-client", () => ({
|
vi.mock("../transport/api-client", () => ({
|
||||||
|
fetchDesktopApiURL: vi.fn(),
|
||||||
resolveDesktopApiURL: (path: string) => `http://127.0.0.1:8080${path}`,
|
resolveDesktopApiURL: (path: string) => `http://127.0.0.1:8080${path}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -27,10 +28,12 @@ import {
|
|||||||
} from "./media-image";
|
} from "./media-image";
|
||||||
|
|
||||||
describe("media image helpers", () => {
|
describe("media image helpers", () => {
|
||||||
it("resolves API asset URLs through the desktop API base and requests png output", () => {
|
it("resolves public asset URLs through public and authenticated desktop candidates", () => {
|
||||||
expect(buildAssetURLCandidates("/api/public/assets/12").at(0)).toBe(
|
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/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", () => {
|
it("centers crop rectangles at the requested ratio", () => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Buffer } from "node:buffer";
|
|||||||
|
|
||||||
import { nativeImage } from "electron";
|
import { nativeImage } from "electron";
|
||||||
|
|
||||||
import { resolveDesktopApiURL } from "../transport/api-client";
|
import { fetchDesktopApiURL, resolveDesktopApiURL } from "../transport/api-client";
|
||||||
|
|
||||||
export interface ImageAssetBlob {
|
export interface ImageAssetBlob {
|
||||||
blob: Blob;
|
blob: Blob;
|
||||||
@@ -20,6 +20,24 @@ export interface CropRect {
|
|||||||
|
|
||||||
const passthroughImageTypes = new Set(["image/png", "image/jpeg", "image/jpg", "image/gif"]);
|
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[] {
|
export function buildAssetURLCandidates(sourceUrl: string): string[] {
|
||||||
const trimmed = sourceUrl.trim();
|
const trimmed = sourceUrl.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
@@ -46,6 +64,11 @@ export function buildAssetURLCandidates(sourceUrl: string): string[] {
|
|||||||
apiAssetUrl.searchParams.set("format", "png");
|
apiAssetUrl.searchParams.set("format", "png");
|
||||||
}
|
}
|
||||||
pushCandidate(apiAssetUrl.toString());
|
pushCandidate(apiAssetUrl.toString());
|
||||||
|
|
||||||
|
const desktopAssetFallbackURL = buildDesktopAssetFallbackURL(apiAssetUrl);
|
||||||
|
if (desktopAssetFallbackURL) {
|
||||||
|
pushCandidate(desktopAssetFallbackURL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pushCandidate(parsed.toString());
|
pushCandidate(parsed.toString());
|
||||||
@@ -56,9 +79,29 @@ export function buildAssetURLCandidates(sourceUrl: string): string[] {
|
|||||||
return candidates;
|
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<ImageAssetBlob | null> {
|
export async function fetchImageAssetBlob(sourceUrl: string): Promise<ImageAssetBlob | null> {
|
||||||
for (const candidate of buildAssetURLCandidates(sourceUrl)) {
|
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) {
|
if (!response?.ok) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -80,7 +123,7 @@ export async function fetchImageAssetBlob(sourceUrl: string): Promise<ImageAsset
|
|||||||
blob: sourceBlob,
|
blob: sourceBlob,
|
||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height,
|
height: size.height,
|
||||||
fileName: sourceType === "image/gif" ? "image.gif" : sourceType === "image/jpeg" || sourceType === "image/jpg" ? "image.jpg" : "image.png",
|
fileName: fileNameForImageType(sourceType),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,24 @@ export function resolveDesktopApiURL(path: string): string {
|
|||||||
return resolveDesktopURL(path);
|
return resolveDesktopURL(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchDesktopApiURL(input: string | URL, init: RequestInit = {}): Promise<Response> {
|
||||||
|
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 {
|
function resolveObservedRequestURL(path: string | undefined, baseURL: string): string {
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return baseURL;
|
return baseURL;
|
||||||
|
|||||||
Reference in New Issue
Block a user