# Migration Guide: 11.12.x → 11.13.0

## Overview

| Category | Details |
|----------|---------|
| **Breaking Changes** | Email verification enabled by default, `termsAndPrivacyAccepted` required by default on sign-up |
| **New Features** | Email Verification service, Sign-Up Checks with `termsAndPrivacyAccepted`, `termsAndPrivacyAcceptedAt` user field, TestHelper cookie support |
| **Bugfixes** | Passkey login response enrichment, Express Response import fix, JWT mode session fallback, Middleware credential fallback (401 fix), RolesGuard REST controller fix |
| **Security** | `@apollo/server` 5.3.0 → 5.4.0 (DoS vulnerability fix) |
| **Removed** | `pm2` and `grunt` devDependencies (does not affect consuming projects) |
| **Migration Effort** | Low (~10 minutes) - Sign-up flow updates + optional email verification configuration |

---

## Quick Migration

```bash
# Update package
npm install @lenne.tech/nest-server@11.13.0

# Verify build
npm run build

# Run tests
npm test
```

**Note:** Email verification is now **enabled by default** (zero-config). Sign-up requires `termsAndPrivacyAccepted: true`. See Breaking Changes if you need to restore the previous behavior.

---

## What's New in 11.13.0

### 1. Email Verification (enabled by default)

Email verification via Better-Auth's `emailVerification` plugin is now **enabled by default** without any configuration needed:

- Users receive a verification email after sign-up
- `verifiedAt` is automatically set when email is verified
- Templates provided in German (`de`) and English (`en`)
- Falls back to nest-server's built-in templates if no project templates exist

```typescript
// Zero-config: email verification is active by default

// To customize:
betterAuth: {
  emailVerification: {
    expiresIn: 86400,           // Token expiration in seconds (default: 24h)
    template: 'custom-verify',  // Custom template name
    locale: 'en',               // Template locale (default: 'en')
    brevoTemplateId: 42,        // Send via Brevo transactional API (optional)
  },
}

// To disable email verification:
betterAuth: {
  emailVerification: false,
}
```

**Configuration behavior:**
- `undefined` / `null` (no config): **enabled** with defaults
- `true`: **enabled** with defaults
- `false`: **disabled**
- `{}`: **enabled** with defaults
- `{ locale: 'de' }`: **enabled** with custom settings
- `{ enabled: false }`: **disabled** (allows pre-configuration)

**Brevo Integration:** When `brevoTemplateId` is set and Brevo is configured (`config.brevo`), verification emails are sent via Brevo's transactional API instead of SMTP/EJS templates. Template variables: `name`, `link`, `appName`, `expiresIn`.

### 2. Sign-Up Checks (enabled by default)

Sign-up validation is now **enabled by default** requiring `termsAndPrivacyAccepted: true`:

```graphql
# Sign-up now requires termsAndPrivacyAccepted
mutation {
  betterAuthSignUp(
    email: "user@example.com"
    password: "hashedPassword"
    name: "User"
    termsAndPrivacyAccepted: true  # Required by default
  ) {
    success
    user { id email }
  }
}
```

When accepted, `termsAndPrivacyAcceptedAt` is stored as a `Date` in the user record.

```typescript
// To disable sign-up checks:
betterAuth: {
  signUpChecks: false,
}

// To customize required fields:
betterAuth: {
  signUpChecks: {
    requiredFields: ['termsAndPrivacyAccepted', 'ageConfirmed'],
  },
}
```

### 3. TestHelper Cookie Support

The `TestHelper` now supports cookie-based authentication for REST API testing, making it easy to test BetterAuth session-based endpoints:

```typescript
// Auto-detection: plain session token -> automatically sets iam.session_token + token cookies
const result = await testHelper.rest('/protected-endpoint', {
  cookies: sessionToken,
});

// Explicit cookie pairs
const result = await testHelper.rest('/protected-endpoint', {
  cookies: { 'iam.session_token': token, 'custom': 'value' },
});

// Raw cookie string
const result = await testHelper.rest('/protected-endpoint', {
  cookies: 'iam.session_token=abc; token=xyz',
});
```

**New static helper methods:**

```typescript
// Build BetterAuth cookie record from session token
const cookies = TestHelper.buildBetterAuthCookies(sessionToken);

// Extract session token from Set-Cookie response headers
const token = TestHelper.extractSessionToken(response);

// Extract all cookies from response
const allCookies = TestHelper.extractCookies(response);
```

