@lenne.tech/nest-server 11.34.0 → 11.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.claude/rules/architecture.md +3 -1
  2. package/.claude/rules/configurable-features.md +3 -3
  3. package/.claude/rules/role-system.md +281 -0
  4. package/CLAUDE.md +11 -1
  5. package/FRAMEWORK-API.md +7 -3
  6. package/dist/core/common/decorators/nested-type.registry.d.ts +2 -0
  7. package/dist/core/common/decorators/nested-type.registry.js +17 -0
  8. package/dist/core/common/decorators/nested-type.registry.js.map +1 -0
  9. package/dist/core/common/decorators/restricted.decorator.d.ts +2 -1
  10. package/dist/core/common/decorators/restricted.decorator.js +52 -16
  11. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  12. package/dist/core/common/decorators/restrictions-checked.marker.d.ts +4 -0
  13. package/dist/core/common/decorators/restrictions-checked.marker.js +30 -0
  14. package/dist/core/common/decorators/restrictions-checked.marker.js.map +1 -0
  15. package/dist/core/common/decorators/unified-field.decorator.d.ts +1 -1
  16. package/dist/core/common/decorators/unified-field.decorator.js +6 -3
  17. package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
  18. package/dist/core/common/enums/role.enum.d.ts +8 -0
  19. package/dist/core/common/enums/role.enum.js +28 -1
  20. package/dist/core/common/enums/role.enum.js.map +1 -1
  21. package/dist/core/common/helpers/execution-context-request.helper.d.ts +7 -0
  22. package/dist/core/common/helpers/execution-context-request.helper.js +31 -0
  23. package/dist/core/common/helpers/execution-context-request.helper.js.map +1 -0
  24. package/dist/core/common/helpers/graphql-ws-context.helper.d.ts +15 -0
  25. package/dist/core/common/helpers/graphql-ws-context.helper.js +96 -0
  26. package/dist/core/common/helpers/graphql-ws-context.helper.js.map +1 -0
  27. package/dist/core/common/helpers/gridfs.helper.js +1 -1
  28. package/dist/core/common/helpers/gridfs.helper.js.map +1 -1
  29. package/dist/core/common/helpers/validation-message.helper.d.ts +3 -0
  30. package/dist/core/common/helpers/validation-message.helper.js +41 -0
  31. package/dist/core/common/helpers/validation-message.helper.js.map +1 -0
  32. package/dist/core/common/interceptors/check-security.interceptor.js +2 -1
  33. package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
  34. package/dist/core/common/interceptors/response-model.interceptor.js +3 -2
  35. package/dist/core/common/interceptors/response-model.interceptor.js.map +1 -1
  36. package/dist/core/common/interfaces/server-options.interface.d.ts +5 -0
  37. package/dist/core/common/pipes/map-and-validate.pipe.js +16 -6
  38. package/dist/core/common/pipes/map-and-validate.pipe.js.map +1 -1
  39. package/dist/core/common/plugins/mongoose-system-role.plugin.d.ts +1 -0
  40. package/dist/core/common/plugins/mongoose-system-role.plugin.js +106 -0
  41. package/dist/core/common/plugins/mongoose-system-role.plugin.js.map +1 -0
  42. package/dist/core/common/plugins/mongoose-tenant.plugin.js +134 -9
  43. package/dist/core/common/plugins/mongoose-tenant.plugin.js.map +1 -1
  44. package/dist/core/common/services/core-s3.service.js +4 -1
  45. package/dist/core/common/services/core-s3.service.js.map +1 -1
  46. package/dist/core/common/services/core-tenant-context.registry.d.ts +10 -0
  47. package/dist/core/common/services/core-tenant-context.registry.js +12 -0
  48. package/dist/core/common/services/core-tenant-context.registry.js.map +1 -0
  49. package/dist/core/modules/auth/guards/roles.guard.d.ts +1 -1
  50. package/dist/core/modules/auth/guards/roles.guard.js +2 -2
  51. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  52. package/dist/core/modules/better-auth/better-auth-roles.guard.js +2 -10
  53. package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
  54. package/dist/core/modules/file/core-file.service.d.ts +3 -1
  55. package/dist/core/modules/file/core-file.service.js +62 -7
  56. package/dist/core/modules/file/core-file.service.js.map +1 -1
  57. package/dist/core/modules/file/file-access.helper.d.ts +18 -0
  58. package/dist/core/modules/file/file-access.helper.js +46 -0
  59. package/dist/core/modules/file/file-access.helper.js.map +1 -0
  60. package/dist/core/modules/file/file-metadata.helper.js +1 -1
  61. package/dist/core/modules/file/file-metadata.helper.js.map +1 -1
  62. package/dist/core/modules/file/file-roles.config.d.ts +10 -0
  63. package/dist/core/modules/file/file-roles.config.js +78 -0
  64. package/dist/core/modules/file/file-roles.config.js.map +1 -0
  65. package/dist/core/modules/file/file-roles.helper.d.ts +2 -2
  66. package/dist/core/modules/file/file-roles.helper.js +8 -20
  67. package/dist/core/modules/file/file-roles.helper.js.map +1 -1
  68. package/dist/core/modules/tenant/core-role-scope.registry.d.ts +28 -0
  69. package/dist/core/modules/tenant/core-role-scope.registry.js +93 -0
  70. package/dist/core/modules/tenant/core-role-scope.registry.js.map +1 -0
  71. package/dist/core/modules/tenant/core-tenant.guard.d.ts +5 -0
  72. package/dist/core/modules/tenant/core-tenant.guard.js +37 -11
  73. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  74. package/dist/core/modules/tenant/core-tenant.helpers.d.ts +9 -1
  75. package/dist/core/modules/tenant/core-tenant.helpers.js +65 -4
  76. package/dist/core/modules/tenant/core-tenant.helpers.js.map +1 -1
  77. package/dist/core/modules/tenant/core-tenant.module.js +5 -0
  78. package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
  79. package/dist/core/modules/tenant/core-tenant.service.d.ts +1 -0
  80. package/dist/core/modules/tenant/core-tenant.service.js +25 -0
  81. package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
  82. package/dist/core/modules/tus/core-tus.service.d.ts +2 -0
  83. package/dist/core/modules/tus/core-tus.service.js +34 -4
  84. package/dist/core/modules/tus/core-tus.service.js.map +1 -1
  85. package/dist/core/modules/tus/index.d.ts +1 -0
  86. package/dist/core/modules/tus/index.js +1 -0
  87. package/dist/core/modules/tus/index.js.map +1 -1
  88. package/dist/core/modules/tus/tus.constants.d.ts +1 -0
  89. package/dist/core/modules/tus/tus.constants.js +2 -1
  90. package/dist/core/modules/tus/tus.constants.js.map +1 -1
  91. package/dist/core/modules/user/core-user.service.js +5 -0
  92. package/dist/core/modules/user/core-user.service.js.map +1 -1
  93. package/dist/core/modules/user/inputs/core-user.input.js +8 -0
  94. package/dist/core/modules/user/inputs/core-user.input.js.map +1 -1
  95. package/dist/core.module.js +10 -0
  96. package/dist/core.module.js.map +1 -1
  97. package/dist/index.d.ts +3 -0
  98. package/dist/index.js +3 -0
  99. package/dist/index.js.map +1 -1
  100. package/dist/server/modules/file/file.service.js +4 -1
  101. package/dist/server/modules/file/file.service.js.map +1 -1
  102. package/dist/test/test.helper.d.ts +1 -0
  103. package/dist/test/test.helper.js +26 -6
  104. package/dist/test/test.helper.js.map +1 -1
  105. package/dist/tsconfig.build.tsbuildinfo +1 -1
  106. package/docs/REQUEST-LIFECYCLE.md +60 -1
  107. package/migration-guides/11.34.0-to-11.34.1.md +132 -0
  108. package/migration-guides/11.34.x-to-11.35.x.md +832 -0
  109. package/package.json +1 -1
  110. package/src/core/common/decorators/nested-type.registry.ts +42 -0
  111. package/src/core/common/decorators/restricted.decorator.ts +149 -21
  112. package/src/core/common/decorators/restrictions-checked.marker.ts +87 -0
  113. package/src/core/common/decorators/unified-field.decorator.ts +4 -3
  114. package/src/core/common/enums/role.enum.ts +116 -0
  115. package/src/core/common/helpers/execution-context-request.helper.ts +102 -0
  116. package/src/core/common/helpers/graphql-ws-context.helper.ts +192 -0
  117. package/src/core/common/helpers/gridfs.helper.ts +18 -2
  118. package/src/core/common/helpers/validation-message.helper.ts +83 -0
  119. package/src/core/common/interceptors/check-security.interceptor.ts +4 -1
  120. package/src/core/common/interceptors/response-model.interceptor.ts +3 -2
  121. package/src/core/common/interfaces/server-options.interface.ts +139 -7
  122. package/src/core/common/pipes/map-and-validate.pipe.ts +25 -7
  123. package/src/core/common/plugins/mongoose-system-role.plugin.ts +193 -0
  124. package/src/core/common/plugins/mongoose-tenant.plugin.ts +244 -12
  125. package/src/core/common/services/core-s3.service.ts +9 -1
  126. package/src/core/common/services/core-tenant-context.registry.ts +50 -0
  127. package/src/core/modules/auth/guards/roles.guard.ts +7 -5
  128. package/src/core/modules/better-auth/better-auth-roles.guard.ts +7 -13
  129. package/src/core/modules/file/INTEGRATION-CHECKLIST.md +95 -1
  130. package/src/core/modules/file/README.md +61 -0
  131. package/src/core/modules/file/core-file.service.ts +166 -9
  132. package/src/core/modules/file/file-access.helper.ts +149 -0
  133. package/src/core/modules/file/file-metadata.helper.ts +13 -5
  134. package/src/core/modules/file/file-roles.config.ts +202 -0
  135. package/src/core/modules/file/file-roles.helper.ts +11 -40
  136. package/src/core/modules/tenant/README.md +20 -1
  137. package/src/core/modules/tenant/core-role-scope.registry.ts +195 -0
  138. package/src/core/modules/tenant/core-tenant.guard.ts +92 -12
  139. package/src/core/modules/tenant/core-tenant.helpers.ts +139 -7
  140. package/src/core/modules/tenant/core-tenant.module.ts +12 -0
  141. package/src/core/modules/tenant/core-tenant.service.ts +56 -0
  142. package/src/core/modules/tus/README.md +46 -7
  143. package/src/core/modules/tus/core-tus.service.ts +94 -5
  144. package/src/core/modules/tus/index.ts +1 -0
  145. package/src/core/modules/tus/tus.constants.ts +17 -1
  146. package/src/core/modules/user/core-user.service.ts +12 -0
  147. package/src/core/modules/user/inputs/core-user.input.ts +28 -2
  148. package/src/core.module.ts +62 -1
  149. package/src/index.ts +3 -0
  150. package/src/server/modules/file/file.service.ts +45 -2
  151. package/src/test/test.helper.ts +54 -11
