diff --git a/src/server/session-config.ts b/src/server/session-config.ts index 8938183..0db9a13 100644 --- a/src/server/session-config.ts +++ b/src/server/session-config.ts @@ -1,5 +1,4 @@ import type { SessionConfig } from "vinxi/http"; -import { env } from "~/env/server"; import { AUTH_CONFIG, expiryToSeconds } from "~/config"; /** @@ -21,17 +20,58 @@ export interface SessionData { rememberMe: boolean; } +/** + * Get session password directly from process.env + * This avoids any bundler-time substitution issues with the validated env object + */ +function getSessionPassword(): string { + // Read directly from process.env at runtime, not from bundled env object + const password = process.env.JWT_SECRET_KEY; + if (!password || password.trim() === "") { + console.error( + `[SessionConfig] JWT_SECRET_KEY missing from process.env! Keys available:`, + Object.keys(process.env) + .filter((k) => k.includes("JWT") || k.includes("SECRET")) + .join(", ") || "none matching JWT/SECRET" + ); + throw new Error( + `JWT_SECRET_KEY is empty at runtime. Ensure it is set as a runtime environment variable in Vercel (not just build-time).` + ); + } + return password; +} + +/** + * Get session config with runtime password validation + * Returns a fresh config each time to ensure env vars are read at call time, + * not at module load time (important for serverless cold starts) + */ +export function getSessionConfig(): SessionConfig { + return { + password: getSessionPassword(), + name: "session", + cookie: { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/" + } + }; +} + /** * Vinxi session configuration - * Uses iron-session style password-based encryption + * Using a getter ensures password is evaluated at access time, not module load time */ export const sessionConfig: SessionConfig = { - password: env.JWT_SECRET_KEY, + get password() { + return getSessionPassword(); + }, name: "session", cookie: { httpOnly: true, - secure: env.NODE_ENV === "production", - sameSite: "lax", // Allow cookies on top-level navigation (OAuth/email redirects) for WebKit compatibility + secure: process.env.NODE_ENV === "production", + sameSite: "lax", path: "/" } }; diff --git a/src/server/session-helpers.ts b/src/server/session-helpers.ts index 1dd1898..0ab9069 100644 --- a/src/server/session-helpers.ts +++ b/src/server/session-helpers.ts @@ -13,7 +13,7 @@ import { env } from "~/env/server"; import { AUTH_CONFIG, expiryToSeconds, CACHE_CONFIG } from "~/config"; import { logAuditEvent } from "./audit"; import type { SessionData } from "./session-config"; -import { sessionConfig } from "./session-config"; +import { sessionConfig, getSessionConfig } from "./session-config"; import { getDeviceInfo } from "./device-utils"; import { cache } from "./cache"; @@ -213,8 +213,10 @@ export async function createAuthSession( }); // Update Vinxi session with dynamic maxAge based on rememberMe + // Use getSessionConfig() to ensure password is read at runtime + const baseConfig = getSessionConfig(); const configWithMaxAge = { - ...sessionConfig, + ...baseConfig, maxAge: rememberMe ? expiryToSeconds(AUTH_CONFIG.REFRESH_TOKEN_EXPIRY_LONG) : undefined // Session cookie (expires on browser close)