From f6aabc8df73fbd3c4675f1305ab50582a69e2369 Mon Sep 17 00:00:00 2001 From: Lesia Soloviova Date: Sun, 29 Mar 2026 17:20:53 +0100 Subject: [PATCH 1/2] fix(netlify): add runtime env fallback generation for server env reads --- frontend/lib/env/runtime-env.generated.ts | 3 ++ frontend/lib/env/server-env.ts | 12 ++++++- frontend/scripts/generate-env-runtime.mjs | 44 +++++++++++++++++++++++ netlify.toml | 2 +- 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 frontend/lib/env/runtime-env.generated.ts create mode 100644 frontend/scripts/generate-env-runtime.mjs diff --git a/frontend/lib/env/runtime-env.generated.ts b/frontend/lib/env/runtime-env.generated.ts new file mode 100644 index 00000000..a84be208 --- /dev/null +++ b/frontend/lib/env/runtime-env.generated.ts @@ -0,0 +1,3 @@ +import 'server-only'; + +export const RUNTIME_ENV: Readonly> = {}; diff --git a/frontend/lib/env/server-env.ts b/frontend/lib/env/server-env.ts index 5923e043..8841e386 100644 --- a/frontend/lib/env/server-env.ts +++ b/frontend/lib/env/server-env.ts @@ -1,5 +1,7 @@ import 'server-only'; +import { RUNTIME_ENV } from './runtime-env.generated'; + type NetlifyEnv = { get?: (key: string) => string | undefined; }; @@ -29,5 +31,13 @@ export function readServerEnv(key: string): string | undefined { const fromProcess = process.env[key]?.trim(); if (fromProcess) return fromProcess; - return readFromNetlifyEnv(key); + const fromNetlify = readFromNetlifyEnv(key); + if (fromNetlify) return fromNetlify; + + return readFromGeneratedRuntimeEnv(key); } + +function readFromGeneratedRuntimeEnv(key: string): string | undefined { + const value = RUNTIME_ENV[key]; + return typeof value === 'string' && value.trim() ? value.trim() : undefined; +} \ No newline at end of file diff --git a/frontend/scripts/generate-env-runtime.mjs b/frontend/scripts/generate-env-runtime.mjs new file mode 100644 index 00000000..8509e4d5 --- /dev/null +++ b/frontend/scripts/generate-env-runtime.mjs @@ -0,0 +1,44 @@ +import { readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const root = process.cwd(); +const examplePath = resolve(root, '.env.example'); +const outputPath = resolve(root, 'lib/env/runtime-env.generated.ts'); + +const keyRegex = /^([A-Z][A-Z0-9_]*)=/; + +const keys = Array.from( + new Set( + readFileSync(examplePath, 'utf8') + .split(/\r?\n/) + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')) + .map(line => { + const match = line.match(keyRegex); + return match ? match[1] : null; + }) + .filter(Boolean) + ) +); + +const entries = []; + +for (const key of keys) { + const value = process.env[key]; + if (typeof value !== 'string' || value.length === 0) continue; + entries.push([key, value]); +} + +const objectBody = entries + .map(([key, value]) => ` ${JSON.stringify(key)}: ${JSON.stringify(value)},`) + .join('\n'); + +const fileContent = `import 'server-only'; + +export const RUNTIME_ENV: Readonly> = { +${objectBody} +}; +`; + +writeFileSync(outputPath, fileContent, 'utf8'); +console.log(`[env] generated runtime-env.generated.ts with ${entries.length} keys`); diff --git a/netlify.toml b/netlify.toml index 12f80de1..185f8f2b 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,6 +1,6 @@ [build] base = "frontend" - command = "npm ci --include=optional && npm run build" + command = "npm ci --include=optional && node scripts/generate-env-runtime.mjs && npm run build" publish = ".next" [build.environment] From f8d381a87dd021e3f3aaaf4a8bc25c013f6daeb5 Mon Sep 17 00:00:00 2001 From: Lesia Soloviova Date: Sun, 29 Mar 2026 18:02:17 +0100 Subject: [PATCH 2/2] fix(netlify env): add generated runtime fallback for server secrets/db config - add build script to generate frontend/lib/env/runtime-env.generated.ts from .env.example allowlist - run generator before Next build in netlify.toml - extend readServerEnv fallback chain: process.env -> Netlify.env.get() -> generated runtime map - scope generated fallback to explicit keys only (APP_ENV/CONTEXT/NETLIFY/DATABASE_URL/DATABASE_URL_LOCAL/AUTH_SECRET/CSRF_SECRET) - keep generated map empty outside develop to reduce exposure in non-develop environments --- frontend/lib/env/server-env.ts | 19 ++++++++++++++++++- frontend/scripts/generate-env-runtime.mjs | 12 ++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/frontend/lib/env/server-env.ts b/frontend/lib/env/server-env.ts index 8841e386..a144ddee 100644 --- a/frontend/lib/env/server-env.ts +++ b/frontend/lib/env/server-env.ts @@ -27,6 +27,21 @@ function readFromNetlifyEnv(key: string): string | undefined { return typeof value === 'string' && value.trim() ? value.trim() : undefined; } +const GENERATED_FALLBACK_KEYS = new Set([ + 'APP_ENV', + 'CONTEXT', + 'NETLIFY', + 'DATABASE_URL', + 'DATABASE_URL_LOCAL', + 'AUTH_SECRET', + 'CSRF_SECRET', +]); + + +function canUseGeneratedFallback(key: string): boolean { + return GENERATED_FALLBACK_KEYS.has(key); +} + export function readServerEnv(key: string): string | undefined { const fromProcess = process.env[key]?.trim(); if (fromProcess) return fromProcess; @@ -34,7 +49,9 @@ export function readServerEnv(key: string): string | undefined { const fromNetlify = readFromNetlifyEnv(key); if (fromNetlify) return fromNetlify; - return readFromGeneratedRuntimeEnv(key); + if (!canUseGeneratedFallback(key)) return undefined; + return readFromGeneratedRuntimeEnv(key); + } function readFromGeneratedRuntimeEnv(key: string): string | undefined { diff --git a/frontend/scripts/generate-env-runtime.mjs b/frontend/scripts/generate-env-runtime.mjs index 8509e4d5..c965acbe 100644 --- a/frontend/scripts/generate-env-runtime.mjs +++ b/frontend/scripts/generate-env-runtime.mjs @@ -4,6 +4,8 @@ import { resolve } from 'node:path'; const root = process.cwd(); const examplePath = resolve(root, '.env.example'); const outputPath = resolve(root, 'lib/env/runtime-env.generated.ts'); +const appEnv = (process.env.APP_ENV ?? '').trim().toLowerCase(); +const isDevelop = appEnv === 'develop'; const keyRegex = /^([A-Z][A-Z0-9_]*)=/; @@ -29,6 +31,16 @@ for (const key of keys) { entries.push([key, value]); } +if (!isDevelop) { + const fileContent = `import 'server-only'; + +export const RUNTIME_ENV: Readonly> = {}; +`; + writeFileSync(outputPath, fileContent, 'utf8'); + console.log(`[env] skipped runtime env generation (APP_ENV=${appEnv || ''})`); + process.exit(0); +} + const objectBody = entries .map(([key, value]) => ` ${JSON.stringify(key)}: ${JSON.stringify(value)},`) .join('\n');