mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
fix: restrict top-level slug route to publishers
This commit is contained in:
@@ -34,35 +34,16 @@ describe("top-level slug route loader", () => {
|
||||
resolveTopLevelSlugRouteMock.mockReset();
|
||||
});
|
||||
|
||||
it("redirects plugin slugs to plugin detail pages", async () => {
|
||||
resolveTopLevelSlugRouteMock.mockResolvedValue({
|
||||
kind: "plugin",
|
||||
name: "@openclaw/codex",
|
||||
href: "/openclaw/plugins/codex",
|
||||
});
|
||||
it("returns not found for plugin aliases without matching publishers", async () => {
|
||||
resolveTopLevelSlugRouteMock.mockResolvedValue(null);
|
||||
|
||||
expect(await runLoader("codex")).toEqual({
|
||||
redirect: {
|
||||
href: "/openclaw/plugins/codex",
|
||||
replace: true,
|
||||
},
|
||||
});
|
||||
expect(await runLoader("codex")).toEqual({ notFound: true });
|
||||
});
|
||||
|
||||
it("redirects skill slugs to canonical owner pages", async () => {
|
||||
resolveTopLevelSlugRouteMock.mockResolvedValue({
|
||||
kind: "skill",
|
||||
owner: "ivangdavila",
|
||||
slug: "codex",
|
||||
});
|
||||
it("returns not found for legacy bare skill slugs", async () => {
|
||||
resolveTopLevelSlugRouteMock.mockResolvedValue(null);
|
||||
|
||||
expect(await runLoader("codex")).toEqual({
|
||||
redirect: {
|
||||
to: "/$owner/skills/$slug",
|
||||
params: { owner: "ivangdavila", slug: "codex" },
|
||||
replace: true,
|
||||
},
|
||||
});
|
||||
expect(await runLoader("expedia")).toEqual({ notFound: true });
|
||||
});
|
||||
|
||||
it("returns publisher profile data for canonical publisher paths", async () => {
|
||||
|
||||
+13
-24
@@ -19,29 +19,21 @@ describe("slug route resolution", () => {
|
||||
queryMock.mockReset();
|
||||
});
|
||||
|
||||
it("resolves Codex to the official OpenClaw plugin", async () => {
|
||||
await expect(resolveTopLevelSlugRoute("codex")).resolves.toEqual({
|
||||
kind: "plugin",
|
||||
name: "@openclaw/codex",
|
||||
href: "/openclaw/plugins/codex",
|
||||
});
|
||||
it("does not resolve top-level OpenClaw plugin aliases without matching publishers", async () => {
|
||||
queryMock.mockResolvedValue(null);
|
||||
|
||||
await expect(resolveTopLevelSlugRoute("codex")).resolves.toBeNull();
|
||||
expect(fetchSkillPageDataMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves extension slugs to their configured npm package names", async () => {
|
||||
await expect(resolveTopLevelSlugRoute("anthropic")).resolves.toEqual({
|
||||
kind: "plugin",
|
||||
name: "@openclaw/anthropic-provider",
|
||||
href: "/openclaw/plugins/anthropic-provider",
|
||||
});
|
||||
|
||||
it("resolves extension slugs to their configured npm package names for legacy owner routes", async () => {
|
||||
await expect(resolveOpenClawPluginSlug("kimi-coding", "openclaw")).resolves.toEqual({
|
||||
kind: "plugin",
|
||||
name: "@openclaw/kimi-provider",
|
||||
href: "/openclaw/plugins/kimi-provider",
|
||||
});
|
||||
|
||||
await expect(resolveTopLevelSlugRoute("diffs-language-pack")).resolves.toEqual({
|
||||
await expect(resolveOpenClawPluginSlug("diffs-language-pack", "openclaw")).resolves.toEqual({
|
||||
kind: "plugin",
|
||||
name: "@openclaw/diffs-language-pack",
|
||||
href: "/openclaw/plugins/diffs-language-pack",
|
||||
@@ -62,7 +54,7 @@ describe("slug route resolution", () => {
|
||||
await expect(resolveOpenClawPluginSlug("codex", "ivangdavila")).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("falls back to skill slug resolution when no official plugin exists", async () => {
|
||||
it("does not fall back to skill slug resolution when no publisher exists", async () => {
|
||||
queryMock.mockResolvedValue(null);
|
||||
fetchSkillPageDataMock.mockResolvedValue({
|
||||
owner: "steipete",
|
||||
@@ -75,11 +67,8 @@ describe("slug route resolution", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await expect(resolveTopLevelSlugRoute("weather")).resolves.toEqual({
|
||||
kind: "skill",
|
||||
owner: "steipete",
|
||||
slug: "weather",
|
||||
});
|
||||
await expect(resolveTopLevelSlugRoute("weather")).resolves.toBeNull();
|
||||
expect(fetchSkillPageDataMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves publisher handles before legacy bare skill slugs", async () => {
|
||||
@@ -93,13 +82,13 @@ describe("slug route resolution", () => {
|
||||
expect(fetchSkillPageDataMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps official OpenClaw aliases ahead of colliding publisher handles", async () => {
|
||||
it("keeps publisher handles ahead of colliding official OpenClaw aliases", async () => {
|
||||
queryMock.mockResolvedValue({ _id: "publishers:tencent", handle: "tencent" });
|
||||
|
||||
await expect(resolveTopLevelSlugRoute("tencent")).resolves.toEqual({
|
||||
kind: "plugin",
|
||||
name: "@openclaw/tencent-provider",
|
||||
href: "/openclaw/plugins/tencent-provider",
|
||||
kind: "publisher",
|
||||
handle: "tencent",
|
||||
publisher: { _id: "publishers:tencent", handle: "tencent" },
|
||||
});
|
||||
expect(fetchSkillPageDataMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
+14
-32
@@ -3,28 +3,20 @@ import { convexHttp } from "../convex/client";
|
||||
import { getOpenClawExtensionPackageName } from "./openClawExtensionSlugs";
|
||||
import { buildPluginDetailHref } from "./pluginRoutes";
|
||||
import type { PublicPublisherListItem } from "./publicUser";
|
||||
import { fetchSkillPageData } from "./skillPage";
|
||||
|
||||
const OPENCLAW_HANDLE = "openclaw";
|
||||
|
||||
type SlugRouteTarget =
|
||||
| {
|
||||
kind: "plugin";
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
| {
|
||||
kind: "skill";
|
||||
owner: string;
|
||||
slug: string;
|
||||
}
|
||||
| {
|
||||
kind: "publisher";
|
||||
handle: string;
|
||||
publisher: PublicPublisherListItem;
|
||||
};
|
||||
type PluginSlugRouteTarget = {
|
||||
kind: "plugin";
|
||||
name: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
type PluginSlugRouteTarget = Extract<SlugRouteTarget, { kind: "plugin" }>;
|
||||
type TopLevelSlugRouteTarget = {
|
||||
kind: "publisher";
|
||||
handle: string;
|
||||
publisher: PublicPublisherListItem;
|
||||
};
|
||||
|
||||
function normalizeSlug(slug: string) {
|
||||
return slug.trim().toLowerCase();
|
||||
@@ -49,10 +41,9 @@ export async function resolveOpenClawPluginSlug(
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function resolveTopLevelSlugRoute(slug: string): Promise<SlugRouteTarget | null> {
|
||||
const plugin = await resolveOpenClawPluginSlug(slug);
|
||||
if (plugin) return plugin;
|
||||
|
||||
export async function resolveTopLevelSlugRoute(
|
||||
slug: string,
|
||||
): Promise<TopLevelSlugRouteTarget | null> {
|
||||
const publisher = await resolvePublisherHandle(slug);
|
||||
if (publisher) {
|
||||
return {
|
||||
@@ -62,16 +53,7 @@ export async function resolveTopLevelSlugRoute(slug: string): Promise<SlugRouteT
|
||||
};
|
||||
}
|
||||
|
||||
const data = await fetchSkillPageData(slug);
|
||||
const owner = data.initialData?.result?.owner?.handle ?? data.owner;
|
||||
const resolvedSlug = data.initialData?.result?.resolvedSlug ?? slug;
|
||||
if (!owner || !data.initialData?.result?.skill) return null;
|
||||
|
||||
return {
|
||||
kind: "skill",
|
||||
owner,
|
||||
slug: resolvedSlug,
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
async function resolvePublisherHandle(handle: string) {
|
||||
|
||||
+4
-19
@@ -1,4 +1,4 @@
|
||||
import { createFileRoute, notFound, redirect } from "@tanstack/react-router";
|
||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||
import { buildPublisherMeta } from "../lib/og";
|
||||
import { resolveTopLevelSlugRoute } from "../lib/slugRoute";
|
||||
import { PublisherProfilePage } from "./user/$handle";
|
||||
@@ -8,24 +8,9 @@ export const Route = createFileRoute("/$slug")({
|
||||
const target = await resolveTopLevelSlugRoute(params.slug);
|
||||
if (!target) throw notFound();
|
||||
|
||||
if (target.kind === "plugin") {
|
||||
throw redirect({
|
||||
href: target.href,
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (target.kind === "publisher") {
|
||||
return {
|
||||
publisher: target.publisher,
|
||||
};
|
||||
}
|
||||
|
||||
throw redirect({
|
||||
to: "/$owner/skills/$slug",
|
||||
params: { owner: target.owner, slug: target.slug },
|
||||
replace: true,
|
||||
});
|
||||
return {
|
||||
publisher: target.publisher,
|
||||
};
|
||||
},
|
||||
head: ({ params, loaderData }) => {
|
||||
if (!loaderData || !("publisher" in loaderData)) return {};
|
||||
|
||||
Reference in New Issue
Block a user