@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* One mint URL normalizer, used by the indexer, the API and the browser.
|
||||
*
|
||||
* The old site had three different ad-hoc normalizers that disagreed (see NOTES.md):
|
||||
* mints were keyed on the raw tag string (so a trailing slash created a duplicate mint)
|
||||
* while reviews were matched on bare hostname (so a review of one path counted for every
|
||||
* path on that host). Rules below, per BACKEND.md:
|
||||
*
|
||||
* - lowercase scheme and host, force https, strip trailing slash, strip default port
|
||||
* - keep the path: mint.minibits.cash/Bitcoin is a different mint from mint.minibits.cash
|
||||
* - reject non-https-able, localhost, private IPs and .onion
|
||||
*/
|
||||
|
||||
export interface NormalizedMint {
|
||||
/** Canonical URL, the primary key. */
|
||||
url: string;
|
||||
/** URL-safe routing slug, unique per url. */
|
||||
host: string;
|
||||
}
|
||||
|
||||
const PRIVATE_IPV4 =
|
||||
/^(10\.|127\.|0\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.)/;
|
||||
|
||||
/** Hostnames that are never a public mint. */
|
||||
function isDisallowedHost(hostname: string): boolean {
|
||||
if (hostname === 'localhost' || hostname.endsWith('.localhost')) return true;
|
||||
if (hostname.endsWith('.onion')) return true;
|
||||
if (hostname.endsWith('.local')) return true;
|
||||
if (hostname === '::1' || hostname === '[::1]') return true;
|
||||
if (PRIVATE_IPV4.test(hostname)) return true;
|
||||
// A bare label with no dot cannot be a public host.
|
||||
if (!hostname.includes('.') && !hostname.includes(':')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a mint URL as found in a Nostr `u` tag or typed by a user.
|
||||
* Returns null when the input is not a usable public mint URL.
|
||||
*/
|
||||
export function normalizeMintUrl(input: string): NormalizedMint | null {
|
||||
const raw = input?.trim();
|
||||
if (!raw) return null;
|
||||
|
||||
// Tolerate a missing scheme ("mint.example.com/x"), which users paste constantly.
|
||||
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`;
|
||||
|
||||
let u: URL;
|
||||
try {
|
||||
u = new URL(withScheme);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
// http:// and https:// are the only schemes a mint speaks; http is upgraded, not kept,
|
||||
// because the browser cannot fetch http from an https page anyway (mixed content).
|
||||
if (u.protocol !== 'https:' && u.protocol !== 'http:') return null;
|
||||
u.protocol = 'https:';
|
||||
|
||||
const hostname = u.hostname.toLowerCase();
|
||||
if (!hostname || isDisallowedHost(hostname)) return null;
|
||||
|
||||
// Drop the default port, query and fragment: none of them identify a mint.
|
||||
const port = u.port === '443' || u.port === '80' ? '' : u.port;
|
||||
const path = u.pathname.replace(/\/+$/, '');
|
||||
|
||||
const url = `https://${hostname}${port ? `:${port}` : ''}${path}`;
|
||||
return { url, host: slugFor(hostname, port, path) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Routing slug. Hostname alone when there is no path, otherwise hostname plus the path
|
||||
* segments joined with `-`. Deterministic; the row is looked up by slug, so it does not
|
||||
* need to be reversible.
|
||||
*/
|
||||
function slugFor(hostname: string, port: string, path: string): string {
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
const base = port ? `${hostname}-${port}` : hostname;
|
||||
if (segments.length === 0) return base;
|
||||
const tail = segments
|
||||
.join('-')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9._-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
return tail ? `${base}-${tail}` : base;
|
||||
}
|
||||
|
||||
/** Display domain for a mint: hostname plus path, no scheme. */
|
||||
export function displayDomain(url: string): string {
|
||||
return url.replace(/^https?:\/\//, '').replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Short display name derived from the URL, for mints whose /v1/info has no name.
|
||||
* Matches the old site's `hostname.replace(/^mint\./,'').replace(/^www\./,'')`.
|
||||
*/
|
||||
export function nameFromUrl(url: string): string {
|
||||
const domain = displayDomain(url);
|
||||
const host = domain.split('/')[0] ?? domain;
|
||||
return host.replace(/^mint\./, '').replace(/^www\./, '');
|
||||
}
|
||||
Reference in New Issue
Block a user