@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.
- package/.smartconfig.json +4 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.platformclient.d.ts +16 -5
- package/dist_ts/classes.platformclient.js +118 -44
- package/dist_ts/email/classes.emailconnector.d.ts +3 -9
- package/dist_ts/email/classes.emailconnector.js +32 -110
- package/dist_ts/email/classes.letterconnector.js +2 -2
- package/dist_ts/email/classes.pushnotificationconnector.js +2 -2
- package/dist_ts/email/classes.smsconnector.js +3 -3
- package/dist_ts/webpush/classes.webpushconnector.js +6 -9
- package/package.json +10 -10
- package/readme.md +13 -7
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.platformclient.ts +118 -49
- package/ts/email/classes.emailconnector.ts +32 -126
- package/ts/email/classes.letterconnector.ts +1 -1
- package/ts/email/classes.pushnotificationconnector.ts +1 -1
- package/ts/email/classes.smsconnector.ts +2 -2
- package/ts/webpush/classes.webpushconnector.ts +5 -7
- /package/{license → license.md} +0 -0
|
@@ -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
|
|
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
|
|
92
|
+
this.typedrouter,
|
|
82
93
|
await this.getConnectionAddress(),
|
|
83
|
-
{
|
|
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()
|
|
105
|
+
await socket.stop();
|
|
87
106
|
throw this.createAbortError();
|
|
88
107
|
}
|
|
89
108
|
this.typedsocket = socket;
|
|
90
109
|
try {
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
504
|
-
|
|
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
|
|
582
|
-
|
|
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
|
|
585
|
-
|
|
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 (
|
|
590
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
186
|
-
|
|
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
|
-
|
|
283
|
+
new plugins.typedrequest.TypedRouter(),
|
|
286
284
|
normalizedUrl,
|
|
287
285
|
{ autoReconnect: true, abortSignal: abortController.signal },
|
|
288
286
|
);
|
/package/{license → license.md}
RENAMED
|
File without changes
|