import { BoxIcon } from "lucide-react";
import { useState } from "react";
import type { IntegrationPlugin } from "@executor-js/sdk/client";
import { getDomain } from "tldts";
// ---------------------------------------------------------------------------
// IntegrationFavicon — renders a small favicon derived from an integration URL.
// Falls back to a neutral icon if the URL is missing or the image fails to load.
// ---------------------------------------------------------------------------
const integrationFaviconDomain = (url: string | undefined): string | null => {
if (!url) return null;
return getDomain(url) ?? (URL.canParse(url) ? getDomain(new URL(url).hostname) : null);
};
// integrations.sh/logo proxies context.dev's Logo Link behind an edge cache
// and is executor's single logo source; Google's favicon service remains in
// the cascade as the fallback the
onError walks to when the proxy is
// unreachable or serves nothing for the domain.
export function integrationFaviconUrl(url: string | undefined, size: number): string | null {
const domain = integrationFaviconDomain(url);
if (!domain) return null;
return `https://integrations.sh/logo/${domain}?sz=${size * 2}`;
}
export function integrationFaviconFallbackUrl(
url: string | undefined,
size: number,
): string | null {
const domain = integrationFaviconDomain(url);
if (!domain) return null;
return `https://www.google.com/s2/favicons?domain=${domain}&sz=${size * 2}`;
}
export function integrationLocalIconUrl(sourceId: string | undefined): string | null {
if (sourceId !== "executor") return null;
return "/favicon-32.png";
}
const KIND_TO_PLUGIN_KEY: Record = {
openapi: "openapi",
mcp: "mcp",
graphql: "graphql",
googleDiscovery: "google",
};
const normalizeUrl = (url: string | undefined): string | null => {
if (!url) return null;
try {
const parsed = new URL(url);
parsed.hash = "";
parsed.searchParams.sort();
return parsed.toString().replace(/\/$/, "");
} catch {
return url.trim().replace(/\/$/, "");
}
};
const googleApiServiceFromUrl = (url: string | undefined): string | null => {
if (!url) return null;
try {
const parsed = new URL(url);
const hostname = parsed.hostname.toLowerCase();
const segments = parsed.pathname.split("/").filter(Boolean);
if (
hostname === "www.googleapis.com" &&
segments[0] === "discovery" &&
segments[2] === "apis" &&
segments[3]
) {
return segments[3];
}
if (hostname === "www.googleapis.com") return segments[0] ?? null;
const suffix = ".googleapis.com";
if (hostname.endsWith(suffix)) {
const service = hostname.slice(0, -suffix.length);
return service.length > 0 ? service : null;
}
} catch {
return null;
}
return null;
};
const normalizeToken = (value: string | undefined): string =>
value?.toLowerCase().replace(/[^a-z0-9]+/g, "") ?? "";
const tokenVariants = (value: string | undefined): readonly string[] => {
const tokens = new Set();
const normalized = normalizeToken(value);
if (normalized.length > 0) tokens.add(normalized);
const parts =
value
?.toLowerCase()
.split(/[^a-z0-9]+/g)
.filter(Boolean) ?? [];
const noise = new Set([
"api",
"mcp",
"openapi",
"rest",
"graphql",
"web",
"com",
"net",
"org",
"dev",
]);
const cleaned = parts.filter((part) => !noise.has(part));
const cleanedToken = cleaned.join("");
if (cleanedToken.length > 0) tokens.add(cleanedToken);
const aliases: Record = {
pscale: "planetscale",
};
for (const part of cleaned) {
tokens.add(part);
const alias = aliases[part];
if (alias) tokens.add(alias);
}
return [...tokens];
};
export function integrationInferredUrl(source: {
readonly id: string;
readonly name?: string;
}): string | null {
const hostPattern = /^[a-z0-9][a-z0-9.-]*\.(?:app|co|com|dev|io|net|org)(?:\/.*)?$/i;
const hostLike = [source.name, source.id.replaceAll("_", ".")]
.filter((value): value is string => value != null && value.length > 0)
.find((value) => hostPattern.test(value.trim()));
if (hostLike) return `https://${hostLike.trim().replace(/^https?:\/\//i, "")}`;
return null;
}
const tokenMatches = (sourceValue: string, presetValue: string): boolean =>
presetValue.length > 0 &&
sourceValue.length > 0 &&
(sourceValue === presetValue ||
sourceValue.includes(presetValue) ||
presetValue.includes(sourceValue));
export function integrationPresetIconUrl(
source: {
readonly id: string;
readonly kind: string;
readonly name?: string;
readonly url?: string;
},
integrationPlugins: readonly IntegrationPlugin[],
): string | null {
const pluginKey = KIND_TO_PLUGIN_KEY[source.kind] ?? source.kind;
const plugin = integrationPlugins.find((p) => p.key === pluginKey);
const presets = plugin?.presets ?? [];
const sourceUrl = normalizeUrl(source.url);
const sourceGoogleService = googleApiServiceFromUrl(source.url);
const sourceTokens = [...tokenVariants(source.id), ...tokenVariants(source.name)];
const preset = presets.find((p) => {
const presetUrl = normalizeUrl(p.url);
const presetGoogleService = googleApiServiceFromUrl(p.url);
const presetTokens = [...tokenVariants(p.id), ...tokenVariants(p.name)];
return (
(sourceUrl !== null && presetUrl === sourceUrl) ||
(sourceGoogleService !== null && presetGoogleService === sourceGoogleService) ||
sourceTokens.some((sourceToken) =>
presetTokens.some((presetToken) => tokenMatches(sourceToken, presetToken)),
)
);
});
return preset?.icon ?? null;
}
// Resolution cascade for the rendered favicon: first non-null, non-failed of an
// explicit preset icon, the bundled local icon for a known source id, the
// integrations.sh logo proxy derived from the source URL, then the Google
// favicon service as a last resort. The built-in executor source has no preset
// icon and no URL, so it resolves ONLY through the sourceId branch: callers
// that drop sourceId fall through to the neutral BoxIcon placeholder.
export function integrationFaviconSrc(args: {
icon?: string | null;
sourceId?: string;
url?: string;
size: number;
failedSrcs?: readonly string[];
}): string | null {
const failedSrcs = args.failedSrcs ?? [];
return (
[
args.icon ?? null,
integrationLocalIconUrl(args.sourceId),
integrationFaviconUrl(args.url, args.size),
integrationFaviconFallbackUrl(args.url, args.size),
].find((candidate) => candidate !== null && !failedSrcs.includes(candidate)) ?? null
);
}
export function IntegrationFavicon({
icon,
sourceId,
url,
size = 16,
}: {
icon?: string | null;
sourceId?: string;
url?: string;
size?: number;
}) {
const [failedSrcs, setFailedSrcs] = useState([]);
const src = integrationFaviconSrc({ icon, sourceId, url, size, failedSrcs });
if (!src) {
return (
);
}
return (
setFailedSrcs((current) => (current.includes(src) ? current : [...current, src]))
}
className="shrink-0 rounded-sm"
style={{ width: size, height: size }}
/>
);
}