# Migration Guide: 11.17.x → 11.18.0

## Overview

| Category | Details |
|----------|---------|
| **Breaking Changes** | None (NestJS patch update `11.1.13` → `11.1.16` requires matching versions) |
| **New Features** | Safety Net Architecture: automatic security without CrudService.process() |
| **Dependency Updates** | NestJS 11.1.16, apollo-server-core removed (−151 packages), 21 packages updated |
| **Migration Effort** | ~5 minutes (update NestJS packages + verify) |

---

## Quick Migration

All Safety Net features are **enabled by default** and fully backward compatible. No code changes required.

However, 11.18.0 updates NestJS core packages to `11.1.16`. Projects **must** update their NestJS packages to match, otherwise pnpm will install duplicate copies with incompatible TypeScript types (e.g. `Types have separate declarations of a private property 'logger'`).

```bash
# 1. Update nest-server
pnpm add @lenne.tech/nest-server@11.18.0

# 2. Update NestJS packages to match (required!)
pnpm add @nestjs/common@11.1.16 @nestjs/core@11.1.16 @nestjs/platform-express@11.1.16
pnpm add -D @nestjs/testing@11.1.16

# 3. Verify build
pnpm run build

# 4. Run tests
pnpm test
```

> **Why?** `@lenne.tech/nest-server` declares NestJS packages in `dependencies` with exact versions. If your project uses a different patch version, pnpm creates duplicate installations which TypeScript treats as incompatible types.

---

## What's New in 11.18.0

### Safety Net Architecture

Previously, security guarantees (password hashing, role protection, secret removal, field filtering) only worked when using `CrudService.process()`. Developers bypassing `process()` with direct Mongoose calls (`this.mainDbModel.findOne()`, `.aggregate()`, etc.) could accidentally expose sensitive data or store plaintext passwords.

**11.18.0 introduces automatic security at the framework level**, ensuring protection works regardless of how data is accessed or written.

### 1. Mongoose Password Hashing Plugin

Automatically hashes passwords on all write operations (`save`, `findOneAndUpdate`, `updateOne`, `updateMany`).

- Detects already-hashed values (BCrypt pattern) to prevent double-hashing
- Supports SHA256 pre-hashing (matching the existing `prepareInput` behavior)
- Configurable skip patterns for sentinel values (e.g. `!LOCKED:REQUIRES_PASSWORD_RESET`)

```typescript
// config.env.ts - enabled by default, examples for customization:

// Default: enabled
security: {
  mongoosePasswordPlugin: true,
}

// With skip patterns for sentinel values
security: {
  mongoosePasswordPlugin: {
    skipPatterns: ['^!LOCKED:'],
  },
}

// Disable entirely
security: {
  mongoosePasswordPlugin: false,
}
```

### 2. Mongoose Role Guard Plugin

Prevents unauthorized users from escalating roles via direct database operations. Only users with ADMIN role (or configured allowed roles) can assign roles to other users.

- Uses `RequestContext` (AsyncLocalStorage) to access the current user
- System operations without request context (e.g. migrations, seeds) are allowed through
- No-user requests (e.g. `signUp`) are allowed through (`currentUser` is `undefined`)
- Configurable for custom role hierarchies
- Programmatic bypass via `RequestContext.runWithBypassRoleGuard()` for authorized service code

```typescript
// config.env.ts - enabled by default, examples for customization:

// Default: only ADMIN can assign roles
security: {
  mongooseRoleGuardPlugin: true,
}

// Allow additional roles to assign roles
security: {
  mongooseRoleGuardPlugin: {
    allowedRoles: ['ORGA', 'COMPANY_ADMIN'],
  },
}

// Programmatic bypass for authorized service operations (e.g. HR_MANAGER creating users with roles):
import { RequestContext } from '@lenne.tech/nest-server';

await RequestContext.runWithBypassRoleGuard(async () => {
  await this.mainDbModel.findByIdAndUpdate(userId, { roles: ['EMPLOYEE'] });
});

// Also works with CrudService.process() via force: true (automatically bypasses the plugin)
```

### 3. Mongoose Audit Fields Plugin

Automatically sets `createdBy` and `updatedBy` fields on save/update operations using the current user from `RequestContext`.

```typescript
// config.env.ts - enabled by default

// Disable if not needed
security: {
  mongooseAuditFieldsPlugin: false,
}
```

### 4. ResponseModelInterceptor

