Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion apps/mesh/src/commerce-discovery/site-url.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { describe, expect, test } from "bun:test";
import { normalizeCommerceSiteUrl } from "./site-url.ts";
import {
isConnectionClaimedForSite,
normalizeCommerceSiteUrl,
} from "./site-url.ts";

describe("normalizeCommerceSiteUrl", () => {
test("adds https to bare hostnames", () => {
Expand Down Expand Up @@ -100,3 +103,36 @@ describe("normalizeCommerceSiteUrl", () => {
}
});
});

describe("isConnectionClaimedForSite", () => {
test("trusts an existing claim when no site is requested (returning session)", () => {
expect(isConnectionClaimedForSite("", "https://a.com")).toBe(true);
expect(isConnectionClaimedForSite(" ", undefined)).toBe(true);
});

test("matches when the requested site equals the claimed site", () => {
expect(
isConnectionClaimedForSite("https://a.com/path", "https://a.com"),
).toBe(true);
});

test("does not match when the requested site differs from the claimed site", () => {
expect(isConnectionClaimedForSite("https://b.com", "https://a.com")).toBe(
false,
);
});

test("does not match when nothing is claimed yet", () => {
expect(isConnectionClaimedForSite("https://a.com", undefined)).toBe(false);
});

test("a malformed non-empty request is NOT treated like an empty one", () => {
// Regression: both "" and "not-a-url" fail normalizeCommerceSiteUrl, but
// only the empty case should bypass the site-match check — otherwise a
// garbled ?siteUrl param would silently reuse whatever site the
// connection happens to already be claimed for.
expect(isConnectionClaimedForSite("not-a-url", "https://a.com")).toBe(
false,
);
});
});
23 changes: 23 additions & 0 deletions apps/mesh/src/commerce-discovery/site-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,26 @@ export function normalizeCommerceSiteUrl(input: string): CommerceSiteUrlResult {
value: url.origin,
};
}

/**
* True when an already-provisioned Commerce Discovery connection's claimed
* site matches the one currently requested.
*
* An empty `requestedSite` means no site was specified (a returning session
* with no `?siteUrl`) — trust the existing claim. A non-empty but malformed
* `requestedSite` must NOT be treated the same as "no site requested": both
* fail `normalizeCommerceSiteUrl`, but only the empty case should bypass the
* match check — otherwise a garbled site param silently skips re-claiming
* and surfaces whatever site the connection happens to already be claimed
* for.
*/
export function isConnectionClaimedForSite(
requestedSite: string,
claimedSiteUrl: string | undefined,
): boolean {
if (!requestedSite.trim()) return true;
const requested = normalizeCommerceSiteUrl(requestedSite);
if (!requested.ok || !claimedSiteUrl) return false;
const claimed = normalizeCommerceSiteUrl(claimedSiteUrl);
return claimed.ok && claimed.value === requested.value;
}
17 changes: 8 additions & 9 deletions apps/mesh/src/web/routes/commerce-onboarding.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { normalizeCommerceSiteUrl } from "@/commerce-discovery/site-url";
import {
isConnectionClaimedForSite,
normalizeCommerceSiteUrl,
} from "@/commerce-discovery/site-url";
import { AuthEntry } from "@/web/components/auth-entry";
import { ErrorBoundary } from "@/web/components/error-boundary";
import { AuthSplitLayout } from "@/web/components/auth-split-layout";
Expand Down Expand Up @@ -778,14 +781,10 @@ function CommerceSetupContent({
// requested site; when the site differs, fall through to setup (idempotent
// re-claim) so the token + metadata.siteUrl follow the site being onboarded.
const requestedSite = initialSiteUrl || siteUrlInput || "";
const requestedNormalized = normalizeCommerceSiteUrl(requestedSite);
const claimedNormalized = connectionSiteUrl
? normalizeCommerceSiteUrl(connectionSiteUrl)
: null;
const claimedForRequestedSite =
!requestedNormalized.ok ||
(!!claimedNormalized?.ok &&
claimedNormalized.value === requestedNormalized.value);
const claimedForRequestedSite = isConnectionClaimedForSite(
requestedSite,
connectionSiteUrl,
);
const setupReady = connectionExists && claimedForRequestedSite;
const currentSiteUrl =
initialSiteUrl || siteUrlInput || connectionSiteUrl || "";
Expand Down
Loading