possible sealsession fix

This commit is contained in:
Michael Freno
2026-01-16 00:27:04 -05:00
parent 962456985b
commit 3981651736
2 changed files with 49 additions and 7 deletions

View File

@@ -1,5 +1,4 @@
import type { SessionConfig } from "vinxi/http"; import type { SessionConfig } from "vinxi/http";
import { env } from "~/env/server";
import { AUTH_CONFIG, expiryToSeconds } from "~/config"; import { AUTH_CONFIG, expiryToSeconds } from "~/config";
/** /**
@@ -21,17 +20,58 @@ export interface SessionData {
rememberMe: boolean; 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 * 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 = { export const sessionConfig: SessionConfig = {
password: env.JWT_SECRET_KEY, get password() {
return getSessionPassword();
},
name: "session", name: "session",
cookie: { cookie: {
httpOnly: true, httpOnly: true,
secure: env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",
sameSite: "lax", // Allow cookies on top-level navigation (OAuth/email redirects) for WebKit compatibility sameSite: "lax",
path: "/" path: "/"
} }
}; };

View File

@@ -13,7 +13,7 @@ import { env } from "~/env/server";
import { AUTH_CONFIG, expiryToSeconds, CACHE_CONFIG } from "~/config"; import { AUTH_CONFIG, expiryToSeconds, CACHE_CONFIG } from "~/config";
import { logAuditEvent } from "./audit"; import { logAuditEvent } from "./audit";
import type { SessionData } from "./session-config"; import type { SessionData } from "./session-config";
import { sessionConfig } from "./session-config"; import { sessionConfig, getSessionConfig } from "./session-config";
import { getDeviceInfo } from "./device-utils"; import { getDeviceInfo } from "./device-utils";
import { cache } from "./cache"; import { cache } from "./cache";
@@ -213,8 +213,10 @@ export async function createAuthSession(
}); });
// Update Vinxi session with dynamic maxAge based on rememberMe // Update Vinxi session with dynamic maxAge based on rememberMe
// Use getSessionConfig() to ensure password is read at runtime
const baseConfig = getSessionConfig();
const configWithMaxAge = { const configWithMaxAge = {
...sessionConfig, ...baseConfig,
maxAge: rememberMe maxAge: rememberMe
? expiryToSeconds(AUTH_CONFIG.REFRESH_TOKEN_EXPIRY_LONG) ? expiryToSeconds(AUTH_CONFIG.REFRESH_TOKEN_EXPIRY_LONG)
: undefined // Session cookie (expires on browser close) : undefined // Session cookie (expires on browser close)