@@ -205,6 +205,7 @@ JWT-based authentication for existing projects:
205
205
  | **Apollo Server** | Full GraphQL server with schema-first or code-first |
206
206
  | **Custom Scalars** | `Date`, `DateTime` (timestamp), `JSON`, `Any` |
207
207
  | **Subscriptions** | WebSocket support via `graphql-ws` with auth. The `PUB_SUB` provider is built from a factory: `CoreRedisPubSub` when `redis` is configured (delivery is then cluster-wide), the in-memory `PubSub` otherwise (delivery only to clients connected to the publishing replica). **Constraint once Redis is in play: every published payload must be JSON-serializable** — it crosses the wire as JSON, so `Date`, class instances, `Map`/`Set` and `undefined` do not survive the round trip. An in-process `PubSub` never had this constraint, so a payload that worked on one replica can silently lose fields on a cluster. Publish plain objects and ISO strings |
208
+ | **Subscription request context** | Since 11.35.0 `CoreModule` installs a context-aware `execute` / `subscribe` pair (inside `subscriptions`, where `ApolloDriver` forwards it) on all three GraphQL driver builders, so every WebSocket operation runs inside a `RequestContext`. Before that a WS operation had **none** — no Express middleware runs on an upgrade, and `CoreTenantGuard.getRequest()` finds no `req` on a subscription context — and `mongooseTenantPlugin` reads "no context" as "system operation, no filter", so a tenant-scoped read while delivering a subscription message returned EVERY tenant's rows. The tenant comes from the handshake's tenant header **validated against an active membership**, else from the subscriber's memberships; an unresolvable tenant leaves the safety net to refuse the read. The async iterator is wrapped too, not only the subscribe call: graphql-js runs `resolve` / `filter` / field resolvers inside its `next()`. See `.claude/rules/role-system.md` → "Tenant context on non-HTTP transports" |
208
209
  | **Complexity Analysis** | Query cost calculation to prevent DoS attacks |
