# Migration Guide: 11.24.x → 11.25.0

## Overview

| Category | Details |
|----------|---------|
| **Breaking Changes** | `cookies` default changed from `false` to `true`; secrets moved from `config.env.ts` to env vars; MongoDB URI no longer falls back to localhost in production |
| **New Features** | Unified CORS configuration (`cors`), configurable token exposure (`exposeTokenInBody`), Brevo transactional overlay via `BREVO_API_KEY`, Dockerfile HEALTHCHECK |
| **Security Fixes** | `secure` cookie flag now also honors app-level `env: 'staging'`; `cors: false` fully disables Apollo CORS (previously silently opened it); `cookieParser` signs with `betterAuth.secret` in IAM-only mode; JWT conversion now runs in hybrid cookie mode (previously skipped) |
| **Bugfixes** | Fixed inconsistency between nest-server and BetterAuth cookie defaults; fixed `resolveJwtToken` incorrectly skipping conversion when `exposeTokenInBody: true` |
| **Migration Effort** | 10–30 minutes (add env vars, review cookie/CORS behavior, update `.env`) |

---

## Quick Migration (If You Don't Want Cookies)

If your project was running without cookies and you want to keep it that way:

```typescript
// config.env.ts — add cookies: false to your environment config
production: {
  cookies: false,  // Explicit opt-out (was the implicit default before 11.25.0)
  // ... rest of config
}
```

If you want to adopt the new cookie default, no changes are needed — cookies are now enabled by default.

---

## What's New in 11.25.0

### 1. Cookies Enabled by Default

Cookies are now enabled by default, consistent with BetterAuth's existing behavior. This means:

- `cookie-parser` middleware is automatically loaded
- CORS is configured with `credentials: true`
- Session cookies are set on authentication responses
- JWT via `Authorization: Bearer` header continues to work independently

**Before (11.24.x):**
```typescript
// cookies was undefined → treated as false
// No cookie-parser, no CORS credentials
```

**After (11.25.0):**
```typescript
// cookies is undefined → treated as true (default)
// cookie-parser loaded, CORS with credentials: true
// To disable: cookies: false
```

### 2. Cookie Configuration Object (`ICookiesConfig`)

The `cookies` property now accepts a configuration object (Boolean Shorthand Pattern):

```typescript
// Boolean (same as before)
cookies: false    // Disabled
cookies: true     // Enabled (default)

// Object (new)
cookies: {
  enabled: true,              // Whether cookies are enabled (default: true)
  exposeTokenInBody: true,    // Keep token in response body alongside cookies (default: false)
}
```

#### `exposeTokenInBody` Option

By default, when cookies are enabled, the token is **removed from the response body** (the httpOnly cookie provides XSS protection — exposing the token in the body would negate this benefit).

Set `exposeTokenInBody: true` when clients need both JWT and cookies in parallel:

```typescript
// Hybrid mode: JWT + Cookies
cookies: { exposeTokenInBody: true }

// Login response includes:
// Response Body: { token: "eyJ...", session: {...}, user: {...} }
// Set-Cookie: iam.session_token=signed_value
```

### 3. Brevo Transactional API — Auto-Config via `BREVO_API_KEY`

The reference `config.env.ts` now auto-wires a `brevo` config block in `production`, `development`, and `local` environments when `BREVO_API_KEY` is set:

```typescript
// Automatically added to config.env.ts (no manual brevo: { ... } needed)
...(process.env.BREVO_API_KEY
  ? {
      brevo: {
        apiKey: process.env.BREVO_API_KEY,
        sender: { email: EMAIL_DEFAULT_SENDER, name: EMAIL_DEFAULT_SENDER_NAME },
      },
    }
  : {})
```

**How it integrates:** Brevo runs IN PARALLEL to SMTP. It is used only when a module explicitly references a `brevoTemplateId`:

```typescript
// config.env.ts
betterAuth: {
  emailVerification: {
    brevoTemplateId: 123,  // Uses Brevo Transactional API with server-side template
    locale: 'de',
  },
}
```

All other emails (password resets without a template, custom app mails, 2FA codes) continue to flow through the SMTP transport configured via `SMTP_HOST/PORT/USER/PASS`.

**Activation:**
```bash
# .env
BREVO_API_KEY=xkeysib-your-api-key
```

`ci` and `e2e` environments intentionally skip Brevo — tests never make external API calls.

### 4. Unified CORS Configuration (`ICorsConfig`)

