# Migration Guide: 11.21.2 → 11.21.3

## Overview

| Category | Details |
|----------|---------|
| **Breaking Changes** | None |
| **New Features** | System role early-returns in CoreTenantGuard, S_NO_ONE defense-in-depth, account collection indices, TestHelper download cookie auth |
| **Bugfixes** | CoreTenantGuard: class-level roles no longer shadow method-level system roles; admin bypass log level changed to `debug` |
| **Migration Effort** | < 5 minutes |

---

## Quick Migration

```bash
# Update package
pnpm add @lenne.tech/nest-server@11.21.3

# Verify build
pnpm run build

# Run tests
pnpm test
```

No code changes required. The bugfix and new features are fully backward compatible.

---

## What's New in 11.21.3

### 1. CoreTenantGuard: System Role Early-Returns (Bugfix)

**Affects:** Projects with `multiTenancy` enabled that use class-level `@Roles()` combined with method-level system roles.

**Problem:** When a controller class had `@Roles(ADMIN)` and a method had `@Roles(S_USER)`, the guard merged both into `[S_USER, ADMIN]`, then filtered out system roles — leaving `checkableRoles = [ADMIN]`. This blocked non-admin users even though the method explicitly allowed any authenticated user.

**Fix:** `CoreTenantGuard` now checks system roles (S_EVERYONE → S_USER → S_VERIFIED) **before** filtering to real roles. If a system role grants access, the guard returns immediately without requiring real role checks.

```typescript
// This pattern now works correctly:
@Roles(RoleEnum.ADMIN)    // Class: admin can always access
@Controller('users')
class UserController {

  @Get('profile')
  @Roles(RoleEnum.S_USER)  // Method: any authenticated user can access
  getProfile() { ... }     // Previously: blocked non-admin users ❌
                            // Now: any authenticated user passes ✅
}
```

**Role resolution semantics:**

| Role Type | Resolution | Example |
|-----------|-----------|---------|
| System roles (S_EVERYONE, S_USER, S_VERIFIED) | Method takes precedence | Class `S_EVERYONE` + Method `S_USER` → `S_USER` applies |
| Real roles (ADMIN, hierarchy, custom) | OR/merged (class extends method) | Class `ADMIN` + Method `'editor'` → ADMIN or editor |

**Tenant header behavior with system roles:**

| System Role | No Header | Header Present |
|-------------|-----------|----------------|
| S_EVERYONE | Pass (public) | Pass + optional tenant context enrichment |
| S_USER | Pass if authenticated | Pass if member (403 if not a member); admin bypass applies |
| S_VERIFIED | Pass if verified | Pass if verified member (403 if not a member); admin bypass applies |

Admin users with `adminBypass: true` (default) always pass tenant membership checks, even when a system role early-return handles the request.

### 2. Admin Bypass Log Level Changed to `debug`

Admin bypass log messages (`Admin bypass: user X accessing tenant Y`) have been downgraded from `log` to `debug` level. This reduces log noise in production. To see admin bypass activity, enable debug-level logging for the `CoreTenantGuard` context.

### 3. S_NO_ONE Defense-in-Depth

`CoreTenantGuard` now explicitly checks for `S_NO_ONE` and throws `ForbiddenException` — previously it relied solely on `RolesGuard`/`BetterAuthRolesGuard` catching `S_NO_ONE` upstream. This adds defense-in-depth for custom guard chain configurations.

### 4. TestHelper: Cookie Auth for Download Methods

**Affects:** Projects using `TestHelper.download()` or `TestHelper.downloadBuffer()` in tests.

The download methods now support cookie-based authentication via a new `TestDownloadOptions` interface:

```typescript
// String form (backward compatible)
await testHelper.download('/files/id/abc', jwtToken);

// Options object with JWT token
await testHelper.download('/files/id/abc', { token: jwtToken });

// Options object with cookie-based session auth
await testHelper.download('/files/id/abc', { cookies: sessionToken });

// Both simultaneously (like rest())
await testHelper.download('/files/id/abc', {
  cookies: sessionToken,
  token: jwtToken,
});
```

The same options apply to `downloadBuffer()`.

### 5. Performance: Account Collection Indices

New MongoDB indices are automatically created on application startup (via `onModuleInit`) for the `account` collection:

- `{ userId: 1 }` — accelerates `$lookup` joins in `getMigrationStatus()` and session lookups
- `{ providerId: 1, userId: 1 }` — accelerates credential account filtering

**No manual action required** — indices are created idempotently (no-op if they already exist). The indices significantly improve `betterAuthMigrationStatus` query performance (up to 97% faster in test environments).

### 6. Documentation: System Role OR Semantics

`docs/REQUEST-LIFECYCLE.md` has been updated with:
- Clarified OR semantics for system roles in `CoreTenantGuard`
- Corrected `S_VERIFIED` check logic (now includes `verifiedAt`)
- Added notes that `S_CREATOR` and `S_SELF` are object-level checks (handled by interceptors, not guards)

---

## Compatibility Notes

### Existing Patterns — No Changes Needed

| Pattern | Status |
|---------|--------|
| Class `@Roles(ADMIN)` + Method `@Roles(S_EVERYONE)` | ✅ Works as before |
| Class `@Roles(ADMIN)` + Method `@Roles(S_USER)` | ✅ Works as before (and now also correct in CoreTenantGuard) |
| Class `@Roles(ADMIN)` + Method `@Roles(ADMIN)` | ✅ Works as before |
| Class `@Roles(ADMIN)` + no method `@Roles()` | ✅ Class fallback applies |
| `download(url, token)` string form | ✅ Backward compatible |
| `@SkipTenantCheck()` decorator | ✅ Works as before |
| BetterAuth auto-skip (`skipTenantCheck`) | ✅ Works as before |

### Edge Case: Class S_EVERYONE + Method S_USER

Previously, class `@Roles(S_EVERYONE)` combined with method `@Roles(S_USER)` resulted in public access (S_EVERYONE overrode S_USER). Now, the method-level `S_USER` takes precedence — the endpoint requires authentication.

This edge case is unlikely in practice (class-level S_EVERYONE with method-level S_USER contradicts intent), but if you intentionally relied on this behavior, add `S_EVERYONE` to the method:

```typescript
// Before: S_EVERYONE on class made this endpoint public
@Roles(RoleEnum.S_USER)
getProfile() { ... }

// After: If you want it public, be explicit on the method
@Roles(RoleEnum.S_EVERYONE)
getProfile() { ... }
```

---

## Module Documentation

### Tenant Module

- **README:** [src/core/modules/tenant/README.md](../src/core/modules/tenant/README.md) — New "System Roles as OR Alternatives" section, updated Tenant Context Rule and @SkipTenantCheck documentation
- **Integration Checklist:** [src/core/modules/tenant/INTEGRATION-CHECKLIST.md](../src/core/modules/tenant/INTEGRATION-CHECKLIST.md)
- **Key Files:**
  - `core-tenant.guard.ts` — System role early-returns, hybrid role resolution, S_NO_ONE defense-in-depth, admin bypass audit log (debug level)
  - `core-tenant.helpers.ts` — Updated JSDoc for `mergeRolesMetadata`, `isSystemRole`

### TestHelper

- **README:** [src/test/README.md](../src/test/README.md) — New download/downloadBuffer section with cookie auth examples
- **Key Files:**
  - `test.helper.ts` — `TestDownloadOptions` interface, cookie auth for downloads

---

## References

- [Request Lifecycle Documentation](../docs/REQUEST-LIFECYCLE.md) — Updated system role semantics
- [nest-server-starter](https://github.com/lenneTech/nest-server-starter) (reference implementation)
