@serve.zone/interfaces 30.12.0 → 30.13.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.
@@ -1,6 +1,6 @@
1
1
  export type TMailGatewayClientType = 'onebox' | 'cloudly' | 'custom' | (string & {});
2
2
  export type TMailAddressPatternType = 'exactAddress' | 'domainWildcard' | 'localPartPattern';
3
- export type TMailInboundTargetType = 'webhook' | 'smtpForward' | 'typedEndpoint' | 'discard' | 'reject';
3
+ export type TMailInboundTargetType = 'webhook' | 'smtpForward' | 'typedEndpoint' | 'coreMail' | 'discard' | 'reject';
4
4
  export type TMailRecipientPolicyMode = 'acceptAll' | 'staticList' | 'cachedResolver' | 'appResolver';
5
5
  export type TMailCredentialType = 'smtp' | 'api';
6
6
  export type TMailCredentialStatus = 'active' | 'disabled' | 'rotated' | 'revoked';
@@ -12,7 +12,12 @@ export type TMailBounceType = 'hard' | 'soft' | 'complaint' | 'unknown';
12
12
  export type TMailInboundProtocol = 'smtp' | 'submission' | 'api';
13
13
  export type TMailRecipientResolutionAction = 'accept' | 'reject' | 'defer';
14
14
  export type TMailDeliveryJournalEventType = 'accepted' | 'queued' | 'deliveryStarted' | 'deliverySucceeded' | 'deliveryDeferred' | 'deliveryFailed' | 'deadLettered' | 'rejected' | 'bounced' | 'complaint';
