fix clickmap wildcard origin launch

This commit is contained in:
mantrakp04 2026-06-16 12:45:30 -07:00
parent e93b7520c4
commit 4288fa4716
3 changed files with 145 additions and 56 deletions

View File

@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { getClickmapOriginOptions, normalizeClickmapOrigin } from "./clickmap-origins";
describe("clickmap origin options", () => {
it("keeps wildcard domains out of launchable origins", () => {
const options = getClickmapOriginOptions({
wildcard: { baseUrl: "https://**.stack-auth.com" },
concrete: { baseUrl: "https://app.stack-auth.com/path?x=1" },
duplicate: { baseUrl: "https://app.stack-auth.com/other" },
});
expect(options).toMatchInlineSnapshot(`
{
"origins": [
{
"id": "duplicate",
"origin": "https://app.stack-auth.com",
},
],
"wildcardDomains": [
{
"baseUrl": "https://**.stack-auth.com",
"id": "wildcard",
},
],
}
`);
});
it("normalizes only HTTP(S) origins", () => {
expect(normalizeClickmapOrigin("https://app.dev.stack-auth.com/dashboard")).toMatchInlineSnapshot(`"https://app.dev.stack-auth.com"`);
expect(normalizeClickmapOrigin("javascript:alert(1)")).toMatchInlineSnapshot(`null`);
});
});

View File

@ -0,0 +1,69 @@
export type ClickmapOrigin = {
id: string,
origin: string,
};
export type ClickmapWildcardDomain = {
id: string,
baseUrl: string,
};
export function normalizeClickmapOrigin(baseUrl: string): string | null {
let url: URL;
try {
url = new URL(baseUrl);
} catch {
return null;
}
if (url.protocol !== "https:" && url.protocol !== "http:") {
return null;
}
return url.origin;
}
function isWildcardDomain(baseUrl: string): boolean {
return baseUrl.includes("*");
}
function compareStrings(a: string, b: string): number {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}
export function getClickmapOriginOptions(trustedDomains: Record<string, { baseUrl?: string | null }>): {
origins: ClickmapOrigin[],
wildcardDomains: ClickmapWildcardDomain[],
} {
const byOrigin = new Map<string, ClickmapOrigin>();
const wildcardDomains: ClickmapWildcardDomain[] = [];
for (const id in trustedDomains) {
const domain = trustedDomains[id];
if (domain.baseUrl == null) {
continue;
}
if (isWildcardDomain(domain.baseUrl)) {
wildcardDomains.push({ id, baseUrl: domain.baseUrl });
continue;
}
const origin = normalizeClickmapOrigin(domain.baseUrl);
if (origin == null) {
continue;
}
byOrigin.set(origin, { id, origin });
}
return {
origins: Array.from(byOrigin.values()).sort((a, b) => compareStrings(a.origin, b.origin)),
wildcardDomains: wildcardDomains.sort((a, b) => compareStrings(a.baseUrl, b.baseUrl)),
};
}

View File

