@serve.zone/interfaces 21.1.0 → 23.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/changelog.md +23 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/appstore/types.d.ts +31 -16
- package/dist_ts/appstore/types.js +208 -11
- package/dist_ts/data/coremail.d.ts +83 -9
- package/dist_ts/data/coremail.js +67 -1
- package/dist_ts/data/coremail.runtime.d.ts +27 -1
- package/dist_ts/data/coremail.runtime.js +800 -36
- package/dist_ts/data/deploymentoperation.js +6 -20
- package/dist_ts/data/deploymentpreflight.d.ts +2 -6
- package/dist_ts/data/deploymentpreflight.js +1 -1
- package/dist_ts/data/hostedapp.d.ts +40 -14
- package/dist_ts/data/hostedapp.js +90 -1
- package/dist_ts/data/immutableimage.d.ts +30 -1
- package/dist_ts/data/immutableimage.js +103 -1
- package/dist_ts/data/index.d.ts +0 -2
- package/dist_ts/data/index.js +1 -3
- package/dist_ts/data/secret.d.ts +186 -13
- package/dist_ts/data/secret.js +543 -45
- package/dist_ts/data/service.d.ts +0 -39
- package/dist_ts/data/service.js +1 -77
- package/dist_ts/data/settings.d.ts +62 -43
- package/dist_ts/data/settings.js +34 -2
- package/dist_ts/platform/storage.d.ts +13 -14
- package/dist_ts/platform/storage.js +1 -1
- package/dist_ts/platform/storagemigration.d.ts +8 -8
- package/dist_ts/platform/storagemigration.golden.d.ts +2 -2
- package/dist_ts/platform/storagemigration.golden.js +19 -88
- package/dist_ts/platform/storagemigration.js +46 -58
- package/dist_ts/platform/types.d.ts +5 -19
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts/requests/coremail.d.ts +12 -5
- package/dist_ts/requests/hostedapp.d.ts +12 -9
- package/dist_ts/requests/hostedapp.js +38 -1
- package/dist_ts/requests/index.d.ts +1 -3
- package/dist_ts/requests/index.js +2 -4
- package/dist_ts/requests/platform.d.ts +0 -1
- package/dist_ts/requests/secret.d.ts +23 -17
- package/dist_ts/requests/secret.js +161 -2
- package/dist_ts/requests/service.d.ts +0 -13
- package/dist_ts/requests/settings.d.ts +3 -3
- package/dist_ts/runtime.d.ts +78 -26
- package/dist_ts/runtime.js +219 -19
- package/package.json +2 -1
- package/readme.md +163 -38
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/appstore/types.ts +251 -25
- package/ts/data/coremail.runtime.ts +1212 -38
- package/ts/data/coremail.ts +120 -8
- package/ts/data/deploymentoperation.ts +7 -31
- package/ts/data/deploymentpreflight.ts +2 -5
- package/ts/data/hostedapp.ts +144 -18
- package/ts/data/immutableimage.ts +150 -1
- package/ts/data/index.ts +0 -2
- package/ts/data/secret.ts +775 -53
- package/ts/data/service.ts +0 -118
- package/ts/data/settings.ts +70 -80
- package/ts/platform/storage.ts +15 -16
- package/ts/platform/storagemigration.golden.ts +18 -95
- package/ts/platform/storagemigration.ts +57 -87
- package/ts/platform/types.ts +6 -19
- package/ts/plugins.ts +2 -0
- package/ts/requests/coremail.ts +13 -4
- package/ts/requests/hostedapp.ts +50 -9
- package/ts/requests/index.ts +0 -4
- package/ts/requests/platform.ts +0 -1
- package/ts/requests/secret.ts +204 -9
- package/ts/requests/service.ts +0 -16
- package/ts/requests/settings.ts +3 -3
- package/ts/runtime.ts +314 -41
- package/dist_ts/data/secretbundle.d.ts +0 -57
- package/dist_ts/data/secretbundle.js +0 -2
- package/dist_ts/data/secretgroup.d.ts +0 -52
- package/dist_ts/data/secretgroup.js +0 -2
- package/dist_ts/requests/secretbundle.d.ts +0 -75
- package/dist_ts/requests/secretbundle.js +0 -4
- package/dist_ts/requests/secretgroup.d.ts +0 -52
- package/dist_ts/requests/secretgroup.js +0 -4
- package/ts/data/secretbundle.ts +0 -65
- package/ts/data/secretgroup.ts +0 -58
- package/ts/requests/secretbundle.ts +0 -103
- package/ts/requests/secretgroup.ts +0 -74
|
@@ -6,18 +6,58 @@ import {
|
|
|
6
6
|
} from '../private/canonicaljson.js';
|
|
7
7
|
import {
|
|
8
8
|
coreMailCredentialVerifierPolicy,
|
|
9
|
+
coreMailLimits,
|
|
10
|
+
coreMailTransferProtocol,
|
|
11
|
+
coreMailTransferTokenPolicy,
|
|
12
|
+
coreMailWorkloadOperationPolicy,
|
|
13
|
+
type ICoreMailBindingReconciliationStatus,
|
|
9
14
|
type ICoreMailBindingCredentialVerifier,
|
|
10
15
|
type ICoreMailBindingDesiredState,
|
|
16
|
+
type ICoreMailContentDescriptor,
|
|
11
17
|
type ICoreMailControlBootstrap,
|
|
12
18
|
type ICoreMailDesiredState,
|
|
19
|
+
type ICoreMailDownloadGrant,
|
|
20
|
+
type ICoreMailEnvelope,
|
|
21
|
+
type ICoreMailErrorData,
|
|
13
22
|
type ICoreMailGatewayDesiredState,
|
|
23
|
+
type ICoreMailGatewayOutboundStatus,
|
|
14
24
|
type ICoreMailGatewayPeerDesiredState,
|
|
25
|
+
type ICoreMailInboundDelivery,
|
|
26
|
+
type ICoreMailInboundDeliveryPage,
|
|
27
|
+
type ICoreMailMailbox,
|
|
28
|
+
type ICoreMailOutboundMessageDescriptor,
|
|
29
|
+
type ICoreMailPartStatus,
|
|
30
|
+
type ICoreMailRuntimeKeyReference,
|
|
31
|
+
type ICoreMailReconciliationStatus,
|
|
32
|
+
type ICoreMailReplicaIdentity,
|
|
33
|
+
type ICoreMailSubmission,
|
|
34
|
+
type ICoreMailTransferGrant,
|
|
35
|
+
type ICoreMailUploadGrant,
|
|
15
36
|
type TCoreMailCapability,
|
|
37
|
+
type TCoreMailBindingState,
|
|
38
|
+
type TCoreMailErrorCode,
|
|
16
39
|
type TCoreMailSha256,
|
|
40
|
+
type TCoreMailWorkloadOperation,
|
|
17
41
|
} from './coremail.js';
|
|
18
42
|
|
|
19
43
|
export const coreMailCanonicalJsonRules = strictCanonicalJsonRules;
|
|
20
44
|
|
|
45
|
+
export const resolveCoreMailWorkloadOperations = (
|
|
46
|
+
stateArg: TCoreMailBindingState,
|
|
47
|
+
capabilitiesArg: readonly TCoreMailCapability[],
|
|
48
|
+
): readonly TCoreMailWorkloadOperation[] => {
|
|
49
|
+
const operations = new Set<TCoreMailWorkloadOperation>();
|
|
50
|
+
for (const capability of ['outbound', 'inbound'] as const) {
|
|
51
|
+
if (!capabilitiesArg.includes(capability)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
for (const operation of coreMailWorkloadOperationPolicy[stateArg][capability]) {
|
|
55
|
+
operations.add(operation);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return Object.freeze([...operations]);
|
|
59
|
+
};
|
|
60
|
+
|
|
21
61
|
export const coreMailContractLimits = Object.freeze({
|
|
22
62
|
maximumIdentifierBytes: 128,
|
|
23
63
|
maximumEnvironmentKeyBytes: 128,
|
|
@@ -27,6 +67,16 @@ export const coreMailContractLimits = Object.freeze({
|
|
|
27
67
|
maximumCredentialsPerAuthority: 16,
|
|
28
68
|
maximumAllowedSendersPerBinding: 1_000,
|
|
29
69
|
maximumInboundRecipientsPerBinding: 10_000,
|
|
70
|
+
maximumMessagesPerMinute: 100_000,
|
|
71
|
+
maximumMessagesPerDay: 10_000_000,
|
|
72
|
+
maximumPendingInbound: 1_000_000,
|
|
73
|
+
maximumDisplayNameBytes: 256,
|
|
74
|
+
maximumSubjectBytes: 768,
|
|
75
|
+
maximumHeaderCount: 32,
|
|
76
|
+
maximumHeaderValueBytes: 768,
|
|
77
|
+
maximumFilenameBytes: 512,
|
|
78
|
+
maximumContentIdBytes: 256,
|
|
79
|
+
maximumBearerTokenBytes: 1_024,
|
|
30
80
|
} as const);
|
|
31
81
|
|
|
32
82
|
export class CoreMailContractError extends Error {
|
|
@@ -145,6 +195,10 @@ const requireString = (
|
|
|
145
195
|
};
|
|
146
196
|
|
|
147
197
|
const identifierRegex = /^[A-Za-z0-9](?:[A-Za-z0-9._:-]{0,126}[A-Za-z0-9])?$/;
|
|
198
|
+
const uuidRegex =
|
|
199
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
200
|
+
const contentTypeRegex =
|
|
201
|
+
/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+(?:; ?[a-z0-9!#$&^_.+-]+=(?:"[^"\r\n]{1,100}"|[a-z0-9!#$&^_.+-]+))*$/i;
|
|
148
202
|
|
|
149
203
|
const requireIdentifier = (valueArg: unknown, pathArg: string): string => {
|
|
150
204
|
const value = requireString(
|
|
@@ -158,13 +212,51 @@ const requireIdentifier = (valueArg: unknown, pathArg: string): string => {
|
|
|
158
212
|
return value;
|
|
159
213
|
};
|
|
160
214
|
|
|
215
|
+
const requireUuid = (valueArg: unknown, pathArg: string): string => {
|
|
216
|
+
if (typeof valueArg !== 'string' || !uuidRegex.test(valueArg)) {
|
|
217
|
+
return fail(`${pathArg} must be a canonical UUID`);
|
|
218
|
+
}
|
|
219
|
+
return valueArg;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const requireContentType = (valueArg: unknown, pathArg: string): string => {
|
|
223
|
+
if (typeof valueArg !== 'string' || !contentTypeRegex.test(valueArg)) {
|
|
224
|
+
return fail(`${pathArg} must be a canonical content type`);
|
|
225
|
+
}
|
|
226
|
+
return valueArg;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const requireDisplayText = (
|
|
230
|
+
valueArg: unknown,
|
|
231
|
+
pathArg: string,
|
|
232
|
+
maximumBytesArg: number,
|
|
233
|
+
allowEmptyArg = false,
|
|
234
|
+
): string => {
|
|
235
|
+
if (
|
|
236
|
+
typeof valueArg !== 'string'
|
|
237
|
+
|| (!allowEmptyArg && valueArg.length === 0)
|
|
238
|
+
|| /[\u0000-\u001f\u007f]/.test(valueArg)
|
|
239
|
+
|| new TextEncoder().encode(valueArg).byteLength > maximumBytesArg
|
|
240
|
+
) {
|
|
241
|
+
return fail(`${pathArg} must be bounded display text`);
|
|
242
|
+
}
|
|
243
|
+
return valueArg;
|
|
244
|
+
};
|
|
245
|
+
|
|
161
246
|
const requireSafeInteger = (
|
|
162
247
|
valueArg: unknown,
|
|
163
248
|
pathArg: string,
|
|
164
249
|
minimumArg: number,
|
|
250
|
+
maximumArg = Number.MAX_SAFE_INTEGER,
|
|
165
251
|
): number => {
|
|
166
|
-
if (
|
|
167
|
-
|
|
252
|
+
if (
|
|
253
|
+
!Number.isSafeInteger(valueArg)
|
|
254
|
+
|| (valueArg as number) < minimumArg
|
|
255
|
+
|| (valueArg as number) > maximumArg
|
|
256
|
+
) {
|
|
257
|
+
return fail(
|
|
258
|
+
`${pathArg} must be a safe integer between ${minimumArg} and ${maximumArg}`,
|
|
259
|
+
);
|
|
168
260
|
}
|
|
169
261
|
return valueArg as number;
|
|
170
262
|
};
|
|
@@ -311,6 +403,94 @@ const normalizeCredentialSet = (
|
|
|
311
403
|
);
|
|
312
404
|
};
|
|
313
405
|
|
|
406
|
+
const requireEnvironmentKey = (valueArg: unknown, pathArg: string): string => {
|
|
407
|
+
const value = requireString(
|
|
408
|
+
valueArg,
|
|
409
|
+
pathArg,
|
|
410
|
+
coreMailContractLimits.maximumEnvironmentKeyBytes,
|
|
411
|
+
);
|
|
412
|
+
if (!/^[A-Z][A-Z0-9_]{0,127}$/.test(value)) {
|
|
413
|
+
fail(`${pathArg} must be a canonical environment key`);
|
|
414
|
+
}
|
|
415
|
+
return value;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
export const normalizeCoreMailRuntimeKeyReference = (
|
|
419
|
+
valueArg: unknown,
|
|
420
|
+
pathArg = 'runtimeKey',
|
|
421
|
+
): ICoreMailRuntimeKeyReference => {
|
|
422
|
+
const record = readRecord(valueArg, pathArg);
|
|
423
|
+
assertKeys(
|
|
424
|
+
record,
|
|
425
|
+
pathArg,
|
|
426
|
+
['keyId', 'version', 'state', 'secretKey'],
|
|
427
|
+
['acceptUntil'],
|
|
428
|
+
);
|
|
429
|
+
const state = record.state === 'current' || record.state === 'retiring'
|
|
430
|
+
? record.state
|
|
431
|
+
: fail(`${pathArg}.state must be current or retiring`);
|
|
432
|
+
if (state === 'current' && Object.hasOwn(record, 'acceptUntil')) {
|
|
433
|
+
fail(`${pathArg}.acceptUntil is forbidden for a current key`);
|
|
434
|
+
}
|
|
435
|
+
if (state === 'retiring' && !Object.hasOwn(record, 'acceptUntil')) {
|
|
436
|
+
fail(`${pathArg}.acceptUntil is required for a retiring key`);
|
|
437
|
+
}
|
|
438
|
+
const normalized: ICoreMailRuntimeKeyReference = {
|
|
439
|
+
keyId: requireIdentifier(record.keyId, `${pathArg}.keyId`),
|
|
440
|
+
version: requireSafeInteger(record.version, `${pathArg}.version`, 1),
|
|
441
|
+
state,
|
|
442
|
+
secretKey: requireEnvironmentKey(record.secretKey, `${pathArg}.secretKey`),
|
|
443
|
+
};
|
|
444
|
+
if (state === 'retiring') {
|
|
445
|
+
normalized.acceptUntil = requireSafeInteger(
|
|
446
|
+
record.acceptUntil,
|
|
447
|
+
`${pathArg}.acceptUntil`,
|
|
448
|
+
1,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
return deepFreezeValue(normalized);
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const normalizeRuntimeKeySet = (
|
|
455
|
+
valueArg: unknown,
|
|
456
|
+
pathArg: string,
|
|
457
|
+
): ICoreMailRuntimeKeyReference[] => {
|
|
458
|
+
const keys = readArray(
|
|
459
|
+
valueArg,
|
|
460
|
+
pathArg,
|
|
461
|
+
coreMailContractLimits.maximumCredentialsPerAuthority,
|
|
462
|
+
).map((entryArg, indexArg) =>
|
|
463
|
+
normalizeCoreMailRuntimeKeyReference(entryArg, `${pathArg}[${indexArg}]`),
|
|
464
|
+
);
|
|
465
|
+
if (keys.length === 0 || keys.filter((entryArg) => entryArg.state === 'current').length !== 1) {
|
|
466
|
+
fail(`${pathArg} must contain exactly one current key`);
|
|
467
|
+
}
|
|
468
|
+
const keyIds = new Set<string>();
|
|
469
|
+
const versions = new Set<number>();
|
|
470
|
+
const secretKeys = new Set<string>();
|
|
471
|
+
for (const key of keys) {
|
|
472
|
+
if (
|
|
473
|
+
keyIds.has(key.keyId)
|
|
474
|
+
|| versions.has(key.version)
|
|
475
|
+
|| secretKeys.has(key.secretKey)
|
|
476
|
+
) {
|
|
477
|
+
fail(`${pathArg} contains a duplicate keyId, version, or secretKey`);
|
|
478
|
+
}
|
|
479
|
+
keyIds.add(key.keyId);
|
|
480
|
+
versions.add(key.version);
|
|
481
|
+
secretKeys.add(key.secretKey);
|
|
482
|
+
}
|
|
483
|
+
const current = keys.find((entryArg) => entryArg.state === 'current')!;
|
|
484
|
+
if (keys.some((entryArg) =>
|
|
485
|
+
entryArg.state === 'retiring' && entryArg.version >= current.version
|
|
486
|
+
)) {
|
|
487
|
+
fail(`${pathArg} current key must have the greatest version`);
|
|
488
|
+
}
|
|
489
|
+
return keys.sort((leftArg, rightArg) =>
|
|
490
|
+
leftArg.version - rightArg.version || compareStrings(leftArg.keyId, rightArg.keyId),
|
|
491
|
+
);
|
|
492
|
+
};
|
|
493
|
+
|
|
314
494
|
const mailboxLocalPartRegex = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+$/;
|
|
315
495
|
const mailboxDomainLabelRegex = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
316
496
|
|
|
@@ -342,6 +522,839 @@ const requireCanonicalMailbox = (valueArg: unknown, pathArg: string): string =>
|
|
|
342
522
|
return value;
|
|
343
523
|
};
|
|
344
524
|
|
|
525
|
+
const normalizeMailbox = (valueArg: unknown, pathArg: string): ICoreMailMailbox => {
|
|
526
|
+
const record = readRecord(valueArg, pathArg);
|
|
527
|
+
assertKeys(record, pathArg, ['address'], ['displayName']);
|
|
528
|
+
const normalized: ICoreMailMailbox = {
|
|
529
|
+
address: requireCanonicalMailbox(record.address, `${pathArg}.address`),
|
|
530
|
+
};
|
|
531
|
+
if (Object.hasOwn(record, 'displayName')) {
|
|
532
|
+
normalized.displayName = requireDisplayText(
|
|
533
|
+
record.displayName,
|
|
534
|
+
`${pathArg}.displayName`,
|
|
535
|
+
coreMailContractLimits.maximumDisplayNameBytes,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
return normalized;
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const normalizeMailboxArray = (
|
|
542
|
+
valueArg: unknown,
|
|
543
|
+
pathArg: string,
|
|
544
|
+
allowEmptyArg: boolean,
|
|
545
|
+
): ICoreMailMailbox[] => {
|
|
546
|
+
const values = readArray(valueArg, pathArg, coreMailLimits.recipientCount);
|
|
547
|
+
if (!allowEmptyArg && values.length === 0) {
|
|
548
|
+
fail(`${pathArg} must not be empty`);
|
|
549
|
+
}
|
|
550
|
+
const normalized = values.map((entryArg, indexArg) =>
|
|
551
|
+
normalizeMailbox(entryArg, `${pathArg}[${indexArg}]`),
|
|
552
|
+
);
|
|
553
|
+
if (new Set(normalized.map((entryArg) => entryArg.address)).size !== normalized.length) {
|
|
554
|
+
fail(`${pathArg} must contain unique mailbox addresses`);
|
|
555
|
+
}
|
|
556
|
+
return normalized;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
export const normalizeCoreMailEnvelope = (
|
|
560
|
+
valueArg: unknown,
|
|
561
|
+
pathArg = 'envelope',
|
|
562
|
+
): ICoreMailEnvelope => {
|
|
563
|
+
const record = readRecord(valueArg, pathArg);
|
|
564
|
+
assertKeys(record, pathArg, ['mailFrom', 'rcptTo']);
|
|
565
|
+
const mailFrom = record.mailFrom === ''
|
|
566
|
+
? ''
|
|
567
|
+
: requireCanonicalMailbox(record.mailFrom, `${pathArg}.mailFrom`);
|
|
568
|
+
const rcptTo = readArray(
|
|
569
|
+
record.rcptTo,
|
|
570
|
+
`${pathArg}.rcptTo`,
|
|
571
|
+
coreMailLimits.recipientCount,
|
|
572
|
+
).map((entryArg, indexArg) =>
|
|
573
|
+
requireCanonicalMailbox(entryArg, `${pathArg}.rcptTo[${indexArg}]`),
|
|
574
|
+
);
|
|
575
|
+
if (rcptTo.length === 0 || new Set(rcptTo).size !== rcptTo.length) {
|
|
576
|
+
fail(`${pathArg}.rcptTo must contain unique recipients`);
|
|
577
|
+
}
|
|
578
|
+
return deepFreezeValue({ mailFrom, rcptTo });
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const forbiddenHeaderNames = new Set([
|
|
582
|
+
'bcc',
|
|
583
|
+
'cc',
|
|
584
|
+
'content-transfer-encoding',
|
|
585
|
+
'content-type',
|
|
586
|
+
'date',
|
|
587
|
+
'delivered-to',
|
|
588
|
+
'dkim-signature',
|
|
589
|
+
'domainkey-signature',
|
|
590
|
+
'from',
|
|
591
|
+
'message-id',
|
|
592
|
+
'mime-version',
|
|
593
|
+
'received',
|
|
594
|
+
'received-spf',
|
|
595
|
+
'reply-to',
|
|
596
|
+
'return-path',
|
|
597
|
+
'sender',
|
|
598
|
+
'subject',
|
|
599
|
+
'to',
|
|
600
|
+
'x-original-to',
|
|
601
|
+
]);
|
|
602
|
+
|
|
603
|
+
const isTransportOwnedHeader = (nameArg: string): boolean =>
|
|
604
|
+
forbiddenHeaderNames.has(nameArg)
|
|
605
|
+
|| nameArg === 'authentication-results'
|
|
606
|
+
|| nameArg.startsWith('arc-')
|
|
607
|
+
|| nameArg.startsWith('resent-')
|
|
608
|
+
|| nameArg.startsWith('x-coremail-');
|
|
609
|
+
|
|
610
|
+
const normalizeContentDescriptor = (
|
|
611
|
+
valueArg: unknown,
|
|
612
|
+
pathArg: string,
|
|
613
|
+
): ICoreMailContentDescriptor => {
|
|
614
|
+
const record = readRecord(valueArg, pathArg);
|
|
615
|
+
assertKeys(
|
|
616
|
+
record,
|
|
617
|
+
pathArg,
|
|
618
|
+
['partId', 'kind', 'contentType', 'sha256', 'lengthBytes'],
|
|
619
|
+
['filename', 'contentId'],
|
|
620
|
+
);
|
|
621
|
+
const kind = record.kind === 'text'
|
|
622
|
+
|| record.kind === 'html'
|
|
623
|
+
|| record.kind === 'attachment'
|
|
624
|
+
? record.kind
|
|
625
|
+
: fail(`${pathArg}.kind is invalid`);
|
|
626
|
+
const maximumBytes = kind === 'text'
|
|
627
|
+
? coreMailLimits.textPartBytes
|
|
628
|
+
: kind === 'html'
|
|
629
|
+
? coreMailLimits.htmlPartBytes
|
|
630
|
+
: coreMailLimits.attachmentBytes;
|
|
631
|
+
const normalized: ICoreMailContentDescriptor = {
|
|
632
|
+
partId: requireIdentifier(record.partId, `${pathArg}.partId`),
|
|
633
|
+
kind,
|
|
634
|
+
contentType: requireContentType(record.contentType, `${pathArg}.contentType`),
|
|
635
|
+
sha256: normalizeCoreMailSha256(record.sha256, `${pathArg}.sha256`),
|
|
636
|
+
lengthBytes: requireSafeInteger(
|
|
637
|
+
record.lengthBytes,
|
|
638
|
+
`${pathArg}.lengthBytes`,
|
|
639
|
+
0,
|
|
640
|
+
maximumBytes,
|
|
641
|
+
),
|
|
642
|
+
};
|
|
643
|
+
if (Object.hasOwn(record, 'filename')) {
|
|
644
|
+
normalized.filename = requireDisplayText(
|
|
645
|
+
record.filename,
|
|
646
|
+
`${pathArg}.filename`,
|
|
647
|
+
coreMailContractLimits.maximumFilenameBytes,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (Object.hasOwn(record, 'contentId')) {
|
|
651
|
+
normalized.contentId = requireDisplayText(
|
|
652
|
+
record.contentId,
|
|
653
|
+
`${pathArg}.contentId`,
|
|
654
|
+
coreMailContractLimits.maximumContentIdBytes,
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
return normalized;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
export const normalizeCoreMailOutboundMessageDescriptor = (
|
|
661
|
+
valueArg: unknown,
|
|
662
|
+
pathArg = 'message',
|
|
663
|
+
): ICoreMailOutboundMessageDescriptor => {
|
|
664
|
+
const record = readRecord(valueArg, pathArg);
|
|
665
|
+
assertKeys(
|
|
666
|
+
record,
|
|
667
|
+
pathArg,
|
|
668
|
+
['sender', 'recipients', 'subject', 'parts'],
|
|
669
|
+
['replyTo', 'headers'],
|
|
670
|
+
);
|
|
671
|
+
const recipientsRecord = readRecord(record.recipients, `${pathArg}.recipients`);
|
|
672
|
+
assertKeys(recipientsRecord, `${pathArg}.recipients`, ['to'], ['cc', 'bcc']);
|
|
673
|
+
const to = normalizeMailboxArray(recipientsRecord.to, `${pathArg}.recipients.to`, false);
|
|
674
|
+
const cc = Object.hasOwn(recipientsRecord, 'cc')
|
|
675
|
+
? normalizeMailboxArray(recipientsRecord.cc, `${pathArg}.recipients.cc`, true)
|
|
676
|
+
: undefined;
|
|
677
|
+
const bcc = Object.hasOwn(recipientsRecord, 'bcc')
|
|
678
|
+
? normalizeMailboxArray(recipientsRecord.bcc, `${pathArg}.recipients.bcc`, true)
|
|
679
|
+
: undefined;
|
|
680
|
+
const recipientAddresses = [...to, ...(cc || []), ...(bcc || [])]
|
|
681
|
+
.map((entryArg) => entryArg.address);
|
|
682
|
+
if (
|
|
683
|
+
recipientAddresses.length > coreMailLimits.recipientCount
|
|
684
|
+
|| new Set(recipientAddresses).size !== recipientAddresses.length
|
|
685
|
+
) {
|
|
686
|
+
fail(`${pathArg}.recipients must contain at most ${coreMailLimits.recipientCount} unique addresses`);
|
|
687
|
+
}
|
|
688
|
+
const parts = readArray(
|
|
689
|
+
record.parts,
|
|
690
|
+
`${pathArg}.parts`,
|
|
691
|
+
coreMailLimits.attachmentCount + 2,
|
|
692
|
+
).map((entryArg, indexArg) =>
|
|
693
|
+
normalizeContentDescriptor(entryArg, `${pathArg}.parts[${indexArg}]`),
|
|
694
|
+
);
|
|
695
|
+
if (
|
|
696
|
+
parts.length === 0
|
|
697
|
+
|| new Set(parts.map((entryArg) => entryArg.partId)).size !== parts.length
|
|
698
|
+
|| parts.filter((entryArg) => entryArg.kind === 'text').length > 1
|
|
699
|
+
|| parts.filter((entryArg) => entryArg.kind === 'html').length > 1
|
|
700
|
+
|| parts.filter((entryArg) => entryArg.kind === 'attachment').length > coreMailLimits.attachmentCount
|
|
701
|
+
|| parts.filter((entryArg) => entryArg.kind === 'attachment')
|
|
702
|
+
.reduce((totalArg, entryArg) => totalArg + entryArg.lengthBytes, 0)
|
|
703
|
+
> coreMailLimits.aggregateAttachmentBytes
|
|
704
|
+
) {
|
|
705
|
+
fail(`${pathArg}.parts is invalid`);
|
|
706
|
+
}
|
|
707
|
+
let headers: ICoreMailOutboundMessageDescriptor['headers'];
|
|
708
|
+
if (Object.hasOwn(record, 'headers')) {
|
|
709
|
+
const seenHeaderNames = new Set<string>();
|
|
710
|
+
headers = readArray(
|
|
711
|
+
record.headers,
|
|
712
|
+
`${pathArg}.headers`,
|
|
713
|
+
coreMailContractLimits.maximumHeaderCount,
|
|
714
|
+
).map((entryArg, indexArg) => {
|
|
715
|
+
const headerPath = `${pathArg}.headers[${indexArg}]`;
|
|
716
|
+
const headerRecord = readRecord(entryArg, headerPath);
|
|
717
|
+
assertKeys(headerRecord, headerPath, ['name', 'value']);
|
|
718
|
+
if (
|
|
719
|
+
typeof headerRecord.name !== 'string'
|
|
720
|
+
|| !/^[A-Za-z][A-Za-z0-9-]{0,62}$/.test(headerRecord.name)
|
|
721
|
+
) {
|
|
722
|
+
return fail(`${headerPath}.name is invalid`);
|
|
723
|
+
}
|
|
724
|
+
const lowerName = headerRecord.name.toLowerCase();
|
|
725
|
+
if (isTransportOwnedHeader(lowerName) || seenHeaderNames.has(lowerName)) {
|
|
726
|
+
return fail(`${headerPath}.name is transport-owned or duplicated`);
|
|
727
|
+
}
|
|
728
|
+
seenHeaderNames.add(lowerName);
|
|
729
|
+
return {
|
|
730
|
+
name: headerRecord.name,
|
|
731
|
+
value: requireDisplayText(
|
|
732
|
+
headerRecord.value,
|
|
733
|
+
`${headerPath}.value`,
|
|
734
|
+
coreMailContractLimits.maximumHeaderValueBytes,
|
|
735
|
+
true,
|
|
736
|
+
),
|
|
737
|
+
};
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
const normalized: ICoreMailOutboundMessageDescriptor = {
|
|
741
|
+
sender: normalizeMailbox(record.sender, `${pathArg}.sender`),
|
|
742
|
+
recipients: {
|
|
743
|
+
to,
|
|
744
|
+
...(cc ? { cc } : {}),
|
|
745
|
+
...(bcc ? { bcc } : {}),
|
|
746
|
+
},
|
|
747
|
+
subject: requireDisplayText(
|
|
748
|
+
record.subject,
|
|
749
|
+
`${pathArg}.subject`,
|
|
750
|
+
coreMailContractLimits.maximumSubjectBytes,
|
|
751
|
+
true,
|
|
752
|
+
),
|
|
753
|
+
parts,
|
|
754
|
+
};
|
|
755
|
+
if (Object.hasOwn(record, 'replyTo')) {
|
|
756
|
+
normalized.replyTo = normalizeMailbox(record.replyTo, `${pathArg}.replyTo`);
|
|
757
|
+
}
|
|
758
|
+
if (headers) {
|
|
759
|
+
normalized.headers = headers;
|
|
760
|
+
}
|
|
761
|
+
return deepFreezeValue(normalized);
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
export const normalizeCoreMailInboundPageLimit = (valueArg: unknown): number =>
|
|
765
|
+
requireSafeInteger(valueArg, 'inboundPageLimit', 1, coreMailLimits.inboundPageSize);
|
|
766
|
+
|
|
767
|
+
const coreMailErrorCodes = new Set<TCoreMailErrorCode>([
|
|
768
|
+
'AUTHENTICATION_FAILED',
|
|
769
|
+
'AUTHENTICATION_EXPIRED',
|
|
770
|
+
'AUTHORITY_REVOKED',
|
|
771
|
+
'CAPABILITY_DENIED',
|
|
772
|
+
'NOT_FOUND',
|
|
773
|
+
'INVALID_REQUEST',
|
|
774
|
+
'INVALID_MAILBOX',
|
|
775
|
+
'INVALID_SENDER',
|
|
776
|
+
'INVALID_RECIPIENT',
|
|
777
|
+
'INVALID_HEADER',
|
|
778
|
+
'PAYLOAD_LIMIT_EXCEEDED',
|
|
779
|
+
'QUOTA_EXCEEDED',
|
|
780
|
+
'IDEMPOTENCY_CONFLICT',
|
|
781
|
+
'STATE_CONFLICT',
|
|
782
|
+
'TRANSFER_GRANT_EXPIRED',
|
|
783
|
+
'TRANSFER_GRANT_REPLAYED',
|
|
784
|
+
'TRANSFER_LENGTH_MISMATCH',
|
|
785
|
+
'TRANSFER_DIGEST_MISMATCH',
|
|
786
|
+
'TRANSFER_ENCODING_UNSUPPORTED',
|
|
787
|
+
'OBJECT_INTEGRITY_CONFLICT',
|
|
788
|
+
'DELIVERY_NOT_FETCHED',
|
|
789
|
+
'DELIVERY_DEFERRED',
|
|
790
|
+
'DELIVERY_FAILED',
|
|
791
|
+
'DELIVERY_DEAD_LETTERED',
|
|
792
|
+
'GATEWAY_UNAVAILABLE',
|
|
793
|
+
'RECONCILIATION_FENCE_MISMATCH',
|
|
794
|
+
]);
|
|
795
|
+
|
|
796
|
+
export const normalizeCoreMailErrorData = (
|
|
797
|
+
valueArg: unknown,
|
|
798
|
+
pathArg = 'error',
|
|
799
|
+
): ICoreMailErrorData => {
|
|
800
|
+
const record = readRecord(valueArg, pathArg);
|
|
801
|
+
assertKeys(record, pathArg, ['code', 'retryable'], ['retryAfterMs']);
|
|
802
|
+
if (typeof record.code !== 'string' || !coreMailErrorCodes.has(record.code as TCoreMailErrorCode)) {
|
|
803
|
+
fail(`${pathArg}.code is invalid`);
|
|
804
|
+
}
|
|
805
|
+
if (typeof record.retryable !== 'boolean') {
|
|
806
|
+
fail(`${pathArg}.retryable must be boolean`);
|
|
807
|
+
}
|
|
808
|
+
const normalized: ICoreMailErrorData = {
|
|
809
|
+
code: record.code as TCoreMailErrorCode,
|
|
810
|
+
retryable: record.retryable as boolean,
|
|
811
|
+
};
|
|
812
|
+
if (Object.hasOwn(record, 'retryAfterMs')) {
|
|
813
|
+
if (!record.retryable) {
|
|
814
|
+
fail(`${pathArg}.retryAfterMs requires retryable to be true`);
|
|
815
|
+
}
|
|
816
|
+
normalized.retryAfterMs = requireSafeInteger(
|
|
817
|
+
record.retryAfterMs,
|
|
818
|
+
`${pathArg}.retryAfterMs`,
|
|
819
|
+
1,
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
return deepFreezeValue(normalized);
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
export const normalizeCoreMailTransferGrant = (
|
|
826
|
+
valueArg: unknown,
|
|
827
|
+
pathArg = 'transferGrant',
|
|
828
|
+
): ICoreMailTransferGrant => {
|
|
829
|
+
const record = readRecord(valueArg, pathArg);
|
|
830
|
+
assertKeys(
|
|
831
|
+
record,
|
|
832
|
+
pathArg,
|
|
833
|
+
[
|
|
834
|
+
'grantId',
|
|
835
|
+
'method',
|
|
836
|
+
'path',
|
|
837
|
+
'bearerToken',
|
|
838
|
+
'sha256',
|
|
839
|
+
'lengthBytes',
|
|
840
|
+
'contentType',
|
|
841
|
+
'issuedAt',
|
|
842
|
+
'expiresAt',
|
|
843
|
+
],
|
|
844
|
+
);
|
|
845
|
+
const grantId = requireUuid(record.grantId, `${pathArg}.grantId`);
|
|
846
|
+
const method = record.method === 'PUT' || record.method === 'GET'
|
|
847
|
+
? record.method
|
|
848
|
+
: fail(`${pathArg}.method must be PUT or GET`);
|
|
849
|
+
const path = requireString(record.path, `${pathArg}.path`, 128);
|
|
850
|
+
if (path !== `${coreMailTransferProtocol.pathPrefix}${grantId}`) {
|
|
851
|
+
fail(`${pathArg}.path must match its grantId`);
|
|
852
|
+
}
|
|
853
|
+
const bearerToken = requireString(
|
|
854
|
+
record.bearerToken,
|
|
855
|
+
`${pathArg}.bearerToken`,
|
|
856
|
+
coreMailContractLimits.maximumBearerTokenBytes,
|
|
857
|
+
);
|
|
858
|
+
let decodedToken: string;
|
|
859
|
+
try {
|
|
860
|
+
decodedToken = globalThis.atob(
|
|
861
|
+
`${bearerToken.replace(/-/g, '+').replace(/_/g, '/')}${'='.repeat((4 - (bearerToken.length % 4)) % 4)}`,
|
|
862
|
+
);
|
|
863
|
+
} catch {
|
|
864
|
+
return fail(`${pathArg}.bearerToken must use ${coreMailTransferTokenPolicy.format}`);
|
|
865
|
+
}
|
|
866
|
+
const canonicalToken = globalThis.btoa(decodedToken)
|
|
867
|
+
.replace(/\+/g, '-')
|
|
868
|
+
.replace(/\//g, '_')
|
|
869
|
+
.replace(/=+$/u, '');
|
|
870
|
+
if (
|
|
871
|
+
decodedToken.length !== coreMailTransferTokenPolicy.decodedBytes
|
|
872
|
+
|| bearerToken.length !== coreMailTransferTokenPolicy.encodedCharacters
|
|
873
|
+
|| canonicalToken !== bearerToken
|
|
874
|
+
) {
|
|
875
|
+
fail(`${pathArg}.bearerToken must use ${coreMailTransferTokenPolicy.format}`);
|
|
876
|
+
}
|
|
877
|
+
const issuedAt = requireSafeInteger(record.issuedAt, `${pathArg}.issuedAt`, 1);
|
|
878
|
+
const expiresAt = requireSafeInteger(record.expiresAt, `${pathArg}.expiresAt`, 1);
|
|
879
|
+
if (expiresAt - issuedAt !== coreMailLimits.transferGrantTtlMs) {
|
|
880
|
+
fail(`${pathArg} must use the fixed transfer grant lifetime`);
|
|
881
|
+
}
|
|
882
|
+
return deepFreezeValue({
|
|
883
|
+
grantId,
|
|
884
|
+
method,
|
|
885
|
+
path,
|
|
886
|
+
bearerToken,
|
|
887
|
+
sha256: normalizeCoreMailSha256(record.sha256, `${pathArg}.sha256`),
|
|
888
|
+
lengthBytes: requireSafeInteger(
|
|
889
|
+
record.lengthBytes,
|
|
890
|
+
`${pathArg}.lengthBytes`,
|
|
891
|
+
0,
|
|
892
|
+
coreMailLimits.serializedMimeBytes,
|
|
893
|
+
),
|
|
894
|
+
contentType: requireContentType(record.contentType, `${pathArg}.contentType`),
|
|
895
|
+
issuedAt,
|
|
896
|
+
expiresAt,
|
|
897
|
+
});
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
export const normalizeCoreMailUploadGrant = (
|
|
901
|
+
valueArg: unknown,
|
|
902
|
+
pathArg = 'uploadGrant',
|
|
903
|
+
): ICoreMailUploadGrant => {
|
|
904
|
+
const grant = normalizeCoreMailTransferGrant(valueArg, pathArg);
|
|
905
|
+
if (grant.method !== 'PUT') {
|
|
906
|
+
fail(`${pathArg}.method must be PUT`);
|
|
907
|
+
}
|
|
908
|
+
return grant as ICoreMailUploadGrant;
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
export const normalizeCoreMailDownloadGrant = (
|
|
912
|
+
valueArg: unknown,
|
|
913
|
+
pathArg = 'downloadGrant',
|
|
914
|
+
): ICoreMailDownloadGrant => {
|
|
915
|
+
const grant = normalizeCoreMailTransferGrant(valueArg, pathArg);
|
|
916
|
+
if (grant.method !== 'GET') {
|
|
917
|
+
fail(`${pathArg}.method must be GET`);
|
|
918
|
+
}
|
|
919
|
+
return grant as ICoreMailDownloadGrant;
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const normalizePartStatus = (valueArg: unknown, pathArg: string): ICoreMailPartStatus => {
|
|
923
|
+
const record = readRecord(valueArg, pathArg);
|
|
924
|
+
assertKeys(record, pathArg, ['partId', 'state', 'sha256', 'lengthBytes']);
|
|
925
|
+
const state = record.state === 'missing'
|
|
926
|
+
|| record.state === 'uploading'
|
|
927
|
+
|| record.state === 'complete'
|
|
928
|
+
|| record.state === 'failed'
|
|
929
|
+
? record.state
|
|
930
|
+
: fail(`${pathArg}.state is invalid`);
|
|
931
|
+
return {
|
|
932
|
+
partId: requireIdentifier(record.partId, `${pathArg}.partId`),
|
|
933
|
+
state,
|
|
934
|
+
sha256: normalizeCoreMailSha256(record.sha256, `${pathArg}.sha256`),
|
|
935
|
+
lengthBytes: requireSafeInteger(
|
|
936
|
+
record.lengthBytes,
|
|
937
|
+
`${pathArg}.lengthBytes`,
|
|
938
|
+
0,
|
|
939
|
+
coreMailLimits.attachmentBytes,
|
|
940
|
+
),
|
|
941
|
+
};
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
const assertOutboundStateFields = (
|
|
945
|
+
stateArg: ICoreMailSubmission['state'],
|
|
946
|
+
valueArg: {
|
|
947
|
+
nextAttemptAt?: number;
|
|
948
|
+
error?: ICoreMailErrorData;
|
|
949
|
+
deliveredAt?: number;
|
|
950
|
+
terminalAt?: number;
|
|
951
|
+
},
|
|
952
|
+
pathArg: string,
|
|
953
|
+
): void => {
|
|
954
|
+
if (stateArg === 'deferred') {
|
|
955
|
+
if (
|
|
956
|
+
valueArg.nextAttemptAt === undefined
|
|
957
|
+
|| !valueArg.error?.retryable
|
|
958
|
+
|| valueArg.terminalAt !== undefined
|
|
959
|
+
|| valueArg.deliveredAt !== undefined
|
|
960
|
+
) {
|
|
961
|
+
fail(`${pathArg} deferred state fields are inconsistent`);
|
|
962
|
+
}
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (stateArg === 'failed' || stateArg === 'deadLettered') {
|
|
966
|
+
if (
|
|
967
|
+
valueArg.terminalAt === undefined
|
|
968
|
+
|| !valueArg.error
|
|
969
|
+
|| valueArg.error.retryable
|
|
970
|
+
|| valueArg.nextAttemptAt !== undefined
|
|
971
|
+
|| valueArg.deliveredAt !== undefined
|
|
972
|
+
) {
|
|
973
|
+
fail(`${pathArg} terminal failure fields are inconsistent`);
|
|
974
|
+
}
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (stateArg === 'delivered') {
|
|
978
|
+
if (
|
|
979
|
+
valueArg.terminalAt === undefined
|
|
980
|
+
|| valueArg.deliveredAt !== valueArg.terminalAt
|
|
981
|
+
|| valueArg.error !== undefined
|
|
982
|
+
|| valueArg.nextAttemptAt !== undefined
|
|
983
|
+
) {
|
|
984
|
+
fail(`${pathArg} delivered state fields are inconsistent`);
|
|
985
|
+
}
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (
|
|
989
|
+
valueArg.error !== undefined
|
|
990
|
+
|| valueArg.terminalAt !== undefined
|
|
991
|
+
|| valueArg.deliveredAt !== undefined
|
|
992
|
+
|| valueArg.nextAttemptAt !== undefined
|
|
993
|
+
) {
|
|
994
|
+
fail(`${pathArg} state contains fields reserved for deferred or terminal outcomes`);
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
const submissionStates = new Set<ICoreMailSubmission['state']>([
|
|
999
|
+
'preparing',
|
|
1000
|
+
'uploading',
|
|
1001
|
+
'ready',
|
|
1002
|
+
'accepted',
|
|
1003
|
+
'queued',
|
|
1004
|
+
'delivering',
|
|
1005
|
+
'delivered',
|
|
1006
|
+
'deferred',
|
|
1007
|
+
'failed',
|
|
1008
|
+
'deadLettered',
|
|
1009
|
+
]);
|
|
1010
|
+
|
|
1011
|
+
export const normalizeCoreMailSubmission = (
|
|
1012
|
+
valueArg: unknown,
|
|
1013
|
+
pathArg = 'submission',
|
|
1014
|
+
): ICoreMailSubmission => {
|
|
1015
|
+
const record = readRecord(valueArg, pathArg);
|
|
1016
|
+
assertKeys(
|
|
1017
|
+
record,
|
|
1018
|
+
pathArg,
|
|
1019
|
+
[
|
|
1020
|
+
'submissionId',
|
|
1021
|
+
'idempotencyKey',
|
|
1022
|
+
'submissionDigest',
|
|
1023
|
+
'state',
|
|
1024
|
+
'parts',
|
|
1025
|
+
'attempts',
|
|
1026
|
+
'createdAt',
|
|
1027
|
+
'updatedAt',
|
|
1028
|
+
],
|
|
1029
|
+
[
|
|
1030
|
+
'transportMessageId',
|
|
1031
|
+
'nextAttemptAt',
|
|
1032
|
+
'error',
|
|
1033
|
+
'acceptedAt',
|
|
1034
|
+
'deliveredAt',
|
|
1035
|
+
'terminalAt',
|
|
1036
|
+
],
|
|
1037
|
+
);
|
|
1038
|
+
if (typeof record.state !== 'string' || !submissionStates.has(record.state as ICoreMailSubmission['state'])) {
|
|
1039
|
+
fail(`${pathArg}.state is invalid`);
|
|
1040
|
+
}
|
|
1041
|
+
const parts = readArray(
|
|
1042
|
+
record.parts,
|
|
1043
|
+
`${pathArg}.parts`,
|
|
1044
|
+
coreMailLimits.attachmentCount + 2,
|
|
1045
|
+
).map((entryArg, indexArg) =>
|
|
1046
|
+
normalizePartStatus(entryArg, `${pathArg}.parts[${indexArg}]`),
|
|
1047
|
+
);
|
|
1048
|
+
if (
|
|
1049
|
+
parts.length === 0
|
|
1050
|
+
|| new Set(parts.map((entryArg) => entryArg.partId)).size !== parts.length
|
|
1051
|
+
) {
|
|
1052
|
+
fail(`${pathArg}.parts must contain unique part identities`);
|
|
1053
|
+
}
|
|
1054
|
+
const normalized: ICoreMailSubmission = {
|
|
1055
|
+
submissionId: requireIdentifier(record.submissionId, `${pathArg}.submissionId`),
|
|
1056
|
+
idempotencyKey: requireString(record.idempotencyKey, `${pathArg}.idempotencyKey`, 256),
|
|
1057
|
+
submissionDigest: normalizeCoreMailSha256(
|
|
1058
|
+
record.submissionDigest,
|
|
1059
|
+
`${pathArg}.submissionDigest`,
|
|
1060
|
+
),
|
|
1061
|
+
state: record.state as ICoreMailSubmission['state'],
|
|
1062
|
+
parts,
|
|
1063
|
+
attempts: requireSafeInteger(record.attempts, `${pathArg}.attempts`, 0),
|
|
1064
|
+
createdAt: requireSafeInteger(record.createdAt, `${pathArg}.createdAt`, 1),
|
|
1065
|
+
updatedAt: requireSafeInteger(record.updatedAt, `${pathArg}.updatedAt`, 1),
|
|
1066
|
+
};
|
|
1067
|
+
for (const key of [
|
|
1068
|
+
'nextAttemptAt',
|
|
1069
|
+
'acceptedAt',
|
|
1070
|
+
'deliveredAt',
|
|
1071
|
+
'terminalAt',
|
|
1072
|
+
] as const) {
|
|
1073
|
+
if (Object.hasOwn(record, key)) {
|
|
1074
|
+
normalized[key] = requireSafeInteger(record[key], `${pathArg}.${key}`, 1);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
if (Object.hasOwn(record, 'transportMessageId')) {
|
|
1078
|
+
normalized.transportMessageId = requireIdentifier(
|
|
1079
|
+
record.transportMessageId,
|
|
1080
|
+
`${pathArg}.transportMessageId`,
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
if (Object.hasOwn(record, 'error')) {
|
|
1084
|
+
normalized.error = normalizeCoreMailErrorData(record.error, `${pathArg}.error`);
|
|
1085
|
+
}
|
|
1086
|
+
assertOutboundStateFields(normalized.state, normalized, pathArg);
|
|
1087
|
+
const hasTransportIdentity = normalized.transportMessageId !== undefined;
|
|
1088
|
+
const hasAcceptedAt = normalized.acceptedAt !== undefined;
|
|
1089
|
+
if (hasTransportIdentity !== hasAcceptedAt) {
|
|
1090
|
+
fail(`${pathArg}.transportMessageId and acceptedAt must appear together`);
|
|
1091
|
+
}
|
|
1092
|
+
if (
|
|
1093
|
+
normalized.state === 'accepted'
|
|
1094
|
+
|| normalized.state === 'queued'
|
|
1095
|
+
|| normalized.state === 'delivering'
|
|
1096
|
+
|| normalized.state === 'delivered'
|
|
1097
|
+
|| normalized.state === 'deferred'
|
|
1098
|
+
|| normalized.state === 'deadLettered'
|
|
1099
|
+
) {
|
|
1100
|
+
if (!hasTransportIdentity) {
|
|
1101
|
+
fail(`${pathArg} state requires transport acceptance fields`);
|
|
1102
|
+
}
|
|
1103
|
+
} else if (
|
|
1104
|
+
(normalized.state === 'preparing'
|
|
1105
|
+
|| normalized.state === 'uploading'
|
|
1106
|
+
|| normalized.state === 'ready')
|
|
1107
|
+
&& hasTransportIdentity
|
|
1108
|
+
) {
|
|
1109
|
+
fail(`${pathArg} state must not contain transport acceptance fields`);
|
|
1110
|
+
}
|
|
1111
|
+
if (
|
|
1112
|
+
normalized.updatedAt < normalized.createdAt
|
|
1113
|
+
|| (
|
|
1114
|
+
normalized.acceptedAt !== undefined
|
|
1115
|
+
&& (
|
|
1116
|
+
normalized.acceptedAt < normalized.createdAt
|
|
1117
|
+
|| normalized.acceptedAt > normalized.updatedAt
|
|
1118
|
+
)
|
|
1119
|
+
)
|
|
1120
|
+
|| (
|
|
1121
|
+
normalized.terminalAt !== undefined
|
|
1122
|
+
&& (
|
|
1123
|
+
normalized.terminalAt < (normalized.acceptedAt || normalized.createdAt)
|
|
1124
|
+
|| normalized.terminalAt > normalized.updatedAt
|
|
1125
|
+
)
|
|
1126
|
+
)
|
|
1127
|
+
|| (
|
|
1128
|
+
normalized.nextAttemptAt !== undefined
|
|
1129
|
+
&& normalized.nextAttemptAt < normalized.updatedAt
|
|
1130
|
+
)
|
|
1131
|
+
) {
|
|
1132
|
+
fail(`${pathArg} timestamps are not chronologically ordered`);
|
|
1133
|
+
}
|
|
1134
|
+
if (
|
|
1135
|
+
(normalized.state === 'ready'
|
|
1136
|
+
|| normalized.state === 'accepted'
|
|
1137
|
+
|| normalized.state === 'queued'
|
|
1138
|
+
|| normalized.state === 'delivering'
|
|
1139
|
+
|| normalized.state === 'delivered'
|
|
1140
|
+
|| normalized.state === 'deferred'
|
|
1141
|
+
|| normalized.state === 'deadLettered'
|
|
1142
|
+
|| (normalized.state === 'failed' && hasTransportIdentity))
|
|
1143
|
+
&& normalized.parts.some((entryArg) => entryArg.state !== 'complete')
|
|
1144
|
+
) {
|
|
1145
|
+
fail(`${pathArg} state requires every part to be complete`);
|
|
1146
|
+
}
|
|
1147
|
+
return deepFreezeValue(normalized);
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
export const normalizeCoreMailGatewayOutboundStatus = (
|
|
1151
|
+
valueArg: unknown,
|
|
1152
|
+
pathArg = 'gatewayStatus',
|
|
1153
|
+
): ICoreMailGatewayOutboundStatus => {
|
|
1154
|
+
const record = readRecord(valueArg, pathArg);
|
|
1155
|
+
assertKeys(
|
|
1156
|
+
record,
|
|
1157
|
+
pathArg,
|
|
1158
|
+
['transportMessageId', 'state', 'attempts', 'updatedAt'],
|
|
1159
|
+
['nextAttemptAt', 'error', 'smtpCode', 'deliveredAt', 'terminalAt'],
|
|
1160
|
+
);
|
|
1161
|
+
if (
|
|
1162
|
+
record.state !== 'accepted'
|
|
1163
|
+
&& record.state !== 'queued'
|
|
1164
|
+
&& record.state !== 'delivering'
|
|
1165
|
+
&& record.state !== 'delivered'
|
|
1166
|
+
&& record.state !== 'deferred'
|
|
1167
|
+
&& record.state !== 'failed'
|
|
1168
|
+
&& record.state !== 'deadLettered'
|
|
1169
|
+
) {
|
|
1170
|
+
fail(`${pathArg}.state is invalid`);
|
|
1171
|
+
}
|
|
1172
|
+
const normalized: ICoreMailGatewayOutboundStatus = {
|
|
1173
|
+
transportMessageId: requireIdentifier(
|
|
1174
|
+
record.transportMessageId,
|
|
1175
|
+
`${pathArg}.transportMessageId`,
|
|
1176
|
+
),
|
|
1177
|
+
state: record.state as ICoreMailGatewayOutboundStatus['state'],
|
|
1178
|
+
attempts: requireSafeInteger(record.attempts, `${pathArg}.attempts`, 0),
|
|
1179
|
+
updatedAt: requireSafeInteger(record.updatedAt, `${pathArg}.updatedAt`, 1),
|
|
1180
|
+
};
|
|
1181
|
+
for (const key of ['nextAttemptAt', 'deliveredAt', 'terminalAt'] as const) {
|
|
1182
|
+
if (Object.hasOwn(record, key)) {
|
|
1183
|
+
normalized[key] = requireSafeInteger(record[key], `${pathArg}.${key}`, 1);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (Object.hasOwn(record, 'smtpCode')) {
|
|
1187
|
+
normalized.smtpCode = requireSafeInteger(record.smtpCode, `${pathArg}.smtpCode`, 100, 599);
|
|
1188
|
+
}
|
|
1189
|
+
if (Object.hasOwn(record, 'error')) {
|
|
1190
|
+
normalized.error = normalizeCoreMailErrorData(record.error, `${pathArg}.error`);
|
|
1191
|
+
}
|
|
1192
|
+
assertOutboundStateFields(normalized.state, normalized, pathArg);
|
|
1193
|
+
if (
|
|
1194
|
+
(normalized.deliveredAt !== undefined && normalized.deliveredAt > normalized.updatedAt)
|
|
1195
|
+
|| (normalized.terminalAt !== undefined && normalized.terminalAt > normalized.updatedAt)
|
|
1196
|
+
|| (normalized.nextAttemptAt !== undefined && normalized.nextAttemptAt < normalized.updatedAt)
|
|
1197
|
+
) {
|
|
1198
|
+
fail(`${pathArg} timestamps are not chronologically ordered`);
|
|
1199
|
+
}
|
|
1200
|
+
return deepFreezeValue(normalized);
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
export const normalizeCoreMailInboundDelivery = (
|
|
1204
|
+
valueArg: unknown,
|
|
1205
|
+
pathArg = 'inboundDelivery',
|
|
1206
|
+
): ICoreMailInboundDelivery => {
|
|
1207
|
+
const record = readRecord(valueArg, pathArg);
|
|
1208
|
+
assertKeys(
|
|
1209
|
+
record,
|
|
1210
|
+
pathArg,
|
|
1211
|
+
[
|
|
1212
|
+
'deliveryId',
|
|
1213
|
+
'transportDeliveryId',
|
|
1214
|
+
'state',
|
|
1215
|
+
'envelope',
|
|
1216
|
+
'rawMime',
|
|
1217
|
+
'receivedAt',
|
|
1218
|
+
'updatedAt',
|
|
1219
|
+
],
|
|
1220
|
+
['messageId', 'subject', 'acknowledgedAt', 'acknowledgedOutcome'],
|
|
1221
|
+
);
|
|
1222
|
+
const state = record.state === 'pending'
|
|
1223
|
+
|| record.state === 'fetching'
|
|
1224
|
+
|| record.state === 'fetched'
|
|
1225
|
+
|| record.state === 'acknowledged'
|
|
1226
|
+
? record.state
|
|
1227
|
+
: fail(`${pathArg}.state is invalid`);
|
|
1228
|
+
const rawMime = readRecord(record.rawMime, `${pathArg}.rawMime`);
|
|
1229
|
+
assertKeys(rawMime, `${pathArg}.rawMime`, ['sha256', 'lengthBytes', 'contentType']);
|
|
1230
|
+
if (rawMime.contentType !== 'message/rfc822') {
|
|
1231
|
+
fail(`${pathArg}.rawMime.contentType must be message/rfc822`);
|
|
1232
|
+
}
|
|
1233
|
+
const normalized: ICoreMailInboundDelivery = {
|
|
1234
|
+
deliveryId: requireIdentifier(record.deliveryId, `${pathArg}.deliveryId`),
|
|
1235
|
+
transportDeliveryId: requireIdentifier(
|
|
1236
|
+
record.transportDeliveryId,
|
|
1237
|
+
`${pathArg}.transportDeliveryId`,
|
|
1238
|
+
),
|
|
1239
|
+
state,
|
|
1240
|
+
envelope: normalizeCoreMailEnvelope(record.envelope, `${pathArg}.envelope`),
|
|
1241
|
+
rawMime: {
|
|
1242
|
+
sha256: normalizeCoreMailSha256(rawMime.sha256, `${pathArg}.rawMime.sha256`),
|
|
1243
|
+
lengthBytes: requireSafeInteger(
|
|
1244
|
+
rawMime.lengthBytes,
|
|
1245
|
+
`${pathArg}.rawMime.lengthBytes`,
|
|
1246
|
+
1,
|
|
1247
|
+
coreMailLimits.serializedMimeBytes,
|
|
1248
|
+
),
|
|
1249
|
+
contentType: 'message/rfc822',
|
|
1250
|
+
},
|
|
1251
|
+
receivedAt: requireSafeInteger(record.receivedAt, `${pathArg}.receivedAt`, 1),
|
|
1252
|
+
updatedAt: requireSafeInteger(record.updatedAt, `${pathArg}.updatedAt`, 1),
|
|
1253
|
+
};
|
|
1254
|
+
if (Object.hasOwn(record, 'messageId')) {
|
|
1255
|
+
normalized.messageId = requireDisplayText(record.messageId, `${pathArg}.messageId`, 998);
|
|
1256
|
+
}
|
|
1257
|
+
if (Object.hasOwn(record, 'subject')) {
|
|
1258
|
+
normalized.subject = requireDisplayText(
|
|
1259
|
+
record.subject,
|
|
1260
|
+
`${pathArg}.subject`,
|
|
1261
|
+
coreMailContractLimits.maximumSubjectBytes,
|
|
1262
|
+
true,
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
if (state === 'acknowledged') {
|
|
1266
|
+
if (
|
|
1267
|
+
!Object.hasOwn(record, 'acknowledgedAt')
|
|
1268
|
+
|| (record.acknowledgedOutcome !== 'processed' && record.acknowledgedOutcome !== 'discarded')
|
|
1269
|
+
) {
|
|
1270
|
+
fail(`${pathArg} acknowledged state fields are incomplete`);
|
|
1271
|
+
}
|
|
1272
|
+
normalized.acknowledgedAt = requireSafeInteger(
|
|
1273
|
+
record.acknowledgedAt,
|
|
1274
|
+
`${pathArg}.acknowledgedAt`,
|
|
1275
|
+
1,
|
|
1276
|
+
);
|
|
1277
|
+
normalized.acknowledgedOutcome = record.acknowledgedOutcome as
|
|
1278
|
+
ICoreMailInboundDelivery['acknowledgedOutcome'];
|
|
1279
|
+
} else if (
|
|
1280
|
+
Object.hasOwn(record, 'acknowledgedAt')
|
|
1281
|
+
|| Object.hasOwn(record, 'acknowledgedOutcome')
|
|
1282
|
+
) {
|
|
1283
|
+
fail(`${pathArg} nonacknowledged state contains acknowledgement fields`);
|
|
1284
|
+
}
|
|
1285
|
+
if (
|
|
1286
|
+
normalized.updatedAt < normalized.receivedAt
|
|
1287
|
+
|| (
|
|
1288
|
+
normalized.acknowledgedAt !== undefined
|
|
1289
|
+
&& (
|
|
1290
|
+
normalized.acknowledgedAt < normalized.receivedAt
|
|
1291
|
+
|| normalized.acknowledgedAt > normalized.updatedAt
|
|
1292
|
+
)
|
|
1293
|
+
)
|
|
1294
|
+
) {
|
|
1295
|
+
fail(`${pathArg} timestamps are not chronologically ordered`);
|
|
1296
|
+
}
|
|
1297
|
+
return deepFreezeValue(normalized);
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
export const normalizeCoreMailInboundCursor = (
|
|
1301
|
+
valueArg: unknown,
|
|
1302
|
+
pathArg = 'inboundCursor',
|
|
1303
|
+
): string => {
|
|
1304
|
+
const cursor = requireString(valueArg, pathArg, coreMailLimits.cursorBytes);
|
|
1305
|
+
let decodedCursor: string;
|
|
1306
|
+
try {
|
|
1307
|
+
decodedCursor = globalThis.atob(
|
|
1308
|
+
`${cursor.replace(/-/g, '+').replace(/_/g, '/')}${'='.repeat((4 - (cursor.length % 4)) % 4)}`,
|
|
1309
|
+
);
|
|
1310
|
+
} catch {
|
|
1311
|
+
return fail(`${pathArg} must be a canonical opaque base64url value`);
|
|
1312
|
+
}
|
|
1313
|
+
const canonicalCursor = globalThis.btoa(decodedCursor)
|
|
1314
|
+
.replace(/\+/g, '-')
|
|
1315
|
+
.replace(/\//g, '_')
|
|
1316
|
+
.replace(/=+$/u, '');
|
|
1317
|
+
if (
|
|
1318
|
+
decodedCursor.length < coreMailLimits.cursorMinimumDecodedBytes
|
|
1319
|
+
|| canonicalCursor !== cursor
|
|
1320
|
+
) {
|
|
1321
|
+
fail(`${pathArg} must be a canonical opaque base64url value`);
|
|
1322
|
+
}
|
|
1323
|
+
return cursor;
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1326
|
+
export const normalizeCoreMailInboundDeliveryPage = (
|
|
1327
|
+
valueArg: unknown,
|
|
1328
|
+
pathArg = 'inboundDeliveryPage',
|
|
1329
|
+
): ICoreMailInboundDeliveryPage => {
|
|
1330
|
+
const record = readRecord(valueArg, pathArg);
|
|
1331
|
+
assertKeys(record, pathArg, ['deliveries'], ['nextCursor']);
|
|
1332
|
+
const deliveries = readArray(
|
|
1333
|
+
record.deliveries,
|
|
1334
|
+
`${pathArg}.deliveries`,
|
|
1335
|
+
coreMailLimits.inboundPageSize,
|
|
1336
|
+
).map((entryArg, indexArg) =>
|
|
1337
|
+
normalizeCoreMailInboundDelivery(entryArg, `${pathArg}.deliveries[${indexArg}]`),
|
|
1338
|
+
);
|
|
1339
|
+
if (new Set(deliveries.map((entryArg) => entryArg.deliveryId)).size !== deliveries.length) {
|
|
1340
|
+
fail(`${pathArg}.deliveries must contain unique delivery identities`);
|
|
1341
|
+
}
|
|
1342
|
+
const normalized: ICoreMailInboundDeliveryPage = { deliveries };
|
|
1343
|
+
if (Object.hasOwn(record, 'nextCursor')) {
|
|
1344
|
+
normalized.nextCursor = normalizeCoreMailInboundCursor(
|
|
1345
|
+
record.nextCursor,
|
|
1346
|
+
`${pathArg}.nextCursor`,
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
if (
|
|
1350
|
+
new TextEncoder().encode(JSON.stringify(normalized)).byteLength
|
|
1351
|
+
> coreMailLimits.inboundPageBytes
|
|
1352
|
+
) {
|
|
1353
|
+
fail(`${pathArg} exceeds the inbound page byte budget`);
|
|
1354
|
+
}
|
|
1355
|
+
return deepFreezeValue(normalized);
|
|
1356
|
+
};
|
|
1357
|
+
|
|
345
1358
|
const normalizeUniqueSortedStrings = (
|
|
346
1359
|
valueArg: unknown,
|
|
347
1360
|
pathArg: string,
|
|
@@ -381,21 +1394,21 @@ const normalizeLimits = (
|
|
|
381
1394
|
assertKeys(
|
|
382
1395
|
record,
|
|
383
1396
|
pathArg,
|
|
384
|
-
[],
|
|
385
1397
|
['messagesPerMinute', 'messagesPerDay', 'maxPendingInbound'],
|
|
386
1398
|
);
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
1399
|
+
const maxima = {
|
|
1400
|
+
messagesPerMinute: coreMailContractLimits.maximumMessagesPerMinute,
|
|
1401
|
+
messagesPerDay: coreMailContractLimits.maximumMessagesPerDay,
|
|
1402
|
+
maxPendingInbound: coreMailContractLimits.maximumPendingInbound,
|
|
1403
|
+
} as const;
|
|
1404
|
+
const normalized = {} as ICoreMailBindingDesiredState['limits'];
|
|
1405
|
+
for (const key of Object.keys(maxima) as Array<keyof typeof maxima>) {
|
|
1406
|
+
normalized[key] = requireSafeInteger(
|
|
1407
|
+
record[key],
|
|
1408
|
+
`${pathArg}.${key}`,
|
|
1409
|
+
1,
|
|
1410
|
+
maxima[key],
|
|
1411
|
+
);
|
|
399
1412
|
}
|
|
400
1413
|
return normalized;
|
|
401
1414
|
};
|
|
@@ -419,15 +1432,18 @@ const normalizeBinding = (
|
|
|
419
1432
|
'credentials',
|
|
420
1433
|
'allowedSenders',
|
|
421
1434
|
'inboundRecipients',
|
|
1435
|
+
'limits',
|
|
422
1436
|
],
|
|
423
|
-
['defaultSender'
|
|
1437
|
+
['defaultSender'],
|
|
424
1438
|
);
|
|
425
|
-
if (record.schemaVersion !==
|
|
426
|
-
fail(`${pathArg}.schemaVersion must be
|
|
1439
|
+
if (record.schemaVersion !== 2) {
|
|
1440
|
+
fail(`${pathArg}.schemaVersion must be 2`);
|
|
427
1441
|
}
|
|
428
|
-
const state = record.state === 'active'
|
|
1442
|
+
const state = record.state === 'active'
|
|
1443
|
+
|| record.state === 'draining'
|
|
1444
|
+
|| record.state === 'disabled'
|
|
429
1445
|
? record.state
|
|
430
|
-
: fail(`${pathArg}.state must be active or disabled`);
|
|
1446
|
+
: fail(`${pathArg}.state must be active, draining, or disabled`);
|
|
431
1447
|
const capabilities = normalizeCapabilities(record.capabilities, `${pathArg}.capabilities`);
|
|
432
1448
|
const allowedSenders = normalizeUniqueSortedStrings(
|
|
433
1449
|
record.allowedSenders,
|
|
@@ -449,7 +1465,7 @@ const normalizeBinding = (
|
|
|
449
1465
|
}
|
|
450
1466
|
|
|
451
1467
|
const normalized: ICoreMailBindingDesiredState = {
|
|
452
|
-
schemaVersion:
|
|
1468
|
+
schemaVersion: 2,
|
|
453
1469
|
bindingId: requireIdentifier(record.bindingId, `${pathArg}.bindingId`),
|
|
454
1470
|
serviceId: requireIdentifier(record.serviceId, `${pathArg}.serviceId`),
|
|
455
1471
|
tenantId: requireIdentifier(record.tenantId, `${pathArg}.tenantId`),
|
|
@@ -459,6 +1475,7 @@ const normalizeBinding = (
|
|
|
459
1475
|
credentials: normalizeCredentialSet(record.credentials, `${pathArg}.credentials`),
|
|
460
1476
|
allowedSenders,
|
|
461
1477
|
inboundRecipients,
|
|
1478
|
+
limits: normalizeLimits(record.limits, `${pathArg}.limits`),
|
|
462
1479
|
};
|
|
463
1480
|
if (Object.hasOwn(record, 'defaultSender')) {
|
|
464
1481
|
normalized.defaultSender = requireCanonicalMailbox(
|
|
@@ -469,9 +1486,6 @@ const normalizeBinding = (
|
|
|
469
1486
|
fail(`${pathArg}.defaultSender must be present in allowedSenders`);
|
|
470
1487
|
}
|
|
471
1488
|
}
|
|
472
|
-
if (Object.hasOwn(record, 'limits')) {
|
|
473
|
-
normalized.limits = normalizeLimits(record.limits, `${pathArg}.limits`);
|
|
474
|
-
}
|
|
475
1489
|
return normalized;
|
|
476
1490
|
};
|
|
477
1491
|
|
|
@@ -521,16 +1535,14 @@ const normalizeGateway = (
|
|
|
521
1535
|
assertKeys(
|
|
522
1536
|
record,
|
|
523
1537
|
pathArg,
|
|
524
|
-
[
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
1538
|
+
[
|
|
1539
|
+
'endpointUrl',
|
|
1540
|
+
'coreMailTransferOrigin',
|
|
1541
|
+
'credentialId',
|
|
1542
|
+
'credentialVersion',
|
|
1543
|
+
'credentialSecretKey',
|
|
1544
|
+
],
|
|
530
1545
|
);
|
|
531
|
-
if (!/^[A-Z][A-Z0-9_]{0,127}$/.test(credentialSecretKey)) {
|
|
532
|
-
fail(`${pathArg}.credentialSecretKey must be a canonical environment key`);
|
|
533
|
-
}
|
|
534
1546
|
return {
|
|
535
1547
|
endpointUrl: requireCanonicalUrl(
|
|
536
1548
|
record.endpointUrl,
|
|
@@ -538,13 +1550,22 @@ const normalizeGateway = (
|
|
|
538
1550
|
'wss:',
|
|
539
1551
|
false,
|
|
540
1552
|
),
|
|
1553
|
+
coreMailTransferOrigin: requireCanonicalUrl(
|
|
1554
|
+
record.coreMailTransferOrigin,
|
|
1555
|
+
`${pathArg}.coreMailTransferOrigin`,
|
|
1556
|
+
'https:',
|
|
1557
|
+
true,
|
|
1558
|
+
),
|
|
541
1559
|
credentialId: requireIdentifier(record.credentialId, `${pathArg}.credentialId`),
|
|
542
1560
|
credentialVersion: requireSafeInteger(
|
|
543
1561
|
record.credentialVersion,
|
|
544
1562
|
`${pathArg}.credentialVersion`,
|
|
545
1563
|
1,
|
|
546
1564
|
),
|
|
547
|
-
credentialSecretKey
|
|
1565
|
+
credentialSecretKey: requireEnvironmentKey(
|
|
1566
|
+
record.credentialSecretKey,
|
|
1567
|
+
`${pathArg}.credentialSecretKey`,
|
|
1568
|
+
),
|
|
548
1569
|
};
|
|
549
1570
|
};
|
|
550
1571
|
|
|
@@ -552,9 +1573,13 @@ export const normalizeCoreMailDesiredState = (
|
|
|
552
1573
|
valueArg: unknown,
|
|
553
1574
|
): ICoreMailDesiredState => {
|
|
554
1575
|
const record = readRecord(valueArg, 'desiredState');
|
|
555
|
-
assertKeys(
|
|
556
|
-
|
|
557
|
-
|
|
1576
|
+
assertKeys(
|
|
1577
|
+
record,
|
|
1578
|
+
'desiredState',
|
|
1579
|
+
['schemaVersion', 'configEpoch', 'bindings', 'gateway', 'cursorKeys'],
|
|
1580
|
+
);
|
|
1581
|
+
if (record.schemaVersion !== 2) {
|
|
1582
|
+
fail('desiredState.schemaVersion must be 2');
|
|
558
1583
|
}
|
|
559
1584
|
const bindings = readArray(
|
|
560
1585
|
record.bindings,
|
|
@@ -583,11 +1608,160 @@ export const normalizeCoreMailDesiredState = (
|
|
|
583
1608
|
compareStrings(leftArg.bindingId, rightArg.bindingId),
|
|
584
1609
|
);
|
|
585
1610
|
return deepFreezeValue({
|
|
586
|
-
schemaVersion:
|
|
1611
|
+
schemaVersion: 2,
|
|
587
1612
|
configEpoch: requireSafeInteger(record.configEpoch, 'desiredState.configEpoch', 1),
|
|
588
1613
|
bindings,
|
|
589
1614
|
gateway: normalizeGateway(record.gateway, 'desiredState.gateway'),
|
|
1615
|
+
cursorKeys: normalizeRuntimeKeySet(record.cursorKeys, 'desiredState.cursorKeys'),
|
|
1616
|
+
});
|
|
1617
|
+
};
|
|
1618
|
+
|
|
1619
|
+
export const normalizeCoreMailReplicaIdentity = (
|
|
1620
|
+
valueArg: unknown,
|
|
1621
|
+
pathArg = 'replica',
|
|
1622
|
+
): ICoreMailReplicaIdentity => {
|
|
1623
|
+
const record = readRecord(valueArg, pathArg);
|
|
1624
|
+
assertKeys(
|
|
1625
|
+
record,
|
|
1626
|
+
pathArg,
|
|
1627
|
+
['taskId', 'serviceId', 'rolloutId', 'rolloutGeneration', 'imageDigest'],
|
|
1628
|
+
);
|
|
1629
|
+
return deepFreezeValue({
|
|
1630
|
+
taskId: requireIdentifier(record.taskId, `${pathArg}.taskId`),
|
|
1631
|
+
serviceId: requireIdentifier(record.serviceId, `${pathArg}.serviceId`),
|
|
1632
|
+
rolloutId: requireIdentifier(record.rolloutId, `${pathArg}.rolloutId`),
|
|
1633
|
+
rolloutGeneration: requireSafeInteger(
|
|
1634
|
+
record.rolloutGeneration,
|
|
1635
|
+
`${pathArg}.rolloutGeneration`,
|
|
1636
|
+
1,
|
|
1637
|
+
),
|
|
1638
|
+
imageDigest: normalizeCoreMailSha256(record.imageDigest, `${pathArg}.imageDigest`),
|
|
1639
|
+
});
|
|
1640
|
+
};
|
|
1641
|
+
|
|
1642
|
+
const normalizeBindingReconciliationStatus = (
|
|
1643
|
+
valueArg: unknown,
|
|
1644
|
+
pathArg: string,
|
|
1645
|
+
): ICoreMailBindingReconciliationStatus => {
|
|
1646
|
+
const record = readRecord(valueArg, pathArg);
|
|
1647
|
+
assertKeys(
|
|
1648
|
+
record,
|
|
1649
|
+
pathArg,
|
|
1650
|
+
[
|
|
1651
|
+
'bindingId',
|
|
1652
|
+
'revision',
|
|
1653
|
+
'state',
|
|
1654
|
+
'pendingInboundCount',
|
|
1655
|
+
'activeSessionsByCredential',
|
|
1656
|
+
],
|
|
1657
|
+
);
|
|
1658
|
+
const state = record.state === 'active'
|
|
1659
|
+
|| record.state === 'draining'
|
|
1660
|
+
|| record.state === 'disabled'
|
|
1661
|
+
? record.state
|
|
1662
|
+
: fail(`${pathArg}.state must be active, draining, or disabled`);
|
|
1663
|
+
const activeSessionsByCredential = readArray(
|
|
1664
|
+
record.activeSessionsByCredential,
|
|
1665
|
+
`${pathArg}.activeSessionsByCredential`,
|
|
1666
|
+
coreMailContractLimits.maximumCredentialsPerAuthority,
|
|
1667
|
+
).map((entryArg, indexArg) => {
|
|
1668
|
+
const entryPath = `${pathArg}.activeSessionsByCredential[${indexArg}]`;
|
|
1669
|
+
const entry = readRecord(entryArg, entryPath);
|
|
1670
|
+
assertKeys(entry, entryPath, ['credentialId', 'version', 'count']);
|
|
1671
|
+
return {
|
|
1672
|
+
credentialId: requireIdentifier(entry.credentialId, `${entryPath}.credentialId`),
|
|
1673
|
+
version: requireSafeInteger(entry.version, `${entryPath}.version`, 1),
|
|
1674
|
+
count: requireSafeInteger(entry.count, `${entryPath}.count`, 1),
|
|
1675
|
+
};
|
|
590
1676
|
});
|
|
1677
|
+
if (
|
|
1678
|
+
new Set(activeSessionsByCredential.map((entryArg) =>
|
|
1679
|
+
`${entryArg.credentialId}:${entryArg.version}`
|
|
1680
|
+
)).size !== activeSessionsByCredential.length
|
|
1681
|
+
) {
|
|
1682
|
+
fail(`${pathArg}.activeSessionsByCredential contains duplicate identities`);
|
|
1683
|
+
}
|
|
1684
|
+
activeSessionsByCredential.sort((leftArg, rightArg) =>
|
|
1685
|
+
leftArg.version - rightArg.version
|
|
1686
|
+
|| compareStrings(leftArg.credentialId, rightArg.credentialId),
|
|
1687
|
+
);
|
|
1688
|
+
return {
|
|
1689
|
+
bindingId: requireIdentifier(record.bindingId, `${pathArg}.bindingId`),
|
|
1690
|
+
revision: requireSafeInteger(record.revision, `${pathArg}.revision`, 1),
|
|
1691
|
+
state,
|
|
1692
|
+
pendingInboundCount: requireSafeInteger(
|
|
1693
|
+
record.pendingInboundCount,
|
|
1694
|
+
`${pathArg}.pendingInboundCount`,
|
|
1695
|
+
0,
|
|
1696
|
+
coreMailContractLimits.maximumPendingInbound,
|
|
1697
|
+
),
|
|
1698
|
+
activeSessionsByCredential,
|
|
1699
|
+
};
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
export const normalizeCoreMailReconciliationStatus = (
|
|
1703
|
+
valueArg: unknown,
|
|
1704
|
+
pathArg = 'reconciliationStatus',
|
|
1705
|
+
): ICoreMailReconciliationStatus => {
|
|
1706
|
+
const record = readRecord(valueArg, pathArg);
|
|
1707
|
+
assertKeys(
|
|
1708
|
+
record,
|
|
1709
|
+
pathArg,
|
|
1710
|
+
[
|
|
1711
|
+
'replica',
|
|
1712
|
+
'state',
|
|
1713
|
+
'appliedConfigEpoch',
|
|
1714
|
+
'appliedDesiredStateDigest',
|
|
1715
|
+
'bindings',
|
|
1716
|
+
'updatedAt',
|
|
1717
|
+
],
|
|
1718
|
+
['errorCode'],
|
|
1719
|
+
);
|
|
1720
|
+
const state = record.state === 'applying'
|
|
1721
|
+
|| record.state === 'ready'
|
|
1722
|
+
|| record.state === 'failed'
|
|
1723
|
+
? record.state
|
|
1724
|
+
: fail(`${pathArg}.state must be applying, ready, or failed`);
|
|
1725
|
+
const bindings = readArray(
|
|
1726
|
+
record.bindings,
|
|
1727
|
+
`${pathArg}.bindings`,
|
|
1728
|
+
coreMailContractLimits.maximumBindings,
|
|
1729
|
+
).map((entryArg, indexArg) =>
|
|
1730
|
+
normalizeBindingReconciliationStatus(entryArg, `${pathArg}.bindings[${indexArg}]`),
|
|
1731
|
+
);
|
|
1732
|
+
if (new Set(bindings.map((entryArg) => entryArg.bindingId)).size !== bindings.length) {
|
|
1733
|
+
fail(`${pathArg}.bindings contains duplicate binding identities`);
|
|
1734
|
+
}
|
|
1735
|
+
bindings.sort((leftArg, rightArg) => compareStrings(leftArg.bindingId, rightArg.bindingId));
|
|
1736
|
+
const hasErrorCode = Object.hasOwn(record, 'errorCode');
|
|
1737
|
+
if ((state === 'failed') !== hasErrorCode) {
|
|
1738
|
+
fail(`${pathArg}.errorCode must appear exactly for failed state`);
|
|
1739
|
+
}
|
|
1740
|
+
const normalized: ICoreMailReconciliationStatus = {
|
|
1741
|
+
replica: normalizeCoreMailReplicaIdentity(record.replica, `${pathArg}.replica`),
|
|
1742
|
+
state,
|
|
1743
|
+
appliedConfigEpoch: requireSafeInteger(
|
|
1744
|
+
record.appliedConfigEpoch,
|
|
1745
|
+
`${pathArg}.appliedConfigEpoch`,
|
|
1746
|
+
1,
|
|
1747
|
+
),
|
|
1748
|
+
appliedDesiredStateDigest: normalizeCoreMailSha256(
|
|
1749
|
+
record.appliedDesiredStateDigest,
|
|
1750
|
+
`${pathArg}.appliedDesiredStateDigest`,
|
|
1751
|
+
),
|
|
1752
|
+
bindings,
|
|
1753
|
+
updatedAt: requireSafeInteger(record.updatedAt, `${pathArg}.updatedAt`, 1),
|
|
1754
|
+
};
|
|
1755
|
+
if (hasErrorCode) {
|
|
1756
|
+
if (
|
|
1757
|
+
typeof record.errorCode !== 'string'
|
|
1758
|
+
|| !coreMailErrorCodes.has(record.errorCode as TCoreMailErrorCode)
|
|
1759
|
+
) {
|
|
1760
|
+
fail(`${pathArg}.errorCode is invalid`);
|
|
1761
|
+
}
|
|
1762
|
+
normalized.errorCode = record.errorCode as TCoreMailErrorCode;
|
|
1763
|
+
}
|
|
1764
|
+
return deepFreezeValue(normalized);
|
|
591
1765
|
};
|
|
592
1766
|
|
|
593
1767
|
export const canonicalizeCoreMailDesiredState = (
|