Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions packages/root/src/cli/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {createViteServer} from '../node/vite.js';
import {DevServerAssetMap} from '../render/asset-map/dev-asset-map.js';
import {dirExists, isDirectory, isJsFile} from '../utils/fsutils.js';
import {findOpenPort} from '../utils/ports.js';
import {randString} from '../utils/rand.js';
import {getSessionCookieSecret} from '../utils/rand.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

Expand Down Expand Up @@ -126,8 +126,7 @@ export async function createDevServer(options?: {
server.use(hooksMiddleware());

// Session middleware for handling session cookies.
const sessionCookieSecret =
rootConfig.server?.sessionCookieSecret || randString(36);
const sessionCookieSecret = getSessionCookieSecret(rootConfig, rootDir);
server.use(cookieParser(sessionCookieSecret));
server.use(sessionMiddleware());

Expand Down
5 changes: 2 additions & 3 deletions packages/root/src/cli/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
BuildAssetMap,
} from '../render/asset-map/build-asset-map.js';
import {fileExists, loadJson} from '../utils/fsutils.js';
import {randString} from '../utils/rand.js';
import {getSessionCookieSecret} from '../utils/rand.js';

type RenderModule = typeof import('../render/render.js');

Expand Down Expand Up @@ -68,8 +68,7 @@ export async function createPreviewServer(options: {
server.use(hooksMiddleware());

// Session middleware for handling session cookies.
const sessionCookieSecret =
rootConfig.server?.sessionCookieSecret || randString(36);
const sessionCookieSecret = getSessionCookieSecret(rootConfig, rootDir);
server.use(cookieParser(sessionCookieSecret));
server.use(sessionMiddleware());

Expand Down
5 changes: 2 additions & 3 deletions packages/root/src/cli/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
BuildAssetMap,
} from '../render/asset-map/build-asset-map.js';
import {fileExists, loadJson} from '../utils/fsutils.js';
import {randString} from '../utils/rand.js';
import {getSessionCookieSecret} from '../utils/rand.js';

type RenderModule = typeof import('../render/render.js');

Expand Down Expand Up @@ -63,8 +63,7 @@ export async function createProdServer(options: {
server.use(hooksMiddleware());

// Session middleware for handling session cookies.
const sessionCookieSecret =
rootConfig.server?.sessionCookieSecret || randString(36);
const sessionCookieSecret = getSessionCookieSecret(rootConfig, rootDir);
server.use(cookieParser(sessionCookieSecret));
server.use(sessionMiddleware());

Expand Down
5 changes: 5 additions & 0 deletions packages/root/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ export interface RootServerConfig {

/**
* Cookie secret for the session middleware.
*
* Generate a secure secret with:
* ```
* node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
* ```
*/
sessionCookieSecret?: string | string[];

Expand Down
37 changes: 37 additions & 0 deletions packages/root/src/utils/rand.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import crypto from 'node:crypto';

import {RootConfig} from '../core/config.js';

export function randString(len: number): string {
const result = [];
const chars =
Expand All @@ -8,3 +12,36 @@ export function randString(len: number): string {
}
return result.join('');
}

/**
* Generates a deterministic session secret based on a seed string (e.g., project path).
* This ensures the same secret is generated for the same seed across dev server restarts,
* while still allowing different projects to have unique secrets.
*/
function deterministicSessionSecret(seed: string): string {
return crypto.createHash('sha256').update(seed).digest('hex');
}

/**
* Gets the session cookie secret for the server.
*
* Returns the configured session cookie secret, or generates one if not provided:
* - In development: uses a deterministic secret based on rootDir for session persistence
* - In production: generates a random secret (sessions won't persist across restarts)
*/
export function getSessionCookieSecret(
rootConfig: RootConfig,
rootDir: string
): string | string[] {
// Use configured secret if provided.
if (rootConfig.server?.sessionCookieSecret) {
return rootConfig.server.sessionCookieSecret;
}

// Use deterministic secret in dev mode for consistent sessions across server restarts.
if (process.env.NODE_ENV === 'development') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you include a short jscomment explaining this line

return deterministicSessionSecret(rootDir);
}

return randString(36);
}