import { isPublicFaviconHost } from "./hostClassification.ts"; /** * Mirrors Codex's generic Browser Use fallback: ask the page origin for its * conventional favicon and let the image element fall back to a browser glyph. * Chrome-backed tools can pass their tab's explicit favicon URL separately. */ function faviconUrlForPage(rawUrl: string | null | undefined, _size = 32): string | null { if (!rawUrl || rawUrl.length > 4096) return null; try { const pageUrl = new URL(rawUrl); if (pageUrl.protocol !== "http:" && pageUrl.protocol !== "https:") return null; return new URL("/favicon.ico", pageUrl.origin).href; } catch { return null; } } const THEMED_FAVICON_BY_HOSTNAME: Readonly< Record> > = { "github.com": { light: "https://github.githubassets.com/favicons/favicon.svg", dark: "https://github.githubassets.com/favicons/favicon-dark.svg", }, }; function themedFaviconUrlForPage( rawUrl: string | null | undefined, appearance: "light" | "dark", ): string | null { if (!rawUrl || rawUrl.length > 4096) return null; try { const pageUrl = new URL(rawUrl); if (pageUrl.protocol !== "http:" && pageUrl.protocol !== "https:") return null; return THEMED_FAVICON_BY_HOSTNAME[pageUrl.hostname.toLowerCase()]?.[appearance] ?? null; } catch { return null; } } /** Accepts image URLs supplied by a trusted provider event. */ function explicitFaviconUrl(rawUrl: string | null | undefined): string | null { if (!rawUrl || rawUrl.length > 4096) return null; try { const url = new URL(rawUrl); return url.protocol === "http:" || url.protocol === "https:" || url.protocol === "data:" ? url.href : null; } catch { return null; } } /** * Chooses a website icon for the app's resolved theme. Provider-supplied * variants mirror Codex's Chrome-selected favicon path. A small site-owned * fallback table covers websites whose conventional favicon is illegible in * one appearance without recoloring full-color icons. */ export function toolActivityFaviconUrl( icon: { readonly pageUrl: string; readonly faviconUrl?: string | undefined; readonly faviconUrlDark?: string | undefined; }, appearance: "light" | "dark", size = 32, ): string | null { if (appearance === "dark") { return ( explicitFaviconUrl(icon.faviconUrlDark) ?? themedFaviconUrlForPage(icon.pageUrl, "dark") ?? explicitFaviconUrl(icon.faviconUrl) ?? faviconUrlForPage(icon.pageUrl, size) ); } return ( explicitFaviconUrl(icon.faviconUrl) ?? themedFaviconUrlForPage(icon.pageUrl, "light") ?? faviconUrlForPage(icon.pageUrl, size) ); } /** Return a public favicon URL without disclosing private or reserved hosts. */ export function faviconUrlForOrigin(rawUrl: string | null | undefined, size = 32): string | null { if (!rawUrl) return null; try { const url = new URL(rawUrl); if (!url.host) return null; if (url.protocol !== "http:" && url.protocol !== "https:") return null; if (!isPublicFaviconHost(url.hostname)) return null; return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(url.host)}&sz=${size}`; } catch { return null; } }