@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.
Files changed (162) hide show
  1. package/.claude/rules/architecture.md +66 -0
  2. package/.claude/rules/better-auth.md +88 -2
  3. package/.claude/rules/package-management.md +23 -14
  4. package/.claude/rules/role-system.md +90 -0
  5. package/.claude/rules/versioning.md +17 -2
  6. package/CLAUDE.md +8 -2
  7. package/FRAMEWORK-API.md +27 -1
  8. package/dist/core/common/decorators/restricted.decorator.d.ts +4 -4
  9. package/dist/core/common/decorators/restricted.decorator.js +29 -26
  10. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  11. package/dist/core/common/exceptions/access-denied.exception.d.ts +4 -0
  12. package/dist/core/common/exceptions/access-denied.exception.js +12 -0
  13. package/dist/core/common/exceptions/access-denied.exception.js.map +1 -0
  14. package/dist/core/common/helpers/clone.helper.d.ts +6 -0
  15. package/dist/core/common/helpers/clone.helper.js +67 -0
  16. package/dist/core/common/helpers/clone.helper.js.map +1 -0
  17. package/dist/core/common/helpers/db.helper.d.ts +2 -14
  18. package/dist/core/common/helpers/db.helper.js +13 -106
  19. package/dist/core/common/helpers/db.helper.js.map +1 -1
  20. package/dist/core/common/helpers/id.helper.d.ts +15 -0
  21. package/dist/core/common/helpers/id.helper.js +104 -0
  22. package/dist/core/common/helpers/id.helper.js.map +1 -0
  23. package/dist/core/common/helpers/input.helper.d.ts +1 -6
  24. package/dist/core/common/helpers/input.helper.js +15 -72
  25. package/dist/core/common/helpers/input.helper.js.map +1 -1
  26. package/dist/core/common/helpers/service.helper.js +4 -3
  27. package/dist/core/common/helpers/service.helper.js.map +1 -1
  28. package/dist/core/common/inputs/combined-filter.input.d.ts +1 -12
  29. package/dist/core/common/inputs/combined-filter.input.js +2 -51
  30. package/dist/core/common/inputs/combined-filter.input.js.map +1 -1
  31. package/dist/core/common/inputs/filter.input.d.ts +10 -1
  32. package/dist/core/common/inputs/filter.input.js +40 -5
  33. package/dist/core/common/inputs/filter.input.js.map +1 -1
  34. package/dist/core/common/services/config.service.js +5 -5
  35. package/dist/core/common/services/config.service.js.map +1 -1
  36. package/dist/core/modules/ai/core-ai.constants.d.ts +22 -0
  37. package/dist/core/modules/ai/core-ai.constants.js +26 -0
  38. package/dist/core/modules/ai/core-ai.constants.js.map +1 -0
  39. package/dist/core/modules/ai/index.d.ts +1 -0
  40. package/dist/core/modules/ai/index.js +1 -0
  41. package/dist/core/modules/ai/index.js.map +1 -1
  42. package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.d.ts +17 -0
  43. package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.js +3 -0
  44. package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.js.map +1 -0
  45. package/dist/core/modules/ai/services/core-ai-budget.service.d.ts +1 -2
  46. package/dist/core/modules/ai/services/core-ai-budget.service.js +7 -5
  47. package/dist/core/modules/ai/services/core-ai-budget.service.js.map +1 -1
  48. package/dist/core/modules/ai/services/core-ai-connection-preference.service.d.ts +1 -2
  49. package/dist/core/modules/ai/services/core-ai-connection-preference.service.js +7 -5
  50. package/dist/core/modules/ai/services/core-ai-connection-preference.service.js.map +1 -1
  51. package/dist/core/modules/ai/services/core-ai-connection.service.d.ts +1 -2
  52. package/dist/core/modules/ai/services/core-ai-connection.service.js +7 -5
  53. package/dist/core/modules/ai/services/core-ai-connection.service.js.map +1 -1
  54. package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +1 -2
  55. package/dist/core/modules/ai/services/core-ai-conversation.service.js +7 -5
  56. package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
  57. package/dist/core/modules/ai/services/core-ai-interaction.service.d.ts +2 -3
  58. package/dist/core/modules/ai/services/core-ai-interaction.service.js +7 -5
  59. package/dist/core/modules/ai/services/core-ai-interaction.service.js.map +1 -1
  60. package/dist/core/modules/ai/services/core-ai-mode.service.d.ts +1 -2
  61. package/dist/core/modules/ai/services/core-ai-mode.service.js +7 -5
  62. package/dist/core/modules/ai/services/core-ai-mode.service.js.map +1 -1
  63. package/dist/core/modules/ai/services/core-ai-prompt-hint.service.d.ts +1 -3
  64. package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js +7 -5
  65. package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js.map +1 -1
  66. package/dist/core/modules/ai/services/core-ai-prompt.service.d.ts +1 -2
  67. package/dist/core/modules/ai/services/core-ai-prompt.service.js +7 -5
  68. package/dist/core/modules/ai/services/core-ai-prompt.service.js.map +1 -1
  69. package/dist/core/modules/ai/services/core-ai-slot.service.d.ts +1 -2
  70. package/dist/core/modules/ai/services/core-ai-slot.service.js +7 -5
  71. package/dist/core/modules/ai/services/core-ai-slot.service.js.map +1 -1
  72. package/dist/core/modules/ai/services/core-ai-tool-grant.service.d.ts +1 -2
  73. package/dist/core/modules/ai/services/core-ai-tool-grant.service.js +7 -5
  74. package/dist/core/modules/ai/services/core-ai-tool-grant.service.js.map +1 -1
  75. package/dist/core/modules/ai/services/core-ai-tool-policy.service.d.ts +1 -2
  76. package/dist/core/modules/ai/services/core-ai-tool-policy.service.js +7 -5
  77. package/dist/core/modules/ai/services/core-ai-tool-policy.service.js.map +1 -1
  78. package/dist/core/modules/ai/services/core-ai.service.d.ts +2 -17
  79. package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
  80. package/dist/core/modules/auth/guards/roles.guard.js +2 -2
  81. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  82. package/dist/core/modules/better-auth/better-auth-roles.guard.js +3 -3
  83. package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
  84. package/dist/core/modules/better-auth/better-auth.config.js +6 -0
  85. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  86. package/dist/core/modules/better-auth/core-better-auth.constants.d.ts +3 -0
  87. package/dist/core/modules/better-auth/core-better-auth.constants.js +7 -0
  88. package/dist/core/modules/better-auth/core-better-auth.constants.js.map +1 -0
  89. package/dist/core/modules/better-auth/core-better-auth.controller.js +1 -3
  90. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  91. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +1 -2
  92. package/dist/core/modules/better-auth/core-better-auth.module.js +23 -20
  93. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  94. package/dist/core/modules/better-auth/core-better-auth.registry.d.ts +4 -0
  95. package/dist/core/modules/better-auth/core-better-auth.registry.js +16 -0
  96. package/dist/core/modules/better-auth/core-better-auth.registry.js.map +1 -0
  97. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -2
  98. package/dist/core/modules/better-auth/core-better-auth.service.js +7 -6
  99. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  100. package/dist/core/modules/better-auth/index.d.ts +1 -0
  101. package/dist/core/modules/better-auth/index.js +1 -0
  102. package/dist/core/modules/better-auth/index.js.map +1 -1
  103. package/dist/core/modules/tenant/core-tenant-member.model.js +3 -3
  104. package/dist/core/modules/tenant/core-tenant-member.model.js.map +1 -1
  105. package/dist/core/modules/tenant/core-tenant.guard.js +7 -6
  106. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  107. package/dist/core/modules/tus/tus.constants.d.ts +1 -0
  108. package/dist/core/modules/tus/tus.constants.js +5 -0
  109. package/dist/core/modules/tus/tus.constants.js.map +1 -0
  110. package/dist/core/modules/tus/tus.module.d.ts +1 -1
  111. package/dist/core/modules/tus/tus.module.js +8 -6
  112. package/dist/core/modules/tus/tus.module.js.map +1 -1
  113. package/dist/index.d.ts +1 -0
  114. package/dist/index.js +1 -0
  115. package/dist/index.js.map +1 -1
  116. package/dist/server/modules/user/user.service.js +1 -1
  117. package/dist/server/modules/user/user.service.js.map +1 -1
  118. package/dist/tsconfig.build.tsbuildinfo +1 -1
  119. package/docs/REQUEST-LIFECYCLE.md +39 -0
  120. package/migration-guides/11.27.6-to-11.27.7.md +374 -0
  121. package/migration-guides/11.27.7-to-11.28.0.md +350 -0
  122. package/package.json +6 -104
  123. package/src/core/common/decorators/restricted.decorator.ts +67 -20
  124. package/src/core/common/exceptions/access-denied.exception.ts +49 -0
  125. package/src/core/common/helpers/clone.helper.ts +110 -0
  126. package/src/core/common/helpers/db.helper.ts +14 -161
  127. package/src/core/common/helpers/id.helper.ts +198 -0
  128. package/src/core/common/helpers/input.helper.ts +21 -88
  129. package/src/core/common/helpers/service.helper.ts +6 -4
  130. package/src/core/common/inputs/combined-filter.input.ts +10 -57
  131. package/src/core/common/inputs/filter.input.ts +92 -1
  132. package/src/core/common/services/config.service.ts +4 -1
  133. package/src/core/modules/ai/core-ai.constants.ts +92 -0
  134. package/src/core/modules/ai/index.ts +1 -0
  135. package/src/core/modules/ai/interfaces/ai-interaction-record.interface.ts +34 -0
  136. package/src/core/modules/ai/services/core-ai-budget.service.ts +8 -4
  137. package/src/core/modules/ai/services/core-ai-connection-preference.service.ts +8 -4
  138. package/src/core/modules/ai/services/core-ai-connection.service.ts +5 -6
  139. package/src/core/modules/ai/services/core-ai-conversation.service.ts +5 -6
  140. package/src/core/modules/ai/services/core-ai-interaction.service.ts +6 -7
  141. package/src/core/modules/ai/services/core-ai-mode.service.ts +8 -2
  142. package/src/core/modules/ai/services/core-ai-prompt-hint.service.ts +7 -4
  143. package/src/core/modules/ai/services/core-ai-prompt.service.ts +8 -2
  144. package/src/core/modules/ai/services/core-ai-slot.service.ts +7 -4
  145. package/src/core/modules/ai/services/core-ai-tool-grant.service.ts +8 -4
  146. package/src/core/modules/ai/services/core-ai-tool-policy.service.ts +8 -4
  147. package/src/core/modules/ai/services/core-ai.service.ts +8 -11
  148. package/src/core/modules/auth/guards/roles.guard.ts +5 -4
  149. package/src/core/modules/better-auth/better-auth-roles.guard.ts +15 -7
  150. package/src/core/modules/better-auth/better-auth.config.ts +16 -0
  151. package/src/core/modules/better-auth/core-better-auth.constants.ts +73 -0
  152. package/src/core/modules/better-auth/core-better-auth.controller.ts +5 -6
  153. package/src/core/modules/better-auth/core-better-auth.module.ts +34 -8
  154. package/src/core/modules/better-auth/core-better-auth.registry.ts +53 -0
  155. package/src/core/modules/better-auth/core-better-auth.service.ts +19 -12
  156. package/src/core/modules/better-auth/index.ts +9 -0
  157. package/src/core/modules/tenant/core-tenant-member.model.ts +8 -3
  158. package/src/core/modules/tenant/core-tenant.guard.ts +26 -11
  159. package/src/core/modules/tus/tus.constants.ts +25 -0
  160. package/src/core/modules/tus/tus.module.ts +7 -2
  161. package/src/index.ts +1 -0
  162. 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