`token` (Authorization header) and `cookies` (Cookie header) can be used simultaneously without conflict.

Full documentation: `src/test/README.md`

### 4. `termsAndPrivacyAcceptedAt` User Field

A new `termsAndPrivacyAcceptedAt` field is added to `CoreUserModel`:

```typescript
// Available on user objects
user.termsAndPrivacyAcceptedAt // Date | undefined
```

This field is automatically set when a user signs up with `termsAndPrivacyAccepted: true`.

---

## Breaking Changes

### 1. Email Verification Now Enabled by Default

**Before (11.12.x):** Email verification was disabled when no `emailVerification` config was present.

**After (11.13.0):** Email verification is enabled by default. Users must verify their email after sign-up.

**Impact:** New sign-ups will trigger a verification email. Unverified users may be restricted depending on your role configuration (e.g., `S_VERIFIED`).

**To restore old behavior:**
```typescript
// config.env.ts
betterAuth: {
  emailVerification: false,
}
```

### 2. Sign-Up Now Requires `termsAndPrivacyAccepted`

**Impact:** Sign-up via BetterAuth will fail if `termsAndPrivacyAccepted: true` is not provided.

**Before (11.12.x):**
```graphql
mutation {
  betterAuthSignUp(email: "user@example.com", password: "pass", name: "User") {
    success
  }
}
```

**After (11.13.0):**
```graphql
mutation {
  betterAuthSignUp(
    email: "user@example.com"
    password: "pass"
    name: "User"
    termsAndPrivacyAccepted: true  # Now required!
  ) {
    success
  }
}
```

**To restore old behavior:**
```typescript
// config.env.ts
betterAuth: {
  signUpChecks: false,
}
```

**For custom resolvers:** Update your `BetterAuthResolver` to include the new parameter and inject `CoreBetterAuthSignUpValidatorService`:

```typescript
import { CoreBetterAuthSignUpValidatorService } from '@lenne.tech/nest-server';

constructor(
  betterAuthService: CoreBetterAuthService,
  userMapper: CoreBetterAuthUserMapper,
  @Optional() signUpValidator?: CoreBetterAuthSignUpValidatorService,
) {
  super(betterAuthService, userMapper, signUpValidator);
}

@Mutation(() => CoreBetterAuthAuthModel)
@Roles(RoleEnum.S_EVERYONE)
override async betterAuthSignUp(
  @Args('email') email: string,
  @Args('password') password: string,
  @Args('name', { nullable: true }) name?: string,
  @Args('termsAndPrivacyAccepted', { nullable: true }) termsAndPrivacyAccepted?: boolean,
): Promise<CoreBetterAuthAuthModel> {
  return super.betterAuthSignUp(email, password, name, termsAndPrivacyAccepted);
}
```

---

## Detailed Migration Steps

### Step 1: Update Package

```bash
npm install @lenne.tech/nest-server@11.13.0
```

### Step 2: Update Sign-Up Calls

If you have sign-up forms or API calls, add `termsAndPrivacyAccepted: true`:

**GraphQL:**
```graphql
mutation {
  betterAuthSignUp(
    email: "user@example.com"
    password: "hashedPassword"
    name: "User"
    termsAndPrivacyAccepted: true  # Add this!
  ) {
    success
  }
}
```

**REST API:**
```json
POST /iam/sign-up/email
{
  "email": "user@example.com",
  "password": "hashedPassword",
  "name": "User",
  "termsAndPrivacyAccepted": true
}
```

**Or disable the requirement in config.env.ts:**
```typescript
betterAuth: {
  signUpChecks: false,
}
```

### Step 3: Update Custom Resolvers and Controllers (if applicable)

**Custom BetterAuthResolver:** Update to include:
1. Import `CoreBetterAuthSignUpValidatorService`
2. Add `@Optional() signUpValidator?` to constructor
3. Add `termsAndPrivacyAccepted` parameter to `betterAuthSignUp` method

See the Breaking Changes section for the code example.

**Custom BetterAuthController:** Update to include:
1. Import `CoreBetterAuthSignUpValidatorService`
2. Add `@Optional() signUpValidator?` to constructor
3. Pass it to `super()`:

