mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 11:10:23 +02:00
fix(editor): re-sign cross-origin attachment URLs (#6029)
Treat absolute cross-origin attachment API URLs as requiring an authenticated metadata refresh, so draft image previews load on split-origin self-hosted deployments backed by presign-capable or CloudFront-signed storage. Same-origin URLs (relative or absolute) and external image URLs keep their existing no-refetch path. Fixes #6049
This commit is contained in:
@@ -335,7 +335,7 @@ describe("Attachment — image dispatch", () => {
|
||||
// through to the durable markdown_url instead.
|
||||
configStore.setState({ cdnDomain: "cdn.example.test", cdnSigned: true });
|
||||
const id = "11111111-2222-3333-4444-555555555555";
|
||||
const markdownUrl = `https://multica-api.copilothub.ai/api/attachments/${id}/download`;
|
||||
const markdownUrl = `/api/attachments/${id}/download`;
|
||||
const att = makeRecord({
|
||||
id,
|
||||
url: "https://cdn.example.test/uploads/ws/shot.png",
|
||||
@@ -362,6 +362,44 @@ describe("Attachment — image dispatch", () => {
|
||||
expect(getAttachmentMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("re-signs a cross-origin API image URL in the web editor", async () => {
|
||||
// The web app normally uses the same-origin /api proxy, so getBaseUrl is
|
||||
// empty. A self-hosted server can still persist an absolute markdown_url
|
||||
// on a different origin, though. Native <img> loading cannot rely on the
|
||||
// app session cookie being accepted by that host, while an authenticated
|
||||
// metadata request can return a freshly signed storage URL.
|
||||
configStore.setState({ cdnDomain: "cdn.example.test", cdnSigned: true });
|
||||
const id = "11111111-2222-3333-4444-555555555555";
|
||||
const markdownUrl = `https://api.example.test/api/attachments/${id}/download`;
|
||||
const signed =
|
||||
"https://cdn.example.test/uploads/ws/shot.png?Signature=fresh&Key-Pair-Id=K";
|
||||
resolverState.attachments = [
|
||||
makeRecord({
|
||||
id,
|
||||
url: "https://cdn.example.test/uploads/ws/shot.png",
|
||||
markdown_url: markdownUrl,
|
||||
download_url: `/api/attachments/${id}/download`,
|
||||
}),
|
||||
];
|
||||
getAttachmentMock.mockResolvedValue(makeRecord({ id, download_url: signed }));
|
||||
|
||||
renderWithQuery(
|
||||
<Attachment
|
||||
attachment={{
|
||||
kind: "url",
|
||||
url: markdownUrl,
|
||||
filename: "shot.png",
|
||||
forceKind: "image",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector("img")?.getAttribute("src")).toBe(signed);
|
||||
});
|
||||
expect(getAttachmentMock).toHaveBeenCalledWith(id);
|
||||
});
|
||||
|
||||
it("re-signs the inline media URL through getAttachment on token-mode clients (MUL-3254)", async () => {
|
||||
// Desktop / mobile webview: file:// document origin, Bearer-token auth.
|
||||
// The auth-gated /api/attachments/<id>/download endpoint 401s as a
|
||||
|
||||
@@ -321,11 +321,12 @@ const RESIGN_STALE_MS = 20 * 60 * 1000;
|
||||
// endpoint (e.g. a reopened issue draft, whose persisted record deliberately
|
||||
// strips the short-lived signed `download_url`). That endpoint needs
|
||||
// credentials: web loads it because the session cookie rides on the <img>
|
||||
// request (same-site), but Desktop's file:// renderer and the mobile webview
|
||||
// are cross-site — no cookie is attached and the Bearer token cannot be put
|
||||
// on a native resource fetch, so the image 401s. Those clients are exactly
|
||||
// the ones with a non-empty `api.getBaseUrl()` (no same-origin /api proxy),
|
||||
// which is the existing platform signal `absolutizeMediaURL` keys off.
|
||||
// request when it is genuinely same-origin. Desktop's file:// renderer, the
|
||||
// mobile webview, and split-origin web deployments cannot rely on that: no
|
||||
// cookie is attached and the Bearer token cannot be put on a native resource
|
||||
// fetch, so the image 401s. Desktop/mobile expose a non-empty
|
||||
// `api.getBaseUrl()`; web can also hit this path when the server emits an
|
||||
// absolute markdown URL whose origin differs from the current page.
|
||||
//
|
||||
// For them, fetch fresh attachment metadata through the authenticated API —
|
||||
// the same re-sign the click-time download path already does — and swap in
|
||||
@@ -338,11 +339,21 @@ function useResignedInlineMediaURL(
|
||||
): string {
|
||||
const idFromPickedUrl = attachmentIdFromDownloadURL(pickedUrl);
|
||||
const resignAttachmentId = attachmentId ?? idFromPickedUrl;
|
||||
const isCrossOriginWebURL = (() => {
|
||||
if (!/^https?:\/\//i.test(pickedUrl) || typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return new URL(pickedUrl).origin !== window.location.origin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
const needsResign =
|
||||
!!resignAttachmentId &&
|
||||
!!pickedUrl &&
|
||||
idFromPickedUrl !== undefined &&
|
||||
(api.getBaseUrl?.() ?? "") !== "";
|
||||
((api.getBaseUrl?.() ?? "") !== "" || isCrossOriginWebURL);
|
||||
|
||||
const { data: fresh } = useQuery({
|
||||
queryKey: ["attachment-inline-resign", resignAttachmentId],
|
||||
|
||||
Reference in New Issue
Block a user