From d24468a5f11c179d57a35048b95a86418aa1e512 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Mon, 18 May 2026 18:39:14 +0100 Subject: [PATCH 1/6] fix(webapp): catch loader/action throws before Remix serializes them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two webapp routes left their loader/action bodies uncaught. When the underlying call (Prisma, etc.) threw, Remix's default error path serialized `error.message` into the 500 response body, surfacing implementation detail to API consumers — and via the SDK, to users. This complements the earlier sweep over `catch (e) { return json({error: e.message}, 500) }` shapes; that fix could not reach routes which had no catch in the first place. Each handler now wraps its body in try/catch, re-throws `Response` instances so auth helpers' `throw json(...)` / `throw redirect(...)` pass through unchanged, logs non-Response errors, and returns a generic body. The polling changelogs widget returns `{ changelogs: [] }` 200 instead of a 500 — degrading silently across a transient blip is better UX for a 60s-cadence widget, and the leak risk is identical (neither shape carries the error message). Covers: - apps/webapp/app/routes/api.v1.projects.\$projectRef.envvars.\$slug.\$name.ts (loader + action) - apps/webapp/app/routes/resources.platform-changelogs.tsx (loader) --- .../sanitize-loader-action-leaks.md | 8 + ...rojects.$projectRef.envvars.$slug.$name.ts | 208 ++++++++++-------- .../routes/resources.platform-changelogs.tsx | 32 ++- 3 files changed, 141 insertions(+), 107 deletions(-) create mode 100644 .server-changes/sanitize-loader-action-leaks.md diff --git a/.server-changes/sanitize-loader-action-leaks.md b/.server-changes/sanitize-loader-action-leaks.md new file mode 100644 index 00000000000..2d439c41ea1 --- /dev/null +++ b/.server-changes/sanitize-loader-action-leaks.md @@ -0,0 +1,8 @@ +--- +area: webapp +type: fix +--- + +Wrap two loaders/actions that previously let thrown errors propagate to Remix's default 500 serializer, which writes `error.message` into the response body. When the underlying call (Prisma, etc.) fails, the raw error string was reaching API consumers — including the SDK, which surfaces it back to users via `TriggerApiError`. Each handler now catches non-Response errors, logs server-side, and returns a generic 500 body. `throw json(...)` / `throw redirect(...)` from auth helpers is re-thrown unchanged. + +Covers `api.v1.projects.$projectRef.envvars.$slug.$name.ts` (loader + action) and `resources.platform-changelogs.tsx` (loader). diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.$name.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.$name.ts index e3081b090e3..00e155622ce 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.$name.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.$name.ts @@ -7,6 +7,7 @@ import { authenticatedEnvironmentForAuthentication, branchNameFromRequest, } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server"; const ParamsSchema = z.object({ @@ -22,73 +23,82 @@ export async function action({ params, request }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - const authenticationResult = await authenticateRequest(request); + try { + const authenticationResult = await authenticateRequest(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const environment = await authenticatedEnvironmentForAuthentication( - authenticationResult, - parsedParams.data.projectRef, - parsedParams.data.slug, - branchNameFromRequest(request) - ); - - // Find the environment variable - const variable = await prisma.environmentVariable.findFirst({ - where: { - key: parsedParams.data.name, - projectId: environment.project.id, - }, - }); - - if (!variable) { - return json({ error: "Environment variable not found" }, { status: 404 }); - } - - const repository = new EnvironmentVariablesRepository(); - - switch (request.method.toUpperCase()) { - case "DELETE": { - const result = await repository.deleteValue(environment.project.id, { - id: variable.id, - environmentId: environment.id, - }); + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - if (result.success) { - return json({ success: true }); - } else { - return json({ error: result.error }, { status: 400 }); - } + const environment = await authenticatedEnvironmentForAuthentication( + authenticationResult, + parsedParams.data.projectRef, + parsedParams.data.slug, + branchNameFromRequest(request) + ); + + // Find the environment variable + const variable = await prisma.environmentVariable.findFirst({ + where: { + key: parsedParams.data.name, + projectId: environment.project.id, + }, + }); + + if (!variable) { + return json({ error: "Environment variable not found" }, { status: 404 }); } - case "PUT": - case "POST": { - const jsonBody = await request.json(); - const body = UpdateEnvironmentVariableRequestBody.safeParse(jsonBody); + const repository = new EnvironmentVariablesRepository(); - if (!body.success) { - return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); - } + switch (request.method.toUpperCase()) { + case "DELETE": { + const result = await repository.deleteValue(environment.project.id, { + id: variable.id, + environmentId: environment.id, + }); - const result = await repository.edit(environment.project.id, { - values: [ - { - value: body.data.value, - environmentId: environment.id, - }, - ], - id: variable.id, - keepEmptyValues: true, - }); - - if (result.success) { - return json({ success: true }); - } else { - return json({ error: result.error }, { status: 400 }); + if (result.success) { + return json({ success: true }); + } else { + return json({ error: result.error }, { status: 400 }); + } + } + case "PUT": + case "POST": { + const jsonBody = await request.json(); + + const body = UpdateEnvironmentVariableRequestBody.safeParse(jsonBody); + + if (!body.success) { + return json( + { error: "Invalid request body", issues: body.error.issues }, + { status: 400 } + ); + } + + const result = await repository.edit(environment.project.id, { + values: [ + { + value: body.data.value, + environmentId: environment.id, + }, + ], + id: variable.id, + keepEmptyValues: true, + }); + + if (result.success) { + return json({ success: true }); + } else { + return json({ error: result.error }, { status: 400 }); + } } } + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to update environment variable", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } } @@ -99,48 +109,54 @@ export async function loader({ params, request }: LoaderFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - const authenticationResult = await authenticateRequest(request); + try { + const authenticationResult = await authenticateRequest(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const environment = await authenticatedEnvironmentForAuthentication( - authenticationResult, - parsedParams.data.projectRef, - parsedParams.data.slug, - branchNameFromRequest(request) - ); - - // Find the environment variable - const variable = await prisma.environmentVariable.findFirst({ - where: { - key: parsedParams.data.name, - projectId: environment.project.id, - }, - }); - - if (!variable) { - return json({ error: "Environment variable not found" }, { status: 404 }); - } + const environment = await authenticatedEnvironmentForAuthentication( + authenticationResult, + parsedParams.data.projectRef, + parsedParams.data.slug, + branchNameFromRequest(request) + ); + + // Find the environment variable + const variable = await prisma.environmentVariable.findFirst({ + where: { + key: parsedParams.data.name, + projectId: environment.project.id, + }, + }); + + if (!variable) { + return json({ error: "Environment variable not found" }, { status: 404 }); + } - const repository = new EnvironmentVariablesRepository(); + const repository = new EnvironmentVariablesRepository(); - const variables = await repository.getEnvironmentWithRedactedSecrets( - environment.project.id, - environment.id, - environment.parentEnvironmentId ?? undefined - ); + const variables = await repository.getEnvironmentWithRedactedSecrets( + environment.project.id, + environment.id, + environment.parentEnvironmentId ?? undefined + ); - const environmentVariable = variables.find((v) => v.key === parsedParams.data.name); + const environmentVariable = variables.find((v) => v.key === parsedParams.data.name); - if (!environmentVariable) { - return json({ error: "Environment variable not found" }, { status: 404 }); - } + if (!environmentVariable) { + return json({ error: "Environment variable not found" }, { status: 404 }); + } - return json({ - name: environmentVariable.key, - value: environmentVariable.value, - isSecret: environmentVariable.isSecret, - }); + return json({ + name: environmentVariable.key, + value: environmentVariable.value, + isSecret: environmentVariable.isSecret, + }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to get environment variable", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/resources.platform-changelogs.tsx b/apps/webapp/app/routes/resources.platform-changelogs.tsx index 8eeabd6b047..ed62de3c1df 100644 --- a/apps/webapp/app/routes/resources.platform-changelogs.tsx +++ b/apps/webapp/app/routes/resources.platform-changelogs.tsx @@ -2,6 +2,7 @@ import { json } from "@remix-run/node"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { useFetcher, type ShouldRevalidateFunction } from "@remix-run/react"; import { useEffect, useRef } from "react"; +import { logger } from "~/services/logger.server"; import { requireUserId } from "~/services/session.server"; import { getRecentChangelogs, verifyOrgMembership } from "~/services/platformNotifications.server"; @@ -12,20 +13,29 @@ export type PlatformChangelogsLoaderData = { }; export async function loader({ request }: LoaderFunctionArgs) { - const userId = await requireUserId(request); - const url = new URL(request.url); - const rawOrganizationId = url.searchParams.get("organizationId") ?? undefined; - const rawProjectId = url.searchParams.get("projectId") ?? undefined; + try { + const userId = await requireUserId(request); + const url = new URL(request.url); + const rawOrganizationId = url.searchParams.get("organizationId") ?? undefined; + const rawProjectId = url.searchParams.get("projectId") ?? undefined; - const { organizationId, projectId } = await verifyOrgMembership({ - userId, - organizationId: rawOrganizationId, - projectId: rawProjectId, - }); + const { organizationId, projectId } = await verifyOrgMembership({ + userId, + organizationId: rawOrganizationId, + projectId: rawProjectId, + }); - const changelogs = await getRecentChangelogs({ userId, organizationId, projectId }); + const changelogs = await getRecentChangelogs({ userId, organizationId, projectId }); - return json({ changelogs }); + return json({ changelogs }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load platform changelogs", { error }); + // Polling widget — degrade silently so a transient DB blip doesn't paint + // the dashboard with errors every 60s. Empty payload keeps the consumer's + // fetcher.data shape stable; the fault is recorded server-side. + return json({ changelogs: [] }); + } } const POLL_INTERVAL_MS = 60_000; From 1c88aed3693460ac8f4f343f5fc2c55dae2ee527 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Mon, 18 May 2026 19:02:52 +0100 Subject: [PATCH 2/6] fix(webapp): sweep remaining api.v1 loaders/actions for thrown-error leaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Earlier passes covered routes with leaking `catch (e) { return json({error: e.message}, 500) }` shapes, and two specific naked routes. This sweep covers the rest of the API surface that doesn't go through `createLoaderApiRoute`/`createActionApiRoute` — 26 files across deploy, projects, orgs, deployments, auth-jwt, artifacts, and alert-channel routes. Each handler now wraps its body in try/catch, re-throws `Response` instances so auth helpers' `throw json(...)` / `throw redirect(...)` pass through unchanged, logs non-Response errors via `logger.error`, and returns a generic `{ error: "Internal Server Error" }` 500. Routes that already had an inner try/catch covering a service call but with auth/parsing outside the try (alertChannels, batches.results, deployments.finalize, several others) get an outer try/catch added so the inner typed-error handling is preserved. The api.v1.authorization-code.ts catch branch was returning `error.message` verbatim — switched to a generic body. Each route verified locally with the established synthetic-throw probe: inject `throw new Error("SYNTHETIC ...")` at the top of the catch'd try, curl with a dummy bearer, confirm the response body is the generic shape and that the synthetic message lands server-side via `logger.error`. Sampled legitimate 4xx paths (no-auth 401s, auth-helper Response throws, happy 200 returns) across each pattern variant to confirm the wrap does not interfere with normal control flow. --- .../sanitize-api-loader-action-leaks-sweep.md | 10 +++ apps/webapp/app/routes/api.v1.artifacts.ts | 6 ++ .../app/routes/api.v1.auth.jwt.claims.ts | 27 +++--- apps/webapp/app/routes/api.v1.auth.jwt.ts | 7 ++ .../app/routes/api.v1.authorization-code.ts | 2 +- .../api.v1.batches.$batchParam.results.ts | 6 ++ ...yments.$deploymentId.background-workers.ts | 6 ++ ...api.v1.deployments.$deploymentId.cancel.ts | 6 ++ .../api.v1.deployments.$deploymentId.fail.ts | 6 ++ ...i.v1.deployments.$deploymentId.finalize.ts | 6 ++ ...loymentId.generate-registry-credentials.ts | 6 ++ ...i.v1.deployments.$deploymentId.progress.ts | 6 ++ .../api.v1.deployments.$deploymentId.ts | 6 ++ ....deployments.$deploymentVersion.promote.ts | 6 ++ .../app/routes/api.v1.deployments.latest.ts | 6 ++ .../routes/api.v1.orgs.$orgParam.projects.ts | 15 +++- ....projects.$projectParam.vercel.projects.ts | 6 ++ apps/webapp/app/routes/api.v1.orgs.ts | 7 ++ .../api.v1.projects.$projectRef.$env.jwt.ts | 83 ++++++++++--------- .../api.v1.projects.$projectRef.$env.ts | 47 ++++++----- ...jects.$projectRef.$env.workers.$tagName.ts | 55 +++++++----- ...i.v1.projects.$projectRef.alertChannels.ts | 6 ++ ...ef.background-workers.$envSlug.$version.ts | 81 +++++++++--------- ...projects.$projectRef.background-workers.ts | 6 ++ .../api.v1.projects.$projectRef.dev-status.ts | 59 +++++++------ apps/webapp/app/routes/api.v1.projects.ts | 6 ++ apps/webapp/app/routes/api.v1.whoami.ts | 47 ++++++----- 27 files changed, 354 insertions(+), 176 deletions(-) create mode 100644 .server-changes/sanitize-api-loader-action-leaks-sweep.md diff --git a/.server-changes/sanitize-api-loader-action-leaks-sweep.md b/.server-changes/sanitize-api-loader-action-leaks-sweep.md new file mode 100644 index 00000000000..e312dc57ffe --- /dev/null +++ b/.server-changes/sanitize-api-loader-action-leaks-sweep.md @@ -0,0 +1,10 @@ +--- +area: webapp +type: fix +--- + +Sweep across the remaining `apps/webapp/app/routes/api.v1.*` raw loaders/actions that previously let thrown errors propagate to Remix's default 500 serializer, which writes `error.message` into the response body. Earlier passes covered routes with leaking `catch` blocks and two specific naked routes; this pass covers the rest of the API surface that doesn't go through `createLoaderApiRoute` / `createActionApiRoute`. + +Each handler now wraps its body in try/catch, re-throws `Response` instances so auth helpers' `throw json(...)` / `throw redirect(...)` pass through unchanged, logs non-Response errors, and returns `{ error: "Internal Server Error" }` 500. For routes that already had an inner try/catch covering a service call but with auth/parsing outside the try (alertChannels, batches.results, deployments.finalize, several others), an outer try/catch is added so the inner typed-error handling is preserved. The `api.v1.authorization-code.ts` catch branch was returning `error.message` verbatim — switched to a generic body. + +Each route was verified locally with a synthetic-throw probe: inject `throw new Error("SYNTHETIC ...")` at the top of the catch'd try, curl the route with a dummy bearer token, confirm the response body is the generic shape and that the synthetic message is captured server-side via `logger.error`. diff --git a/apps/webapp/app/routes/api.v1.artifacts.ts b/apps/webapp/app/routes/api.v1.artifacts.ts index 82ae3f53756..64ed2ceda4b 100644 --- a/apps/webapp/app/routes/api.v1.artifacts.ts +++ b/apps/webapp/app/routes/api.v1.artifacts.ts @@ -13,6 +13,7 @@ export async function action({ request }: ActionFunctionArgs) { return json({ error: "Method Not Allowed" }, { status: 405 }); } + try { const authenticationResult = await authenticateRequest(request, { apiKey: true, organizationAccessToken: false, @@ -88,4 +89,9 @@ export async function action({ request }: ActionFunctionArgs) { } } ); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to create artifact", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.auth.jwt.claims.ts b/apps/webapp/app/routes/api.v1.auth.jwt.claims.ts index 0091078dbb5..b62874d5608 100644 --- a/apps/webapp/app/routes/api.v1.auth.jwt.claims.ts +++ b/apps/webapp/app/routes/api.v1.auth.jwt.claims.ts @@ -1,19 +1,26 @@ import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; export async function action({ request }: LoaderFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + try { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const claims = { - sub: authenticationResult.environment.id, - pub: true, - }; + const claims = { + sub: authenticationResult.environment.id, + pub: true, + }; - return json(claims); + return json(claims); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to read auth jwt claims", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.auth.jwt.ts b/apps/webapp/app/routes/api.v1.auth.jwt.ts index b95b1eb7877..76e78fe910a 100644 --- a/apps/webapp/app/routes/api.v1.auth.jwt.ts +++ b/apps/webapp/app/routes/api.v1.auth.jwt.ts @@ -1,6 +1,7 @@ import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; import { z } from "zod"; import { generateJWT as internal_generateJWT } from "@trigger.dev/core/v3"; @@ -14,6 +15,7 @@ const RequestBodySchema = z.object({ }); export async function action({ request }: LoaderFunctionArgs) { + try { // Next authenticate the request const authenticationResult = await authenticateApiRequest(request); @@ -46,4 +48,9 @@ export async function action({ request }: LoaderFunctionArgs) { }); return json({ token: jwt }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to mint auth jwt", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.authorization-code.ts b/apps/webapp/app/routes/api.v1.authorization-code.ts index b924b67500a..3f90d72b1f0 100644 --- a/apps/webapp/app/routes/api.v1.authorization-code.ts +++ b/apps/webapp/app/routes/api.v1.authorization-code.ts @@ -32,7 +32,7 @@ export async function action({ request }: ActionFunctionArgs) { error: error.message, }); - return json({ error: error.message }, { status: 400 }); + return json({ error: "Internal Server Error" }, { status: 500 }); } return json({ error: "Something went wrong" }, { status: 500 }); diff --git a/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts b/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts index 1a5889fab1d..002914abd07 100644 --- a/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts +++ b/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts @@ -12,6 +12,7 @@ const ParamsSchema = z.object({ }); export async function loader({ request, params }: LoaderFunctionArgs) { + try { // Authenticate the request const authenticationResult = await authenticateApiRequest(request); @@ -40,4 +41,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { logger.error("Failed to load batch results", { error }); return json({ error: "Something went wrong, please try again." }, { status: 500 }); } + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load batch results (outer)", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts index c22399ef60e..3b1c3ec4505 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts @@ -23,6 +23,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } + try { // Next authenticate the request const authenticationResult = await authenticateApiRequest(request); @@ -76,4 +77,9 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Failed to create background worker" }, { status: 500 }); } + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to create deployment background worker", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts index dd209d4494b..217bcd6c03e 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts @@ -20,6 +20,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } + try { const authenticationResult = await authenticateRequest(request, { apiKey: true, organizationAccessToken: false, @@ -69,4 +70,9 @@ export async function action({ request, params }: ActionFunctionArgs) { } } ); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to cancel deployment", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts index 5edea5636e7..a71cbdcf7f9 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts @@ -21,6 +21,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } + try { // Next authenticate the request const authenticationResult = await authenticateApiRequest(request); @@ -49,4 +50,9 @@ export async function action({ request, params }: ActionFunctionArgs) { }, { status: 200 } ); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to fail deployment", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts index 9bafd8644af..44ded1da718 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts @@ -22,6 +22,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } + try { // Next authenticate the request const authenticationResult = await authenticateApiRequest(request); @@ -59,4 +60,9 @@ export async function action({ request, params }: ActionFunctionArgs) { logger.error("Error finalizing deployment", { error }); return json({ error: "Internal server error" }, { status: 500 }); } + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to finalize deployment", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts index 161f37f930b..35aa5f8819a 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts @@ -24,6 +24,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } + try { const authenticationResult = await authenticateRequest(request, { apiKey: true, organizationAccessToken: false, @@ -97,4 +98,9 @@ export async function action({ request, params }: ActionFunctionArgs) { } } ); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to generate registry credentials", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts index 2c78c59f552..2680065d3ff 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts @@ -20,6 +20,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } + try { const authenticationResult = await authenticateRequest(request, { apiKey: true, organizationAccessToken: false, @@ -78,4 +79,9 @@ export async function action({ request, params }: ActionFunctionArgs) { } } ); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to progress deployment", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.ts index d0593e564fd..fd60fb9f4e6 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.ts @@ -16,6 +16,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } + try { // Next authenticate the request const authenticationResult = await authenticateApiRequest(request); @@ -82,4 +83,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { })) : undefined, } satisfies GetDeploymentResponseBody); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load deployment", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentVersion.promote.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentVersion.promote.ts index 893b260dc82..10f9c655a9d 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentVersion.promote.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentVersion.promote.ts @@ -22,6 +22,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } + try { // Next authenticate the request const authenticationResult = await authenticateApiRequest(request); @@ -67,4 +68,9 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Failed to promote deployment" }, { status: 500 }); } } + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to promote deployment", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.deployments.latest.ts b/apps/webapp/app/routes/api.v1.deployments.latest.ts index 6f31f58fcc2..2c50964f9a0 100644 --- a/apps/webapp/app/routes/api.v1.deployments.latest.ts +++ b/apps/webapp/app/routes/api.v1.deployments.latest.ts @@ -5,6 +5,7 @@ import { authenticateApiRequest } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; export async function loader({ request }: LoaderFunctionArgs) { + try { // Next authenticate the request const authenticationResult = await authenticateApiRequest(request); @@ -38,4 +39,9 @@ export async function loader({ request }: LoaderFunctionArgs) { imageReference: deployment.imageReference, errorData: deployment.errorData, }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load latest deployment", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts b/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts index dc1791dabc5..443982ed7b2 100644 --- a/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts +++ b/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts @@ -20,6 +20,7 @@ const ParamsSchema = z.object({ export async function loader({ request, params }: LoaderFunctionArgs) { logger.info("get projects", { url: request.url }); + try { const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); if (!authenticationResult) { @@ -66,9 +67,15 @@ export async function loader({ request, params }: LoaderFunctionArgs) { })); return json(result); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to list org projects", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } export async function action({ request, params }: ActionFunctionArgs) { + try { const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); if (!authenticationResult) { @@ -110,7 +117,8 @@ export async function action({ request, params }: ActionFunctionArgs) { ); if (error) { - return json({ error: error.message }, { status: 400 }); + logger.error("Failed to create project", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } const result: GetProjectResponseBody = { @@ -128,6 +136,11 @@ export async function action({ request, params }: ActionFunctionArgs) { }; return json(result); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to create org project", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } function orgParamWhereClause(orgParam: string) { diff --git a/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts b/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts index aaf54685888..4d39298a766 100644 --- a/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts +++ b/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts @@ -29,6 +29,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return apiCors(request, json({})); } + try { const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); if (!authenticationResult) { @@ -143,5 +144,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }, }) ); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to fetch Vercel projects", { error }); + return apiCors(request, json({ error: "Internal Server Error" }, { status: 500 })); + } } diff --git a/apps/webapp/app/routes/api.v1.orgs.ts b/apps/webapp/app/routes/api.v1.orgs.ts index 626162f234b..615df7c5ce5 100644 --- a/apps/webapp/app/routes/api.v1.orgs.ts +++ b/apps/webapp/app/routes/api.v1.orgs.ts @@ -2,9 +2,11 @@ import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { GetOrgsResponseBody } from "@trigger.dev/core/v3"; import { prisma } from "~/db.server"; +import { logger } from "~/services/logger.server"; import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; export async function loader({ request }: LoaderFunctionArgs) { + try { const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); if (!authenticationResult) { @@ -34,4 +36,9 @@ export async function loader({ request }: LoaderFunctionArgs) { })); return json(result); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to list orgs", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts index e4b48ece05e..b9cb61e1f20 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts @@ -5,6 +5,7 @@ import { authenticatedEnvironmentForAuthentication, authenticateRequest, } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; const ParamsSchema = z.object({ projectRef: z.string(), @@ -21,52 +22,58 @@ const RequestBodySchema = z.object({ }); export async function action({ request, params }: ActionFunctionArgs) { - const authenticationResult = await authenticateRequest(request, { - personalAccessToken: true, - organizationAccessToken: true, - apiKey: false, - }); + try { + const authenticationResult = await authenticateRequest(request, { + personalAccessToken: true, + organizationAccessToken: true, + apiKey: false, + }); - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } - const parsedParams = ParamsSchema.safeParse(params); + const parsedParams = ParamsSchema.safeParse(params); - if (!parsedParams.success) { - return json({ error: "Invalid Params" }, { status: 400 }); - } + if (!parsedParams.success) { + return json({ error: "Invalid Params" }, { status: 400 }); + } - const { projectRef, env } = parsedParams.data; - const triggerBranch = request.headers.get("x-trigger-branch") ?? undefined; + const { projectRef, env } = parsedParams.data; + const triggerBranch = request.headers.get("x-trigger-branch") ?? undefined; - const runtimeEnv = await authenticatedEnvironmentForAuthentication( - authenticationResult, - projectRef, - env, - triggerBranch - ); + const runtimeEnv = await authenticatedEnvironmentForAuthentication( + authenticationResult, + projectRef, + env, + triggerBranch + ); - const parsedBody = RequestBodySchema.safeParse(await request.json()); + const parsedBody = RequestBodySchema.safeParse(await request.json()); - if (!parsedBody.success) { - return json( - { error: "Invalid request body", issues: parsedBody.error.issues }, - { status: 400 } - ); - } + if (!parsedBody.success) { + return json( + { error: "Invalid request body", issues: parsedBody.error.issues }, + { status: 400 } + ); + } - const claims = { - sub: runtimeEnv.id, - pub: true, - ...parsedBody.data.claims, - }; + const claims = { + sub: runtimeEnv.id, + pub: true, + ...parsedBody.data.claims, + }; - const jwt = await internal_generateJWT({ - secretKey: runtimeEnv.apiKey, - payload: claims, - expirationTime: parsedBody.data.expirationTime ?? "1h", - }); + const jwt = await internal_generateJWT({ + secretKey: runtimeEnv.apiKey, + payload: claims, + expirationTime: parsedBody.data.expirationTime ?? "1h", + }); - return json({ token: jwt }); + return json({ token: jwt }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to generate env JWT", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts index e0349aab558..218cc580dd3 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts @@ -7,6 +7,7 @@ import { authenticateRequest, branchNameFromRequest, } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; const ParamsSchema = z.object({ projectRef: z.string(), @@ -24,25 +25,31 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const { projectRef, env } = parsedParams.data; - const authenticationResult = await authenticateRequest(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); + try { + const authenticationResult = await authenticateRequest(request); + + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } + + const environment = await authenticatedEnvironmentForAuthentication( + authenticationResult, + projectRef, + env, + branchNameFromRequest(request) + ); + + const result: GetProjectEnvResponse = { + apiKey: environment.apiKey, + name: environment.project.name, + apiUrl: processEnv.API_ORIGIN ?? processEnv.APP_ORIGIN, + projectId: environment.project.id, + }; + + return json(result); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load project env", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } - - const environment = await authenticatedEnvironmentForAuthentication( - authenticationResult, - projectRef, - env, - branchNameFromRequest(request) - ); - - const result: GetProjectEnvResponse = { - apiKey: environment.apiKey, - name: environment.project.name, - apiUrl: processEnv.API_ORIGIN ?? processEnv.APP_ORIGIN, - projectId: environment.project.id, - }; - - return json(result); } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.workers.$tagName.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.workers.$tagName.ts index ddb398b4c21..07774339dc8 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.workers.$tagName.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.workers.$tagName.ts @@ -9,6 +9,7 @@ import { authenticatedEnvironmentForAuthentication, authenticateRequest, } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; const ParamsSchema = z.object({ projectRef: z.string(), @@ -23,34 +24,37 @@ const HeadersSchema = z.object({ type ParamsSchema = z.infer; export async function loader({ request, params }: LoaderFunctionArgs) { - const authenticationResult = await authenticateRequest(request, { - personalAccessToken: true, - organizationAccessToken: true, - apiKey: false, - }); + try { + const authenticationResult = await authenticateRequest(request, { + personalAccessToken: true, + organizationAccessToken: true, + apiKey: false, + }); - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } - const parsedParams = ParamsSchema.safeParse(params); + const parsedParams = ParamsSchema.safeParse(params); - if (!parsedParams.success) { - return json({ error: "Invalid Params" }, { status: 400 }); - } - const { projectRef, env } = parsedParams.data; + if (!parsedParams.success) { + return json({ error: "Invalid Params" }, { status: 400 }); + } + const { projectRef, env } = parsedParams.data; - const parsedHeaders = HeadersSchema.safeParse(Object.fromEntries(request.headers)); - const triggerBranch = parsedHeaders.success ? parsedHeaders.data["x-trigger-branch"] : undefined; + const parsedHeaders = HeadersSchema.safeParse(Object.fromEntries(request.headers)); + const triggerBranch = parsedHeaders.success + ? parsedHeaders.data["x-trigger-branch"] + : undefined; - const runtimeEnv = await authenticatedEnvironmentForAuthentication( - authenticationResult, - projectRef, - env, - triggerBranch - ); + const runtimeEnv = await authenticatedEnvironmentForAuthentication( + authenticationResult, + projectRef, + env, + triggerBranch + ); - const currentWorker = await findCurrentWorkerFromEnvironment( + const currentWorker = await findCurrentWorkerFromEnvironment( { id: runtimeEnv.id, type: runtimeEnv.type, @@ -109,5 +113,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) { urls, }; - return json(response); + return json(response); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load worker by tag", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts index a2f2dcf417f..91a19358185 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts @@ -15,6 +15,7 @@ const ParamsSchema = z.object({ }); export async function action({ request, params }: ActionFunctionArgs) { + try { const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); if (!authenticationResult) { @@ -92,4 +93,9 @@ export async function action({ request, params }: ActionFunctionArgs) { logger.error("Failed to create alert channel", { error }); return json({ error: "Something went wrong, please try again." }, { status: 500 }); } + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to create alert channel (outer)", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.$envSlug.$version.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.$envSlug.$version.ts index 6b044c9e833..e09bc510d91 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.$envSlug.$version.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.$envSlug.$version.ts @@ -6,6 +6,7 @@ import { authenticatedEnvironmentForAuthentication, branchNameFromRequest, } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; import zlib from "node:zlib"; const ParamsSchema = z.object({ @@ -21,44 +22,45 @@ export async function loader({ params, request }: LoaderFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - const authenticationResult = await authenticateRequest(request); + try { + const authenticationResult = await authenticateRequest(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const environment = await authenticatedEnvironmentForAuthentication( - authenticationResult, - parsedParams.data.projectRef, - parsedParams.data.envSlug, - branchNameFromRequest(request) - ); + const environment = await authenticatedEnvironmentForAuthentication( + authenticationResult, + parsedParams.data.projectRef, + parsedParams.data.envSlug, + branchNameFromRequest(request) + ); - // Find the background worker and tasks and files - const backgroundWorker = await prisma.backgroundWorker.findFirst({ - where: { - runtimeEnvironmentId: environment.id, - version: parsedParams.data.version, - }, - include: { - tasks: true, - files: { - include: { - tasks: { - select: { - slug: true, + // Find the background worker and tasks and files + const backgroundWorker = await prisma.backgroundWorker.findFirst({ + where: { + runtimeEnvironmentId: environment.id, + version: parsedParams.data.version, + }, + include: { + tasks: true, + files: { + include: { + tasks: { + select: { + slug: true, + }, }, }, }, }, - }, - }); + }); - if (!backgroundWorker) { - return json({ error: "Background worker not found" }, { status: 404 }); - } + if (!backgroundWorker) { + return json({ error: "Background worker not found" }, { status: 404 }); + } - return json({ + return json({ id: backgroundWorker.friendlyId, version: backgroundWorker.version, cliVersion: backgroundWorker.cliVersion, @@ -74,14 +76,19 @@ export async function loader({ params, request }: LoaderFunctionArgs) { retryConfig: task.retryConfig, queueConfig: task.queueConfig, })), - files: backgroundWorker.files.map((file) => ({ - id: file.friendlyId, - filePath: file.filePath, - contentHash: file.contentHash, - contents: decompressContent(file.contents), - tasks: Array.from(new Set(file.tasks.map((task) => task.slug))), - })), - }); + files: backgroundWorker.files.map((file) => ({ + id: file.friendlyId, + filePath: file.filePath, + contentHash: file.contentHash, + contents: decompressContent(file.contents), + tasks: Array.from(new Set(file.tasks.map((task) => task.slug))), + })), + }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load background worker", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } function decompressContent(compressedBuffer: Uint8Array): string { diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts index bc9842f0afa..b4b0384bf65 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts @@ -25,6 +25,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } + try { // Next authenticate the request const authenticationResult = await authenticateApiRequest(request); @@ -74,4 +75,9 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Failed to create background worker" }, { status: 500 }); } + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to create project background worker", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.dev-status.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.dev-status.ts index f5f632a8223..7d536042fe6 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.dev-status.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.dev-status.ts @@ -5,37 +5,44 @@ import { authenticatedEnvironmentForAuthentication, authenticateRequest, } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; const ParamsSchema = z.object({ projectRef: z.string(), }); export async function loader({ request, params }: LoaderFunctionArgs) { - const authenticationResult = await authenticateRequest(request, { - personalAccessToken: true, - organizationAccessToken: true, - apiKey: false, - }); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + try { + const authenticationResult = await authenticateRequest(request, { + personalAccessToken: true, + organizationAccessToken: true, + apiKey: false, + }); + + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } + + const parsedParams = ParamsSchema.safeParse(params); + + if (!parsedParams.success) { + return json({ error: "Invalid Params" }, { status: 400 }); + } + + const { projectRef } = parsedParams.data; + + const runtimeEnv = await authenticatedEnvironmentForAuthentication( + authenticationResult, + projectRef, + "dev" + ); + + const isConnected = await devPresence.isConnected(runtimeEnv.id); + + return json({ isConnected }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load dev status", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } - - const parsedParams = ParamsSchema.safeParse(params); - - if (!parsedParams.success) { - return json({ error: "Invalid Params" }, { status: 400 }); - } - - const { projectRef } = parsedParams.data; - - const runtimeEnv = await authenticatedEnvironmentForAuthentication( - authenticationResult, - projectRef, - "dev" - ); - - const isConnected = await devPresence.isConnected(runtimeEnv.id); - - return json({ isConnected }); } diff --git a/apps/webapp/app/routes/api.v1.projects.ts b/apps/webapp/app/routes/api.v1.projects.ts index 3a12417dce0..ec7d2828d03 100644 --- a/apps/webapp/app/routes/api.v1.projects.ts +++ b/apps/webapp/app/routes/api.v1.projects.ts @@ -8,6 +8,7 @@ import { authenticateApiRequestWithPersonalAccessToken } from "~/services/person export async function loader({ request }: LoaderFunctionArgs) { logger.info("get projects", { url: request.url }); + try { const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); if (!authenticationResult) { @@ -51,4 +52,9 @@ export async function loader({ request }: LoaderFunctionArgs) { })); return json(result); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to list projects", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.whoami.ts b/apps/webapp/app/routes/api.v1.whoami.ts index 0ebb70b4491..f0dea9a7a57 100644 --- a/apps/webapp/app/routes/api.v1.whoami.ts +++ b/apps/webapp/app/routes/api.v1.whoami.ts @@ -2,32 +2,39 @@ import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { prisma } from "~/db.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; export async function loader({ request }: LoaderFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + try { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const environmentWithUser = await prisma.runtimeEnvironment.findUnique({ - select: { - orgMember: { - select: { - userId: true, + const environmentWithUser = await prisma.runtimeEnvironment.findUnique({ + select: { + orgMember: { + select: { + userId: true, + }, }, }, - }, - where: { - id: authenticationResult.environment.id, - }, - }); + where: { + id: authenticationResult.environment.id, + }, + }); - const result = { - ...authenticationResult.environment, - userId: environmentWithUser?.orgMember?.userId, - }; + const result = { + ...authenticationResult.environment, + userId: environmentWithUser?.orgMember?.userId, + }; - return json(result); + return json(result); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load whoami", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } From 7529ea8753391ae615ae440b6d1a3c0db16dc33f Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Mon, 18 May 2026 19:19:26 +0100 Subject: [PATCH 3/6] fix(webapp): preserve preexisting 400 status in two catches, just hide leak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous sweep collapsed two existing 400 branches to 500 along with the leak-sanitization. Keep the 400 status — the inner branches were catching errors that callers had been informed of via 400, and clients may depend on that. Just replace the leaky `error.message` body with a generic per-route message. - api.v1.orgs.$orgParam.projects.ts (createProject failure): 500 → 400 with `"Failed to create project"`. - api.v1.authorization-code.ts (instanceof Error branch): 500 → 400 with `"Failed to create authorization code"`. Both branches probed locally: synthetic failure forces the path and the response is the documented 400 + generic body. --- apps/webapp/app/routes/api.v1.authorization-code.ts | 2 +- apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.authorization-code.ts b/apps/webapp/app/routes/api.v1.authorization-code.ts index 3f90d72b1f0..2e5c1aadf25 100644 --- a/apps/webapp/app/routes/api.v1.authorization-code.ts +++ b/apps/webapp/app/routes/api.v1.authorization-code.ts @@ -32,7 +32,7 @@ export async function action({ request }: ActionFunctionArgs) { error: error.message, }); - return json({ error: "Internal Server Error" }, { status: 500 }); + return json({ error: "Failed to create authorization code" }, { status: 400 }); } return json({ error: "Something went wrong" }, { status: 500 }); diff --git a/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts b/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts index 443982ed7b2..6443007c388 100644 --- a/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts +++ b/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts @@ -118,7 +118,7 @@ export async function action({ request, params }: ActionFunctionArgs) { if (error) { logger.error("Failed to create project", { error }); - return json({ error: "Internal Server Error" }, { status: 500 }); + return json({ error: "Failed to create project" }, { status: 400 }); } const result: GetProjectResponseBody = { From a2e9e80d15c4352d7929c05ed5c4905ad75f9079 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Mon, 18 May 2026 19:50:17 +0100 Subject: [PATCH 4/6] style(webapp): indent wrapped bodies under outer try --- apps/webapp/app/routes/api.v1.artifacts.ts | 130 ++++++------ apps/webapp/app/routes/api.v1.auth.jwt.ts | 64 +++--- .../api.v1.batches.$batchParam.results.ts | 42 ++-- ...yments.$deploymentId.background-workers.ts | 88 ++++---- ...api.v1.deployments.$deploymentId.cancel.ts | 84 ++++---- .../api.v1.deployments.$deploymentId.fail.ts | 42 ++-- ...i.v1.deployments.$deploymentId.finalize.ts | 58 ++--- ...loymentId.generate-registry-credentials.ts | 132 ++++++------ ...i.v1.deployments.$deploymentId.progress.ts | 100 ++++----- .../api.v1.deployments.$deploymentId.ts | 114 +++++----- ....deployments.$deploymentVersion.promote.ts | 72 +++---- .../app/routes/api.v1.deployments.latest.ts | 56 ++--- .../routes/api.v1.orgs.$orgParam.projects.ts | 192 ++++++++--------- ....projects.$projectParam.vercel.projects.ts | 198 +++++++++--------- apps/webapp/app/routes/api.v1.orgs.ts | 44 ++-- ...i.v1.projects.$projectRef.alertChannels.ts | 126 +++++------ ...projects.$projectRef.background-workers.ts | 78 +++---- apps/webapp/app/routes/api.v1.projects.ts | 70 +++---- 18 files changed, 845 insertions(+), 845 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.artifacts.ts b/apps/webapp/app/routes/api.v1.artifacts.ts index 64ed2ceda4b..c74c66a222d 100644 --- a/apps/webapp/app/routes/api.v1.artifacts.ts +++ b/apps/webapp/app/routes/api.v1.artifacts.ts @@ -14,81 +14,81 @@ export async function action({ request }: ActionFunctionArgs) { } try { - const authenticationResult = await authenticateRequest(request, { - apiKey: true, - organizationAccessToken: false, - personalAccessToken: false, - }); + const authenticationResult = await authenticateRequest(request, { + apiKey: true, + organizationAccessToken: false, + personalAccessToken: false, + }); - if (!authenticationResult || !authenticationResult.result.ok) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult || !authenticationResult.result.ok) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const [, rawBody] = await tryCatch(request.json()); - const body = CreateArtifactRequestBody.safeParse(rawBody ?? {}); + const [, rawBody] = await tryCatch(request.json()); + const body = CreateArtifactRequestBody.safeParse(rawBody ?? {}); - if (!body.success) { - return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); - } + if (!body.success) { + return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); + } - const { environment: authenticatedEnv } = authenticationResult.result; + const { environment: authenticatedEnv } = authenticationResult.result; - const service = new ArtifactsService(); - return await service - .createArtifact(body.data.type, authenticatedEnv, body.data.contentLength) - .match( - (result) => { - return json( - { - artifactKey: result.artifactKey, - uploadUrl: result.uploadUrl, - uploadFields: result.uploadFields, - expiresAt: result.expiresAt.toISOString(), - } satisfies CreateArtifactResponseBody, - { status: 201 } - ); - }, - (error) => { - switch (error.type) { - case "artifact_size_exceeds_limit": { - logger.warn("Artifact size exceeds limit", { error }); - const sizeMB = parseFloat((error.contentLength / (1024 * 1024)).toFixed(1)); - const limitMB = parseFloat((error.sizeLimit / (1024 * 1024)).toFixed(1)); + const service = new ArtifactsService(); + return await service + .createArtifact(body.data.type, authenticatedEnv, body.data.contentLength) + .match( + (result) => { + return json( + { + artifactKey: result.artifactKey, + uploadUrl: result.uploadUrl, + uploadFields: result.uploadFields, + expiresAt: result.expiresAt.toISOString(), + } satisfies CreateArtifactResponseBody, + { status: 201 } + ); + }, + (error) => { + switch (error.type) { + case "artifact_size_exceeds_limit": { + logger.warn("Artifact size exceeds limit", { error }); + const sizeMB = parseFloat((error.contentLength / (1024 * 1024)).toFixed(1)); + const limitMB = parseFloat((error.sizeLimit / (1024 * 1024)).toFixed(1)); - let errorMessage; + let errorMessage; - switch (body.data.type) { - case "deployment_context": - errorMessage = `Artifact size (${sizeMB} MB) exceeds the allowed limit of ${limitMB} MB. Make sure you are in the correct directory of your Trigger.dev project. Reach out to us if you are seeing this error consistently.`; - break; - default: - body.data.type satisfies never; - errorMessage = `Artifact size (${sizeMB} MB) exceeds the allowed limit of ${limitMB} MB`; + switch (body.data.type) { + case "deployment_context": + errorMessage = `Artifact size (${sizeMB} MB) exceeds the allowed limit of ${limitMB} MB. Make sure you are in the correct directory of your Trigger.dev project. Reach out to us if you are seeing this error consistently.`; + break; + default: + body.data.type satisfies never; + errorMessage = `Artifact size (${sizeMB} MB) exceeds the allowed limit of ${limitMB} MB`; + } + return json( + { + error: errorMessage, + }, + { status: 400 } + ); + } + case "failed_to_create_presigned_post": { + logger.error("Failed to create presigned POST", { error }); + return json({ error: "Failed to generate artifact upload URL" }, { status: 500 }); + } + case "artifacts_bucket_not_configured": { + logger.error("Artifacts bucket not configured", { error }); + return json({ error: "Internal server error" }, { status: 500 }); + } + default: { + error satisfies never; + logger.error("Failed creating artifact", { error }); + return json({ error: "Internal server error" }, { status: 500 }); } - return json( - { - error: errorMessage, - }, - { status: 400 } - ); - } - case "failed_to_create_presigned_post": { - logger.error("Failed to create presigned POST", { error }); - return json({ error: "Failed to generate artifact upload URL" }, { status: 500 }); - } - case "artifacts_bucket_not_configured": { - logger.error("Artifacts bucket not configured", { error }); - return json({ error: "Internal server error" }, { status: 500 }); - } - default: { - error satisfies never; - logger.error("Failed creating artifact", { error }); - return json({ error: "Internal server error" }, { status: 500 }); } } - } - ); + ); } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to create artifact", { error }); diff --git a/apps/webapp/app/routes/api.v1.auth.jwt.ts b/apps/webapp/app/routes/api.v1.auth.jwt.ts index 76e78fe910a..c38cdeb14ac 100644 --- a/apps/webapp/app/routes/api.v1.auth.jwt.ts +++ b/apps/webapp/app/routes/api.v1.auth.jwt.ts @@ -16,38 +16,38 @@ const RequestBodySchema = z.object({ export async function action({ request }: LoaderFunctionArgs) { try { - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const parsedBody = RequestBodySchema.safeParse(await request.json()); - - if (!parsedBody.success) { - return json( - { error: "Invalid request body", issues: parsedBody.error.issues }, - { status: 400 } - ); - } - - const claims = { - sub: authenticationResult.environment.id, - pub: true, - ...parsedBody.data.claims, - }; - - // Sign with the environment's current canonical key, not the raw header key, - // so JWTs minted with a revoked (grace-window) key still validate — validation - // in jwtAuth.server.ts uses environment.apiKey. - const jwt = await internal_generateJWT({ - secretKey: authenticationResult.environment.apiKey, - payload: claims, - expirationTime: parsedBody.data.expirationTime ?? "1h", - }); - - return json({ token: jwt }); + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); + + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } + + const parsedBody = RequestBodySchema.safeParse(await request.json()); + + if (!parsedBody.success) { + return json( + { error: "Invalid request body", issues: parsedBody.error.issues }, + { status: 400 } + ); + } + + const claims = { + sub: authenticationResult.environment.id, + pub: true, + ...parsedBody.data.claims, + }; + + // Sign with the environment's current canonical key, not the raw header key, + // so JWTs minted with a revoked (grace-window) key still validate — validation + // in jwtAuth.server.ts uses environment.apiKey. + const jwt = await internal_generateJWT({ + secretKey: authenticationResult.environment.apiKey, + payload: claims, + expirationTime: parsedBody.data.expirationTime ?? "1h", + }); + + return json({ token: jwt }); } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to mint auth jwt", { error }); diff --git a/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts b/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts index 002914abd07..edb19736691 100644 --- a/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts +++ b/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts @@ -13,34 +13,34 @@ const ParamsSchema = z.object({ export async function loader({ request, params }: LoaderFunctionArgs) { try { - // Authenticate the request - const authenticationResult = await authenticateApiRequest(request); + // Authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing API Key" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing API Key" }, { status: 401 }); + } - const parsed = ParamsSchema.safeParse(params); + const parsed = ParamsSchema.safeParse(params); - if (!parsed.success) { - return json({ error: "Invalid or missing run ID" }, { status: 400 }); - } + if (!parsed.success) { + return json({ error: "Invalid or missing run ID" }, { status: 400 }); + } - const { batchParam } = parsed.data; + const { batchParam } = parsed.data; - try { - const presenter = new ApiBatchResultsPresenter(); - const result = await presenter.call(batchParam, authenticationResult.environment); + try { + const presenter = new ApiBatchResultsPresenter(); + const result = await presenter.call(batchParam, authenticationResult.environment); - if (!result) { - return json({ error: "Batch not found" }, { status: 404 }); - } + if (!result) { + return json({ error: "Batch not found" }, { status: 404 }); + } - return json(result); - } catch (error) { - logger.error("Failed to load batch results", { error }); - return json({ error: "Something went wrong, please try again." }, { status: 500 }); - } + return json(result); + } catch (error) { + logger.error("Failed to load batch results", { error }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); + } } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to load batch results (outer)", { error }); diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts index 3b1c3ec4505..26503c14f9d 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts @@ -24,59 +24,59 @@ export async function action({ request, params }: ActionFunctionArgs) { } try { - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const authenticatedEnv = authenticationResult.environment; + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const { deploymentId } = parsedParams.data; + const authenticatedEnv = authenticationResult.environment; - const rawBody = await request.json(); - const body = CreateBackgroundWorkerRequestBody.safeParse(rawBody); + const { deploymentId } = parsedParams.data; - if (!body.success) { - return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); - } + const rawBody = await request.json(); + const body = CreateBackgroundWorkerRequestBody.safeParse(rawBody); - const service = new CreateDeploymentBackgroundWorkerServiceV4(); + if (!body.success) { + return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); + } - try { - const backgroundWorker = await service.call(authenticatedEnv, deploymentId, body.data); + const service = new CreateDeploymentBackgroundWorkerServiceV4(); + + try { + const backgroundWorker = await service.call(authenticatedEnv, deploymentId, body.data); + + if (!backgroundWorker) { + return json({ error: "Failed to create background worker" }, { status: 500 }); + } + + return json( + { + id: backgroundWorker.friendlyId, + version: backgroundWorker.version, + contentHash: backgroundWorker.contentHash, + }, + { status: 200 } + ); + } catch (e) { + // Customer-facing validation failures (invalid task config, customer cron + // expression, etc.). The handler returns 4xx with the message; system + // handles it gracefully, no alert needed. + if (e instanceof ServiceValidationError) { + logger.warn("Failed to create background worker", { error: e.message }); + return json({ error: e.message }, { status: e.status ?? 400 }); + } + if (e instanceof CreateDeclarativeScheduleError) { + logger.warn("Failed to create background worker", { error: e.message }); + return json({ error: e.message }, { status: 400 }); + } + + logger.error("Failed to create background worker", { error: e }); - if (!backgroundWorker) { return json({ error: "Failed to create background worker" }, { status: 500 }); } - - return json( - { - id: backgroundWorker.friendlyId, - version: backgroundWorker.version, - contentHash: backgroundWorker.contentHash, - }, - { status: 200 } - ); - } catch (e) { - // Customer-facing validation failures (invalid task config, customer cron - // expression, etc.). The handler returns 4xx with the message; system - // handles it gracefully, no alert needed. - if (e instanceof ServiceValidationError) { - logger.warn("Failed to create background worker", { error: e.message }); - return json({ error: e.message }, { status: e.status ?? 400 }); - } - if (e instanceof CreateDeclarativeScheduleError) { - logger.warn("Failed to create background worker", { error: e.message }); - return json({ error: e.message }, { status: 400 }); - } - - logger.error("Failed to create background worker", { error: e }); - - return json({ error: "Failed to create background worker" }, { status: 500 }); - } } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to create deployment background worker", { error }); diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts index 217bcd6c03e..56b93fe17e3 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts @@ -21,55 +21,55 @@ export async function action({ request, params }: ActionFunctionArgs) { } try { - const authenticationResult = await authenticateRequest(request, { - apiKey: true, - organizationAccessToken: false, - personalAccessToken: false, - }); + const authenticationResult = await authenticateRequest(request, { + apiKey: true, + organizationAccessToken: false, + personalAccessToken: false, + }); - if (!authenticationResult || !authenticationResult.result.ok) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult || !authenticationResult.result.ok) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const { environment: authenticatedEnv } = authenticationResult.result; - const { deploymentId } = parsedParams.data; + const { environment: authenticatedEnv } = authenticationResult.result; + const { deploymentId } = parsedParams.data; - const [, rawBody] = await tryCatch(request.json()); - const body = CancelDeploymentRequestBody.safeParse(rawBody ?? {}); + const [, rawBody] = await tryCatch(request.json()); + const body = CancelDeploymentRequestBody.safeParse(rawBody ?? {}); - if (!body.success) { - return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); - } + if (!body.success) { + return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); + } - const deploymentService = new DeploymentService(); + const deploymentService = new DeploymentService(); - return await deploymentService - .cancelDeployment(authenticatedEnv, deploymentId, { - canceledReason: body.data.reason, - }) - .match( - () => { - return new Response(null, { status: 204 }); - }, - (error) => { - switch (error.type) { - case "deployment_not_found": - return json({ error: "Deployment not found" }, { status: 404 }); - case "failed_to_delete_deployment_timeout": - return new Response(null, { status: 204 }); // not a critical error, ignore - case "deployment_cannot_be_cancelled": - return json( - { error: "Deployment is already in a final state and cannot be canceled" }, - { status: 409 } - ); - case "other": - default: - error.type satisfies "other"; - return json({ error: "Internal server error" }, { status: 500 }); + return await deploymentService + .cancelDeployment(authenticatedEnv, deploymentId, { + canceledReason: body.data.reason, + }) + .match( + () => { + return new Response(null, { status: 204 }); + }, + (error) => { + switch (error.type) { + case "deployment_not_found": + return json({ error: "Deployment not found" }, { status: 404 }); + case "failed_to_delete_deployment_timeout": + return new Response(null, { status: 204 }); // not a critical error, ignore + case "deployment_cannot_be_cancelled": + return json( + { error: "Deployment is already in a final state and cannot be canceled" }, + { status: 409 } + ); + case "other": + default: + error.type satisfies "other"; + return json({ error: "Internal server error" }, { status: 500 }); + } } - } - ); + ); } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to cancel deployment", { error }); diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts index a71cbdcf7f9..5fdb6bfa9a0 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts @@ -22,34 +22,34 @@ export async function action({ request, params }: ActionFunctionArgs) { } try { - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const authenticatedEnv = authenticationResult.environment; + const authenticatedEnv = authenticationResult.environment; - const { deploymentId } = parsedParams.data; + const { deploymentId } = parsedParams.data; - const rawBody = await request.json(); - const body = FailDeploymentRequestBody.safeParse(rawBody); + const rawBody = await request.json(); + const body = FailDeploymentRequestBody.safeParse(rawBody); - if (!body.success) { - return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); - } + if (!body.success) { + return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); + } - const service = new FailDeploymentService(); - await service.call(authenticatedEnv, deploymentId, body.data); + const service = new FailDeploymentService(); + await service.call(authenticatedEnv, deploymentId, body.data); - return json( - { - id: deploymentId, - }, - { status: 200 } - ); + return json( + { + id: deploymentId, + }, + { status: 200 } + ); } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to fail deployment", { error }); diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts index 44ded1da718..c1ce30bbe79 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts @@ -23,43 +23,43 @@ export async function action({ request, params }: ActionFunctionArgs) { } try { - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const authenticatedEnv = authenticationResult.environment; + const authenticatedEnv = authenticationResult.environment; - const { deploymentId } = parsedParams.data; + const { deploymentId } = parsedParams.data; - const rawBody = await request.json(); - const body = FinalizeDeploymentRequestBody.safeParse(rawBody); + const rawBody = await request.json(); + const body = FinalizeDeploymentRequestBody.safeParse(rawBody); - if (!body.success) { - return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); - } + if (!body.success) { + return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); + } - try { - const service = new FinalizeDeploymentService(); - await service.call(authenticatedEnv, deploymentId, body.data); + try { + const service = new FinalizeDeploymentService(); + await service.call(authenticatedEnv, deploymentId, body.data); - return json( - { - id: deploymentId, - }, - { status: 200 } - ); - } catch (error) { - if (error instanceof ServiceValidationError) { - return json({ error: error.message }, { status: 400 }); - } + return json( + { + id: deploymentId, + }, + { status: 200 } + ); + } catch (error) { + if (error instanceof ServiceValidationError) { + return json({ error: error.message }, { status: 400 }); + } - logger.error("Error finalizing deployment", { error }); - return json({ error: "Internal server error" }, { status: 500 }); - } + logger.error("Error finalizing deployment", { error }); + return json({ error: "Internal server error" }, { status: 500 }); + } } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to finalize deployment", { error }); diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts index 35aa5f8819a..89aaad4301e 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts @@ -25,79 +25,79 @@ export async function action({ request, params }: ActionFunctionArgs) { } try { - const authenticationResult = await authenticateRequest(request, { - apiKey: true, - organizationAccessToken: false, - personalAccessToken: false, - }); + const authenticationResult = await authenticateRequest(request, { + apiKey: true, + organizationAccessToken: false, + personalAccessToken: false, + }); - if (!authenticationResult || !authenticationResult.result.ok) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult || !authenticationResult.result.ok) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const { environment: authenticatedEnv } = authenticationResult.result; - const { deploymentId } = parsedParams.data; + const { environment: authenticatedEnv } = authenticationResult.result; + const { deploymentId } = parsedParams.data; - const [, rawBody] = await tryCatch(request.json()); - const body = ProgressDeploymentRequestBody.safeParse(rawBody ?? {}); + const [, rawBody] = await tryCatch(request.json()); + const body = ProgressDeploymentRequestBody.safeParse(rawBody ?? {}); - if (!body.success) { - return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); - } + if (!body.success) { + return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); + } - const deploymentService = new DeploymentService(); + const deploymentService = new DeploymentService(); - return await deploymentService.generateRegistryCredentials(authenticatedEnv, deploymentId).match( - (result) => { - return json( - { - username: result.username, - password: result.password, - expiresAt: result.expiresAt.toISOString(), - repositoryUri: result.repositoryUri, - } satisfies GenerateRegistryCredentialsResponseBody, - { status: 200 } - ); - }, - (error) => { - switch (error.type) { - case "deployment_not_found": - return json({ error: "Deployment not found" }, { status: 404 }); - case "deployment_has_no_image_reference": - logger.error( - "Failed to generate registry credentials: deployment_has_no_image_reference", - { deploymentId } - ); - return json({ error: "Deployment has no image reference" }, { status: 409 }); - case "deployment_is_already_final": - return json( - { error: "Failed to generate registry credentials: deployment_is_already_final" }, - { status: 409 } - ); - case "missing_registry_credentials": - logger.error("Failed to generate registry credentials: missing_registry_credentials", { - deploymentId, - }); - return json({ error: "Missing registry credentials" }, { status: 409 }); - case "registry_not_supported": - logger.error("Failed to generate registry credentials: registry_not_supported", { - deploymentId, - }); - return json({ error: "Registry not supported" }, { status: 409 }); - case "registry_region_not_supported": - logger.error("Failed to generate registry credentials: registry_region_not_supported", { - deploymentId, - }); - return json({ error: "Registry region not supported" }, { status: 409 }); - case "other": - default: - error.type satisfies "other"; - logger.error("Failed to generate registry credentials", { error: error.cause }); - return json({ error: "Internal server error" }, { status: 500 }); + return await deploymentService.generateRegistryCredentials(authenticatedEnv, deploymentId).match( + (result) => { + return json( + { + username: result.username, + password: result.password, + expiresAt: result.expiresAt.toISOString(), + repositoryUri: result.repositoryUri, + } satisfies GenerateRegistryCredentialsResponseBody, + { status: 200 } + ); + }, + (error) => { + switch (error.type) { + case "deployment_not_found": + return json({ error: "Deployment not found" }, { status: 404 }); + case "deployment_has_no_image_reference": + logger.error( + "Failed to generate registry credentials: deployment_has_no_image_reference", + { deploymentId } + ); + return json({ error: "Deployment has no image reference" }, { status: 409 }); + case "deployment_is_already_final": + return json( + { error: "Failed to generate registry credentials: deployment_is_already_final" }, + { status: 409 } + ); + case "missing_registry_credentials": + logger.error("Failed to generate registry credentials: missing_registry_credentials", { + deploymentId, + }); + return json({ error: "Missing registry credentials" }, { status: 409 }); + case "registry_not_supported": + logger.error("Failed to generate registry credentials: registry_not_supported", { + deploymentId, + }); + return json({ error: "Registry not supported" }, { status: 409 }); + case "registry_region_not_supported": + logger.error("Failed to generate registry credentials: registry_region_not_supported", { + deploymentId, + }); + return json({ error: "Registry region not supported" }, { status: 409 }); + case "other": + default: + error.type satisfies "other"; + logger.error("Failed to generate registry credentials", { error: error.cause }); + return json({ error: "Internal server error" }, { status: 500 }); + } } - } - ); + ); } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to generate registry credentials", { error }); diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts index 2680065d3ff..671f42606a2 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts @@ -21,64 +21,64 @@ export async function action({ request, params }: ActionFunctionArgs) { } try { - const authenticationResult = await authenticateRequest(request, { - apiKey: true, - organizationAccessToken: false, - personalAccessToken: false, - }); + const authenticationResult = await authenticateRequest(request, { + apiKey: true, + organizationAccessToken: false, + personalAccessToken: false, + }); - if (!authenticationResult || !authenticationResult.result.ok) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult || !authenticationResult.result.ok) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const { environment: authenticatedEnv } = authenticationResult.result; - const { deploymentId } = parsedParams.data; + const { environment: authenticatedEnv } = authenticationResult.result; + const { deploymentId } = parsedParams.data; - const [, rawBody] = await tryCatch(request.json()); - const body = ProgressDeploymentRequestBody.safeParse(rawBody ?? {}); + const [, rawBody] = await tryCatch(request.json()); + const body = ProgressDeploymentRequestBody.safeParse(rawBody ?? {}); - if (!body.success) { - return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); - } + if (!body.success) { + return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); + } - const deploymentService = new DeploymentService(); + const deploymentService = new DeploymentService(); - return await deploymentService - .progressDeployment(authenticatedEnv, deploymentId, { - contentHash: body.data.contentHash, - git: body.data.gitMeta, - runtime: body.data.runtime, - buildServerMetadata: body.data.buildServerMetadata, - }) - .match( - () => { - return new Response(null, { status: 204 }); - }, - (error) => { - switch (error.type) { - case "failed_to_extend_deployment_timeout": { - logger.warn("Failed to extend deployment timeout", { error: error.cause }); - return new Response(null, { status: 204 }); // ignore these errors for now - } - case "deployment_not_found": - return json({ error: "Deployment not found" }, { status: 404 }); - case "deployment_cannot_be_progressed": - return json( - { error: "Deployment is not in a progressable state (PENDING or INSTALLING)" }, - { status: 409 } - ); - case "failed_to_create_remote_build": { - logger.error("Failed to create remote Depot build", { error: error.cause }); - return json({ error: "Failed to create remote build" }, { status: 500 }); + return await deploymentService + .progressDeployment(authenticatedEnv, deploymentId, { + contentHash: body.data.contentHash, + git: body.data.gitMeta, + runtime: body.data.runtime, + buildServerMetadata: body.data.buildServerMetadata, + }) + .match( + () => { + return new Response(null, { status: 204 }); + }, + (error) => { + switch (error.type) { + case "failed_to_extend_deployment_timeout": { + logger.warn("Failed to extend deployment timeout", { error: error.cause }); + return new Response(null, { status: 204 }); // ignore these errors for now + } + case "deployment_not_found": + return json({ error: "Deployment not found" }, { status: 404 }); + case "deployment_cannot_be_progressed": + return json( + { error: "Deployment is not in a progressable state (PENDING or INSTALLING)" }, + { status: 409 } + ); + case "failed_to_create_remote_build": { + logger.error("Failed to create remote Depot build", { error: error.cause }); + return json({ error: "Failed to create remote build" }, { status: 500 }); + } + case "other": + default: + error.type satisfies "other"; + return json({ error: "Internal server error" }, { status: 500 }); } - case "other": - default: - error.type satisfies "other"; - return json({ error: "Internal server error" }, { status: 500 }); } - } - ); + ); } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to progress deployment", { error }); diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.ts index fd60fb9f4e6..dd16fabf24c 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.ts @@ -17,72 +17,72 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } try { - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const authenticatedEnv = authenticationResult.environment; + const authenticatedEnv = authenticationResult.environment; - const { deploymentId } = parsedParams.data; + const { deploymentId } = parsedParams.data; - const deployment = await prisma.workerDeployment.findFirst({ - where: { - friendlyId: deploymentId, - environmentId: authenticatedEnv.id, - }, - include: { - worker: { - include: { - tasks: true, + const deployment = await prisma.workerDeployment.findFirst({ + where: { + friendlyId: deploymentId, + environmentId: authenticatedEnv.id, + }, + include: { + worker: { + include: { + tasks: true, + }, }, + integrationDeployments: true, }, - integrationDeployments: true, - }, - }); + }); - if (!deployment) { - return json({ error: "Deployment not found" }, { status: 404 }); - } + if (!deployment) { + return json({ error: "Deployment not found" }, { status: 404 }); + } - return json({ - id: deployment.friendlyId, - status: deployment.status, - contentHash: deployment.contentHash, - shortCode: deployment.shortCode, - version: deployment.version, - imageReference: deployment.imageReference, - imagePlatform: deployment.imagePlatform, - commitSHA: deployment.commitSHA, - externalBuildData: - deployment.externalBuildData as GetDeploymentResponseBody["externalBuildData"], - errorData: deployment.errorData as GetDeploymentResponseBody["errorData"], - worker: deployment.worker - ? { - id: deployment.worker.friendlyId, - version: deployment.worker.version, - tasks: deployment.worker.tasks.map((task) => ({ - id: task.friendlyId, - slug: task.slug, - filePath: task.filePath, - exportName: task.exportName ?? "@deprecated", - })), - } - : undefined, - integrationDeployments: - deployment.integrationDeployments.length > 0 - ? deployment.integrationDeployments.map((id) => ({ - id: id.id, - integrationName: id.integrationName, - integrationDeploymentId: id.integrationDeploymentId, - commitSHA: id.commitSHA, - createdAt: id.createdAt, - })) + return json({ + id: deployment.friendlyId, + status: deployment.status, + contentHash: deployment.contentHash, + shortCode: deployment.shortCode, + version: deployment.version, + imageReference: deployment.imageReference, + imagePlatform: deployment.imagePlatform, + commitSHA: deployment.commitSHA, + externalBuildData: + deployment.externalBuildData as GetDeploymentResponseBody["externalBuildData"], + errorData: deployment.errorData as GetDeploymentResponseBody["errorData"], + worker: deployment.worker + ? { + id: deployment.worker.friendlyId, + version: deployment.worker.version, + tasks: deployment.worker.tasks.map((task) => ({ + id: task.friendlyId, + slug: task.slug, + filePath: task.filePath, + exportName: task.exportName ?? "@deprecated", + })), + } : undefined, - } satisfies GetDeploymentResponseBody); + integrationDeployments: + deployment.integrationDeployments.length > 0 + ? deployment.integrationDeployments.map((id) => ({ + id: id.id, + integrationName: id.integrationName, + integrationDeploymentId: id.integrationDeploymentId, + commitSHA: id.commitSHA, + createdAt: id.createdAt, + })) + : undefined, + } satisfies GetDeploymentResponseBody); } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to load deployment", { error }); diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentVersion.promote.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentVersion.promote.ts index 10f9c655a9d..9f3b4cad185 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentVersion.promote.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentVersion.promote.ts @@ -23,51 +23,51 @@ export async function action({ request, params }: ActionFunctionArgs) { } try { - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const authenticatedEnv = authenticationResult.environment; + const authenticatedEnv = authenticationResult.environment; - const url = new URL(request.url); - const allowRollbacks = url.searchParams.get("allowRollbacks") === "true"; + const url = new URL(request.url); + const allowRollbacks = url.searchParams.get("allowRollbacks") === "true"; - const { deploymentVersion } = parsedParams.data; + const { deploymentVersion } = parsedParams.data; - const deployment = await prisma.workerDeployment.findFirst({ - where: { - version: deploymentVersion, - environmentId: authenticatedEnv.id, - }, - }); + const deployment = await prisma.workerDeployment.findFirst({ + where: { + version: deploymentVersion, + environmentId: authenticatedEnv.id, + }, + }); - if (!deployment) { - return json({ error: "Deployment not found" }, { status: 404 }); - } + if (!deployment) { + return json({ error: "Deployment not found" }, { status: 404 }); + } - try { - const service = new ChangeCurrentDeploymentService(); - await service.call(deployment, "promote", allowRollbacks); + try { + const service = new ChangeCurrentDeploymentService(); + await service.call(deployment, "promote", allowRollbacks); - return json( - { - id: deployment.friendlyId, - version: deployment.version, - shortCode: deployment.shortCode, - }, - { status: 200 } - ); - } catch (error) { - if (error instanceof ServiceValidationError) { - return json({ error: error.message }, { status: 400 }); - } else { - return json({ error: "Failed to promote deployment" }, { status: 500 }); + return json( + { + id: deployment.friendlyId, + version: deployment.version, + shortCode: deployment.shortCode, + }, + { status: 200 } + ); + } catch (error) { + if (error instanceof ServiceValidationError) { + return json({ error: error.message }, { status: 400 }); + } else { + return json({ error: "Failed to promote deployment" }, { status: 500 }); + } } - } } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to promote deployment", { error }); diff --git a/apps/webapp/app/routes/api.v1.deployments.latest.ts b/apps/webapp/app/routes/api.v1.deployments.latest.ts index 2c50964f9a0..b8dcb667856 100644 --- a/apps/webapp/app/routes/api.v1.deployments.latest.ts +++ b/apps/webapp/app/routes/api.v1.deployments.latest.ts @@ -6,39 +6,39 @@ import { logger } from "~/services/logger.server"; export async function loader({ request }: LoaderFunctionArgs) { try { - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const authenticatedEnv = authenticationResult.environment; + const authenticatedEnv = authenticationResult.environment; - const deployment = await prisma.workerDeployment.findFirst({ - where: { - type: WorkerInstanceGroupType.UNMANAGED, - environmentId: authenticatedEnv.id, - }, - orderBy: { - createdAt: "desc", - }, - }); + const deployment = await prisma.workerDeployment.findFirst({ + where: { + type: WorkerInstanceGroupType.UNMANAGED, + environmentId: authenticatedEnv.id, + }, + orderBy: { + createdAt: "desc", + }, + }); - if (!deployment) { - return json({ error: "Deployment not found" }, { status: 404 }); - } + if (!deployment) { + return json({ error: "Deployment not found" }, { status: 404 }); + } - return json({ - id: deployment.friendlyId, - status: deployment.status, - contentHash: deployment.contentHash, - shortCode: deployment.shortCode, - version: deployment.version, - imageReference: deployment.imageReference, - errorData: deployment.errorData, - }); + return json({ + id: deployment.friendlyId, + status: deployment.status, + contentHash: deployment.contentHash, + shortCode: deployment.shortCode, + version: deployment.version, + imageReference: deployment.imageReference, + errorData: deployment.errorData, + }); } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to load latest deployment", { error }); diff --git a/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts b/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts index 6443007c388..47eb82c6930 100644 --- a/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts +++ b/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts @@ -21,52 +21,52 @@ export async function loader({ request, params }: LoaderFunctionArgs) { logger.info("get projects", { url: request.url }); try { - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); - } - - const { orgParam } = ParamsSchema.parse(params); - - const projects = await prisma.project.findMany({ - where: { - organization: { - ...orgParamWhereClause(orgParam), - deletedAt: null, - members: { - some: { - userId: authenticationResult.userId, + const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); + + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } + + const { orgParam } = ParamsSchema.parse(params); + + const projects = await prisma.project.findMany({ + where: { + organization: { + ...orgParamWhereClause(orgParam), + deletedAt: null, + members: { + some: { + userId: authenticationResult.userId, + }, }, }, + version: "V3", + deletedAt: null, }, - version: "V3", - deletedAt: null, - }, - include: { - organization: true, - }, - }); - - if (!projects) { - return json({ error: "Projects not found" }, { status: 404 }); - } + include: { + organization: true, + }, + }); + + if (!projects) { + return json({ error: "Projects not found" }, { status: 404 }); + } + + const result: GetProjectsResponseBody = projects.map((project) => ({ + id: project.id, + externalRef: project.externalRef, + name: project.name, + slug: project.slug, + createdAt: project.createdAt, + organization: { + id: project.organization.id, + title: project.organization.title, + slug: project.organization.slug, + createdAt: project.organization.createdAt, + }, + })); - const result: GetProjectsResponseBody = projects.map((project) => ({ - id: project.id, - externalRef: project.externalRef, - name: project.name, - slug: project.slug, - createdAt: project.createdAt, - organization: { - id: project.organization.id, - title: project.organization.title, - slug: project.organization.slug, - createdAt: project.organization.createdAt, - }, - })); - - return json(result); + return json(result); } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to list org projects", { error }); @@ -76,66 +76,66 @@ export async function loader({ request, params }: LoaderFunctionArgs) { export async function action({ request, params }: ActionFunctionArgs) { try { - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); + const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } - const { orgParam } = ParamsSchema.parse(params); + const { orgParam } = ParamsSchema.parse(params); - const organization = await prisma.organization.findFirst({ - where: { - ...orgParamWhereClause(orgParam), - deletedAt: null, - members: { - some: { - userId: authenticationResult.userId, + const organization = await prisma.organization.findFirst({ + where: { + ...orgParamWhereClause(orgParam), + deletedAt: null, + members: { + some: { + userId: authenticationResult.userId, + }, }, }, - }, - }); - - if (!organization) { - return json({ error: "Organization not found" }, { status: 404 }); - } - - const body = await request.json(); - const parsedBody = CreateProjectRequestBody.safeParse(body); - - if (!parsedBody.success) { - return json({ error: "Invalid request body" }, { status: 400 }); - } - - const [error, project] = await tryCatch( - createProject({ - organizationSlug: organization.slug, - name: parsedBody.data.name, - userId: authenticationResult.userId, - version: "v3", - }) - ); - - if (error) { - logger.error("Failed to create project", { error }); - return json({ error: "Failed to create project" }, { status: 400 }); - } - - const result: GetProjectResponseBody = { - id: project.id, - externalRef: project.externalRef, - name: project.name, - slug: project.slug, - createdAt: project.createdAt, - organization: { - id: project.organization.id, - title: project.organization.title, - slug: project.organization.slug, - createdAt: project.organization.createdAt, - }, - }; + }); + + if (!organization) { + return json({ error: "Organization not found" }, { status: 404 }); + } + + const body = await request.json(); + const parsedBody = CreateProjectRequestBody.safeParse(body); + + if (!parsedBody.success) { + return json({ error: "Invalid request body" }, { status: 400 }); + } + + const [error, project] = await tryCatch( + createProject({ + organizationSlug: organization.slug, + name: parsedBody.data.name, + userId: authenticationResult.userId, + version: "v3", + }) + ); + + if (error) { + logger.error("Failed to create project", { error }); + return json({ error: "Failed to create project" }, { status: 400 }); + } + + const result: GetProjectResponseBody = { + id: project.id, + externalRef: project.externalRef, + name: project.name, + slug: project.slug, + createdAt: project.createdAt, + organization: { + id: project.organization.id, + title: project.organization.title, + slug: project.organization.slug, + createdAt: project.organization.createdAt, + }, + }; - return json(result); + return json(result); } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to create org project", { error }); diff --git a/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts b/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts index 4d39298a766..6fc85d69cc3 100644 --- a/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts +++ b/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts @@ -30,120 +30,120 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } try { - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return apiCors( - request, - json({ error: "Invalid or Missing Access Token" }, { status: 401 }) - ); - } - - const parsedParams = ParamsSchema.safeParse(params); - if (!parsedParams.success) { - return apiCors( - request, - json({ error: "Invalid parameters" }, { status: 400 }) - ); - } - - const { organizationSlug, projectParam } = parsedParams.data; - - const result = await fromPromise( - (async () => { - // Find the project, verifying org membership - const project = await prisma.project.findFirst({ - where: { - slug: projectParam, - organization: { - slug: organizationSlug, - members: { - some: { - userId: authenticationResult.userId, + const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); + + if (!authenticationResult) { + return apiCors( + request, + json({ error: "Invalid or Missing Access Token" }, { status: 401 }) + ); + } + + const parsedParams = ParamsSchema.safeParse(params); + if (!parsedParams.success) { + return apiCors( + request, + json({ error: "Invalid parameters" }, { status: 400 }) + ); + } + + const { organizationSlug, projectParam } = parsedParams.data; + + const result = await fromPromise( + (async () => { + // Find the project, verifying org membership + const project = await prisma.project.findFirst({ + where: { + slug: projectParam, + organization: { + slug: organizationSlug, + members: { + some: { + userId: authenticationResult.userId, + }, }, }, + deletedAt: null, }, - deletedAt: null, - }, - select: { - id: true, - name: true, - slug: true, - organizationId: true, - }, - }); - - if (!project) { - return { type: "not_found" as const }; - } - - // Get Vercel integration for the project - const vercelService = new VercelIntegrationService(); - const integration = await vercelService.getVercelProjectIntegration(project.id); + select: { + id: true, + name: true, + slug: true, + organizationId: true, + }, + }); - return { type: "success" as const, project, integration }; - })(), - (error) => error - ); + if (!project) { + return { type: "not_found" as const }; + } - if (result.isErr()) { - logger.error("Failed to fetch Vercel projects", { - error: result.error, - organizationSlug, - projectParam, - }); + // Get Vercel integration for the project + const vercelService = new VercelIntegrationService(); + const integration = await vercelService.getVercelProjectIntegration(project.id); - return apiCors( - request, - json({ error: "Internal server error" }, { status: 500 }) + return { type: "success" as const, project, integration }; + })(), + (error) => error ); - } - if (result.value.type === "not_found") { - return apiCors( - request, - json({ error: "Project not found" }, { status: 404 }) - ); - } + if (result.isErr()) { + logger.error("Failed to fetch Vercel projects", { + error: result.error, + organizationSlug, + projectParam, + }); - const { project, integration } = result.value; + return apiCors( + request, + json({ error: "Internal server error" }, { status: 500 }) + ); + } + + if (result.value.type === "not_found") { + return apiCors( + request, + json({ error: "Project not found" }, { status: 404 }) + ); + } + + const { project, integration } = result.value; + + if (!integration) { + return apiCors( + request, + json({ + connected: false, + vercelProject: null, + config: null, + syncEnvVarsMapping: null, + }) + ); + } + + const { parsedIntegrationData } = integration; - if (!integration) { return apiCors( request, json({ - connected: false, - vercelProject: null, - config: null, - syncEnvVarsMapping: null, + connected: true, + vercelProject: { + id: parsedIntegrationData.vercelProjectId, + name: parsedIntegrationData.vercelProjectName, + teamId: parsedIntegrationData.vercelTeamId, + }, + config: { + atomicBuilds: parsedIntegrationData.config.atomicBuilds, + pullEnvVarsBeforeBuild: parsedIntegrationData.config.pullEnvVarsBeforeBuild, + vercelStagingEnvironment: parsedIntegrationData.config.vercelStagingEnvironment, + }, + syncEnvVarsMapping: parsedIntegrationData.syncEnvVarsMapping, + triggerProject: { + id: project.id, + name: project.name, + slug: project.slug, + }, }) ); - } - - const { parsedIntegrationData } = integration; - - return apiCors( - request, - json({ - connected: true, - vercelProject: { - id: parsedIntegrationData.vercelProjectId, - name: parsedIntegrationData.vercelProjectName, - teamId: parsedIntegrationData.vercelTeamId, - }, - config: { - atomicBuilds: parsedIntegrationData.config.atomicBuilds, - pullEnvVarsBeforeBuild: parsedIntegrationData.config.pullEnvVarsBeforeBuild, - vercelStagingEnvironment: parsedIntegrationData.config.vercelStagingEnvironment, - }, - syncEnvVarsMapping: parsedIntegrationData.syncEnvVarsMapping, - triggerProject: { - id: project.id, - name: project.name, - slug: project.slug, - }, - }) - ); } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to fetch Vercel projects", { error }); diff --git a/apps/webapp/app/routes/api.v1.orgs.ts b/apps/webapp/app/routes/api.v1.orgs.ts index 615df7c5ce5..31ef3783f3e 100644 --- a/apps/webapp/app/routes/api.v1.orgs.ts +++ b/apps/webapp/app/routes/api.v1.orgs.ts @@ -7,35 +7,35 @@ import { authenticateApiRequestWithPersonalAccessToken } from "~/services/person export async function loader({ request }: LoaderFunctionArgs) { try { - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); + const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } - const orgs = await prisma.organization.findMany({ - where: { - deletedAt: null, - members: { - some: { - userId: authenticationResult.userId, + const orgs = await prisma.organization.findMany({ + where: { + deletedAt: null, + members: { + some: { + userId: authenticationResult.userId, + }, }, }, - }, - }); + }); - if (!orgs) { - return json({ error: "Orgs not found" }, { status: 404 }); - } + if (!orgs) { + return json({ error: "Orgs not found" }, { status: 404 }); + } - const result: GetOrgsResponseBody = orgs.map((org) => ({ - id: org.id, - title: org.title, - slug: org.slug, - createdAt: org.createdAt, - })); + const result: GetOrgsResponseBody = orgs.map((org) => ({ + id: org.id, + title: org.title, + slug: org.slug, + createdAt: org.createdAt, + })); - return json(result); + return json(result); } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to list orgs", { error }); diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts index 91a19358185..73597075615 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts @@ -16,83 +16,83 @@ const ParamsSchema = z.object({ export async function action({ request, params }: ActionFunctionArgs) { try { - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); + const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); - } - - const parsedParams = ParamsSchema.safeParse(params); + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } - if (!parsedParams.success) { - return json({ error: "Invalid Params" }, { status: 400 }); - } + const parsedParams = ParamsSchema.safeParse(params); - const { projectRef } = parsedParams.data; + if (!parsedParams.success) { + return json({ error: "Invalid Params" }, { status: 400 }); + } - const rawBody = await request.json(); + const { projectRef } = parsedParams.data; - const body = ApiCreateAlertChannel.safeParse(rawBody); + const rawBody = await request.json(); - if (!body.success) { - return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); - } + const body = ApiCreateAlertChannel.safeParse(rawBody); - const service = new CreateAlertChannelService(); + if (!body.success) { + return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); + } - try { - if (body.data.channel === "email") { - if (!body.data.channelData.email) { - return json({ error: "Email is required" }, { status: 422 }); + const service = new CreateAlertChannelService(); + + try { + if (body.data.channel === "email") { + if (!body.data.channelData.email) { + return json({ error: "Email is required" }, { status: 422 }); + } + + const alertChannel = await service.call(projectRef, authenticationResult.userId, { + name: body.data.name, + alertTypes: body.data.alertTypes.map((type) => + ApiAlertChannelPresenter.alertTypeFromApi(type) + ), + channel: { + type: "EMAIL", + email: body.data.channelData.email, + }, + deduplicationKey: body.data.deduplicationKey, + environmentTypes: body.data.environmentTypes, + }); + + return json(await ApiAlertChannelPresenter.alertChannelToApi(alertChannel)); } - const alertChannel = await service.call(projectRef, authenticationResult.userId, { - name: body.data.name, - alertTypes: body.data.alertTypes.map((type) => - ApiAlertChannelPresenter.alertTypeFromApi(type) - ), - channel: { - type: "EMAIL", - email: body.data.channelData.email, - }, - deduplicationKey: body.data.deduplicationKey, - environmentTypes: body.data.environmentTypes, - }); - - return json(await ApiAlertChannelPresenter.alertChannelToApi(alertChannel)); - } + if (body.data.channel === "webhook") { + if (!body.data.channelData.url) { + return json({ error: "webhook url is required" }, { status: 422 }); + } + + const alertChannel = await service.call(projectRef, authenticationResult.userId, { + name: body.data.name, + alertTypes: body.data.alertTypes.map((type) => + ApiAlertChannelPresenter.alertTypeFromApi(type) + ), + channel: { + type: "WEBHOOK", + url: body.data.channelData.url, + secret: body.data.channelData.secret, + }, + deduplicationKey: body.data.deduplicationKey, + environmentTypes: body.data.environmentTypes, + }); + + return json(await ApiAlertChannelPresenter.alertChannelToApi(alertChannel)); + } - if (body.data.channel === "webhook") { - if (!body.data.channelData.url) { - return json({ error: "webhook url is required" }, { status: 422 }); + return json({ error: "Invalid channel type" }, { status: 422 }); + } catch (error) { + if (error instanceof ServiceValidationError) { + return json({ error: error.message }, { status: 422 }); } - const alertChannel = await service.call(projectRef, authenticationResult.userId, { - name: body.data.name, - alertTypes: body.data.alertTypes.map((type) => - ApiAlertChannelPresenter.alertTypeFromApi(type) - ), - channel: { - type: "WEBHOOK", - url: body.data.channelData.url, - secret: body.data.channelData.secret, - }, - deduplicationKey: body.data.deduplicationKey, - environmentTypes: body.data.environmentTypes, - }); - - return json(await ApiAlertChannelPresenter.alertChannelToApi(alertChannel)); + logger.error("Failed to create alert channel", { error }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } - - return json({ error: "Invalid channel type" }, { status: 422 }); - } catch (error) { - if (error instanceof ServiceValidationError) { - return json({ error: error.message }, { status: 422 }); - } - - logger.error("Failed to create alert channel", { error }); - return json({ error: "Something went wrong, please try again." }, { status: 500 }); - } } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to create alert channel (outer)", { error }); diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts index b4b0384bf65..12e5eb24dde 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts @@ -26,55 +26,55 @@ export async function action({ request, params }: ActionFunctionArgs) { } try { - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const authenticatedEnv = authenticationResult.environment; + const authenticatedEnv = authenticationResult.environment; - const { projectRef } = parsedParams.data; + const { projectRef } = parsedParams.data; - const rawBody = await request.json(); - const body = CreateBackgroundWorkerRequestBody.safeParse(rawBody); + const rawBody = await request.json(); + const body = CreateBackgroundWorkerRequestBody.safeParse(rawBody); - if (!body.success) { - return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); - } + if (!body.success) { + return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); + } - const service = new CreateBackgroundWorkerService(); + const service = new CreateBackgroundWorkerService(); - try { - const backgroundWorker = await service.call(projectRef, authenticatedEnv, body.data); + try { + const backgroundWorker = await service.call(projectRef, authenticatedEnv, body.data); - return json( - { - id: backgroundWorker.friendlyId, - version: backgroundWorker.version, - contentHash: backgroundWorker.contentHash, - }, - { status: 200 } - ); - } catch (e) { - // Customer-facing validation failures (invalid task config, customer cron - // expression, etc.). The handler returns 4xx with the message; system - // handles it gracefully, no alert needed. - if (e instanceof ServiceValidationError) { - logger.warn("Failed to create background worker", { error: e.message }); - return json({ error: e.message }, { status: 400 }); - } - if (e instanceof CreateDeclarativeScheduleError) { - logger.warn("Failed to create background worker", { error: e.message }); - return json({ error: e.message }, { status: 400 }); - } + return json( + { + id: backgroundWorker.friendlyId, + version: backgroundWorker.version, + contentHash: backgroundWorker.contentHash, + }, + { status: 200 } + ); + } catch (e) { + // Customer-facing validation failures (invalid task config, customer cron + // expression, etc.). The handler returns 4xx with the message; system + // handles it gracefully, no alert needed. + if (e instanceof ServiceValidationError) { + logger.warn("Failed to create background worker", { error: e.message }); + return json({ error: e.message }, { status: 400 }); + } + if (e instanceof CreateDeclarativeScheduleError) { + logger.warn("Failed to create background worker", { error: e.message }); + return json({ error: e.message }, { status: 400 }); + } - logger.error("Failed to create background worker", { error: e }); + logger.error("Failed to create background worker", { error: e }); - return json({ error: "Failed to create background worker" }, { status: 500 }); - } + return json({ error: "Failed to create background worker" }, { status: 500 }); + } } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to create project background worker", { error }); diff --git a/apps/webapp/app/routes/api.v1.projects.ts b/apps/webapp/app/routes/api.v1.projects.ts index ec7d2828d03..372a8108f41 100644 --- a/apps/webapp/app/routes/api.v1.projects.ts +++ b/apps/webapp/app/routes/api.v1.projects.ts @@ -9,49 +9,49 @@ export async function loader({ request }: LoaderFunctionArgs) { logger.info("get projects", { url: request.url }); try { - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); + const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } - const projects = await prisma.project.findMany({ - where: { - organization: { - deletedAt: null, - members: { - some: { - userId: authenticationResult.userId, + const projects = await prisma.project.findMany({ + where: { + organization: { + deletedAt: null, + members: { + some: { + userId: authenticationResult.userId, + }, }, }, + version: "V3", + deletedAt: null, }, - version: "V3", - deletedAt: null, - }, - include: { - organization: true, - }, - }); + include: { + organization: true, + }, + }); - if (!projects) { - return json({ error: "Projects not found" }, { status: 404 }); - } + if (!projects) { + return json({ error: "Projects not found" }, { status: 404 }); + } - const result: GetProjectsResponseBody = projects.map((project) => ({ - id: project.id, - externalRef: project.externalRef, - name: project.name, - slug: project.slug, - createdAt: project.createdAt, - organization: { - id: project.organization.id, - title: project.organization.title, - slug: project.organization.slug, - createdAt: project.organization.createdAt, - }, - })); + const result: GetProjectsResponseBody = projects.map((project) => ({ + id: project.id, + externalRef: project.externalRef, + name: project.name, + slug: project.slug, + createdAt: project.createdAt, + organization: { + id: project.organization.id, + title: project.organization.title, + slug: project.organization.slug, + createdAt: project.organization.createdAt, + }, + })); - return json(result); + return json(result); } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to list projects", { error }); From dbca4f26d017e20a19bbbe8287d7c60c87d1decc Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Mon, 18 May 2026 19:59:32 +0100 Subject: [PATCH 5/6] fix(webapp): map malformed JSON body to 400 in fail-deployment action --- .../webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts index 5fdb6bfa9a0..43eb45c5364 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts @@ -52,6 +52,9 @@ export async function action({ request, params }: ActionFunctionArgs) { ); } catch (error) { if (error instanceof Response) throw error; + if (error instanceof SyntaxError) { + return json({ error: "Invalid JSON body" }, { status: 400 }); + } logger.error("Failed to fail deployment", { error }); return json({ error: "Internal Server Error" }, { status: 500 }); } From af7bbdec9871d5e3263a75addd49018790b2b0a1 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Tue, 19 May 2026 08:48:02 +0100 Subject: [PATCH 6/6] chore(server-changes): collapse into one terse changelog-style note --- .../sanitize-api-loader-action-leaks-sweep.md | 10 ---------- .server-changes/sanitize-loader-action-leaks.md | 4 +--- 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 .server-changes/sanitize-api-loader-action-leaks-sweep.md diff --git a/.server-changes/sanitize-api-loader-action-leaks-sweep.md b/.server-changes/sanitize-api-loader-action-leaks-sweep.md deleted file mode 100644 index e312dc57ffe..00000000000 --- a/.server-changes/sanitize-api-loader-action-leaks-sweep.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -area: webapp -type: fix ---- - -Sweep across the remaining `apps/webapp/app/routes/api.v1.*` raw loaders/actions that previously let thrown errors propagate to Remix's default 500 serializer, which writes `error.message` into the response body. Earlier passes covered routes with leaking `catch` blocks and two specific naked routes; this pass covers the rest of the API surface that doesn't go through `createLoaderApiRoute` / `createActionApiRoute`. - -Each handler now wraps its body in try/catch, re-throws `Response` instances so auth helpers' `throw json(...)` / `throw redirect(...)` pass through unchanged, logs non-Response errors, and returns `{ error: "Internal Server Error" }` 500. For routes that already had an inner try/catch covering a service call but with auth/parsing outside the try (alertChannels, batches.results, deployments.finalize, several others), an outer try/catch is added so the inner typed-error handling is preserved. The `api.v1.authorization-code.ts` catch branch was returning `error.message` verbatim — switched to a generic body. - -Each route was verified locally with a synthetic-throw probe: inject `throw new Error("SYNTHETIC ...")` at the top of the catch'd try, curl the route with a dummy bearer token, confirm the response body is the generic shape and that the synthetic message is captured server-side via `logger.error`. diff --git a/.server-changes/sanitize-loader-action-leaks.md b/.server-changes/sanitize-loader-action-leaks.md index 2d439c41ea1..f4cb871f4cf 100644 --- a/.server-changes/sanitize-loader-action-leaks.md +++ b/.server-changes/sanitize-loader-action-leaks.md @@ -3,6 +3,4 @@ area: webapp type: fix --- -Wrap two loaders/actions that previously let thrown errors propagate to Remix's default 500 serializer, which writes `error.message` into the response body. When the underlying call (Prisma, etc.) fails, the raw error string was reaching API consumers — including the SDK, which surfaces it back to users via `TriggerApiError`. Each handler now catches non-Response errors, logs server-side, and returns a generic 500 body. `throw json(...)` / `throw redirect(...)` from auth helpers is re-thrown unchanged. - -Covers `api.v1.projects.$projectRef.envvars.$slug.$name.ts` (loader + action) and `resources.platform-changelogs.tsx` (loader). +Expand API error response sanitization to additional loaders and actions so internal exception messages (Prisma errors, etc.) no longer leak to callers via 5xx response bodies.