Skip to content

Commit 17bcce6

Browse files
committed
Added a hash to the HTTP callback URLs
1 parent aad1278 commit 17bcce6

File tree

6 files changed

+58
-15
lines changed

6 files changed

+58
-15
lines changed

apps/webapp/app/presenters/v3/WaitpointListPresenter.server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { BasePresenter } from "./basePresenter.server";
1212
import { type WaitpointSearchParams } from "~/components/runs/v3/WaitpointTokenFilters";
1313
import { determineEngineVersion } from "~/v3/engineVersion.server";
1414
import { type WaitpointTokenStatus, type WaitpointTokenItem } from "@trigger.dev/core/v3";
15-
import { generateWaitpointCallbackUrl } from "./WaitpointPresenter.server";
15+
import { generateHttpCallbackUrl } from "~/services/httpCallback.server";
1616

1717
const DEFAULT_PAGE_SIZE = 25;
1818

@@ -24,6 +24,7 @@ export type WaitpointListOptions = {
2424
id: string;
2525
engine: RunEngineVersion;
2626
};
27+
apiKey: string;
2728
};
2829
resolver: WaitpointResolver;
2930
// filters
@@ -267,7 +268,7 @@ export class WaitpointListPresenter extends BasePresenter {
267268
success: true,
268269
tokens: tokensToReturn.map((token) => ({
269270
id: token.friendlyId,
270-
callbackUrl: generateWaitpointCallbackUrl(token.id),
271+
callbackUrl: generateHttpCallbackUrl(token.id, environment.apiKey),
271272
status: waitpointStatusToApiStatus(token.status, token.outputIsError),
272273
completedAt: token.completedAt ?? undefined,
273274
timeoutAt: token.completedAfter ?? undefined,

apps/webapp/app/presenters/v3/WaitpointPresenter.server.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { type RunListItem, RunListPresenter } from "./RunListPresenter.server";
55
import { waitpointStatusToApiStatus } from "./WaitpointListPresenter.server";
66
import { WaitpointId } from "@trigger.dev/core/v3/isomorphic";
77
import { env } from "~/env.server";
8+
import { generateHttpCallbackUrl } from "~/services/httpCallback.server";
89

910
export type WaitpointDetail = NonNullable<Awaited<ReturnType<WaitpointPresenter["call"]>>>;
1011

@@ -24,6 +25,7 @@ export class WaitpointPresenter extends BasePresenter {
2425
environmentId,
2526
},
2627
select: {
28+
id: true,
2729
friendlyId: true,
2830
type: true,
2931
status: true,
@@ -45,6 +47,11 @@ export class WaitpointPresenter extends BasePresenter {
4547
take: 5,
4648
},
4749
tags: true,
50+
environment: {
51+
select: {
52+
apiKey: true,
53+
},
54+
},
4855
},
4956
});
5057

@@ -89,7 +96,7 @@ export class WaitpointPresenter extends BasePresenter {
8996
resolver: waitpoint.resolver,
9097
callbackUrl:
9198
waitpoint.resolver === "HTTP_CALLBACK"
92-
? generateWaitpointCallbackUrl(waitpoint.friendlyId)
99+
? generateHttpCallbackUrl(waitpoint.id, waitpoint.environment.apiKey)
93100
: undefined,
94101
status: waitpointStatusToApiStatus(waitpoint.status, waitpoint.outputIsError),
95102
idempotencyKey: waitpoint.idempotencyKey,
@@ -108,9 +115,3 @@ export class WaitpointPresenter extends BasePresenter {
108115
};
109116
}
110117
}
111-
112-
export function generateWaitpointCallbackUrl(waitpointId: string) {
113-
return `${
114-
env.API_ORIGIN ?? env.APP_ORIGIN
115-
}/api/v1/waitpoints/http-callback/${WaitpointId.toFriendlyId(waitpointId)}/callback`;
116-
}

apps/webapp/app/routes/api.v1.waitpoints.http-callback.$waitpointFriendlyId.callback.ts renamed to apps/webapp/app/routes/api.v1.waitpoints.http-callback.$waitpointFriendlyId.callback.$hash.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import { WaitpointId } from "@trigger.dev/core/v3/isomorphic";
88
import { z } from "zod";
99
import { $replica } from "~/db.server";
1010
import { env } from "~/env.server";
11+
import { verifyHttpCallbackHash } from "~/services/httpCallback.server";
1112
import { logger } from "~/services/logger.server";
1213
import { engine } from "~/v3/runEngine.server";
1314

1415
const paramsSchema = z.object({
1516
waitpointFriendlyId: z.string(),
17+
hash: z.string(),
1618
});
1719

1820
export async function action({ request, params }: ActionFunctionArgs) {
@@ -25,20 +27,31 @@ export async function action({ request, params }: ActionFunctionArgs) {
2527
return json({ error: "Request body too large" }, { status: 413 });
2628
}
2729

28-
const { waitpointFriendlyId } = paramsSchema.parse(params);
30+
const { waitpointFriendlyId, hash } = paramsSchema.parse(params);
2931
const waitpointId = WaitpointId.toId(waitpointFriendlyId);
3032

3133
try {
3234
const waitpoint = await $replica.waitpoint.findFirst({
3335
where: {
3436
id: waitpointId,
3537
},
38+
include: {
39+
environment: {
40+
select: {
41+
apiKey: true,
42+
},
43+
},
44+
},
3645
});
3746

3847
if (!waitpoint) {
3948
throw json({ error: "Waitpoint not found" }, { status: 404 });
4049
}
4150

51+
if (!verifyHttpCallbackHash(waitpoint.id, hash, waitpoint.environment.apiKey)) {
52+
throw json({ error: "Invalid URL, hash doesn't match" }, { status: 401 });
53+
}
54+
4255
if (waitpoint.status === "COMPLETED") {
4356
return json<CompleteWaitpointTokenResponseBody>({
4457
success: true,

apps/webapp/app/routes/engine.v1.waitpoints.http-callback.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@ import {
44
CreateWaitpointTokenRequestBody,
55
} from "@trigger.dev/core/v3";
66
import { WaitpointId } from "@trigger.dev/core/v3/isomorphic";
7-
import { env } from "~/env.server";
87
import { createWaitpointTag, MAX_TAGS_PER_WAITPOINT } from "~/models/waitpointTag.server";
98
import {
109
ApiWaitpointListPresenter,
1110
ApiWaitpointListSearchParams,
1211
} from "~/presenters/v3/ApiWaitpointListPresenter.server";
13-
import { generateWaitpointCallbackUrl } from "~/presenters/v3/WaitpointPresenter.server";
14-
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
12+
import { generateHttpCallbackUrl } from "~/services/httpCallback.server";
1513
import {
1614
createActionApiRoute,
1715
createLoaderApiRoute,
@@ -84,7 +82,7 @@ const { action } = createActionApiRoute(
8482
return json<CreateWaitpointHttpCallbackResponseBody>(
8583
{
8684
id: WaitpointId.toFriendlyId(result.waitpoint.id),
87-
url: generateWaitpointCallbackUrl(result.waitpoint.id),
85+
url: generateHttpCallbackUrl(result.waitpoint.id, authentication.environment.apiKey),
8886
isCached: result.isCached,
8987
},
9088
{ status: 200 }

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const apiRateLimiter = authorizationRateLimitMiddleware({
5959
"/api/v1/usage/ingest",
6060
"/api/v1/auth/jwt/claims",
6161
/^\/api\/v1\/runs\/[^\/]+\/attempts$/, // /api/v1/runs/$runFriendlyId/attempts
62-
/^\/api\/v1\/waitpoints\/http-callback\/[^\/]+\/callback$/, // /api/v1/waitpoints/http-callback/$waitpointFriendlyId/callback
62+
/^\/api\/v1\/waitpoints\/http-callback\/[^\/]+\/callback\/[^\/]+$/, // /api/v1/waitpoints/http-callback/$waitpointFriendlyId/callback/$hash
6363
],
6464
log: {
6565
rejections: env.API_RATE_LIMIT_REJECTION_LOGS_ENABLED === "1",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { WaitpointId } from "@trigger.dev/core/v3/isomorphic";
2+
import nodeCrypto from "node:crypto";
3+
import { env } from "~/env.server";
4+
5+
export function generateHttpCallbackUrl(waitpointId: string, apiKey: string) {
6+
const hash = generateHttpCallbackHash(waitpointId, apiKey);
7+
8+
return `${
9+
env.API_ORIGIN ?? env.APP_ORIGIN
10+
}/api/v1/waitpoints/http-callback/${WaitpointId.toFriendlyId(waitpointId)}/callback/${hash}`;
11+
}
12+
13+
function generateHttpCallbackHash(waitpointId: string, apiKey: string) {
14+
const hmac = nodeCrypto.createHmac("sha256", apiKey);
15+
hmac.update(waitpointId);
16+
return hmac.digest("hex");
17+
}
18+
19+
export function verifyHttpCallbackHash(waitpointId: string, hash: string, apiKey: string) {
20+
const expectedHash = generateHttpCallbackHash(waitpointId, apiKey);
21+
22+
if (
23+
hash.length === expectedHash.length &&
24+
nodeCrypto.timingSafeEqual(Buffer.from(hash, "hex"), Buffer.from(expectedHash, "hex"))
25+
) {
26+
return true;
27+
}
28+
29+
return false;
30+
}

0 commit comments

Comments
 (0)