15
- export type TMailSubmissionErrorCode = 'INVALID_REPLY_TO' | 'REPLY_TO_FIELD_HEADER_CONFLICT';
15
+ /**
16
+ * Error codes a mail submission may return. The CoreMail-fronted path maps the
17
+ * matching `TCoreMailErrorCode` values through unchanged so applications can
18
+ * branch on them regardless of which transport carried the submission.
19
+ */
20
+ export type TMailSubmissionErrorCode = 'INVALID_REPLY_TO' | 'REPLY_TO_FIELD_HEADER_CONFLICT' | 'AUTHENTICATION_FAILED' | 'CAPABILITY_DENIED' | 'INVALID_REQUEST' | 'INVALID_MAILBOX' | 'INVALID_SENDER' | 'INVALID_RECIPIENT' | 'INVALID_HEADER' | 'PAYLOAD_LIMIT_EXCEEDED' | 'QUOTA_EXCEEDED' | 'IDEMPOTENCY_CONFLICT' | 'STATE_CONFLICT' | 'NOT_FOUND';
16
21
  /**
17
22
  * Runtime owner of a mail resource. `appInstanceId` is the deployed WorkApp or service
18
23
  * instance, not an App Store template id.
@@ -48,11 +53,16 @@ export interface IMailTypedEndpointTarget {
48
53
  queueName?: string;
49
54
  timeoutMs?: number;
50
55
  }
56
+ export interface IMailCoreMailTarget {
57
+ /** CoreMail service identity that owns the inbound mailbox. */
58
+ coreMailServiceId: string;
59
+ }
51
60
  export interface IMailInboundTarget {
52
61
  type: TMailInboundTargetType;
53
62
  webhook?: IMailWebhookTarget;
54
63
  smtpForward?: IMailSmtpForwardTarget;
55
64
  typedEndpoint?: IMailTypedEndpointTarget;
65
+ coreMail?: IMailCoreMailTarget;
56
66
  reject?: {
57
67
  smtpCode: number;
58
68
  message: string;
@@ -175,8 +185,11 @@ export interface IWorkAppMailBinding {
175
185
  export type TServiceMailSubmissionTlsMode = 'plain' | 'starttls' | 'implicitTls';
176
186
  export interface IServiceMailInboundConfig {
177
187
  enabled?: boolean;
178
- /** Platformclient apps use typedEndpoint; third-party apps with their own SMTP server use smtpForward. */
179
- targetType?: 'typedEndpoint' | 'smtpForward';
188
+ /**
189
+ * Platformclient apps use typedEndpoint; third-party apps with their own SMTP
190
+ * server use smtpForward; services bound to a CoreMail workload use coreMail.
191
+ */
192
+ targetType?: 'typedEndpoint' | 'smtpForward' | 'coreMail';
180
193
  /** TypedRequest method exposed by platformclient over TypedSocket for inbound delivery. */
181
194
  typedMethod?: string;
182
195
  /** Published Docker/Swarm port that dcrouter may SMTP-forward inbound mail to. */
@@ -1,5 +1,5 @@
1
1
  import type * as plugins from '../plugins.js';
2
- import type { ICoreMailEnvelope, ICoreMailInboundDeliveryPage, ICoreMailDownloadGrant, ICoreMailGatewayMessageDescriptor, ICoreMailGatewayOutboundStatus, ICoreMailInboundDelivery, ICoreMailOutboundMessageDescriptor, ICoreMailRecipientResolution, ICoreMailReconciliationStatus, ICoreMailReplicaIdentity, ICoreMailSubmission, ICoreMailUploadGrant, TCoreMailInboundAckOutcome, TCoreMailSha256, TCoreMailWorkloadOperation } from '../data/coremail.js';
2
+ import type { ICoreMailEnvelope, ICoreMailInboundDeliveryPage, ICoreMailDownloadGrant, ICoreMailGatewayMessageDescriptor, ICoreMailGatewayOutboundStatus, ICoreMailInboundDelivery, ICoreMailOutboundMessageDescriptor, ICoreMailRecipientResolution, ICoreMailReconciliationStatus, ICoreMailReplicaIdentity, ICoreMailServiceMailStatistics, ICoreMailSubmission, ICoreMailUploadGrant, TCoreMailInboundAckOutcome, TCoreMailSha256, TCoreMailWorkloadOperation } from '../data/coremail.js';
3
3
  import type { IMailConnectionInfo } from '../data/mail.js';
4
4
  export interface IReq_CoreMailAuthenticateWorkload extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_CoreMailAuthenticateWorkload> {
5
5
  method: 'coreMailAuthenticateWorkload';
@@ -164,6 +164,29 @@ export interface IReq_CoreMailGetReconciliationStatus extends plugins.typedreque
164
164
  status: ICoreMailReconciliationStatus;
165
165
  };
166
166
  }
167
+ /**
168
+ * Control-session read of per-service mail counters. Authority comes from the
169
+ * authenticated control session; the request carries no tenant or binding
170
+ * authority of its own.
171
+ */
172
+ export interface IReq_CoreMailGetServiceMailStatistics extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_CoreMailGetServiceMailStatistics> {
173
+ method: 'coreMailGetServiceMailStatistics';
174
+ request: {
175
+ /** Inclusive first UTC calendar day, `YYYY-MM-DD`. */
176
+ fromDayUtc: string;
177
+ /** Inclusive last UTC calendar day, `YYYY-MM-DD`. */
178
+ toDayUtc: string;
179
+ /** Optional service filter. Omitted means every service known to this CoreMail. */
180
+ serviceIds?: string[];
181
+ /** Opaque CoreMail-owned composite cursor. */
182
+ cursor?: string;
183
+ limit: number;
184
+ };
185
+ response: {
186
+ statistics: ICoreMailServiceMailStatistics[];
187
+ nextCursor?: string;
188
+ };
189
+ }
167
190
  export interface IReq_CoreMailGatewayAuthenticate extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_CoreMailGatewayAuthenticate> {
168
191
  method: 'coreMailGatewayAuthenticate';
169
192
  request: {
@@ -179,6 +202,13 @@ export interface IReq_CoreMailGatewayAuthenticate extends plugins.typedrequestIn
179
202
  coreMailTransferOrigin: string;
180
203
  };
181
204
  }
205
+ /**
206
+ * CoreMail binds every issued routing handle to a digest of the exact
207
+ * normalized `envelope` and `source` sent here. The following
208
+ * `coreMailGatewayPrepareInboundHandoff` call must therefore resend
209
+ * byte-identical values: the full `rcptTo` set in the same order, the same
210
+ * `mailFrom`, and the same connection source fields.
211
+ */
182
212
  export interface IReq_CoreMailGatewayResolveRecipients extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_CoreMailGatewayResolveRecipients> {
183
213
  method: 'coreMailGatewayResolveRecipients';
184
214
  request: {
@@ -190,6 +220,13 @@ export interface IReq_CoreMailGatewayResolveRecipients extends plugins.typedrequ
190
220
  resolutions: ICoreMailRecipientResolution[];
191
221
  };
192
222
  }
223
+ /**
224
+ * The handoff is validated against the routing handles issued by
225
+ * `coreMailGatewayResolveRecipients`. `message.envelope` and `source` must be
226
+ * byte-identical to the resolve-time values, including the full `rcptTo` set in
227
+ * the same order, because CoreMail recomputes the envelope digest the handles
228
+ * are bound to.
229
+ */
193
230
  export interface IReq_CoreMailGatewayPrepareInboundHandoff extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_CoreMailGatewayPrepareInboundHandoff> {
194
231
  method: 'coreMailGatewayPrepareInboundHandoff';
195
232
  request: {
@@ -246,6 +283,12 @@ export interface IReq_CoreMailGatewayCompleteOutboundHandoff extends plugins.typ
246
283
  replayed: boolean;
247
284
  };
248
285
  }
286
+ /**
287
+ * Status polling for one accepted transport message. `status.updatedAt` must be
288
+ * monotonic per `transportMessageId`. A `transportMessageId` that is
289
+ * permanently unknown to the gateway must be answered with a terminal `failed`
290
+ * status rather than an error, because CoreMail retries errors indefinitely.
291
+ */
249
292
  export interface IReq_CoreMailGatewayGetOutboundStatus extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_CoreMailGatewayGetOutboundStatus> {
250
293
  method: 'coreMailGatewayGetOutboundStatus';
251
294
  request: {
@@ -1,4 +1,5 @@
1
1
  import * as plugins from '../plugins.js';
2
+ import type { ICoreMailGatewayPeerDesiredState } from '../data/coremail.js';
2
3
  import type { IIdentity } from '../data/user.js';
3
4
  import type { IMailAddressBinding, IMailCredentialOneTimeSecret, IMailConnectionInfo, IMailDeliveryJournalEvent, IMailDeliveryStatus, IMailDomainAuthority, IMailEnvelope, IMailInboundMessagePayload, IMailOutboundIdentity, IMailOutboundMessagePayload, IMailRecipientResolution, IMailResourceOwner, IMailSpoolItem, IWorkAppMailBinding, TMailSubmissionErrorCode } from '../data/mail.js';
4
5
  export interface IMailControlRequestAuth {
@@ -89,6 +90,55 @@ export interface IReq_DeleteMailAddressBinding extends plugins.typedrequestInter
89
90
  message?: string;
90
91
  };
91
92
  }
93
+ /**
94
+ * dcrouter-owned CoreMail gateway peer state.
95
+ *
96
+ * `peer.transferOrigin` must equal the `gateway.coreMailTransferOrigin` carried
97
+ * in that CoreMail service's own desired state. The producer (Cloudly or
98
+ * Onebox) is the single owner of that string: dcrouter never infers it from a
99
+ * socket, Host header, TypedSocket tag, or peer input.
100
+ */
101
+ export interface IReq_ListCoreMailGatewayPeers extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_ListCoreMailGatewayPeers> {
102
+ method: 'listCoreMailGatewayPeers';
103
+ request: {
104
+ auth: IMailControlRequestAuth;
105
+ owner: IMailResourceOwner;
106
+ };
107
+ response: {
108
+ peers: ICoreMailGatewayPeerDesiredState[];
109
+ };
110
+ }
111
+ /**
112
+ * Creates or replaces one CoreMail gateway peer. On success the accepted peer is
113
+ * returned in its stored form so the producer can compare the exact applied
114
+ * state. `owner` is required (not partial) because a peer is producer-owned
115
+ * provisioning state, never a filtered listing.
116
+ */
117
+ export interface IReq_SyncCoreMailGatewayPeer extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_SyncCoreMailGatewayPeer> {
118
+ method: 'syncCoreMailGatewayPeer';
119
+ request: {
120
+ auth: IMailControlRequestAuth;
121
+ owner: IMailResourceOwner;
122
+ peer: ICoreMailGatewayPeerDesiredState;
123
+ };
124
+ response: {
125
+ success: boolean;
126
+ peer?: ICoreMailGatewayPeerDesiredState;
127
+ message?: string;
128
+ };
129
+ }
130
+ export interface IReq_DeleteCoreMailGatewayPeer extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_DeleteCoreMailGatewayPeer> {
131
+ method: 'deleteCoreMailGatewayPeer';
132
+ request: {
133
+ auth: IMailControlRequestAuth;
134
+ owner: IMailResourceOwner;
135
+ coreMailServiceId: string;
136
+ };
137
+ response: {
138
+ success: boolean;
139
+ message?: string;
140
+ };
141
+ }
92
142
  export interface IReq_ListWorkAppMailBindings extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_ListWorkAppMailBindings> {
93
143
  method: 'listWorkAppMailBindings';
94
144
  request: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/interfaces",
3
- "version": "30.12.0",
3
+ "version": "30.13.0",
4
4
  "private": false,
5
5
  "description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
6
6
  "exports": {
package/readme.md CHANGED
@@ -1217,6 +1217,17 @@ may continue its next configured resolver. Consumers must apply
1217
1217
  `data.normalizeCoreMailRecipientResolutions()` against the exact requested
1218
1218
  recipient set before acting on a peer response.
1219
1219
 
1220
+ CoreMail binds every issued routing handle to a digest of the exact normalized
1221
+ `envelope` and `source` sent at resolve time. A following
1222
+ `coreMailGatewayPrepareInboundHandoff` must resend byte-identical values,
1223
+ including the complete `rcptTo` set in the same order. Both peers use
1224
+ `data.normalizeCoreMailGatewayMessage` and
1225
+ `data.normalizeCoreMailConnectionInfo` so the digested bytes come from one
1226
+ shared implementation. Outbound status is monotonic per `transportMessageId`,
1227
+ and a permanently unknown `transportMessageId` is answered with a terminal
1228
+ `failed` status rather than an error, because CoreMail retries errors
1229
+ indefinitely.
1230
+
1220
1231
  Large content never travels inside TypedRequest JSON. Outbound body parts and
1221
1232
  attachments use prepare/upload/complete operations with short-lived one-time
1222
1233
  HTTP transfer grants. Inbound delivery uses bounded delivery listing followed
@@ -1245,6 +1256,23 @@ credential-lifecycle inconsistencies. CoreMail credential verifiers use the
1245
1256
  versioned `argon2id-v1` format and the exact policy exported as
1246
1257
  `data.coreMailCredentialVerifierPolicy`.
1247
1258
 
1259
+ `data.ICoreMailDesiredState.smtp` is the optional CoreMail SMTP submission
1260
+ listener (MSA). It carries the listener port, the advertised EHLO hostname, and
1261
+ the runtime secret keys holding certificate and private-key PEM text; plaintext
1262
+ PEM material never appears in desired state. SMTP AUTH uses the binding
1263
+ identity directly: the username is the `bindingId` and the password is any
1264
+ current or retiring credential secret of that binding. The listener stays
1265
+ unavailable until the referenced PEM material resolves. Omitting `smtp` keeps
1266
+ the exact canonical bytes and digest of an API-only CoreMail. Accepted
1267
+ submissions report their entry point through the optional
1268
+ `ICoreMailSubmission.source`, either `api` or `smtp`.
1269
+
1270
+ `data.ICoreMailServiceMailStatistics` reports per-service outbound and inbound
1271
+ counters for one UTC calendar day. Control sessions read them through
1272
+ `coreMailGetServiceMailStatistics` using an inclusive day range, an optional
1273
+ service filter, and an opaque cursor. Apply
1274
+ `data.normalizeCoreMailServiceMailStatistics` before using reported counters.
1275
+
1248
1276
  `data.ICoreMailControlBootstrap` is installed before ordinary reconciliation.
1249
1277
  It contains the CoreMail service identity and verifier metadata only. The
1250
1278
  matching plaintext control credential is delivered exclusively to Coreflow
@@ -1269,7 +1297,13 @@ CoreMail transfer origin associated with an authenticated CoreMail service.
1269
1297
  The same origin is carried in CoreMail desired state and returned by workload
1270
1298
  and gateway authentication. It must never be inferred from a socket, Host
1271
1299
  header, TypedSocket tag, or unrestricted peer input. CoreMail compares both
1272
- authoritative views before handing over a path-only transfer grant.
1300
+ authoritative views before handing over a path-only transfer grant. Cloudly and
1301
+ Onebox provision that peer state through `listCoreMailGatewayPeers`,
1302
+ `syncCoreMailGatewayPeer`, and `deleteCoreMailGatewayPeer` on the gateway-client
1303
+ mail surface, and the producer is the single owner of `peer.transferOrigin`. A
1304
+ mail inbound target may point at a CoreMail service using `type: 'coreMail'`
1305
+ with `coreMail.coreMailServiceId`, mirrored by the `coreMail` member of
1306
+ `IServiceMailInboundConfig.targetType`.
1273
1307
 
1274
1308
  Inbound delivery pagination uses an opaque CoreMail-owned `cursor`, is bounded
1275
1309
  by `data.coreMailLimits.inboundPageSize`, and returns `nextCursor` only when
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '30.12.0',
6
+ version: '30.13.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
@@ -20,17 +20,22 @@ import {
20
20
  type ICoreMailEnvelope,
21
21
  type ICoreMailErrorData,
22
22
  type ICoreMailGatewayDesiredState,
23
+ type ICoreMailGatewayMessageDescriptor,
23
24
  type ICoreMailGatewayOutboundStatus,
24
25
  type ICoreMailGatewayPeerDesiredState,
25
26
  type ICoreMailInboundDelivery,
26
27
  type ICoreMailInboundDeliveryPage,
28
+ type ICoreMailInboundMailCounters,
27
29
  type ICoreMailMailbox,
30
+ type ICoreMailOutboundMailCounters,
28
31
  type ICoreMailOutboundMessageDescriptor,
29
32
  type ICoreMailPartStatus,
30
33
  type ICoreMailRecipientResolution,
31
34
  type ICoreMailRuntimeKeyReference,
32
35
  type ICoreMailReconciliationStatus,
33
36
  type ICoreMailReplicaIdentity,
37
+ type ICoreMailServiceMailStatistics,
38
+ type ICoreMailSmtpDesiredState,
34
39
  type ICoreMailSubmission,
35
40
  type ICoreMailTransferGrant,
36
41
  type ICoreMailUploadGrant,
@@ -38,8 +43,10 @@ import {
38
43
  type TCoreMailBindingState,
39
44
  type TCoreMailErrorCode,
40
45
  type TCoreMailSha256,
46
+ type TCoreMailSubmissionSource,
41
47
  type TCoreMailWorkloadOperation,
42
48
  } from './coremail.js';
49
+ import type { IMailConnectionInfo } from './mail.js';
43
50
 
44
51
  export const coreMailCanonicalJsonRules = strictCanonicalJsonRules;
45
52
 
@@ -79,6 +86,7 @@ export const coreMailContractLimits = Object.freeze({
79
86
  maximumContentIdBytes: 256,
80
87
  maximumBearerTokenBytes: 1_024,
81
88
  maximumRecipientResolutionMessageBytes: 512,
89
+ maximumConnectionInfoFieldBytes: 512,
82
90
  } as const);
83
91
 
84
92
  export class CoreMailContractError extends Error {
@@ -533,6 +541,24 @@ const requireCanonicalMailbox = (valueArg: unknown, pathArg: string): string =>
533
541
  return value;
534
542
  };
535
543
 
544
+ const requireCanonicalHostname = (valueArg: unknown, pathArg: string): string => {
545
+ const value = requireString(
546
+ valueArg,
547
+ pathArg,
548
+ coreMailContractLimits.maximumMailboxBytes,
549
+ );
550
+ const labels = value.split('.');
551
+ if (
552
+ value !== value.toLowerCase()
553
+ || value.length > 253
554
+ || labels.length < 2
555
+ || labels.some((labelArg) => !mailboxDomainLabelRegex.test(labelArg))
556
+ ) {
557
+ fail(`${pathArg} must be a canonical lowercase hostname`);
558
+ }
559
+ return value;
560
+ };
561
+
536
562
  const normalizeMailbox = (valueArg: unknown, pathArg: string): ICoreMailMailbox => {
537
563
  const record = readRecord(valueArg, pathArg);
538
564
  assertKeys(record, pathArg, ['address'], ['displayName']);
@@ -1157,6 +1183,7 @@ export const normalizeCoreMailSubmission = (
1157
1183
  ],
1158
1184
  [
1159
1185
  'transportMessageId',
1186
+ 'source',
1160
1187
  'nextAttemptAt',
1161
1188
  'error',
1162
1189
  'acceptedAt',
@@ -1209,6 +1236,12 @@ export const normalizeCoreMailSubmission = (
1209
1236
  `${pathArg}.transportMessageId`,
1210
1237
  );
1211
1238
  }
1239
+ if (Object.hasOwn(record, 'source')) {
1240
+ if (record.source !== 'api' && record.source !== 'smtp') {
1241
+ fail(`${pathArg}.source must be api or smtp`);
1242
+ }
1243
+ normalized.source = record.source as TCoreMailSubmissionSource;
1244
+ }
1212
1245
  if (Object.hasOwn(record, 'error')) {
1213
1246
  normalized.error = normalizeCoreMailErrorData(record.error, `${pathArg}.error`);
1214
1247
  }
@@ -1698,6 +1731,38 @@ const normalizeGateway = (
1698
1731
  };
1699
1732
  };
1700
1733
 
1734
+ const normalizeSmtpDesiredState = (
1735
+ valueArg: unknown,
1736
+ pathArg: string,
1737
+ ): ICoreMailSmtpDesiredState => {
1738
+ const record = readRecord(valueArg, pathArg);
1739
+ assertKeys(record, pathArg, ['enabled', 'port', 'hostname', 'tls']);
1740
+ if (typeof record.enabled !== 'boolean') {
1741
+ fail(`${pathArg}.enabled must be boolean`);
1742
+ }
1743
+ const tlsRecord = readRecord(record.tls, `${pathArg}.tls`);
1744
+ assertKeys(
1745
+ tlsRecord,
1746
+ `${pathArg}.tls`,
1747
+ ['certificatePemSecretKey', 'privateKeyPemSecretKey'],
1748
+ );
1749
+ return {
1750
+ enabled: record.enabled as boolean,
1751
+ port: requireSafeInteger(record.port, `${pathArg}.port`, 1, 65_535),
1752
+ hostname: requireCanonicalHostname(record.hostname, `${pathArg}.hostname`),
1753
+ tls: {
1754
+ certificatePemSecretKey: requireEnvironmentKey(
1755
+ tlsRecord.certificatePemSecretKey,
1756
+ `${pathArg}.tls.certificatePemSecretKey`,
1757
+ ),
1758
+ privateKeyPemSecretKey: requireEnvironmentKey(
1759
+ tlsRecord.privateKeyPemSecretKey,
1760
+ `${pathArg}.tls.privateKeyPemSecretKey`,
1761
+ ),
1762
+ },
1763
+ };
1764
+ };
1765
+
1701
1766
  export const normalizeCoreMailDesiredState = (
1702
1767
  valueArg: unknown,
1703
1768
  ): ICoreMailDesiredState => {
@@ -1706,6 +1771,7 @@ export const normalizeCoreMailDesiredState = (
1706
1771
  record,
1707
1772
  'desiredState',
1708
1773
  ['schemaVersion', 'configEpoch', 'bindings', 'gateway', 'cursorKeys'],
1774
+ ['smtp'],
1709
1775
  );
1710
1776
  if (record.schemaVersion !== 2) {
1711
1777
  fail('desiredState.schemaVersion must be 2');
@@ -1736,13 +1802,17 @@ export const normalizeCoreMailDesiredState = (
1736
1802
  bindings.sort((leftArg, rightArg) =>
1737
1803
  compareStrings(leftArg.bindingId, rightArg.bindingId),
1738
1804
  );
1739
- return deepFreezeValue({
1805
+ const normalized: ICoreMailDesiredState = {
1740
1806
  schemaVersion: 2,
1741
1807
  configEpoch: requireSafeInteger(record.configEpoch, 'desiredState.configEpoch', 1),
1742
1808
  bindings,
1743
1809
  gateway: normalizeGateway(record.gateway, 'desiredState.gateway'),
1744
1810
  cursorKeys: normalizeRuntimeKeySet(record.cursorKeys, 'desiredState.cursorKeys'),
1745
- });
1811
+ };
1812
+ if (Object.hasOwn(record, 'smtp')) {
1813
+ normalized.smtp = normalizeSmtpDesiredState(record.smtp, 'desiredState.smtp');
1814
+ }
1815
+ return deepFreezeValue(normalized);
1746
1816
  };
1747
1817
 
1748
1818
  export const normalizeCoreMailReplicaIdentity = (
@@ -1976,3 +2046,209 @@ export const normalizeCoreMailGatewayPeerDesiredState = (
1976
2046
  ),
1977
2047
  });
1978
2048
  };
2049
+
2050
+ const utcCalendarDayRegex = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
2051
+
2052
+ const requireUtcCalendarDay = (valueArg: unknown, pathArg: string): string => {
2053
+ const value = requireString(valueArg, pathArg, 10);
2054
+ if (!utcCalendarDayRegex.test(value)) {
2055
+ fail(`${pathArg} must be a canonical YYYY-MM-DD UTC day`);
2056
+ }
2057
+ const [year, month, day] = value.split('-').map(Number);
2058
+ const parsed = new Date(Date.UTC(year, month - 1, day));
2059
+ if (
2060
+ parsed.getUTCFullYear() !== year
2061
+ || parsed.getUTCMonth() + 1 !== month
2062
+ || parsed.getUTCDate() !== day
2063
+ ) {
2064
+ fail(`${pathArg} must be a real UTC calendar day`);
2065
+ }
2066
+ return value;
2067
+ };
2068
+
2069
+ const normalizeOutboundMailCounters = (
2070
+ valueArg: unknown,
2071
+ pathArg: string,
2072
+ ): ICoreMailOutboundMailCounters => {
2073
+ const record = readRecord(valueArg, pathArg);
2074
+ const keys = [
2075
+ 'submittedApi',
2076
+ 'submittedSmtp',
2077
+ 'delivered',
2078
+ 'deferred',
2079
+ 'failed',
2080
+ 'deadLettered',
2081
+ ] as const;
2082
+ assertKeys(record, pathArg, [...keys]);
2083
+ const normalized = {} as ICoreMailOutboundMailCounters;
2084
+ for (const key of keys) {
2085
+ normalized[key] = requireSafeInteger(record[key], `${pathArg}.${key}`, 0);
2086
+ }
2087
+ return normalized;
2088
+ };
2089
+
2090
+ const normalizeInboundMailCounters = (
2091
+ valueArg: unknown,
2092
+ pathArg: string,
2093
+ ): ICoreMailInboundMailCounters => {
2094
+ const record = readRecord(valueArg, pathArg);
2095
+ const keys = [
2096
+ 'received',
2097
+ 'acknowledgedProcessed',
2098
+ 'acknowledgedDiscarded',
2099
+ ] as const;
2100
+ assertKeys(record, pathArg, [...keys]);
2101
+ const normalized = {} as ICoreMailInboundMailCounters;
2102
+ for (const key of keys) {
2103
+ normalized[key] = requireSafeInteger(record[key], `${pathArg}.${key}`, 0);
2104
+ }
2105
+ return normalized;
2106
+ };
2107
+
2108
+ export const normalizeCoreMailServiceMailStatistics = (
2109
+ valueArg: unknown,
2110
+ pathArg = 'serviceMailStatistics',
2111
+ ): ICoreMailServiceMailStatistics => {
2112
+ const record = readRecord(valueArg, pathArg);
2113
+ assertKeys(
2114
+ record,
2115
+ pathArg,
2116
+ [
2117
+ 'tenantId',
2118
+ 'serviceId',
2119
+ 'bindingId',
2120
+ 'dayUtc',
2121
+ 'outbound',
2122
+ 'inbound',
2123
+ 'updatedAt',
2124
+ ],
2125
+ );
2126
+ return deepFreezeValue({
2127
+ tenantId: requireIdentifier(record.tenantId, `${pathArg}.tenantId`),
2128
+ serviceId: requireIdentifier(record.serviceId, `${pathArg}.serviceId`),
2129
+ bindingId: requireIdentifier(record.bindingId, `${pathArg}.bindingId`),
2130
+ dayUtc: requireUtcCalendarDay(record.dayUtc, `${pathArg}.dayUtc`),
2131
+ outbound: normalizeOutboundMailCounters(record.outbound, `${pathArg}.outbound`),
2132
+ inbound: normalizeInboundMailCounters(record.inbound, `${pathArg}.inbound`),
2133
+ updatedAt: requireSafeInteger(record.updatedAt, `${pathArg}.updatedAt`, 1),
2134
+ });
2135
+ };
2136
+
2137
+ /**
2138
+ * Strict gateway message descriptor normalization.
2139
+ *
2140
+ * CoreMail digests the normalized envelope together with the normalized
2141
+ * connection source, so the emitted field set and order is part of the
2142
+ * contract and must not change.
2143
+ */
2144
+ export const normalizeCoreMailGatewayMessage = (
2145
+ valueArg: unknown,
2146
+ pathArg = 'gatewayMessage',
2147
+ ): ICoreMailGatewayMessageDescriptor => {
2148
+ const record = readRecord(valueArg, pathArg);
2149
+ assertKeys(record, pathArg, ['envelope', 'rawMime']);
2150
+ const rawMime = readRecord(record.rawMime, `${pathArg}.rawMime`);
2151
+ assertKeys(rawMime, `${pathArg}.rawMime`, ['sha256', 'lengthBytes', 'contentType']);
2152
+ if (rawMime.contentType !== 'message/rfc822') {
2153
+ fail(`${pathArg}.rawMime.contentType must be message/rfc822`);
2154
+ }
2155
+ const normalized: ICoreMailGatewayMessageDescriptor = {
2156
+ envelope: normalizeCoreMailEnvelope(record.envelope, `${pathArg}.envelope`),
2157
+ rawMime: {
2158
+ sha256: normalizeCoreMailSha256(rawMime.sha256, `${pathArg}.rawMime.sha256`),
2159
+ lengthBytes: requireSafeInteger(
2160
+ rawMime.lengthBytes,
2161
+ `${pathArg}.rawMime.lengthBytes`,
2162
+ 1,
2163
+ coreMailLimits.serializedMimeBytes,
2164
+ ),
2165
+ contentType: 'message/rfc822',
2166
+ },
2167
+ };
2168
+ return deepFreezeValue(normalized);
2169
+ };
2170
+
2171
+ const requireConnectionInfoField = (valueArg: unknown, pathArg: string): string => {
2172
+ const value = requireString(
2173
+ valueArg,
2174
+ pathArg,
2175
+ coreMailContractLimits.maximumConnectionInfoFieldBytes,
2176
+ );
2177
+ if (value.trim() !== value) {
2178
+ fail(`${pathArg} must not carry leading or trailing whitespace`);
2179
+ }
2180
+ return value;
2181
+ };
2182
+
2183
+ /**
2184
+ * Strict connection-source normalization for gateway resolve and handoff
2185
+ * requests. CoreMail binds routing handles to a digest of the normalized
2186
+ * envelope and this normalized source, so the emitted field set and order is
2187
+ * part of the contract and must not change.
2188
+ */
2189
+ export const normalizeCoreMailConnectionInfo = (
2190
+ valueArg: unknown,
2191
+ pathArg = 'connectionInfo',
2192
+ ): IMailConnectionInfo => {
2193
+ const record = readRecord(valueArg, pathArg);
2194
+ assertKeys(
2195
+ record,
2196
+ pathArg,
2197
+ ['protocol'],
2198
+ [
2199
+ 'remoteAddress',
2200
+ 'proxyAddress',
2201
+ 'heloName',
2202
+ 'tls',
2203
+ 'authUsername',
2204
+ 'authenticatedCredentialId',
2205
+ ],
2206
+ );
2207
+ if (
2208
+ record.protocol !== 'smtp'
2209
+ && record.protocol !== 'submission'
2210
+ && record.protocol !== 'api'
2211
+ ) {
2212
+ fail(`${pathArg}.protocol must be smtp, submission, or api`);
2213
+ }
2214
+ const normalized: IMailConnectionInfo = {
2215
+ protocol: record.protocol as IMailConnectionInfo['protocol'],
2216
+ };
2217
+ if (record.remoteAddress !== undefined) {
2218
+ normalized.remoteAddress = requireConnectionInfoField(
2219
+ record.remoteAddress,
2220
+ `${pathArg}.remoteAddress`,
2221
+ );
2222
+ }
2223
+ if (record.proxyAddress !== undefined) {
2224
+ normalized.proxyAddress = requireConnectionInfoField(
2225
+ record.proxyAddress,
2226
+ `${pathArg}.proxyAddress`,
2227
+ );
2228
+ }
2229
+ if (record.heloName !== undefined) {
2230
+ normalized.heloName = requireConnectionInfoField(
2231
+ record.heloName,
2232
+ `${pathArg}.heloName`,
2233
+ );
2234
+ }
2235
+ if (record.tls !== undefined) {
2236
+ if (typeof record.tls !== 'boolean') {
2237
+ fail(`${pathArg}.tls must be boolean`);
2238
+ }
2239
+ normalized.tls = record.tls as boolean;
2240
+ }
2241
+ if (record.authUsername !== undefined) {
2242
+ normalized.authUsername = requireConnectionInfoField(
2243
+ record.authUsername,
2244
+ `${pathArg}.authUsername`,
2245
+ );
2246
+ }
2247
+ if (record.authenticatedCredentialId !== undefined) {
2248
+ normalized.authenticatedCredentialId = requireConnectionInfoField(
2249
+ record.authenticatedCredentialId,
2250
+ `${pathArg}.authenticatedCredentialId`,
2251
+ );
2252
+ }
2253
+ return deepFreezeValue(normalized);
2254
+ };