@lenne.tech/nest-server 11.27.5 → 11.27.7

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 (147) hide show
  1. package/.claude/rules/architecture.md +66 -0
  2. package/.claude/rules/better-auth.md +88 -2
  3. package/.claude/rules/configurable-features.md +2 -2
  4. package/.claude/rules/package-management.md +23 -14
  5. package/.claude/rules/testing.md +27 -9
  6. package/CLAUDE.md +12 -4
  7. package/FRAMEWORK-API.md +1 -1
  8. package/bin/migrate.js +84 -25
  9. package/dist/core/common/decorators/restricted.decorator.d.ts +4 -4
  10. package/dist/core/common/decorators/restricted.decorator.js +21 -22
  11. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  12. package/dist/core/common/helpers/clone.helper.d.ts +6 -0
  13. package/dist/core/common/helpers/clone.helper.js +67 -0
  14. package/dist/core/common/helpers/clone.helper.js.map +1 -0
  15. package/dist/core/common/helpers/cookies.helper.d.ts +1 -0
  16. package/dist/core/common/helpers/cookies.helper.js +33 -16
  17. package/dist/core/common/helpers/cookies.helper.js.map +1 -1
  18. package/dist/core/common/helpers/db.helper.d.ts +2 -14
  19. package/dist/core/common/helpers/db.helper.js +13 -106
  20. package/dist/core/common/helpers/db.helper.js.map +1 -1
  21. package/dist/core/common/helpers/id.helper.d.ts +15 -0
  22. package/dist/core/common/helpers/id.helper.js +104 -0
  23. package/dist/core/common/helpers/id.helper.js.map +1 -0
  24. package/dist/core/common/helpers/input.helper.d.ts +1 -6
  25. package/dist/core/common/helpers/input.helper.js +11 -70
  26. package/dist/core/common/helpers/input.helper.js.map +1 -1
  27. package/dist/core/common/inputs/combined-filter.input.d.ts +1 -12
  28. package/dist/core/common/inputs/combined-filter.input.js +2 -51
  29. package/dist/core/common/inputs/combined-filter.input.js.map +1 -1
  30. package/dist/core/common/inputs/filter.input.d.ts +10 -1
  31. package/dist/core/common/inputs/filter.input.js +40 -5
  32. package/dist/core/common/inputs/filter.input.js.map +1 -1
  33. package/dist/core/common/services/config.service.js +5 -5
  34. package/dist/core/common/services/config.service.js.map +1 -1
  35. package/dist/core/modules/ai/core-ai.constants.d.ts +22 -0
  36. package/dist/core/modules/ai/core-ai.constants.js +26 -0
  37. package/dist/core/modules/ai/core-ai.constants.js.map +1 -0
  38. package/dist/core/modules/ai/index.d.ts +1 -0
  39. package/dist/core/modules/ai/index.js +1 -0
  40. package/dist/core/modules/ai/index.js.map +1 -1
  41. package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.d.ts +17 -0
  42. package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.js +3 -0
  43. package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.js.map +1 -0
  44. package/dist/core/modules/ai/services/core-ai-budget.service.d.ts +1 -2
  45. package/dist/core/modules/ai/services/core-ai-budget.service.js +7 -5
  46. package/dist/core/modules/ai/services/core-ai-budget.service.js.map +1 -1
  47. package/dist/core/modules/ai/services/core-ai-connection-preference.service.d.ts +1 -2
  48. package/dist/core/modules/ai/services/core-ai-connection-preference.service.js +7 -5
  49. package/dist/core/modules/ai/services/core-ai-connection-preference.service.js.map +1 -1
  50. package/dist/core/modules/ai/services/core-ai-connection.service.d.ts +1 -2
  51. package/dist/core/modules/ai/services/core-ai-connection.service.js +7 -5
  52. package/dist/core/modules/ai/services/core-ai-connection.service.js.map +1 -1
  53. package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +1 -2
  54. package/dist/core/modules/ai/services/core-ai-conversation.service.js +7 -5
  55. package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
  56. package/dist/core/modules/ai/services/core-ai-interaction.service.d.ts +2 -3
  57. package/dist/core/modules/ai/services/core-ai-interaction.service.js +7 -5
  58. package/dist/core/modules/ai/services/core-ai-interaction.service.js.map +1 -1
  59. package/dist/core/modules/ai/services/core-ai-mode.service.d.ts +1 -2
  60. package/dist/core/modules/ai/services/core-ai-mode.service.js +7 -5
  61. package/dist/core/modules/ai/services/core-ai-mode.service.js.map +1 -1
  62. package/dist/core/modules/ai/services/core-ai-prompt-hint.service.d.ts +1 -3
  63. package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js +7 -5
  64. package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js.map +1 -1
  65. package/dist/core/modules/ai/services/core-ai-prompt.service.d.ts +1 -2
  66. package/dist/core/modules/ai/services/core-ai-prompt.service.js +7 -5
  67. package/dist/core/modules/ai/services/core-ai-prompt.service.js.map +1 -1
  68. package/dist/core/modules/ai/services/core-ai-slot.service.d.ts +1 -2
  69. package/dist/core/modules/ai/services/core-ai-slot.service.js +7 -5
  70. package/dist/core/modules/ai/services/core-ai-slot.service.js.map +1 -1
  71. package/dist/core/modules/ai/services/core-ai-tool-grant.service.d.ts +1 -2
  72. package/dist/core/modules/ai/services/core-ai-tool-grant.service.js +7 -5
  73. package/dist/core/modules/ai/services/core-ai-tool-grant.service.js.map +1 -1
  74. package/dist/core/modules/ai/services/core-ai-tool-policy.service.d.ts +1 -2
  75. package/dist/core/modules/ai/services/core-ai-tool-policy.service.js +7 -5
  76. package/dist/core/modules/ai/services/core-ai-tool-policy.service.js.map +1 -1
  77. package/dist/core/modules/ai/services/core-ai.service.d.ts +2 -17
  78. package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
  79. package/dist/core/modules/better-auth/better-auth-roles.guard.js +2 -2
  80. package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
  81. package/dist/core/modules/better-auth/better-auth.config.js +12 -8
  82. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  83. package/dist/core/modules/better-auth/core-better-auth.constants.d.ts +3 -0
  84. package/dist/core/modules/better-auth/core-better-auth.constants.js +7 -0
  85. package/dist/core/modules/better-auth/core-better-auth.constants.js.map +1 -0
  86. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +1 -2
  87. package/dist/core/modules/better-auth/core-better-auth.module.js +23 -20
  88. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  89. package/dist/core/modules/better-auth/core-better-auth.registry.d.ts +4 -0
  90. package/dist/core/modules/better-auth/core-better-auth.registry.js +16 -0
  91. package/dist/core/modules/better-auth/core-better-auth.registry.js.map +1 -0
  92. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -2
  93. package/dist/core/modules/better-auth/core-better-auth.service.js +7 -6
  94. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  95. package/dist/core/modules/better-auth/index.d.ts +1 -0
  96. package/dist/core/modules/better-auth/index.js +1 -0
  97. package/dist/core/modules/better-auth/index.js.map +1 -1
  98. package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
  99. package/dist/core/modules/migrate/migration-runner.js +3 -2
  100. package/dist/core/modules/migrate/migration-runner.js.map +1 -1
  101. package/dist/core/modules/tus/tus.constants.d.ts +1 -0
  102. package/dist/core/modules/tus/tus.constants.js +5 -0
  103. package/dist/core/modules/tus/tus.constants.js.map +1 -0
  104. package/dist/core/modules/tus/tus.module.d.ts +1 -1
  105. package/dist/core/modules/tus/tus.module.js +8 -6
  106. package/dist/core/modules/tus/tus.module.js.map +1 -1
  107. package/dist/tsconfig.build.tsbuildinfo +1 -1
  108. package/docs/REQUEST-LIFECYCLE.md +17 -4
  109. package/migration-guides/11.27.5-to-11.27.6.md +359 -0
  110. package/migration-guides/11.27.6-to-11.27.7.md +374 -0
  111. package/package.json +11 -104
  112. package/src/core/common/decorators/restricted.decorator.ts +33 -8
  113. package/src/core/common/helpers/clone.helper.ts +110 -0
  114. package/src/core/common/helpers/cookies.helper.ts +101 -26
  115. package/src/core/common/helpers/db.helper.ts +14 -161
  116. package/src/core/common/helpers/id.helper.ts +198 -0
  117. package/src/core/common/helpers/input.helper.ts +12 -84
  118. package/src/core/common/inputs/combined-filter.input.ts +10 -57
  119. package/src/core/common/inputs/filter.input.ts +92 -1
  120. package/src/core/common/interfaces/server-options.interface.ts +35 -6
  121. package/src/core/common/services/config.service.ts +4 -1
  122. package/src/core/modules/ai/core-ai.constants.ts +92 -0
  123. package/src/core/modules/ai/index.ts +1 -0
  124. package/src/core/modules/ai/interfaces/ai-interaction-record.interface.ts +34 -0
  125. package/src/core/modules/ai/services/core-ai-budget.service.ts +8 -4
  126. package/src/core/modules/ai/services/core-ai-connection-preference.service.ts +8 -4
  127. package/src/core/modules/ai/services/core-ai-connection.service.ts +5 -6
  128. package/src/core/modules/ai/services/core-ai-conversation.service.ts +5 -6
  129. package/src/core/modules/ai/services/core-ai-interaction.service.ts +6 -7
  130. package/src/core/modules/ai/services/core-ai-mode.service.ts +8 -2
  131. package/src/core/modules/ai/services/core-ai-prompt-hint.service.ts +7 -4
  132. package/src/core/modules/ai/services/core-ai-prompt.service.ts +8 -2
  133. package/src/core/modules/ai/services/core-ai-slot.service.ts +7 -4
  134. package/src/core/modules/ai/services/core-ai-tool-grant.service.ts +8 -4
  135. package/src/core/modules/ai/services/core-ai-tool-policy.service.ts +8 -4
  136. package/src/core/modules/ai/services/core-ai.service.ts +8 -11
  137. package/src/core/modules/better-auth/README.md +10 -4
  138. package/src/core/modules/better-auth/better-auth-roles.guard.ts +11 -5
  139. package/src/core/modules/better-auth/better-auth.config.ts +90 -15
  140. package/src/core/modules/better-auth/core-better-auth.constants.ts +73 -0
  141. package/src/core/modules/better-auth/core-better-auth.module.ts +34 -8
  142. package/src/core/modules/better-auth/core-better-auth.registry.ts +53 -0
  143. package/src/core/modules/better-auth/core-better-auth.service.ts +19 -12
  144. package/src/core/modules/better-auth/index.ts +9 -0
  145. package/src/core/modules/migrate/migration-runner.ts +16 -2
  146. package/src/core/modules/tus/tus.constants.ts +25 -0
  147. package/src/core/modules/tus/tus.module.ts +7 -2
