fix(ui): never treat a remote logo URL as a bundled asset

The bundled-path check was an unanchored substring match, so a
user-supplied logo URL that happened to carry /assets/logos/ or
/_next/static/media/ in its path, and whose filename collided with one of
the 36 manifest entries, would pick up a dark-mode treatment meant only
for assets we ship.

assetPaths already draws this line for resolveLogoSrc, which returns an
external src untouched. Export that predicate instead of writing a second
one, and require a treated src to clear it.

The existing test only covered a remote URL with a bare filename, which
passed either way. The new ones fail without the guard.
This commit is contained in:
Yuneng Jiang 2026-08-27 17:19:54 -07:00
parent 1a9045efd4
commit 51959feb89
No known key found for this signature in database
4 changed files with 22 additions and 2 deletions

View file

@ -81,6 +81,11 @@ describe("Logo", () => {
expect(screen.getByRole("img", { name: "Ext logo" })).toHaveClass("w-5 h-5", { exact: true });
});
it("does not treat a user-supplied logo URL that mimics the bundled asset path", () => {
render(<Logo src="https://cdn.example.com/assets/logos/github.svg" label="Ext" className="w-5 h-5" />);
expect(screen.getByRole("img", { name: "Ext logo" })).toHaveClass("w-5 h-5", { exact: true });
});
it("applies the treatment to a provider logo resolved through the bundler", () => {
render(<Logo provider="openrouter" className="w-5 h-5" />);
expect(screen.getByRole("img", { name: "openrouter logo" })).toHaveClass("dark:[filter:brightness(0)_invert(1)]");

View file

@ -3,6 +3,8 @@ import { normalizeRootPath } from "@/lib/http/resolveApiBase";
const EXTERNAL_SRC = /^(https?:|data:|blob:|\/\/)/i;
export const isExternalAssetSrc = (value: string): boolean => EXTERNAL_SRC.test(value);
/**
* Prefix a root-relative asset path (e.g. "/ui/assets/logos/openai.svg") with the
* proxy's server root path so it resolves when the UI is mounted under a sub-path
@ -22,7 +24,7 @@ export const withServerRoot = (path: string, root: string): string => {
*/
export const resolveLogoSrc = (value: string | null | undefined, root: string = serverRootPath): string | undefined => {
if (!value) return undefined;
if (EXTERNAL_SRC.test(value)) return value;
if (isExternalAssetSrc(value)) return value;
if (value.includes("/_next/static/")) return value;
const prefix = normalizeRootPath(root);
if (prefix && (value === prefix || value.startsWith(`${prefix}/`))) return value;

View file

@ -35,6 +35,17 @@ describe("logoTreatmentFor", () => {
expect(logoTreatmentFor("https://cdn.example.com/github.svg")).toBeUndefined();
});
it("does not treat a remote URL that also carries a bundled-looking path", () => {
expect(logoTreatmentFor("https://cdn.example.com/assets/logos/github.svg")).toBeUndefined();
expect(logoTreatmentFor("http://cdn.example.com/_next/static/media/github.abc123.svg")).toBeUndefined();
expect(logoTreatmentFor("//cdn.example.com/assets/logos/github.svg")).toBeUndefined();
expect(logoTreatmentFor("HTTPS://CDN.EXAMPLE.COM/assets/logos/github.svg")).toBeUndefined();
});
it("does not treat a data URL that happens to contain a bundled-looking path", () => {
expect(logoTreatmentFor("data:image/svg+xml,/assets/logos/github.svg")).toBeUndefined();
});
it("does not treat a non-logo path whose filename collides with a bundled asset", () => {
expect(logoTreatmentFor("/uploads/user/github.svg")).toBeUndefined();
});

View file

@ -1,3 +1,5 @@
import { isExternalAssetSrc } from "@/lib/assetPaths";
export type LogoTreatment = "invert" | "plate";
const BUNDLED_LOGO_PATH = /(?:\/assets\/logos\/|\/_next\/static\/media\/)/;
@ -49,7 +51,7 @@ const withoutBundlerHash = (basename: string): string | undefined => {
};
export const logoTreatmentFor = (src: string | null | undefined): LogoTreatment | undefined => {
if (!src || !BUNDLED_LOGO_PATH.test(src)) return undefined;
if (!src || isExternalAssetSrc(src) || !BUNDLED_LOGO_PATH.test(src)) return undefined;
const basename = basenameOf(src);
const key = basename === undefined ? undefined : withoutBundlerHash(basename);
return key === undefined ? undefined : TREATMENT_BY_ASSET[key];