Skip to content

Commit 71bc83b

Browse files
committed
useTaskTrigger react hook that allows triggering a task from the client
1 parent f3f04ec commit 71bc83b

File tree

15 files changed

+617
-229
lines changed

15 files changed

+617
-229
lines changed
Lines changed: 112 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import { fromZodError } from "zod-validation-error";
2-
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
31
import { json } from "@remix-run/server-runtime";
4-
import { TriggerTaskRequestBody } from "@trigger.dev/core/v3";
2+
import { generateJWT as internal_generateJWT, TriggerTaskRequestBody } from "@trigger.dev/core/v3";
3+
import { TaskRun } from "@trigger.dev/database";
54
import { z } from "zod";
65
import { env } from "~/env.server";
7-
import { authenticateApiRequest } from "~/services/apiAuth.server";
6+
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
87
import { logger } from "~/services/logger.server";
9-
import { parseRequestJsonAsync } from "~/utils/parseRequestJson.server";
8+
import { createActionApiRoute } from "~/services/routeBuiilders/apiBuilder.server";
109
import { ServiceValidationError } from "~/v3/services/baseService.server";
1110
import { OutOfEntitlementError, TriggerTaskService } from "~/v3/services/triggerTask.server";
12-
import { startActiveSpan } from "~/v3/tracer.server";
1311