```typescript
import { CoreBetterAuthSignUpValidatorService } from '@lenne.tech/nest-server';

constructor(
  betterAuthService: CoreBetterAuthService,
  userMapper: CoreBetterAuthUserMapper,
  configService: ConfigService,
  @Optional() signUpValidator?: CoreBetterAuthSignUpValidatorService,
) {
  super(betterAuthService, userMapper, configService, signUpValidator);
}
```

### Step 4: Configure Email Verification (optional)

If you want to customize email verification behavior:

```typescript
// config.env.ts
betterAuth: {
  emailVerification: {
    locale: 'de',               // German email templates
    expiresIn: 172800,          // 48h instead of default 24h
    template: 'my-verify',     // Custom EJS template name
  },
}
```

Or create custom templates in your project's templates directory:
- `templates/email-verification-de.ejs` (German)
- `templates/email-verification-en.ejs` (English)
- `templates/email-verification.ejs` (Fallback)

Available template variables: `name`, `link`, `expiresIn`, `appName`.

### Step 5: Test Authentication Flows

Test the following flows in your application:
- [ ] Sign-Up with `termsAndPrivacyAccepted: true` succeeds
- [ ] Sign-Up without `termsAndPrivacyAccepted` returns error `LTNS_0021`
- [ ] Email verification link appears in console (local/development mode)
- [ ] `verifiedAt` is set after email verification
- [ ] `termsAndPrivacyAcceptedAt` is stored in user record
- [ ] Email/Password Sign-In works for verified users
- [ ] Existing users (already verified) are not affected

---

## Compatibility Notes

### Existing Users

Existing users in your database are **not affected** by the email verification change. Only new sign-ups trigger verification.

### Projects Without BetterAuth

If your project does not use BetterAuth (`betterAuth` not configured), these changes have **no impact**.

### Frontend Integration

Update your sign-up forms to include the `termsAndPrivacyAccepted` checkbox:

```typescript
// Example: Nuxt/Vue
const signUp = async () => {
  await authClient.signUp.email({
    email: form.email,
    password: form.password,
    name: form.name,
    termsAndPrivacyAccepted: form.termsAccepted, // Add this
  });
};
```

---

## Troubleshooting

### Sign-Up Fails with Error LTNS_0021

**Cause:** `termsAndPrivacyAccepted` is not provided or is `false`.

**Solution:** Ensure your sign-up form sends `termsAndPrivacyAccepted: true`, or disable the check:
```typescript
betterAuth: {
  signUpChecks: false,
}
```

### Sign-In Fails with Error LTNS_0023

**Cause:** Email verification is enabled (default) and the user has not verified their email yet. The server rejects sign-in attempts with `#LTNS_0023: Email verification required`.

**Solution:**
1. Ensure the user clicks the verification link sent via email (check console in local mode)
2. Or disable email verification:
```typescript
betterAuth: {
  emailVerification: false,
}
```

**Note:** This check also applies to 2FA verification (`verifyTotp`) and REST sign-in (`POST /iam/sign-in/email`). Unverified users are blocked from all authentication paths.

### Users Cannot Sign In After Sign-Up

**Cause:** Email verification is enabled, and the user has not verified their email yet.

**Solution:** Check your email configuration (SMTP or Brevo). In local/development mode, the verification URL is printed to the console. Users need to click the verification link before signing in with `S_VERIFIED`-protected endpoints.

### Email Verification Emails Not Sent

**Cause:** No email service configured (no SMTP and no Brevo).

**Solution:** Configure either SMTP or Brevo:
```typescript
// SMTP
email: {
  smtp: { host: 'smtp.example.com', port: 587 },
  defaultSender: { email: 'no-reply@example.com', name: 'My App' },
}

// Or Brevo
brevo: {
  apiKey: process.env.BREVO_API_KEY,
}
betterAuth: {
  emailVerification: {
    brevoTemplateId: 42,
  },
}
```

**Note:** The verification URL is always logged to the console regardless of email configuration.

### 401 Errors After Switching from Cookie to JWT Mode

**Cause:** Stale httpOnly session cookies (`iam.session_token`) from a previous cookie-based configuration persist in the browser. These cookies are sent automatically with requests and can interfere with Bearer token authentication, as the server receives conflicting authentication sources.

