@iocium/favicon-fetcher
Version:
Favicon and BIMI logo fetcher for Cloudflare Workers and browser-compatible environments
105 lines (101 loc) • 4.97 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.FaviconFetcherLib = {}));
})(this, (function (exports) { 'use strict';
/**
* FaviconFetcher allows downloading favicons or BIMI logos from a hostname using known services.
*/
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}`
};
exports.FaviconFetcher = FaviconFetcher;
}));