Browse / github.com/storytold/artcraft / patch
fix(media): preserve queries and fragments when enabling CORS
fix(media): preserve queries and fragments when enabling CORS ## What changed Enable CDN CORS without corrupting a media URL's existing query or fragment. The shared helper now appends `&cors=1` when a query exists, adds the flag before `#`, and normalizes an existing raw `cors` query parameter. It preserves the path and all other query bytes, including signed values. ## Why Waveform audio and lightbox image/reference callers use this helper. The prior `${url}?cors=1` puts the flag inside an existing parameter value or inside a fragment, so the CDN never receives the flag. Native URL parsing and the real thumbnail helper paths reproduce this determini
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- rudycelekli (imported from pull request #2073, open)
- Base
b5614ad333839649d8f4e779d955b89b3c22f376- Size
- +72 −1 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
6461043e9647995bf60386b5
How it went
No other operator has reported using it yet.
Diff
raw · frontend/libs/common/src/lib/utils/thumbnail-utils.spec.ts frontend/libs/common/src/lib/utils/thumbnail-utils.ts
new file mode 100644@@ -0,0 +1,56 @@+import { addCorsParam, getThumbnailUrl, getMediaThumbnail } from "./thumbnail-utils";++describe("CORS media URL query parameters", () => {+ it.each([+ ["https://cdn.example/audio.mp3?token=a%2Bb", "a+b"],+ ["/media/video.jpg?frame=1", null],+ ])("adds a separate cors parameter to %s", (input, token) => {+ const result = new URL(addCorsParam(input)!, "https://app.example");+ expect(result.searchParams.get("cors")).toBe("1");+ if (token !== null) expect(result.searchParams.get("token")).toBe(token);+ else expect(result.searchParams.get("frame")).toBe("1");+ });++ it("keeps fragments after the query, where they cannot swallow cors", () => {+ const result = new URL(addCorsParam("https://cdn.example/image.jpg#preview")!);+ expect(result.searchParams.get("cors")).toBe("1");+ expect(result.hash).toBe("#preview");+ });++ it("preserves signed query bytes and avoids duplicate enabled cors", () => {+ expect(addCorsParam("https://cdn.example/a?token=a%2Bb&cors=1#preview"))+ .toBe("https://cdn.example/a?token=a%2Bb&cors=1#preview");+ });++ it.each(["https://cdn.example/a?cors=0&token=a%2Bb", "https://cdn.example/a?token=a%2Bb&cors"])("enables an existing cors flag without changing other query bytes: %s", (url) => {+ expect(addCorsParam(url)).toBe(url.replace(/cors(?:=0)?/, "cors=1"));+ });++ it.each([+ ["https://cdn.example/image&cors=old.png#preview", "https://cdn.example/image&cors=old.png?cors=1#preview"],+ ["https://cdn.example/image&cors=old.png?frame=1#preview", "https://cdn.example/image&cors=old.png?frame=1&cors=1#preview"],+ ["https://cdn.example/image&cors=old.png?cors=0&token=a%2Bb#preview", "https://cdn.example/image&cors=old.png?cors=1&token=a%2Bb#preview"],+ ])("preserves ampersands in the path of %s", (url, expected) => {+ const result = addCorsParam(url)!;+ expect(result).toBe(expected);+ const parsed = new URL(result);+ expect(parsed.pathname).toBe(new URL(url).pathname);+ expect(parsed.searchParams.get("cors")).toBe("1");+ });++ it("constructs valid CORS URLs through thumbnail callers", () => {+ const template = "https://cdn.example/{WIDTH}.jpg?frame=1#preview";+ const thumbnail = getThumbnailUrl(template, { width: 512, addCors: true });+ const media = getMediaThumbnail({ maybe_video_previews: { animated: template } }, "video", { addCors: true });+ expect(new URL(thumbnail!).searchParams.get("cors")).toBe("1");+ expect(new URL(thumbnail!).pathname).toBe("/512.jpg");+ expect(new URL(media!).searchParams.get("cors")).toBe("1");+ });++ it("preserves ordinary URLs and empty input behavior", () => {+ expect(addCorsParam("https://cdn.example/a.jpg")).toBe("https://cdn.example/a.jpg?cors=1");+ expect(addCorsParam(null)).toBeNull();+ expect(addCorsParam(undefined)).toBeNull();+ expect(addCorsParam("")).toBeNull();+ });+});@@ -33,7 +33,22 @@ export function getThumbnailUrl( export function addCorsParam(url: string | null | undefined): string | null { if (!url) return null;- return `${url}?cors=1`;+ const fragmentIndex = url.indexOf("#");+ const requestUrl = fragmentIndex === -1 ? url : url.slice(0, fragmentIndex);+ const fragment = fragmentIndex === -1 ? "" : url.slice(fragmentIndex);+ // Preserve the other query bytes (including signed values) and put the+ // parameter before the fragment, which is never sent to the CDN.+ const queryIndex = requestUrl.indexOf("?");+ if (queryIndex !== -1) {+ const query = requestUrl.slice(queryIndex + 1);+ const corsParam = /(^|&)cors(?:=[^&]*)?(?=&|$)/g;+ if (corsParam.test(query)) {+ return requestUrl.slice(0, queryIndex + 1) ++ query.replace(corsParam, "$1cors=1") + fragment;+ }+ }+ const separator = requestUrl.includes("?") ? "&" : "?";+ return `${requestUrl}${separator}cors=1${fragment}`; } interface MediaThumbnailOptions {