// ────────────────────────────────────────────── // 🔒 GUARD: This module MUST NEVER be imported client-side. // It contains secrets: DB tokens, JWT keys, AWS credentials, API tokens. // ────────────────────────────────────────────── // Build-time guard — Vite dead-code eliminates everything below when SSR=true. // If this module is bundled client-side, it throws before any secrets are parsed. if (!import.meta.env.SSR) { throw new Error( "[SERVER-ONLY] src/env/server.ts was imported client-side! " + "This module contains server-only secrets (database credentials, JWT keys, API tokens) " + "and must never be bundled in client code." ); } // Runtime defense-in-depth — throws if somehow evaluated in a browser context. if (typeof window !== "undefined") { throw new Error( "[SERVER-ONLY] src/env/server.ts was loaded in a browser context. " + "This is a critical security violation." ); } import { z } from "zod"; const serverEnvSchema = z.object({ NODE_ENV: z.enum(["development", "test", "production"]), JWT_SECRET_KEY: z.string().min(1), AWS_REGION: z.string().min(1), AWS_S3_BUCKET_NAME: z.string().min(1), MY_AWS_ACCESS_KEY: z.string().min(1), MY_AWS_SECRET_KEY: z.string().min(1), GOOGLE_CLIENT_SECRET: z.string().min(1), GITHUB_CLIENT_SECRET: z.string().min(1), EMAIL_SERVER: z.string().min(1), EMAIL_FROM: z.string().min(1), SENDINBLUE_KEY: z.string().min(1), TURSO_DB_URL: z.string().min(1), TURSO_DB_TOKEN: z.string().min(1), TURSO_LINEAGE_URL: z.string().min(1), TURSO_LINEAGE_TOKEN: z.string().min(1), TURSO_DB_API_TOKEN: z.string().min(1), LINEAGE_OFFLINE_SERIALIZATION_SECRET: z.string().min(1), GITEA_URL: z.string().min(1), GITEA_TOKEN: z.string().min(1), GITHUB_API_TOKEN: z.string().min(1), VITE_DOMAIN: z.string().min(1), VITE_AWS_BUCKET_STRING: z.string().min(1), VITE_DOWNLOAD_BUCKET_STRING: z.string().min(1), VITE_GOOGLE_CLIENT_ID: z.string().min(1), VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE: z.string().min(1), VITE_GITHUB_CLIENT_ID: z.string().min(1), VITE_WEBSOCKET: z.string().min(1), VITE_INFILL_ENDPOINT: z.string().min(1), INFILL_BEARER_TOKEN: z.string().min(1), NESSA_DB_URL: z.string().min(1), NESSA_DB_TOKEN: z.string().min(1), // Clerk authentication — Nessa auth is now Clerk-backed. The // legacy self-issued JWT signing env var was removed. NESSA_CLERK_SECRET: z.string().min(1), NESSA_CLERK_JWT_ISSUER: z.string().min(1), // Clerk webhook signing secret (Svix). Used to verify `user.created` / // `user.updated` webhook payloads. Find it under Clerk Dashboard → // Webhooks → your endpoint → Signing Secret (starts with `whsec_`). NESSA_CLERK_WEBHOOK_SECRET: z.string().min(1), LINEAGE_JWT_SECRET: z.string().min(32), APPLE_CLIENT_ID_NESSA: z.string().min(1).optional(), APPLE_CLIENT_ID_LINEAGE: z.string().min(1).optional(), VITE_TURNSTILE_SITE_KEY: z.string().min(1), TURNSTILE_SECRET_KEY: z.string().min(1), NOOK_DB_URL: z.string().min(1), NOOK_DB_TOKEN: z.string().min(1), NOOK_LICENSE_PRIVATE_KEY: z.string().min(1), NOOK_STRIPE_SK: z.string().min(1), NOOK_STRIPE_WEBHOOK_SECRET: z.string().min(1), NOOK_STRIPE_PRICE_ID: z.string().min(1) }); export type ServerEnv = z.infer; export const validateServerEnv = ( envVars: Record ): ServerEnv => { try { return serverEnvSchema.parse(envVars); } catch (error) { if (error instanceof z.ZodError) { const formattedErrors = error.format(); const missingVars = Object.entries(formattedErrors) .filter( ([key, value]) => key !== "_errors" && typeof value === "object" && value._errors?.length > 0 && value._errors[0] === "Required" ) .map(([key, _]) => key); const invalidVars = Object.entries(formattedErrors) .filter( ([key, value]) => key !== "_errors" && typeof value === "object" && value._errors?.length > 0 && value._errors[0] !== "Required" ) .map(([key, value]) => ({ key, error: value._errors[0] })); let errorMessage = "Environment validation failed:\n"; if (missingVars.length > 0) { errorMessage += `Missing required variables: ${missingVars.join(", ")}\n`; } if (invalidVars.length > 0) { errorMessage += "Invalid values:\n"; invalidVars.forEach(({ key, error }) => { errorMessage += ` ${key}: ${error}\n`; }); } console.error(errorMessage); throw new Error(errorMessage); } console.error("Environment validation failed with unknown error:", error); throw new Error("Environment validation failed with unknown error"); } }; const validateAndExportEnv = (): ServerEnv => { try { const validated = validateServerEnv(process.env); console.log("✅ Environment validation successful"); return validated; } catch (error) { console.error("❌ Environment validation failed:", error); throw error; } }; export const env = validateAndExportEnv(); export const isMissingEnvVar = (varName: string): boolean => { return !process.env[varName] || process.env[varName]?.trim() === ""; }; export const getMissingEnvVars = (): string[] => { const requiredServerVars = [ "NODE_ENV", "JWT_SECRET_KEY", "AWS_REGION", "AWS_S3_BUCKET_NAME", "MY_AWS_ACCESS_KEY", "MY_AWS_SECRET_KEY", "GOOGLE_CLIENT_SECRET", "GITHUB_CLIENT_SECRET", "EMAIL_SERVER", "EMAIL_FROM", "SENDINBLUE_KEY", "TURSO_DB_URL", "TURSO_DB_TOKEN", "TURSO_LINEAGE_URL", "TURSO_LINEAGE_TOKEN", "TURSO_DB_API_TOKEN", "LINEAGE_OFFLINE_SERIALIZATION_SECRET", "GITEA_URL", "GITEA_TOKEN", "GITHUB_API_TOKEN", "VITE_DOMAIN", "VITE_AWS_BUCKET_STRING", "VITE_DOWNLOAD_BUCKET_STRING", "VITE_GOOGLE_CLIENT_ID", "VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE", "VITE_GITHUB_CLIENT_ID", "VITE_WEBSOCKET", "NESSA_DB_URL", "NESSA_DB_TOKEN", "NESSA_CLERK_SECRET", "NESSA_CLERK_JWT_ISSUER", "NESSA_CLERK_WEBHOOK_SECRET", "LINEAGE_JWT_SECRET" ]; return requiredServerVars.filter((varName) => isMissingEnvVar(varName)); };