Skip to content

Commit 2210fe2

Browse files
committed
fix(webapp): tighten sessions create + list auth
- Derive isCached from the upsert result (id mismatch = pre-existing row) instead of doing a separate findFirst first. The pre-check was racy — two concurrent first-time POSTs could both return 201 with isCached: false. Using the returned row's id is atomic and saves a round-trip. - Scope the list endpoint's authorization to the standard action/resource pattern (matches api.v1.runs.ts): task-scoped JWTs can list sessions filtered by their task, and broader super-scopes (read:sessions, read:all, admin) authorize unfiltered listing. - Log and swallow unexpected errors on POST rather than returning the raw error.message. Prisma/internal messages can leak column names and query fragments.
1 parent ff46f33 commit 2210fe2

File tree

1 file changed

+11
-12
lines changed

1 file changed

+11
-12
lines changed

apps/webapp/app/routes/api.v1.sessions.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SessionId } from "@trigger.dev/core/v3/isomorphic";
1010
import type { Prisma, Session } from "@trigger.dev/database";
1111
import { $replica, prisma, type PrismaClient } from "~/db.server";
1212
import { clickhouseClient } from "~/services/clickhouseInstance.server";
13+
import { logger } from "~/services/logger.server";
1314
import { serializeSession } from "~/services/realtime/sessions.server";
1415
import { SessionsRepository } from "~/services/sessionsRepository/sessionsRepository.server";
1516
import {
@@ -28,6 +29,11 @@ export const loader = createLoaderApiRoute(
2829
searchParams: ListSessionsQueryParams,
2930
allowJWT: true,
3031
corsStrategy: "all",
32+
authorization: {
33+
action: "read",
34+
resource: (_, __, searchParams) => ({ tasks: searchParams["filter[taskIdentifier]"] }),
35+
superScopes: ["read:sessions", "read:all", "admin"],
36+
},
3137
findResource: async () => 1,
3238
},
3339
async ({ searchParams, authentication }) => {
@@ -93,17 +99,11 @@ const { action } = createActionApiRoute(
9399
if (body.externalId) {
94100
// Atomic upsert — two concurrent POSTs with the same externalId both
95101
// converge to the same row without either hitting a 500 from the
96-
// unique constraint.
102+
// unique constraint. Derive isCached from the upsert result: if the
103+
// row pre-existed, the returned id won't match the one we just
104+
// generated. Saves a round-trip and is race-free.
97105
const { id, friendlyId } = SessionId.generate();
98106
const externalId = body.externalId;
99-
const pre = await prisma.session.findFirst({
100-
where: {
101-
runtimeEnvironmentId: authentication.environment.id,
102-
externalId,
103-
},
104-
select: { id: true },
105-
});
106-
isCached = pre !== null;
107107

108108
session = await prisma.session.upsert({
109109
where: {
@@ -128,6 +128,7 @@ const { action } = createActionApiRoute(
128128
},
129129
update: {},
130130
});
131+
isCached = session.id !== id;
131132
} else {
132133
const { id, friendlyId } = SessionId.generate();
133134
session = await prisma.session.create({
@@ -155,9 +156,7 @@ const { action } = createActionApiRoute(
155156
if (error instanceof ServiceValidationError) {
156157
return json({ error: error.message }, { status: 422 });
157158
}
158-
if (error instanceof Error) {
159-
return json({ error: error.message }, { status: 500 });
160-
}
159+
logger.error("Failed to create session", { error });
161160
return json({ error: "Something went wrong" }, { status: 500 });
162161
}
163162
}

0 commit comments

Comments
 (0)