1412
const ParamsSchema = z.object({
1513
taskId: z.string(),
@@ -20,115 +18,125 @@ export const HeadersSchema = z.object({
2018
"trigger-version": z.string().nullish(),
2119
"x-trigger-span-parent-as-link": z.coerce.number().nullish(),
2220
"x-trigger-worker": z.string().nullish(),
21+
"x-trigger-client": z.string().nullish(),
2322
traceparent: z.string().optional(),
2423
tracestate: z.string().optional(),
2524
});
2625

27-
export async function action({ request, params }: ActionFunctionArgs) {
28-
// Ensure this is a POST request
29-
if (request.method.toUpperCase() !== "POST") {
30-
return { status: 405, body: "Method Not Allowed" };
31-
}
32-
33-
logger.debug("TriggerTask action", { headers: Object.fromEntries(request.headers) });
34-
35-
// Next authenticate the request
36-
const authenticationResult = await authenticateApiRequest(request);
37-
38-
if (!authenticationResult) {
39-
return json({ error: "Invalid or Missing API key" }, { status: 401 });
40-
}
41-
42-
const contentLength = request.headers.get("content-length");
43-
44-
if (!contentLength || parseInt(contentLength) > env.TASK_PAYLOAD_MAXIMUM_SIZE) {
45-
return json({ error: "Request body too large" }, { status: 413 });
46-
}
26+
const { action, loader } = createActionApiRoute(
27+
{
28+
headers: HeadersSchema,
29+
params: ParamsSchema,
30+
body: TriggerTaskRequestBody,
31+
allowJWT: true,
32+
maxContentLength: env.TASK_PAYLOAD_MAXIMUM_SIZE,
33+
authorization: {
34+
action: "write",
35+
resource: (params) => ({ tasks: params.taskId }),
36+
superScopes: ["write:tasks", "admin"],
37+
},
38+
corsStrategy: "all",
39+
},
40+
async ({ body, headers, params, authentication }) => {
41+
const {
42+
"idempotency-key": idempotencyKey,
43+
"trigger-version": triggerVersion,
44+
"x-trigger-span-parent-as-link": spanParentAsLink,
45+
traceparent,
46+
tracestate,
47+
"x-trigger-worker": isFromWorker,
48+
"x-trigger-client": triggerClient,
49+
} = headers;
50+
51+
const service = new TriggerTaskService();
52+
53+
try {
54+
const traceContext =
55+
traceparent && isFromWorker /// If the request is from a worker, we should pass the trace context
56+
? { traceparent, tracestate }
57+
: undefined;
58+
59+
logger.debug("Triggering task", {
60+
taskId: params.taskId,
61+
idempotencyKey,
62+
triggerVersion,
63+
headers,
64+
options: body.options,
65+
isFromWorker,
66+
traceContext,
67+
});
68+
69+
const run = await service.call(params.taskId, authentication.environment, body, {
70+
idempotencyKey: idempotencyKey ?? undefined,
71+
triggerVersion: triggerVersion ?? undefined,
72+
traceContext,
73+
spanParentAsLink: spanParentAsLink === 1,
74+
});
75+
76+
if (!run) {
77+
return json({ error: "Task not found" }, { status: 404 });
78+
}
4779

48-
const rawHeaders = Object.fromEntries(request.headers);
80+
const $responseHeaders = await responseHeaders(
81+
run,
82+
authentication.environment,
83+
triggerClient
84+
);
4985

50-
const headers = HeadersSchema.safeParse(rawHeaders);
86+
return json(
87+
{
88+
id: run.friendlyId,
89+
},
90+
{
91+
headers: $responseHeaders,
92+
}
93+
);
94+
} catch (error) {
95+
if (error instanceof ServiceValidationError) {
96+
return json({ error: error.message }, { status: 422 });
97+
} else if (error instanceof OutOfEntitlementError) {
98+
return json({ error: error.message }, { status: 422 });
99+
} else if (error instanceof Error) {
100+
return json({ error: error.message }, { status: 400 });
101+
}
51102

52-
if (!headers.success) {
53-
return json({ error: "Invalid headers" }, { status: 400 });
103+
return json({ error: "Something went wrong" }, { status: 500 });
104+
}
54105
}
55-
56-
const {
57-
"idempotency-key": idempotencyKey,
58-
"trigger-version": triggerVersion,
59-
"x-trigger-span-parent-as-link": spanParentAsLink,
60-
traceparent,
61-
tracestate,
62-
"x-trigger-worker": isFromWorker,
63-
} = headers.data;
64-
65-
const { taskId } = ParamsSchema.parse(params);
66-
67-
// Now parse the request body
68-
const anyBody = await parseRequestJsonAsync(request, { taskId });
69-
70-
const body = await startActiveSpan("TriggerTaskRequestBody.safeParse()", async (span) => {
71-
return TriggerTaskRequestBody.safeParse(anyBody);
106+
);
107+
108+
async function responseHeaders(
109+
run: TaskRun,
110+
environment: AuthenticatedEnvironment,
111+
triggerClient?: string | null
112+
): Promise<Record<string, string>> {
113+
const claimsHeader = JSON.stringify({
114+
sub: run.runtimeEnvironmentId,
115+
pub: true,
72116
});
73117

74-
if (!body.success) {
75-
return json(
76-
{ error: fromZodError(body.error, { prefix: "Invalid trigger call" }).toString() },
77-
{ status: 400 }
78-
);
79-
}
80-
81-
const service = new TriggerTaskService();
82-
83-
try {
84-
const traceContext =
85-
traceparent && isFromWorker /// If the request is from a worker, we should pass the trace context
86-
? { traceparent, tracestate }
87-
: undefined;
88-
89-
logger.debug("Triggering task", {
90-
taskId,
91-
idempotencyKey,
92-
triggerVersion,
93-
headers: Object.fromEntries(request.headers),
94-
options: body.data.options,
95-
isFromWorker,
96-
traceContext,
118+
if (triggerClient === "browser") {
119+
const claims = {
120+
sub: run.runtimeEnvironmentId,
121+
pub: true,
122+
scopes: [`read:runs:${run.friendlyId}`],
123+
};
124+
125+
const jwt = await internal_generateJWT({
126+
secretKey: environment.apiKey,
127+
payload: claims,
128+
expirationTime: "1h",
97129
});
98130

99-
const run = await service.call(taskId, authenticationResult.environment, body.data, {
100-
idempotencyKey: idempotencyKey ?? undefined,
101-
triggerVersion: triggerVersion ?? undefined,
102-
traceContext,
103-
spanParentAsLink: spanParentAsLink === 1,
104-
});
105-
106-
if (!run) {
107-
return json({ error: "Task not found" }, { status: 404 });
108-
}
109-
110-
return json(
111-
{
112-
id: run.friendlyId,
113-
},
114-
{
115-
headers: {
116-
"x-trigger-jwt-claims": JSON.stringify({
117-
sub: authenticationResult.environment.id,
118-
pub: true,
119-
}),
120-
},
121-
}
122-
);
123-
} catch (error) {
124-
if (error instanceof ServiceValidationError) {
125-
return json({ error: error.message }, { status: 422 });
126-
} else if (error instanceof OutOfEntitlementError) {
127-
return json({ error: error.message }, { status: 422 });
128-
} else if (error instanceof Error) {
129-
return json({ error: error.message }, { status: 400 });
130-
}
131-
132-
return json({ error: "Something went wrong" }, { status: 500 });
131+
return {
132+
"x-trigger-jwt-claims": claimsHeader,
133+
"x-trigger-jwt": jwt,
134+
};
133135
}
136+
137+
return {
138+
"x-trigger-jwt-claims": claimsHeader,
139+
};
134140
}
141+
142+
export { action, loader };

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type AuthorizationAction = "read"; // Add more actions as needed
1+
export type AuthorizationAction = "read" | "write"; // Add more actions as needed
22

33
const ResourceTypes = ["tasks", "tags", "runs", "batch"] as const;
44

0 commit comments

Comments
 (0)