diff --git a/apps/web/lib/env.ts b/apps/web/lib/env.ts index 617bc15..9609460 100644 --- a/apps/web/lib/env.ts +++ b/apps/web/lib/env.ts @@ -4,6 +4,16 @@ const Bool = z .union([z.boolean(), z.enum(["true", "false", "1", "0"])]) .transform((v) => v === true || v === "true" || v === "1"); +/** + * Treat an empty string as "not set". + * + * docker-compose renders `${VAR:-}` as an empty string rather than omitting + * the key, so an unset optional var arrives as "" and would otherwise fail + * `.url()` / `.min(1)` validation and take the whole app down at boot. + */ +const optional = (schema: T) => + z.preprocess((v) => (v === "" ? undefined : v), schema.optional()); + const envSchema = z.object({ NODE_ENV: z.enum(["development", "test", "production"]).default("development"), LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), @@ -25,7 +35,7 @@ const envSchema = z.object({ // // Set this to the MCP application's issuer. Defaults to OIDC_ISSUER for // single-application setups. - OIDC_ISSUER_MCP: z.string().url().optional(), + OIDC_ISSUER_MCP: optional(z.string().url()), OIDC_CLIENT_ID_WEB: z.string().min(1), OIDC_CLIENT_SECRET_WEB: z.string().min(1), OIDC_CLIENT_ID_MCP: z.string().min(1), @@ -40,7 +50,7 @@ const envSchema = z.object({ // every token arrives without an `aud` claim (-> 401 "claim invalid: aud"). // // Defaults to the `aud-` convention used in the README setup. - OIDC_AUDIENCE_SCOPE: z.string().min(1).optional(), + OIDC_AUDIENCE_SCOPE: optional(z.string().min(1)), // Database DATABASE_URL: z.string().url(), diff --git a/docker-compose.yml b/docker-compose.yml index 0b4307b..02f6a2a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -113,6 +113,10 @@ services: OIDC_CLIENT_SECRET_WEB: ${OIDC_CLIENT_SECRET_WEB:?required} OIDC_CLIENT_ID_MCP: ${OIDC_CLIENT_ID_MCP:?required} OIDC_AUDIENCE: ${OIDC_AUDIENCE:?required} + # Optional. This block is an explicit allow-list, not env_file — a var + # added to .env but not listed here never reaches the container. + OIDC_ISSUER_MCP: ${OIDC_ISSUER_MCP:-} + OIDC_AUDIENCE_SCOPE: ${OIDC_AUDIENCE_SCOPE:-} DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}