fix(shared): profile.ts must not reference the URL global type
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s

packages/shared compiles without the DOM or Node type libraries, so `URL` has
no type there even though it exists at runtime everywhere this code runs. The
web and server builds resolved the same file under their own libs and passed;
only the shared package's own `tsc` — which CI runs first — failed, on both
runners. Reach the constructor through globalThis with a minimal structural
type instead of pulling a lib into a types-only package. Runtime unchanged,
so the deployed site (82798d5c) needs no redeploy for this.

Lesson recorded for the local checklist: run `pnpm --filter @backspace/shared
build` too — the root `typecheck` script already does; the web build alone
does not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 23:44:35 -03:00
co-authored by Claude Fable 5.1
parent 82798d5c5a
commit e7094014b7
+17 -6
View File
@@ -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<T extends string>(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<string, unknown>;
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)
: [];