@lenne.tech/nest-server 11.21.1 → 11.21.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/common/interfaces/server-options.interface.d.ts +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +2 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +45 -8
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -8
- package/src/core/common/interfaces/server-options.interface.ts +26 -0
- package/src/core/modules/better-auth/README.md +20 -1
- package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +20 -1
- package/src/core/modules/tenant/README.md +33 -0
- package/src/core/modules/tenant/core-tenant.guard.ts +85 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.21.
|
|
3
|
+
"version": "11.21.2",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"class-validator": "0.15.1",
|
|
98
98
|
"compression": "1.8.1",
|
|
99
99
|
"cookie-parser": "1.4.7",
|
|
100
|
-
"dotenv": "17.
|
|
100
|
+
"dotenv": "17.4.0",
|
|
101
101
|
"ejs": "5.0.1",
|
|
102
102
|
"express": "5.2.1",
|
|
103
103
|
"graphql": "16.13.2",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"graphql-upload": "15.0.2",
|
|
107
107
|
"js-sha256": "0.11.1",
|
|
108
108
|
"json-to-graphql-query": "2.3.0",
|
|
109
|
-
"lodash": "4.
|
|
109
|
+
"lodash": "4.18.1",
|
|
110
110
|
"mongodb": "7.1.1",
|
|
111
111
|
"mongoose": "9.3.3",
|
|
112
112
|
"multer": "2.1.1",
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"@nestjs/cli": "11.0.17",
|
|
126
126
|
"@nestjs/schematics": "11.0.10",
|
|
127
127
|
"@nestjs/testing": "11.1.17",
|
|
128
|
-
"@swc/cli": "0.8.
|
|
128
|
+
"@swc/cli": "0.8.1",
|
|
129
129
|
"@swc/core": "1.15.21",
|
|
130
130
|
"@types/compression": "1.8.1",
|
|
131
131
|
"@types/cookie-parser": "1.4.10",
|
|
@@ -182,10 +182,7 @@
|
|
|
182
182
|
"rollup@>=4.0.0 <4.60.1": "4.60.1",
|
|
183
183
|
"ajv@<6.14.0": "6.14.0",
|
|
184
184
|
"ajv@>=7.0.0-alpha.0 <8.18.0": "8.18.0",
|
|
185
|
-
"file-type@>=13.0.0 <21.3.2": "21.3.2",
|
|
186
185
|
"undici@>=7.0.0 <7.24.0": "7.24.3",
|
|
187
|
-
"yauzl@<3.2.1": "3.2.1",
|
|
188
|
-
"flatted@<=3.4.1": "3.4.2",
|
|
189
186
|
"srvx@<0.11.13": "0.11.13",
|
|
190
187
|
"handlebars@>=4.0.0 <4.7.9": "4.7.9",
|
|
191
188
|
"brace-expansion@<1.1.13": "1.1.13",
|
|
@@ -193,7 +190,8 @@
|
|
|
193
190
|
"picomatch@<2.3.2": "2.3.2",
|
|
194
191
|
"picomatch@>=4.0.0 <4.0.4": "4.0.4",
|
|
195
192
|
"path-to-regexp@>=8.0.0 <8.4.0": "8.4.1",
|
|
196
|
-
"kysely@>=0.26.0 <0.28.15": "0.28.15"
|
|
193
|
+
"kysely@>=0.26.0 <0.28.15": "0.28.15",
|
|
194
|
+
"lodash@>=4.0.0 <4.18.0": "4.18.1"
|
|
197
195
|
},
|
|
198
196
|
"onlyBuiltDependencies": [
|
|
199
197
|
"bcrypt",
|
|
@@ -2401,6 +2401,32 @@ interface IBetterAuthBase {
|
|
|
2401
2401
|
*/
|
|
2402
2402
|
secret?: string;
|
|
2403
2403
|
|
|
2404
|
+
/**
|
|
2405
|
+
* Skip tenant validation on IAM endpoints.
|
|
2406
|
+
*
|
|
2407
|
+
* When true (default), IAM endpoints (sign-up, sign-in, sign-out, session, etc.)
|
|
2408
|
+
* skip the CoreTenantGuard tenant membership check. This is the correct default
|
|
2409
|
+
* because authentication typically happens BEFORE tenant context is established.
|
|
2410
|
+
*
|
|
2411
|
+
* Set to false for scenarios where tenant context is known at login time:
|
|
2412
|
+
* - Subdomain-based tenancy (tenant-a.crm.example.com)
|
|
2413
|
+
* - Invite links with embedded tenant (crm.example.com/invite/abc123)
|
|
2414
|
+
* - Tenant-specific login pages (crm.example.com/login?org=tenant-a)
|
|
2415
|
+
* - Tenant-specific auth policies (e.g., one tenant requires SSO)
|
|
2416
|
+
*
|
|
2417
|
+
* @default true
|
|
2418
|
+
*
|
|
2419
|
+
* @example
|
|
2420
|
+
* ```typescript
|
|
2421
|
+
* // Default: IAM endpoints skip tenant validation (correct for most cases)
|
|
2422
|
+
* betterAuth: { skipTenantCheck: true }
|
|
2423
|
+
*
|
|
2424
|
+
* // Tenant-aware authentication (subdomain-based tenancy, invite links, etc.)
|
|
2425
|
+
* betterAuth: { skipTenantCheck: false }
|
|
2426
|
+
* ```
|
|
2427
|
+
*/
|
|
2428
|
+
skipTenantCheck?: boolean;
|
|
2429
|
+
|
|
2404
2430
|
/**
|
|
2405
2431
|
* Sign-up checks configuration.
|
|
2406
2432
|
*
|
|
@@ -2137,4 +2137,23 @@ For frontend integration with Better-Auth, see the **[Integration Checklist](./I
|
|
|
2137
2137
|
1. **Password Hashing**: Always hash passwords with SHA256 client-side before sending
|
|
2138
2138
|
2. **2FA Redirect**: Check for `twoFactorRedirect: true` in sign-in response
|
|
2139
2139
|
3. **Passkey Session**: Passkey auth returns session without user - call `validateSession()` to fetch user data
|
|
2140
|
-
4. **Credentials**: Use `credentials: 'include'` for cross-origin cookie handling
|
|
2140
|
+
4. **Credentials**: Use `credentials: 'include'` for cross-origin cookie handling
|
|
2141
|
+
|
|
2142
|
+
---
|
|
2143
|
+
|
|
2144
|
+
## Multi-Tenancy Integration
|
|
2145
|
+
|
|
2146
|
+
When both BetterAuth and Multi-Tenancy (`multiTenancy: {}`) are active, IAM endpoints
|
|
2147
|
+
automatically skip tenant validation by default (`skipTenantCheck: true`). This is the
|
|
2148
|
+
correct default — authentication happens before tenant context is established.
|
|
2149
|
+
|
|
2150
|
+
For tenant-aware authentication scenarios (subdomain-based tenancy, invite links, etc.),
|
|
2151
|
+
set `skipTenantCheck: false`:
|
|
2152
|
+
|
|
2153
|
+
```typescript
|
|
2154
|
+
betterAuth: {
|
|
2155
|
+
skipTenantCheck: false, // IAM endpoints require valid X-Tenant-Id header
|
|
2156
|
+
}
|
|
2157
|
+
```
|
|
2158
|
+
|
|
2159
|
+
See the [CoreTenantModule README](../tenant/README.md#betterauth-iam-integration) for details.
|
|
@@ -133,6 +133,23 @@ import { SkipTenantCheck, Roles, RoleEnum } from '@lenne.tech/nest-server';
|
|
|
133
133
|
async listMyTenants() { ... }
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
### 9. BetterAuth (IAM) Coexistence
|
|
137
|
+
|
|
138
|
+
When both `multiTenancy` and `betterAuth` are active, IAM endpoints (sign-in, sign-up, session, etc.)
|
|
139
|
+
automatically skip tenant validation when no `X-Tenant-Id` header is sent (`betterAuth.skipTenantCheck: true`, default).
|
|
140
|
+
If a header IS present, normal membership validation runs.
|
|
141
|
+
|
|
142
|
+
For tenant-aware authentication (subdomain-based, invite links, SSO per tenant), opt out:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// config.env.ts
|
|
146
|
+
betterAuth: {
|
|
147
|
+
skipTenantCheck: false, // IAM endpoints require valid X-Tenant-Id header
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
See [Tenant README — BetterAuth Integration](./README.md#betterauth-iam-integration) for details.
|
|
152
|
+
|
|
136
153
|
## Verification Checklist
|
|
137
154
|
|
|
138
155
|
- [ ] `pnpm run build` succeeds
|
|
@@ -143,6 +160,7 @@ async listMyTenants() { ... }
|
|
|
143
160
|
- [ ] Non-member gets 403 "Not a member of this tenant"
|
|
144
161
|
- [ ] Unauthenticated + header → 403 "Authentication required for tenant access"
|
|
145
162
|
- [ ] Public endpoint accessing tenantId-schema without context throws 403 (Safety Net)
|
|
163
|
+
- [ ] IAM endpoints work without `X-Tenant-Id` header when `skipTenantCheck: true` (default)
|
|
146
164
|
|
|
147
165
|
## Security
|
|
148
166
|
|
|
@@ -162,4 +180,5 @@ async listMyTenants() { ... }
|
|
|
162
180
|
| Querying membership without bypass | Empty results due to tenant filter | Use `RequestContext.runWithBypassTenantGuard()` |
|
|
163
181
|
| Public endpoint accessing tenantId-schema | 403 Safety Net exception | Use `@SkipTenantCheck()` + `RequestContext.runWithBypassTenantGuard()` |
|
|
164
182
|
| Passing user-supplied tenantId to create() | Cross-tenant write possible | Let plugin auto-set tenantId from context |
|
|
165
|
-
| Custom hierarchy doesn't match config | Roles fail unexpectedly | Ensure `createHierarchyRoles()` input matches `multiTenancy.roleHierarchy` |
|
|
183
|
+
| Custom hierarchy doesn't match config | Roles fail unexpectedly | Ensure `createHierarchyRoles()` input matches `multiTenancy.roleHierarchy` |
|
|
184
|
+
| `@SkipTenantCheck()` on BetterAuth handler | Redundant since v11.21.2 | Remove — auto-skip handles this via `betterAuth.skipTenantCheck` (default) |
|
|
@@ -207,6 +207,7 @@ multiTenancy: {
|
|
|
207
207
|
```
|
|
208
208
|
|
|
209
209
|
**Cache behavior:**
|
|
210
|
+
|
|
210
211
|
- **Positive-only:** Only active memberships are cached. Non-member lookups always hit the DB (security-first).
|
|
211
212
|
- **Auto-invalidation:** `CoreTenantService.addMember/removeMember/updateMemberRole` automatically invalidate the cache.
|
|
212
213
|
- **Config-change detection:** Cache is flushed when `multiTenancy` config changes (e.g., `roleHierarchy` update).
|
|
@@ -261,8 +262,40 @@ export class TenantMemberController {
|
|
|
261
262
|
}
|
|
262
263
|
```
|
|
263
264
|
|
|
265
|
+
### BetterAuth (IAM) Integration
|
|
266
|
+
|
|
267
|
+
When both Multi-Tenancy and BetterAuth are active, IAM endpoints (sign-up, sign-in, sign-out,
|
|
268
|
+
session, etc.) automatically skip tenant validation by default. This is because authentication
|
|
269
|
+
typically happens before tenant context is established.
|
|
270
|
+
|
|
271
|
+
**Behavior with `skipTenantCheck: true` (default):**
|
|
272
|
+
|
|
273
|
+
- No `X-Tenant-Id` header → skip tenant validation, proceed without tenant context
|
|
274
|
+
- `X-Tenant-Id` header IS provided → validate normally (membership check, tenant context set)
|
|
275
|
+
|
|
276
|
+
The tenant is optional but respected when present. This allows tenant-aware auth flows
|
|
277
|
+
(subdomain-based, invite links, etc.) to coexist with the default cross-tenant behavior.
|
|
278
|
+
|
|
279
|
+
**Configuration:**
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// config.env.ts — Default: skip tenant validation on IAM endpoints (most projects)
|
|
283
|
+
betterAuth: {
|
|
284
|
+
skipTenantCheck: true, // (default) No X-Tenant-Id header → skip; header present → validate
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// config.env.ts — Tenant-aware auth (subdomain-based, invite links, SSO per tenant)
|
|
288
|
+
betterAuth: {
|
|
289
|
+
skipTenantCheck: false, // IAM endpoints ALWAYS require valid X-Tenant-Id header
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
When `skipTenantCheck: false`, IAM endpoints will require a valid `X-Tenant-Id` header
|
|
294
|
+
and the user must be a member of that tenant for protected endpoints.
|
|
295
|
+
|
|
264
296
|
## Related
|
|
265
297
|
|
|
266
298
|
- [Integration Checklist](./INTEGRATION-CHECKLIST.md)
|
|
267
299
|
- [Configurable Features](../../../.claude/rules/configurable-features.md)
|
|
300
|
+
|
|
268
301
|
- [Request Lifecycle](../../../docs/REQUEST-LIFECYCLE.md)
|
|
@@ -52,11 +52,18 @@ interface CachedTenantIds {
|
|
|
52
52
|
* - Tenant context (header present): checks against membership.role only (user.roles ignored)
|
|
53
53
|
* - No tenant context: checks against user.roles
|
|
54
54
|
*
|
|
55
|
+
* BetterAuth (IAM) auto-skip behavior (skipTenantCheck config, default: true):
|
|
56
|
+
* - No X-Tenant-Id header: skip tenant validation entirely (auth before tenant is the expected case)
|
|
57
|
+
* - X-Tenant-Id header IS present: fall through to normal validation (membership check + context)
|
|
58
|
+
* This allows tenant-aware auth flows (subdomain-based, invite links, etc.) to coexist with
|
|
59
|
+
* the default cross-tenant behavior. The tenant is optional but respected when provided.
|
|
60
|
+
*
|
|
55
61
|
* Flow:
|
|
56
62
|
* 1. Config check: multiTenancy enabled?
|
|
57
63
|
* 2. Parse header (X-Tenant-Id, max 128 chars, trimmed)
|
|
58
64
|
* 3. @SkipTenantCheck → role check against user.roles, no tenant context
|
|
59
65
|
* 4. Read @Roles() metadata, filter out system roles
|
|
66
|
+
* 5. BetterAuth auto-skip (betterAuth.skipTenantCheck config + no header) → skip, no tenant context
|
|
60
67
|
*
|
|
61
68
|
* HEADER PRESENT:
|
|
62
69
|
* - System ADMIN (adminBypass: true) → set req.tenantId + isAdminBypass
|
|
@@ -190,18 +197,35 @@ export class CoreTenantGuard implements CanActivate, OnModuleDestroy {
|
|
|
190
197
|
const checkableRoles = hasNonSystemRoles ? roles.filter((r) => !isSystemRole(r)) : [];
|
|
191
198
|
const minRequiredLevel = checkableRoles.length > 0 ? getMinRequiredLevel(checkableRoles) : undefined;
|
|
192
199
|
|
|
193
|
-
// @SkipTenantCheck → no tenant context, but role check against user.roles
|
|
194
|
-
const
|
|
200
|
+
// @SkipTenantCheck decorator → no tenant context, but role check against user.roles
|
|
201
|
+
const hasSkipDecorator = this.reflector.getAllAndOverride<boolean>(SKIP_TENANT_CHECK_KEY, [
|
|
195
202
|
context.getHandler(),
|
|
196
203
|
context.getClass(),
|
|
197
204
|
]);
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
if (hasSkipDecorator) {
|
|
206
|
+
return this.skipWithUserRoleCheck(checkableRoles, user, isAdmin);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Auto-skip tenant check for BetterAuth (IAM) handlers when configured,
|
|
210
|
+
// but ONLY when no X-Tenant-Id header is present.
|
|
211
|
+
// - No header: skip tenant validation (auth before tenant is the expected case for most projects)
|
|
212
|
+
// - Header present: fall through to normal validation (membership check, tenant context set)
|
|
213
|
+
// This allows tenant-aware auth scenarios to coexist with the default cross-tenant behavior.
|
|
214
|
+
// Default config: betterAuth.skipTenantCheck = true (note: distinct from @SkipTenantCheck decorator above).
|
|
215
|
+
if (!hasSkipDecorator && !headerTenantId && this.isBetterAuthHandler(context)) {
|
|
216
|
+
const betterAuthConfig = ConfigService.configFastButReadOnly?.betterAuth;
|
|
217
|
+
// Boolean shorthand: `betterAuth: true` → skip, `betterAuth: false` → no skip
|
|
218
|
+
const shouldSkip =
|
|
219
|
+
betterAuthConfig !== null && betterAuthConfig !== undefined && typeof betterAuthConfig === 'object'
|
|
220
|
+
? betterAuthConfig.skipTenantCheck !== false // default: true
|
|
221
|
+
: betterAuthConfig !== false; // true/undefined → skip; false → no skip
|
|
222
|
+
|
|
223
|
+
if (shouldSkip) {
|
|
224
|
+
this.logger.debug(
|
|
225
|
+
`BetterAuth auto-skip: ${context.getClass().name}::${context.getHandler().name} — no X-Tenant-Id header, skipping tenant validation`,
|
|
226
|
+
);
|
|
227
|
+
return this.skipWithUserRoleCheck(checkableRoles, user, isAdmin);
|
|
203
228
|
}
|
|
204
|
-
return true;
|
|
205
229
|
}
|
|
206
230
|
|
|
207
231
|
// === HEADER PRESENT ===
|
|
@@ -212,7 +236,9 @@ export class CoreTenantGuard implements CanActivate, OnModuleDestroy {
|
|
|
212
236
|
request.tenantId = headerTenantId;
|
|
213
237
|
request.isAdminBypass = true;
|
|
214
238
|
const requiredRole = checkableRoles.length > 0 ? checkableRoles.join(',') : 'none';
|
|
215
|
-
|
|
239
|
+
// Sanitize control characters to prevent log injection
|
|
240
|
+
const safeTenantId = headerTenantId.replace(/[\r\n\t]/g, '_');
|
|
241
|
+
this.logger.log(`Admin bypass: user ${user.id} accessing tenant ${safeTenantId} (required: ${requiredRole})`);
|
|
216
242
|
return true;
|
|
217
243
|
}
|
|
218
244
|
|
|
@@ -438,4 +464,54 @@ export class CoreTenantGuard implements CanActivate, OnModuleDestroy {
|
|
|
438
464
|
}
|
|
439
465
|
}
|
|
440
466
|
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Skip tenant validation but still check non-system roles against user.roles.
|
|
470
|
+
* Shared by @SkipTenantCheck decorator path and BetterAuth auto-skip path.
|
|
471
|
+
*/
|
|
472
|
+
private skipWithUserRoleCheck(checkableRoles: string[], user: any, isAdmin: boolean): true {
|
|
473
|
+
if (checkableRoles.length > 0) {
|
|
474
|
+
// Defense-in-depth: reject unauthenticated access even if RolesGuard is absent
|
|
475
|
+
if (!user) {
|
|
476
|
+
throw new ForbiddenException('Authentication required');
|
|
477
|
+
}
|
|
478
|
+
if (!isAdmin && !checkRoleAccess(checkableRoles, user.roles, undefined)) {
|
|
479
|
+
throw new ForbiddenException('Insufficient role');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Check if the current request is handled by a BetterAuth (IAM) handler
|
|
487
|
+
* (controller or resolver). Used for auto-skip tenant check on IAM endpoints.
|
|
488
|
+
*
|
|
489
|
+
* Uses require() instead of import to avoid a circular dependency:
|
|
490
|
+
* tenant module → better-auth module (which may depend on tenant module indirectly).
|
|
491
|
+
* The require() is lazy and resolved only when needed (Node.js caches the result).
|
|
492
|
+
*/
|
|
493
|
+
private isBetterAuthHandler(context: ExecutionContext): boolean {
|
|
494
|
+
const handler = context.getClass();
|
|
495
|
+
try {
|
|
496
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
497
|
+
const { CoreBetterAuthController } =
|
|
498
|
+
require('../better-auth/core-better-auth.controller') as typeof import('../better-auth/core-better-auth.controller');
|
|
499
|
+
if (handler === CoreBetterAuthController || handler.prototype instanceof CoreBetterAuthController) {
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
} catch {
|
|
503
|
+
/* BetterAuth controller not available */
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
507
|
+
const { CoreBetterAuthResolver } =
|
|
508
|
+
require('../better-auth/core-better-auth.resolver') as typeof import('../better-auth/core-better-auth.resolver');
|
|
509
|
+
if (handler === CoreBetterAuthResolver || handler.prototype instanceof CoreBetterAuthResolver) {
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
} catch {
|
|
513
|
+
/* BetterAuth resolver not available */
|
|
514
|
+
}
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
441
517
|
}
|