Skip to content
Open
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
40 changes: 40 additions & 0 deletions __tests__/dev-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,46 @@ describe('dev-env / buildDevEnv', () => {
expect(env.app.env.NUXT_PUBLIC_STORAGE_PREFIX).toBe('crm');
});

test('gives the App a session password so logins work on the built server', () => {
// Regression: `lt dev test` serves the *built* Nitro server, which never reads the project's
// .env. Without a password every login answered 500 ("H3Error: Empty password") and roughly
// half of a project's Playwright suite failed on a cause unrelated to its tests.
const env = buildDevEnv({ apiInternalPort: 4010, appInternalPort: 4011, identity: fullIdentity });

// h3 rejects anything shorter than 32 characters
expect(env.app.env.NUXT_SESSION_PASSWORD).toHaveLength(32);
});

test('derives the session password deterministically per project', () => {
const first = buildDevEnv({ apiInternalPort: 4010, appInternalPort: 4011, identity: fullIdentity });
const second = buildDevEnv({ apiInternalPort: 4020, appInternalPort: 4021, identity: fullIdentity });

// Same slug → same value, so a restart does not invalidate open sessions and every shard
// of `lt dev test --shard N` agrees
expect(second.app.env.NUXT_SESSION_PASSWORD).toBe(first.app.env.NUXT_SESSION_PASSWORD);

const other = buildDevEnv({
apiInternalPort: 4010,
appInternalPort: 4011,
identity: { ...fullIdentity, slug: 'shop' },
});

// Different project → different value, so one stack's cookies never validate against another
expect(other.app.env.NUXT_SESSION_PASSWORD).not.toBe(first.app.env.NUXT_SESSION_PASSWORD);
});

test('never overrides a session password the project already set', () => {
// A project with real session data must keep its own value — lt dev only fills the gap
const env = buildDevEnv({
apiInternalPort: 4010,
appInternalPort: 4011,
baseEnv: { NUXT_SESSION_PASSWORD: 'project-owned-value-with-32-chars' },
identity: fullIdentity,
});

expect(env.app.env.NUXT_SESSION_PASSWORD).toBe('project-owned-value-with-32-chars');
});

test('pins HOST to 127.0.0.1 for both API and App so Caddy upstream stays unambiguous', () => {
// Regression: without HOST=127.0.0.1 Nuxt / Nest may bind to
// `[::1]` only on macOS, and Caddy's IPv4 upstream gets a
Expand Down
3 changes: 3 additions & 0 deletions src/lib/dev-env-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ export function writeEnvBridge(projectRoot: string, devEnv: DevEnv, dbName?: str
'NUXT_PUBLIC_SITE_URL',
'NUXT_PUBLIC_STORAGE_PREFIX',
'NUXT_PUBLIC_API_PROXY',
// Not needed by the runner itself, but external suites check it to tell a stack that
// can log in from one that will 500 on every login (see dev-env.ts).
'NUXT_SESSION_PASSWORD',
'NSC__MONGOOSE__URI',
'DATABASE_URL',
// Legacy aliases — see dev-env.ts for the rationale.
Expand Down
25 changes: 25 additions & 0 deletions src/lib/dev-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
* subdomains succeed. Without this Nuxt SSR fails with "unable to
* get local issuer certificate" when the app calls its own API.
*/
import { createHash } from 'node:crypto';

import { detectCaddyRootCa } from './dev-env-bridge';
import { DevIdentity } from './dev-identity';

Expand Down Expand Up @@ -108,6 +110,17 @@ export function buildDevEnv(input: BuildDevEnvInput): DevEnv {
// same-origin trickery is no longer required.
NUXT_PUBLIC_API_PROXY: 'false',
NUXT_PUBLIC_STORAGE_PREFIX: identity.slug,
// Nuxt/h3 sessions refuse to start without a password (>= 32 chars): every login
// answers 500 "H3Error: Empty password". `nuxt dev` papers over this by reading the
// project's .env, but `lt dev test` serves the *built* Nitro server, which never does —
// so a project with a perfectly good .env still saw half its E2E suite fail on an error
// that has nothing to do with its tests.
//
// Derived from the slug rather than random so sessions survive a restart and every
// shard of `lt dev test --shard N` agrees. Local-only by construction: it never reaches
// a deployed environment, and a project that sets its own value keeps it (baseEnv wins
// because this key is only added when the inherited env has none).
...(baseEnv.NUXT_SESSION_PASSWORD ? {} : { NUXT_SESSION_PASSWORD: deriveSessionPassword(identity.slug) }),
PORT: String(appInternalPort),
// macOS: the default $TMPDIR (/var/folders/…/T/, ~49 chars) pushes Nuxt's
// vite-node IPC socket path past the 104-char UNIX sun_path limit, so the
Expand All @@ -125,3 +138,15 @@ export function buildDevEnv(input: BuildDevEnvInput): DevEnv {
function buildPostgresUrl(dbName: string): string {
return `postgresql://${dbName}:${dbName}@localhost:5432/${dbName}`;
}

/**
* Stable local session password for a project's app process.
*
* 32 hex chars — h3 rejects anything shorter. Deterministic per slug: the same project always
* gets the same value, so restarting the stack does not invalidate open sessions and parallel
* shards stay consistent. Not a secret in any meaningful sense and not meant to be one; it exists
* so a local stack boots without hand-set environment variables.
*/
function deriveSessionPassword(slug: string): string {
return createHash('sha256').update(`lt-dev:session:${slug}`).digest('hex').slice(0, 32);
}
2 changes: 1 addition & 1 deletion src/lib/dev-patches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export function patchClaudeMd(file: string, options: { dbName?: string; identity
if (dbName) lines.push(`- DB: \`mongodb://127.0.0.1/${dbName}\``);
lines.push('');
lines.push(
'Env vars set automatically by `lt dev up`: `BASE_URL`, `APP_URL`, `NUXT_API_URL`, `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_SITE_URL`, `NUXT_PUBLIC_STORAGE_PREFIX`, `NSC__MONGOOSE__URI`, `DATABASE_URL`. **Never assume `localhost:3000` / `localhost:3001` for this project** — those are the framework defaults, not the active URLs.',
'Env vars set automatically by `lt dev up`: `BASE_URL`, `APP_URL`, `NUXT_API_URL`, `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_SITE_URL`, `NUXT_PUBLIC_STORAGE_PREFIX`, `NUXT_SESSION_PASSWORD`, `NSC__MONGOOSE__URI`, `DATABASE_URL`. **Never assume `localhost:3000` / `localhost:3001` for this project** — those are the framework defaults, not the active URLs.',
);
lines.push('');
lines.push(endMarker);
Expand Down
Loading