209
210
  | **Enum Registration** | `registerEnum()` helper for GraphQL enum types |
210
211
  | **Upload Support** | `graphqlUploadExpress()` for multipart file uploads |
@@ -1217,6 +1218,64 @@ export class User extends CorePersistenceModel {
1217
1218
  }
1218
1219
  ```
1219
1220
 
1221
+ #### Nested data: only a DECLARED type is enforced (11.35.0+)
1222
+
1223
+ The check recurses, but it can only find metadata for a nested value whose class it knows. An embedded
1224
+ subdocument read out of MongoDB is a **plain object** (`CoreModel.map()` is a shallow `Object.assign`;
1225
+ `prepareOutput()` and `ResponseModelInterceptor` map the top level only), and `Object` carries no
1226
+ `@Restricted` metadata. Before 11.35.0 that meant every nested restriction silently evaluated to "no
1227
+ restrictions at all"; since 11.35.0 the nested type registry `@UnifiedField` fills is consulted, so a
1228
+ plain nested value is matched against the class its parent DECLARED — at every level, and for every
1229
+ item of a declared array.
1230
+
1231
+ ```typescript
1232
+ @UnifiedField({ type: () => Insurance }) // enforced
1233
+ insurance?: Insurance;
1234
+
1235
+ @UnifiedField({ isArray: true, type: () => Insurance }) // enforced, every item
1236
+ insurances?: Insurance[];
1237
+
1238
+ @UnifiedField({ isAny: true }) // NOT reached — nothing declares the type
1239
+ extra?: any;
1240
+ ```
1241
+
1242
+ An undeclared nested type stays unchecked on purpose: such a value is just as legitimately free-form
1243
+ JSON, a `Map` or a scalar, and failing closed would strip far more than it protects. **If a nested
1244
+ field must be protected, declare its type.** A class-level `@Restricted` on a nested type strips the
1245
+ contents and leaves an empty container (the `checkObjectItself: false` default).
1246
+
1247
+ ### File access — two layers, and why neither is optional
1248
+
1249
+ Files are the one resource whose authorization does NOT go through `CrudService.process()`, so the
1250
+ decorator model above does not reach them. GridFS is reached through the native MongoDB driver and S3
1251
+ through its own SDK, which means `mongooseTenantPlugin` never runs on a file store — and that is why
1252
+ `CoreFileController` and `CoreFileResolver` carry `@SkipTenantCheck()`.
1253
+
1254
+ | Layer | What it answers | Where |
1255
+ |-------|-----------------|-------|
1256
+ | `file.downloadRoles` / `uploadRoles` / `deleteRoles` | "may this caller reach the route at all" | applied onto the base-class members at boot by `applyFileRoles()`, read by the role guards |
1257
+ | `CoreFileService.checkRights()` | "…but only THIS file" | the service, once per operation, with `checkInputType` naming the path (`'id'`, `'filename'`, `'filterArgs'`, `'file'`, `'files'`) |
1258
+
1259
+ **The first layer cannot express the second.** The role names resolve against `user.roles` — a global
1260
+ attribute — never against `membership.role`, so no configuration can say "only their own tenant's
1261
+ files". That sentence needs data, and the data lives in the file's `metadata`.
1262
+
1263
+ **File ids are not secrets.** An ObjectId is 4 bytes of timestamp + 5 bytes of randomness generated once
1264
+ PER PROCESS + a 3-byte incrementing counter, so a caller who holds one valid id (their own upload) knows
1265
+ the random part and a counter reference point; neighbouring files sit on neighbouring values. The file
1266
+ routes are also not rate-limited by the framework. So a widened role gate without a per-file rule is
1267
+ practically enumerable, not theoretically.
1268
+
1269
+ Since 11.35.0 the second layer is a declaration rather than code — `file.access`, one value per project
1270
+ class (`'public'`, `'authenticated'`, `'owner'`, `'tenant'`, or `'custom'` to write your own, the
1271
+ default). `'owner'` / `'tenant'` also STAMP the metadata they decide on as the service writes. When the
1272
+ gate is widened past `ADMIN` and neither `file.access` nor an override declares a policy, the service
1273
+ warns at boot — the difference between a decision and an omission is the only thing a boot check can
1274
+ usefully detect.
1275
+
1276
+ Full model, per-driver behaviour and the audit checklist: `src/core/modules/file/README.md` § Access
1277
+ control and `src/core/modules/file/INTEGRATION-CHECKLIST.md`.
1278
+
1220
1279
  ### @UnifiedField() — Schema & Validation
1221
1280
 
1222
1281
  Single decorator that replaces `@Field()`, `@ApiProperty()`, `@IsOptional()`, and more:
@@ -1459,7 +1518,7 @@ const allOrders = await RequestContext.runWithBypassTenantGuard(async () => {
1459
1518
 
1460
1519
  // Exclude specific schemas from tenant filtering
1461
1520
  multiTenancy: {
1462
- excludeSchemas: ['User', 'Session'], // model names, not collection names
1521
+ excludeSchemas: [], // model names, not collection names — this turns isolation OFF per model
1463
1522
  }
1464
1523
  ```
