mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix(web): canonicalize scoped plugin paths
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const redirectMock = vi.fn((options: unknown) => ({ redirect: options }));
|
||||
|
||||
vi.mock("@tanstack/react-router", () => ({
|
||||
createFileRoute: () => (config: unknown) => ({ __config: config }),
|
||||
notFound: () => ({ notFound: true }),
|
||||
redirect: (options: unknown) => redirectMock(options),
|
||||
}));
|
||||
|
||||
type RedirectRoute = {
|
||||
__config: {
|
||||
beforeLoad: (args: { location: { pathname: string }; params: Record<string, string> }) => never;
|
||||
};
|
||||
};
|
||||
|
||||
async function loadRoute(path: string): Promise<RedirectRoute> {
|
||||
return ((await import(path)) as { Route: RedirectRoute }).Route;
|
||||
}
|
||||
|
||||
describe("scoped plugin route redirects", () => {
|
||||
beforeEach(() => {
|
||||
redirectMock.mockClear();
|
||||
});
|
||||
|
||||
it("canonicalizes raw scoped plugin detail paths", async () => {
|
||||
const route = await loadRoute("../routes/plugins/$scope/$name");
|
||||
|
||||
expect(() =>
|
||||
route.__config.beforeLoad({
|
||||
location: { pathname: "/plugins/@clawkit/clawkit-creative-studio" },
|
||||
params: { scope: "@clawkit", name: "clawkit-creative-studio" },
|
||||
}),
|
||||
).toThrow();
|
||||
expect(redirectMock).toHaveBeenCalledWith({
|
||||
href: "/plugins/%40clawkit%2Fclawkit-creative-studio",
|
||||
statusCode: 308,
|
||||
});
|
||||
});
|
||||
|
||||
it("canonicalizes raw scoped plugin security paths", async () => {
|
||||
const route = await loadRoute("../routes/plugins/$scope/$name/security/$scanner");
|
||||
|
||||
expect(() =>
|
||||
route.__config.beforeLoad({
|
||||
location: { pathname: "/plugins/@clawkit/clawkit-creative-studio/security/virustotal" },
|
||||
params: { scope: "@clawkit", name: "clawkit-creative-studio", scanner: "virustotal" },
|
||||
}),
|
||||
).toThrow();
|
||||
expect(redirectMock).toHaveBeenCalledWith({
|
||||
href: "/plugins/%40clawkit%2Fclawkit-creative-studio/security/virustotal",
|
||||
statusCode: 308,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves nested security paths through the scoped plugin parent", async () => {
|
||||
const route = await loadRoute("../routes/plugins/$scope/$name");
|
||||
|
||||
expect(() =>
|
||||
route.__config.beforeLoad({
|
||||
location: { pathname: "/plugins/@clawkit/clawkit-creative-studio/security/virustotal" },
|
||||
params: { scope: "@clawkit", name: "clawkit-creative-studio" },
|
||||
}),
|
||||
).toThrow();
|
||||
expect(redirectMock).toHaveBeenCalledWith({
|
||||
href: "/plugins/%40clawkit%2Fclawkit-creative-studio/security/virustotal",
|
||||
statusCode: 308,
|
||||
});
|
||||
});
|
||||
|
||||
it("canonicalizes raw scoped legacy package paths", async () => {
|
||||
const route = await loadRoute("../routes/packages/$scope/$name");
|
||||
|
||||
expect(() =>
|
||||
route.__config.beforeLoad({
|
||||
location: { pathname: "/packages/@clawkit/clawkit-creative-studio" },
|
||||
params: { scope: "@clawkit", name: "clawkit-creative-studio" },
|
||||
}),
|
||||
).toThrow();
|
||||
expect(redirectMock).toHaveBeenCalledWith({
|
||||
href: "/plugins/%40clawkit%2Fclawkit-creative-studio",
|
||||
statusCode: 308,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -35,9 +35,12 @@ import { Route as PackagesNameRouteImport } from './routes/packages/$name'
|
||||
import { Route as OrgsHandleRouteImport } from './routes/orgs/$handle'
|
||||
import { Route as CliAuthRouteImport } from './routes/cli/auth'
|
||||
import { Route as OwnerSlugRouteImport } from './routes/$owner/$slug'
|
||||
import { Route as PluginsScopeNameRouteImport } from './routes/plugins/$scope/$name'
|
||||
import { Route as PackagesScopeNameRouteImport } from './routes/packages/$scope/$name'
|
||||
import { Route as OwnerSlugSettingsRouteImport } from './routes/$owner/$slug/settings'
|
||||
import { Route as PluginsNameSecurityScannerRouteImport } from './routes/plugins/$name/security/$scanner'
|
||||
import { Route as OwnerSlugSecurityScannerRouteImport } from './routes/$owner/$slug/security/$scanner'
|
||||
import { Route as PluginsScopeNameSecurityScannerRouteImport } from './routes/plugins/$scope/$name/security/$scanner'
|
||||
|
||||
const UploadRoute = UploadRouteImport.update({
|
||||
id: '/upload',
|
||||
@@ -169,6 +172,16 @@ const OwnerSlugRoute = OwnerSlugRouteImport.update({
|
||||
path: '/$owner/$slug',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const PluginsScopeNameRoute = PluginsScopeNameRouteImport.update({
|
||||
id: '/plugins/$scope/$name',
|
||||
path: '/plugins/$scope/$name',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const PackagesScopeNameRoute = PackagesScopeNameRouteImport.update({
|
||||
id: '/packages/$scope/$name',
|
||||
path: '/packages/$scope/$name',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const OwnerSlugSettingsRoute = OwnerSlugSettingsRouteImport.update({
|
||||
id: '/settings',
|
||||
path: '/settings',
|
||||
@@ -186,6 +199,12 @@ const OwnerSlugSecurityScannerRoute =
|
||||
path: '/security/$scanner',
|
||||
getParentRoute: () => OwnerSlugRoute,
|
||||
} as any)
|
||||
const PluginsScopeNameSecurityScannerRoute =
|
||||
PluginsScopeNameSecurityScannerRouteImport.update({
|
||||
id: '/security/$scanner',
|
||||
path: '/security/$scanner',
|
||||
getParentRoute: () => PluginsScopeNameRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
@@ -215,8 +234,11 @@ export interface FileRoutesByFullPath {
|
||||
'/souls/': typeof SoulsIndexRoute
|
||||
'/users/': typeof UsersIndexRoute
|
||||
'/$owner/$slug/settings': typeof OwnerSlugSettingsRoute
|
||||
'/packages/$scope/$name': typeof PackagesScopeNameRoute
|
||||
'/plugins/$scope/$name': typeof PluginsScopeNameRouteWithChildren
|
||||
'/$owner/$slug/security/$scanner': typeof OwnerSlugSecurityScannerRoute
|
||||
'/plugins/$name/security/$scanner': typeof PluginsNameSecurityScannerRoute
|
||||
'/plugins/$scope/$name/security/$scanner': typeof PluginsScopeNameSecurityScannerRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
@@ -246,8 +268,11 @@ export interface FileRoutesByTo {
|
||||
'/souls': typeof SoulsIndexRoute
|
||||
'/users': typeof UsersIndexRoute
|
||||
'/$owner/$slug/settings': typeof OwnerSlugSettingsRoute
|
||||
'/packages/$scope/$name': typeof PackagesScopeNameRoute
|
||||
'/plugins/$scope/$name': typeof PluginsScopeNameRouteWithChildren
|
||||
'/$owner/$slug/security/$scanner': typeof OwnerSlugSecurityScannerRoute
|
||||
'/plugins/$name/security/$scanner': typeof PluginsNameSecurityScannerRoute
|
||||
'/plugins/$scope/$name/security/$scanner': typeof PluginsScopeNameSecurityScannerRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
@@ -278,8 +303,11 @@ export interface FileRoutesById {
|
||||
'/souls/': typeof SoulsIndexRoute
|
||||
'/users/': typeof UsersIndexRoute
|
||||
'/$owner/$slug/settings': typeof OwnerSlugSettingsRoute
|
||||
'/packages/$scope/$name': typeof PackagesScopeNameRoute
|
||||
'/plugins/$scope/$name': typeof PluginsScopeNameRouteWithChildren
|
||||
'/$owner/$slug/security/$scanner': typeof OwnerSlugSecurityScannerRoute
|
||||
'/plugins/$name/security/$scanner': typeof PluginsNameSecurityScannerRoute
|
||||
'/plugins/$scope/$name/security/$scanner': typeof PluginsScopeNameSecurityScannerRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
@@ -311,8 +339,11 @@ export interface FileRouteTypes {
|
||||
| '/souls/'
|
||||
| '/users/'
|
||||
| '/$owner/$slug/settings'
|
||||
| '/packages/$scope/$name'
|
||||
| '/plugins/$scope/$name'
|
||||
| '/$owner/$slug/security/$scanner'
|
||||
| '/plugins/$name/security/$scanner'
|
||||
| '/plugins/$scope/$name/security/$scanner'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
@@ -342,8 +373,11 @@ export interface FileRouteTypes {
|
||||
| '/souls'
|
||||
| '/users'
|
||||
| '/$owner/$slug/settings'
|
||||
| '/packages/$scope/$name'
|
||||
| '/plugins/$scope/$name'
|
||||
| '/$owner/$slug/security/$scanner'
|
||||
| '/plugins/$name/security/$scanner'
|
||||
| '/plugins/$scope/$name/security/$scanner'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
@@ -373,8 +407,11 @@ export interface FileRouteTypes {
|
||||
| '/souls/'
|
||||
| '/users/'
|
||||
| '/$owner/$slug/settings'
|
||||
| '/packages/$scope/$name'
|
||||
| '/plugins/$scope/$name'
|
||||
| '/$owner/$slug/security/$scanner'
|
||||
| '/plugins/$name/security/$scanner'
|
||||
| '/plugins/$scope/$name/security/$scanner'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
@@ -404,6 +441,8 @@ export interface RootRouteChildren {
|
||||
SkillsIndexRoute: typeof SkillsIndexRoute
|
||||
SoulsIndexRoute: typeof SoulsIndexRoute
|
||||
UsersIndexRoute: typeof UsersIndexRoute
|
||||
PackagesScopeNameRoute: typeof PackagesScopeNameRoute
|
||||
PluginsScopeNameRoute: typeof PluginsScopeNameRouteWithChildren
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@@ -590,6 +629,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof OwnerSlugRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/plugins/$scope/$name': {
|
||||
id: '/plugins/$scope/$name'
|
||||
path: '/plugins/$scope/$name'
|
||||
fullPath: '/plugins/$scope/$name'
|
||||
preLoaderRoute: typeof PluginsScopeNameRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/packages/$scope/$name': {
|
||||
id: '/packages/$scope/$name'
|
||||
path: '/packages/$scope/$name'
|
||||
fullPath: '/packages/$scope/$name'
|
||||
preLoaderRoute: typeof PackagesScopeNameRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/$owner/$slug/settings': {
|
||||
id: '/$owner/$slug/settings'
|
||||
path: '/settings'
|
||||
@@ -611,6 +664,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof OwnerSlugSecurityScannerRouteImport
|
||||
parentRoute: typeof OwnerSlugRoute
|
||||
}
|
||||
'/plugins/$scope/$name/security/$scanner': {
|
||||
id: '/plugins/$scope/$name/security/$scanner'
|
||||
path: '/security/$scanner'
|
||||
fullPath: '/plugins/$scope/$name/security/$scanner'
|
||||
preLoaderRoute: typeof PluginsScopeNameSecurityScannerRouteImport
|
||||
parentRoute: typeof PluginsScopeNameRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,6 +700,17 @@ const PluginsNameRouteWithChildren = PluginsNameRoute._addFileChildren(
|
||||
PluginsNameRouteChildren,
|
||||
)
|
||||
|
||||
interface PluginsScopeNameRouteChildren {
|
||||
PluginsScopeNameSecurityScannerRoute: typeof PluginsScopeNameSecurityScannerRoute
|
||||
}
|
||||
|
||||
const PluginsScopeNameRouteChildren: PluginsScopeNameRouteChildren = {
|
||||
PluginsScopeNameSecurityScannerRoute: PluginsScopeNameSecurityScannerRoute,
|
||||
}
|
||||
|
||||
const PluginsScopeNameRouteWithChildren =
|
||||
PluginsScopeNameRoute._addFileChildren(PluginsScopeNameRouteChildren)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AboutRoute: AboutRoute,
|
||||
@@ -667,6 +738,8 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
SkillsIndexRoute: SkillsIndexRoute,
|
||||
SoulsIndexRoute: SoulsIndexRoute,
|
||||
UsersIndexRoute: UsersIndexRoute,
|
||||
PackagesScopeNameRoute: PackagesScopeNameRoute,
|
||||
PluginsScopeNameRoute: PluginsScopeNameRouteWithChildren,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createFileRoute, notFound, redirect } from "@tanstack/react-router";
|
||||
|
||||
function scopedPluginPath(scope: string, name: string) {
|
||||
if (!scope.startsWith("@") || !name) {
|
||||
throw notFound();
|
||||
}
|
||||
return `/plugins/${encodeURIComponent(`${scope}/${name}`)}`;
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/packages/$scope/$name")({
|
||||
beforeLoad: ({ params }) => {
|
||||
throw redirect({
|
||||
href: scopedPluginPath(params.scope, params.name),
|
||||
statusCode: 308,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createFileRoute, notFound, redirect } from "@tanstack/react-router";
|
||||
|
||||
function scopedPluginPath(scope: string, name: string, suffix = "") {
|
||||
if (!scope.startsWith("@") || !name) {
|
||||
throw notFound();
|
||||
}
|
||||
return `/plugins/${encodeURIComponent(`${scope}/${name}`)}${suffix}`;
|
||||
}
|
||||
|
||||
function scopedPluginSuffix(pathname: string, scope: string, name: string) {
|
||||
const prefix = `/plugins/${scope}/${name}`;
|
||||
if (!pathname.startsWith(`${prefix}/`)) {
|
||||
return "";
|
||||
}
|
||||
return pathname.slice(prefix.length);
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/plugins/$scope/$name")({
|
||||
beforeLoad: ({ location, params }) => {
|
||||
throw redirect({
|
||||
href: scopedPluginPath(
|
||||
params.scope,
|
||||
params.name,
|
||||
scopedPluginSuffix(location.pathname, params.scope, params.name),
|
||||
),
|
||||
statusCode: 308,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createFileRoute, notFound, redirect } from "@tanstack/react-router";
|
||||
|
||||
function scopedPluginSecurityPath(scope: string, name: string, scanner: string) {
|
||||
if (!scope.startsWith("@") || !name) {
|
||||
throw notFound();
|
||||
}
|
||||
return `/plugins/${encodeURIComponent(`${scope}/${name}`)}/security/${encodeURIComponent(scanner)}`;
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/plugins/$scope/$name/security/$scanner")({
|
||||
beforeLoad: ({ params }) => {
|
||||
throw redirect({
|
||||
href: scopedPluginSecurityPath(params.scope, params.name, params.scanner),
|
||||
statusCode: 308,
|
||||
});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user