- export const AI_PROMPT_MODEL = 'AiPrompt';
14
- export const AI_PROMPT_CLASS = 'AI_PROMPT_CLASS';
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
- /** Mongoose injection token for the slot model. */
15
- export const AI_SLOT_MODEL = 'AiSlot';
14
+ import { AI_SLOT_CLASS, AI_SLOT_MODEL } from '../core-ai.constants';
16
15
 
17
- /** DI token for the slot model constructor. */
18
- export const AI_SLOT_CLASS = 'AI_SLOT_CLASS';
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
- /** Mongoose injection token. */
10
- export const AI_TOOL_GRANT_MODEL = 'AiToolGrant';
11
- /** DI token for the model constructor. */
12
- export const AI_TOOL_GRANT_CLASS = 'AI_TOOL_GRANT_CLASS';
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
- /** Mongoose injection token. */
10
- export const AI_TOOL_POLICY_MODEL = 'AiToolPolicy';
11
- /** DI token for the model constructor. */
12
- export const AI_TOOL_POLICY_CLASS = 'AI_TOOL_POLICY_CLASS';
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
- * Record passed to {@link CoreAiService.audit} for each prompt run.
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 interface AiInteractionRecord {
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 UnauthorizedException(ErrorCode.UNAUTHORIZED);
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 UnauthorizedException(ErrorCode.UNAUTHORIZED);
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 { CoreBetterAuthModule } from './core-better-auth.module';
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 CoreBetterAuthModule static reference.
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 CoreBetterAuthModule
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 = CoreBetterAuthModule.getTokenServiceInstance();
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 UnauthorizedException(ErrorCode.UNAUTHORIZED);
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
- if (
937
- error instanceof BadRequestException ||
938
- error instanceof UnauthorizedException ||
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 { BETTER_AUTH_CONFIG, BETTER_AUTH_COOKIE_DOMAIN, CoreBetterAuthService } from './core-better-auth.service';
40
+ import { CoreBetterAuthService } from './core-better-auth.service';
35
41
 
36
42
  /**
37
- * Token for injecting the better-auth instance
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 const BETTER_AUTH_INSTANCE = 'BETTER_AUTH_INSTANCE';
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) and BetterAuthRolesGuard
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 this.tokenServiceInstance;
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
- CoreBetterAuthModule.tokenServiceInstance = this.tokenService;
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.module';
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
- if (!user) throw new UnauthorizedException('Access to tenant membership denied');
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
- throw new UnauthorizedException('Access to tenant membership denied');
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
  }