diff --git a/apps/api/src/lib/csp.ts b/apps/api/src/lib/csp.ts index b5bfa269..be49b7eb 100644 --- a/apps/api/src/lib/csp.ts +++ b/apps/api/src/lib/csp.ts @@ -3,7 +3,7 @@ const SENTRY_ORIGINS = ["https://*.ingest.us.sentry.io"]; const SCALAR_FONT_ORIGIN = "https://fonts.scalar.com"; export function buildCsp(isDocs: boolean): string { - const connectSrc = ["'self'", ...POSTHOG_ORIGINS, ...SENTRY_ORIGINS].join(" "); + const connectSrc = ["'self'", "data:", ...POSTHOG_ORIGINS, ...SENTRY_ORIGINS].join(" "); const fontSrc = isDocs ? `'self' data: ${SCALAR_FONT_ORIGIN}` : "'self' data:"; const scriptSrc = isDocs ? "'self' 'unsafe-inline' https://us-assets.i.posthog.com" diff --git a/apps/web/src/components/tools/qr-generate-preview.tsx b/apps/web/src/components/tools/qr-generate-preview.tsx index 487c7d07..48319bca 100644 --- a/apps/web/src/components/tools/qr-generate-preview.tsx +++ b/apps/web/src/components/tools/qr-generate-preview.tsx @@ -56,17 +56,12 @@ export function QrGeneratePreview() { color: store.useCustomCornerColors ? store.cornerDotColor : undefined, }, backgroundOptions: store.bgTransparent ? { color: "transparent" } : { color: store.bgColor }, - ...(store.logoDataUrl - ? { - image: store.logoDataUrl, - imageOptions: { - hideBackgroundDots: store.hideBackgroundDots, - imageSize: store.logoSize, - margin: store.logoMargin, - crossOrigin: "anonymous" as const, - }, - } - : {}), + image: store.logoDataUrl || "", + imageOptions: { + hideBackgroundDots: store.hideBackgroundDots, + imageSize: store.logoSize, + margin: store.logoMargin, + }, }; }, [ data, diff --git a/apps/web/src/components/tools/qr-generate-settings.tsx b/apps/web/src/components/tools/qr-generate-settings.tsx index ccffc71d..563c65fd 100644 --- a/apps/web/src/components/tools/qr-generate-settings.tsx +++ b/apps/web/src/components/tools/qr-generate-settings.tsx @@ -384,17 +384,12 @@ export function QrGenerateSettings() { color: store.useCustomCornerColors ? store.cornerDotColor : undefined, }, backgroundOptions: store.bgTransparent ? { color: "transparent" } : { color: store.bgColor }, - ...(store.logoDataUrl - ? { - image: store.logoDataUrl, - imageOptions: { - hideBackgroundDots: store.hideBackgroundDots, - imageSize: store.logoSize, - margin: store.logoMargin, - crossOrigin: "anonymous" as const, - }, - } - : {}), + image: store.logoDataUrl || "", + imageOptions: { + hideBackgroundDots: store.hideBackgroundDots, + imageSize: store.logoSize, + margin: store.logoMargin, + }, } as never); qr.download({ diff --git a/tests/unit/api/csp.test.ts b/tests/unit/api/csp.test.ts index 9c1a3b90..b94a249c 100644 --- a/tests/unit/api/csp.test.ts +++ b/tests/unit/api/csp.test.ts @@ -64,4 +64,12 @@ describe("buildCsp", () => { const sources = parseDirective(buildCsp(false), "img-src"); expect(sources).toContain("https://tile.openstreetmap.org"); }); + + it.each([ + true, + false, + ])("connect-src allows data: URIs for client-side blob operations (isDocs=%s)", (isDocs) => { + const sources = parseDirective(buildCsp(isDocs), "connect-src"); + expect(sources).toContain("data:"); + }); }); diff --git a/tests/unit/web/zustand-stores.test.ts b/tests/unit/web/zustand-stores.test.ts index 317b0661..2549c585 100644 --- a/tests/unit/web/zustand-stores.test.ts +++ b/tests/unit/web/zustand-stores.test.ts @@ -548,6 +548,28 @@ describe("useQrStore", () => { expect(useQrStore.getState().logoDataUrl).toBeNull(); }); + it("setLogoFile with a File reads it as a data URL", async () => { + const file = new File(["fake-png-data"], "logo.png", { type: "image/png" }); + useQrStore.getState().setLogoFile(file); + await vi.waitFor(() => { + expect(useQrStore.getState().logoDataUrl).not.toBeNull(); + }); + const s = useQrStore.getState(); + expect(s.logoFile).toBe(file); + expect(s.logoDataUrl).toMatch(/^data:/); + }); + + it("setLogoFile(null) after a file clears both fields", async () => { + const file = new File(["fake-png-data"], "logo.png", { type: "image/png" }); + useQrStore.getState().setLogoFile(file); + await vi.waitFor(() => { + expect(useQrStore.getState().logoDataUrl).not.toBeNull(); + }); + useQrStore.getState().setLogoFile(null); + expect(useQrStore.getState().logoFile).toBeNull(); + expect(useQrStore.getState().logoDataUrl).toBeNull(); + }); + it("reset restores all defaults", () => { useQrStore.getState().setContentType("wifi"); useQrStore.getState().setTextData("changed");