@@ -60,6 +60,72 @@ Key areas: JWT, MongoDB, GraphQL, email, security, static assets
60
60
  - `CheckResponseInterceptor` - Filters restricted fields
61
61
  - `CheckSecurityInterceptor` - Processes `securityCheck()` methods
62
62
 
63
+ ## DI Token Placement (SWC-Safe)
64
+
65
+ **Rule: DI tokens belong in an import-free leaf file (`*.constants.ts` / `*.enums.ts`) — never in `*.module.ts` or `*.service.ts`.**
66
+
67
+ A token declared in a module that the service imports (or vice versa) makes the two files import each other. That cycle compiles fine under tsc, but `@Inject(TOKEN)` is a constructor-parameter decorator evaluated at **class-definition time** — so on the cycle it reads a `const` that is still in its temporal dead zone. Under SWC → CommonJS (`nest start -b swc`) the app dies at startup:
68
+
69
+ ```
70
+ ReferenceError: Cannot access 'BETTER_AUTH_INSTANCE' before initialization
71
+ ```
72
+
73
+ **This is invisible to `tsc`, to `pnpm test` (vitest runs SWC through Vite's cycle-tolerant module runner) and to `oxlint` (which has no `import/no-cycle` rule).** It is caught only by `pnpm run check:swc-tdz`.
74
+
75
+ ### The general rule: a cycle is fatal only when dereferenced at evaluation time
76
+
77
+ The cycle alone is survivable. What kills it is **reading a TDZ-subject binding (`const` / `class` / `let`) while the module is still initializing**:
78
+
79
+ | Deref location | Evaluated | Danger |
80
+ |----------------|-----------|--------|
81
+ | Decorator argument (`@UnifiedField({ type: X })`, `@Inject(TOKEN)`) | class-definition time | ☠️ **fatal on a cycle** |
82
+ | `design:type` / `design:paramtypes` metadata (from `emitDecoratorMetadata`) | class-definition time | ☠️ **fatal on a cycle** — and userland cannot make it lazy |
83
+ | Static / class field initializer | class-definition time | ☠️ **fatal on a cycle** |
84
+ | Top-level `const alias = X` | module-evaluation time | ☠️ **fatal on a cycle** |
85
+ | Inside a function or method body | call time | ✅ safe (both modules are done by then) |
86
+ | `export function` declaration | hoisted | ✅ TDZ-immune — prefer over `const` arrows on cycle-adjacent files |
87
+ | `import type` | erased | ✅ not a runtime edge at all |
88
+
89
+ **A lazy thunk is often NOT enough.** `type: () => X` defers the decorator argument, but `emitDecoratorMetadata` still emits an eager `design:type` for the property, and SWC's `typeof` guard does not protect the member expression it compiles to. To be safe you must remove the **import edge** — merge the modules, or extract the shared binding into a leaf.
90
+
91
+ ### `check:swc-tdz` loads every module as its own entry point
92
+
93
+ Whether such a cycle throws depends on **which module the graph is entered through**. A barrel-only check is not enough: `filter.input` ↔ `combined-filter.input` crashed on a direct `require()` of `combined-filter.input` while the barrel loaded green, because the barrel happened to pull `filter.input` in first. So the guard requires each compiled file separately (`scripts/check-swc-tdz.mjs`).
94
+
95
+ ### Status per module
96
+
97
+ Repo-wide cycles went from **10 → 5**, and **every DI token in `src/core/` now lives in an import-free leaf**. The five that remain are, per an SWC-emit audit, **not runtime cycles at all** — type-only imports that madge reports but both compilers erase (their emits are empty).
98
+
99
+ Both invariants are enforced by `tests/unit/import-cycle-invariants.spec.ts`, which fails if a token reappears in a `*.module.ts` / `*.service.ts` or if a leaf grows an import. That matters, because the guard below catches the *crash*, not the *disarming* of a safety property — those are different things, and only the second one is silent.
100
+
101
+ | Module | Token / type leaf |
102
+ |--------|-------------------|
103
+ | `better-auth` | `core-better-auth.constants.ts` (tokens) + `core-better-auth.registry.ts` (static service refs, `import type` only, deliberately **not** barrel-exported) |
104
+ | `ai` | `core-ai.constants.ts` (all 22 `AI_*` tokens) + `interfaces/ai-interaction-record.interface.ts` |
105
+ | `tus` | `tus.constants.ts` (`TUS_CONFIG`) |
106
+ | `tenant` | `core-tenant.enums.ts` |
107
+ | `auth` | `interfaces/auth-provider.interface.ts` |
108
+ | `common/helpers` | `id.helper.ts` (ID cluster, out of `db.helper`) + `clone.helper.ts` (`clone`/`deepFreeze`, out of `input.helper`) |
109
+ | `common/inputs` | `FilterInput` + `CombinedFilterInput` merged into `filter.input.ts` — declaration order is load-bearing |
110
+ | `common/decorators` | `restricted.decorator` is on **zero** cycles; its exports are hoisted `function` declarations (TDZ-immune) as defense in depth |
111
+
112
+ Every old location re-exports what it lost, so no import path broke — the public API is byte-identical (472 exports, verified by diffing both versions).
113
+
114
+ ### What each of these actually was
115
+
116
+ Worth knowing, because the pattern repeats:
117
+
118
+ - **`CombinedFilterInput` was already crashing.** `require('.../combined-filter.input.js')` threw. It stayed hidden because the barrel happens to pull `filter.input` in first — a deep import or a reordering of `src/index.ts` would have surfaced it. A lazy thunk does **not** fix that shape: `emitDecoratorMetadata` still emits an eager `design:type`.
119
+ - **`restricted.decorator` sat on two cycles at once**, in the file that drives field-level access control. Removing the `db.helper` edge felt like the fix and left the `config.service` one fully intact. **Removing one edge is not removing the cycle** — re-run madge and confirm the file appears in *zero* cycles.
120
+ - **The AI interaction record was held apart by one keyword.** `import type` erases the edge; an IDE "organize imports" widening it to a value import would have armed a `design:paramtypes` deref, silently.
121
+ - **`TUS_CONFIG` and the 22 `AI_*` tokens** never crashed — their graphs happened to be acyclic. Each was one back-import away, with nothing watching.
122
+
123
+ ### The lesson that cost the most time
124
+
125
+ Removing one edge is not the same as removing the cycle. `restricted.decorator` was on **two** cycles through different paths; extracting the ID helpers out of `db.helper` felt like the fix and left the second one (via `config.service`) fully intact — with `madge` happily reporting the file as still cyclic. Always re-run `npx madge --circular --extensions ts src/` after an extraction and check the file appears in **zero** cycles, rather than assuming the edge you removed was the only one.
126
+
127
+ Full background, failure analysis and the mistakes table: `.claude/rules/better-auth.md` §6.
128
+
63
129
  ## Model Inheritance
64
130
 
65
131
  - `CorePersistenceModel` - Base for database entities
@@ -199,6 +199,8 @@ The BetterAuth module provides two RolesGuard implementations:
199
199
  **Solution:** `BetterAuthRolesGuard` with NO constructor dependencies:
200
200
 
201
201
  ```typescript
202
+ import { getBetterAuthTokenService } from './core-better-auth.registry';
203
+
202
204
  @Injectable()
203
205
  export class BetterAuthRolesGuard implements CanActivate {
204
206
  // NO constructor dependencies - avoids mixin DI conflict
@@ -207,14 +209,19 @@ export class BetterAuthRolesGuard implements CanActivate {
207
209
  // Use Reflect.getMetadata directly (not NestJS Reflector)
208
210
  const roles = Reflect.getMetadata('roles', context.getHandler());
209
211
 
210
- // Access services via static module reference
211
- const tokenService = CoreBetterAuthModule.getTokenServiceInstance();
212
+ // Read the token service from the registry LEAF — never from CoreBetterAuthModule.
213
+ // Importing the module here re-creates the guard <-> module import cycle (see §6).
214
+ const tokenService = getBetterAuthTokenService();
212
215
 
213
216
  // ... role checking logic identical to RolesGuard
214
217
  }
215
218
  }
216
219
  ```
217
220
 
221
+ > The guard must NOT do `CoreBetterAuthModule.getTokenServiceInstance()`. That static accessor is
222
+ > still public API and still works — but calling it *from the guard* means importing the module,
223
+ > which is exactly the cycle §6 exists to prevent. Everywhere else, the static accessor is fine.
224
+
218
225
  ### Guard Selection Logic
219
226
 
220
227
  In `CoreBetterAuthModule.createDeferredModule()`:
@@ -250,6 +257,84 @@ Both guards implement identical security logic:
250
257
  2. **New system roles** → Add to BOTH guards
251
258
  3. **Token verification changes** → Update `BetterAuthTokenService` (shared by both)
252
259
  4. **Testing** → Test both Legacy Mode and IAM-Only Mode
260
+ 5. **Reaching services from `BetterAuthRolesGuard`** → go through `core-better-auth.registry.ts`,
261
+ **never** through `CoreBetterAuthModule` (see §6 — importing the module from the guard
262
+ re-creates an import cycle)
263
+
264
+ ## 6. DI Token Placement (SWC-Safe)
265
+
266
+ ### The rule
267
+
268
+ **DI tokens and static service references belong in an import-free leaf file — never in
269
+ `*.module.ts` or `*.service.ts`.**
270
+
271
+ | File | Contents | Imports |
272
+ |------|----------|---------|
273
+ | `core-better-auth.constants.ts` | `BETTER_AUTH_INSTANCE`, `BETTER_AUTH_CONFIG`, `BETTER_AUTH_COOKIE_DOMAIN` | **none** |
274
+ | `core-better-auth.registry.ts` | `BetterAuthTokenService` reference for `BetterAuthRolesGuard` | **`import type` only** (erased by tsc and SWC) |
275
+
276
+ ### The problem
277
+
278
+ The tokens used to live in the module (`BETTER_AUTH_INSTANCE`) and the service (`BETTER_AUTH_CONFIG`,
279
+ `BETTER_AUTH_COOKIE_DOMAIN`), so module and service imported each other.
280
+
281
+ **Error (only under `nest start -b swc` / `nest build -b swc`):**
282
+
283
+ ```
284
+ ReferenceError: Cannot access 'BETTER_AUTH_INSTANCE' before initialization
285
+ ```
286
+
287
+ The lethal ingredient is **not the cycle by itself** — it is a cycle **plus a read of the cyclic
288
+ binding at module-evaluation time**. `@Inject(BETTER_AUTH_INSTANCE)` is a constructor-parameter
289
+ decorator, and decorator arguments are evaluated when the class is *defined*, i.e. while the module
290
+ is still initializing. On a cycle, the importing side then reads a `const` that is still in its
291
+ temporal dead zone.
292
+
293
+ This is why the same cycle is harmless when both sides only dereference each other **inside method
294
+ bodies** (deferred to call time) — and why such a cycle is nonetheless a loaded gun: hoisting a
295
+ lazy lookup into a static field, or adding a typed constructor parameter (which emits
296
+ `design:paramtypes` at top level), weaponizes it instantly.
297
+
298
+ ### The solution
299
+
300
+ ```typescript
301
+ // core-better-auth.constants.ts — imports NOTHING
302
+ export const BETTER_AUTH_INSTANCE = 'BETTER_AUTH_INSTANCE';
303
+
304
+ // core-better-auth.module.ts AND core-better-auth.service.ts
305
+ import { BETTER_AUTH_INSTANCE } from './core-better-auth.constants';
306
+ ```
307
+
308
+ A file with zero imports can never be mid-evaluation when someone imports it — in any module
309
+ system, under any compiler.
310
+
311
+ ### Why you cannot rely on the test suite here
312
+
313
+ This bug class is **invisible** to everything except one specific step:
314
+
315
+ | Tool | Sees it? | Why |
316
+ |------|:--------:|-----|
317
+ | `tsc` / `pnpm run build` | ❌ | Compiles the cycle without complaint |
318
+ | `pnpm test` (vitest) | ❌ | vitest runs SWC through **Vite's module runner**, whose getter-based live bindings tolerate cycles |
319
+ | `oxlint` | ❌ | oxlint does **not** implement `import/no-cycle` |
320
+ | `pnpm run check:swc-tdz` | ✅ | SWC → CommonJS → `require()`: the exact path consumers hit |
321
+ | `tests/unit/better-auth-di-tokens.spec.ts` | ✅ | Asserts the leaf invariant structurally |
322
+
323
+ The bug shipped to `develop` with a fully green CI. Treat a green `pnpm test` as **no evidence** on
324
+ this question.
325
+
326
+ ### Common mistakes
327
+
328
+ | Mistake | Symptom | Fix |
329
+ |---------|---------|-----|
330
+ | Importing a token from `./core-better-auth.module` or `./core-better-auth.service` inside the better-auth module | Green tsc + green tests; `ReferenceError` for consumers on SWC | Import from `./core-better-auth.constants` |
331
+ | Adding any runtime `import` to `core-better-auth.constants.ts` | `better-auth-di-tokens.spec.ts` fails | Keep it a leaf; move whatever you needed elsewhere |
332
+ | Importing `CoreBetterAuthModule` into `better-auth-roles.guard.ts` | Re-creates the guard ↔ module cycle | Use `getBetterAuthTokenService()` from `./core-better-auth.registry` |
333
+ | Hoisting `getBetterAuthTokenService()` into a static field / class property initializer | Evaluation-time deref → TDZ crash returns | Keep the lookup inside the method body |
334
+
335
+ > The backward-compat re-exports in `core-better-auth.module.ts` / `core-better-auth.service.ts` are
336
+ > marked `@deprecated` and exist **only** for external deep importers. Never use them from inside
337
+ > the module — that is precisely the path that re-creates the cycle.
253
338
 
254
339
  ## Summary
255
340
 
@@ -260,3 +345,4 @@ Both guards implement identical security logic:
260
345
  | Testing | Full coverage, all tests pass, security tests included |
261
346
  | Customization | Use correct registration pattern, re-declare Resolver decorators |
262
347
  | Guards | Maintain both RolesGuard and BetterAuthRolesGuard in sync |
348
+ | DI Tokens | Import-free leaf file only — never in `*.module.ts` / `*.service.ts` (§6) |
@@ -216,8 +216,8 @@ This pattern is currently applied to:
216
216
  | BetterAuth Tenant Skip | `betterAuth.skipTenantCheck` | Explicit Boolean | `true` (default). When `true` and no `X-Tenant-Id` header is sent, IAM endpoints (controller + resolver) skip `CoreTenantGuard` tenant validation. When header IS present, normal membership validation runs regardless. Set `false` for tenant-aware auth scenarios (subdomain-based, invite links, SSO per tenant) |
217
217
  | Debug Process Input | `debugProcessInput` | Explicit Boolean | `false` (default). When `true`, logs a debug message when `prepareInput()` changes the input type during `process()`. Has performance cost due to `JSON.stringify` on every `process()` call — enable only for debugging |
218
218
  | JSONTransport Production Guard | `email.smtp` with `jsonTransport` | Runtime Guard | Throws `Error` when `email.smtp` has a truthy `jsonTransport` property in `production` or `staging` environments (read from config `env` field). JSONTransport silently discards all outgoing mail — the guard prevents accidental misconfiguration that causes password-reset, 2FA, and verification emails to vanish. Use `{ jsonTransport: true }` only in CI/e2e/local environments |
219
- | Cookies | `cookies` | Boolean Shorthand (default true) | `true` (enabled), `exposeTokenInBody: false`. When enabled: loads `cookie-parser`, sets CORS `credentials: true`, sets signed httpOnly session cookies. When `exposeTokenInBody: true`: token stays in response body alongside cookies (for hybrid JWT+Cookie auth). JWT via `Authorization: Bearer` always works independently |
220
- | CORS | `cors` | Boolean Shorthand | `enabled: true`, `allowAll: false`, `deriveAppUrl: true`. Origins come from `appUrl`/`baseUrl`, resolved by the shared `resolveServerUrls()` helper (`cookies.helper.ts`) that ALL three CORS layers use (GraphQL, REST, BetterAuth `trustedOrigins`) — they can no longer drift. `appUrl` resolution: explicit → localhost default (`http://localhost:3001`, only for `env: local`/`ci`/`e2e` with a localhost `baseUrl`) → derived from `baseUrl` by stripping a leading `api.` label (`https://api.example.com` → `https://example.com`). **Security:** the derived origin receives credentialed CORS; set `deriveAppUrl: false` when the apex domain is not trusted, then list the frontend origin via `appUrl`/`allowedOrigins`. The derivation never yields a bare TLD (`https://api.dev` unchanged) and never emits the opaque `null` origin (non-http(s) `baseUrl` passes through verbatim). `allowAll: true` mirrors request origin. `enabled: false` disables CORS on all layers including BetterAuth. Explicit `betterAuth.trustedOrigins` always takes precedence |
219
+ | Cookies | `cookies` | Boolean Shorthand (default true) | `true` (enabled), `exposeTokenInBody: false`. When enabled: loads `cookie-parser`, sets CORS `credentials: true`, sets signed httpOnly session cookies. When `exposeTokenInBody: true`: token stays in response body alongside cookies (for hybrid JWT+Cookie auth). JWT via `Authorization: Bearer` always works independently. **BetterAuth cookie name (since v11.27.6):** `createBetterAuthInstance()` pins `advanced.useSecureCookies: false` so BetterAuth's native handlers read the same UNPREFIXED `<cookiePrefix>.session_token` the helper writes (fixes a `401` split-brain on 2FA/passkey/`/token`); the `Secure` attribute is still applied on an `https://` baseURL via `advanced.defaultCookieAttributes`. Opt back into the `__Secure-` prefix with `betterAuth.options.advanced.useSecureCookies: true` only when BetterAuth manages cookies entirely |
220
+ | CORS | `cors` | Boolean Shorthand | `enabled: true`, `allowAll: false`, `deriveAppUrl: true`. Origins come from `appUrl`/`baseUrl`, resolved by the shared `resolveServerUrls()` helper (`cookies.helper.ts`) that ALL three CORS layers use (GraphQL, REST, BetterAuth `trustedOrigins`) — they can no longer drift. `appUrl` resolution: explicit → derived from a **host-split** localhost `baseUrl` (its `api.` label strips to a sibling host: `https://api.crm.localhost` → `https://crm.localhost`, as served by `lt dev up`; the port is preserved) → localhost default (`http://localhost:3001`, only for `env: local`/`ci`/`e2e` with a **port-split** localhost `baseUrl` — one host, API `:3000`, app `:3001`; `https://api.localhost` strips to the bare `localhost` the API already answers on and is therefore a port split, not a host split) → derived from `baseUrl` by stripping a leading `api.` label (`https://api.example.com` → `https://example.com`). **Security:** the derived origin receives credentialed CORS; set `deriveAppUrl: false` when the apex domain is not trusted, then list the frontend origin via `appUrl`/`allowedOrigins` (a host-split localhost `baseUrl` then falls back to the localhost default). The derivation never yields a bare TLD (`https://api.dev` unchanged) and never emits the opaque `null` origin (non-http(s) `baseUrl` passes through verbatim). `allowAll: true` mirrors the request origin for REST/GraphQL, but BetterAuth's `trustedOrigins` still resolve to `[appUrl]` (+ passkey origins) — an origin check has no "allow everything" mode, so a separately hosted frontend must appear in `appUrl`/`allowedOrigins` (or set `betterAuth.trustedOrigins` explicitly). `enabled: false` disables CORS on all layers including BetterAuth (`trustedOrigins: []`, which still trusts BetterAuth's own `baseURL`). Explicit `betterAuth.trustedOrigins` always takes precedence |
221
221
  | AI Assistant | `ai` | Presence Implies Enabled | Core: `maxIterations: 5`, `defaultMode: 'auto'` (or `'plan'`), `rateLimit` (presence implies enabled: `max: 20`, `windowSeconds: 60`), `systemPrompt`, `documentation` (injected into the system prompt), `encryptionSecret`. **DB-backed LLM connections** (`aiConnections`, admin CRUD) with AES-256-GCM-encrypted API keys (`AiCryptoService`, secret from `ai.encryptionSecret` / `NSC__AI__ENCRYPTION_SECRET` / `SECRETS_ENCRYPTION_KEY`; `apiKeyEncrypted` is a global `secretFields` entry, never returned — only `hasApiKey`); optional `defaultConnection` one-time seed. **Provider abstraction** (`ILlmProvider`, default `OpenAiCompatibleProvider` for any OpenAI-compatible endpoint via `fetch`; per-connection `supportsNativeTools`/`supportsJsonResponse` capabilities, emulated tool calling when native tools are unavailable). **Tool registry** (`AiToolRegistry`, tools self-register, role-filtered; tools may be `mutating`/`destructive` and define `authorize()` for pre-flight data-level checks). **Plan mode** (`input.mode: 'plan'`): full plan → pre-flight authorize ALL steps → all-or-nothing execution with a translated (de/en) error when any step is not permitted. **Confirmation policy**: `confirmation.mutating: { default, enforced }` + client `input.requireConfirmation` (ignored when enforced); `destructive` always confirms. **Client metadata** (`input.metadata`: URL/nav/console logs, untrusted+capped). **Multi-turn conversations** (`aiConversations`, owner-scoped). **SSE streaming** (`POST /ai/stream`). **Audit** (`audit: false` → persist to `aiInteractions`, admin-readable). **Token budgets** (`budget: { period: 'day'|'month'|'none', user: { maxTokens?, maxPrompts? }, tenant: { maxTokens?, maxPrompts? } }`, requires audit): per-user AND per-tenant limits with config defaults; admins override per user/tenant at runtime (`aiBudgetLimits`, `CoreAiBudgetService`). Resolution: override → default → unlimited (missing/0 = unlimited). Enforced before the run (HTTP 429 + translated). Each response carries a compact `budget` summary (promptTokens, usedTokens, remainingTokens, resetAt); full breakdown via `aiUsage` query / `GET /ai/usage`. **Self-optimizing prompts**: the system prompt is assembled from keyed fragments (`CoreAiPromptBuilderService` ships built-in defaults; works with zero rows). Admin-editable overrides per slot (`aiSlots`, admin CRUD, `/ai/slots`) scoped by `key`/`locale`/`capability`/`tenantId`, with tenant override/reset semantics and placeholder tokens resolved at run time via the placeholder registry. **Governed learning loop** (`promptLearning: { enabled: true, autoApply: false }`): tool errors record `suggested` hints (`aiPromptHints`, admin CRUD, `/ai/prompt-hints`) that only reach the prompt once admin-approved (or auto-approved when `autoApply`); hints only ADD guidance, never relax permissions. **Context window** (`contextWindow`, default 8192; auto-detected per connection via `ILlmProvider.detectContextWindow()` — Ollama `/api/show` probe / known-model table / Claude alias — and persisted): per-user/session history is trimmed (oldest non-system turns dropped, last truncated) and tool-results capped to `maxToolResultChars` (default 12000) so a session never overflows the model. **MCP server** (`mcp: false` → `/ai/mcp` Streamable HTTP, Bearer auth, lazy `@modelcontextprotocol/sdk`; `mcp: { oauth: true, oauthSecret }` adds OAuth 2.1 — HMAC tokens + PKCE S256 + dynamic registration via `mountAiMcpOAuth(app)` in main.ts). Overrides via `CoreModule.forRoot(env, { ai: { budgetService, connectionResolver, connectionService, controller, conversationService, interactionService, mcpClientService, modeService, placeholderRegistry, preferenceService, promptBuilder, promptHintService, promptService, resolver, service, slotService, toolGrantService, toolPolicyService } })` |
