@serve.zone/platformclient 2.4.0 → 3.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.
@@ -0,0 +1,931 @@
1
+ import * as plugins from '../plugins.js';
2
+
3
+ type TCoreMailOperation =
4
+ plugins.servezoneInterfaces.data.TCoreMailWorkloadOperation;
5
+ type TCoreMailSubmission =
6
+ plugins.servezoneInterfaces.data.ICoreMailSubmission;
7
+ type TCoreMailDelivery =
8
+ plugins.servezoneInterfaces.data.ICoreMailInboundDelivery;
9
+ type TCoreMailUploadGrant =
10
+ plugins.servezoneInterfaces.data.ICoreMailUploadGrant;
11
+ type TCoreMailDownloadGrant =
12
+ plugins.servezoneInterfaces.data.ICoreMailDownloadGrant;
13
+
14
+ export interface ISzCoreMailConnectorOptions {
15
+ endpointUrl: string;
16
+ bindingId: string;
17
+ credentialId: string;
18
+ credentialVersion: number;
19
+ credentialSecret: string;
20
+ }
21
+
22
+ export interface ISzCoreMailUploadPartOptions {
23
+ submissionId: string;
24
+ part: plugins.servezoneInterfaces.data.ICoreMailContentDescriptor;
25
+ bytes: Uint8Array;
26
+ }
27
+
28
+ export interface ISzCoreMailInboundFetchResult {
29
+ delivery: TCoreMailDelivery;
30
+ bytes: Uint8Array;
31
+ }
32
+
33
+ interface ICoreMailAuthenticatedSession {
34
+ bindingState: 'active' | 'draining';
35
+ capabilities: Array<'outbound' | 'inbound'>;
36
+ allowedOperations: TCoreMailOperation[];
37
+ transferOrigin: string;
38
+ }
39
+
40
+ interface ICoreMailConnection {
41
+ generation: number;
42
+ socket: plugins.typedsocket.TypedSocket;
43
+ statusSubscription: { unsubscribe(): void };
44
+ session: ICoreMailAuthenticatedSession;
45
+ }
46
+
47
+ interface ICoreMailPendingConnection {
48
+ generation: number;
49
+ promise: Promise<ICoreMailConnection>;
50
+ }
51
+
52
+ class CoreMailTransferError extends Error {
53
+ public constructor(
54
+ messageArg: string,
55
+ public readonly ambiguous: boolean,
56
+ ) {
57
+ super(messageArg);
58
+ this.name = 'CoreMailTransferError';
59
+ }
60
+ }
61
+
62
+ const identifierRegex = /^[A-Za-z0-9](?:[A-Za-z0-9._:-]{0,126}[A-Za-z0-9])?$/;
63
+
64
+ export class SzCoreMailConnector {
65
+ private options?: Readonly<ISzCoreMailConnectorOptions>;
66
+ private stopped = false;
67
+ private generation = 0;
68
+ private connection?: ICoreMailConnection;
69
+ private pendingConnection?: ICoreMailPendingConnection;
70
+ private readonly stopController = new AbortController();
71
+ private readonly activeOperations = new Set<Promise<unknown>>();
72
+ private stopPromise?: Promise<void>;
73
+
74
+ public get isConfigured(): boolean {
75
+ return Boolean(this.options);
76
+ }
77
+
78
+ public configure(optionsArg: ISzCoreMailConnectorOptions): void {
79
+ if (this.stopped) {
80
+ throw new Error('CoreMail connector has stopped.');
81
+ }
82
+ const options = this.normalizeOptions(optionsArg);
83
+ if (this.options) {
84
+ if (!this.optionsEqual(this.options, options)) {
85
+ throw new Error('CoreMail connector configuration is one-shot.');
86
+ }
87
+ return;
88
+ }
89
+ this.options = Object.freeze(options);
90
+ }
91
+
92
+ public async init(): Promise<void> {
93
+ await this.getAuthenticatedConnection();
94
+ }
95
+
96
+ public async prepareOutboundSubmission(
97
+ requestArg:
98
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailPrepareOutboundSubmission['request'],
99
+ ): Promise<
100
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailPrepareOutboundSubmission['response']
101
+ > {
102
+ const response = await this.fire<
103
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailPrepareOutboundSubmission
104
+ >(
105
+ 'coreMailPrepareOutboundSubmission',
106
+ {
107
+ idempotencyKey: requestArg.idempotencyKey,
108
+ message: plugins.servezoneInterfaces.data
109
+ .normalizeCoreMailOutboundMessageDescriptor(requestArg.message),
110
+ },
111
+ );
112
+ if (typeof response.replayed !== 'boolean') {
113
+ throw new Error('CoreMail returned an invalid replay state.');
114
+ }
115
+ return {
116
+ submission: plugins.servezoneInterfaces.data
117
+ .normalizeCoreMailSubmission(response.submission),
118
+ replayed: response.replayed,
119
+ };
120
+ }
121
+
122
+ public async uploadOutboundPart(
123
+ optionsArg: ISzCoreMailUploadPartOptions,
124
+ ): Promise<TCoreMailSubmission> {
125
+ return await this.trackOperation(this.uploadOutboundPartInner(optionsArg));
126
+ }
127
+
128
+ public async finalizeOutboundSubmission(
129
+ submissionIdArg: string,
130
+ ): Promise<TCoreMailSubmission> {
131
+ const response = await this.fire<
132
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailFinalizeOutboundSubmission
133
+ >('coreMailFinalizeOutboundSubmission', { submissionId: submissionIdArg });
134
+ return plugins.servezoneInterfaces.data
135
+ .normalizeCoreMailSubmission(response.submission);
136
+ }
137
+
138
+ public async getOutboundSubmission(
139
+ submissionIdArg: string,
140
+ ): Promise<TCoreMailSubmission> {
141
+ const response = await this.fire<
142
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailGetOutboundSubmission
143
+ >('coreMailGetOutboundSubmission', { submissionId: submissionIdArg });
144
+ return plugins.servezoneInterfaces.data
145
+ .normalizeCoreMailSubmission(response.submission);
146
+ }
147
+
148
+ public async listInboundDeliveries(
149
+ requestArg:
150
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailListInboundDeliveries['request'],
151
+ ): Promise<plugins.servezoneInterfaces.data.ICoreMailInboundDeliveryPage> {
152
+ const response = await this.fire<
153
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailListInboundDeliveries
154
+ >('coreMailListInboundDeliveries', requestArg);
155
+ return plugins.servezoneInterfaces.data
156
+ .normalizeCoreMailInboundDeliveryPage(response);
157
+ }
158
+
159
+ public async fetchInboundDelivery(
160
+ deliveryArg: TCoreMailDelivery,
161
+ ): Promise<ISzCoreMailInboundFetchResult> {
162
+ return await this.trackOperation(this.fetchInboundDeliveryInner(deliveryArg));
163
+ }
164
+
165
+ public async acknowledgeInboundDelivery(
166
+ requestArg:
167
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailAcknowledgeInboundDelivery['request'],
168
+ ): Promise<
169
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailAcknowledgeInboundDelivery['response']
170
+ > {
171
+ const response = await this.fire<
172
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailAcknowledgeInboundDelivery
173
+ >('coreMailAcknowledgeInboundDelivery', requestArg);
174
+ if (typeof response.replayed !== 'boolean') {
175
+ throw new Error('CoreMail returned an invalid acknowledgement replay state.');
176
+ }
177
+ return {
178
+ delivery: plugins.servezoneInterfaces.data
179
+ .normalizeCoreMailInboundDelivery(response.delivery),
180
+ replayed: response.replayed,
181
+ };
182
+ }
183
+
184
+ public stop(): Promise<void> {
185
+ this.stopPromise ??= this.stopInner();
186
+ return this.stopPromise;
187
+ }
188
+
189
+ private normalizeOptions(
190
+ optionsArg: ISzCoreMailConnectorOptions,
191
+ ): ISzCoreMailConnectorOptions {
192
+ const endpointUrl = new URL(optionsArg.endpointUrl);
193
+ if (
194
+ optionsArg.endpointUrl.trim() !== optionsArg.endpointUrl
195
+ || endpointUrl.protocol !== 'https:'
196
+ || endpointUrl.username
197
+ || endpointUrl.password
198
+ || endpointUrl.search
199
+ || endpointUrl.hash
200
+ || endpointUrl.pathname !== '/socket'
201
+ || endpointUrl.toString() !== optionsArg.endpointUrl
202
+ ) {
203
+ throw new Error('CoreMail endpointUrl must be one canonical HTTPS /socket URL.');
204
+ }
205
+ for (const [name, value] of [
206
+ ['bindingId', optionsArg.bindingId],
207
+ ['credentialId', optionsArg.credentialId],
208
+ ] as const) {
209
+ if (typeof value !== 'string' || !identifierRegex.test(value)) {
210
+ throw new Error(`CoreMail ${name} must be canonical.`);
211
+ }
212
+ }
213
+ if (
214
+ !Number.isSafeInteger(optionsArg.credentialVersion)
215
+ || optionsArg.credentialVersion < 1
216
+ ) {
217
+ throw new Error('CoreMail credentialVersion must be a positive integer.');
218
+ }
219
+ if (
220
+ typeof optionsArg.credentialSecret !== 'string'
221
+ || optionsArg.credentialSecret.length < 32
222
+ || optionsArg.credentialSecret.length > 512
223
+ ) {
224
+ throw new Error('CoreMail credentialSecret must contain 32 to 512 characters.');
225
+ }
226
+ return {
227
+ endpointUrl: optionsArg.endpointUrl,
228
+ bindingId: optionsArg.bindingId,
229
+ credentialId: optionsArg.credentialId,
230
+ credentialVersion: optionsArg.credentialVersion,
231
+ credentialSecret: optionsArg.credentialSecret,
232
+ };
233
+ }
234
+
235
+ private optionsEqual(
236
+ leftArg: Readonly<ISzCoreMailConnectorOptions>,
237
+ rightArg: Readonly<ISzCoreMailConnectorOptions>,
238
+ ): boolean {
239
+ return leftArg.endpointUrl === rightArg.endpointUrl
240
+ && leftArg.bindingId === rightArg.bindingId
241
+ && leftArg.credentialId === rightArg.credentialId
242
+ && leftArg.credentialVersion === rightArg.credentialVersion
243
+ && leftArg.credentialSecret === rightArg.credentialSecret;
244
+ }
245
+
246
+ private requireOptions(): Readonly<ISzCoreMailConnectorOptions> {
247
+ if (this.stopped) {
248
+ throw this.createAbortError();
249
+ }
250
+ if (!this.options) {
251
+ throw new Error('CoreMail connector is not configured.');
252
+ }
253
+ return this.options;
254
+ }
255
+
256
+ private async getAuthenticatedConnection(): Promise<ICoreMailConnection> {
257
+ this.requireOptions();
258
+ if (
259
+ this.connection
260
+ && this.connection.socket.getStatus() === 'connected'
261
+ ) {
262
+ return this.connection;
263
+ }
264
+ if (this.pendingConnection) {
265
+ return await this.pendingConnection.promise;
266
+ }
267
+ const generation = ++this.generation;
268
+ const promise = this.connectAndAuthenticate(generation);
269
+ this.pendingConnection = { generation, promise };
270
+ try {
271
+ return await promise;
272
+ } finally {
273
+ if (this.pendingConnection?.promise === promise) {
274
+ this.pendingConnection = undefined;
275
+ }
276
+ }
277
+ }
278
+
279
+ private async connectAndAuthenticate(
280
+ generationArg: number,
281
+ ): Promise<ICoreMailConnection> {
282
+ const options = this.requireOptions();
283
+ let socket: plugins.typedsocket.TypedSocket | undefined;
284
+ let statusSubscription: { unsubscribe(): void } | undefined;
285
+ let socketInvalidated = false;
286
+ try {
287
+ socket = await this.createSocket(options.endpointUrl);
288
+ this.assertGeneration(generationArg);
289
+ statusSubscription = socket.statusSubject.subscribe((statusArg) => {
290
+ if (statusArg !== 'connected') {
291
+ socketInvalidated = true;
292
+ this.invalidateConnection(socket!, generationArg, statusSubscription!);
293
+ }
294
+ });
295
+ const response = await this.trackOperation(socket.createTypedRequest<
296
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailAuthenticateWorkload
297
+ >('coreMailAuthenticateWorkload', undefined, {
298
+ timeoutMs: 4_500,
299
+ abortSignal: this.stopController.signal,
300
+ }).fire({
301
+ bindingId: options.bindingId,
302
+ credentialId: options.credentialId,
303
+ credentialVersion: options.credentialVersion,
304
+ credentialSecret: options.credentialSecret,
305
+ }));
306
+ this.assertGeneration(generationArg);
307
+ if (socket.getStatus() !== 'connected') {
308
+ throw new Error('CoreMail disconnected during authentication.');
309
+ }
310
+ const connection: ICoreMailConnection = {
311
+ generation: generationArg,
312
+ socket,
313
+ statusSubscription,
314
+ session: this.normalizeAuthenticationResponse(response, options),
315
+ };
316
+ this.connection = connection;
317
+ return connection;
318
+ } catch (error) {
319
+ statusSubscription?.unsubscribe();
320
+ if (!socketInvalidated) {
321
+ await socket?.stop().catch(() => undefined);
322
+ }
323
+ throw error;
324
+ }
325
+ }
326
+
327
+ private async createSocket(
328
+ endpointUrlArg: string,
329
+ ): Promise<plugins.typedsocket.TypedSocket> {
330
+ return await plugins.typedsocket.TypedSocket.createClient(
331
+ new plugins.typedrequest.TypedRouter(),
332
+ endpointUrlArg,
333
+ {
334
+ autoReconnect: false,
335
+ maxRetries: 0,
336
+ abortSignal: this.stopController.signal,
337
+ },
338
+ );
339
+ }
340
+
341
+ private normalizeAuthenticationResponse(
342
+ responseArg:
343
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailAuthenticateWorkload['response'],
344
+ optionsArg: Readonly<ISzCoreMailConnectorOptions>,
345
+ ): ICoreMailAuthenticatedSession {
346
+ if (
347
+ !responseArg
348
+ || typeof responseArg !== 'object'
349
+ || Array.isArray(responseArg)
350
+ || JSON.stringify(Object.keys(responseArg).sort()) !== JSON.stringify([
351
+ 'allowedOperations',
352
+ 'authenticated',
353
+ 'bindingState',
354
+ 'capabilities',
355
+ 'coreMailTransferOrigin',
356
+ 'credentialId',
357
+ 'credentialVersion',
358
+ ])
359
+ || responseArg.authenticated !== true
360
+ || responseArg.credentialId !== optionsArg.credentialId
361
+ || responseArg.credentialVersion !== optionsArg.credentialVersion
362
+ || (responseArg.bindingState !== 'active' && responseArg.bindingState !== 'draining')
363
+ || !Array.isArray(responseArg.capabilities)
364
+ || responseArg.capabilities.some((entryArg) =>
365
+ entryArg !== 'outbound' && entryArg !== 'inbound'
366
+ )
367
+ || new Set(responseArg.capabilities).size !== responseArg.capabilities.length
368
+ || !Array.isArray(responseArg.allowedOperations)
369
+ ) {
370
+ throw new Error('CoreMail returned invalid workload authentication authority.');
371
+ }
372
+ const expectedOperations =
373
+ plugins.servezoneInterfaces.data.resolveCoreMailWorkloadOperations(
374
+ responseArg.bindingState,
375
+ responseArg.capabilities,
376
+ );
377
+ if (
378
+ JSON.stringify(responseArg.allowedOperations)
379
+ !== JSON.stringify(expectedOperations)
380
+ ) {
381
+ throw new Error('CoreMail returned inconsistent workload operations.');
382
+ }
383
+ return Object.freeze({
384
+ bindingState: responseArg.bindingState,
385
+ capabilities: Object.freeze([...responseArg.capabilities]) as
386
+ Array<'outbound' | 'inbound'>,
387
+ allowedOperations: Object.freeze([...responseArg.allowedOperations]) as
388
+ TCoreMailOperation[],
389
+ transferOrigin: this.normalizeTransferOrigin(
390
+ responseArg.coreMailTransferOrigin,
391
+ ),
392
+ });
393
+ }
394
+
395
+ private normalizeTransferOrigin(valueArg: unknown): string {
396
+ if (typeof valueArg !== 'string') {
397
+ throw new Error('CoreMail returned an invalid transfer origin.');
398
+ }
399
+ const url = new URL(valueArg);
400
+ if (
401
+ url.protocol !== 'https:'
402
+ || url.username
403
+ || url.password
404
+ || url.search
405
+ || url.hash
406
+ || url.pathname !== '/'
407
+ || url.origin !== valueArg
408
+ ) {
409
+ throw new Error('CoreMail returned a noncanonical transfer origin.');
410
+ }
411
+ return valueArg;
412
+ }
413
+
414
+ private async fire<
415
+ TRequest extends plugins.typedrequestInterfaces.ITypedRequest,
416
+ >(
417
+ methodArg: TRequest['method'] & TCoreMailOperation,
418
+ requestArg: TRequest['request'],
419
+ ): Promise<TRequest['response']> {
420
+ const connection = await this.getAuthenticatedConnection();
421
+ if (!connection.session.allowedOperations.includes(methodArg)) {
422
+ throw new Error(`CoreMail operation ${methodArg} is unavailable.`);
423
+ }
424
+ const response = await this.trackOperation(
425
+ connection.socket.createTypedRequest<TRequest>(methodArg, undefined, {
426
+ timeoutMs: 30_000,
427
+ abortSignal: this.stopController.signal,
428
+ }).fire(requestArg),
429
+ );
430
+ this.assertGeneration(connection.generation);
431
+ return response;
432
+ }
433
+
434
+ private async prepareUploadGrant(
435
+ submissionIdArg: string,
436
+ partIdArg: string,
437
+ ): Promise<{ grant: TCoreMailUploadGrant; connection: ICoreMailConnection }> {
438
+ const connection = await this.getAuthenticatedConnection();
439
+ const method: TCoreMailOperation = 'coreMailPrepareOutboundPartUpload';
440
+ if (!connection.session.allowedOperations.includes(method)) {
441
+ throw new Error(`CoreMail operation ${method} is unavailable.`);
442
+ }
443
+ const response = await this.trackOperation(connection.socket.createTypedRequest<
444
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailPrepareOutboundPartUpload
445
+ >(method, undefined, {
446
+ timeoutMs: 30_000,
447
+ abortSignal: this.stopController.signal,
448
+ }).fire({ submissionId: submissionIdArg, partId: partIdArg }));
449
+ this.assertGeneration(connection.generation);
450
+ return {
451
+ grant: plugins.servezoneInterfaces.data.normalizeCoreMailUploadGrant(
452
+ response.grant,
453
+ ),
454
+ connection,
455
+ };
456
+ }
457
+
458
+ private async uploadOutboundPartInner(
459
+ optionsArg: ISzCoreMailUploadPartOptions,
460
+ ): Promise<TCoreMailSubmission> {
461
+ if (!(optionsArg.bytes instanceof Uint8Array)) {
462
+ throw new Error('CoreMail outbound part bytes must be a Uint8Array.');
463
+ }
464
+ const expectedSha256 = plugins.servezoneInterfaces.data
465
+ .normalizeCoreMailSha256(optionsArg.part.sha256);
466
+ const actualSha256 = await this.createSha256(optionsArg.bytes);
467
+ if (
468
+ optionsArg.bytes.byteLength !== optionsArg.part.lengthBytes
469
+ || actualSha256 !== expectedSha256
470
+ ) {
471
+ throw new Error('CoreMail outbound part bytes do not match their descriptor.');
472
+ }
473
+
474
+ let lastError: unknown;
475
+ for (let grantAttempt = 0; grantAttempt < 2; grantAttempt++) {
476
+ const { grant, connection } = await this.prepareUploadGrant(
477
+ optionsArg.submissionId,
478
+ optionsArg.part.partId,
479
+ );
480
+ this.assertUploadGrantContext(grant, optionsArg.part, connection);
481
+ let uploadError: unknown;
482
+ try {
483
+ await this.putTransfer(grant, connection.session.transferOrigin, optionsArg.bytes);
484
+ } catch (errorArg) {
485
+ uploadError = errorArg;
486
+ }
487
+
488
+ let completionError: unknown;
489
+ for (let completionAttempt = 0; completionAttempt < 2; completionAttempt++) {
490
+ try {
491
+ const response = await this.fire<
492
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailCompleteOutboundPartUpload
493
+ >('coreMailCompleteOutboundPartUpload', {
494
+ submissionId: optionsArg.submissionId,
495
+ partId: optionsArg.part.partId,
496
+ grantId: grant.grantId,
497
+ sha256: expectedSha256,
498
+ lengthBytes: optionsArg.bytes.byteLength,
499
+ });
500
+ return plugins.servezoneInterfaces.data
501
+ .normalizeCoreMailSubmission(response.submission);
502
+ } catch (errorArg) {
503
+ completionError = errorArg;
504
+ if (!this.hasCoreMailErrorCode(errorArg, 'INVALID_REQUEST')) {
505
+ throw uploadError ?? errorArg;
506
+ }
507
+ if (completionAttempt === 0) {
508
+ await this.waitForDelay(25);
509
+ }
510
+ }
511
+ }
512
+
513
+ const current = await this.getOutboundSubmission(optionsArg.submissionId);
514
+ const part = current.parts.find((entryArg) =>
515
+ entryArg.partId === optionsArg.part.partId
516
+ );
517
+ if (part?.state === 'complete') {
518
+ return current;
519
+ }
520
+ lastError = uploadError ?? completionError;
521
+ const uploadWasAmbiguous = uploadError instanceof CoreMailTransferError
522
+ && uploadError.ambiguous;
523
+ if (
524
+ grantAttempt === 0
525
+ && (uploadWasAmbiguous || !uploadError)
526
+ && this.hasCoreMailErrorCode(completionError, 'INVALID_REQUEST')
527
+ ) {
528
+ continue;
529
+ }
530
+ throw lastError;
531
+ }
532
+ throw lastError ?? new Error('CoreMail outbound part upload failed.');
533
+ }
534
+
535
+ private assertUploadGrantContext(
536
+ grantArg: TCoreMailUploadGrant,
537
+ partArg: plugins.servezoneInterfaces.data.ICoreMailContentDescriptor,
538
+ connectionArg: ICoreMailConnection,
539
+ ): void {
540
+ this.assertGeneration(connectionArg.generation);
541
+ this.resolveTransferUrl(grantArg, connectionArg.session.transferOrigin);
542
+ if (
543
+ grantArg.method !== 'PUT'
544
+ || grantArg.sha256 !== partArg.sha256
545
+ || grantArg.lengthBytes !== partArg.lengthBytes
546
+ || grantArg.contentType !== partArg.contentType
547
+ ) {
548
+ throw new Error('CoreMail upload grant does not match the prepared part.');
549
+ }
550
+ }
551
+
552
+ private async putTransfer(
553
+ grantArg: TCoreMailUploadGrant,
554
+ transferOriginArg: string,
555
+ bytesArg: Uint8Array,
556
+ ): Promise<void> {
557
+ await this.trackOperation(this.putTransferInner(
558
+ grantArg,
559
+ transferOriginArg,
560
+ bytesArg,
561
+ ));
562
+ }
563
+
564
+ private async putTransferInner(
565
+ grantArg: TCoreMailUploadGrant,
566
+ transferOriginArg: string,
567
+ bytesArg: Uint8Array,
568
+ ): Promise<void> {
569
+ const deadline = this.createTransferDeadline(
570
+ plugins.servezoneInterfaces.data.coreMailLimits.transferOverallTimeoutMs,
571
+ );
572
+ try {
573
+ let response: Response;
574
+ try {
575
+ response = await this.fetchTransfer(
576
+ this.resolveTransferUrl(grantArg, transferOriginArg),
577
+ {
578
+ method: 'PUT',
579
+ headers: {
580
+ Authorization: `Bearer ${grantArg.bearerToken}`,
581
+ 'Content-Type': grantArg.contentType,
582
+ },
583
+ body: this.copyBytesToArrayBuffer(bytesArg),
584
+ cache: 'no-store',
585
+ credentials: 'omit',
586
+ redirect: 'error',
587
+ signal: deadline.controller.signal,
588
+ },
589
+ );
590
+ } catch (error) {
591
+ if (deadline.controller.signal.aborted) {
592
+ throw deadline.controller.signal.reason || error;
593
+ }
594
+ throw new CoreMailTransferError('CoreMail upload response is ambiguous.', true);
595
+ }
596
+ if (response.status !== 204) {
597
+ await response.body?.cancel().catch(() => undefined);
598
+ throw new CoreMailTransferError(
599
+ `CoreMail upload failed with HTTP ${response.status}.`,
600
+ false,
601
+ );
602
+ }
603
+ } finally {
604
+ deadline.cleanup();
605
+ }
606
+ }
607
+
608
+ private async fetchInboundDeliveryInner(
609
+ deliveryArg: TCoreMailDelivery,
610
+ ): Promise<ISzCoreMailInboundFetchResult> {
611
+ const delivery = plugins.servezoneInterfaces.data
612
+ .normalizeCoreMailInboundDelivery(deliveryArg);
613
+ const connection = await this.getAuthenticatedConnection();
614
+ const method: TCoreMailOperation = 'coreMailPrepareInboundFetch';
615
+ if (!connection.session.allowedOperations.includes(method)) {
616
+ throw new Error(`CoreMail operation ${method} is unavailable.`);
617
+ }
618
+ const response = await this.trackOperation(connection.socket.createTypedRequest<
619
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailPrepareInboundFetch
620
+ >(method, undefined, {
621
+ timeoutMs: 30_000,
622
+ abortSignal: this.stopController.signal,
623
+ }).fire({ deliveryId: delivery.deliveryId }));
624
+ this.assertGeneration(connection.generation);
625
+ const grant = plugins.servezoneInterfaces.data
626
+ .normalizeCoreMailDownloadGrant(response.grant);
627
+ this.assertDownloadGrantContext(grant, delivery, connection);
628
+ const bytes = await this.getTransfer(
629
+ grant,
630
+ connection.session.transferOrigin,
631
+ );
632
+ const completed = await this.completeInboundFetchWithRetry(delivery, grant);
633
+ return { delivery: completed, bytes };
634
+ }
635
+
636
+ private assertDownloadGrantContext(
637
+ grantArg: TCoreMailDownloadGrant,
638
+ deliveryArg: TCoreMailDelivery,
639
+ connectionArg: ICoreMailConnection,
640
+ ): void {
641
+ this.assertGeneration(connectionArg.generation);
642
+ this.resolveTransferUrl(grantArg, connectionArg.session.transferOrigin);
643
+ if (
644
+ grantArg.method !== 'GET'
645
+ || grantArg.sha256 !== deliveryArg.rawMime.sha256
646
+ || grantArg.lengthBytes !== deliveryArg.rawMime.lengthBytes
647
+ || grantArg.contentType !== deliveryArg.rawMime.contentType
648
+ ) {
649
+ throw new Error('CoreMail download grant does not match the selected delivery.');
650
+ }
651
+ }
652
+
653
+ private async getTransfer(
654
+ grantArg: TCoreMailDownloadGrant,
655
+ transferOriginArg: string,
656
+ ): Promise<Uint8Array> {
657
+ return await this.trackOperation(this.getTransferInner(grantArg, transferOriginArg));
658
+ }
659
+
660
+ private async getTransferInner(
661
+ grantArg: TCoreMailDownloadGrant,
662
+ transferOriginArg: string,
663
+ ): Promise<Uint8Array> {
664
+ const deadline = this.createTransferDeadline(
665
+ plugins.servezoneInterfaces.data.coreMailLimits.transferOverallTimeoutMs,
666
+ );
667
+ let headerTimeout: ReturnType<typeof setTimeout> | undefined = setTimeout(() => {
668
+ deadline.controller.abort(new Error('CoreMail transfer header timeout.'));
669
+ }, plugins.servezoneInterfaces.data.coreMailLimits.transferHeaderTimeoutMs);
670
+ let responseBody: ReadableStream<Uint8Array> | undefined;
671
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
672
+ let idleTimeout: ReturnType<typeof setTimeout> | undefined;
673
+ try {
674
+ const response = await this.fetchTransfer(
675
+ this.resolveTransferUrl(grantArg, transferOriginArg),
676
+ {
677
+ method: 'GET',
678
+ headers: { Authorization: `Bearer ${grantArg.bearerToken}` },
679
+ cache: 'no-store',
680
+ credentials: 'omit',
681
+ redirect: 'error',
682
+ signal: deadline.controller.signal,
683
+ },
684
+ );
685
+ clearTimeout(headerTimeout);
686
+ headerTimeout = undefined;
687
+ responseBody = response.body || undefined;
688
+ if (
689
+ response.status !== 200
690
+ || response.headers.get('content-type') !== grantArg.contentType
691
+ || response.headers.get('content-length') !== String(grantArg.lengthBytes)
692
+ || response.headers.get('cache-control') !== 'no-store'
693
+ || response.headers.get('x-content-type-options') !== 'nosniff'
694
+ || !responseBody
695
+ ) {
696
+ throw new Error('CoreMail download response metadata is invalid.');
697
+ }
698
+ reader = responseBody.getReader();
699
+ const chunks: Uint8Array[] = [];
700
+ let totalLength = 0;
701
+ const resetIdleTimeout = () => {
702
+ if (idleTimeout) clearTimeout(idleTimeout);
703
+ idleTimeout = setTimeout(() => {
704
+ deadline.controller.abort(new Error('CoreMail transfer idle timeout.'));
705
+ }, plugins.servezoneInterfaces.data.coreMailLimits.transferIdleTimeoutMs);
706
+ };
707
+ resetIdleTimeout();
708
+ while (true) {
709
+ const result = await reader.read();
710
+ if (result.done) break;
711
+ resetIdleTimeout();
712
+ totalLength += result.value.byteLength;
713
+ if (totalLength > grantArg.lengthBytes) {
714
+ throw new Error('CoreMail download exceeds its declared length.');
715
+ }
716
+ chunks.push(result.value);
717
+ }
718
+ if (idleTimeout) clearTimeout(idleTimeout);
719
+ if (totalLength !== grantArg.lengthBytes) {
720
+ throw new Error('CoreMail download length does not match its grant.');
721
+ }
722
+ const bytes = new Uint8Array(totalLength);
723
+ let offset = 0;
724
+ for (const chunk of chunks) {
725
+ bytes.set(chunk, offset);
726
+ offset += chunk.byteLength;
727
+ }
728
+ if (await this.createSha256(bytes) !== grantArg.sha256) {
729
+ throw new Error('CoreMail download digest does not match its grant.');
730
+ }
731
+ return bytes;
732
+ } catch (error) {
733
+ if (reader) {
734
+ await reader.cancel().catch(() => undefined);
735
+ } else {
736
+ await responseBody?.cancel().catch(() => undefined);
737
+ }
738
+ throw error;
739
+ } finally {
740
+ clearTimeout(headerTimeout);
741
+ if (idleTimeout) clearTimeout(idleTimeout);
742
+ deadline.cleanup();
743
+ }
744
+ }
745
+
746
+ private async completeInboundFetchWithRetry(
747
+ deliveryArg: TCoreMailDelivery,
748
+ grantArg: TCoreMailDownloadGrant,
749
+ ): Promise<TCoreMailDelivery> {
750
+ let lastError: unknown;
751
+ for (let attempt = 0; attempt < 4; attempt++) {
752
+ try {
753
+ const response = await this.fire<
754
+ plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailCompleteInboundFetch
755
+ >('coreMailCompleteInboundFetch', {
756
+ deliveryId: deliveryArg.deliveryId,
757
+ grantId: grantArg.grantId,
758
+ sha256: grantArg.sha256,
759
+ lengthBytes: grantArg.lengthBytes,
760
+ });
761
+ const delivery = plugins.servezoneInterfaces.data
762
+ .normalizeCoreMailInboundDelivery(response.delivery);
763
+ if (delivery.deliveryId !== deliveryArg.deliveryId) {
764
+ throw new Error('CoreMail completed another inbound delivery.');
765
+ }
766
+ return delivery;
767
+ } catch (errorArg) {
768
+ lastError = errorArg;
769
+ if (
770
+ attempt === 3
771
+ || !this.hasCoreMailErrorCode(errorArg, 'INVALID_REQUEST')
772
+ ) {
773
+ throw errorArg;
774
+ }
775
+ await this.waitForDelay(25 * (2 ** attempt));
776
+ }
777
+ }
778
+ throw lastError;
779
+ }
780
+
781
+ private resolveTransferUrl(
782
+ grantArg: TCoreMailUploadGrant | TCoreMailDownloadGrant,
783
+ transferOriginArg: string,
784
+ ): string {
785
+ const url = new URL(grantArg.path, transferOriginArg);
786
+ if (
787
+ url.origin !== transferOriginArg
788
+ || url.pathname !== grantArg.path
789
+ || url.search
790
+ || url.hash
791
+ ) {
792
+ throw new Error('CoreMail transfer grant escapes its authenticated origin.');
793
+ }
794
+ return url.toString();
795
+ }
796
+
797
+ private async createSha256(
798
+ bytesArg: Uint8Array,
799
+ ): Promise<plugins.servezoneInterfaces.data.TCoreMailSha256> {
800
+ const digest = new Uint8Array(
801
+ await globalThis.crypto.subtle.digest(
802
+ 'SHA-256',
803
+ this.copyBytesToArrayBuffer(bytesArg),
804
+ ),
805
+ );
806
+ return `sha256:${Array.from(digest)
807
+ .map((byteArg) => byteArg.toString(16).padStart(2, '0'))
808
+ .join('')}`;
809
+ }
810
+
811
+ private copyBytesToArrayBuffer(bytesArg: Uint8Array): ArrayBuffer {
812
+ const bytes = new Uint8Array(bytesArg.byteLength);
813
+ bytes.set(bytesArg);
814
+ return bytes.buffer;
815
+ }
816
+
817
+ private async fetchTransfer(
818
+ inputArg: string,
819
+ initArg: RequestInit,
820
+ ): Promise<Response> {
821
+ return await globalThis.fetch(inputArg, initArg);
822
+ }
823
+
824
+ private createTransferDeadline(timeoutMsArg: number): {
825
+ controller: AbortController;
826
+ cleanup: () => void;
827
+ } {
828
+ const controller = new AbortController();
829
+ const onStop = () => controller.abort(this.stopController.signal.reason);
830
+ if (this.stopController.signal.aborted) {
831
+ onStop();
832
+ } else {
833
+ this.stopController.signal.addEventListener('abort', onStop, { once: true });
834
+ }
835
+ const timeout = setTimeout(() => {
836
+ controller.abort(new Error('CoreMail transfer overall timeout.'));
837
+ }, timeoutMsArg);
838
+ return {
839
+ controller,
840
+ cleanup: () => {
841
+ clearTimeout(timeout);
842
+ this.stopController.signal.removeEventListener('abort', onStop);
843
+ },
844
+ };
845
+ }
846
+
847
+ private hasCoreMailErrorCode(
848
+ errorArg: unknown,
849
+ codeArg: plugins.servezoneInterfaces.data.TCoreMailErrorCode,
850
+ ): boolean {
851
+ if (!(errorArg instanceof plugins.typedrequest.TypedResponseError)) {
852
+ return false;
853
+ }
854
+ try {
855
+ return plugins.servezoneInterfaces.data
856
+ .normalizeCoreMailErrorData(errorArg.errorData).code === codeArg;
857
+ } catch {
858
+ return false;
859
+ }
860
+ }
861
+
862
+ private async waitForDelay(delayMsArg: number): Promise<void> {
863
+ if (this.stopController.signal.aborted) throw this.createAbortError();
864
+ await this.trackOperation(new Promise<void>((resolveArg, rejectArg) => {
865
+ const timeout = setTimeout(() => {
866
+ this.stopController.signal.removeEventListener('abort', onAbort);
867
+ resolveArg();
868
+ }, delayMsArg);
869
+ const onAbort = () => {
870
+ clearTimeout(timeout);
871
+ rejectArg(this.createAbortError());
872
+ };
873
+ this.stopController.signal.addEventListener('abort', onAbort, { once: true });
874
+ }));
875
+ }
876
+
877
+ private invalidateConnection(
878
+ socketArg: plugins.typedsocket.TypedSocket,
879
+ generationArg: number,
880
+ subscriptionArg: { unsubscribe(): void },
881
+ ): void {
882
+ const ownsConnection = this.connection?.socket === socketArg
883
+ && this.connection.generation === generationArg;
884
+ const ownsPending = this.pendingConnection?.generation === generationArg;
885
+ if (!ownsConnection && !ownsPending) return;
886
+ this.generation++;
887
+ if (ownsConnection) this.connection = undefined;
888
+ if (ownsPending) this.pendingConnection = undefined;
889
+ subscriptionArg.unsubscribe();
890
+ void socketArg.stop().catch(() => undefined);
891
+ }
892
+
893
+ private assertGeneration(generationArg: number): void {
894
+ if (this.stopped || generationArg !== this.generation) {
895
+ throw this.createAbortError();
896
+ }
897
+ }
898
+
899
+ private trackOperation<T>(promiseArg: Promise<T>): Promise<T> {
900
+ this.activeOperations.add(promiseArg);
901
+ return promiseArg.finally(() => {
902
+ this.activeOperations.delete(promiseArg);
903
+ });
904
+ }
905
+
906
+ private async stopInner(): Promise<void> {
907
+ this.stopped = true;
908
+ this.generation++;
909
+ this.stopController.abort(this.createAbortError());
910
+ const connection = this.connection;
911
+ const pendingConnection = this.pendingConnection;
912
+ this.connection = undefined;
913
+ this.pendingConnection = undefined;
914
+ connection?.statusSubscription.unsubscribe();
915
+ await Promise.allSettled([
916
+ connection?.socket.stop(),
917
+ pendingConnection?.promise.then(async (resultArg) => {
918
+ resultArg.statusSubscription.unsubscribe();
919
+ await resultArg.socket.stop();
920
+ }),
921
+ ...this.activeOperations,
922
+ ].filter(Boolean) as Promise<unknown>[]);
923
+ this.options = undefined;
924
+ }
925
+
926
+ private createAbortError(): Error {
927
+ const error = new Error('CoreMail connector operation aborted.');
928
+ error.name = 'AbortError';
929
+ return error;
930
+ }
931
+ }