@lenne.tech/nest-server 11.41.8 → 11.42.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 (70) hide show
  1. package/.claude/rules/configurable-features.md +4 -3
  2. package/.claude/rules/role-system.md +31 -0
  3. package/.claude/rules/testing.md +4 -4
  4. package/CLAUDE.md +6 -0
  5. package/FRAMEWORK-API.md +3 -2
  6. package/dist/core/common/helpers/graphql-ws-context.helper.js +1 -1
  7. package/dist/core/common/helpers/graphql-ws-context.helper.js.map +1 -1
  8. package/dist/core/common/helpers/outbound-url.helper.d.ts +14 -0
  9. package/dist/core/common/helpers/outbound-url.helper.js +234 -0
  10. package/dist/core/common/helpers/outbound-url.helper.js.map +1 -0
  11. package/dist/core/common/interfaces/server-options.interface.d.ts +5 -0
  12. package/dist/core/common/middleware/request-context.middleware.js +1 -0
  13. package/dist/core/common/middleware/request-context.middleware.js.map +1 -1
  14. package/dist/core/common/services/request-context.service.d.ts +1 -0
  15. package/dist/core/common/services/request-context.service.js.map +1 -1
  16. package/dist/core/modules/ai/core-ai-mcp.controller.js +5 -2
  17. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  18. package/dist/core/modules/ai/services/core-ai-mcp.service.d.ts +1 -1
  19. package/dist/core/modules/ai/services/core-ai-mcp.service.js +3 -2
  20. package/dist/core/modules/ai/services/core-ai-mcp.service.js.map +1 -1
  21. package/dist/core/modules/ai/tools/ai-tool.registry.js +15 -2
  22. package/dist/core/modules/ai/tools/ai-tool.registry.js.map +1 -1
  23. package/dist/core/modules/tenant/core-tenant-role.helper.d.ts +2 -0
  24. package/dist/core/modules/tenant/core-tenant-role.helper.js +36 -0
  25. package/dist/core/modules/tenant/core-tenant-role.helper.js.map +1 -0
  26. package/dist/core/modules/tenant/core-tenant.guard.d.ts +7 -1
  27. package/dist/core/modules/tenant/core-tenant.guard.js +56 -4
  28. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  29. package/dist/core/modules/tus/core-tus.service.d.ts +11 -0
  30. package/dist/core/modules/tus/core-tus.service.js +54 -14
  31. package/dist/core/modules/tus/core-tus.service.js.map +1 -1
  32. package/dist/core/modules/tus/tus.constants.d.ts +1 -0
  33. package/dist/core/modules/tus/tus.constants.js +2 -1
  34. package/dist/core/modules/tus/tus.constants.js.map +1 -1
  35. package/dist/core/modules/tus/tus.module.d.ts +2 -0
  36. package/dist/core/modules/tus/tus.module.js +14 -8
  37. package/dist/core/modules/tus/tus.module.js.map +1 -1
  38. package/dist/core/modules/user/core-user.service.d.ts +1 -0
  39. package/dist/core/modules/user/core-user.service.js +24 -0
  40. package/dist/core/modules/user/core-user.service.js.map +1 -1
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +2 -0
  43. package/dist/index.js.map +1 -1
  44. package/dist/tsconfig.build.tsbuildinfo +1 -1
  45. package/docs/REQUEST-LIFECYCLE.md +5 -3
  46. package/migration-guides/11.41.8-to-11.41.9.md +116 -0
  47. package/migration-guides/11.41.x-to-11.42.x.md +161 -0
  48. package/migration-guides/11.9.x-to-11.10.x.md +3 -2
  49. package/package.json +2 -1
  50. package/src/core/common/helpers/graphql-ws-context.helper.ts +1 -1
  51. package/src/core/common/helpers/outbound-url.helper.ts +310 -0
  52. package/src/core/common/interfaces/server-options.interface.ts +54 -0
  53. package/src/core/common/middleware/request-context.middleware.ts +1 -0
  54. package/src/core/common/services/request-context.service.ts +9 -0
  55. package/src/core/modules/ai/README.md +25 -0
  56. package/src/core/modules/ai/core-ai-mcp.controller.ts +22 -3
  57. package/src/core/modules/ai/services/core-ai-mcp.service.ts +9 -3
  58. package/src/core/modules/ai/tools/ai-tool.registry.ts +33 -3
  59. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +18 -11
  60. package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +14 -0
  61. package/src/core/modules/tenant/README.md +80 -8
  62. package/src/core/modules/tenant/core-tenant-role.helper.ts +75 -0
  63. package/src/core/modules/tenant/core-tenant.guard.ts +78 -5
  64. package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +38 -11
  65. package/src/core/modules/tus/README.md +88 -14
  66. package/src/core/modules/tus/core-tus.service.ts +125 -30
  67. package/src/core/modules/tus/tus.constants.ts +12 -0
  68. package/src/core/modules/tus/tus.module.ts +41 -8
  69. package/src/core/modules/user/core-user.service.ts +53 -0
  70. package/src/index.ts +2 -0
