@lenne.tech/nest-server 11.27.6 → 11.28.0
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/.claude/rules/architecture.md +66 -0
- package/.claude/rules/better-auth.md +88 -2
- package/.claude/rules/package-management.md +23 -14
- package/.claude/rules/role-system.md +90 -0
- package/.claude/rules/versioning.md +17 -2
- package/CLAUDE.md +8 -2
- package/FRAMEWORK-API.md +27 -1
- package/dist/core/common/decorators/restricted.decorator.d.ts +4 -4
- package/dist/core/common/decorators/restricted.decorator.js +29 -26
- package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
- package/dist/core/common/exceptions/access-denied.exception.d.ts +4 -0
- package/dist/core/common/exceptions/access-denied.exception.js +12 -0
- package/dist/core/common/exceptions/access-denied.exception.js.map +1 -0
- package/dist/core/common/helpers/clone.helper.d.ts +6 -0
- package/dist/core/common/helpers/clone.helper.js +67 -0
- package/dist/core/common/helpers/clone.helper.js.map +1 -0
- package/dist/core/common/helpers/db.helper.d.ts +2 -14
- package/dist/core/common/helpers/db.helper.js +13 -106
- package/dist/core/common/helpers/db.helper.js.map +1 -1
- package/dist/core/common/helpers/id.helper.d.ts +15 -0
- package/dist/core/common/helpers/id.helper.js +104 -0
- package/dist/core/common/helpers/id.helper.js.map +1 -0
- package/dist/core/common/helpers/input.helper.d.ts +1 -6
- package/dist/core/common/helpers/input.helper.js +15 -72
- package/dist/core/common/helpers/input.helper.js.map +1 -1
- package/dist/core/common/helpers/service.helper.js +4 -3
- package/dist/core/common/helpers/service.helper.js.map +1 -1
- package/dist/core/common/inputs/combined-filter.input.d.ts +1 -12
- package/dist/core/common/inputs/combined-filter.input.js +2 -51
- package/dist/core/common/inputs/combined-filter.input.js.map +1 -1
- package/dist/core/common/inputs/filter.input.d.ts +10 -1
- package/dist/core/common/inputs/filter.input.js +40 -5
- package/dist/core/common/inputs/filter.input.js.map +1 -1
- package/dist/core/common/services/config.service.js +5 -5
- package/dist/core/common/services/config.service.js.map +1 -1
- package/dist/core/modules/ai/core-ai.constants.d.ts +22 -0
- package/dist/core/modules/ai/core-ai.constants.js +26 -0
- package/dist/core/modules/ai/core-ai.constants.js.map +1 -0
- package/dist/core/modules/ai/index.d.ts +1 -0
- package/dist/core/modules/ai/index.js +1 -0
- package/dist/core/modules/ai/index.js.map +1 -1
- package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.d.ts +17 -0
- package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.js +3 -0
- package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-budget.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-budget.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-budget.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-connection-preference.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-connection-preference.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-connection-preference.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-connection.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-connection.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-connection.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-conversation.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-interaction.service.d.ts +2 -3
- package/dist/core/modules/ai/services/core-ai-interaction.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-interaction.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-mode.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-mode.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-mode.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-prompt-hint.service.d.ts +1 -3
- package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-prompt.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-prompt.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-prompt.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-slot.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-slot.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-slot.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-tool-grant.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-tool-grant.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-tool-grant.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-tool-policy.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-tool-policy.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-tool-policy.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai.service.d.ts +2 -17
- package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
- package/dist/core/modules/auth/guards/roles.guard.js +2 -2
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-roles.guard.js +3 -3
- package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +6 -0
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.constants.d.ts +3 -0
- package/dist/core/modules/better-auth/core-better-auth.constants.js +7 -0
- package/dist/core/modules/better-auth/core-better-auth.constants.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.js +1 -3
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.d.ts +1 -2
- package/dist/core/modules/better-auth/core-better-auth.module.js +23 -20
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.registry.d.ts +4 -0
- package/dist/core/modules/better-auth/core-better-auth.registry.js +16 -0
- package/dist/core/modules/better-auth/core-better-auth.registry.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -2
- package/dist/core/modules/better-auth/core-better-auth.service.js +7 -6
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +1 -0
- package/dist/core/modules/better-auth/index.js +1 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant-member.model.js +3 -3
- package/dist/core/modules/tenant/core-tenant-member.model.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.guard.js +7 -6
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tus/tus.constants.d.ts +1 -0
- package/dist/core/modules/tus/tus.constants.js +5 -0
- package/dist/core/modules/tus/tus.constants.js.map +1 -0
- package/dist/core/modules/tus/tus.module.d.ts +1 -1
- package/dist/core/modules/tus/tus.module.js +8 -6
- package/dist/core/modules/tus/tus.module.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/user/user.service.js +1 -1
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +39 -0
- package/migration-guides/11.27.6-to-11.27.7.md +374 -0
- package/migration-guides/11.27.7-to-11.28.0.md +350 -0
- package/package.json +6 -104
- package/src/core/common/decorators/restricted.decorator.ts +67 -20
- package/src/core/common/exceptions/access-denied.exception.ts +49 -0
- package/src/core/common/helpers/clone.helper.ts +110 -0
- package/src/core/common/helpers/db.helper.ts +14 -161
- package/src/core/common/helpers/id.helper.ts +198 -0
- package/src/core/common/helpers/input.helper.ts +21 -88
- package/src/core/common/helpers/service.helper.ts +6 -4
- package/src/core/common/inputs/combined-filter.input.ts +10 -57
- package/src/core/common/inputs/filter.input.ts +92 -1
- package/src/core/common/services/config.service.ts +4 -1
- package/src/core/modules/ai/core-ai.constants.ts +92 -0
- package/src/core/modules/ai/index.ts +1 -0
- package/src/core/modules/ai/interfaces/ai-interaction-record.interface.ts +34 -0
- package/src/core/modules/ai/services/core-ai-budget.service.ts +8 -4
- package/src/core/modules/ai/services/core-ai-connection-preference.service.ts +8 -4
- package/src/core/modules/ai/services/core-ai-connection.service.ts +5 -6
- package/src/core/modules/ai/services/core-ai-conversation.service.ts +5 -6
- package/src/core/modules/ai/services/core-ai-interaction.service.ts +6 -7
- package/src/core/modules/ai/services/core-ai-mode.service.ts +8 -2
- package/src/core/modules/ai/services/core-ai-prompt-hint.service.ts +7 -4
- package/src/core/modules/ai/services/core-ai-prompt.service.ts +8 -2
- package/src/core/modules/ai/services/core-ai-slot.service.ts +7 -4
- package/src/core/modules/ai/services/core-ai-tool-grant.service.ts +8 -4
- package/src/core/modules/ai/services/core-ai-tool-policy.service.ts +8 -4
- package/src/core/modules/ai/services/core-ai.service.ts +8 -11
- package/src/core/modules/auth/guards/roles.guard.ts +5 -4
- package/src/core/modules/better-auth/better-auth-roles.guard.ts +15 -7
- package/src/core/modules/better-auth/better-auth.config.ts +16 -0
- package/src/core/modules/better-auth/core-better-auth.constants.ts +73 -0
- package/src/core/modules/better-auth/core-better-auth.controller.ts +5 -6
- package/src/core/modules/better-auth/core-better-auth.module.ts +34 -8
- package/src/core/modules/better-auth/core-better-auth.registry.ts +53 -0
- package/src/core/modules/better-auth/core-better-auth.service.ts +19 -12
- package/src/core/modules/better-auth/index.ts +9 -0
- package/src/core/modules/tenant/core-tenant-member.model.ts +8 -3
- package/src/core/modules/tenant/core-tenant.guard.ts +26 -11
- package/src/core/modules/tus/tus.constants.ts +25 -0
- package/src/core/modules/tus/tus.module.ts +7 -2
- package/src/index.ts +1 -0
- package/src/server/modules/user/user.service.ts +3 -2
|
@@ -10,8 +10,14 @@ import { CoreAiPromptCreateInput } from '../inputs/core-ai-prompt-create.input';
|
|
|
10
10
|
import { CoreAiPromptUpdateInput } from '../inputs/core-ai-prompt-update.input';
|
|
11
11
|
import { AiPromptDocument, CoreAiPrompt } from '../models/core-ai-prompt.model';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
import { AI_PROMPT_CLASS, AI_PROMPT_MODEL } from '../core-ai.constants';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated Import from `../core-ai.constants` instead. Re-exported only so existing deep imports
|
|
17
|
+
* keep working; the tokens are declared in an import-free leaf so no cycle can form around them
|
|
18
|
+
* (SWC-safe — see core-ai.constants.ts).
|
|
19
|
+
*/
|
|
20
|
+
export { AI_PROMPT_CLASS, AI_PROMPT_MODEL } from '../core-ai.constants';
|
|
15
21
|
|
|
16
22
|
const VALID_SCOPES = new Set(['tenant', 'user']);
|
|
17
23
|
|
|
@@ -11,11 +11,14 @@ import { CoreAiSlotCreateInput } from '../inputs/core-ai-slot-create.input';
|
|
|
11
11
|
import { CoreAiSlotUpdateInput } from '../inputs/core-ai-slot-update.input';
|
|
12
12
|
import { AiSlotDocument, CoreAiSlot } from '../models/core-ai-slot.model';
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
export const AI_SLOT_MODEL = 'AiSlot';
|
|
14
|
+
import { AI_SLOT_CLASS, AI_SLOT_MODEL } from '../core-ai.constants';
|
|
16
15
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* @deprecated Import from `../core-ai.constants` instead. Re-exported only so existing deep imports
|
|
18
|
+
* keep working; the tokens are declared in an import-free leaf so no cycle can form around them
|
|
19
|
+
* (SWC-safe — see core-ai.constants.ts).
|
|
20
|
+
*/
|
|
21
|
+
export { AI_SLOT_CLASS, AI_SLOT_MODEL } from '../core-ai.constants';
|
|
19
22
|
|
|
20
23
|
/** A resolved prompt fragment ready for placeholder rendering + assembly. */
|
|
21
24
|
export interface ResolvedPromptFragment {
|
|
@@ -6,10 +6,14 @@ import { CrudService } from '../../../common/services/crud.service';
|
|
|
6
6
|
import { CoreModelConstructor } from '../../../common/types/core-model-constructor.type';
|
|
7
7
|
import { AiToolGrantDocument, CoreAiToolGrant } from '../models/core-ai-tool-grant.model';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
|
|
9
|
+
import { AI_TOOL_GRANT_CLASS, AI_TOOL_GRANT_MODEL } from '../core-ai.constants';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated Import from `../core-ai.constants` instead. Re-exported only so existing deep imports
|
|
13
|
+
* keep working; the tokens are declared in an import-free leaf so no cycle can form around them
|
|
14
|
+
* (SWC-safe — see core-ai.constants.ts).
|
|
15
|
+
*/
|
|
16
|
+
export { AI_TOOL_GRANT_CLASS, AI_TOOL_GRANT_MODEL } from '../core-ai.constants';
|
|
13
17
|
|
|
14
18
|
/** Scope of a persisted permission decision. */
|
|
15
19
|
export type AiToolGrantScope = 'conversation' | 'tenant' | 'user';
|
|
@@ -6,10 +6,14 @@ import { CrudService } from '../../../common/services/crud.service';
|
|
|
6
6
|
import { CoreModelConstructor } from '../../../common/types/core-model-constructor.type';
|
|
7
7
|
import { AiToolPolicyDocument, CoreAiToolPolicy } from '../models/core-ai-tool-policy.model';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
|
|
9
|
+
import { AI_TOOL_POLICY_CLASS, AI_TOOL_POLICY_MODEL } from '../core-ai.constants';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated Import from `../core-ai.constants` instead. Re-exported only so existing deep imports
|
|
13
|
+
* keep working; the tokens are declared in an import-free leaf so no cycle can form around them
|
|
14
|
+
* (SWC-safe — see core-ai.constants.ts).
|
|
15
|
+
*/
|
|
16
|
+
export { AI_TOOL_POLICY_CLASS, AI_TOOL_POLICY_MODEL } from '../core-ai.constants';
|
|
13
17
|
|
|
14
18
|
export interface AiToolPolicyDecision {
|
|
15
19
|
/** Final decision: 'allow', 'deny' or 'ask'. */
|
|
@@ -27,20 +27,17 @@ import { AiPromptFeedbackSignal, CoreAiPromptHintService } from './core-ai-promp
|
|
|
27
27
|
import { AiToolGrantScope, CoreAiToolGrantService } from './core-ai-tool-grant.service';
|
|
28
28
|
import { CoreAiToolPolicyService } from './core-ai-tool-policy.service';
|
|
29
29
|
import { CoreAiModeService } from './core-ai-mode.service';
|
|
30
|
+
import type { AiInteractionRecord } from '../interfaces/ai-interaction-record.interface';
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
|
-
*
|
|
33
|
+
* @deprecated Import from `../interfaces/ai-interaction-record.interface` instead. Re-exported only
|
|
34
|
+
* so existing imports keep working.
|
|
35
|
+
*
|
|
36
|
+
* The type was moved out of this file because `core-ai-interaction.service` needs it while this file
|
|
37
|
+
* imports THAT service — an import cycle held apart only by an `import type`. See the interface's
|
|
38
|
+
* docblock.
|
|
33
39
|
*/
|
|
34
|
-
export
|
|
35
|
-
actions: { name: string; success: boolean }[];
|
|
36
|
-
connectionId: string;
|
|
37
|
-
iterations: number;
|
|
38
|
-
prompt: string;
|
|
39
|
-
responseText: string;
|
|
40
|
-
tenantId?: string;
|
|
41
|
-
usage?: { completionTokens?: number; promptTokens?: number; totalTokens?: number };
|
|
42
|
-
userId?: string;
|
|
43
|
-
}
|
|
40
|
+
export type { AiInteractionRecord } from '../interfaces/ai-interaction-record.interface';
|
|
44
41
|
|
|
45
42
|
/**
|
|
46
43
|
* Event emitted by {@link CoreAiService.promptStream} over SSE.
|
|
@@ -137,9 +137,10 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
|
|
|
137
137
|
]);
|
|
138
138
|
const roles = mergeRolesMetadata(reflectorRoles);
|
|
139
139
|
|
|
140
|
-
// Check if locked - always deny
|
|
140
|
+
// Check if locked - always deny. 403, never 401: the endpoint is locked permanently, so
|
|
141
|
+
// authenticating can never grant access and a 401 ("authenticate and retry") would be a lie.
|
|
141
142
|
if (roles && roles.includes(RoleEnum.S_NO_ONE)) {
|
|
142
|
-
throw new
|
|
143
|
+
throw new ForbiddenException(ErrorCode.ACCESS_DENIED);
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
// If no roles required, or S_EVERYONE is set, allow access without authentication
|
|
@@ -293,9 +294,9 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
|
|
|
293
294
|
]);
|
|
294
295
|
const roles = mergeRolesMetadata(reflectorRoles);
|
|
295
296
|
|
|
296
|
-
// Check if locked
|
|
297
|
+
// Check if locked — 403, never 401 (see canActivate: authenticating can never unlock it)
|
|
297
298
|
if (roles && roles.includes(RoleEnum.S_NO_ONE)) {
|
|
298
|
-
throw new
|
|
299
|
+
throw new ForbiddenException(ErrorCode.ACCESS_DENIED);
|
|
299
300
|
}
|
|
300
301
|
|
|
301
302
|
// Check roles
|
|
@@ -13,7 +13,7 @@ import { ErrorCode } from '../error-code';
|
|
|
13
13
|
import { isMultiTenancyActive, isSystemRole, mergeRolesMetadata } from '../tenant/core-tenant.helpers';
|
|
14
14
|
import { BetterAuthTokenService } from './better-auth-token.service';
|
|
15
15
|
import { BetterAuthenticatedUser } from './better-auth.types';
|
|
16
|
-
import {
|
|
16
|
+
import { getBetterAuthTokenService } from './core-better-auth.registry';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* BetterAuth Roles Guard
|
|
@@ -33,7 +33,8 @@ import { CoreBetterAuthModule } from './core-better-auth.module';
|
|
|
33
33
|
* IMPORTANT: This guard has NO constructor dependencies. This is intentional because
|
|
34
34
|
* NestJS DI has issues resolving Reflector/ModuleRef for APP_GUARD providers in dynamic modules.
|
|
35
35
|
* Instead, we use Reflect.getMetadata directly to read decorator metadata, and access
|
|
36
|
-
* BetterAuthTokenService via
|
|
36
|
+
* BetterAuthTokenService via the core-better-auth.registry leaf module — NOT via
|
|
37
|
+
* CoreBetterAuthModule, which would make guard and module import each other.
|
|
37
38
|
*/
|
|
38
39
|
@Injectable()
|
|
39
40
|
export class BetterAuthRolesGuard implements CanActivate {
|
|
@@ -41,12 +42,17 @@ export class BetterAuthRolesGuard implements CanActivate {
|
|
|
41
42
|
private tokenService: BetterAuthTokenService | null = null;
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
|
-
* Get BetterAuthTokenService lazily from
|
|
45
|
-
* This avoids DI issues while still allowing token verification
|
|
45
|
+
* Get BetterAuthTokenService lazily from the better-auth registry.
|
|
46
|
+
* This avoids DI issues while still allowing token verification.
|
|
47
|
+
*
|
|
48
|
+
* The lookup goes through core-better-auth.registry.ts rather than
|
|
49
|
+
* CoreBetterAuthModule on purpose: importing the module here would make guard
|
|
50
|
+
* and module import each other, and that cycle is one decorator away from the
|
|
51
|
+
* SWC temporal-dead-zone crash documented in core-better-auth.constants.ts.
|
|
46
52
|
*/
|
|
47
53
|
private getTokenService(): BetterAuthTokenService | null {
|
|
48
54
|
if (!this.tokenService) {
|
|
49
|
-
this.tokenService =
|
|
55
|
+
this.tokenService = getBetterAuthTokenService();
|
|
50
56
|
}
|
|
51
57
|
return this.tokenService;
|
|
52
58
|
}
|
|
@@ -82,9 +88,11 @@ export class BetterAuthRolesGuard implements CanActivate {
|
|
|
82
88
|
// Combine handler and class roles (handler takes precedence, like Reflector.getAll)
|
|
83
89
|
const roles = mergeRolesMetadata([handlerRoles, classRoles]);
|
|
84
90
|
|
|
85
|
-
// Check if locked - always deny
|
|
91
|
+
// Check if locked - always deny. 403, never 401: the endpoint is locked permanently, so
|
|
92
|
+
// authenticating can never grant access and a 401 ("authenticate and retry") would be a lie.
|
|
93
|
+
// Kept in sync with RolesGuard (see .claude/rules/better-auth.md).
|
|
86
94
|
if (roles && roles.includes(RoleEnum.S_NO_ONE)) {
|
|
87
|
-
throw new
|
|
95
|
+
throw new ForbiddenException(ErrorCode.ACCESS_DENIED);
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
// If no roles required, or S_EVERYONE is set, allow access without authentication
|
|
@@ -476,6 +476,22 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Crea
|
|
|
476
476
|
finalConfig.advanced = {
|
|
477
477
|
...(betterAuthConfig.advanced as Record<string, unknown>),
|
|
478
478
|
...(optionsAdvanced as Record<string, unknown>),
|
|
479
|
+
// `defaultCookieAttributes` needs a DEEP merge, not the shallow spread above.
|
|
480
|
+
// It is the key that carries our restored `secure: true` (see the `useSecureCookies: false`
|
|
481
|
+
// block), so a consumer setting it for an entirely unrelated reason — `{ partitioned: true }`,
|
|
482
|
+
// `{ domain: … }` — would wholesale-replace the object and silently strip `Secure` from every
|
|
483
|
+
// session cookie the native handlers forward (2FA verify, social callback, magic link) on an
|
|
484
|
+
// https deployment. Session-establishing cookies, in the clear, with no error anywhere.
|
|
485
|
+
// Re-apply `secure: true` as the BASE and let the consumer's keys spread over it, so an
|
|
486
|
+
// EXPLICIT `secure: false` still wins while an unrelated key can no longer clobber it.
|
|
487
|
+
...(secureCookies && {
|
|
488
|
+
defaultCookieAttributes: {
|
|
489
|
+
secure: true,
|
|
490
|
+
...((optionsAdvanced as Record<string, unknown>).defaultCookieAttributes as
|
|
491
|
+
| Record<string, unknown>
|
|
492
|
+
| undefined),
|
|
493
|
+
},
|
|
494
|
+
}),
|
|
479
495
|
};
|
|
480
496
|
}
|
|
481
497
|
} else {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-injection tokens of the better-auth module.
|
|
3
|
+
*
|
|
4
|
+
* These live in a dedicated file — instead of core-better-auth.module.ts /
|
|
5
|
+
* core-better-auth.service.ts — so that module and service never have to import
|
|
6
|
+
* each other. The tokens used to be split across both files
|
|
7
|
+
* (BETTER_AUTH_INSTANCE in the module, BETTER_AUTH_CONFIG /
|
|
8
|
+
* BETTER_AUTH_COOKIE_DOMAIN in the service), which made the two files import
|
|
9
|
+
* each other in a cycle. That cycle happened to work under tsc/CommonJS by
|
|
10
|
+
* evaluation-order luck, but crashed SWC-compiled builds (`nest start -b swc`)
|
|
11
|
+
* with a temporal-dead-zone error:
|
|
12
|
+
*
|
|
13
|
+
* ReferenceError: Cannot access 'BETTER_AUTH_INSTANCE' before initialization
|
|
14
|
+
*
|
|
15
|
+
* The lethal ingredient is not the cycle by itself — it is a cycle PLUS a read of
|
|
16
|
+
* the cyclic binding at module-evaluation time. `@Inject(BETTER_AUTH_INSTANCE)` is
|
|
17
|
+
* a constructor-parameter decorator, and decorator arguments are evaluated when the
|
|
18
|
+
* class is defined, i.e. while the module is still initializing. On a cycle the
|
|
19
|
+
* importing side then reads a `const` that has not been initialized yet.
|
|
20
|
+
*
|
|
21
|
+
* This file imports nothing, so it can never be mid-evaluation when someone
|
|
22
|
+
* imports it — in any module system, under any compiler. That makes the
|
|
23
|
+
* initialization order of the tokens deterministic everywhere.
|
|
24
|
+
*
|
|
25
|
+
* WHY THIS MATTERS FOR FUTURE CHANGES
|
|
26
|
+
* -----------------------------------
|
|
27
|
+
* `tsc` does NOT catch a regression here, and neither does the test suite: vitest
|
|
28
|
+
* runs SWC through Vite's module runner, whose getter-based live bindings tolerate
|
|
29
|
+
* cycles. Only the SWC → CommonJS → `require()` path fails, which is exactly what
|
|
30
|
+
* `pnpm run check:swc-tdz` exercises. If you move a token back into the module or
|
|
31
|
+
* the service, everything stays green locally and breaks for consumers.
|
|
32
|
+
*
|
|
33
|
+
* The rule, in short: **DI tokens belong in an import-free leaf file.**
|
|
34
|
+
* See .claude/rules/better-auth.md §6.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Token for injecting the better-auth instance.
|
|
39
|
+
*
|
|
40
|
+
* Injected type: `BetterAuthInstance | null` — null when better-auth is disabled.
|
|
41
|
+
* Declared `@Optional()` in the CoreBetterAuthService constructor.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* import { Inject, Injectable, Optional } from '@nestjs/common';
|
|
46
|
+
* import { BETTER_AUTH_INSTANCE, BetterAuthInstance } from '@lenne.tech/nest-server';
|
|
47
|
+
*
|
|
48
|
+
* @Injectable()
|
|
49
|
+
* export class MyService {
|
|
50
|
+
* constructor(
|
|
51
|
+
* @Optional() @Inject(BETTER_AUTH_INSTANCE) private readonly auth: BetterAuthInstance | null,
|
|
52
|
+
* ) {}
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export const BETTER_AUTH_INSTANCE = 'BETTER_AUTH_INSTANCE';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Injection token for the resolved BetterAuth configuration.
|
|
60
|
+
*
|
|
61
|
+
* Injected type: `IBetterAuth | null` — null when better-auth is disabled.
|
|
62
|
+
* Declared `@Optional()` in the CoreBetterAuthService constructor.
|
|
63
|
+
*/
|
|
64
|
+
export const BETTER_AUTH_CONFIG = 'BETTER_AUTH_CONFIG';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Injection token for the resolved cross-subdomain cookie domain.
|
|
68
|
+
* Set during Better-Auth instance creation, undefined if disabled.
|
|
69
|
+
*
|
|
70
|
+
* Injected type: `string | null | undefined`.
|
|
71
|
+
* Declared `@Optional()` in the CoreBetterAuthService constructor.
|
|
72
|
+
*/
|
|
73
|
+
export const BETTER_AUTH_COOKIE_DOMAIN = 'BETTER_AUTH_COOKIE_DOMAIN';
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Controller,
|
|
6
6
|
Get,
|
|
7
7
|
HttpCode,
|
|
8
|
+
HttpException,
|
|
8
9
|
HttpStatus,
|
|
9
10
|
InternalServerErrorException,
|
|
10
11
|
Logger,
|
|
@@ -932,12 +933,10 @@ export class CoreBetterAuthController {
|
|
|
932
933
|
} catch (error) {
|
|
933
934
|
this.logger.error(`Better Auth handler error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
934
935
|
|
|
935
|
-
// Re-throw NestJS exceptions
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
error instanceof InternalServerErrorException
|
|
940
|
-
) {
|
|
936
|
+
// Re-throw NestJS exceptions. Matching HttpException (not an enumeration of subclasses) keeps
|
|
937
|
+
// any deliberately thrown status intact — an enumeration silently masks everything it forgets
|
|
938
|
+
// (e.g. a 403 from the rights checks) as a 500.
|
|
939
|
+
if (error instanceof HttpException) {
|
|
941
940
|
throw error;
|
|
942
941
|
}
|
|
943
942
|
|
|
@@ -28,15 +28,34 @@ import { CoreBetterAuthRateLimitMiddleware } from './core-better-auth-rate-limit
|
|
|
28
28
|
import { CoreBetterAuthRateLimiter } from './core-better-auth-rate-limiter.service';
|
|
29
29
|
import { CoreBetterAuthSignUpValidatorService } from './core-better-auth-signup-validator.service';
|
|
30
30
|
import { CoreBetterAuthUserMapper } from './core-better-auth-user.mapper';
|
|
31
|
+
import { BETTER_AUTH_CONFIG, BETTER_AUTH_COOKIE_DOMAIN, BETTER_AUTH_INSTANCE } from './core-better-auth.constants';
|
|
31
32
|
import { CoreBetterAuthController } from './core-better-auth.controller';
|
|
32
33
|
import { CoreBetterAuthMiddleware } from './core-better-auth.middleware';
|
|
34
|
+
import {
|
|
35
|
+
getBetterAuthTokenService,
|
|
36
|
+
resetBetterAuthRegistry,
|
|
37
|
+
setBetterAuthTokenService,
|
|
38
|
+
} from './core-better-auth.registry';
|
|
33
39
|
import { CoreBetterAuthResolver } from './core-better-auth.resolver';
|
|
34
|
-
import {
|
|
40
|
+
import { CoreBetterAuthService } from './core-better-auth.service';
|
|
35
41
|
|
|
36
42
|
/**
|
|
37
|
-
*
|
|
43
|
+
* @deprecated Import from `./core-better-auth.constants` instead. Re-exported only
|
|
44
|
+
* so existing deep imports keep working; it will be dropped in a future MINOR.
|
|
45
|
+
*
|
|
46
|
+
* Do NOT import this token from here inside the better-auth module itself — the
|
|
47
|
+
* token must come from the constants leaf, or module and service start importing
|
|
48
|
+
* each other again and SWC builds crash with a temporal-dead-zone ReferenceError
|
|
49
|
+
* (see core-better-auth.constants.ts).
|
|
50
|
+
*
|
|
51
|
+
* NOTE: the `@deprecated` tag above is documentation only — TypeScript does not
|
|
52
|
+
* honor JSDoc on an `export … from` declaration. Do NOT try to "fix" that by
|
|
53
|
+
* re-declaring the token as a local `const`: that would give the barrel two
|
|
54
|
+
* distinct declarations of the same name and turn its `export *` into a TS2308
|
|
55
|
+
* ambiguity error. The mechanical guards are `pnpm run check:swc-tdz` and
|
|
56
|
+
* `tests/unit/better-auth-di-tokens.spec.ts`.
|
|
38
57
|
*/
|
|
39
|
-
export
|
|
58
|
+
export { BETTER_AUTH_INSTANCE } from './core-better-auth.constants';
|
|
40
59
|
|
|
41
60
|
/**
|
|
42
61
|
* Options for CoreBetterAuthModule.forRoot()
|
|
@@ -239,10 +258,11 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
239
258
|
// Safety Net: Track if forRoot() has already been called to detect duplicate registration
|
|
240
259
|
private static forRootCalled = false;
|
|
241
260
|
private static cachedDynamicModule: DynamicModule | null = null;
|
|
242
|
-
// Static references for lazy GraphQL driver (autoRegister: false)
|
|
261
|
+
// Static references for lazy GraphQL driver (autoRegister: false).
|
|
262
|
+
// The BetterAuthTokenService reference lives in core-better-auth.registry.ts instead,
|
|
263
|
+
// so BetterAuthRolesGuard can read it without importing this module (cycle-free).
|
|
243
264
|
private static serviceInstance: CoreBetterAuthService | null = null;
|
|
244
265
|
private static userMapperInstance: CoreBetterAuthUserMapper | null = null;
|
|
245
|
-
private static tokenServiceInstance: BetterAuthTokenService | null = null;
|
|
246
266
|
private static resolvedCookieDomain: string | undefined = undefined;
|
|
247
267
|
|
|
248
268
|
/**
|
|
@@ -279,11 +299,14 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
279
299
|
|
|
280
300
|
/**
|
|
281
301
|
* Gets the static BetterAuthTokenService instance.
|
|
282
|
-
* Used by BetterAuthRolesGuard for token verification when user isn't already on request.
|
|
283
302
|
* Safe because guards are invoked only after module initialization.
|
|
303
|
+
*
|
|
304
|
+
* Delegates to the core-better-auth.registry leaf module, which is the single
|
|
305
|
+
* source of truth. BetterAuthRolesGuard reads the registry directly instead of
|
|
306
|
+
* calling this method, so that guard and module never import each other.
|
|
284
307
|
*/
|
|
285
308
|
static getTokenServiceInstance(): BetterAuthTokenService | null {
|
|
286
|
-
return
|
|
309
|
+
return getBetterAuthTokenService();
|
|
287
310
|
}
|
|
288
311
|
|
|
289
312
|
/**
|
|
@@ -315,7 +338,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
315
338
|
CoreBetterAuthModule.userMapperInstance = this.userMapper;
|
|
316
339
|
}
|
|
317
340
|
if (this.tokenService) {
|
|
318
|
-
|
|
341
|
+
setBetterAuthTokenService(this.tokenService);
|
|
319
342
|
}
|
|
320
343
|
|
|
321
344
|
// Configure rate limiter with stored config
|
|
@@ -747,6 +770,9 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
747
770
|
this.serviceInstance = null;
|
|
748
771
|
this.userMapperInstance = null;
|
|
749
772
|
this.resolvedCookieDomain = undefined;
|
|
773
|
+
// Reset the BetterAuthTokenService reference used by BetterAuthRolesGuard.
|
|
774
|
+
// Without this a token service from a previous testing module leaks into the next one.
|
|
775
|
+
resetBetterAuthRegistry();
|
|
750
776
|
// Reset shared RolesGuard registry (shared with CoreAuthModule)
|
|
751
777
|
RolesGuardRegistry.reset();
|
|
752
778
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime registry for better-auth singletons that must be reachable from files
|
|
3
|
+
* which cannot import core-better-auth.module.ts.
|
|
4
|
+
*
|
|
5
|
+
* `BetterAuthRolesGuard` deliberately has no constructor dependencies (see
|
|
6
|
+
* .claude/rules/better-auth.md §5), so it cannot receive `BetterAuthTokenService`
|
|
7
|
+
* through DI and has to look it up statically. Reading it from
|
|
8
|
+
* `CoreBetterAuthModule` would make guard and module import each other — the same
|
|
9
|
+
* cycle shape that crashed SWC builds when the DI tokens still lived in the module
|
|
10
|
+
* and the service (see core-better-auth.constants.ts).
|
|
11
|
+
*
|
|
12
|
+
* This file therefore holds the reference instead. It uses only `import type`,
|
|
13
|
+
* which both tsc and SWC erase entirely, so it emits no `require()` at all and is
|
|
14
|
+
* a true leaf: it can never be mid-evaluation when someone imports it, in any
|
|
15
|
+
* module system, under any compiler.
|
|
16
|
+
*
|
|
17
|
+
* @internal Populated by CoreBetterAuthModule.onModuleInit(). Read it through the
|
|
18
|
+
* public `CoreBetterAuthModule.getTokenServiceInstance()` unless you are in a file
|
|
19
|
+
* that the module itself imports — importing the module from there would re-create
|
|
20
|
+
* the cycle.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { BetterAuthTokenService } from './better-auth-token.service';
|
|
24
|
+
|
|
25
|
+
let tokenServiceInstance: BetterAuthTokenService | null = null;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Stores the BetterAuthTokenService singleton.
|
|
29
|
+
* Called by CoreBetterAuthModule during module initialization.
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export function setBetterAuthTokenService(service: BetterAuthTokenService | null): void {
|
|
33
|
+
tokenServiceInstance = service;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns the BetterAuthTokenService singleton, or null when better-auth is
|
|
38
|
+
* disabled or the module has not initialized yet.
|
|
39
|
+
*
|
|
40
|
+
* Safe to call from guards: they run only after module initialization.
|
|
41
|
+
*/
|
|
42
|
+
export function getBetterAuthTokenService(): BetterAuthTokenService | null {
|
|
43
|
+
return tokenServiceInstance;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Clears the registry. Called by CoreBetterAuthModule.reset() so tests do not
|
|
48
|
+
* leak a token service from a previous testing module into the next one.
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export function resetBetterAuthRegistry(): void {
|
|
52
|
+
tokenServiceInstance = null;
|
|
53
|
+
}
|
|
@@ -13,7 +13,7 @@ import { resolveBetterAuthCookiePrefix } from './better-auth-cookie-prefix.helpe
|
|
|
13
13
|
import { BetterAuthInstance } from './better-auth.config';
|
|
14
14
|
import { BetterAuthSessionUser } from './core-better-auth-user.mapper';
|
|
15
15
|
import { convertExpressHeaders, parseCookieHeader, signCookieValueIfNeeded } from './core-better-auth-web.helper';
|
|
16
|
-
import { BETTER_AUTH_INSTANCE } from './core-better-auth.
|
|
16
|
+
import { BETTER_AUTH_CONFIG, BETTER_AUTH_COOKIE_DOMAIN, BETTER_AUTH_INSTANCE } from './core-better-auth.constants';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Result of a session validation
|
|
@@ -28,6 +28,24 @@ export interface SessionResult {
|
|
|
28
28
|
user: BetterAuthSessionUser | null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* @deprecated Import from `./core-better-auth.constants` instead. Re-exported only
|
|
33
|
+
* so existing deep imports keep working; it will be dropped in a future MINOR.
|
|
34
|
+
*
|
|
35
|
+
* Do NOT import these tokens from here inside the better-auth module itself — they
|
|
36
|
+
* must come from the constants leaf, or module and service start importing each
|
|
37
|
+
* other again and SWC builds crash with a temporal-dead-zone ReferenceError
|
|
38
|
+
* (see core-better-auth.constants.ts).
|
|
39
|
+
*
|
|
40
|
+
* NOTE: the `@deprecated` tag above is documentation only — TypeScript does not
|
|
41
|
+
* honor JSDoc on an `export … from` declaration. Do NOT try to "fix" that by
|
|
42
|
+
* re-declaring the tokens as local `const`s: that would give the barrel two
|
|
43
|
+
* distinct declarations of the same name and turn its `export *` into a TS2308
|
|
44
|
+
* ambiguity error. The mechanical guards are `pnpm run check:swc-tdz` and
|
|
45
|
+
* `tests/unit/better-auth-di-tokens.spec.ts`.
|
|
46
|
+
*/
|
|
47
|
+
export { BETTER_AUTH_CONFIG, BETTER_AUTH_COOKIE_DOMAIN } from './core-better-auth.constants';
|
|
48
|
+
|
|
31
49
|
/**
|
|
32
50
|
* CoreBetterAuthService provides a NestJS-friendly wrapper around the better-auth instance.
|
|
33
51
|
*
|
|
@@ -51,17 +69,6 @@ export interface SessionResult {
|
|
|
51
69
|
* }
|
|
52
70
|
* ```
|
|
53
71
|
*/
|
|
54
|
-
/**
|
|
55
|
-
* Injection token for resolved BetterAuth configuration
|
|
56
|
-
*/
|
|
57
|
-
export const BETTER_AUTH_CONFIG = 'BETTER_AUTH_CONFIG';
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Injection token for resolved cross-subdomain cookie domain.
|
|
61
|
-
* Set during Better-Auth instance creation, undefined if disabled.
|
|
62
|
-
*/
|
|
63
|
-
export const BETTER_AUTH_COOKIE_DOMAIN = 'BETTER_AUTH_COOKIE_DOMAIN';
|
|
64
|
-
|
|
65
72
|
@Injectable()
|
|
66
73
|
export class CoreBetterAuthService implements OnModuleInit {
|
|
67
74
|
private readonly logger = new Logger(CoreBetterAuthService.name);
|
|
@@ -39,8 +39,17 @@ export * from './core-better-auth-signup-validator.service';
|
|
|
39
39
|
export * from './core-better-auth-token.helper';
|
|
40
40
|
export * from './core-better-auth-user.mapper';
|
|
41
41
|
export * from './core-better-auth-web.helper';
|
|
42
|
+
export * from './core-better-auth.constants';
|
|
42
43
|
export * from './core-better-auth.controller';
|
|
43
44
|
export * from './core-better-auth.middleware';
|
|
44
45
|
export * from './core-better-auth.module';
|
|
46
|
+
// NOT exported: './core-better-auth.registry'.
|
|
47
|
+
//
|
|
48
|
+
// It is module-internal plumbing that only exists so BetterAuthRolesGuard can reach
|
|
49
|
+
// BetterAuthTokenService without importing CoreBetterAuthModule (see .claude/rules/better-auth.md
|
|
50
|
+
// §6). Re-exporting it would put `setBetterAuthTokenService()` on the public API — letting a
|
|
51
|
+
// consumer swap or null the very token service the guard makes its authorization decisions with.
|
|
52
|
+
// Consumers that need the instance already have the public, read-only
|
|
53
|
+
// `CoreBetterAuthModule.getTokenServiceInstance()`.
|
|
45
54
|
export * from './core-better-auth.resolver';
|
|
46
55
|
export * from './core-better-auth.service';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { UnauthorizedException } from '@nestjs/common';
|
|
2
1
|
import { ObjectType } from '@nestjs/graphql';
|
|
3
2
|
import { Schema } from '@nestjs/mongoose';
|
|
4
3
|
|
|
5
4
|
import { Restricted } from '../../common/decorators/restricted.decorator';
|
|
6
5
|
import { UnifiedField } from '../../common/decorators/unified-field.decorator';
|
|
7
6
|
import { RoleEnum } from '../../common/enums/role.enum';
|
|
7
|
+
import { accessDeniedException } from '../../common/exceptions/access-denied.exception';
|
|
8
8
|
import { CorePersistenceModel } from '../../common/models/core-persistence.model';
|
|
9
9
|
import { RequestContext } from '../../common/services/request-context.service';
|
|
10
10
|
import { DefaultHR, TenantMemberStatus } from './core-tenant.enums';
|
|
@@ -98,7 +98,11 @@ export class CoreTenantMemberModel extends CorePersistenceModel {
|
|
|
98
98
|
*/
|
|
99
99
|
override securityCheck(user: any, force?: boolean): this {
|
|
100
100
|
if (force) return this;
|
|
101
|
-
|
|
101
|
+
// accessDeniedException picks the status from the requester's auth state: 401 here (no user),
|
|
102
|
+
// 403 below (authenticated, but not owner / admin / tenant manager). Model-level denials must
|
|
103
|
+
// use this helper too — a permission error must never reach the client as a 401, or frontends
|
|
104
|
+
// treat it as an expired session and log the user out.
|
|
105
|
+
if (!user) throw accessDeniedException(user);
|
|
102
106
|
|
|
103
107
|
// Own membership or system admin
|
|
104
108
|
if (user.id === this.user || user.hasRole?.(RoleEnum.ADMIN)) return this;
|
|
@@ -116,6 +120,7 @@ export class CoreTenantMemberModel extends CorePersistenceModel {
|
|
|
116
120
|
return this;
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
|
|
123
|
+
// Reached only when `user` is set (the `!user` case returned above), so this is a 403.
|
|
124
|
+
throw accessDeniedException(user);
|
|
120
125
|
}
|
|
121
126
|
}
|