Auto-converts plain objects and Mongoose documents into model instances with `securityCheck()` and correct constructor metadata. This ensures `@Restricted` field filtering and `securityCheck()` work even when `CrudService.process()` is bypassed.

**Model class resolution order:**
1. Explicit `@ResponseModel(ModelClass)` decorator (for REST)
2. GraphQL `TypeMetadataStorage` lookup (automatic for `@Query`/`@Mutation` return types)
3. Swagger `@ApiOkResponse` / `@ApiCreatedResponse` type (automatic for REST)
4. No conversion (existing interceptors work as before)

Results are cached per route handler for zero-cost subsequent lookups.

```typescript
// config.env.ts - enabled by default

// Enable with debug warnings (logs when auto-conversion happens)
security: {
  responseModelInterceptor: { debug: true },
}

// Disable entirely
security: {
  responseModelInterceptor: false,
}
```

**For REST controllers**, you can use the `@ResponseModel()` decorator to explicitly specify the model class:

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

@ResponseModel(User)
@Get(':id')
async getUser(@Param('id') id: string): Promise<User> {
  // Even if this returns a plain object, the interceptor will convert it
  return this.mainDbModel.findById(id).exec();
}
```

### 5. TranslateResponseInterceptor

Automatically applies `_translations` to response objects based on the `Accept-Language` header. Ensures translations work even when `CrudService.process()` is bypassed.

- Includes early bailout: skips recursive traversal when no `_translations` are present
- Zero overhead for non-translated responses

```typescript
// config.env.ts - enabled by default

// Disable entirely
security: {
  translateResponseInterceptor: false,
}
```

### 6. Fallback Secret Removal

`CheckSecurityInterceptor` now includes a configurable fallback that removes known secret fields from all responses, regardless of whether the response is a model instance.

```typescript
// config.env.ts - enabled by default

// Default secret fields
security: {
  secretFields: ['password', 'verificationToken', 'passwordResetToken', 'refreshTokens', 'tempTokens'],
}

// Custom secret fields
security: {
  secretFields: ['password', 'apiKey', 'internalToken'],
}
```

### 7. RequestContext Service

New `RequestContext` service using Node.js `AsyncLocalStorage` provides the current user and request language in any context — including Mongoose hooks — without dependency injection.

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

// In any service, helper, or Mongoose hook:
const currentUser = RequestContext.getCurrentUser();
const language = RequestContext.getLanguage();
```

### 8. ModelRegistry Service

Central registry mapping Mongoose model names to CoreModel classes. Auto-populated when `ModuleService` is constructed. Used internally by `ResponseModelInterceptor`.

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

// Lookup a model class by Mongoose model name
const UserClass = ModelRegistry.getModelClass('User');
```

### 9. ModuleService Helper Methods

Two new helper methods for services that use direct Mongoose queries:

```typescript
// processResult(): Simplified post-processing (population + prepareOutput)
const doc = await this.mainDbModel.findById(id).exec();
return this.processResult(doc, serviceOptions);