1465
1524
 
@@ -0,0 +1,132 @@
1
+ # Migration Guide: 11.34.0 → 11.34.1
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ |----------|---------|
7
+ | **Breaking Changes** | None in signatures. One **client-visible text** change: validation error messages are now rendered the way class-validator itself renders them (§1, §2) |
8
+ | **Bugfixes** | A custom `message` in `ValidationOptions` is no longer ignored (§1). Default messages no longer reach API clients with raw placeholders such as `$constraint1` (§2) |
9
+ | **Migration Effort** | Nothing to configure. Read §1 and §2 if any test — or any frontend — asserts on exact validation error strings. Read §3 before you put `$value` in a custom message |
10
+
11
+ `MapAndValidatePipe` re-implements class-validator's validation executor, so it also has to render
12
+ error messages the way that executor does. It did neither: it built every message from the
13
+ constraint's `defaultMessage()` alone, and substituted only `$property`.
14
+
15
+ ---
16
+
17
+ ## Quick Migration
18
+
19
+ ```bash
20
+ pnpm update @lenne.tech/nest-server@11.34.1
21
+ pnpm run build
22
+ pnpm test
23
+ ```
24
+
25
+ No configuration change is required, and no source change is required in a typical project.
26
+
27
+ **Vendor-mode projects: this release ADDS a file, so the sync is atomic.**
28
+ `src/core/common/pipes/map-and-validate.pipe.ts` now imports the new
29
+ `src/core/common/helpers/validation-message.helper.ts`. Taking the pipe without the helper does not
30
+ fail a rights check or a test — it fails the build. Sync both files together.
31
+
32
+ ---
33
+
34
+ ## 1. A custom `message` in `ValidationOptions` is now honored
35
+
36
+ Previously the pipe built the message from the constraint's own `defaultMessage()` and never looked
37
+ at `metadata.message`, so the message you wrote was silently discarded and the client received the
38
+ generic default instead.
39
+
40
+ ```typescript
41
+ class UpdateUserInput {
42
+ @IsIn(['admin'], { each: true, message: 'roles may only contain assignable roles (admin)' })
43
+ @IsOptional()
44
+ roles?: string[];
45
+ }
46
+ ```
47
+
48
+ | | Before | After |
49
+ |---|---|---|
50
+ | Response | `each value in roles must be one of the following values: $constraint1` | `roles may only contain assignable roles (admin)` |
51
+
52
+ Custom messages now take precedence over the constraint's default — the same order class-validator's
53
+ own executor uses. The function form (`message: (args) => string`) works too, and its result is
54
+ interpolated afterwards.
55
+
56
+ **What you may notice:** a custom message that was written but never delivered starts being
57
+ delivered. If a frontend or a test was written against the *default* text your project was
58
+ accidentally serving, it now sees the text the decorator actually asks for.
59
+
60
+ ---
61
+
62
+ ## 2. Default messages no longer leak raw placeholders
63
+
64
+ Only `$property` was substituted, so every other token reached the client verbatim:
65
+
66
+ | | Before | After |
67
+ |---|---|---|
68
+ | Response | `each value in roles must be one of the following values: $constraint1` | `each value in roles must be one of the following values: alpha, beta` |
69
+
70
+ All four special tokens are now interpolated — `$constraint1..N`, `$value`, `$property` and
71
+ `$target` — rendered exactly as class-validator renders them (arrays joined with `, `, symbols by
72
+ their description).
73
+
74
+ **Scope of the text change:** 44 of class-validator's built-in messages carry a `$constraint` token,
75
+ so those 44 change text. None of the built-ins uses `$value` or `$target`, so no built-in message
76
+ changes *shape* — they stop showing a placeholder and start showing the value it stood for.
77
+
78
+ **What to check:** any test or frontend string comparison that asserts on a full validation message.
79
+ A test that was pinned to the broken output (`…: $constraint1`) fails and should be updated to the
80
+ interpolated text.
81
+
82
+ This also applies to `@IsDefined`, which since class-validator 0.13 is the one built-in that does not
83
+ register through `ValidateBy` and therefore took a separate code path with the same two gaps.
84
+
85
+ ---
86
+
87
+ ## 3. Security: `$value` echoes the submitted value, and validation errors are not scrubbed
88
+
89
+ `$value` now works — which means a custom message containing it renders the value the client
90
+ submitted, straight back into the error response.
91
+
92
+ **Validation errors bypass the secret-field filtering you may be assuming.**
93
+ `security.secretFields` is applied by `CheckSecurityInterceptor` on the **response** path, whereas
94
+ the `BadRequestException` raised by the pipe goes to the exception filter and is forwarded verbatim.
95
+ The guard inside the renderer is a **type** guard (`boolean | number | string`), not a secrecy guard
96
+ — it will happily echo a password or a token.
97
+
98
+ ```typescript
99
+ // DANGEROUS — the rejected password is echoed back in the error response
100
+ @MinLength(12, { message: 'password "$value" is too short' })
101
+ password: string;
102
+
103
+ // SAFE — describe the rule, never the submitted value
104
+ @MinLength(12, { message: 'password must be at least $constraint1 characters' })
105
+ password: string;
106
+ ```
107
+
108
+ No built-in message uses `$value`, so reaching this is opt-in: it requires a custom message that
109
+ names the token. Note that class-validator's own `validate()` behaves identically — this is not a
110
+ new exposure introduced by the framework, but it becomes reachable in projects where `$value`
111
+ previously did nothing.
112
+
113
+ **Action:** grep your input classes for `$value` in custom messages and confirm none of them sits on
114
+ a credential, token, or otherwise sensitive field.
115
+
116
+ ---
117
+
118
+ ## Under the hood
119
+
120
+ The token renderer is a port of class-validator internals
121
+ (`ValidationUtils.replaceMessageSpecialTokens`, `constraintToString`), which are not part of its
122
+ public API. Because vendor-mode projects resolve their own class-validator, that port can meet a
123
+ version this framework never installed. It is therefore pinned by an invariant test that compares it
124
+ against the installed implementation over a fixture table, so a divergence surfaces as a failing
125
+ test rather than as two renderers disagreeing about one decorator.
126
+
127
+ ---
128
+
129
+ ## Module Documentation
130
+
131
+ - Request lifecycle and the validation pipe: [`docs/REQUEST-LIFECYCLE.md`](../docs/REQUEST-LIFECYCLE.md)
132
+ - Field decorators and validation: `src/core/common/decorators/unified-field.decorator.ts`