A new `cors` property controls CORS across all three layers: GraphQL (Apollo), REST (Express), and BetterAuth (`trustedOrigins`).

```typescript
// Allow all origins (development)
cors: { allowAll: true }

// Specific additional origins (production)
cors: { allowedOrigins: ['https://admin.example.com'] }
// Combined with appUrl and baseUrl automatically

// Disable CORS on all layers including BetterAuth
cors: { enabled: false }

// Environment variable support
cors: {
  allowedOrigins: process.env.CORS_ALLOWED_ORIGINS?.split(',').filter(Boolean),
}
```

**Origin resolution priority:**
1. `cors.allowAll: true` → mirrors request origin (allows all)
2. `cors.allowedOrigins` + `appUrl` + `baseUrl` → deduplicated list
3. Only `appUrl`/`baseUrl` → those origins
4. Nothing configured → **no credentialed CORS** (secure default — same-origin requests still work; cross-origin browsers will fail the preflight). The legacy "allow all" fallback was removed in v11.25.0 because it combined with `credentials: true` would allow any website to perform credentialed requests. Configure `appUrl`/`baseUrl` or `cors.allowedOrigins` explicitly.

**BetterAuth integration:**
- `cors.allowAll` → BetterAuth `trustedOrigins` is set to `undefined` (allows all)
- `cors.allowedOrigins` → merged with `appUrl`/`baseUrl` for BetterAuth `trustedOrigins`
- `cors.enabled: false` → BetterAuth `trustedOrigins` set to `[]` (no origins allowed)
- Explicit `betterAuth.trustedOrigins` always takes precedence (backward compatible)

---

## Breaking Changes

### Change 1: `cookies` Default from `false` to `true`

**Impact:** Projects that did not explicitly set `cookies` will now have cookie handling enabled.

**Symptoms if not addressed:**
- Session cookies will be set on authentication responses
- CORS will include `credentials: true`
- Tokens may be removed from response body (if your code reads `response.token`)

**Fix:**
```typescript
// config.env.ts — if you want to keep the old behavior
production: {
  cookies: false,  // Explicit opt-out
}

// OR if you want cookies but need tokens in body
production: {
  cookies: { exposeTokenInBody: true },
}
```

### Change 2: Hardcoded Secret Fallbacks Removed from Production Config

**Impact:** Most of these environment variables were already read in 11.24.x. What changed in 11.25.0 is that the **hardcoded fallback strings in the production block** (e.g. `'SECRET_OR_PRIVATE_KEY_LOCAL'`, `'mongodb://127.0.0.1/nest-server-prod'`) were removed. Production deployments that previously relied on the reference config without setting env vars will now fail fast at startup with a clearer misconfiguration signal instead of silently booting with insecure defaults.

- **No action needed** if you already set all required env vars in your `.env` / secret store.
- **Action required** if you depended on the reference-config fallbacks — add the listed variables to your production environment.

Test environments (`ci`/`e2e`/`local`/`development`) still use hardcoded fallback secrets so tests run without a `.env` file (see the dedicated warning section further below).

**Variables read from the environment in the production config block:**

| Config Path | Environment Variable | Notes |
|-------------|---------------------|-------|
| `mongoose.uri` | `MONGODB_URI` | Required in production (no fallback) |
| `baseUrl` | `BASE_URL` | Used for CORS / Passkey / email links / OAuth callbacks |
| `jwt.secret` | `JWT_SECRET` | Required only for Legacy Auth |
| `jwt.refresh.secret` | `JWT_REFRESH_SECRET` | Required only for Legacy Auth |
| `betterAuth.secret` | `BETTER_AUTH_SECRET` | Required, min. 32 chars |
| `email.defaultSender.email` | `EMAIL_DEFAULT_SENDER` | |
| `email.defaultSender.name` | `EMAIL_DEFAULT_SENDER_NAME` | Default: `"Nest Server"` |
| `email.smtp.host` | `SMTP_HOST` | |
| `email.smtp.port` | `SMTP_PORT` | Default: `587` |
| `email.smtp.secure` | `SMTP_SECURE` | Default: `true` |
| `email.smtp.auth.user` | `SMTP_USER` | |
| `email.smtp.auth.pass` | `SMTP_PASS` | |
| `cors.allowedOrigins` | `CORS_ALLOWED_ORIGINS` | Comma-separated |
| `brevo.apiKey` | `BREVO_API_KEY` | **NEW** — activates Brevo overlay automatically |