**Solution:** httpOnly cookies cannot be cleared via JavaScript. Use server-side sign-out to clear them:
```typescript
// Call sign-out endpoint with credentials to clear httpOnly cookies
await fetch('/api/iam/sign-out', { method: 'POST', credentials: 'include' });

// Then sign in fresh with the new configuration
```

**Prevention:** When switching from `cookies: true` to `cookies: false`, ensure all users are signed out first.

### BetterAuth Authentication Fails with `cookies: false`

**Cause:** When `cookies: false` is set without `betterAuth.jwt: true`, BetterAuth cannot establish sessions. The server will log a startup warning about this misconfiguration.

**Solution:** When using `cookies: false`, always enable the JWT plugin:
```typescript
// config.env.ts
{
  cookies: false,
  betterAuth: {
    jwt: true,  // Required when cookies: false
  },
}
```

**Note:** Even with `jwt: true`, the BetterAuth programmatic API returns session tokens (not JWTs). The JWT plugin enriches only native HTTP handler responses. Session tokens work for authentication via `Authorization: Bearer <session-token>` header.

---

## Bugfixes

### 1. Passkey Login Response Enrichment

Better Auth's `@better-auth/passkey` plugin returns only `{ session }` in the verify-authentication response, despite its OpenAPI spec declaring both `{ session, user }`. This caused the frontend to not receive user data after passkey login, preventing proper auth state setup and dashboard redirect.

**Fix:** The API middleware now enriches passkey verify-authentication responses by fetching user data from the database when the response only contains a session. No action needed from consumers.

### 2. Express Response Import Fix

The API middleware imported `Response` from Express, which shadowed the global Web API `Response` constructor. This caused `new Response(...)` calls to fail silently in certain environments (e.g., Vitest SSR). The Express import is now aliased as `ExpressResponse`.

### 3. JWT Mode Session Fallback

In JWT mode, the session middleware now resolves database sessions for JWT-authenticated users. This ensures that BetterAuth plugin endpoints (2FA, Passkey) can authenticate via session token, even when the client only sends a JWT.

The middleware also supports JWT tokens stored in cookies (`lt-jwt-token`), bridging the gap between cookie-based frontend storage and JWT verification.

### 4. Cookie/JWT Startup Validation

The server now validates authentication configuration at startup and logs warnings for common misconfigurations:
- `cookies: false` without `betterAuth.jwt: true`
- Missing BetterAuth secret

### 5. Cryptographically Secure ID Generation

The `generateId()` method in `CoreBetterAuthUserMapper` now uses `crypto.randomBytes(21)` instead of `Math.random()`. This ensures cryptographically secure random IDs for user mapping and internal token generation.

### 6. Middleware Credential Fallback (401 Fix)

When a request included an **invalid Authorization header** alongside a **valid session cookie**, the middleware would return 401 instead of falling back to the cookie. This affected three areas:

- **`extractSessionToken()` in `core-better-auth-web.helper.ts`**: `isSessionToken()` returned `true` for non-JWT garbage in the Authorization header, preventing cookie fallback. Fix: Added `skipAuthHeader` option, used by Strategy 3 (cookie fallback) when the Auth header was already tried and failed.
- **`CoreBetterAuthModule.configure()`**: After `reset()` in tests, the `betterAuthEnabled` static field was `false` because the `createDeferredModule()` factory did not re-set it. This caused middleware to not be registered. Fix: `configure()` now only checks `this.betterAuthService?.isEnabled()` (injected instance) instead of the static field.
- **`createDeferredModule()` factory**: Now also sets `this.betterAuthEnabled = !!this.authInstance` for consistency with `forRootAsync()`.

No action needed from consumers — this is a purely internal fix.

### 7. RolesGuard REST Controller Fix

`RolesGuard.getRequest()` failed for REST controllers because `GqlExecutionContext.create(context).getContext()` returns a truthy value (the `next` function) for HTTP contexts, but `.req` on it is `undefined`.

**Before:** `ctx.getContext() ? ctx.getContext().req : context.switchToHttp().getRequest()`
**After:** `ctx.getContext()?.req || context.switchToHttp().getRequest()`

No action needed from consumers — REST controllers with `@Roles()` now work correctly.

---

## Security Fix

### `@apollo/server` Updated to 5.4.0