222
222
 
223
223
  ## Module Override Pattern (via `ICoreModuleOverrides`)
@@ -97,9 +97,23 @@ peerDependencies may use ranges when necessary for compatibility with consuming
97
97
 
98
98
  The `pnpm-lock.yaml` file must always be committed. It provides additional reproducibility even if someone accidentally introduces a version range.
99
99
 
100
+ ## Package Manager: pnpm 11
101
+
102
+ This repo is pinned to **pnpm 11** via the `packageManager` field (corepack/`pnpm/action-setup` follow it, so CI and Docker use it automatically — no version is hardcoded anywhere else).
103
+
104
+ pnpm 11 **no longer reads the `pnpm` field in `package.json`**, and `.npmrc` is auth/registry only. All pnpm-specific settings live in **`pnpm-workspace.yaml`**:
105
+
106
+ - `overrides:` — the security overrides (see below)
107
+ - `allowBuilds:` — a **map** of `pkg: true|false` classifying every package that has an install script (canonical v11 form; replaces `onlyBuiltDependencies`). Native builds we need are `true` (`bcrypt`, `@swc/core`, …); telemetry like `@scarf/scarf` is `false`. **Every build-script package must be classified**, or `pnpm install` exits non-zero with `ERR_PNPM_IGNORED_BUILDS` and appends a broken stub.
108
+ - `nodeLinker`, `autoInstallPeers`, `strictPeerDependencies`, `peerDependencyRules` — moved here from `.npmrc` (camelCase).
109
+
110
+ `pnpm audit`: pnpm 10.x is broken (npm retired the legacy audit endpoint → HTTP 410); pnpm 11 uses the working bulk-advisory endpoint. `scripts/check.mjs` degrades the retired-endpoint failure to a non-blocking warning as a safety net, so `check` stays green + honest even if a future endpoint change lands.
111
+
100
112
  ## Overrides
