Skip to content

Commit c099e76

Browse files
committed
Don't emit favicons for LAN hosts (fix Local Network Access prompt)
Records with .local / localhost / private-IP domains (e.g. vtex.local x34) were producing https://<host>/favicon.ico img requests, which the browser treats as accessing the user's local network — triggering a permission prompt on integrations.sh. Add a shared faviconUrl() guard that returns null for non-public hosts; those fall back to the monogram everywhere (pipeline + domain/detail pages + homepage client).
1 parent 396935e commit c099e76

8 files changed

Lines changed: 42 additions & 14 deletions

File tree

output/index.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

output/openapi.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:d30bedeedaccbe52d41ae5191bb8f964c36c1f7aba6c803c86095e72384e0e1e
3-
size 58961348
2+
oid sha256:7d0b60f9d8ddc2fe01f481554f540d2d37e3886d48f0c3c782d1a02cb0192dfa
3+
size 58957466

public/api.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

scripts/normalize.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { join, dirname } from "node:path";
33
import { fileURLToPath } from "node:url";
44
import { getDomain } from "tldts";
55
import type { Integration, Feed, Kind, ExtractedTool } from "../src/lib/types.ts";
6+
import { faviconUrl } from "../src/lib/favicon.ts";
67

78
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
89
const SOURCES = join(ROOT, "sources");
@@ -309,7 +310,7 @@ function buildCli(): Integration[] {
309310
name: c.name,
310311
description: c.description ?? "",
311312
url: c.docs,
312-
icon: `https://${c.domain}/favicon.ico`,
313+
icon: faviconUrl(c.domain) ?? undefined,
313314
categories: [],
314315
feeds: ["cli-seed" as Feed],
315316
cli: { install: c.install, domain: c.domain, docs: c.docs, repo: c.repo },
@@ -387,7 +388,7 @@ function applyFavicons(recs: Integration[]): Integration[] {
387388
if (!domain) return { ...r, icon: undefined };
388389
return {
389390
...r,
390-
icon: `https://${domain}/favicon.ico`,
391+
icon: faviconUrl(domain) ?? undefined,
391392
};
392393
});
393394
}
@@ -528,8 +529,9 @@ function buildIndex(all: Integration[]) {
528529
name: remapped ? r.name.replace(/^.*?[-]\s*/, "") : r.name,
529530
description: r.description.slice(0, 240),
530531
url: r.url,
531-
// Icon is the provider's own apex-domain favicon — never a third-party host.
532-
icon: domain ? `https://${domain}/favicon.ico` : undefined,
532+
// Icon is the provider's own apex-domain favicon — never a third-party host,
533+
// and never a LAN address (.local/private hosts return null).
534+
icon: faviconUrl(domain) ?? undefined,
533535
domain,
534536
categories: r.categories,
535537
feeds: r.feeds,

src/lib/favicon.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Private/non-routable host ranges. We must NOT emit favicon URLs for these:
2+
// a public page requesting a LAN address (`.local`, localhost, private IP)
3+
// triggers the browser's Local Network Access permission prompt.
4+
const PRIVATE_IP = /^(10|127)\.\d|^192\.168\.|^172\.(1[6-9]|2\d|3[01])\./;
5+
6+
/** Favicon URL for a domain, or null when the host isn't publicly routable. */
7+
export function faviconUrl(domain: string | null | undefined): string | null {
8+
if (!domain) return null;
9+
const d = domain.toLowerCase();
10+
if (!d.includes(".")) return null; // single-label host (not a public FQDN)
11+
if (d === "localhost" || d.endsWith(".local") || d.endsWith(".internal") || d.endsWith(".localhost")) return null;
12+
if (PRIVATE_IP.test(d)) return null;
13+
return `https://${domain}/favicon.ico`;
14+
}

src/pages/[kind]/[slug].astro

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Base from "~/layouts/Base.astro";
33
import Breadcrumb from "~/components/Breadcrumb.astro";
44
import Markdown from "~/components/Markdown.tsx";
55
import { byKind, kindLabel, domainById } from "~/lib/data.ts";
6+
import { faviconUrl } from "~/lib/favicon.ts";
67
import type { Kind } from "~/lib/types.ts";
78
89
export function getStaticPaths() {
@@ -17,7 +18,7 @@ export function getStaticPaths() {
1718
1819
const { item } = Astro.props;
1920
const domain = domainById.get(item.id) ?? item.slug;
20-
const favicon = `https://${domain}/favicon.ico`;
21+
const favicon = faviconUrl(domain);
2122
const letter = (item.name[0] ?? "?").toUpperCase();
2223
let specHost: string | null = null;
2324
if (item.openapi?.specUrl) {
@@ -50,7 +51,7 @@ const description = (item.description || `${item.name} ${kindLabel[item.kind]} d
5051
<header class="head">
5152
<div class="favicon">
5253
<span class="fav-letter">{letter}</span>
53-
<img src={favicon} width="22" height="22" alt="" loading="lazy" onerror="this.remove()" />
54+
{favicon && <img src={favicon} width="22" height="22" alt="" loading="lazy" onerror="this.remove()" />}
5455
</div>
5556
<div class="head-body">
5657
<h1>{item.name}</h1>

src/pages/d/[domain].astro

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
import Base from "~/layouts/Base.astro";
33
import Breadcrumb from "~/components/Breadcrumb.astro";
4+
import { faviconUrl } from "~/lib/favicon.ts";
45
import { index, byId } from "~/lib/data.ts";
56
import type { IndexRecord } from "~/lib/data.ts";
67
import type { Kind } from "~/lib/types.ts";
@@ -48,7 +49,7 @@ const sections = KIND_ORDER.map((kind) => ({
4849
),
4950
})).filter((s) => s.items.length > 0);
5051
51-
const icon = `https://${domain}/favicon.ico`;
52+
const icon = faviconUrl(domain);
5253
const letter = (domain[0] ?? "?").toUpperCase();
5354
5455
const formatList = sections.map((s) => SECTION_LABEL[s.kind].replace(" · ", "/")).join(", ");
@@ -83,7 +84,7 @@ const metaFor = (r: IndexRecord): string => {
8384
<header class="head">
8485
<div class="favicon">
8586
<span class="fav-letter">{letter}</span>
86-
<img src={icon} width="22" height="22" alt="" loading="lazy" onerror="this.remove()" />
87+
{icon && <img src={icon} width="22" height="22" alt="" loading="lazy" onerror="this.remove()" />}
8788
</div>
8889
<div class="head-body">
8990
<h1>{domain}</h1>

src/pages/index.astro

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,14 @@ const counts = {
7272

7373
const esc = (s) =>
7474
String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
75-
const fav = (d) => `https://${d}/favicon.ico`;
75+
// Favicon URL, or "" for non-public hosts (.local / localhost / private IP)
76+
// — requesting those from a public page triggers a Local Network Access prompt.
77+
const fav = (d) => {
78+
const h = String(d).toLowerCase();
79+
if (!h.includes(".") || h === "localhost" || h.endsWith(".local") || h.endsWith(".internal") ||
80+
h.endsWith(".localhost") || /^(10|127)\.\d|^192\.168\.|^172\.(1[6-9]|2\d|3[01])\./.test(h)) return "";
81+
return `https://${d}/favicon.ico`;
82+
};
7683
const dpath = (d) => `/d/${encodeURIComponent(d)}/`;
7784

7885
const buildGroups = (recs) => {
@@ -93,7 +100,10 @@ const counts = {
93100

94101
const groupRowHtml = (g) => {
95102
const ph = esc((g.domain[0] || "?").toUpperCase());
96-
const icon = `<img class="c-fav" src="${esc(g.icon || fav(g.domain))}" alt="" loading="lazy" width="18" height="18" onerror="this.outerHTML='<span class=&quot;ph&quot;>${ph}</span>'">`;
103+
const src = g.icon || fav(g.domain);
104+
const icon = src
105+
? `<img class="c-fav" src="${esc(src)}" alt="" loading="lazy" width="18" height="18" onerror="this.outerHTML='<span class=&quot;ph&quot;>${ph}</span>'">`
106+
: `<span class="ph">${ph}</span>`;
97107
const chips = TORDER.filter((t) => g.types[t]).map((t) => {
98108
const c = g.types[t];
99109
return `<a class="chip" href="${dpath(g.domain)}#${t}">${TLABEL[t]}${c.count > 1 ? `<span class="n">${c.count}</span>` : ""}</a>`;

0 commit comments

Comments
 (0)