@@ -107,10 +107,10 @@ JWT-based authentication for existing projects:
107
107
  | **Response Filter Interceptor** | Enforces `@Restricted()` field-level access |
108
108
  | **Translation Interceptor** | Applies `_translations` based on `Accept-Language` |
109
109
  | **Secret Fields Removal** | Configurable fallback removal of password, tokens, etc. |
110
- | **RequestContext** | `AsyncLocalStorage`-based context for current user in Mongoose hooks |
110
+ | **RequestContext** | `AsyncLocalStorage`-based context for current user in Mongoose hooks. `fromRequest` (11.42.0) marks a context created for a CLIENT operation (HTTP middleware, GraphQL WebSocket), so service-level checks such as `assertTenantRole()` tell an anonymous request apart from system work |
111
111
  | **Query Complexity** | GraphQL query complexity analysis to prevent DoS |
112
112
  | **Tenant Isolation** | Header-based multi-tenant isolation with membership validation (opt-in) |
113
- | **Tenant Guard** | `CoreTenantGuard` validates tenant membership; system roles (`S_EVERYONE`, `S_USER`, `S_VERIFIED`) are checked as OR alternatives before real roles; hierarchy roles (`@Roles(DefaultHR.MEMBER)`), `@SkipTenantCheck()`, BetterAuth auto-skip (`betterAuth.skipTenantCheck`) |
113
+ | **Tenant Guard** | `CoreTenantGuard` validates tenant membership — and, with `multiTenancy.isTenantActive` (11.42.0), that the tenant is not deactivated; system roles (`S_EVERYONE`, `S_USER`, `S_VERIFIED`) are checked as OR alternatives before real roles; hierarchy roles (`@Roles(DefaultHR.MEMBER)`), `@SkipTenantCheck()`, BetterAuth auto-skip (`betterAuth.skipTenantCheck`) |
114
114
  | **Tenant Plugin Safety Net** | Mongoose tenant plugin throws `ForbiddenException` when tenant-schema is accessed without valid tenant context |
115
115
  | **API Tokens** | `apiTokens` config (opt-in): USER tokens act as their user without global roles; TENANT tokens (multi-tenancy only) act with the lowest tenant role in their own tenant. `CoreApiTokenMiddleware` authenticates `Authorization: Bearer` / `x-api-key`; every guard denies a token unless the route declares `@ApiTokenScopes()`. Signed short-lived assertions for embedded pages. See `src/core/modules/api-token/README.md` |
116
116
 
@@ -165,7 +165,7 @@ JWT-based authentication for existing projects:
165
165
  | **REST Endpoints** | `GET /files/id/:id`, `GET /files/:filename` (core, gated by `file.downloadRoles`, default ADMIN); `POST /files/upload`, `DELETE /files/:id` (project-specific) |
166
166
  | **GraphQL Endpoints** | `getFileInfo` (`file.downloadRoles`), `uploadFile` / `uploadFiles` (`file.uploadRoles`), `deleteFile` (`file.deleteRoles`) — all default ADMIN |
167
167
  | **File access control** | Roles are the coarse filter; per-file rules go in `CoreFileService.checkRights()` using metadata written at upload time. Cover BOTH the `id` and the `filename` branch — the filename route authorizes on the by-name lookup alone when presigned S3 downloads are on, and `deleteFileByName()` always does. Working reference: `src/server/modules/file/file.service.ts` (with `file.downloadRoles: [S_USER]` in `src/config.env.ts`, so the rule is actually reached). Both file classes carry `@SkipTenantCheck()` — GridFS is not tenant-scoped |
