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
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:
@@ -136,6 +136,18 @@ const HEX = /^#[0-9a-fA-F]{6}$/;
|
|||||||
* proxied — a profile is not a place to load arbitrary third-party pixels. */
|
* 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'];
|
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 {
|
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;
|
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;
|
if (typeof value !== 'string') return null;
|
||||||
const v = value.trim();
|
const v = value.trim();
|
||||||
if (v.length === 0 || v.length > max) return null;
|
if (v.length === 0 || v.length > max) return null;
|
||||||
try {
|
const u = parseUrl(v);
|
||||||
const u = new URL(v);
|
return u && u.protocol === 'https:' ? u.toString() : null;
|
||||||
return u.protocol === 'https:' ? u.toString() : null;
|
|
||||||
} catch { return null; }
|
|
||||||
}
|
}
|
||||||
function coverUrl(value: unknown): string | null {
|
function coverUrl(value: unknown): string | null {
|
||||||
const u = httpsUrl(value, 300);
|
const u = httpsUrl(value, 300);
|
||||||
if (!u) return null;
|
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. */
|
/** First grapheme only — one emoji, not a sentence hidden in the tag. */
|
||||||
function oneGrapheme(value: unknown): string {
|
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 o = (l && typeof l === 'object' ? l : {}) as Record<string, unknown>;
|
||||||
const url = httpsUrl(o.url, PROFILE_LIMITS.linkUrl);
|
const url = httpsUrl(o.url, PROFILE_LIMITS.linkUrl);
|
||||||
if (!url) return null;
|
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 };
|
return { label, url };
|
||||||
}).filter((l): l is ProfileLink => l !== null).slice(0, PROFILE_LIMITS.links)
|
}).filter((l): l is ProfileLink => l !== null).slice(0, PROFILE_LIMITS.links)
|
||||||
: [];
|
: [];
|
||||||
|
|||||||
Reference in New Issue
Block a user