@serve.zone/interfaces 21.1.0 → 22.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.
@@ -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 (!Number.isSafeInteger(valueArg) || (valueArg as number) < minimumArg) {
167
- return fail(`${pathArg} must be a safe integer greater than or equal to ${minimumArg}`);
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
- if (Object.keys(record).length === 0) {
388
- fail(`${pathArg} must not be empty`);
389
- }
390
- const normalized: NonNullable<ICoreMailBindingDesiredState['limits']> = {};
391
- for (const key of [
392
- 'messagesPerMinute',
393
- 'messagesPerDay',
394
- 'maxPendingInbound',
395
- ] as const) {
396
- if (Object.hasOwn(record, key)) {
397
- normalized[key] = requireSafeInteger(record[key], `${pathArg}.${key}`, 1);
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', 'limits'],
1437
+ ['defaultSender'],
424
1438
  );
425
- if (record.schemaVersion !== 1) {
426
- fail(`${pathArg}.schemaVersion must be 1`);
1439
+ if (record.schemaVersion !== 2) {
1440
+ fail(`${pathArg}.schemaVersion must be 2`);
427
1441
  }
428
- const state = record.state === 'active' || record.state === 'disabled'
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: 1,
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
- ['endpointUrl', 'credentialId', 'credentialVersion', 'credentialSecretKey'],
525
- );
526
- const credentialSecretKey = requireString(
527
- record.credentialSecretKey,
528
- `${pathArg}.credentialSecretKey`,
529
- coreMailContractLimits.maximumEnvironmentKeyBytes,
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(record, 'desiredState', ['schemaVersion', 'configEpoch', 'bindings', 'gateway']);
556
- if (record.schemaVersion !== 1) {
557
- fail('desiredState.schemaVersion must be 1');
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: 1,
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 = (