168
- | **TUS Module** | Resumable uploads via tus.io protocol (creation, termination, expiration), gated by `tus.roles` (default `S_USER`); `OPTIONS` stays public for the CORS preflight |
168
+ | **TUS Module** | Resumable uploads via tus.io protocol (creation, termination, expiration), gated by `tus.roles` (default `S_USER`); `OPTIONS` stays public for the CORS preflight. Records owner and, with `multiTenancy` only, the validated tenant at upload creation; a custom service plugs in via `TusModule.forRoot({ service })` |
169
169
  | **GridFS Migration** | Completed TUS uploads auto-migrate to GridFS |
170
170
  | **CORS Support** | Automatic CORS headers for browser uploads |
171
171
 
@@ -373,6 +373,8 @@ The following diagram shows the exact order of execution from HTTP request to re
373
373
  | - Non-admin + header + no membership = always 403 |
374
374
  | - Checks configurable roleHierarchy levels |
375
375
  | - Admin bypass: sets isAdminBypass (sees all data) |
376
+ | - multiTenancy.isTenantActive (11.42.0): deactivated |
377
+ | tenant = 403 / dropped from tenantIds (not admin) |
376
378
  | - Sets tenantId in RequestContext |
377
379
  | - @SkipTenantCheck() opts out of tenant validation |
378
380
  | - BetterAuth auto-skip: IAM handlers skip tenant |
