import type { ActionFunctionArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { ImportEnvironmentVariablesRequestBody } from "@trigger.dev/core/v3"; import { parse } from "dotenv"; import { z } from "zod"; import { authenticateRequest, authenticatedEnvironmentForAuthentication, branchNameFromRequest, } from "~/services/apiAuth.server"; import { authorizeEnvVarApiRequest } from "~/services/environmentVariableApiAccess.server"; import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server"; const ParamsSchema = z.object({ projectRef: z.string(), slug: z.string(), }); export async function action({ params, request }: ActionFunctionArgs) { const parsedParams = ParamsSchema.safeParse(params); if (!parsedParams.success) { return json({ error: "Invalid params" }, { status: 400 }); } const authenticationResult = await authenticateRequest(request, { personalAccessToken: true, organizationAccessToken: true, apiKey: true, }); 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) ); const denied = await authorizeEnvVarApiRequest({ request, authType: authenticationResult.type, organizationId: environment.organizationId, projectId: environment.project.id, envType: environment.type, action: "write", }); if (denied) return denied; const repository = new EnvironmentVariablesRepository(); const body = await parseImportBody(request); const result = await repository.create(environment.project.id, { override: typeof body.override === "boolean" ? body.override : false, isSecret: body.isSecret, environmentIds: [environment.id], // Pass parent environment ID so new variables can inherit isSecret from parent parentEnvironmentId: environment.parentEnvironmentId ?? undefined, variables: Object.entries(body.variables).map(([key, value]) => ({ key, value, })), lastUpdatedBy: body.source, }); // Only sync parent variables if this is a branch environment if (environment.parentEnvironmentId && body.parentVariables) { const parentResult = await repository.create(environment.project.id, { override: typeof body.override === "boolean" ? body.override : false, isSecret: body.isSecret, environmentIds: [environment.parentEnvironmentId], variables: Object.entries(body.parentVariables).map(([key, value]) => ({ key, value, })), lastUpdatedBy: body.source, }); let childFailure = !result.success ? result : undefined; let parentFailure = !parentResult.success ? parentResult : undefined; if (result.success || parentResult.success) { return json({ success: true }); } else { return json( { error: childFailure?.error || parentFailure?.error || "Unknown error", variableErrors: childFailure?.variableErrors || parentFailure?.variableErrors, }, { status: 400 } ); } } if (result.success) { return json({ success: true }); } else { return json({ error: result.error, variableErrors: result.variableErrors }, { status: 400 }); } } async function parseImportBody(request: Request): Promise { const contentType = request.headers.get("content-type") ?? "application/json"; if (contentType.includes("multipart/form-data")) { const formData = await request.formData(); const file = formData.get("variables"); const override = formData.get("override") === "true"; if (file instanceof File) { const buffer = await file.arrayBuffer(); const variables = parse(Buffer.from(buffer)); return { variables, override }; } else { throw json({ error: "Invalid file" }, { status: 400 }); } } else { const rawBody = await request.json(); const body = ImportEnvironmentVariablesRequestBody.safeParse(rawBody); if (!body.success) { throw json({ error: "Invalid body" }, { status: 400 }); } return body.data; } }