Add getRegistrableDomain() for extracting root domains from hostnames (with compound suffix support: .co.uk, .com.au, etc.) and getFaviconUrlForRemoteUrl() for converting remote URLs to Google Favicons API URLs. Used by GraphQL/OpenAPI/MCP plugins for iconUrl auto-generation.
Unified · Split
| @@ -0,0 +1,86 @@ | |||
| 1 | const COMMON_COMPOUND_SUFFIXES = new Set([ | ||
| 2 | "ac.uk", | ||
| 3 | "co.in", | ||
| 4 | "co.jp", | ||
| 5 | "co.nz", | ||
| 6 | "co.uk", | ||
| 7 | "com.au", | ||
| 8 | "com.br", | ||
| 9 | "com.mx", | ||
| 10 | "net.au", | ||
| 11 | "org.au", | ||
| 12 | "org.uk", | ||
| 13 | ]); | ||
| 14 | |||
| 15 | const RAW_HOSTS = new Set([ | ||
| 16 | "cdn.jsdelivr.net", | ||
| 17 | "raw.github.com", | ||
| 18 | "raw.githubusercontent.com", | ||
| 19 | "unpkg.com", | ||
| 20 | ]); | ||
| 21 | |||
| 22 | function trimOrNull(value: string | null | undefined): string | null { | ||
| 23 | if (value == null) { | ||
| 24 | return null; | ||
| 25 | } | ||
| 26 | |||
| 27 | const trimmed = value.trim(); | ||
| 28 | return trimmed.length > 0 ? trimmed : null; | ||
| 29 | } | ||
| 30 | |||
| 31 | function isIpv4Address(value: string): boolean { | ||
| 32 | return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value); | ||
| 33 | } | ||
| 34 | |||
| 35 | export function getRegistrableDomain(hostname: string): string | null { | ||
| 36 | const normalized = hostname.trim().toLowerCase().replace(/^\.+|\.+$/g, ""); | ||
| 37 | if (!normalized) { | ||
| 38 | return null; | ||
| 39 | } | ||
| 40 | |||
| 41 | if (normalized === "localhost" || isIpv4Address(normalized)) { | ||
| 42 | return normalized; | ||
| 43 | } | ||
| 44 | |||
| 45 | const parts = normalized.split(".").filter((part) => part.length > 0); | ||
| 46 | if (parts.length < 2) { | ||
| 47 | return null; | ||
| 48 | } | ||
| 49 | |||
| 50 | const suffix = parts.slice(-2).join("."); | ||
| 51 | if (parts.length >= 3 && COMMON_COMPOUND_SUFFIXES.has(suffix)) { | ||
| 52 | return parts.slice(-3).join("."); | ||
| 53 | } | ||
| 54 | |||
| 55 | return parts.slice(-2).join("."); | ||
| 56 | } | ||
| 57 | |||
| 58 | export function getFaviconUrlForRemoteUrl( | ||
| 59 | value: string | null | undefined, | ||
| 60 | options: { | ||
| 61 | allowRawHosts?: boolean; | ||
| 62 | } = {}, | ||
| 63 | ): string | null { | ||
| 64 | const trimmed = trimOrNull(value); | ||
| 65 | if (!trimmed) { | ||
| 66 | return null; | ||
| 67 | } | ||
| 68 | |||
| 69 | try { | ||
| 70 | const url = new URL(trimmed); | ||
| 71 | if (url.protocol !== "http:" && url.protocol !== "https:") { | ||
| 72 | return null; | ||
| 73 | } | ||
| 74 | |||
| 75 | if (!options.allowRawHosts && RAW_HOSTS.has(url.hostname)) { | ||
| 76 | return null; | ||
| 77 | } | ||
| 78 | |||
| 79 | const domain = getRegistrableDomain(url.hostname); | ||
| 80 | return domain | ||
| 81 | ? `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=32` | ||
| 82 | : null; | ||
| 83 | } catch { | ||
| 84 | return null; | ||
| 85 | } | ||
| 86 | } | ||
| @@ -3,6 +3,7 @@ export * from "./catalog-sync-result"; · 2 unmodified lines | |||
| 3 | export * from "./discovery"; | 3 | export * from "./discovery"; |
| 4 | export * from "./discovery-models"; | 4 | export * from "./discovery-models"; |
| 5 | export * from "./effect-errors"; | 5 | export * from "./effect-errors"; |
| 6 | export * from "./favicon"; | ||
| 6 | export * from "./shared"; | 7 | export * from "./shared"; |
| 7 | export * from "./source-models"; | 8 | export * from "./source-models"; |
| 8 | export * from "./types"; | 9 | export * from "./types"; |