101
113
 
102
- Package overrides are configured in the `pnpm.overrides` section of `package.json` and are typically used to force transitive dependencies to a security-patched version.
114
+ Package overrides live in the `overrides:` section of **`pnpm-workspace.yaml`** (they moved out of `package.json`'s `pnpm.overrides` in the pnpm 11 upgrade). They force transitive dependencies to a security-patched version.
115
+
116
+ **Keep the set minimal.** On the pnpm 11 upgrade the list was pruned from 36 to the 9 still load-bearing — an override is only necessary if removing it lets the package resolve back INTO its vulnerable range (verify with a with/without lockfile diff; `pnpm audit` is the arbiter). Each surviving entry carries its CVE rationale as a comment. Remove an entry once its parent dependency ships a fixed version.
103
117
 
104
118
  ### Rule: Override Targets MUST Be Fixed Versions
105
119
 
@@ -115,19 +129,14 @@ The **target** of an override (the value on the right-hand side) MUST be a fixed
115
129
 
116
130
  The **key** (left-hand side) of an override entry selects which installed versions the override applies to. Both forms are valid:
117
131
 
118
- ```json
119
- {
120
- "pnpm": {
121
- "overrides": {
122
- // Form 1: Replace ALL versions of a package with a fixed one
123
- "lodash": "4.17.23",
124
-
125
- // Form 2: Replace only vulnerable versions with a fixed patched one
126
- "minimatch@<3.1.4": "3.1.4",
127
- "path-to-regexp@>=8.0.0 <8.4.0": "8.4.2"
128
- }
129
- }
130
- }
132
+ ```yaml
133
+ # pnpm-workspace.yaml
134
+ overrides:
135
+ # Form 1: Replace ALL versions of a package with a fixed one
136
+ 'lodash': '4.17.23'
137
+ # Form 2: Replace only vulnerable versions with a fixed patched one
138
+ 'minimatch@<3.1.4': '3.1.4'
139
+ 'path-to-regexp@>=8.0.0 <8.4.0': '8.4.2'
131
140
  ```
132
141
 
133
142
  Form 2 is preferred for security-driven overrides because it leaves non-vulnerable versions untouched, which reduces the blast radius of the override.
@@ -2,20 +2,34 @@
2
2
 
3
3
  ## Test Framework
4
4
 
5
- - **Vitest** - Primary test framework (migrated from Jest)
6
- - Configuration: `vitest.config.ts`
7
- - Test files: `tests/` directory with `.e2e-spec.ts` suffix
5
+ **Vitest**, split across two runners. Which one claims a file is decided purely by its filename:
6
+
7
+ | Runner | Config | Test files | Needs MongoDB |
8
+ |--------|--------|-----------|:-------------:|
9
+ | Unit | `vitest.config.ts` | `src/**/*.spec.ts`, `tests/unit/**/*.spec.ts` | No |
10
+ | E2E | `vitest-e2e.config.ts` | `tests/**/*.e2e-spec.ts`, `tests/stories/**/*.story.test.ts` | Yes |
11
+
12
+ A file matching neither pattern would run nowhere. `tests/unit/test-file-routing.spec.ts` asserts
13
+ that every `*.spec.ts` / `*.test.ts` in the repo is claimed by **exactly one** runner, so a
14
+ mis-named suite fails the build instead of silently passing. Type-only tests
15
+ (`tests/types/*.type-test.ts`) are compiled by `pnpm run test:types`, never executed.
8
16
 
9
17
  ## Running Tests
10
18
 
11
19
  ```bash
12
- # Run all E2E tests (default)
20
+ # Unit + E2E (default) — the E2E half needs MongoDB
13
21
  pnpm test
14
22
 
15
- # Run with coverage
23
+ # Unit tests only (fast, no MongoDB)
24
+ pnpm run vitest:unit
25
+
26
+ # E2E tests only
27
+ pnpm run test:e2e
28
+
29
+ # Both suites with coverage → coverage/unit + coverage/e2e
16
30
  pnpm run test:cov
17
31
 
18
- # Run in CI mode
32
+ # Run in CI mode (unit + E2E with NODE_ENV=ci)
19
33
  pnpm run test:ci
20
34
 
21
35
  # Debug open handles
@@ -27,11 +41,15 @@ pnpm run test:cleanup
27
41
 
28
42
  ## Test Environment
29
43
 
30
- - Environment: `NODE_ENV=e2e` (via `pnpm test` → `vitest-e2e.config.ts`)
31
- - Database: **one unique database per run** (`nest-server-e2e-run-<ts>-p<pid>`), created by `tests/global-setup.ts` so concurrent runs cannot interfere with each other
44
+ - Environment: `NODE_ENV=e2e` for the E2E runner (`pnpm run vitest`). The unit runner sets no
45
+ `NODE_ENV`; vitest defaults it to `test`, and `getEnvironmentConfig()` falls back to `config.local`.
46
+ - Both runners load `tests/setup.ts` via `setupFiles` (Nest Logger restricted to `error`/`fatal`,
47
+ `@UnifiedField` deprecation warnings filtered).
48
+ - Database (E2E only): **one unique database per run** (`nest-server-e2e-run-<ts>-p<pid>`), created by `tests/global-setup.ts` so concurrent runs cannot interfere with each other
32
49
  - DB lifecycle (`tests/db-lifecycle.reporter.ts`): run passes → DB dropped immediately + stale run DBs from crashed/failed runs collected; run fails → DB kept for debugging, removed by the next successful run. An externally set `MONGODB_URI` (CI) opts out of the scheme.
33
50
  - Test helper: `src/test/test.helper.ts`
34
- - Coverage: Collected from `src/**/*.{ts,js}`
51
+ - Coverage: Collected from `src/**/*.{ts,js}`. The two runners are separate vitest processes, so
52
+ they write separate reports (`coverage/unit`, `coverage/e2e`) rather than overwriting each other.
35
53
 
36
54
  ## Test Best Practices
37
55
 
package/CLAUDE.md CHANGED
@@ -106,15 +106,23 @@ pnpm start # Start in local mode
106
106
  pnpm run start:dev # Development mode with watch
107
107
 
108
108
  # Testing (ALWAYS run before completing changes)
109
- pnpm test # Run E2E tests (Vitest)
110
- pnpm run test:cov # With coverage
109
+ pnpm test # Unit + E2E tests (Vitest); E2E half needs MongoDB
110
+ pnpm run vitest:unit # Unit tests only (fast, no MongoDB)
111
+ pnpm run test:e2e # E2E tests only
112
+ pnpm run test:cov # Both suites with coverage (coverage/unit + coverage/e2e)
111
113
  npx vitest run --config vitest-e2e.config.ts --reporter=hanging-process # Debug open handles
112
114
  pnpm run test:cleanup # Remove leftover test artifacts (.txt, .bin)
113
115
 
114
116
  # Linting & Formatting
115
- pnpm run lint # ESLint check
117
+ pnpm run lint # oxlint check
116
118
  pnpm run lint:fix # Auto-fix
117
- pnpm run format # Prettier format
119
+ pnpm run format # oxfmt format
120
+
121
+ # Import-cycle / SWC safety (part of `check`)
122
+ pnpm run check:swc-tdz # SWC→CJS build, loads EVERY module as its own entry point.
123
+ # The only step that catches a temporal-dead-zone crash from an
124
+ # import cycle — tsc, vitest and oxlint are all blind to it.
125
+ # See .claude/rules/architecture.md → "DI Token Placement (SWC-Safe)"
118
126
 
119
127
  # Package Development
120
128
  pnpm run build:dev # Build for local development (use with pnpm link)
package/FRAMEWORK-API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @lenne.tech/nest-server — Framework API Reference
2
2
 
3
- > Auto-generated from source code on 2026-07-10 (v11.27.5)
3
+ > Auto-generated from source code on 2026-07-15 (v11.27.7)
4
4
  > File: `FRAMEWORK-API.md` — compact, machine-readable API surface for Claude Code
5
5
 
6
6
  ## CoreModule.forRoot()
package/bin/migrate.js CHANGED
@@ -5,36 +5,95 @@
5
5
  *
6
6
  * This is a shim that makes the nest-server migration CLI available as "migrate"
7
7
  * for drop-in compatibility with projects using the @nodepit/migrate package.
8
+ *
9
+ * The same file has to work from three locations, because `lt fullstack
10
+ * convert-mode` copies it verbatim into vendored projects:
11
+ *
12
+ * npm mode node_modules/@lenne.tech/nest-server/bin/ → ../dist/core/**.js
13
+ * vendor repo <project>/bin/ → ../dist/src/core/**.js
14
+ * (or ../src/core/**.ts via ts-node)
15
+ * vendor image <project>/dist/bin/ → ../src/core/**.js
16
+ *
17
+ * Compiled candidates are probed first, so a production image never reaches the
18
+ * ts-node branch — ts-node is a devDependency and gets pruned there.
19
+ *
20
+ * NOTE on the vendored-image layout: it assumes the build preserves the `src/`
21
+ * prefix (`dist/src/core/...`, matching `lt fullstack convert-mode`). A project
22
+ * that instead flattens `rootDir: 'src'` to `dist/core/...` is still resolved in
23
+ * *repo* mode (candidate 1) but not from the `dist/bin/` image variant.
24
+ *
25
+ * The resolver is exported (and pure, with an injectable `existsSync`) so the
26
+ * three-layout probe + not-found branch can be unit-tested without spawning a
27
+ * migration run; the CLI only executes when this file is invoked directly.
8
28
  */
9
29
 
10
30
  const fs = require('fs');
11
- // Check if running from built dist or src
12
31
  const path = require('path');
13
32
 
14
- const distPath = path.join(__dirname, '../dist/core/modules/migrate/cli/migrate-cli.js');
15
- const srcPath = path.join(__dirname, '../src/core/modules/migrate/cli/migrate-cli.ts');
16
-
17
- let cliPath;
18
-
19
- if (fs.existsSync(distPath)) {
20
- // Production: use built version
21
- cliPath = distPath;
22
- } else if (fs.existsSync(srcPath)) {
23
- // Development: register ts-node and use source
24
- try {
25
- require('ts-node/register');
26
- cliPath = srcPath;
27
- } catch (e) {
28
- console.error('Error: ts-node is required in development mode');
29
- console.error('Install it with: npm install --save-dev ts-node');
30
- process.exit(1);
33
+ const CLI_SUBPATH = 'core/modules/migrate/cli/migrate-cli';
34
+
35
+ /**
36
+ * Resolve the migrate CLI entry across the npm + vendored layouts.
37
+ *
38
+ * Compiled `.js` candidates (most specific first) are probed before the TypeScript
39
+ * source, so a production image with `ts-node` pruned never picks the source path.
40
+ *
41
+ * @param {string} dirname - Directory of the shim (`__dirname` at runtime).
42
+ * @param {(p: string) => boolean} [existsSync] - Injected for tests; defaults to `fs.existsSync`.
43
+ * @returns {{ cliPath: string | null, needsTsNode: boolean, candidates: string[] }}
44
+ * `cliPath` is the resolved entry (null when nothing matched); `needsTsNode` is true only for the
45
+ * TypeScript-source fallback; `candidates` is every path probed, for the not-found diagnostic.
46
+ */
47
+ function resolveCliPath(dirname, existsSync = fs.existsSync) {
48
+ const compiledCandidates = [
49
+ // npm package layout: bin/ → dist/core/...
50
+ path.join(dirname, '..', 'dist', `${CLI_SUBPATH}.js`),
51
+ // vendored build output: dist/bin/ → dist/src/core/...
52
+ path.join(dirname, '..', 'src', `${CLI_SUBPATH}.js`),
53
+ // vendored repo root: bin/ → dist/src/core/...
54
+ path.join(dirname, '..', 'dist', 'src', `${CLI_SUBPATH}.js`),
55
+ ];
56
+ // TypeScript source, used for local development via ts-node.
57
+ const sourcePath = path.join(dirname, '..', 'src', `${CLI_SUBPATH}.ts`);
58
+ const candidates = [...compiledCandidates, sourcePath];
59
+
60
+ const compiled = compiledCandidates.find((candidate) => existsSync(candidate));
61
+ if (compiled) {
62
+ return { candidates, cliPath: compiled, needsTsNode: false };
63
+ }
64
+ if (existsSync(sourcePath)) {
65
+ return { candidates, cliPath: sourcePath, needsTsNode: true };
31
66
  }
32
- } else {
33
- console.error('Error: Migration CLI not found');
34
- console.error('Make sure @lenne.tech/nest-server is properly built');
35
- process.exit(1);
67
+ return { candidates, cliPath: null, needsTsNode: false };
36
68
  }
37
69
 
38
- // Load and run the CLI
39
- const { main } = require(cliPath);
40
- main();
70
+ module.exports = { CLI_SUBPATH, resolveCliPath };
71
+
72
+ // Only run the CLI when invoked directly (`node bin/migrate.js` / the `migrate` bin).
73
+ // Importing this module for tests must not spawn a migration run.
74
+ if (require.main === module) {
75
+ const { candidates, cliPath, needsTsNode } = resolveCliPath(__dirname);
76
+
77
+ if (!cliPath) {
78
+ console.error('Error: Migration CLI not found. Looked for:');
79
+ for (const candidate of candidates) {
80
+ console.error(` - ${candidate}`);
81
+ }
82
+ process.exit(1);
83
+ }
84
+
85
+ if (needsTsNode) {
86
+ // Development: register ts-node so the TypeScript source can be required.
87
+ try {
88
+ require('ts-node/register');
89
+ } catch {
90
+ console.error('Error: ts-node is required to run migrations from TypeScript sources');
91
+ console.error('Install it with: pnpm add -D ts-node');
92
+ process.exit(1);
93
+ }
94
+ }
95
+
96
+ // Load and run the CLI
97
+ const { main } = require(cliPath);
98
+ main();
99
+ }
@@ -6,9 +6,9 @@ export type RestrictedType = (RequireAtLeastOne<{
6
6
  processType?: ProcessType;
7
7
  roles?: string | string[];
8
8
  }, 'memberOf' | 'roles'> | string | string[])[];
9
- export declare const Restricted: (...rolesOrMember: RestrictedType) => ClassDecorator & PropertyDecorator;
10
- export declare const getRestricted: (object: unknown, propertyKey?: string) => RestrictedType;
11
- export declare const checkRestricted: (data: any, user: {
9
+ export declare function Restricted(...rolesOrMember: RestrictedType): ClassDecorator & PropertyDecorator;
10
+ export declare function getRestricted(object: unknown, propertyKey?: string): RestrictedType;
11
+ export declare function checkRestricted(data: any, user: {
12
12
  emailVerified?: any;
13
13
  hasRole: (roles: string[]) => boolean;
14
14
  id: any;
@@ -28,4 +28,4 @@ export declare const checkRestricted: (data: any, user: {
28
28
  processType?: ProcessType;
29
29
  removeUndefinedFromResultArray?: boolean;
30
30
  throwError?: boolean;
31
- }, processedObjects?: WeakSet<object>) => any;
31
+ }, processedObjects?: WeakSet<object>): any;