@idp.global/sdk 5.0.1 → 6.0.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.
@@ -2,6 +2,7 @@ import * as plugins from './plugins.js';
2
2
 
3
3
  export interface IIdpGlobalServerClientOptions {
4
4
  baseUrl?: string;
5
+ backendToken?: string;
5
6
  }
6
7
 
7
8
  export const defaultIdpGlobalBaseUrl = 'https://idp.global';
@@ -27,35 +28,158 @@ export class IdpMfaRequiredError extends Error {
27
28
  export class IdpGlobalServerClient {
28
29
  private typedrouter: any = new plugins.typedrequest.TypedRouter();
29
30
  private typedsocket?: plugins.typedsocket.TypedSocket;
30
- private typedsocketDeferred = plugins.smartpromise.defer<plugins.typedsocket.TypedSocket>();
31
+ private typedsocketDeferred = this.createConnectionDeferred();
32
+ private publicKeyPushHandlers: Array<(publicKeyPemArg: string) => void | Promise<void>> = [];
33
+ private blocklistPushHandlers: Array<(blockedJwtIdsArg: string[]) => void | Promise<void>> = [];
34
+ private connectionGeneration = 0;
35
+ private pendingSocketPromise?: Promise<plugins.typedsocket.TypedSocket>;
36
+ private connectionAbortController?: AbortController;
37
+ private stopWaiters = new Set<(errorArg: Error) => void>();
38
+ private stopping = false;
31
39
 
32
- constructor(private optionsArg: IIdpGlobalServerClientOptions = {}) {}
40
+ constructor(private optionsArg: IIdpGlobalServerClientOptions = {}) {
41
+ this.typedrouter.addTypedHandler(
42
+ new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_PushPublicKeyForValidation>(
43
+ 'pushPublicKeyForValidation',
44
+ async (requestArg) => {
45
+ await Promise.all(this.publicKeyPushHandlers.map((handlerArg) => handlerArg(requestArg.publicKeyPem)));
46
+ return {};
47
+ }
48
+ )
49
+ );
50
+ this.typedrouter.addTypedHandler(
51
+ new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_PushOrGetJwtIdBlocklist>(
52
+ 'pushOrGetJwtIdBlocklist',
53
+ async (requestArg) => {
54
+ if (requestArg.blockedJwtIds) {
55
+ await Promise.all(this.blocklistPushHandlers.map((handlerArg) => handlerArg(requestArg.blockedJwtIds!)));
56
+ }
57
+ return {};
58
+ }
59
+ )
60
+ );
61
+ }
33
62
 
34
63
  public async connect(): Promise<plugins.typedsocket.TypedSocket> {
64
+ if (this.stopping) {
65
+ throw new Error('IdpGlobalServerClient is stopping');
66
+ }
35
67
  if (this.typedsocketDeferred.claimed) {
36
68
  return this.typedsocketDeferred.promise;
37
69
  }
38
- this.typedsocketDeferred.claim();
39
- this.typedsocket = await plugins.typedsocket.TypedSocket.createClient(
40
- this.typedrouter,
41
- this.getTypedRequestUrl(),
42
- );
43
- this.typedsocketDeferred.resolve(this.typedsocket);
44
- return this.typedsocketDeferred.promise;
70
+ const deferred = this.typedsocketDeferred;
71
+ const generation = this.connectionGeneration;
72
+ deferred.claim();
73
+ let createdSocket: plugins.typedsocket.TypedSocket | undefined;
74
+ let socketPromise: Promise<plugins.typedsocket.TypedSocket> | undefined;
75
+ const connectionAbortController = new AbortController();
76
+ try {
77
+ this.connectionAbortController = connectionAbortController;
78
+ socketPromise = plugins.typedsocket.TypedSocket.createClient(
79
+ this.typedrouter,
80
+ this.getTypedRequestUrl(),
81
+ { autoReconnect: true, abortSignal: connectionAbortController.signal },
82
+ );
83
+ this.pendingSocketPromise = socketPromise;
84
+ createdSocket = await this.raceWithStop(socketPromise);
85
+ if (generation !== this.connectionGeneration) {
86
+ await createdSocket.stop();
87
+ if (deferred.status === 'pending') {
88
+ deferred.reject(new Error('IdpGlobalServerClient stopped while connecting'));
89
+ }
90
+ return deferred.promise;
91
+ }
92
+ this.typedsocket = createdSocket;
93
+ deferred.resolve(createdSocket);
94
+ } catch (error) {
95
+ if (generation === this.connectionGeneration && (!createdSocket || this.typedsocket === createdSocket)) {
96
+ this.typedsocket = undefined;
97
+ }
98
+ await createdSocket?.stop().catch(() => undefined);
99
+ if (deferred.status === 'pending') {
100
+ deferred.reject(error);
101
+ }
102
+ if (this.typedsocketDeferred === deferred) {
103
+ this.typedsocketDeferred = this.createConnectionDeferred();
104
+ }
105
+ throw error;
106
+ } finally {
107
+ if (socketPromise && this.pendingSocketPromise === socketPromise) {
108
+ this.pendingSocketPromise = undefined;
109
+ }
110
+ if (this.connectionAbortController === connectionAbortController) {
111
+ this.connectionAbortController = undefined;
112
+ }
113
+ }
114
+ return deferred.promise;
45
115
  }
46
116
 
47
117
  public async stop(): Promise<void> {
48
- await this.typedsocket?.stop();
118
+ this.stopping = true;
119
+ this.connectionGeneration++;
120
+ const stopError = new Error('IdpGlobalServerClient stopped');
121
+ this.rejectStopWaiters(stopError);
122
+ const socket = this.typedsocket;
123
+ const pendingSocketPromise = this.pendingSocketPromise;
124
+ this.connectionAbortController?.abort();
125
+ this.connectionAbortController = undefined;
49
126
  this.typedsocket = undefined;
127
+ if (this.typedsocketDeferred.status === 'pending' && this.typedsocketDeferred.claimed) {
128
+ this.typedsocketDeferred.reject(stopError);
129
+ }
130
+ await socket?.stop();
131
+ const pendingSocket = await pendingSocketPromise?.catch(() => undefined);
132
+ if (pendingSocket && pendingSocket !== socket) {
133
+ await pendingSocket.stop().catch(() => undefined);
134
+ }
135
+ this.typedsocketDeferred = this.createConnectionDeferred();
136
+ this.pendingSocketPromise = undefined;
137
+ this.stopping = false;
138
+ }
139
+
140
+ public onPublicKeyPush(handlerArg: (publicKeyPemArg: string) => void | Promise<void>): () => void {
141
+ this.publicKeyPushHandlers.push(handlerArg);
142
+ return () => {
143
+ this.publicKeyPushHandlers = this.publicKeyPushHandlers.filter((handler) => handler !== handlerArg);
144
+ };
145
+ }
146
+
147
+ public onBlocklistPush(handlerArg: (blockedJwtIdsArg: string[]) => void | Promise<void>): () => void {
148
+ this.blocklistPushHandlers.push(handlerArg);
149
+ return () => {
150
+ this.blocklistPushHandlers = this.blocklistPushHandlers.filter((handler) => handler !== handlerArg);
151
+ };
152
+ }
153
+
154
+ public async registerBackendService(backendTokenArg = this.optionsArg.backendToken): Promise<void> {
155
+ const backendToken = this.requireBackendToken(backendTokenArg);
156
+ const socket = await this.connect();
157
+ await this.applyBackendServiceTag(socket, backendToken);
158
+ }
159
+
160
+ public async getPublicKeyForValidation(backendTokenArg = this.optionsArg.backendToken): Promise<string> {
161
+ const backendToken = this.requireBackendToken(backendTokenArg);
162
+ const socket = await this.connect();
163
+ const request = socket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetPublicKeyForValidation>('getPublicKeyForValidation');
164
+ const response = await this.raceWithStop(request.fire({ backendToken }));
165
+ return response.publicKeyPem;
166
+ }
167
+
168
+ public async getJwtIdBlocklist(backendTokenArg = this.optionsArg.backendToken): Promise<string[]> {
169
+ const backendToken = this.requireBackendToken(backendTokenArg);
170
+ const socket = await this.connect();
171
+ const request = socket.createTypedRequest<plugins.idpInterfaces.request.IReq_PushOrGetJwtIdBlocklist>('pushOrGetJwtIdBlocklist');
172
+ const response = await this.raceWithStop(request.fire({ backendToken }));
173
+ return response.blockedJwtIds || [];
50
174
  }
51
175
 
52
176
  public async loginWithEmailAndPassword(optionsArg: { email: string; password: string }): Promise<IIdpPasswordAuthResult> {
53
177
  const socket = await this.connect();
54
178
  const loginRequest = socket.createTypedRequest<plugins.idpInterfaces.request.IReq_LoginWithEmailOrUsernameAndPassword>('loginWithEmailOrUsernameAndPassword');
55
- const loginResponse = await loginRequest.fire({
179
+ const loginResponse = await this.raceWithStop(loginRequest.fire({
56
180
  username: optionsArg.email,
57
181
  password: optionsArg.password,
58
- });
182
+ }));
59
183
  if (loginResponse.twoFaNeeded) {
60
184
  throw new IdpMfaRequiredError({
61
185
  mfaChallengeToken: (loginResponse as any).mfaChallengeToken,
@@ -67,13 +191,13 @@ export class IdpGlobalServerClient {
67
191
  }
68
192
 
69
193
  const refreshRequest = socket.createTypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>('refreshJwt');
70
- const refreshResponse = await refreshRequest.fire({ refreshToken: loginResponse.refreshToken });
194
+ const refreshResponse = await this.raceWithStop(refreshRequest.fire({ refreshToken: loginResponse.refreshToken }));
71
195
  if (!refreshResponse.jwt) {
72
196
  throw new Error('IdP did not return a JWT');
73
197
  }
74
198
 
75
199
  const whoIsRequest = socket.createTypedRequest<plugins.idpInterfaces.request.IReq_WhoIs>('whoIs');
76
- const whoIsResponse = await whoIsRequest.fire({ jwt: refreshResponse.jwt });
200
+ const whoIsResponse = await this.raceWithStop(whoIsRequest.fire({ jwt: refreshResponse.jwt }));
77
201
  return {
78
202
  user: whoIsResponse.user,
79
203
  jwt: refreshResponse.jwt,
@@ -91,4 +215,47 @@ export class IdpGlobalServerClient {
91
215
  }
92
216
  return baseUrl;
93
217
  }
218
+
219
+ private requireBackendToken(backendTokenArg?: string): string {
220
+ if (!backendTokenArg) {
221
+ throw new Error('backendToken is required for backend JWT validation APIs');
222
+ }
223
+ return backendTokenArg;
224
+ }
225
+
226
+ private async applyBackendServiceTag(
227
+ socketArg: plugins.typedsocket.TypedSocket,
228
+ backendTokenArg: string
229
+ ): Promise<void> {
230
+ await this.raceWithStop(socketArg.setTag<plugins.idpInterfaces.tags.ITag_BackendService>('backend-service', {
231
+ backendToken: backendTokenArg,
232
+ }));
233
+ }
234
+
235
+ private createConnectionDeferred() {
236
+ const deferred = plugins.smartpromise.defer<plugins.typedsocket.TypedSocket>();
237
+ deferred.promise.catch(() => undefined);
238
+ return deferred;
239
+ }
240
+
241
+ private raceWithStop<T>(promiseArg: Promise<T>): Promise<T> {
242
+ promiseArg.catch(() => undefined);
243
+ if (this.stopping) {
244
+ return Promise.reject(new Error('IdpGlobalServerClient stopped'));
245
+ }
246
+ return new Promise<T>((resolve, reject) => {
247
+ const stopReject = (errorArg: Error) => reject(errorArg);
248
+ this.stopWaiters.add(stopReject);
249
+ promiseArg.then(resolve, reject).finally(() => {
250
+ this.stopWaiters.delete(stopReject);
251
+ });
252
+ });
253
+ }
254
+
255
+ private rejectStopWaiters(errorArg: Error): void {
256
+ for (const stopWaiter of this.stopWaiters) {
257
+ stopWaiter(errorArg);
258
+ }
259
+ this.stopWaiters.clear();
260
+ }
94
261
  }