When `BREVO_API_KEY` is set, the production config auto-configures a `brevo` config block with sender derived from `EMAIL_DEFAULT_SENDER` / `EMAIL_DEFAULT_SENDER_NAME`. No manual `brevo: { ... }` in `config.env.ts` is required anymore.

**Removed from the reference config (still supported in `IServerOptions`):**
- `email.mailjet` — the `MailjetService` remains available; only the hardcoded reference-config entry was removed. If your project actively uses it, keep your `email.mailjet` block.
- `email.passwordResetLink` / `email.verificationLink` — removed from the reference config. Both keys are still accepted by `IServerOptions` and read from `ConfigService` at runtime; derive them from `baseUrl`/`appUrl` if you don't need deployment-specific overrides.

**Fix:** Update your `.env` file with the required variables. See `.env.example` in the repo root — it documents all variables (required ones uncommented, optional ones commented out), including SMTP baseline and the optional Brevo Transactional API overlay for template-based mails.

```bash
# Production .env
MONGODB_URI=mongodb://host:27017/my-app
BASE_URL=https://api.example.com
BETTER_AUTH_SECRET=<32+ char secret>
SMTP_HOST=smtp.example.com
SMTP_USER=user
SMTP_PASS=password
EMAIL_DEFAULT_SENDER=noreply@example.com
```

---

## Detailed Migration Steps

### Step 1: Update Package

```bash
pnpm install @lenne.tech/nest-server@11.25.0

# If package.json dependencies changed
pnpm run update
```

### Step 2: Decide on Cookie Strategy

| Strategy | Config | Use Case |
|----------|--------|----------|
| **Cookie-only** (new default) | No config needed | Web apps with browser-based auth |
| **JWT-only** (old default) | `cookies: false` | Mobile apps, pure API clients |
| **Hybrid** | `cookies: { exposeTokenInBody: true }` | Apps needing both JWT + cookies |

### Step 3: Configure CORS (Optional)

```typescript
// Production: restrict to specific origins
cors: {
  allowedOrigins: process.env.CORS_ALLOWED_ORIGINS?.split(',').filter(Boolean),
}

// Development: allow all
cors: { allowAll: true }
```

### Step 4: Verify Build and Tests

```bash
pnpm run build
pnpm test
```

---

## Compatibility Notes

### JWT Authentication
JWT via `Authorization: Bearer <token>` continues to work independently of cookie configuration. The middleware token priority remains:
1. Authorization header (JWT) → always processed first
2. JWT from cookie (`lt-jwt-token`) → fallback when no auth header
3. Session cookie → final fallback

### Existing `betterAuth.trustedOrigins`
If your project explicitly sets `betterAuth.trustedOrigins`, it takes precedence over the new `cors` configuration. No changes needed.

### Legacy Auth
The Legacy Auth controller and resolver also respect `exposeTokenInBody`. Both `token` and `refreshToken` are kept in the response body when `exposeTokenInBody: true`.

Legacy auth cookies (`token`, `refreshToken`) are now set with **secure defaults**:
- `httpOnly: true` (no JavaScript access — XSS mitigation)
- `sameSite: 'lax'` (CSRF mitigation)
- `secure: true` in production (HTTPS-only)

### `secure` Cookie Flag — Staging Coverage

Auth cookies' `secure` flag is now `true` when EITHER:
- `config.env === 'production'` or `config.env === 'staging'` (app-level), OR
- `process.env.NODE_ENV === 'production'` (runtime)

Previously only `NODE_ENV` was checked. Staging deployments that set `config.env: 'staging'` but forgot `NODE_ENV=production` were silently sending auth cookies over HTTP.

### Apollo GraphQL CORS — `cors: false` Now Fully Disables

In v11.24.x, setting `cors: false` passed `cors: {}` to Apollo, which Apollo interprets as "CORS with default origin behavior" (i.e. it sent CORS headers with `Access-Control-Allow-Origin: *`). The fix in v11.25.0 passes `cors: false` to Apollo directly, which fully disables Apollo's CORS handling. No backward-compatible workaround is possible — projects that relied on the old permissive behavior must now explicitly set `cors: { allowAll: true }` instead.

### `cookieParser` Signing Secret — Fallback Chain

`main.ts` now picks a signing secret for the `cookie-parser` middleware via a fallback chain:

1. `jwt.secret` (Legacy Auth)
2. `betterAuth.secret` (IAM-only mode)
3. Unsigned cookies (no secret anywhere)

