diff --git a/packages/shared/src/profile.ts b/packages/shared/src/profile.ts index e4c6d94d..99594959 100644 --- a/packages/shared/src/profile.ts +++ b/packages/shared/src/profile.ts @@ -136,6 +136,18 @@ const HEX = /^#[0-9a-fA-F]{6}$/; * proxied — a profile is not a place to load arbitrary third-party pixels. */ const COVER_HOSTS = ['cdn.cloudflare.steamstatic.com', 'cdn.akamai.steamstatic.com', 'shared.cloudflare.steamstatic.com', 'shared.akamai.steamstatic.com', 'steamcdn-a.akamaihd.net']; +/** + * This package compiles without the DOM or Node type libraries, so the `URL` + * global has no type here even though it exists at runtime everywhere this + * code runs (browsers, Node ≥ 10). Reach it through globalThis with a minimal + * structural type rather than pulling a whole lib into a types-only package. + */ +interface UrlLike { protocol: string; hostname: string; toString(): string } +const UrlCtor = (globalThis as unknown as { URL: new (s: string) => UrlLike }).URL; +function parseUrl(value: string): UrlLike | null { + try { return new UrlCtor(value); } catch { return null; } +} + function pick(value: unknown, allowed: readonly T[], fallback: T): T { return typeof value === 'string' && (allowed as readonly string[]).includes(value) ? (value as T) : fallback; } @@ -149,15 +161,14 @@ function httpsUrl(value: unknown, max: number): string | null { if (typeof value !== 'string') return null; const v = value.trim(); if (v.length === 0 || v.length > max) return null; - try { - const u = new URL(v); - return u.protocol === 'https:' ? u.toString() : null; - } catch { return null; } + const u = parseUrl(v); + return u && u.protocol === 'https:' ? u.toString() : null; } function coverUrl(value: unknown): string | null { const u = httpsUrl(value, 300); if (!u) return null; - try { return COVER_HOSTS.includes(new URL(u).hostname) ? u : null; } catch { return null; } + const parsed = parseUrl(u); + return parsed && COVER_HOSTS.includes(parsed.hostname) ? u : null; } /** First grapheme only — one emoji, not a sentence hidden in the tag. */ function oneGrapheme(value: unknown): string { @@ -209,7 +220,7 @@ export function normalizeProfileStyle(input: unknown): ProfileStyle { const o = (l && typeof l === 'object' ? l : {}) as Record; const url = httpsUrl(o.url, PROFILE_LIMITS.linkUrl); if (!url) return null; - const label = str(o.label, PROFILE_LIMITS.linkLabel) || new URL(url).hostname.replace(/^www\./, ''); + const label = str(o.label, PROFILE_LIMITS.linkLabel) || (parseUrl(url)?.hostname ?? url).replace(/^www\./, ''); return { label, url }; }).filter((l): l is ProfileLink => l !== null).slice(0, PROFILE_LIMITS.links) : [];