@ -18,29 +18,16 @@ import {
Typography,
} from "@/components/ui";
import { DesignAnalyticsCard } from "@/components/design-components/analytics-card";
import { DesignAlert } from "@/components/design-components/alert";
import { useRouter } from "@/components/router";
import type { AnalyticsClickmapTokenResponse } from "@hexclave/shared/dist/interface/admin-metrics";
import {
CLICKMAP_OVERLAY_TOKEN_STORAGE_KEY,
CLICKMAP_OVERLAY_TOKEN_UPDATED_EVENT,
} from "@hexclave/shared/dist/utils/analytics-clickmap-overlay";
import { typedEntries } from "@hexclave/shared/dist/utils/objects";
import { stringCompare } from "@hexclave/shared/dist/utils/strings";
import { ArrowRight, GlobeHemisphereWest } from "@phosphor-icons/react";
import { useEffect, useMemo, useState } from "react";
type ClickmapOrigin = {
id: string,
origin: string,
};
function normalizeOrigin(baseUrl: string): string | null {
try {
return new URL(baseUrl).origin;
} catch {
return null;
}
}
import { getClickmapOriginOptions, normalizeClickmapOrigin, type ClickmapOrigin } from "./clickmap-origins";
// The clickmap token is a self-describing JWT (its payload carries the project
// and origin it was minted for), so the snippet only has to hand over the token
@ -118,19 +105,8 @@ export default function PageClient() {
setCustomOrigin(window.location.origin);
}, []);
const origins = useMemo(() => {
const byOrigin = new Map<string, ClickmapOrigin>();
for (const [id, domain] of typedEntries(config.domains.trustedDomains)) {
if (domain.baseUrl == null) {
continue;
}
const origin = normalizeOrigin(domain.baseUrl);
if (origin == null) {
continue;
}
byOrigin.set(origin, { id, origin });
}
return Array.from(byOrigin.values()).sort((a, b) => stringCompare(a.origin, b.origin));
const { origins, wildcardDomains } = useMemo(() => {
return getClickmapOriginOptions(config.domains.trustedDomains);
}, [config.domains.trustedDomains]);
async function showClickmap(origin: ClickmapOrigin) {
@ -166,39 +142,41 @@ export default function PageClient() {
description="Launch the clickmap toolbar on a trusted domain."
fillWidth
>
{config.domains.allowLocalhost && (
<DesignAnalyticsCard gradient="slate" className="p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-end">
<div className="min-w-0 flex-1 space-y-1">
<Typography className="font-medium">Localhost origin</Typography>
<Typography type="p" variant="secondary" className="text-xs">
Use the exact origin shown in the browser address bar for your local site.
</Typography>
<Input value={customOrigin} onChange={(event) => setCustomOrigin(event.target.value)} placeholder="http://localhost:3000" />
</div>
<Button
className="gap-1.5"
disabled={customOrigin.trim() === ""}
onClick={async () => {
const origin = normalizeOrigin(customOrigin);
if (origin == null) {
window.alert("Enter a valid HTTP(S) origin, for example http://localhost:3000.");
return;
}
await showClickmap({ id: "localhost", origin });
}}
>
Show clickmap
<ArrowRight className="h-4 w-4" />
</Button>
<DesignAnalyticsCard gradient="slate" className="p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-end">
<div className="min-w-0 flex-1 space-y-1">
<Typography className="font-medium">Exact page origin</Typography>
<Typography type="p" variant="secondary" className="text-xs">
Use the exact origin shown in the browser address bar, including for domains matched by a wildcard.
</Typography>
<Input value={customOrigin} onChange={(event) => setCustomOrigin(event.target.value)} placeholder="https://app.example.com" />
</div>
</DesignAnalyticsCard>
)}
<Button
className="gap-1.5"
disabled={customOrigin.trim() === ""}
onClick={async () => {
const origin = normalizeClickmapOrigin(customOrigin);
if (origin == null) {
window.alert("Enter a valid HTTP(S) origin, for example https://app.example.com.");
return;
}
await showClickmap({ id: "exact-origin", origin });
}}
>
Show clickmap
<ArrowRight className="h-4 w-4" />
</Button>
</div>
</DesignAnalyticsCard>
{origins.length === 0 ? (
<Alert className="rounded-2xl">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<span>Add a trusted domain before launching a production clickmap.</span>
<span>
{wildcardDomains.length === 0
? "Add a trusted domain before launching a production clickmap."
: "Enter an exact origin that matches a wildcard domain, or add a concrete trusted domain."}
</span>
<Button
className="shrink-0 gap-1.5"
onClick={() => router.push(`/projects/${project.id}/domains`)}
@ -232,6 +210,14 @@ export default function PageClient() {
</DesignAnalyticsCard>
)}
{wildcardDomains.length > 0 && (
<DesignAlert
variant="info"
title="Wildcard domains need an exact origin"
description={`Enter the concrete site origin above. ${wildcardDomains.map((domain) => domain.baseUrl).join(", ")} can match real pages, but cannot be opened directly as a clickmap target.`}
/>
)}
<ClickmapTokenDialog
origin={selectedOrigin}
token={token}