Skip to content

Commit 2c720f6

Browse files
carderneTrigger.dev RepoOps
authored andcommitted
fix(webapp): require private auth for chat snapshot uploads
Public session access tokens can no longer request upload URLs for durable chat snapshots. Private runtimes can still upload snapshots, and scoped public reads continue to support chat resumption. Mono-RevId: e7b10f9fc6749dca7a9fb4cc096c0941a294d8ff
1 parent ea0e89c commit 2c720f6

3 files changed

Lines changed: 73 additions & 1 deletion

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Prevent public session access tokens from authorizing durable chat snapshot uploads

‎apps/webapp/app/routes/api.v1.sessions.$sessionId.snapshot-url.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const ParamsSchema = z.object({
1616

1717
const routeConfig = {
1818
params: ParamsSchema,
19-
allowJWT: true,
2019
corsStrategy: "all" as const,
2120
findResource: async (
2221
params: z.infer<typeof ParamsSchema>,
@@ -44,6 +43,7 @@ const route = createActionApiRoute(
4443
{
4544
...routeConfig,
4645
method: "PUT",
46+
allowJWT: false,
4747
authorization: {
4848
action: "write",
4949
resource: (params, _, __, ___, session) => sessionResource(params.sessionId, session),
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { generateJWT } from "@trigger.dev/core/v3/jwt";
2+
import type { TestServer } from "@internal/testcontainers/webapp";
3+
import { startTestServer } from "@internal/testcontainers/webapp";
4+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
5+
import { seedTestApiSession } from "./helpers/seedTestApiSession";
6+
import { seedTestEnvironment } from "./helpers/seedTestEnvironment";
7+
8+
vi.setConfig({ testTimeout: 180_000, hookTimeout: 240_000 });
9+
10+
let server: TestServer;
11+
12+
beforeAll(async () => {
13+
server = await startTestServer({
14+
extraEnv: {
15+
OBJECT_STORE_BASE_URL: "http://object-store.example.test",
16+
OBJECT_STORE_ACCESS_KEY_ID: "test-access-key",
17+
OBJECT_STORE_SECRET_ACCESS_KEY: "test-secret-key",
18+
},
19+
});
20+
}, 240_000);
21+
22+
afterAll(async () => {
23+
await server?.stop();
24+
}, 120_000);
25+
26+
async function setupSession() {
27+
const seed = await seedTestEnvironment(server.prisma);
28+
const session = await seedTestApiSession(server.prisma, seed.environment);
29+
const publicToken = await generateJWT({
30+
secretKey: seed.apiKey,
31+
payload: {
32+
pub: true,
33+
sub: seed.environment.id,
34+
scopes: [`read:sessions:${session.friendlyId}`, `write:sessions:${session.friendlyId}`],
35+
},
36+
expirationTime: "15m",
37+
});
38+
39+
return { ...seed, session, publicToken };
40+
}
41+
42+
function requestSnapshotUrl(sessionId: string, method: "GET" | "PUT", token: string) {
43+
return server.webapp.fetch(`/api/v1/sessions/${sessionId}/snapshot-url`, {
44+
method,
45+
headers: { Authorization: `Bearer ${token}` },
46+
});
47+
}
48+
49+
describe("session snapshot URL auth", () => {
50+
it("denies a presigned PUT to a public session token", async () => {
51+
const { session, publicToken } = await setupSession();
52+
53+
const response = await requestSnapshotUrl(session.friendlyId, "PUT", publicToken);
54+
55+
expect(response.status).toBe(401);
56+
});
57+
58+
it("returns a presigned PUT to the private runtime API key", async () => {
59+
const { apiKey, session } = await setupSession();
60+
61+
const response = await requestSnapshotUrl(session.friendlyId, "PUT", apiKey);
62+
63+
expect(response.status).toBe(200);
64+
await expect(response.json()).resolves.toEqual({ presignedUrl: expect.any(String) });
65+
});
66+
});

0 commit comments

Comments
 (0)