@lenne.tech/nest-server 11.41.9 → 11.42.1

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 (67) 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 +4 -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 +6 -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 +1 -0
  30. package/dist/core/modules/tus/core-tus.service.js +29 -0
  31. package/dist/core/modules/tus/core-tus.service.js.map +1 -1
  32. package/dist/core/modules/tus/interfaces/tus-config.interface.d.ts +2 -1
  33. package/dist/core/modules/tus/interfaces/tus-config.interface.js +1 -0
  34. package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -1
  35. package/dist/core/modules/user/core-user.service.d.ts +1 -0
  36. package/dist/core/modules/user/core-user.service.js +24 -0
  37. package/dist/core/modules/user/core-user.service.js.map +1 -1
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +2 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/tsconfig.build.tsbuildinfo +1 -1
  42. package/docs/REQUEST-LIFECYCLE.md +4 -2
  43. package/migration-guides/11.41.8-to-11.41.9.md +5 -0
  44. package/migration-guides/11.41.x-to-11.42.x.md +161 -0
  45. package/migration-guides/11.42.0-to-11.42.1.md +66 -0
  46. package/migration-guides/11.9.x-to-11.10.x.md +3 -2
  47. package/package.json +8 -7
  48. package/src/core/common/helpers/graphql-ws-context.helper.ts +1 -1
  49. package/src/core/common/helpers/outbound-url.helper.ts +310 -0
  50. package/src/core/common/interfaces/server-options.interface.ts +71 -0
  51. package/src/core/common/middleware/request-context.middleware.ts +1 -0
  52. package/src/core/common/services/request-context.service.ts +9 -0
  53. package/src/core/modules/ai/README.md +25 -0
  54. package/src/core/modules/ai/core-ai-mcp.controller.ts +22 -3
  55. package/src/core/modules/ai/services/core-ai-mcp.service.ts +9 -3
  56. package/src/core/modules/ai/tools/ai-tool.registry.ts +33 -3
  57. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +18 -11
  58. package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +14 -0
  59. package/src/core/modules/tenant/README.md +80 -8
  60. package/src/core/modules/tenant/core-tenant-role.helper.ts +75 -0
  61. package/src/core/modules/tenant/core-tenant.guard.ts +78 -5
  62. package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +3 -1
  63. package/src/core/modules/tus/README.md +20 -0
  64. package/src/core/modules/tus/core-tus.service.ts +45 -0
  65. package/src/core/modules/tus/interfaces/tus-config.interface.ts +4 -1
  66. package/src/core/modules/user/core-user.service.ts +53 -0
  67. package/src/index.ts +2 -0
