fix(canvas): normalize legacy attachment content URLs #1845

Merged
hongming merged 1 commits from fix/legacy-content-attachment-uri into main 2026-05-25 13:19:22 +00:00
2 changed files with 39 additions and 0 deletions
@@ -113,6 +113,31 @@ describe("resolveAttachmentHref — platform-pending: scheme (poll-mode uploads)
});
});
describe("resolveAttachmentHref — legacy platform content URLs", () => {
const chatWs = "chat-ws-aaaaaaaa";
const sourceWs = "d76977b1-d620-4f42-a57e-111111111111";
const fileID = "e2dfaf2e-1111-4abc-9999-222222222222";
it("rewrites /workspaces/<ws>/content/<file>/content to the authenticated pending-upload endpoint", () => {
const url = resolveAttachmentHref(
chatWs,
`/workspaces/${sourceWs}/content/${fileID}/content`,
);
expect(url).toContain(`/workspaces/${sourceWs}/pending-uploads/${fileID}/content`);
expect(url).not.toContain(`/workspaces/${chatWs}/`);
});
it("treats legacy content URLs as platform attachments so previews fetch with auth headers", () => {
expect(isPlatformAttachment(`/workspaces/${sourceWs}/content/${fileID}/content`)).toBe(true);
});
it("passes malformed legacy content URLs through unchanged", () => {
const malformed = `/workspaces/${sourceWs}/content//content`;
expect(resolveAttachmentHref(chatWs, malformed)).toBe(malformed);
expect(isPlatformAttachment(malformed)).toBe(false);
});
});
describe("isPlatformAttachment", () => {
it("returns true for platform-pending: URIs", () => {
expect(isPlatformAttachment("platform-pending:abc/file")).toBe(true);
@@ -125,6 +125,8 @@ export async function uploadChatFiles(
* - `/workspace/...` (bare absolute path inside the container)
* - `platform-pending:<wsid>/<file_id>` (poll-mode upload, staged
* on platform side; resolves to /pending-uploads/<file_id>/content)
* - `/workspaces/<wsid>/content/<file_id>/content` (legacy platform
* content URL; normalizes to the same pending-upload endpoint)
* Everything that looks like an allowed-root container path is
* rewritten to the authenticated /chat/download endpoint. HTTP(S)
* URIs pass through unchanged so we can also render links to
@@ -163,6 +165,11 @@ export function resolveAttachmentHref(
}
return uri;
}
const legacy = parseLegacyPlatformContentUri(uri);
if (legacy) {
const [wsid, fileID] = legacy;
return `${PLATFORM_URL}/workspaces/${encodeURIComponent(wsid)}/pending-uploads/${encodeURIComponent(fileID)}/content`;
}
const containerPath = normalizeWorkspaceUri(uri);
if (containerPath) {
return `${PLATFORM_URL}/workspaces/${workspaceId}/chat/download?path=${encodeURIComponent(containerPath)}`;
@@ -175,6 +182,7 @@ export function resolveAttachmentHref(
* downloadChatFile rather than letting the browser navigate. */
export function isPlatformAttachment(uri: string): boolean {
if (uri.startsWith("platform-pending:")) return true;
if (parseLegacyPlatformContentUri(uri)) return true;
return normalizeWorkspaceUri(uri) !== null;
}
@@ -183,6 +191,12 @@ export function isPlatformAttachment(uri: string): boolean {
* mirror the server's `allowedRoots` allowlist. */
const ALLOWED_CONTAINER_ROOTS = ["/configs", "/workspace", "/home", "/plugins"];
function parseLegacyPlatformContentUri(uri: string): [string, string] | null {
const m = uri.match(/^\/workspaces\/([^/]+)\/content\/([^/]+)\/content(?:[?#].*)?$/);
if (!m || !m[1] || !m[2]) return null;
return [m[1], m[2]];
}
function normalizeWorkspaceUri(uri: string): string | null {
let path: string | null = null;
if (uri.startsWith("workspace:")) {