IAM-only projects previously had `cookieParser()` called without a signing secret even though a `betterAuth.secret` was configured. Signed cookies (`req.signedCookies`) are now verifiable in IAM-only mode.

### Hybrid Mode JWT Conversion (`resolveJwtToken`)

`CoreBetterAuthService.resolveJwtToken()` now converts opaque session tokens to proper JWTs when BOTH:
- `cookies.exposeTokenInBody: true` (hybrid mode)
- `betterAuth.jwt.enabled: true`

In 11.24.x the conversion was unconditionally skipped as soon as cookies were enabled, which meant hybrid clients received an opaque session token in the body instead of the expected JWT. Pure JWT-only mode (`cookies: false`) and cookie-only mode (default) behave unchanged.

### Production Startup Guard for `exposeTokenInBody`

Since v11.25.0, the framework **throws at startup** if `cookies.exposeTokenInBody: true` is set in `production` or `staging` environments. This prevents accidental XSS-risk misconfiguration (the httpOnly cookie protection is negated when the token is also in the body). Test environments (`ci`, `e2e`, `development`, `local`) are unaffected.

If you need hybrid JWT+Cookie in production, keep `exposeTokenInBody: false` and use the separate `GET /iam/token` endpoint on the client to fetch a JWT on demand.

### Secret Fallbacks in Dev/CI/E2E Environments

The `ci`, `e2e`, `development`, and `local` environment configs use **hardcoded fallback secrets** (e.g., `'SECRET_OR_PRIVATE_KEY_LOCAL'`, `'BETTER_AUTH_SECRET_LOCAL_32_CHARS_M'`) so that tests run without a `.env` file. These values are **publicly visible in this repository**.

> **Warning:** Do NOT deploy to production with `NODE_ENV=development` (or `ci`/`e2e`/`local`) AND missing real secrets. Any attacker can forge JWTs signed with the public fallback key. In production, `NODE_ENV=production` MUST be set, and `BETTER_AUTH_SECRET`, `JWT_SECRET`, `JWT_REFRESH_SECRET` MUST come from environment variables (the production config has no fallbacks — missing values cause startup failure).

### Dockerfile HEALTHCHECK

v11.25.0 adds a `HEALTHCHECK` directive to the default `Dockerfile`:

```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  CMD wget -qO- http://127.0.0.1:3000/health || exit 1
```

**Requirement:** The `/health` endpoint is provided by `HealthCheckModule`, which is enabled via `healthCheck` in `config.env.ts`. If your project disables it, Docker will keep reporting the container as `unhealthy` and orchestrators (Docker Swarm, Kubernetes readiness/liveness, docker-compose `depends_on.service.condition: service_healthy`) will refuse to route traffic or will restart the container.

**To enable:**
```typescript
// config.env.ts
production: {
  healthCheck: {}, // Uses defaults — enables /health endpoint
  // or opt out: healthCheck: false
}
```

**If you already had a custom Dockerfile:** The HEALTHCHECK is optional — remove the directive if your orchestrator performs its own probes.

### Deprecated: Static CoreModule Cookie Helpers

The static helpers `CoreModule.isCookiesEnabled()` and `CoreModule.isExposeTokenInBodyEnabled()` are now marked `@deprecated`. Use the standalone exports from the framework instead — they are framework-scoped (no `CoreModule` import), tree-shakeable, and consistent with the other helpers in `cookies.helper.ts`.

```typescript
// Before (11.24.x and earlier)
import { CoreModule } from '@lenne.tech/nest-server';
if (CoreModule.isCookiesEnabled(config.cookies)) { ... }

// After (11.25.0+)
import { isCookiesEnabled, isExposeTokenInBodyEnabled } from '@lenne.tech/nest-server';
if (isCookiesEnabled(config.cookies)) { ... }
```

Both static methods still work (they delegate internally) — they will be removed in a future MINOR release.

### MongoDB URI — No Production Fallback

Production config no longer falls back to `mongodb://127.0.0.1/nest-server-prod` when `MONGODB_URI` is unset. Starting the server without a valid `MONGODB_URI` in production will now fail fast. This prevents silent misconfiguration where the app would otherwise connect to an unintended local database.

---

## Troubleshooting

### Tokens missing from API responses
**Cause:** Cookies are now enabled by default, which removes tokens from the response body.
**Fix:** Set `cookies: { exposeTokenInBody: true }` or `cookies: false`.

### CORS errors in browser
**Cause:** CORS now includes `credentials: true` by default, which requires explicit origins.
**Fix:** Set `cors: { allowAll: true }` for development, or configure `cors.allowedOrigins` for production.