@@ -0,0 +1,116 @@
1
+ # Migration Guide: 11.41.8 → 11.41.9
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
7
+ | **New Features** | A custom TUS service via `TusModule.forRoot({ service })`, and an overridable `onUploadCreate()` (§1) |
8
+ | **Behaviour Changes** | Only with `multiTenancy` configured: TUS uploads record the validated tenant, and a tenant header naming a tenant the caller does not belong to is refused with 403 (§2) |
9
+ | **Breaking Changes** | None for projects without `multiTenancy` |
10
+ | **Migration Effort** | None — `pnpm update`. Projects that want upload rules (quota, validation) adopt §1 |
11
+
12
+ ## Quick Migration
13
+
14
+ ```bash
15
+ pnpm update @lenne.tech/nest-server
16
+ pnpm run build && pnpm test
17
+ ```
18
+
19
+ Vendor-mode projects pick up the core update; nothing else to change.
20
+
21
+ ## What's New in 11.41.9
22
+
23
+ ### 1. A custom TUS service, and a hook before the first byte
24
+
25
+ Until now `TusModule` constructed `CoreTusService` directly in both of its provider factories, so a
26
+ project could not plug in a subclass at all — and `onUploadCreate` sat inline inside a private method,
27
+ so even a subclass could not have changed it. Both are open now:
28
+
29
+ ```typescript
30
+ import { CoreTusService } from '@lenne.tech/nest-server';
31
+ import { Upload } from '@tus/server';
32
+
33
+ export class TusService extends CoreTusService {
34
+ protected override async onUploadCreate(req: any, upload: Upload) {
35
+ const quota = this.options?.moduleRef?.get(QuotaService, { strict: false });
36
+ if (!(await quota?.hasRoomFor(req, upload.size))) {
37
+ const error = new Error('Storage quota exceeded');
38
+ (error as any).status_code = 413;
39
+ throw error;
40
+ }
41
+ return super.onUploadCreate(req, upload);
42
+ }
43
+ }
44
+
45
+ // server.module.ts
46
+ TusModule.forRoot({ controller: TusController, service: TusService });
47
+ ```
48
+
49
+ `onUploadCreate(req, upload)` runs once per upload, after the client declared `Upload-Length` and
50
+ before anything is stored — the one point at which a quota can refuse an upload without the store
51
+ having grown first.
52
+
53
+ Three rules, all in the TUS [INTEGRATION-CHECKLIST](../src/core/modules/tus/INTEGRATION-CHECKLIST.md)
54
+ Step 2:
55
+
56
+ - **Keep the constructor signature `(connection, options?)`.** The module constructs the service
57
+ itself, with exactly those arguments. Project providers are reached at call time through
58
+ `this.options?.moduleRef`, which the module now passes in.
59
+ - **Return `super.onUploadCreate()`.** It records the owner and the tenant and enforces
60
+ `allowedTypes`; skipping it produces owner-less uploads and accepts every file type.
61
+ - **Throw an `Error` with `status_code`** to refuse — that is how `@tus/server` turns it into a
62
+ response.
63
+
64
+ ### 2. With multi-tenancy: TUS uploads carry their tenant
65
+
66
+ **Projects without `multiTenancy` see no change** — no header is read, no field is written.
67
+
68
+ With `multiTenancy` configured (and not `enabled: false`), TUS uploads used to carry no tenant at
69
+ all: the routes carry `@SkipTenantCheck()`, the file stores sit outside Mongoose, and so every
70
+ TUS-uploaded file bypassed tenant filtering and any per-tenant storage accounting. Now an upload that
71
+ sends the tenant header is validated against the caller's active memberships — the same check
72
+ GraphQL subscriptions use — and the finished file gets `metadata.tenantId`:
73
+
74
+ | Request | 11.41.8 | 11.41.9 |
75
+ | ---------------------------------------------------- | --------------------- | ----------------------- |
76
+ | No tenant header | tenant-less | tenant-less (unchanged) |
77
+ | Header naming a tenant the caller belongs to | tenant-less | `metadata.tenantId` set |
78
+ | Header naming a tenant the caller does NOT belong to | accepted, tenant-less | **403**, nothing stored |
79
+
80
+ **Send the header from the TUS client.** A tenant header added by an interceptor of the project's
81
+ generated API client does not reach `tus-js-client`. Without it the upload silently stays tenant-less,
82
+ outside tenant filtering and per-tenant quotas. Pass it explicitly, e.g.
83
+ `useLtTusUpload({ headers: { 'X-Tenant-Id': currentTenantId } })`.
84
+
85
+ The tenant is recorded under the framework-owned metadata key `TUS_TENANT_METADATA_KEY`
86
+ (`ltTenantId`), next to the owner key. Like the owner key it is always overwritten, so a client
87
+ cannot put a tenant into its own `Upload-Metadata`.
88
+
89
+ **What this changes downstream:** the file module's `'tenant'` access preset and any
90
+ `checkRights()` rule reading `metadata.tenantId` now see the tenant on TUS-uploaded files. Under the
91
+ `'tenant'` preset such files used to be refused for every non-admin (no tenant on the file); new
92
+ uploads are now readable within their tenant. Files uploaded before 11.41.9 keep no tenant.
93
+
94
+ ## Compatibility Notes
95
+
96
+ | Pattern | Status |
97
+ | --------------------------------------------------------------------- | ----------------------------------------------------------------- |
98
+ | `TusModule.forRoot()` / `forRoot({ controller })` | Unchanged |
99
+ | Project without `multiTenancy` | Unchanged — nothing read, nothing written (§2) |
100
+ | Multi-tenant client sending a tenant header it is a member of | Works; files now carry `tenantId` (§2) |
101
+ | Multi-tenant client sending a header for a foreign tenant | Now 403 (§2) |
102
+ | Own `TusModule` replacement that provides a `CoreTusService` subclass | Keeps working; `forRoot({ service })` is now the simpler way (§1) |
103
+
104
+ ## Troubleshooting
105
+
106
+ | Symptom | Cause | Fix |
107
+ | ---------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------- |
108
+ | TUS `POST` answers 403 after the update | The tenant header names a tenant the caller is not an active member of | Send the caller's own tenant, or no header (§2) |
109
+ | An `onUploadCreate` override never runs | The subclass is not registered | `TusModule.forRoot({ service: TusService })` (§1) |
110
+ | Dependencies are `undefined` in the service subclass | Declared as constructor parameters | Resolve via `this.options?.moduleRef` (§1) |
111
+ | Uploads have no `ownerId` after adding an override | `onUploadCreate` override does not call `super` | `return super.onUploadCreate(req, upload)` (§1) |
112
+
113
+ ## Module Documentation
114
+
115
+ - [TUS README](../src/core/modules/tus/README.md) — Custom Service, Validate an Upload Before It Starts, Tenant Scoping
116
+ - [TUS INTEGRATION-CHECKLIST](../src/core/modules/tus/INTEGRATION-CHECKLIST.md) — Step 2
@@ -0,0 +1,161 @@
1
+ # Migration Guide: 11.41.x → 11.42.x
2
+
3
+ ## Why this is a MINOR
4
+
5
+ Per `.claude/rules/versioning.md` the MAJOR mirrors NestJS, so **MINOR is where breaking changes
6
+ live**. Two behaviour changes hit multi-tenant projects only: with `multiTenancy.adminBypass: false`
7
+ a platform administrator can no longer set a tenant account's e-mail address or password (§4), and
8
+ the AI tool registry resolves tool roles by the membership role of the request's tenant (§3). A call
9
+ that used to succeed can now be refused, so this is `11.42.0` rather than a patch.
10
+
11
+ **Projects without `multiTenancy` see no behaviour change.** Everything else in this release is
12
+ opt-in.
13
+
14
+ ## Overview
15
+
16
+ | Category | Details |
17
+ | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
18
+ | **New (opt-in)** | `outboundFetch()` and friends — an SSRF guard for URLs a user entered (§1) |
19
+ | **New (opt-in)** | `multiTenancy.isTenantActive` — a deactivated tenant stops working (§2) |
20
+ | **New** | `assertTenantRole()` / `hasTenantRole()` — tenant roles enforced in services, on every entry point (§3) |
21
+ | **Behaviour change** (multi-tenancy) | AI tool roles (chat and MCP) resolve like `@Roles()` in the tenant guard (§3) |
22
+ | **Behaviour change** (`adminBypass: false`) | No credential changes of other accounts through `updateUser`; no ADMIN bypass in the AI tool registry (§4) |
23
+ | **Dependency** | `undici` 7.30.0 is a runtime dependency (§1) — nothing to do |
24
+ | **Migration Effort** | None without multi-tenancy. With it: check §3 and §4; adopt §1–§3 where they apply |
25
+
26
+ ## Quick Migration
27
+
28
+ ```bash
29
+ pnpm update @lenne.tech/nest-server
30
+ pnpm run build && pnpm test
31
+ ```
32
+
33
+ Vendor-mode projects pick up the core update; `undici` reaches them through `dependencies`.
34
+
35
+ ## What's New in 11.42.0
36
+
37
+ ### 1. SSRF guard for user-entered URLs
38
+
39
+ When the server calls a URL a USER entered — a webhook, an export target, an ERP address — it calls it
40
+ from inside its own network. Without a check, a user points it at the database, other containers or
41
+ the cloud metadata service (`169.254.169.254`) and reads the answer back.
42
+
43
+ ```typescript
44
+ import { outboundFetch, OutboundUrlBlockedError } from '@lenne.tech/nest-server';
45
+
46
+ const response = await outboundFetch(webhook.url, { body, headers, method: 'POST' });
47
+ // an ERP in the same network, freed explicitly:
48
+ await outboundFetch(target.url, init, { allowedHosts: ['erp.internal:8443'] });
49
+ ```
50
+
51
+ - **Only for user-entered URLs.** Configured targets (AI providers, SMTP, S3, Redis, Brevo) do not use
52
+ it; nothing is applied globally.
53
+ - Checked twice: on the URL before sending (`OutboundUrlBlockedError`, readable) and on every
54
+ connection — the only place that sees a redirect's next hop and DNS rebinding. A refusal on a later
55
+ hop surfaces as undici's `TypeError: fetch failed` with the `OutboundUrlBlockedError` as `cause`.
56
+ - Internal targets are allowed in `local`, `e2e` and `ci`, refused elsewhere — including the deployed `test` stage.
57
+ `security.allowPrivateOutboundTargets` (`NSC__SECURITY__ALLOW_PRIVATE_OUTBOUND_TARGETS=true|false`)
58
+ decides explicitly.
59
+ - `assertOutboundUrlAllowed(url)` checks without sending; `createOutboundDispatcher()` returns the
60
+ guarded undici `Agent` for code that uses undici directly. `outboundFetch()` uses undici's own
61
+ `fetch`: Node 22's built-in `fetch` bundles a different undici major than the agent.
62
+
63
+ ### 2. Deactivated tenants stop working
64
+
65
+ `CoreTenantGuard` validated the membership only, so a tenant a project switched off kept working for
66
+ its members, its API tokens and every request without a tenant header. Tell the guard:
67
+
68
+ ```typescript
69
+ // config.env.ts
70
+ multiTenancy: {
71
+ isTenantActive: async (tenantId, { connection }) =>
72
+ (await connection.collection('tenants').findOne({ _id: new Types.ObjectId(tenantId) }))?.active !== false,
73
+ }
74
+ ```
75
+
76
+ Then call `tenantGuard.invalidateTenant(tenantId)` right after switching a tenant on or off (answers are
77
+ cached for `cacheTtlMs`; the call broadcasts to every replica with Redis). Effects: 403 on header paths
78
+ and for tenant tokens, no tenant context on public routes, the tenant dropped from the header-less
79
+ tenant list, no tenant for a new WebSocket subscription (one already open keeps its tenant until the client reconnects — the same holds today for a removed membership). A platform administrator under `adminBypass` still gets in.
80
+ Asked after the membership check, so a non-member learns nothing. A function cannot come through
81
+ `NSC__*` — set it in `config.env.ts`.
82
+
83
+ **If your project already has its own "tenant active" guard**, it can stay; the hook additionally
84
+ covers the header-less tenant list, which such a guard usually misses.
85
+
86
+ ### 3. Tenant roles on every entry point — services, MCP and AI chat
87
+
88
+ `@Roles()` protects a route. MCP tools, AI tools, queue processors and other services call a service
89
+ directly and never pass it. Enforce a tenant role in the service:
90
+
91
+ ```typescript
92
+ import { assertTenantRole, DefaultHR } from '@lenne.tech/nest-server';
93
+
94
+ async updateSettings(input: SettingsInput, serviceOptions?: ServiceOptions) {
95
+ assertTenantRole(DefaultHR.OWNER, 'Only owners may change the settings');
96
+ // …
97
+ }
98
+ ```
99
+
100
+ Signed in without the role → 403; anonymous client request → 401; system work (cron, migration, seed —
101
+ no client request) → passes. The last two are told apart by the new `RequestContext.fromRequest`,
102
+ which `RequestContextMiddleware` and the GraphQL WebSocket context set. **If you build a
103
+ `RequestContext` yourself for a client operation** (a custom transport), set `fromRequest: true`.
104
+
105
+ **Behaviour change, multi-tenancy only:** the AI tool registry (chat and `/ai/mcp`) now resolves a
106
+ tool's `roles` like the tenant guard resolves `@Roles()` — a global role against `user.roles`, a tenant
107
+ role against the membership role in the request's tenant (by hierarchy), and no ADMIN bypass where
108
+ `adminBypass: false`. Before, every role was compared with the global `user.roles`. If a tool lists a
109
+ TENANT role and a user relied on holding that name globally, the tool now follows the membership
110
+ instead. An AI client acts within the signed-in user's rights and tenant scope — never beyond.
111
+
112
+ **MCP sessions follow the current user.** A session used to keep the user object from its `initialize`
113
+ request for its whole life, so a role revoked mid-session kept working over MCP until the client
114
+ reconnected. The controller now refreshes the session's user on every request, and
115
+ `CoreAiMcpService.createServer()` receives a **getter** (`() => AiToolUser`) instead of the user object.
116
+ A project that overrides `createServer(user)` must accept the getter (a plain object is still accepted
117
+ when you call it yourself) and read it per request, not once.
118
+
119
+ ### 4. `adminBypass: false` holds for credentials
120
+
121
+ With multi-tenancy and `adminBypass: false`, `PATCH /users/:id` / `updateUser` now refuse (403) a change
122
+ of another account's e-mail address or password — whoever asks, including the account's creator
123
+ (`S_CREATOR` may update, and the e-mail field is open to every updater). Changing the address and
124
+ requesting a reset was a way to take over a tenant account that `adminBypass: false` exists to rule
125
+ out.
126
+
127
+ Unaffected: the account itself, profile fields, an unchanged address sent along, deletion, and
128
+ system work without a current user (invitation, password reset). Administrator accounts are NOT
129
+ exempt as targets — an administrator may write `roles`, so "promote to ADMIN, then change the address"
130
+ would otherwise open every account in two requests. Support for a
131
+ tenant user goes through the password-reset flow. A project that needs a different rule overrides
132
+ `CoreUserService.assertCredentialChangeAllowed()`.
133
+
134
+ ## Compatibility Notes
135
+
136
+ | Pattern | Status |
137
+ | --------------------------------------------------- | --------------------------------------------------------------------- |
138
+ | Project without `multiTenancy` | Unchanged |
139
+ | `multiTenancy` with `adminBypass` on (default) | Unchanged except §3 (AI tool roles follow the membership) |
140
+ | `multiTenancy` with `adminBypass: false` | §3 and §4 apply |
141
+ | Project-own tenant-active guard | Keeps working; the hook adds the header-less path |
142
+ | Project-own SSRF check | Keeps working; consider `outboundFetch()` for redirects and rebinding |
143
+ | Custom `RequestContext.run()` for client operations | Set `fromRequest: true` (§3) |
144
+ | Override of `CoreAiMcpService.createServer(user)` | Receives a getter now — call it per request (§3) |
145
+
146
+ ## Troubleshooting
147
+
148
+ | Symptom | Cause | Fix |
149
+ | -------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------- |
150
+ | `403` when an admin changes a tenant user's e-mail or password | `adminBypass: false` (§4) | Let the user change it, or use the reset flow |
151
+ | An AI tool disappeared for some users | Tool roles follow the membership under multi-tenancy (§3) | List the tenant role the users hold in the tenant, or a global role |
152
+ | `Tenant is inactive` (403) | `isTenantActive` returned `false` | Reactivate, then `invalidateTenant(tenantId)` |
153
+ | A reactivated tenant is still refused | Cached answer | `tenantGuard.invalidateTenant(tenantId)` |
154
+ | `OutboundUrlBlockedError` locally | `security.allowPrivateOutboundTargets: false` set, or a non-local `env` | Unset it, or list the host in `allowedHosts` |
155
+ | `TypeError: fetch failed` from `outboundFetch()` | A redirect or DNS answer pointed inside | Inspect `error.cause` |
156
+
157
+ ## Module Documentation
158
+
159
+ - [Tenant README](../src/core/modules/tenant/README.md) — Admin Bypass, Deactivated Tenants, Service-Level Role Checks
160
+ - [Tenant INTEGRATION-CHECKLIST](../src/core/modules/tenant/INTEGRATION-CHECKLIST.md) — step 11
161
+ - [AI README](../src/core/modules/ai/README.md) — Tool access is the user's access
@@ -124,7 +124,8 @@ New `CoreBetterAuthChallengeService` enables Passkey authentication in **cookiel
124
124
  **Client-side usage (JWT mode):**
