From 3683d9a88915987be5bf478ee03ffee6786a3281 Mon Sep 17 00:00:00 2001 From: Hafiz Muhammad Awais Hassan <109977640+mianawais78@users.noreply.github.com> Date: Wed, 19 Aug 2026 03:31:35 +0500 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfix(api):=20treat=20blank=20optional?= =?UTF-8?q?=20env=20values=20as=20unset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `.env.example` ships `VERCEL_TOKEN`, `VERCEL_OIDC_TOKEN`, `VERCEL_TEAM_ID` and `VERCEL_PROJECT_ID` with no value, and dotenv loads a bare `KEY=` as an empty string rather than omitting it. The schema marked those fields optional but validated them with `min(1)`, so an empty string counted as present and failed. `readConfig` parses instead of safe-parsing, which means the API crashed at import time with a `ZodError` naming all four keys. Copying the example file, the documented first step of the quickstart, therefore produced an instance that could not boot. Normalize blank strings to `undefined` before validation so an unset credential stays unset, while a value that is only whitespace is still rejected rather than silently accepted. Co-authored-by: Cursor --- services/api/src/config.ts | 25 +++++++++++++++---------- services/api/test/config.test.ts | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/services/api/src/config.ts b/services/api/src/config.ts index caa5ff50..c36870c2 100644 --- a/services/api/src/config.ts +++ b/services/api/src/config.ts @@ -8,9 +8,14 @@ import type { AppConfig } from "./types.js"; const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "../../.."); loadDotenv({ path: join(repoRoot, ".env"), quiet: true }); -const OptionalUrl = z.preprocess( - (value) => (typeof value === "string" && value.trim() === "" ? undefined : value), - z.string().url().optional(), +const blankToUndefined = (value: unknown) => + typeof value === "string" && value.trim() === "" ? undefined : value; + +const OptionalUrl = z.preprocess(blankToUndefined, z.string().url().optional()); + +const OptionalNonEmptyString = z.preprocess( + blankToUndefined, + z.string().trim().min(1).optional(), ); const OptionalGithubOrganization = z.preprocess( @@ -69,13 +74,13 @@ const EnvSchema = z S3_SECRET_KEY: z.string().optional(), S3_BUCKET: z.string().optional(), AWS_REGION: z.string().optional(), - FACILITY_AWS_CODEBUILD_PROJECT: z.string().trim().min(1).optional(), - FACILITY_AWS_CODEBUILD_CACHE_BASE_LOCATION: z.string().trim().min(1).optional(), - VERCEL_TOKEN: z.string().trim().min(1).optional(), - VERCEL_OIDC_TOKEN: z.string().trim().min(1).optional(), - VERCEL_TEAM_ID: z.string().trim().min(1).optional(), - VERCEL_PROJECT_ID: z.string().trim().min(1).optional(), - PACKAGE_REGISTRY_TOKEN: z.string().trim().min(1).optional(), + FACILITY_AWS_CODEBUILD_PROJECT: OptionalNonEmptyString, + FACILITY_AWS_CODEBUILD_CACHE_BASE_LOCATION: OptionalNonEmptyString, + VERCEL_TOKEN: OptionalNonEmptyString, + VERCEL_OIDC_TOKEN: OptionalNonEmptyString, + VERCEL_TEAM_ID: OptionalNonEmptyString, + VERCEL_PROJECT_ID: OptionalNonEmptyString, + PACKAGE_REGISTRY_TOKEN: OptionalNonEmptyString, GITHUB_APP_ID: z.string().optional(), GITHUB_APP_PRIVATE_KEY: z.string().optional(), GITHUB_APP_WEBHOOK_SECRET: z.string().optional(), diff --git a/services/api/test/config.test.ts b/services/api/test/config.test.ts index f9993665..fdede7e0 100644 --- a/services/api/test/config.test.ts +++ b/services/api/test/config.test.ts @@ -155,6 +155,22 @@ describe("API configuration", () => { }); }); + it("treats blank Vercel credentials from a copied .env.example as unset", () => { + expect( + readConfig({ + ...validEnv, + VERCEL_TOKEN: "", + VERCEL_OIDC_TOKEN: "", + VERCEL_TEAM_ID: "", + VERCEL_PROJECT_ID: "", + }), + ).toMatchObject({ + vercelToken: undefined, + vercelTeamId: undefined, + vercelProjectId: undefined, + }); + }); + it("accepts a Vercel sandbox project binding without exposing a token fallback", () => { expect( readConfig({