@lenne.tech/nest-server 11.42.2 → 11.42.3
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/configurable-features.md +2 -1
- package/.claude/rules/role-system.md +31 -3
- package/.claude/rules/testing.md +4 -4
- package/CLAUDE.md +5 -0
- package/FRAMEWORK-API.md +1 -1
- package/dist/core/common/helpers/graphql-ws-connection.helper.d.ts +43 -0
- package/dist/core/common/helpers/graphql-ws-connection.helper.js +219 -0
- package/dist/core/common/helpers/graphql-ws-connection.helper.js.map +1 -0
- package/dist/core/common/helpers/graphql-ws-context.helper.d.ts +2 -1
- package/dist/core/common/helpers/graphql-ws-context.helper.js +54 -18
- package/dist/core/common/helpers/graphql-ws-context.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +1 -0
- package/dist/core/modules/auth/services/core-auth.service.d.ts +1 -0
- package/dist/core/modules/auth/services/core-auth.service.js +19 -0
- package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.d.ts +2 -1
- package/dist/core/modules/better-auth/better-auth.config.js +27 -1
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.js +4 -0
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +2 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +20 -8
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/user/core-user.service.js +11 -6
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core.module.d.ts +3 -0
- package/dist/core.module.js +100 -42
- package/dist/core.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/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +1 -0
- package/migration-guides/11.42.2-to-11.42.3.md +145 -0
- package/package.json +1 -1
- package/src/core/common/helpers/graphql-ws-connection.helper.ts +382 -0
- package/src/core/common/helpers/graphql-ws-context.helper.ts +91 -18
- package/src/core/common/interfaces/server-options.interface.ts +32 -2
- package/src/core/modules/auth/services/core-auth.service.ts +37 -0
- package/src/core/modules/better-auth/better-auth.config.ts +46 -1
- package/src/core/modules/better-auth/core-better-auth.service.ts +9 -1
- package/src/core/modules/tenant/README.md +8 -8
- package/src/core/modules/tenant/core-tenant.guard.ts +41 -12
- package/src/core/modules/user/core-user.service.ts +19 -6
- package/src/core.module.ts +166 -55
- package/src/index.ts +1 -0
package/src/core.module.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DynamicModule,
|
|
4
|
+
Global,
|
|
5
|
+
Logger,
|
|
6
|
+
MiddlewareConsumer,
|
|
7
|
+
Module,
|
|
8
|
+
NestModule,
|
|
9
|
+
UnauthorizedException,
|
|
10
|
+
} from '@nestjs/common';
|
|
3
11
|
import { APP_INTERCEPTOR, APP_PIPE, DiscoveryModule } from '@nestjs/core';
|
|
4
12
|
import { GraphQLModule } from '@nestjs/graphql';
|
|
5
13
|
import { MongooseModule } from '@nestjs/mongoose';
|
|
@@ -8,6 +16,7 @@ import graphqlUploadExpress = require('graphql-upload/graphqlUploadExpress.js');
|
|
|
8
16
|
import mongoose from 'mongoose';
|
|
9
17
|
|
|
10
18
|
import { merge } from './core/common/helpers/config.helper';
|
|
19
|
+
import { registerWsConnection } from './core/common/helpers/graphql-ws-connection.helper';
|
|
11
20
|
import {
|
|
12
21
|
buildRequestContextAwareExecute,
|
|
13
22
|
buildRequestContextAwareSubscribe,
|
|
@@ -48,6 +57,7 @@ import { MailjetService } from './core/common/services/mailjet.service';
|
|
|
48
57
|
import { ModelDocService } from './core/common/services/model-doc.service';
|
|
49
58
|
import { TemplateService } from './core/common/services/template.service';
|
|
50
59
|
import { CoreAiModule } from './core/modules/ai/core-ai.module';
|
|
60
|
+
import { isLegacyJwt } from './core/modules/better-auth/core-better-auth-token.helper';
|
|
51
61
|
import { CoreBetterAuthUserMapper } from './core/modules/better-auth/core-better-auth-user.mapper';
|
|
52
62
|
import { CoreBetterAuthModule } from './core/modules/better-auth/core-better-auth.module';
|
|
53
63
|
import { CoreBetterAuthService } from './core/modules/better-auth/core-better-auth.service';
|
|
@@ -665,23 +675,22 @@ export class CoreModule implements NestModule {
|
|
|
665
675
|
connectionParams?.Authorization?.split(' ')[1] ?? headers.Authorization?.split(' ')[1];
|
|
666
676
|
|
|
667
677
|
if (authToken) {
|
|
668
|
-
//
|
|
669
|
-
const
|
|
670
|
-
headers: { authorization: `Bearer ${authToken}` },
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
if (!session || !sessionUser) {
|
|
674
|
-
throw new UnauthorizedException('Invalid or expired session');
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Map to full user with roles
|
|
678
|
-
const user = await userMapper.mapSessionUser(sessionUser);
|
|
678
|
+
// Resolved like an HTTP request with the same header — see authenticateIamWsToken().
|
|
679
|
+
const user = await CoreModule.authenticateIamWsToken(betterAuthService, userMapper, authToken);
|
|
679
680
|
if (!user) {
|
|
680
|
-
throw new UnauthorizedException('
|
|
681
|
+
throw new UnauthorizedException('Invalid or expired session');
|
|
681
682
|
}
|
|
682
683
|
|
|
683
684
|
extra.user = user;
|
|
684
685
|
extra.headers = connectionParams ?? headers;
|
|
686
|
+
// Re-checked for the life of the socket — see graphql-ws-connection.helper.ts.
|
|
687
|
+
registerWsConnection({
|
|
688
|
+
carrier: extra,
|
|
689
|
+
reauthenticate: () =>
|
|
690
|
+
CoreModule.authenticateIamWsToken(betterAuthService, userMapper, authToken),
|
|
691
|
+
socket: extra.socket,
|
|
692
|
+
user,
|
|
693
|
+
});
|
|
685
694
|
return extra;
|
|
686
695
|
}
|
|
687
696
|
|
|
@@ -690,29 +699,30 @@ export class CoreModule implements NestModule {
|
|
|
690
699
|
},
|
|
691
700
|
},
|
|
692
701
|
'subscriptions-transport-ws': {
|
|
693
|
-
onConnect: async (connectionParams) => {
|
|
702
|
+
onConnect: async (connectionParams, webSocket) => {
|
|
694
703
|
const enableAuth = graphQlOpts?.enableSubscriptionAuth ?? true;
|
|
695
704
|
|
|
696
705
|
if (enableAuth) {
|
|
697
706
|
const authToken: string = connectionParams?.Authorization?.split(' ')[1];
|
|
698
707
|
|
|
699
708
|
if (authToken) {
|
|
700
|
-
//
|
|
701
|
-
const
|
|
702
|
-
headers: { authorization: `Bearer ${authToken}` },
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
if (!session || !sessionUser) {
|
|
706
|
-
throw new UnauthorizedException('Invalid or expired session');
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Map to full user with roles
|
|
710
|
-
const user = await userMapper.mapSessionUser(sessionUser);
|
|
709
|
+
// Resolved like an HTTP request with the same header — see authenticateIamWsToken().
|
|
710
|
+
const user = await CoreModule.authenticateIamWsToken(betterAuthService, userMapper, authToken);
|
|
711
711
|
if (!user) {
|
|
712
|
-
throw new UnauthorizedException('
|
|
712
|
+
throw new UnauthorizedException('Invalid or expired session');
|
|
713
713
|
}
|
|
714
714
|
|
|
715
|
-
|
|
715
|
+
// subscriptions-transport-ws hands each operation a copy of this object; the
|
|
716
|
+
// registration survives that copy — see graphql-ws-connection.helper.ts.
|
|
717
|
+
const connectionContext = { headers: connectionParams, user };
|
|
718
|
+
registerWsConnection({
|
|
719
|
+
carrier: connectionContext,
|
|
720
|
+
reauthenticate: () =>
|
|
721
|
+
CoreModule.authenticateIamWsToken(betterAuthService, userMapper, authToken),
|
|
722
|
+
socket: webSocket,
|
|
723
|
+
user,
|
|
724
|
+
});
|
|
725
|
+
return connectionContext;
|
|
716
726
|
}
|
|
717
727
|
|
|
718
728
|
throw new UnauthorizedException('Missing authentication token');
|
|
@@ -781,21 +791,22 @@ export class CoreModule implements NestModule {
|
|
|
781
791
|
connectionParams?.Authorization?.split(' ')[1] ?? headers.Authorization?.split(' ')[1];
|
|
782
792
|
|
|
783
793
|
if (authToken) {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
if (!session || !sessionUser) {
|
|
789
|
-
throw new UnauthorizedException('Invalid or expired session');
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
const user = await userMapper.mapSessionUser(sessionUser);
|
|
794
|
+
// Resolved like an HTTP request with the same header — see authenticateIamWsToken().
|
|
795
|
+
const user = await CoreModule.authenticateIamWsToken(betterAuthService, userMapper, authToken);
|
|
793
796
|
if (!user) {
|
|
794
|
-
throw new UnauthorizedException('
|
|
797
|
+
throw new UnauthorizedException('Invalid or expired session');
|
|
795
798
|
}
|
|
796
799
|
|
|
797
800
|
extra.user = user;
|
|
798
801
|
extra.headers = connectionParams ?? headers;
|
|
802
|
+
// Re-checked for the life of the socket — see graphql-ws-connection.helper.ts.
|
|
803
|
+
registerWsConnection({
|
|
804
|
+
carrier: extra,
|
|
805
|
+
reauthenticate: () =>
|
|
806
|
+
CoreModule.authenticateIamWsToken(betterAuthService, userMapper, authToken),
|
|
807
|
+
socket: extra.socket,
|
|
808
|
+
user,
|
|
809
|
+
});
|
|
799
810
|
return extra;
|
|
800
811
|
}
|
|
801
812
|
|
|
@@ -804,7 +815,7 @@ export class CoreModule implements NestModule {
|
|
|
804
815
|
},
|
|
805
816
|
},
|
|
806
817
|
'subscriptions-transport-ws': {
|
|
807
|
-
onConnect: async (connectionParams) => {
|
|
818
|
+
onConnect: async (connectionParams, webSocket) => {
|
|
808
819
|
const enableAuth = graphQlOpts?.enableSubscriptionAuth ?? true;
|
|
809
820
|
|
|
810
821
|
if (enableAuth) {
|
|
@@ -818,20 +829,23 @@ export class CoreModule implements NestModule {
|
|
|
818
829
|
const authToken: string = connectionParams?.Authorization?.split(' ')[1];
|
|
819
830
|
|
|
820
831
|
if (authToken) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
if (!session || !sessionUser) {
|
|
826
|
-
throw new UnauthorizedException('Invalid or expired session');
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
const user = await userMapper.mapSessionUser(sessionUser);
|
|
832
|
+
// Resolved like an HTTP request with the same header — see authenticateIamWsToken().
|
|
833
|
+
const user = await CoreModule.authenticateIamWsToken(betterAuthService, userMapper, authToken);
|
|
830
834
|
if (!user) {
|
|
831
|
-
throw new UnauthorizedException('
|
|
835
|
+
throw new UnauthorizedException('Invalid or expired session');
|
|
832
836
|
}
|
|
833
837
|
|
|
834
|
-
|
|
838
|
+
// subscriptions-transport-ws hands each operation a copy of this object; the
|
|
839
|
+
// registration survives that copy — see graphql-ws-connection.helper.ts.
|
|
840
|
+
const connectionContext = { headers: connectionParams, user };
|
|
841
|
+
registerWsConnection({
|
|
842
|
+
carrier: connectionContext,
|
|
843
|
+
reauthenticate: () =>
|
|
844
|
+
CoreModule.authenticateIamWsToken(betterAuthService, userMapper, authToken),
|
|
845
|
+
socket: webSocket,
|
|
846
|
+
user,
|
|
847
|
+
});
|
|
848
|
+
return connectionContext;
|
|
835
849
|
}
|
|
836
850
|
|
|
837
851
|
throw new UnauthorizedException('Missing authentication token');
|
|
@@ -900,14 +914,20 @@ export class CoreModule implements NestModule {
|
|
|
900
914
|
connectionParams?.Authorization?.split(' ')[1] ?? headers.Authorization?.split(' ')[1];
|
|
901
915
|
if (authToken) {
|
|
902
916
|
// verify authToken/getJwtPayLoad
|
|
903
|
-
const
|
|
904
|
-
const user = await authService.validateUser(payload);
|
|
917
|
+
const user = await CoreModule.authenticateLegacyWsToken(authService, authToken);
|
|
905
918
|
if (!user) {
|
|
906
919
|
throw new UnauthorizedException('No user found for token');
|
|
907
920
|
}
|
|
908
921
|
// the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
|
|
909
922
|
extra.user = user;
|
|
910
923
|
extra.headers = connectionParams ?? headers;
|
|
924
|
+
// Re-checked for the life of the socket — see graphql-ws-connection.helper.ts.
|
|
925
|
+
registerWsConnection({
|
|
926
|
+
carrier: extra,
|
|
927
|
+
reauthenticate: () => CoreModule.authenticateLegacyWsToken(authService, authToken),
|
|
928
|
+
socket: extra.socket,
|
|
929
|
+
user,
|
|
930
|
+
});
|
|
911
931
|
return extra;
|
|
912
932
|
}
|
|
913
933
|
|
|
@@ -916,20 +936,28 @@ export class CoreModule implements NestModule {
|
|
|
916
936
|
},
|
|
917
937
|
},
|
|
918
938
|
'subscriptions-transport-ws': {
|
|
919
|
-
onConnect: async (connectionParams) => {
|
|
939
|
+
onConnect: async (connectionParams, webSocket) => {
|
|
920
940
|
if (enableSubscriptionAuth) {
|
|
921
941
|
// get authToken from authorization header
|
|
922
942
|
const authToken: string = connectionParams?.Authorization?.split(' ')[1];
|
|
923
943
|
|
|
924
944
|
if (authToken) {
|
|
925
945
|
// verify authToken/getJwtPayLoad
|
|
926
|
-
const
|
|
927
|
-
const user = await authService.validateUser(payload);
|
|
946
|
+
const user = await CoreModule.authenticateLegacyWsToken(authService, authToken);
|
|
928
947
|
if (!user) {
|
|
929
948
|
throw new UnauthorizedException('No user found for token');
|
|
930
949
|
}
|
|
931
950
|
// the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
|
|
932
|
-
|
|
951
|
+
// subscriptions-transport-ws hands each operation a copy of this object; the
|
|
952
|
+
// registration survives that copy — see graphql-ws-connection.helper.ts.
|
|
953
|
+
const connectionContext = { headers: connectionParams, user };
|
|
954
|
+
registerWsConnection({
|
|
955
|
+
carrier: connectionContext,
|
|
956
|
+
reauthenticate: () => CoreModule.authenticateLegacyWsToken(authService, authToken),
|
|
957
|
+
socket: webSocket,
|
|
958
|
+
user,
|
|
959
|
+
});
|
|
960
|
+
return connectionContext;
|
|
933
961
|
}
|
|
934
962
|
|
|
935
963
|
throw new UnauthorizedException('Missing authentication token');
|
|
@@ -943,6 +971,89 @@ export class CoreModule implements NestModule {
|
|
|
943
971
|
};
|
|
944
972
|
}
|
|
945
973
|
|
|
974
|
+
/**
|
|
975
|
+
* Resolve the bearer token of an IAM WebSocket handshake to a user — and re-resolve it for a
|
|
976
|
+
* connection that is already open (the `reauthenticate` of `registerWsConnection()`). Answers
|
|
977
|
+
* `null` when the token does not authenticate (any more).
|
|
978
|
+
*
|
|
979
|
+
* Mirrors Strategy 1 of `CoreBetterAuthMiddleware` — a Better-Auth JWT first, then the session
|
|
980
|
+
* store — because that is what an HTTP request carrying the same header gets. Until 11.42.3 the
|
|
981
|
+
* handshake asked `getSession()` with an `authorization` header, which Better-Auth reads only
|
|
982
|
+
* with its `bearer` plugin, and the framework does not install that plugin: every IAM handshake
|
|
983
|
+
* with a bearer token was refused as "Invalid or expired session". `getSession()` stays as the
|
|
984
|
+
* last resort, so a project that added the bearer plugin itself resolves exactly as before.
|
|
985
|
+
*
|
|
986
|
+
* A JWT is checked statelessly (signature, expiry), as on HTTP: signing out ends the session
|
|
987
|
+
* behind it, not the JWT, which lapses on its own (`betterAuth.jwt.expiresIn`, 15 min by default).
|
|
988
|
+
*/
|
|
989
|
+
private static async authenticateIamWsToken(
|
|
990
|
+
betterAuthService: Pick<
|
|
991
|
+
CoreBetterAuthService,
|
|
992
|
+
'getSession' | 'getSessionByToken' | 'isJwtEnabled' | 'verifyJwtToken'
|
|
993
|
+
>,
|
|
994
|
+
userMapper: Pick<CoreBetterAuthUserMapper, 'mapSessionUser'>,
|
|
995
|
+
authToken: string,
|
|
996
|
+
): Promise<any> {
|
|
997
|
+
if (!authToken) {
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
if (authToken.split('.').length === 3 && !isLegacyJwt(authToken) && betterAuthService.isJwtEnabled()) {
|
|
1001
|
+
const payload = await betterAuthService.verifyJwtToken(authToken);
|
|
1002
|
+
if (payload?.sub) {
|
|
1003
|
+
return userMapper.mapSessionUser({
|
|
1004
|
+
email: payload.email || '',
|
|
1005
|
+
emailVerified: payload.emailVerified,
|
|
1006
|
+
id: payload.sub,
|
|
1007
|
+
name: payload.name,
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const stored = await betterAuthService.getSessionByToken(authToken);
|
|
1012
|
+
if (stored?.session && stored?.user) {
|
|
1013
|
+
return userMapper.mapSessionUser(stored.user);
|
|
1014
|
+
}
|
|
1015
|
+
const { session, user: sessionUser } = await betterAuthService.getSession({
|
|
1016
|
+
headers: { authorization: `Bearer ${authToken}` },
|
|
1017
|
+
});
|
|
1018
|
+
return session && sessionUser ? userMapper.mapSessionUser(sessionUser) : null;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* The legacy counterpart of `authenticateIamWsToken()`: the token's signature and expiry are verified
|
|
1023
|
+
* exactly as passport-jwt does on HTTP (`CoreAuthService.verifyJwt()`), then `validateUser()` ties it
|
|
1024
|
+
* to its device's live refresh token, so a logout, a password reset or a token refresh ends it.
|
|
1025
|
+
*
|
|
1026
|
+
* Until 11.42.3 this read the token with `decodeJwt()`, which checks neither signature nor expiry:
|
|
1027
|
+
* an expired token — or a copy re-signed with any key — opened sockets for as long as its `tokenId`
|
|
1028
|
+
* matched the device. A token that is not a JWT at all decoded to `null`, which `validateUser()` then
|
|
1029
|
+
* dereferenced: the handshake closed with 4500 and the TypeError as its reason.
|
|
1030
|
+
*
|
|
1031
|
+
* An AuthService that does not extend `CoreAuthService` may lack `verifyJwt()`. It keeps the
|
|
1032
|
+
* unverified read rather than losing its WebSockets on upgrade, and is warned about once.
|
|
1033
|
+
*/
|
|
1034
|
+
private static async authenticateLegacyWsToken(authService: any, authToken: string): Promise<any> {
|
|
1035
|
+
if (!authToken) {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
let payload: any;
|
|
1039
|
+
if (typeof authService?.verifyJwt === 'function') {
|
|
1040
|
+
payload = await authService.verifyJwt(authToken);
|
|
1041
|
+
} else {
|
|
1042
|
+
if (!CoreModule.unverifiedLegacyWsTokenWarned) {
|
|
1043
|
+
CoreModule.unverifiedLegacyWsTokenWarned = true;
|
|
1044
|
+
new Logger('CoreModule').warn(
|
|
1045
|
+
'The AuthService passed to CoreModule.forRoot() has no verifyJwt(): legacy WebSocket tokens are ' +
|
|
1046
|
+
'read WITHOUT checking signature or expiry. Extend CoreAuthService to close that.',
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
payload = authService.decodeJwt(authToken);
|
|
1050
|
+
}
|
|
1051
|
+
return payload ? authService.validateUser(payload) : null;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/** Whether the "no verifyJwt()" warning was already logged in this process. */
|
|
1055
|
+
private static unverifiedLegacyWsTokenWarned = false;
|
|
1056
|
+
|
|
946
1057
|
/**
|
|
947
1058
|
* @deprecated Use `isCookiesEnabled` from `core/common/helpers/cookies.helper` instead.
|
|
948
1059
|
*/
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export * from './core/common/helpers/execution-context-request.helper';
|
|
|
40
40
|
export * from './core/common/helpers/file.helper';
|
|
41
41
|
export * from './core/common/helpers/filter.helper';
|
|
42
42
|
export * from './core/common/helpers/graphql.helper';
|
|
43
|
+
export * from './core/common/helpers/graphql-ws-connection.helper';
|
|
43
44
|
export * from './core/common/helpers/graphql-ws-context.helper';
|
|
44
45
|
export * from './core/common/helpers/interceptor.helper';
|
|
45
46
|
export * from './core/common/helpers/gridfs.helper';
|