From 4e8bec34ea4764b981a3176caac9d1f4decd2506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Wed, 17 Dec 2025 12:59:20 +0200 Subject: [PATCH 1/3] Disable package endpoints that are not community-scoped Content moderators currently reject packages by marking the PackageListing object rejected. PackageListings are community-scoped. Some of the endpoints are package scoped, i.e. they are not related to any communities or listings. These endpoints therefore currently have no way of telling if a package has been rejected or not. In worst case, the rejected packages can contain malware, so under no circumstances do we want to show such packages on the website where unsuspecting users can be tricked to download the mods. Therefore the pages of the website that use such endpoints are disabled until we figure out how to address the problem. Since the old website doesn't have unscoped endpoints, from feature parity perspective the new one doesn't need to have them either. Footgun warning: the website still uses some endpoints that are not community-scoped, such as "get README for this version of the package". This is fine as long as the page where the data is shown takes measures to ensure the content can be shown by querying the package listing endpoint too and showing 404 error if the package is rejected. --- apps/cyberstorm-remix/app/routes.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/cyberstorm-remix/app/routes.ts b/apps/cyberstorm-remix/app/routes.ts index ca19895c9..508412a28 100644 --- a/apps/cyberstorm-remix/app/routes.ts +++ b/apps/cyberstorm-remix/app/routes.ts @@ -48,6 +48,11 @@ export default [ route(":namespaceId/:packageId/edit", "p/packageEdit.tsx"), ]), ]), + /** + * Disabled until we figure out how to ensure the community-agnostic + * endpoints won't serve rejected packages (rejections are listing-, + * and thus community-specific). + route( "/p/:namespaceId/:packageId/v/:packageVersion", "p/packageVersionWithoutCommunity.tsx", @@ -66,6 +71,7 @@ export default [ ), ] ), + */ route( "/c/:communityId/p/:namespaceId/:packageId/dependants", "p/dependants/Dependants.tsx" From f0afdce75028dae147a345cdfb0c41973eb11152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Thu, 18 Dec 2025 17:35:17 +0200 Subject: [PATCH 2/3] Combine the three components used to list package's dependencies - The only difference between the components was that one expected the route to contain communityId which is used to fetch the latest version number, while the two other expected packageVersion to be included in the URL. Loaders can easily check the route params and serve both cases - We may want to do something similar to readme and versions components that also exist in triplicate - The part of the code that fetches the package listing details when packageVersion is not provided by the URL now uses the same approach as the PackageListing component does. This fixes an issue where the tab would show 404 when viewing a rejected package's dependencies, even if the user was authententicated and had permissions to view it - React-router uses the component name as the default id, but since we have multiple routes for the same component, we need to define ids manually --- .../tabs/Required/PackageVersionRequired.tsx | 89 ---------- ...PackageVersionWithoutCommunityRequired.tsx | 89 ---------- .../app/p/tabs/Required/Required.tsx | 161 +++++++++++------- apps/cyberstorm-remix/app/routes.ts | 11 +- 4 files changed, 102 insertions(+), 248 deletions(-) delete mode 100644 apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionRequired.tsx delete mode 100644 apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionWithoutCommunityRequired.tsx diff --git a/apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionRequired.tsx b/apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionRequired.tsx deleted file mode 100644 index bd8d99c21..000000000 --- a/apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionRequired.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Suspense } from "react"; -import { type LoaderFunctionArgs } from "react-router"; -import { useLoaderData, Await } from "react-router"; -import { DapperTs } from "@thunderstore/dapper-ts"; -import { SkeletonBox } from "@thunderstore/cyberstorm"; -import { PaginatedDependencies } from "~/commonComponents/PaginatedDependencies/PaginatedDependencies"; -import { - getPublicEnvVariables, - getSessionTools, -} from "cyberstorm/security/publicEnvVariables"; - -export async function loader({ params, request }: LoaderFunctionArgs) { - if (params.namespaceId && params.packageId && params.packageVersion) { - const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); - const dapper = new DapperTs(() => { - return { - apiHost: publicEnvVariables.VITE_API_URL, - sessionId: undefined, - }; - }); - const searchParams = new URL(request.url).searchParams; - const page = searchParams.get("page"); - - return { - version: await dapper.getPackageVersionDetails( - params.namespaceId, - params.packageId, - params.packageVersion - ), - dependencies: await dapper.getPackageVersionDependencies( - params.namespaceId, - params.packageId, - params.packageVersion, - page === null ? undefined : Number(page) - ), - }; - } - throw new Response("Package version dependencies not found", { status: 404 }); -} - -export async function clientLoader({ params, request }: LoaderFunctionArgs) { - if (params.namespaceId && params.packageId && params.packageVersion) { - const tools = getSessionTools(); - const dapper = new DapperTs(() => { - return { - apiHost: tools?.getConfig().apiHost, - sessionId: tools?.getConfig().sessionId, - }; - }); - const searchParams = new URL(request.url).searchParams; - const page = searchParams.get("page"); - - return { - version: dapper.getPackageVersionDetails( - params.namespaceId, - params.packageId, - params.packageVersion - ), - dependencies: dapper.getPackageVersionDependencies( - params.namespaceId, - params.packageId, - params.packageVersion, - page === null ? undefined : Number(page) - ), - }; - } - throw new Response("Package version dependencies not found", { status: 404 }); -} - -export default function PackageVersionRequired() { - const { dependencies } = useLoaderData(); - - return ( - } - > - Error occurred while loading required dependencies - } - > - {(resolvedDependencies) => ( - - )} - - - ); -} diff --git a/apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionWithoutCommunityRequired.tsx b/apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionWithoutCommunityRequired.tsx deleted file mode 100644 index af6cdd0ac..000000000 --- a/apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionWithoutCommunityRequired.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Suspense } from "react"; -import { type LoaderFunctionArgs } from "react-router"; -import { useLoaderData, Await } from "react-router"; -import { DapperTs } from "@thunderstore/dapper-ts"; -import { SkeletonBox } from "@thunderstore/cyberstorm"; -import { PaginatedDependencies } from "~/commonComponents/PaginatedDependencies/PaginatedDependencies"; -import { - getPublicEnvVariables, - getSessionTools, -} from "cyberstorm/security/publicEnvVariables"; - -export async function loader({ params, request }: LoaderFunctionArgs) { - if (params.namespaceId && params.packageId && params.packageVersion) { - const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); - const dapper = new DapperTs(() => { - return { - apiHost: publicEnvVariables.VITE_API_URL, - sessionId: undefined, - }; - }); - const searchParams = new URL(request.url).searchParams; - const page = searchParams.get("page"); - - return { - version: await dapper.getPackageVersionDetails( - params.namespaceId, - params.packageId, - params.packageVersion - ), - dependencies: await dapper.getPackageVersionDependencies( - params.namespaceId, - params.packageId, - params.packageVersion, - page === null ? undefined : Number(page) - ), - }; - } - throw new Response("Package version dependencies not found", { status: 404 }); -} - -export async function clientLoader({ params, request }: LoaderFunctionArgs) { - if (params.namespaceId && params.packageId && params.packageVersion) { - const tools = getSessionTools(); - const dapper = new DapperTs(() => { - return { - apiHost: tools?.getConfig().apiHost, - sessionId: tools?.getConfig().sessionId, - }; - }); - const searchParams = new URL(request.url).searchParams; - const page = searchParams.get("page"); - - return { - version: dapper.getPackageVersionDetails( - params.namespaceId, - params.packageId, - params.packageVersion - ), - dependencies: dapper.getPackageVersionDependencies( - params.namespaceId, - params.packageId, - params.packageVersion, - page === null ? undefined : Number(page) - ), - }; - } - throw new Response("Package version dependencies not found", { status: 404 }); -} - -export default function PackageVersionWithoutCommunityRequired() { - const { dependencies } = useLoaderData(); - - return ( - } - > - Error occurred while loading required dependencies - } - > - {(resolvedDependencies) => ( - - )} - - - ); -} diff --git a/apps/cyberstorm-remix/app/p/tabs/Required/Required.tsx b/apps/cyberstorm-remix/app/p/tabs/Required/Required.tsx index 64d76108c..09dec6cc0 100644 --- a/apps/cyberstorm-remix/app/p/tabs/Required/Required.tsx +++ b/apps/cyberstorm-remix/app/p/tabs/Required/Required.tsx @@ -1,84 +1,115 @@ import { Suspense } from "react"; -import { type LoaderFunctionArgs } from "react-router"; -import { useLoaderData, Await } from "react-router"; -import { DapperTs } from "@thunderstore/dapper-ts"; +import { Await, type LoaderFunctionArgs, useLoaderData } from "react-router"; + import { SkeletonBox } from "@thunderstore/cyberstorm"; -import { PaginatedDependencies } from "~/commonComponents/PaginatedDependencies/PaginatedDependencies"; +import { DapperTs } from "@thunderstore/dapper-ts"; + +import { PaginatedDependencies } from "app/commonComponents/PaginatedDependencies/PaginatedDependencies"; +import { getPrivateListing, getPublicListing } from "app/p/listingUtils"; import { getPublicEnvVariables, getSessionTools, } from "cyberstorm/security/publicEnvVariables"; +const Dependency404 = new Response("Package dependencies not found", { + status: 404, +}); + +const getPageFromUrl = (url: string): number | undefined => { + const searchParams = new URL(url).searchParams; + const maybePage = searchParams.get("page"); + return maybePage ? Number(maybePage) : undefined; +}; + export async function loader({ params, request }: LoaderFunctionArgs) { - if (params.communityId && params.namespaceId && params.packageId) { - const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); - const dapper = new DapperTs(() => { - return { - apiHost: publicEnvVariables.VITE_API_URL, - sessionId: undefined, - }; - }); - const searchParams = new URL(request.url).searchParams; - const page = searchParams.get("page"); - const listing = await dapper.getPackageListingDetails( - params.communityId, - params.namespaceId, - params.packageId - ); - - return { - version: await dapper.getPackageVersionDetails( - params.namespaceId, - params.packageId, - listing.latest_version_number - ), - dependencies: await dapper.getPackageVersionDependencies( - params.namespaceId, - params.packageId, - listing.latest_version_number, - page === null ? undefined : Number(page) - ), - }; + const { communityId, namespaceId, packageId, packageVersion } = params; + + // Either communityId or packageVersion is required depending on route. + if (!namespaceId || !packageId || (!communityId && !packageVersion)) { + throw Dependency404; } - throw new Response("Package version dependencies not found", { status: 404 }); + + const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); + const dapper = new DapperTs(() => ({ + apiHost: publicEnvVariables.VITE_API_URL, + sessionId: undefined, + })); + + let version: string; + + if (packageVersion) { + version = packageVersion; + } else if (communityId) { + const listingArgs = { communityId, namespaceId, packageId }; + const listing = await getPublicListing(dapper, listingArgs); + + // Listing that's not available on unauthenticated SSR request + // might be available for authenticated user on client. Return + // undefined rather than throw error to allow refetch on client. + if (!listing) { + return { dependencies: undefined }; + } + + version = listing.latest_version_number; + } else { + throw Dependency404; // Can't happen, satisfies TypeScript + } + + return { + dependencies: await dapper.getPackageVersionDependencies( + namespaceId, + packageId, + version, + getPageFromUrl(request.url) + ), + }; } export async function clientLoader({ params, request }: LoaderFunctionArgs) { - if (params.communityId && params.namespaceId && params.packageId) { - const tools = getSessionTools(); - const dapper = new DapperTs(() => { - return { - apiHost: tools?.getConfig().apiHost, - sessionId: tools?.getConfig().sessionId, - }; - }); - const searchParams = new URL(request.url).searchParams; - const page = searchParams.get("page"); - const listing = await dapper.getPackageListingDetails( - params.communityId, - params.namespaceId, - params.packageId - ); - - return { - version: dapper.getPackageVersionDetails( - params.namespaceId, - params.packageId, - listing.latest_version_number - ), - dependencies: dapper.getPackageVersionDependencies( - params.namespaceId, - params.packageId, - listing.latest_version_number, - page === null ? undefined : Number(page) - ), - }; + const { communityId, namespaceId, packageId, packageVersion } = params; + + // Either communityId or packageVersion is required depending on route. + if (!namespaceId || !packageId || (!communityId && !packageVersion)) { + throw Dependency404; } - throw new Response("Package version dependencies not found", { status: 404 }); + + const tools = getSessionTools(); + const dapper = new DapperTs(() => ({ + apiHost: tools?.getConfig().apiHost, + sessionId: tools?.getConfig().sessionId, + })); + + let version: string; + + if (packageVersion) { + version = packageVersion; + } else if (communityId) { + const listingArgs = { communityId, namespaceId, packageId }; + const listing = await getPrivateListing(dapper, listingArgs); + version = listing.latest_version_number; + } else { + throw Dependency404; // Can't happen, satisfies TypeScript + } + + return { + dependencies: dapper.getPackageVersionDependencies( + namespaceId, + packageId, + version, + getPageFromUrl(request.url) + ), + }; } +clientLoader.hydrate = true; + export default function PackageVersionRequired() { - const { dependencies } = useLoaderData(); + const { dependencies } = useLoaderData(); + + // SSR failed to fetch, retry as authenticated user on client. + if (dependencies === undefined) { + return ; + } return ( Date: Thu, 18 Dec 2025 17:46:31 +0200 Subject: [PATCH 3/3] Reduce duplicate requests made in listing's Required tab - Parent route PackageListing and sub route Required both request package listing data from backend - Using the recently added juiced up Dapper instance, multiple request to same endpoint with same arguments withing a request are deduplicated - Technically this doesn't help in packageListingVersion. While it makes the request, its sub route doesn't since it has the required data available as URL parameter. I decided to change the parent route to use the deduplicating Dapper version anyway for consistency's sake --- apps/cyberstorm-remix/app/p/packageListing.tsx | 9 +++------ .../app/p/packageListingVersion.tsx | 15 ++++----------- .../app/p/tabs/Required/Required.tsx | 13 +++---------- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/apps/cyberstorm-remix/app/p/packageListing.tsx b/apps/cyberstorm-remix/app/p/packageListing.tsx index a28757665..e04e61be6 100644 --- a/apps/cyberstorm-remix/app/p/packageListing.tsx +++ b/apps/cyberstorm-remix/app/p/packageListing.tsx @@ -55,6 +55,7 @@ import { import { PackageLikeAction } from "@thunderstore/cyberstorm-forms"; import type { CurrentUser } from "@thunderstore/dapper/types"; import { DapperTs, type DapperTsInterface } from "@thunderstore/dapper-ts"; +import { getDapperForRequest } from "cyberstorm/utils/dapperSingleton"; import { getPublicListing, getPrivateListing } from "./listingUtils"; import { ManagementTools } from "./components/PackageListing/ManagementTools"; @@ -132,7 +133,7 @@ async function getPackageListingStatus( return undefined; } -export async function clientLoader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params, request }: LoaderFunctionArgs) { const { communityId, namespaceId, packageId } = params; if (!communityId || !namespaceId || !packageId) { @@ -140,11 +141,7 @@ export async function clientLoader({ params }: LoaderFunctionArgs) { } const tools = getSessionTools(); - const config = tools.getConfig(); - const dapper = new DapperTs(() => ({ - apiHost: config.apiHost, - sessionId: config.sessionId, - })); + const dapper = getDapperForRequest(request); const listing = await getPrivateListing(dapper, { communityId, diff --git a/apps/cyberstorm-remix/app/p/packageListingVersion.tsx b/apps/cyberstorm-remix/app/p/packageListingVersion.tsx index 26a070927..ef9418478 100644 --- a/apps/cyberstorm-remix/app/p/packageListingVersion.tsx +++ b/apps/cyberstorm-remix/app/p/packageListingVersion.tsx @@ -40,10 +40,8 @@ import { import { DapperTs } from "@thunderstore/dapper-ts"; import { type OutletContextShape } from "~/root"; import { CopyButton } from "~/commonComponents/CopyButton/CopyButton"; -import { - getPublicEnvVariables, - getSessionTools, -} from "cyberstorm/security/publicEnvVariables"; +import { getPublicEnvVariables } from "cyberstorm/security/publicEnvVariables"; +import { getDapperForRequest } from "cyberstorm/utils/dapperSingleton"; import { getPrivateListing, getPublicListing } from "./listingUtils"; import { type PackageListingDetails, @@ -77,19 +75,14 @@ export async function loader({ params }: LoaderFunctionArgs) { }; } -export async function clientLoader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params, request }: LoaderFunctionArgs) { const { communityId, namespaceId, packageId, packageVersion } = params; if (!communityId || !namespaceId || !packageId || !packageVersion) { throw new Response("Package not found", { status: 404 }); } - const tools = getSessionTools(); - const config = tools.getConfig(); - const dapper = new DapperTs(() => ({ - apiHost: config.apiHost, - sessionId: config.sessionId, - })); + const dapper = getDapperForRequest(request); const listing = await getPrivateListing(dapper, { communityId, diff --git a/apps/cyberstorm-remix/app/p/tabs/Required/Required.tsx b/apps/cyberstorm-remix/app/p/tabs/Required/Required.tsx index 09dec6cc0..d2fcace55 100644 --- a/apps/cyberstorm-remix/app/p/tabs/Required/Required.tsx +++ b/apps/cyberstorm-remix/app/p/tabs/Required/Required.tsx @@ -6,10 +6,8 @@ import { DapperTs } from "@thunderstore/dapper-ts"; import { PaginatedDependencies } from "app/commonComponents/PaginatedDependencies/PaginatedDependencies"; import { getPrivateListing, getPublicListing } from "app/p/listingUtils"; -import { - getPublicEnvVariables, - getSessionTools, -} from "cyberstorm/security/publicEnvVariables"; +import { getPublicEnvVariables } from "cyberstorm/security/publicEnvVariables"; +import { getDapperForRequest } from "cyberstorm/utils/dapperSingleton"; const Dependency404 = new Response("Package dependencies not found", { status: 404, @@ -73,12 +71,7 @@ export async function clientLoader({ params, request }: LoaderFunctionArgs) { throw Dependency404; } - const tools = getSessionTools(); - const dapper = new DapperTs(() => ({ - apiHost: tools?.getConfig().apiHost, - sessionId: tools?.getConfig().sessionId, - })); - + const dapper = getDapperForRequest(request); let version: string; if (packageVersion) {