212 lines
7.2 KiB
TypeScript
212 lines
7.2 KiB
TypeScript
import { EngineServiceValidationError } from "@internal/run-engine";
|
|
import { json } from "@remix-run/server-runtime";
|
|
import {
|
|
generateJWT as internal_generateJWT,
|
|
RunEngineVersionSchema,
|
|
TriggerTaskRequestBody,
|
|
} from "@trigger.dev/core/v3";
|
|
import type { TaskRun } from "@trigger.dev/database";
|
|
import { z } from "zod";
|
|
import { prisma } from "~/db.server";
|
|
import { env } from "~/env.server";
|
|
import type { ApiAuthenticationResultSuccess } from "~/services/apiAuth.server";
|
|
import { getOneTimeUseToken } from "~/services/apiAuth.server";
|
|
import { logger } from "~/services/logger.server";
|
|
import { extractJwtSigningSecretKey } from "~/services/realtime/jwtAuth.server";
|
|
import { determineRealtimeStreamsVersion } from "~/services/realtime/v1StreamsGlobal.server";
|
|
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
|
import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server";
|
|
import {
|
|
handleRequestIdempotency,
|
|
saveRequestIdempotency,
|
|
} from "~/utils/requestIdempotency.server";
|
|
import { sanitizeTriggerSource } from "~/utils/triggerSource";
|
|
import { runStore } from "~/v3/runStore.server";
|
|
import { ServiceValidationError } from "~/v3/services/baseService.server";
|
|
import { OutOfEntitlementError, TriggerTaskService } from "~/v3/services/triggerTask.server";
|
|
|
|
const ParamsSchema = z.object({
|
|
taskId: z.string(),
|
|
});
|
|
|
|
export const HeadersSchema = z.object({
|
|
"idempotency-key": z
|
|
.string()
|
|
.max(2048, "idempotency-key must be 2048 characters or less")
|
|
.nullish(),
|
|
"idempotency-key-ttl": z.string().nullish(),
|
|
"trigger-version": z.string().nullish(),
|
|
"x-trigger-span-parent-as-link": z.coerce.number().nullish(),
|
|
"x-trigger-worker": z.string().nullish(),
|
|
"x-trigger-client": z.string().nullish(),
|
|
"x-trigger-engine-version": RunEngineVersionSchema.nullish(),
|
|
"x-trigger-request-idempotency-key": z.string().nullish(),
|
|
"x-trigger-realtime-streams-version": z.string().nullish(),
|
|
"x-trigger-source": z.string().nullish(),
|
|
traceparent: z.string().optional(),
|
|
tracestate: z.string().optional(),
|
|
});
|
|
|
|
const { action, loader } = createActionApiRoute(
|
|
{
|
|
headers: HeadersSchema,
|
|
params: ParamsSchema,
|
|
body: TriggerTaskRequestBody,
|
|
allowJWT: true,
|
|
maxContentLength: env.TASK_PAYLOAD_MAXIMUM_SIZE,
|
|
authorization: {
|
|
action: "trigger",
|
|
resource: (params) => ({ type: "tasks", id: params.taskId }),
|
|
},
|
|
corsStrategy: "all",
|
|
},
|
|
async ({ body, headers, params, authentication }) => {
|
|
const {
|
|
"idempotency-key": idempotencyKey,
|
|
"idempotency-key-ttl": idempotencyKeyTTL,
|
|
"trigger-version": triggerVersion,
|
|
"x-trigger-span-parent-as-link": spanParentAsLink,
|
|
traceparent,
|
|
tracestate,
|
|
"x-trigger-worker": isFromWorker,
|
|
"x-trigger-client": _triggerClient,
|
|
"x-trigger-engine-version": engineVersion,
|
|
"x-trigger-request-idempotency-key": requestIdempotencyKey,
|
|
"x-trigger-realtime-streams-version": realtimeStreamsVersion,
|
|
"x-trigger-source": triggerSourceHeader,
|
|
} = headers;
|
|
|
|
const cachedResponse = await handleRequestIdempotency(requestIdempotencyKey, {
|
|
requestType: "trigger",
|
|
findCachedEntity: async (cachedRequestId) => {
|
|
return await runStore.findRun(
|
|
{
|
|
id: cachedRequestId,
|
|
},
|
|
{
|
|
select: {
|
|
friendlyId: true,
|
|
},
|
|
},
|
|
prisma
|
|
);
|
|
},
|
|
buildResponse: (cachedRun) => ({
|
|
id: cachedRun.friendlyId,
|
|
isCached: false,
|
|
}),
|
|
buildResponseHeaders: async (responseBody, cachedEntity) => {
|
|
return await responseHeaders(cachedEntity, authentication);
|
|
},
|
|
});
|
|
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
|
|
const service = new TriggerTaskService();
|
|
|
|
try {
|
|
const traceContext = isFromWorker
|
|
? { traceparent, tracestate }
|
|
: { external: { traceparent, tracestate } };
|
|
|
|
const oneTimeUseToken = await getOneTimeUseToken(authentication);
|
|
|
|
const idempotencyKeyExpiresAt = resolveIdempotencyKeyTTL(idempotencyKeyTTL);
|
|
|
|
const result = await service.call(
|
|
params.taskId,
|
|
authentication.environment,
|
|
body,
|
|
{
|
|
idempotencyKey: idempotencyKey ?? undefined,
|
|
idempotencyKeyExpiresAt: idempotencyKeyExpiresAt,
|
|
triggerVersion: triggerVersion ?? undefined,
|
|
traceContext,
|
|
spanParentAsLink: spanParentAsLink === 1,
|
|
oneTimeUseToken,
|
|
realtimeStreamsVersion: determineRealtimeStreamsVersion(
|
|
realtimeStreamsVersion ?? undefined
|
|
),
|
|
triggerSource: isFromWorker
|
|
? "sdk"
|
|
: (sanitizeTriggerSource(triggerSourceHeader) ?? "api"),
|
|
triggerAction: "trigger",
|
|
},
|
|
engineVersion ?? undefined
|
|
);
|
|
|
|
if (!result) {
|
|
return json({ error: "Task not found" }, { status: 404 });
|
|
}
|
|
|
|
// Skip request-idempotency caching when the gate diverted to the
|
|
// mollifier buffer. `result.run.id` is a synthesised cuid with no
|
|
// corresponding PG row, so a lost-response SDK retry that reaches
|
|
// `handleRequestIdempotency` would lookup that id, miss in PG, and
|
|
// fall through to a fresh trigger — producing a duplicate buffer
|
|
// entry for triggers without a task-level idempotency key (the
|
|
// task-level path still dedupes via the buffer's SETNX in
|
|
// `findBufferedRunWithIdempotency`). Accepting the retry-as-fresh-
|
|
// trigger semantics here is bounded by the drainer's eventual
|
|
// materialisation: once the run lands in PG, normal request-
|
|
// idempotency from that point forward works as usual.
|
|
if (!result.isMollified) {
|
|
await saveRequestIdempotency(requestIdempotencyKey, "trigger", result.run.id);
|
|
}
|
|
|
|
const $responseHeaders = await responseHeaders(result.run, authentication);
|
|
|
|
return json(
|
|
{
|
|
id: result.run.friendlyId,
|
|
isCached: result.isCached,
|
|
},
|
|
{
|
|
headers: $responseHeaders,
|
|
status: 200,
|
|
}
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof ServiceValidationError) {
|
|
return json({ error: error.message }, { status: error.status ?? 422 });
|
|
} else if (error instanceof EngineServiceValidationError) {
|
|
return json({ error: error.message }, { status: error.status ?? 422 });
|
|
} else if (error instanceof OutOfEntitlementError) {
|
|
return json({ error: error.message }, { status: 422 });
|
|
}
|
|
|
|
logger.error("Trigger task failed", { error });
|
|
return json({ error: "Something went wrong" }, { status: 500 });
|
|
}
|
|
}
|
|
);
|
|
|
|
async function responseHeaders(
|
|
run: Pick<TaskRun, "friendlyId">,
|
|
authentication: ApiAuthenticationResultSuccess
|
|
): Promise<Record<string, string>> {
|
|
const { environment, realtime } = authentication;
|
|
|
|
const claims = {
|
|
sub: environment.id,
|
|
pub: true,
|
|
scopes: [`read:runs:${run.friendlyId}`, `write:inputStreams:${run.friendlyId}`],
|
|
realtime,
|
|
};
|
|
|
|
const jwt = await internal_generateJWT({
|
|
secretKey: extractJwtSigningSecretKey(environment),
|
|
payload: claims,
|
|
expirationTime: "1h",
|
|
});
|
|
|
|
return {
|
|
"x-trigger-jwt-claims": JSON.stringify(claims),
|
|
"x-trigger-jwt": jwt,
|
|
};
|
|
}
|
|
|
|
export { action, loader };
|