diff --git a/canvas/src/components/tabs/chat/__tests__/uploads.test.ts b/canvas/src/components/tabs/chat/__tests__/uploads.test.ts index 54a298a16..da703dd99 100644 --- a/canvas/src/components/tabs/chat/__tests__/uploads.test.ts +++ b/canvas/src/components/tabs/chat/__tests__/uploads.test.ts @@ -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//content//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); diff --git a/canvas/src/components/tabs/chat/uploads.ts b/canvas/src/components/tabs/chat/uploads.ts index 430991102..102a9e7e1 100644 --- a/canvas/src/components/tabs/chat/uploads.ts +++ b/canvas/src/components/tabs/chat/uploads.ts @@ -125,6 +125,8 @@ export async function uploadChatFiles( * - `/workspace/...` (bare absolute path inside the container) * - `platform-pending:/` (poll-mode upload, staged * on platform side; resolves to /pending-uploads//content) + * - `/workspaces//content//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:")) {