From 63605aa603d98ea811f3684bb5a1d539f14c67b1 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:40:27 -0700 Subject: [PATCH 1/2] Source integration favicons from integrations.sh/logo integrations.sh now proxies context.dev's Logo Link behind an edge cache, so it becomes the single logo source; the Google favicon service stays in the resolution cascade as the onError fallback. --- .../components/integration-favicon.test.tsx | 21 +++++++++++-- .../src/components/integration-favicon.tsx | 31 +++++++++++++++---- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/packages/react/src/components/integration-favicon.test.tsx b/packages/react/src/components/integration-favicon.test.tsx index 2bee279e3..fc786d511 100644 --- a/packages/react/src/components/integration-favicon.test.tsx +++ b/packages/react/src/components/integration-favicon.test.tsx @@ -1,6 +1,7 @@ import { describe, expect, it } from "@effect/vitest"; import { + integrationFaviconFallbackUrl, integrationFaviconSrc, integrationFaviconUrl, integrationInferredUrl, @@ -9,8 +10,14 @@ import { } from "./integration-favicon"; describe("IntegrationFavicon", () => { - it("uses the favicon service that handles provider-specific icon locations", () => { + it("uses the integrations.sh logo proxy as the primary favicon source", () => { expect(integrationFaviconUrl("https://api.github.com/graphql", 20)).toBe( + "https://integrations.sh/logo/github.com?sz=40", + ); + }); + + it("falls back to the Google favicon service", () => { + expect(integrationFaviconFallbackUrl("https://api.github.com/graphql", 20)).toBe( "https://www.google.com/s2/favicons?domain=github.com&sz=40", ); }); @@ -18,10 +25,20 @@ describe("IntegrationFavicon", () => { it("does not request favicons for local URLs", () => { expect(integrationFaviconUrl("http://localhost:3000/private", 20)).toBeNull(); expect(integrationFaviconUrl("http://127.0.0.1:3000/private", 20)).toBeNull(); + expect(integrationFaviconFallbackUrl("http://localhost:3000/private", 20)).toBeNull(); }); - it("sends only the registrable domain to the favicon service", () => { + it("sends only the registrable domain to the logo proxy", () => { expect(integrationFaviconUrl("https://api.github.com/private", 20)).toBe( + "https://integrations.sh/logo/github.com?sz=40", + ); + }); + + it("walks the cascade from the proxy to the favicon service on failure", () => { + const url = "https://api.github.com/graphql"; + const primary = integrationFaviconSrc({ url, size: 20 }); + expect(primary).toBe("https://integrations.sh/logo/github.com?sz=40"); + expect(integrationFaviconSrc({ url, size: 20, failedSrcs: [primary ?? ""] })).toBe( "https://www.google.com/s2/favicons?domain=github.com&sz=40", ); }); diff --git a/packages/react/src/components/integration-favicon.tsx b/packages/react/src/components/integration-favicon.tsx index 40e5b248c..648303a6b 100644 --- a/packages/react/src/components/integration-favicon.tsx +++ b/packages/react/src/components/integration-favicon.tsx @@ -8,9 +8,26 @@ import { getDomain } from "tldts"; // Falls back to a neutral icon if the URL is missing or the image fails to load. // --------------------------------------------------------------------------- -export function integrationFaviconUrl(url: string | undefined, size: number): string | null { +const integrationFaviconDomain = (url: string | undefined): string | null => { if (!url) return null; - const domain = getDomain(url) ?? (URL.canParse(url) ? getDomain(new URL(url).hostname) : 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}`; } @@ -161,10 +178,11 @@ export function integrationPresetIconUrl( } // 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, then a -// favicon-service URL derived from the source URL. 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. +// 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; @@ -178,6 +196,7 @@ export function integrationFaviconSrc(args: { args.icon ?? null, integrationLocalIconUrl(args.sourceId), integrationFaviconUrl(args.url, args.size), + integrationFaviconFallbackUrl(args.url, args.size), ].find((candidate) => candidate !== null && !failedSrcs.includes(candidate)) ?? null ); } From ed09aacc3cdc3b487899eb92948b358899ec56bc Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Wed, 1 Jul 2026 19:48:31 -0700 Subject: [PATCH 2/2] Route preset vendor favicons through integrations.sh/logo Preset icons that pointed at vendor favicon.ico paths (brittle: vendors move or serve HTML) now use the same /logo proxy the derived favicon path uses. Deliberate keeps: svgl.app and fonts.gstatic.com product logos (per-product marks for Google/Microsoft sub-products the domain-keyed proxy can't distinguish), bundled marketing assets, and the app's own favicon links. --- packages/plugins/graphql/src/sdk/presets.ts | 8 +++--- packages/plugins/mcp/src/sdk/presets.ts | 20 +++++++------- packages/plugins/microsoft/src/sdk/presets.ts | 2 +- packages/plugins/openapi/src/sdk/presets.ts | 26 +++++++++---------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/plugins/graphql/src/sdk/presets.ts b/packages/plugins/graphql/src/sdk/presets.ts index 8a44c15e9..f648f8800 100644 --- a/packages/plugins/graphql/src/sdk/presets.ts +++ b/packages/plugins/graphql/src/sdk/presets.ts @@ -24,7 +24,7 @@ export const graphqlPresets: readonly GraphqlPreset[] = [ summary: "Projects, merge requests, pipelines, and users.", url: "https://gitlab.com/api/graphql", endpoint: "https://gitlab.com/api/graphql", - icon: "https://gitlab.com/favicon.ico", + icon: "https://integrations.sh/logo/gitlab.com", featured: true, }, { @@ -33,7 +33,7 @@ export const graphqlPresets: readonly GraphqlPreset[] = [ summary: "Issues, projects, teams, and cycles.", url: "https://api.linear.app/graphql", endpoint: "https://api.linear.app/graphql", - icon: "https://linear.app/favicon.ico", + icon: "https://integrations.sh/logo/linear.app", featured: true, }, { @@ -42,7 +42,7 @@ export const graphqlPresets: readonly GraphqlPreset[] = [ summary: "Boards, items, columns, and workspace automation.", url: "https://api.monday.com/v2", endpoint: "https://api.monday.com/v2", - icon: "https://monday.com/favicon.ico", + icon: "https://integrations.sh/logo/monday.com", }, { id: "anilist", @@ -50,6 +50,6 @@ export const graphqlPresets: readonly GraphqlPreset[] = [ summary: "Anime and manga database — no auth required.", url: "https://graphql.anilist.co", endpoint: "https://graphql.anilist.co", - icon: "https://anilist.co/img/icons/favicon-32x32.png", + icon: "https://integrations.sh/logo/anilist.co", }, ]; diff --git a/packages/plugins/mcp/src/sdk/presets.ts b/packages/plugins/mcp/src/sdk/presets.ts index 3c9b01ef1..2a04f6ef9 100644 --- a/packages/plugins/mcp/src/sdk/presets.ts +++ b/packages/plugins/mcp/src/sdk/presets.ts @@ -38,7 +38,7 @@ export const mcpPresets: readonly McpPreset[] = [ summary: "Search and read documentation from any GitHub repo.", url: "https://mcp.deepwiki.com/mcp", endpoint: "https://mcp.deepwiki.com/mcp", - icon: "https://deepwiki.com/favicon.ico", + icon: "https://integrations.sh/logo/deepwiki.com", featured: true, }, { @@ -47,7 +47,7 @@ export const mcpPresets: readonly McpPreset[] = [ summary: "Up-to-date docs and code examples for any library.", url: "https://mcp.context7.com/mcp", endpoint: "https://mcp.context7.com/mcp", - icon: "https://context7.com/favicon.ico", + icon: "https://integrations.sh/logo/context7.com", featured: true, }, { @@ -56,7 +56,7 @@ export const mcpPresets: readonly McpPreset[] = [ summary: "Cloud browser sessions for web scraping and automation.", url: "https://mcp.browserbase.com/mcp", endpoint: "https://mcp.browserbase.com/mcp", - icon: "https://www.browserbase.com/favicon.ico", + icon: "https://integrations.sh/logo/browserbase.com", featured: true, }, { @@ -65,7 +65,7 @@ export const mcpPresets: readonly McpPreset[] = [ summary: "Crawl and scrape websites into structured data.", url: "https://mcp.firecrawl.dev/mcp", endpoint: "https://mcp.firecrawl.dev/mcp", - icon: "https://www.firecrawl.dev/favicon.ico", + icon: "https://integrations.sh/logo/firecrawl.dev", featured: true, }, { @@ -74,7 +74,7 @@ export const mcpPresets: readonly McpPreset[] = [ summary: "Serverless Postgres — branches, queries, and management.", url: "https://mcp.neon.tech/mcp", endpoint: "https://mcp.neon.tech/mcp", - icon: "https://neon.tech/favicon/favicon.ico", + icon: "https://integrations.sh/logo/neon.tech", featured: true, }, { @@ -83,7 +83,7 @@ export const mcpPresets: readonly McpPreset[] = [ summary: "Query, analyze, and monitor your logs and event data.", url: "https://mcp.axiom.co/mcp", endpoint: "https://mcp.axiom.co/mcp", - icon: "https://axiom.co/favicon.ico", + icon: "https://integrations.sh/logo/axiom.co", featured: true, }, { @@ -92,7 +92,7 @@ export const mcpPresets: readonly McpPreset[] = [ summary: "Manage payments, subscriptions, and billing via MCP.", url: "https://mcp.stripe.com", endpoint: "https://mcp.stripe.com", - icon: "https://stripe.com/favicon.ico", + icon: "https://integrations.sh/logo/stripe.com", featured: true, }, { @@ -101,7 +101,7 @@ export const mcpPresets: readonly McpPreset[] = [ summary: "Issues, projects, teams, and cycles via MCP.", url: "https://mcp.linear.app/mcp", endpoint: "https://mcp.linear.app/mcp", - icon: "https://linear.app/favicon.ico", + icon: "https://integrations.sh/logo/linear.app", featured: true, }, { @@ -110,7 +110,7 @@ export const mcpPresets: readonly McpPreset[] = [ summary: "Databases, pages, blocks, and search via MCP.", url: "https://mcp.notion.com/mcp", endpoint: "https://mcp.notion.com/mcp", - icon: "https://www.notion.com/front-static/favicon.ico", + icon: "https://integrations.sh/logo/notion.com", featured: true, }, { @@ -127,7 +127,7 @@ export const mcpPresets: readonly McpPreset[] = [ summary: "Workers, KV, D1, R2, and DNS management via MCP.", url: "https://mcp.cloudflare.com/mcp", endpoint: "https://mcp.cloudflare.com/mcp", - icon: "https://cloudflare.com/favicon.ico", + icon: "https://integrations.sh/logo/cloudflare.com", }, { id: "chrome-devtools", diff --git a/packages/plugins/microsoft/src/sdk/presets.ts b/packages/plugins/microsoft/src/sdk/presets.ts index ad4e25569..581886fc4 100644 --- a/packages/plugins/microsoft/src/sdk/presets.ts +++ b/packages/plugins/microsoft/src/sdk/presets.ts @@ -27,7 +27,7 @@ export interface MicrosoftGraphScopePreset { readonly audience: MicrosoftGraphScopeAudience; } -const MICROSOFT_ICON = "https://www.microsoft.com/favicon.ico"; +const MICROSOFT_ICON = "https://integrations.sh/logo/microsoft.com"; const svglIcon = (name: string) => `https://svgl.app/library/${name}.svg`; export const MICROSOFT_GRAPH_OPENAPI_URL = diff --git a/packages/plugins/openapi/src/sdk/presets.ts b/packages/plugins/openapi/src/sdk/presets.ts index 2d3f5dcb2..e044f6b64 100644 --- a/packages/plugins/openapi/src/sdk/presets.ts +++ b/packages/plugins/openapi/src/sdk/presets.ts @@ -13,7 +13,7 @@ const openApiOnlyPresets: readonly OpenApiPreset[] = [ name: "Stripe", summary: "Payments, subscriptions, customers, and invoices.", url: "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json", - icon: "https://stripe.com/favicon.ico", + icon: "https://integrations.sh/logo/stripe.com", featured: true, }, { @@ -29,7 +29,7 @@ const openApiOnlyPresets: readonly OpenApiPreset[] = [ name: "Vercel", summary: "Deployments, domains, projects, and edge config.", url: "https://openapi.vercel.sh", - icon: "https://vercel.com/favicon.ico", + icon: "https://integrations.sh/logo/vercel.com", featured: true, }, { @@ -37,7 +37,7 @@ const openApiOnlyPresets: readonly OpenApiPreset[] = [ name: "Cloudflare", summary: "DNS, workers, pages, R2, and security rules.", url: "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json", - icon: "https://cloudflare.com/favicon.ico", + icon: "https://integrations.sh/logo/cloudflare.com", featured: true, }, { @@ -45,7 +45,7 @@ const openApiOnlyPresets: readonly OpenApiPreset[] = [ name: "Neon", summary: "Serverless Postgres: projects, branches, and endpoints.", url: "https://neon.tech/api_spec/release/v2.json", - icon: "https://neon.tech/favicon/favicon.ico", + icon: "https://integrations.sh/logo/neon.tech", featured: true, }, { @@ -77,7 +77,7 @@ const openApiOnlyPresets: readonly OpenApiPreset[] = [ name: "Exa", summary: "Web search, similar links, content retrieval, and answers.", url: "https://raw.githubusercontent.com/exa-labs/openapi-spec/refs/heads/master/exa-openapi-spec.yaml", - icon: "https://exa.ai/images/favicon-32x32.png", + icon: "https://integrations.sh/logo/exa.ai", featured: true, }, { @@ -85,7 +85,7 @@ const openApiOnlyPresets: readonly OpenApiPreset[] = [ name: "Exa Websets", summary: "Websets, enrichments, webhooks, and monitors.", url: "https://raw.githubusercontent.com/exa-labs/openapi-spec/refs/heads/master/exa-websets-spec.yaml", - icon: "https://exa.ai/images/favicon-32x32.png", + icon: "https://integrations.sh/logo/exa.ai", featured: true, }, { @@ -93,49 +93,49 @@ const openApiOnlyPresets: readonly OpenApiPreset[] = [ name: "Axiom", summary: "Log ingestion, querying, datasets, and monitors.", url: "https://axiom.co/docs/restapi/versions/v2.json", - icon: "https://axiom.co/favicon.ico", + icon: "https://integrations.sh/logo/axiom.co", }, { id: "asana", name: "Asana", summary: "Tasks, projects, teams, and workspace management.", url: "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/asana.com/1.0/openapi.yaml", - icon: "https://asana.com/favicon.ico", + icon: "https://integrations.sh/logo/asana.com", }, { id: "twilio", name: "Twilio", summary: "SMS, voice, video, and messaging APIs.", url: "https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json", - icon: "https://twilio.com/favicon.ico", + icon: "https://integrations.sh/logo/twilio.com", }, { id: "digitalocean", name: "DigitalOcean", summary: "Droplets, Kubernetes, databases, and networking.", url: "https://raw.githubusercontent.com/digitalocean/openapi/main/specification/DigitalOcean-public.v2.yaml", - icon: "https://assets.digitalocean.com/favicon.ico", + icon: "https://integrations.sh/logo/digitalocean.com", }, { id: "petstore", name: "Petstore", summary: "Classic OpenAPI demo, no auth required.", url: "https://petstore3.swagger.io/api/v3/openapi.json", - icon: "https://petstore3.swagger.io/favicon-32x32.png", + icon: "https://integrations.sh/logo/petstore3.swagger.io", }, { id: "val-town", name: "Val Town", summary: "Vals, runs, blobs, and email/web endpoints.", url: "https://api.val.town/openapi.json", - icon: "https://www.val.town/favicon.svg", + icon: "https://integrations.sh/logo/val.town", }, { id: "resend", name: "Resend", summary: "Transactional email sending and domain management.", url: "https://raw.githubusercontent.com/resend/resend-openapi/main/resend.yaml", - icon: "https://resend.com/static/favicons/favicon.ico", + icon: "https://integrations.sh/logo/resend.com", }, { id: "spotify",