Restore source icon favicon lookup
Unified · Split
| @@ -3,21 +3,20 @@ import { describe, expect, it } from "@effect/vitest"; · 2 unmodified lines | |||
| 3 | import { sourceFaviconUrl } from "./source-favicon"; | 3 | import { sourceFaviconUrl } from "./source-favicon"; |
| 4 | 4 | ||
| 5 | describe("SourceFavicon", () => { | 5 | describe("SourceFavicon", () => { |
| 6 | it("uses the source site's own favicon for public URLs", () => { | 6 | it("uses the favicon service that handles provider-specific icon locations", () => { |
| 7 | expect(sourceFaviconUrl("https://api.github.com/graphql", 20)).toBe( | 7 | expect(sourceFaviconUrl("https://api.github.com/graphql", 20)).toBe( |
| 8 | "https://github.com/favicon.ico?sz=40", | 8 | "https://www.google.com/s2/favicons?domain=github.com&sz=40", |
| 9 | ); | 9 | ); |
| 10 | }); | 10 | }); |
| 11 | 11 | ||
| 12 | it("does not request favicons for local URLs", () => { | 12 | it("does not request favicons for local URLs", () => { |
| 13 | expect(sourceFaviconUrl("http://localhost:3000/private", 20)).toBeNull(); | 13 | expect(sourceFaviconUrl("http://localhost:3000/private", 20)).toBeNull(); |
| 14 | expect(sourceFaviconUrl("http://127.0.0.1:3000/private", 20)).toBeNull(); | 14 | expect(sourceFaviconUrl("http://127.0.0.1:3000/private", 20)).toBeNull(); |
| 15 | expect(sourceFaviconUrl("http://api.local/private", 20)).toBeNull(); | ||
| 16 | }); | 15 | }); |
| 17 | 16 | ||
| 18 | it("does not send source URLs to a third-party favicon service", () => { | 17 | it("sends only the registrable domain to the favicon service", () => { |
| 19 | expect(sourceFaviconUrl("https://internal.example.test/private", 20)).not.toContain( | 18 | expect(sourceFaviconUrl("https://api.github.com/private", 20)).toBe( |
| 20 | "google.com", | 19 | "https://www.google.com/s2/favicons?domain=github.com&sz=40", |
| 21 | ); | 20 | ); |
| 22 | }); | 21 | }); |
| 23 | }); | 22 | }); |
| @@ -3,38 +3,15 @@ import { useState } from "react"; · 2 unmodified lines | |||
| 3 | import { getDomain } from "tldts"; | 3 | import { getDomain } from "tldts"; |
| 4 | 4 | ||
| 5 | // --------------------------------------------------------------------------- | 5 | // --------------------------------------------------------------------------- |
| 6 | // SourceFavicon — renders the source site's own public favicon. | 6 | // SourceFavicon — renders a small favicon derived from a source URL. |
| 7 | // Do not fetch third-party favicon services here; source URLs may be private. | 7 | // Falls back to a neutral icon if the URL is missing or the image fails to load. |
| 8 | // --------------------------------------------------------------------------- | 8 | // --------------------------------------------------------------------------- |
| 9 | 9 | ||
| 10 | const IPV4_PATTERN = /^\d{1,3}(?:\.\d{1,3}){3}$/; | ||
| 11 | |||
| 12 | const isIpHostname = (hostname: string): boolean => | ||
| 13 | IPV4_PATTERN.test(hostname) || hostname.includes(":"); | ||
| 14 | |||
| 15 | export function sourceFaviconUrl(url: string | undefined, size: number): string | null { | 10 | export function sourceFaviconUrl(url: string | undefined, size: number): string | null { |
| 16 | if (!url) return null; | 11 | if (!url) return null; |
| 17 | if (!URL.canParse(url)) return null; | 12 | const domain = getDomain(url) ?? (URL.canParse(url) ? getDomain(new URL(url).hostname) : null); |
| 18 | |||
| 19 | const parsed = new URL(url); | ||
| 20 | if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null; | ||
| 21 | |||
| 22 | const hostname = parsed.hostname.toLowerCase(); | ||
| 23 | if ( | ||
| 24 | hostname === "localhost" || | ||
| 25 | hostname.endsWith(".localhost") || | ||
| 26 | hostname.endsWith(".local") || | ||
| 27 | isIpHostname(hostname) | ||
| 28 | ) { | ||
| 29 | return null; | ||
| 30 | } | ||
| 31 | |||
| 32 | const domain = getDomain(hostname); | ||
| 33 | if (!domain) return null; | 13 | if (!domain) return null; |
| 34 | 14 | return `https://www.google.com/s2/favicons?domain=${domain}&sz=${size * 2}`; | |
| 35 | const favicon = new URL(`https://${domain}/favicon.ico`); | ||
| 36 | favicon.searchParams.set("sz", String(size * 2)); | ||
| 37 | return favicon.toString(); | ||
| 38 | } | 15 | } |
| 39 | 16 | ||
| 40 | export function SourceFavicon({ url, size = 16 }: { url?: string; size?: number }) { | 17 | export function SourceFavicon({ url, size = 16 }: { url?: string; size?: number }) { |