@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
|
@@ -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
|
-
//
|
|
211
|
-
|
|
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) |
|
|
@@ -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
|
|
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
|
-
```
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
@@ -21,6 +21,57 @@ System roles are used for **runtime checks only** and must **NEVER** be stored i
|
|
|
21
21
|
| `S_EVERYONE` | Public access | Always true |
|
|
22
22
|
| `S_NO_ONE` | Locked access | Always false |
|
|
23
23
|
|
|
24
|
+
### `object` means the PERSISTED object — never the request payload
|
|
25
|
+
|
|
26
|
+
For `S_SELF` and `S_CREATOR`, "object" is the record loaded from the database
|
|
27
|
+
(`serviceOptions.dbObject`), **not** the input DTO. This distinction is security-critical.
|
|
28
|
+
|
|
29
|
+
On the **input** path the DTO is fully attacker-controlled. Deciding `S_SELF` from it would let an
|
|
30
|
+
authenticated attacker unlock an owner-restricted field on someone else's record just by putting
|
|
31
|
+
their own id in the body — the service writes to the target it was called with, not to the id in the
|
|
32
|
+
payload:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
PATCH /users/<victim-id> { "id": "<attacker-id>", "iban": "DE...attacker" }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
On the **output** path there is no attacker-controlled input: the object being checked *is* the
|
|
39
|
+
persisted record (and a list is checked per item), so it is the correct comparison target there.
|
|
40
|
+
|
|
41
|
+
Both `check()` (`input.helper.ts`) and `checkRestricted()` (`restricted.decorator.ts`) implement this.
|
|
42
|
+
If you write your own rights check, compare against `dbObject` on input — never against the DTO.
|
|
43
|
+
|
|
44
|
+
### ⚠️ `S_CREATOR` is the CREATOR of the record — which is often an admin, not the user
|
|
45
|
+
|
|
46
|
+
`createdBy` is set by the audit plugin to whoever **created the record**. On a self-signup that is the
|
|
47
|
+
user themselves. But in an **invite or admin-provisioning flow it is the inviting admin** — and it
|
|
48
|
+
stays that way forever.
|
|
49
|
+
|
|
50
|
+
So `@Restricted(S_CREATOR)` on a **User input field** does not mean "the user may edit their own
|
|
51
|
+
field". It means **"whoever created this account may edit it"** — granting the inviter permanent
|
|
52
|
+
write access to the invited user's record.
|
|
53
|
+
|
|
54
|
+
That is almost never the intent, and it is dangerous on exactly the fields people reach for it on:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// DANGEROUS on an invite-based system: the inviting workspace admin IS the creator, so this
|
|
58
|
+
// lets them rewrite the invited member's email — and then trigger a password reset.
|
|
59
|
+
@UnifiedField({ roles: [RoleEnum.ADMIN, RoleEnum.S_CREATOR] })
|
|
60
|
+
email?: string;
|
|
61
|
+
|
|
62
|
+
// SAFE: only a system admin, and the user changes their own email through the verification-gated
|
|
63
|
+
// BetterAuth changeEmail flow, never through a generic update DTO.
|
|
64
|
+
@UnifiedField({ roles: [RoleEnum.ADMIN] })
|
|
65
|
+
email?: string;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Upgrade note:** before v11.28.x, `S_SELF`/`S_CREATOR` on an *input* field never actually fired —
|
|
69
|
+
the check read the claim off the DTO, and `MapAndValidatePipe` strips `id`/`createdBy` from payloads
|
|
70
|
+
(they are not `@UnifiedField`s). Such fields were therefore effectively **admin-only-or-denied**.
|
|
71
|
+
Now that the check reads the persisted object, they start working — and a field that looked
|
|
72
|
+
owner-restricted may suddenly become writable by an admin who provisioned the record. **Audit every
|
|
73
|
+
`S_SELF`/`S_CREATOR` on an input type before upgrading.**
|
|
74
|
+
|
|
24
75
|
## Critical Rule
|
|
25
76
|
|
|
26
77
|
```typescript
|
|
@@ -79,6 +130,45 @@ The role system is evaluated in:
|
|
|
79
130
|
- `CheckResponseInterceptor` - Filters fields based on `@Restricted()` decorators
|
|
80
131
|
- `CheckSecurityInterceptor` - Processes `securityCheck()` methods
|
|
81
132
|
|
|
133
|
+
## Status Codes: 401 vs 403 (v11.28.0+)
|
|
134
|
+
|
|
135
|
+
**All five permission layers answer with the same policy.** Getting this wrong has a concrete
|
|
136
|
+
consequence: SPA auth layers treat 401 as "session expired" and log the user out — so a mere
|
|
137
|
+
permission error returned as 401 kicks a logged-in user out of the whole app.
|
|
138
|
+
|
|
139
|
+
| Situation | Status | Message |
|
|
140
|
+
|-----------|--------|---------|
|
|
141
|
+
| Requester is **not authenticated** | **401** | `ErrorCode.UNAUTHORIZED` |
|
|
142
|
+
| Requester **is authenticated** but lacks a right | **403** | `ErrorCode.ACCESS_DENIED` |
|
|
143
|
+
| `S_NO_ONE` (locked for everyone) | **403 always**, even for anonymous requesters | `ErrorCode.ACCESS_DENIED` |
|
|
144
|
+
|
|
145
|
+
`S_NO_ONE` is 403 even without a session because authenticating can *never* unlock it — a 401 would
|
|
146
|
+
tell the client to log in and retry, which is a lie.
|
|
147
|
+
|
|
148
|
+
Exception: `ErrorCode.EMAIL_VERIFICATION_REQUIRED` is a legitimate **401** (thrown at sign-in, where
|
|
149
|
+
no session exists yet). Frontends must branch on the ErrorCode, not on the status alone, so this one
|
|
150
|
+
does not trigger the logout flow.
|
|
151
|
+
|
|
152
|
+
### Never hand-roll the decision
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { accessDeniedException } from '@lenne.tech/nest-server';
|
|
156
|
+
|
|
157
|
+
// CORRECT — one policy, native exceptions (instanceof / @Catch keep working)
|
|
158
|
+
throw accessDeniedException(currentUser);
|
|
159
|
+
throw accessDeniedException(currentUser, 'Custom message');
|
|
160
|
+
|
|
161
|
+
// WRONG — drifts from the framework policy and mishandles falsy-but-present ids (0, '')
|
|
162
|
+
throw currentUser?.id ? new ForbiddenException() : new UnauthorizedException();
|
|
163
|
+
|
|
164
|
+
// WRONG — a permission error must never be a 401 for an authenticated user
|
|
165
|
+
throw new UnauthorizedException('Missing rights');
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**This applies to `securityCheck()` in your models too.** `CoreTenantMemberModel` is the reference
|
|
169
|
+
implementation. A model that throws `UnauthorizedException` from `securityCheck()` reintroduces the
|
|
170
|
+
auto-logout bug in your own project.
|
|
171
|
+
|
|
82
172
|
## @Roles vs @UseGuards
|
|
83
173
|
|
|
84
174
|
**IMPORTANT: `@Roles()` already handles JWT authentication internally.**
|
|
@@ -23,12 +23,27 @@
|
|
|
23
23
|
## Release Process
|
|
24
24
|
|
|
25
25
|
1. Make changes and ensure all tests pass (`pnpm test`)
|
|
26
|
-
2.
|
|
27
|
-
|
|
26
|
+
2. **Bump the version in BOTH version-carrying files — they must never drift:**
|
|
27
|
+
- `package.json` → `version`
|
|
28
|
+
- `spectaql.yml` → `info.version` (feeds the published GraphQL API docs)
|
|
29
|
+
|
|
30
|
+
`spectaql.yml` is derived from `package.json` by `extras/update-spectaql-version.mjs`, but that
|
|
31
|
+
script only runs via `pnpm run docs` — it is **not** part of `pnpm run build`. A release commit
|
|
32
|
+
that bumps only `package.json` therefore ships API docs advertising the previous version. Run
|
|
33
|
+
`node extras/update-spectaql-version.mjs` (or edit the file) and commit both together.
|
|
34
|
+
3. Build the package (`pnpm run build`) — this also regenerates `FRAMEWORK-API.md` with the new version
|
|
28
35
|
4. Publish to npm
|
|
29
36
|
5. Update and test in [nest-server-starter](https://github.com/lenneTech/nest-server-starter)
|
|
30
37
|
6. Commit changes to starter with migration notes
|
|
31
38
|
|
|
39
|
+
### Version Consistency Check
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Both must print the same version
|
|
43
|
+
grep -m1 '"version"' package.json
|
|
44
|
+
grep -m1 '^ version:' spectaql.yml
|
|
45
|
+
```
|
|
46
|
+
|
|
32
47
|
## Package Distribution
|
|
33
48
|
|
|
34
49
|
- **NPM Package**: `@lenne.tech/nest-server`
|
package/CLAUDE.md
CHANGED
|
@@ -114,9 +114,15 @@ npx vitest run --config vitest-e2e.config.ts --reporter=hanging-process # Debu
|
|
|
114
114
|
pnpm run test:cleanup # Remove leftover test artifacts (.txt, .bin)
|
|
115
115
|
|
|
116
116
|
# Linting & Formatting
|
|
117
|
-
pnpm run lint #
|
|
117
|
+
pnpm run lint # oxlint check
|
|
118
118
|
pnpm run lint:fix # Auto-fix
|
|
119
|
-
pnpm run 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)"
|
|
120
126
|
|
|
121
127
|
# Package Development
|
|
122
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-
|
|
3
|
+
> Auto-generated from source code on 2026-07-15 (v11.28.0)
|
|
4
4
|
> File: `FRAMEWORK-API.md` — compact, machine-readable API surface for Claude Code
|
|
5
5
|
|
|
6
6
|
## CoreModule.forRoot()
|
|
@@ -283,6 +283,31 @@ Generic: `CrudService<Model, CreateInput, UpdateInput>`
|
|
|
283
283
|
| `tus` | README, CHECKLIST | `src/core/modules/tus/` |
|
|
284
284
|
| `user` | — | `src/core/modules/user/` |
|
|
285
285
|
|
|
286
|
+
## Errors & Status Codes
|
|
287
|
+
|
|
288
|
+
One 401/403 policy across all permission layers (role guards, tenant guard, `check()`,
|
|
289
|
+
`checkRestricted()`, model `securityCheck()`):
|
|
290
|
+
|
|
291
|
+
| Situation | Status | Message |
|
|
292
|
+
|-----------|--------|---------|
|
|
293
|
+
| Requester is **not authenticated** | **401** | `ErrorCode.UNAUTHORIZED` |
|
|
294
|
+
| Requester **is authenticated** but lacks a right | **403** | `ErrorCode.ACCESS_DENIED` |
|
|
295
|
+
| `S_NO_ONE` (locked for everyone, even admins) | **403 always** | `ErrorCode.ACCESS_DENIED` |
|
|
296
|
+
|
|
297
|
+
Never hand-roll the decision — use `accessDeniedException(user)`. It returns the **native**
|
|
298
|
+
`ForbiddenException` / `UnauthorizedException`, so `instanceof` checks and `@Catch(...)` filters
|
|
299
|
+
in consuming projects keep working.
|
|
300
|
+
|
|
301
|
+
### Exported error helpers
|
|
302
|
+
|
|
303
|
+
| Export | Purpose | Path |
|
|
304
|
+
|--------|---------|------|
|
|
305
|
+
| `ExpiredRefreshTokenException` | Exception for expired refresh token | `src/core/modules/auth/exceptions/expired-refresh-token.exception.ts` |
|
|
306
|
+
| `ExpiredTokenException` | Exception for expired token | `src/core/modules/auth/exceptions/expired-token.exception.ts` |
|
|
307
|
+
| `InvalidTokenException` | Exception for invalid token | `src/core/modules/auth/exceptions/invalid-token.exception.ts` |
|
|
308
|
+
| `LegacyAuthDisabledException` | Exception thrown when Legacy Auth endpoints are accessed but disabled | `src/core/modules/auth/exceptions/legacy-auth-disabled.exception.ts` |
|
|
309
|
+
| `accessDeniedException()` | Creates the access error that matches the requester's auth state (RFC 9110, mirrors RolesGuard): | `src/core/common/exceptions/access-denied.exception.ts` |
|
|
310
|
+
|
|
286
311
|
## Key Source Files
|
|
287
312
|
|
|
288
313
|
| File | Purpose |
|
|
@@ -293,6 +318,7 @@ Generic: `CrudService<Model, CreateInput, UpdateInput>`
|
|
|
293
318
|
| `src/core/common/services/crud.service.ts` | CrudService base class |
|
|
294
319
|
| `src/core/common/services/config.service.ts` | ConfigService (global) |
|
|
295
320
|
| `src/core/common/decorators/` | @Restricted, @Roles, @CurrentUser, @UnifiedField |
|
|
321
|
+
| `src/core/common/exceptions/` | accessDeniedException — the 401/403 policy |
|
|
296
322
|
| `src/core/common/interceptors/` | CheckResponse, CheckSecurity, ResponseModel |
|
|
297
323
|
| `docs/REQUEST-LIFECYCLE.md` | Complete request lifecycle |
|
|
298
324
|
| `.claude/rules/` | Detailed rules for architecture, security, testing |
|
|
@@ -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
|
|
10
|
-
export declare
|
|
11
|
-
export declare
|
|
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>)
|
|
31
|
+
}, processedObjects?: WeakSet<object>): any;
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
|
|
3
|
+
exports.Restricted = Restricted;
|
|
4
|
+
exports.getRestricted = getRestricted;
|
|
5
|
+
exports.checkRestricted = checkRestricted;
|
|
5
6
|
require("reflect-metadata");
|
|
6
7
|
const _ = require("lodash");
|
|
8
|
+
const process_type_enum_1 = require("../enums/process-type.enum");
|
|
7
9
|
const role_enum_1 = require("../enums/role.enum");
|
|
8
|
-
const
|
|
10
|
+
const access_denied_exception_1 = require("../exceptions/access-denied.exception");
|
|
11
|
+
const id_helper_1 = require("../helpers/id.helper");
|
|
9
12
|
const request_context_service_1 = require("../services/request-context.service");
|
|
10
13
|
const core_tenant_helpers_1 = require("../../modules/tenant/core-tenant.helpers");
|
|
11
14
|
const restrictedMetaKey = Symbol('restricted');
|
|
12
|
-
|
|
15
|
+
function Restricted(...rolesOrMember) {
|
|
13
16
|
return Reflect.metadata(restrictedMetaKey, rolesOrMember);
|
|
14
|
-
}
|
|
15
|
-
exports.Restricted = Restricted;
|
|
17
|
+
}
|
|
16
18
|
const restrictedMetadataCache = new WeakMap();
|
|
17
|
-
|
|
19
|
+
function getRestricted(object, propertyKey) {
|
|
18
20
|
if (!object) {
|
|
19
21
|
return null;
|
|
20
22
|
}
|
|
@@ -38,9 +40,8 @@ const getRestricted = (object, propertyKey) => {
|
|
|
38
40
|
: Reflect.getMetadata(restrictedMetaKey, object);
|
|
39
41
|
classCache.set(cacheKey, metadata);
|
|
40
42
|
return metadata;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const checkRestricted = (data, user, options = {}, processedObjects = new WeakSet()) => {
|
|
43
|
+
}
|
|
44
|
+
function checkRestricted(data, user, options = {}, processedObjects = new WeakSet()) {
|
|
44
45
|
const config = {
|
|
45
46
|
allowCreatorOfParent: true,
|
|
46
47
|
checkObjectItself: false,
|
|
@@ -70,7 +71,7 @@ const checkRestricted = (data, user, options = {}, processedObjects = new WeakSe
|
|
|
70
71
|
!Array.isArray(sample) &&
|
|
71
72
|
sample.constructor &&
|
|
72
73
|
sample.constructor !== Object) {
|
|
73
|
-
const classRestrictions =
|
|
74
|
+
const classRestrictions = getRestricted(sample.constructor) || [];
|
|
74
75
|
if (classRestrictions.length) {
|
|
75
76
|
const hasCreatorOrSelf = classRestrictions.some((r) => r === role_enum_1.RoleEnum.S_CREATOR ||
|
|
76
77
|
r === role_enum_1.RoleEnum.S_SELF ||
|
|
@@ -84,7 +85,7 @@ const checkRestricted = (data, user, options = {}, processedObjects = new WeakSe
|
|
|
84
85
|
r.roles === role_enum_1.RoleEnum.S_CREATOR ||
|
|
85
86
|
r.roles === role_enum_1.RoleEnum.S_SELF)));
|
|
86
87
|
if (!hasCreatorOrSelf) {
|
|
87
|
-
const sampleResult =
|
|
88
|
+
const sampleResult = checkRestricted(sample, user, config, processedObjects);
|
|
88
89
|
if (sampleResult === undefined || sampleResult === null) {
|
|
89
90
|
if (config.throwError) {
|
|
90
91
|
return data;
|
|
@@ -93,7 +94,7 @@ const checkRestricted = (data, user, options = {}, processedObjects = new WeakSe
|
|
|
93
94
|
}
|
|
94
95
|
const result = [sampleResult];
|
|
95
96
|
for (let i = 1; i < data.length; i++) {
|
|
96
|
-
result.push(
|
|
97
|
+
result.push(checkRestricted(data[i], user, config, processedObjects));
|
|
97
98
|
}
|
|
98
99
|
if (!config.throwError && config.removeUndefinedFromResultArray) {
|
|
99
100
|
return result.filter((item) => item !== undefined);
|
|
@@ -102,7 +103,7 @@ const checkRestricted = (data, user, options = {}, processedObjects = new WeakSe
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
|
-
let result = data.map((item) =>
|
|
106
|
+
let result = data.map((item) => checkRestricted(item, user, config, processedObjects));
|
|
106
107
|
if (!config.throwError && config.removeUndefinedFromResultArray) {
|
|
107
108
|
result = result.filter((item) => item !== undefined);
|
|
108
109
|
}
|
|
@@ -138,13 +139,14 @@ const checkRestricted = (data, user, options = {}, processedObjects = new WeakSe
|
|
|
138
139
|
if (roles.includes(role_enum_1.RoleEnum.S_NO_ONE)) {
|
|
139
140
|
return false;
|
|
140
141
|
}
|
|
142
|
+
const owner = config.processType === process_type_enum_1.ProcessType.INPUT ? config.dbObject : data;
|
|
141
143
|
if (roles.includes(role_enum_1.RoleEnum.S_EVERYONE) ||
|
|
142
144
|
user?.hasRole?.(roles) ||
|
|
143
145
|
(user?.id && roles.includes(role_enum_1.RoleEnum.S_USER)) ||
|
|
144
|
-
(roles.includes(role_enum_1.RoleEnum.S_SELF) && (0,
|
|
146
|
+
(roles.includes(role_enum_1.RoleEnum.S_SELF) && (0, id_helper_1.equalIds)(owner, user)) ||
|
|
145
147
|
(roles.includes(role_enum_1.RoleEnum.S_CREATOR) &&
|
|
146
|
-
(('createdBy' in
|
|
147
|
-
(config.allowCreatorOfParent && !('createdBy' in
|
|
148
|
+
((owner && 'createdBy' in owner && (0, id_helper_1.equalIds)(owner.createdBy, user)) ||
|
|
149
|
+
(config.allowCreatorOfParent && owner && !('createdBy' in owner) && config.isCreatorOfParent))) ||
|
|
148
150
|
(roles.includes(role_enum_1.RoleEnum.S_VERIFIED) && (user?.verified || user?.verifiedAt || user?.emailVerified)) ||
|
|
149
151
|
(user?.id && (0, core_tenant_helpers_1.checkRoleAccess)(roles, user?.roles, request_context_service_1.RequestContext.get()?.tenantRole))) {
|
|
150
152
|
valid = true;
|
|
@@ -175,7 +177,7 @@ const checkRestricted = (data, user, options = {}, processedObjects = new WeakSe
|
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
179
|
}
|
|
178
|
-
if ((0,
|
|
180
|
+
if ((0, id_helper_1.getIncludedIds)(members, user)) {
|
|
179
181
|
valid = true;
|
|
180
182
|
}
|
|
181
183
|
}
|
|
@@ -185,7 +187,7 @@ const checkRestricted = (data, user, options = {}, processedObjects = new WeakSe
|
|
|
185
187
|
}
|
|
186
188
|
return valid;
|
|
187
189
|
};
|
|
188
|
-
const objectRestrictions =
|
|
190
|
+
const objectRestrictions = getRestricted(data.constructor) || [];
|
|
189
191
|
if (config.checkObjectItself) {
|
|
190
192
|
const objectIsValid = validateRestricted(objectRestrictions);
|
|
191
193
|
if (!objectIsValid) {
|
|
@@ -193,7 +195,7 @@ const checkRestricted = (data, user, options = {}, processedObjects = new WeakSe
|
|
|
193
195
|
console.debug(`The current user has no access rights for ${data.constructor?.name}`);
|
|
194
196
|
}
|
|
195
197
|
if (config.throwError) {
|
|
196
|
-
throw
|
|
198
|
+
throw (0, access_denied_exception_1.accessDeniedException)(user);
|
|
197
199
|
}
|
|
198
200
|
return null;
|
|
199
201
|
}
|
|
@@ -205,25 +207,26 @@ const checkRestricted = (data, user, options = {}, processedObjects = new WeakSe
|
|
|
205
207
|
if (data[propertyKey] === undefined && config.ignoreUndefined) {
|
|
206
208
|
continue;
|
|
207
209
|
}
|
|
208
|
-
const restricted =
|
|
210
|
+
const restricted = getRestricted(data, propertyKey) || [];
|
|
209
211
|
const concatenatedRestrictions = config.mergeRoles && objectRestrictions.length ? _.uniq(objectRestrictions.concat(restricted)) : restricted;
|
|
210
212
|
const valid = validateRestricted(concatenatedRestrictions);
|
|
211
213
|
if (valid) {
|
|
214
|
+
const parent = config.processType === process_type_enum_1.ProcessType.INPUT ? config.dbObject : data;
|
|
212
215
|
config.isCreatorOfParent =
|
|
213
|
-
(0,
|
|
214
|
-
|
|
216
|
+
(0, id_helper_1.equalIds)(parent, user) ||
|
|
217
|
+
(parent && 'createdBy' in parent ? (0, id_helper_1.equalIds)(parent.createdBy, user) : config.isCreatorOfParent);
|
|
218
|
+
data[propertyKey] = checkRestricted(data[propertyKey], user, config, processedObjects);
|
|
215
219
|
}
|
|
216
220
|
else {
|
|
217
221
|
if (config.debug) {
|
|
218
222
|
console.debug(`The current user has no access rights for ${propertyKey}${data.constructor?.name ? ` of ${data.constructor.name}` : ''}`);
|
|
219
223
|
}
|
|
220
224
|
if (config.throwError) {
|
|
221
|
-
throw
|
|
225
|
+
throw (0, access_denied_exception_1.accessDeniedException)(user);
|
|
222
226
|
}
|
|
223
227
|
delete data[propertyKey];
|
|
224
228
|
}
|
|
225
229
|
}
|
|
226
230
|
return data;
|
|
227
|
-
}
|
|
228
|
-
exports.checkRestricted = checkRestricted;
|
|
231
|
+
}
|
|
229
232
|
//# sourceMappingURL=restricted.decorator.js.map
|