@serve.zone/platformclient 3.0.1 → 4.0.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.
@@ -15,6 +15,8 @@ export interface ISzPlatformClientOptions {
15
15
  token?: string;
16
16
  url?: string;
17
17
  platformUrl?: string;
18
+ /** Explicit HTTPS Cloudly origin. Bindings never select this authority. */
19
+ cloudlyUrl?: string;
18
20
  binding?: plugins.servezoneInterfaces.platform.IPlatformBinding;
19
21
  bindings?: plugins.servezoneInterfaces.platform.IPlatformBinding[];
20
22
  coreMail?: ISzCoreMailConnectorOptions;
@@ -26,7 +28,13 @@ export class SzPlatformClient {
26
28
  private connectionAddress?: string;
27
29
  public platformBindings: plugins.servezoneInterfaces.platform.IPlatformBinding[] = [];
28
30
  public typedrouter: plugins.typedrequest.TypedRouter = new plugins.typedrequest.TypedRouter();
29
- public typedsocket!: plugins.typedsocket.TypedSocket;
31
+ public typedsocket?: plugins.typedsocket.TypedSocket;
32
+ private identity?: plugins.servezoneInterfaces.data.IIdentity;
33
+ private authenticationGeneration = 0;
34
+ private authenticationAbort?: AbortController;
35
+ private authenticationOperations = new Set<Promise<unknown>>();
36
+ private renewalTimer?: ReturnType<typeof setTimeout>;
37
+ private statusSubscription?: { unsubscribe(): void };
30
38
  private qenvInstance = new plugins.qenv.Qenv();
31
39
  private stopped = false;
32
40
  private initPromise?: Promise<void>;
@@ -77,25 +85,41 @@ export class SzPlatformClient {
77
85
  return;
78
86
  }
79
87
  if (!this.authorizationString) throw new Error('authorizationString is required');
88
+ if (this.authorizationString.length > 8192 || /[\x00-\x20\x7f]/.test(this.authorizationString)) {
89
+ throw new Error('Platform authorization is invalid.');
90
+ }
80
91
  const socket = await plugins.typedsocket.TypedSocket.createClient(
81
- this.typedrouter as any,
92
+ this.typedrouter,
82
93
  await this.getConnectionAddress(),
83
- { abortSignal: this.platformSocketAbortController.signal },
94
+ {
95
+ autoReconnect: true, maxRetries: 100, initialBackoffMs: 1000, maxBackoffMs: 30_000,
96
+ abortSignal: this.platformSocketAbortController.signal,
97
+ restoreConnection: (context) => {
98
+ this.invalidateAuthentication();
99
+ const generation = this.authenticationGeneration;
100
+ return this.trackAuthentication(this.authenticate(context.createTypedRequest, context.abortSignal, generation));
101
+ },
102
+ },
84
103
  );
85
104
  if (this.stopped) {
86
- await socket.stop().catch(() => undefined);
105
+ await socket.stop();
87
106
  throw this.createAbortError();
88
107
  }
89
108
  this.typedsocket = socket;
90
109
  try {
91
- await this.setAuthorizationTag();
110
+ this.statusSubscription = socket.statusSubject.subscribe((status) => {
111
+ if (status === 'disconnected') this.invalidateAuthentication();
112
+ if (status === 'connected') this.scheduleRenewal(socket);
113
+ });
114
+ this.scheduleRenewal(socket);
92
115
  this.assertRunning();
93
116
  if (this.coreMailConnector.isConfigured) {
94
117
  await this.coreMailConnector.init();
95
118
  this.assertRunning();
96
119
  }
97
120
  } catch (error) {
98
- await socket.stop().catch(() => undefined);
121
+ this.invalidateAuthentication();
122
+ await socket.stop();
99
123
  throw error;
100
124
  }
101
125
  }
@@ -131,7 +155,7 @@ export class SzPlatformClient {
131
155
  optionsArg.authorization ||
132
156
  optionsArg.token ||
133
157
  this.authorizationString;
134
- this.connectionAddress = optionsArg.url || optionsArg.platformUrl || this.connectionAddress;
158
+ this.connectionAddress = optionsArg.cloudlyUrl || optionsArg.url || optionsArg.platformUrl || this.connectionAddress;
135
159
  this.platformBindings = this.mergePlatformBindings(
136
160
  this.platformBindings,
137
161
  [optionsArg.binding, ...(optionsArg.bindings || [])].filter(
@@ -146,34 +170,12 @@ export class SzPlatformClient {
146
170
  private async getConnectionAddress() {
147
171
  const connectionAddress =
148
172
  this.connectionAddress ||
149
- (await this.getConfiguredEnvVar('SERVEZONE_PLATFORM_URL')) ||
150
- this.getConnectionAddressFromBindings();
151
- if (!connectionAddress) throw new Error('SERVEZONE_PLATFORM_URL or platform binding endpoint is required');
152
- return connectionAddress;
153
- }
154
-
155
- private getConnectionAddressFromBindings() {
156
- for (const binding of this.platformBindings) {
157
- if (binding.desiredState === 'disabled' || binding.status === 'failed') {
158
- continue;
159
- }
160
- if (!this.isSupportedConnectorCapability(binding.capability)) {
161
- continue;
162
- }
163
- const endpoint = binding.endpoints?.find((endpointArg) => {
164
- return endpointArg.protocol === 'typedrequest' || endpointArg.protocol === 'http';
165
- });
166
- const endpointUrl = endpoint?.internalUrl || endpoint?.externalUrl;
167
- if (endpointUrl) {
168
- return endpointUrl;
169
- }
170
- }
171
- }
172
-
173
- private isSupportedConnectorCapability(
174
- capabilityArg: plugins.servezoneInterfaces.platform.TPlatformCapability
175
- ) {
176
- return ['email', 'sms', 'pushnotification', 'letter'].includes(capabilityArg);
173
+ (await this.getConfiguredEnvVar('SERVEZONE_PLATFORM_URL'));
174
+ if (!connectionAddress) throw new Error('An explicit Cloudly URL is required.');
175
+ const parsed = new URL(connectionAddress);
176
+ if (parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.search || parsed.hash
177
+ || parsed.pathname !== '/') throw new Error('Cloudly URL must be an HTTPS origin.');
178
+ return parsed.origin;
177
179
  }
178
180
 
179
181
  private async getConfiguredAuthorizationString() {
@@ -183,19 +185,81 @@ export class SzPlatformClient {
183
185
  );
184
186
  }
185
187
 
186
- private async setAuthorizationTag() {
187
- if (!this.authorizationString) {
188
- return;
189
- }
188
+ public requireTypedSocket(): plugins.typedsocket.TypedSocket {
189
+ this.assertRunning();
190
+ if (!this.typedsocket || this.typedsocket.getStatus() !== 'connected' || !this.identity
191
+ || this.identity.expiresAt <= Date.now()) throw new Error('Cloudly platform session is unavailable.');
192
+ return this.typedsocket;
193
+ }
194
+
195
+ private invalidateAuthentication(): void {
196
+ this.authenticationGeneration++;
197
+ this.authenticationAbort?.abort();
198
+ this.authenticationAbort = undefined;
199
+ this.identity = undefined;
200
+ if (this.renewalTimer) clearTimeout(this.renewalTimer);
201
+ this.renewalTimer = undefined;
202
+ }
203
+
204
+ private trackAuthentication<T>(operation: Promise<T>): Promise<T> {
205
+ this.authenticationOperations.add(operation);
206
+ void operation.then(() => this.authenticationOperations.delete(operation), () => this.authenticationOperations.delete(operation));
207
+ return operation;
208
+ }
209
+
210
+ private async authenticate(createRequest: plugins.typedsocket.TTypedSocketRestoreRequestFactory,
211
+ signal: AbortSignal, generation: number): Promise<void> {
212
+ const requireCurrent = () => {
213
+ this.assertRunning(); signal.throwIfAborted();
214
+ if (generation !== this.authenticationGeneration) throw this.createAbortError();
215
+ };
190
216
  try {
191
- await this.typedsocket.setTag('authorization' as any, this.authorizationString);
192
- } catch (error) {
193
- if (await this.getConfiguredEnvVar('SERVEZONE_PLATFORMCLIENT_DEBUG')) {
194
- console.warn('Could not set platform authorization tag:', (error as Error).message);
217
+ requireCurrent();
218
+ const exchange = createRequest<plugins.servezoneInterfaces.requests.identity.IRequest_Any_Cloudly_CoreflowManager_GetIdentityByToken>(
219
+ 'getIdentityByToken');
220
+ exchange.skipHooks = true;
221
+ const response = await exchange.fire({ token: this.authorizationString! }, { maxRetries: 0, abortSignal: signal });
222
+ requireCurrent();
223
+ const identity = response.identity;
224
+ if (!identity || identity.type !== 'machine' || identity.role !== 'api'
225
+ || typeof identity.userId !== 'string' || !/^service-machine:[a-f0-9]{64}$/.test(identity.userId)
226
+ || !Number.isSafeInteger(identity.expiresAt) || identity.expiresAt <= Date.now() + 1000) {
227
+ throw new Error('Invalid platform identity.');
195
228
  }
229
+ const requestData = plugins.servezoneInterfaces.data.snapshotRegisterCloudlyClientSessionRequestV1({
230
+ schemaVersion: 1, identity: { jwt: identity.jwt },
231
+ });
232
+ const registration = createRequest<plugins.servezoneInterfaces.requests.runtimesession.IReq_Any_Cloudly_RegisterCloudlyClientSessionV1>(
233
+ 'registerCloudlyClientSessionV1');
234
+ registration.skipHooks = true;
235
+ const registered = await registration.fire(requestData, { maxRetries: 0, abortSignal: signal });
236
+ plugins.servezoneInterfaces.data.snapshotRegisterCloudlyClientSessionResponseV1(registered);
237
+ requireCurrent();
238
+ this.identity = { type: identity.type, role: identity.role, userId: identity.userId,
239
+ name: identity.userId, expiresAt: identity.expiresAt, jwt: requestData.identity.jwt };
240
+ } catch {
241
+ if (this.stopped || signal.aborted || generation !== this.authenticationGeneration) throw this.createAbortError();
242
+ throw new Error('Cloudly platform authentication failed.');
196
243
  }
197
244
  }
198
245
 
246
+ private scheduleRenewal(socket: plugins.typedsocket.TypedSocket): void {
247
+ if (this.renewalTimer) clearTimeout(this.renewalTimer);
248
+ if (this.stopped || this.typedsocket !== socket || !this.identity) return;
249
+ const generation = this.authenticationGeneration;
250
+ const remaining = this.identity.expiresAt - Date.now();
251
+ this.renewalTimer = setTimeout(() => {
252
+ this.renewalTimer = undefined;
253
+ if (this.stopped || this.typedsocket !== socket || generation !== this.authenticationGeneration) return;
254
+ const abort = new AbortController(); this.authenticationAbort = abort;
255
+ const work = this.trackAuthentication(this.authenticate(
256
+ (method) => socket.createTypedRequest(method), abort.signal, generation));
257
+ void work.then(() => this.scheduleRenewal(socket), () => {
258
+ if (!this.stopped && generation === this.authenticationGeneration) void this.stop().catch(() => undefined);
259
+ });
260
+ }, Math.max(1, Math.min(30 * 60 * 1000, Math.floor(remaining / 2))));
261
+ }
262
+
199
263
  public stop(): Promise<void> {
200
264
  this.stopPromise ??= this.stopInner();
201
265
  return this.stopPromise;
@@ -203,21 +267,26 @@ export class SzPlatformClient {
203
267
 
204
268
  private async stopInner(): Promise<void> {
205
269
  this.stopped = true;
270
+ this.invalidateAuthentication();
271
+ this.statusSubscription?.unsubscribe();
272
+ this.statusSubscription = undefined;
206
273
  this.platformSocketAbortController.abort(this.createAbortError());
207
274
  const socket = this.typedsocket;
208
- await Promise.allSettled([
275
+ this.typedsocket = undefined;
276
+ const results = await Promise.allSettled([
209
277
  this.emailConnector.stop(),
210
278
  this.webPushConnector.stop(),
211
279
  this.coreMailConnector.stop(),
212
280
  socket?.stop(),
213
- this.initPromise,
214
281
  ]);
215
- if (this.typedsocket && this.typedsocket !== socket) {
216
- await this.typedsocket.stop().catch(() => undefined);
217
- }
282
+ await this.initPromise?.catch(() => undefined);
283
+ while (this.authenticationOperations.size) await Promise.allSettled([...this.authenticationOperations]);
284
+ const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
285
+ if (failures.length) throw new AggregateError(failures.map(result => result.reason), 'Platform client shutdown failed.');
218
286
  }
219
287
 
220
- private assertRunning(): void {
288
+ /** Connector admission guard; shutdown is terminal for this client lifetime. */
289
+ public assertRunning(): void {
221
290
  if (this.stopped) {
222
291
  throw this.createAbortError();
223
292
  }
@@ -106,12 +106,11 @@ export class SzEmailConnector {
106
106
  private mailTypedSocketUrl?: string;
107
107
  private mailTypedSocketPromise?: Promise<plugins.typedsocket.TypedSocket>;
108
108
  private mailTypedSocketAbortController?: AbortController;
109
- private mailTypedSocketStatusSubscription?: { unsubscribe: () => void };
109
+ private readonly mailRouter = new plugins.typedrequest.TypedRouter();
110
+ private readonly restorationOperations = new Set<Promise<void>>();
110
111
  private inboundHandler?: TServiceMailInboundHandler;
111
112
  private inboundTypedHandlerRegistered = false;
112
113
  private inboundEndpointRegistrations = new Set<string>();
113
- private inboundEndpointReplayPromise?: Promise<void>;
114
- private readonly inboundEndpointReplayRetryDelaysMs = [500, 1000, 2000, 5000];
115
114
 
116
115
  constructor(platformClientRefArg: SzPlatformClient) {
117
116
  this.platformClientRef = platformClientRefArg;
@@ -136,7 +135,7 @@ export class SzEmailConnector {
136
135
  }
137
136
 
138
137
  const typedRequest =
139
- this.platformClientRef.typedsocket.createTypedRequest<plugins.servezoneInterfaces.platform.email.IReq_SendEmail>(
138
+ this.platformClientRef.requireTypedSocket().createTypedRequest<plugins.servezoneInterfaces.platform.email.IReq_SendEmail>(
140
139
  'sendEmail'
141
140
  );
142
141
  const response = await typedRequest.fire(optionsArg);
@@ -303,15 +302,13 @@ export class SzEmailConnector {
303
302
  const credentials = await this.getServiceMailTypedCredentials(addressArg);
304
303
  const typedSocket = await this.getMailTypedSocket(credentials.typedUrl);
305
304
  const result = await this.registerServiceMailEndpoint(typedSocket, credentials);
306
- const registeredAddresses = result.addresses?.length ? result.addresses : [credentials.from];
307
- for (const address of registeredAddresses) {
308
- this.inboundEndpointRegistrations.add(address.trim().toLowerCase());
309
- }
305
+ this.inboundEndpointRegistrations.add(credentials.from);
310
306
  return result;
311
307
  }
312
308
 
313
309
  public async stop(): Promise<void> {
314
310
  await this.stopMailTypedSocket();
311
+ while (this.restorationOperations.size) await Promise.allSettled([...this.restorationOperations]);
315
312
  }
316
313
 
317
314
  public normalizeInboundMessage(payloadArg: unknown): IServiceMailInboundMessage {
@@ -361,7 +358,7 @@ export class SzEmailConnector {
361
358
  private ensureInboundTypedHandler(): void {
362
359
  if (this.inboundTypedHandlerRegistered) return;
363
360
  this.inboundTypedHandlerRegistered = true;
364
- this.platformClientRef.typedrouter.addTypedHandler(
361
+ this.mailRouter.addTypedHandler(
365
362
  new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.mail.IReq_DeliverInboundMail>(
366
363
  'deliverInboundMail',
367
364
  async (dataArg) => {
@@ -445,6 +442,7 @@ export class SzEmailConnector {
445
442
  }
446
443
 
447
444
  private async getMailTypedSocket(urlArg: string): Promise<plugins.typedsocket.TypedSocket> {
445
+ this.platformClientRef.assertRunning();
448
446
  const normalizedUrl = this.normalizeTypedSocketUrl(urlArg);
449
447
  if (this.mailTypedSocket && this.mailTypedSocketUrl === normalizedUrl) {
450
448
  return this.mailTypedSocket;
@@ -455,20 +453,27 @@ export class SzEmailConnector {
455
453
  if (this.mailTypedSocket || this.mailTypedSocketPromise) {
456
454
  await this.stopMailTypedSocket();
457
455
  }
456
+ this.platformClientRef.assertRunning();
458
457
  this.mailTypedSocketUrl = normalizedUrl;
459
458
  const abortController = new AbortController();
460
459
  this.mailTypedSocketAbortController = abortController;
461
460
  const socketPromise = plugins.typedsocket.TypedSocket.createClient(
462
- this.platformClientRef.typedrouter,
461
+ this.mailRouter,
463
462
  normalizedUrl,
464
- { autoReconnect: true, abortSignal: abortController.signal },
463
+ { autoReconnect: true, abortSignal: abortController.signal,
464
+ restoreConnection: (context) => {
465
+ const operation = this.restoreMailConnection(context, normalizedUrl, abortController.signal);
466
+ this.restorationOperations.add(operation);
467
+ void operation.then(() => this.restorationOperations.delete(operation), () => this.restorationOperations.delete(operation));
468
+ return operation;
469
+ },
470
+ },
465
471
  );
466
472
  this.mailTypedSocketPromise = socketPromise;
467
473
  try {
468
474
  const socket = await socketPromise;
469
475
  if (this.mailTypedSocketPromise === socketPromise && this.mailTypedSocketUrl === normalizedUrl) {
470
476
  this.mailTypedSocket = socket;
471
- this.watchMailTypedSocketStatus(socket);
472
477
  } else {
473
478
  await socket.stop();
474
479
  }
@@ -487,148 +492,49 @@ export class SzEmailConnector {
487
492
  const socket = this.mailTypedSocket;
488
493
  const socketPromise = this.mailTypedSocketPromise;
489
494
  const abortController = this.mailTypedSocketAbortController;
490
- const statusSubscription = this.mailTypedSocketStatusSubscription;
491
495
  this.mailTypedSocket = undefined;
492
496
  this.mailTypedSocketPromise = undefined;
493
497
  this.mailTypedSocketUrl = undefined;
494
498
  this.mailTypedSocketAbortController = undefined;
495
- this.mailTypedSocketStatusSubscription = undefined;
496
- statusSubscription?.unsubscribe();
497
499
  abortController?.abort();
498
500
  if (socket) {
499
501
  await socket.stop();
500
502
  return;
501
503
  }
502
504
  if (socketPromise) {
503
- try {
504
- const pendingSocket = await socketPromise;
505
- await pendingSocket.stop();
506
- } catch {
507
- // The pending connection already failed; there is no socket to stop.
508
- }
509
- }
510
- }
511
-
512
- private watchMailTypedSocketStatus(socketArg: plugins.typedsocket.TypedSocket): void {
513
- this.mailTypedSocketStatusSubscription?.unsubscribe();
514
- this.mailTypedSocketStatusSubscription = socketArg.statusSubject.subscribe((statusArg) => {
515
- void this.handleMailTypedSocketStatus(socketArg, statusArg);
516
- });
517
- }
518
-
519
- private async handleMailTypedSocketStatus(
520
- socketArg: plugins.typedsocket.TypedSocket,
521
- statusArg: plugins.typedsocket.TConnectionStatus,
522
- ): Promise<void> {
523
- if (statusArg !== 'connected' || this.mailTypedSocket !== socketArg) {
524
- return;
525
- }
526
- try {
527
- await this.replayInboundEndpointRegistrationsWithRetry(socketArg);
528
- } catch (error) {
529
- console.warn('Could not re-register service mail endpoint after reconnect:', (error as Error).message);
530
- }
531
- }
532
-
533
- private async replayInboundEndpointRegistrationsWithRetry(
534
- socketArg: plugins.typedsocket.TypedSocket,
535
- ): Promise<void> {
536
- if (!this.inboundEndpointRegistrations.size) {
537
- return;
505
+ const pendingSocket = await socketPromise.catch(() => undefined);
506
+ await pendingSocket?.stop();
538
507
  }
539
- if (this.inboundEndpointReplayPromise) {
540
- return await this.inboundEndpointReplayPromise;
541
- }
542
-
543
- const replayPromise = this.replayInboundEndpointRegistrationsWithRetryInner(socketArg);
544
- this.inboundEndpointReplayPromise = replayPromise;
545
- try {
546
- await replayPromise;
547
- } finally {
548
- if (this.inboundEndpointReplayPromise === replayPromise) {
549
- this.inboundEndpointReplayPromise = undefined;
550
- }
551
- }
552
- }
553
-
554
- private async replayInboundEndpointRegistrationsWithRetryInner(
555
- socketArg: plugins.typedsocket.TypedSocket,
556
- ): Promise<void> {
557
- let lastError: unknown;
558
- const delays = [0, ...this.inboundEndpointReplayRetryDelaysMs];
559
- for (let attemptIndex = 0; attemptIndex < delays.length; attemptIndex++) {
560
- if (this.mailTypedSocket !== socketArg) {
561
- return;
562
- }
563
- if (attemptIndex > 0) {
564
- const shouldContinue = await this.waitForInboundEndpointReplayRetry(socketArg, delays[attemptIndex]);
565
- if (!shouldContinue) {
566
- return;
567
- }
568
- }
569
- try {
570
- await this.replayInboundEndpointRegistrationsOnce(socketArg);
571
- return;
572
- } catch (error) {
573
- lastError = error;
574
- }
575
- }
576
- throw lastError instanceof Error
577
- ? lastError
578
- : new Error('Could not re-register service mail endpoint after reconnect');
579
508
  }
580
509
 
581
- private async replayInboundEndpointRegistrationsOnce(
582
- socketArg: plugins.typedsocket.TypedSocket,
510
+ private async restoreMailConnection(
511
+ context: plugins.typedsocket.ITypedSocketConnectionRestoreContext,
512
+ url: string, signal: AbortSignal,
583
513
  ): Promise<void> {
584
- for (const address of this.inboundEndpointRegistrations.values()) {
585
- if (this.mailTypedSocket !== socketArg) {
586
- return;
587
- }
514
+ for (const address of this.inboundEndpointRegistrations) {
515
+ signal.throwIfAborted(); context.abortSignal.throwIfAborted();
588
516
  const credentials = await this.getServiceMailTypedCredentials(address);
589
- if (this.normalizeTypedSocketUrl(credentials.typedUrl) !== this.mailTypedSocketUrl) {
590
- continue;
591
- }
592
- await this.registerServiceMailEndpoint(socketArg, credentials);
593
- }
594
- }
595
-
596
- private async waitForInboundEndpointReplayRetry(
597
- socketArg: plugins.typedsocket.TypedSocket,
598
- delayMsArg: number,
599
- ): Promise<boolean> {
600
- if (this.mailTypedSocket !== socketArg) {
601
- return false;
602
- }
603
- const abortSignal = this.mailTypedSocketAbortController?.signal;
604
- if (abortSignal?.aborted) {
605
- return false;
517
+ if (credentials.typedUrl !== url) continue;
518
+ await this.registerServiceMailEndpoint({ createTypedRequest: context.createTypedRequest }, credentials, context.abortSignal);
519
+ signal.throwIfAborted(); context.abortSignal.throwIfAborted();
606
520
  }
607
- return await new Promise<boolean>((resolve) => {
608
- const resolveOnce = (resultArg: boolean) => {
609
- clearTimeout(timeout);
610
- abortSignal?.removeEventListener('abort', abortHandler);
611
- resolve(resultArg && this.mailTypedSocket === socketArg);
612
- };
613
- const abortHandler = () => resolveOnce(false);
614
- const timeout = setTimeout(() => resolveOnce(true), delayMsArg);
615
- abortSignal?.addEventListener('abort', abortHandler, { once: true });
616
- });
617
521
  }
618
522
 
619
523
  private async registerServiceMailEndpoint(
620
- typedSocketArg: plugins.typedsocket.TypedSocket,
524
+ typedSocketArg: Pick<plugins.typedsocket.TypedSocket, 'createTypedRequest'>,
621
525
  credentialsArg: IServiceMailTypedCredentials,
526
+ abortSignal?: AbortSignal,
622
527
  ): Promise<plugins.servezoneInterfaces.requests.mail.IReq_RegisterServiceMailEndpoint['response']> {
623
528
  const request = typedSocketArg.createTypedRequest<plugins.servezoneInterfaces.requests.mail.IReq_RegisterServiceMailEndpoint>('registerServiceMailEndpoint');
529
+ request.skipHooks = true;
624
530
  const result = await request.fire({
625
531
  auth: {
626
532
  credentialId: credentialsArg.credentialId,
627
533
  credentialSecret: credentialsArg.credentialSecret,
628
534
  },
629
535
  addresses: [credentialsArg.from],
630
- });
631
- if (!result.success) {
536
+ }, { maxRetries: 0, abortSignal });
537
+ if (!result.success || result.addresses?.length !== 1 || result.addresses[0] !== credentialsArg.from) {
632
538
  throw new Error(result.message || 'Could not register service mail endpoint');
633
539
  }
634
540
  return result;
@@ -21,7 +21,7 @@ export class SzLetterConnector {
21
21
  }
22
22
 
23
23
  const typedRequest =
24
- this.platformClientRef.typedsocket.createTypedRequest<plugins.servezoneInterfaces.platform.letter.IReq_SendLetter>(
24
+ this.platformClientRef.requireTypedSocket().createTypedRequest<plugins.servezoneInterfaces.platform.letter.IReq_SendLetter>(
25
25
  'sendLetter'
26
26
  );
27
27
  const response = await typedRequest.fire(optionsArg);
@@ -20,7 +20,7 @@ export class SzPushNotificationConnector {
20
20
  }
21
21
 
22
22
  const typedRequest =
23
- this.platformClientRef.typedsocket.createTypedRequest<plugins.servezoneInterfaces.platform.pushnotification.IReq_SendPushNotification>(
23
+ this.platformClientRef.requireTypedSocket().createTypedRequest<plugins.servezoneInterfaces.platform.pushnotification.IReq_SendPushNotification>(
24
24
  'sendPushNotification'
25
25
  );
26
26
  const response = await typedRequest.fire(optionsArg);
@@ -22,7 +22,7 @@ export class SzSmsConnector {
22
22
  return;
23
23
  }
24
24
 
25
- const typedrequest = this.platformClientRef.typedsocket.createTypedRequest<plugins.servezoneInterfaces.platform.sms.IReq_SendSms>(
25
+ const typedrequest = this.platformClientRef.requireTypedSocket().createTypedRequest<plugins.servezoneInterfaces.platform.sms.IReq_SendSms>(
26
26
  'sendSms'
27
27
  );
28
28
  const response = await typedrequest.fire(messageArg);
@@ -40,7 +40,7 @@ export class SzSmsConnector {
40
40
 
41
41
 
42
42
  const typedrequest =
43
- this.platformClientRef.typedsocket.createTypedRequest<plugins.servezoneInterfaces.platform.sms.IReq_SendVerificationCode>(
43
+ this.platformClientRef.requireTypedSocket().createTypedRequest<plugins.servezoneInterfaces.platform.sms.IReq_SendVerificationCode>(
44
44
  'sendVerificationCode'
45
45
  );
46
46
  const response = await typedrequest.fire(recipientArg);
@@ -182,12 +182,8 @@ export class SzWebPushConnector {
182
182
  return;
183
183
  }
184
184
  if (socketPromise) {
185
- try {
186
- const pendingSocket = await socketPromise;
187
- await pendingSocket.stop();
188
- } catch {
189
- // A failed or aborted connection leaves no socket to close.
190
- }
185
+ const pendingSocket = await socketPromise.catch(() => undefined);
186
+ await pendingSocket?.stop();
191
187
  }
192
188
  }
193
189
 
@@ -268,6 +264,7 @@ export class SzWebPushConnector {
268
264
  }
269
265
 
270
266
  private async getWebPushTypedSocket(urlArg: string): Promise<plugins.typedsocket.TypedSocket> {
267
+ this.platformClientRef.assertRunning();
271
268
  const normalizedUrl = this.normalizeTypedSocketUrl(urlArg);
272
269
  if (this.webPushTypedSocket && this.webPushTypedSocketUrl === normalizedUrl) {
273
270
  return this.webPushTypedSocket;
@@ -278,11 +275,12 @@ export class SzWebPushConnector {
278
275
  if (this.webPushTypedSocket || this.webPushTypedSocketPromise) {
279
276
  await this.stop();
280
277
  }
278
+ this.platformClientRef.assertRunning();
281
279
  this.webPushTypedSocketUrl = normalizedUrl;
282
280
  const abortController = new AbortController();
283
281
  this.webPushTypedSocketAbortController = abortController;
284
282
  const socketPromise = plugins.typedsocket.TypedSocket.createClient(
285
- this.platformClientRef.typedrouter,
283
+ new plugins.typedrequest.TypedRouter(),
286
284
  normalizedUrl,
287
285
  { autoReconnect: true, abortSignal: abortController.signal },
288
286
  );
File without changes