Updated from 5.3.0 to 5.4.0 to fix a **high severity DoS vulnerability**. No API changes — drop-in update.

**Action:** Run `npm install` after updating `@lenne.tech/nest-server`.

---

## Removed Dependencies

### `pm2` and `grunt` Removed

The following devDependencies have been removed from nest-server:

| Package | Version | Reason |
|---------|---------|--------|
| `pm2` | 6.0.14 | No longer used, had low-severity ReDoS vulnerability |
| `grunt` | 1.6.1 | Only used for pm2-based production start |
| `grunt-bg-shell` | 2.3.3 | Grunt plugin |
| `grunt-contrib-clean` | 2.0.1 | Grunt plugin |
| `grunt-contrib-watch` | 1.1.0 | Grunt plugin |
| `grunt-sync` | 0.8.2 | Grunt plugin |

The `start:prod` script now uses `NODE_ENV=production node dist/main.js` instead of Grunt.

**Impact on consuming projects:** None — these were devDependencies of nest-server and not exposed to consumers. If your project uses pm2 independently, it is not affected.

---

## New Exports

The following are newly exported from `@lenne.tech/nest-server`:

```typescript
// Email Verification Service
export { CoreBetterAuthEmailVerificationService } from './core/modules/better-auth/core-better-auth-email-verification.service';

// Sign-Up Validator Service
export { CoreBetterAuthSignUpValidatorService } from './core/modules/better-auth/core-better-auth-signup-validator.service';

// Configuration Interfaces
export { IBetterAuthEmailVerificationConfig, IBetterAuthSignUpChecksConfig } from './core/common/interfaces/server-options.interface';
```

---

## New Test Files

| Test File | Coverage |
|-----------|----------|
| `tests/stories/better-auth-email-verification.story.test.ts` | Email verification flow, LTNS_0023 enforcement, sign-up with verification |
| `tests/stories/better-auth-jwt-middleware.story.test.ts` | JWT-to-session resolution, Authorization header priority over cookies, cookie-less mode session handling |
| `tests/stories/better-auth-security.e2e-spec.ts` | Cookie/JWT startup validation, security configuration checks |
| `tests/stories/middleware-credential-fallback.e2e-spec.ts` | Middleware credential fallback: all 3 strategies (Auth header, lt-jwt-token cookie, session cookie), invalid header + valid cookie, parallel auth mode |

## TestHelper Documentation

Full reference for REST, GraphQL, and cookie-based testing:
- **Documentation:** [src/test/README.md](../src/test/README.md)

---

## Module Documentation

### Better-Auth Module

- **README:** [src/core/modules/better-auth/README.md](../src/core/modules/better-auth/README.md)
- **Integration Checklist:** [src/core/modules/better-auth/INTEGRATION-CHECKLIST.md](../src/core/modules/better-auth/INTEGRATION-CHECKLIST.md)
- **Customization Guide:** [src/core/modules/better-auth/CUSTOMIZATION.md](../src/core/modules/better-auth/CUSTOMIZATION.md)
- **Reference Implementation:** `src/server/modules/iam/`
- **Key Files:**
  - `core-better-auth-email-verification.service.ts` - Email verification logic and template resolution
  - `core-better-auth-signup-validator.service.ts` - Sign-up validation with configurable required fields

### Module Registration Patterns

When customizing BetterAuth, use the appropriate registration pattern:

| Pattern | Use When | Configuration |
|---------|----------|---------------|
| **Zero-Config** | No customization | `CoreModule.forRoot(envConfig)` |
| **Config-based** | Custom Controller/Resolver | `betterAuth: { controller, resolver }` |
| **Separate Module** | Full control, additional providers | `betterAuth: { autoRegister: false }` |

**See [CUSTOMIZATION.md](../src/core/modules/better-auth/CUSTOMIZATION.md) for detailed guidance on:**
- Customizing Controller, Resolver, and Services
- Email template customization (EJS and Brevo)
- Avoiding the "forRoot() called twice" warning

---

## References

- [Better-Auth Module](../src/core/modules/better-auth/) - Core module implementation
- [nest-server-starter](https://github.com/lenneTech/nest-server-starter) - Reference implementation
- [Better-Auth Documentation](https://www.better-auth.com/) - Official Better-Auth docs
- [Configurable Features Pattern](../.claude/rules/configurable-features.md) - Configuration patterns used
