favicon.ts

2.4 KB · raw

1/**2 * Resolves a favicon URL for a given domain.3 *4 * On Chrome MV3 with the `favicon` permission, returns a URL backed by the5 * browser's own favicon cache (no network, no third party). On Firefox and6 * other browsers where the API is unavailable, returns null so callers can7 * render a generic fallback icon.8 *9 * Never fetches from any external URL. No domain name leaves the browser.10 */11export function faviconUrl(domain: string): string | null {12  if (!domain) return null;13  if (typeof chrome === "undefined" || !chrome.runtime?.getURL) return null;1415  try {16    const url = new URL(chrome.runtime.getURL("/_favicon/"));17    url.searchParams.set("pageUrl", `https://${domain}`);18    url.searchParams.set("size", "32");19    return url.toString();20  } catch {21    return null;22  }23}2425// Renders an img element to an 8×8 canvas and returns its RGB pixels as a26// compact string. Used to fingerprint what Chrome returns for "no favicon".27function imgSig(img: HTMLImageElement): string {28  const c = document.createElement("canvas");29  c.width = c.height = 8;30  const ctx = c.getContext("2d")!;31  ctx.drawImage(img, 0, 0, 8, 8);32  const d = ctx.getImageData(0, 0, 8, 8).data;33  let s = "";34  for (let i = 0; i < d.length; i += 4) s += `${d[i]},${d[i + 1]},${d[i + 2]};`;35  return s;36}3738// Cached promise so the globe reference image is only loaded once per page.39let _globeSig: Promise<string | null> | undefined;4041/**42 * Returns the pixel signature of what Chrome's _favicon/ endpoint returns43 * when it has no stored favicon for a domain (the default globe icon).44 * Cached after the first call. Returns null when the favicon API is45 * unavailable (Firefox) or the canvas read is blocked.46 */47export function globeFaviconSig(): Promise<string | null> {48  if (_globeSig) return _globeSig;49  _globeSig = new Promise<string | null>((resolve) => {50    const src = faviconUrl("zz-peach-no-favicon-sentinel.nonexistent");51    if (!src) { resolve(null); return; }52    const img = new Image();53    img.onload = () => { try { resolve(imgSig(img)); } catch { resolve(null); } };54    img.onerror = () => resolve(null);55    img.src = src;56  });57  return _globeSig;58}5960/**61 * Returns the 8×8 canvas pixel signature for a loaded img element.62 * Returns null if the canvas read is blocked (e.g. security error).63 */64export function faviconImgSig(img: HTMLImageElement): string | null {65  try { return imgSig(img); } catch { return null; }66}