@@ -61,7 +61,8 @@ interface CachedTenantIds {
61
61
  * Cache invalidation message broadcast between replicas via Redis pub/sub
62
62
  */
63
63
  interface TenantCacheInvalidation {
64
- scope: 'all' | 'user';
64
+ scope: 'all' | 'tenant' | 'user';
65
+ tenantId?: string;
65
66
  userId?: string;
66
67
  }
67
68
 
@@ -148,6 +149,9 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
148
149
  */
149
150
  private readonly tenantIdsCache = new Map<string, CachedTenantIds>();
150
151
 
152
+ /** Answers of `multiTenancy.isTenantActive`, keyed by tenant id (same TTL as the other caches). */
153
+ private readonly tenantActiveCache = new Map<string, { active: boolean; expiresAt: number }>();
154
+
151
155
  /** Cache TTL in milliseconds. Configurable via multiTenancy.cacheTtlMs (default: 30s, 0 = disabled) */
152
156
  private cacheTtlMs: number = 30_000;
153
157
  /** Maximum cache entries before eviction */
@@ -223,7 +227,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
223
227
 
224
228
  if (trimmed) {
225
229
  const membership = await this.findMembershipCached(user.id, trimmed);
226
- if (!membership) {
230
+ if (!membership || !(await this.isTenantActive(trimmed))) {
227
231
  return {};
228
232
  }
229
233
  return { tenantId: trimmed, tenantRole: membership.role as string };
@@ -288,6 +292,17 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
288
292
  }
289
293
  this.membershipCache.clear();
290
294
  this.tenantIdsCache.clear();
295
+ this.tenantActiveCache.clear();
296
+ }
297
+
298
+ /**
299
+ * Forget the cached `multiTenancy.isTenantActive` answer for a tenant.
300
+ * Call this right after switching a tenant on or off — otherwise the previous answer holds for up to
301
+ * `cacheTtlMs`. Broadcast to every replica when Redis is enabled.
302
+ */
303
+ invalidateTenant(tenantId: string): void {
304
+ this.tenantActiveCache.delete(tenantId);
305
+ this.publishInvalidation({ scope: 'tenant', tenantId });
291
306
  }
292
307
 
293
308
  /**
@@ -349,6 +364,10 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
349
364
  request,
350
365
  });
351
366
  if (apiTokenKind === ApiTokenKind.TENANT) {
367
+ // The token is bound to its tenant (set on the request by the helper); a deactivated one ends it.
368
+ if (request.tenantId) {
369
+ await this.assertTenantActive(request.tenantId);
370
+ }
352
371
  return true;
353
372
  }
354
373
  // A user token restricted to one tenant is bound to it like a header naming it — the helper has
@@ -397,7 +416,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
397
416
  // Never block access — S_EVERYONE endpoints are always public.
398
417
  if (headerTenantId && request.user?.id) {
399
418
  const membership = await this.findMembershipCached(request.user.id, headerTenantId);
400
- if (membership) {
419
+ if (membership && (await this.isTenantActive(headerTenantId))) {
401
420
  request.tenantId = headerTenantId;
402
421
  request.tenantRole = capApiTokenTenantRole(request.user, membership.role as string) ?? undefined;
403
422
  }
@@ -520,6 +539,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
520
539
  if (!membership) {
521
540
  throw new ForbiddenException('Not a member of this tenant');
522
541
  }
542
+ await this.assertTenantActive(headerTenantId);
523
543
 
524
544
  // A user token with maxTenantRole acts with the lower of its cap and the membership role; `null`
525
545
  // (a role the hierarchy cannot compare with the cap) satisfies no tenant role at all.
@@ -620,7 +640,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
620
640
  const now = Date.now();
621
641
  const cached = this.tenantIdsCache.get(cacheKey);
622
642
  if (cached && now < cached.expiresAt) {
623
- request.tenantIds = cached.ids;
643
+ request.tenantIds = await this.filterActiveTenants(cached.ids);
624
644
  return;
625
645
  }
626
646
  }
@@ -648,7 +668,8 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
648
668
  ids = memberships.map((m) => m.tenant as string);
649
669
  }
650
670
 
651
- request.tenantIds = ids;
671
+ // The cache keeps the membership answer; activity is filtered per request through its own cache.
672
+ request.tenantIds = await this.filterActiveTenants(ids);
652
673
 
653
674
  // Store in process-level cache when enabled
654
675
  if (ttl > 0) {
@@ -689,6 +710,8 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
689
710
  this.clearAll();
690
711
  } else if (parsed?.scope === 'user' && parsed.userId) {
691
712
  this.clearUser(parsed.userId);
713
+ } else if (parsed?.scope === 'tenant' && parsed.tenantId) {
714
+ this.tenantActiveCache.delete(parsed.tenantId);
692
715
  }
693
716
  }
694
717
 
@@ -698,6 +721,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
698
721
  protected clearAll(): void {
699
722
  this.membershipCache.clear();
700
723
  this.tenantIdsCache.clear();
724
+ this.tenantActiveCache.clear();
701
725
  }
702
726
 
703
727
  /**
@@ -814,6 +838,54 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
814
838
  this.tenantIdsCache.delete(key);
815
839
  }
816
840
  }
841
+ for (const [key, entry] of this.tenantActiveCache.entries()) {
842
+ if (now >= entry.expiresAt) {
843
+ this.tenantActiveCache.delete(key);
844
+ }
845
+ }
846
+ }
847
+
848
+ /**
849
+ * Whether a tenant may be used at all — `multiTenancy.isTenantActive`, cached for `cacheTtlMs`.
850
+ *
851
+ * Always true without the hook. Asked only AFTER the membership check, so a non-member learns
852
+ * nothing about a tenant's state. Protected so a custom guard can answer it differently.
853
+ */
854
+ protected async isTenantActive(tenantId: string): Promise<boolean> {
855
+ const hook = ConfigService.configFastButReadOnly?.multiTenancy?.isTenantActive;
856
+ if (typeof hook !== 'function') {
857
+ return true;
858
+ }
859
+ const ttl = this.cacheTtlMs;
860
+ const now = Date.now();
861
+ if (ttl > 0) {
862
+ const cached = this.tenantActiveCache.get(tenantId);
863
+ if (cached && now < cached.expiresAt) {
864
+ return cached.active;
865
+ }
866
+ }
867
+ const active = (await hook(tenantId, { connection: this.memberModel.db })) !== false;
868
+ if (ttl > 0) {
869
+ this.evictIfOverCapacity(this.tenantActiveCache);
870
+ this.tenantActiveCache.set(tenantId, { active, expiresAt: now + ttl });
871
+ }
872
+ return active;
873
+ }
874
+
875
+ /** 403 for a deactivated tenant. */
876
+ private async assertTenantActive(tenantId: string): Promise<void> {
877
+ if (!(await this.isTenantActive(tenantId))) {
878
+ throw new ForbiddenException('Tenant is inactive');
879
+ }
880
+ }
881
+
882
+ /** The tenant list without deactivated tenants — unchanged (and free) without the hook. */
883
+ private async filterActiveTenants(ids: string[]): Promise<string[]> {
884
+ if (typeof ConfigService.configFastButReadOnly?.multiTenancy?.isTenantActive !== 'function') {
885
+ return ids;
886
+ }
887
+ const active = await Promise.all(ids.map((id) => this.isTenantActive(id)));
888
+ return ids.filter((_id, index) => active[index]);
817
889
  }
818
890
 
819
891
  /**
@@ -847,6 +919,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
847
919
  if (!membership) {
848
920
  throw new ForbiddenException('Not a member of this tenant');
849
921
  }
922
+ await this.assertTenantActive(headerTenantId);
850
923
  request.tenantId = headerTenantId;
851
924
  request.tenantRole = capApiTokenTenantRole(user, membership.role as string) ?? undefined;
852
925
  return true;
@@ -174,7 +174,9 @@ every caller allowed to use TUS — and accepts every file type.
174
174
 
175
175
  **Multi-tenancy:** with `multiTenancy` configured, the core validates the tenant header against the
176
176
  caller's memberships and stores `metadata.tenantId` on the finished file; a non-member gets 403.
177
- Without `multiTenancy` nothing changes. Nothing to do in the project either way.
177
+ Without `multiTenancy` nothing changes. On the server nothing to do. On the client, send the tenant header with the TUS upload itself
178
+ (`useLtTusUpload({ headers })`): a header added by the project's API-client interceptor does not reach
179
+ `tus-js-client`, and without it the upload silently stays tenant-less.
178
180
 
179
181
  ---
180
182
 
@@ -107,6 +107,17 @@ The TUS server automatically handles CORS headers for browser-based clients:
107
107
  - `Upload-Offset`
108
108
  - `Upload-Metadata`
109
109
 
110
+ **`Access-Control-Allow-Origin` follows the API's CORS configuration (11.42.1+).** The tus handler
111
+ writes its responses itself, and without `allowedOrigins` `@tus/server` answers `*`. With cookie
112
+ authentication and separate app and API origins the request is credentialed, and every browser
113
+ refuses `*` there — uploads failed in the browser although the server answered 201. The origins now
114
+ come from `buildCorsConfig()` (`appUrl`, `baseUrl`, `cors.allowedOrigins`, `cors.allowAll`), the same
115
+ decision REST, GraphQL and Better-Auth make; `tus.allowedOrigins` overrides. Without credentialed CORS
116
+ (cookies off, or no origin resolvable) the tus default `*` stays.
117
+
118
+ The preflight (`OPTIONS`) is answered by Nest's CORS layer and was never affected — test an
119
+ **authenticated** request with an `Origin` header when checking this.
120
+
110
121
  ---
111
122
 
112
123
  ## Configuration
@@ -392,6 +403,15 @@ for them.
392
403
  | Header naming a tenant the caller belongs to | the finished file gets `metadata.tenantId` |
393
404
  | Header naming a tenant the caller does NOT belong to | **403**, nothing is stored |
394
405
 
406
+ **The header has to come from the TUS client itself.** Projects usually add `X-Tenant-Id` in an
407
+ interceptor of their generated API client, and `tus-js-client` does not go through that client. Without
408
+ an explicit header the upload is accepted, stays tenant-less and bypasses tenant filtering and any
409
+ per-tenant quota, with no error anywhere. Pass it to the upload, e.g. with nuxt-extensions:
410
+
411
+ ```typescript
412
+ const { addFiles } = useLtTusUpload({ headers: { 'X-Tenant-Id': currentTenantId } });
413
+ ```
414
+
395
415
  The tenant is recorded under `TUS_TENANT_METADATA_KEY` (`ltTenantId`), next to the owner under
396
416
  `TUS_OWNER_METADATA_KEY`. Both keys are framework-owned and always overwritten, so a client cannot
397
417
  put either of them into its own `Upload-Metadata`. `metadata.tenantId` on the finished file is what
@@ -9,6 +9,7 @@ import { Readable } from 'stream';
9
9
 
10
10
  import { GridFSHelper } from '../../common/helpers/gridfs.helper';
11
11
  import { ITusConfig } from '../../common/interfaces/server-options.interface';
12
+ import { buildCorsConfig } from '../../common/helpers/cookies.helper';
12
13
  import { ConfigService } from '../../common/services/config.service';
13
14
  import { CoreRedisService } from '../../common/services/core-redis.service';
14
15
  import { CoreS3Service } from '../../common/services/core-s3.service';
@@ -638,6 +639,46 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
638
639
  return { metadata };
639
640
  }
640
641
 
642
+ /**
643
+ * The origins the tus handler may answer a cross-origin request with — or `undefined` to keep the
644
+ * tus server's own default (`*`).
645
+ *
646
+ * The tus handler writes its responses itself, past the API's CORS layer, and without
647
+ * `allowedOrigins` @tus/server sets `Access-Control-Allow-Origin: *`. On a credentialed request —
648
+ * cookie authentication with separate app and API origins — every browser refuses that, so no
649
+ * browser upload could succeed although the server answered 201. Preflights and 401s were fine
650
+ * (Nest answers those), which is why nothing noticed: only an AUTHENTICATED request reaches it.
651
+ *
652
+ * Taken from `tus.allowedOrigins` when set, else from `buildCorsConfig()` — the same decision REST,
653
+ * GraphQL and Better-Auth make. Without credentialed CORS configured the previous default stays.
654
+ */
655
+ protected resolveCorsOrigins(): ((origin: string) => boolean) | undefined {
656
+ const toOrigin = (value: string): string => {
657
+ try {
658
+ return new URL(value).origin;
659
+ } catch {
660
+ return value;
661
+ }
662
+ };
663
+ const fromList = (list: string[]) => {
664
+ const allowed = new Set(list.filter((entry) => typeof entry === 'string').map(toOrigin));
665
+ return (origin: string) => allowed.has(toOrigin(origin));
666
+ };
667
+
668
+ if (Array.isArray(this.config.allowedOrigins) && this.config.allowedOrigins.length) {
669
+ return fromList(this.config.allowedOrigins);
670
+ }
671
+ const options = this.options?.configService?.configFastButReadOnly ?? ConfigService.configFastButReadOnly ?? {};
672
+ const cors = buildCorsConfig(options);
673
+ if (cors.credentials !== true) {
674
+ return undefined;
675
+ }
676
+ if (cors.origin === true) {
677
+ return () => true;
678
+ }
679
+ return Array.isArray(cors.origin) ? fromList(cors.origin as string[]) : undefined;
680
+ }
681
+
641
682
  /**
642
683
  * The VALIDATED tenant a new upload belongs to, or `undefined`.
643
684
  *
@@ -716,8 +757,12 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
716
757
  // correct for the single-replica deployment that configuration describes.
717
758
  const locker = this.createLocker();
718
759
 
760
+ const cors = this.resolveCorsOrigins();
719
761
  const server = new Server({
720
762
  allowedHeaders: this.config.allowedHeaders || DEFAULT_TUS_ALLOWED_HEADERS,
763
+ // Without these, @tus/server answers every request with `Access-Control-Allow-Origin: *`,
764
+ // which a browser refuses on a credentialed (cookie) request — see resolveCorsOrigins().
765
+ ...(cors ? { allowedCredentials: true, allowedOrigins: cors } : {}),
721
766
  datastore,
722
767
  ...(locker ? { locker } : {}),
723
768
  maxSize: this.config.maxSize,
@@ -31,13 +31,16 @@ export const DEFAULT_TUS_ALLOWED_HEADERS: string[] = [];
31
31
  * Default TUS configuration
32
32
  */
33
33
  export const DEFAULT_TUS_CONFIG: Required<
34
- Omit<ITusConfig, 'allowedTypes' | 'creation' | 'expiration'> & {
34
+ Omit<ITusConfig, 'allowedOrigins' | 'allowedTypes' | 'creation' | 'expiration'> & {
35
+ allowedOrigins: undefined;
35
36
  allowedTypes: undefined;
36
37
  creation: boolean;
37
38
  expiration: ITusExpirationConfig;
38
39
  }
39
40
  > = {
40
41
  allowedHeaders: DEFAULT_TUS_ALLOWED_HEADERS,
42
+ // Derived from the API CORS configuration (CoreTusService.resolveCorsOrigins())
43
+ allowedOrigins: undefined,
41
44
  allowedTypes: undefined,
42
45
  checksum: true,
43
46
  concatenation: true,
@@ -5,6 +5,7 @@ import { sha256 } from 'js-sha256';
5
5
  import { Document, Model } from 'mongoose';
6
6
 
7
7
  import { looksLikeSystemRole, SYSTEM_ROLE_PREFIX } from '../../common/enums/role.enum';
8
+ import { accessDeniedException } from '../../common/exceptions/access-denied.exception';
8
9
  import { resolveAppUrlFromConfig } from '../../common/helpers/cookies.helper';
9
10
  import { maskEmail } from '../../common/helpers/logging.helper';
10
11
  import { assignPlain, isQueryableString, prepareServiceOptionsForCreate } from '../../common/helpers/input.helper';
@@ -673,6 +674,8 @@ export abstract class CoreUserService<
673
674
  const oldUser = (await this.mainDbModel.findById(id).lean().exec()) as null | TUser;
674
675
  const oldEmail = oldUser?.email;
675
676
 
677
+ this.assertCredentialChangeAllowed(oldUser, input, serviceOptions);
678
+
676
679
  // Capture the submitted password for the IAM sync before super.update()
677
680
  // hashes it in place.
678
681
  //
@@ -738,6 +741,56 @@ export abstract class CoreUserService<
738
741
  return updatedUser;
739
742
  }
740
743
 
744
+ /**
745
+ * Refuses a change of another account's e-mail address or password where the configuration promises
746
+ * tenant isolation from platform administrators.
747
+ *
748
+ * With multi-tenancy active and `adminBypass: false`, a platform administrator deliberately has no
749
+ * way into a tenant's data. Setting a tenant user's address or password is a way around that: change
750
+ * the address, request a reset, sign in as that person. The same holds for whoever created the
751
+ * account, because `S_CREATOR` may update it and the e-mail field is open to everyone who may.
752
+ *
753
+ * Untouched by design:
754
+ * - the account changing its own credentials,
755
+ * - profile fields, and an e-mail address sent along unchanged,
756
+ * - work without a signed-in user (invitation, password reset), which is the system's own,
757
+ * - every configuration other than multi-tenancy with `adminBypass: false`.
758
+ *
759
+ * `force` does not lift it: the rule is about WHO acts, and system work is defined by the absence of
760
+ * a current user. Protected so a project can widen or narrow it.
761
+ */
762
+ protected assertCredentialChangeAllowed(
763
+ target: null | TUser,
764
+ input: TUserInput,
765
+ serviceOptions?: ServiceOptions,
766
+ ): void {
767
+ const multiTenancy = ConfigService.configFastButReadOnly?.multiTenancy;
768
+ if (!multiTenancy || multiTenancy.enabled === false || multiTenancy.adminBypass !== false) {
769
+ return;
770
+ }
771
+ const currentUser = serviceOptions?.currentUser;
772
+ if (!currentUser?.id || !target) {
773
+ return;
774
+ }
775
+ // No exemption for administrator TARGETS: an administrator may write `roles`, so "promote the
776
+ // tenant account to ADMIN, then change its address" would open every account in two requests.
777
+ const targetId = String((target as any)._id ?? (target as any).id);
778
+ if (String(currentUser.id) === targetId) {
779
+ return;
780
+ }
781
+
782
+ const submitted = input as { email?: unknown; password?: unknown };
783
+ const normalize = (value: unknown) => (typeof value === 'string' ? value.trim().toLowerCase() : value);
784
+ const changesEmail = submitted?.email !== undefined && normalize(submitted.email) !== normalize(target.email);
785
+ const changesPassword = submitted?.password !== undefined;
786
+ if (changesEmail || changesPassword) {
787
+ throw accessDeniedException(
788
+ currentUser,
789
+ 'The e-mail address and password of a tenant account can only be changed by the account itself',
790
+ );
791
+ }
792
+ }
793
+
741
794
  /**
742
795
  * Delete user with automatic cleanup of IAM auth data
743
796
  *
package/src/index.ts CHANGED
@@ -47,6 +47,7 @@ export * from './core/common/helpers/input.helper';
47
47
  export * from './core/common/helpers/logging.helper';
48
48
  export * from './core/common/helpers/meta.helper';
49
49
  export * from './core/common/helpers/model.helper';
50
+ export * from './core/common/helpers/outbound-url.helper';
50
51
  export * from './core/common/helpers/process-diagnostics.helper';
51
52
  export * from './core/common/helpers/register-enum.helper';
52
53
  export * from './core/common/helpers/scim.helper';
@@ -258,6 +259,7 @@ export * from './core/modules/tenant/core-tenant.decorators';
258
259
  export * from './core/modules/tenant/core-tenant.enums';
259
260
  export * from './core/modules/tenant/core-tenant.guard';
260
261
  export * from './core/modules/tenant/core-role-scope.registry';
262
+ export * from './core/modules/tenant/core-tenant-role.helper';
261
263
  export * from './core/modules/tenant/core-tenant.helpers';
262
264
  export * from './core/modules/tenant/core-tenant.module';
263
265
  export * from './core/modules/tenant/core-tenant.service';