-
Notifications
You must be signed in to change notification settings - Fork 7
feat: env validation #140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: env validation #140
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -91,4 +91,6 @@ jobs: | |
| ${{ runner.os }}-node-modules- | ||
|
|
||
| - name: Build | ||
| env: | ||
| SKIP_ENV_VALIDATION: "1" | ||
| run: npm run build | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,25 @@ | ||
| /** @type {import('next').NextConfig} */ | ||
| const nextConfig = {}; | ||
| import { fileURLToPath } from "node:url"; | ||
| import createJiti from "jiti"; | ||
| import { | ||
| PHASE_DEVELOPMENT_SERVER, | ||
| PHASE_PRODUCTION_BUILD, | ||
| } from "next/constants.js"; | ||
|
|
||
| export default nextConfig; | ||
| /** @type {import('next').NextConfig | ((phase: string) => import('next').NextConfig)} */ | ||
| export default function nextConfig(phase) { | ||
| const jiti = createJiti(fileURLToPath(import.meta.url)); | ||
|
|
||
| const skipValidation = process.env.SKIP_ENV_VALIDATION === "1"; | ||
|
|
||
| if (!skipValidation) { | ||
| if ( | ||
| phase === PHASE_DEVELOPMENT_SERVER || | ||
| phase === PHASE_PRODUCTION_BUILD | ||
| ) { | ||
| jiti("./src/env/server.ts"); | ||
| jiti("./src/env/client.ts"); | ||
| } | ||
| } | ||
|
|
||
| return {}; | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,7 @@ | |
| "@radix-ui/react-tooltip": "^1.2.8", | ||
| "@reown/appkit": "^1.6.8", | ||
| "@reown/appkit-adapter-ethers5": "^1.6.8", | ||
| "@t3-oss/env-nextjs": "^0.13.8", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainSearch for remaining process.env usages and update to env.*. To ensure completeness of the migration, scan for direct process.env access (excluding env modules and config). Also applies to: 53-53 🏁 Script executed: #!/bin/bash
# Find raw process.env usages outside env modules and config
rg -nP --type=ts --type=tsx -C2 '(?<!env\/)(?<!next\.config)\bprocess\.env\.[A-Z0-9_]+' -g '!**/src/env/**' -g '!**/next.config.*'Length of output: 68 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Searching for raw process.env usages in ts/tsx/js/jsx files (excluding src/env and next.config.*)..."
rg -n -S 'process\.env\.' -g '!src/env/**' -g '!**/next.config.*' -g '!node_modules/**' -g '!dist/**' -g '!build/**' -g '!**/.git/**' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' -C2 || true
echo "Done."Length of output: 4167 Replace remaining direct process.env usages with env. (client vs server as appropriate).* Found raw process.env references that must be migrated (ignore src/env/* — that’s the env module):
Update public vars to the env client accessor (env.NEXT_PUBLIC_…), and server-only vars (DATABASE_URL, PORT, NODE_ENV, VERCEL_URL) to the server env accessor from your env module. Re-run a repo-wide search (excluding src/env/** and next.config.*) to confirm no raw process.env remain. 🤖 Prompt for AI Agents |
||
| "@tanstack/react-query": "^4.36.1", | ||
| "@trpc/client": "^10.45.2", | ||
| "@trpc/react-query": "^10.45.2", | ||
|
|
@@ -63,9 +64,10 @@ | |
| "ulid": "^2.3.0", | ||
| "validator": "^13.12.0", | ||
| "viem": "^2.21.48", | ||
| "zod": "^3.24.2" | ||
| "zod": "^3.25.76" | ||
| }, | ||
| "devDependencies": { | ||
| "jiti": "1.21.7", | ||
| "@biomejs/biome": "^1.9.4", | ||
| "@types/crypto-js": "^4.2.2", | ||
| "@types/node": "^20", | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,6 +31,8 @@ import { | |||||||||||||
| SelectTrigger, | ||||||||||||||
| SelectValue, | ||||||||||||||
| } from "@/components/ui/select"; | ||||||||||||||
| import { env } from "@/env/client"; | ||||||||||||||
| import { env as serverEnv } from "@/env/server"; | ||||||||||||||
| import { COMPLIANCE_COUNTRIES } from "@/lib/constants/compliance"; | ||||||||||||||
|
Comment on lines
+34
to
36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainDon’t import server env in a client component; trim/dedupe origins. Importing "@/env/server" in a "use client" file risks bundling server-only values and can break builds. Also, normalize the comma list and avoid duplicates. Apply this diff: -import { env as serverEnv } from "@/env/server";
+// Avoid importing server env in client code
...
const TRUSTED_ORIGINS = useMemo(() => {
- const origins = env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS
- ? env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS.split(",")
- : ["https://request.network", "https://core-api-staging.pay.so"];
+ const originsBase = env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS
+ ? env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS.split(",")
+ : ["https://request.network", "https://core-api-staging.pay.so"];
+ const origins = originsBase.map((s) => s.trim()).filter(Boolean);
// Add localhost in development
- if (serverEnv.NODE_ENV === "development") {
- origins.push("http://localhost:3000");
- }
+ if (process.env.NODE_ENV === "development") {
+ origins.push("http://localhost:3000");
+ }
- return origins;
+ return Array.from(new Set(origins));
}, []);Run to ensure no other client files import the server env: Also applies to: 72-79 🏁 Script executed: #!/bin/bash
# Find client components importing "@/env/server"
mapfile -t CLIENTS < <(rg -l -nP --type=tsx '^\s*"use client";')
for f in "${CLIENTS[@]}"; do
rg -n '@/env/server' "$f" || true
doneLength of output: 84 🏁 Script executed: #!/bin/bash
# Find client components importing "@/env/server" without process substitution or --type
PAT="^\s*['\"]use client['\"]\s*;?"
CLIENT_FILES=$(rg -l -nP "$PAT" -g '**/*.{tsx,jsx}' --color=never || true)
if [ -z "$CLIENT_FILES" ]; then
echo "NO_CLIENT_FILES_FOUND"
exit 0
fi
printf '%s\n' "$CLIENT_FILES" | while IFS= read -r f; do
echo "---- $f ----"
rg -n --color=never '@/env/server' "$f" || echo "No @/env/server import in $f"
doneLength of output: 5499 Remove server env imports from client components; trim/dedupe trusted origins Found '@/env/server' imported in client files:
Apply the diff below to compliance-form.tsx (remove server import, trim origins, dedupe). For src/trpc/react.tsx remove the server env import and move the logic server-side or replace with client-safe NEXT_PUBLIC_* env vars. -import { env as serverEnv } from "@/env/server";
+// Avoid importing server env in client code
...
const TRUSTED_ORIGINS = useMemo(() => {
- const origins = env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS
- ? env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS.split(",")
- : ["https://request.network", "https://core-api-staging.pay.so"];
+ const originsBase = env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS
+ ? env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS.split(",")
+ : ["https://request.network", "https://core-api-staging.pay.so"];
+ const origins = originsBase.map((s) => s.trim()).filter(Boolean);
// Add localhost in development
- if (serverEnv.NODE_ENV === "development") {
- origins.push("http://localhost:3000");
- }
+ if (process.env.NODE_ENV === "development") {
+ origins.push("http://localhost:3000");
+ }
- return origins;
+ return Array.from(new Set(origins));
}, []);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| import { | ||||||||||||||
| BeneficiaryType, | ||||||||||||||
|
|
@@ -67,12 +69,12 @@ export function ComplianceForm({ user }: { user: User }) { | |||||||||||||
|
|
||||||||||||||
| const iframeRef = useRef<HTMLIFrameElement>(null); | ||||||||||||||
| const TRUSTED_ORIGINS = useMemo(() => { | ||||||||||||||
| const origins = process.env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS | ||||||||||||||
| ? process.env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS.split(",") | ||||||||||||||
| const origins = env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS | ||||||||||||||
| ? env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS.split(",") | ||||||||||||||
| : ["https://request.network", "https://core-api-staging.pay.so"]; | ||||||||||||||
|
|
||||||||||||||
| // Add localhost in development | ||||||||||||||
| if (process.env.NODE_ENV === "development") { | ||||||||||||||
| if (serverEnv.NODE_ENV === "development") { | ||||||||||||||
| origins.push("http://localhost:3000"); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,21 @@ | ||||||||||||
| import { createEnv } from "@t3-oss/env-nextjs"; | ||||||||||||
| import { z } from "zod"; | ||||||||||||
|
|
||||||||||||
| export const env = createEnv({ | ||||||||||||
| client: { | ||||||||||||
| NEXT_PUBLIC_REOWN_PROJECT_ID: z.string().min(1), | ||||||||||||
| NEXT_PUBLIC_API_TERMS_CONDITIONS: z.string().url(), | ||||||||||||
| NEXT_PUBLIC_GTM_ID: z.string().optional(), | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Tighten GTM ID validation. Reduce silent misconfig by validating the GTM container ID shape. - NEXT_PUBLIC_GTM_ID: z.string().optional(),
+ NEXT_PUBLIC_GTM_ID: z
+ .string()
+ .regex(/^GTM-[A-Z0-9]+$/i, "Expected GTM container ID like GTM-XXXXXXX")
+ .optional(),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS: z.string().optional(), | ||||||||||||
| NEXT_PUBLIC_DEMO_MEETING: z.string().optional(), | ||||||||||||
| }, | ||||||||||||
| runtimeEnv: { | ||||||||||||
| NEXT_PUBLIC_REOWN_PROJECT_ID: process.env.NEXT_PUBLIC_REOWN_PROJECT_ID, | ||||||||||||
| NEXT_PUBLIC_API_TERMS_CONDITIONS: | ||||||||||||
| process.env.NEXT_PUBLIC_API_TERMS_CONDITIONS, | ||||||||||||
| NEXT_PUBLIC_GTM_ID: process.env.NEXT_PUBLIC_GTM_ID, | ||||||||||||
| NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS: | ||||||||||||
| process.env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS, | ||||||||||||
| NEXT_PUBLIC_DEMO_MEETING: process.env.NEXT_PUBLIC_DEMO_MEETING, | ||||||||||||
| }, | ||||||||||||
| }); | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { createEnv } from "@t3-oss/env-nextjs"; | ||
| import { z } from "zod"; | ||
|
|
||
| export const env = createEnv({ | ||
| server: { | ||
| ENCRYPTION_KEY: z.string().min(1), | ||
| DATABASE_URL: z.string().min(1), | ||
| GOOGLE_CLIENT_ID: z.string().min(1), | ||
| GOOGLE_CLIENT_SECRET: z.string().min(1), | ||
| GOOGLE_REDIRECT_URI: z.string().min(1), | ||
| CURRENT_ENCRYPTION_VERSION: z.string().min(1), | ||
| REQUEST_API_URL: z.string().url(), | ||
| REQUEST_API_KEY: z.string().min(1), | ||
| WEBHOOK_SECRET: z.string().min(1), | ||
| FEE_PERCENTAGE_FOR_PAYMENT: z.string().default(""), | ||
| FEE_ADDRESS_FOR_PAYMENT: z.string().default(""), | ||
| REDIS_URL: z.string().default(""), | ||
| INVOICE_PROCESSING_TTL: z.string().default(""), | ||
| NODE_ENV: z.enum(["development", "production", "test"]), | ||
| VERCEL_URL: z.string().optional(), | ||
| PORT: z.coerce.number().optional(), | ||
| }, | ||
| experimental__runtimeEnv: process.env, | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,8 +1,9 @@ | ||||||||||||||||||||||||||
| import { env } from "@/env/server"; | ||||||||||||||||||||||||||
| import axios from "axios"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export const apiClient = axios.create({ | ||||||||||||||||||||||||||
| baseURL: process.env.REQUEST_API_URL, | ||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||
| "x-api-key": process.env.REQUEST_API_KEY, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| baseURL: env.REQUEST_API_URL, | ||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||
| "x-api-key": env.REQUEST_API_KEY, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
Comment on lines
+5
to
9
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a client-wide timeout for outbound API calls. External calls without timeouts can hang threads and degrade reliability. Apply this diff: export const apiClient = axios.create({
baseURL: env.REQUEST_API_URL,
headers: {
"x-api-key": env.REQUEST_API_KEY,
},
+ timeout: 10000, // 10s fail-fast
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,10 @@ | ||
| import { env } from "@/env/server"; | ||
|
|
||
| export type EncryptionVersion = "v1"; | ||
|
|
||
| export function getEncryptionKey(version: EncryptionVersion = "v1"): string { | ||
| return ENCRYPTION_KEYS[version]; | ||
| } | ||
| const ENCRYPTION_KEYS = { | ||
| v1: process.env.ENCRYPTION_KEY as string, | ||
| v1: env.ENCRYPTION_KEY as string, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Also skip validation during lint to avoid false CI failures.
Some linters/tsc tasks load Next config and would trigger env validation. Mirror SKIP_ENV_VALIDATION for the lint step.
Apply this diff:
📝 Committable suggestion
🤖 Prompt for AI Agents