Skip to content

Commit 907038d

Browse files
committed
fix(webapp): require a scope on the project runtimes query
listCurrentProductionProjectRuntimes took organizationId and userId as optional and spread each filter conditionally, so a call passing neither would have narrowed to `version: "V3", deletedAt: null` and returned every V3 project on the instance. The scope is now a required union of exactly one of the two, so a scopeless call cannot typecheck, and the runtime-updates loader throws a 404 on a missing organizationId instead of relying on the route builder's fail-closed scope check.
1 parent 344710d commit 907038d

2 files changed

Lines changed: 20 additions & 15 deletions

File tree

apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.runtime-updates/route.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const loader = dashboardLoader(
1414
{
1515
params: OrganizationParamsSchema,
1616
// Membership-scoped resolve, like the Team settings loader: the RBAC gate below enforces the
17-
// role, this is the tenant floor. An unresolved org yields no scope, which fails closed.
17+
// role, this is the tenant floor. An unresolved org yields no scope, which the loader rejects.
1818
context: async (params, request) => {
1919
const userId = await getUserId(request);
2020
if (!userId) return {};
@@ -28,9 +28,12 @@ export const loader = dashboardLoader(
2828
},
2929
},
3030
async ({ context, params }) => {
31-
const runtimes = await listCurrentProductionProjectRuntimes({
32-
organizationId: context.organizationId,
33-
});
31+
const organizationId = context.organizationId;
32+
if (!organizationId) {
33+
throw new Response("Not Found", { status: 404 });
34+
}
35+
36+
const runtimes = await listCurrentProductionProjectRuntimes({ organizationId });
3437

3538
const needsUpdate: ProjectRuntimeRow[] = [];
3639
const otherProjects: ProjectRuntimeRow[] = [];

apps/webapp/app/services/projectRuntimeUpdates.server.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,25 @@ import { NODE_RUNTIME_UPDATE_MAJOR, nodeMajor } from "@trigger.dev/core/v3";
22
import { CURRENT_DEPLOYMENT_LABEL } from "@trigger.dev/core/v3/isomorphic";
33
import { prisma } from "~/db.server";
44

5-
type Options = {
6-
organizationId?: string;
7-
userId?: string;
8-
};
5+
/**
6+
* The scope is required and exactly one of the two applies: without it the `where` below would
7+
* collapse to every V3 project on the instance, so a scopeless call must not typecheck.
8+
*/
9+
type Scope =
10+
| { organizationId: string; userId?: never }
11+
| { userId: string; organizationId?: never };
912

10-
export async function listCurrentProductionProjectRuntimes({ organizationId, userId }: Options) {
13+
export async function listCurrentProductionProjectRuntimes(scope: Scope) {
1114
const projects = await prisma.project.findMany({
1215
where: {
13-
...(organizationId ? { organizationId } : {}),
14-
...(userId
15-
? {
16+
...(scope.organizationId !== undefined
17+
? { organizationId: scope.organizationId }
18+
: {
1619
organization: {
1720
deletedAt: null,
18-
members: { some: { userId } },
21+
members: { some: { userId: scope.userId } },
1922
},
20-
}
21-
: {}),
23+
}),
2224
version: "V3",
2325
deletedAt: null,
2426
},

0 commit comments

Comments
 (0)