Skip to content

Commit de23891

Browse files
authored
fix(netlify): add runtime env fallback generation for server env reads (#433)
* fix(netlify): add runtime env fallback generation for server env reads * 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
1 parent 10d2e45 commit de23891

File tree

4 files changed

+88
-2
lines changed

4 files changed

+88
-2
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import 'server-only';
2+
3+
export const RUNTIME_ENV: Readonly<Record<string, string>> = {};

frontend/lib/env/server-env.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'server-only';
22

3+
import { RUNTIME_ENV } from './runtime-env.generated';
4+
35
type NetlifyEnv = {
46
get?: (key: string) => string | undefined;
57
};
@@ -25,9 +27,34 @@ function readFromNetlifyEnv(key: string): string | undefined {
2527
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
2628
}
2729

30+
const GENERATED_FALLBACK_KEYS = new Set([
31+
'APP_ENV',
32+
'CONTEXT',
33+
'NETLIFY',
34+
'DATABASE_URL',
35+
'DATABASE_URL_LOCAL',
36+
'AUTH_SECRET',
37+
'CSRF_SECRET',
38+
]);
39+
40+
41+
function canUseGeneratedFallback(key: string): boolean {
42+
return GENERATED_FALLBACK_KEYS.has(key);
43+
}
44+
2845
export function readServerEnv(key: string): string | undefined {
2946
const fromProcess = process.env[key]?.trim();
3047
if (fromProcess) return fromProcess;
3148

32-
return readFromNetlifyEnv(key);
49+
const fromNetlify = readFromNetlifyEnv(key);
50+
if (fromNetlify) return fromNetlify;
51+
52+
if (!canUseGeneratedFallback(key)) return undefined;
53+
return readFromGeneratedRuntimeEnv(key);
54+
3355
}
56+
57+
function readFromGeneratedRuntimeEnv(key: string): string | undefined {
58+
const value = RUNTIME_ENV[key];
59+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
60+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { readFileSync, writeFileSync } from 'node:fs';
2+
import { resolve } from 'node:path';
3+
4+
const root = process.cwd();
5+
const examplePath = resolve(root, '.env.example');
6+
const outputPath = resolve(root, 'lib/env/runtime-env.generated.ts');
7+
const appEnv = (process.env.APP_ENV ?? '').trim().toLowerCase();
8+
const isDevelop = appEnv === 'develop';
9+
10+
const keyRegex = /^([A-Z][A-Z0-9_]*)=/;
11+
12+
const keys = Array.from(
13+
new Set(
14+
readFileSync(examplePath, 'utf8')
15+
.split(/\r?\n/)
16+
.map(line => line.trim())
17+
.filter(line => line && !line.startsWith('#'))
18+
.map(line => {
19+
const match = line.match(keyRegex);
20+
return match ? match[1] : null;
21+
})
22+
.filter(Boolean)
23+
)
24+
);
25+
26+
const entries = [];
27+
28+
for (const key of keys) {
29+
const value = process.env[key];
30+
if (typeof value !== 'string' || value.length === 0) continue;
31+
entries.push([key, value]);
32+
}
33+
34+
if (!isDevelop) {
35+
const fileContent = `import 'server-only';
36+
37+
export const RUNTIME_ENV: Readonly<Record<string, string>> = {};
38+
`;
39+
writeFileSync(outputPath, fileContent, 'utf8');
40+
console.log(`[env] skipped runtime env generation (APP_ENV=${appEnv || '<empty>'})`);
41+
process.exit(0);
42+
}
43+
44+
const objectBody = entries
45+
.map(([key, value]) => ` ${JSON.stringify(key)}: ${JSON.stringify(value)},`)
46+
.join('\n');
47+
48+
const fileContent = `import 'server-only';
49+
50+
export const RUNTIME_ENV: Readonly<Record<string, string>> = {
51+
${objectBody}
52+
};
53+
`;
54+
55+
writeFileSync(outputPath, fileContent, 'utf8');
56+
console.log(`[env] generated runtime-env.generated.ts with ${entries.length} keys`);

netlify.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[build]
22
base = "frontend"
3-
command = "npm ci --include=optional && npm run build"
3+
command = "npm ci --include=optional && node scripts/generate-env-runtime.mjs && npm run build"
44
publish = ".next"
55

66
[build.environment]

0 commit comments

Comments
 (0)