// applyTranslationInput(): Translation-aware input processing
const oldDoc = await this.mainDbModel.findById(id).lean();
input = this.applyTranslationInput(input, oldDoc, serviceOptions.language);
```

---

## Configuration Reference

All new features follow the "enabled by default, opt-out via `false`" pattern:

| Config Path | Type | Default | Description |
|-------------|------|---------|-------------|
| `security.mongoosePasswordPlugin` | `boolean \| { skipPatterns }` | `true` | Auto password hashing |
| `security.mongooseRoleGuardPlugin` | `boolean \| { allowedRoles }` | `true` | Role escalation prevention |
| `security.mongooseAuditFieldsPlugin` | `boolean` | `true` | Auto createdBy/updatedBy |
| `security.responseModelInterceptor` | `boolean \| { debug }` | `true` | Plain→Model auto-conversion |
| `security.translateResponseInterceptor` | `boolean` | `true` | Auto translation application |
| `security.secretFields` | `string[]` | `['password', ...]` | Global secret field removal |
| `security.checkSecurityInterceptor.removeSecretFields` | `boolean` | `true` | Fallback secret removal |
| `security.checkSecurityInterceptor.secretFields` | `string[]` | `['password', ...]` | Interceptor-specific secret fields |

---

## Compatibility Notes

### Existing CrudService Usage

**No changes required.** All existing code using `CrudService.process()` continues to work exactly as before. The Safety Net is purely additive — it catches cases that previously had no protection.

### Double-Hashing Prevention

The password plugin detects already-hashed values using BCrypt pattern matching (`/^\$2[aby]\$\d+\$/`). If `CrudService.process()` already hashes a password via `prepareInput()`, the plugin will skip it. No double-hashing occurs.

### Double-Mapping Prevention

The `ResponseModelInterceptor` checks `instanceof` and the `_objectAlreadyCheckedForRestrictions` flag. If `CrudService.process()` already mapped and checked an object, the interceptor skips it. No duplicate processing.

### Direct Mongoose Usage (Advanced)

Projects that intentionally use direct Mongoose calls now get automatic security for free. For best results:

- **Output:** Return plain documents — the `ResponseModelInterceptor` handles conversion
- **Input:** Passwords are hashed automatically, roles are protected automatically
- **Audit fields:** `createdBy`/`updatedBy` are set automatically when `RequestContext` is available
- **Translations:** Use `this.applyTranslationInput()` before writes, responses are translated automatically

### Performance

K6 benchmarks (10 VUs, 30s) show negligible overhead:

| Operation | Overhead vs Baseline |
|-----------|---------------------|
| signUp (bcrypt) | < 1% |
| signIn (bcrypt) | < 1% |
| getUser | ~2% (< 0.1ms absolute) |
| updateUser | ~7% (< 0.2ms absolute) |
| REST getUser | ~14% (~5ms absolute) |
| **Total throughput** | **-1.6%** |

---

## Troubleshooting

### Sentinel password values are being hashed

If your application uses sentinel values for password locking (e.g. `!LOCKED:REQUIRES_PASSWORD_RESET`), configure skip patterns:

```typescript
security: {
  mongoosePasswordPlugin: {
    skipPatterns: ['^!LOCKED:'],
  },
}
```

### Roles are empty after creating users in a service

The Role Guard Plugin blocks role assignments from non-admin users. This affects services where a logged-in non-admin user creates other users with roles (e.g. HR systems, team management).

**Option A: Programmatic bypass** (for specific service operations):
```typescript
import { RequestContext } from '@lenne.tech/nest-server';

// Wrap the operation — current user context is preserved
await RequestContext.runWithBypassRoleGuard(async () => {
  await this.mainDbModel.create({ email, roles: ['EMPLOYEE'] });
});
```

**Option B: CrudService force mode** (bypasses role guard automatically):
```typescript
return this.process(
  async () => this.mainDbModel.findByIdAndUpdate(id, { roles }),
  { serviceOptions, force: true },
);
```

**Option C: Config-based** (permanently allow specific roles to assign roles):
```typescript
security: {
  mongooseRoleGuardPlugin: {
    allowedRoles: ['ORGA', 'HR_MANAGER'],
  },
}
```

> **Note:** `signUp` without a logged-in user is NOT affected — when no `currentUser` exists, the plugin allows role changes.

### ResponseModelInterceptor not converting REST responses

For REST controllers, the interceptor resolves model types via Swagger metadata or the `@ResponseModel()` decorator. If neither is present, no auto-conversion occurs. Add the decorator explicitly:

```typescript
@ResponseModel(User)
@Get(':id')
async getUser(@Param('id') id: string): Promise<User> { ... }
```

### Debug: Which responses are being auto-converted?

Enable debug mode to see warnings when the interceptor converts plain objects:

```typescript
security: {
  responseModelInterceptor: { debug: true },
}
```

---

## New Exports

The following new exports are available from `@lenne.tech/nest-server`:

```typescript
// Decorators
export { ResponseModel } from '@lenne.tech/nest-server';

// Interceptors
export { ResponseModelInterceptor } from '@lenne.tech/nest-server';
export { TranslateResponseInterceptor } from '@lenne.tech/nest-server';

// Plugins
export { mongoosePasswordPlugin } from '@lenne.tech/nest-server';
export { mongooseRoleGuardPlugin } from '@lenne.tech/nest-server';
export { mongooseAuditFieldsPlugin } from '@lenne.tech/nest-server';

// Services
export { ModelRegistry } from '@lenne.tech/nest-server';
export { RequestContext } from '@lenne.tech/nest-server';

// Middleware
export { RequestContextMiddleware } from '@lenne.tech/nest-server';

// Helpers
export { resolveResponseModelClass } from '@lenne.tech/nest-server';
```

---

## References

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