125
125
  ```typescript
126
126
  // Use composable methods which handle challengeId automatically
127
- const { authenticateWithPasskey, registerPasskey } = useBetterAuth();
127
+ // useLtAuth() from @lenne.tech/nuxt-extensions; starters before nuxt-extensions called it useBetterAuth()
128
+ const { authenticateWithPasskey, registerPasskey } = useLtAuth();
128
129
 
129
130
  // Authentication
130
131
  const result = await authenticateWithPasskey();
@@ -495,7 +496,7 @@ betterAuth: {
495
496
  **Solution:** Ensure you're using the composable methods that handle `challengeId`:
496
497
  ```typescript
497
498
  // CORRECT: Use composable methods
498
- const { authenticateWithPasskey } = useBetterAuth();
499
+ const { authenticateWithPasskey } = useLtAuth(); // formerly useBetterAuth() in the starter
499
500
  await authenticateWithPasskey();
500
501
 
501
502
  // WRONG: Direct client call (doesn't handle challengeId in JWT mode)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.41.8",
3
+ "version": "11.42.0",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -137,6 +137,7 @@
137
137
  "rxjs": "7.8.2",
138
138
  "supertest": "7.3.0",
139
139
  "ts-morph": "28.0.0",
140
+ "undici": "7.30.0",
140
141
  "ws": "8.21.3",
141
142
  "yuml-diagram": "1.2.0"
142
143
  },
@@ -93,7 +93,7 @@ export async function buildWsRequestContext(contextValue: any): Promise<IRequest
93
93
  }
94
94
 
95
95
  const user = readUser(contextValue);
96
- const context: IRequestContext = { currentUser: user };
96
+ const context: IRequestContext = { currentUser: user, fromRequest: true };
97
97
 
98
98
  // Language: same source as the HTTP path, when the transport carried it.
99
99
  const acceptLanguage =