UNPKG

@iocium/favicon-fetcher

Version:

Favicon and BIMI logo fetcher for Cloudflare Workers and browser-compatible environments

95 lines (94 loc) 4.19 kB
/** * FaviconFetcher allows downloading favicons or BIMI logos from a hostname using known services. */ export class FaviconFetcher { /** * @param hostname The domain name to fetch the favicon/logo for. * @param options Optional configuration including headers, iconHorse API key, and BIMI DNS settings. */ constructor(hostname, options) { this.hostname = hostname; this.options = options; if (!hostname) throw new Error('Hostname is required'); } /** * Fetches the favicon or BIMI logo for the configured hostname using the specified service. * * @param service The provider to use (default is 'google'). * @returns A FaviconResult containing the image, status, and metadata. * @throws If the fetch fails or BIMI DNS record is missing/invalid. */ async fetchFavicon(service = "google") { if (service === "bimi") { const dohUrl = this.options?.dohServerUrl || 'https://cloudflare-dns.com/dns-query'; const bimiDomain = `default._bimi.${this.hostname}`; const dnsUrl = `${dohUrl}?name=${bimiDomain}&type=TXT`; const dnsResp = await fetch(dnsUrl, { headers: { Accept: "application/dns-json", }, }); if (!dnsResp.ok) { throw new Error(`BIMI DNS query failed: ${dnsResp.statusText}`); } const dnsData = await dnsResp.json(); const txtRecords = dnsData?.Answer?.map((a) => a.data.replace(/^"|"$/g, '')) || []; const lRecord = txtRecords.find(txt => txt.includes('l=')); const logoUrlMatch = lRecord?.match(/l=([^;]+)/); if (!logoUrlMatch) { throw new Error('No BIMI l= logo URL found in TXT record'); } const logoUrl = logoUrlMatch[1]; const response = await fetch(logoUrl); if (!response.ok) { throw new Error(`Failed to fetch BIMI logo: ${response.statusText}`); } const content = await response.arrayBuffer(); return { url: logoUrl, contentType: response.headers.get('content-type'), content, status: response.status }; } const urlFn = FaviconFetcher.serviceUrls[service]; const url = urlFn(this.hostname); const corsProxy = this.options?.useCorsProxy === true ? 'https://corsproxy.io/?' : typeof this.options?.useCorsProxy === 'string' ? this.options.useCorsProxy : ''; const fetchUrl = corsProxy + url; const headers = { ...(this.options?.headers || {}) }; // Enforce icon.horse API key protection if (service === "iconHorse" && this.options?.iconHorseApiKey) { headers["X-API-Key"] = this.options.iconHorseApiKey; } const response = await fetch(fetchUrl, { headers }); if (!response.ok) { throw new Error(`Failed to fetch favicon from ${service}: ${response.statusText}`); } const content = await response.arrayBuffer(); return { url, contentType: response.headers.get('content-type'), content, status: response.status }; } } FaviconFetcher.serviceUrls = { google: (hostname) => `https://www.google.com/s2/favicons?domain=${hostname}`, duckduckgo: (hostname) => `https://icons.duckduckgo.com/ip3/${hostname}.ico`, bitwarden: (hostname) => `https://icons.bitwarden.net/${hostname}/icon.png`, yandex: (hostname) => `https://favicon.yandex.net/favicon/${hostname}`, fastmail: (hostname) => `https://www.fastmailcdn.com/avatar/${hostname}`, iconHorse: (hostname) => `https://icon.horse/icon/${hostname}`, nextdns: (hostname) => `https://favicons.nextdns.io/${hostname}@2x.png`, iocium: (hostname) => `https://icons.iocium.net/icon/${hostname}`, faviconis: (hostname) => `https://favicon.is/${hostname}`, faviconim: (hostname) => `https://favicon.im/${hostname}` };