### Tests failing with "token is null"
**Cause:** Test environment needs `exposeTokenInBody: true` if tests read tokens from response body.
**Fix:** Set `cookies: { exposeTokenInBody: true }` in test/CI/E2E config.

### `Error: SECURITY: cookies.exposeTokenInBody must not be true in production or staging`
**Cause:** The new production safety guard — `exposeTokenInBody: true` is blocked in `production`/`staging` envs.
**Fix:** Either remove `exposeTokenInBody` from those env blocks, or use `GET /iam/token` on the client to fetch a JWT on demand.

### Hybrid client receives opaque session token instead of JWT
**Cause:** Fixed in 11.25.0 — `resolveJwtToken` previously skipped conversion whenever cookies were enabled.
**Fix:** Upgrading to 11.25.0 fixes this automatically. Ensure `betterAuth.jwt.enabled: true` is set.

### Container stays in `unhealthy` state
**Cause:** The new Dockerfile HEALTHCHECK probes `/health`, but `HealthCheckModule` is disabled.
**Fix:** Enable `healthCheck: {}` in your `config.env.ts`, or remove the `HEALTHCHECK` directive if you don't use it.

### MongoDB connection refused after upgrade in production
**Cause:** The production config no longer falls back to `mongodb://127.0.0.1/nest-server-prod` when `MONGODB_URI` is unset.
**Fix:** Set `MONGODB_URI` explicitly in your production `.env` / secret store.

---

## Module Documentation

### Core Module
- **Shared Helper (NEW):** [src/core/common/helpers/cookies.helper.ts](../src/core/common/helpers/cookies.helper.ts) — `isCookiesEnabled`, `isExposeTokenInBodyEnabled`, `isCorsDisabled`, `isProductionLikeEnv`, `getDefaultAuthCookieOptions`, `setLegacyAuthCookies`, `assertCookiesProductionSafe`, `buildCorsConfig`, `shouldConvertSessionTokenToJwt`
- **Core Module:** [src/core.module.ts](../src/core.module.ts) — `buildCorsConfig()` integration, `assertCookiesProductionSafe()` guard, deprecated static helpers
- **Interface:** [src/core/common/interfaces/server-options.interface.ts](../src/core/common/interfaces/server-options.interface.ts) — `ICookiesConfig`, `ICorsConfig`
- **Bootstrap:** [src/main.ts](../src/main.ts) — Conditional `cookieParser` with fallback chain, tri-case CORS logic

### BetterAuth Module
- **Config:** [src/core/modules/better-auth/better-auth.config.ts](../src/core/modules/better-auth/better-auth.config.ts) — `buildTrustedOrigins()` with `serverCorsConfig`
- **Cookie Helper:** [src/core/modules/better-auth/core-better-auth-cookie.helper.ts](../src/core/modules/better-auth/core-better-auth-cookie.helper.ts) — `exposeTokenInBody` parameter, `env` field for staging `secure` flag
- **Service:** [src/core/modules/better-auth/core-better-auth.service.ts](../src/core/modules/better-auth/core-better-auth.service.ts) — `resolveJwtToken()` hybrid-mode fix
- **Integration Checklist:** [src/core/modules/better-auth/INTEGRATION-CHECKLIST.md](../src/core/modules/better-auth/INTEGRATION-CHECKLIST.md)

### Legacy Auth Module
- **Controller:** [src/core/modules/auth/core-auth.controller.ts](../src/core/modules/auth/core-auth.controller.ts) — Updated `processCookies()`
- **Resolver:** [src/core/modules/auth/core-auth.resolver.ts](../src/core/modules/auth/core-auth.resolver.ts) — Updated `processCookies()`

### Infrastructure
- **Dockerfile:** [Dockerfile](../Dockerfile) — New `HEALTHCHECK` directive
- **Env Example:** [.env.example](../.env.example) — Consolidated (required + optional in one file)
- **Request Lifecycle:** [docs/REQUEST-LIFECYCLE.md](../docs/REQUEST-LIFECYCLE.md) — Express-middleware section documents `cookie-parser` and CORS ordering
- **API Reference:** [FRAMEWORK-API.md](../FRAMEWORK-API.md) — Auto-generated, includes `ICookiesConfig` + `ICorsConfig` field lists

---

## References

- [Configurable Features Pattern](./../.claude/rules/configurable-features.md)
- [nest-server-starter](https://github.com/lenneTech/nest-server-starter) (reference implementation)
