Add shared LNURL types, indexing helpers, and warnings.

Introduce lnurl as a first-class mint type with probe/announcement fields
and shared helpers the API and web can both rely on.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
michilis
2026-08-22 03:44:27 +02:00
co-authored by Cursor
parent 36c01861f5
commit c97b44018d
8 changed files with 1553 additions and 14 deletions
+64
View File
@@ -28,6 +28,9 @@ function isDisallowedHost(hostname: string): boolean {
if (hostname.endsWith('.local')) return true;
if (PRIVATE_IPV4.test(hostname)) return true;
if (isPrivateIpv6(hostname)) return true;
// Anything that parses as an IP literal is judged by the resolved-address rule too,
// so the two checks cannot disagree about, say, 100.64.0.1 or 224.0.0.1.
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname) && isPrivateIpAddress(hostname)) return true;
// A bare label with no dot cannot be a public host.
if (!hostname.includes('.') && !hostname.includes(':')) return true;
return false;
@@ -50,6 +53,67 @@ function isPrivateIpv6(hostname: string): boolean {
return false;
}
/**
* Is this literal IP address one the indexer must never connect to?
*
* Written against a *resolved* address rather than a hostname, which is the difference
* between this and `isDisallowedHost` above: `mint.example.com` looks like an ordinary
* public name and can resolve to `127.0.0.1`, and only the answer DNS gave can tell you
* so. `POST /api/index` fetches URLs a stranger typed, so it resolves first and checks
* every address here before a socket is opened.
*
* Broader than the hostname rule on purpose. Beyond loopback, link-local and the three
* RFC-1918 ranges it also refuses carrier-grade NAT (100.64/10), `0.0.0.0/8`, the
* benchmarking and documentation ranges, multicast and the broadcast address: none of
* them is a public mint, and each of them is somewhere on a network this server can see
* and a stranger should not be able to point it at.
*/
export function isPrivateIpAddress(value: string): boolean {
const ip = value.trim().toLowerCase().replace(/^\[|\]$/g, '');
if (!ip) return true;
const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(ip);
if (v4) {
const [a, b] = [Number(v4[1]), Number(v4[2])];
if (a === undefined || b === undefined || a > 255 || b > 255) return true;
if (a === 0 || a === 10 || a === 127) return true; // this-network, RFC1918, loopback
if (a === 169 && b === 254) return true; // link-local
if (a === 172 && b >= 16 && b <= 31) return true; // RFC1918
if (a === 192 && b === 168) return true; // RFC1918
if (a === 192 && b === 0) return true; // IETF protocol assignments / 192.0.2.0 docs
if (a === 198 && (b === 18 || b === 19)) return true; // benchmarking
if (a === 198 && b === 51) return true; // documentation
if (a === 203 && b === 0) return true; // documentation
if (a === 100 && b >= 64 && b <= 127) return true; // carrier-grade NAT
if (a >= 224) return true; // multicast, reserved, broadcast
return false;
}
if (!ip.includes(':')) return true; // Not an address this function understands.
// An IPv4-mapped or IPv4-compatible address is judged on its IPv4 half.
const mapped = /:((?:\d{1,3}\.){3}\d{1,3})$/.exec(ip);
if (mapped?.[1]) return isPrivateIpAddress(mapped[1]);
if (ip === '::' || ip === '::1') return true;
if (/^f[cd]/.test(ip)) return true; // unique local, fc00::/7
if (/^fe[89ab]/.test(ip)) return true; // link-local, fe80::/10
if (/^ff/.test(ip)) return true; // multicast
return false;
}
/**
* Is this hostname one the indexer must never fetch, before DNS is consulted at all?
*
* The cheap half of the check: `localhost`, `.onion`, `.local`, a bare label with no
* dot, and an IP literal that is already disqualified by `isPrivateIpAddress`. Exported
* so the on-demand indexer can refuse the obvious cases without paying for a lookup,
* and so a redirect hop can be judged by the same rule its origin was.
*/
export function isBlockedHostname(hostname: string): boolean {
return isDisallowedHost(hostname.toLowerCase());
}
/**
* May the indexer fetch this URL? http(s) only, and never a private or local host.
*